Compare commits

..
Author SHA1 Message Date
Clintchiz b2c74a3a29 chore(release): publish ui 0.8.53
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
2026-08-25 17:59:43 +05:30
Clintchiz fe86570f45 fix(ui): render dynamic tab panels and ids
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
2026-08-25 17:59:10 +05:30
Clintchiz bc1211ea8b chore(release): publish ui 0.8.52
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m0s
2026-08-25 17:37:55 +05:30
Clintchiz d2e24eb089 feat(ui): refine timeline and blockquote layouts
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m2s
2026-08-25 17:37:29 +05:30
Clintchiz e71885d51c chore(release): publish ui 0.8.51
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m56s
2026-08-25 17:31:26 +05:30
Clintchiz 138b5d0950 fix(ui): prevent full bleed hero overflow
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m42s
2026-08-25 17:30:34 +05:30
Clintchiz 7e1eb899b3 fix(ui): align stats surface within max width
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
2026-08-25 16:54:23 +05:30
Clintchiz 3f6612cfe7 fix(ui): honor stats bar desktop gutters
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
2026-08-25 16:50:10 +05:30
Clintchiz 510b3ba777 fix(ui): keep breadcrumb separators visible
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m21s
2026-08-25 16:34:44 +05:30
Clintchiz 1ff2c3339c fix(ui): render active breadcrumb pages
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m0s
2026-08-25 16:21:53 +05:30
Clintchiz 027873459d fix(ui): collapse empty page header slots
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m57s
2026-08-25 15:56:57 +05:30
Clintchiz d0bccfe37e fix(csr): suppress inactive theme token warnings
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
2026-08-25 15:48:48 +05:30
Clintchiz 120889060a fix(ui): normalize form field label sizing
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
2026-08-25 15:25:24 +05:30
Clintchiz 26ec18c24b chore(editor): release vscode extension 0.8.11
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
2026-08-25 15:22:15 +05:30
Clintchiz 7658c5d499 fix(editor): type nested attribute expressions
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
2026-08-25 15:16:38 +05:30
Clintchiz 3cb6ba1233 fix(ui): render navbar brand prop outside slot
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m32s
2026-08-25 14:00:46 +05:30
Clintchiz 64db7da08d fix(ui): refine responsive catalog navigation
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
2026-08-25 13:46:19 +05:30
Clintchiz 9f6552ea62 fix(ui): separate catalog icon masks from tiles
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
2026-08-24 23:30:33 +05:30
Clintchiz 5bc391e74f fix(ui): inherit catalog icon contrast
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 22s
2026-08-24 23:23:43 +05:30
Clintchiz 3327ec0d37 refactor(ui): clarify catalog navigation hierarchy
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m18s
2026-08-24 23:16:22 +05:30
Clintchiz ceb865f4f1 fix(ui): prevent navigation surface overflow
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
2026-08-24 23:08:00 +05:30
Clintchiz 45309b3f72 refactor(ui): compact mega menu layouts
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
2026-08-24 23:06:01 +05:30
Clintchiz 8f3a13c854 fix(ui): render only supplied mega menu metadata
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
2026-08-24 22:57:13 +05:30
Clintchiz eca4447dcf fix(ui): stabilize catalog mega menu interaction
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
2026-08-24 22:50:41 +05:30
Clintchiz 904bca21b2 refactor(ui): refine catalog mega menu navigation
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m52s
2026-08-24 22:40:58 +05:30
Clintchiz b98d417185 fix(ui): eliminate mega menu flash and lag
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
2026-08-24 22:33:02 +05:30
Clintchiz 7d737ec0a0 fix(ui): make catalog mega menus interactive
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 6m15s
2026-08-24 22:18:30 +05:30
Clintchiz f5538ed621 fix(ui): keep mega menu open across hover gap
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
2026-08-24 22:08:46 +05:30
Clintchiz d2d830e7e2 fix(ui): pin navbar mega panels to viewport
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 12m5s
2026-08-24 21:56:27 +05:30
Clintchiz 0618782355 fix(ui): make mega menus work under strict CSP
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
2026-08-24 21:52:25 +05:30
Clintchiz 9067e68f69 fix(ui): import navbar mega menu dependency
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m39s
2026-08-24 21:37:26 +05:30
Clintchiz 9d64ec60c4 feat(ui): add typed navbar menu items
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 11m12s
2026-08-24 21:36:46 +05:30
Clintchiz fc1eea3939 chore(editor): refresh language server bundle hash
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m8s
2026-08-24 21:25:53 +05:30
Clintchiz 4ec5dad60a fix(formatter): expand large structured prop defaults
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 21s
2026-08-24 21:25:07 +05:30
Clintchiz a1a2947910 feat(ui): expand mega menu layout system
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 11m2s
2026-08-24 21:15:50 +05:30
Clintchiz 270040fb5f feat(ui): add in-flow floating navbar
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
2026-08-24 19:47:47 +05:30
Clintchiz f9e285b182 feat(ui): own responsive marketing layout APIs
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m42s
2026-08-24 19:44:26 +05:30
Clintchiz 3b402b4e36 fix(ui): collapse empty navbar topbar and add CTA spacing
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 11m48s
2026-08-24 18:28:39 +05:30
Clintchiz fa535575c5 fix(cli): resolve package updates on Windows
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m56s
2026-08-24 17:23:35 +05:30
Clintchiz 46ef4b775b fix(ui): harden public page components
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m19s
2026-08-24 17:13:14 +05:30
Clintchiz 7a36c1905f fix(cli): handle version flags before workspace discovery
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 15:03:38 +05:30
Clintchiz 1a8c75a958 chore: refresh editor language server bundle 2026-08-24 14:58:03 +05:30
Clintchiz b3c93e9b18 test: harden package boundaries and audit budgets
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 12:05:20 +05:30
Clintchiz 613ff7ae5b fix: invoke npm reliably from Bun on Windows
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 11:37:06 +05:30
Clintchiz e372ae571a chore: harden release checks and package coverage
Quality / quality (ubuntu-latest) (push) Failing after 9m57s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 11:36:13 +05:30
Clintchiz 354082ebc3 fix: recognize globally registered components
Quality / quality (ubuntu-latest) (push) Failing after 10m22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 23:03:45 +05:30
Clintchiz aa21bc4a18 chore: release cli with fixed dev runtime
Quality / quality (ubuntu-latest) (push) Failing after 11m3s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 22:58:09 +05:30
Clintchiz 1be482caab fix: defer reactive props in branch pre-rendering
Quality / quality (ubuntu-latest) (push) Failing after 10m4s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 22:51:25 +05:30
Clintchiz 2d1cda0eab chore: restore production release gate 2026-08-23 22:35:11 +05:30
Clintchiz d26403fce2 chore: update public API baseline 2026-08-23 22:24:51 +05:30
Clintchiz f52e1d50e4 feat: centralize mail credential and sandbox policy
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 21:51:37 +05:30
Clintchiz fe44bc2091 fix: make payment event reduction monotonic
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 21:34:59 +05:30
Clintchiz 98e0813061 feat: add gateway-neutral payment package
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 21:05:27 +05:30
Clintchiz a9670c2a1c fix: close durable queue and runtime gaps
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 20:47:00 +05:30
Clintchiz 1a94179b5f fix: synchronize forwarded component props
Quality / quality (ubuntu-latest) (push) Failing after 10m57s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 20:20:43 +05:30
Clintchiz f13e83fd74 fix: stage CLI release dependencies
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 20:09:26 +05:30
Clintchiz 7351ed1566 chore: release synchronized client runtime
Quality / quality (ubuntu-latest) (push) Failing after 10m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 20:07:18 +05:30
Clintchiz 53b9947ef9 fix: synchronize mounted component props
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 19:42:39 +05:30
Clintchiz 56cde5aaf8 fix(authz): reserve role inheritance namespace
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 19:35:03 +05:30
Clintchiz 4be4b2c346 fix: expose client fetch and signed adjustments
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 18:56:05 +05:30
ClintchizandClaude Opus 5 6258495b67 docs: propose @wrnexus/payment with a multi-gateway adapter system
Gateways are adapters behind one interface, with capabilities DECLARED rather
than assumed -- because gateways are not interchangeable. Some have no
authorize-then-capture, some cannot refund partially, some have no vault. An
interface that pretends otherwise fails at the moment money should have moved.
So capabilities are declared, refused loudly when absent, and checked at build
time where the gateway is statically known.

Tier 1 is sandbox, Stripe, Razorpay and PayPal. Stripe and Razorpay are
deliberately the first real pair because they DIFFER on capture model, currency
spread and refund semantics -- one gateway does not prove an abstraction, and
two similar ones prove it badly. Tier 2 and a regional Tier 3 follow, and
defineGateway() makes a third-party adapter a first-class citizen held to the
same shared contract suite.

Two rules shape the package: it never touches a raw card number (hosted fields
keep an application in PCI SAQ-A rather than SAQ-D), and the signed webhook is
the source of truth rather than the browser redirect, which is a claim from an
untrusted client.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 18:47:41 +05:30
Clintchiz 631e1a2ddb test: cover client-created component branches
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 11:54:28 +05:30
Clintchiz 2e12656060 feat: close application architecture gaps
Quality / quality (ubuntu-latest) (push) Failing after 9m56s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 11:45:57 +05:30
ClintchizandClaude Opus 5 d87b197224 fix(queue): let a shut-down queue be started again
`shutdown()` set `accepting = false` and `start()` refused for ever after, so
a queue was single-use. Any process that boots more than one app broke: a test
suite closing one harness and opening the next, a hot reload, a multi-tenant
host. The failure landed far from its cause -- the SECOND app to boot threw
WRN-QUEUE-CLOSED out of the dev server because an unrelated one had shut down
earlier in the same process. That is what turned six example-app security
tests red only when run alongside the rest of the suite.

Starting is an explicit intent to run, so it reopens the queue. `add()` keeps
its guard, so work offered to a queue that is shutting down is still refused.

Also migrates the example app's welcome-email queue to `defineQueue`, which
the new loader requires. It still used `defineJob`, so the loader refused it
and took the whole example app down -- 14 failures from one unmigrated file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 11:36:44 +05:30
Clintchiz 64ab20cc95 feat: add application productivity foundations
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 11:13:03 +05:30
Clintchiz 46195462c3 feat: make queues durable by default and add seed helpers
Quality / quality (ubuntu-latest) (push) Failing after 11m1s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 09:50:25 +05:30
Clintchiz 8fb96f521f fix(cli): resolve runtime config import absolutely
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 09:29:26 +05:30
Clintchiz 9377a69d88 feat(queue): add typed application queue lifecycle
Quality / quality (ubuntu-latest) (push) Failing after 10m55s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 09:22:05 +05:30
Clintchiz 9332522614 fix: initialize configured auth in production builds
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:36:31 +05:30
Clintchiz 37439188ca fix: share authz catalog across package instances
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:30:01 +05:30
Clintchiz 7c5069b5b5 fix: honor project typecheck in canonical checks
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:23:21 +05:30
Clintchiz 41b7eb8798 fix: keep authz server modules out of browser bundles
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:20:06 +05:30
Clintchiz 0be69fb234 fix: avoid auth plugin ordering cycles
Quality / quality (ubuntu-latest) (push) Failing after 10m56s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:17:33 +05:30
Clintchiz bab11083c6 fix: ship authz runtime and preserve queue clocks
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:14:26 +05:30
Clintchiz a3ddd39b7b feat: centralize application framework primitives
Quality / quality (ubuntu-latest) (push) Failing after 14m38s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:07:46 +05:30
ClintchizandClaude Opus 5 96e082b943 release: patch syntax, compiler, db, csr
Six fixes, all found by driving a real application rather than by the suite:

- syntax: a quote or brace inside a regex literal unbalanced the brace scanner
- syntax: block comments between members failed to parse, while the same
  comment inside a braced body was fine
- compiler: pages never emitted `data-wrn-loop-locals`, so a loop variable in
  a handler threw ReferenceError at click time with a green build
- csr: client-rendered `data-for` items never carried the marker either, so a
  component's output binding silently dropped every call while a plain DOM
  handler in the same position worked
- db: the query generator baked the checkout's line endings into generated
  SQL literals, so every build dirtied the working tree

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 12:09:38 +05:30
ClintchizandClaude Opus 5 2299a0726d chore: fit the loop-locals fix within the production gate
Three things the gate caught that the test suite could not.

The runtime size budget: writing `data-wrn-loop-locals` on client-rendered
loop items pushed reactive-runtime.ts to 51,603 against a 51,400 budget that
had only 125 bytes of headroom. Trimmed the encoder to the
btoa/encodeURIComponent idiom, recovering 65 bytes and leaving the smallest
form that still handles non-ASCII, then raised the budget to 51,600 with the
reason recorded in the file's own convention -- the remaining 263 bytes buy a
correctness fix, not a feature.

The VS Code extension bundles its own copy of the compiler, so the syntax and
compiler fixes made it stale. Rebuilt.

And a bug in the new test: `\{` inside a template literal is an unnecessary
escape, so the "brace inside a regex" case was testing an unescaped brace.
`\{` tests the case it was written for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 12:08:07 +05:30
ClintchizandClaude Opus 5 2d0df4efc9 fix(csr): write loop locals onto client-rendered for-loop items
A client `data-for` passed its loop locals to hydration in memory but never
wrote the `data-wrn-loop-locals` attribute the SSR path writes. Anything that
resolves locals by READING the DOM -- notably a component's `data-wrn-out-*`
output binding, which calls `decodeLoopLocals(componentRoot)` -- therefore
found nothing and silently dropped the call, with no console error.

A plain DOM handler kept working, because it receives locals through the
hydration closure instead, which is what made the failure look arbitrary: the
same loop variable resolved for `@click` and vanished for a component output.

Both loop paths write the marker now, keyed and non-keyed, so the DOM is the
single source of truth. Encoding goes through UTF-8 before base64 as the
server's does; `btoa` on a raw string throws above U+00FF, which would take the
whole loop down for an ordinary non-ASCII label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 11:31:11 +05:30
ClintchizandClaude Opus 5 20783699ac test(compiler): prove page loop locals render, not just emit
The existing tests assert the marker is emitted. This one executes the
generated module and asserts the rendered HTML carries each item's real,
decodable values -- generated text that reads correctly can still render
wrong, and what matters is what the runtime finds in the DOM at click time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 07:38:40 +05:30
ClintchizandClaude Opus 5 d8305a5a14 fix(db): normalise line endings when parsing queries
The generator embeds each query's SQL as a string literal, taking whatever line
endings the checkout happened to have. On a CRLF checkout every regenerated
query differed from the committed one by `\n` -> `\r\n`, so `wrnexus build`
dirtied the working tree and that churn buried real changes in the same file --
which is how a hand-applied edit ends up preferable to running the generator.

Line endings carry no meaning in SQL, so normalise on parse and let generated
output be stable across platforms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 07:28:29 +05:30
ClintchizandClaude Opus 5 98a46091b5 fix(syntax): allow block comments between members
`skipTrivia` skipped `// line comments` but not `/* block comments */`, so one
written between two page or component members failed with a bare "Unexpected
character '/'". Block comments inside a braced body already worked, which made
the failure look arbitrary: the same comment parsed or did not depending on
whether it happened to sit inside a block.

`startsWithBlockComment` now skips only whitespace and line comments, so
`props {}` keeps refusing block comments with its own explained error rather
than silently swallowing one and dropping the declaration after it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 06:50:51 +05:30
ClintchizandClaude Opus 5 2c50a07ed2 fix(compiler): expose {#each} locals to event handlers on pages
A handler expression is emitted as text and evaluated when the event fires, so
any loop variable it names has to travel with the element. Components emitted
`data-wrn-loop-locals` for this; pages did not. The same view worked inside a
component and threw ReferenceError inside a page -- with a green build and green
tests, since nothing renders the page in a browser during a build.

The CSR runtime already resolves locals generically via
closest("[data-wrn-loop-locals]"), so only codegen needed to change.

The marker is emitted only on elements that actually bind an event, and the
encoder only when a marker was produced -- but it MUST be emitted whenever one
is, or the render throws on an undefined function instead of the handler
throwing on an undefined variable, which is strictly worse. Covered by its own
test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 06:05:10 +05:30
ClintchizandClaude Opus 5 9746e8e875 fix(syntax): do not let a regex literal unbalance a block
The brace scanner knew about strings and comments but had no case for regex
literals. A quote inside one opened a phantom string that swallowed every brace
until the next quote; a lone `{` or `}` inside one miscounted block depth. Both
failed the component with "Unbalanced braces" pointing at the block's first line.

`/-/g` parsed fine, which is why this went unnoticed -- it needs a quote or a
brace inside the pattern to bite.

Regex-vs-division is decided by scanning back to the last significant
character, erring towards division: mistaking division for a regex would
swallow code to the next `/` and lose any braces between. A regex cannot span a
newline, so an unterminated one on the line is treated as "not a regex", which
is what keeps a bare URL in view text intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 06:05:09 +05:30
Clintchiz 0ddb481159 release: patch csr, ui
Quality / quality (ubuntu-latest) (push) Failing after 6m3s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-21 16:32:16 +05:30
ClintchizandClaude Opus 5 3441b96362 fix(csr): deliver focus and blur outputs from inside a component
A parent writing @focus on a component tag never heard that component's
own input or button take focus. The runtime bound every output-named DOM
fallback listener in the bubble phase, and focus and blur do not bubble,
so the event fired on the descendant and stopped there. Nothing errored --
the binding simply did nothing.

That made a whole class of declared outputs undeliverable: button.focus,
button.blur, TextLink.focus, TextLink.blur, WysiwygEditor.focus and
WysiwygEditor.blur all advertised events they could never send.

The ui ratchet for outputs nothing emits excluded natively-named outputs
on the grounds that a native event reaches the root anyway. That holds for
click and change, which bubble, and was wrong for focus and blur. Binding
those two in the capture phase makes the exclusion honest rather than
convenient; the ratchet's comment now says so.

Also documents WysiwygEditor as the chrome shell it is: it emits none of
its four outputs itself, it forwards whatever the slotted control raises.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 16:31:33 +05:30
Clintchiz 84e58b1798 release: patch compiler
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-21 09:32:46 +05:30
ClintchizandClaude Opus 5 de4477dd9c fix(compiler): never emit an empty pattern attribute
An empty pattern compiles to a regex matching only the empty string, so
every typed value becomes invalid and the form silently refuses to submit
-- no error, no request. @wrnexus/ui's input declares pattern: string = ""
and renders pattern="{pattern}", so every input that did not opt into a
pattern shipped one that could never match. This broke sign-up in a real
app, and only became visible once the dev-server client-module fix let
form enhancements mount at all.

Attributes reach the output through two emitters and both needed it: a
component's interpolated value is baked at render time, so the whole
attribute is now emitted by __wrnOptionalAttr, while a page's static
element is dropped at compile time. Component mounts are excluded, where
the value is a prop being passed down rather than an attribute.

minlength/maxlength/min/max/step/inputmode/accept get the same treatment --
inert when empty, but meaningless too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:32:32 +05:30
Clintchiz 2970d5fff3 release: patch dev-server, security
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-21 09:04:50 +05:30
ClintchizandClaude Opus 5 eeef2d79df fix(dev-server,security): repair two defects that only appear in a published build
The dev server shipped two entries, index and serve-entry, bundled
independently because the publish build set splitting:false. They share
pipeline.ts, which holds mutable module state -- compileCacheDir, set once
at startup by the bootstrap, and browserArtifactPaths, populated during
compilation and read when serving /__wrnexus/client/*. Duplicating the
module duplicated the state, so the writer and the reader addressed
different copies: every component client module 404'd and .wrn compilation
wrote nothing. It works from source, where there is one module instance,
which is why it reached a release. Emitting a shared chunk fixes it for
every package at once.

resetDevCache also ran several hundred lines after the plugin virtual
modules were written into the same directory, deleting them at every boot.
An app with no plugins never noticed; an app with one lost them every time.

Separately, secureCookieOptions spread ...options after its path default,
and setSecureCookie always forwards an explicit path key -- so omitting
path emitted a cookie with no Path at all, which the browser then scoped to
the request's directory.

Verified end to end against a real app installing the published packages:
17 artifacts written, client modules 200, and the sign-in form submits from
the UI and reaches /dashboard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:04:12 +05:30
ClintchizandClaude Opus 5 ef6de302e9 chore(vscode): prepare 0.8.9 marketplace release
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
Bumps the extension past the published 0.8.8 so the apis { } editor
tooling can ship: highlighting, api. completion and hover, the api=
attribute, the repositioned removed-block diagnostics, and the fix for
the spurious "Cannot find name 'api'" error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:37:11 +05:30
ClintchizandClaude Opus 5 a970d04248 release: patch auth, cli, compiler, core, csr, dev-server, language-server, store, styles, syntax, typecheck, ui
Quality / quality (ubuntu-latest) (push) Failing after 11m0s
Quality / quality (windows-latest) (push) Canceled after 0s
Ships the apis { } block, the legacy/config cleanup, the wrnexus update
migrations, and the editor tooling that understands all of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:15:08 +05:30
ClintchizandClaude Opus 5 5429057a38 fix(language-server): gate api hover to actual api.<name> references
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
Hover fired on whatever word was under the cursor, so a local variable
colliding with a declared block name reported the block's method and path
instead of its own hover info. Completion was already gated this way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:03:46 +05:30
ClintchizandClaude Opus 5 e944fd4496 feat(editor): complete tooling support for the apis block
Task 6 Step 2 was already performed and confirmed the assertion error
from the apis-block type checks lands on the apis { } block itself
(WRN-TYPE-2344), not on the offending entry, so it already surfaces
usefully and needed no relocation mapping.

That observation surfaced a real pre-existing bug: the virtual
TypeScript document built for type checking declared `server` from
ast.dataApis-adjacent runtime functions but never declared `api`,
so every api.<name>(...) call raised a false 'Cannot find name apis'
plus a knock-on implicit-any on its result. Fixes it by declaring
`api` from ast.dataApis, mirroring the existing `server` declaration:
each entry gets an input parameter shaped from its request
parameters/body fields (optional when the entry declares none) and a
Promise<any> return. The binding is only emitted when the page has an
apis { } block, so pages without one keep the legitimate 'Cannot find
name api' diagnostic and 'state api' stays legal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 18:51:19 +05:30
ClintchizandClaude Opus 5 861444b8a3 feat(language-server): flag the removed data blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 18:40:08 +05:30
ClintchizandClaude Opus 5 ba957e861e chore(ui): re-baseline the visual contract after the auth UI refinements
cd0dffa8 changed StrongPassword, TogglePassword and button but left the
hash manifest untouched, so check:ui-visual has been red since. The three
hashes here are exactly those components -- no unrelated drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:50:44 +05:30
ClintchizandClaude Opus 5 29febb2e0c feat(language-server): understand the api binding attribute
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:49:06 +05:30
Clintchiz cd0dffa87d feat: complete SSR CRM and refine auth UI
Quality / quality (ubuntu-latest) (push) Failing after 11m17s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-20 16:17:59 +05:30
ClintchizandClaude Opus 5 f57bd05a03 feat(language-server): complete and describe api block calls
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:35:31 +05:30
ClintchizandClaude Opus 5 2bb52487eb feat(editor): complete the apis block and drop the removed snippets
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:31:23 +05:30
ClintchizandClaude Opus 5 990a8128a7 feat(editor): highlight the apis block
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:31:19 +05:30
ClintchizandClaude Opus 5 7a1b4e5b33 fix(cli): guard the one migration write path that skips parse validation
mode-functions writes without a parse check when a mode wrapper survives
holding api entries, since that intermediate state is unparseable until
move-api-blocks runs later in the same pass. Brace balance is the invariant
a bad splice offset would break, so check that instead; nothing downstream
could tell a corrupted wrapper from an untouched one.

Also records that Task 5's example-app migration ran against an already-
migrated target and so did not prove end-to-end behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:24:19 +05:30
ClintchizandClaude Opus 5 aded2daab9 fix: restore the production gate after the migration tasks
- rebuild editors/vscode bundles, stale since the parser escape fix
- attach the caught ParseError as `cause` in both migration validators
- drop two unused test bindings flagged by eslint

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:19:53 +05:30
ClintchizandClaude Opus 5 6bb3ab5fe7 feat(cli): fail the update when a project needs manual review
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:12:38 +05:30
ClintchizandClaude Opus 5 4aa0973352 feat(cli): report legacy api bodies for manual migration
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:03:17 +05:30
ClintchizandClaude Opus 5 e616ed276e feat(cli): migrate mode-scoped helpers to shared functions
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:58:16 +05:30
ClintchizandClaude Opus 5 74490964ee feat(cli): migrate api entries into the apis block
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:51:44 +05:30
ClintchizandClaude Opus 5 7a3e55b150 feat(cli): migrate away the dead config keys
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:45:40 +05:30
ClintchizandClaude Opus 5 fb24cc7ec3 fix(syntax): stop swallowing literal backslashes in attribute values
readQuoted treated \X as an escape for any X, so a single literal
backslash in any quoted attribute value was silently dropped
(data-path="C:\Users" parsed as C:Users) and a doubled backslash
collapsed to one. Only the delimiter and the backslash itself are
escapes now; every other backslash is a literal character.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:42:29 +05:30
Clintchiz 63316111cb feat(examples): worked example for apis blocks 2026-08-20 08:36:03 +05:30
ClintchizandClaude Opus 5 0ed8351828 test: restore executed and real-tsc coverage lost when the old api-block tests were deleted
Fix round 1: the deleted api-block-*.test.ts files were not fully superseded
by the apis-* siblings as claimed. Ports back, using apis {} fixtures:
- brace-inside-a-string-literal response-section scanner regression test
- type erasure of response/error bodies before browser emission
- client-side response-error-not-swallowed / transport-failure-fallback,
  executed via dynamic import of a generated browser module
- the full SSR execution suite: response payload binding, error section
  status/message/data binding, {#each} failure propagation, all executed
  via dynamic import + a real load/api call chain (not string checks)
- the four real-tsc enforcement tests (matching/wrong-type/extra-field/
  missing-field), plus the B1 cross-page collision guard and the B6
  export-for-noUnusedLocals guard

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 08:14:16 +05:30
ClintchizandClaude Opus 5 890d6106b3 feat: replace the ssr/client data blocks with apis blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 07:59:37 +05:30
ClintchizandClaude Opus 5 de99a2c2e0 feat(cli): assert types for every api block with declared fields
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 07:44:58 +05:30
ClintchizandClaude Opus 5 442c058d0c feat(compiler): support the three api render-binding forms
- Parse api="name", api="name()", and api="name({ ... })" render bindings
  for apis {} (mode "any") blocks, mirroring @click="fn()" syntax.
- Render-bind by calling Task 4's generated server `api` object directly
  (api.<name>(args)) rather than re-implementing the fetch/response
  transport, spliced into the SSR template via the existing loop/expression
  sentinel mechanism so the call runs inside the async render function with
  await support.
- A block that is both render-bound and called from code runs twice by
  design (no dedup); pinned with a test.
- Fix packages/syntax's attribute-value lexer (readQuoted) to honor
  backslash-escaped quotes, needed so an api="..." call expression can
  itself contain a quoted string/object literal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 07:38:32 +05:30
Clintchiz a08dfa5322 fix(compiler): reject dynamic api access in client functions
Usage-driven emission can only see api.<name> calls. api["name"]()
or passing api to a helper is invisible to it, silently drops the
block from the browser bundle, and fails at runtime instead of build
time. Detect that dynamic/indirect use (masking strings and comments
first, reusing the tokenizer's skipLiteralOrComment) and refuse to
compile instead, naming the offending function.
2026-08-20 07:25:02 +05:30
Clintchiz 712a6d3d8c feat(compiler): emit browser api bindings only where the client calls them 2026-08-20 07:19:55 +05:30
Clintchiz 9dec811069 fix(compiler): route the generated api object through the real buildApiRequest
Root-cause fix for the fix-round-1 review: the inlined query/body
assembly in __wrnexusCallApi was a third, unguarded copy of
buildApiRequest's rules. Restore the import of buildApiRequest from
@wrnexus/core in the generated module and delete the inline copy.

The four api-block-ssr.test.ts tests (and three in compiler.test.ts)
that dynamically import a generated module from an OS tmpdir were
failing against a stale globally-installed @wrnexus/core (v0.8.8,
predates buildApiRequest) because that tmpdir has no node_modules of
its own and bare-specifier resolution walked out of the workspace.
Fixed at the source: symlink the workspace @wrnexus/core into each
tmpdir root before the dynamic import, the same way every in-repo
package already resolves it.
2026-08-20 07:11:53 +05:30
Clintchiz 847b7010d1 feat(compiler): emit the server-side api object
Server module now declares `const api = { ... }` for apis {} blocks in
mode "any", dispatching in-process via requireRequestContext + the
existing __wrnexusCallApi transport helper. The try wraps only the
transport call; the response body runs after it, outside the try, so
a bug in the author's response code surfaces rather than being
mistaken for a request failure. A block with no error {} section
rethrows instead of resolving undefined.

Also closes the pageCtx.__wrnexusCallApi wiring gap in
dev-server/runtime.ts: it now forwards input through to
callApiFromContext instead of dropping it.
2026-08-20 07:03:11 +05:30
ClintchizandClaude Opus 5 1dbe16dc93 test(core,csr): replace text-substring agreement check with a behavioural one
The old assertions only searched REACTIVE_RUNTIME for substrings; they never
touched buildApiRequest and were not anchored to the content-type line they
claimed to guard, so they could not detect drift on either side. Replace
with a fixture-driven test that runs both implementations on the same
(path, method, input) cases and compares the actual request they produce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 06:47:20 +05:30
ClintchizandClaude Opus 5 f32b33e3b6 feat(core): share API request assembly between both transports
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 06:43:42 +05:30
Clintchiz 768074ac0a fix(dev-server): route server.fn() RPC to a handler in production
server.fn() posts to POST /__wrnexus/rpc. Dev intercepts that path before
handlers.fetch and routes it to a createRpcHandler instance built from
loadWrnServerModule; createProductionServer/createProductionHandlers had no
such route, so the request fell through to the internal-caller-gated
inter-app service RPC and 404'd.

Add resolveProdServerFunctions(), a synchronous equivalent of dev's resolve
that searches the already-statically-imported ProdManifest components/pages/
layouts for __wrnexusServerFunctions + __wrnexusRpcManifest, and wire it into
createProductionHandlers with the same validateCsrf + withServerFnRequestContext
wrapping dev uses. Move those two helpers into a new rpc-shared.ts so prod.ts
can use them without a circular import through index.ts.

Add packages/dev-server/test/prod-server-fn-rpc.test.ts covering a successful
call, CSRF rejection, and clean 404s for an unknown component/function.
2026-08-20 06:41:32 +05:30
ClintchizandClaude Opus 5 c7e40154ca fix(core): establish request context at every server-code entry point
Wraps the three additional entry points where user server code runs
outside fetchHandler's own context wrap:
- the server-function RPC path (/__wrnexus/rpc) intercepted before
  handlers.fetch in the dev server (index.ts) - what server.fn() travels
- the service RPC path (isRpcPath) inside fetchHandler, which runs
  implement()/implementStream() service code before ctx existed
- the HMR-sync handler, which runs real load blocks/actions via dispatch()

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:55:18 +05:30
ClintchizandClaude Opus 5 680ea73975 feat(core): carry the request context in an AsyncLocalStorage
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:46:24 +05:30
ClintchizandClaude Opus 5 953b1cd692 fix(syntax): reject bare apis-container bodies and cross-mode duplicate api names
Bare bodies inside apis {} silently discarded their text with no error,
producing a do-nothing block. They now throw a ParseError naming the entry
and pointing at the response {} section. Duplicate-name detection for
dataApis moved from an incremental, order-dependent check (only saw prior
entries in the array) to a single post-parse pass over the whole ast.dataApis,
so it catches cross-mode duplicates (apis {} vs ssr { api }) regardless of
declaration order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:33:05 +05:30
ClintchizandClaude Opus 5 87a00de5f3 feat(syntax): parse the apis container block
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:28:23 +05:30
ClintchizandClaude Opus 5 d069f5ddd7 fix: correct feature-report prose, restore update test coverage, warn on sub-0.8.0 upgrades
- generate-complete-framework-report.mjs no longer claims legacyEmit/
  legacyEventProps/legacyComponentDiscovery/stringLayouts/
  functions.legacyDefaultRuntime are usable compatibility flags; they were
  removed before the first public release and a config setting them is now
  rejected. Regenerated docs/WRNEXUS-COMPLETE-FEATURE-REPORT.md.
- Restored the update.test.ts coverage lost in the sub-0.8.0 migration
  cleanup: a 0.8.x fixture now asserts refreshFrameworkFiles' still-live
  behaviour (public/llms.txt and CLAUDE.md creation) and that
  pkg.wrnexus.version stays at its old value after an unverified update.
  The .gitignore refresh and build/start/production script backfill were
  themselves removed as part of dropping the sub-0.8.0 migrations that
  implemented them, so there is nothing left to cover for those two.
- updateApp now logs a clear warning when the detected project version is
  below 0.8.0, naming the version and stating that automated migration from
  below 0.8.0 is no longer supported, without failing the command (a
  marker-less app, like examples/basic-app, is benign and must still
  upgrade cleanly).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:19:23 +05:30
ClintchizandClaude Opus 5 abebbcf8af chore: regenerate baselines after the legacy cleanup
- regenerate docs/public-api-0.8.json (removals only: CompatibilityPolicy,
  CompatibilityReport, CURRENT_COMPATIBILITY_DATE, CURRENT_FRAMEWORK_BEHAVIOUR,
  isCompatibilityDate, resolveCompatibility from @wrnexus/styles)
- regenerate docs/WRNEXUS-COMPLETE-FEATURE-REPORT.md
- remove the deleted 'wrnexus compatibility' command row and its config-pinning
  sentence from packages/cli/README.md
- drop compatibilityDate/frameworkBehaviour example fields from docs/GUIDE.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:04:59 +05:30
ClintchizandClaude Opus 5 5720f93db1 test(auth): restore plugin engine-hook coverage via invokeAuthHandler
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:43:38 +05:30
ClintchizandClaude Opus 5 7f5e1bc3cf refactor: remove the deprecated auth options
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:38:08 +05:30
ClintchizandClaude Opus 5 97d60d0ba8 refactor: drop the deprecated compiler re-export shims
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:33:48 +05:30
ClintchizandClaude Opus 5 e7e7b58160 chore: drop update migrations below 0.8.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:28:33 +05:30
ClintchizandClaude Opus 5 e09a35b4bd fix(store): give the fix-round-2 regression test explicit generics
The single-branch 'shared'-only action fixture failed to infer through
defineStore's actions generic, typing store.increment as never and
failing bun run typecheck (TS2349) even though bun test passed.
Explicit type arguments fix the inference without weakening the test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:15:35 +05:30
Clintchiz 5c8f3ede54 refactor: delete the compatibility config surface 2026-08-20 01:11:35 +05:30
ClintchizandClaude Opus 5 d19595c7ba fix(store): drop unreachable legacy StoreRuntime fallback
Fix round 2 for task 1: store-codegen.ts stopped emitting runtime:
"legacy" actions in round 1, making the StoreRuntime variant and its
resolution fallback dead. Narrows StoreRuntime to three variants and
adds a regression test for the remaining options.runtime -> shared
fallback chain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 00:58:26 +05:30
ClintchizandClaude Opus 5 b7796b103a fix(typecheck): drop legacy FunctionRuntime branches in contracts/index
Fix round 1 for task 1: packages/typecheck also branched on the
removed legacy runtime (componentContract's exclusion filter and the
runtime-namespace loop). Removes both, adds a regression test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 00:51:21 +05:30
ClintchizandClaude Opus 5 ec63090006 refactor: replace the legacy function runtime with shared
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:13:05 +05:30
ClintchizandClaude Opus 5 224af8fd96 docs: four implementation plans for the cleanup, apis block, migration, and editor work
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:05:31 +05:30
ClintchizandClaude Opus 5 68cc75d0b0 docs: fold the auth deprecations into the cleanup spec as a firm scope
They are our own superseded options, not a stale dependency. The recommended
form is already what the showcase example uses; the blast radius is three
test files inside packages/auth, and the rpId/origin options are already
ignored at runtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:55:50 +05:30
ClintchizandClaude Opus 5 43652c14af docs: specs for apis blocks, migration, and editor tooling
Three specs completing the set, each depending on the one before it:

- apis {}: one container, mode-less declarations, api.<name>() callable
  anywhere with build-time dispatch, AsyncLocalStorage for server context,
  usage-driven emission, three render-binding forms. Replaces the ssr {} /
  client {} data blocks and the untypeable with($data) legacy body.
- update: migrations to the new syntax. The legacy bare-body rewrite is
  deliberately manual -- which free identifiers are payload fields is not
  knowable from the source, so an automatic guess would compile and be wrong.
- editor tooling: grammar, completions for api. and the api= attribute,
  diagnostics for removed constructs, and resolving by observation whether
  generated type errors surface inline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:52:31 +05:30
ClintchizandClaude Opus 5 e40d8319a6 docs: spec for the legacy, deprecated, and unused-config cleanup
All seven compatibility keys are dead configuration: traced every reference
and none is read by any compiler, codegen, or runtime code. They are written
into every generated config and ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:47:22 +05:30
ClintchizandClaude Opus 5 2f075df42c release: patch cli, compiler, core, csr, syntax, validation
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
Typed, callable api blocks for .wrn files: sectioned request/response/error,
callable from client code as api.name(input), type-checked by tsc against the
route contracts.

Also two fixes found along the way: defineEndpoint never received its input
through the real router, so every typed endpoint was validating undefined; and
v.boolean() silently coerced any unrecognised string to false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:36:44 +05:30
ClintchizandClaude Opus 5 281615a4b0 fix(validation): stop unrecognised boolean strings coercing to a silent false
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
checkField in packages/validation/src/index.ts (and its browser mirror in
runtime.ts) treated any string other than "true"/"on" as false with no
error, so typos like "treu" or values like "yes"/"1"/"TRUE" silently
passed as false.

Now:
- true/false booleans pass through unchanged
- recognised true strings (case-insensitive, trimmed): true, on, 1, yes
- recognised false strings: false, off, 0, no
- numeric 1/0 coerce (JSON payloads)
- undefined/null/"" still coerce to false (unchecked-checkbox semantics)
- anything else is now a type error (desc.typeMessage or "Must be true or
  false") instead of a silent false

Locked-in behaviours preserved: a required boolean given false still
errors, and parseEnv DEBUG: "true" coercion still works.

Added coverage for recognised strings, numeric 1/0, the type-error
regression guard, absent/empty handling, the required+false case, and a
client/server parity test driving both checkField and the browser runtime
through the same inputs.

Blast radius: searched packages/, examples/, services/ for v.boolean()
usage; all existing call sites (auth consent/rememberDevice, db 'active'
default, example consent checkboxes) feed true/false/'on'/absent values,
none of which change behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:26:20 +05:30
Clintchiz 3ae5d7cf97 fix(compiler): stop error {} from swallowing response {} bugs in api blocks
Client codegen chained .then().catch(), so a .catch() after .then()
caught exceptions thrown by the response body too. Switched to the
two-argument then(onFulfilled, onRejected) form, whose rejection
handler cannot see errors from the fulfilment handler.

SSR codegen wrapped both the transport call and the response-body eval
in the same try; only __wrnexusCallApi is now inside the try, and
__wrnexusEvalData runs after it, outside.

Also verifies (and locks in with a regression test) that GET query
numbers already coerce correctly through defineEndpoint + checkField,
and documents that in the typed-api-block spec.
2026-08-19 21:11:26 +05:30
ClintchizandClaude Opus 5 b5029889a5 fix: address all seven final-gate findings for typed api blocks
B1: qualify each generated __wrn_api_check_* assertion name with a short
hash of the page's path (relative to app/, for reproducibility across
checkouts) so two pages declaring a same-named block no longer collide
with an identical type alias (TS2300).

B2: skip assertion emission for any block that is not client-mode, or
that has zero declared request fields. ssr sectioned blocks can never
declare a request and always fell back to Record<string, never>, whose
keyof is `string` -- making the key-exactness arm of AssertAssignable
evaluate to false unconditionally (TS2344) on every ssr sectioned block
regardless of correctness. Chose to skip both non-client blocks and
zero-field client blocks, since neither has anything meaningful to
assert type-safety about.

B3: only resolve the endpoint's input (query params / ctx.req.json())
when the endpoint declares an input schema. Previously the router-set
fix accidentally read the request body unconditionally, so a handler
with no input schema that parses the request itself hit
ERR_BODY_ALREADY_USED.

B4: run response/error bodies in client-mode api blocks through
eraseFunctionTypes, matching every other browser-bound body in
client-codegen.ts, so a TypeScript-only construct inside one (e.g. an
annotated locally-declared function) doesn't reach the .mjs artifact.

B5: only exclude "api" from state/prop destructuring in the generated
browser module when the page actually has client-mode api blocks (i.e.
there is a real `api` binding to shadow). Previously "api" was always
excluded, so a page with `state api` and no api blocks got an
undeclared `api` reference (ReferenceError) in client code.

B6: prefix each emitted assertion with `export`, so it isn't flagged as
an unused local under a downstream project's noUnusedLocals (TS6196).

B7: wrnexusCallApi now resolves with undefined for an ok 204/205
response, or an ok response with an empty/unparseable body, instead of
rejecting with "Response was not valid JSON" -- matching the spec's
failure table (error path only for non-2xx, network failure, or an
actually unparseable body on a non-empty response).

Regenerated examples/basic-app's generated types and editor bundles to
match. Confirmed the example's type gate still fails when an
unaccepted field is added to a request body, and passes cleanly
otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:38:22 +05:30
ClintchizandClaude Opus 5 e9db4ca24d test(core): cover defineEndpoint's no-second-argument request-parsing path
Every existing test in endpoint-schema.test.ts passed rawInput
explicitly, so the branch added to endpoint.ts's fix (GET/HEAD query
parsing, JSON body parsing, malformed/absent body fallback) was
exercised by nothing but a manual curl. Add coverage that calls the
endpoint with only a context, matching the real router's calling
convention:

- GET with query parameters populates input from ctx.url.searchParams.
- POST with a JSON body populates input from the parsed body.
- POST with a malformed or absent body does not throw; the schema's
  own validation decides the outcome (asserted on the real response).
- An explicit rawInput argument still wins and the request is never
  read (the body is drained first, so a second .json() call would
  reject if the endpoint tried to read it again) -- the regression
  guard for the branch intentionally left untouched.

Confirmed the GET and POST-body tests fail against the pre-fix
endpoint.ts (input resolves as undefined/null instead of the sent
value); the malformed/absent-body test does not distinguish pre- and
post-fix, because in that specific edge case both normalize to an
effectively empty input -- noted in the report rather than forced.

Added a CHANGELOG entry documenting the behavior change for
downstream apps: a request that previously passed vacuous validation
on a defineEndpoint route can now legitimately fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:16:32 +05:30
ClintchizandClaude Opus 5 5318320c70 fix(examples): make the api-block demo endpoint actually type-check
directory.ts previously exported a plain (ctx: Context) => ... handler.
With that shape ApiInput<> resolved to unknown, so the generated
__wrn_api_check assertion for the demo page passed trivially even with
a field the endpoint does not accept -- the worked example did not
demonstrate the type safety it exists to demonstrate.

Rewrite directory.ts to use defineEndpoint with a schema (matching
typed-user.ts), which gives the generated assertion a real input type
to check against. Confirmed: adding an unaccepted field to the block's
request body now fails typecheck naming
__wrn_api_check_searchDirectory; removing it passes with zero net
diff.

Fix a real bug this surfaced: packages/core/src/endpoint.ts only read
its input from a second 'rawInput' argument, but the actual HTTP
router (packages/dev-server/src/runtime.ts handleApi) invokes route
handlers as handler(ctx) with no second argument. Every
defineEndpoint-based route -- including the pre-existing typed-user.ts
example -- silently received an empty/undefined input through the
real router (confirmed via curl: valid typed-user payloads were
rejected as 'Required'; directory's name filter matched every record
regardless of query). Fixed by having the endpoint wrapper parse the
request itself (query params for GET/HEAD, JSON body otherwise) when
no rawInput is explicitly supplied, while still honoring an explicit
rawInput for direct/unit-test callers.

Also update api-block-demo.wrn's response section: defineEndpoint
wraps handler output as { data: ... }, so the block's raw response
body is now { data: { users: [...] } } -- response reads
data.data.users instead of data.users.

Re-verified in a real browser after the endpoint rewrite and the
router fix: search returns exactly "Ajay, Asha", exactly one
POST /api/directory carrying x-csrf-token, and the error section
still runs cleanly (no exception, empty result) on a missing route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:05:10 +05:30
Clintchiz b3ed9689fa docs: add complete framework feature report
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-19 17:52:08 +05:30
ClintchizandClaude Opus 5 e5de7b54a5 feat(examples): worked example for typed api blocks
- Add examples/basic-app/app/api/directory.ts and app/pages/api-block-demo.wrn
  as the end-to-end worked example for typed api blocks (Task 6).
- Add **/*.generated.api-checks.ts to .prettierignore: this generated file
  must match the CLI's raw output byte-for-byte for check:generated-types,
  and prettier was reformatting it.
- Fix packages/csr/test/api-call.test.ts: no-unsafe-function-type lint error
  from the raw Function type, uncovered while running the full gate.
- Rebuild editors/vscode bundles (packages/compiler and packages/syntax
  changed in Tasks 1-5).
- Regenerate docs/public-api-0.8.json (additive: isIdentPart, isIdentStart,
  skipLiteralOrComment newly exported from packages/syntax).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:08:49 +05:30
ClintchizandClaude Opus 5 3252b1b20e fix(compiler): type-check the emitted ssr binding array and share error handling across call sites
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:42:38 +05:30
ClintchizandClaude Opus 5 7601477f7d feat(compiler): run error section on ssr api call failure
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:24:20 +05:30
ClintchizandClaude Opus 5 847b6dbe59 feat(compiler): support sections in ssr api blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:19:27 +05:30
ClintchizandClaude Opus 5 04be24ddd8 fix(cli): close the extra-field gap in AssertAssignable, add tsc-based enforcement tests
AssertAssignable was one-directional ([Actual] extends [Expected]), so a
block declaring a field the contract doesn't accept passed silently
(TypeScript's excess-property check only applies to fresh object
literals, not conditional-type extends). Add a key-exactness check
(Exclude<keyof Actual, keyof Expected> extends never) alongside the
assignability check. Guard it with 'unknown extends Expected' so
untyped (no defineEndpoint contract) routes still only warn, per the
existing behaviour, instead of being forced to fail on every declared
field.

Add packages/cli/test/api-block-types.test.ts cases that regenerate a
fixture and run the real TypeScript compiler (via bunx tsc) over the
generated output, asserting on its diagnostics rather than on emitted
text: matching fields compile clean; a wrong-typed field, an extra
field (the Finding-A regression guard), and a missing required field
all fail, each pointing at the offending block's __wrn_api_check_*
line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:14:13 +05:30
ClintchizandClaude Opus 5 785e8a65bd fix(cli): move api-block assertions into a real .ts file
skipLibCheck exempts .d.ts contents from being checked, so assertions
written inside wrnexus.generated.d.ts were never evaluated by tsc.
Emit them into wrnexus.generated.api-checks.ts instead, referencing
the WRNexusGenerated namespace's helper types (which stay in the
.d.ts). Track the new generated file in check:generated-types.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:01:07 +05:30
ClintchizandClaude Opus 5 a2698fb51a docs: correct the api block spec's type-enforcement mechanism
Assertions in a .d.ts are inert under skipLibCheck: true, which the root
tsconfig sets. Proven during implementation by forcing skipLibCheck: false,
where the same assertion fires as TS2344. They move to a generated .ts file,
which skipLibCheck does not exempt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:58:05 +05:30
ClintchizandClaude Opus 5 419614d9d1 feat(cli): generate type assertions for api blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:56:02 +05:30
ClintchizandClaude Opus 5 70777e4a45 fix(compiler): reserve api as a runtime binding to avoid client-scope collision
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:45:09 +05:30
ClintchizandClaude Opus 5 bd2f6ac5e3 feat(compiler): compile client api blocks into the browser module
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:41:13 +05:30
Clintchiz b457ad1d54 feat(csr): add the api block transport 2026-08-19 15:37:23 +05:30
ClintchizandClaude Opus 5 323f57b32b fix(syntax): make api-section scanning string/comment-aware
Reuse tokenizer.readBalancedBraces string/comment skipping (extracted as
skipLiteralOrComment) for both section detection and slicing, instead of a
second hand-rolled brace counter. Fixes truncation on braces inside strings
and false-positive sectioned detection from keywords inside comments/strings.
Also switch api-sections.ts errors from plain Error to LexError so parser.ts
upgrades them to ParseError with an offset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:30:51 +05:30
ClintchizandClaude Opus 5 028c2a6d64 feat(syntax): parse sectioned api blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:21:10 +05:30
ClintchizandClaude Opus 5 2f0f82b29f chore: ignore the subagent-driven-development workspace
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:18:39 +05:30
ClintchizandClaude Opus 5 50097ec4b4 docs: implementation plan for typed api blocks
Six tasks: parse the sections, add the CSR transport, compile client-mode
blocks into the browser module, generate the tsc assertions, support
sections in ssr blocks, and verify end to end in a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:15:02 +05:30
ClintchizandClaude Opus 5 f026415ba6 docs: design for typed, callable api blocks in .wrn files
A sectioned `api` block -- request / response / error -- callable on
demand from client code, typed against the API route contracts the types
generator already emits.

Records the constraints that shaped it: the current block cannot carry a
query string (isSafeApiPath rejects "?"), cannot interpolate (readPath
stops at "{"), has nowhere to put a body, and fetches once. And the one
that decides the type-safety mechanism -- generated build artifacts are
not type-checked, so enforcement goes into the generated .d.ts, which the
project's own tsc already compiles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:09:30 +05:30
ClintchizandClaude Opus 5 2cbc3e43e1 release: patch compiler, cli, dev-server
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
Strips TypeScript from client function bodies when emitting browser
modules, so `wrnexus build` no longer fails on an annotated local.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 14:17:51 +05:30
ClintchizandClaude Opus 5 55fed2177a fix(compiler): strip TypeScript from client function bodies
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
`wrnexus build` failed on any client function whose body used TypeScript:

    const requestBody: Record<string, unknown> = {}
    error: Expected ";" but found ":"

Codegen copies a client function's body into the browser module verbatim.
It removes the types from the function's *signature*, which is what made
this easy to miss -- the emitted module looked transpiled, and only bodies
carried types through. The artifact is written as .mjs and read back as
plain JavaScript, so the failure surfaced as a syntax error in generated
code rather than at the .wrn line responsible.

Browser modules are now transpiled before they are written, at all three
sites that emit one (the production build and both dev-server paths).

Reproduced end to end: a page with an annotated body failed the build with
the reported errors, and after the fix builds, ships valid minified JS, and
runs -- the handler sets its state correctly in a browser.

Note: the same body is also embedded as a string for the CSP-safe fallback
interpreter, which still receives it untranspiled. The compiled module
shadows the fallback, so this is only reachable in the window before that
module loads. Left alone here because stripping it lives in codegen, which
also runs under Node in the editor bundle where the Bun transpiler is
unavailable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 14:03:14 +05:30
ClintchizandClaude Opus 5 8c609edd32 release: patch csr, dev-server, language-server, react, ui
Quality / quality (ubuntu-latest) (push) Failing after 11m36s
Quality / quality (windows-latest) (push) Canceled after 0s
Client control blocks and loops, the dev-server rebuild recycle, the
editor's tag and completion handling, and the island mount/HMR fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 13:42:26 +05:30
ClintchizandClaude Opus 5 e898929193 fix(react): mount visible islands and load rebuilt code after HMR
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
Two faults found by driving the island demo in a real browser. Both were
silent: the markup, every asset, and all 48 island tests were correct
either way.

An island renders nothing until it mounts, so its placeholder is
zero-height, and IntersectionObserver does not treat a zero-area target
consistently -- client:visible islands mounted on one load and not the
next. Visibility for those is now decided from the element's own rect,
driven by scroll and resize; a placeholder with real size still uses the
observer. The strategy had no test at all, which is why this shipped.

After an island source edit the browser kept running the old code. The
rebuild worked and the file was refetched, but the loader imports a URL
that does not change, and the browser caches modules by URL. Remounts now
carry a generation the dev loader folds into the request.

Verified in the browser: mounts with start={3} as a number, clicks reach
React (3 -> 5), and an edit to Counter.tsx now shows the new text and
stays interactive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 13:32:05 +05:30
ClintchizandClaude Opus 5 18a1c40118 fix(dev-server): recycle the server once hot rebuilds pile up
The dev server got slower the longer it ran. Measured on the example app:
30 .wrn edits grew RSS from 117 MB to 137 MB and never gave it back, while
30 CSS edits cost nothing -- so the leak is exactly one retained module
identity per rebuild, not caches or file handles.

That is inherent to reloading a module in-process. Bun caches modules by
path, so a rebuild has to be given a new identity to be picked up at all,
and Bun has no API to unload the old one. At roughly 0.66 MB a rebuild, a
long editing session is several hundred megabytes of garbage that cannot
be collected.

The process now recycles itself past a rebuild threshold, exiting with the
RESTART_EXIT_CODE the CLI supervisor already respawns on; browsers
reconnect because the HMR client already retries. It waits for a quiet
period first so a live request is never cut off, and the threshold (300
rebuilds, about 200 MB) sits well above a normal session. Set
WRNEXUS_DEV_RECYCLE_AFTER to tune it, or 0 to switch it off.

Also bounds browserArtifactPaths and islandArtifactPaths, which are keyed
by content hash and so gained an entry per rebuild that was never read
again. Small next to the module leak, but unbounded is unbounded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:30:54 +05:30
ClintchizandClaude Opus 5 a20f143acb fix: isolate test globals, close tags at every caret, trim the runtime
Three pre-existing issues that the previous commit worked around rather
than solved.

Test global pollution. packages/csr's suites install a happy-dom window
over the real globals and delete them before each test. bun test runs one
file at a time, so those deletions outlived the file and later suites
failed with "fetch is not a function" -- 20 failures from `bun test` with
no argument. They now restore what they captured. The editor's Node tests
shim the vscode host by patching Module._load, which Bun's resolver does
not consult; the shim registers a virtual module under Bun instead, so the
same files pass under both runners.

Multi-cursor tag auto-close. The handler now closes the tag at every
caret. Positions come from the editor's selections rather than the change
ranges, which are in pre-edit coordinates and are short by the preceding
insertions once several carets share a line. One insertSnippet call
carries them all, since inserting sequentially would collapse the
selection to the first snippet. Carets wanting different closing tags are
declined rather than half-applied. Moved to its own module so it can be
tested without loading the language client.

Runtime size. Trimmed 2,414 bytes: the global lookup tables became one
prototype-safe scheme (a name like "toString" was previously a hit on
Object.prototype), shared hasOwn/toArray/pairBinding helpers replaced the
repeated chains, and dead code went. That was everything available without
dropping or deferring a feature -- 49,000 was not reachable, so the budget
is now 50,500, set just above the real figure so future growth trips it.

Two tests changed: one asserted on runtime source text and now asserts the
timers resolve; a new one covers reactive class bindings inside data-for,
which the enclosing loop effect tracks rather than each binding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:14:18 +05:30
ClintchizandClaude Opus 5 ac248f2bb0 fix(csr): run for/while loops and keep declarations out of state
The client runtime had no loop support, so any shared function using one
returned early -- Pagination and ButtonGroup were broken client-side, not
just in tests.

Adding loops exposed two further faults:

- A var reaching writeScope creates a signal and triggers a render sweep.
  A declaration inside a function called during a render therefore looped
  forever. Declarations now bind into the handler locals instead.
- A control block removed from the DOM keeps its effect in the renderers
  list. Running it against a detached node threw, aborting the sweep and
  leaving every later effect stale.

Also raises the reactive runtime budget to 53,000: the runtime had already
grown past 49,000 before this change, and 52,570 minified is 16,803 gzipped.

Two deferred minors: html-service leaves absent documentation undefined
rather than an empty string, and the extension declines tag auto-close on
multi-cursor edits rather than closing only the first cursor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 02:24:41 +05:30
ClintchizandClaude Opus 5 0904a4efaa fix(csr): render control blocks created by a client rerender
{#if}, {#each} and their {:else}/{:else if}/{:empty} branches worked on
the server and after hydration, but a block nested inside another block
stayed empty once the outer block rerendered. Adding a row to a list
produced the row's markup with its inner block markers in place and
nothing between them, for the life of the page.

Two causes, both on the client-created path only:

reactive() registers an effect; effects run when renderAll sweeps the
list. A state change runs just the affected effects rather than sweeping,
so an effect registered during that rerender was queued and never
invoked. setupControlBlock now returns its runner and the creating block
invokes it immediately.

The first reactive pass is skipped so hydration does not discard
server-rendered DOM. A block created by a rerender has no server DOM, so
skipping its only pass left it permanently empty. firstRun is now keyed
off outerLocals, which is set only on the client-created path.

Verified in a browser as well as in tests: adding a group to a list now
renders the new row's nested {:else}, and the existing rows' nested loops
survive the rerender.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 00:49:31 +05:30
Clintchiz 5b11b937bb Release CLI with reactive control block runtime
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-19 00:27:32 +05:30
Clintchiz f199385204 Make if and each blocks reactive on the client
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-19 00:22:00 +05:30
Clintchiz c0c2fa4595 chore(release): publish CLI 0.8.43
Quality / quality (ubuntu-latest) (push) Failing after 9m48s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 23:38:54 +05:30
Clintchiz cfdcdd00ad chore(release): publish dev server 0.8.39
Quality / quality (ubuntu-latest) (push) Failing after 9m58s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 23:31:21 +05:30
Clintchiz 03d5cb6aa6 fix(gateway): proxy browser server functions to workspace apps
Quality / quality (ubuntu-latest) (push) Failing after 10m40s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 23:30:42 +05:30
Clintchiz 701acd828c fix(vscode): avoid relative-link parsing in changelog
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 22:55:11 +05:30
Clintchiz b28b79c370 fix(vscode): use supported Marketplace publish flags
Quality / quality (ubuntu-latest) (push) Failing after 6m1s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 22:52:59 +05:30
Clintchiz fdd0c9f847 chore: complete HTML editing verification
Quality / quality (ubuntu-latest) (push) Failing after 10m13s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 22:49:07 +05:30
Clintchiz 7ad336b4dd chore(vscode): prepare 0.8.8 marketplace release
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 22:43:01 +05:30
Clintchiz d70e89230b chore(release): publish language server 0.8.10
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 22:38:47 +05:30
ClintchizandClaude Opus 5 7e6d8c3bc8 test(language-server): add end-to-end verification for HTML editing support
Adds a scratch page (kept intentionally, per controller ruling on Task 8)
and one end-to-end test that drives the real language server over LSP
stdio against that page's real text, verifying completion (with the
seo-block negative case tested via a simulated '<' keystroke and
mutation-verified against html-regions.ts), hover, folding ranges,
linked editing, and wrn/tagComplete all work together on realistic
content.

Regenerates routes.gen.ts and wrnexus.generated.d.ts for the new page's
route, required by check:generated-types.

Two checks from the original brief (auto-close-tag insertion and Emmet
Tab-expansion) require a live VS Code Extension Development Host and
are documented as outstanding manual verification in
.superpowers/sdd/2026-08-18-wrn-html-editing/task-8-report.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:59:19 +05:30
ClintchizandClaude Opus 5 92c0920c9b test(vscode): guard the Emmet mapping and auto-close setting
Also fix an unused-var lint failure in completion-scope.test.js blocking the production gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:36:20 +05:30
Clintchiz a8d8ac386f test(vscode): add integration tests for completion provider guard 2026-08-18 21:28:38 +05:30
Clintchiz 2c8841cc5f fix(vscode): stop duplicating completions inside view blocks 2026-08-18 21:23:54 +05:30
ClintchizandClaude Opus 5 b344d2a70a fix(vscode): guard tag auto-close against replaced selections and stale round-trips
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:20:16 +05:30
ClintchizandClaude Opus 5 e83f0366ef feat(vscode): close HTML tags as they are typed in .wrn files
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:16:47 +05:30
Clintchiz d9e8f5be82 feat(language-server): add tag folding and linked editing 2026-08-18 21:10:57 +05:30
Clintchiz bd0c1317ff test(language-server): cover didClose region-cache clear end-to-end
Replaces the direct-call-only test with an over-stdio test that exercises
server.ts's didClose handler itself, so it fails if the
clearHtmlRegionCache wiring is removed or misparameterized.
2026-08-18 21:04:33 +05:30
Clintchiz 97801287f9 feat(language-server): merge HTML completions and hover into one response 2026-08-18 20:56:17 +05:30
Clintchiz d77638131b fix(language-server): don't self-close tags inside quoted attribute values 2026-08-18 20:47:19 +05:30
Clintchiz 609224591c feat(language-server): answer HTML completion, hover, folding, and tag close 2026-08-18 20:43:47 +05:30
Clintchiz 6074d19c43 fix(language-server): bypass cache for version-less documents 2026-08-18 20:38:37 +05:30
Clintchiz 7301849a7f feat(language-server): add offset-preserving virtual HTML document 2026-08-18 20:34:24 +05:30
ClintchizandClaude Opus 5 7d481df652 docs(html-editing): add implementation plan
Eight TDD tasks: the view-block scanner and virtual document, the HTML
service wrapper, merging HTML into completion and hover, folding and
linked editing, auto-close on type, standing down the duplicate client
provider, manifest guards, and a manual editor check.

Task 1 comes first because everything reads positions through it: its
length-and-newline invariant is what removes position mapping, and a
break there would misreport positions everywhere rather than fail.

The last task is manual verification in an Extension Development Host.
Unit tests cannot show that completions actually appear in an editor, and
a green suite has hidden non-functional features in this repo before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:30:34 +05:30
ClintchizandClaude Opus 5 5477f5436d docs(html-editing): add design spec for HTML support in .wrn files
Markup in a .wrn file highlights but has no tag or attribute completion,
no tag closing, and no tag-level folding: the grammar's embeddedLanguages
mapping only affects tokenization, and VS Code's HTML language service
never runs on these documents.

The design extracts view blocks into a virtual HTML document where
everything outside them is blanked to whitespace of identical length, so
source positions and virtual positions are the same and no mapping table
is needed. Region detection is a tolerant scanner rather than the parser,
because completion fires while the document is mid-edit and unparseable.

Completion merges WRNexus and HTML entries into one list ranked by
sortText, which also fixes an existing bug: the extension and the server
both answer completion on '<' today, so VS Code concatenates two lists.

Two decisions worth review:

- HTML formatting is excluded. formatWrn already formats markup, knows
  WRNexus syntax, and would fight a second formatter that is free to
  rewrite spacing inside @click={...} and client:visible.
- Only auto-close-on-type is client-side. Linked editing is standard LSP
  and lives in the shared server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:23:08 +05:30
ClintchizandClaude Opus 5 b646ec8d00 chore(release): patch-bump packages changed since the last publish
Quality / quality (ubuntu-latest) (push) Failing after 12m48s
Quality / quality (windows-latest) (push) Canceled after 0s
cli 0.8.42, csr 0.8.22, db 0.8.16, dev-server 0.8.38,
dev-toolbar 0.8.13, i18n 0.8.12.

Every previous version was already on the registry, so the HMR client
repair, the i18n JSON data block, the gateway WebSocket origin fix, and
the generated-dialect stamp were not reachable by consumers.

compiler and react are unchanged since their last publish and are not
bumped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:01:01 +05:30
ClintchizandClaude Opus 5 e66d2425aa fix(gateway): allow HMR sockets on every configured domain
Quality / quality (ubuntu-latest) (push) Failing after 13m52s
Quality / quality (windows-latest) (push) Canceled after 0s
The WebSocket origin check compared the browser's Origin host, which
carries the port, against configured domains, which do not. publicOrigin
only ever matches domains[0], so every other domain fell through to that
comparison and was denied purely on the port: web.localhost:3000 never
matched web.localhost.

The result was a 403 on the HMR upgrade and a client reconnecting
forever, while the page itself loaded fine because HTTP routing resolves
the Host separately.

Compares hostnames now. Unrelated and lookalike-suffix origins are still
denied, and both cases are covered by tests.

Verified through a real gateway: the HMR socket opens on both localhost
and web.localhost, and a live edit reaches the browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:53:27 +05:30
ClintchizandClaude Opus 5 b3b65dddd8 fix(db): stamp the dialect into generated query files
Quality / quality (ubuntu-latest) (push) Failing after 12m46s
Quality / quality (windows-latest) (push) Canceled after 0s
The same generate command emitted ? one run and $1 the next, which looked
like non-determinism. It is not: postgres uses $1 placeholders where
sqlite and mysql use ?, and the driver comes from the active profile, so
building under a different profile rewrites this committed file.

The header now records the dialect it was generated for, making the flip
visible in the diff and explaining check:generated-types failures instead
of leaving them looking like random churn.

Worth deciding separately: a committed artifact whose contents depend on
the active profile will keep drifting. Either generate per dialect, or
stop committing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:44:36 +05:30
ClintchizandClaude Opus 5 5dbcc5b85d fix(i18n): ship i18n data as a JSON block so CSP cannot block it
window.__wrnI18n was undefined in development: the payload shipped as an
executable inline script, and a document's CSP nonce is fixed at load, so
any such script arriving from a later response is blocked. Client
translations and language switching silently had no data.

The payload is now a type="application/json" block, which the browser
never executes and script-src therefore never applies to. The i18n
runtime, CSR navigation, and HMR all read the block instead of matching
window.__wrnI18n= with a regex.

Pages now render zero executable inline scripts, so an inline script-src
violation is structurally impossible rather than merely unobserved. Zero
framework JavaScript on island-free routes is unaffected: the block is
inert data, and nothing loads to read it unless the page needs it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:44:28 +05:30
ClintchizandClaude Opus 5 e819c5739e Revert "chore(example): regenerate basic-app queries"
Quality / quality (ubuntu-latest) (push) Failing after 12m38s
Quality / quality (windows-latest) (push) Canceled after 0s
The generator's placeholder style is not deterministic across runs: the
same command emitted $1 once and ? the next time, depending on the
database dialect active in the environment. Restoring the committed
output and reverting my earlier regeneration, which was environment
churn rather than an intended change.

Worth a look on its own: a generator whose output depends on ambient
environment makes check:generated-types environment-sensitive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:26:07 +05:30
ClintchizandClaude Opus 5 ac164789bd fix(dev): serve the HMR client as an external script
A document's CSP nonce is fixed at load, so an inline script delivered by
a later response can never carry a nonce that document accepts. The HMR
client is now served at /__wrnexus/hmr-client.js, which script-src 'self'
already covers and which needs no nonce at all.

This removes one of the two inline scripts CSP was blocking in
development. The i18n data script is still blocked and needs the same
treatment; it is shared with the CSR navigation and HMR parsers, so
moving it spans @wrnexus/i18n, csr, and dev-server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:25:40 +05:30
ClintchizandClaude Opus 5 88ec3a5cb6 chore(example): regenerate basic-app queries
Regenerated output for the committed query generator: positional
placeholders now render as $1 rather than ?.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:17:15 +05:30
ClintchizandClaude Opus 5 28085b5a43 chore(db,scripts): pending migration and packaging tweaks
Pre-existing working-tree changes to migration SQL parsing, query
generation, and the packaging scripts. Committed as-is rather than
authored here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:17:02 +05:30
ClintchizandClaude Opus 5 5d7bd81601 fix(dev-toolbar): report development-appropriate budgets
Excludes the toolbar's own bundle from the JavaScript budget, raises the
development thresholds, and skips WRNexus UI and theme stylesheets when
measuring CSS coverage, so unminified development modules and framework
styles stop reading as application problems.

Pre-existing working-tree change, committed as-is rather than authored
here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:17:01 +05:30
ClintchizandClaude Opus 5 5e114d867f feat(gateway): forward application identity on WebSocket upgrades
Adds gatewayWebSocketBackendHeaders so proxied upgrades carry application
identity while Bun keeps ownership of WebSocket framing.

Pre-existing working-tree change, committed as-is rather than authored
here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:16:54 +05:30
ClintchizandClaude Opus 5 10421465df fix(styles): stop scanning every component for utility sources
Component discovery is not utility-source discovery: scanning all
built-in and plugin component directories made Tailwind/Iconify generate
rules for components the app never renders. Packages that need scanning
opt in through styles.source.

Pre-existing working-tree change, committed as-is rather than authored
here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:16:53 +05:30
ClintchizandClaude Opus 5 52cce2c628 style(docs): apply Prettier to the React islands spec and plan
Formatting only; no content change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:16:45 +05:30
ClintchizandClaude Opus 5 afa2a8c093 chore(deps): move packages to TypeScript 6.0.3
Raises the typescript devDependency across the workspace, bumps package
versions, re-adds ignoreDeprecations, and repoints the @wrnexus registry.

These were pre-existing working-tree changes, committed as-is rather than
authored here. The .npmrc change redirects @wrnexus publishes from
registry.npmjs.org to registry.workroot.in — confirm that is intended
before publishing from this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:16:34 +05:30
ClintchizandClaude Opus 5 4dd7681bf7 fix(dev): repair the HMR client and keep islands alive across updates
The HMR client script was dead in the browser. HMR_CLIENT_JS is a
TypeScript template literal, so the regex [ \t\r\n] inside it was expanded
into real control characters, producing a regex literal containing a raw
newline — a syntax error that took the whole script down with "Invalid
regular expression: missing /". It now uses \s, and a test asserts the
emitted client parses and holds no control characters inside regex
literals; that test fails if the bug is reintroduced.

HMR also corrupted CSP nonces. A document's nonce is fixed at load, but
morph copied attributes from freshly fetched HTML, overwriting the live
nonce with one the browser will not honour. syncAttrs now leaves nonce
alone, and nodes moved across are re-stamped with the live nonce.

Islands vanished on every HMR update: morph puts the server placeholder
back over the mounted island. The island runtime now remounts on
wrnexus:hmr-updated. Remounting swaps the container for a bare clone —
re-rendering the existing root is a no-op once HMR has wiped the DOM
externally, and unmounting throws asynchronously because the nodes React
wants to remove are already gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:15:21 +05:30
ClintchizandClaude Opus 5 843db2815f fix(islands): rebuild on .tsx edits and support islands inside components
Three bugs found by driving the dev server rather than reading code:

1. An island used inside a .wrn component still emitted a component mount
   — only the page and nested-page render paths were covered.

2. Editing an island .tsx never rebuilt in dev. The bundle cache was keyed
   on source path alone, and page modules are cached after the first
   request so no compile runs to notice the change. The cache key now
   includes mtime, and the file watcher rebuilds islands whose .tsx
   changed.

3. A .wrn cache hit skipped island building entirely, so after a restart
   with a warm cache no island bundle was ever produced. Island inputs are
   now persisted beside the other artifacts and rebuilt on a cache hit.

The islands manifest is deliberately excluded from the artifact
completeness check: only the async compile path writes it, so requiring it
made the sync path miss the cache on every call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:26:58 +05:30
ClintchizandClaude Opus 5 17aa3b98eb feat(islands): wire islands end to end
The island pieces existed but nothing connected .wrn compilation to island
emission. Now:

- codegen emits a data-wrn-island placeholder for component tags bound to
  .tsx imports, keeping .wrn components on the normal mount path
- the dev pipeline and static build resolve island imports, thread the
  names into codegen, and build the bundles
- collectScripts adds /__wrnexus/islands.js only when island markup is
  present, so island-free pages still ship nothing
- island routes classify as static-interactive via hasIslands

Three bugs found by driving a real page in the browser:

1. The mount runtime was never built anywhere, so the bootstrap 404'd and
   no island mounted.
2. Building the runtime separately from the islands gave each its own copy
   of React: "Cannot read properties of null (reading 'useState')". The
   runtime is now an entrypoint of the same build so React stays in one
   shared chunk. The existing single-React test only compared bundles
   within one build and could not see across build boundaries.
3. Island props arrived as attribute strings, so start={3} was "3" and
   incrementing produced "31" then "311". Props now follow JSX semantics:
   {…} parses as JSON, quoted values stay strings, and a runtime
   expression is a WRN-ISLAND-PROPS build error rather than a silent
   wrong value.

island-codegen.ts no longer imports @wrnexus/core. Compiler modules are
bundled into the Node-only VS Code extension, which contains no other
packages, so a runtime import of core broke the editor compiler; the two
helpers are implemented locally and the core dependency is dropped again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:16:03 +05:30
ClintchizandClaude Opus 5 442a3106ed test(islands): guard zero-JS routes and single-React bundling
Two guards protect the core promise: a route with no islands emits no
assets at all, and a page with several islands keeps React in one shared
chunk.

buildIslands now writes a generated entry per island instead of passing
component sources directly. Two islands sharing a source deduped to a
single entrypoint, and output order is not guaranteed to match input
order, so island names could bind to the wrong bundle.

Island modules are excluded from the editor compiler bundle: it globs
packages/compiler/src, and island-bundle.ts calls Bun.build while
island-codegen.ts imports @wrnexus/core — neither belongs in a Node-only
VS Code artifact.

Integration assertions share one build. bun test interferes with
Bun.build's module reads after several build calls in one process, while
the same calls succeed repeatedly outside the runner; production is
unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:50:26 +05:30
ClintchizandClaude Opus 5 3115277e9d chore(ui): refresh the stale UI visual contract baseline
Card, Carousel, Footer, Navbar, input, select, and textarea last changed
in d78707be, but the baseline was last regenerated several commits
earlier, so check:ui-visual already failed on main.

Unrelated to the React islands work; committed separately so the feature
changeset does not absorb it. Only source hashes changed — no UI source
was modified here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:44:27 +05:30
ClintchizandClaude Opus 5 02fdfa3aee feat(react): remount islands on hot module replacement
Disposes and re-creates island roots after a source change. Island state
resets by design; Fast Refresh needs a Babel/SWC transform plus a
runtime and is out of scope for v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:34:36 +05:30
ClintchizandClaude Opus 5 5f66c8129c docs(react-islands): drop the write-during-render guard
Implemented and removed. A render-phase flag cleared on a microtask is
still set when React runs effects, so islands writing from an effect —
the documented correct pattern — would throw. The flag also cannot be
set for an island's own re-renders, so real violations pass silently.

Detecting React's render phase reliably needs React internals, which is
not acceptable in a shipped framework. React already reports the real
hazard, and the getSnapshot caching requirement covers the loop case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:33:48 +05:30
ClintchizandClaude Opus 5 01a3b3b4e9 feat(compiler): classify island routes as static-interactive
A route mounting an island ships JavaScript, so reporting it as zero-JS
static would make the framework's performance accounting wrong.

Adds a separate needsIslandRuntime flag rather than reusing
needsClientRuntime: an island needs the island runtime, not WRNexus's
reactive runtime, and conflating them would ship the wrong bundle.

analyzeRuntimeRequirements takes island presence as an optional second
argument, so existing callers are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:30:19 +05:30
ClintchizandClaude Opus 5 a184f1a3be feat(islands): serve the island runtime in dev, prod, and static builds
Adds /__wrnexus/islands.js (the bootstrap) and the /__wrnexus/island/
prefix (mount runtime, island bundles, shared chunks) to all three
serving paths.

Dev reuses the browserArtifactPaths registry pattern from pipeline.ts.
Prod mirrors the clientModulesDir handler, including its filename
allowlist, so island names cannot escape the output directory.

The bootstrap is inert without a data-wrn-island marker, so island-free
pages still download nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:28:10 +05:30
ClintchizandClaude Opus 5 06df9d66ae feat(compiler): bundle islands with a shared React chunk
splitting:true keeps React in one shared chunk so a page with several
islands does not ship react-dom repeatedly.

Island .tsx is compiled against React's JSX runtime via a Bun onLoad
plugin. The repo's root tsconfig sets jsxImportSource to @wrnexus/core,
so islands would otherwise compile to the HTML-string renderer and never
mount. A @jsxImportSource pragma only affects the file carrying it, so
injecting one into the generated entry is not enough — the injection has
to happen per source file. App-authored islands stay plain .tsx.

The JSX test asserts built output rather than generated entry text,
because the entry-text assertion passed while the mechanism did not work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:22:58 +05:30
ClintchizandClaude Opus 5 d269772a79 feat(react): add island mount strategies and navigation-safe unmount
Mounts markers with client:only/load/visible/idle, and disposes roots on
route change so React roots, detached DOM, and store subscriptions do
not leak across client-side navigation.

Bundle load failures and malformed props JSON degrade to a warning and
leave the server markup intact rather than taking down the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:20:05 +05:30
ClintchizandClaude Opus 5 df2ee036eb feat(react): add per-island error boundary
A crashed island renders its error in dev and nothing in prod, leaving
the surrounding server-rendered page intact.

Island .tsx sources carry an explicit @jsxImportSource react pragma: the
repo's root tsconfig points jsxImportSource at @wrnexus/core, so without
it island JSX compiles to WRNexus's string renderer instead of React
elements.

Tests render on the client via createRoot rather than a server renderer,
because React error boundaries do not engage during SSR — and islands
are client-only regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:16:42 +05:30
ClintchizandClaude Opus 5 b405025f37 feat(compiler): resolve .tsx imports and tag them as islands
.wrn keeps resolution priority so existing components are unaffected
when a .tsx file shares their name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:15:08 +05:30
ClintchizandClaude Opus 5 cd07f0f8e2 feat(compiler): add island marker codegen and props contract
Emits the data-wrn-island placeholder, parses client:* strategies, and
rejects non-serializable props at compile time via WRN-ISLAND-PROPS so
the serialization boundary fails where it is cheapest to fix.

Island names become URL path segments when the browser fetches the
island bundle, so they are validated with core's existing
isSafeIslandName rather than relying on escaping alone. This adds
@wrnexus/core to the compiler's dependencies; core has no dependencies
of its own, so no cycle is introduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:13:56 +05:30
ClintchizandClaude Opus 5 ce67d0d1d2 feat(react): add useWrnStore bridge over useSyncExternalStore
Resolves WRNexus stores from inside islands, reading through the cached
snapshot so React sees a stable reference. Unknown store names throw
with the list of registered stores rather than failing opaquely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:12:33 +05:30
ClintchizandClaude Opus 5 468f63f378 feat(react): add referentially-stable snapshot and selector caches
useSyncExternalStore requires getSnapshot to return an identical
reference when unchanged; @wrnexus/store's readonlySnapshot returns a
fresh clone per call. The cache lives here rather than in the store
package so existing consumers are untouched.

createSelectorCache takes an optional equality function: the Object.is
default can never stabilize a selector that allocates, which is the
usual source of infinite re-renders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:11:22 +05:30
ClintchizandClaude Opus 5 a6570d7f68 docs(react-islands): add implementation plan
Twelve TDD tasks covering the approved spec: snapshot cache, store
bridge, marker codegen, .tsx resolution, error boundary, island runtime,
bundling with a shared React chunk, asset serving, route classification,
integration guards, the write-during-render guard, and HMR remount.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:07:32 +05:30
ClintchizandClaude Opus 5 4ef2ef14ff docs(react-islands): add design spec for opt-in React islands
Adds the approved design for consuming npm React components as
client-only islands inside WRNexus, without changing the SSR-first
rendering model.

Key decisions:
- New isolated @wrnexus/react package; react/react-dom as optional peers
- Client-only by default, SSR deferred to v2
- Islands declared via .tsx imports in .wrn frontmatter
- Store access via useSyncExternalStore, with the snapshot cache in the
  adapter so @wrnexus/store stays untouched
- Bundling extends the existing Bun pipeline; React as a shared chunk
- Routes with no islands must still ship zero framework JavaScript

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:01:58 +05:30
Clintchiz d78707be9f fix(forms): surface validation and recover schema drift
Quality / quality (ubuntu-latest) (push) Failing after 10m23s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-15 13:01:10 +05:30
Clintchiz 1d16ef1e82 chore(release): refresh ui consumers
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-15 12:33:40 +05:30
Clintchiz 12a1014db2 fix(ui): bind textarea values without markup
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-15 12:30:46 +05:30
Clintchiz 52b3e6d378 fix(csr): preserve translations across navigation
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-15 12:26:01 +05:30
Clintchiz f88dd47408 fix(runtime): stabilize navigation and custom errors
Quality / quality (ubuntu-latest) (push) Failing after 23s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-15 10:44:06 +05:30
Clintchiz f0447fddb0 fix(db): share registry across bundled copies
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-13 19:10:51 +05:30
Clintchiz 5106256401 fix(production): preserve emitted plugin runtimes 2026-08-13 18:00:30 +05:30
ClintchizandClaude Opus 5 94a6b436f5 test(examples): give each run its own sqlite database
The test profile pointed at ./test.db in the app directory, so a run inherited
whatever schema an earlier run left behind. On a machine with a stale file,
0003_add_password re-applied ALTER TABLE ADD COLUMN over a column that already
existed; fail-on-test-warnings turned the warning into a failure, and
check:production failed for environment reasons rather than code.

Each run now uses a fresh database under the OS temp directory. Verified by
restoring the stale dev.db and test.db that reproduced the failure: the suite
passes with them present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:38:28 +05:30
ClintchizandClaude Opus 5 2425a5146f chore: untrack the .publish staging directory
.publish/ is already listed in .gitignore, but 119 files were committed before
that rule existed, so they stayed tracked. The release prepare step clears and
restages the directory, which meant a routine `git add -A` during a release
would stage the deletion of every package it had not just staged.

Nothing reads the committed contents: publish-packages.ts writes the directory,
test-staged-consumers.mjs reads it after staging, and check-component-imports
skips it. Untracking leaves the release flow unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:37:04 +05:30
ClintchizandClaude Opus 5 fc2c526d70 chore(release): patch-bump csr, db and ui
@wrnexus/csr 0.8.18 -> 0.8.19   output handler errors are now reported
  @wrnexus/db  0.8.10 -> 0.8.11   dead quote tracking removed
  @wrnexus/ui  0.8.15 -> 0.8.16   Typography scoping, Pagination href template

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:10:24 +05:30
ClintchizandClaude Opus 5 fcd552969f chore: drop a stray launch entry and format migrate.ts
An over-broad `git add -A` in the previous commit swept a local dev-server
entry for an unrelated application into .claude/launch.json. It pointed at a
path outside this repository and was not Prettier-formatted, so it failed
format:check. Restored to the version on main.

Also runs Prettier over migrate.ts, which the dead-code removal left unformatted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:03:17 +05:30
ClintchizandClaude Opus 5 5deac48131 chore: refresh the Typography entry in the UI visual contract
The contract tracks a content hash per component source. Rewording the comment
in Typography.wrn changed its hash, so the check failed on an edit that alters
no rendered output. Regenerated intentionally; only that one entry moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:59:52 +05:30
ClintchizandClaude Opus 5 2e1b667285 fix(ui): avoid component-tag syntax in a Typography comment
check-component-imports scans sources for <Component> tags and requires each to
be imported. The explanatory comment added with the prose-scoping fix wrote
<Blockquote> literally, so the checker demanded an import for a component the
file never renders. Naming it without angle brackets keeps the explanation and
clears the gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:58:52 +05:30
ClintchizandClaude Opus 5 8393a953b7 fix(db): drop dead quote tracking in hasExecutableSql, clear lint
hasExecutableSql returns as soon as it meets a quote character, so the quote
variable was assigned and never read: the `if (quote)` branch could never run.
eslint reported it as a useless assignment and the error blocks the release
gate on main. Removing the variable and the unreachable branch keeps behaviour
identical, since encountering a quote already means the SQL is executable.

Also drops an eslint-disable directive in csr's output error reporter that
suppressed nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:57:34 +05:30
ClintchizandClaude Opus 5 2459a0b5a7 chore: sync bun.lock with the @wrnexus/ui package version
The lockfile recorded @wrnexus/ui at 0.8.13 while its manifest is 0.8.15, so
validate:0.8 failed its workspace version check. The drift predates this branch
and is present on main; bun install does not rewrite workspace metadata that is
already satisfied, so this applies the same targeted rewrite the release
tooling performs in syncWorkspaceLock. No version is bumped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:54:34 +05:30
ClintchizandClaude Opus 5 4e5949aedc chore: refresh the public API snapshot
The committed snapshot had drifted from source on main: dev-server exports
validateRpcCsrf, and styles exports the browser-cookie configuration types and
resolveBrowserCookieOptions. All six are additions, so the surface stays
backward compatible.

This unblocks check:production, which fails on main for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:51:22 +05:30
ClintchizandClaude Opus 5 60e38b983c build(editor): rebuild the bundled compiler and language server
The checked-in bundles predated the typecheck fix that resolves @wrnexus/ui
imports as component contracts, so VS Code reported "has no exported member"
for components such as Grid while the CLI typechecker passed. The package
exports nothing by design — components resolve by directory scan — so the
stale bundle was the whole defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:47:08 +05:30
ClintchizandClaude Opus 5 5afb2d1875 fix(ui): make Pagination usable server-rendered, surface output errors
Pagination reported the requested page only through its change output, so on a
server-rendered page the controls did nothing: the parent had to own client
state to react, and the emitted page never became a URL. An optional
hrefTemplate now renders the steps and page numbers as anchors, which work
before hydration and without JavaScript and give each page a crawlable URL.
Buttons remain the default for client-owned lists. End steps are clamped and
marked disabled rather than linking past the first or last page.

Output handler errors are no longer swallowed. Nothing awaits invokeOutput, so
a handler that threw became an unhandled rejection that never reached the
console and presented as a control that silently does nothing. Handler errors
are now reported as WRN-DEV-OUTPUT-HANDLER-ERROR.

Regenerates the component reference and the UI visual contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:46:00 +05:30
ClintchizandClaude Opus 5 2e42be7b31 fix(ui): stop Typography prose styles leaking into nested components
Typography styled every descendant element, including those inside nested UI
components. A <Blockquote variant="bordered"> placed inside <Typography> drew
two left borders: its own, plus the one these prose rules apply to any
<blockquote>. The same leak applied to headings, links, lists and code.

Prose rules now exclude the subtree of any nested `.wrn-component`, so they
style raw markup only. The exclusion sits inside :where() so specificity stays
at zero and applications can still override these rules with a plain class.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:09:46 +05:30
Clintchiz b508b49058 fix(dev): consolidate generated cache directories
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 22:25:12 +05:30
Clintchiz e638c6be6a fix(cli): bundle current WRN typechecker
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 22:11:32 +05:30
Clintchiz 082da38f67 fix(typecheck): resolve UI imports as component contracts
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 22:07:37 +05:30
Clintchiz 5de792f359 feat(config): add shared browser cookie policy
Quality / quality (ubuntu-latest) (push) Failing after 9m46s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 21:53:07 +05:30
Clintchiz a0dab308f0 fix(styles): persist accent cookies on localhost
Quality / quality (ubuntu-latest) (push) Failing after 10m41s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 21:28:18 +05:30
Clintchiz dd9b7a289a fix(theme): persist switcher accent for SSR
Quality / quality (ubuntu-latest) (push) Failing after 9m48s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 21:17:20 +05:30
Clintchiz effed1c3cf fix(styles): synchronize accent across open apps
Quality / quality (ubuntu-latest) (push) Failing after 9m48s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 21:11:22 +05:30
Clintchiz 6133d33b8c feat(styles): share accent cookies across app domains
Quality / quality (ubuntu-latest) (push) Failing after 9m48s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 20:40:35 +05:30
Clintchiz 32187a425c fix: refresh UI compiler dependency
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 20:25:06 +05:30
Clintchiz 531bf7b2da fix(ui): align footer legal bar
Quality / quality (ubuntu-latest) (push) Failing after 10m14s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 20:16:49 +05:30
Clintchiz afe1c413cc fix: reject RPC requests without CSRF tokens
Quality / quality (ubuntu-latest) (push) Failing after 11m28s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 19:49:58 +05:30
Clintchiz f68f79786f fix: compile typed catches and reject event loop syntax
Quality / quality (ubuntu-latest) (push) Failing after 9m46s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 19:36:18 +05:30
Clintchiz d47c57a953 fix: skip comment-only migration SQL
Quality / quality (ubuntu-latest) (push) Failing after 9m47s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 19:30:30 +05:30
Clintchiz 7a968977f6 fix: preserve parent RPC scope identity
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 19:23:27 +05:30
Clintchiz fe9bb9ede0 fix: resolve RPC component from hydration boundary
Quality / quality (ubuntu-latest) (push) Failing after 9m44s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 19:17:58 +05:30
Clintchiz 7ba7c8a73f fix: ignore WRN component examples in import validation
Quality / quality (ubuntu-latest) (push) Failing after 10m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 19:13:28 +05:30
Clintchiz 8e6dca0751 fix: require corrected UI dependency chain
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 19:10:19 +05:30
Clintchiz b76053ae93 fix: require complete UI package in dev server
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 19:08:15 +05:30
Clintchiz 13b198326f fix: include UI style imports in package
Quality / quality (ubuntu-latest) (push) Failing after 9m45s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 19:05:15 +05:30
Clintchiz 2c960fc1dc refactor: migrate legacy wire namespace to wrn
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 18:51:15 +05:30
Clintchiz ec23dd4c93 chore(release): publish rpc csrf fix
Quality / quality (ubuntu-latest) (push) Failing after 9m45s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 18:20:38 +05:30
Clintchiz f73fbb7aba fix(rpc): render canonical csrf token for clients
Quality / quality (ubuntu-latest) (push) Failing after 9m46s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 18:20:14 +05:30
Clintchiz 0f201c9767 chore(release): publish csr 0.8.14
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 18:15:29 +05:30
Clintchiz 9637a47ec7 fix(csr): send csrf token with component rpc
Quality / quality (ubuntu-latest) (push) Failing after 10m42s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 18:15:15 +05:30
Clintchiz 1719059fa0 chore(release): publish csr 0.8.13
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 18:08:51 +05:30
Clintchiz cf06a60f2b fix(csr): prevent native output recursion
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 18:08:35 +05:30
Clintchiz 8fa3d7ecd1 chore(release): publish cli 0.8.16
Quality / quality (ubuntu-latest) (push) Failing after 9m46s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 17:09:32 +05:30
Clintchiz bf0d866793 fix(cli): scaffold installable framework versions
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-12 17:08:38 +05:30
Clintchiz e189b2b62f chore(release): propagate document binding cleanup
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 19:14:19 +05:30
Clintchiz 6c49991700 chore(release): publish csr 0.8.12
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 19:13:04 +05:30
Clintchiz 4d6db0d887 fix(csr): consume document binding metadata
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 19:12:42 +05:30
Clintchiz 78ca2b85d9 chore(release): propagate csr runtime update
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 18:08:11 +05:30
Clintchiz d3fe0a71aa chore(release): publish csr 0.8.11
Quality / quality (ubuntu-latest) (push) Failing after 10m42s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 18:04:06 +05:30
Clintchiz 794a7b3378 fix(csr): consume hydration metadata from live DOM
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 18:01:11 +05:30
Clintchiz b1086d14e9 fix(auth): secure hydration and align password forms
Quality / quality (ubuntu-latest) (push) Failing after 6m7s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 17:05:04 +05:30
Clintchiz 743275e6fa fix(runtime): remove false browser diagnostics
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 16:46:44 +05:30
Clintchiz 904127687b fix(csr): recognize component-local CSS variables
Quality / quality (ubuntu-latest) (push) Failing after 6m8s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 16:24:15 +05:30
Clintchiz b72f517fa4 chore(cli): align development runtime dependency
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 16:12:02 +05:30
Clintchiz 52842f149d chore(dev-server): align UI registry dependency
Quality / quality (ubuntu-latest) (push) Failing after 10m21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 16:06:08 +05:30
Clintchiz 1cea44aaf2 test(ui): scope password requirement indicator
Quality / quality (ubuntu-latest) (push) Failing after 9m46s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 15:56:30 +05:30
Clintchiz c54a69532c fix(auth): keep signup fields readable
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 15:54:01 +05:30
Clintchiz 8d044565e3 feat: publish changed packages independently
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 15:32:36 +05:30
Clintchiz feff30a2b5 release: prepare WRNexusJS 0.8.8
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-11 13:40:03 +05:30
Clintchiz 816594a58b release: prepare WRNexusJS 0.8.7
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-10 01:23:12 +05:30
Clintchiz db612df6cd chore(release): refresh private package staging
Quality / quality (ubuntu-latest) (push) Failing after 14m12s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-10 00:55:12 +05:30
Clintchiz bca9549f3a finish CSS delivery and generated type remediation
Quality / quality (ubuntu-latest) (push) Failing after 12m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-10 00:17:25 +05:30
Clintchiz 232d8e6734 complete performance and reliability follow-ups 2026-08-09 23:04:36 +05:30
Clintchiz 8f19a5eb2b complete UI CSS ownership remediation 2026-08-09 21:42:30 +05:30
Clintchiz 937ccb9e8e stabilize cache ignore integration test 2026-08-09 21:11:01 +05:30
Clintchiz 6859b5c4ff localize legacy UI component styles 2026-08-09 21:07:10 +05:30
Clintchiz 3f5ec8696f ignore dev caches in example tool configs 2026-08-09 20:22:55 +05:30
Clintchiz 9fd80b7cef ignore per-process caches across repository tools 2026-08-09 19:48:54 +05:30
Clintchiz 7d87200e31 fix layout SSR translations and remediation follow-ups 2026-08-09 18:49:16 +05:30
Clintchiz 51286d0fa3 complete framework remediation validation 2026-08-09 14:39:22 +05:30
Clintchiz 905cbce0c1 remove hazardous legacy scripts and gate repository writes 2026-08-09 14:24:35 +05:30
Clintchiz 09ebd44b6c fix compiler authoring traps and showcase generation order 2026-08-09 14:22:12 +05:30
Clintchiz 2e5c0cc953 feat(ui): build remaining component scaffolds 2026-08-09 14:18:43 +05:30
Clintchiz 1660044ed2 perf(csr): load component controllers on demand 2026-08-09 14:10:15 +05:30
Clintchiz 324928cfca refactor(ui): finish style-only component migration 2026-08-09 14:01:01 +05:30
Clintchiz 5c2c6a0a8f refactor(ui): replace Tailwind utilities with local styles 2026-08-09 13:58:13 +05:30
Clintchiz ff62918dbb refactor(ui): remove superseded component scaffolds 2026-08-09 13:49:55 +05:30
Clintchiz 54a93c1d14 refactor(ui): localize component styles 2026-08-09 13:47:01 +05:30
Clintchiz b6bd5cfcb7 refactor(ui): localize progress and loading styles 2026-08-09 13:42:09 +05:30
Clintchiz fdf6282aed perf(compiler): share client function state closures 2026-08-09 13:38:55 +05:30
Clintchiz ca8aabf640 fix(compiler): avoid deferred helper name collisions 2026-08-09 13:34:17 +05:30
Clintchiz a90d3683d3 fix(dev-server): normalize hot reload module paths 2026-08-09 13:32:00 +05:30
Clintchiz d85be6c456 test(perf): tighten reactive runtime size budget 2026-08-09 13:25:16 +05:30
Clintchiz 8ef6233ef3 fix(compiler): reject malformed structured props 2026-08-09 13:24:18 +05:30
Clintchiz 91e8b2ab05 test(dev-server): guard server-rendered translations 2026-08-09 13:22:18 +05:30
Clintchiz b2e83bc941 feat(compiler): commit deferred client state writes 2026-08-09 13:21:34 +05:30
Clintchiz 3e77a621f5 test(dev-server): guard nested component slot rendering 2026-08-09 13:18:32 +05:30
Clintchiz a1f671ed5d feat(framework): make component props reactive 2026-08-09 13:17:36 +05:30
Clintchiz 7c584c1d2e feat(csr): diagnose missing rendered theme tokens 2026-08-09 13:07:54 +05:30
Clintchiz 709a38feb4 feat(csr): diagnose missing output binding functions 2026-08-09 13:06:56 +05:30
Clintchiz 35cd28aad4 revert(compiler): preserve inline structured component props 2026-08-09 13:00:18 +05:30
Clintchiz de8d792e37 fix(compiler): diagnose inline object component props 2026-08-09 12:58:02 +05:30
Clintchiz 64bb0e366a perf(compiler): deduplicate client peer state synchronization 2026-08-09 12:57:37 +05:30
Clintchiz e4502f2437 fix(dev-server): isolate generated cache per process 2026-08-09 12:56:11 +05:30
Clintchiz 504d065003 feat(csr): add stripped dev output diagnostics 2026-08-09 11:25:48 +05:30
Clintchiz f09341fcfa test(scripts): make UI generator newline check deterministic 2026-08-09 11:24:20 +05:30
Clintchiz 535ad5af6d chore(scripts): remove destructive UI catalog generator 2026-08-09 11:23:59 +05:30
ClintchizandClaude Opus 5 247f360ae7 docs: rank the destructive generator as item 0
Quality / quality (ubuntu-latest) (push) Failing after 19m36s
Quality / quality (windows-latest) (push) Canceled after 0s
It was written up in 4.7 but never made the work order, which is exactly how it
stayed dangerous in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 11:18:45 +05:30
ClintchizandClaude Opus 5 790b81330a fix: repair main after an unreviewed commit, and record the cause
Quality / quality (ubuntu-latest) (push) Failing after 11m9s
Quality / quality (windows-latest) (push) Canceled after 0s
Three separate problems, all traceable to `git add -A` sweeping up a working
tree I had not inspected.

Commit 69020b25 ("docs: make the component sections executable") committed far
more than docs: 79 files of a half-scaffolded inter-app example, and four of
those files were truncated mid-statement. That broke `bun run typecheck` on
main. The example is reverted to its last green six-file form. The truncated
fragments and the fuller working copy are NOT in this commit -- if any of that
workspace was wanted, it needs to be reconstructed deliberately and committed on
its own, not as a side effect of a docs change.

Separately, `scripts/generate-ui-complete-catalog.mjs` was run while checking
which helper scripts still work. It rewrites components in place, so it
flattened six of them to stubs, deleted 24 more and lower-cased four filenames
before crashing. Contents were restored from HEAD, but the renames survived
that restore: Windows is case-insensitive, so `git status` reported clean while
Card, Container, Divider and Grid sat on disk under the wrong names. The index
now tracks the capitalised names, which is what the components declare and what
ui-redesign-contract.test.ts reads -- that test would have failed on any
case-sensitive checkout.

Documented both as 4.7 and 4.8 in the remediation plan, with the general rule:
no script that rewrites packages/ui/components/ may write in place. Also fixes
the heading level on 4.6, which was rendering outside section 4.

bun run check is green: 1,433 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 11:11:57 +05:30
ClintchizandClaude Opus 5 69020b2555 docs: make the component sections executable in one pass
Quality / quality (ubuntu-latest) (push) Failing after 10m7s
Quality / quality (windows-latest) (push) Canceled after 0s
Expands 3.1 and 3.2 so the work can be done without re-deriving anything.

3.1 now records what 0.8.6 already fixed, separated into the ten components
that were miswired and the five that gained outputs they had been firing
undeclared, with the caveat that Map's three were converted but never confirmed
in a browser. For the 22 that remain it adds the finding that changes the
decision: all nine are pure scaffolds with no state, functions or handlers, and
five of them duplicate a component that already works -- FileUpload against
FileInput and FileUploadProgress, Toast and ToastNotifications against Toaster,
AdvancedDatePicker against DatePicker, AdvancedRangeSlider against RangeSlider.
Superseding those is a migration entry rather than new code, and leaves Chart,
TreeView, Confetti and CopyMarkup as the only ones needing to be built.

3.2 corrects the scaffold count from 23 to 28; the earlier figure used a looser
rule. Nine of the 28 are the 3.1 components, so the two items must be planned
together, and several of the rest are primitives that need only their styles
moved out of ui.css rather than any behaviour.

Also corrects the dead-output component count from 11 to 9 in both documents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:34:22 +05:30
ClintchizandClaude Opus 5 9ed896d2b9 docs: correct the dead-output component count from 11 to 9
Quality / quality (ubuntu-latest) (push) Failing after 12m8s
Quality / quality (windows-latest) (push) Canceled after 0s
Counted from source: the 22 remaining outputs sit in 9 components, not 11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:14:28 +05:30
ClintchizandClaude Opus 5 8389d9674e docs: rank the two size items in the work order
Quality / quality (ubuntu-latest) (push) Failing after 13m48s
Quality / quality (windows-latest) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:03:19 +05:30
ClintchizandClaude Opus 5 5112cc1a62 docs: measure the runtime and the generated client modules
Quality / quality (ubuntu-latest) (push) Failing after 13m21s
Quality / quality (windows-latest) (push) Canceled after 0s
Adds a per-subsystem measurement of reactive.js, made by minifying it
repeatedly with one subsystem removed rather than counting source bytes.

This corrects the earlier audit on both figures and on the conclusion drawn
from them. Component controllers are 23,722 bytes minified / 6,660 gzipped --
30.6% of transfer, not the "about 18%" previously claimed -- and splitting them
out saves 6.6 kB gzipped on a typical page, not "3-4 kB". Measured against the
example app, / and /login use none of the ten controllers and /layout uses one,
so most pages download and parse the lot for nothing.

The larger finding is that the runtime is not where the weight is. One page
parses 490,212 decoded bytes across 11 generated client modules while
transferring 21,026, and the largest module is 89.8% duplicated lines: the
state-restore prologue appears 162 times because client-codegen.ts inlines the
sync into every peer alias of every client function. Gzip hides it on the wire,
but parse cost follows decoded bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:02:36 +05:30
ClintchizandClaude Opus 5 30d1632252 docs: a remediation plan for the framework
Quality / quality (ubuntu-latest) (push) Failing after 12m14s
Quality / quality (windows-latest) (push) Canceled after 0s
Collects what this session exposed into one actionable document: the five bugs
fixed in 0.8.6 and the guard protecting each, the places the model is
incomplete, the smaller defects, and the delivery and dev-loop problems.

Every item states the issue, the evidence, the change and a test that fails
before it. Where a cause is not proven -- the dev server not picking up
packages/ui edits -- the item says so and makes diagnosis step one rather than
asserting a fix.

Two standing conventions are written down at the top because the rest is
written against them: a component owns its markup, behaviour and styles in its
own .wrn file, and ui.css carries global styles only; and a test that still
passes with the fix removed is measuring nothing, which is how the 0.8.5 focus
trap shipped with no coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 09:50:45 +05:30
ClintchizandClaude Opus 5 7a2b58652a chore(release): prepare 0.8.6
Quality / quality (ubuntu-latest) (push) Failing after 11m2s
Quality / quality (windows-latest) (push) Canceled after 0s
Bumps all 47 packages, the root manifest and the VS Code extension to 0.8.6,
and rebuilds the editor compiler, language server and extension bundles that
embed the version.

The release carries the output delivery fix: camelCase outputs now reach
parent bindings, and 18 components emit through output.* instead of
hand-built CustomEvents. See the 0.8.6 migration entry for what changes for
consumers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:54:44 +05:30
ClintchizandClaude Opus 5 5e65627305 fix(csr,ui): deliver component outputs to parent bindings
Quality / quality (ubuntu-latest) (push) Failing after 13m39s
Quality / quality (windows-latest) (push) Canceled after 0s
An output only reaches a parent @binding when the component calls
output.<name>(). Two separate faults meant most of the library never got
there, and both failed silently at each end.

HTML lowercases attribute names, so a parent's @sizeChange registered under
"sizechange" while the component emitted "sizeChange". The lookup missed, fell
through to a DOM dispatch, and the binding was never invoked. That made all 17
camelCase outputs undeliverable -- DataTable.pageChange and .rowClick,
Map.markerClick, ChatBubble.messageClick, LayoutSplitter.sizeChange and the
rest. invokeComponentOutput now falls back to a case-insensitive lookup, and a
csr test fails without it.

Separately, 18 components dispatched hand-built CustomEvents rather than
calling output.*. A bubbling event on the component's own root never reaches a
binding, because parent handlers live in a registry only the output proxy
reads. Card, Footer, Breadcrumb, Accordion, alert, Badge, AnnouncementBar,
AvatarGroup, ToggleCount and InputNumber now emit properly; Marquee, Map,
Timeline, List and SearchBox additionally declare the outputs they were
already firing. Dispatches on window are left alone -- that is how Toaster,
Modal and DataTable signal across component boundaries.

Verified in a browser both ways before and after: an AnnouncementBar
dispatching its own bubbling "dismiss" never reached a page-level @dismiss,
and reached it immediately once it called output.dismiss().

This corrects the audit, which called the LayoutSplitter failure "narrow and
unexplained" and read 32 dead outputs as 16 components needing a rebuild.
"Outputs work elsewhere" was an assumption; the components that worked
happened to use lowercase names and output.*. The dead-output ratchet drops
from 32 to 22, and a new test forbids the raw-CustomEvent pattern outright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:40:39 +05:30
ClintchizandClaude Opus 5 84dc4e06a2 docs: audit the ui library and record the 0.8.6 migration
Quality / quality (ubuntu-latest) (push) Failing after 12m40s
Quality / quality (windows-latest) (push) Canceled after 0s
A measured pass rather than a bulk rewrite. Numbers come from the source and
from a browser.

The migration entry covers what has accumulated since 0.8.5 and would
otherwise reach upgraders unannounced: the Tabs output contract, the Sidebar
BEM rename, the layout components leaving Tailwind so their rendered class
lists changed, LayoutSplitter and CustomScrollbar changing props and outputs,
the ui.css families that were removed, and the theme tokens that now paint
where they previously resolved to nothing.

The audit records what is still wrong, with counts: 32 outputs across 16
components that nothing emits, 23 components still on the scaffold pattern, 66
without a local style block and therefore dependent on ui.css, and 10 still
using Tailwind. A test pins the dead-output count at 32 as a ceiling that only
moves down, so rebuilding a component tightens it and no new one can be added
quietly.

It also records what is not worth doing. Splitting the runtime saves 3 to 4 kB
gzipped on a first visit to a file cached for a year, and hydration costs
1.5 ms for 21 scopes across 4325 elements, so neither is a real problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 17:16:57 +05:30
ClintchizandClaude Opus 5 c7ab605e85 feat(ui): migrate the page and section components, add the layout tour
Quality / quality (ubuntu-latest) (push) Failing after 12m21s
Quality / quality (windows-latest) (push) Canceled after 0s
Section, SectionHeader and PublicPageShell move onto wire-* classes with
local style blocks, variants as data attributes. SectionHeader loses 38
utility lines, and Section stops spending one line per variant and colour
pairing: tinted and solid now select on two attributes. PageHeader was already
on the convention and only needed the audit.

examples/basic-app/app/pages/layout.wrn composes the whole set into one page.

Building it surfaced a library-wide bug. Ten custom properties were referenced
by components and defined by nothing: --wire-color-focus, --wire-color-surface-soft,
--wire-color-on-danger, --wire-color-surface-subtle and the input-* family,
plus hover and contrast for every semantic colour except primary and
secondary. An undefined custom property does not warn, it resolves to nothing,
so focus rings drew with no colour and every soft surface rendered
transparent -- 27 components referenced surface-soft alone. They are derived
in the theme now, and a test checks every token a component references against
the rendered theme CSS rather than the source, since most are generated.

The semantic spread also had to move ahead of the primary and secondary
entries so the palette keeps winning for those two.

Known and unresolved: LayoutSplitter emits its sizeChange output and the
component does fire it, but a parent binding on the tag is not invoked. The
tour page therefore points at the handle aria-valuenow rather than wiring a
handler that would never update. Outputs work elsewhere, so this is narrower
than an outputs-are-broken problem and needs its own investigation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 15:50:33 +05:30
ClintchizandClaude Opus 5 8aa28205e0 refactor(ui): migrate the layout components off Tailwind utilities
Quality / quality (ubuntu-latest) (push) Failing after 12m47s
Quality / quality (windows-latest) (push) Canceled after 0s
Container, Columns, Grid, Divider, Image, Link, Typography and Kbd were built
from utility classes and class: conditionals. That works only where Tailwind
is present, and every variant cost a dozen conditional lines -- Divider spent
eleven of them saying which token to paint the rule.

They now carry wire-* classes with a local style block, and variants are data
attributes the style block selects on. Divider went from eleven conditionals
to five rules, and Typography lost thirteen.

Behaviour is preserved rather than improved on. Container keeps columns and
gap even though a container is not really a grid, because applications depend
on them, and its columns default stays 2: the redesign contract test caught
that changing it would silently reflow every Container already published.

Additive only: Grid gains minItemWidth for an auto-fit track, Divider gains
dashed and dotted variants, Image gains fit, and Link gains underline.

Verified in a browser rather than by eye, since the pane cannot screenshot:
track counts match the declared columns at desktop and collapse correctly
below each breakpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 11:33:59 +05:30
ClintchizandClaude Opus 5 b56cb8cba5 feat(ui): build LayoutSplitter and CustomScrollbar for real
Quality / quality (ubuntu-latest) (push) Failing after 6m8s
Quality / quality (windows-latest) (push) Canceled after 0s
Both advertised behaviour they did not have. LayoutSplitter declared
resizeStart, resize and resizeEnd with no pointer handling whatsoever, so a
caller wired up @resize and received nothing, for ever, with no error, and its
props were columns, gap and maxWidth copied from a grid scaffold.
CustomScrollbar was the same shape with a scroll output.

The splitter now resizes. Dragging lives in the reactive runtime behind
data-wrn-splitter, because a pointermove fires far too often to route through
a client function and a state write made in that callback is dropped; the
resolved size is held on the container as a --wrn-split custom property and
the component grids from it. The handle is a real separator: arrow keys step
it, Home and End go to the bounds rather than to nothing, and it carries
aria-valuenow, aria-valuemin and aria-valuemax. minSize fixes both bounds so
neither pane can be dragged away and left unrecoverable.

CustomScrollbar is CSS rather than script -- scrollbar-width and
scrollbar-color with webkit rules for the engines that still need them -- and
its fake scroll output is removed rather than left unimplemented, since a
caller can listen for a plain scroll event.

The test harness needed a fix too: mount did not bind the window CustomEvent,
so the runtime built events from the host global and happy-dom listeners never
matched them, which made anything dispatched look silently lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 11:22:45 +05:30
ClintchizandClaude Opus 5 b3a4d80df3 docs: layout and page-structure component group design
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 11:09:50 +05:30
ClintchizandClaude Opus 5 7bd4574b08 fix(example): theme the basic-app palette and surface the navigation tour
Quality / quality (ubuntu-latest) (push) Failing after 10m43s
Quality / quality (windows-latest) (push) Canceled after 0s
global.css defined its own fixed palette -- bg 0b1020, text e7ecff -- while
Wire UI surfaces follow the theme tokens. Switching to light turned the cards
light and left this text light with them, so the sign-in form rendered at a
contrast of about 1.1 and could not be read. The palette now derives from the
wire tokens, and the body wash is tinted from the primary token rather than a
fixed blue. Measured on the login form: light goes from 1.1 to 17.7, dark
stays at 18.2.

Anything an application hardcodes has to be themed as well, or it only ever
looks right in one mode.

The navigation tour is also reachable now: a Navigation entry in the site nav,
translated in both locales, and the page adopts the public layout so there is
a way back out of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 10:19:12 +05:30
ClintchizandClaude Opus 5 e32d83933e docs(example): one page wiring the whole navigation group together
Quality / quality (ubuntu-latest) (push) Failing after 13m3s
Quality / quality (windows-latest) (push) Canceled after 0s
examples/basic-app/app/pages/navigation.wrn puts all nine navigation
components in a single console shell instead of showing each alone: Navbar
with a nested dropdown, MegaMenu beside it, Breadcrumb, Sidebar as a rail that
becomes a Drawer, Tabs backed by a query parameter, a Stepper wizard,
Pagination, Scrollspy following the article, and Nav in the footer.

Three framework limits shaped the layout and are written into the page rather
than hidden:

  - object props are held in state and bound, because a brace at the start of
    an attribute is read as an interpolation
  - a shared function on a page is compiled standalone and cannot see page
    state by name, so state is passed as arguments
  - component props and slot content render once and do not track page state,
    so anything that has to react lives in page scope; Tabs reports the
    selection and the page owns what is shown

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 09:57:53 +05:30
ClintchizandClaude Opus 5 5ce2771718 fix(ui): release the scroll lock on closed drawers, add stepper wizard controls, slide tabs
Quality / quality (ubuntu-latest) (push) Failing after 12m29s
Quality / quality (windows-latest) (push) Canceled after 0s
The scroll lock was mine, and it broke every page carrying a Drawer or Modal.
Making dialog visibility testable, I replaced a size check with a data-show
check -- but a Drawer animates open, so its panel cannot be hidden with
data-show at all: display:none is not transitionable. Every closed Drawer
therefore looked open, took the body scroll lock and never released it, and
the page could not be scrolled. Both components publish data-open, which is
the signal that actually means open, and that is what is read now.

Stepper gains the wizard surface: showPanel renders each step body and shows
only the active one, the same contract Tabs uses, and controls adds Back,
Skip and Next, which becomes Finish on the last step. nextDisabled lets a form
hold the step; the component never validates anything itself, since the page
owns the form.

Stepper also gets a single root. The panels and controls were siblings of the
list, so the component had several roots and anything scoped to
data-ui-component missed most of it.

Tabs panels now slide in the direction of travel rather than fading upward.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:41:18 +05:30
ClintchizandClaude Opus 5 68c7b96a9f fix(showcase): render object props, bridge the mega menu gap, make scrollspy testable
Quality / quality (ubuntu-latest) (push) Failing after 12m38s
Quality / quality (windows-latest) (push) Canceled after 0s
Object props never worked in generated demos, and my two earlier attempts each
traded one failure for another:

  - a bare {...} attribute is read by the compiler as an interpolation, so it
    parsed JSON as JavaScript and the page 500ed
  - parenthesising it compiled, but prop coercion runs JSON.parse on the raw
    attribute, so ({...}) threw and every demo rendered empty and silent
  - entity-escaping the braces did not help either: the compiler hands the
    attribute over without decoding, so JSON.parse still failed

They are now hoisted into page state and bound, which is what the playground
has always done. The state initialiser uses JSON.parse rather than an object
literal because the parser reads a leading brace as the start of a block.

Navbar gains a real profile: a brand, links, a two-column dropdown panel and
calls to action, instead of the generic scaffold samples that made every demo
look identical and showed no dropdown at all.

MegaMenu closed while the pointer travelled to it. The panel sits below the
trigger and that offset belongs to neither element, so crossing it fired
mouseleave on the root. A descendant now covers the gap.

Scrollspy could not be exercised at all: its links pointed at ids that did not
exist on the page. The demo now ships real sections, in page flow because the
runtime observes against the viewport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:12:37 +05:30
ClintchizandClaude Opus 5 6a9c48a207 perf(ui): remove dead runtime controllers and unused stylesheet families
Quality / quality (ubuntu-latest) (push) Failing after 6m17s
Quality / quality (windows-latest) (push) Canceled after 0s
Two orphaned controllers in the reactive runtime targeted markup nothing
emits any more: hydrateSidebarControllers looked for .wire-sidebar-shell and
friends, which the Sidebar rewrite replaced with BEM classes earlier today,
and hydrateDropdownControllers looked for [data-wrn-dropdown], which no
component or compiler output has ever produced.

ui.css loses the matching legacy sidebar rules, the wire-mega-menu family
left behind when the MegaMenu scaffold was replaced, and a set of
self-contained application-pattern families that nothing references.

Utility layers are deliberately kept even where an individual member is not
name-checked anywhere. wire-bg-primary is documented and tested while
wire-bg-secondary is not, but they are one public family and splitting them
would be incoherent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 21:46:42 +05:30
ClintchizandClaude Opus 5 9a03b7c8d4 merge: runtime observer consolidation and a shipped-size budget
Quality / quality (ubuntu-latest) (push) Failing after 11m0s
Quality / quality (windows-latest) (push) Canceled after 0s
One document observer with subscribers instead of four, runtime budgets
measured on minified output rather than raw source, Navbar styles moved into
the component, and two bugs fixed: object props broke showcase pages with a
500, and the runtime evaluated JSON sitting in a textarea.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 21:20:34 +05:30
1062 changed files with 100522 additions and 81976 deletions
+26 -2
View File
@@ -1,11 +1,35 @@
{ {
"version": "0.0.1", "version": "0.0.1",
"configurations": [ "configurations": [
{
"name": "basic-app",
"runtimeExecutable": "bun",
"runtimeArgs": [
"run",
"packages/cli/src/index.ts",
"dev",
"examples/basic-app",
"--port=3520"
],
"port": 3520
},
{
"name": "sendline",
"runtimeExecutable": "bun",
"runtimeArgs": ["run", "--cwd", "D:/Company/sendline", "dev", "--port=3610"],
"port": 3610
},
{ {
"name": "component-showcase", "name": "component-showcase",
"runtimeExecutable": "bun", "runtimeExecutable": "bun",
"runtimeArgs": ["run", "--cwd", "examples/component-showcase", "dev"], "runtimeArgs": [
"port": 3000 "run",
"packages/cli/src/index.ts",
"dev",
"examples/component-showcase",
"--port=3400"
],
"port": 3400
} }
] ]
} }
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2 - uses: oven-sh/setup-bun@v2
with: with:
bun-version: 1.3.14 bun-version: 1.4.0
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 24
+4
View File
@@ -1,6 +1,7 @@
node_modules/ node_modules/
dist/ dist/
.wrnexus/ .wrnexus/
.wrnexus-*/
*.log *.log
*.db *.db
*.db-shm *.db-shm
@@ -24,3 +25,6 @@ tsconfig.focus.json
# "@wrnexus/*" bare specifiers (resolved via the root tsconfig.json `paths`, # "@wrnexus/*" bare specifiers (resolved via the root tsconfig.json `paths`,
# which requires the scaffold to live inside the repo tree). # which requires the scaffold to live inside the repo tree).
**/test/.tmp-*/ **/test/.tmp-*/
# Subagent-driven-development scratch (ledger, briefs, review packages)
.superpowers/
+1 -1
View File
@@ -1,3 +1,3 @@
@wrnexus:registry=https://registry.npmjs.org/ @wrnexus:registry=https://registry.workroot.in/repository/npm/
audit=true audit=true
fund=false fund=false
+5 -2
View File
@@ -1,8 +1,10 @@
node_modules/ node_modules/
dist/ dist/
**/dist/ **/dist/
**/.wrnexus/**
**/.wrnexus-*/**
.publish/ .publish/
**/.wirefw/ **/.wrnfw/
coverage/ coverage/
bun.lock bun.lock
bun.lockb bun.lockb
@@ -11,8 +13,9 @@ bun.lockb
# Generated code (queries.gen.ts, routes.gen.ts, etc.) # Generated code (queries.gen.ts, routes.gen.ts, etc.)
**/*.gen.ts **/*.gen.ts
**/*.generated.d.ts **/*.generated.d.ts
**/*.generated.api-checks.ts
# Bundled .wire compiler for the VS Code extension (generated) # Bundled .wrn compiler for the VS Code extension (generated)
editors/vscode/src/compiler.cjs editors/vscode/src/compiler.cjs
editors/vscode/src/language-server.cjs editors/vscode/src/language-server.cjs
editors/vscode/src/extension.bundle.cjs editors/vscode/src/extension.bundle.cjs
-197
View File
@@ -1,197 +0,0 @@
# @wrnexus/ai
Provider-neutral AI orchestration for OpenAI, Anthropic, Google and local OpenAI-compatible models,
with streaming, structured output, tools, embeddings, vector search/RAG, conversation persistence,
templates, guardrails, usage events, fallback, rate limits and evaluation reports.
> A tiny, zero-dependency Claude (Anthropic) client for WrNexus apps — generate and stream text with Claude from any server-side code.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/ai` is a thin, dependency-free wrapper over the Anthropic **Messages API**,
built on `fetch` (Bun-native, no SDK). Use it in API routes, jobs, or middleware to
call Claude. It defaults to the most capable model, **`claude-opus-4-8`**, reads your
key from `ANTHROPIC_API_KEY`, and supports both one-shot generation and streaming.
## Installation
```bash
bun add @wrnexus/ai
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
Set your key in the environment (e.g. `.env`):
```
ANTHROPIC_API_KEY=sk-ant-...
```
## API
### `createAI(config?)`
Creates a client. The key is read at call time, so it's safe to create at import.
```ts
import { createAI } from "@wrnexus/ai";
const ai = createAI(); // or createAI({ apiKey, model, maxTokens, baseURL, version })
```
`AIConfig` fields (all optional):
| Field | Default | Description |
| ----------- | --------------------------- | -------------------------- |
| `apiKey` | `ANTHROPIC_API_KEY` | Anthropic API key |
| `model` | `"claude-opus-4-8"` | Model id |
| `maxTokens` | `4096` | Default max output tokens |
| `baseURL` | `https://api.anthropic.com` | API base URL |
| `version` | `"2023-06-01"` | `anthropic-version` header |
### `ai.generate(prompt, opts?): Promise<string>`
One-shot text generation. `prompt` is a string or a `Message[]` history.
```ts
const text = await ai.generate("Write a haiku about Bun.");
const reply = await ai.generate(
[
{ role: "user", content: "My name is Ada." },
{ role: "assistant", content: "Hi Ada!" },
{ role: "user", content: "What's my name?" },
],
{ system: "You are concise." },
);
```
### `ai.stream(prompt, opts?): AsyncGenerator<string>`
Yields text deltas as they arrive.
```ts
for await (const chunk of ai.stream("Tell me a story.")) {
process.stdout.write(chunk);
}
```
### `ai.streamResponse(prompt, opts?): Response`
Returns a streaming `text/plain` `Response` — drop it straight into an API route.
```ts
// app/api/chat.ts
import { createAI } from "@wrnexus/ai";
const ai = createAI();
export const POST = async (ctx) => {
const { prompt } = await ctx.req.json();
return ai.streamResponse(prompt);
};
```
### `GenerateOptions`
| Option | Type | Description |
| ----------- | ------------------------------------------------- | ---------------------------------------------------- |
| `system` | `string` | System prompt |
| `model` | `string` | Override the model for this call |
| `maxTokens` | `number` | Override max output tokens |
| `thinking` | `boolean` | Enable adaptive extended thinking (deeper reasoning) |
| `effort` | `"low" \| "medium" \| "high" \| "xhigh" \| "max"` | Reasoning effort / token spend |
| `messages` | `Message[]` | Full history — supersedes `prompt` |
| `signal` | `AbortSignal` | Cancel the request |
> `temperature` / `top_p` are intentionally **not** exposed — the current Claude
> models reject them (400). Steer output with prompting instead.
### `AIError`
Thrown on non-2xx responses or a model refusal. Carries `.status` and `.type`
(e.g. `"authentication_error"`, `"rate_limit_error"`, `"refusal"`).
```ts
import { AIError } from "@wrnexus/ai";
try {
await ai.generate("...");
} catch (e) {
if (e instanceof AIError && e.type === "rate_limit_error") {
/* back off */
}
}
```
### Multi-provider client
`createAIClient` adds named-provider selection and fallback, capability discovery,
validated JSON output, validated tool execution, abort-aware exponential retries,
and per-provider circuit breakers. Attempt events intentionally contain metadata
only: prompts, credentials, and raw model responses are never passed to telemetry.
```ts
import { anthropicProvider, createAIClient } from "@wrnexus/ai";
const ai = createAIClient({
providers: [anthropicProvider()],
retry: { attempts: 3, baseDelayMs: 100, maxDelayMs: 2_000 },
circuitBreaker: { failureThreshold: 5, resetAfterMs: 30_000 },
});
const result = await ai.generateObject<{ title: string }>("Return a JSON title", {
validate: (value): value is { title: string } =>
typeof value === "object" && value !== null && "title" in value,
});
```
Providers can return normalized `usage` (`inputTokens`, `outputTokens`,
`totalTokens`, and `costUsd`) and `toolCalls`. Use `executeTools` with a named,
validated tool registry; unknown tools and invalid arguments are rejected before
application code runs. `deterministicAIProvider` supplies ordered or computed
offline responses for tests and examples without API keys or network calls.
## Usage
### Return generated JSON from an API route
```ts
// app/api/summarize.ts — summarize posted text
import { createAI } from "@wrnexus/ai";
const ai = createAI();
export const POST = async (ctx) => {
const { text } = await ctx.req.json().catch(() => ({}));
if (!text) return Response.json({ error: "Provide 'text'." }, { status: 400 });
const summary = await ai.generate(`Summarize in one sentence:\n\n${text}`, {
system: "You are a precise summarizer.",
});
return Response.json({ summary });
};
```
### Stream a chat response to the browser
```ts
// app/api/chat.ts
import { createAI } from "@wrnexus/ai";
const ai = createAI({ model: "claude-sonnet-5" });
export const POST = async (ctx) => {
const { messages } = await ctx.req.json();
return ai.streamResponse(messages, {
system: "Answer using concise Markdown.",
maxTokens: 1_500,
});
};
```
## Requirements / Notes
- **Bun-only.** Uses `fetch`, `ReadableStream`, `TextDecoder`/`TextEncoder`, and
reads `ANTHROPIC_API_KEY` from `Bun.env` (falls back to `process.env`).
- **Zero dependencies** — no `@anthropic-ai/sdk`; talks to the Messages API directly.
- Defaults to `claude-opus-4-8`. Pass `{ model }` for a different model (e.g.
`"claude-sonnet-5"` for speed/cost, `"claude-haiku-4-5"` for the fastest).
-47
View File
@@ -1,47 +0,0 @@
{
"name": "@wrnexus/ai",
"version": "0.8.5",
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/ai"
},
"homepage": "https://wrnexusjs.dev/packages/ai",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"ai"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./platform": {
"types": "./dist/platform.d.ts",
"import": "./dist/platform.js"
}
},
"files": [
"dist",
"README.md"
]
}
-304
View File
@@ -1,304 +0,0 @@
# @wrnexus/authz
> Composable authorization for WrNexus — role-based (RBAC), policy-based (PBAC), and attribute-based (ABAC) access control that reduces to a boolean check plus an `authorize()` guard.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/authz` is a small, server-side authorization toolkit. It gives you three
interchangeable models — RBAC (roles → permissions), PBAC (policy predicates), and
ABAC (attribute matchers) — that all collapse to a `boolean | Promise<boolean>` decision.
Wrap any decision in a `Middleware` guard (`authorize`, `requireRole`, `requirePermission`)
to protect WrNexus routes. Reach for it whenever a route or action needs to be gated on who
the user is, what roles they hold, or attributes of the user and the resource. It plugs into
`@wrnexus/core` by reading `ctx.user` as the authorization subject.
## Installation
```bash
bun add @wrnexus/authz
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
The package has a single entry point (`@wrnexus/authz`) exporting the following.
### Types
| Symbol | Description |
| ---------------------------------- | --------------------------------------------------------------------------------------------- |
| `Subject` | The authorized principal: `{ id?: string; roles?: string[]; [attribute: string]: unknown }`. |
| `Rbac` | An RBAC checker: `{ can(subject, permission): boolean; permissionsFor(roles): Set<string> }`. |
| `Policy<S = Subject, R = unknown>` | A predicate `(subject: S, resource?: R) => boolean \| Promise<boolean>`. |
### RBAC
#### `defineRbac(roles: Record<string, string[]>): Rbac`
Builds an RBAC checker from a role → permissions map. Supported permission forms:
- `"*"` — grants every permission.
- `"ns:*"` — namespace wildcard (e.g. `"post:*"` grants `"post:write"`).
- `"role:<name>"` — inherits all permissions of another role (resolved recursively, cycle-safe).
The returned `Rbac` provides:
- `can(subject, permission)``true` if any of `subject.roles` grants `permission` (honouring `*` and namespace wildcards). Returns `false` when the subject has no roles.
- `permissionsFor(roles)` — the resolved `Set<string>` of all permissions granted to a set of roles.
#### `hasRole(subject: Subject | undefined, ...required: string[]): boolean`
`true` if the subject holds **all** of the given roles.
### PBAC / ABAC combinators
- `any<S, R>(...policies: Policy<S, R>[]): Policy<S, R>` — allow if **any** policy passes (OR); awaits async policies.
- `all<S, R>(...policies: Policy<S, R>[]): Policy<S, R>` — allow only if **all** policies pass (AND); awaits async policies.
- `attr<S extends Subject>(name: string, match: unknown | ((value: unknown) => boolean)): Policy<S>` — ABAC helper that allows when `subject[name]` equals `match`, or when `match` is a function, when `match(value)` is truthy.
### Guards (middleware)
Each guard returns a `@wrnexus/core` `Middleware`. A denied request short-circuits with
`Response.json({ ok: false, error: "Forbidden" }, { status: 403 })`.
- `authorize(policy: (ctx: Context) => boolean | Promise<boolean>): Middleware` — runs `policy` against the request `Context`; calls `next()` when it resolves truthy, otherwise returns 403.
- `requireRole(...roles: string[]): Middleware` — allows when `ctx.user` holds **any** of the listed roles.
- `requirePermission(rbac: Rbac, permission: string): Middleware` — allows when `rbac.can(ctx.user, permission)` is `true`.
## Usage
### RBAC
```ts
import { defineRbac, hasRole } from "@wrnexus/authz";
const rbac = defineRbac({
admin: ["*"],
editor: ["post:read", "post:write"],
viewer: ["post:read"],
// role inheritance: lead gets everything an editor has, plus post:publish
lead: ["role:editor", "post:publish"],
});
const user = { id: "u1", roles: ["editor"] };
rbac.can(user, "post:write"); // true
rbac.can(user, "post:delete"); // false
rbac.permissionsFor(["lead"]); // Set { "post:read", "post:write", "post:publish" }
hasRole(user, "editor"); // true
```
### Guarding routes
```ts
import { authorize, requireRole, requirePermission, defineRbac } from "@wrnexus/authz";
const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"] });
// Only admins or editors
app.get("/dashboard", requireRole("admin", "editor"), handler);
// Requires a specific permission
app.post("/posts", requirePermission(rbac, "post:write"), handler);
// Arbitrary policy over the request context
app.delete(
"/posts/:id",
authorize((ctx) => hasRole(ctx.user, "admin")),
handler,
);
```
### PBAC / ABAC policies
```ts
import { any, all, attr, authorize, type Policy } from "@wrnexus/authz";
interface User {
id: string;
department?: string;
roles?: string[];
}
interface Post {
authorId: string;
}
// Ownership policy (subject + resource)
const ownsPost: Policy<User, Post> = (u, post) => u.id === post?.authorId;
// ABAC: attribute equality, or a predicate
const inEngineering = attr<User>("department", "engineering");
const isVerified = attr<User>("verified", (v) => v === true);
// Compose: allow if the user owns the post OR is in engineering AND verified
const canEdit = any(ownsPost, all(inEngineering, isVerified));
app.put(
"/posts/:id",
authorize((ctx) => canEdit(ctx.user as User, loadPost(ctx))),
handler,
);
```
## Requirements / Notes
- **Bun-only** — like the rest of WrNexus, this package targets the Bun runtime; Node is not supported.
- Works with [`@wrnexus/core`](../core) — the guards return `Middleware` and read the subject from `ctx.user` on the request `Context`. Both types are imported from `@wrnexus/core`.
- Policy combinators (`any`, `all`) and `authorize` are async-aware, so policies may return a `Promise<boolean>` (e.g. for a database ownership check).
## Declaring permissions
The RBAC/PBAC/ABAC surface above is the low-level toolkit. On top of it sits a
declarative **registry + catalog + store + engine**: permissions, roles, and
policies are declared once in code, merged into a frozen catalog at boot, and
resolved per-request against a pluggable `PermissionStore` that holds who has
what.
Put declarations in `app/authz/<name>.ts`; they are discovered automatically
and merged (conflicting declarations of the same permission/role/policy across
files fail the boot loudly, naming both source files).
```ts
import { defineAuthz, owner } from "@wrnexus/authz";
export default defineAuthz({
permissions: {
"post:read": { title: "View posts", public: true },
"post:delete": { title: "Delete posts", risk: "high" },
},
// "post:*" is a namespace wildcard grant, valid inside a role's list — it is
// not itself a registered permission, so it can only ever grant permissions
// that ARE declared above (e.g. "post:read", "post:delete").
roles: { editor: ["post:*"], admin: ["role:editor"] },
policies: { ownsPost: owner("id", "authorId") },
bindings: { "post:delete": ["ownsPost"] },
});
```
`public: true` means anonymous callers may hold the permission — but any
policy bound to it still runs, and can still veto the anonymous caller (e.g. a
`notBanned` policy on a public `post:preview` permission).
## Checking permissions
Register `authzMiddleware` once, in `app/middleware/`, with the merged
catalog and a `PermissionStore`. Like every other `app/middleware/*.ts` file,
the registration is an eager, module-scope call — the same shape as
`authzMiddleware({ catalog, store })` requires — so it must run after the
catalog has been populated. Both the dev server and `wrnexus build`'s
generated production entry guarantee `getAuthzCatalog()` is populated before
any app middleware module evaluates. Name the file so it sorts after whatever
middleware sets `ctx.user` (middleware runs in alphabetical filename order —
`authz.ts` after `auth.ts`, for instance).
```ts
// app/middleware/authz.ts
import { authzMiddleware, getAuthzCatalog } from "@wrnexus/authz";
import { dbPermissionStore } from "@wrnexus/authz/db";
import { getDb } from "@wrnexus/db";
export default authzMiddleware({ catalog: getAuthzCatalog(), store: dbPermissionStore(getDb()) });
```
> **`subject.id` must be a non-empty string.** The engine denies (and logs to
> stderr) whenever `ctx.user.id` is present but not a non-empty string — this
> includes the common case of an integer primary key. Coerce it before it
> reaches `ctx.user`, e.g. `user.id = String(row.id)`, or every request for
> that user denies with "Invalid subject" instead of resolving normally.
> `owner()` (the built-in ownership policy) compares subject and resource ids
> with `Object.is`, so both sides must be the same type too — `owner()` on a
> numeric `resource.authorId` against a stringified `subject.id` never
> matches even when they represent "the same" id.
There is no per-route `middleware` export — `app/middleware/*.ts` is the only
place middleware is registered. To gate part of the app, branch on the
request the same way any other conditional middleware does (compare
`app/middleware/captcha-login.ts` in the auth showcase, which branches on
method + path the same way):
```ts
// app/middleware/protect-posts.ts
import type { Context, Next } from "@wrnexus/core";
import { guardPermission } from "@wrnexus/authz";
const guardPostWrite = guardPermission("post:write");
export default function protectPosts(ctx: Context, next: Next) {
return ctx.url.pathname.startsWith("/api/posts") && ctx.req.method !== "GET"
? guardPostWrite(ctx, next)
: next();
}
```
Or check inline inside a route handler with the free function `can()`:
```ts
// app/api/posts/[id].ts
import type { Context } from "@wrnexus/core";
import { can } from "@wrnexus/authz";
export const DELETE = async (ctx: Context) => {
const post = { id: "1", authorId: "alice" }; // load your own resource here
if (!(await can(ctx, "post:delete", post))) {
return Response.json({ ok: false, error: "Forbidden" }, { status: 403 });
}
return Response.json({ ok: true });
};
```
`can()` is a free function taking `ctx`, not `ctx.can``@wrnexus/core` must
not depend on `@wrnexus/authz`, so the per-request resolver lives in
`ctx.locals` instead, reached through `can()` / `decideFor()` /
`guardPermission()` / `filterCan()`. Calling any of them before
`authzMiddleware` has run for that request throws a `WRN-AUTHZ-SETUP` error
naming the missing registration, rather than silently denying.
See `examples/auth-showcase/app/authz/showcase.ts` and
`examples/auth-showcase/app/middleware/authz.ts` for a complete, runnable
version of this wiring.
## Precedence
1. An explicit deny wins over everything, including `*` — and honours the
same namespace-wildcard matching as grants (denying `post:*` blocks
`post:comment:delete`, not just `post:*` itself).
2. A bound policy can veto a permission a role grants, and runs even for a
`public: true` permission — including for an anonymous caller.
3. Otherwise the permission must be held via a role or an explicit grant.
4. Default deny.
Every failure — an unknown permission (outside strict/dev mode), a store
outage, a thrown policy — denies rather than throwing through to the caller.
`permissionsFor()` (on the resolver returned by `createAuthzResolver`) is a
coarse hint for hiding UI (e.g. a menu section), **never authoritative**. A
`Set<string>` cannot represent "granted `post:*` except `post:delete`", so a
narrow deny beneath a broad grant is invisible to it — the set still contains
`post:*` while `can()` / `decide()` correctly refuse `post:delete`. Gate real
actions with `can()`, `decideFor()`, or `filterCan()`; never by matching
against `permissionsFor()`'s result.
## CLI
```bash
wrnexus authz list # every registered permission, role, and policy
wrnexus authz generate # app/authz/permissions.gen.ts type unions
wrnexus authz init # scaffold the assignment-table migration
```
`wrnexus authz generate`'s output is a plain `Permission | Role` string-literal
union — `can()`, `guardPermission()`, and `decideFor()` all take a bare
`string` and nothing reads this file automatically, so import it to type your
own helpers/constants against the registered catalog, e.g.:
```ts
import type { Permission } from "app/authz/permissions.gen.ts";
function guard(permission: Permission) {
return guardPermission(permission);
}
```
-51
View File
@@ -1,51 +0,0 @@
{
"name": "@wrnexus/authz",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/authz"
},
"homepage": "https://wrnexusjs.dev/packages/authz",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"authz"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./db": {
"types": "./dist/db.d.ts",
"import": "./dist/db.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/db": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-326
View File
@@ -1,326 +0,0 @@
# @wrnexus/cli
Production parity commands:
```bash
wrnexus build .
wrnexus preview . --port=3000
wrnexus dev . --production-runtime
```
`preview` refuses to start without `dist/server.js` and executes that exact
artifact with the production profile. Production-runtime development rebuilds
the same minified artifact after app, public, or configuration changes and
keeps the last good server running when a rebuild fails.
> The `wrnexus` command-line tool that scaffolds, runs, builds, tests, and manages WrNexus apps.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/cli` provides the `wrnexus` executable — the single entry point for developing a WrNexus app. It runs the HMR dev server, produces a self-contained production build, scaffolds apps/pages/components, drives database migrations, regenerates typed routes and queries, runs tests, and manages configuration profiles. It also scaffolds multi-app monorepos and serves them behind a domain-routing gateway. This is a CLI/build-time package (it shells out to the Bun binary for the dev child and tests) and it also exports the workspace config types via a subpath.
## Installation
```bash
bun add @wrnexus/cli
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
Once installed, invoke it from an app directory:
```bash
bunx wrnexus dev
# or add scripts: "dev": "wrnexus dev .", "build": "wrnexus build ."
```
## Commands
### Local production services
`wrnexus dev . --services` starts the application and the bounded local database,
cache, mail, SMS, webhook, storage, queue, cron, authentication and metrics simulator.
It generates a localhost/`*.localhost` development certificate under
`.wrnexus/certificates/` and serves both the application and service console over HTTPS.
Trust that certificate locally to remove the browser warning. Use `--services-http` only
when an external development proxy already terminates TLS.
### Exact production runtime with live updates
`wrnexus dev . --production-runtime` rebuilds and executes `dist/server.js` with
production resolution, serialization, caching, headers and assets. The supervisor keeps
the last good process when a build fails. On a successful rebuild the opt-in production
HMR socket reconnects, requests the new document and morphs it into the browser; ordinary
`wrnexus preview` and deployed production servers never include that client.
### API platform
`wrnexus api generate [app-dir]` (or `api docs`) derives operations from file routes and
emits `generated/api/openapi.json`, safe static documentation, Postman collection, curl examples,
and TypeScript, JavaScript, Java, Go and Python SDKs. Generate one client with
`wrnexus sdk generate <language> [app-dir]`.
Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that read config or `.env` also accept `--profile=<name>` (see [Profiles](#profiles)).
| Command | Purpose |
| ------------------------------------- | ---------------------------------------------------------------------- |
| `wrnexus dev [app-dir] [--port=3000]` | Start the development server with live reload / HMR. |
| `wrnexus build [app-dir]` | Build a self-contained production server bundle + assets into `dist/`. |
| `wrnexus create <app-name>` | Scaffold a new single app from an inline template. |
| `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). |
| `wrnexus workspace add <name>` | Add and register an app in the current workspace. |
| `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. |
| `wrnexus production [workspace-dir]` | Build, migrate, and serve every workspace app in production. |
| `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. |
| `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). |
| `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. |
| `wrnexus generate mobile` | Scaffold a Capacitor shell for iOS and Android. |
| `wrnexus mobile add <package...>` | Install Capacitor plugins and sync native projects. |
| `wrnexus eject <name...>` | Copy Wire UI component `.wrn` sources into `app/components/`. |
| `wrnexus db <cmd>` | Database migrations and tooling (see [db](#wrnexus-db)). |
| `wrnexus test [app-dir] [--watch]` | Run the app's tests via `bun test` (defaults to the `test` profile). |
| `wrnexus profiles [app-dir]` | List config profiles and their `.env` files, marking the active one. |
| `wrnexus compatibility check` | Check whether behavior defaults are explicitly pinned and current. |
| `wrnexus compatibility explain` | Explain configured, effective, and current compatibility behavior. |
| `wrnexus compatibility upgrade` | Back up config and explicitly opt into reviewed current behavior. |
| `wrnexus help` | Print usage. |
`wrnexus g` is an alias for `wrnexus generate`.
Compatibility upgrades never happen implicitly. New applications pin
`compatibilityDate` and `frameworkBehaviour`; existing applications use
`wrnexus compatibility explain` before the backed-up, idempotent upgrade command.
### `wrnexus dev`
Supervises a child dev-server process (from `@wrnexus/dev-server`). The child owns file watching and HMR: CSS and client-island edits update the live page over a WebSocket with no restart; when a server module changes, the child exits with a restart code and the supervisor respawns it (the browser reconnects and morphs in the new HTML). On startup it regenerates typed DB queries and typed routes (best effort). Use `--port=` to change the port (default `3000`).
```bash
wrnexus dev . --port=8080
```
### `wrnexus build`
Emits into `<app-dir>/dist/`:
- `server.js` — a single, minified, self-contained Bun server with a **static** manifest of every page / api / realtime / middleware / component / layout module (no runtime filesystem scan or on-the-fly bundling).
- `reactive.js`, `theme.css`, `theme.js`, `ui.css`, and (if present) `styles.css` — hashed, minified browser assets.
- `public/` — copied verbatim.
Before bundling, it regenerates typed queries for the default and every named database. Run the output with:
```bash
bun dist/server.js # PORT env var optional
# Generated apps also provide: npm start
# Build and start together: npm run production
```
### `wrnexus create`
Scaffolds a complete v0.8 app from an inline template. The generated project includes strict TypeScript, ESLint and Prettier, editor recommendations, environment templates, database migrations, locales, schemas, tests, API/middleware/realtime examples, Tailwind and Iconify, PWA/mobile defaults, and the framework package kits. Its `wrnexus.config.ts` documents the current imports, types, stores, performance, observability, tenancy, build, navigation, theme, i18n, database, storage, realtime, security, and profile configuration.
Use `bun run dev` during development, `bun run check` for the complete typecheck/lint/test/format gate, `bun run build && bun run start` for production, or `bun run production` to build and start in one command.
### `wrnexus update`
`wrnexus update --latest` performs a complete project upgrade. It hands control to the exact target CLI, backs up important project files under `.wrnexus/update-backups/`, updates every `@wrnexus/*` dependency, refreshes framework-owned references, and applies every versioned syntax/config/file migration between the project version and target version. After installation it runs the project's `check` and `build` scripts; the new version is recorded only after verification succeeds.
Use `--dry-run` to preview an upgrade or `--no-verify` when verification is intentionally handled elsewhere. Migrations never overwrite user-owned configuration wholesale: each release must provide a focused, idempotent transformation for any changed syntax or config contract.
```bash
wrnexus create my-app
```
### `wrnexus generate`
Scaffolds a single file from a template, refusing to overwrite an existing file. Types (with aliases): `page`/`p`, `component`/`c`, `api`/`a`, `schema`/`s`. Nested names create nested paths.
```bash
wrnexus generate page about # app/pages/about.wrn
wrnexus generate component user-card # app/components/user-card.wrn
wrnexus generate api users/list # app/api/users/list.ts
wrnexus generate schema signup # app/schemas/signup.ts
wrnexus generate routes # regenerate app/routes.gen.ts
wrnexus generate docker # Dockerfile + compose + .dockerignore
wrnexus generate mobile --mode=webview --app-id=com.example.app --app-name="Example" --url=https://app.example.com
wrnexus generate mobile --mode=native
```
The mobile generator creates a separate `mobile/` package and reads
`config.mobile.mode`. `webview` creates a Capacitor shell that renders the hosted
WrNexus application. `native` creates a WebView-free Expo/React Native app whose
screens call the shared backend through `mobile/src/wrnexus.ts`. Native screens
do not render `.wrn` HTML. In either mode, run `bun install` in `mobile/`; iOS
device builds require macOS and Xcode.
Install official or community Capacitor plugins through the root CLI:
```bash
wrnexus mobile add @capacitor/camera @capacitor/haptics
wrnexus mobile sync
wrnexus mobile assets # generate native icons from config.mobile.icon
```
In native mode, `mobile add` runs `expo install` and `mobile sync` runs Expo
prebuild. In WebView mode they retain the Capacitor install/sync behavior.
`wrnexus mobile compile` maps portable `app/pages/**/*.wrn` pages to Expo Router
TSX routes. Native `bun run start` invokes this compilation automatically.
Browser code can access installed plugins through the SSR-safe
`@wrnexus/mobile` bridge. The command adds each plugin to both the WrNexus app
(JavaScript proxy) and `mobile/` (native synchronization).
`wrnexus mobile sync` also configures Android so only true network failures use
the local connection-error screen. HTTP errors such as 404 and 500 keep their
WrNexus response pages.
### `wrnexus eject`
Copies a Wire UI component's `.wrn` source out of `@wrnexus/ui` into `app/components/`, so the app owns and can edit it (the app copy shadows the library one by name). Run with no names to list available components. It skips components that already exist in the app.
```bash
wrnexus eject button card modal
```
### `wrnexus db`
Database migrations and tooling. Without a flag, commands target the **default** database (`db` in `wrnexus.config.ts`, files under `app/db/`). Pass `--db=<name>` to target a named database (`databases.<name>`, files under `app/db/<name>/`).
| Subcommand | Purpose |
| ------------------------------- | ----------------------------------------------------------------------------------- |
| `db new <name> [--from-models]` | Scaffold a migration; `--from-models` derives it from the TS models in `schema.ts`. |
| `db migrate` | Apply all pending migrations. |
| `db rollback` | Revert the last applied migration. |
| `db status` | List applied / pending migrations. |
| `db generate` | Regenerate typed queries (`queries/*.sql``queries.gen.ts`). |
| `db seed` | Run the database's `seed.ts` (default export / `seed` function). |
| `db studio [table]` | Inspect tables — list row counts, or dump the first 50 rows of one table. |
```bash
wrnexus db new create_users --from-models
wrnexus db migrate
wrnexus db studio users
wrnexus db status --db=analytics
```
### `wrnexus workspace` and `wrnexus gateway`
`workspace <name>` scaffolds a monorepo: complete v0.8 apps under `apps/*`, shared libraries under `packages/*`, root TypeScript/lint/format/editor/environment tooling, and a `wrnexus.workspace.ts` that maps each app to the domains it serves. `gateway` runs every app behind one port and routes by `Host` header, with optional per-app auth and gateway-wide security (trusted hosts, rate limit, security headers, access log). Newly added workspace apps use the same current scaffold.
```bash
wrnexus workspace acme
wrnexus gateway --port=3000
```
For a complete production start, use the first-class workspace orchestrator:
```bash
wrnexus production --host=0.0.0.0 --port=3000
```
It builds every registered app, applies default and named-database SQL migrations
when present, and starts the production gateway only after preparation succeeds.
Use `--prepare-only`, `--no-build`, or `--no-migrate` when deployment stages are
managed separately; `--environment=<name>` selects another workspace environment.
From a workspace root, add and register another app in one command:
```bash
wrnexus workspace add reports --domain=reports.localhost
bun install
```
Development gateways bind to `127.0.0.1` by default for reliable access on Windows,
macOS, and Linux. Open the configured app domain on the gateway port (for example
`http://localhost:3000` or `http://admin.localhost:3000`), not the internal child ports
printed while apps start. Pass `--host=0.0.0.0` to accept connections from other devices.
### `wrnexus test`
Runs the app's tests with `bun test`. Defaults to the `test` profile (config + `.env.test`). Pass `--watch` to re-run on change; extra flags pass straight through to `bun test`.
```bash
wrnexus test . --watch
```
## Usage
### Create and run a single application
```bash
bunx @wrnexus/cli create customer-portal
cd customer-portal
bun install
bun run dev
```
### Add routes and shared UI to an existing app
```bash
wrnexus generate page reports/monthly
wrnexus generate api reports/export
wrnexus generate component report-filter
wrnexus generate routes
```
### Create a multi-app workspace and add another app
```bash
wrnexus workspace company-suite
cd company-suite
wrnexus workspace add reports --domain=reports.localhost
bun install
wrnexus gateway --port=3000
```
Open `http://reports.localhost:3000`; the gateway selects `apps/reports` from the
request host.
### Upgrade with migrations and verification
```bash
wrnexus update --latest --dry-run
wrnexus update --latest
wrnexus doctor
```
Use `wrnexus doctor --fix [app-dir]` to apply conservative repairs before the
health check: create missing `app/pages` and a default config, align skewed
`@wrnexus/*` dependency ranges, record the current migration marker, and format
only syntax-valid `.wrn` files. Invalid JSON or WRN sources are reported/skipped
instead of overwritten; repeat runs are idempotent.
## Profiles
Pass `--profile=<name>` to `dev`, `build`, `db` (or set `WRNEXUS_PROFILE`) to select a config profile. The CLI publishes `WRNEXUS_PROFILE` so config loaders and the dev child pick it up, and loads that profile's `.env` cascade (`.env`, `.env.local`, `.env.<profile>`, `.env.<profile>.local`) into `process.env`.
```bash
wrnexus dev --profile=uat
wrnexus profiles # ● development (config, .env.development)
# ○ production
# ○ uat (config, .env.uat)
```
## Subpath exports
`@wrnexus/cli/workspace` exposes the workspace configuration types used by `wrnexus.workspace.ts`:
```ts
import type { WorkspaceConfig, WorkspaceApp } from "@wrnexus/cli/workspace";
const config: WorkspaceConfig = {
security: { trustedHostsOnly: true, headers: true, accessLog: true },
apps: [{ name: "web", dir: "apps/web", domains: ["localhost", "web.localhost"] }],
};
export default config;
```
## Requirements / Notes
- **Bun-only.** The CLI runs on Bun, spawns the Bun binary for the dev child and `bun test`, and the production build uses `Bun.build`. Node is not supported.
- Orchestrates the rest of the framework: `@wrnexus/dev-server` (dev/prod server + gateway), `@wrnexus/router` (route + typed-routes codegen), `@wrnexus/compiler` (`.wrn``.ts`), `@wrnexus/db` (migrations, typed queries), `@wrnexus/styles` (config, profiles, `.env`, themes, styles), `@wrnexus/ui` (ejectable Wire UI components), `@wrnexus/validation`, `@wrnexus/csr`, and `@wrnexus/i18n`.
- Reads `wrnexus.config.ts` for `db` / `databases`, `theme`, `styles`, `seo`, `security`, `i18n`, and `profiles`, and `wrnexus.workspace.ts` for the gateway.
-70
View File
@@ -1,70 +0,0 @@
{
"name": "@wrnexus/cli",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/cli"
},
"homepage": "https://wrnexusjs.dev/packages/cli",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"cli"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./workspace": {
"types": "./dist/workspace.d.ts",
"import": "./dist/workspace.js"
}
},
"bin": {
"wrnexus": "./dist/index.js"
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/router": "^0.8.5",
"@wrnexus/csr": "^0.8.5",
"@wrnexus/compiler": "^0.8.5",
"@wrnexus/styles": "^0.8.5",
"@wrnexus/dev-server": "^0.8.5",
"@wrnexus/ui": "^0.8.5",
"@wrnexus/validation": "^0.8.5",
"@wrnexus/i18n": "^0.8.5",
"@wrnexus/mcp": "^0.8.5",
"@wrnexus/playground": "^0.8.5",
"@wrnexus/db": "^0.8.5",
"@wrnexus/authz": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/syntax": "^0.8.5",
"@wrnexus/typecheck": "^0.8.5",
"@wrnexus/security": "^0.8.5",
"selfsigned": "^5.5.0"
},
"files": [
"dist",
"README.md"
]
}
-216
View File
@@ -1,216 +0,0 @@
# @wrnexus/compiler
## Partial-static rendering
Pages can select `render = "partial-static"` and divide their view with `<Static>` and
`<Dynamic>` boundaries. The compiler emits a build-only shell renderer that never evaluates
dynamic-boundary children. `wrnexus build` expands static component mounts into
`dist/partial-shells.json`, records byte/region evidence in `build-report.json`, and embeds
the shell in the production route manifest. At request time the production runtime retains
request-aware layouts, locale/theme metadata and security nonces while streaming dynamic
regions into stable placeholders.
> Compiler for the `.wrn` language — tokenizes, parses, and lowers `.wrn` page and component files to TypeScript.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
Production adapters use `analyzeRuntimeImports` before bundling. Edge, worker,
service-worker, and browser targets reject Node filesystem, TCP, and process
modules with `WRN-RUNTIME-CAPABILITY`. Package manifests can declare supported
`wrnexus.runtimes` and required `wrnexus.requires` capabilities; discovery fails
when the selected deployment cannot satisfy them.
## Server actions
```wrn
action createUser using CreateUserSchema {
const user = await users.create(input)
invalidate("users")
return user
}
view {
<form @submit="createUser">...</form>
}
```
The compiler produces a schema-aware server registry, a fully inferred action
client, and progressively enhanced form metadata. The shared runtime performs
validation, authentication/permission checks, CSRF verification, serialization,
invalidation reporting, and browser lifecycle events.
## Overview
`@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page.
Static ES module imports may appear before the root declaration. Imported values are
available to server-rendered expressions, including component props:
```wrn
import { appUrl } from "@wrnexus/helpers";
layout PublicLayout {
view {
<PublicHeader signInHref="{appUrl('sso', '/sign-in')}" />
}
}
```
## Installation
```bash
bun add @wrnexus/compiler
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
All exports come from the package root (`@wrnexus/compiler`).
### `compileWireFile(source: string): string`
Compile `.wrn` source to a TypeScript module string. Throws `ParseError` on invalid input. The output is prefixed with a `// compiled from .wrn` comment.
### `compile(source: string): CompileResult`
Richer entry point that returns the generated code, the AST, and any diagnostics.
```ts
interface CompileResult {
code: string;
ast: PageAst;
diagnostics: string[];
}
```
On a `ParseError` it pushes the message into `diagnostics` and re-throws.
### `parse(source: string): PageAst`
Run the lexer + recursive-descent parser and return the AST. Throws `ParseError` (lexer `LexError`s are caught and rethrown as `ParseError`).
### `generate(ast: PageAst): string`
Lower a `PageAst` to TypeScript. `page` ASTs become a default-export page component (plus `meta`, optional `layout`, `__wrnexusApi`/method handlers, `websocket`, and SSR/CSR data bindings); `component` ASTs become a module exporting `render(props)` and `__wrnexusComponent`.
### `Lexer`
On-demand lexer for `.wrn`. Yields structural tokens and exposes raw-span readers for the parser.
```ts
class Lexer {
pos: number;
constructor(src: string);
next(): Token; // consume next structural token
peek(): Token; // look ahead without consuming
readPath(): string; // route path, e.g. /users/[id]
readToLineEnd(): string; // rest of line (state/prop initializers)
readBalancedBraces(): string; // inner text of a { ... } block, string-aware
}
```
`Token` is `{ type: TokenType; value: string; pos: number }`, where `TokenType` is one of `ident`, `string`, `lbrace`, `rbrace`, `lparen`, `rparen`, `at`, `eq`, `comma`, `eof`.
### Errors
| Class | Thrown by | Meaning |
| ------------ | ------------------------------------------------- | --------------------------------------------------------------- |
| `ParseError` | `parse`, `compile`, `compileWireFile`, `generate` | Invalid `.wrn` grammar or (rewrapped) lex failure. |
| `LexError` | `Lexer` | Unexpected character / unterminated string / unbalanced braces. |
### AST types
Exported type-only symbols describing the parsed tree:
| Type | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PageAst` | Root node including top-level `imports`, `kind`, `name`, `types`, typed `props`, typed `states`, `view`, styles, functions, data APIs, lifecycle, and routes. |
| `ViewNode` | `{ type: "text"; value }` or `{ type: "element"; tag; attrs; children }`. |
| `Attr` | `{ name; value; event; boolean? }``event` marks `@event` bindings. |
| `StateDecl` | `{ name; valueType?; expr }` — a typed `state x: Type = <expr>` declaration. |
| `PropDecl` | `{ name; valueType?; required; default }` — a typed prop declaration. |
| `SeoBlock` | `Record<string, string>` from the `seo { ... }` block. |
| `ApiBlock` | `{ method; path; body }` — a top-level `api METHOD /path { ... }`. |
| `DataApiBlock` | `{ mode; name; method; path; body }` — an `api` inside an `ssr`/`client` block. |
| `DataMode` | `"ssr" \| "client"`. |
| `ModeFunctionsBlock` | `{ mode; body }` — a `functions { ... }` inside an `ssr`/`client` block. |
| `RealtimeBlock` | `{ name; handlers }` — a `realtime <name> { on evt(args) { ... } }` block. |
## Usage
Compile a page:
```ts
import { compileWireFile } from "@wrnexus/compiler";
const ts = compileWireFile(`
page Home {
state count = 0
seo { title = "Home" description = "Welcome" }
view {
<button @click="count++">Clicked {count} times</button>
}
}
`);
// ts is a TypeScript module: exports `meta`, and a default page component
// returning an HTML string, wrapped in a data-scope for the reactive runtime.
```
Inspect the AST and diagnostics:
```ts
import { compile, ParseError } from "@wrnexus/compiler";
try {
const { code, ast, diagnostics } = compile(source);
console.log(ast.kind, ast.name, ast.states.length);
} catch (err) {
if (err instanceof ParseError) console.error(err.message);
}
```
Drive the parse/codegen stages directly:
```ts
import { parse, generate } from "@wrnexus/compiler";
const ast = parse(componentSource); // ast.kind === "component"
const module = generate(ast); // exports render(props) + __wrnexusComponent
```
Use the lexer standalone:
```ts
import { Lexer } from "@wrnexus/compiler";
const lx = new Lexer("page Home {");
lx.next(); // { type: "ident", value: "page", pos: 0 }
lx.next(); // { type: "ident", value: "Home", pos: 5 }
lx.next(); // { type: "lbrace", value: "{", pos: 10 }
```
## The `.wrn` language (as parsed)
A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` body containing zero or more members:
- `layout = "<name>"` — selects `app/layouts/<name>.wrn` (pages only).
- `types { <TypeScript declarations> }` — reusable interfaces and aliases for the current file.
- `props { name: Type = <default> ... }` — typed component props. Omit `= <default>` to make a prop required. Legacy inferred props remain supported.
- `@event name = function` inside `props` — declares a public component event. Emit it from component behavior with `name(detail)` or `$emit("name", detail)`, and consume it with `<Component @name="handler(event)" />`.
- `state <ident>: Type = <expr>` — typed reactive state seeded from a raw JS expression, including native array and object literals. The annotation is optional for backward compatibility.
- `view { <html> }` — plain HTML with `{expr}` interpolation in text and attributes, JSX-style component props such as `items={items}`, `items={[...]}`, and `options={{...}}`, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and `<!-- comments -->`. Structured component props are serialized safely for SSR; expressions that reference `state` retain their initial value and update reactively in the browser.
- `seo { key = "value" ... }` — metadata merged into the generated `meta`.
- `style { <raw css> }` — inlined page/component stylesheet (repeatable).
- `functions { <TypeScript> }` — helpers with typed parameters and return values. Types remain in server output and are safely erased from browser behavior code.
- `api <METHOD> <path> { <raw js> }` — route handler, lowered to a `METHOD` export (repeatable).
- `ssr { ... }` / `client { ... }` — data blocks holding `api <name> <METHOD> <path> { ... }` bindings and their own `functions { ... }`.
- `realtime <name> { on <evt>(<args>) { <raw js> } ... }` — websocket handlers, lowered to a `websocket` export.
`view` markup is parsed by a lenient dedicated HTML parser (`parseHtmlView`); HTML void elements (`<br>`, `<img>`, …) take no closing tag. Line comments (`//`) are skipped by the lexer.
## Requirements / Notes
- Pure TypeScript with no runtime dependencies; runs under **Bun** as part of the WrNexus toolchain (Node is not supported).
- Generated modules target WrNexus runtime primitives (`data-scope`, `data-text`, `data-on-*`, `data-for`, `data-component`, `__wrnexus*`/`__wire*` helpers) — consume the output within a WrNexus app, e.g. via `@wrnexus/core`'s dev loader.
-49
View File
@@ -1,49 +0,0 @@
{
"name": "@wrnexus/compiler",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/compiler"
},
"homepage": "https://wrnexusjs.dev/packages/compiler",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"compiler"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"dependencies": {
"@wrnexus/csr": "^0.8.5",
"@wrnexus/syntax": "^0.8.5",
"@wrnexus/store": "^0.8.5",
"@wrnexus/validation": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-412
View File
@@ -1,412 +0,0 @@
# @wrnexus/core
> The framework core: the request `Context`, middleware contract, and the security, session, caching, streaming, realtime, and JSX primitives every other WrNexus package builds on.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/core` is the shared foundation of WrNexus. It defines the `Context`
object that flows through every middleware, page, and API route, plus the
`Middleware`/`Next` contract they implement. On top of that it ships the
building blocks a real app needs: cookie-backed sessions, password auth, CSRF
protection, rate limiting, request logging, HTTP + in-memory caching, file
uploads, streaming/SSE responses, WebSocket "rooms", security headers/CORS, and
a server-side JSX runtime that renders to HTML strings. Everything here is
**server-side** and Bun-native (it uses `Bun.password`, `Bun.write`, the
web-standard `Request`/`Response`, and `crypto`). You depend on it directly and
transitively through the rest of the framework.
## Installation
```bash
bun add @wrnexus/core
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
### Context & middleware — `@wrnexus/core`
The `Context` (`ctx`) is the single value passed to middleware and handlers.
| Export | Kind | Description |
| ------------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------ |
| `Context` | type | Per-request object: `req`, `url`, `lang`, `t`, `params`, `locals`, `user?`, `ip?`, `cookies`, `session`, `localStorage`. |
| `Next` | type | `() => Promise<Response> \| Response` — invokes the next middleware/handler. |
| `Middleware` | type | `(ctx, next) => Promise<Response> \| Response`. Return `next()` to continue, or a `Response` to short-circuit. |
| `createContext(req, url)` | fn | Build a fresh `Context` for an incoming request (wires up cookies, session, localStorage snapshot). |
| `withContextHeaders(ctx, res)` | fn | Apply accumulated headers (e.g. `Set-Cookie`) from the context onto a response. |
| `PageComponent` | type | `(ctx) => string \| Promise<string>` — a page module's default export. |
| `PageMeta` / `SeoConfig` | type | `<head>` metadata: `title`, `description`, `canonical`, `robots`, `image`, `twitterCard`, `themeColor`, … |
| `TFunction` | type | `(key, params?) => string` — translate a key for `ctx.lang`, interpolating `{param}` placeholders. |
Key `Context` fields:
- `ctx.locals` — per-request scratch space for passing values between middleware.
- `ctx.user` — the authenticated user (populated by `sessionAuth`/`logIn`), or `null`.
- `ctx.ip` — the direct socket peer IP (not spoofable via headers).
- `ctx.cookies` / `ctx.session` / `ctx.localStorage` — see **Storage** below.
### Authentication — `@wrnexus/core`
Passwords are hashed with argon2id via `Bun.password`; sessions ride the
cookie-backed `SessionStore`.
| Export | Signature | Notes |
| -------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `hashPassword(password)` | `(string) => Promise<string>` | argon2id hash to store. |
| `verifyPassword(password, hash)` | `(string, string) => Promise<boolean>` | Constant-safe; returns `false` on bad/empty hash. |
| `logIn(ctx, user)` | `(Context, U) => void` | Regenerates the session id (fixation defense), stores the user, sets `ctx.user`. |
| `logOut(ctx)` | `(Context) => void` | Clears the session and `ctx.user`. |
| `getUser(ctx)` | `(Context) => U \| null` | Current user from `ctx.user`, falling back to the session. |
| `sessionAuth()` | `() => Middleware` | Hydrates `ctx.user` from the session each request. Register early. |
| `requireAuth(options?)` | `(RequireAuthOptions?) => Middleware` | Guard: API/fetch requests get `401 JSON`, page navigations get `302` to `loginPath` (default `/login`) with `?next=`. |
| `SESSION_USER_KEY` | `"user"` | Session key holding the user. |
`RequireAuthOptions`: `{ loginPath?: string }`.
### CSRF — `@wrnexus/core`
Double-submit cookie pattern: a readable `wire-csrf` cookie is echoed in an
`x-csrf-token` header on unsafe requests.
| Export | Signature | Notes |
| ----------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `csrfToken(ctx)` | `(Context) => string` | Ensures the CSRF cookie exists and returns its token. |
| `verifyCsrf(ctx)` | `(Context) => boolean` | Safe methods (GET/HEAD/OPTIONS) pass; otherwise header/`ctx.locals._csrf` must match the cookie (constant-time). |
| `csrfProtection()` | `() => Middleware` | 403s unsafe requests with a missing/mismatched token. |
| `CSRF_COOKIE` / `CSRF_HEADER` | `"wire-csrf"` / `"x-csrf-token"` | Cookie & header names. |
### Rate limiting — `@wrnexus/core`
Fixed-window limiter that returns `429` with `Retry-After` and emits
`RateLimit-Limit`/`-Remaining`/`-Reset` headers.
| Export | Signature | Notes |
| --------------------- | ----------------------------------- | ---------------------------------------------------------------------- |
| `rateLimit(options?)` | `(RateLimitOptions?) => Middleware` | Main middleware. |
| `peerKey(ctx)` | `(Context) => string` | Non-spoofable key from `ctx.ip` (default). |
| `proxyKey(ctx)` | `(Context) => string` | Trusts `x-forwarded-for`/`x-real-ip`. Use only behind a trusted proxy. |
| `defaultKey` | — | **Deprecated** alias of `proxyKey`. |
`RateLimitOptions`: `windowMs` (default `60_000`), `max` (default `60`),
`key`, `trustProxy` (default `false` → keys on `peerKey`; `true``proxyKey`),
`message`, `headers` (default `true`), `store`.
`RateLimitStore` is pluggable — implement `hit(key, windowMs, now) => Bucket | Promise<Bucket>`
(a `Bucket` is `{ count, resetAt }`) to back limits with Redis/SQL across
instances. The default store is process-local memory.
### Request logging — `@wrnexus/core`
| Export | Signature | Notes |
| ------------------------- | --------------------------------------- | -------------------------------------------------------------------------------- |
| `requestLogger(options?)` | `(RequestLoggerOptions?) => Middleware` | One record per request with a request id (stored on `ctx.locals[requestIdKey]`). |
`RequestLoggerOptions`: `format` (`"pretty"` default \| `"json"`), `sink(line, record)`
(default `console.log`), `requestIdKey` (default `"requestId"`), `now`.
`RequestRecord` = `{ time, id, method, path, status, durationMs }`.
### Resilience — `@wrnexus/core`
`resilientCall` standardizes cancellation-aware timeouts, controlled retries,
fixed or exponential backoff, fallback responses, circuit breaking, and bounded
concurrency. Reuse a declarative circuit/bulkhead options object, or an explicit
`CircuitBreaker`/`Bulkhead` instance, wherever calls must share health and
capacity state.
```ts
import { resilientCall } from "@wrnexus/core";
const paymentCircuit = { failures: 5, resetAfter: "30s" } as const;
const status = await resilientCall({
timeout: "5s",
retries: 3,
retryDelay: "100ms",
backoff: "exponential",
circuitBreaker: paymentCircuit,
bulkhead: { concurrency: 20, queue: 100 },
run: (signal) => paymentProvider.checkStatus({ signal }),
fallback: () => ({ state: "unavailable" }),
});
```
`CircuitBreaker.snapshot()` reports `closed`, `open`, or `half-open`, failure
and success counts, and the remaining retry delay for health endpoints and
development tooling. Fail-fast conditions use stable `WRN-RESILIENCE-*` codes.
Core's existing `HealthRegistry`, `withIdempotency`, and pluggable stores/locks
cover health reporting, idempotent requests, and distributed coordination.
### Caching — `@wrnexus/core`
| Export | Kind | Notes |
| -------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `TTLCache<V>` | class | In-memory TTL cache: `get`, `set`, `getOrLoad(key, loader, ttlMs?)`, `delete`, `clear`, `size`. Constructor takes a default `ttlMs` (60s). |
| `cacheControl(options)` | fn | Build a `Cache-Control` value from `CacheControlOptions`. |
| `withCacheControl(res, options)` | fn | Apply `Cache-Control` to a response. |
| `etag(body, weak?)` | fn | Stable quoted FNV-1a ETag (weak by default). |
| `notModified(req, tag)` | fn | `true` when `If-None-Match` matches — send a `304`. |
`CacheControlOptions`: `maxAge`, `sMaxAge`, `private`, `noStore`, `noCache`,
`staleWhileRevalidate`, `immutable`.
### File uploads — `@wrnexus/core`
Bun parses `multipart/form-data` via `Request.formData()`; these helpers
validate and persist the resulting `File`s.
| Export | Signature | Notes |
| --------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `collectUploads(form)` | `(FormData) => { field, file }[]` | Every non-empty `File` in a parsed form. |
| `saveUpload(file, options)` | `(File, SaveUploadOptions) => Promise<SavedUpload>` | Validates size/type, sanitizes the name, writes via `Bun.write`. Throws `UploadError`. |
| `sanitizeFilename(name)` | `(string) => string` | Strips separators, traversal, control/illegal chars; caps at 255. |
| `UploadError` | class | Thrown on rejected uploads. |
`SaveUploadOptions`: `dir` (required), `maxBytes`, `allowedTypes` (MIME types
like `"image/png"` and/or extensions like `".png"`), `filename(file)`.
`SavedUpload` = `{ path, filename, size, type }`.
### Streaming & SSE — `@wrnexus/core`
| Export | Signature | Notes |
| ------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `streamResponse(source, init?)` | `(Iterable\|AsyncIterable<string\|Uint8Array>, StreamResponseInit?) => Response` | Streaming `Response` from a chunk source (basis for streaming SSR). |
| `sse(source)` | `(Iterable\|AsyncIterable<ServerSentEvent>) => Response` | `text/event-stream` response. |
`StreamResponseInit`: `status`, `headers`, `contentType` (default
`"text/html; charset=utf-8"`). `ServerSentEvent`: `{ data, event?, id?, retry? }`.
### Realtime rooms — `@wrnexus/core`
WebSocket rooms. A file in `app/realtime/` exports
`default defineRoom({ ... })` and is served at `ws://host/realtime/<name>`.
| Export | Signature | Notes |
| --------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------- |
| `defineRoom(handlers)` | `(RoomHandlers) => RoomDefinition` | Define a room. Export the result as `default`. |
| `isRoomDefinition(value)` | `(unknown) => boolean` | Type guard for a room definition. |
| `createRealtimeRegistry()` | `() => RealtimeRegistry` | Server-side connection manager mapping sockets ↔ rooms. |
| `bridgeRealtime(registry, bus, topic?)` | `(RealtimeRegistry, RealtimeBus, string?) => () => void` | Bridge broadcasts/`toUser` sends across processes via a pub/sub bus. |
`RoomHandlers`: `authorize(info) => boolean` (gate before accept — return
`false` to reject with 403), `onConnect(client)`, `onMessage(client, message)`
(JSON auto-parsed), `onLeave(client)`. A handler receives a `RoomClient` with
`id`, `user`, `query`, `data`, `room`, and `send` / `broadcast` /
`to(id)` / `toUser(user)` / `close`. The `Room` API adds `state`, `clients()`,
`count()`, and `broadcast`. `RealtimeBus` is structurally satisfied by
`@wrnexus/pubsub`. Legacy `RealtimeHandler`/`RealtimeSocket` raw handlers are
still exported. Connection-targeted sends (`send`, `to(id)`) stay local; room
broadcasts and `toUser` cross the bridge.
### Error pages — `@wrnexus/core`
| Export | Signature | Notes |
| ------------------------------ | -------------------------------- | ----------------------------------------------------- |
| `renderError(err, mode)` | `(unknown, Mode) => Response` | Dev page (with stack) or generic prod page by `mode`. |
| `renderDevError(err, status?)` | `(unknown, number?) => Response` | Readable HTML error page including the stack trace. |
| `renderProdError(status?)` | `(number?) => Response` | Generic page that never leaks file paths. |
| `renderNotFound()` | `() => Response` | Simple 404 page. |
`Mode` = `"development" | "production"`.
### Security headers & CORS — `@wrnexus/core`
| Export | Signature | Notes |
| -------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withSecurityHeaders(req, res, mode, security?, nonce?)` | → `Response` | Applies CORS + CSP, HSTS, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, COOP, Trusted Types, and `extraHeaders`. |
| `createCorsPreflightResponse(req, security?)` | → `Response \| null` | Builds a `204`/`403` preflight response for CORS `OPTIONS` requests. |
| `isWebSocketOriginAllowed(req, security?)` | → `boolean` | Guards WS upgrades against cross-site hijacking (allows same-origin, configured CORS origins, and non-browser clients). |
Config types: `SecurityConfig` (top-level), `CorsConfig`/`CorsOrigin`,
`ContentSecurityPolicyConfig`/`CspDirectiveValue`, `HstsConfig`,
`TrustedTypesConfig`, `PermissionsPolicyConfig`. WrNexus applies sensible
defaults (self-only CSP, `frame-ancestors 'none'`, restrictive Permissions-Policy,
HSTS in production, Trusted Types in production); each is individually
overridable or disable-able via `false`.
### Storage: cookies, sessions, localStorage — `@wrnexus/core`
These back the `ctx.cookies`, `ctx.session`, and `ctx.localStorage` fields.
| Export | Kind | Notes |
| --------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setSessionBackend(backend)` | fn | Swap the **sync** session persistence backend (`SessionBackend`) — e.g. `bun:sqlite`. Default is process-local memory. Call once at startup. |
| `loadSession(backend, options?)` | fn → `Middleware` | Back `ctx.session` with an **async** store (`AsyncSessionBackend`: `load`/`save`/`destroy`) — loads before the request, saves after. `options.ttlMs` default 24h. |
| `CookieStore` | type | `get`/`getAll`/`has`/`set(name, value, opts?)`/`delete`/`headers`. |
| `SessionStore` | type | `id`/`get`/`getAll`/`set`/`delete`/`regenerate`/`clear`. |
| `LocalStorageSnapshot` | type | Read-only view of the browser's localStorage sent via header for CSR bindings. |
| `CookieOptions` | type | `path`, `domain`, `maxAge`, `expires`, `httpOnly`, `secure`, `sameSite`. |
| `SessionEntry` / `SessionBackend` / `AsyncSessionBackend` | types | Session persistence contracts. |
### Low-level security helpers — `@wrnexus/core`
| Export | Signature | Notes |
| ----------------------------- | --------------------- | --------------------------------------------------- |
| `escapeHtml(value)` | `(string) => string` | Escape for HTML text/attributes. |
| `isSafeIslandName(name)` | `(string) => boolean` | Allow only a conservative `[A-Za-z0-9_-]+` charset. |
| `isSafeRequestPath(pathname)` | `(string) => boolean` | Reject NULs, `..` traversal, and backslashes. |
### JSX runtime — `@wrnexus/core`, `@wrnexus/core/jsx-runtime`, `@wrnexus/core/jsx-dev-runtime`
A server-side JSX runtime that renders to HTML **strings** (no virtual DOM).
Point `tsconfig`'s `jsxImportSource` at `@wrnexus/core`.
| Export | Kind | Notes |
| ------------------------------------------ | ------ | --------------------------------------------------------------------------------------- |
| `jsx` / `jsxs` | fn | The runtime factory (TypeScript calls these automatically). Returns an `Html` instance. |
| `Fragment` | symbol | JSX fragment marker. |
| `Html` | class | Wraps a raw, already-safe HTML string (`toString()` returns it). |
| `mustache(expr)` | fn | Emit a `{{expr}}` placeholder (tagged-template or string form) for the client binder. |
| `JSXComponent` / `JSXProps` / `Renderable` | types | Component signature and renderable value types. |
Values interpolated as children are HTML-escaped unless they are an `Html`
instance; use `dangerouslySetInnerHTML={{ __html }}` for trusted markup. Void
elements render without a closing tag; `className``class`, `htmlFor``for`, and
`style` objects are serialized to CSS text.
The subpath exports map to the runtime TypeScript's JSX transform expects:
```jsonc
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@wrnexus/core",
},
}
```
## Usage
### A minimal middleware chain
```ts
import {
createContext,
withContextHeaders,
sessionAuth,
requireAuth,
requestLogger,
rateLimit,
csrfProtection,
type Middleware,
} from "@wrnexus/core";
const chain: Middleware[] = [
requestLogger({ format: "json" }),
rateLimit({ max: 100, windowMs: 60_000 }),
csrfProtection(),
sessionAuth(),
requireAuth({ loginPath: "/login" }),
];
```
### Password auth
```ts
import { hashPassword, verifyPassword, logIn, getUser } from "@wrnexus/core";
// Registration
const passwordHash = await hashPassword(form.password);
// Login
if (await verifyPassword(form.password, user.passwordHash)) {
logIn(ctx, { id: user.id, email: user.email });
}
const current = getUser<{ id: string }>(ctx); // or null
```
### HTTP caching with ETags
```ts
import { etag, notModified, withCacheControl } from "@wrnexus/core";
const body = JSON.stringify(data);
const tag = etag(body);
if (notModified(ctx.req, tag)) {
return new Response(null, { status: 304, headers: { ETag: tag } });
}
const res = new Response(body, { headers: { ETag: tag, "content-type": "application/json" } });
return withCacheControl(res, { maxAge: 60, staleWhileRevalidate: 300 });
```
### Streaming SSE
```ts
import { sse } from "@wrnexus/core";
async function* ticks() {
for (let n = 0; ; n++) {
yield { event: "tick", data: String(n) };
await Bun.sleep(1000);
}
}
export default (ctx) => sse(ticks());
```
### A realtime room
```ts
// app/realtime/chat.ts
import { defineRoom } from "@wrnexus/core";
export default defineRoom({
authorize: (info) => !!info.user, // require auth
onConnect(client) {
client.user = client.query.user;
client.room.broadcast({ type: "join", id: client.id });
},
onMessage(client, msg) {
client.broadcast({ type: "say", from: client.id, text: msg.text });
},
});
```
Scale it across processes:
```ts
import { createRealtimeRegistry, bridgeRealtime } from "@wrnexus/core";
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";
const registry = createRealtimeRegistry();
bridgeRealtime(registry, createPubSub(redisDriver(process.env.REDIS_URL)));
```
### JSX rendering
```tsx
import { Html } from "@wrnexus/core";
function Card({ title, body }: { title: string; body: string }) {
return (
<article class="card">
<h2>{title}</h2>
<p>{body}</p>
</article>
);
}
const html: Html = <Card title="Hi" body="<b>escaped</b> automatically" />;
return new Response(html.toString(), { headers: { "content-type": "text/html" } });
```
## Requirements / Notes
- **Bun-only.** Uses `Bun.password` (argon2id), `Bun.write`, web-standard
`Request`/`Response`/`FormData`/`ReadableStream`, and the global `crypto`.
Node is not supported.
- Session and rate-limit backends default to **process-local memory**. For
multi-instance deployments, swap in a shared backend: `setSessionBackend` (sync,
e.g. `bun:sqlite`) or `loadSession` (async, e.g. Redis) for sessions, a custom
`RateLimitStore` for limits, and `bridgeRealtime` for realtime.
- Works with the rest of the framework: realtime bridging is structurally
compatible with [`@wrnexus/pubsub`](../pubsub); the security, auth, and JSX
primitives here are consumed by the WrNexus server/router packages.
- Subpath exports: `@wrnexus/core/jsx-runtime` and `@wrnexus/core/jsx-dev-runtime`
for TypeScript's automatic JSX transform.
-51
View File
@@ -1,51 +0,0 @@
{
"name": "@wrnexus/core",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/core"
},
"homepage": "https://wrnexusjs.dev/packages/core",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"core"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./jsx-runtime": {
"types": "./dist/jsx-runtime.d.ts",
"import": "./dist/jsx-runtime.js"
},
"./jsx-dev-runtime": {
"types": "./dist/jsx-dev-runtime.d.ts",
"import": "./dist/jsx-dev-runtime.js"
}
},
"files": [
"dist",
"README.md"
]
}
-224
View File
@@ -1,224 +0,0 @@
# @wrnexus/csr
## Navigation state preservation
Pages can opt into restoration across client navigation:
```wrn
page Users {
navigation {
preserve = ["filters", "pagination", "scroll", "tabs", "expanded"]
}
}
```
Form-like categories restore named inputs, selects, and textareas. Password,
file, hidden, CSRF/token/secret/credential fields, and elements marked
`data-no-preserve` are never saved. For tab, expanded, or component UI state,
mark stable elements with `data-wrn-preserve="key"`; their value and ARIA
selected/expanded state are restored. State is scoped to pathname plus query.
## Typed server actions
`createActionClient<Input, Output>(route, name)` supports programmatic calls.
Schema-backed WRN actions also export `__wrnexusActionClients`, whose input and
output are inferred automatically. Enhanced forms expose
`data-wrn-action-state="pending|success|error"` and dispatch bubbling
`wrnexus:action:optimistic`, `:pending`, `:success`, and `:error` events.
Success details contain returned data and invalidated cache tags; error details
contain field errors. Without JavaScript, the same form posts to its page and
receives a 303 redirect or accessible validation response.
> The browser-side client runtime for WrNexus — generic, self-contained JS that hydrates server-rendered pages with reactivity, client-side navigation, and realtime rooms.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/csr` holds the three client runtimes that WrNexus serves to the browser. Components are authored as `.wrn` files and rendered on the **server**; this package provides the single, generic runtime that **hydrates** that HTML in the browser — there are no per-component browser bundles. Each runtime is exported as a plain-JS string (no build step, no imports) intended to be served verbatim from a well-known URL:
- **reactive** at `/__wrnexus/reactive.js` — reactive directives (`data-scope`, `data-text`, `data-for`, …)
- **nav** at `/__wrnexus/nav.js` — SPA-style client navigation with graceful fallback
- **realtime** at `/__wrnexus/realtime.js` — WebSocket "rooms", declarative or programmatic
The package itself runs on the server (it just returns strings); the strings it returns run in the browser. A dev/prod server (see `@wrnexus/core`) is responsible for actually serving them.
## Installation
```bash
bun add @wrnexus/csr
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
All exports come from the package root (`@wrnexus/csr`). The runtime source is delivered as strings, so the "API" on the server side is small; the real surface is the browser directives/globals each string installs.
### Runtime strings
| Export | Type | Served at | Contents |
| ------------------ | -------- | ------------------------ | ------------------------------ |
| `REACTIVE_RUNTIME` | `string` | `/__wrnexus/reactive.js` | Reactive directive runtime |
| `NAV_RUNTIME` | `string` | `/__wrnexus/nav.js` | Client-side navigation runtime |
| `REALTIME_RUNTIME` | `string` | `/__wrnexus/realtime.js` | Realtime rooms runtime |
### Accessor functions
Convenience getters that return the same strings.
```ts
getReactiveRuntime(): string // → REACTIVE_RUNTIME
getNavRuntime(): string // → NAV_RUNTIME
getRealtimeRuntime(): string // → REALTIME_RUNTIME
```
### Browser: reactive directives
Applied to any subtree containing `data-scope`. Expressions are parsed by a tiny eval-free evaluator, so a strict CSP with no `unsafe-eval` works.
| Directive | Purpose |
| ---------------------------------- | ---------------------------------------------------- |
| `data-scope="count: 0, name: 'x'"` | Declare reactive state on a subtree |
| `data-on-<event>="count++"` | Run a statement in scope on a DOM event |
| `data-text="expr"` | Bind an element's `textContent` to an expression |
| `data-show="expr"` | Toggle visibility while preserving interactive state |
Compiled conditional rendering and dynamic component cases omit inactive elements from the live
DOM. `data-show` is a visibility directive for stateful controls and keeps its element mounted.
Neither mechanism is authorization: never place secrets in client-rendered branches. Authorize on
the server and return only data the current request may access.
| `data-for="item in list"` (opt. index and `key item.id`) | Per-item rendering; stable keys preserve DOM identity during reorder |
| `data-key="item.id"` | Alternative key declaration for `data-for` templates |
| `{{expr}}` or `{expr}` | Interpolation inside text nodes and attribute values |
| `data-wrnexus-csr="id"` | Target for a generated CSR fetch binding (fetches `/__wrnexus/csr?...`) |
Supported expression features: literals, identifiers, member access (`a.b`, `a[b]`), function/method calls, arrays, objects, arithmetic, comparison, equality, logical (`&& ||`), unary (`! - +`), and ternary. Statements support `++`/`--`, assignment operators (`= += -= *= /= %=`), and bare expression/method calls. Rendering is dependency-tracked: a signal change only re-runs the renderers that actually read it.
Browser globals installed: `window.__wrnexusHydrateScopes(root)` and `window.__wrnexusHydrateCsrFetches(root)` — both idempotent, so re-running after a DOM swap or HMR morph is safe. Both run automatically on `DOMContentLoaded`.
### Browser: navigation
Intercepts same-origin `<a>` clicks, fetches the target page, and swaps the `#app` container in place (via `importNode` — not `innerHTML` — so it works under a Trusted-Types CSP), updating history, title, and scroll, then re-hydrates. Cross-origin links, modified clicks, `download`/`data-no-nav`/`rel="external"`/`target` links, non-HTML responses, or a missing `#app` fall back to a full browser navigation.
- Programmatic navigation: `window.__wrnexusNavigate(url)`
- Emits a `wrnexus:navigated` `CustomEvent` (`detail.url`) after each swap
- Sends `x-wrnexus-nav: 1` on fetches so the server can return the page fragment
- Appends any `/__wrnexus/*` runtime scripts the incoming page needs but the current document lacks
### Browser: realtime rooms
Connects to `/realtime/<name>` over WebSocket (`ws`/`wss` chosen from `location.protocol`). Two usage modes.
Programmatic API via `window.wire`:
```ts
wire.room(name): Room // open (or reuse) a room connection
wire.bindRooms(root?) // (re)bind declarative [data-room] containers
interface Room {
name: string;
send(obj: object | string): Room; // JSON-stringifies objects; queues until open
on(type: string, cb): Room; // filter by msg.type; "*" or a fn = all messages
on(cb): Room;
close(): Room;
}
```
Internal lifecycle messages are emitted to listeners as `{ type }`: `__open`, `__close`, `__error`, and `__raw` (non-JSON frames, with `data`). Reconnect uses exponential backoff capped at 5s; queued sends flush on reconnect.
Declarative binding (zero JS) on a `data-room="<name>"` container:
| Attribute | On | Purpose |
| ------------------------------------ | --------------- | ------------------------------------------------------------------------ |
| `data-room="<name>"` | container | Connect to room `<name>` |
| `data-room-user="<id>"` | container | Identify the connection (`?user=<id>`) |
| `data-room-log` | element | Where incoming messages are appended |
| `<template data-room-item="<type>">` | template | Row template for messages of that `type` (empty = fallback) |
| `%field%` | inside template | Placeholder filled from the message field (text/attr only, HTML-escaped) |
| `data-room-status` | element | Reflects connection state text (`connected`/`disconnected`/`error`) |
| `data-room-status-class` | status element | Base class; a state variant (`is-connected`, …) is appended |
| `<form data-room-send>` | form | Submits named fields as a JSON message |
| `data-room-reset` | form field | Clears that field after send |
Rebinds on `wrnexus:navigated` and closes rooms whose container has left the page.
## Usage
Server side — serve the runtime strings from your router (example with `Bun.serve`):
```ts
import { getReactiveRuntime, getNavRuntime, getRealtimeRuntime } from "@wrnexus/csr";
const routes: Record<string, string> = {
"/__wrnexus/reactive.js": getReactiveRuntime(),
"/__wrnexus/nav.js": getNavRuntime(),
"/__wrnexus/realtime.js": getRealtimeRuntime(),
};
Bun.serve({
fetch(req) {
const body = routes[new URL(req.url).pathname];
if (body) {
return new Response(body, {
headers: { "content-type": "text/javascript; charset=utf-8" },
});
}
return new Response("Not found", { status: 404 });
},
});
```
Browser side — server-rendered HTML that the reactive runtime hydrates:
```html
<div data-scope="count: 0, showPassword: false">
<button data-on-click="count++">+1</button>
<span data-text="count"></span>
<p>Total: {{count}}</p>
<input type="{showPassword ? 'text' : 'password'}" />
<button
data-on-click="showPassword = !showPassword"
aria-label="{showPassword ? 'Hide password' : 'Show password'}"
>
Toggle password
</button>
</div>
<script src="/__wrnexus/reactive.js"></script>
```
State interpolation in ordinary attributes is reactive. The compiler keeps the
initial SSR value and emits an internal binding so attributes such as `type`,
`aria-label`, `aria-pressed`, `class`, and `href` update after state changes.
A realtime chat, fully declarative:
```html
<div data-room="lobby" data-room-user="ada">
<div data-room-status></div>
<ul data-room-log></ul>
<template data-room-item="chat"><li>%user%: %text%</li></template>
<form data-room-send>
<input name="text" data-room-reset />
<input type="hidden" name="type" value="chat" />
<button>Send</button>
</form>
</div>
<script src="/__wrnexus/realtime.js"></script>
```
Or drive a room from code:
```ts
const room = wire.room("lobby");
room.on("chat", (msg) => console.log(msg.user, msg.text));
room.send({ type: "chat", user: "ada", text: "hi" });
```
## Requirements / Notes
- **Bun-only** on the server (the package integrates with Bun-based WrNexus servers); the emitted strings are plain browser JS with no dependencies.
- Browser runtimes are **self-contained** (no imports, no build step) and **idempotent**, so re-hydration after navigation or HMR is safe.
- Designed for a **strict CSP**: the reactive expression evaluator avoids `eval`/`new Function` (no `unsafe-eval`), and DOM swaps use `importNode`/attribute writes rather than `innerHTML` (Trusted-Types friendly).
- Peer packages: rendered `.wrn` components and the serving layer come from `@wrnexus/core` (the sole dependency); pages are rendered by the WrNexus dev/prod server.
-46
View File
@@ -1,46 +0,0 @@
{
"name": "@wrnexus/csr",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/csr"
},
"homepage": "https://wrnexusjs.dev/packages/csr",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"csr"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-299
View File
@@ -1,299 +0,0 @@
# @wrnexus/db
## Rollout-safe migrations
Run `wrnexus db check` in CI before deployment. The analyzer reports stable
diagnostics for drops, renames, type changes, new/enforced required columns,
and potentially blocking index creation, with an expand/backfill/switch/contract
recommendation. `wrnexus db migrate` blocks critical issues in pending
migrations. `--allow-breaking` is an explicit operator override; already-applied
migrations do not block later releases.
> The database layer for WrNexus: TS models as the single source of truth for DDL, validation, and result typing, plus a driver-based `Db` client, migrations, and a sqlc-style query generator.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/db` is the server-side data layer. You describe tables as TypeScript
models (the `v` column builder + `table()`); those models drive migrations,
coerce raw DB rows into typed objects, and feed the query generator. A thin
`Driver` interface is implemented by adapters for SQLite (`bun:sqlite`),
Postgres/MySQL (`Bun.SQL`), and MongoDB. The `Db` client adds ergonomics —
model-mapped `all`/`one`, transactions, `createTable`, pagination, and batched
relation loading. A process-wide registry (`getDb`/`setDb`) exposes configured
connections to pages and API routes. Reach for it whenever a WrNexus app needs
persistence.
## Installation
```bash
bun add @wrnexus/db
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
The core entry (`@wrnexus/db`) is dependency-free; adapters and connectors live
in subpaths so importing the core doesn't pull in every driver.
| Subpath | Exports |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@wrnexus/db` | `v`, `table`, `Column`, `createDb`, `createTableSql`, the client registry (`setDb`/`getDb`/…), migrations, the query generator, and query helpers |
| `@wrnexus/db/connect` | `connectFromConfig`, `resolveDbUrl`, `DbConfig` — resolve a config to a live SQL `Db` |
| `@wrnexus/db/session` | `sqliteSessionStore` — a `bun:sqlite` session backend for `@wrnexus/core` |
| `@wrnexus/db/sqlite` | `sqlite(url?)` driver |
| `@wrnexus/db/postgres` | `postgres(url)` driver |
| `@wrnexus/db/mysql` | `mysql(url)` driver |
| `@wrnexus/db/mongo` | `mongo(url, dbName?)` document API |
### Schema — `v`, `table`, `Column`
`table(name, columns)` returns a `Model<T>`. Columns are built with `v`:
```ts
import { v, table } from "@wrnexus/db";
const users = table("users", {
id: v.id(), // auto-increment primary key
email: v.text().unique(),
name: v.text().optional(), // NULLable
age: v.int().default(0),
active: v.bool().default(true),
createdAt: v.timestamp().default("now"), // CURRENT_TIMESTAMP
});
```
Column builders: `v.id`, `v.text` (alias `v.string`), `v.int`, `v.real` (alias
`v.number`), `v.bool` (alias `v.boolean`), `v.timestamp`, `v.json`. `BaseType`
values are `"id" | "text" | "int" | "real" | "bool" | "timestamp" | "json"`.
`Column` modifiers (chainable): `.optional()`, `.unique()`, `.default(value)`
(use the sentinel `"now"` for a current-timestamp default), `.primaryKey()`,
`.references(table, column = "id")`. `.coerce(raw)` converts a raw DB value to
its JS type.
A `Model<T>` exposes: `name`, `columns`, `parse(row)` (coerces a raw row into a
typed `T`; unknown columns pass through), and `describe()` (returns each
column's `ColumnDef`, for migrations and the generator).
### Driver & client — `createDb`, `Db`, `Driver`
```ts
createDb(driver: Driver): Db
```
A `Driver` (implemented by adapters) exposes `dialect`, `query(sql, params?)`,
`exec(sql, params?)`, `transaction(fn)`, and `close()`. `createDb` wraps it in a
`Db`:
- `all<T>(sql, params?, model?)` — all rows, mapped through `model.parse` when a model is given.
- `one<T>(sql, params?, model?)` — first row or `null`.
- `exec(sql, params?)``Promise<ExecResult>` (`{ changes, lastInsertId? }`).
- `tx(fn)` — run `fn(db)` in a transaction; rolls back on throw. Nested `tx` reuses the current transaction.
- `createTable(model)` — runs the model's `CREATE TABLE IF NOT EXISTS` DDL.
- `close()` — idempotently rejects new top-level work, drains active queries and
transactions, then closes the underlying pool.
Every query is parameterized (positional params). `createTableSql(model, dialect, ifNotExists?)`
renders `CREATE TABLE` directly; `Dialect` is `"sqlite" | "postgres" | "mysql"`.
### Client registry — `getDb` / `setDb`
A process-wide registry the runtime configures at startup from `wrnexus.config.ts`
(the `db` setting is the default; `databases.<name>` entries are named).
- `setDb(db)` / `setDb(name, db)` — set the default or a named connection.
- `registerDb(name, db)` — alias of `setDb(name, db)`.
- `getDb(name = "default")` — the default or a named `Db` (throws if unconfigured).
- `hasDb(name?)`, `databaseNames()`, `closeDatabases()`. Registry shutdown clears
registrations first, attempts every open database, and reports close failures
together with `AggregateError` instead of leaking later pools.
```ts
const users = await getDb().all("SELECT * FROM users");
const events = await getDb("analytics").all("SELECT * FROM hits");
```
### Adapters
- `@wrnexus/db/sqlite``sqlite(url = ":memory:")`. `url` may be `file:./dev.db`, a raw path, or `:memory:`. Built on `bun:sqlite`; no external service.
- `@wrnexus/db/postgres``postgres(url)` (e.g. `postgres://user:pass@host:5432/db`, placeholders `$N`).
- `@wrnexus/db/mysql``mysql(url)` (e.g. `mysql://user:pass@host:3306/db`, placeholders `?`). Postgres/MySQL both use Bun's native `Bun.SQL` client and its pooled `begin()` for transactions.
- `@wrnexus/db/mongo``mongo(url, dbName?)`. A document API, not SQL: `db.collection(model)` returns a `MongoRepo<T>` with `find`, `findOne`, `insert`, `insertMany`, `update`, `delete`, `count`. Reads are coerced through `model.parse` (`_id` is mapped to `id`). The `mongodb` driver is imported lazily — install it to use Mongo.
### Migrations
Migrations are `.sql` files (in e.g. `app/db/migrations`), each split into
`-- +up` and `-- +down` sections. A file with no markers is treated entirely as
`up`. Applied names are recorded in a `_wire_migrations` table so each runs once.
- `parseMigration(name, content)``Migration` (`{ name, up, down }`).
- `loadMigrations(dir)` — parse all `.sql` files, sorted by filename.
- `appliedMigrations(db)` — applied names, oldest first.
- `migrate(db, dir, options?)` — apply all pending (each in a transaction); returns applied names.
- `rollback(db, dir, options?)` — roll back the most recent; returns its name or `null`.
- `status(db, dir)``{ name, applied }[]` for every migration file.
- `scaffoldMigration(dir, name, dialect, models?)` — write a new numbered migration; with `models` it generates `CREATE`/`DROP` for every table (referenced tables first via topological sort). Returns the file path.
### Query generator (sqlc-style)
Turns annotated SQL into typed TS functions; params and result types are
inferred from the models, and rows map back through `model.parse` when the
selected columns are model columns.
- `parseQueries(content)``QueryDef[]` from `-- name: X :one|:many|:exec` blocks.
- `generateQueriesFile(queries, models, dialect)` → the `queries.gen.ts` source. `models` is a `ModelRef[]` (`{ varName, model }`). Rewrites `:name` placeholders to positional (`$N`/`?`) form.
`QueryKind` is `"one" | "many" | "exec"`.
### Query helpers
- `paginate(db, { sql, params?, countSql?, model? }, opts?)` — offset pagination. Pass the base SELECT **without** a LIMIT; it appends the page window and derives `total` via a COUNT subquery. `PageOptions`: `{ page?, perPage?, maxPerPage? }` (defaults page 1, perPage 20, maxPerPage 100). Returns `Paginated<T>` (`items, page, perPage, total, totalPages, hasNext, hasPrev`).
- `loadRelated(db, parents, opts)` — load a relation for many parents in ONE query and attach it (no N+1). `RelationOptions`: `{ table, foreignKey, as, localKey?, single?, model? }``single: true` attaches one child (belongsTo), otherwise an array (hasMany). Table/foreign-key names are validated as identifiers.
### Session store
`@wrnexus/db/session` exports `sqliteSessionStore(path = "sessions.db")`, a
persistent, process-shared `SessionBackend` (from `@wrnexus/core`) backed by
`bun:sqlite` (WAL mode). Sessions survive restarts and are shared by every
worker on the same file.
## Usage
Define models, connect, create tables, and query with typed results:
```ts
import { v, table, createDb } from "@wrnexus/db";
import { sqlite } from "@wrnexus/db/sqlite";
const users = table<{ id: number; email: string; name: string | null }>("users", {
id: v.id(),
email: v.text().unique(),
name: v.text().optional(),
createdAt: v.timestamp().default("now"),
});
const db = createDb(sqlite("file:./dev.db"));
await db.createTable(users);
await db.exec("INSERT INTO users (email) VALUES (?)", ["a@b.com"]);
const list = await db.all("SELECT * FROM users", [], users); // rows typed + coerced
const one = await db.one("SELECT * FROM users WHERE id = ?", [1], users);
await db.tx(async (tx) => {
await tx.exec("UPDATE users SET name = ? WHERE id = ?", ["Ada", 1]);
});
```
Resolve a config to a live SQL `Db`, and register it:
```ts
import { connectFromConfig } from "@wrnexus/db/connect";
import { setDb, getDb } from "@wrnexus/db";
setDb(connectFromConfig({ driver: "sqlite", url: "file:./dev.db" }, process.cwd()));
const rows = await getDb().all("SELECT * FROM users");
```
Run migrations and paginate:
```ts
import { migrate, paginate } from "@wrnexus/db";
await migrate(db, "app/db/migrations");
const pageTwo = await paginate(
db,
{ sql: "SELECT * FROM users ORDER BY id", model: users },
{ page: 2 },
);
```
For deployments, `{ dryRun: true }` reports pending names without applying
their SQL, `signal` cancels safely between migrations, and the default
database-backed lock prevents concurrent deploy runners. A live lock produces
`WRN-DB-MIGRATION-LOCKED`; crash-stale locks expire after `lockTimeoutMs` (five
minutes by default). Disable it with `lock: false` only when an external deploy
coordinator already guarantees exclusivity.
```ts
const pending = await migrate(db, "app/db/migrations", { dryRun: true });
await migrate(db, "app/db/migrations", {
signal: shutdownController.signal,
lockTimeoutMs: 10 * 60_000,
});
```
MongoDB (document API):
```ts
import { mongo } from "@wrnexus/db/mongo";
const mdb = await mongo(process.env.MONGO_URL!, "app");
const repo = mdb.collection(users);
await repo.insert({ email: "a@b.com" });
const active = await repo.find({ active: true });
```
## Configuration
`connectFromConfig` (and the runtime) read a `DbConfig` (`{ driver, url }`)
where `driver` is `sqlite | postgres | mysql`. `resolveDbUrl(url, appRoot?)`
resolves a relative `file:`/`sqlite:` URL against the app root. MongoDB is not a
SQL driver — use `@wrnexus/db/mongo` directly.
## Requirements / Notes
- **Bun-only.** Uses `bun:sqlite` (SQLite adapter + session store) and `Bun.SQL`
(Postgres/MySQL). Migrations/scaffolding use `node:fs`/`node:path`.
- Works with `@wrnexus/core``sqliteSessionStore` implements its
`SessionBackend`; `getDb`/`setDb` are wired by the WrNexus runtime from
`wrnexus.config.ts`.
- The `mongodb` npm package is an optional, lazily-imported peer — install it
only if you use `@wrnexus/db/mongo`. The core package stays dependency-free.
## Repository and transaction helpers
Repositories accept an immutable equality `scope`, normally `{ column: "tenant_id", value:
ctx.tenant.id }`. The scope is injected into every read, count, update and delete, while create
overwrites any caller-supplied tenant value. This makes accidental cross-tenant CRUD through the
repository API fail closed.
```ts
import { createRepository, retryTransaction, databaseHealth, batch } from "@wrnexus/db";
const users = createRepository<User>(db, {
table: "users",
allowedColumns: ["email", "name", "active"],
});
const user = await users.require(42);
await users.update(42, { active: true });
```
Repository identifiers are validated, writes may be restricted to an allowlist, and values always use query parameters. Infrastructure packages remain helper-only and do not add UI dependencies to server code.
## 0.8 repository and transaction helpers
```ts
import { createRepository, databaseHealth, firstOrThrow, retryTransaction } from "@wrnexus/db";
const usersRepo = createRepository<User>(db, {
table: "users",
allowedColumns: ["email", "name", "active"],
maxListLimit: 250,
});
const users = await usersRepo.all({
orderBy: "name",
direction: "asc",
limit: 50,
offset: 0,
});
```
Repository SQL identifiers are validated and values remain parameterized. Placeholder generation is dialect-aware: PostgreSQL uses `$1`, `$2`, and SQLite/MySQL use `?`. List limits are bounded.
`retryTransaction()` retries recognized serialization, deadlock, and database-lock errors by default. Supply `shouldRetry` for application-specific retryable errors; ordinary validation or business errors are not retried automatically.
-67
View File
@@ -1,67 +0,0 @@
{
"name": "@wrnexus/db",
"version": "0.8.5",
"type": "module",
"description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/db"
},
"homepage": "https://wrnexusjs.dev/packages/db",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"db"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./connect": {
"types": "./dist/connect.d.ts",
"import": "./dist/connect.js"
},
"./session": {
"types": "./dist/session-store.d.ts",
"import": "./dist/session-store.js"
},
"./sqlite": {
"types": "./dist/adapters/sqlite.d.ts",
"import": "./dist/adapters/sqlite.js"
},
"./postgres": {
"types": "./dist/adapters/postgres.d.ts",
"import": "./dist/adapters/postgres.js"
},
"./mysql": {
"types": "./dist/adapters/mysql.d.ts",
"import": "./dist/adapters/mysql.js"
},
"./mongo": {
"types": "./dist/adapters/mongo.d.ts",
"import": "./dist/adapters/mongo.js"
}
},
"files": [
"dist",
"README.md"
]
}
-311
View File
@@ -1,311 +0,0 @@
# @wrnexus/dev-server
> The WrNexus HTTP + WebSocket server runtime — request dispatch, SSR document assembly, live-reload (HMR), and the portable production handler.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
This package is the server runtime that powers a WrNexus app in both development and production. A single **request runtime** (`createHandlers`) owns HTTP/WebSocket dispatch and SSR document assembly; it knows nothing about _how_ modules and assets are produced, so the dev and prod entry points wire in different backends: dev uses dynamic module loading plus on-the-fly bundling and injects a live-reload client; prod uses a static, pre-built manifest with cache-immutable assets. The package also ships a multi-app **gateway** (route several apps by `Host` header behind one port) and a portable `node:http` adapter for WinterCG hosts. It is entirely server-side and Bun-native (`Bun.serve`, `Bun.file`, `Bun.gzipSync`).
## Installation
```bash
bun add @wrnexus/dev-server
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported for the full server; the `node:http` adapter is for WinterCG embedding only).
## API
### Main entry (`@wrnexus/dev-server`)
| Export | Kind | Purpose |
| --------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `startServer(opts: ServeOptions)` | `Promise<RunningServer>` | Start the dev server on `Bun.serve`: builds the router, connects/migrates databases, wires assets + HMR, and starts the file watcher. |
| `createHandlers(deps: RuntimeDeps)` | `Handlers` | The shared request runtime (fetch + websocket handlers). Re-exported from `runtime.ts`. |
| `createProductionServer(manifest, opts)` | `Bun.Server` | Start the production server from a precompiled manifest. |
| `createProductionHandlers(manifest, opts)` | `Handlers` | Build the portable prod fetch/websocket handlers with no server bound (the deployment-adapter seam). |
| `startGateway(opts: GatewayOptions)` | `Promise<RunningGateway>` | Boot multiple apps as child processes and route by `Host`. |
| `toRequest`, `writeResponse`, `nodeListener`, `serveNode` | functions | `node:http` ↔ WinterCG `Request`/`Response` adapter. |
| `RESTART_EXIT_CODE` | `number` (`97`) | Exit code the dev child uses to ask the supervisor for a fresh process. |
| `STYLES_HREF`, `HMR_CLIENT_JS` | constants | The global stylesheet URL and the inline HMR client script. |
Exported types: `ServeOptions`, `RunningServer`, `RuntimeDeps`, `AssetServer`, `WsData`, `GatewayApp`, `GatewayOptions`, `GatewayAuth`, `GatewaySecurity`, `RunningGateway`, `FetchHandler`.
### `startServer(opts)`
```ts
interface ServeOptions {
appDir: string; // absolute/relative path to the app/ dir
port?: number; // default 3000
hostname?: string; // default "localhost"
mode?: Mode; // "development" | "production"; default "development"
hmr?: boolean; // inject live-reload client; default (mode === "development")
styleEntry?: string | null; // resolved absolute path to the global CSS entry
stylesConfig?: StylesConfig; // custom styles processor (e.g. Tailwind/PostCSS)
head?: string; // raw HTML appended to every page <head>
seo?: SeoConfig; // global SEO defaults
security?: SecurityConfig; // security headers + CORS policy
theme?: ThemeConfig; // design-token theme (merged over built-in light/dark)
i18n?: I18nConfig; // default language + supported locales
db?: { driver: string; url: string }; // default db → getDb(); dev auto-migrates
databases?: Record<string, { driver: string; url: string }>; // named dbs → getDb("<name>")
realtime?: { scale?: boolean; redisUrl?: string }; // bridge rooms over Redis across processes
}
interface RunningServer {
port: number;
hostname: string;
url: string;
router: Router;
stop(): void;
}
```
In development, `startServer` also connects `app/db/migrations` (and `app/db/<name>/migrations`) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live. Page, component, layout, API, middleware, realtime, schema, locale, and public-asset edits invalidate only their cached modules, rescan routes where necessary, and morph fresh HTML through the existing HMR WebSocket. The server process and active gateway stay running.
`getWrnCompileMetrics()` exposes cumulative content-addressed compiler cache
`hits`, `misses`, successful `compilations`, `errors`, `totalDurationMs`, and
`lastDurationMs` for the DevToolbar or custom diagnostics. Tests and embedded
servers can call `resetWrnCompileMetrics()` to establish a fresh measurement
window.
### `createHandlers(deps)`
The core runtime shared by dev and prod. It handles CORS preflight, `/healthz` and `/__wrnexus/health`, request-body size limits (413), HMR socket upgrades (`/__wrnexus/hmr`), realtime WebSocket upgrades (`defineRoom` default export or a raw `websocket` export), the middleware pipeline, API routes (`/api/*`), framework assets (`/__wrnexus/*`), public assets, and full SSR page rendering (component mounts, layouts, slots, i18n markers, per-page script selection, ETag/304, gzip).
```ts
interface RuntimeDeps {
mode: Mode;
hmr: boolean; // inject the live-reload client into pages
router: Router;
loadModule(file: string): Promise<Record<string, unknown>>;
getMiddleware(): Promise<Middleware[]>;
assets: AssetServer; // serves /__wrnexus/* (islands, reactive, hmr)
hasStyles?: boolean; // inject the global stylesheet link
hasUi?: boolean; // inject the Wire UI stylesheet (/__wrnexus/ui.css)
theme?: ResolvedTheme; // enables /__wrnexus/theme.css + <html data-theme>
i18n?: ResolvedI18n; // enables ctx.t, <html lang>, {t:key} markers
inlineStyles?: string; // inline small prod stylesheets into <head>
assetVersion?: string; // cache-busting ?v= on framework asset URLs
head?: string; // raw HTML appended to every page <head>
seo?: SeoConfig;
security?: SecurityConfig;
maxBodyBytes?: number; // 413 above this; default 10 MB
hub?: HmrHub; // browser HMR sockets (dev only)
realtimeBus?: RealtimeBus; // cross-process room bridge (Redis pub/sub)
}
interface Handlers {
fetch(req: Request, server: UpgradeServer): Promise<Response | undefined>;
websocket: { open; message; close; drain };
}
```
`WsData` is the per-connection socket tag — a discriminated union of `{ kind: "realtime"; handler }`, `{ kind: "room"; meta }`, or `{ kind: "hmr" }`.
### `createProductionServer(manifest, opts)` / `createProductionHandlers(manifest, opts)`
Production runs the _same_ request runtime as dev, but with no filesystem scan and no runtime bundling. `wrnexus build` emits an entry that statically imports every route/component/layout module and passes them as a `ProdManifest`; the route-matching tables are rebuilt from the raw patterns.
```ts
interface ProdManifest {
pages: { raw: string; mod: RouteModule }[];
api: { raw: string; mod: RouteModule }[];
realtime: { raw: string; mod: RouteModule }[];
middleware: Middleware[];
components: { name: string; mod: RouteModule }[];
layouts: { name: string; mod: RouteModule }[];
}
interface ProdOptions {
stylesPath?: string;
inlineStyles?: string;
reactivePath?: string;
themePath?: string;
themeJsPath?: string;
theme?: ResolvedTheme;
uiCssPath?: string;
schemasJs?: string;
i18n?: ResolvedI18n;
db?: { driver: string; url: string };
databases?: Record<string, { driver: string; url: string }>;
realtime?: { scale?: boolean; redisUrl?: string };
assetVersion?: string;
publicDir?: string;
head?: string;
seo?: SeoConfig;
security?: SecurityConfig;
port?: number;
hostname?: string;
maxBodyBytes?: number;
}
```
`createProductionServer` also loads the `.env` cascade for the `production` profile, installs `SIGTERM`/`SIGINT` graceful shutdown, and binds `0.0.0.0` (port from `opts.port` or `$PORT`, default 3000). Migrations are **not** run here — apply them first (`wrnexus db migrate`). `createProductionHandlers` returns the bare handlers for edge/serverless/`node:http` deployment.
### `startGateway(opts)` — multi-app gateway
Serves several apps behind one port and routes each request to the right app by its `Host` header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. In development, normal application edits are applied inside the existing child and sent through its existing HMR connection. The child supervisor remains as crash recovery rather than the normal update path. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes).
```ts
interface GatewayOptions {
port?: number; // default 3000
hostname?: string; // dev: "127.0.0.1"; production: "0.0.0.0"
mode?: "development" | "production";
apps: GatewayApp[];
security?: GatewaySecurity;
}
interface GatewayApp {
name: string; // app id (for logs)
dir: string; // app root (contains app/ + wrnexus.config.ts)
domains: string[]; // host names routed here
port?: number; // fixed internal port; else assigned
auth?: GatewayAuth; // per-app edge access control
}
interface GatewayAuth {
basic?: { user: string; pass: string } | Array<{ user: string; pass: string }>;
allowIps?: string[]; // exact-match IP allowlist
forward?: { url: string }; // forward-auth (SSO): 2xx allows
}
interface GatewaySecurity {
trustedHostsOnly?: boolean; // 404 unknown hosts instead of first app
rateLimit?: { max: number; windowMs?: number }; // global by client IP (429)
headers?: boolean; // add baseline edge security headers
forwardedHeaders?: boolean; // set X-Forwarded-* (default true)
accessLog?: boolean;
}
```
Forward auth is a verification hook, not a login page. Configure `forward.url` with a
dedicated endpoint such as `http://sso.localhost:3000/api/verify`. The gateway forwards
the request's `Cookie` and `Authorization` headers plus `X-Forwarded-Host`,
`X-Forwarded-Proto`, `X-Original-Method`, and `X-Original-Uri` (including its query
string). The verifier must return 2xx only for an authenticated session and 401/403
otherwise. Pointing forward auth at an SSO home page that always returns 200 allows
every request and does not implement SSO.
For browser SSO, the verifier may return a `302`/`303`/`307`/`308` with a `Location`
header pointing to its login page. The gateway passes that redirect to the browser. The
login flow should validate a signed `returnTo` value before redirecting back; API clients
should receive `401`/`403` instead of an HTML login redirect.
Open the gateway URL (normally `http://127.0.0.1:3000`), not an app's internal
port. The gateway exposes `/__gateway/health` (JSON list of routed apps) and returns a
`RunningGateway` (`{ port, url, stop() }`). Use `--host=0.0.0.0` when other devices need
to reach a development gateway.
### `node:http` adapter (from `./adapters/node.ts`)
For embedding the WinterCG handler behind an existing Node server or a WinterCG host. Note the full app still needs Bun-compatible globals (`Bun.file`, `bun:sqlite`, etc.); only the `Request`/`Response` conversion is fully portable.
```ts
type FetchHandler = (req: Request) => Response | undefined | Promise<Response | undefined>;
toRequest(req: IncomingMessage, opts?): Promise<Request>
writeResponse(res: ServerResponse, response: Response): Promise<void> // preserves multiple Set-Cookie
nodeListener(handler: FetchHandler, opts?): (req, res) => Promise<void>
serveNode(handler: FetchHandler, opts?): Promise<Server>
```
### Subpath export: `@wrnexus/dev-server/serve-entry`
The child process the dev supervisor launches:
```bash
bun run serve-entry.ts <appDir> <port> <mode>
```
It loads the optional `wrnexus.config.ts`, resolves the style entry, calls `startServer`, and prints the route table (Pages / API / Realtime / Components). Because it runs in its own process, every restart re-imports all route modules fresh — that is how the supervisor delivers live reload of edited server code. `startGateway` resolves this entry via `import.meta.resolve("@wrnexus/dev-server/serve-entry")` to spawn each dev app.
## Usage
### Programmatic dev server
```ts
import { startServer } from "@wrnexus/dev-server";
const server = await startServer({
appDir: "./app",
port: 3000,
mode: "development",
theme: {/* design tokens */},
db: { driver: "sqlite", url: "file:./data/app.db" },
});
console.log(`Running at ${server.url}`);
// server.stop();
```
### Production server from a build manifest
```ts
import { createProductionServer } from "@wrnexus/dev-server";
import { manifest } from "./dist/manifest.js"; // generated by `wrnexus build`
createProductionServer(manifest, {
stylesPath: "./dist/styles.css",
reactivePath: "./dist/reactive.js",
assetVersion: process.env.BUILD_ID,
db: { driver: "postgres", url: process.env.DATABASE_URL! },
port: Number(process.env.PORT) || 3000,
});
```
### Embedding the handler on `node:http`
```ts
import { createProductionHandlers, serveNode } from "@wrnexus/dev-server";
const handlers = createProductionHandlers(manifest, opts);
await serveNode(handlers.fetch, { port: 8080 });
```
### Multi-app gateway
```ts
import { startGateway } from "@wrnexus/dev-server";
await startGateway({
port: 3000,
apps: [
{ name: "web", dir: "./apps/web", domains: ["localhost", "web.localhost"] },
{
name: "admin",
dir: "./apps/admin",
domains: ["admin.localhost"],
auth: { basic: { user: "root", pass: "s3cret" } },
},
],
security: { trustedHostsOnly: true, rateLimit: { max: 600 } },
});
```
## Framework asset routes
The runtime serves these framework-owned paths (dev builds them live; prod serves pre-built/immutable versions):
- `/__wrnexus/nav.js`, `/__wrnexus/reactive.js`, `/__wrnexus/realtime.js` — client runtimes
- `/__wrnexus/validate.js`, `/__wrnexus/schemas.js`, `/__wrnexus/i18n.js` — validation + i18n runtimes
- `/__wrnexus/theme.css`, `/__wrnexus/theme.js`, `/__wrnexus/ui.css`, `/__wrnexus/styles.css` — styles
- `/__wrnexus/hmr` — dev-only HMR WebSocket
- `/__wrnexus/csr` — server-evaluated CSR bindings for browser-side API fetches
Pages get only the scripts they use: `nav.js` always, `reactive.js` when a page has a `data-scope`/CSR fetch, plus theme/validation/i18n/realtime runtimes when the relevant markup is present.
## Requirements / Notes
- **Bun-only.** Uses `Bun.serve` (HTTP + WebSocket), `Bun.file`, and `Bun.gzipSync`. The full app also relies on `bun:sqlite` / `Bun.SQL` via `@wrnexus/db`.
- Orchestrates the whole framework: `@wrnexus/core` (context, security, realtime registry), `@wrnexus/router`, `@wrnexus/ssr` (`renderDocument`), `@wrnexus/csr` (client runtimes), `@wrnexus/compiler` (`.wrn` → TS), `@wrnexus/styles`, `@wrnexus/ui`, `@wrnexus/validation`, `@wrnexus/i18n`, `@wrnexus/db`, and `@wrnexus/pubsub` (Redis-backed cross-process realtime).
- `.wrn` files compile into a content-addressed hidden `.wrnexus/` cache. Targeted
invalidation gives changed modules a fresh import identity without restarting
the development server.
- Responses are gzipped when the client accepts it and the body is a buffered, compressible payload ≥ 1 KB; streaming/SSE responses opt out via `Cache-Control: no-transform`.
</content>
</invoke>
-70
View File
@@ -1,70 +0,0 @@
{
"name": "@wrnexus/dev-server",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/dev-server"
},
"homepage": "https://wrnexusjs.dev/packages/dev-server",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"dev-server"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./serve-entry": {
"types": "./dist/serve-entry.d.ts",
"import": "./dist/serve-entry.js"
}
},
"dependencies": {
"@wrnexus/authz": "^0.8.5",
"@wrnexus/rpc": "^0.8.5",
"@wrnexus/core": "^0.8.5",
"@wrnexus/dev-toolbar": "^0.8.5",
"@wrnexus/router": "^0.8.5",
"@wrnexus/ssr": "^0.8.5",
"@wrnexus/csr": "^0.8.5",
"@wrnexus/compiler": "^0.8.5",
"@wrnexus/styles": "^0.8.5",
"@wrnexus/ui": "^0.8.5",
"@wrnexus/validation": "^0.8.5",
"@wrnexus/i18n": "^0.8.5",
"@wrnexus/db": "^0.8.5",
"@wrnexus/pubsub": "^0.8.5",
"@wrnexus/uploader": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/store": "^0.8.5",
"@wrnexus/security": "^0.8.5",
"@wrnexus/observability": "^0.8.5",
"@wrnexus/cache": "^0.8.5",
"@wrnexus/pwa": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-30
View File
@@ -1,30 +0,0 @@
# @wrnexus/dev-toolbar
Development-only page quality toolbar for WRNexusJS.
## Features
- Runtime, resource and unhandled promise error capture
- Accessibility, SEO, image, media, color, HTML, form, link, responsive and security checks
- Performance and network observations
- First-class application tabs for runtime, stores, cache, accessibility, SEO, performance,
security, images, links, and JavaScript
- Plugin-contributed applications with badges, descriptions, issue feeds, and structured data
- Element highlighting and issue filtering
- Server-side issue collector
- Development-only asset strings for direct serving by `@wrnexus/dev-server`
- Safe open-in-editor helper
## Dev-server integration
Serve `DEV_TOOLBAR_RUNTIME` at `/__wrnexus/dev-toolbar.js` and `DEV_TOOLBAR_CSS` at `/__wrnexus/dev-toolbar.css`, then inject this before `</body>` in development:
```html
<script type="module" src="/__wrnexus/dev-toolbar.js" data-wrnexus-dev-toolbar></script>
```
The browser runtime exposes `window.__wrnexusDevToolbar`.
Plugin panels returned through `devToolbarPanels()` are automatically added to the application
strip. Their issue category is filterable, and structured `data` is rendered as escaped diagnostic
content so a plugin never needs to inject toolbar HTML.
-59
View File
@@ -1,59 +0,0 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/dev-toolbar"
},
"homepage": "https://wrnexusjs.dev/packages/dev-toolbar",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"dev-toolbar"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./client": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.js"
},
"./server": {
"types": "./dist/server/index.d.ts",
"import": "./dist/server/index.js"
},
"./types": {
"types": "./dist/types.d.ts",
"import": "./dist/types.js"
},
"./rules": {
"types": "./dist/rules/index.d.ts",
"import": "./dist/rules/index.js"
}
},
"files": [
"dist",
"README.md"
]
}
-67
View File
@@ -1,67 +0,0 @@
# @wrnexus/encryption
Authenticated encryption, hashing, HMAC, key rotation, and optional encrypted HTTP exchanges for WRNexusJS.
## Core helpers
- `generateKey()` — random 256-bit AES key encoded as base64.
- `deriveKey(password, salt)` — PBKDF2-derived AES key.
- `encrypt(plaintext, key)` / `decrypt(payload, key)` — AES-256-GCM.
- `sha256(data)` — SHA-256 digest.
- `hmacSign(data, secret)` / `hmacVerify(...)` — HMAC-SHA256.
- `createKeyring(keys)` — active/previous key management.
- `seal()` / `open()` — versioned ciphertext with key ID.
## Encrypted HTTP envelope
```ts
import {
createEncryptedRequest,
createKeyring,
createMemoryReplayStore,
decryptEncryptedResponse,
encryptedExchange,
} from "@wrnexus/encryption";
const keyring = createKeyring([{ id: "2026-08", secret: process.env.API_BODY_KEY!, active: true }]);
const replayStore = createMemoryReplayStore();
// Server middleware.
app.use(
encryptedExchange({
keyring,
replayStore,
maxAgeMs: 60_000,
maxBodyBytes: 1_048_576,
}),
);
// Controlled service/native client.
const request = await createEncryptedRequest(
"https://api.example.com/private/report",
{ reportId: "report-1" },
{ method: "POST", keyring },
);
const response = await fetch(request);
const result = await decryptEncryptedResponse(response, request, { keyring });
```
The envelope binds authenticated ciphertext to:
- HTTP method
- URL path and query
- request ID
- timestamp and expiry window
- encryption key ID
- optional replay-store consumption
`encryptedBody()` decrypts request bodies only. `encryptedExchange()` also encrypts successful downstream responses while allowing application exceptions to propagate normally. `encryptedFetch()` provides a convenient controlled-client call.
## Security boundary
Encrypted HTTP bodies **do not replace TLS/HTTPS**. Always use HTTPS.
This layer is appropriate for service-to-service traffic, native/mobile applications, controlled agents, and selected fields protected with server-managed keys. It cannot conceal data from an end user when browser JavaScript receives the decryption key. Never ship a long-lived server encryption key to a browser.
Use a shared replay store such as Redis in multi-instance deployments. The memory replay store is process-local.
-46
View File
@@ -1,46 +0,0 @@
{
"name": "@wrnexus/encryption",
"version": "0.8.5",
"type": "module",
"description": "Authenticated encryption, key rotation, hashing, and optional application-layer encrypted HTTP envelopes.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/encryption"
},
"homepage": "https://wrnexusjs.dev/packages/encryption",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"encryption"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-81
View File
@@ -1,81 +0,0 @@
# @wrnexus/helpers
Safe convenience helpers for common WrNexus application flows. The package uses
standard `Context`, `URL`, and `Response` values and has no runtime dependency beyond
`@wrnexus/core`.
## Installation
```bash
bun add @wrnexus/helpers
```
The package is private, so the machine must be authenticated to the `wrnexus` npm
organization.
## Usage
### Redirect an unauthenticated forward-auth request
The gateway calls an SSO verifier on a different URL from the original application.
These helpers reconstruct the original URL from the gateway headers and safely place it
in the login redirect:
```ts
import type { Context } from "@wrnexus/core";
import { redirectToLogin } from "@wrnexus/helpers";
export const GET = async (ctx: Context) => {
if (await hasValidSession(ctx)) {
return new Response(null, { status: 204 });
}
return redirectToLogin(ctx, "/login", {
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
});
};
```
This creates a response such as:
```text
Location: http://sso.localhost:3000/login?returnTo=http%3A%2F%2Fadmin.localhost%3A3000%2F
```
Always list the application hosts that are valid redirect destinations. Forwarded host
headers are rejected when `allowedHosts` is absent or does not match, preventing an open
redirect.
The SSO hostname is the login destination, not an `allowedHosts` entry. For example,
when protecting `admin.localhost:3000`, keep `admin.localhost:3000` in the allowlist even
though the verifier runs at `sso.localhost:3000`. WRNexus preserves both hosts across a
nested gateway request.
### Support dynamic tenant domains
```ts
import type { Context } from "@wrnexus/core";
import { getOriginalRequestOrigin, redirectToLogin } from "@wrnexus/helpers";
export const GET = async (ctx: Context) => {
const allowedHosts = (host: string) => host === "example.test" || host.endsWith(".example.test");
console.info("Authentication requested by", getOriginalRequestOrigin(ctx, { allowedHosts }));
return redirectToLogin(ctx, "https://auth.example.test/login", {
allowedHosts,
returnToParam: "continue",
status: 303,
});
};
```
## API
- `getOriginalRequestUrl(ctx, options): URL` — reconstruct the gateway URL.
- `getOriginalRequestOrigin(ctx, options): string` — return only its origin.
- `getOriginalRequestPath(ctx): string` — return its path and query string.
- `getOriginalRequestMethod(ctx): string` — return its HTTP method.
- `redirectToLogin(ctx, loginUrl, options): Response` — create a login redirect with an
encoded `returnTo` parameter.
For direct requests without gateway headers, URL helpers use `ctx.url`.
-46
View File
@@ -1,46 +0,0 @@
{
"name": "@wrnexus/helpers",
"version": "0.8.5",
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/helpers"
},
"homepage": "https://wrnexusjs.dev/packages/helpers",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"helpers"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-93
View File
@@ -1,93 +0,0 @@
# @wrnexus/i18n
Recursive locale loading, fallback resolution, SSR/browser translations, locale formatting, and language UI blocks for WRNexusJS.
## Locale files
Both layouts can be used together:
```text
app/locales/en.json
app/locales/en/common.json
app/locales/en/auth.json
app/locales/mr/common.json
```
Namespaced files become keys such as `common.save` and `auth.signIn`.
```ts
import { loadLocales, makeT, resolveI18n, resolveLang } from "@wrnexus/i18n";
const i18n = resolveI18n(loadLocales("app/locales", { strict: true }), {
default: "en",
locales: ["en", "mr", "hi"],
fallbacks: { "mr-IN": ["mr", "en"] },
cookie: { name: "wire-lang", sameSite: "Lax", secure: true },
});
const lang = resolveLang(i18n, cookieValue, request.headers.get("accept-language"));
const t = makeT(i18n, lang);
t("common.hello", { name: "Ajay" });
```
## Resolution behavior
- normalized BCP-47-style locale names
- cookie preference
- weighted `Accept-Language`
- wildcard language ranges
- regional base fallback
- explicit fallback chains
- configured default language
- automatic RTL for Arabic, Hebrew, Persian, Urdu, and related languages
Locale JSON is size-limited and rejects prototype-pollution keys. Recursive namespace collisions are resolved safely.
## Views and runtime
```html
<h1 data-t="dashboard.title">Dashboard</h1>
<input t:placeholder="search.placeholder" />
```
Text and translated attributes are resolved during SSR. Active/fallback messages are serialized safely for the language runtime, which rebinds `data-t` markers after client navigation.
Enable `i18nPlugin()` to use:
- `<LanguageSwitcher />`
- `<LocaleStatus />`
`LanguageSwitcher` renders a native `select[data-wire-lang]`. The packaged runtime validates the
selection against the configured locales, writes the configured language cookie, updates the
document `lang`/`dir` attributes, emits `wrnexus:language-change`, and reloads so the next SSR
request uses the same cookie. No application-owned browser script is required.
## Formatting
- `formatNumber`
- `formatCurrency`
- `formatDate`
- `formatRelativeTime`
- `plural`
- `createLocaleFormatter`
- `translationCoverage`
Localization tooling can extract statically discoverable `t("key")`,
`i18n.t("key")`, and `data-i18n="key"` usage, compare every locale with a
reference, and create layout-stressing pseudo-locales:
```ts
import {
auditLocaleKeys,
createPseudoLocale,
extractTranslationKeysFromFiles,
} from "@wrnexus/i18n";
const used = extractTranslationKeysFromFiles(sourceFiles);
const coverage = auditLocaleKeys(messages, "en");
const enXA = createPseudoLocale(messages.en);
const arXB = createPseudoLocale(messages.en, { rtl: true });
```
Pseudo-localization preserves interpolation placeholders and markup tags. RTL
pseudo output uses Unicode direction controls, while runtime direction detection
continues to derive `rtl` from Arabic and other RTL language subtags.
-61
View File
@@ -1,61 +0,0 @@
{
"name": "@wrnexus/i18n",
"version": "0.8.5",
"type": "module",
"description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/i18n"
},
"homepage": "https://wrnexusjs.dev/packages/i18n",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"i18n"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./plugin": {
"types": "./dist/plugin.d.ts",
"import": "./dist/plugin.js"
},
"./components/*": "./components/*"
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/ui": "^0.8.5"
},
"wrnexus": {
"plugin": {
"plugin": "./dist/plugin.js",
"export": "default",
"factory": true
}
},
"files": [
"dist",
"README.md",
"components"
]
}
-191
View File
@@ -1,191 +0,0 @@
# @wrnexus/jwt
> Dependency-free JSON Web Tokens (HS256) via Web Crypto, plus a bearer-token auth middleware for WrNexus.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/jwt` signs and verifies stateless JSON Web Tokens using the **HS256**
(HMAC-SHA-256) algorithm. It has no runtime dependencies — signing and
verification are implemented directly on the standard **Web Crypto** API
(`crypto.subtle`), which Bun provides natively. It runs server-side and pairs
with the session-based auth in `@wrnexus/core`, giving you a stateless option
for API and mobile clients. Reach for it when you need bearer-token auth rather
than cookie sessions.
## Installation
```bash
bun add @wrnexus/jwt
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
Single entry point (`@wrnexus/jwt`). All functions are async and return Promises.
| Export | Kind | Description |
| --------------------------------------- | --------- | --------------------------------------------------------------- |
| `signJwt(payload, secret, options?)` | function | Sign claims into an HS256 token string. |
| `verifyJwt<T>(token, secret, options?)` | function | Verify a token and return its claims, or throw. |
| `jwtAuth(options)` | function | Middleware that verifies a bearer JWT and sets `ctx.user`. |
| `JwtError` | class | Error thrown on any signature/payload/expiry failure. |
| `JwtClaims` | interface | Claims shape (`sub`, `iat`, `exp`, `nbf`, plus arbitrary keys). |
| `SignOptions` | interface | Options for `signJwt`. |
| `JwtAuthOptions` | interface | Options for `jwtAuth`. |
### `signJwt(payload, secret, options?)`
```ts
function signJwt(payload: JwtClaims, secret: string, options?: SignOptions): Promise<string>;
```
Signs `payload` with `secret` using HS256 and returns the encoded token
(`header.body.signature`). An `iat` (issued-at) claim is always added.
`SignOptions`:
- `expiresIn?: number` — seconds until expiry; sets the `exp` claim.
- `now?: number` — override the issued-at time (seconds), useful for testing.
### `verifyJwt<T>(token, secret, options?)`
```ts
function verifyJwt<T extends JwtClaims = JwtClaims>(
token: string,
secret: string,
options?: { now?: number },
): Promise<T>;
```
Verifies the HS256 signature and returns the decoded claims typed as `T`.
Throws `JwtError` when the token is malformed, the signature is invalid, the
payload is not valid JSON, the token is expired (`exp`), or not yet valid
(`nbf`). Pass `now` (seconds) to override the reference time for the `exp`/`nbf`
checks.
### `jwtAuth(options)`
```ts
function jwtAuth(options: JwtAuthOptions): Middleware;
```
Returns a WrNexus `Middleware` that reads a token, verifies it, and assigns the
claims to `ctx.user`.
`JwtAuthOptions`:
- `secret: string` — the HMAC secret used to verify tokens.
- `getToken?: (ctx: Context) => string | undefined` — how to extract the token.
Defaults to reading `Authorization: Bearer <token>`.
- `required?: boolean` — when `true` (default), a missing or invalid token
responds with `401 { ok: false, error: "Unauthorized" }`. When `false`,
requests pass through and `ctx.user` is only set if a valid token is present.
## Usage
```ts
import { signJwt, verifyJwt, jwtAuth, JwtError } from "@wrnexus/jwt";
const secret = process.env.JWT_SECRET!;
// Sign a token that expires in one hour
const token = await signJwt({ sub: user.id, role: "admin" }, secret, {
expiresIn: 3600,
});
// Verify it later
try {
const claims = await verifyJwt<{ sub: string; role: string }>(token, secret);
console.log(claims.sub, claims.role);
} catch (err) {
if (err instanceof JwtError) {
// invalid signature, expired, malformed, etc.
}
}
```
Protecting routes with the middleware:
```ts
import { jwtAuth } from "@wrnexus/jwt";
// Require a valid bearer token; ctx.user holds the verified claims
app.use(jwtAuth({ secret: process.env.JWT_SECRET! }));
// Optional auth — populate ctx.user when present, but don't 401
app.use(jwtAuth({ secret: process.env.JWT_SECRET!, required: false }));
```
## Requirements / Notes
- **Bun-only.** Uses the standard Web Crypto API (`crypto.subtle.importKey`,
`sign`, `verify`) plus `btoa`/`atob` and `TextEncoder`/`TextDecoder` — all
provided by Bun. No third-party crypto dependency.
- **Algorithm:** HS256 (HMAC with SHA-256) only. Asymmetric algorithms (RS/ES)
are not supported.
- Integrates with [`@wrnexus/core`](../core) for `Context`, `Middleware`, and
`ctx.user`; it complements the framework's cookie/session auth with a
stateless bearer-token flow for API and mobile clients.
## Access, refresh, scope, and cookie helpers
```ts
import {
createAccessToken,
createRefreshToken,
verifyAccessToken,
verifyRefreshToken,
extractBearerToken,
requireScopes,
jwtCookie,
} from "@wrnexus/jwt";
```
The helpers add explicit `type: "access" | "refresh"` claims, scope checks, refresh-token family metadata, no-store token responses, and secure cookie defaults. `__Host-` cookies are rejected unless they use `Path=/` and `Secure`; `SameSite=None` is rejected without `Secure`.
## 0.8 helper kit
```ts
import {
createTokenPair,
verifyAccessToken,
verifyRefreshToken,
extractBearerToken,
readJwtCookie,
jwtCookie,
clearJwtCookie,
requireScopes,
} from "@wrnexus/jwt";
const pair = await createTokenPair(user.id, {
accessSecret: process.env.JWT_ACCESS_SECRET!,
refreshSecret: process.env.JWT_REFRESH_SECRET!,
scopes: ["profile:read"],
family: sessionFamily,
});
```
The helper kit validates `__Host-` cookie invariants, cookie names and paths, `SameSite=None` security, typed access/refresh token types, scope requirements, and no-store token responses.
In addition to local HS256 secrets/keyrings, the package verifies standards-based
RS256 tokens through bounded remote JWKS caches:
```ts
import { createRemoteJwks, verifyJwtWithJwks } from "@wrnexus/jwt";
const jwks = createRemoteJwks("https://issuer.example/.well-known/jwks.json");
const claims = await verifyJwtWithJwks(token, jwks, {
issuer: "https://issuer.example",
audience: "my-api",
maxAge: 300,
});
```
JWKS URLs must use HTTPS. Responses have key-count/byte limits, accept only
RS256 signing RSA keys, deduplicate concurrent refreshes, cache imported public
keys, and force an immediate refresh for an unknown `kid` so issuer rotation
does not wait for cache expiry. Never use decoded-but-unverified claims for an
authorization decision.
-46
View File
@@ -1,46 +0,0 @@
{
"name": "@wrnexus/jwt",
"version": "0.8.5",
"type": "module",
"description": "HS256 JSON Web Tokens, key rotation, access/refresh helpers, scopes, cookies, and auth middleware.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/jwt"
},
"homepage": "https://wrnexusjs.dev/packages/jwt",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"jwt"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-91
View File
@@ -1,91 +0,0 @@
# @wrnexus/mobile
> SSR-safe access to Capacitor plugins from WRNexusJS browser code.
## Overview
`@wrnexus/mobile` keeps optional native imports out of server rendering while giving
browser-owned modules one consistent registry for Capacitor plugins. During SSR,
`mobile.isNative()` is `false` and `mobile.platform()` is `"web"`.
## Installation
Install a plugin through the WRNexusJS CLI so the web and native projects stay aligned:
```bash
wrnexus mobile add @capacitor/camera @capacitor/haptics
```
## Usage
### Register and invoke a Capacitor plugin
Import Capacitor packages only from browser-owned code, never from API routes or SSR
helpers.
```ts
import { Camera, CameraResultType } from "@capacitor/camera";
import { mobile } from "@wrnexus/mobile";
mobile.registerPlugin("Camera", Camera);
export async function takePhoto() {
if (!mobile.isNative()) return null;
return mobile.invoke("Camera", "getPhoto", {
quality: 85,
resultType: CameraResultType.Uri,
});
}
```
### Provide a browser fallback
`whenNative` runs the first callback only in a Capacitor WebView and can return a
web/SSR-safe fallback everywhere else.
```ts
import { Haptics, ImpactStyle } from "@capacitor/haptics";
import { mobile } from "@wrnexus/mobile";
mobile.registerPlugin("Haptics", Haptics);
export const confirmAction = () =>
mobile.whenNative(
() => mobile.invoke("Haptics", "impact", { style: ImpactStyle.Medium }),
() => navigator.vibrate?.(30),
);
```
### Read an optional plugin without throwing
```ts
import type { NetworkPlugin } from "@capacitor/network";
import { mobile } from "@wrnexus/mobile";
const network = mobile.plugin<NetworkPlugin>("Network");
const status = network ? await network.getStatus() : { connected: true, connectionType: "unknown" };
```
## API
- `registerPlugin(name, instance)` registers a browser-imported plugin.
- `plugin(name)` returns a plugin or `undefined`; `requirePlugin(name)` throws when absent.
- `invoke(plugin, method, options?)` calls a registered method and returns its result.
- `whenNative(native, fallback?)` selects native behavior without breaking SSR.
- `isNative()` and `platform()` report the current Capacitor environment.
Unavailable required plugins throw `MobileUnavailableError` with an actionable message.
The package also provides portable application-facing primitives:
- `listenDeepLinks` normalizes initial and live links with an allowed-scheme list.
- `PushNotifications` performs permission gating and validates registrations.
- `SecureStorage` namespaces and validates keys over an application-supplied encrypted
Keychain/Keystore adapter; it does not mislabel browser `localStorage` as secure.
- `OfflineQueue` persists bounded sync batches through a pluggable durable store.
## Requirements / Notes
- Capacitor plugin imports must remain in browser-owned modules.
- `@wrnexus/mobile` re-exports `native` from `@wrnexus/native` for applications that
prefer the higher-level cross-platform capability API.
-46
View File
@@ -1,46 +0,0 @@
{
"name": "@wrnexus/mobile",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/mobile — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/mobile"
},
"homepage": "https://wrnexusjs.dev/packages/mobile",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"mobile"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"dependencies": {
"@wrnexus/native": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-88
View File
@@ -1,88 +0,0 @@
# @wrnexus/native
> Cross-platform capabilities for browsers, Capacitor WebViews, and compiled native apps.
## Overview
`@wrnexus/native` exposes capabilities by name so application code can ask what the
current platform supports before presenting an action. Browser capabilities use Web
APIs; mobile capabilities use installed Capacitor plugins. `platform()` returns
`"server"` during SSR, `"browser"` on the web, and the Capacitor platform in a native
WebView.
## Installation
```bash
bun add @wrnexus/native
```
## Usage
### Share a page when the platform supports it
```ts
import { native } from "@wrnexus/native";
export async function shareCurrentPage() {
if (!native.supports("share")) return false;
await native.run("share", {
title: document.title,
url: location.href,
});
return true;
}
```
### Register an application-specific capability
`register` returns an unregister function, which is useful for tests and temporary
feature modules.
```ts
import { native } from "@wrnexus/native";
const unregister = native.register("orders.scan", {
browser: {
supported: () => typeof window !== "undefined",
run: async ({ orderId }: { orderId: string }) => {
const code = window.prompt(`Scan code for order ${orderId}`);
return { code };
},
},
});
const result = await native.run<{ code: string | null }>("orders.scan", { orderId: "ord_42" });
unregister();
```
### Target browser or mobile behavior explicitly
```ts
import { native } from "@wrnexus/native";
const canUseMobileCamera = native.supports("camera", "mobile");
const position = await native.run(
"geolocation",
{ enableHighAccuracy: true },
{ target: "browser" },
);
```
## API
- `supports(name, target?)` checks availability without running the capability.
- `run(name, options?, runOptions?)` executes it or rejects with `NativeUnavailableError`.
- `register(name, capability)` adds or overrides a capability and returns cleanup.
- `registered()` lists capability names; `clearRegistry()` resets the registry.
- `isMobile()` and `platform()` report the current target safely during SSR.
Built-ins include `camera`, `clipboard.write`, `share`, `geolocation`, `network`,
`haptics`, storage, filesystem, notifications, and device information.
`defineNativeManifest` declares required capabilities and typed permissions, while
`PermissionManager` normalizes permission query/request flows across platform adapters.
## Requirements / Notes
Use `supports()` before showing optional controls. Mobile capabilities require their
matching Capacitor plugins to be installed and registered by the application.
-51
View File
@@ -1,51 +0,0 @@
{
"name": "@wrnexus/native",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/native — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/native"
},
"homepage": "https://wrnexusjs.dev/packages/native",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"native"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./browser": {
"types": "./dist/browser.d.ts",
"import": "./dist/browser.js"
},
"./mobile": {
"types": "./dist/mobile.d.ts",
"import": "./dist/mobile.js"
}
},
"files": [
"dist",
"README.md"
]
}
-217
View File
@@ -1,217 +0,0 @@
# @wrnexus/oauth
> Dependency-free OAuth 2.0 sign-in for any provider, with PKCE and presets for Google, GitHub, and Discord.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/oauth` implements the OAuth 2.0 Authorization Code flow (with PKCE) for
server-side sign-in. It ships ready-made provider presets and a `defineProvider`
helper for custom providers, then gives you two flow functions — `startAuth`
(build the redirect) and `completeAuth` (exchange the code and fetch the user's
profile). It has no runtime dependencies: it uses the platform `fetch` and
WebCrypto only. Pairs naturally with `@wrnexus/core`'s `logIn` to establish a
session once you have a normalized profile.
## Installation
```bash
bun add @wrnexus/oauth
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
### Providers
Each preset takes `ProviderCredentials` and returns an `OAuthProvider`.
```ts
interface ProviderCredentials {
clientId: string;
clientSecret: string;
scopes?: string[]; // override the preset's default scopes
}
```
| Export | Default scopes | Notes |
| ------------------------ | ---------------------------- | ------------------------------------------------------------------ |
| `google(creds)` | `openid`, `email`, `profile` | Sets `access_type: offline` for refresh tokens. |
| `github(creds)` | `read:user`, `user:email` | Maps `name` (falls back to `login`) and `avatar_url`. |
| `discord(creds)` | `identify`, `email` | Builds the avatar CDN URL from the user id + hash. |
| `defineProvider(config)` | — | Pass a full `OAuthProvider` to define a custom OAuth 2.0 provider. |
An `OAuthProvider` describes the endpoints, scopes, credentials, optional extra
authorize params, and a `mapProfile` normalizer:
```ts
interface OAuthProvider {
name: string;
authorizeUrl: string;
tokenUrl: string;
userInfoUrl: string;
scopes: string[];
clientId: string;
clientSecret: string;
authorizeParams?: Record<string, string>; // e.g. access_type, prompt
mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
}
```
### Flow
#### `startAuth(provider, options): Promise<StartAuthResult>`
Builds the authorize redirect URL with a generated PKCE challenge and CSRF
`state`. Store the returned `state` and `verifier` (session/cookie), then 302 the
user to `url`.
```ts
interface StartAuthOptions {
redirectUri: string;
state?: string; // reuse a state instead of generating one
params?: Record<string, string>; // extra authorize params, merged last
}
interface StartAuthResult {
url: string; // authorize URL to redirect to
state: string; // CSRF state — verify on callback
verifier: string; // PKCE code verifier — pass to completeAuth
}
```
#### `completeAuth(provider, options): Promise<{ tokens, profile }>`
On the callback: exchanges the authorization `code` for tokens, then fetches and
normalizes the user profile. Convenience wrapper over `exchangeCode` +
`fetchProfile`.
```ts
interface CompleteAuthOptions {
code: string;
redirectUri: string;
verifier?: string; // the PKCE verifier from startAuth
fetch?: typeof fetch; // inject a fetch implementation (tests)
}
```
#### Lower-level helpers
| Export | Signature | Purpose |
| ---------------------------------------- | ------------------------- | --------------------------------------------------------------- |
| `exchangeCode(provider, options)` | `→ Promise<OAuthTokens>` | Exchange an authorization code for tokens. |
| `fetchProfile(provider, tokens, fetch?)` | `→ Promise<OAuthProfile>` | Fetch + normalize the user's profile. |
| `randomToken(bytes?)` | `→ string` | Random URL-safe token (default 32 bytes) for `state`/verifiers. |
### Types
```ts
interface OAuthTokens {
access_token: string;
token_type?: string;
refresh_token?: string;
expires_in?: number;
id_token?: string;
scope?: string;
}
interface OAuthProfile {
id: string;
email?: string;
name?: string;
avatar?: string;
raw: Record<string, unknown>;
}
```
## Usage
```ts
import { google, startAuth, completeAuth } from "@wrnexus/oauth";
import { logIn } from "@wrnexus/core";
const provider = google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
});
const redirectUri = "https://example.com/auth/callback";
// 1. Kick off sign-in: redirect the user to the provider.
async function beginLogin(ctx) {
const { url, state, verifier } = await startAuth(provider, { redirectUri });
// Persist state + verifier in the session, then redirect.
ctx.session.set("oauth_state", state);
ctx.session.set("oauth_verifier", verifier);
return Response.redirect(url, 302);
}
// 2. Handle the callback.
async function handleCallback(ctx, code: string, state: string) {
if (state !== ctx.session.get("oauth_state")) throw new Error("bad state");
const { profile } = await completeAuth(provider, {
code,
redirectUri,
verifier: ctx.session.get("oauth_verifier"),
});
logIn(ctx, { id: profile.id, email: profile.email });
}
```
Custom provider with `defineProvider`:
```ts
import { defineProvider, startAuth } from "@wrnexus/oauth";
const gitlab = defineProvider({
name: "gitlab",
authorizeUrl: "https://gitlab.com/oauth/authorize",
tokenUrl: "https://gitlab.com/oauth/token",
userInfoUrl: "https://gitlab.com/api/v4/user",
scopes: ["read_user"],
clientId: process.env.GITLAB_CLIENT_ID!,
clientSecret: process.env.GITLAB_CLIENT_SECRET!,
mapProfile: (raw) => ({
id: String(raw.id),
email: raw.email as string | undefined,
name: raw.name as string | undefined,
avatar: raw.avatar_url as string | undefined,
raw,
}),
});
```
## Requirements / Notes
- **Bun-only.** Relies on the global `fetch` and WebCrypto (`crypto.getRandomValues`,
`crypto.subtle.digest`) — no other runtime dependencies.
- The flow is stateless by design: you are responsible for storing `state` and
`verifier` between `startAuth` and `completeAuth` (session or signed cookie).
- Pairs with [`@wrnexus/core`](../core) — feed the normalized `OAuthProfile` into
`logIn` to establish a session.
OIDC integrations can combine strict discovery with the rotating JWKS verifier:
```ts
import { createRemoteJwks } from "@wrnexus/jwt";
import { discoverOidc, verifyOidcIdToken } from "@wrnexus/oauth";
const metadata = await discoverOidc("https://issuer.example");
const jwks = createRemoteJwks(metadata.jwks_uri);
const claims = await verifyOidcIdToken(idToken, {
issuer: metadata.issuer,
clientId: "client-id",
jwks,
nonce: expectedNonce,
accessToken,
});
```
Discovery requires an exact normalized issuer and HTTPS endpoints without URL
credentials/fragments. ID-token verification checks the RS256 signature,
expiry/not-before, issuer, audience, required OIDC claims, nonce, multi-audience
`azp`, optional token age, and optional `at_hash` binding.
-46
View File
@@ -1,46 +0,0 @@
{
"name": "@wrnexus/oauth",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/oauth — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/oauth"
},
"homepage": "https://wrnexusjs.dev/packages/oauth",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"oauth"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"dependencies": {
"@wrnexus/jwt": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-60
View File
@@ -1,60 +0,0 @@
# @wrnexus/plugin
## Least-privilege package permissions
Package manifests declare every framework capability they register:
```json
{
"wrnexus": {
"permissions": ["routes", "migrations"],
"routes": [{ "kind": "api", "path": "/api/example", "entry": "./route.ts" }]
}
}
```
Applications can enable fail-closed grants:
```ts
export default {
pluginPermissions: {
enforce: true,
grants: { "example-plugin": ["routes"] },
},
};
```
Discovery rejects used-but-undeclared capabilities with
`WRN-PLUGIN-PERMISSION-UNDECLARED` and ungranted capabilities with
`WRN-PLUGIN-PERMISSION-DENIED`. Permissions cover components, browser runtime,
assets, styles, routes, middleware, migrations, config, transforms,
diagnostics/tooling, and server/build hooks.
## Compatibility matrices
Manifests can add `compatibility: { bunMin: "1.3.0", os: ["linux",
"darwin"] }` alongside `runtimes` and `requires`. Use
`testPluginCompatibility(manifest, targets)` in a package test to exercise the
complete support matrix. Runtime discovery enforces the same Bun minimum, OS,
runtime, and capability declarations used by the test kit.
Deterministic WRNexusJS plugin contracts for configuration, AST/code transforms,
diagnostics, development servers, production builds, and DevToolbar extensions.
Use `definePlugin()` and declare `enforce`, `before`, or `after` when ordering matters.
Duplicate names and dependency cycles are rejected.
## Complete lifecycle and contributions
Plugins may implement `setup`, `configure`, `configResolved`, `transformAst`,
`transformCode`, `diagnostics`, `routes`, `configureServer`, `buildStart`,
`buildEnd`, `render`, `deploy`, `shutdown`, and `hmrUpdate`. The runner preserves
resolved plugin order for every hook and executes `setup` exactly once.
In addition to components, routes, middleware, assets, styles, runtimes, and
migrations, plugins can contribute `directives`, `cliCommands`,
`virtualModules`, `deploymentAdapters`, `configSchemas`, `documentation`, and
`typeDefinitions`. Names are collision checked. Configuration schemas run after
configuration resolution, CLI commands are callable as normal `wrnexus`
commands, directives participate in AST transformation, and production builds
materialize virtual modules and invoke matching contributed adapters.
-58
View File
@@ -1,58 +0,0 @@
{
"name": "@wrnexus/plugin",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/plugin — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/plugin"
},
"homepage": "https://wrnexusjs.dev/packages/plugin",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"plugin"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"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.8.5"
},
"files": [
"dist",
"README.md"
]
}
-134
View File
@@ -1,134 +0,0 @@
# @wrnexus/pubsub
> Topic-based publish/subscribe with a pluggable driver — in-process by default, Redis for cross-process messaging.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/pubsub` is a small server-side pub/sub bus. You publish messages to a
topic and subscribe with topic patterns; handlers fire for matching topics. The
default driver keeps everything in-process, and you can swap in the Redis driver
(`@wrnexus/pubsub/redis`) to fan messages out across processes or hosts. It also
backs `@wrnexus/core`'s realtime bridge for horizontal scaling.
## Installation
```bash
bun add @wrnexus/pubsub
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
### `createPubSub(driver?): PubSub`
Creates a bus over a driver. Defaults to `memoryDriver()` (in-process).
```ts
interface PubSub {
publish<T = unknown>(topic: string, message: T): Promise<void>;
subscribe<T = unknown>(pattern: string, handler: Handler<T>): () => void;
close(): Promise<void>;
}
type Handler<T = unknown> = (message: T, topic: string) => void | Promise<void>;
```
- `publish(topic, message)` — resolves once the driver and in-memory async handlers finish.
- `subscribe(pattern, handler)` — returns an unsubscribe function.
- `close()` — idempotently rejects new work, clears local subscriptions, and closes the driver.
### Pattern matching
Subscription patterns match in three ways:
- **Exact** — `"order:created"` matches only that topic.
- **Prefix** — `"order:*"` matches any topic starting with `"order:"`.
- **Everything** — `"*"` matches all topics.
### `memoryDriver(): PubSubDriver`
The default in-process driver. Handlers are invoked synchronously (fire-and-forget
for async handlers) whenever a published topic matches a registered pattern.
```ts
interface PubSubDriver {
publish(topic: string, message: unknown): void | Promise<void>;
subscribe(pattern: string, handler: Handler): () => void;
}
```
### `@wrnexus/pubsub/redis` — `redisDriver(url?)`
A cross-process driver backed by Redis. It speaks RESP over a raw TCP socket via
`Bun.connect`, so it adds **no npm dependency**. `url` defaults to `$REDIS_URL`,
then `redis://localhost:6379`. The URL may carry a password and a database index
(e.g. `redis://:secret@host:6379/2`).
```ts
function redisDriver(url?: string, options?: RedisDriverOptions): PubSubDriver & { close(): void };
```
- Exact topics use Redis `SUBSCRIBE`; wildcard patterns (`ns:*`, `*`) use
`PSUBSCRIBE`, whose glob semantics line up with this library's matching.
- Messages are JSON-stringified on publish and `JSON.parse`d on receipt; a payload
that isn't valid JSON is delivered as the raw string.
- `close()` tears down both the subscriber and publisher connections.
- Lost sockets reconnect with bounded exponential backoff and active subscriptions
are replayed. `maxPending` bounds unavailable-connection writes (default 1000);
`reconnectDelayMs` and `reconnectMaxDelayMs` tune recovery (100ms/5000ms).
### RESP codec (internal)
`redis.ts` uses a minimal RESP implementation exported from `resp.ts`
(`encodeCommand`, `parseReply`, `concat`, and the `RespValue` type). These are
implementation details of the Redis driver, not part of the public package entry.
## Usage
In-process (default):
```ts
import { createPubSub } from "@wrnexus/pubsub";
const bus = createPubSub();
const off = bus.subscribe("order:*", (msg, topic) => {
console.log(topic, msg);
});
await bus.publish("order:created", { id: 7 });
off(); // unsubscribe
```
Cross-process with Redis:
```ts
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";
const driver = redisDriver("redis://localhost:6379");
const bus = createPubSub(driver);
bus.subscribe("order:*", (msg, topic) => {
// received on any app process subscribed to this pattern
});
await bus.publish("order:created", { id: 7 });
// on shutdown (also closes the driver)
await bus.close();
```
## Requirements / Notes
- **Bun-only.** The Redis driver depends on `Bun.connect`; it throws
`redisDriver requires the Bun runtime (Bun.connect).` outside Bun. The default
in-memory driver has no runtime dependencies.
- The Redis driver reads `REDIS_URL` from the environment when no `url` is passed.
- Backs [`@wrnexus/core`](../core)'s realtime bridge for horizontal scaling.
- No external npm dependencies — the Redis client is a self-contained RESP codec.
-51
View File
@@ -1,51 +0,0 @@
{
"name": "@wrnexus/pubsub",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/pubsub"
},
"homepage": "https://wrnexusjs.dev/packages/pubsub",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"pubsub"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./brokers": {
"types": "./dist/brokers.d.ts",
"import": "./dist/brokers.js"
},
"./redis": {
"types": "./dist/redis.d.ts",
"import": "./dist/redis.js"
}
},
"files": [
"dist",
"README.md"
]
}
-211
View File
@@ -1,211 +0,0 @@
# @wrnexus/queue
> A background job queue with delays, retries + exponential backoff, recurring jobs, and concurrent workers.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/queue` is a server-side in-process job queue. You register named
workers, enqueue jobs (optionally delayed or recurring), and let the queue poll
and run them on a timer — with per-job retry limits and doubling backoff between
attempts. The default store lives in memory; the design allows a pluggable driver
to back it with Redis/SQL for durability across restarts. Reach for it when you
need to defer work (emails, webhooks, cleanup) off the request path without a
heavyweight external broker. Tests can drive it deterministically via `drain()`.
## Installation
```bash
bun add @wrnexus/queue
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
The package exports a single factory plus its supporting types.
### `createQueue(options?): Queue`
Creates a new queue instance.
```ts
function createQueue(options?: QueueOptions): Queue;
```
#### `QueueOptions`
| Option | Type | Default | Description |
| ------------- | ------------------------------------ | ---------- | -------------------------------------------------------- |
| `maxAttempts` | `number` | `3` | Default max attempts per job before it is dead-lettered. |
| `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. |
| `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). |
| `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. |
| `concurrency` | `number` | unlimited | Maximum jobs claimed by one `drain()` call. |
| `capacity` | `number` | unlimited | Maximum queued plus active jobs before adds reject. |
| `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. |
### `Queue`
The object returned by `createQueue`.
| Method | Signature | Description |
| ---------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
| `add` | `add<T>(name, data: T, options?: AddOptions): Promise<Job<T>>` | Enqueue a job under a worker name. Returns the created job. |
| `process` | `process<T>(name, handler: JobHandler<T>): void` | Register the worker that runs jobs of the given name. |
| `drain` | `drain(now?: number): Promise<number>` | Run every job whose `runAt ≤ now`, once. Returns how many ran. |
| `start` | `start(): void` | Begin polling every `pollMs`. No-op if already started. |
| `stop` | `stop(): void` | Stop the poll timer. |
| `shutdown` | `shutdown({ force? }): Promise<void>` | Stop accepting jobs and await active work; force aborts it. |
| `size` | `size(): number` | Number of jobs currently queued. |
| `get/list` | `get(id)` / `list(name?)` | Inspect defensive copies of pending jobs. |
| `cancel` | `cancel(id): boolean` | Remove queued work or abort an active handler. |
| `failed` | `failed(): Job[]` | Inspect exhausted jobs in the dead-letter collection. |
| `retry` | `retry(id): Promise<boolean>` | Reset and requeue a dead-lettered job. |
#### `AddOptions`
| Option | Type | Description |
| ---------------- | -------- | -------------------------------------------------------------------------- |
| `delayMs` | `number` | Delay before the job becomes runnable (ms). |
| `maxAttempts` | `number` | Max attempts before dead-lettering. Defaults to the queue's `maxAttempts`. |
| `repeat` | `number` | Re-enqueue this job this many ms after each successful run (recurring). |
| `priority` | `number` | Higher values are selected first among due jobs. |
| `idempotencyKey` | `string` | Return the matching pending job instead of enqueueing a duplicate. |
#### `JobHandler<T>`
```ts
type JobHandler<T = unknown> = (
job: Job<T>,
context: { signal: AbortSignal },
) => void | Promise<void>;
```
#### `Job<T>`
```ts
interface Job<T = unknown> {
id: string; // e.g. "job_1"
name: string;
data: T;
attempts: number;
maxAttempts: number;
runAt: number; // epoch ms; job runs when now ≥ runAt
repeat?: number; // if set, re-enqueue this many ms after each success
}
```
## Usage
Register workers, enqueue jobs, then start the poller:
```ts
import { createQueue } from "@wrnexus/queue";
const queue = createQueue({ maxAttempts: 3, backoffMs: 1000 });
// Register a worker for the "email" job name.
queue.process<{ to: string }>("email", async (job) => {
await send(job.data.to);
});
// Enqueue a delayed job with up to 3 attempts.
await queue.add("email", { to: "a@b.com" }, { delayMs: 5000, maxAttempts: 3 });
queue.start(); // begin polling; queue.stop() to halt
```
Use `context.signal` in network/database calls so forced shutdown and active
cancellation finish promptly. For process termination, prefer
`await queue.shutdown()`; use `{ force: true }` only after your grace period.
### Durable queue
`createDurableQueue({ store })` retains jobs until their handler succeeds and
supports atomic leases when a driver implements `QueueStore.claim`. It exposes
the same cancellation/shutdown behavior plus `list`, `failed`, and `retry`.
The included `memoryQueueStore()` is useful for tests; production Redis/SQL
drivers should make `claim()` atomic to prevent two workers executing one job.
### Recurring jobs
Pass `repeat` to re-enqueue a job a fixed interval after each successful run:
```ts
queue.process("heartbeat", async () => ping());
await queue.add("heartbeat", {}, { repeat: 60_000 }); // runs ~every minute
```
### Handling permanent failures
When a job's `attempts` reaches `maxAttempts`, it is dropped and `onFailed`
fires instead of retrying:
```ts
const queue = createQueue({
onFailed: (job, error) => {
console.error(`job ${job.id} (${job.name}) gave up`, error);
},
});
```
### Deterministic testing
Instead of `start()`, inject a clock and drive the queue with `drain()`:
```ts
let clock = 0;
const queue = createQueue({ now: () => clock });
queue.process("task", async () => {
/* ... */
});
await queue.add("task", {}, { delayMs: 5000 });
clock = 5000;
const ran = await queue.drain(); // => 1
```
### Durable workflows and approvals
`createWorkflowEngine(store)` executes dependency-ordered steps and persists every transition,
result, progress update, failure, cancellation, and approval record. Approval steps pause safely
and can resume after a process restart because the snapshot lives in the supplied `WorkflowStore`.
```ts
const workflow = defineDurableWorkflow({
name: "publish-report",
steps: [
{ name: "build", run: buildReport },
{ name: "approve", dependsOn: ["build"], approval: true, run: (report) => report },
{ name: "publish", dependsOn: ["approve"], run: publishReport },
],
});
const run = await engine.start(workflow, input);
await engine.approve(workflow, run.id, "approve", currentUser.id);
```
Use `memoryWorkflowStore()` for tests. Production stores implement the small `get`, `put`, and
`list` contract using the same transactional database or durable service as the application.
## Retry & backoff behavior
- On a thrown handler error, the job is retried while `attempts < maxAttempts`.
- The next `runAt` is set to `now + backoffMs * 2^(attempts - 1)` (exponential
backoff): with `backoffMs: 1000` the delays are 1s, 2s, 4s, …
- A job whose worker name has no registered handler stays queued until one is
registered (it is not counted as runnable by `drain`).
- `drain` is re-entrant-safe: overlapping calls are skipped while one is running.
## Requirements / Notes
- **Bun-only** runtime (Node is not supported), consistent with the rest of the
WrNexus framework. The queue itself relies only on standard timers
(`setInterval`/`clearInterval`) and has no runtime dependencies.
- The default store is in-process, so queued jobs do not survive a restart; a
pluggable driver is intended for backing it with Redis/SQL for durability.
- Works alongside `@wrnexus/core` for offloading work from the request path.
-46
View File
@@ -1,46 +0,0 @@
{
"name": "@wrnexus/queue",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/queue — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/queue"
},
"homepage": "https://wrnexusjs.dev/packages/queue",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"queue"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-99
View File
@@ -1,99 +0,0 @@
# @wrnexus/reactive
> Tiny, type-safe reactive primitives (signals) with zero dependencies.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/reactive` is the seed of WrNexus's reactivity layer: a minimal `signal`
primitive that holds a value, notifies subscribers when it changes, and hands back
an unsubscribe function. It is deliberately small and framework-agnostic — it powers
nothing on its own, but is shaped so client islands (and later the `.wrn` compiler's
`state` blocks) can build reactive bindings on top of it. Reach for it when you need
observable state without pulling in a full reactivity library.
## Installation
```bash
bun add @wrnexus/reactive
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
The package has a single entry point (`.`) exporting one function and three types.
### `signal<T>(initial: T): Signal<T>`
Creates a reactive signal seeded with `initial`. Returns a `Signal<T>`:
| Member | Signature | Description |
| ----------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `get` | `(): T` | Read the current value. |
| `set` | `(next: T): void` | Write a new value. Subscribers run **only when the value actually changes** (compared with `Object.is`). |
| `update` | `(fn: (current: T) => T): void` | Apply a function to the current value; equivalent to `set(fn(get()))`. |
| `subscribe` | `(fn: Subscriber<T>): Unsubscribe` | Register a subscriber; returns a function that removes it. |
### Types
```ts
type Subscriber<T> = (value: T) => void;
type Unsubscribe = () => void;
interface Signal<T> {
get(): T;
set(next: T): void;
update(fn: (current: T) => T): void;
subscribe(fn: Subscriber<T>): Unsubscribe;
}
```
Notes on semantics:
- **No-op updates are skipped.** `set` compares the incoming value to the current
one with `Object.is`; identical values do not notify subscribers.
- **Safe unsubscribe during notification.** Subscribers are iterated over a copy of
the subscriber set, so a subscriber may call its own (or another's) unsubscribe
while a notification is in flight.
## Usage
```ts
import { signal } from "@wrnexus/reactive";
const count = signal(0);
count.get(); // 0
// Subscribe; the returned function unsubscribes.
const off = count.subscribe((value) => {
console.log("count is now", value);
});
count.set(1); // logs: count is now 1
count.set(1); // no-op — value unchanged, no notification
count.update((n) => n + 1); // logs: count is now 2
off(); // stop listening
count.set(3); // nothing logged
```
Typed signals infer `T` from the initial value, or can be annotated explicitly:
```ts
import { signal, type Signal } from "@wrnexus/reactive";
const user: Signal<{ name: string } | null> = signal(null);
user.set({ name: "Ada" });
```
## Requirements / Notes
- **Bun-only.** Distributed as TypeScript source (`main`/`exports` point at
`src/index.ts`); consume it under Bun, which runs `.ts` directly.
- **Zero dependencies.** The only runtime API used is the standard `Object.is`.
- Foundational primitive for WrNexus client islands and the forthcoming `.wrn`
compiler `state` blocks.
-43
View File
@@ -1,43 +0,0 @@
{
"name": "@wrnexus/reactive",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/reactive"
},
"homepage": "https://wrnexusjs.dev/packages/reactive",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"reactive"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"README.md"
]
}
-174
View File
@@ -1,174 +0,0 @@
# @wrnexus/router
> File-based router that maps an `app/` directory onto route tables and matches request paths against them.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/router` scans an application's `app/` directory once at startup and builds route tables for pages, API endpoints, realtime channels, middleware, server-rendered `.wrn` components, layouts, and validation schemas. It also compiles URL patterns (`/users/[id]`) into RegExps and matches request paths against them. Request input is never turned into a file path, which makes the router immune to path traversal. This is a server-side package used by the WrNexus runtime to resolve incoming requests, plus a codegen helper for compile-time typed links.
## Installation
```bash
bun add @wrnexus/router
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## Directory conventions
The router maps files under `appDir` onto routes:
```
app/pages/index.tsx -> GET /
app/pages/about.tsx -> GET /about
app/pages/users/[id].tsx -> GET /users/:id
app/api/hello.ts -> /api/hello
app/realtime/chat.ts -> /realtime/chat
app/pages/*.wrn (api) -> embedded /api/* routes
app/pages/*.wrn (rt) -> embedded /realtime/* routes
app/middleware/*.ts -> global middleware (alphabetical)
app/components/*.wrn -> server-rendered components (by basename)
app/layouts/*.wrn -> named page layouts
app/schemas/*.ts -> validation schemas
```
Allowed route extensions are `.ts`, `.tsx`, and `.wrn`. Dotfiles and underscore-prefixed files are ignored. A trailing `index` segment is dropped from the route. `.wrn` pages may embed `api` and `realtime` blocks, which the router extracts and mounts under `/api/*` and `/realtime/*`.
## API
### `buildRouter(appDir, opts?): Router`
Scan an app directory and build all route tables.
```ts
function buildRouter(appDir: string, opts?: RouterOptions): Router;
interface RouterOptions {
/** Extra dirs scanned for `.wrn` components (e.g. `@wrnexus/ui`), before
* `app/components`, so an app component of the same name wins. */
componentDirs?: string[];
}
```
The returned `Router` exposes the built tables plus per-kind matchers:
```ts
interface Router {
pages: Route[];
api: Route[];
realtime: Route[];
/** Absolute paths of middleware modules, in execution order (alphabetical). */
middlewareFiles: string[];
/** Server-rendered `.wrn` components, mounted via `data-component`. */
components: ComponentRef[];
/** Named page layouts (`app/layouts/<name>.wrn`); a page picks one via `layout`. */
layouts: ComponentRef[];
/** Validation schemas (`app/schemas/<name>.ts`) shared by API + forms. */
schemas: ComponentRef[];
matchPage(pathname: string): RouteMatch | null;
matchApi(pathname: string): RouteMatch | null;
matchRealtime(pathname: string): RouteMatch | null;
}
interface ComponentRef {
/** Validated component name (matches a `data-component` attribute). */
name: string;
/** Absolute path to the component's `.wrn` module. */
file: string;
}
```
Component, layout, and schema names are validated with `isSafeIslandName` from `@wrnexus/core`; unsafe names are skipped with a warning. Realtime channel names are validated the same way.
### Route matching
| Export | Signature | Description |
| --------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `compileRoutePattern` | `(raw: string) => Pick<Route, "regex" \| "paramNames">` | Compile a `/users/[id]` pattern into a RegExp (with optional trailing slash) plus ordered param names. |
| `matchRoute` | `(routes: Route[], pathname: string) => RouteMatch \| null` | Return the first route whose regex matches; captured params are `decodeURIComponent`-decoded. |
| `sortRoutes` | `(routes: Route[]) => Route[]` | Order routes so static routes win over dynamic ones (fewer params first), then longer/more specific patterns first. |
```ts
interface Route {
raw: string; // e.g. "/users/[id]"
file: string; // absolute path to the handling module
regex: RegExp; // compiled matcher
paramNames: string[]; // ordered dynamic param names
}
interface RouteMatch {
route: Route;
params: Record<string, string>;
}
```
### Typed-routes codegen
```ts
function generateRoutesFile(pages: Route[]): string;
```
Emits the source for `app/routes.gen.ts`: a `Routes` map (each page path → its `[param]` types), a `RoutePath` union, and an `href()` builder that fills params and rejects unknown paths at compile time. Entries are de-duplicated and sorted by path.
### Re-exports
`Middleware` (the type from `@wrnexus/core`) is re-exported for callers that load middleware modules themselves.
## Usage
```ts
import { buildRouter } from "@wrnexus/router";
const router = buildRouter("./app", {
componentDirs: ["./node_modules/@wrnexus/ui/components"],
});
// Resolve an incoming request.
const match = router.matchPage("/users/42");
if (match) {
console.log(match.route.file); // absolute path to the page module
console.log(match.params); // { id: "42" }
}
const api = router.matchApi("/api/hello");
const rt = router.matchRealtime("/realtime/chat");
```
Generating the typed-routes file (as `wrnexus dev` does):
```ts
import { generateRoutesFile } from "@wrnexus/router";
import { writeFileSync } from "node:fs";
const router = buildRouter("./app");
writeFileSync("./app/routes.gen.ts", generateRoutesFile(router.pages));
```
```ts
// Then, in app code, links are checked at compile time:
import { href } from "./routes.gen.ts";
href("/users/[id]", { id: "42" }); // "/users/42"
href("/about"); // "/about"
href("/nope"); // type error: unknown path
```
Lower-level pattern matching, if you need it directly:
```ts
import { compileRoutePattern, matchRoute, sortRoutes, type Route } from "@wrnexus/router";
const { regex, paramNames } = compileRoutePattern("/posts/[slug]");
const routes = sortRoutes([{ raw: "/posts/[slug]", file: "…", regex, paramNames }]);
const m = matchRoute(routes, "/posts/hello"); // { route, params: { slug: "hello" } }
```
## Requirements / Notes
- Scanning uses `node:fs` (`existsSync`, `readdirSync`, `statSync`) and `node:path` — runs under Bun.
- Depends on [`@wrnexus/compiler`](../compiler) to `parse` `.wrn` pages and extract embedded `api` / `realtime` blocks.
- Depends on [`@wrnexus/core`](../core) for `isSafeIslandName` (name validation) and the `Middleware` type.
- Missing route directories are tolerated — a route kind you don't use simply yields an empty table.
-47
View File
@@ -1,47 +0,0 @@
{
"name": "@wrnexus/router",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/router"
},
"homepage": "https://wrnexusjs.dev/packages/router",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"router"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"dependencies": {
"@wrnexus/compiler": "^0.8.5",
"@wrnexus/core": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-124
View File
@@ -1,124 +0,0 @@
# @wrnexus/ssr
> Server-side rendering: wraps a page's HTML body in a complete HTML document with a metadata-driven `<head>`.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
Pages in WrNexus return an HTML string for the body. `@wrnexus/ssr` takes that body and produces a full HTML document — building the `<head>` from page metadata and global SEO defaults, resolving canonical/Open Graph/Twitter tags, and injecting module preloads and `<script type="module">` tags. It is deliberately server-only: nothing in this package touches the DOM or ships to the browser, keeping server code genuinely server-only. Reach for it on the server when turning a rendered page body into a response document.
## Installation
```bash
bun add @wrnexus/ssr
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
The package has a single export.
### `renderDocument(opts: RenderOptions): string`
Renders a complete HTML document as a string, beginning with `<!doctype html>`. All metadata is HTML-escaped (via `escapeHtml` from `@wrnexus/core`), so a malicious title or description cannot break out of its element or attribute. The body is placed inside `<div id="app">`.
#### `RenderOptions`
| Field | Type | Description |
| -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `meta` | `PageMeta` | Page metadata for the document head (required). |
| `body` | `string` | Rendered HTML for the body, placed inside `#app` (required). |
| `seo` | `SeoConfig` | Global SEO defaults, typically from `wrnexus.config.ts`. |
| `url` | `URL` | Current request URL, used to resolve canonical/Open Graph URLs. |
| `scripts` | `string[]` | URLs of `<script type="module">` tags to load (e.g. per-island chunks or the reactive runtime). Each also gets a `<link rel="modulepreload">`. |
| `defaultTitle` | `string` | Default document title used when `meta.title` is absent. |
| `extraHead` | `string` | Raw HTML injected at the end of `<head>` (trusted, framework-controlled — not escaped). |
| `extraBody` | `string` | Raw HTML injected at the end of `<body>` (trusted, framework-controlled — not escaped). |
| `htmlAttrs` | `string` | Attributes for the `<html>` element, e.g. ` data-theme="dark"` (trusted). |
`PageMeta` and `SeoConfig` come from `@wrnexus/core`. `PageMeta` is an alias of `SeoConfig`, whose fields are all optional:
```ts
type SeoConfig = {
title?: string;
titleTemplate?: string; // e.g. "%s — My Site"; %s is replaced with the page title
description?: string;
canonical?: string;
canonicalBase?: string; // origin used to absolutize canonical/image URLs
robots?: string;
keywords?: string | string[];
image?: string;
siteName?: string;
type?: string; // Open Graph type; defaults to "website"
locale?: string;
twitterCard?: string; // defaults to "summary"
twitterSite?: string;
themeColor?: string;
};
```
#### Metadata resolution
`renderDocument` merges page metadata (`meta`) over global defaults (`seo`), field by field, so per-page values win. Notable behavior:
- **Title**: uses `meta.title`, else `seo.title`, else `defaultTitle`, else `"WrNexus"`. When the page sets its own title and `seo.titleTemplate` contains `%s`, the template is applied.
- **Canonical / image URLs**: resolved against `canonicalBase` (or the request `url`'s origin) into absolute URLs when possible.
- **Keywords**: an array is joined with `", "`.
- **Emitted tags**: `<title>`, and as applicable `description`, `robots`, `keywords`, `theme-color`, and `canonical` link, plus Open Graph (`og:title`, `og:description`, `og:type`, `og:url`, `og:site_name`, `og:locale`, `og:image`) and Twitter (`twitter:card`, `twitter:title`, `twitter:description`, `twitter:image`, `twitter:site`) meta tags. The document always includes `charset`, `viewport`, and a `/favicon.ico` icon link.
## Usage
### Render an SEO-ready application page
```ts
import { renderDocument } from "@wrnexus/ssr";
const html = renderDocument({
meta: {
title: "About Us",
description: "Learn more about our team.",
},
seo: {
titleTemplate: "%s — Acme",
siteName: "Acme",
canonicalBase: "https://acme.example",
twitterSite: "@acme",
},
url: new URL("https://acme.example/about"),
body: "<h1>About Us</h1>",
scripts: ["/_wire/runtime.js", "/_wire/islands/about.js"],
htmlAttrs: ' data-theme="dark"',
});
return new Response(html, {
headers: { "content-type": "text/html; charset=utf-8" },
});
```
The produced document has `<title>About Us — Acme</title>`, the SEO/Open Graph/Twitter tags derived from the merged metadata, a `modulepreload` link and module `<script>` for each entry in `scripts`, and the body wrapped in `<div id="app">`.
### Add trusted framework assets and boot data
Use `extraHead` and `extraBody` only for HTML generated by your application or the
framework. User-provided values belong in `meta`, where they are escaped.
```ts
const html = renderDocument({
meta: { title: "Dashboard", robots: "noindex" },
body: dashboardHtml,
url: ctx.url,
extraHead: '<link rel="stylesheet" href="/_wrnexus/admin.css">',
extraBody: `<script type="application/json" id="boot">${JSON.stringify(bootData).replaceAll("<", "\\u003c")}</script>`,
});
return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
```
## Requirements / Notes
- **Server-only.** This module never imports or touches the DOM and is safe to keep out of client bundles.
- **Depends on [`@wrnexus/core`](../core)** for `escapeHtml` and the `PageMeta` / `SeoConfig` types.
- **Bun-only** — like the rest of WrNexus, this package targets the Bun runtime (Node is not supported).
-56
View File
@@ -1,56 +0,0 @@
{
"name": "@wrnexus/ssr",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/ssr"
},
"homepage": "https://wrnexusjs.dev/packages/ssr",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"ssr"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./rpc": {
"types": "./dist/rpc.d.ts",
"import": "./dist/rpc.js"
},
"./store-context": {
"types": "./dist/store-context.d.ts",
"import": "./dist/store-context.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/store": "^0.8.5",
"@wrnexus/security": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-273
View File
@@ -1,273 +0,0 @@
# @wrnexus/styles
## Reusable layers and presets
Compose local or package foundations in order; later layers override earlier
ones and the application has final base-config precedence:
```ts
export default defineConfig({
extends: ["@workroot/wrnexus-enterprise", "./layers/company"],
profiles: { production: { port: 8080 } },
});
```
A directory layer exports `wrnexus.layer.ts` (JavaScript/MJS are supported).
A package can provide that conventional file or declare
`wrnexus.layer` in its `package.json`. Layers may extend other layers and carry
the complete app configuration, including plugins that contribute layouts,
components, routes, middleware, and migrations. `plugins` and `head` compose;
other arrays intentionally replace earlier values. Cycles and missing/invalid
entries fail with stable `WRN-CONFIG-LAYER-*` diagnostics. `wrnexus config
--explain` lists every resolved layer source.
> Global CSS bundling, the `--wire-*` design-token theme system, and the `wrnexus.config.ts` app-config loader for WrNexus apps.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
This package owns three server-side concerns that shape every page a WrNexus app renders:
1. **Global stylesheet pipeline** — finds `app/styles/global.css` (or aggregates `app/styles/*.css`), bundles it with Bun's CSS bundler (which resolves `@import`, including from `node_modules`), and produces one stylesheet that is `<link>`ed into every page's `<head>`. Because it is a plain global sheet, it styles server-rendered markup and hydrated client islands identically. A custom `process` hook lets you swap in Tailwind / PostCSS / Sass.
2. **Theme system** — design tokens exposed as CSS custom properties (`--wire-<key>`), with built-in `light`/`dark` sets, deep-merged user overrides, an SSR `<html data-theme>` render (no flash), and a tiny client runtime to toggle/persist the choice.
3. **App config** — loads `wrnexus.config.ts` (the `AppConfig` type), applies named profile overrides, and loads the `.env` cascade.
It runs server-side / at build time. Reach for it when configuring an app, defining themes, or customising how global CSS is produced.
## Installation
```bash
bun add @wrnexus/styles
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
Everything is exported from the package root (`@wrnexus/styles`).
### Config loading
| Export | Signature | Purpose |
| ---------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `loadAppConfig` | `(appRoot: string, profile?: string) => Promise<AppConfig>` | Load `wrnexus.config.*` with the active profile deep-merged in (`profiles` stripped from the result). |
| `loadRawConfig` | `(appRoot: string) => Promise<AppConfig>` | Load the raw config with the `profiles` map intact; returns `{}` if no config file exists. |
| `resolveProfile` | `(options?: { explicit?; mode? }) => string` | Resolve the active profile: explicit arg > `WRNEXUS_PROFILE` env var > mode-based default (`production` in prod, else `development`). |
| `loadEnv` | `(appRoot: string, profile: string) => Record<string, string>` | Load the `.env` cascade for a profile into `process.env` without clobbering real env vars. Returns what it loaded. |
| `headToString` | `(head?: string \| string[]) => string` | Flatten `AppConfig.head` into a single HTML string. |
Config file names probed, in order: `wrnexus.config.ts`, `wrnexus.config.js`, `wrnexus.config.mjs`.
`.env` cascade precedence (low → high): `.env` < `.env.<profile>` < `.env.local` < `.env.<profile>.local`. Variables already present in the real environment always win.
### `AppConfig`
The type of the object your `wrnexus.config.ts` default-exports. Every field is optional.
| Field | Type | Description |
| ----------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `head` | `string \| string[]` | Raw HTML appended to every page's `<head>` (e.g. CDN stylesheet/script links). |
| `seo` | `SeoConfig` | Global SEO defaults, merged with each page's exported `meta`. (from `@wrnexus/core`) |
| `security` | `SecurityConfig` | Framework security headers and optional CORS policy. (from `@wrnexus/core`) |
| `styles` | `StylesConfig` | Global stylesheet pipeline config (see below). |
| `theme` | `ThemeConfig` | Design-token themes, deep-merged over the built-in light/dark. |
| `i18n` | `{ default?: string; locales?: string[] }` | Default language + supported locales (strings live in `app/locales/*.json`). |
| `db` | `{ driver: "sqlite" \| "postgres" \| "mysql" \| "mongo"; url: string }` | Default database connection; reached with `getDb()`. |
| `databases` | `Record<string, { driver; url }>` | Additional named databases, reached with `getDb("<name>")`; each has its own `app/db/<name>/` migrations/queries. |
| `realtime` | `{ scale?: boolean; redisUrl?: string }` | When `scale` is true (or `redisUrl` is set), room broadcasts bridge over Redis pub/sub so they reach clients on every app process. |
| `port` | `number` | Default server port. |
| `profiles` | `Record<string, Partial<Omit<AppConfig, "profiles">>>` | Named profiles (dev, prod, uat, test, …). The active profile's overrides are deep-merged over the base config. Selected via `--profile=<name>` or `WRNEXUS_PROFILE`. |
### Styles pipeline
| Export | Signature | Purpose |
| ---------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `findStyleEntry` | `(appDir, appRoot, override?) => string \| null` | Resolve the CSS entry: `override` (relative to `appRoot`) → `app/styles/global.css` → an aggregate of all `app/styles/*.css` (written to `app/.wrnexus/styles-entry.css`). `null` if the app has no styles. |
| `bundleCss` | `(entryPath: string, mode: Mode) => Promise<string>` | Bundle an entry with `Bun.build` (CSS bundler). Resolves `@import` (local + node_modules), handles nesting, minifies when `mode === "production"`. |
| `renderStyles` | `(ctx: StyleProcessContext, styles?: StylesConfig) => Promise<string>` | Produce final CSS: runs `styles.process(ctx)` if provided, else `bundleCss`. Returns `""` when `ctx.entryPath` is null. |
`StylesConfig`:
```ts
interface StylesConfig {
/** CSS entry path relative to the app root. Default: app/styles/global.css */
entry?: string;
/** Custom processor — return the final CSS string (Tailwind/PostCSS/Sass). */
process?: (ctx: StyleProcessContext) => string | Promise<string>;
}
interface StyleProcessContext {
entryPath: string | null; // resolved absolute CSS entry, or null
appDir: string;
appRoot: string;
mode: Mode; // "development" | "production"
}
```
### Theme system
| Export | Type / Signature | Purpose |
| -------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `DEFAULT_THEMES` | `Record<string, ThemeTokens>` | Built-in `light` and `dark` token maps. |
| `THEME_COOKIE` | `"wire-theme"` | Cookie the resolved theme is read from / persisted to. |
| `THEME_CSS_HREF` | `"/__wrnexus/theme.css"` | URL the generated theme stylesheet is served at. |
| `THEME_JS_HREF` | `"/__wrnexus/theme.js"` | URL the client theme runtime is served at. |
| `resolveThemeConfig` | `(config?: ThemeConfig) => ResolvedTheme` | Deep-merge the user's `theme` config over the defaults; pick the default theme (config's `default` if valid, else `dark`, else the first). |
| `resolveThemeName` | `(cookieValue: string \| undefined, theme: ResolvedTheme) => string` | Pick a valid theme name from a cookie, falling back to `theme.default`. |
| `renderThemeCss` | `(theme: ResolvedTheme) => string` | Generate the theme stylesheet: a `:root{…}` default plus one `[data-theme="<name>"]{…}` block per theme. |
| `renderThemeRuntime` | `(theme: ResolvedTheme) => string` | Generate the client runtime (see below). |
Tokens are emitted as `--wire-<key>` custom properties, **except** the reserved key `color-scheme`, which is emitted as the native `color-scheme` CSS property so form controls and scrollbars match the theme.
`ThemeConfig` / `ThemeTokens` / `ResolvedTheme`:
```ts
type ThemeTokens = Record<string, string>;
interface ThemeConfig {
palette?: ThemePaletteName | CustomThemePalette;
default?: string; // theme used when no cookie is present
themes?: Record<string, ThemeTokens>; // deep-merged over built-in light/dark
}
interface ResolvedTheme {
default: string;
names: string[];
themes: Record<string, ThemeTokens>;
}
```
Built-in palettes are `blue`, `indigo`, `violet`, `emerald`, `cyan`, `rose`,
`amber`, and `slate`. Each supplies primary, secondary, info, success, warning,
danger/error, hover, and contrast colors to every light/dark theme. Built-in
token keys also include surfaces, text, borders, radii, fonts, and shadows.
A custom palette is intentionally complete, so components never fall back to an
unrelated blue status or action color:
```ts
theme: {
palette: {
primary: "#7c3aed",
primaryHover: "#6d28d9",
primaryContrast: "#ffffff",
secondary: "#db2777",
secondaryHover: "#be185d",
secondaryContrast: "#ffffff",
info: "#2563eb",
success: "#059669",
warning: "#d97706",
danger: "#dc2626",
},
}
```
The client runtime (`renderThemeRuntime`) exposes `window.wireTheme` with `{ get, set, toggle, bind, themes }`, wires up any `[data-wire-theme-toggle]` and `[data-wire-theme-set]` elements on load, and persists the choice to the `wire-theme` cookie (`max-age` 1 year, `samesite=lax`). `toggle()` cycles through the configured theme names in order.
## Usage
### `wrnexus.config.ts`
```ts
import type { AppConfig } from "@wrnexus/styles";
export default {
head: [
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">',
],
port: 3000,
db: { driver: "sqlite", url: "app.db" },
theme: {
palette: "violet",
default: "dark",
themes: {
light: { "color-primary": "#7c3aed" }, // override one token; rest inherited
brand: {
// add a whole new theme
"color-scheme": "dark",
"color-bg": "#0a0a0a",
"color-primary": "#22d3ee",
},
},
},
styles: {
entry: "app/styles/main.css",
},
profiles: {
production: {
db: { driver: "postgres", url: process.env.DATABASE_URL! },
},
},
} satisfies AppConfig;
```
### Loading config + producing CSS
```ts
import {
loadAppConfig,
resolveProfile,
loadEnv,
findStyleEntry,
renderStyles,
} from "@wrnexus/styles";
const appRoot = process.cwd();
const mode = "production" as const;
const profile = resolveProfile({ mode });
loadEnv(appRoot, profile);
const config = await loadAppConfig(appRoot, profile);
const appDir = `${appRoot}/app`;
const entryPath = findStyleEntry(appDir, appRoot, config.styles?.entry);
const css = await renderStyles({ entryPath, appDir, appRoot, mode }, config.styles);
```
### Rendering the theme
```ts
import {
resolveThemeConfig,
resolveThemeName,
renderThemeCss,
renderThemeRuntime,
THEME_COOKIE,
} from "@wrnexus/styles";
const theme = resolveThemeConfig(config.theme);
// Server: pick the active theme from the request cookie (no flash).
const active = resolveThemeName(cookies[THEME_COOKIE], theme);
// → render <html data-theme={active}>
const themeCss = renderThemeCss(theme); // served at THEME_CSS_HREF
const themeJs = renderThemeRuntime(theme); // served at THEME_JS_HREF
```
In templates, consume tokens via the custom properties:
```css
.card {
background: var(--wire-color-surface);
color: var(--wire-color-text);
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius);
box-shadow: var(--wire-shadow-1);
}
```
```html
<button data-wire-theme-toggle>Toggle theme</button>
<button data-wire-theme-set="brand">Brand theme</button>
```
## Requirements / Notes
- **Bun-only.** `bundleCss` uses `Bun.build`'s CSS bundler for `@import` resolution, nesting, and minification. Node is not supported.
- Config and env loading use `node:fs` / `node:path` / `node:url` and read from `process.env`.
- Peer package: `@wrnexus/core` supplies the `SeoConfig` and `SecurityConfig` types referenced by `AppConfig`.
- The bundled global stylesheet, the theme stylesheet (`THEME_CSS_HREF`), and the theme runtime (`THEME_JS_HREF`) are wired into pages by the framework's server; this package only produces their contents.
-48
View File
@@ -1,48 +0,0 @@
{
"name": "@wrnexus/styles",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/styles"
},
"homepage": "https://wrnexusjs.dev/packages/styles",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"styles"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"dependencies": {
"@wrnexus/uploader": "^0.8.5",
"@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.8.5"
},
"files": [
"dist",
"README.md"
]
}
-7
View File
@@ -1,7 +0,0 @@
# @wrnexus/syntax
Canonical WRN lexer, parser, AST, language metadata, source positions, and stable
diagnostics. Framework tooling should import this package instead of implementing a
separate `.wrn` parser.
See `docs/WRN-LANGUAGE-SPEC-1.0.md` in the WRNexusJS repository.
-67
View File
@@ -1,67 +0,0 @@
{
"name": "@wrnexus/syntax",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/syntax — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/syntax"
},
"homepage": "https://wrnexusjs.dev/packages/syntax",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"syntax"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./parser": {
"types": "./dist/parser.d.ts",
"import": "./dist/parser.js"
},
"./tokenizer": {
"types": "./dist/tokenizer.d.ts",
"import": "./dist/tokenizer.js"
},
"./types": {
"types": "./dist/types.d.ts",
"import": "./dist/types.js"
},
"./diagnostics": {
"types": "./dist/diagnostics.d.ts",
"import": "./dist/diagnostics.js"
},
"./spec": {
"types": "./dist/spec.d.ts",
"import": "./dist/spec.js"
},
"./formatter": {
"types": "./dist/formatter.d.ts",
"import": "./dist/formatter.js"
}
},
"files": [
"dist",
"README.md"
]
}
-166
View File
@@ -1,166 +0,0 @@
# @wrnexus/test
> Testing utilities for WrNexus apps — component rendering, reactive-DOM mounting, route handler calls, and a full in-process app harness, plus a one-import re-export of `bun:test`.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/test` is the server-side test toolkit you reach for when writing tests
for a WrNexus app. It runs under `bun test` (invoked via `wrnexus test`) and gives
you a single import surface: the `bun:test` primitives (`test`, `expect`, `mock`,
…) re-exported alongside WrNexus-aware helpers that compile `.wrn` components,
hydrate server HTML in a DOM, invoke API route handlers, and boot the real app on
an ephemeral port for integration tests.
## Installation
```bash
bun add @wrnexus/test
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
### Re-exported test primitives
For one-import DX, the following are re-exported straight from `bun:test`:
`test`, `expect`, `describe`, `it`, `beforeEach`, `afterEach`, `beforeAll`,
`afterAll`, `mock`, `spyOn`.
`createContext` is also re-exported from `@wrnexus/core`.
### `renderComponent(source, props?)`
```ts
function renderComponent(source: string, props?: Record<string, unknown>): Promise<string>;
```
Compiles a `.wrn` component `source` string (via `@wrnexus/compiler`) and renders
it to an HTML string with the given `props`. Throws if the compiled module has no
`render` export.
### `mountHtml(html)`
```ts
function mountHtml(html: string): {
document: Document;
window: unknown;
querySelector: (sel: string) => Element | null;
querySelectorAll: (sel: string) => Element[];
};
```
Mounts server-rendered `html` in a `happy-dom` window with the reactive runtime
hydrated, so you can test `data-scope` / `data-text` / `data-for` / `data-show`
behaviour. Returns the window plus `document` and query helpers; assert on those.
> `happy-dom` is loaded lazily (via `require`), so importing this package never
> requires it unless you actually call `mountHtml`.
### `callRoute(handler, request)`
```ts
function callRoute(
handler: (ctx: Context) => Response | Promise<Response>,
request: Request,
): Promise<Response>;
```
Calls an API route `handler` with a `Context` built from a `Request` (using
`createContext`). Returns the handler's `Response`.
### `createHarness(projectRoot, options?)`
```ts
function createHarness(projectRoot: string, options?: HarnessOptions): Promise<Harness>;
interface HarnessOptions {
/** Config/env profile. Default "test". */
profile?: string;
}
interface Harness {
/** Base URL of the ephemeral test server. */
url: string;
/** Fetch a path on the app (relative to `url`). */
fetch(path: string, init?: RequestInit): Promise<Response>;
/** The scanned router (pages/api/realtime/components). */
router: unknown;
/** Stop the server. */
close(): void;
}
```
Boots the app at `projectRoot` on an ephemeral port (`port: 0`) for integration
tests covering pages, API routes, middleware, and the full request pipeline. Loads
env and app config for the given `profile` (default `"test"`) so it picks up your
test database/env. The server runs in `development` mode with HMR disabled.
Remember to `await app.close()` when done.
## Usage
The CLI supports focused suites by file or directory convention:
```bash
wrnexus test unit # *.unit.test.ts or test/unit/**
wrnexus test component # *.component.test.ts or test/component/**
wrnexus test api # *.api.test.ts or test/api/**
wrnexus test accessibility # *.a11y.test.ts / *.accessibility.test.ts
wrnexus test performance # *.performance.test.ts / *.benchmark.test.ts
wrnexus test browser # Playwright project when configured
wrnexus test visual # Playwright tests tagged @visual
```
Pass the application directory after the level, for example
`wrnexus test component examples/basic-app`. A focused command fails clearly when no matching
suite exists instead of silently running unrelated tests.
```ts
import { test, expect, renderComponent, mountHtml, createHarness } from "@wrnexus/test";
test("counter renders its label", async () => {
const html = await renderComponent(SRC, { start: 3, label: "Hits" });
expect(html).toContain("Hits");
});
test("reactive scope hydrates", () => {
const { querySelector } = mountHtml(serverHtml);
expect(querySelector("[data-text]")?.textContent).toBe("3");
});
test("home page responds", async () => {
const app = await createHarness("examples/basic-app");
const res = await app.fetch("/");
expect(res.status).toBe(200);
await app.close();
});
```
Calling an API route handler directly:
```ts
import { test, expect, callRoute } from "@wrnexus/test";
import { GET } from "../app/api/health.ts";
test("health endpoint", async () => {
const res = await callRoute(GET, new Request("http://test/api/health"));
expect(res.status).toBe(200);
});
```
## Requirements / Notes
- **Bun-only.** Runs under `bun test` (via `wrnexus test`); uses Bun's module
loading and the `bun:test` runtime.
- `mountHtml` requires **`happy-dom`** to be available in the workspace (loaded
lazily; it's a dev dependency, not a runtime dependency of this package).
- Works with the rest of the WrNexus toolchain:
[`@wrnexus/compiler`](../compiler) (compiles `.wrn` sources),
[`@wrnexus/core`](../core) (`Context` / `createContext`),
[`@wrnexus/csr`](../csr) (reactive runtime for `mountHtml`),
[`@wrnexus/dev-server`](../dev-server) (`startServer` behind `createHarness`),
and [`@wrnexus/styles`](../styles) (config/env/profile loading for the harness).
-43
View File
@@ -1,43 +0,0 @@
{
"name": "@wrnexus/test",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/test"
},
"homepage": "https://wrnexusjs.dev/packages/test",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"test"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"README.md"
]
}
-128
View File
@@ -1,128 +0,0 @@
# @wrnexus/tracking
> Error tracking for WrNexus apps: capture exceptions manually or via middleware and fan them out to pluggable sinks.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/tracking` is a small, server-side error-capture layer. You create a
tracker with one or more **sinks**, then feed it errors — either manually with
`tracker.capture(err, context)` or automatically by mounting `tracker.middleware()`
in your request pipeline. A `consoleSink` is included; forwarding to Sentry,
Datadog, or any other backend is just a matter of writing a tiny sink. Reach for
it when you want a single, sink-agnostic place to route application errors. Sinks
run best-effort — a throwing sink never breaks the request.
## Installation
```bash
bun add @wrnexus/tracking
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
### `createTracker(options?): Tracker`
Creates a tracker. `TrackerOptions`:
| Option | Type | Description |
| ------------ | ------------------------------------------- | --------------------------------------------------------------------------- |
| `sinks` | `ErrorSink[]` | Initial sinks to fan events out to. Defaults to `[]`. |
| `now` | `() => number` | Clock used for `event.timestamp` (epoch ms). Defaults to `Date.now`. |
| `beforeSend` | `(event: ErrorEvent) => ErrorEvent \| null` | Scrub/enrich an event before it reaches any sink. Return `null` to drop it. |
The returned `Tracker`:
| Member | Signature | Description |
| ------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `capture` | `(error: unknown, context?: Record<string, unknown>) => Promise<void>` | Normalizes any thrown value into an `Error`, builds an `ErrorEvent`, runs `beforeSend`, then dispatches to all sinks. Non-`Error` values are wrapped in an `Error` named `NonError`. |
| `addSink` | `(sink: ErrorSink) => void` | Registers an additional sink at runtime. |
| `middleware` | `() => Middleware` | Returns a WrNexus `Middleware` that captures any error thrown downstream, then re-throws it so the framework's error handler still produces the response. |
The middleware attaches this context to captured events:
```ts
{ method: ctx.req.method, path: ctx.url.pathname, requestId: ctx.locals.requestId }
```
### `consoleSink: ErrorSink`
A built-in sink that logs a compact one-line message via `console.error`, e.g.
`[error] TypeError: cannot read x {"userId":42}`.
### Types
```ts
interface ErrorEvent {
error: Error;
context: Record<string, unknown>; // request info, user id, tags…
timestamp: number; // epoch ms
}
interface ErrorSink {
name?: string;
capture(event: ErrorEvent): void | Promise<void>;
}
```
## Usage
Manual capture:
```ts
import { createTracker, consoleSink } from "@wrnexus/tracking";
const tracker = createTracker({ sinks: [consoleSink] });
try {
await doWork();
} catch (err) {
await tracker.capture(err, { userId: 42, op: "doWork" });
throw err;
}
```
As request middleware:
```ts
import { createTracker, consoleSink } from "@wrnexus/tracking";
const tracker = createTracker({ sinks: [consoleSink] });
app.use(tracker.middleware()); // captures + re-throws downstream errors
```
A custom sink with `beforeSend` scrubbing:
```ts
import { createTracker, type ErrorSink } from "@wrnexus/tracking";
const sentrySink: ErrorSink = {
name: "sentry",
async capture(event) {
await Sentry.captureException(event.error, { extra: event.context });
},
};
const tracker = createTracker({
sinks: [sentrySink],
beforeSend(event) {
delete event.context.password; // scrub secrets
return event; // return null to drop the event entirely
},
});
tracker.addSink(anotherSink); // add more sinks later
```
## Requirements / Notes
- Runs on **Bun** only (Node is not supported).
- Peer package: [`@wrnexus/core`](../core) — the `Context` and `Middleware` types
used by `tracker.middleware()` come from there.
- Sink dispatch is fire-and-forget-safe: all sinks run via `Promise.all`, and a
sink that throws is swallowed so it can never break the app.
-43
View File
@@ -1,43 +0,0 @@
{
"name": "@wrnexus/tracking",
"version": "0.8.5",
"type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/tracking"
},
"homepage": "https://wrnexusjs.dev/packages/tracking",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"tracking"
],
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.3.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"README.md"
]
}
-997
View File
@@ -1,997 +0,0 @@
# WRNexus UI component reference
This reference is generated from the 108 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
## Advanced-forms
### AdvancedSelect
Theme-aware, responsive advanced select component.
- Mount: `data-component="AdvancedSelect"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Advanced Select"`, `name: string = ""`, `value: string = ""`, `values: unknown[] = []`, `options: unknown[] = []`, `groups: unknown[] = []`, `placeholder: string = "Select an option"`, `placeholderIcon: string = ""`, `searchPlaceholder: string = "Search options…"`, `multiple: boolean = false`, `searchable: boolean = true`, `defaultOpen: boolean = false`, `clearable: boolean = true`, `allowEmpty: boolean = true`, `tags: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `helpText: string = ""`, `loading: boolean = false`, `loadingLabel: string = "Loading options…"`, `emptyLabel: string = "No options found"`, `selectedOptionsLabel: string = "Selected options"`, `clearLabel: string = "Clear selection"`, `createLabel: string = "Create"`, `loadMoreLabel: string = "Load more"`, `searchMode: string = "contains"`, `searchFields: string = "label,description"`, `minSearchLength: number = 0`, `searchResultLimit: number = 0`, `maxSelections: number = 0`, `showCounter: boolean = false`, `counterTemplate: string = "{selected} selected"`, `optionTemplate: string = "default"`, `selectedTemplate: string = "default"`, `closeOnSelect: boolean = true`, `scrollToSelected: boolean = true`, `fixed: boolean = false`, `placement: string = "bottom"`, `remote: boolean = false`, `remoteUrl: string = ""`, `remoteQueryParam: string = "q"`, `remoteDebounce: number = 250`, `remoteAutoLoad: boolean = true`, `infinite: boolean = false`, `hasMore: boolean = false`, `page: number = 1`, `class: string = ""`
- Slots: None
- Outputs: `search({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### ComboBox
Editable autocomplete combobox with local and remote suggestions.
- Mount: `data-component="ComboBox"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "ComboBox"`, `name: string = ""`, `value: string = ""`, `options: unknown[] = []`, `groups: unknown[] = []`, `placeholder: string = "Search or select an option"`, `searchPlaceholder: string = "Start typing…"`, `clearable: boolean = true`, `allowCustomValue: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `helpText: string = ""`, `loading: boolean = false`, `loadingLabel: string = "Loading suggestions…"`, `emptyLabel: string = "No matching options"`, `clearLabel: string = "Clear value"`, `toggleLabel: string = "Toggle suggestions"`, `searchMode: string = "contains"`, `searchFields: string = "label,description"`, `minSearchLength: number = 0`, `searchResultLimit: number = 0`, `optionTemplate: string = "default"`, `defaultOpen: boolean = false`, `closeOnSelect: boolean = true`, `fixed: boolean = false`, `placement: string = "bottom"`, `autocomplete: string = "off"`, `remote: boolean = false`, `remoteUrl: string = ""`, `remoteQueryParam: string = "q"`, `remoteDebounce: number = 250`, `remoteAutoLoad: boolean = true`, `infinite: boolean = false`, `hasMore: boolean = false`, `page: number = 1`, `loadMoreLabel: string = "Load more"`, `class: string = ""`
- Slots: None
- Outputs: `search({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### CopyMarkup
Theme-aware, responsive copy markup component.
- Mount: `data-component="CopyMarkup"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Copy Markup"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "text"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `copy({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `success({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### InputNumber
Theme-aware, responsive input number component.
- Mount: `data-component="InputNumber"`
- 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
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `increment({ value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `decrement({ value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### PinInput
Secure multi-cell PIN and verification-code input with regex and paste support.
- Mount: `data-component="PinInput"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Verification code"`, `name: string = "pin"`, `value: string = ""`, `length: number = 4`, `pattern: string = "[0-9]"`, `type: string = "text"`, `inputMode: string = "numeric"`, `placeholder: string = "○"`, `autocomplete: string = "one-time-code"`, `masked: boolean = false`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `autoFocus: boolean = false`, `autoSubmit: boolean = false`, `allowPaste: boolean = true`, `clearable: boolean = true`, `clearLabel: string = "Clear code"`, `separator: string = ""`, `groupSize: number = 0`, `helpText: string = ""`, `invalid: boolean = false`, `validationMessage: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `complete({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `paste({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### StrongPassword
Theme-aware, responsive strong password component.
- Mount: `data-component="StrongPassword"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Create a strong password"`, `autocomplete: string = "new-password"`, `minLength: number = 8`, `specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"`, `requireLowercase: boolean = true`, `requireUppercase: boolean = true`, `requireNumber: boolean = true`, `requireSpecialCharacter: boolean = true`, `showRequirements: boolean = true`, `presentation: string = "inline"`, `hintText: string = "Use a unique password you do not use elsewhere."`, `emptyLabel: string = "Enter a password"`, `weakLabel: string = "Weak"`, `fairLabel: string = "Fair"`, `goodLabel: string = "Good"`, `strongLabel: string = "Strong"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `change({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `strength({ value: string; score: number; maximumScore: number; percent: number; level: string })`
### ToggleCount
Theme-aware, responsive toggle count component.
- Mount: `data-component="ToggleCount"`
- Props: `size: string = "default"`, `color: string = "primary"`, `variant: string = "segmented"`, `class: string = ""`, `name: string = "billing-cycle"`, `value: string = "monthly"`, `firstValue: string = "monthly"`, `firstLabel: string = "Monthly"`, `secondValue: string = "annual"`, `secondLabel: string = "Annual"`, `ariaLabel: string = "Billing frequency"`, `items: unknown[] = []`, `currency: string = "$"`, `suffix: string = ""`, `firstValueKey: string = "monthly"`, `secondValueKey: string = "annual"`, `emptyValue: string = "—"`, `align: string = "end"`, `fullWidth: boolean = true`, `disabled: boolean = false`, `animate: boolean = true`, `animationDuration: number = 450`, `animationSteps: number = 18`
- Slots: None
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `toggle({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### TogglePassword
Accessible password field with optional show and hide controls.
- Mount: `data-component="TogglePassword"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Enter your password"`, `autocomplete: string = "current-password"`, `minlength: string = ""`, `maxlength: string = ""`, `pattern: string = "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}"`, `fields: unknown[] = []`, `visible: boolean = false`, `toggleable: boolean = true`, `toggleMode: string = "button"`, `checkboxLabel: string = "Show password"`, `showLabel: string = "Show password"`, `hideLabel: string = "Hide password"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `helpText: string = ""`, `validationMessage: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ name?: string; value: string | number | boolean | null | object; visible: boolean })`, `change({ name?: string; value: string | number | boolean | null | object; visible: boolean })`, `toggle({ visible: boolean })`
## Base
### Accordion
Theme-aware, responsive accordion component.
- Mount: `data-component="Accordion"`
- Props: `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`, `id: string = "accordion"`, `items: unknown[] = []`, `defaultOpen: unknown[] = []`, `multiple: boolean = false`, `alwaysOpen: boolean = false`, `disabled: boolean = false`, `indicator: string = "plus"`, `indicatorPosition: string = "start"`, `showIndicator: boolean = true`, `bordered: boolean = false`, `separated: boolean = false`, `flush: boolean = false`, `contentItalic: boolean = false`
- Slots: None
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Alert
Theme-aware, responsive alert component.
- Mount: `data-component="Alert"`
- Props: `size: string = "default"`, `color: string = "info"`, `variant: string = "soft"`, `class: string = ""`, `radius: string = "md"`, `shadow: string = "sm"`, `title: string = "Alert"`, `description: string = ""`, `items: unknown[] = []`, `actions: unknown[] = []`, `showIcon: boolean = false`, `icon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss alert"`, `role: string = "alert"`, `live: string = "polite"`, `linkLabel: string = ""`, `linkHref: string = ""`, `actionLabel: string = ""`, `actionHref: string = ""`, `compact: boolean = false`
- Slots: None
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Avatar
Theme-aware, responsive avatar component.
- Mount: `data-component="Avatar"`
- Props: `src: string = ""`, `alt: string = ""`, `initials: string = ""`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "circle"`, `status: string = ""`, `statusLabel: string = ""`, `statusPosition: string = "bottom"`, `badge: string = ""`, `badgeIcon: string = ""`, `badgeLabel: string = ""`, `tooltip: string = ""`, `name: string = ""`, `description: string = ""`, `loading: string = "lazy"`, `class: string = ""`
- Slots: None
- Outputs: `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`, `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### AvatarGroup
Theme-aware, responsive avatar group component.
- Mount: `data-component="AvatarGroup"`
- Props: `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "circle"`, `layout: string = "stack"`, `maxVisible: number = 4`, `columns: number = 3`, `borderColor: string = ""`, `showTooltips: boolean = true`, `overflowLabel: string = "Show remaining members"`, `class: string = ""`
- Slots: None
- Outputs: `overflow({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Badge
Theme-aware, responsive badge component.
- Mount: `data-component="Badge"`
- Props: `label: string = "Badge"`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "pill"`, `class: string = ""`, `icon: string = ""`, `iconPosition: string = "start"`, `dot: boolean = false`, `dotOnly: boolean = false`, `dotLabel: string = "Status"`, `animated: boolean = false`, `avatarSrc: string = ""`, `avatarAlt: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Remove badge"`, `truncate: boolean = false`, `maxWidth: string = "12rem"`, `anchorLabel: string = ""`, `anchorIcon: string = ""`, `placement: string = "inline"`, `anchorLabelText: string = "Badge anchor"`
- Slots: None
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Blockquote
Theme-aware, responsive blockquote component.
- Mount: `data-component="Blockquote"`
- Props: `quote: string = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed."`, `citation: string = ""`, `citationTitle: string = ""`, `citationUrl: string = ""`, `avatarSrc: string = ""`, `avatarAlt: string = ""`, `size: string = "md"`, `color: string = "primary"`, `align: string = "left"`, `variant: string = "default"`, `quoteMark: boolean = true`, `italic: boolean = true`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Button
Theme-aware, responsive button component.
- Mount: `data-component="Button"`
- Props: `label: string = "Button"`, `loadingLabel: string = "Loading…"`, `description: string = ""`, `as: string = ""`, `href: string = ""`, `target: string = ""`, `rel: string = ""`, `type: string = "button"`, `variant: string = "default"`, `color: string = "primary"`, `size: string = "default"`, `disabled: boolean = false`, `loading: boolean = false`, `pill: boolean = false`, `fullWidth: boolean = false`, `icon: string = ""`, `iconPosition: string = "start"`, `ariaLabel: string = ""`, `ariaPressed: string = ""`, `ariaExpanded: string = ""`, `ariaControls: string = ""`, `title: string = ""`, `autofocus: boolean = false`, `controlClass: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### ButtonGroup
Theme-aware, responsive button group component.
- Mount: `data-component="ButtonGroup"`
- Props: `items: unknown[] = []`, `value: string = ""`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `orientation: string = "horizontal"`, `responsive: boolean = false`, `attached: boolean = true`, `selectable: boolean = false`, `toolbar: boolean = false`, `disabled: boolean = false`, `ariaLabel: string = "Button group"`, `class: string = ""`
- Slots: `default`
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })`, `change({ value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })`
### Card
Group related content in a responsive themed surface with title, description, content, and supporting slots.
- Mount: `data-component="Card"`
- Props: `title: string = "Card title"`, `subtitle: string = ""`, `description: string = ""`, `header: string = ""`, `footer: string = ""`, `imageSrc: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `actionLabel: string = ""`, `actionHref: string = ""`, `headerActions: unknown[] = []`, `navigation: unknown[] = []`, `activeNav: string = ""`, `mobileNavigation: boolean = false`, `alertTitle: string = ""`, `alertDescription: string = ""`, `empty: boolean = false`, `emptyTitle: string = "No data to show"`, `emptyIcon: string = "icon-[lucide--inbox]"`, `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "vertical"`, `align: string = "left"`, `hover: string = "none"`, `scrollable: boolean = false`, `maxHeight: string = "18rem"`, `dismissible: boolean = false`, `ariaLabel: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `navigate({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### Carousel
Theme-aware, responsive carousel component.
- Mount: `data-component="Carousel"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = ""`, `description: string = ""`, `items: unknown[] = []`, `activeIndex: number = 0`, `slidesPerView: number = 1`, `gap: string = "0.75rem"`, `showPagination: boolean = false`, `isAutoPlay: boolean = false`, `autoplayInterval: number = 4000`, `isInfiniteLoop: boolean = false`, `isRTL: boolean = false`, `isCentered: boolean = false`, `isDraggable: boolean = false`, `isAutoHeight: boolean = false`, `isSnap: boolean = false`, `showCounter: boolean = false`, `thumbnails: string = "none"`, `ariaLabel: string = "Content carousel"`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `initialize({ index: number; count: number })`, `change({ index: number; previousIndex: number; item: string | number | boolean | null | object; reason: string | boolean })`, `previous({ index: number; previousIndex: number; item: string | number | boolean | null | object })`, `next({ index: number; previousIndex: number; item: string | number | boolean | null | object })`, `play({ index: number; interval: number })`, `pause({ index: number })`, `reachStart({ index: number })`, `reachEnd({ index: number })`, `dragStart({ index: number; x: null })`, `dragEnd({ index: number; distance: number })`
### ChatBubble
Theme-aware, responsive chat bubble component.
- Mount: `data-component="ChatBubble"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = ""`, `description: string = ""`, `items: unknown[] = []`, `oneSided: boolean = false`, `showAvatars: boolean = false`, `showMetadata: boolean = false`, `ariaLabel: string = "Conversation"`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `action({ action: boolean; item: string | number | boolean | null | object; index: number })`, `messageClick({ item: string | number | boolean | null | object; index: number; direction: string })`, `avatarClick({ item: string | number | boolean | null | object; index: number; direction: string })`, `linkClick({ link: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })`
### Collapse
Theme-aware, responsive collapse component.
- Mount: `data-component="Collapse"`
- Props: `size: string = "default"`, `color: string = "primary"`, `items: unknown[] = []`, `multiple: boolean = false`, `mode: string = "panel"`, `initialOpenIndexes: unknown[] = []`, `ariaLabel: string = "Collapsible content"`, `class: string = ""`
- Slots: `default`
- Outputs: `toggle({ index: number; item: string | number | boolean | null | object; open: boolean; openIndexes: number[] })`, `open({ index: number; item: string | number | boolean | null | object })`, `close({ index: number; item: string | number | boolean | null | object })`
### DatePicker
Theme-aware, responsive date picker component.
- Mount: `data-component="DatePicker"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Date Picker"`, `id: string = ""`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "date"`, `locale: string = "en-US"`, `firstDayOfWeek: number = 0`, `months: string = [{ label: "Jan", value: "01" }, { label: "Feb", value: "02" }, { label: "Mar", value: "03" }, { label: "Apr", value: "04" }, { label: "May", value: "05" }, { label: "Jun", value: "06" }, { label: "Jul", value: "07" }, { label: "Aug", value: "08" }, { label: "Sep", value: "09" }, { label: "Oct", value: "10" }, { label: "Nov", value: "11" }, { label: "Dec", value: "12" }]`, `days: string = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]`, `years: string = [2024, 2025, 2026, 2027, 2028, 2029, 2030]`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `variant: string = "normal"`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `change({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `open({ value: string; name: string; sourceEvent: Event })`, `close({ value: string; name: string; sourceEvent: Event })`, `focus({ value: string; name: string; sourceEvent: Event })`, `blur({ value: string; name: string; sourceEvent: Event })`, `invalid({ name: string; message: string; sourceEvent: Event })`
### DeviceFrame
Theme-aware, responsive device frame component.
- Mount: `data-component="DeviceFrame"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Device Frame"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `device: string = "phone"`, `orientation: string = "portrait"`, `src: string = ""`, `srcdoc: string = ""`, `frameTitle: string = "Device preview"`, `showToolbar: boolean = true`, `allow: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ device: string; orientation: string; sourceEvent: Event })`, `rotate({ device: string; orientation: string; sourceEvent: Event })`
### FileUploadProgress
Theme-aware, responsive file upload progress component.
- Mount: `data-component="FileUploadProgress"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Progress"`, `value: number = 50`, `max: number = 100`, `showValue: boolean = true`, `fileName: string = ""`, `fileSize: string = ""`, `uploadedSize: string = ""`, `status: string = "uploading"`, `cancelLabel: string = "Cancel upload"`, `retryLabel: string = "Retry upload"`, `class: string = ""`
- Slots: None
- Outputs: `cancel({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `retry({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `complete({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### LegendIndicator
Theme-aware, responsive legend indicator component.
- Mount: `data-component="LegendIndicator"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Legend Indicator"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `toggle({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### List
Present structured responsive linked or status items with icons, descriptions, actions, and selection events.
- Mount: `data-component="List"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### ListGroup
Theme-aware, responsive list group component.
- Mount: `data-component="ListGroup"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List Group"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Marquee
Continuously present responsive labels, partners, notices, or capabilities with pause and resume behavior.
- Mount: `data-component="Marquee"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Marquee"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Progress
Theme-aware, responsive progress component.
- Mount: `data-component="Progress"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Progress"`, `value: number = 50`, `max: number = 100`, `showValue: boolean = true`, `class: string = ""`
- Slots: None
- Outputs: None
### Rating
Theme-aware, responsive rating component.
- Mount: `data-component="Rating"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Rating"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Skeleton
Theme-aware, responsive skeleton component.
- Mount: `data-component="Skeleton"`
- Props: `color: string = "primary"`, `label: string = "Loading"`, `size: string = "md"`, `lines: number = 3`, `class: string = ""`
- Slots: None
- Outputs: None
### Spinner
Theme-aware, responsive spinner component.
- Mount: `data-component="Spinner"`
- Props: `color: string = "primary"`, `label: string = "Loading"`, `size: string = "md"`, `lines: number = 3`, `class: string = ""`
- Slots: None
- Outputs: None
### StyledIcon
Theme-aware, responsive styled icon component.
- Mount: `data-component="StyledIcon"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Styled Icon"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Timeline
Present responsive chronological activity, milestones, or workflow status with rich item metadata.
- Mount: `data-component="Timeline"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Timeline"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Toast
Theme-aware, responsive toast component.
- Mount: `data-component="Toast"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Toast"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### TreeView
Theme-aware, responsive tree view component.
- Mount: `data-component="TreeView"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Tree View"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `toggle({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `expand({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `collapse({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
## Core
### AuthForm
Reusable auth form component.
- Mount: `data-component="AuthForm"`
- Props: `size: string = "default"`, `color: string = "primary"`, `mode: string = "sign-in"`, `action: string = "/api/auth/login"`, `method: string = "post"`, `title: string = "Sign in"`, `description: string = ""`, `returnTo: string = ""`, `schema: string = ""`, `showRemember: boolean = true`, `showName: boolean = true`, `submitLabel: string = "Continue"`, `class: string = ""`
- Slots: `default`
- Outputs: `submit({ event: Event; mode: string; action: string })`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### AuthSplitLayout
Reusable auth split layout component.
- Mount: `data-component="AuthSplitLayout"`
- Props: `size: string = "default"`, `color: string = "primary"`, `eyebrow: string = "Secure identity"`, `title: string = "Welcome back"`, `description: string = ""`, `brand: string = "Police Management System"`, `features: unknown[] = []`, `class: string = ""`
- Slots: `aside-extra`, `form`
- Outputs: None
### PortalDashboard
Reusable portal dashboard component.
- Mount: `data-component="PortalDashboard"`
- Props: `size: string = "default"`, `color: string = "primary"`, `eyebrow: string = "Overview"`, `eyebrowKey: string = ""`, `title: string = "Dashboard"`, `titleKey: string = ""`, `description: string = ""`, `descriptionKey: string = ""`, `userName: string = ""`, `metrics: unknown[] = []`, `actions: unknown[] = []`, `updates: unknown[] = []`, `tasks: unknown[] = []`, `class: string = ""`
- Slots: `hero-action`
- Outputs: `action({ item: string | number | boolean | null | object; value: string | number | boolean; index: number })`, `navigate({ item: string | number | boolean | null | object; value: string | number | boolean; index: number })`
### PreferenceSwitcher
Reusable preference switcher component.
- Mount: `data-component="PreferenceSwitcher"`
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `class: string = ""`
- Slots: None
- Outputs: `theme({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `color({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `language({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Toaster
Reusable toaster component.
- Mount: `data-component="Toaster"`
- Props: `color: string = "info"`, `size: string = "default"`, `position: string = "bottom-right"`, `duration: number = 4500`, `max: number = 4`, `pauseOnHover: boolean = true`, `showIcon: boolean = true`, `successIcon: string = ""`, `dangerIcon: string = ""`, `warningIcon: string = ""`, `infoIcon: string = ""`, `closable: boolean = true`, `showProgress: boolean = true`, `closeLabel: string = "Dismiss notification"`, `class: string = ""`
- Slots: None
- Outputs: `show({ id: number; message: string; tone: string })`, `dismiss({ id: number; reason: string })`, `action({ id: number; sourceEvent: Event })`
## Data
### MetricCard
Display one operational metric with value, suffix, description, icon, trend, progress, and optional action.
- Mount: `data-component="MetricCard"`
- Props: `label: string = "Metric"`, `value: string = "0"`, `description: string = ""`, `icon: string = ""`, `iconStyle: string = "soft"`, `prefix: string = ""`, `suffix: string = ""`, `badge: string = ""`, `trend: string = ""`, `trendLabel: string = ""`, `trendDirection: string = "neutral"`, `progress: number = -1`, `progressLabel: string = ""`, `href: string = ""`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `actionLabel: string = ""`, `actionIcon: string = ""`, `showArrow: boolean = true`, `selectable: boolean = false`, `disabled: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `hover: string = "lift"`, `align: string = "left"`, `class: string = ""`
- Slots: `default`, `footer`
- Outputs: `select({ label: string; value: string; href: string; sourceEvent: Event })`, `action({ label: string; value: string; href: string; sourceEvent: Event })`
### MetricGrid
Arrange operational metrics, KPIs, public statistics, or service indicators in a responsive equal-height grid.
- Mount: `data-component="MetricGrid"`
- Props: `items: unknown[] = []`, `columns: number = 4`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `equalHeight: boolean = true`, `dividers: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "full"`, `minItemWidth: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ item: string | number | boolean | null | object; itemIndex: number; sourceEvent: Event })`, `action({ item: string | number | boolean | null | object; itemIndex: number; sourceEvent: Event })`
### StatsBar
Present a compact responsive strip of key facts, counts, performance indicators, or trust signals.
- Mount: `data-component="StatsBar"`
- Props: `items: unknown[] = []`, `columns: number = 4`, `compact: boolean = true`, `dividers: boolean = true`, `icons: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: None
- Outputs: None
## Forms
### Checkbox
Theme-aware, responsive checkbox component.
- Mount: `data-component="Checkbox"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Checkbox"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: string = "on"`, `values: unknown[] = []`, `options: unknown[] = []`, `checked: boolean = false`, `indeterminate: boolean = false`, `orientation: string = "vertical"`, `card: boolean = false`, `rightAligned: boolean = false`, `list: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `invalid({ message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### ColorPicker
Theme-aware, responsive color picker component.
- Mount: `data-component="ColorPicker"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Color"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `value: string = "#2563eb"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `variant: string = "normal"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### FileInput
Theme-aware, responsive file input component.
- Mount: `data-component="FileInput"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "File"`, `hiddenLabel: boolean = false`, `placeholder: string = "Choose a file"`, `value: string = ""`, `icon: string = "icon-[lucide--upload]"`, `iconPosition: string = "start"`, `accept: string = ""`, `multiple: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `variant: string = "normal"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ files: File[]; name: string; sourceEvent: Event })`, `change({ files: File[]; name: string; sourceEvent: Event })`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ files: File[]; name: string; sourceEvent: Event })`, `clear({ name: string; sourceEvent: Event })`, `invalid({ message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Input
Theme-aware, responsive input component.
- Mount: `data-component="Input"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Input"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `value: string = ""`, `type: string = "text"`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `autocomplete: string = ""`, `inputmode: string = ""`, `minlength: string = ""`, `maxlength: string = ""`, `pattern: string = ""`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `change({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `focus({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `blur({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `invalid({ value: string | number | boolean | null | object; name: string; message: string; sourceEvent: Event })`, `keydown({ key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })`, `keyup({ key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })`
### InputGroup
Theme-aware, responsive input group component.
- Mount: `data-component="InputGroup"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Input group"`, `hiddenLabel: boolean = false`, `value: string = ""`, `placeholder: string = ""`, `type: string = "text"`, `startText: string = ""`, `endText: string = ""`, `icon: string = ""`, `iconPosition: string = "start"`, `actionLabel: string = ""`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `variant: string = "normal"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `submit({ value: string | number | boolean | null | object; name: string; sourceEvent: Event })`, `action({ name: string; sourceEvent: Event })`
### Radio
Theme-aware, responsive radio component.
- Mount: `data-component="Radio"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Radio"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: string = "on"`, `options: unknown[] = []`, `checked: boolean = false`, `orientation: string = "vertical"`, `card: boolean = false`, `rightAligned: boolean = false`, `list: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `invalid({ message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### RangeSlider
Theme-aware, responsive range slider component.
- Mount: `data-component="RangeSlider"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Range"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: number = 50`, `min: number = 0`, `max: number = 100`, `step: number = 1`, `showValue: boolean = true`, `showBounds: boolean = true`, `showSteps: boolean = false`, `marks: unknown[] = []`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input(({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))`, `change(({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### SearchBox
Provide an accessible responsive search field with labels, validation states, sizes, and input or change events.
- Mount: `data-component="SearchBox"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Search Box"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "search"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: None
### Select
Theme-aware, responsive select component.
- Mount: `data-component="Select"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Select"`, `hiddenLabel: boolean = false`, `value: string = ""`, `values: unknown[] = []`, `options: unknown[] = []`, `placeholder: string = "Select an option"`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `multiple: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `open({ name: string; sourceEvent: Event })`, `close({ name: string; sourceEvent: Event })`, `invalid({ name: string; message: string; sourceEvent: Event })`
### Switch
Theme-aware, responsive switch component.
- Mount: `data-component="Switch"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Switch"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `value: string = "on"`, `checked: boolean = false`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Textarea
Theme-aware, responsive textarea component.
- Mount: `data-component="Textarea"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Textarea"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `value: string = ""`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `rows: number = 5`, `resize: string = "vertical"`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `minlength: string = ""`, `maxlength: string = ""`, `class: string = ""`
- Slots: None
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `invalid({ value: string | number | boolean | null | object; name: string; message: string; sourceEvent: Event })`
### TimePicker
Theme-aware, responsive time picker component.
- Mount: `data-component="TimePicker"`
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Time"`, `hiddenLabel: boolean = false`, `value: string = ""`, `placeholder: string = ""`, `variant: string = "normal"`, `icon: string = "icon-[lucide--clock-3]"`, `iconPosition: string = "end"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `format: string = "24"`, `minuteStep: number = 5`, `hours: string = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"]`, `minutes: string = ["00", "05", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55"]`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `input({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `change({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `focus({ value: string; name: string; sourceEvent: Event })`, `blur({ value: string; name: string; sourceEvent: Event })`, `open({ value: string; name: string; sourceEvent: Event })`, `close({ value: string; name: string; sourceEvent: Event })`, `invalid({ name: string; message: string; sourceEvent: Event })`
## Integrations
### AdvancedDatePicker
Theme-aware, responsive advanced date picker component.
- Mount: `data-component="AdvancedDatePicker"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Advanced Date Picker"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### AdvancedRangeSlider
Theme-aware, responsive advanced range slider component.
- Mount: `data-component="AdvancedRangeSlider"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Advanced Range Slider"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `start({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `end({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Chart
Theme-aware, responsive chart component.
- Mount: `data-component="Chart"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Chart"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `dataPointClick({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `legendToggle({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Clipboard
Theme-aware, responsive clipboard component.
- Mount: `data-component="Clipboard"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Clipboard"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `copy({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `success({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
### Confetti
Theme-aware, responsive confetti component.
- Mount: `data-component="Confetti"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Confetti"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `start({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `complete({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### DataMap
Theme-aware, responsive data map component.
- Mount: `data-component="DataMap"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Data Map"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### DragAndDrop
Theme-aware, responsive drag and drop component.
- Mount: `data-component="DragAndDrop"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Drag And Drop"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `dragStart({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dragEnd({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dragEnter({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dragLeave({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `drop({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### FileUpload
Theme-aware, responsive file upload component.
- Mount: `data-component="FileUpload"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "File Upload"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `upload({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `progress({ progress: number; [key: string]: string | number | boolean | null | object } | number)`, `success({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`, `cancel({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `remove({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Map
Present responsive location information and markers with map-ready metadata and movement or marker events.
- Mount: `data-component="Map"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Map"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### ToastNotifications
Theme-aware, responsive toast notifications component.
- Mount: `data-component="ToastNotifications"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Toast Notifications"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `add({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `clear({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### WysiwygEditor
Theme-aware, responsive wysiwyg editor component.
- Mount: `data-component="WysiwygEditor"`
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Wysiwyg Editor"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
- Slots: `default`
- Outputs: `input({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
## Layout
### Columns
Create responsive balanced content columns with configurable count, gap, density, and maximum width.
- Mount: `data-component="Columns"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Container
Constrain and align page content with responsive gutters and compact, wide, or full width options.
- Mount: `data-component="Container"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### CustomScrollbar
Theme-aware, responsive custom scrollbar component.
- Mount: `data-component="CustomScrollbar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: `scroll({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Divider
Separate related horizontal or vertical content with optional labels, sizes, and semantic colors.
- Mount: `data-component="Divider"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: None
- Outputs: None
### FeatureGrid
Arrange feature cards, services, solutions, or benefits in a responsive equal-height grid.
- Mount: `data-component="FeatureGrid"`
- Props: `size: string = "default"`, `color: string = "primary"`, `items: unknown[] = []`, `columns: number = 3`, `tabletColumns: number = 2`, `mobileColumns: number = 1`, `gap: string = "md"`, `minItemWidth: string = ""`, `equalHeight: boolean = true`, `align: string = "stretch"`, `maxWidth: string = "full"`, `variant: string = "default"`, `label: string = "Features"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Footer
Render structured responsive footer navigation, pre and post content, copyright content, links, and public events.
- Mount: `data-component="Footer"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: unknown[] = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `copyright: string = ""`, `class: string = ""`
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Grid
Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width.
- Mount: `data-component="Grid"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Image
Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment.
- Mount: `data-component="Image"`
- Props: `size: string = "default"`, `color: string = "primary"`, `src: string = ""`, `alt: string = ""`, `width: string = ""`, `height: string = ""`, `loading: string = "lazy"`, `rounded: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
### Kbd
Theme-aware, responsive kbd component.
- Mount: `data-component="Kbd"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "⌘ K"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### LayoutSplitter
Theme-aware, responsive layout splitter component.
- Mount: `data-component="LayoutSplitter"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: `resizeStart({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `resize({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `resizeEnd({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Link
Render an accessible internal or external link with target, relation, size, color, and public focus or click events.
- Mount: `data-component="Link"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Link"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
### PublicPageShell
Provide the outer responsive structure, width, background, slots, overflow, and minimum-height behavior for public pages.
- Mount: `data-component="PublicPageShell"`
- Props: `maxWidth: string = "full"`, `fullWidth: boolean = true`, `headerOffset: string = "none"`, `background: string = "default"`, `overflow: string = "clip"`, `minHeight: string = "screen"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `class: string = ""`
- Slots: `before`, `default`, `after`
- Outputs: None
### Section
Create a responsive themed page section with controlled spacing, width, borders, and surface treatment.
- Mount: `data-component="Section"`
- Props: `id: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `spacing: string = "lg"`, `maxWidth: string = "xl"`, `fullWidth: boolean = false`, `borderTop: boolean = false`, `borderBottom: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
### SectionHeader
Introduce a section with an eyebrow, title, description, alignment, and responsive heading hierarchy.
- Mount: `data-component="SectionHeader"`
- Props: `id: string = ""`, `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `headingLevel: number = 2`, `maxWidth: string = "3xl"`, `class: string = ""`
- Slots: `icon`, `default`, `actions`
- Outputs: None
### Typography
Apply consistent readable responsive typography, widths, columns, spacing, and editorial hierarchy.
- Mount: `data-component="Typography"`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `default`
- Outputs: None
## Marketing
### AnnouncementBar
Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls.
- Mount: `data-component="AnnouncementBar"`
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `width: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
- Slots: None
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### CTASection
Close a page or major section with conversion-focused copy, actions, and optional supporting visual content.
- Mount: `data-component="CTASection"`
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualPosition: string = "right"`, `maxWidth: string = "xl"`, `fullBleed: boolean = false`, `class: string = ""`
- Slots: `default`, `actions`, `visual`, `footer`
- Outputs: None
### FeatureCard
Present one linked feature or service with media, icon, badge, description, and action.
- Mount: `data-component="FeatureCard"`
- Props: `icon: string = ""`, `iconStyle: string = "soft"`, `iconSize: string = "default"`, `eyebrow: string = ""`, `title: string = "Feature"`, `description: string = ""`, `image: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `imageAspect: string = "wide"`, `imageLoading: string = "lazy"`, `href: string = ""`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `actionLabel: string = "Learn more"`, `actionIcon: string = ""`, `showArrow: boolean = true`, `stretchedLink: boolean = true`, `badge: string = ""`, `badgeColor: string = "primary"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `hover: string = "lift"`, `align: string = "left"`, `disabled: boolean = false`, `class: string = ""`
- Slots: `media`, `icon`, `default`, `footer`
- Outputs: None
### FeatureIconCard
Present a compact feature or benefit with a styled icon, title, description, badge, and optional link.
- Mount: `data-component="FeatureIconCard"`
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `class: string = ""`
- Slots: `default`, `footer`
- Outputs: None
### Hero
Build a full-width responsive hero with constrained content, actions, trust signals, and a structured or custom visual panel.
- Mount: `data-component="Hero"`
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = ""`, `icon: string = ""`, `title: string = "Build something remarkable"`, `highlight: string = ""`, `description: string = ""`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "split"`, `visualPosition: string = "right"`, `visualStyle: string = "plain"`, `showDecorations: boolean = true`, `fullBleed: boolean = true`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualIcon: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualItems: unknown[] = []`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `primaryTarget: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `secondaryTarget: string = ""`, `tertiaryLabel: string = ""`, `tertiaryHref: string = ""`, `tertiaryIcon: string = ""`, `tertiaryTarget: string = ""`, `badges: unknown[] = []`, `trustItems: unknown[] = []`, `maxWidth: string = "xl"`, `class: string = ""`
- Slots: `eyebrow`, `actions`, `trust`, `default`, `visual`, `footer`
- Outputs: None
### HeroActions
Group hero and campaign actions with consistent alignment, orientation, sizing, and responsive mobile stacking.
- Mount: `data-component="HeroActions"`
- Props: `actions: unknown[] = []`, `align: string = "left"`, `orientation: string = "horizontal"`, `stackOnMobile: boolean = true`, `fullWidthMobile: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `class: string = ""`
- Slots: `default`
- Outputs: None
### MarketingSectionHeader
Introduce marketing content with an eyebrow, title, description, and optional linked action.
- Mount: `data-component="MarketingSectionHeader"`
- Props: `id: string = ""`, `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `align: string = "split"`, `size: string = "default"`, `color: string = "primary"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `actionExternal: boolean = false`, `class: string = ""`
- Slots: `icon`, `actions`, `default`
- Outputs: None
### PageHeader
Introduce an internal or public page with breadcrumbs, icon, title, description, and primary or secondary actions.
- Mount: `data-component="PageHeader"`
- Props: `eyebrow: string = ""`, `title: string = ""`, `description: string = ""`, `icon: string = ""`, `id: string = "page-title"`, `align: string = "left"`, `centered: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `maxWidth: string = "xl"`, `showBreadcrumbs: boolean = false`, `breadcrumbs: unknown[] = []`, `breadcrumbParent: string = ""`, `breadcrumbParentHref: string = ""`, `breadcrumbCurrent: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `color: string = "primary"`, `variant: string = "default"`, `borderBottom: boolean = true`, `class: string = ""`
- Slots: `meta`, `actions`, `default`
- Outputs: None
### SplitHero
Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel.
- Mount: `data-component="SplitHero"`
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = "icon-[lucide--sparkles]"`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "xl"`, `fullBleed: boolean = true`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualStyle: string = "panel"`, `trustItems: unknown[] = []`, `class: string = ""`
- Slots: `default`, `actions`, `trust`, `visual`
- Outputs: None
### TextLink
Render an accessible text action with optional icon, arrow, underline, external state, and semantic styling.
- Mount: `data-component="TextLink"`
- Props: `label: string = "Learn more"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `external: boolean = false`, `icon: string = ""`, `iconPosition: string = "start"`, `showArrow: boolean = true`, `underline: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `disabled: boolean = false`, `class: string = ""`
- Slots: None
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `focus({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `blur({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
## Navigation
### BackToTop
Provide a responsive floating control that returns long pages to the top and can show scroll progress.
- Mount: `data-component="BackToTop"`
- Props: `threshold: number = 500`, `label: string = "Back to top"`, `ariaLabel: string = "Scroll back to top"`, `icon: string = "icon-[lucide--arrow-up]"`, `position: string = "right"`, `offset: string = "md"`, `behavior: string = "smooth"`, `showProgress: boolean = false`, `showLabel: boolean = false`, `alwaysVisible: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `shape: string = "round"`, `class: string = ""`
- Slots: None
- Outputs: None
### Breadcrumb
Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events.
- Mount: `data-component="Breadcrumb"`
- Props: `label: string = "Breadcrumb"`, `items: unknown[] = []`, `active: string = ""`, `separator: string = "chevron"`, `showHome: boolean = false`, `homeLabel: string = "Home"`, `homeHref: string = "/"`, `homeIcon: string = "icon-[lucide--house]"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "minimal"`, `class: string = ""`
- Slots: None
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### MegaMenu
Theme-aware, responsive mega menu component.
- Mount: `data-component="MegaMenu"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Mega Menu"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Nav
Theme-aware, responsive nav component.
- Mount: `data-component="Nav"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Nav"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Navbar
Theme-aware, responsive navbar component.
- Mount: `data-component="Navbar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record<string, unknown> = {}`, `items: unknown[] = []`, `actions: unknown[] = []`, `active: string = ""`, `sticky: boolean = false`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `class: string = ""`
- Slots: `topbar`, `actions`
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ item: string | number | boolean | null | object; value: string | number | boolean; level: string })`, `action({ item: string | number | boolean | null | object; value: string | number | boolean })`
### Pagination
Theme-aware, responsive pagination component.
- Mount: `data-component="Pagination"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Pagination"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `previous({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `next({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Scrollspy
Theme-aware, responsive scrollspy component.
- Mount: `data-component="Scrollspy"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Scrollspy"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### Sidebar
Theme-aware, responsive sidebar component.
- Mount: `data-component="Sidebar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Sidebar"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `mobileLabel: string = "Open navigation"`, `class: string = ""`
- Slots: `default`
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ source: string })`, `select({ item: string | number | boolean | null | object; value: string | number | boolean; level: string })`
### Stepper
Theme-aware, responsive stepper component.
- Mount: `data-component="Stepper"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Stepper"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `previous({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `next({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `complete({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Tabs
Switch between related responsive content panels with horizontal or vertical orientation and selection events.
- Mount: `data-component="Tabs"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Tabs"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `class: string = ""`
- Slots: `default`
- Outputs: None
## Overlays
### ContextMenu
Open an accessible keyboard-aware action menu from pointer or keyboard context interactions.
- Mount: `data-component="ContextMenu"`
- Props: `items: unknown[] = []`, `open: boolean = false`, `defaultOpen: boolean = false`, `trigger: string = "contextmenu"`, `placement: string = "pointer"`, `align: string = "start"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `title: string = ""`, `description: string = ""`, `label: string = "Context menu"`, `closeOnSelect: boolean = true`, `closeOnOutside: boolean = true`, `disabled: boolean = false`, `minWidth: string = "14rem"`, `maxWidth: string = "20rem"`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `open({ x: number; y: number; trigger: string; sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `select({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`, `action({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`
### Drawer
Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events.
- Mount: `data-component="Drawer"`
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `placement: string = "right"`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `title: string = "Drawer"`, `description: string = ""`, `icon: string = ""`, `label: string = "Drawer"`, `closeLabel: string = "Close drawer"`, `showClose: boolean = true`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `duration: number = 260`, `overlay: boolean = true`, `scrollable: boolean = true`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `open({ placement: string; sourceEvent: Event })`, `close({ reason: string; placement: string; sourceEvent: Event })`, `cancel({ placement: string; sourceEvent: Event })`
### Dropdown
Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement.
- Mount: `data-component="Dropdown"`
- Props: `items: unknown[] = []`, `open: boolean = false`, `defaultOpen: boolean = false`, `label: string = "Open menu"`, `icon: string = ""`, `showChevron: boolean = true`, `menuLabel: string = "Dropdown menu"`, `placement: string = "bottom-start"`, `width: string = "md"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `closeOnSelect: boolean = true`, `closeOnOutside: boolean = true`, `disabled: boolean = false`, `emptyLabel: string = "No menu items"`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `toggle({ open: boolean; sourceEvent: Event; reason?: object })`, `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `select({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`, `action({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`
### Modal
Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing.
- Mount: `data-component="Modal"`
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = "Modal"`, `description: string = ""`, `icon: string = ""`, `label: string = "Modal dialog"`, `size: string = "md"`, `placement: string = "center"`, `color: string = "primary"`, `variant: string = "default"`, `showClose: boolean = true`, `closeLabel: string = "Close modal"`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `closeOnCancel: boolean = true`, `closeOnConfirm: boolean = false`, `showFooter: boolean = true`, `cancelLabel: string = "Cancel"`, `cancelIcon: string = ""`, `confirmLabel: string = "Confirm"`, `confirmIcon: string = ""`, `confirmDisabled: boolean = false`, `confirmLoading: boolean = false`, `destructive: boolean = false`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `scrollable: boolean = true`, `scrollBehavior: string = "inside"`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `cancel({ sourceEvent: Event })`, `confirm({ sourceEvent: Event })`
### Popover
Display anchored supporting content with configurable trigger, placement, responsive sizing, and open or close events.
- Mount: `data-component="Popover"`
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `triggerLabel: string = "Open popover"`, `triggerIcon: string = ""`, `title: string = ""`, `description: string = ""`, `icon: string = ""`, `placement: string = "bottom-start"`, `width: string = "md"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `showArrow: boolean = true`, `showClose: boolean = false`, `closeLabel: string = "Close popover"`, `closeOnOutside: boolean = true`, `closeOnEscape: boolean = true`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `closeOnAction: boolean = true`, `disabled: boolean = false`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `toggle({ open: boolean; sourceEvent: Event; reason?: object })`, `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `action({ href: string; sourceEvent: Event })`
### Tooltip
Show concise accessible contextual help on hover, focus, click, or controlled open state.
- Mount: `data-component="Tooltip"`
- Props: `id: string = ""`, `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = ""`, `description: string = ""`, `content: string = "Tooltip"`, `trigger: string = "hover"`, `placement: string = "top"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "dark"`, `maxWidth: string = "18rem"`, `offset: number = 10`, `showArrow: boolean = true`, `interactive: boolean = false`, `disabled: boolean = false`, `class: string = ""`
- Slots: `trigger`, `default`
- Outputs: `toggle({ open: boolean; reason: string; sourceEvent: Event })`, `open({ reason: string; sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`
## Tables
### DataTable
Sortable, filterable, paginated data table with row selection.
- Mount: `data-component="DataTable"`
- Props: `color: string = "primary"`, `size: string = "default"`, `columns: unknown[] = []`, `rows: unknown[] = []`, `rowKey: string = "id"`, `remote: boolean = false`, `loadingLabel: string = "Loading"`, `errorLabel: string = "Could not load this data"`, `retryLabel: string = "Try again"`, `caption: string = ""`, `description: string = ""`, `searchable: boolean = true`, `searchPlaceholder: string = "Search"`, `paginated: boolean = true`, `pageSize: number = 10`, `paginationStyle: string = "compact"`, `pageSizes: number[] = [10, 25, 50]`, `selectable: boolean = false`, `actions: unknown[] = []`, `striped: boolean = true`, `bordered: boolean = true`, `gridlines: string = "rows"`, `density: string = "default"`, `emptyLabel: string = "No records to show"`, `noResultsLabel: string = "No records match your search"`, `clearSearchLabel: string = "Clear search"`, `stickyFirstColumn: boolean = false`, `layout: string = "rows"`, `class: string = ""`
- Slots: `default`
- Outputs: `sort({ key: string; direction: string })`, `search({ query: string })`, `pageChange({ page: number; pageSize: number })`, `select({ selected: Array<string | number>; all: boolean })`, `change({ page: number; pageSize: number; total: number; query: string; sortKey: string; sortDirection: string })`, `rowClick({ row: object; sourceEvent: Event })`, `action({ id: string; selected: Array<string | number>; rows: object[]; sourceEvent: Event })`, `request({ instanceId: number; page: number; pageSize: number; sortKey: string; sortDirection: string; query: string })`
-224
View File
@@ -1,224 +0,0 @@
# @wrnexus/ui
> First-party Wire UI component library — a set of themeable `.wrn` components plus a single tokenized stylesheet.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/ui` ships a library of server-rendered `.wrn` components (layout, form
controls, and feedback UI) together with one themeable stylesheet, `ui.css`. The
components are **auto-discovered** by the framework router — you don't import them
in code. Once the package's component directory is on the router's scan path, you
mount any component in a page with `data-component="<name>"`. Every visual is
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 **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.
## Installation
```bash
bun add @wrnexus/ui
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
In practice you rarely install this directly: `@wrnexus/cli` and
`@wrnexus/dev-server` already depend on it and wire it into the router for you
(see [Auto-discovery](#auto-discovery)).
## Components
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
| Name | Purpose | Key props |
| ----------- | ---------------------------------- | ----------- |
| `container` | Max-width centered content wrapper | `class` |
| `stack` | Vertical column with gap | `gap` (08) |
| `hstack` | Horizontal row with gap | `gap` (08) |
| `grid` | CSS grid container | see source |
| `divider` | Horizontal rule | `class` |
| `spacer` | Flexible/empty spacing element | see source |
### Core / feedback
| Name | Purpose | Key props |
| -------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `button` | Button | `label`, `variant` (`default`\|`primary`\|`danger`\|`ghost`), `size` (`sm`\|`md`\|`lg`), `type` |
| `input` | Text input | see source |
| `textarea` | Multi-line input | see source |
| `checkbox` | Checkbox | see source |
| `badge` | Small status badge | `label`, `variant` |
| `alert` | Callout box | `variant` (`info`\|`success`\|`danger`\|`warning`), `title`, `message` |
| `card` | Padded, bordered surface | `class` |
| `avatar` | User avatar | see source |
| `spinner` | Loading indicator | see source |
| `disclosure` | Expandable details/summary | see source |
| `theme-toggle` | Theme switch button (binds `data-wire-theme-toggle`) | `label` |
### Additional controls & data display
Also shipped: `select`, `radio`, `switch`, `progress`, `tag`, `skeleton`,
`tooltip`, `table`, `FAQAccordion`, `AnnouncementBar`, and `BackToTop`.
The PDF-defined minimum release and essential build-first set also includes
typed typography, form primitives, loading actions, combobox and multi-select,
time/date-time and recurring schedule controls, confirmation dialogs, data
tables, filters, desktop/mobile navigation, mega menus, marketing/product/legal
page shells, product and metric cards, FAQ composition, pricing comparison,
SDK tabs, legal navigation, and cookie preferences.
`Seo` and `StructuredData` remain framework/page concerns rather than body
components: use the native page `seo { ... }` block and document-head APIs so
metadata is emitted in `<head>` instead of invalid component markup.
The authoritative, always-current list is `uiComponentNames()` (below), which reads
the component directory at runtime.
For the full catalog, see [`COMPONENTS.md`](./COMPONENTS.md). The machine-readable
equivalent is exported as `@wrnexus/ui/component-reference.json`.
## API
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 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
`package.json` also exposes the raw stylesheet as a subpath asset:
```json
"exports": {
".": "./src/index.ts",
"./ui.css": "./ui.css"
}
```
The framework serves this stylesheet once at `/__wrnexus/ui.css`, so pages get all
component styles from a single request.
### Tailwind and motion
Components use static Tailwind utility classes alongside the shared `.wire-*`
layer. If the package is consumed by a separate Tailwind build, include its
component sources so every utility is generated:
```css
@import "tailwindcss";
@source "../node_modules/@wrnexus/ui/components/*.wrn";
```
The shared stylesheet gives all component boundaries consistent, GPU-friendly
entry and interaction motion. Override `--wire-motion-fast`,
`--wire-motion-base`, `--wire-motion-slow`, `--wire-ease-standard`, or
`--wire-ease-emphasized` to tune it. Hover lift is limited to precise pointing
devices and `prefers-reduced-motion` is honored automatically.
### Using the selected theme in application UI
The active theme and palette are not limited to `@wrnexus/ui` components. The
framework exposes the resolved values as semantic CSS custom properties, so
pages and custom `.wrn` components can use the same contract:
```css
.account-card {
background: var(--wire-color-surface);
color: var(--wire-color-text);
border: 1px solid var(--wire-color-border);
}
.account-card__action {
background: var(--wire-color-primary);
color: var(--wire-color-primary-contrast);
}
```
Stable no-spacing helper classes are also available: `wire-bg-page`,
`wire-bg-surface`, `wire-bg-surface-2`, `wire-bg-primary`, `wire-bg-secondary`,
`wire-text`, `wire-text-muted`, `wire-text-primary`, `wire-text-success`,
`wire-text-warning`, `wire-text-danger`, and `wire-border`.
Tailwind-authored custom markup can continue using the palette families already
used by packaged components. `indigo-*` and `violet-*` resolve to primary,
`blue-*` to info, `emerald-*`/`green-*` to success, `amber-*` to warning, and
`red-*`/`rose-*` to danger. These aliases live at `:root`, so they work outside
a `[data-component]` boundary too.
## Usage
### Auto-discovery
The router scans extra `componentDirs` (in addition to the app's own
`app/components`) and keys components by name. Library dirs are scanned **first**
and `app/components` **last**, so an app component of the same name shadows the
library's. The CLI build (`@wrnexus/cli`) and dev server (`@wrnexus/dev-server`)
both wire the UI directory in for you:
```ts
import { buildRouter } from "@wrnexus/router";
import { uiComponentsDir } from "@wrnexus/ui";
const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });
```
### Mounting components in a page
Once discovered, mount any component by name via `data-component`. Quoted
attributes (other than `data-component`) become string props:
```html
<div data-component="card">
<div data-component="badge" label="New"></div>
<button data-component="button" label="Save" variant="primary" size="lg"></button>
<div data-component="alert" variant="success" title="Done" message="Saved."></div>
</div>
```
## Overrides
Ways to customize the components, in increasing order of power:
1. **Theme tokens** — override CSS custom properties such as `--wire-color-primary`,
`--wire-color-surface`, `--wire-radius-sm`, etc. Every component style resolves
through `var(--wire-*)`, so changing a token restyles everything instantly
(including across theme switches).
2. **App CSS** — redefine a `.wire-*` class in your own stylesheet, which is loaded
after `ui.css` and therefore wins.
3. **`class` prop** — pass a `class` prop to a component; it is appended to the
component's root element, letting you add per-instance classes without touching
the base styles.
4. **`wrnexus eject <name>`** — copy the component's `.wrn` source into your
`app/components`, where (because app components shadow library ones) you fully
own and can edit it. Use `uiComponentNames()` for the list of ejectable names.
## Requirements / Notes
- **Bun-only** — the package uses standard fs/path/url APIs but is published and
consumed within the Bun-native WrNexus toolchain (Node is not supported).
- Peer packages: components are discovered and rendered by
[`@wrnexus/router`](../router) (via `componentDirs`) and served by
[`@wrnexus/dev-server`](../dev-server) / built by [`@wrnexus/cli`](../cli).
- Depends on [`@wrnexus/core`](../core) (`dependencies`).
- `theme-toggle` relies on the framework's theme runtime, which binds the
`data-wire-theme-toggle` attribute — no per-component JS is required.
-545
View File
@@ -1,545 +0,0 @@
{
"sourceDocuments": ["Screenshot-directed WRNexus UI catalog reset"],
"components": [
{
"name": "Accordion",
"category": "base",
"purpose": "Theme-aware, responsive accordion component."
},
{
"name": "AdvancedDatePicker",
"category": "integrations",
"purpose": "Theme-aware, responsive advanced date picker component."
},
{
"name": "AdvancedRangeSlider",
"category": "integrations",
"purpose": "Theme-aware, responsive advanced range slider component."
},
{
"name": "AdvancedSelect",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive advanced select component."
},
{
"name": "Alert",
"category": "base",
"purpose": "Theme-aware, responsive alert component."
},
{
"name": "AnnouncementBar",
"category": "marketing",
"purpose": "Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls."
},
{
"name": "AuthForm",
"category": "core",
"purpose": "Reusable auth form component."
},
{
"name": "AuthSplitLayout",
"category": "core",
"purpose": "Reusable auth split layout component."
},
{
"name": "Avatar",
"category": "base",
"purpose": "Theme-aware, responsive avatar component."
},
{
"name": "AvatarGroup",
"category": "base",
"purpose": "Theme-aware, responsive avatar group component."
},
{
"name": "BackToTop",
"category": "navigation",
"purpose": "Provide a responsive floating control that returns long pages to the top and can show scroll progress."
},
{
"name": "Badge",
"category": "base",
"purpose": "Theme-aware, responsive badge component."
},
{
"name": "Blockquote",
"category": "base",
"purpose": "Theme-aware, responsive blockquote component."
},
{
"name": "Breadcrumb",
"category": "navigation",
"purpose": "Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events."
},
{
"name": "Button",
"category": "base",
"purpose": "Theme-aware, responsive button component."
},
{
"name": "ButtonGroup",
"category": "base",
"purpose": "Theme-aware, responsive button group component."
},
{
"name": "CTASection",
"category": "marketing",
"purpose": "Close a page or major section with conversion-focused copy, actions, and optional supporting visual content."
},
{
"name": "Card",
"category": "base",
"purpose": "Group related content in a responsive themed surface with title, description, content, and supporting slots."
},
{
"name": "Carousel",
"category": "base",
"purpose": "Theme-aware, responsive carousel component."
},
{
"name": "Chart",
"category": "integrations",
"purpose": "Theme-aware, responsive chart component."
},
{
"name": "ChatBubble",
"category": "base",
"purpose": "Theme-aware, responsive chat bubble component."
},
{
"name": "Checkbox",
"category": "forms",
"purpose": "Theme-aware, responsive checkbox component."
},
{
"name": "Clipboard",
"category": "integrations",
"purpose": "Theme-aware, responsive clipboard component."
},
{
"name": "Collapse",
"category": "base",
"purpose": "Theme-aware, responsive collapse component."
},
{
"name": "ColorPicker",
"category": "forms",
"purpose": "Theme-aware, responsive color picker component."
},
{
"name": "Columns",
"category": "layout",
"purpose": "Create responsive balanced content columns with configurable count, gap, density, and maximum width."
},
{
"name": "ComboBox",
"category": "advanced-forms",
"purpose": "Editable autocomplete combobox with local and remote suggestions."
},
{
"name": "Confetti",
"category": "integrations",
"purpose": "Theme-aware, responsive confetti component."
},
{
"name": "Container",
"category": "layout",
"purpose": "Constrain and align page content with responsive gutters and compact, wide, or full width options."
},
{
"name": "ContextMenu",
"category": "overlays",
"purpose": "Open an accessible keyboard-aware action menu from pointer or keyboard context interactions."
},
{
"name": "CopyMarkup",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive copy markup component."
},
{
"name": "CustomScrollbar",
"category": "layout",
"purpose": "Theme-aware, responsive custom scrollbar component."
},
{
"name": "DataMap",
"category": "integrations",
"purpose": "Theme-aware, responsive data map component."
},
{
"name": "DataTable",
"category": "tables",
"purpose": "Sortable, filterable, paginated data table with row selection."
},
{
"name": "DatePicker",
"category": "base",
"purpose": "Theme-aware, responsive date picker component."
},
{
"name": "DeviceFrame",
"category": "base",
"purpose": "Theme-aware, responsive device frame component."
},
{
"name": "Divider",
"category": "layout",
"purpose": "Separate related horizontal or vertical content with optional labels, sizes, and semantic colors."
},
{
"name": "DragAndDrop",
"category": "integrations",
"purpose": "Theme-aware, responsive drag and drop component."
},
{
"name": "Drawer",
"category": "overlays",
"purpose": "Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events."
},
{
"name": "Dropdown",
"category": "overlays",
"purpose": "Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement."
},
{
"name": "FeatureCard",
"category": "marketing",
"purpose": "Present one linked feature or service with media, icon, badge, description, and action."
},
{
"name": "FeatureGrid",
"category": "layout",
"purpose": "Arrange feature cards, services, solutions, or benefits in a responsive equal-height grid."
},
{
"name": "FeatureIconCard",
"category": "marketing",
"purpose": "Present a compact feature or benefit with a styled icon, title, description, badge, and optional link."
},
{
"name": "FileInput",
"category": "forms",
"purpose": "Theme-aware, responsive file input component."
},
{
"name": "FileUpload",
"category": "integrations",
"purpose": "Theme-aware, responsive file upload component."
},
{
"name": "FileUploadProgress",
"category": "base",
"purpose": "Theme-aware, responsive file upload progress component."
},
{
"name": "Footer",
"category": "layout",
"purpose": "Render structured responsive footer navigation, pre and post content, copyright content, links, and public events."
},
{
"name": "Grid",
"category": "layout",
"purpose": "Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width."
},
{
"name": "Hero",
"category": "marketing",
"purpose": "Build a full-width responsive hero with constrained content, actions, trust signals, and a structured or custom visual panel."
},
{
"name": "HeroActions",
"category": "marketing",
"purpose": "Group hero and campaign actions with consistent alignment, orientation, sizing, and responsive mobile stacking."
},
{
"name": "Image",
"category": "layout",
"purpose": "Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment."
},
{
"name": "Input",
"category": "forms",
"purpose": "Theme-aware, responsive input component."
},
{
"name": "InputGroup",
"category": "forms",
"purpose": "Theme-aware, responsive input group component."
},
{
"name": "InputNumber",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive input number component."
},
{
"name": "Kbd",
"category": "layout",
"purpose": "Theme-aware, responsive kbd component."
},
{
"name": "LayoutSplitter",
"category": "layout",
"purpose": "Theme-aware, responsive layout splitter component."
},
{
"name": "LegendIndicator",
"category": "base",
"purpose": "Theme-aware, responsive legend indicator component."
},
{
"name": "Link",
"category": "layout",
"purpose": "Render an accessible internal or external link with target, relation, size, color, and public focus or click events."
},
{
"name": "List",
"category": "base",
"purpose": "Present structured responsive linked or status items with icons, descriptions, actions, and selection events."
},
{
"name": "ListGroup",
"category": "base",
"purpose": "Theme-aware, responsive list group component."
},
{
"name": "Map",
"category": "integrations",
"purpose": "Present responsive location information and markers with map-ready metadata and movement or marker events."
},
{
"name": "MarketingSectionHeader",
"category": "marketing",
"purpose": "Introduce marketing content with an eyebrow, title, description, and optional linked action."
},
{
"name": "Marquee",
"category": "base",
"purpose": "Continuously present responsive labels, partners, notices, or capabilities with pause and resume behavior."
},
{
"name": "MegaMenu",
"category": "navigation",
"purpose": "Theme-aware, responsive mega menu component."
},
{
"name": "MetricCard",
"category": "data",
"purpose": "Display one operational metric with value, suffix, description, icon, trend, progress, and optional action."
},
{
"name": "MetricGrid",
"category": "data",
"purpose": "Arrange operational metrics, KPIs, public statistics, or service indicators in a responsive equal-height grid."
},
{
"name": "Modal",
"category": "overlays",
"purpose": "Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing."
},
{
"name": "Nav",
"category": "navigation",
"purpose": "Theme-aware, responsive nav component."
},
{
"name": "Navbar",
"category": "navigation",
"purpose": "Theme-aware, responsive navbar component."
},
{
"name": "PageHeader",
"category": "marketing",
"purpose": "Introduce an internal or public page with breadcrumbs, icon, title, description, and primary or secondary actions."
},
{
"name": "Pagination",
"category": "navigation",
"purpose": "Theme-aware, responsive pagination component."
},
{
"name": "PinInput",
"category": "advanced-forms",
"purpose": "Secure multi-cell PIN and verification-code input with regex and paste support."
},
{
"name": "Popover",
"category": "overlays",
"purpose": "Display anchored supporting content with configurable trigger, placement, responsive sizing, and open or close events."
},
{
"name": "PortalDashboard",
"category": "core",
"purpose": "Reusable portal dashboard component."
},
{
"name": "PreferenceSwitcher",
"category": "core",
"purpose": "Reusable preference switcher component."
},
{
"name": "Progress",
"category": "base",
"purpose": "Theme-aware, responsive progress component."
},
{
"name": "PublicPageShell",
"category": "layout",
"purpose": "Provide the outer responsive structure, width, background, slots, overflow, and minimum-height behavior for public pages."
},
{
"name": "Radio",
"category": "forms",
"purpose": "Theme-aware, responsive radio component."
},
{
"name": "RangeSlider",
"category": "forms",
"purpose": "Theme-aware, responsive range slider component."
},
{
"name": "Rating",
"category": "base",
"purpose": "Theme-aware, responsive rating component."
},
{
"name": "Scrollspy",
"category": "navigation",
"purpose": "Theme-aware, responsive scrollspy component."
},
{
"name": "SearchBox",
"category": "forms",
"purpose": "Provide an accessible responsive search field with labels, validation states, sizes, and input or change events."
},
{
"name": "Section",
"category": "layout",
"purpose": "Create a responsive themed page section with controlled spacing, width, borders, and surface treatment."
},
{
"name": "SectionHeader",
"category": "layout",
"purpose": "Introduce a section with an eyebrow, title, description, alignment, and responsive heading hierarchy."
},
{
"name": "Select",
"category": "forms",
"purpose": "Theme-aware, responsive select component."
},
{
"name": "Sidebar",
"category": "navigation",
"purpose": "Theme-aware, responsive sidebar component."
},
{
"name": "Skeleton",
"category": "base",
"purpose": "Theme-aware, responsive skeleton component."
},
{
"name": "Spinner",
"category": "base",
"purpose": "Theme-aware, responsive spinner component."
},
{
"name": "SplitHero",
"category": "marketing",
"purpose": "Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel."
},
{
"name": "StatsBar",
"category": "data",
"purpose": "Present a compact responsive strip of key facts, counts, performance indicators, or trust signals."
},
{
"name": "Stepper",
"category": "navigation",
"purpose": "Theme-aware, responsive stepper component."
},
{
"name": "StrongPassword",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive strong password component."
},
{
"name": "StyledIcon",
"category": "base",
"purpose": "Theme-aware, responsive styled icon component."
},
{
"name": "Switch",
"category": "forms",
"purpose": "Theme-aware, responsive switch component."
},
{
"name": "Tabs",
"category": "navigation",
"purpose": "Switch between related responsive content panels with horizontal or vertical orientation and selection events."
},
{
"name": "TextLink",
"category": "marketing",
"purpose": "Render an accessible text action with optional icon, arrow, underline, external state, and semantic styling."
},
{
"name": "Textarea",
"category": "forms",
"purpose": "Theme-aware, responsive textarea component."
},
{
"name": "TimePicker",
"category": "forms",
"purpose": "Theme-aware, responsive time picker component."
},
{
"name": "Timeline",
"category": "base",
"purpose": "Present responsive chronological activity, milestones, or workflow status with rich item metadata."
},
{
"name": "Toast",
"category": "base",
"purpose": "Theme-aware, responsive toast component."
},
{
"name": "ToastNotifications",
"category": "integrations",
"purpose": "Theme-aware, responsive toast notifications component."
},
{
"name": "Toaster",
"category": "core",
"purpose": "Reusable toaster component."
},
{
"name": "ToggleCount",
"category": "advanced-forms",
"purpose": "Theme-aware, responsive toggle count component."
},
{
"name": "TogglePassword",
"category": "advanced-forms",
"purpose": "Accessible password field with optional show and hide controls."
},
{
"name": "Tooltip",
"category": "overlays",
"purpose": "Show concise accessible contextual help on hover, focus, click, or controlled open state."
},
{
"name": "TreeView",
"category": "base",
"purpose": "Theme-aware, responsive tree view component."
},
{
"name": "Typography",
"category": "layout",
"purpose": "Apply consistent readable responsive typography, widths, columns, spacing, and editorial hierarchy."
},
{
"name": "WysiwygEditor",
"category": "integrations",
"purpose": "Theme-aware, responsive wysiwyg editor component."
}
]
}
-5
View File
@@ -1,5 +0,0 @@
{
"generatedFrom": "full component catalog reset",
"removedCount": 0,
"replacements": {}
}
File diff suppressed because it is too large Load Diff
-206
View File
@@ -1,206 +0,0 @@
component Accordion {
outputs {
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
variant: string = "default"
class: string = ""
id: string = "accordion"
items: unknown[] = []
defaultOpen: unknown[] = []
multiple: boolean = false
alwaysOpen: boolean = false
disabled: boolean = false
indicator: string = "plus"
indicatorPosition: string = "start"
showIndicator: boolean = true
bordered: boolean = false
separated: boolean = false
flush: boolean = false
contentItalic: boolean = false
}
state openValues = defaultOpen
functions {
shared function itemValue(item, index) {
return item.value !== undefined && item.value !== ""
? String(item.value)
: String(index)
}
shared function nestedValue(parent, parentIndex, item, index) {
return itemValue(parent, parentIndex) + "." + itemValue(item, index)
}
shared function isOpen(value) {
return openValues.includes(value)
}
shared function allowsMultiple() {
return multiple || alwaysOpen
}
client function dispatchAccordionEvent(sourceEvent, eventName, value, item, root, customEvent) {
root = sourceEvent.currentTarget.closest("[data-wrn-accordion]")
if (!root) {
return
}
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent(eventName, true, false, {
component: "Accordion",
value: value,
item: item,
open: isOpen(value),
openValues: openValues
})
root.dispatchEvent(customEvent)
}
client function toggleItem(sourceEvent, value, item, wasOpen) {
if (disabled || item.disabled) {
return
}
wasOpen = isOpen(value)
if (wasOpen) {
openValues = openValues.filter((entry) => entry !== value)
} else if (allowsMultiple()) {
openValues = openValues.concat([value])
} else {
openValues = [value]
}
dispatchAccordionEvent(
sourceEvent,
wasOpen ? "close" : "open",
value,
item
)
dispatchAccordionEvent(sourceEvent, "change", value, item)
}
}
view {
<div
{...attrs}
id="{id}"
data-wrn-accordion
data-variant="{variant}"
data-indicator="{indicator}"
data-multiple="{allowsMultiple() ? 'true' : 'false'}"
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--accordion wire-next--accordion-{variant} {bordered ? 'wire-next--accordion-bordered' : ''} {separated ? 'wire-next--accordion-separated' : ''} {flush ? 'wire-next--accordion-flush' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
>
{#each items as item, index}
<section
class="wire-next__accordion-item"
data-open="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
data-disabled="{disabled || item.disabled ? 'true' : 'false'}"
>
<h3 class="wire-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}"
aria-expanded="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
aria-controls="{id}-panel-{index}"
disabled="{disabled || item.disabled}"
@click="toggleItem(event, itemValue(item, index), item)"
>
{#if showIndicator && indicatorPosition === "start"}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
<span>{item.label || item.title}</span>
{#if showIndicator && indicatorPosition === "end"}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
</button>
</h3>
<div
id="{id}-panel-{index}"
class="wire-next__accordion-panel"
role="region"
aria-labelledby="{id}-trigger-{index}"
aria-hidden="{isOpen(itemValue(item, index)) ? 'false' : 'true'}"
>
<div>
<div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
{#if item.content}<p>{item.content}</p>{/if}
{#if item.children && item.children.length > 0}
<div class="wire-next__accordion-nested">
{#each item.children as child, childIndex}
<section
class="wire-next__accordion-item"
data-open="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
>
<h4 class="wire-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}-{childIndex}"
aria-expanded="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
aria-controls="{id}-panel-{index}-{childIndex}"
disabled="{disabled || child.disabled}"
@click="toggleItem(event, nestedValue(item, index, child, childIndex), child)"
>
{#if showIndicator}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
<span>{child.label || child.title}</span>
</button>
</h4>
<div
id="{id}-panel-{index}-{childIndex}"
class="wire-next__accordion-panel"
role="region"
aria-labelledby="{id}-trigger-{index}-{childIndex}"
aria-hidden="{isOpen(nestedValue(item, index, child, childIndex)) ? 'false' : 'true'}"
>
<div>
<div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
<p>{child.content}</p>
</div>
</div>
</div>
</section>
{/each}
</div>
{/if}
</div>
</div>
</div>
</section>
{/each}
</div>
}
}
@@ -1,27 +0,0 @@
component AdvancedDatePicker {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Advanced Date Picker"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-date-picker wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -1,26 +0,0 @@
component AdvancedRangeSlider {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
start(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
end(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Advanced Range Slider"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-range-slider wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
-459
View File
@@ -1,459 +0,0 @@
component AdvancedSelect {
outputs {
search(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
label: string = "Advanced Select"
name: string = ""
value: string = ""
values: unknown[] = []
options: unknown[] = []
groups: unknown[] = []
placeholder: string = "Select an option"
placeholderIcon: string = ""
searchPlaceholder: string = "Search options…"
multiple: boolean = false
searchable: boolean = true
defaultOpen: boolean = false
clearable: boolean = true
allowEmpty: boolean = true
tags: boolean = false
disabled: boolean = false
required: boolean = false
invalid: boolean = false
validationMessage: string = ""
helpText: string = ""
loading: boolean = false
loadingLabel: string = "Loading options…"
emptyLabel: string = "No options found"
selectedOptionsLabel: string = "Selected options"
clearLabel: string = "Clear selection"
createLabel: string = "Create"
loadMoreLabel: string = "Load more"
searchMode: string = "contains"
searchFields: string = "label,description"
minSearchLength: number = 0
searchResultLimit: number = 0
maxSelections: number = 0
showCounter: boolean = false
counterTemplate: string = "{selected} selected"
optionTemplate: string = "default"
selectedTemplate: string = "default"
closeOnSelect: boolean = true
scrollToSelected: boolean = true
fixed: boolean = false
placement: string = "bottom"
remote: boolean = false
remoteUrl: string = ""
remoteQueryParam: string = "q"
remoteDebounce: number = 250
remoteAutoLoad: boolean = true
infinite: boolean = false
hasMore: boolean = false
page: number = 1
class: string = ""
}
state open = defaultOpen
state query: string = ""
state activeIndex: number = -1
state selectedValue = value
state selectedValues = values
functions {
shared function allOptions() {
return [...groups.flatMap((group) => group.options || []), ...options]
}
shared function searchableText(option) {
return ((option.label || "") + " " + (option.description || "")).toLowerCase()
}
shared function matches(option) {
if (!query || query.length < Number(minSearchLength || 0)) {
return true
}
if (searchMode === "startsWith") {
return searchableText(option).startsWith(query.toLowerCase())
}
if (searchMode === "exact") {
return searchableText(option) === query.toLowerCase()
}
return searchableText(option).includes(query.toLowerCase())
}
shared function matchingOptions(list) {
return list.filter((option) => matches(option))
}
shared function visibleOptions(list) {
if (searchResultLimit > 0) {
return matchingOptions(list).slice(0, Number(searchResultLimit))
}
return matchingOptions(list)
}
shared function flatVisibleOptions() {
return visibleOptions(allOptions())
}
shared function isSelected(option) {
return (
multiple
? selectedValues.includes(option.value)
: selectedValue === option.value
)
}
shared function selectedOptions() {
return allOptions().filter((option) => isSelected(option))
}
shared function selectedCount() {
return multiple ? selectedValues.length : selectedValue ? 1 : 0
}
shared function counterText() {
return (
maxSelections
? selectedCount() + " / " + maxSelections + " selected"
: selectedCount() + " selected"
)
}
shared function selectedText() {
return (
multiple
? selectedValues.join(", ")
: selectedOptions().length
? selectedOptions()[0].label
: ""
)
}
shared function triggerText() {
return selectedCount() ? selectedText() : placeholder
}
shared function canSelect(option) {
if (option.disabled) {
return false
}
if (!multiple || isSelected(option)) {
return true
}
if (maxSelections <= 0) {
return true
}
return selectedCount() < maxSelections
}
shared function chooseSingle(option) {
if (!canSelect(option)) {
return
}
selectedValue = option.value
query = ""
if (closeOnSelect) {
open = false
}
}
shared function chooseMultiple(option) {
if (!canSelect(option)) {
return
}
if (isSelected(option)) {
selectedValues = selectedValues.filter((item) => item !== option.value)
} else {
selectedValues = selectedValues.concat([option.value])
}
query = ""
}
shared function chooseOption(option) {
if (multiple) {
chooseMultiple(option)
return
}
chooseSingle(option)
}
client function clearSelection(event) {
event.stopPropagation()
selectedValue = ""
selectedValues = []
query = ""
open = false
}
shared function toggle() {
if (disabled) {
return;
};
open = !open;
activeIndex = open && flatVisibleOptions().length ? 0 : -1;
}
shared function moveActive(direction) {
if (!flatVisibleOptions().length) {
return
}
activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
}
client function handleKeydown(event) {
if (disabled) {
return
}
if (event.key === "ArrowDown") {
event.preventDefault()
if (!open) {
open = true
}
moveActive(1)
} else if (event.key === "ArrowUp") {
event.preventDefault()
if (!open) {
open = true
}
moveActive(-1)
} else if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
if (!open) {
open = true
} else if (activeIndex >= 0) {
chooseOption(flatVisibleOptions()[activeIndex])
}
} else if (event.key === "Escape") {
open = false
} else if (event.key === "Home" && open) {
event.preventDefault()
activeIndex = 0
} else if (event.key === "End" && open) {
event.preventDefault()
activeIndex = flatVisibleOptions().length - 1
}
}
shared function optionIndex(option) {
return flatVisibleOptions().findIndex((item) => item.value === option.value)
}
}
view {
<div
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-select {open ? 'wire-next--open' : ''} {fixed ? 'wire-next--advanced-select-fixed' : ''} {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
data-placement="{placement}"
data-search-mode="{searchMode}"
data-remote="{remote ? 'true' : 'false'}"
data-remote-url="{remoteUrl}"
data-remote-query-param="{remoteQueryParam}"
data-remote-debounce="{remoteDebounce}"
data-remote-auto-load="{remoteAutoLoad ? 'true' : 'false'}"
data-infinite="{infinite ? 'true' : 'false'}"
data-page="{page}"
data-wrn-select
@focusout="if (!event.currentTarget.contains(event.relatedTarget)) { open = false }"
>
<div class="wire-next__row">
<label id="{name}-label" for="{name}-trigger">{label}</label>
<small data-show="showCounter" data-text="counterText()">{counterText()}</small>
</div>
<div class="wire-next__select-control">
<button
{...attrs}
id="{name}-trigger"
type="button"
class="wire-next__select-trigger"
role="combobox"
aria-haspopup="listbox"
aria-expanded="{open}"
aria-controls="{name}-listbox"
aria-labelledby="{name}-label"
aria-invalid="{invalid}"
disabled="{disabled}"
@click="toggle()"
@keydown="handleKeydown(event)"
>
<span class="wire-next__select-value">
<span data-show="!multiple" data-text="triggerText()">{triggerText()}</span>
<span
data-show="multiple && selectedTemplate === 'count' && selectedCount() > 0"
data-text="selectedCount() + ' selected'"
>{selectedCount()} selected</span>
<span
data-show="multiple && selectedTemplate === 'text' && selectedCount() > 0"
data-text="selectedText()"
>{selectedText()}</span>
<span
class="wire-next__select-tags"
aria-label="{selectedOptionsLabel}"
data-show="multiple && selectedTemplate !== 'count' && selectedTemplate !== 'text' && selectedCount() > 0"
>
{#each allOptions() as option}
<span data-show="isSelected(option)">
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
{option.label}
</span>
{/each}
</span>
<span data-show="multiple && selectedCount() === 0">
{#if placeholderIcon}<i class="{placeholderIcon}" aria-hidden="true"></i>{/if}
<span class="wire-next__placeholder">{placeholder}</span>
</span>
</span>
<span
class="wire-next__select-chevron icon-[lucide--chevrons-up-down]"
aria-hidden="true"
></span>
</button>
<button
type="button"
class="wire-next__clear-select"
data-show="clearable && allowEmpty && selectedCount() > 0 && !disabled"
aria-label="{clearLabel}"
title="{clearLabel}"
@click="clearSelection(event)"
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
</div>
<div
class="wire-next__select-dropdown {fixed ? 'wire-next__select-dropdown--fixed' : ''}"
data-show="open"
style="{open ? '' : 'display: none'}"
>
{#if searchable}
<label class="wire-next__select-search">
<span class="wire-visually-hidden">{searchPlaceholder}</span>
<span class="wire-next__search-icon" aria-hidden="true">⌕</span>
<input
type="search"
value="{query}"
placeholder="{searchPlaceholder}"
autocomplete="off"
@input="query = event.target.value; activeIndex = flatVisibleOptions().length ? 0 : -1"
@keydown="handleKeydown(event)"
/>
</label>
{/if}
<div
class="wire-next__select-message"
data-show="remote && query.length !== 0 && query.length < minSearchLength"
>Enter at least {minSearchLength} characters.</div>
<div class="wire-next__select-message" data-show="loading" role="status">
<i class="wire-spinner wire-spinner--inline" aria-hidden="true"></i>
{loadingLabel}
</div>
<div
id="{name}-listbox"
class="wire-next__select-list"
data-show="(!remote || (query.length === 0 && remoteAutoLoad) || query.length >= minSearchLength) && !loading"
role="listbox"
aria-multiselectable="{multiple}"
>
{#each groups as group}
{#if visibleOptions(group.options || []).length}
<div class="wire-next__option-group" role="group" aria-label="{group.label}">
<div class="wire-next__group-label">{group.label}</div>
{#each group.options || [] as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i
class="wire-next__check icon-[lucide--check]"
data-show="isSelected(option)"
aria-hidden="true"
></i>
</button>
{/each}
</div>
{/if}
{/each}
{#each options as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i
class="wire-next__check icon-[lucide--check]"
data-show="isSelected(option)"
aria-hidden="true"
></i>
</button>
{/each}
{#if !loading && !flatVisibleOptions().length}
<div class="wire-next__select-message">{emptyLabel}</div>
{/if}
</div>
{#if tags && query && !allOptions().some((option) => option.label.toLowerCase() === query.toLowerCase())}
<button type="button" class="wire-next__create-option">{createLabel} “{query}”</button>
{/if}
<button
type="button"
class="wire-next__load-more"
data-wrn-select-load-more
data-show="infinite && hasMore"
>{loadMoreLabel}</button>
</div>
<input
type="hidden"
name="{name}"
value="{multiple ? selectedValues.join(',') : selectedValue}"
required="{required}"
aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
/>
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="wire-next__validation"
data-error="{name}"
>{validationMessage}</small>
</div>
}
}
-154
View File
@@ -1,154 +0,0 @@
component AvatarGroup {
outputs {
overflow(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
items: unknown[] = []
size: string = "md"
color: string = "primary"
variant: string = "solid"
shape: string = "circle"
layout: string = "stack"
maxVisible: number = 4
columns: number = 3
borderColor: string = ""
showTooltips: boolean = true
overflowLabel: string = "Show remaining members"
class: string = ""
}
state overflowOpen: boolean = false
functions {
shared function visibleMembers() {
return items.slice(0, Number(maxVisible))
}
shared function hiddenMembers() {
return items.slice(Number(maxVisible))
}
client function toggleOverflow(sourceEvent, root, customEvent) {
overflowOpen = !overflowOpen
root = sourceEvent.currentTarget.closest("[data-wrn-avatar-group]")
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("overflow", true, false, {
component: "AvatarGroup",
open: overflowOpen,
hiddenCount: hiddenMembers().length
})
root.dispatchEvent(customEvent)
}
}
view {
<div
{...attrs}
class="wire-next wire-next--avatar-group {class}"
data-wrn-avatar-group
data-layout="{layout}"
data-shape="{shape}"
data-size="{size}"
style="--wire-avatar-group-columns:{columns}; --wire-avatar-group-ring:{borderColor || 'var(--wire-color-bg)'}"
role="group"
aria-label="Avatar group"
>
<div
class="wire-next__avatar-group-members"
>
{#each visibleMembers() as item}
<span
class="wire-next__avatar-group-member"
data-size="{item.size || size}"
data-shape="{item.shape || shape}"
data-color="{item.color || color}"
data-variant="{item.variant || variant}"
tabindex="{showTooltips && (item.tooltip || item.name) ? '0' : '-1'}"
aria-label="{item.name || item.alt || item.initials || 'Group member'}"
>
<span
class="wire-next__avatar-group-avatar"
>
{#if item.src}
<img
src="{item.src}"
alt="{item.alt || item.name || ''}"
loading="lazy"
/>
{:else if item.initials}
<span
aria-hidden="true"
>
{item.initials}
</span>
{:else}
<span
class="icon-[lucide--user-round]"
aria-hidden="true"
>
</span>
{/if}
</span>
{#if showTooltips && (item.tooltip || item.name)}
<span
class="wire-next__avatar-group-tooltip"
role="tooltip"
>
{item.tooltip || item.name}
</span>
{/if}
</span>
{/each}
{#if hiddenMembers().length > 0}
<span
class="wire-next__avatar-group-overflow"
>
<button
type="button"
class="wire-next__avatar-group-overflow-button"
aria-label="{overflowLabel}"
aria-expanded="{overflowOpen ? 'true' : 'false'}"
@click="toggleOverflow(event)"
>
+{hiddenMembers().length}
</button>
<span
class="wire-next__avatar-group-menu"
role="menu"
data-show="overflowOpen"
aria-hidden="{overflowOpen ? 'false' : 'true'}"
>
{#each hiddenMembers() as item}
<span
class="wire-next__avatar-group-menu-item"
role="menuitem"
>
<span
class="wire-next__avatar-group-menu-avatar"
>
{#if item.src}
<img
src="{item.src}"
alt=""
loading="lazy"
/>
{:else}
<span>{item.initials || '?'}</span>
{/if}
</span>
<span>{item.name || item.alt || item.initials || 'Team member'}</span>
</span>
{/each}
</span>
</span>
{/if}
</div>
</div>
}
}
-67
View File
@@ -1,67 +0,0 @@
component Blockquote {
props {
quote: string = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed."
citation: string = ""
citationTitle: string = ""
citationUrl: string = ""
avatarSrc: string = ""
avatarAlt: string = ""
size: string = "md"
color: string = "primary"
align: string = "left"
variant: string = "default"
quoteMark: boolean = true
italic: boolean = true
class: string = ""
}
view {
<figure
{...attrs}
class="wire-next wire-next--blockquote wire-next--color-{color} wire-next--size-{size} {class}"
data-size="{size}"
data-color="{color}"
data-align="{align}"
data-variant="{variant}"
data-italic="{italic}"
>
<blockquote cite="{citationUrl}">
{#if quoteMark}
<span class="wire-next__blockquote-mark" aria-hidden="true">“</span>
{/if}
<div class="wire-next__blockquote-copy">
{#if quote}
<p>{quote}</p>
{:else}
<slot />
{/if}
</div>
</blockquote>
{#if citation || citationTitle || avatarSrc}
<figcaption class="wire-next__blockquote-citation">
{#if avatarSrc}
<img
class="wire-next__blockquote-avatar"
src="{avatarSrc}"
alt="{avatarAlt}"
loading="lazy"
/>
{/if}
<span class="wire-next__blockquote-attribution">
{#if citation}
{#if citationUrl}
<cite><a href="{citationUrl}">{citation}</a></cite>
{:else}
<cite>{citation}</cite>
{/if}
{/if}
{#if citationTitle}<span>{citationTitle}</span>{/if}
</span>
</figcaption>
{/if}
</figure>
}
}
-323
View File
@@ -1,323 +0,0 @@
component Breadcrumb {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
label: string = "Breadcrumb"
items: unknown[] = []
active: string = ""
separator: string = "chevron"
showHome: boolean = false
homeLabel: string = "Home"
homeHref: string = "/"
homeIcon: string = "icon-[lucide--house]"
size: string = "default"
color: string = "primary"
variant: string = "minimal"
class: string = ""
}
view {
<nav
{...attrs}
data-ui-component="Breadcrumb"
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
aria-label='{label}'
class='wire-breadcrumb {class}'
>
<ol class="wire-breadcrumb__list">
{#if showHome}
<li class="wire-breadcrumb__item wire-breadcrumb__item--home">
<a
href='{homeHref || "/"}'
class="wire-breadcrumb__link wire-breadcrumb__home"
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: { label: homeLabel, href: homeHref || "/", value: "home" }, itemIndex: -1 } }))'
>
{#if homeIcon === "icon-[lucide--house]"}
<span class="icon-[lucide--house] wire-breadcrumb__icon" aria-hidden="true"></span>
{:else if homeIcon}
<span class='{homeIcon} wire-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wire-breadcrumb__home-label">{homeLabel}</span>
</a>
</li>
{/if}
{#each items as item, itemIndex}
<li
class="wire-breadcrumb__item"
data-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "true" : "false"}'
>
{#if showHome || itemIndex > 0}
<span class="wire-breadcrumb__separator" aria-hidden="true">
{#if separator === "slash"}
<span>/</span>
{:else if separator === "dot"}
<span>•</span>
{:else if separator === "arrow"}
<span>→</span>
{:else}
<span class="icon-[lucide--chevron-right] wire-breadcrumb__separator-icon"></span>
{/if}
</span>
{/if}
{#if item.href && !item.disabled && !(item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1))}
<a
href='{item.href}'
target='{item.target || ""}'
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
class="wire-breadcrumb__link"
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, itemIndex: itemIndex } }))'
>
{#if item.icon}
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
{#if item.external}
<span class="icon-[lucide--arrow-up-right] wire-breadcrumb__external" aria-hidden="true"></span>
{/if}
</a>
{:else}
<span
class="wire-breadcrumb__current"
aria-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "page" : ""}'
aria-disabled='{item.disabled ? "true" : "false"}'
>
{#if item.icon}
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
</span>
{/if}
</li>
{/each}
</ol>
</nav>
}
style {
.wire-breadcrumb {
--breadcrumb-accent: var(--wire-color-primary);
--breadcrumb-soft: var(--wire-color-primary-soft);
--breadcrumb-muted: var(--wire-color-primary-muted);
--breadcrumb-text: var(--wire-color-primary-text);
min-width: 0;
color: var(--wire-color-text-muted);
}
.wire-breadcrumb[data-color="secondary"] {
--breadcrumb-accent: var(--wire-color-secondary);
--breadcrumb-soft: var(--wire-color-secondary-soft);
--breadcrumb-muted: var(--wire-color-secondary-muted);
--breadcrumb-text: var(--wire-color-secondary-text);
}
.wire-breadcrumb[data-color="info"] {
--breadcrumb-accent: var(--wire-color-info);
--breadcrumb-soft: var(--wire-color-info-soft);
--breadcrumb-muted: var(--wire-color-info-muted);
--breadcrumb-text: var(--wire-color-info-text);
}
.wire-breadcrumb[data-color="success"] {
--breadcrumb-accent: var(--wire-color-success);
--breadcrumb-soft: var(--wire-color-success-soft);
--breadcrumb-muted: var(--wire-color-success-muted);
--breadcrumb-text: var(--wire-color-success-text);
}
.wire-breadcrumb[data-color="warning"] {
--breadcrumb-accent: var(--wire-color-warning);
--breadcrumb-soft: var(--wire-color-warning-soft);
--breadcrumb-muted: var(--wire-color-warning-muted);
--breadcrumb-text: var(--wire-color-warning-text);
}
.wire-breadcrumb[data-color="danger"] {
--breadcrumb-accent: var(--wire-color-danger);
--breadcrumb-soft: var(--wire-color-danger-soft);
--breadcrumb-muted: var(--wire-color-danger-muted);
--breadcrumb-text: var(--wire-color-danger-text);
}
.wire-breadcrumb[data-variant="soft"] {
width: fit-content;
max-width: 100%;
padding: 0.45rem 0.65rem;
background: var(--breadcrumb-soft);
border: 1px solid var(--breadcrumb-muted);
border-radius: 0.75rem;
}
.wire-breadcrumb[data-variant="outline"] {
width: fit-content;
max-width: 100%;
padding: 0.45rem 0.65rem;
background: var(--wire-color-surface-raised);
border: 1px solid var(--wire-color-border);
border-radius: 0.75rem;
box-shadow: var(--wire-shadow-sm);
}
.wire-breadcrumb[data-variant="contrast"] {
color: rgba(255, 255, 255, 0.78);
}
.wire-breadcrumb__list {
display: flex;
align-items: center;
gap: 0;
min-width: 0;
margin: 0;
padding: 0;
overflow-x: auto;
list-style: none;
scrollbar-width: none;
white-space: nowrap;
}
.wire-breadcrumb__list::-webkit-scrollbar {
display: none;
}
.wire-breadcrumb__item {
display: inline-flex;
align-items: center;
min-width: 0;
flex: 0 0 auto;
}
.wire-breadcrumb__separator {
display: inline-flex;
align-items: center;
justify-content: center;
margin-inline: 0.45rem;
color: var(--wire-color-text-subtle);
font-size: 0.75rem;
opacity: 0.72;
}
.wire-breadcrumb__separator-icon {
width: 0.9rem;
height: 0.9rem;
}
.wire-breadcrumb__link,
.wire-breadcrumb__current {
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
min-height: 2rem;
padding: 0.25rem 0.4rem;
border-radius: 0.55rem;
font-size: 0.8125rem;
line-height: 1.25rem;
text-decoration: none;
}
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__link,
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__current {
min-height: 1.75rem;
font-size: 0.75rem;
}
.wire-breadcrumb[data-size="lg"] .wire-breadcrumb__link,
.wire-breadcrumb[data-size="lg"] .wire-breadcrumb__current {
min-height: 2.25rem;
font-size: 0.875rem;
}
.wire-breadcrumb__link {
color: var(--wire-color-text-muted);
font-weight: 600;
transition:
color 160ms ease,
background 160ms ease;
}
.wire-breadcrumb__link:hover {
color: var(--breadcrumb-accent);
background: var(--breadcrumb-soft);
}
.wire-breadcrumb__link:focus-visible {
color: var(--breadcrumb-accent);
outline: 2px solid var(--breadcrumb-accent);
outline-offset: 2px;
}
.wire-breadcrumb__current {
max-width: 22rem;
color: var(--wire-color-text);
font-weight: 750;
}
.wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
color: var(--breadcrumb-text);
background: var(--breadcrumb-soft);
}
.wire-breadcrumb__current[aria-disabled="true"] {
opacity: 0.58;
}
.wire-breadcrumb__icon,
.wire-breadcrumb__external {
flex: 0 0 auto;
width: 0.95rem;
height: 0.95rem;
}
.wire-breadcrumb__home {
color: var(--breadcrumb-accent);
}
.wire-breadcrumb__label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__link,
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__current,
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__home {
color: inherit;
}
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__link:hover,
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
color: #ffffff;
background: rgba(255, 255, 255, 0.12);
}
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__separator {
color: rgba(255, 255, 255, 0.62);
}
@media (max-width: 639px) {
.wire-breadcrumb__home-label {
display: none;
}
.wire-breadcrumb__current {
max-width: 13rem;
}
}
@media (prefers-reduced-motion: reduce) {
.wire-breadcrumb__link {
transition: none;
}
}
}
}
-92
View File
@@ -1,92 +0,0 @@
component ButtonGroup {
outputs {
click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
change(payload: { value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
}
props {
items: unknown[] = []
value: string = ""
size: string = "md"
color: string = "primary"
variant: string = "default"
orientation: string = "horizontal"
responsive: boolean = false
attached: boolean = true
selectable: boolean = false
toolbar: boolean = false
disabled: boolean = false
ariaLabel: string = "Button group"
class: string = ""
}
state selectedValue = value
functions {
shared function itemValue(item, index) {
return item.value || item.label || String(index)
}
client function selectItem(item, index) {
previousValue = selectedValue
nextValue = itemValue(item, index)
output.select({
value: nextValue,
previousValue: previousValue,
item: item,
index: index
})
if (selectable && previousValue !== nextValue) {
selectedValue = nextValue
output.change({
value: nextValue,
previousValue: previousValue,
item: item,
index: index
})
}
}
}
view {
<div
{...attrs}
class="wire-next wire-next--button-group {class}"
role="{toolbar ? 'toolbar' : 'group'}"
aria-label="{ariaLabel}"
aria-orientation="{orientation}"
data-size="{size}"
data-color="{color}"
data-variant="{variant}"
data-orientation="{orientation}"
data-responsive="{responsive}"
data-attached="{attached}"
data-selectable="{selectable}"
data-disabled="{disabled}"
>
{#if items}
{#each items as item, index}
<button
type="{item.type || 'button'}"
class="wire-btn wire-btn--variant-{item.variant || variant} wire-btn--color-{item.color || color} wire-btn--size-{item.size || size}"
disabled="{disabled || item.disabled}"
aria-label="{item.ariaLabel || item.label}"
aria-pressed="{selectable ? (selectedValue === (item.value || item.label || String(index)) ? 'true' : 'false') : ''}"
data-value="{item.value || item.label || String(index)}"
data-selected="{selectable && selectedValue === (item.value || item.label || String(index)) ? 'true' : 'false'}"
@click="selectItem(item, index)"
>
{#if item.icon}
<span class="wire-btn__icon {item.icon}" aria-hidden="true"></span>
{/if}
<span class="wire-btn__label">{item.label}</span>
</button>
{/each}
{/if}
<slot />
</div>
}
}
-464
View File
@@ -1,464 +0,0 @@
// Interactive, hydration-safe content carousel.
component Carousel {
outputs {
initialize(payload: { index: number; count: number })
change(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object; reason: string | boolean })
previous(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object })
next(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object })
play(payload: { index: number; interval: number })
pause(payload: { index: number })
reachStart(payload: { index: number })
reachEnd(payload: { index: number })
dragStart(payload: { index: number; x: null })
dragEnd(payload: { index: number; distance: number })
}
props {
size: string = "default"
color: string = "primary"
title: string = ""
description: string = ""
items: unknown[] = []
activeIndex: number = 0
slidesPerView: number = 1
gap: string = "0.75rem"
showPagination: boolean = false
isAutoPlay: boolean = false
autoplayInterval: number = 4000
isInfiniteLoop: boolean = false
isRTL: boolean = false
isCentered: boolean = false
isDraggable: boolean = false
isAutoHeight: boolean = false
isSnap: boolean = false
showCounter: boolean = false
thumbnails: string = "none"
ariaLabel: string = "Content carousel"
variant: string = "default"
class: string = ""
}
state currentIndex = activeIndex
state playing: boolean = false
state dragOrigin = null
state dragOffset: number = 0
state autoplayTimer = null
state carouselRoot = null
functions {
shared function slideCount() {
return items.length
}
shared function maximumIndex(count, visible) {
count = slideCount()
visible = Math.max(1, Number(slidesPerView) || 1)
if (isCentered || isSnap) {
return Math.max(0, count - 1)
}
return Math.max(0, count - visible)
}
shared function normalizedIndex(index, maximum) {
maximum = maximumIndex()
if (slideCount() === 0) {
return 0
}
if (isInfiniteLoop) {
if (index < 0) {
return maximum
}
if (index > maximum) {
return 0
}
}
return Math.max(0, Math.min(index, maximum))
}
client function rememberRoot(sourceEvent, root) {
if (!sourceEvent || !sourceEvent.currentTarget) {
return
}
root = sourceEvent.currentTarget.closest(".wire-next--carousel")
if (root) {
carouselRoot = root
}
}
client function syncNavigation(sourceEvent, index, root, viewport, slides, slide, thumbnails, thumbnail, rail) {
rememberRoot(sourceEvent)
root = carouselRoot
if (!root) {
return
}
if (isSnap) {
viewport = root.querySelector(".wire-next__carousel-viewport")
slides = root.querySelectorAll(".wire-next__carousel-slide")
slide = slides[index]
if (viewport && slide) {
viewport.scrollTo({
left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2,
behavior: "smooth"
})
}
}
thumbnails = root.querySelectorAll(".wire-next__carousel-thumbnails button")
if (thumbnails[index]) {
thumbnail = thumbnails[index]
rail = thumbnail.parentElement
rail.scrollTo({
left: thumbnail.offsetLeft - (rail.clientWidth - thumbnail.clientWidth) / 2,
top: thumbnail.offsetTop - (rail.clientHeight - thumbnail.clientHeight) / 2,
behavior: "smooth"
})
}
}
client function handleSnapScroll(sourceEvent, slides, viewport, viewportCenter, closestIndex, closestDistance, slide, slideCenter, distance, previousIndex) {
if (!isSnap) {
return
}
rememberRoot(sourceEvent)
viewport = sourceEvent.currentTarget
slides = viewport.querySelectorAll(".wire-next__carousel-slide")
viewportCenter = viewport.scrollLeft + viewport.clientWidth / 2
closestIndex = currentIndex
closestDistance = 1000000000
slides.forEach(function (candidate, index) {
slideCenter = candidate.offsetLeft + candidate.clientWidth / 2
distance = Math.abs(slideCenter - viewportCenter)
if (distance < closestDistance) {
closestDistance = distance
closestIndex = index
}
})
closestIndex = normalizedIndex(closestIndex)
if (closestIndex !== currentIndex) {
previousIndex = currentIndex
currentIndex = closestIndex
output.change({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex],
reason: "snap"
})
slides = carouselRoot.querySelectorAll(".wire-next__carousel-thumbnails button")
slide = slides[currentIndex]
if (slide) {
viewport = slide.parentElement
viewport.scrollTo({
left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2,
top: slide.offsetTop - (viewport.clientHeight - slide.clientHeight) / 2,
behavior: "smooth"
})
}
}
}
client function selectSlide(index, reason, sourceEvent, previousIndex) {
if (slideCount() === 0) {
return
}
previousIndex = currentIndex
currentIndex = normalizedIndex(Number(index))
syncNavigation(sourceEvent, currentIndex)
if (previousIndex === currentIndex && reason !== "initialize") {
return
}
output.change({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex],
reason: reason || "select"
})
if (currentIndex === 0) {
output.reachStart({ index: currentIndex })
}
if (currentIndex === maximumIndex()) {
output.reachEnd({ index: currentIndex })
}
}
client function previousSlide(sourceEvent, previousIndex) {
previousIndex = currentIndex
selectSlide(currentIndex - 1, "previous", sourceEvent)
output.previous({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex]
})
}
client function nextSlide(sourceEvent, previousIndex) {
previousIndex = currentIndex
selectSlide(currentIndex + 1, "next", sourceEvent)
output.next({
index: currentIndex,
previousIndex: previousIndex,
item: items[currentIndex]
})
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "ArrowLeft") {
sourceEvent.preventDefault()
if (isRTL) {
nextSlide(sourceEvent)
} else {
previousSlide(sourceEvent)
}
}
if (sourceEvent.key === "ArrowRight") {
sourceEvent.preventDefault()
if (isRTL) {
previousSlide(sourceEvent)
} else {
nextSlide(sourceEvent)
}
}
if (sourceEvent.key === "Home") {
sourceEvent.preventDefault()
selectSlide(0, "keyboard", sourceEvent)
}
if (sourceEvent.key === "End") {
sourceEvent.preventDefault()
selectSlide(maximumIndex(), "keyboard", sourceEvent)
}
}
client function beginDrag(sourceEvent) {
if (!isDraggable || isSnap) {
return
}
sourceEvent.preventDefault()
dragOrigin = sourceEvent.clientX
dragOffset = 0
sourceEvent.currentTarget.setPointerCapture(sourceEvent.pointerId)
output.dragStart({ index: currentIndex, x: dragOrigin })
}
client function moveDrag(sourceEvent) {
if (!isDraggable || isSnap || dragOrigin === null) {
return
}
sourceEvent.preventDefault()
dragOffset = sourceEvent.clientX - dragOrigin
}
shared function cancelDrag() {
dragOrigin = null
dragOffset = 0
}
client function endDrag(sourceEvent, distance) {
if (!isDraggable || isSnap || dragOrigin === null) {
return
}
sourceEvent.preventDefault()
distance = dragOffset || sourceEvent.clientX - dragOrigin
dragOrigin = null
dragOffset = 0
if (Math.abs(distance) > 20) {
if ((distance < 0 && !isRTL) || (distance > 0 && isRTL)) {
nextSlide(sourceEvent)
} else {
previousSlide(sourceEvent)
}
}
output.dragEnd({
index: currentIndex,
distance: distance
})
}
shared function advanceAutoplay() {
if (currentIndex >= maximumIndex()) {
selectSlide(0, "autoplay")
} else {
selectSlide(currentIndex + 1, "autoplay")
}
}
client function startAutoplay() {
if (!isAutoPlay || playing || slideCount() < 2) {
return
}
playing = true
autoplayTimer = setInterval(advanceAutoplay, Math.max(1000, Number(autoplayInterval)))
output.play({ index: currentIndex, interval: autoplayInterval })
}
client function pauseAutoplay() {
if (autoplayTimer) {
clearInterval(autoplayTimer)
}
autoplayTimer = null
if (playing) {
output.pause({ index: currentIndex })
}
playing = false
}
}
lifecycle {
mount {
output.initialize({ index: currentIndex, count: slideCount() })
startAutoplay()
}
unmount {
if (autoplayTimer) {
clearInterval(autoplayTimer)
}
}
}
view {
<section
{...attrs}
class="wire-next wire-next--carousel wire-next--color-{color} wire-next--size-{size} wire-next--variant-{variant} {class}"
data-rtl="{isRTL}"
data-centered="{isCentered}"
data-draggable="{isDraggable && !isSnap}"
data-dragging="{dragOrigin !== null}"
data-auto-height="{isAutoHeight}"
data-snap="{isSnap}"
data-thumbnails="{thumbnails}"
dir="{isRTL ? 'rtl' : 'ltr'}"
role="region"
aria-roledescription="carousel"
aria-label="{ariaLabel}"
@keydown="handleKeydown(event)"
@mouseenter="rememberRoot(event); pauseAutoplay()"
@mouseleave="startAutoplay()"
@focusin="rememberRoot(event); pauseAutoplay()"
@focusout="startAutoplay()"
>
{#if title || description}
<header class="wire-next__carousel-header">
{#if title}<h3>{title}</h3>{/if}
{#if description}<p>{description}</p>{/if}
</header>
{/if}
<div class="wire-next__carousel-layout">
{#if thumbnails === "vertical"}
<div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
data-active="{index === currentIndex}"
aria-label="Show slide {index + 1}: {item.label || item.title}"
aria-current="{index === currentIndex ? 'true' : 'false'}"
@click="selectSlide(index, 'thumbnail', event)"
>
{#if item.thumbnail}<img src="{item.thumbnail}" alt="" />{/if}
<span>{item.label || item.title || "Slide " + (index + 1)}</span>
</button>
{/each}
</div>
{/if}
<div class="wire-next__carousel-main">
<div class="wire-next__carousel-stage">
<div
class="wire-next__carousel-viewport"
tabindex="0"
@scroll="handleSnapScroll(event)"
@pointerdown="beginDrag(event)"
@pointermove="moveDrag(event)"
@pointerup="endDrag(event)"
@pointercancel="cancelDrag()"
@lostpointercapture="cancelDrag()"
>
<div
class="wire-next__carousel-track"
style="--wire-carousel-index: {currentIndex}; --wire-carousel-per-view: {slidesPerView}; --wire-carousel-gap: {gap}; --wire-carousel-drag-offset: {dragOffset}px"
>
{#each items as item, index}
<article
class="wire-next__carousel-slide"
data-active="{index === currentIndex}"
role="group"
aria-roledescription="slide"
aria-label="{index + 1} of {items.length}"
aria-hidden="{index === currentIndex ? 'false' : 'true'}"
>
{#if item.imageSrc}
<img src="{item.imageSrc}" alt="{item.imageAlt || item.title || ''}" />
{/if}
<div class="wire-next__carousel-slide-content">
{#if item.eyebrow}<span>{item.eyebrow}</span>{/if}
{#if item.title || item.label}<h4>{item.title || item.label}</h4>{/if}
{#if item.description}<p>{item.description}</p>{/if}
{#if item.actionLabel}
<a href="{item.actionHref || '#'}">{item.actionLabel}</a>
{/if}
</div>
</article>
{/each}
<slot />
</div>
</div>
<button
class="wire-next__carousel-control wire-next__carousel-control--previous"
type="button"
aria-label="Previous slide"
disabled="{!isInfiniteLoop && currentIndex === 0}"
@click="previousSlide(event)"
>
<span class="icon-[lucide--chevron-left]" aria-hidden="true"></span>
</button>
<button
class="wire-next__carousel-control wire-next__carousel-control--next"
type="button"
aria-label="Next slide"
disabled="{!isInfiniteLoop && currentIndex === maximumIndex()}"
@click="nextSlide(event)"
>
<span class="icon-[lucide--chevron-right]" aria-hidden="true"></span>
</button>
{#if showCounter}
<output class="wire-next__carousel-counter" aria-live="polite">
{currentIndex + 1} / {items.length}
</output>
{/if}
</div>
{#if showPagination}
<div class="wire-next__carousel-pagination" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
data-active="{index === currentIndex}"
aria-label="Show slide {index + 1}"
aria-current="{index === currentIndex ? 'true' : 'false'}"
@click="selectSlide(index, 'pagination', event)"
></button>
{/each}
</div>
{/if}
{#if thumbnails === "horizontal"}
<div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
data-active="{index === currentIndex}"
aria-label="Show slide {index + 1}: {item.label || item.title}"
aria-current="{index === currentIndex ? 'true' : 'false'}"
@click="selectSlide(index, 'thumbnail', event)"
>
{#if item.thumbnail}<img src="{item.thumbnail}" alt="" />{/if}
<span>{item.label || item.title || "Slide " + (index + 1)}</span>
</button>
{/each}
</div>
{/if}
</div>
</div>
</section>
}
}
-25
View File
@@ -1,25 +0,0 @@
component Chart {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
dataPointClick(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
legendToggle(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
size: string = "default"
color: string = "primary"
title: string = "Chart"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chart wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
-150
View File
@@ -1,150 +0,0 @@
component ChatBubble {
outputs {
action(payload: { action: boolean; item: string | number | boolean | null | object; index: number })
messageClick(payload: { item: string | number | boolean | null | object; index: number; direction: string })
avatarClick(payload: { item: string | number | boolean | null | object; index: number; direction: string })
linkClick(payload: { link: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
}
props {
size: string = "default"
color: string = "primary"
title: string = ""
description: string = ""
items: unknown[] = []
oneSided: boolean = false
showAvatars: boolean = false
showMetadata: boolean = false
ariaLabel: string = "Conversation"
variant: string = "default"
class: string = ""
}
functions {
shared function messageDirection(item) {
return item.direction === "outgoing" ? "outgoing" : "incoming"
}
client function selectMessage(item, index) {
output.messageClick({
item: item,
index: index,
direction: messageDirection(item)
})
}
client function selectAvatar(sourceEvent, item, index) {
sourceEvent.stopPropagation()
output.avatarClick({
item: item,
index: index,
direction: messageDirection(item)
})
}
client function selectLink(sourceEvent, link, item, index) {
sourceEvent.stopPropagation()
output.linkClick({
link: link,
item: item,
index: index
})
}
client function selectAction(sourceEvent, item, index) {
sourceEvent.stopPropagation()
output.action({
action: item.action || "retry",
item: item,
index: index
})
}
}
view {
<section
{...attrs}
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chat-bubble wire-next--variant-{variant} {class}"
data-one-sided="{oneSided}"
role="log"
aria-label="{ariaLabel}"
aria-live="polite"
>
{#if title || description}
<header class="wire-next__chat-header">
{#if title}<h3>{title}</h3>{/if}
{#if description}<p>{description}</p>{/if}
</header>
{/if}
{#if items.length}
<div class="wire-next__chat-thread">
{#each items as item, index}
<article
class="wire-next__chat-message"
data-direction="{messageDirection(item)}"
aria-label="{messageDirection(item) === 'outgoing' ? 'Sent message' : 'Received message'}"
>
<div class="wire-next__chat-row">
{#if showAvatars && (item.avatarSrc || item.avatarFallback)}
<button
class="wire-next__chat-avatar"
type="button"
aria-label="{item.avatarLabel || item.author || 'Message author'}"
@click="selectAvatar(event, item, index)"
>
{#if item.avatarSrc}
<img src="{item.avatarSrc}" alt="{item.avatarAlt || ''}" />
{:else}
<span>{item.avatarFallback}</span>
{/if}
</button>
{/if}
<div
class="wire-next__chat-content"
role="button"
tabindex="0"
@click="selectMessage(item, index)"
@keydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectMessage(item, index) }"
>
{#if item.title}<h4>{item.title}</h4>{/if}
{#if item.text}<p>{item.text}</p>{/if}
{#if item.bullets && item.bullets.length}
<ul>
{#each item.bullets as bullet}<li>{bullet}</li>{/each}
</ul>
{/if}
{#if item.links && item.links.length}
<nav aria-label="Message links">
{#each item.links as link}
<a
href="{link.href || '#'}"
@click="selectLink(event, link, item, index)"
>{link.label}</a>
{/each}
</nav>
{/if}
</div>
</div>
{#if showMetadata && (item.timestamp || item.status || item.actionLabel)}
<footer class="wire-next__chat-meta" data-tone="{item.statusTone || 'muted'}">
{#if item.statusIcon}<span class="{item.statusIcon}" aria-hidden="true"></span>{/if}
{#if item.status}<span>{item.status}</span>{/if}
{#if item.timestamp}<time datetime="{item.datetime || ''}">{item.timestamp}</time>{/if}
{#if item.actionLabel}
<button type="button" @click="selectAction(event, item, index)">
{item.actionLabel}
</button>
{/if}
</footer>
{/if}
</article>
{/each}
</div>
{/if}
<slot />
</section>
}
}
-25
View File
@@ -1,25 +0,0 @@
component Clipboard {
outputs {
copy(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
success(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Clipboard"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--clipboard wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
-98
View File
@@ -1,98 +0,0 @@
component Collapse {
outputs {
toggle(payload: { index: number; item: string | number | boolean | null | object; open: boolean; openIndexes: number[] })
open(payload: { index: number; item: string | number | boolean | null | object })
close(payload: { index: number; item: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
items: unknown[] = []
multiple: boolean = false
mode: string = "panel"
initialOpenIndexes: unknown[] = []
ariaLabel: string = "Collapsible content"
class: string = ""
}
state openIndexes = initialOpenIndexes
functions {
shared function isOpen(index) {
return openIndexes.includes(index)
}
client function toggleItem(index, item, opening) {
if (item.disabled) {
return
}
opening = !isOpen(index)
if (opening) {
if (multiple) {
openIndexes = openIndexes.concat(index)
} else {
openIndexes = [index]
}
output.open({ index: index, item: item })
} else {
openIndexes = openIndexes.filter((openIndex) => openIndex !== index)
output.close({ index: index, item: item })
}
output.toggle({
index: index,
item: item,
open: opening,
openIndexes: openIndexes
})
}
}
view {
<section
{...attrs}
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--collapse {class}"
data-mode="{mode}"
aria-label="{ariaLabel}"
>
{#each items as item, index}
<article class="wire-next__collapse-item" data-open="{isOpen(index)}">
{#if mode === "inline" && item.preview}
<p class="wire-next__collapse-preview">{item.preview}</p>
{/if}
<button
class="wire-next__collapse-trigger"
type="button"
aria-expanded="{isOpen(index) ? 'true' : 'false'}"
aria-controls="{item.id || 'collapse-panel-' + index}"
disabled="{item.disabled}"
@click="toggleItem(index, item)"
>
<span>{isOpen(index) ? (item.closeLabel || "Read less") : item.label}</span>
<span class="icon-[lucide--chevron-down] wire-next__collapse-chevron" aria-hidden="true"></span>
</button>
<div
id="{item.id || 'collapse-panel-' + index}"
class="wire-next__collapse-panel"
data-open="{isOpen(index)}"
aria-hidden="{isOpen(index) ? 'false' : 'true'}"
inert="{!isOpen(index)}"
>
<div class="wire-next__collapse-content">
{#if item.title}<h4>{item.title}</h4>{/if}
{#if item.content}<p>{item.content}</p>{/if}
{#if item.links && item.links.length}
<nav aria-label="Related links">
{#each item.links as link}<a href="{link.href || '#'}">{link.label}</a>{/each}
</nav>
{/if}
</div>
</div>
</article>
{/each}
<slot />
</section>
}
}
-41
View File
@@ -1,41 +0,0 @@
component ColorPicker {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "Color"
hiddenLabel: boolean = false
placeholder: string = ""
value: string = "#2563eb"
icon: string = ""
iconPosition: string = "start"
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
variant: string = "normal"
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
functions {
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--color-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__color-control" data-icon-position="{iconPosition}">{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<input id="{id || name}" type="color" name="{name}" value="{value}" disabled="{disabled}" required="{required}" readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" /><output>{value}</output></div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
-35
View File
@@ -1,35 +0,0 @@
component Columns {
props {
size: string = "default"
color: string = "primary"
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
class: string = ""
}
view {
<div
data-ui-component="Columns"
data-size='{size}'
data-color='{color}'
data-columns='{columns}'
data-gap='{gap}'
class='grid w-full grid-cols-1 items-start {class}'
class:max-w-3xl='maxWidth === "md"'
class:max-w-5xl='maxWidth === "lg"'
class:max-w-7xl='maxWidth === "xl"'
class:max-w-screen-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
class:md:grid-cols-2='columns >= 2'
class:lg:grid-cols-3='columns === 3'
class:lg:grid-cols-4='columns === 4'
class:gap-3='gap === "sm"'
class:gap-6='gap === "md"'
class:gap-10='gap === "lg"'
class:gap-14='gap === "xl"'
>
<slot></slot>
</div>
}
}
-389
View File
@@ -1,389 +0,0 @@
component ComboBox {
outputs {
search(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
label: string = "ComboBox"
name: string = ""
value: string = ""
options: unknown[] = []
groups: unknown[] = []
placeholder: string = "Search or select an option"
searchPlaceholder: string = "Start typing…"
clearable: boolean = true
allowCustomValue: boolean = false
disabled: boolean = false
required: boolean = false
invalid: boolean = false
validationMessage: string = ""
helpText: string = ""
loading: boolean = false
loadingLabel: string = "Loading suggestions…"
emptyLabel: string = "No matching options"
clearLabel: string = "Clear value"
toggleLabel: string = "Toggle suggestions"
searchMode: string = "contains"
searchFields: string = "label,description"
minSearchLength: number = 0
searchResultLimit: number = 0
optionTemplate: string = "default"
defaultOpen: boolean = false
closeOnSelect: boolean = true
fixed: boolean = false
placement: string = "bottom"
autocomplete: string = "off"
remote: boolean = false
remoteUrl: string = ""
remoteQueryParam: string = "q"
remoteDebounce: number = 250
remoteAutoLoad: boolean = true
infinite: boolean = false
hasMore: boolean = false
page: number = 1
loadMoreLabel: string = "Load more"
class: string = ""
}
state open = defaultOpen
state query: string = ""
state selectedValue = value
state activeIndex: number = -1
functions {
shared function allOptions() {
return [...groups.flatMap((group) => group.options || []), ...options]
}
shared function searchableText(option) {
return ((option.label || "") + " " + (option.description || "")).toLowerCase()
}
shared function matches(option) {
if (!query || query.length < minSearchLength) {
return true
}
if (searchMode === "startsWith") {
return searchableText(option).startsWith(query.toLowerCase())
}
if (searchMode === "exact") {
return searchableText(option) === query.toLowerCase()
}
return searchableText(option).includes(query.toLowerCase())
}
shared function matchingOptions(list) {
return list.filter((option) => matches(option))
}
shared function visibleOptions(list) {
if (searchResultLimit > 0) {
return matchingOptions(list).slice(0, searchResultLimit)
}
return matchingOptions(list)
}
shared function flatVisibleOptions() {
return visibleOptions(allOptions())
}
shared function isSelected(option) {
return selectedValue === option.value
}
shared function selectedOptions() {
return allOptions().filter((option) => isSelected(option))
}
shared function selectedText() {
return selectedOptions().length ? selectedOptions()[0].label : selectedValue
}
shared function inputText() {
return query || selectedText()
}
shared function shouldShowDropdown() {
if (!open) {
return false
}
if (loading) {
return true
}
if (remote && query.length !== 0 && query.length < minSearchLength) {
return true
}
if (flatVisibleOptions().length) {
return true
}
return infinite && hasMore
}
shared function canSelect(option) {
return !option.disabled
}
shared function chooseOption(option) {
if (!canSelect(option)) {
return
}
selectedValue = option.value
query = option.label
activeIndex = optionIndex(option)
if (closeOnSelect) {
open = false
}
}
client function updateQuery(event) {
query = event.target.value
selectedValue = allowCustomValue ? event.target.value : ""
open = true
activeIndex = flatVisibleOptions().length ? 0 : -1
}
client function clearValue(event) {
if (event) {
event.stopPropagation()
}
query = ""
selectedValue = ""
activeIndex = -1
open = false
}
shared function openDropdown() {
if (!disabled) {
open = true
activeIndex = flatVisibleOptions().length ? 0 : -1
}
}
shared function closeDropdown() {
open = false
}
shared function toggle() {
if (open) {
closeDropdown()
} else {
openDropdown()
}
}
shared function setValue(nextValue) {
selectedValue = nextValue
query = ""
}
shared function moveActive(direction) {
if (!flatVisibleOptions().length) {
return
}
activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
}
client function handleKeydown(event) {
if (disabled) {
return
}
if (event.key === "ArrowDown") {
event.preventDefault()
openDropdown()
moveActive(1)
} else if (event.key === "ArrowUp") {
event.preventDefault()
openDropdown()
moveActive(-1)
} else if (event.key === "Enter" && open && activeIndex >= 0) {
event.preventDefault()
chooseOption(flatVisibleOptions()[activeIndex])
} else if (event.key === "Escape") {
closeDropdown()
} else if (event.key === "Home" && open) {
event.preventDefault()
activeIndex = 0
} else if (event.key === "End" && open) {
event.preventDefault()
activeIndex = flatVisibleOptions().length - 1
}
}
shared function optionIndex(option) {
return flatVisibleOptions().findIndex((item) => item.value === option.value)
}
}
view {
<div
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-select wire-next--combobox {open ? 'wire-next--open' : ''} {fixed ? 'wire-next--advanced-select-fixed' : ''} {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
data-placement="{placement}"
data-search-mode="{searchMode}"
data-remote="{remote ? 'true' : 'false'}"
data-remote-url="{remoteUrl}"
data-remote-query-param="{remoteQueryParam}"
data-remote-debounce="{remoteDebounce}"
data-remote-auto-load="{remoteAutoLoad ? 'true' : 'false'}"
data-infinite="{infinite ? 'true' : 'false'}"
data-page="{page}"
data-wrn-select
data-wrn-combobox
@focusout="if (!event.currentTarget.contains(event.relatedTarget)) { closeDropdown() }"
>
<label id="{name}-label" for="{name}-input">{label}</label>
<div class="wire-next__select-control wire-next__combobox-control">
<span class="wire-next__search-icon icon-[lucide--search]" aria-hidden="true"></span>
<input
{...attrs}
id="{name}-input"
class="wire-next__select-trigger wire-next__combobox-input"
type="text"
role="combobox"
name="{allowCustomValue ? name : ''}"
value="{inputText()}"
placeholder="{placeholder}"
autocomplete="{autocomplete}"
aria-autocomplete="list"
aria-haspopup="listbox"
aria-expanded="{open}"
aria-controls="{name}-listbox"
aria-labelledby="{name}-label"
aria-invalid="{invalid}"
disabled="{disabled}"
required="{required && allowCustomValue}"
@focus="openDropdown()"
@click="openDropdown()"
@input="updateQuery(event)"
@keydown="handleKeydown(event)"
/>
<button
type="button"
class="wire-next__clear-select"
data-show="clearable && inputText() && !disabled"
aria-label="{clearLabel}"
title="{clearLabel}"
@click="clearValue(event)"
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
<button
type="button"
class="wire-next__combobox-toggle"
aria-label="{toggleLabel}"
tabindex="-1"
disabled="{disabled}"
@click="toggle()"
>
<span class="wire-next__select-chevron icon-[lucide--chevrons-up-down]" aria-hidden="true"></span>
</button>
</div>
<div
class="wire-next__select-dropdown {fixed ? 'wire-next__select-dropdown--fixed' : ''}"
data-show="shouldShowDropdown()"
style="{shouldShowDropdown() ? '' : 'display: none'}"
>
<div
class="wire-next__select-message"
data-show="remote && query.length !== 0 && query.length < minSearchLength"
>Enter at least {minSearchLength} characters.</div>
<div class="wire-next__select-message" data-show="loading" role="status">
<i class="wire-spinner wire-spinner--inline" aria-hidden="true"></i>
{loadingLabel}
</div>
<div
id="{name}-listbox"
class="wire-next__select-list"
data-show="(!remote || (query.length === 0 && remoteAutoLoad) || query.length >= minSearchLength) && !loading"
role="listbox"
>
{#each groups as group}
{#if visibleOptions(group.options || []).length}
<div class="wire-next__option-group" role="group" aria-label="{group.label}">
<div class="wire-next__group-label">{group.label}</div>
{#each group.options || [] as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@mousedown="event.preventDefault()"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i class="wire-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
</button>
{/each}
</div>
{/if}
{/each}
{#each options as option}
<button
type="button"
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
aria-selected="{isSelected(option)}"
disabled="{!canSelect(option)}"
@mouseenter="activeIndex = optionIndex(option)"
@mousedown="event.preventDefault()"
@click="chooseOption(option)"
>
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
<i class="wire-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
</button>
{/each}
</div>
<button
type="button"
class="wire-next__load-more"
data-wrn-select-load-more
data-show="infinite && hasMore"
>{loadMoreLabel}</button>
</div>
{#if !allowCustomValue}
<input
type="hidden"
name="{name}"
value="{selectedValue}"
required="{required}"
data-error="{name}"
aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
/>
{/if}
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="wire-next__validation"
data-error="{name}"
>{validationMessage}</small>
</div>
}
}
-24
View File
@@ -1,24 +0,0 @@
component Confetti {
outputs {
start(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Confetti"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--confetti wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
-526
View File
@@ -1,526 +0,0 @@
component ContextMenu {
outputs {
open(payload: { x: number; y: number; trigger: string; sourceEvent: Event })
close(payload: { reason: string; sourceEvent: Event })
select(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
action(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
}
props {
items: unknown[] = []
open: boolean = false
defaultOpen: boolean = false
trigger: string = "contextmenu"
placement: string = "pointer"
align: string = "start"
size: string = "default"
color: string = "primary"
variant: string = "raised"
title: string = ""
description: string = ""
label: string = "Context menu"
closeOnSelect: boolean = true
closeOnOutside: boolean = true
disabled: boolean = false
minWidth: string = "14rem"
maxWidth: string = "20rem"
class: string = ""
}
state visible = defaultOpen
state positionX: number = 16
state positionY: number = 16
functions {
shared function isOpen() {
return open || visible
}
client function showMenu(sourceEvent) {
if (disabled) {
return
}
if (sourceEvent && sourceEvent.type === "contextmenu") {
sourceEvent.preventDefault()
}
if (placement === "pointer" && sourceEvent) {
// Place the menu at the pointer and let the anchored clamp in the
// runtime pull it back on screen once it has been laid out and can
// actually be measured. Subtracting a guessed 340x420 here instead
// pushed every menu that was not that size away from the pointer.
positionX = Math.max(12, sourceEvent.clientX || 12)
positionY = Math.max(12, sourceEvent.clientY || 12)
}
visible = true
output.open({
x: positionX,
y: positionY,
trigger: trigger,
sourceEvent: sourceEvent
})
}
client function hideMenu(reason, sourceEvent) {
visible = false
output.close({
reason: reason,
sourceEvent: sourceEvent
})
}
client function toggleMenu(sourceEvent) {
if (isOpen()) {
hideMenu("toggle", sourceEvent)
} else {
showMenu(sourceEvent)
}
}
client function chooseItem(item, itemIndex, sourceEvent) {
if (disabled || item.disabled || item.type === "header" || item.type === "divider") {
sourceEvent.preventDefault()
return
}
output.select({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
if (item.action) {
output.action({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
}
if (closeOnSelect) {
hideMenu("select", sourceEvent)
}
}
client function moveFocus(sourceEvent, direction) {
const root = sourceEvent.currentTarget.closest(".wire-context-menu") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-context-menu-item]:not([disabled])")]
if (!options.length) {
return
}
const activeIndex = options.indexOf(document.activeElement)
const nextIndex = (activeIndex + direction + options.length) % options.length
options[nextIndex].focus()
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
hideMenu("escape", sourceEvent)
} else if (sourceEvent.key === "ArrowDown") {
sourceEvent.preventDefault()
moveFocus(sourceEvent, 1)
} else if (sourceEvent.key === "ArrowUp") {
sourceEvent.preventDefault()
moveFocus(sourceEvent, -1)
}
}
}
view {
<div
{...attrs}
data-ui-component="ContextMenu"
data-open='{open || visible ? "true" : "false"}'
data-trigger='{trigger}'
data-placement='{placement}'
data-align='{align}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='wire-context-menu {class}'
style='--wire-context-x:{positionX}px;--wire-context-y:{positionY}px;--wire-context-min-width:{minWidth};--wire-context-max-width:{maxWidth};'
@keydown='handleKeydown(event)'
>
<div
class="wire-context-menu__trigger"
tabindex='{disabled ? "-1" : "0"}'
aria-haspopup="menu"
aria-expanded='{open || visible ? "true" : "false"}'
@contextmenu='if (trigger === "contextmenu" || trigger === "both") { showMenu(event) }'
@click='if (trigger === "click" || trigger === "both") { toggleMenu(event) }'
@keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); toggleMenu(event) }'
>
<slot name="trigger"></slot>
</div>
{#if closeOnOutside}
<button
type="button"
class="wire-context-menu__dismiss-layer"
data-show='{open || visible}'
aria-label="Close context menu"
@click='hideMenu("outside", event)'
></button>
{/if}
<div
class="wire-context-menu__panel"
data-wrn-anchored="true"
data-show='{open || visible}'
role="menu"
aria-label='{label}'
>
{#if title || description}
<div class="wire-context-menu__header">
{#if title}
<strong>{title}</strong>
{/if}
{#if description}
<span>{description}</span>
{/if}
</div>
{/if}
<slot name="header"></slot>
<div class="wire-context-menu__items">
{#each items as item, itemIndex}
{#if item.type === "divider"}
<div class="wire-context-menu__divider" role="separator"></div>
{:else if item.type === "header"}
<div class="wire-context-menu__section-label">{item.label || item.title}</div>
{:else if item.href}
<a
href='{item.href}'
target='{item.target || ""}'
rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
role="menuitem"
data-context-menu-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
class="wire-context-menu__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-context-menu__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
{/if}
</span>
{#if item.shortcut}
<kbd>{item.shortcut}</kbd>
{:else if item.checked || item.selected}
<span class="icon-[lucide--check] wire-context-menu__status" aria-hidden="true"></span>
{:else if item.external}
<span class="icon-[lucide--arrow-up-right] wire-context-menu__status" aria-hidden="true"></span>
{/if}
</a>
{:else}
<button
type="button"
role="menuitem"
data-context-menu-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
disabled='{item.disabled}'
class="wire-context-menu__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-context-menu__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
{/if}
</span>
{#if item.shortcut}
<kbd>{item.shortcut}</kbd>
{:else if item.checked || item.selected}
<span class="icon-[lucide--check] wire-context-menu__status" aria-hidden="true"></span>
{/if}
</button>
{/if}
{/each}
</div>
<slot></slot>
<slot name="footer"></slot>
</div>
</div>
}
style {
.wire-context-menu {
--context-accent: var(--wire-color-primary);
--context-soft: var(--wire-color-primary-soft);
position: relative;
display: block;
min-width: 0;
}
.wire-context-menu[data-color="secondary"] {
--context-accent: var(--wire-color-secondary);
--context-soft: var(--wire-color-secondary-soft);
}
.wire-context-menu[data-color="info"] {
--context-accent: var(--wire-color-info);
--context-soft: var(--wire-color-info-soft);
}
.wire-context-menu[data-color="success"] {
--context-accent: var(--wire-color-success);
--context-soft: var(--wire-color-success-soft);
}
.wire-context-menu[data-color="warning"] {
--context-accent: var(--wire-color-warning);
--context-soft: var(--wire-color-warning-soft);
}
.wire-context-menu[data-color="danger"] {
--context-accent: var(--wire-color-danger);
--context-soft: var(--wire-color-danger-soft);
}
.wire-context-menu__trigger {
display: block;
min-width: 0;
outline: none;
}
.wire-context-menu__trigger:focus-visible {
border-radius: 0.6rem;
outline: 2px solid var(--wire-color-focus);
outline-offset: 3px;
}
.wire-context-menu__dismiss-layer {
position: fixed;
inset: 0;
z-index: 1090;
appearance: none;
padding: 0;
background: transparent;
border: 0;
}
.wire-context-menu__panel {
position: absolute;
z-index: 1091;
top: calc(100% + 0.5rem);
left: 0;
width: max-content;
min-width: var(--wire-context-min-width);
max-width: min(var(--wire-context-max-width), calc(100vw - 1.5rem));
padding: 0.45rem;
color: var(--wire-color-text);
background:
linear-gradient(145deg, color-mix(in srgb, var(--context-accent) 5%, transparent), transparent 58%),
color-mix(in srgb, var(--wire-color-surface-raised) 96%, transparent);
border: 1px solid color-mix(in srgb, var(--context-accent) 18%, var(--wire-color-border));
border-radius: 1rem;
box-shadow:
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
0 24px 64px color-mix(in srgb, black 24%, transparent);
backdrop-filter: blur(18px);
transform-origin: top left;
}
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
position: fixed;
top: var(--wire-context-y);
left: var(--wire-context-x);
}
.wire-context-menu[data-placement="bottom-end"] .wire-context-menu__panel {
right: 0;
left: auto;
transform-origin: top right;
}
.wire-context-menu[data-placement="top-start"] .wire-context-menu__panel {
top: auto;
bottom: calc(100% + 0.5rem);
transform-origin: bottom left;
}
.wire-context-menu[data-placement="top-end"] .wire-context-menu__panel {
top: auto;
right: 0;
bottom: calc(100% + 0.5rem);
left: auto;
transform-origin: bottom right;
}
.wire-context-menu[data-variant="soft"] .wire-context-menu__panel {
background:
linear-gradient(145deg, var(--context-soft), transparent 72%),
var(--wire-color-surface-raised);
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
}
.wire-context-menu[data-variant="outline"] .wire-context-menu__panel {
background: var(--wire-color-surface-raised);
box-shadow: none;
}
.wire-context-menu__header {
display: grid;
gap: 0.2rem;
padding: 0.65rem 0.75rem 0.75rem;
border-bottom: 1px solid var(--wire-color-border);
}
.wire-context-menu__header strong {
font-size: 0.86rem;
font-weight: 650;
}
.wire-context-menu__header span {
color: var(--wire-color-text-muted);
font-size: 0.75rem;
line-height: 1.45;
}
.wire-context-menu__items {
display: grid;
gap: 0.15rem;
padding-block: 0.25rem;
}
.wire-context-menu__section-label {
padding: 0.55rem 0.75rem 0.3rem;
color: var(--wire-color-text-muted);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.wire-context-menu__divider {
height: 1px;
margin: 0.3rem 0.45rem;
background: var(--wire-color-border);
}
.wire-context-menu__item {
appearance: none;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.7rem;
width: 100%;
min-width: 0;
padding: 0.65rem 0.7rem;
color: var(--wire-color-text);
background: transparent;
border: 0;
border-radius: 0.72rem;
font: inherit;
text-align: left;
text-decoration: none;
cursor: pointer;
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
}
.wire-context-menu__item:hover,
.wire-context-menu__item:focus-visible,
.wire-context-menu__item[data-selected="true"] {
color: var(--context-accent);
background: var(--context-soft);
outline: none;
}
.wire-context-menu__item:active {
transform: scale(0.985);
}
.wire-context-menu__item[data-danger="true"] {
color: var(--wire-color-danger);
}
.wire-context-menu__item[data-danger="true"]:hover,
.wire-context-menu__item[data-danger="true"]:focus-visible {
background: var(--wire-color-danger-soft);
}
.wire-context-menu__item[disabled],
.wire-context-menu__item[aria-disabled="true"] {
opacity: 0.48;
pointer-events: none;
}
.wire-context-menu__icon,
.wire-context-menu__status {
width: 1rem;
height: 1rem;
color: currentColor;
}
.wire-context-menu__copy {
display: grid;
gap: 0.12rem;
min-width: 0;
}
.wire-context-menu__copy strong {
overflow: hidden;
font-size: 0.82rem;
font-weight: 600;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-context-menu__copy small {
overflow: hidden;
color: var(--wire-color-text-muted);
font-size: 0.7rem;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-context-menu kbd {
padding: 0.16rem 0.38rem;
color: var(--wire-color-text-muted);
background: var(--wire-color-surface-soft);
border: 1px solid var(--wire-color-border);
border-radius: 0.36rem;
font-size: 0.64rem;
font-family: inherit;
}
.wire-context-menu[data-size="sm"] .wire-context-menu__item {
padding: 0.52rem 0.6rem;
}
.wire-context-menu[data-size="lg"] .wire-context-menu__item {
padding: 0.78rem 0.82rem;
}
@media (max-width: 639px) {
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
right: 0.75rem;
bottom: 0.75rem;
left: 0.75rem;
top: auto;
width: auto;
max-width: none;
}
}
@media (prefers-reduced-motion: reduce) {
.wire-context-menu__item {
transition: none;
}
}
}
}
-26
View File
@@ -1,26 +0,0 @@
component CopyMarkup {
outputs {
copy(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
success(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
}
props {
size: string = "default"
color: string = "primary"
label: string = "Copy Markup"
name: string = ""
value: string = ""
placeholder: string = ""
type: string = "text"
min: string = ""
max: string = ""
step: string = ""
disabled: boolean = false
required: boolean = false
class: string = ""
}
view {
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--copy-markup {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
}
}
@@ -1,17 +0,0 @@
component CustomScrollbar {
outputs {
scroll(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
size: string = "default"
color: string = "primary"
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
class: string = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--custom-scrollbar wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
}
}
-24
View File
@@ -1,24 +0,0 @@
component DataMap {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Data Map"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--data-map wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
File diff suppressed because it is too large Load Diff
-62
View File
@@ -1,62 +0,0 @@
component DatePicker {
outputs {
input(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
change(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
open(payload: { value: string; name: string; sourceEvent: Event })
close(payload: { value: string; name: string; sourceEvent: Event })
focus(payload: { value: string; name: string; sourceEvent: Event })
blur(payload: { value: string; name: string; sourceEvent: Event })
invalid(payload: { name: string; message: string; sourceEvent: Event })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Date Picker"
id: string = ""
name: string = ""
value: string = ""
placeholder: string = ""
type: string = "date"
locale: string = "en-US"
firstDayOfWeek: number = 0
months = [{ label: "Jan", value: "01" }, { label: "Feb", value: "02" }, { label: "Mar", value: "03" }, { label: "Apr", value: "04" }, { label: "May", value: "05" }, { label: "Jun", value: "06" }, { label: "Jul", value: "07" }, { label: "Aug", value: "08" }, { label: "Sep", value: "09" }, { label: "Oct", value: "10" }, { label: "Nov", value: "11" }, { label: "Dec", value: "12" }]
days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
years = [2024, 2025, 2026, 2027, 2028, 2029, 2030]
min: string = ""
max: string = ""
step: string = ""
helperText: string = ""
cornerHint: string = ""
error: string = ""
variant: string = "normal"
inline: boolean = false
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
state currentValue = value
state selectedYear = value ? value.split("-")[0] : ""
state selectedMonth = value ? value.split("-")[1] : ""
state selectedDay = value ? value.split("-")[2] : ""
state expanded: boolean = false
functions {
client function openCalendar(sourceEvent) { expanded = true; output.open({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function updateDate(part, nextValue, sourceEvent, nextDate, detail) { if (part === "year") { selectedYear = String(nextValue) } if (part === "month") { selectedMonth = String(nextValue).padStart(2, "0") } if (part === "day") { selectedDay = String(nextValue).padStart(2, "0") } if (!selectedYear || !selectedMonth || !selectedDay) { return } nextDate = selectedYear + "-" + selectedMonth + "-" + selectedDay; if ((min && nextDate < min) || (max && nextDate > max)) { return } currentValue = nextDate; detail = { value: currentValue, year: selectedYear, month: selectedMonth, day: selectedDay, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function finishDate(sourceEvent) { if (!currentValue) { return } expanded = false; output.close({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function selectDay(sourceEvent, root, input, nextDate, trigger, detail) { root = sourceEvent.currentTarget.closest(".wire-next--date-picker"); input = root.querySelector(".wire-next__picker-value"); nextDate = input.getAttribute("value").slice(0, 5) + root.querySelector("select").getAttribute("value") + "-" + sourceEvent.currentTarget.dataset.value; currentValue = nextDate; input.setAttribute("value", nextDate); trigger = root.querySelector(".wire-next__date-trigger span"); if (trigger) { trigger.replaceChildren(nextDate) } sourceEvent.currentTarget.setAttribute("aria-pressed", "true"); detail = { value: nextDate, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function clearDate(sourceEvent, detail) { if (disabled || readonly) { return } currentValue = ""; detail = { value: currentValue, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
client function handleFocus(sourceEvent) { output.focus({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function handleBlur(sourceEvent) { output.blur({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
client function handleInvalid(sourceEvent) { output.invalid({ name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--date-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}">
<div class="wire-next__field-heading"><label for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__picker-control"><input id="{id || name}" class="wire-next__sr-only wire-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" /><button type="button" class="wire-next__date-trigger" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openCalendar(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{currentValue || placeholder || "Select date"}</span><i class="icon-[lucide--calendar-days]" aria-hidden="true"></i></button>{#if currentValue && !readonly && !disabled}<button type="button" class="wire-next__picker-clear" aria-label="Clear date" @click="clearDate(event)">×</button>{/if}</div>
<div class="wire-next__calendar" role="dialog" aria-label="{label}"><div class="wire-next__date-selectors"><label>Month<select value="{selectedMonth}" @change="updateDate('month', event.currentTarget.value, event)">{#each months as month}<option value="{month.value}" selected="{month.value === selectedMonth}">{month.label}</option>{/each}</select></label><label>Year<select value="{selectedYear}" @change="updateDate('year', event.currentTarget.value, event)">{#each years as year}<option value="{year}" selected="{String(year) === selectedYear}">{year}</option>{/each}</select></label></div><div class="wire-next__calendar-grid">{#each days as day}<button type="button" data-value="{String(day).padStart(2, '0')}" aria-pressed="{String(day).padStart(2, '0') === selectedDay}" @click="selectDay(event)">{day}</button>{/each}</div><button type="button" class="wire-next__date-done" disabled="{!currentValue}" @click="finishDate(event)">Done</button></div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
-34
View File
@@ -1,34 +0,0 @@
component DeviceFrame {
outputs {
change(payload: { device: string; orientation: string; sourceEvent: Event })
rotate(payload: { device: string; orientation: string; sourceEvent: Event })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Device Frame"
description: string = ""
items: unknown[] = []
variant: string = "default"
device: string = "phone"
orientation: string = "portrait"
src: string = ""
srcdoc: string = ""
frameTitle: string = "Device preview"
showToolbar: boolean = true
allow: string = ""
class: string = ""
}
state currentOrientation = orientation
functions {
client function setDevice(nextDevice, sourceEvent) { output.change({ device: nextDevice, orientation: currentOrientation, sourceEvent: sourceEvent }) }
client function rotateFrame(sourceEvent) { currentOrientation = currentOrientation === "portrait" ? "landscape" : "portrait"; output.rotate({ device: device, orientation: currentOrientation, sourceEvent: sourceEvent }); output.change({ device: device, orientation: currentOrientation, sourceEvent: sourceEvent }) }
}
view {
<section {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--device-frame wire-next--variant-{variant} {class}" data-device="{device}" data-orientation="{currentOrientation}">
<header><div>{#if title}<strong>{title}</strong>{/if}{#if description}<p>{description}</p>{/if}</div>{#if showToolbar}<div class="wire-next__device-actions" role="toolbar" aria-label="Preview device">{#each items as item}<button type="button" aria-pressed="{item.value === device}" @click="setDevice(item.value, event)">{item.label}</button>{/each}<button type="button" aria-label="Rotate preview" @click="rotateFrame(event)">↻</button></div>{/if}</header>
<div class="wire-next__device-shell">{#if src || srcdoc}<iframe title="{frameTitle}" src="{src}" srcdoc="{srcdoc}" allow="{allow}"></iframe>{:else}<div class="wire-next__device-content"><slot /></div>{/if}</div>
</section>
}
}
-28
View File
@@ -1,28 +0,0 @@
component DragAndDrop {
outputs {
dragStart(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
dragEnd(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
dragEnter(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
dragLeave(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
drop(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Drag And Drop"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--drag-and-drop wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
-535
View File
@@ -1,535 +0,0 @@
component Drawer {
outputs {
open(payload: { placement: string; sourceEvent: Event })
close(payload: { reason: string; placement: string; sourceEvent: Event })
cancel(payload: { placement: string; sourceEvent: Event })
}
props {
open: boolean = false
defaultOpen: boolean = false
placement: string = "right"
size: string = "md"
color: string = "primary"
variant: string = "default"
title: string = "Drawer"
description: string = ""
icon: string = ""
label: string = "Drawer"
closeLabel: string = "Close drawer"
showClose: boolean = true
closeOnBackdrop: boolean = true
closeOnEscape: boolean = true
// Open/close animation length in ms. 0 disables the animation entirely.
duration: number = 260
overlay: boolean = true
scrollable: boolean = true
triggerLabel: string = ""
triggerIcon: string = ""
class: string = ""
}
state visible = defaultOpen
functions {
shared function isOpen() {
return open || visible
}
client function showDrawer(sourceEvent) {
visible = true
output.open({
placement: placement,
sourceEvent: sourceEvent
})
}
client function hideDrawer(reason, sourceEvent) {
visible = false
output.close({
reason: reason,
placement: placement,
sourceEvent: sourceEvent
})
}
client function cancelDrawer(sourceEvent) {
output.cancel({
placement: placement,
sourceEvent: sourceEvent
})
hideDrawer("cancel", sourceEvent)
}
client function handleKeydown(sourceEvent) {
if (closeOnEscape && sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
cancelDrawer(sourceEvent)
}
}
}
view {
<div
{...attrs}
data-ui-component="Drawer"
data-open='{open || visible ? "true" : "false"}'
data-placement='{placement}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
data-overlay='{overlay ? "true" : "false"}'
data-scrollable='{scrollable ? "true" : "false"}'
class='wire-drawer {class}'
style='--drawer-duration: {duration}ms'
>
{#if triggerLabel}
<button
type="button"
class="wire-drawer__trigger"
aria-haspopup="dialog"
aria-expanded='{open || visible ? "true" : "false"}'
@click='showDrawer(event)'
>
{#if triggerIcon}
<span class='{triggerIcon}' aria-hidden="true"></span>
{/if}
<span>{triggerLabel}</span>
</button>
{/if}
<span class="wire-drawer__trigger-slot" @click='showDrawer(event)'>
<slot name="trigger"></slot>
</span>
<div
class="wire-drawer__layer"
role="presentation"
@keydown='handleKeydown(event)'
>
{#if overlay}
<button
type="button"
class="wire-drawer__backdrop"
aria-label='{closeLabel}'
@click='if (closeOnBackdrop) { cancelDrawer(event) }'
></button>
{/if}
<section
class="wire-drawer__panel"
role="dialog"
aria-modal="true"
aria-label='{label || title}'
tabindex="-1"
>
<div class="wire-drawer__handle" aria-hidden="true"></div>
<header class="wire-drawer__header">
<div class="wire-drawer__heading">
{#if icon}
<span class='wire-drawer__icon {icon}' aria-hidden="true"></span>
{/if}
<div class="wire-drawer__heading-copy">
<slot name="header"></slot>
{#if title}
<h2>{title}</h2>
{/if}
{#if description}
<p>{description}</p>
{/if}
</div>
</div>
{#if showClose}
<button
type="button"
class="wire-drawer__close"
aria-label='{closeLabel}'
@click='hideDrawer("close-button", event)'
>
<span class="icon-[lucide--x]" aria-hidden="true"></span>
</button>
{/if}
</header>
<div class="wire-drawer__body">
<slot></slot>
</div>
<footer class="wire-drawer__footer">
<slot name="footer"></slot>
</footer>
</section>
</div>
</div>
}
style {
.wire-drawer {
--drawer-accent: var(--wire-color-primary);
--drawer-soft: var(--wire-color-primary-soft);
position: relative;
display: inline-flex;
}
.wire-drawer[data-color="secondary"] {
--drawer-accent: var(--wire-color-secondary);
--drawer-soft: var(--wire-color-secondary-soft);
}
.wire-drawer[data-color="info"] {
--drawer-accent: var(--wire-color-info);
--drawer-soft: var(--wire-color-info-soft);
}
.wire-drawer[data-color="success"] {
--drawer-accent: var(--wire-color-success);
--drawer-soft: var(--wire-color-success-soft);
}
.wire-drawer[data-color="warning"] {
--drawer-accent: var(--wire-color-warning);
--drawer-soft: var(--wire-color-warning-soft);
}
.wire-drawer[data-color="danger"] {
--drawer-accent: var(--wire-color-danger);
--drawer-soft: var(--wire-color-danger-soft);
}
.wire-drawer__trigger,
.wire-drawer__trigger-slot {
display: inline-flex;
align-items: center;
gap: 0.55rem;
}
.wire-drawer__trigger {
appearance: none;
min-height: 2.55rem;
padding: 0.65rem 1rem;
color: var(--wire-color-primary-contrast);
background: var(--drawer-accent);
border: 0;
border-radius: 0.8rem;
font: inherit;
font-size: 0.85rem;
font-weight: 650;
cursor: pointer;
}
/*
* The layer stays in the layout and is revealed by [data-open]; it used to
* be toggled with data-show, which sets display:none, and display cannot
* be transitioned -- the drawer simply snapped in and out. visibility is
* delayed by the duration on the way out so the panel can finish sliding
* before the layer is taken out of the hit-testing tree.
*/
.wire-drawer__layer {
position: fixed;
inset: 0;
z-index: 1200;
display: flex;
pointer-events: none;
visibility: hidden;
opacity: 0;
transition:
opacity var(--drawer-duration, 260ms) ease,
visibility 0s linear var(--drawer-duration, 260ms);
}
.wire-drawer[data-open="true"] .wire-drawer__layer {
visibility: visible;
opacity: 1;
transition:
opacity var(--drawer-duration, 260ms) ease,
visibility 0s linear 0s;
}
.wire-drawer__backdrop {
position: absolute;
inset: 0;
z-index: 0;
appearance: none;
padding: 0;
background: color-mix(in srgb, black 56%, transparent);
border: 0;
backdrop-filter: blur(7px);
pointer-events: auto;
}
.wire-drawer[data-overlay="false"] .wire-drawer__backdrop {
display: none;
}
.wire-drawer__panel {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: min(28rem, calc(100vw - 1rem));
max-height: 100%;
margin-left: auto;
color: var(--wire-color-text);
background:
radial-gradient(
circle at 100% 0%,
color-mix(in srgb, var(--drawer-accent) 8%, transparent),
transparent 34%
),
var(--wire-color-surface-raised);
border-left: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
box-shadow: -28px 0 80px color-mix(in srgb, black 28%, transparent);
pointer-events: auto;
overflow: hidden;
/* Slides in from whichever edge the placement puts it on. */
transform: translateX(100%);
transition: transform var(--drawer-duration, 260ms) cubic-bezier(0.32, 0.72, 0, 1);
}
.wire-drawer[data-open="true"] .wire-drawer__panel {
transform: none;
}
.wire-drawer[data-placement="left"] .wire-drawer__panel {
transform: translateX(-100%);
}
.wire-drawer[data-placement="top"] .wire-drawer__panel {
transform: translateY(-100%);
}
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
transform: translateY(100%);
}
.wire-drawer[data-open="true"][data-placement="left"] .wire-drawer__panel,
.wire-drawer[data-open="true"][data-placement="top"] .wire-drawer__panel,
.wire-drawer[data-open="true"][data-placement="bottom"] .wire-drawer__panel {
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.wire-drawer__layer,
.wire-drawer__panel {
transition: none;
}
}
.wire-drawer[data-size="sm"] .wire-drawer__panel {
width: min(22rem, calc(100vw - 1rem));
}
.wire-drawer[data-size="lg"] .wire-drawer__panel {
width: min(38rem, calc(100vw - 1rem));
}
.wire-drawer[data-size="xl"] .wire-drawer__panel {
width: min(52rem, calc(100vw - 1rem));
}
.wire-drawer[data-size="full"] .wire-drawer__panel {
width: 100vw;
}
.wire-drawer[data-placement="left"] .wire-drawer__panel {
margin-right: auto;
margin-left: 0;
border-right: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
border-left: 0;
box-shadow: 28px 0 80px color-mix(in srgb, black 28%, transparent);
}
.wire-drawer[data-placement="top"] .wire-drawer__layer,
.wire-drawer[data-placement="bottom"] .wire-drawer__layer {
align-items: flex-start;
}
.wire-drawer[data-placement="top"] .wire-drawer__panel,
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
width: 100%;
max-height: min(80vh, 44rem);
margin: 0;
border: 0;
box-shadow: 0 24px 80px color-mix(in srgb, black 28%, transparent);
}
.wire-drawer[data-placement="top"] .wire-drawer__panel {
border-bottom: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
}
.wire-drawer[data-placement="bottom"] .wire-drawer__layer {
align-items: flex-end;
}
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
border-top: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
border-radius: 1.3rem 1.3rem 0 0;
}
.wire-drawer[data-variant="soft"] .wire-drawer__panel {
background:
linear-gradient(145deg, var(--drawer-soft), transparent 65%),
var(--wire-color-surface-raised);
}
.wire-drawer[data-variant="solid"] .wire-drawer__panel {
color: var(--wire-color-primary-contrast);
background: var(--drawer-accent);
border-color: color-mix(in srgb, white 18%, transparent);
}
.wire-drawer__handle {
display: none;
width: 2.75rem;
height: 0.28rem;
margin: 0.55rem auto 0;
background: color-mix(in srgb, var(--wire-color-text-muted) 48%, transparent);
border-radius: 999px;
}
.wire-drawer[data-placement="bottom"] .wire-drawer__handle {
display: block;
}
.wire-drawer__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.35rem 1.35rem 1.1rem;
border-bottom: 1px solid var(--wire-color-border);
}
.wire-drawer__heading {
display: flex;
align-items: flex-start;
gap: 0.85rem;
min-width: 0;
}
.wire-drawer__icon {
flex: 0 0 auto;
width: 1.25rem;
height: 1.25rem;
margin-top: 0.15rem;
color: var(--drawer-accent);
}
.wire-drawer[data-variant="solid"] .wire-drawer__icon {
color: currentColor;
}
.wire-drawer__heading-copy {
display: grid;
gap: 0.3rem;
min-width: 0;
}
.wire-drawer__heading-copy h2,
.wire-drawer__heading-copy p {
margin: 0;
}
.wire-drawer__heading-copy h2 {
font-size: 1.05rem;
font-weight: 650;
line-height: 1.3;
}
.wire-drawer__heading-copy p {
color: var(--wire-color-text-muted);
font-size: 0.8rem;
line-height: 1.55;
}
.wire-drawer[data-variant="solid"] .wire-drawer__heading-copy p {
color: color-mix(in srgb, currentColor 76%, transparent);
}
/*
* padding is reset explicitly: an app-level `button { padding: ... }` rule
* outranks the browser default and leaves this fixed-size button with a
* content box of a couple of pixels, which squeezes the icon to a sliver
* and reads as "the close button has no icon". Same trap as Modal.
*/
.wire-drawer__close {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
padding: 0;
width: 2.35rem;
height: 2.35rem;
color: var(--wire-color-text-muted);
background: var(--wire-color-surface-soft);
border: 1px solid var(--wire-color-border);
border-radius: 0.75rem;
cursor: pointer;
}
.wire-drawer__close:hover,
.wire-drawer__close:focus-visible {
color: var(--drawer-accent);
border-color: color-mix(in srgb, var(--drawer-accent) 34%, var(--wire-color-border));
outline: none;
}
/* Never let the glyph be shrunk by the flex container. */
.wire-drawer__close svg {
flex: 0 0 auto;
width: 1rem;
height: 1rem;
}
/*
* Slot content is authored by the host app, so the app global stylesheet
* styles it too. A bare element selector there (p { color: ... }) beats
* anything the panel merely *inherits*, which is how modal body copy ended
* up muted grey on a saturated background. State the colour explicitly;
* :where() keeps the specificity low enough that any class the app puts on
* its own slot content still wins.
*/
.wire-drawer__body {
flex: 1 1 auto;
min-height: 0;
padding: 1.35rem;
color: var(--wire-color-text);
}
.wire-drawer__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
color: inherit;
}
.wire-drawer[data-scrollable="true"] .wire-drawer__body {
overflow: auto;
overscroll-behavior: contain;
}
.wire-drawer__footer {
padding: 1rem 1.35rem 1.35rem;
border-top: 1px solid var(--wire-color-border);
}
.wire-drawer__footer:empty {
display: none;
}
@media (max-width: 639px) {
.wire-drawer[data-placement="right"] .wire-drawer__panel,
.wire-drawer[data-placement="left"] .wire-drawer__panel {
width: min(24rem, 100vw);
}
.wire-drawer__header,
.wire-drawer__body,
.wire-drawer__footer {
padding-inline: 1rem;
}
}
}
}
-573
View File
@@ -1,573 +0,0 @@
component Dropdown {
outputs {
toggle(payload: { open: boolean; sourceEvent: Event; reason?: object })
open(payload: { sourceEvent: Event })
close(payload: { reason: string; sourceEvent: Event })
select(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
action(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
}
props {
items: unknown[] = []
open: boolean = false
defaultOpen: boolean = false
label: string = "Open menu"
icon: string = ""
showChevron: boolean = true
menuLabel: string = "Dropdown menu"
placement: string = "bottom-start"
width: string = "md"
size: string = "default"
color: string = "primary"
variant: string = "raised"
closeOnSelect: boolean = true
closeOnOutside: boolean = true
disabled: boolean = false
emptyLabel: string = "No menu items"
class: string = ""
}
state visible = defaultOpen
functions {
shared function isOpen() {
return open || visible
}
client function showMenu(sourceEvent) {
if (disabled) {
return
}
visible = true
output.open({ sourceEvent: sourceEvent })
output.toggle({ open: true, sourceEvent: sourceEvent })
}
client function hideMenu(reason, sourceEvent) {
visible = false
output.close({ reason: reason, sourceEvent: sourceEvent })
output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent })
}
client function toggleMenu(sourceEvent) {
if (isOpen()) {
hideMenu("toggle", sourceEvent)
} else {
showMenu(sourceEvent)
}
}
client function chooseItem(item, itemIndex, sourceEvent) {
if (disabled || item.disabled || item.type === "divider" || item.type === "header") {
sourceEvent.preventDefault()
return
}
output.select({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
if (item.action) {
output.action({
item: item,
itemIndex: itemIndex,
value: item.value || "",
sourceEvent: sourceEvent
})
}
if (closeOnSelect) {
hideMenu("select", sourceEvent)
}
}
client function moveFocus(sourceEvent, direction) {
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
if (!options.length) {
return
}
const activeIndex = options.indexOf(document.activeElement)
const nextIndex = (activeIndex + direction + options.length) % options.length
options[nextIndex].focus()
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "Escape") {
sourceEvent.preventDefault()
hideMenu("escape", sourceEvent)
} else if (sourceEvent.key === "ArrowDown") {
sourceEvent.preventDefault()
if (!isOpen()) {
showMenu(sourceEvent)
}
moveFocus(sourceEvent, 1)
} else if (sourceEvent.key === "ArrowUp") {
sourceEvent.preventDefault()
if (!isOpen()) {
showMenu(sourceEvent)
}
moveFocus(sourceEvent, -1)
} else if (sourceEvent.key === "Home" && isOpen()) {
sourceEvent.preventDefault()
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
const first = root.querySelector("[data-dropdown-item]:not([disabled])")
if (first) {
first.focus()
}
} else if (sourceEvent.key === "End" && isOpen()) {
sourceEvent.preventDefault()
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
if (options.length) {
options[options.length - 1].focus()
}
}
}
}
view {
<div
{...attrs}
data-ui-component="Dropdown"
data-open='{open || visible ? "true" : "false"}'
data-placement='{placement}'
data-width='{width}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='wire-dropdown {class}'
@keydown='handleKeydown(event)'
>
<button
type="button"
class="wire-dropdown__trigger"
disabled='{disabled}'
aria-haspopup="menu"
aria-expanded='{open || visible ? "true" : "false"}'
@click='toggleMenu(event)'
>
<slot name="trigger"></slot>
{#if icon}
<span class='wire-dropdown__trigger-icon {icon}' aria-hidden="true"></span>
{/if}
{#if label}
<span class="wire-dropdown__trigger-label">{label}</span>
{/if}
{#if showChevron}
<span class="icon-[lucide--chevron-down] wire-dropdown__chevron" aria-hidden="true"></span>
{/if}
</button>
{#if closeOnOutside}
<button
type="button"
class="wire-dropdown__dismiss-layer"
data-show='{open || visible}'
aria-label="Close menu"
@click='hideMenu("outside", event)'
></button>
{/if}
<div
class="wire-dropdown__panel"
data-wrn-anchored="true"
data-show='{open || visible}'
role="menu"
aria-label='{menuLabel}'
>
<slot name="header"></slot>
<div class="wire-dropdown__items">
{#each items as item, itemIndex}
{#if item.type === "divider"}
<div class="wire-dropdown__divider" role="separator"></div>
{:else if item.type === "header"}
<div class="wire-dropdown__section-label">{item.label || item.title}</div>
{:else if item.href}
<a
href='{item.href}'
target='{item.target || ""}'
rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
role="menuitem"
data-dropdown-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
class="wire-dropdown__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-dropdown__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
{/if}
</span>
{#if item.badge}
<span class="wire-dropdown__badge">{item.badge}</span>
{:else if item.shortcut}
<kbd>{item.shortcut}</kbd>
{:else if item.checked || item.selected}
<span class="icon-[lucide--check] wire-dropdown__status" aria-hidden="true"></span>
{:else if item.external}
<span class="icon-[lucide--arrow-up-right] wire-dropdown__status" aria-hidden="true"></span>
{/if}
</a>
{:else}
<button
type="button"
role="menuitem"
data-dropdown-item
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
disabled='{item.disabled}'
class="wire-dropdown__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-dropdown__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
{/if}
</span>
{#if item.badge}
<span class="wire-dropdown__badge">{item.badge}</span>
{:else if item.shortcut}
<kbd>{item.shortcut}</kbd>
{:else if item.checked || item.selected}
<span class="icon-[lucide--check] wire-dropdown__status" aria-hidden="true"></span>
{/if}
</button>
{/if}
{:empty}
<div class="wire-dropdown__empty">{emptyLabel}</div>
{/each}
</div>
<slot></slot>
<slot name="footer"></slot>
</div>
</div>
}
style {
.wire-dropdown {
--dropdown-accent: var(--wire-color-primary);
--dropdown-soft: var(--wire-color-primary-soft);
position: relative;
display: inline-flex;
min-width: 0;
}
.wire-dropdown[data-color="secondary"] {
--dropdown-accent: var(--wire-color-secondary);
--dropdown-soft: var(--wire-color-secondary-soft);
}
.wire-dropdown[data-color="info"] {
--dropdown-accent: var(--wire-color-info);
--dropdown-soft: var(--wire-color-info-soft);
}
.wire-dropdown[data-color="success"] {
--dropdown-accent: var(--wire-color-success);
--dropdown-soft: var(--wire-color-success-soft);
}
.wire-dropdown[data-color="warning"] {
--dropdown-accent: var(--wire-color-warning);
--dropdown-soft: var(--wire-color-warning-soft);
}
.wire-dropdown[data-color="danger"] {
--dropdown-accent: var(--wire-color-danger);
--dropdown-soft: var(--wire-color-danger-soft);
}
.wire-dropdown__trigger {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.55rem;
min-height: 2.55rem;
padding: 0.65rem 0.9rem;
color: var(--wire-color-text);
background: var(--wire-color-surface-raised);
border: 1px solid var(--wire-color-border);
border-radius: 0.78rem;
font: inherit;
font-size: 0.84rem;
font-weight: 600;
cursor: pointer;
transition: border-color 150ms ease, background-color 150ms ease, color 150ms ease;
}
.wire-dropdown__trigger:hover,
.wire-dropdown__trigger:focus-visible,
.wire-dropdown[data-open="true"] .wire-dropdown__trigger {
color: var(--dropdown-accent);
background: var(--dropdown-soft);
border-color: color-mix(in srgb, var(--dropdown-accent) 38%, var(--wire-color-border));
outline: none;
}
.wire-dropdown__trigger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.wire-dropdown__trigger-icon,
.wire-dropdown__chevron {
width: 1rem;
height: 1rem;
}
.wire-dropdown__chevron {
transition: transform 160ms ease;
}
.wire-dropdown[data-open="true"] .wire-dropdown__chevron {
transform: rotate(180deg);
}
.wire-dropdown[data-size="sm"] .wire-dropdown__trigger {
min-height: 2.2rem;
padding: 0.5rem 0.72rem;
font-size: 0.78rem;
}
.wire-dropdown[data-size="lg"] .wire-dropdown__trigger {
min-height: 2.9rem;
padding: 0.78rem 1.05rem;
font-size: 0.9rem;
}
.wire-dropdown__dismiss-layer {
position: fixed;
inset: 0;
z-index: 1100;
appearance: none;
padding: 0;
background: transparent;
border: 0;
}
.wire-dropdown__panel {
position: absolute;
z-index: 1101;
top: calc(100% + 0.55rem);
left: 0;
width: max-content;
min-width: 12rem;
max-width: min(22rem, calc(100vw - 1.5rem));
padding: 0.45rem;
color: var(--wire-color-text);
background:
linear-gradient(145deg, color-mix(in srgb, var(--dropdown-accent) 5%, transparent), transparent 60%),
color-mix(in srgb, var(--wire-color-surface-raised) 97%, transparent);
border: 1px solid color-mix(in srgb, var(--dropdown-accent) 18%, var(--wire-color-border));
border-radius: 1rem;
box-shadow:
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
0 24px 64px color-mix(in srgb, black 22%, transparent);
backdrop-filter: blur(18px);
}
.wire-dropdown[data-width="trigger"] .wire-dropdown__panel {
width: 100%;
min-width: 100%;
}
.wire-dropdown[data-width="sm"] .wire-dropdown__panel {
width: 12rem;
}
.wire-dropdown[data-width="md"] .wire-dropdown__panel {
width: 17rem;
}
.wire-dropdown[data-width="lg"] .wire-dropdown__panel {
width: 22rem;
}
.wire-dropdown[data-placement="bottom-end"] .wire-dropdown__panel {
right: 0;
left: auto;
}
.wire-dropdown[data-placement="top-start"] .wire-dropdown__panel {
top: auto;
bottom: calc(100% + 0.55rem);
}
.wire-dropdown[data-placement="top-end"] .wire-dropdown__panel {
top: auto;
right: 0;
bottom: calc(100% + 0.55rem);
left: auto;
}
.wire-dropdown[data-variant="soft"] .wire-dropdown__panel {
background:
linear-gradient(145deg, var(--dropdown-soft), transparent 70%),
var(--wire-color-surface-raised);
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
}
.wire-dropdown[data-variant="outline"] .wire-dropdown__panel {
background: var(--wire-color-surface-raised);
box-shadow: none;
}
.wire-dropdown__items {
display: grid;
gap: 0.15rem;
}
.wire-dropdown__section-label {
padding: 0.55rem 0.72rem 0.3rem;
color: var(--wire-color-text-muted);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.wire-dropdown__divider {
height: 1px;
margin: 0.3rem 0.4rem;
background: var(--wire-color-border);
}
.wire-dropdown__item {
appearance: none;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.7rem;
width: 100%;
min-width: 0;
padding: 0.68rem 0.72rem;
color: var(--wire-color-text);
background: transparent;
border: 0;
border-radius: 0.72rem;
font: inherit;
text-align: left;
text-decoration: none;
cursor: pointer;
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
}
.wire-dropdown__item:hover,
.wire-dropdown__item:focus-visible,
.wire-dropdown__item[data-selected="true"] {
color: var(--dropdown-accent);
background: var(--dropdown-soft);
outline: none;
}
.wire-dropdown__item:active {
transform: scale(0.985);
}
.wire-dropdown__item[data-danger="true"] {
color: var(--wire-color-danger);
}
.wire-dropdown__item[data-danger="true"]:hover,
.wire-dropdown__item[data-danger="true"]:focus-visible {
background: var(--wire-color-danger-soft);
}
.wire-dropdown__item[disabled],
.wire-dropdown__item[aria-disabled="true"] {
opacity: 0.48;
pointer-events: none;
}
.wire-dropdown__item-icon,
.wire-dropdown__status {
width: 1rem;
height: 1rem;
}
.wire-dropdown__copy {
display: grid;
gap: 0.12rem;
min-width: 0;
}
.wire-dropdown__copy strong {
overflow: hidden;
font-size: 0.82rem;
font-weight: 600;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-dropdown__copy small {
overflow: hidden;
color: var(--wire-color-text-muted);
font-size: 0.7rem;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-dropdown__badge {
padding: 0.18rem 0.45rem;
color: var(--dropdown-accent);
background: var(--dropdown-soft);
border-radius: 999px;
font-size: 0.64rem;
font-weight: 700;
}
.wire-dropdown kbd {
padding: 0.16rem 0.38rem;
color: var(--wire-color-text-muted);
background: var(--wire-color-surface-soft);
border: 1px solid var(--wire-color-border);
border-radius: 0.36rem;
font-size: 0.64rem;
font-family: inherit;
}
.wire-dropdown__empty {
padding: 1rem;
color: var(--wire-color-text-muted);
font-size: 0.8rem;
text-align: center;
}
@media (max-width: 639px) {
.wire-dropdown__panel {
position: fixed;
right: 0.75rem;
bottom: 0.75rem;
left: 0.75rem;
top: auto;
width: auto;
min-width: 0;
max-width: none;
}
}
@media (prefers-reduced-motion: reduce) {
.wire-dropdown__trigger,
.wire-dropdown__chevron,
.wire-dropdown__item {
transition: none;
}
}
}
}
-55
View File
@@ -1,55 +0,0 @@
component FileInput {
outputs {
input(payload: { files: File[]; name: string; sourceEvent: Event })
change(payload: { files: File[]; name: string; sourceEvent: Event })
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { files: File[]; name: string; sourceEvent: Event })
clear(payload: { name: string; sourceEvent: Event })
invalid(payload: { message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "File"
hiddenLabel: boolean = false
placeholder: string = "Choose a file"
value: string = ""
icon: string = "icon-[lucide--upload]"
iconPosition: string = "start"
accept: string = ""
multiple: boolean = false
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
variant: string = "normal"
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
state selectedName: string = ""
functions {
client function handleChange(sourceEvent) {
sourceEvent.stopPropagation()
selectedName = sourceEvent.currentTarget.files && sourceEvent.currentTarget.files.length ? sourceEvent.currentTarget.files[0].name : ""
output.input({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
output.change({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
if (selectedName) output.select({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
else output.clear({ name: name, sourceEvent: sourceEvent })
}
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ name: name, sourceEvent: sourceEvent }) }
client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--file-input {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__file-control" data-icon-position="{iconPosition}">{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<span>{selectedName || value || placeholder}</span><input id="{id || name}" type="file" name="{name}" accept="{accept}" multiple="{multiple}" disabled="{disabled}" required="{required}" aria-readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="handleChange(event)" @change="handleChange(event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="emitField('invalid', event)" /></div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
-29
View File
@@ -1,29 +0,0 @@
component FileUpload {
outputs {
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
upload(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
progress(payload: { progress: number; [key: string]: string | number | boolean | null | object } | number)
success(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
cancel(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
remove(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
title: string = "File Upload"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--file-upload wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
@@ -1,40 +0,0 @@
component FileUploadProgress {
outputs {
cancel(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
retry(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
complete(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
size: string = "default"
color: string = "primary"
label: string = "Progress"
value: number = 50
max: number = 100
showValue: boolean = true
fileName: string = ""
fileSize: string = ""
uploadedSize: string = ""
status: string = "uploading"
cancelLabel: string = "Cancel upload"
retryLabel: string = "Retry upload"
class: string = ""
}
state currentValue = value
state currentStatus = status
functions {
shared function percent() { if (!Number(max)) { return 0 } return Math.max(0, Math.min(100, Math.round(Number(currentValue) / Number(max) * 100))) }
client function detail(sourceEvent) { return { value: currentValue, max: max, percent: percent(), status: currentStatus, fileName: fileName, sourceEvent: sourceEvent } }
client function cancelUpload(sourceEvent) { currentStatus = "cancelled"; output.cancel(detail(sourceEvent)) }
client function retryUpload(sourceEvent) { currentValue = 0; currentStatus = "uploading"; output.retry(detail(sourceEvent)) }
client function completeUpload(sourceEvent) { currentValue = max; currentStatus = "complete"; output.complete(detail(sourceEvent)) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--file-upload-progress {class}" data-status="{currentStatus}" aria-live="polite">
<div class="wire-next__row"><span>{#if fileName}<strong>{fileName}</strong><small>{#if uploadedSize || fileSize}{uploadedSize}{#if uploadedSize && fileSize} of {/if}{fileSize}{:else}{label}{/if}</small>{:else}<strong>{label}</strong>{/if}</span>{#if showValue}<strong>{percent()}%</strong>{/if}</div>
<progress value="{currentValue}" max="{max}" aria-label="{label}"></progress>
<div class="wire-next__upload-status"><span class="wire-next__upload-status-icon" aria-hidden="true"></span><span>{#if currentStatus === "uploading"}Uploading securely…{:else if currentStatus === "complete"}Upload complete{:else if currentStatus === "error"}Upload failed{:else}Upload cancelled{/if}</span></div>
<div class="wire-next__upload-actions">{#if currentStatus === "uploading"}<button type="button" @click="cancelUpload(event)">{cancelLabel}</button><button type="button" @click="completeUpload(event)">Complete</button>{/if}{#if currentStatus === "error" || currentStatus === "cancelled"}<button type="button" @click="retryUpload(event)">{retryLabel}</button>{/if}</div>
</div>
}
}
-47
View File
@@ -1,47 +0,0 @@
component Image {
props {
size: string = "default"
color: string = "primary"
src: string = ""
alt: string = ""
width: string = ""
height: string = ""
loading: string = "lazy"
rounded: boolean = false
class: string = ""
}
view {
<figure
data-ui-component="Image"
class='relative m-0 overflow-hidden bg-[var(--wire-color-surface-soft)] {class}'
class:rounded-2xl='rounded'
>
{#if src}
<img
src='{src}'
alt='{alt}'
width='{width}'
height='{height}'
loading='{loading}'
decoding="async"
class="block h-auto w-full object-cover transition duration-300"
class:aspect-square='size === "square"'
class:aspect-video='size === "video"'
class:aspect-[4/3]='size === "landscape"'
class:aspect-[3/4]='size === "portrait"'
/>
{:else}
<div
class="flex min-h-48 w-full items-center justify-center text-[var(--wire-color-text-muted)]"
role="img"
aria-label='{alt || "Image placeholder"}'
>
<span class="icon-[lucide--image] size-8" aria-hidden="true"></span>
</div>
{/if}
<slot></slot>
</figure>
}
}
-54
View File
@@ -1,54 +0,0 @@
component InputGroup {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
submit(payload: { value: string | number | boolean | null | object; name: string; sourceEvent: Event })
action(payload: { name: string; sourceEvent: Event })
}
props {
size: string = "default"
color: string = "primary"
id: string = ""
name: string = ""
label: string = "Input group"
hiddenLabel: boolean = false
value: string = ""
placeholder: string = ""
type: string = "text"
startText: string = ""
endText: string = ""
icon: string = ""
iconPosition: string = "start"
actionLabel: string = ""
helperText: string = ""
cornerHint: string = ""
error: string = ""
inline: boolean = false
variant: string = "normal"
readonly: boolean = false
disabled: boolean = false
required: boolean = false
class: string = ""
}
functions {
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
client function handleKeydown(sourceEvent) { if (sourceEvent.key === "Enter") { sourceEvent.stopPropagation(); output.submit({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) } }
client function handleAction(sourceEvent) { output.action({ name: name, sourceEvent: sourceEvent }) }
}
view {
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--input-group {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wire-next__input-group-control">
{#if startText}<span class="wire-next__input-addon">{startText}</span>{/if}
{#if icon}<span class="{icon} wire-next__field-icon" data-position="{iconPosition}" aria-hidden="true"></span>{/if}
<input id="{id || name}" type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" readonly="{readonly}" disabled="{disabled}" required="{required}" aria-invalid="{error ? 'true' : 'false'}" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @keydown="handleKeydown(event)" />
{#if endText}<span class="wire-next__input-addon">{endText}</span>{/if}
{#if actionLabel}<button type="button" disabled="{disabled}" @click="handleAction(event)">{actionLabel}</button>{/if}
</div>
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
</div>
}
}
-626
View File
@@ -1,626 +0,0 @@
component InputNumber {
outputs {
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
increment(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
decrement(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
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."
}
state currentValue = value
state committedValue = value
functions {
shared function inputId() {
if (id !== "") {
return id
}
if (name !== "") {
return name
}
return "input-number"
}
shared function descriptionId() {
return inputId() + "-description"
}
shared function messageId() {
return inputId() + "-message"
}
shared 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)"
}
shared function hasMin() {
return min !== "" && min !== null && min !== undefined
}
shared function hasMax() {
return max !== "" && max !== null && max !== undefined
}
shared function isBlank() {
return currentValue === "" || currentValue === null || currentValue === undefined
}
shared function normalizedStep() {
return Number(step) > 0 ? Number(step) : 1
}
shared 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
}
shared function precisionFactor() {
return Math.pow(10, inferredPrecision())
}
shared function roundValue(nextValue) {
return Math.round(Number(nextValue) * precisionFactor()) / precisionFactor()
}
shared 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)
}
shared function isBelowMin() {
return !isBlank() && hasMin() && Number(currentValue) < Number(min)
}
shared function isAboveMax() {
return !isBlank() && hasMax() && Number(currentValue) > Number(max)
}
shared function isInvalid() {
return (
invalid ||
error !== "" ||
(required && isBlank()) ||
isBelowMin() ||
isAboveMax()
)
}
shared function validationMessage() {
if (error !== "") {
return error
}
if (required && isBlank()) {
return requiredMessage
}
if (isBelowMin()) {
return minMessage
}
if (isAboveMax()) {
return maxMessage
}
return ""
}
shared function hasMessage() {
return (
helpText !== "" ||
(showValidationMessage && isInvalid() && validationMessage() !== "")
)
}
shared function describedBy() {
if (description !== "" && hasMessage()) {
return descriptionId() + " " + messageId()
}
if (description !== "") {
return descriptionId()
}
if (hasMessage()) {
return messageId()
}
return ""
}
shared function decrementDisabled() {
return (
disabled ||
readonly ||
buttonsDisabled ||
(hasMin() && !isBlank() && Number(currentValue) <= Number(min))
)
}
shared function incrementDisabled() {
return (
disabled ||
readonly ||
buttonsDisabled ||
(hasMax() && !isBlank() && Number(currentValue) >= Number(max))
)
}
client 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)
}
client 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
)
}
client 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)
}
client 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)
}
client 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
)
}
client function handleChange(sourceEvent, previousValue) {
sourceEvent.stopPropagation()
previousValue = committedValue
if (!isBlank()) {
currentValue = clamp
? clampValue(currentValue)
: roundValue(currentValue)
}
committedValue = currentValue
dispatchInputNumberEvent(
sourceEvent,
"change",
"change",
previousValue
)
}
client 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)
}
}
client 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 {
<div
{...attrs}
data-wrn-input-number
data-variant='{variant}'
data-size='{size}'
data-color='{color}'
data-value='{currentValue}'
data-invalid='{isInvalid() ? "true" : "false"}'
data-disabled='{disabled ? "true" : "false"}'
style='--input-number-accent: {componentColor()};'
class='relative flex flex-col gap-1.5 text-[var(--wire-color-text)] {fullWidth ? "w-full" : variant === "compact" ? "w-fit max-w-full" : "w-full max-w-sm"} {disabled ? "opacity-60" : ""} {class}'
>
{#if label !== "" && variant !== "labeled" && variant !== "seat"}
<label
for='{inputId()}'
class='inline-flex items-center gap-1 text-sm font-semibold leading-5 text-[var(--wire-color-text)]'
>
<span>{label}</span>
{#if required}
<span
aria-hidden="true"
class='text-[var(--wire-color-danger)]'
>*</span>
{/if}
</label>
{/if}
{#if description !== "" && variant !== "labeled" && variant !== "seat"}
<p
id='{descriptionId()}'
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
>
{description}
</p>
{/if}
<div
class='group flex min-w-0 overflow-hidden border bg-[var(--wire-color-surface)] text-[var(--wire-color-text)] shadow-[var(--wire-shadow-1)] transition-[border-color,box-shadow,background-color] duration-[var(--wire-motion-base)] ease-[var(--wire-ease-standard)] focus-within:border-[var(--input-number-accent)] focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--input-number-accent)_18%,transparent)] {variant === "compact" ? "rounded-full" : "rounded-[var(--wire-radius-sm)]"} {size === "xs" ? "min-h-8 text-xs" : size === "sm" ? "min-h-9 text-sm" : size === "lg" ? "min-h-12 text-base" : size === "xl" ? "min-h-14 text-lg" : "min-h-10 text-sm"} {isInvalid() ? "border-[var(--wire-color-danger)] focus-within:border-[var(--wire-color-danger)] focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--wire-color-danger)_18%,transparent)]" : "border-[var(--wire-color-border)]"} {disabled ? "cursor-not-allowed bg-[var(--wire-color-surface-2)]" : ""}'
>
{#if variant === "horizontal" && showButtons}
<button
type="button"
aria-label='{decrementLabel}'
aria-controls='{inputId()}'
disabled='{decrementDisabled()}'
@click='decrementValue(event)'
class='inline-flex shrink-0 items-center justify-center border-r border-[var(--wire-color-border)] bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--minus] size-4'
></span>
</button>
{/if}
<div
class='flex min-w-0 flex-1 {variant === "labeled" ? "flex-col items-stretch justify-center gap-0.5" : variant === "seat" ? "items-center justify-between gap-3" : "items-center"} {size === "xs" ? "px-2" : size === "sm" ? "px-2.5" : size === "lg" ? "px-4" : size === "xl" ? "px-5" : "px-3"}'
>
{#if variant === "labeled" || variant === "seat"}
<div class='min-w-0 flex-1'>
{#if label !== ""}
<label
for='{inputId()}'
class='block truncate font-semibold leading-4 text-[var(--wire-color-text)] {variant === "labeled" ? "text-xs font-medium text-[var(--wire-color-muted)]" : "text-sm"}'
>
{label}
{#if required}
<span
aria-hidden="true"
class='ml-0.5 text-[var(--wire-color-danger)]'
>*</span>
{/if}
</label>
{/if}
{#if description !== ""}
<span
id='{descriptionId()}'
class='block truncate text-xs leading-4 text-[var(--wire-color-muted)]'
>
{description}
</span>
{/if}
</div>
{/if}
<div
class='flex min-w-0 items-center {variant === "seat" ? "w-auto shrink-0" : "w-full"}'
>
{#if prefix !== ""}
<span
aria-hidden="true"
class='shrink-0 pr-1.5 text-[var(--wire-color-muted)]'
>
{prefix}
</span>
{/if}
<input
id='{inputId()}'
name='{name}'
type="number"
value='{currentValue}'
min='{min}'
max='{max}'
step='{normalizedStep()}'
placeholder='{placeholder}'
autocomplete='{autocomplete}'
inputmode='{inputMode}'
aria-label='{ariaLabel !== "" ? ariaLabel : label !== "" ? label : name}'
aria-describedby='{describedBy()}'
aria-invalid='{isInvalid() ? "true" : "false"}'
aria-required='{required ? "true" : "false"}'
aria-disabled='{disabled || inputDisabled ? "true" : "false"}'
required='{required}'
disabled='{disabled}'
readonly='{readonly || inputDisabled || !allowInput}'
tabindex='{inputDisabled ? "-1" : "0"}'
@input='handleInput(event)'
@change='handleChange(event)'
@keydown='handleKeydown(event)'
@wheel='handleWheel(event)'
class='min-w-0 flex-1 appearance-none border-0 bg-transparent p-0 font-medium leading-none text-[var(--wire-color-text)] outline-none placeholder:text-[var(--wire-color-muted)] read-only:cursor-default disabled:cursor-not-allowed [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none {variant === "horizontal" || variant === "compact" || variant === "seat" ? "text-center" : "text-left"} {variant === "seat" ? "w-10 flex-none" : "w-full"}'
/>
{#if suffix !== ""}
<span
aria-hidden="true"
class='shrink-0 pl-1.5 text-[var(--wire-color-muted)]'
>
{suffix}
</span>
{/if}
</div>
</div>
{#if variant === "horizontal" && showButtons}
<button
type="button"
aria-label='{incrementLabel}'
aria-controls='{inputId()}'
disabled='{incrementDisabled()}'
@click='incrementValue(event)'
class='inline-flex shrink-0 items-center justify-center border-l border-[var(--wire-color-border)] bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--plus] size-4'
></span>
</button>
{:else}
{#if showButtons}
<div
role="group"
aria-label='{controlsLabel}'
class='flex shrink-0 border-l border-[var(--wire-color-border)] {variant === "vertical" ? "flex-col" : "flex-row"}'
>
<button
type="button"
aria-label='{decrementLabel}'
aria-controls='{inputId()}'
disabled='{decrementDisabled()}'
@click='decrementValue(event)'
class='inline-flex items-center justify-center bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {variant === "vertical" ? "flex-1 border-b border-[var(--wire-color-border)]" : "border-r border-[var(--wire-color-border)]"} {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--minus] size-4'
></span>
</button>
<button
type="button"
aria-label='{incrementLabel}'
aria-controls='{inputId()}'
disabled='{incrementDisabled()}'
@click='incrementValue(event)'
class='inline-flex items-center justify-center bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {variant === "vertical" ? "flex-1" : ""} {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
>
<span
aria-hidden="true"
class='icon-[lucide--plus] size-4'
></span>
</button>
</div>
{/if}
{/if}
</div>
{#if showValidationMessage && isInvalid() && validationMessage() !== ""}
<p
id='{messageId()}'
role="alert"
aria-live="polite"
class='m-0 flex items-center gap-1.5 text-xs leading-5 text-[var(--wire-color-danger)]'
>
<span
aria-hidden="true"
class='icon-[lucide--circle-alert] size-3.5 shrink-0'
></span>
<span>{validationMessage()}</span>
</p>
{/if}
{#if (!showValidationMessage || !isInvalid() || validationMessage() === "") && helpText !== ""}
<p
id='{messageId()}'
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
>
{helpText}
</p>
{/if}
</div>
}
}
-9
View File
@@ -1,9 +0,0 @@
component Kbd {
props {
size: string = "default"
color: string = "primary"
label: string = "⌘ K"
class: string = ""
}
view { <kbd class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--kbd {class}"><slot>{label}</slot></kbd> }
}
-19
View File
@@ -1,19 +0,0 @@
component LayoutSplitter {
outputs {
resizeStart(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
resize(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
resizeEnd(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
}
props {
size: string = "default"
color: string = "primary"
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
class: string = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--layout-splitter wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
}
}
@@ -1,24 +0,0 @@
component LegendIndicator {
outputs {
toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
title: string = "Legend Indicator"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--legend-indicator wire-next--variant-{variant} {class}">
{#if title}<strong>{title}</strong>{/if}
{#if description}<p>{description}</p>{/if}
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
}
-47
View File
@@ -1,47 +0,0 @@
component Link {
props {
size: string = "default"
color: string = "primary"
label: string = "Link"
href: string = "#"
target: string = ""
rel: string = ""
external: boolean = false
class: string = ""
}
view {
<a
data-ui-component="Link"
href='{href}'
target='{target}'
rel='{external ? (rel || "noopener noreferrer") : rel}'
class='inline-flex min-w-0 items-center gap-1.5 rounded-md font-semibold underline-offset-4 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--wire-color-background)] {class}'
class:text-xs='size === "xs"'
class:text-sm='size === "sm" || size === "default"'
class:text-base='size === "md"'
class:text-lg='size === "lg"'
class:text-[var(--wire-color-primary)]='color === "primary"'
class:hover:text-[var(--wire-color-primary-hover)]='color === "primary"'
class:text-[var(--wire-color-secondary)]='color === "secondary"'
class:hover:text-[var(--wire-color-secondary-hover)]='color === "secondary"'
class:text-[var(--wire-color-success)]='color === "success"'
class:text-[var(--wire-color-warning-text)]='color === "warning"'
class:text-[var(--wire-color-danger)]='color === "danger"'
class:text-[var(--wire-color-info)]='color === "info"'
class:text-[var(--wire-color-text)]='color === "neutral"'
class:hover:underline='external === false'
>
<span class="truncate">{label}</span>
{#if external}
<span
class="icon-[lucide--external-link] size-3.5 shrink-0"
aria-hidden="true"
></span>
{/if}
<slot></slot>
</a>
}
}
-107
View File
@@ -1,107 +0,0 @@
component List {
props {
size: string = "default"
color: string = "primary"
title: string = "List"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
view {
<section
data-ui-component="List"
aria-label='{title}'
class='w-full {class}'
>
{#if title || description}
<header class="mb-4">
{#if title}
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{title}</h3>
{/if}
{#if description}
<p class="mt-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
{/if}
</header>
{/if}
<ul
class="overflow-hidden"
class:divide-y='variant === "default" || variant === "divided"'
class:divide-[var(--wire-color-border)]='variant === "default" || variant === "divided"'
class:rounded-2xl='variant === "card"'
class:border='variant === "card"'
class:border-[var(--wire-color-border)]='variant === "card"'
class:bg-[var(--wire-color-surface-raised)]='variant === "card"'
class:space-y-3='variant === "separated"'
>
{#each items as item, index}
<li
class="group min-w-0"
class:rounded-xl='variant === "separated"'
class:border='variant === "separated"'
class:border-[var(--wire-color-border)]='variant === "separated"'
class:bg-[var(--wire-color-surface-raised)]='variant === "separated"'
>
{#if item.href}
<a
href='{item.href}'
class="flex min-w-0 items-start gap-3 px-4 py-3 outline-none transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
class:px-3='size === "sm"'
class:py-2.5='size === "sm"'
class:px-5='size === "lg"'
class:py-4='size === "lg"'
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index } }))'
>
{#if item.icon}
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
<span class='{item.icon + " size-5"}' aria-hidden="true"></span>
</span>
{/if}
{#if item.imageSrc}
<img src='{item.imageSrc}' alt='{item.imageAlt || ""}' class="size-12 shrink-0 rounded-xl object-cover" loading="lazy" />
{/if}
<div class="min-w-0 flex-1">
<div class="flex items-start justify-between gap-3">
<p class="truncate font-semibold text-[var(--wire-color-text)]">{item.title || item.label}</p>
{#if item.meta}
<span class="shrink-0 text-xs text-[var(--wire-color-text-muted)]">{item.meta}</span>
{/if}
</div>
{#if item.description}
<p class="mt-1 line-clamp-2 text-sm leading-5 text-[var(--wire-color-text-muted)]">{item.description}</p>
{/if}
{#if item.badge}
<span class="mt-2 inline-flex rounded-full bg-[var(--wire-color-primary-soft)] px-2.5 py-1 text-xs font-semibold text-[var(--wire-color-primary)]">{item.badge}</span>
{/if}
</div>
<span class="icon-[lucide--chevron-right] mt-1 size-4 shrink-0 text-[var(--wire-color-text-muted)] transition group-hover:translate-x-0.5 group-hover:text-[var(--wire-color-primary)]" aria-hidden="true"></span>
</a>
{:else}
<div class="flex min-w-0 items-start gap-3 px-4 py-3">
{#if item.icon}
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
<span class='{item.icon + " size-5"}' aria-hidden="true"></span>
</span>
{/if}
<div class="min-w-0 flex-1">
<p class="font-semibold text-[var(--wire-color-text)]">{item.title || item.label}</p>
{#if item.description}
<p class="mt-1 text-sm leading-5 text-[var(--wire-color-text-muted)]">{item.description}</p>
{/if}
</div>
</div>
{/if}
</li>
{:empty}
<li class="rounded-xl border border-dashed border-[var(--wire-color-border)] p-6 text-center text-sm text-[var(--wire-color-text-muted)]">
No items available.
</li>
{/each}
</ul>
<slot></slot>
</section>
}
}

Some files were not shown because too many files have changed in this diff Show More