Compare commits

...
Author SHA1 Message Date
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
ClintchizandClaude Opus 5 361129b6ac perf(build): budget the runtime on what ships, not on source bytes
The runtime budgets measured raw source, which counts comments -- and the
production build minifies, so comments cost a visitor nothing. The metric
therefore rewarded deleting explanatory comments over writing smaller code,
and could not tell a real feature from a wall of prose.

They now measure the minified output, which is what is actually served:
/__wrnexus/reactive.js is its own file, minified, with an immutable year-long
cache. The reactive runtime is 69684 minified against a 80000 budget, from
175246 raw -- roughly 21kB gzipped, fetched once.

Also fixes two real bugs found while testing the showcase:

  - object-valued props were serialised as a bare {...} attribute, which the
    compiler read as interpolation and tried to parse as JavaScript. That
    returned 500 for /components/navbar. Arrays start with [ and were never
    affected, which is why only object props broke. All 108 pages now render.

  - the runtime walked text nodes inside textarea, script and style, so a
    JSON sample in a textarea was evaluated away.

Navbar styles move out of ui.css into the component, matching the rest of the
navigation group. No declarations changed: 4519 before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 21:20:19 +05:30
ClintchizandClaude Opus 5 8069541cd9 refactor(csr): one document observer with subscribers
Overlay clamping, dialog focus, roving focus and scrollspy each ran their own
MutationObserver over the same stream of records. They now share one, with the
per-feature work registered as subscribers. Every subscriber already defers,
so the extra callbacks are cheap and the bookkeeping is paid for once.

The attribute filter stays explicit rather than observing everything: an
unfiltered observer would see the tabindex the roving code writes and loop on
its own output.

This is better structured but it is not a fix for the size budget -- it buys
82 bytes of headroom, not room to grow. Splitting the runtime so a page pays
only for the behaviour it uses is still the outstanding decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:26:57 +05:30
ClintchizandClaude Opus 5 4a82640c5b merge: navigation components phase 3
Quality / quality (ubuntu-latest) (push) Failing after 12m47s
Quality / quality (windows-latest) (push) Canceled after 0s
Scrollspy built, Navbar given roving focus, and the invalid empty aria-current
fixed across Navbar and Breadcrumb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:22:10 +05:30
ClintchizandClaude Opus 5 52660cbb8e feat(ui): build Scrollspy, fix aria-current across the navigation group
Scrollspy replaces a scaffold that rendered bare anchors. The runtime observes
the sections the links point at and writes the marker straight onto the links:
an IntersectionObserver callback fires long after the client function that
registered it returned, so a state write there would be dropped.

Navbar and Breadcrumb both emitted aria-current="" for every inactive link.
That is not a valid value -- the attribute takes a token or must be absent --
so every link claimed a state it did not have. Breadcrumb had it too, despite
being the strongest component in the group.

Navbar also takes roving arrow-key focus across its menu bar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:18:25 +05:30
ClintchizandClaude Opus 5 ca2f9451ab merge: navigation components phase 1 and 2
Quality / quality (ubuntu-latest) (push) Failing after 12m45s
Quality / quality (windows-latest) (push) Canceled after 0s
Roving arrow-key focus in the runtime, then Nav, Pagination, Stepper,
MegaMenu, a Sidebar rebuilt on Drawer, and a Tabs rewritten off Tailwind onto
wire classes with real outputs and url-backed selection.

Also fixes the modal focus trap shipped in 0.8.5, which gated on
getBoundingClientRect and so never ran under test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:54:26 +05:30
777 changed files with 61125 additions and 49369 deletions
+20 -2
View File
@@ -1,11 +1,29 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "basic-app",
"runtimeExecutable": "bun",
"runtimeArgs": [
"run",
"packages/cli/src/index.ts",
"dev",
"examples/basic-app",
"--port=3520"
],
"port": 3520
},
{
"name": "component-showcase",
"runtimeExecutable": "bun",
"runtimeArgs": ["run", "--cwd", "examples/component-showcase", "dev"],
"port": 3000
"runtimeArgs": [
"run",
"packages/cli/src/index.ts",
"dev",
"examples/component-showcase",
"--port=3400"
],
"port": 3400
}
]
}
+1
View File
@@ -1,6 +1,7 @@
node_modules/
dist/
.wrnexus/
.wrnexus-*/
*.log
*.db
*.db-shm
+4 -2
View File
@@ -1,8 +1,10 @@
node_modules/
dist/
**/dist/
**/.wrnexus/**
**/.wrnexus-*/**
.publish/
**/.wirefw/
**/.wrnfw/
coverage/
bun.lock
bun.lockb
@@ -12,7 +14,7 @@ bun.lockb
**/*.gen.ts
**/*.generated.d.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/language-server.cjs
editors/vscode/src/extension.bundle.cjs
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ai",
"version": "0.8.5",
"version": "0.8.9",
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.8.5",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT",
@@ -41,8 +41,8 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/db": "^0.8.5"
"@wrnexus/core": "^0.8.9",
"@wrnexus/db": "^0.8.9"
},
"files": [
"dist",
+3 -3
View File
@@ -79,7 +79,7 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r
| `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 eject <name...>` | Copy WrNexus 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. |
@@ -179,7 +179,7 @@ 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.
Copies a WrNexus 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
@@ -322,5 +322,5 @@ 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`.
- 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 WrNexus 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.
+18 -18
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.8.5",
"version": "0.8.17",
"type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT",
@@ -44,23 +44,23 @@
"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",
"@wrnexus/core": "^0.8.9",
"@wrnexus/router": "^0.8.9",
"@wrnexus/csr": "^0.8.16",
"@wrnexus/compiler": "^0.8.9",
"@wrnexus/styles": "^0.8.10",
"@wrnexus/dev-server": "^0.8.16",
"@wrnexus/ui": "^0.8.12",
"@wrnexus/validation": "^0.8.9",
"@wrnexus/i18n": "^0.8.9",
"@wrnexus/mcp": "^0.8.8",
"@wrnexus/playground": "^0.8.8",
"@wrnexus/db": "^0.8.9",
"@wrnexus/authz": "^0.8.9",
"@wrnexus/plugin": "^0.8.8",
"@wrnexus/syntax": "^0.8.8",
"@wrnexus/typecheck": "^0.8.8",
"@wrnexus/security": "^0.8.8",
"selfsigned": "^5.5.0"
},
"files": [
+9 -8
View File
@@ -69,7 +69,7 @@ bun add @wrnexus/compiler
All exports come from the package root (`@wrnexus/compiler`).
### `compileWireFile(source: string): string`
### `compileWrnFile(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.
@@ -115,10 +115,10 @@ class Lexer {
### 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. |
| Class | Thrown by | Meaning |
| ------------ | ------------------------------------------------ | --------------------------------------------------------------- |
| `ParseError` | `parse`, `compile`, `compileWrnFile`, `generate` | Invalid `.wrn` grammar or (rewrapped) lex failure. |
| `LexError` | `Lexer` | Unexpected character / unterminated string / unbalanced braces. |
### AST types
@@ -143,9 +143,9 @@ Exported type-only symbols describing the parsed tree:
Compile a page:
```ts
import { compileWireFile } from "@wrnexus/compiler";
import { compileWrnFile } from "@wrnexus/compiler";
const ts = compileWireFile(`
const ts = compileWrnFile(`
page Home {
state count = 0
seo { title = "Home" description = "Welcome" }
@@ -201,6 +201,7 @@ A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` bo
- `@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.
- Client functions automatically commit state changed by `setTimeout` callbacks. For other deferred callbacks (observers, third-party APIs, or detached promise callbacks), call the injected `commit()` function after changing local state; returning/awaiting a promise also commits through the normal function boundary.
- `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.
@@ -213,4 +214,4 @@ A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` bo
## 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.
- Generated modules target WrNexus runtime primitives (`data-scope`, `data-text`, `data-on-*`, `data-for`, `data-component`, `__wrnexus*`/`__wrn*` helpers) — consume the output within a WrNexus app, e.g. via `@wrnexus/core`'s dev loader.
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.8.5",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT",
@@ -37,10 +37,10 @@
}
},
"dependencies": {
"@wrnexus/csr": "^0.8.5",
"@wrnexus/syntax": "^0.8.5",
"@wrnexus/store": "^0.8.5",
"@wrnexus/validation": "^0.8.5"
"@wrnexus/csr": "^0.8.16",
"@wrnexus/syntax": "^0.8.8",
"@wrnexus/store": "^0.8.8",
"@wrnexus/validation": "^0.8.9"
},
"files": [
"dist",
+8 -8
View File
@@ -37,7 +37,7 @@ The `Context` (`ctx`) is the single value passed to middleware and handlers.
| `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). |
| `createContext(req, url)` | fn | Build a fresh `Context` for an incoming request (connects 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`, … |
@@ -70,15 +70,15 @@ cookie-backed `SessionStore`.
### CSRF — `@wrnexus/core`
Double-submit cookie pattern: a readable `wire-csrf` cookie is echoed in an
Double-submit cookie pattern: a readable `wrn-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. |
| 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` | `"wrn-csrf"` / `"x-csrf-token"` | Cookie & header names. |
### Rate limiting — `@wrnexus/core`
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
"version": "0.8.5",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT",
+4 -4
View File
@@ -111,11 +111,11 @@ Intercepts same-origin `<a>` clicks, fetches the target page, and swaps the `#ap
Connects to `/realtime/<name>` over WebSocket (`ws`/`wss` chosen from `location.protocol`). Two usage modes.
Programmatic API via `window.wire`:
Programmatic API via `window.wrn`:
```ts
wire.room(name): Room // open (or reuse) a room connection
wire.bindRooms(root?) // (re)bind declarative [data-room] containers
wrn.room(name): Room // open (or reuse) a room connection
wrn.bindRooms(root?) // (re)bind declarative [data-room] containers
interface Room {
name: string;
@@ -211,7 +211,7 @@ A realtime chat, fully declarative:
Or drive a room from code:
```ts
const room = wire.room("lobby");
const room = wrn.room("lobby");
room.on("chat", (msg) => console.log(msg.user, msg.text));
room.send({ type: "chat", user: "ada", text: "hi" });
```
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.8.5",
"version": "0.8.16",
"type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT",
@@ -37,7 +37,7 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5"
"@wrnexus/core": "^0.8.9"
},
"files": [
"dist",
+2 -2
View File
@@ -128,7 +128,7 @@ const events = await getDb("analytics").all("SELECT * FROM hits");
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.
`up`. Applied names are recorded in a `_wrn_migrations` table so each runs once.
- `parseMigration(name, content)``Migration` (`{ name, up, down }`).
- `loadMigrations(dir)` — parse all `.sql` files, sorted by filename.
@@ -249,7 +249,7 @@ SQL driver — use `@wrnexus/db/mongo` directly.
- **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
`SessionBackend`; `getDb`/`setDb` are connected 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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
"version": "0.8.5",
"version": "0.8.9",
"type": "module",
"description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.",
"license": "MIT",
+12 -12
View File
@@ -6,7 +6,7 @@ Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web fr
## 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`).
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 wrn 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
@@ -21,16 +21,16 @@ bun add @wrnexus/dev-server
### 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. |
| Export | Kind | Purpose |
| --------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `startServer(opts: ServeOptions)` | `Promise<RunningServer>` | Start the dev server on `Bun.serve`: builds the router, connects/migrates databases, connects 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`.
@@ -85,7 +85,7 @@ interface RuntimeDeps {
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)
hasUi?: boolean; // inject the WrNexus 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>
+22 -22
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.8.5",
"version": "0.8.16",
"type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT",
@@ -41,27 +41,27 @@
}
},
"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"
"@wrnexus/authz": "^0.8.9",
"@wrnexus/rpc": "^0.8.9",
"@wrnexus/core": "^0.8.9",
"@wrnexus/dev-toolbar": "^0.8.9",
"@wrnexus/router": "^0.8.9",
"@wrnexus/ssr": "^0.8.9",
"@wrnexus/csr": "^0.8.16",
"@wrnexus/compiler": "^0.8.9",
"@wrnexus/styles": "^0.8.10",
"@wrnexus/ui": "^0.8.12",
"@wrnexus/validation": "^0.8.9",
"@wrnexus/i18n": "^0.8.9",
"@wrnexus/db": "^0.8.9",
"@wrnexus/pubsub": "^0.8.9",
"@wrnexus/uploader": "^0.8.9",
"@wrnexus/plugin": "^0.8.8",
"@wrnexus/store": "^0.8.8",
"@wrnexus/security": "^0.8.8",
"@wrnexus/observability": "^0.8.8",
"@wrnexus/cache": "^0.8.8",
"@wrnexus/pwa": "^0.8.8"
},
"files": [
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.8.5",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
"license": "MIT",
-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"
]
}
+2 -2
View File
@@ -22,7 +22,7 @@ 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 },
cookie: { name: "wrn-lang", sameSite: "Lax", secure: true },
});
const lang = resolveLang(i18n, cookieValue, request.headers.get("accept-language"));
@@ -57,7 +57,7 @@ Enable `i18nPlugin()` to use:
- `<LanguageSwitcher />`
- `<LocaleStatus />`
`LanguageSwitcher` renders a native `select[data-wire-lang]`. The packaged runtime validates the
`LanguageSwitcher` renders a native `select[data-wrn-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.
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
"version": "0.8.5",
"version": "0.8.9",
"type": "module",
"description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.",
"license": "MIT",
@@ -42,9 +42,9 @@
"./components/*": "./components/*"
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/ui": "^0.8.5"
"@wrnexus/core": "^0.8.9",
"@wrnexus/plugin": "^0.8.8",
"@wrnexus/ui": "^0.8.12"
},
"wrnexus": {
"plugin": {
-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"
]
}
+5 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
"version": "0.8.5",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT",
@@ -44,6 +44,10 @@
"import": "./dist/redis.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.8.9",
"@wrnexus/rpc": "^0.8.9"
},
"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"
]
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/router",
"version": "0.8.5",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT",
@@ -37,8 +37,8 @@
}
},
"dependencies": {
"@wrnexus/compiler": "^0.8.5",
"@wrnexus/core": "^0.8.5"
"@wrnexus/compiler": "^0.8.9",
"@wrnexus/core": "^0.8.9"
},
"files": [
"dist",
+1 -1
View File
@@ -89,7 +89,7 @@ const html = renderDocument({
},
url: new URL("https://acme.example/about"),
body: "<h1>About Us</h1>",
scripts: ["/_wire/runtime.js", "/_wire/islands/about.js"],
scripts: ["/_wrn/runtime.js", "/_wrn/islands/about.js"],
htmlAttrs: ' data-theme="dark"',
});
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
"version": "0.8.5",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT",
@@ -45,9 +45,9 @@
}
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/store": "^0.8.5",
"@wrnexus/security": "^0.8.5"
"@wrnexus/core": "^0.8.9",
"@wrnexus/store": "^0.8.8",
"@wrnexus/security": "^0.8.8"
},
"files": [
"dist",
+13 -13
View File
@@ -21,7 +21,7 @@ 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.
> Global CSS bundling, the `--wrn-*` 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.
@@ -30,7 +30,7 @@ Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web fr
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.
2. **Theme system** — design tokens exposed as CSS custom properties (`--wrn-<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.
@@ -111,7 +111,7 @@ interface StyleProcessContext {
| 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_COOKIE` | `"wrn-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). |
@@ -119,7 +119,7 @@ interface StyleProcessContext {
| `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.
Tokens are emitted as `--wrn-<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`:
@@ -164,7 +164,7 @@ theme: {
}
```
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.
The client runtime (`renderThemeRuntime`) exposes `window.wrnTheme` with `{ get, set, toggle, bind, themes }`, connects up any `[data-wrn-theme-toggle]` and `[data-wrn-theme-set]` elements on load, and persists the choice to the `wrn-theme` cookie (`max-age` 1 year, `samesite=lax`). `toggle()` cycles through the configured theme names in order.
## Usage
@@ -252,17 +252,17 @@ 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);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius);
box-shadow: var(--wrn-shadow-1);
}
```
```html
<button data-wire-theme-toggle>Toggle theme</button>
<button data-wire-theme-set="brand">Brand theme</button>
<button data-wrn-theme-toggle>Toggle theme</button>
<button data-wrn-theme-set="brand">Brand theme</button>
```
## Requirements / Notes
@@ -270,4 +270,4 @@ In templates, consume tokens via the custom properties:
- **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.
- The bundled global stylesheet, the theme stylesheet (`THEME_CSS_HREF`), and the theme runtime (`THEME_JS_HREF`) are connected into pages by the framework's server; this package only produces their contents.
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
"version": "0.8.5",
"version": "0.8.10",
"type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT",
@@ -37,9 +37,9 @@
}
},
"dependencies": {
"@wrnexus/uploader": "^0.8.5",
"@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.8.5"
"@wrnexus/uploader": "^0.8.9",
"@wrnexus/core": "^0.8.9",
"@wrnexus/plugin": "^0.8.8"
},
"files": [
"dist",
-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"
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/test",
"version": "0.8.5",
"version": "0.8.9",
"type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT",
-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"
]
}
+42 -96
View File
@@ -1,6 +1,6 @@
# 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.
This reference is generated from the 102 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
## Advanced-forms
@@ -22,15 +22,6 @@ Editable autocomplete combobox with local and remote suggestions.
- 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.
@@ -229,7 +220,7 @@ Present structured responsive linked or status items with icons, descriptions, a
- 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
- Outputs: `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### ListGroup
@@ -247,7 +238,7 @@ Continuously present responsive labels, partners, notices, or capabilities with
- 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
- Outputs: `pause({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `resume({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Progress
@@ -301,25 +292,16 @@ Present responsive chronological activity, milestones, or workflow status with r
- 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 })`
- Outputs: `select({ 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 = ""`
- Props: `title: string = "Tree View"`, `description: string = ""`, `items: unknown[] = []`, `valueKey: string = "value"`, `labelKey: string = "label"`, `defaultExpanded: unknown[] = []`, `selected: string = ""`, `size: string = "default"`, `color: string = "primary"`, `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 })`
- Outputs: `select({ item: object; value: string; sourceEvent?: Event })`, `toggle({ item: object; value: string; expanded: boolean; sourceEvent?: Event })`, `expand({ item: object; value: string; sourceEvent?: Event })`, `collapse({ item: object; value: string; sourceEvent?: Event })`
## Core
@@ -469,7 +451,7 @@ Provide an accessible responsive search field with labels, validation states, si
- 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
- Outputs: `search({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `clear({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### Select
@@ -509,50 +491,32 @@ Theme-aware, responsive time picker component.
## 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 = ""`
- Props: `title: string = "Chart"`, `description: string = ""`, `items: unknown[] = []`, `valueKey: string = "value"`, `labelKey: string = "label"`, `height: number = 240`, `showLegend: boolean = true`, `showValues: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "bar"`, `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[])`
- Outputs: `select({ item: object; index: number; sourceEvent?: Event })`, `dataPointClick({ item: object; index: number; sourceEvent?: Event })`, `legendToggle({ item: object; index: number; hidden: boolean; sourceEvent?: Event })`
### 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 = ""`
- Props: `value: string = ""`, `label: string = "Copy"`, `copiedLabel: string = "Copied"`, `errorLabel: string = "Copy failed"`, `title: string = "Clipboard"`, `description: string = ""`, `code: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `disabled: boolean = false`, `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)`
- Outputs: `copy({ value: string; sourceEvent?: Event })`, `success({ value: string; sourceEvent?: Event })`, `error({ error: Error | string; value: string; sourceEvent?: Event })`
### 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 = ""`
- Props: `label: string = "Celebrate"`, `duration: number = 1200`, `count: number = 24`, `autoStart: boolean = false`, `disabled: boolean = false`, `size: string = "default"`, `color: string = "primary"`, `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 })`
- Outputs: `start({ sourceEvent?: Event; duration: number })`, `complete({ sourceEvent?: Event; duration: number })`
### DataMap
@@ -572,15 +536,6 @@ Theme-aware, responsive drag and drop component.
- 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.
@@ -588,16 +543,7 @@ Present responsive location information and markers with map-ready metadata and
- 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 })`
- Outputs: `markerClick({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `zoom({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
### WysiwygEditor
@@ -624,7 +570,7 @@ Create responsive balanced content columns with configurable count, gap, density
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 = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `centered: boolean = true`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -633,16 +579,16 @@ Constrain and align page content with responsive gutters and compact, wide, or f
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 = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `axis: string = "vertical"`, `thickness: number = 8`, `maxHeight: string = "20rem"`, `radius: string = "999px"`, `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[])`
- Outputs: None
### 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 = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = ""`, `orientation: string = "horizontal"`, `variant: string = "solid"`, `class: string = ""`
- Slots: None
- Outputs: None
@@ -669,7 +615,7 @@ Render structured responsive footer navigation, pre and post content, copyright
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 = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `columns: number = 2`, `gap: string = "md"`, `maxWidth: string = "xl"`, `minItemWidth: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -678,7 +624,7 @@ Arrange arbitrary content in a responsive configurable CSS grid with stable colu
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 = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `src: string = ""`, `alt: string = ""`, `width: string = ""`, `height: string = ""`, `loading: string = "lazy"`, `fit: string = "cover"`, `rounded: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -687,7 +633,7 @@ Render a responsive image with explicit dimensions, loading behavior, alternativ
Theme-aware, responsive kbd component.
- Mount: `data-component="Kbd"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "K"`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "K"`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -696,16 +642,16 @@ Theme-aware, responsive kbd component.
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[])`
- Props: `color: string = "primary"`, `size: number = 50`, `orientation: string = "horizontal"`, `minSize: number = 15`, `step: number = 5`, `label: string = "Resize panels"`, `class: string = ""`
- Slots: `start`, `end`, `default`
- Outputs: `sizeChange({ size: number })`
### 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 = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Link"`, `href: string = "#"`, `target: string = ""`, `rel: string = ""`, `underline: string = "hover"`, `external: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: None
@@ -862,18 +808,18 @@ Show responsive hierarchical navigation with home support, separators, current-p
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 = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `footer: string = ""`, `defaultOpen: boolean = false`, `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)`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`
### 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 = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `label: string = "Main"`, `collapsible: boolean = true`, `toggleLabel: string = "Menu"`, `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)`
- Outputs: `select({ item: object; value: string })`
### Navbar
@@ -889,45 +835,45 @@ Theme-aware, responsive navbar component.
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 = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `page: number = 1`, `pageSize: number = 10`, `total: number = 0`, `variant: string = "compact"`, `siblingCount: number = 1`, `showSummary: boolean = true`, `label: string = "Pagination"`, `previousLabel: string = "Previous"`, `nextLabel: string = "Next"`, `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 })`
- Outputs: `change({ page: number; pageSize: number })`, `previous({ page: number })`, `next({ page: number })`
### 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 = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `label: string = "On this page"`, `heading: string = ""`, `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)`
- Outputs: `change({ href: string; label: string })`
### 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 })`
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Sidebar"`, `items: unknown[] = []`, `active: string = ""`, `mobileLabel: string = "Open navigation"`, `drawerTitle: string = "Navigation"`, `class: string = ""`
- Slots: `default`, `drawer`
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent: Event })`, `close({ source: string })`, `select({ item: object; value: string; level: number })`
### 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 })`
- Props: `color: string = "primary"`, `size: string = "default"`, `steps: unknown[] = []`, `active: number = 0`, `orientation: string = "horizontal"`, `clickable: boolean = false`, `label: string = "Progress"`, `showPanel: boolean = false`, `controls: boolean = false`, `allowSkip: boolean = false`, `nextDisabled: boolean = false`, `backLabel: string = "Back"`, `nextLabel: string = "Next"`, `skipLabel: string = "Skip"`, `finishLabel: string = "Finish"`, `class: string = ""`
- Slots: `step-{index}`, `panel-{index}`, `default`
- Outputs: `change({ index: number; step: object })`, `back({ index: number; step: object })`, `next({ index: number; step: object })`, `skip({ index: number; step: object })`, `finish({ index: number; step: 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
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `mode: string = "client"`, `param: string = "tab"`, `label: string = "Tabs"`, `class: string = ""`
- Slots: `panel-{valueOf(item, index)}`, `default`
- Outputs: `change({ value: string; item: object; index: number })`, `select({ value: string; item: object; index: number })`
## Overlays
+40 -44
View File
@@ -1,6 +1,6 @@
# @wrnexus/ui
> First-party Wire UI component library — a set of themeable `.wrn` components plus a single tokenized stylesheet.
> First-party WrNexus 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.
@@ -11,7 +11,7 @@ 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
driven by `var(--wrn-*)` 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.
@@ -30,7 +30,7 @@ bun add @wrnexus/ui
> (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
`@wrnexus/dev-server` already depend on it and wrn it into the router for you
(see [Auto-discovery](#auto-discovery)).
## Components
@@ -54,19 +54,19 @@ element), and most render their body from either a named prop or the default slo
### 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` |
| 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-wrn-theme-toggle`) | `label` |
### Additional controls & data display
@@ -100,7 +100,7 @@ are `.wrn` files rendered server-side.
| ------------------ | -------------------------- | ------------------------------------------------------------------------------------------ |
| `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). |
| `uiCss` | `() => string` | The `ui.css` file contents (all `.wrn-*` 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. |
@@ -118,21 +118,17 @@ are `.wrn` files rendered server-side.
The framework serves this stylesheet once at `/__wrnexus/ui.css`, so pages get all
component styles from a single request.
### Tailwind and motion
### Component styles 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";
```
Components own their BEM styles in local `style {}` blocks and do not require a
Tailwind scan. `ui.css` supplies only global tokens, resets, and shared
primitives. Applications can still use Tailwind independently in their own
source files.
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
entry and interaction motion. Override `--wrn-motion-fast`,
`--wrn-motion-base`, `--wrn-motion-slow`, `--wrn-ease-standard`, or
`--wrn-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
@@ -143,21 +139,21 @@ 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);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
border: 1px solid var(--wrn-color-border);
}
.account-card__action {
background: var(--wire-color-primary);
color: var(--wire-color-primary-contrast);
background: var(--wrn-color-primary);
color: var(--wrn-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`.
Stable no-spacing helper classes are also available: `wrn-bg-page`,
`wrn-bg-surface`, `wrn-bg-surface-2`, `wrn-bg-primary`, `wrn-bg-secondary`,
`wrn-text`, `wrn-text-muted`, `wrn-text-primary`, `wrn-text-success`,
`wrn-text-warning`, `wrn-text-danger`, and `wrn-border`.
Tailwind-authored custom markup can continue using the palette families already
used by packaged components. `indigo-*` and `violet-*` resolve to primary,
@@ -173,7 +169,7 @@ 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:
both wrn the UI directory in for you:
```ts
import { buildRouter } from "@wrnexus/router";
@@ -199,11 +195,11 @@ attributes (other than `data-component`) become string props:
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
1. **Theme tokens** — override CSS custom properties such as `--wrn-color-primary`,
`--wrn-color-surface`, `--wrn-radius-sm`, etc. Every component style resolves
through `var(--wrn-*)`, 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
2. **App CSS** — redefine a `.wrn-*` 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
@@ -221,4 +217,4 @@ Ways to customize the components, in increasing order of power:
[`@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.
`data-wrn-theme-toggle` attribute — no per-component JS is required.
-30
View File
@@ -6,16 +6,6 @@
"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",
@@ -151,11 +141,6 @@
"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",
@@ -221,11 +206,6 @@
"category": "forms",
"purpose": "Theme-aware, responsive file input component."
},
{
"name": "FileUpload",
"category": "integrations",
"purpose": "Theme-aware, responsive file upload component."
},
{
"name": "FileUploadProgress",
"category": "base",
@@ -496,16 +476,6 @@
"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",
+10 -3
View File
@@ -1,5 +1,12 @@
{
"generatedFrom": "full component catalog reset",
"removedCount": 0,
"replacements": {}
"generatedFrom": "framework remediation 0.8.7",
"removedCount": 6,
"replacements": {
"AdvancedDatePicker": "DatePicker",
"AdvancedRangeSlider": "RangeSlider",
"FileUpload": "FileInput",
"Toast": "Toaster",
"ToastNotifications": "Toaster",
"CopyMarkup": "Clipboard"
}
}
File diff suppressed because it is too large Load Diff
+296 -24
View File
@@ -49,22 +49,24 @@ component Accordion {
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, {
// Named rather than computed: an output is resolved as a property name, so
// output[eventName] would not reach a parent binding.
client function dispatchAccordionEvent(sourceEvent, eventName, value, item, payload) {
payload = {
component: "Accordion",
value: value,
item: item,
open: isOpen(value),
openValues: openValues
})
root.dispatchEvent(customEvent)
}
if (eventName === "open") {
output.open(payload)
} else if (eventName === "close") {
output.close(payload)
} else {
output.change(payload)
}
}
client function toggleItem(sourceEvent, value, item, wasOpen) {
@@ -101,15 +103,15 @@ component 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}"
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--accordion wrn-next--accordion-{variant} {bordered ? 'wrn-next--accordion-bordered' : ''} {separated ? 'wrn-next--accordion-separated' : ''} {flush ? 'wrn-next--accordion-flush' : ''} {disabled ? 'wrn-next--disabled' : ''} {class}"
>
{#each items as item, index}
<section
class="wire-next__accordion-item"
class="wrn-next__accordion-item"
data-open="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
data-disabled="{disabled || item.disabled ? 'true' : 'false'}"
>
<h3 class="wire-next__accordion-heading">
<h3 class="wrn-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}"
@@ -119,7 +121,7 @@ component Accordion {
@click="toggleItem(event, itemValue(item, index), item)"
>
{#if showIndicator && indicatorPosition === "start"}
<span class="wire-next__accordion-indicator" aria-hidden="true">
<span class="wrn-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
@@ -129,7 +131,7 @@ component Accordion {
{/if}
<span>{item.label || item.title}</span>
{#if showIndicator && indicatorPosition === "end"}
<span class="wire-next__accordion-indicator" aria-hidden="true">
<span class="wrn-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
@@ -142,23 +144,23 @@ component Accordion {
<div
id="{id}-panel-{index}"
class="wire-next__accordion-panel"
class="wrn-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' : ''}">
<div class="wrn-next__accordion-content {contentItalic ? 'wrn-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">
<div class="wrn-next__accordion-nested">
{#each item.children as child, childIndex}
<section
class="wire-next__accordion-item"
class="wrn-next__accordion-item"
data-open="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
>
<h4 class="wire-next__accordion-heading">
<h4 class="wrn-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}-{childIndex}"
@@ -168,7 +170,7 @@ component Accordion {
@click="toggleItem(event, nestedValue(item, index, child, childIndex), child)"
>
{#if showIndicator}
<span class="wire-next__accordion-indicator" aria-hidden="true">
<span class="wrn-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
@@ -181,13 +183,13 @@ component Accordion {
</h4>
<div
id="{id}-panel-{index}-{childIndex}"
class="wire-next__accordion-panel"
class="wrn-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' : ''}">
<div class="wrn-next__accordion-content {contentItalic ? 'wrn-next__accordion-content-italic' : ''}">
<p>{child.content}</p>
</div>
</div>
@@ -203,4 +205,274 @@ component Accordion {
{/each}
</div>
}
style {
.wrn-next--accordion {
--wrn-accordion-accent: var(--wrn-component-color);
display: grid;
width: 100%;
color: var(--wrn-color-text);
}
.wrn-next__accordion-item {
min-width: 0;
}
.wrn-next__accordion-heading {
margin: 0;
font: inherit;
}
.wrn-next__accordion-heading > button {
display: flex;
width: 100%;
align-items: center;
gap: 0.75rem;
padding: 0.85rem 0;
border: 0;
background: transparent;
color: var(--wrn-color-text);
font: inherit;
font-size: 0.88rem;
font-weight: 700;
text-align: left;
cursor: pointer;
}
.wrn-next__accordion-heading > button > span:not(.wrn-next__accordion-indicator) {
min-width: 0;
flex: 1;
}
.wrn-next__accordion-heading > button:focus-visible {
border-radius: var(--wrn-radius-sm);
outline: 2px solid var(--wrn-accordion-accent);
outline-offset: 2px;
}
.wrn-next__accordion-heading > button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.wrn-next__accordion-indicator {
display: inline-flex;
width: 1rem;
height: 1rem;
flex: 0 0 1rem;
align-items: center;
justify-content: center;
color: var(--wrn-color-muted);
font-size: 1rem;
font-weight: 500;
line-height: 1;
transition:
color var(--wrn-motion-fast) var(--wrn-ease-standard),
transform var(--wrn-motion-fast) var(--wrn-ease-standard);
}
.wrn-next__accordion-indicator > span {
display: inline-flex;
align-items: center;
justify-content: center;
}
.wrn-next__accordion-indicator > [class*="icon-["] {
width: 0.95rem;
height: 0.95rem;
}
.wrn-next__accordion-item[data-open="true"]
> .wrn-next__accordion-heading
.wrn-next__accordion-indicator {
color: var(--wrn-accordion-accent);
transform: rotate(45deg);
}
.wrn-next--accordion[data-indicator="chevron"]
.wrn-next__accordion-item[data-open="true"]
> .wrn-next__accordion-heading
.wrn-next__accordion-indicator {
transform: rotate(180deg);
}
.wrn-next__accordion-item[data-open="true"] > .wrn-next__accordion-heading > button {
color: var(--wrn-accordion-accent);
}
.wrn-next__accordion-panel {
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transition:
grid-template-rows var(--wrn-motion-base) var(--wrn-ease-standard),
opacity var(--wrn-motion-fast) var(--wrn-ease-standard);
}
.wrn-next__accordion-item[data-open="true"] > .wrn-next__accordion-panel {
grid-template-rows: 1fr;
opacity: 1;
}
.wrn-next__accordion-panel > div {
overflow: hidden;
}
.wrn-next__accordion-content {
padding: 0 0 1rem 1.75rem;
color: var(--wrn-color-muted);
font-size: 0.86rem;
line-height: 1.65;
}
.wrn-next__accordion-content > p {
color: inherit;
}
.wrn-next__accordion-content-italic > p::first-line {
font-style: italic;
}
.wrn-next__accordion-nested {
display: grid;
margin-top: 0.5rem;
}
.wrn-next__accordion-nested .wrn-next__accordion-content {
padding-left: 1.5rem;
}
.wrn-next--accordion-bordered {
overflow: hidden;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
}
.wrn-next--accordion-bordered > .wrn-next__accordion-item + .wrn-next__accordion-item {
border-top: 1px solid var(--wrn-color-border);
}
.wrn-next--accordion-bordered
> .wrn-next__accordion-item
> .wrn-next__accordion-heading
> button {
padding-inline: 1rem;
}
.wrn-next--accordion-bordered
> .wrn-next__accordion-item
> .wrn-next__accordion-panel
.wrn-next__accordion-content {
padding-inline: 1rem;
}
.wrn-next--accordion-separated {
gap: 0.5rem;
}
.wrn-next--accordion-separated > .wrn-next__accordion-item {
overflow: hidden;
border: 1px solid transparent;
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface-2);
}
.wrn-next--accordion-separated > .wrn-next__accordion-item[data-open="true"] {
border-color: var(--wrn-color-border);
}
.wrn-next--accordion-separated
> .wrn-next__accordion-item
> .wrn-next__accordion-heading
> button,
.wrn-next--accordion-separated
> .wrn-next__accordion-item
> .wrn-next__accordion-panel
.wrn-next__accordion-content {
padding-inline: 1rem;
}
.wrn-next--accordion-flush {
border-radius: 0;
background: transparent;
box-shadow: none;
}
@media (prefers-reduced-motion: reduce) {
.wrn-next__accordion-panel,
.wrn-next__accordion-indicator {
transition: none;
}
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
@@ -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>
}
}
+159 -30
View File
@@ -1,3 +1,5 @@
import SelectStyles from "../styles/SelectStyles.wrn"
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)
@@ -253,7 +255,7 @@ component AdvancedSelect {
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}"
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--advanced-select {open ? 'wrn-next--open' : ''} {fixed ? 'wrn-next--advanced-select-fixed' : ''} {invalid ? 'wrn-next--invalid' : ''} {disabled ? 'wrn-next--disabled' : ''} {class}"
data-placement="{placement}"
data-search-mode="{searchMode}"
data-remote="{remote ? 'true' : 'false'}"
@@ -266,17 +268,17 @@ component AdvancedSelect {
data-wrn-select
@focusout="if (!event.currentTarget.contains(event.relatedTarget)) { open = false }"
>
<div class="wire-next__row">
<div class="wrn-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">
<div class="wrn-next__select-control">
<button
{...attrs}
id="{name}-trigger"
type="button"
class="wire-next__select-trigger"
class="wrn-next__select-trigger"
role="combobox"
aria-haspopup="listbox"
aria-expanded="{open}"
@@ -287,7 +289,7 @@ component AdvancedSelect {
@click="toggle()"
@keydown="handleKeydown(event)"
>
<span class="wire-next__select-value">
<span class="wrn-next__select-value">
<span data-show="!multiple" data-text="triggerText()">{triggerText()}</span>
<span
data-show="multiple && selectedTemplate === 'count' && selectedCount() > 0"
@@ -298,7 +300,7 @@ component AdvancedSelect {
data-text="selectedText()"
>{selectedText()}</span>
<span
class="wire-next__select-tags"
class="wrn-next__select-tags"
aria-label="{selectedOptionsLabel}"
data-show="multiple && selectedTemplate !== 'count' && selectedTemplate !== 'text' && selectedCount() > 0"
>
@@ -306,25 +308,25 @@ component AdvancedSelect {
<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}
{#if optionTemplate === "color" && option.color}<i class="wrn-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 class="wrn-next__placeholder">{placeholder}</span>
</span>
</span>
<span
class="wire-next__select-chevron icon-[lucide--chevrons-up-down]"
class="wrn-next__select-chevron icon-[lucide--chevrons-up-down]"
aria-hidden="true"
></span>
</button>
<button
type="button"
class="wire-next__clear-select"
class="wrn-next__clear-select"
data-show="clearable && allowEmpty && selectedCount() > 0 && !disabled"
aria-label="{clearLabel}"
title="{clearLabel}"
@@ -335,14 +337,14 @@ component AdvancedSelect {
</div>
<div
class="wire-next__select-dropdown {fixed ? 'wire-next__select-dropdown--fixed' : ''}"
class="wrn-next__select-dropdown {fixed ? 'wrn-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>
<label class="wrn-next__select-search">
<span class="wrn-visually-hidden">{searchPlaceholder}</span>
<span class="wrn-next__search-icon" aria-hidden="true">⌕</span>
<input
type="search"
value="{query}"
@@ -355,28 +357,28 @@ component AdvancedSelect {
{/if}
<div
class="wire-next__select-message"
class="wrn-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>
<div class="wrn-next__select-message" data-show="loading" role="status">
<i class="wrn-spinner wrn-spinner--inline" aria-hidden="true"></i>
{loadingLabel}
</div>
<div
id="{name}-listbox"
class="wire-next__select-list"
class="wrn-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>
<div class="wrn-next__option-group" role="group" aria-label="{group.label}">
<div class="wrn-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' : ''}"
class="wrn-next__select-option {isSelected(option) ? 'wrn-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wrn-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
@@ -387,10 +389,10 @@ component AdvancedSelect {
>
{#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}
{#if optionTemplate === "color" && option.color}<i class="wrn-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]"
class="wrn-next__check icon-[lucide--check]"
data-show="isSelected(option)"
aria-hidden="true"
></i>
@@ -403,7 +405,7 @@ component AdvancedSelect {
{#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' : ''}"
class="wrn-next__select-option {isSelected(option) ? 'wrn-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wrn-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
@@ -414,10 +416,10 @@ component AdvancedSelect {
>
{#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}
{#if optionTemplate === "color" && option.color}<i class="wrn-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]"
class="wrn-next__check icon-[lucide--check]"
data-show="isSelected(option)"
aria-hidden="true"
></i>
@@ -425,16 +427,16 @@ component AdvancedSelect {
{/each}
{#if !loading && !flatVisibleOptions().length}
<div class="wire-next__select-message">{emptyLabel}</div>
<div class="wrn-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>
<button type="button" class="wrn-next__create-option">{createLabel} “{query}”</button>
{/if}
<button
type="button"
class="wire-next__load-more"
class="wrn-next__load-more"
data-wrn-select-load-more
data-show="infinite && hasMore"
>{loadMoreLabel}</button>
@@ -451,9 +453,136 @@ component AdvancedSelect {
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="wire-next__validation"
class="wrn-next__validation"
data-error="{name}"
>{validationMessage}</small>
</div>
<SelectStyles hidden />
}
style {
.wrn-next__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.wrn-next__select-search {
position: relative;
display: block;
}
.wrn-next__select-search input {
padding-left: 2.25em;
}
.wrn-next__select-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.wrn-next__select-tags > span {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.3rem 0.55rem;
border-radius: 999px;
color: var(--wrn-component-color);
background: color-mix(in srgb, var(--wrn-component-color) 12%, transparent);
font-size: 0.75em;
font-weight: 700;
}
.wrn-next__select-tags img {
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
object-fit: cover;
}
.wrn-next__select-value {
display: flex;
min-width: 0;
align-items: center;
gap: 0.55em;
}
.wrn-next__placeholder {
overflow: hidden;
color: var(--wrn-color-muted);
text-overflow: ellipsis;
white-space: nowrap;
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+288 -17
View File
@@ -30,38 +30,34 @@ component AvatarGroup {
return items.slice(Number(maxVisible))
}
client function toggleOverflow(sourceEvent, root, customEvent) {
client function toggleOverflow() {
overflowOpen = !overflowOpen
root = sourceEvent.currentTarget.closest("[data-wrn-avatar-group]")
customEvent = document.createEvent("CustomEvent")
customEvent.initCustomEvent("overflow", true, false, {
output.overflow({
component: "AvatarGroup",
open: overflowOpen,
hiddenCount: hiddenMembers().length
})
root.dispatchEvent(customEvent)
}
}
view {
<div
{...attrs}
class="wire-next wire-next--avatar-group {class}"
class="wrn-component wrn-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)'}"
style="--wrn-avatar-group-columns:{columns}; --wrn-avatar-group-ring:{borderColor || 'var(--wrn-color-bg)'}"
role="group"
aria-label="Avatar group"
>
<div
class="wire-next__avatar-group-members"
class="wrn-next__avatar-group-members"
>
{#each visibleMembers() as item}
<span
class="wire-next__avatar-group-member"
class="wrn-next__avatar-group-member"
data-size="{item.size || size}"
data-shape="{item.shape || shape}"
data-color="{item.color || color}"
@@ -70,7 +66,7 @@ component AvatarGroup {
aria-label="{item.name || item.alt || item.initials || 'Group member'}"
>
<span
class="wire-next__avatar-group-avatar"
class="wrn-next__avatar-group-avatar"
>
{#if item.src}
<img
@@ -95,7 +91,7 @@ component AvatarGroup {
{#if showTooltips && (item.tooltip || item.name)}
<span
class="wire-next__avatar-group-tooltip"
class="wrn-next__avatar-group-tooltip"
role="tooltip"
>
{item.tooltip || item.name}
@@ -106,11 +102,11 @@ component AvatarGroup {
{#if hiddenMembers().length > 0}
<span
class="wire-next__avatar-group-overflow"
class="wrn-next__avatar-group-overflow"
>
<button
type="button"
class="wire-next__avatar-group-overflow-button"
class="wrn-next__avatar-group-overflow-button"
aria-label="{overflowLabel}"
aria-expanded="{overflowOpen ? 'true' : 'false'}"
@click="toggleOverflow(event)"
@@ -119,18 +115,18 @@ component AvatarGroup {
</button>
<span
class="wire-next__avatar-group-menu"
class="wrn-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"
class="wrn-next__avatar-group-menu-item"
role="menuitem"
>
<span
class="wire-next__avatar-group-menu-avatar"
class="wrn-next__avatar-group-menu-avatar"
>
{#if item.src}
<img
@@ -151,4 +147,279 @@ component AvatarGroup {
</div>
</div>
}
style {
/* --- Avatar group -------------------------------------------------------- */
.wrn-next--avatar-group {
--wrn-avatar-group-size: 3rem;
position: relative;
display: inline-flex;
width: fit-content;
max-width: 100%;
}
.wrn-next--avatar-group[data-size="xs"] {
--wrn-avatar-group-size: 1.75rem;
}
.wrn-next--avatar-group[data-size="sm"] {
--wrn-avatar-group-size: 2.25rem;
}
.wrn-next--avatar-group[data-size="md"],
.wrn-next--avatar-group[data-size="default"] {
--wrn-avatar-group-size: 3rem;
}
.wrn-next--avatar-group[data-size="lg"] {
--wrn-avatar-group-size: 4rem;
}
.wrn-next--avatar-group[data-size="xl"] {
--wrn-avatar-group-size: 5rem;
}
.wrn-next__avatar-group-members {
display: flex;
align-items: center;
}
.wrn-next--avatar-group[data-layout="stack"] .wrn-next__avatar-group-member,
.wrn-next--avatar-group[data-layout="stack"] .wrn-next__avatar-group-overflow {
margin-left: calc(var(--wrn-avatar-group-size) * -0.22);
}
.wrn-next--avatar-group[data-layout="stack"] :first-child {
margin-left: 0;
}
.wrn-next--avatar-group[data-layout="grid"] .wrn-next__avatar-group-members {
display: grid;
grid-template-columns: repeat(var(--wrn-avatar-group-columns), var(--wrn-avatar-group-size));
gap: 0.65rem;
}
.wrn-next__avatar-group-member,
.wrn-next__avatar-group-overflow {
--wrn-avatar-group-color: var(--wrn-color-primary);
position: relative;
display: inline-grid;
width: var(--wrn-avatar-group-size);
height: var(--wrn-avatar-group-size);
flex: none;
place-items: center;
border-radius: 999px;
outline: none;
transition:
z-index var(--wrn-motion-fast),
transform var(--wrn-motion-base) var(--wrn-ease-emphasized);
}
.wrn-next__avatar-group-member[data-color="secondary"] {
--wrn-avatar-group-color: #737373;
}
.wrn-next__avatar-group-member[data-color="success"] {
--wrn-avatar-group-color: var(--wrn-color-success);
}
.wrn-next__avatar-group-member[data-color="info"] {
--wrn-avatar-group-color: var(--wrn-color-info);
}
.wrn-next__avatar-group-member[data-color="warning"] {
--wrn-avatar-group-color: var(--wrn-color-warning);
}
.wrn-next__avatar-group-member[data-color="danger"] {
--wrn-avatar-group-color: var(--wrn-color-danger);
}
.wrn-next__avatar-group-member[data-shape="rounded"] {
border-radius: calc(var(--wrn-radius-sm, 0.5rem) * 0.72);
}
.wrn-next__avatar-group-avatar,
.wrn-next__avatar-group-overflow-button {
display: grid;
width: 100%;
height: 100%;
overflow: hidden;
place-items: center;
color: white;
border: 2px solid var(--wrn-avatar-group-ring);
border-radius: inherit;
background: var(--wrn-avatar-group-color, var(--wrn-color-primary));
box-shadow: 0 5px 14px rgb(0 0 0 / 0.16);
font: inherit;
font-size: calc(var(--wrn-avatar-group-size) * 0.3);
font-weight: 800;
}
.wrn-next__avatar-group-avatar img,
.wrn-next__avatar-group-menu-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.wrn-next__avatar-group-overflow-button {
cursor: pointer;
color: var(--wrn-color-text);
background: var(--wrn-color-surface-2);
}
.wrn-next__avatar-group-member:hover,
.wrn-next__avatar-group-member:focus-visible,
.wrn-next__avatar-group-overflow:focus-within {
z-index: 5;
transform: translateY(-3px);
}
.wrn-next__avatar-group-member:focus-visible,
.wrn-next__avatar-group-overflow-button:focus-visible {
outline: 2px solid var(--wrn-color-primary);
outline-offset: 3px;
}
.wrn-next__avatar-group-tooltip {
position: absolute;
left: 50%;
bottom: calc(100% + 0.65rem);
z-index: 20;
width: max-content;
max-width: 14rem;
padding: 0.4rem 0.6rem;
color: var(--wrn-color-bg);
border-radius: 0.4rem;
background: var(--wrn-color-text);
box-shadow: 0 8px 22px rgb(0 0 0 / 0.22);
font-size: 0.75rem;
font-weight: 650;
pointer-events: none;
opacity: 0;
transform: translate(-50%, 0.25rem);
transition:
opacity var(--wrn-motion-fast),
transform var(--wrn-motion-fast);
}
.wrn-next__avatar-group-member:hover .wrn-next__avatar-group-tooltip,
.wrn-next__avatar-group-member:focus-visible .wrn-next__avatar-group-tooltip {
opacity: 1;
transform: translate(-50%, 0);
}
.wrn-next__avatar-group-menu {
position: absolute;
top: calc(100% + 0.65rem);
right: 0;
z-index: 30;
display: grid;
width: max-content;
min-width: 12rem;
gap: 0.25rem;
padding: 0.45rem;
color: var(--wrn-color-text);
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm, 0.5rem);
background: var(--wrn-color-surface);
box-shadow: 0 16px 38px rgb(0 0 0 / 0.22);
}
.wrn-next__avatar-group-menu-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.45rem 0.55rem;
border-radius: 0.4rem;
font-size: 0.82rem;
}
.wrn-next__avatar-group-menu-item:hover {
background: var(--wrn-color-surface-2);
}
.wrn-next__avatar-group-menu-avatar {
display: grid;
width: 1.75rem;
height: 1.75rem;
overflow: hidden;
flex: none;
place-items: center;
border-radius: 999px;
background: var(--wrn-color-surface-2);
font-size: 0.65rem;
font-weight: 800;
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+278 -6
View File
@@ -18,7 +18,7 @@ component Blockquote {
view {
<figure
{...attrs}
class="wire-next wire-next--blockquote wire-next--color-{color} wire-next--size-{size} {class}"
class="wrn-component wrn-next--blockquote wrn-component--color-{color} wrn-component--size-{size} {class}"
data-size="{size}"
data-color="{color}"
data-align="{align}"
@@ -27,10 +27,10 @@ component Blockquote {
>
<blockquote cite="{citationUrl}">
{#if quoteMark}
<span class="wire-next__blockquote-mark" aria-hidden="true">“</span>
<span class="wrn-next__blockquote-mark" aria-hidden="true">“</span>
{/if}
<div class="wire-next__blockquote-copy">
<div class="wrn-next__blockquote-copy">
{#if quote}
<p>{quote}</p>
{:else}
@@ -40,17 +40,17 @@ component Blockquote {
</blockquote>
{#if citation || citationTitle || avatarSrc}
<figcaption class="wire-next__blockquote-citation">
<figcaption class="wrn-next__blockquote-citation">
{#if avatarSrc}
<img
class="wire-next__blockquote-avatar"
class="wrn-next__blockquote-avatar"
src="{avatarSrc}"
alt="{avatarAlt}"
loading="lazy"
/>
{/if}
<span class="wire-next__blockquote-attribution">
<span class="wrn-next__blockquote-attribution">
{#if citation}
{#if citationUrl}
<cite><a href="{citationUrl}">{citation}</a></cite>
@@ -64,4 +64,276 @@ component Blockquote {
{/if}
</figure>
}
style {
.wrn-next--blockquote {
--wrn-blockquote-color: var(--wrn-component-color, var(--wrn-color-primary));
--wrn-blockquote-font-size: 1.125rem;
--wrn-blockquote-mark-size: 4.25rem;
display: grid;
gap: 1.25rem;
width: 100%;
color: var(--wrn-color-text);
text-align: left;
}
.wrn-next--blockquote[data-size="xs"] {
--wrn-blockquote-font-size: 0.875rem;
--wrn-blockquote-mark-size: 3rem;
}
.wrn-next--blockquote[data-size="sm"] {
--wrn-blockquote-font-size: 1rem;
--wrn-blockquote-mark-size: 3.5rem;
}
.wrn-next--blockquote[data-size="md"],
.wrn-next--blockquote[data-size="default"] {
--wrn-blockquote-font-size: 1.125rem;
}
.wrn-next--blockquote[data-size="lg"] {
--wrn-blockquote-font-size: clamp(1.35rem, 2.4vw, 1.75rem);
--wrn-blockquote-mark-size: 5rem;
}
.wrn-next--blockquote[data-size="xl"] {
--wrn-blockquote-font-size: clamp(1.65rem, 3vw, 2.25rem);
--wrn-blockquote-mark-size: 5.75rem;
}
.wrn-next--blockquote > blockquote {
position: relative;
display: block;
min-width: 0;
margin: 0;
padding-top: calc(var(--wrn-blockquote-mark-size) * 0.78);
}
.wrn-next__blockquote-mark {
position: absolute;
top: 0;
inset-inline: 0;
display: block;
width: 100%;
color: color-mix(in srgb, var(--wrn-blockquote-color) 28%, var(--wrn-color-text));
font-family: Georgia, "Times New Roman", serif;
font-size: var(--wrn-blockquote-mark-size);
font-style: normal;
font-weight: 700;
line-height: 0.8;
text-align: start;
user-select: none;
}
.wrn-next__blockquote-copy {
min-width: 0;
font-family: Georgia, "Times New Roman", serif;
font-size: var(--wrn-blockquote-font-size);
line-height: 1.55;
text-wrap: pretty;
}
.wrn-next--blockquote[data-italic="true"] .wrn-next__blockquote-copy {
font-style: italic;
}
.wrn-next__blockquote-copy > :first-child {
margin-top: 0;
}
.wrn-next__blockquote-copy > :last-child {
margin-bottom: 0;
}
.wrn-next__blockquote-citation {
display: inline-flex;
gap: 0.75rem;
align-items: center;
width: fit-content;
margin-inline-start: 0;
color: var(--wrn-color-muted);
font-size: 0.875rem;
line-height: 1.35;
}
.wrn-next__blockquote-avatar {
width: 2.5rem;
height: 2.5rem;
flex: 0 0 auto;
border: 2px solid color-mix(in srgb, var(--wrn-blockquote-color) 35%, transparent);
border-radius: 999px;
object-fit: cover;
}
.wrn-next__blockquote-attribution {
display: grid;
gap: 0.15rem;
}
.wrn-next__blockquote-attribution cite {
color: var(--wrn-color-text);
font-style: normal;
font-weight: 700;
}
.wrn-next__blockquote-attribution a {
color: inherit;
text-decoration-color: color-mix(in srgb, var(--wrn-blockquote-color) 55%, transparent);
text-underline-offset: 0.2em;
}
.wrn-next__blockquote-attribution a:hover {
color: var(--wrn-blockquote-color);
}
.wrn-next--blockquote[data-align="center"] {
text-align: center;
}
.wrn-next--blockquote[data-align="right"],
.wrn-next--blockquote[data-align="end"] {
text-align: right;
}
.wrn-next--blockquote[data-align="center"] > blockquote,
.wrn-next--blockquote[data-align="right"] > blockquote,
.wrn-next--blockquote[data-align="end"] > blockquote {
text-align: inherit;
}
.wrn-next--blockquote[data-align="center"] .wrn-next__blockquote-mark {
text-align: center;
}
.wrn-next--blockquote[data-align="right"] .wrn-next__blockquote-mark,
.wrn-next--blockquote[data-align="end"] .wrn-next__blockquote-mark {
text-align: end;
}
.wrn-next--blockquote[data-align="center"] .wrn-next__blockquote-citation {
margin-inline: auto;
}
.wrn-next--blockquote[data-align="right"] .wrn-next__blockquote-citation,
.wrn-next--blockquote[data-align="end"] .wrn-next__blockquote-citation {
margin-inline-start: auto;
}
.wrn-next--blockquote[data-variant="bordered"],
.wrn-next--blockquote[data-variant="border"],
.wrn-next--blockquote[data-variant="left-border"] {
padding-inline-start: 1.5rem;
border-inline-start: 4px solid var(--wrn-blockquote-color);
}
.wrn-next--blockquote[data-variant="bordered"] .wrn-next__blockquote-mark,
.wrn-next--blockquote[data-variant="border"] .wrn-next__blockquote-mark,
.wrn-next--blockquote[data-variant="left-border"] .wrn-next__blockquote-mark {
display: none;
}
.wrn-next--blockquote[data-variant="bordered"] > blockquote,
.wrn-next--blockquote[data-variant="border"] > blockquote,
.wrn-next--blockquote[data-variant="left-border"] > blockquote {
display: block;
padding-top: 0;
}
.wrn-next--blockquote[data-variant="bordered"] .wrn-next__blockquote-copy,
.wrn-next--blockquote[data-variant="border"] .wrn-next__blockquote-copy,
.wrn-next--blockquote[data-variant="left-border"] .wrn-next__blockquote-copy {
padding-top: 0;
}
.wrn-next--blockquote[data-variant="bordered"] .wrn-next__blockquote-citation,
.wrn-next--blockquote[data-variant="border"] .wrn-next__blockquote-citation,
.wrn-next--blockquote[data-variant="left-border"] .wrn-next__blockquote-citation {
margin-inline-start: 0;
}
@media (max-width: 480px) {
.wrn-next--blockquote {
--wrn-blockquote-mark-size: 3.25rem;
--wrn-blockquote-font-size: 1rem;
gap: 1rem;
}
.wrn-next--blockquote[data-size="lg"],
.wrn-next--blockquote[data-size="xl"] {
--wrn-blockquote-font-size: 1.25rem;
}
.wrn-next__blockquote-citation {
margin-inline-start: 0;
}
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+90 -90
View File
@@ -26,34 +26,34 @@ label: string = "Breadcrumb"
data-color='{color}'
data-variant='{variant}'
aria-label='{label}'
class='wire-breadcrumb {class}'
class='wrn-breadcrumb {class}'
>
<ol class="wire-breadcrumb__list">
<ol class="wrn-breadcrumb__list">
{#if showHome}
<li class="wire-breadcrumb__item wire-breadcrumb__item--home">
<li class="wrn-breadcrumb__item wrn-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 } }))'
class="wrn-breadcrumb__link wrn-breadcrumb__home"
@click='output.select({ 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>
<span class="icon-[lucide--house] wrn-breadcrumb__icon" aria-hidden="true"></span>
{:else if homeIcon}
<span class='{homeIcon} wire-breadcrumb__icon' aria-hidden="true"></span>
<span class='{homeIcon} wrn-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wire-breadcrumb__home-label">{homeLabel}</span>
<span class="wrn-breadcrumb__home-label">{homeLabel}</span>
</a>
</li>
{/if}
{#each items as item, itemIndex}
<li
class="wire-breadcrumb__item"
class="wrn-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">
<span class="wrn-breadcrumb__separator" aria-hidden="true">
{#if separator === "slash"}
<span>/</span>
{:else if separator === "dot"}
@@ -61,7 +61,7 @@ label: string = "Breadcrumb"
{:else if separator === "arrow"}
<span>→</span>
{:else}
<span class="icon-[lucide--chevron-right] wire-breadcrumb__separator-icon"></span>
<span class="icon-[lucide--chevron-right] wrn-breadcrumb__separator-icon"></span>
{/if}
</span>
{/if}
@@ -71,30 +71,30 @@ label: string = "Breadcrumb"
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 } }))'
class="wrn-breadcrumb__link"
@click='output.select({ item: item, itemIndex: itemIndex })'
>
{#if item.icon}
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
<span class='{item.icon} wrn-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
<span class="wrn-breadcrumb__label">{item.label || item.title}</span>
{#if item.external}
<span class="icon-[lucide--arrow-up-right] wire-breadcrumb__external" aria-hidden="true"></span>
<span class="icon-[lucide--arrow-up-right] wrn-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" : ""}'
class="wrn-breadcrumb__current"
aria-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "page" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
>
{#if item.icon}
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
<span class='{item.icon} wrn-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
<span class="wrn-breadcrumb__label">{item.label || item.title}</span>
</span>
{/if}
</li>
@@ -104,52 +104,52 @@ label: string = "Breadcrumb"
}
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);
.wrn-breadcrumb {
--breadcrumb-accent: var(--wrn-color-primary);
--breadcrumb-soft: var(--wrn-color-primary-soft);
--breadcrumb-muted: var(--wrn-color-primary-muted);
--breadcrumb-text: var(--wrn-color-primary-text);
min-width: 0;
color: var(--wire-color-text-muted);
color: var(--wrn-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);
.wrn-breadcrumb[data-color="secondary"] {
--breadcrumb-accent: var(--wrn-color-secondary);
--breadcrumb-soft: var(--wrn-color-secondary-soft);
--breadcrumb-muted: var(--wrn-color-secondary-muted);
--breadcrumb-text: var(--wrn-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);
.wrn-breadcrumb[data-color="info"] {
--breadcrumb-accent: var(--wrn-color-info);
--breadcrumb-soft: var(--wrn-color-info-soft);
--breadcrumb-muted: var(--wrn-color-info-muted);
--breadcrumb-text: var(--wrn-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);
.wrn-breadcrumb[data-color="success"] {
--breadcrumb-accent: var(--wrn-color-success);
--breadcrumb-soft: var(--wrn-color-success-soft);
--breadcrumb-muted: var(--wrn-color-success-muted);
--breadcrumb-text: var(--wrn-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);
.wrn-breadcrumb[data-color="warning"] {
--breadcrumb-accent: var(--wrn-color-warning);
--breadcrumb-soft: var(--wrn-color-warning-soft);
--breadcrumb-muted: var(--wrn-color-warning-muted);
--breadcrumb-text: var(--wrn-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);
.wrn-breadcrumb[data-color="danger"] {
--breadcrumb-accent: var(--wrn-color-danger);
--breadcrumb-soft: var(--wrn-color-danger-soft);
--breadcrumb-muted: var(--wrn-color-danger-muted);
--breadcrumb-text: var(--wrn-color-danger-text);
}
.wire-breadcrumb[data-variant="soft"] {
.wrn-breadcrumb[data-variant="soft"] {
width: fit-content;
max-width: 100%;
padding: 0.45rem 0.65rem;
@@ -158,21 +158,21 @@ label: string = "Breadcrumb"
border-radius: 0.75rem;
}
.wire-breadcrumb[data-variant="outline"] {
.wrn-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);
background: var(--wrn-color-surface-raised);
border: 1px solid var(--wrn-color-border);
border-radius: 0.75rem;
box-shadow: var(--wire-shadow-sm);
box-shadow: var(--wrn-shadow-sm);
}
.wire-breadcrumb[data-variant="contrast"] {
.wrn-breadcrumb[data-variant="contrast"] {
color: rgba(255, 255, 255, 0.78);
}
.wire-breadcrumb__list {
.wrn-breadcrumb__list {
display: flex;
align-items: center;
gap: 0;
@@ -185,34 +185,34 @@ label: string = "Breadcrumb"
white-space: nowrap;
}
.wire-breadcrumb__list::-webkit-scrollbar {
.wrn-breadcrumb__list::-webkit-scrollbar {
display: none;
}
.wire-breadcrumb__item {
.wrn-breadcrumb__item {
display: inline-flex;
align-items: center;
min-width: 0;
flex: 0 0 auto;
}
.wire-breadcrumb__separator {
.wrn-breadcrumb__separator {
display: inline-flex;
align-items: center;
justify-content: center;
margin-inline: 0.45rem;
color: var(--wire-color-text-subtle);
color: var(--wrn-color-text-subtle);
font-size: 0.75rem;
opacity: 0.72;
}
.wire-breadcrumb__separator-icon {
.wrn-breadcrumb__separator-icon {
width: 0.9rem;
height: 0.9rem;
}
.wire-breadcrumb__link,
.wire-breadcrumb__current {
.wrn-breadcrumb__link,
.wrn-breadcrumb__current {
display: inline-flex;
align-items: center;
gap: 0.4rem;
@@ -225,97 +225,97 @@ label: string = "Breadcrumb"
text-decoration: none;
}
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__link,
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__current {
.wrn-breadcrumb[data-size="sm"] .wrn-breadcrumb__link,
.wrn-breadcrumb[data-size="sm"] .wrn-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 {
.wrn-breadcrumb[data-size="lg"] .wrn-breadcrumb__link,
.wrn-breadcrumb[data-size="lg"] .wrn-breadcrumb__current {
min-height: 2.25rem;
font-size: 0.875rem;
}
.wire-breadcrumb__link {
color: var(--wire-color-text-muted);
.wrn-breadcrumb__link {
color: var(--wrn-color-text-muted);
font-weight: 600;
transition:
color 160ms ease,
background 160ms ease;
}
.wire-breadcrumb__link:hover {
.wrn-breadcrumb__link:hover {
color: var(--breadcrumb-accent);
background: var(--breadcrumb-soft);
}
.wire-breadcrumb__link:focus-visible {
.wrn-breadcrumb__link:focus-visible {
color: var(--breadcrumb-accent);
outline: 2px solid var(--breadcrumb-accent);
outline-offset: 2px;
}
.wire-breadcrumb__current {
.wrn-breadcrumb__current {
max-width: 22rem;
color: var(--wire-color-text);
color: var(--wrn-color-text);
font-weight: 750;
}
.wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
.wrn-breadcrumb__item[data-current="true"] .wrn-breadcrumb__current {
color: var(--breadcrumb-text);
background: var(--breadcrumb-soft);
}
.wire-breadcrumb__current[aria-disabled="true"] {
.wrn-breadcrumb__current[aria-disabled="true"] {
opacity: 0.58;
}
.wire-breadcrumb__icon,
.wire-breadcrumb__external {
.wrn-breadcrumb__icon,
.wrn-breadcrumb__external {
flex: 0 0 auto;
width: 0.95rem;
height: 0.95rem;
}
.wire-breadcrumb__home {
.wrn-breadcrumb__home {
color: var(--breadcrumb-accent);
}
.wire-breadcrumb__label {
.wrn-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 {
.wrn-breadcrumb[data-variant="contrast"] .wrn-breadcrumb__link,
.wrn-breadcrumb[data-variant="contrast"] .wrn-breadcrumb__current,
.wrn-breadcrumb[data-variant="contrast"] .wrn-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 {
.wrn-breadcrumb[data-variant="contrast"] .wrn-breadcrumb__link:hover,
.wrn-breadcrumb[data-variant="contrast"] .wrn-breadcrumb__item[data-current="true"] .wrn-breadcrumb__current {
color: #ffffff;
background: rgba(255, 255, 255, 0.12);
}
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__separator {
.wrn-breadcrumb[data-variant="contrast"] .wrn-breadcrumb__separator {
color: rgba(255, 255, 255, 0.62);
}
@media (max-width: 639px) {
.wire-breadcrumb__home-label {
.wrn-breadcrumb__home-label {
display: none;
}
.wire-breadcrumb__current {
.wrn-breadcrumb__current {
max-width: 13rem;
}
}
@media (prefers-reduced-motion: reduce) {
.wire-breadcrumb__link {
.wrn-breadcrumb__link {
transition: none;
}
}
+216 -4
View File
@@ -54,7 +54,7 @@ items: unknown[] = []
view {
<div
{...attrs}
class="wire-next wire-next--button-group {class}"
class="wrn-component wrn-next--button-group {class}"
role="{toolbar ? 'toolbar' : 'group'}"
aria-label="{ariaLabel}"
aria-orientation="{orientation}"
@@ -71,7 +71,7 @@ items: unknown[] = []
{#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}"
class="wrn-btn wrn-btn--variant-{item.variant || variant} wrn-btn--color-{item.color || color} wrn-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') : ''}"
@@ -80,13 +80,225 @@ items: unknown[] = []
@click="selectItem(item, index)"
>
{#if item.icon}
<span class="wire-btn__icon {item.icon}" aria-hidden="true"></span>
<span class="wrn-btn__icon {item.icon}" aria-hidden="true"></span>
{/if}
<span class="wire-btn__label">{item.label}</span>
<span class="wrn-btn__label">{item.label}</span>
</button>
{/each}
{/if}
<slot />
</div>
}
style {
.wrn-next--button-group {
display: inline-flex;
align-items: stretch;
width: fit-content;
max-width: 100%;
isolation: isolate;
}
.wrn-next--button-group[data-orientation="vertical"] {
flex-direction: column;
}
.wrn-next--button-group[data-attached="false"] {
gap: 0.5rem;
}
.wrn-next--button-group[data-disabled="true"] {
cursor: not-allowed;
opacity: 0.6;
}
.wrn-next--button-group[data-attached="true"] > :is(.wrn-btn, .wrn-action) {
position: relative;
margin: 0;
}
.wrn-next--button-group[data-attached="true"] > .wrn-action {
display: flex;
}
.wrn-next--button-group[data-attached="true"] > :is(.wrn-btn, .wrn-action) :is(.wrn-btn) {
height: 100%;
}
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
> :is(.wrn-btn, .wrn-action):not(:first-child) {
margin-inline-start: -1px;
}
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
> .wrn-btn:not(:first-child):not(:last-child),
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
> .wrn-action:not(:first-child):not(:last-child)
> .wrn-btn {
border-radius: 0;
}
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
> .wrn-btn:first-child:not(:last-child),
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
> .wrn-action:first-child:not(:last-child)
> .wrn-btn {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
> .wrn-btn:last-child:not(:first-child),
.wrn-next--button-group[data-attached="true"][data-orientation="horizontal"]
> .wrn-action:last-child:not(:first-child)
> .wrn-btn {
border-start-start-radius: 0;
border-end-start-radius: 0;
}
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
> :is(.wrn-btn, .wrn-action):not(:first-child) {
margin-top: -1px;
}
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
> .wrn-btn:not(:first-child):not(:last-child),
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
> .wrn-action:not(:first-child):not(:last-child)
> .wrn-btn {
border-radius: 0;
}
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
> .wrn-btn:first-child:not(:last-child),
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
> .wrn-action:first-child:not(:last-child)
> .wrn-btn {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
> .wrn-btn:last-child:not(:first-child),
.wrn-next--button-group[data-attached="true"][data-orientation="vertical"]
> .wrn-action:last-child:not(:first-child)
> .wrn-btn {
border-start-start-radius: 0;
border-start-end-radius: 0;
}
.wrn-next--button-group > :is(.wrn-btn, .wrn-action):focus-within,
.wrn-next--button-group > .wrn-btn:focus-visible {
z-index: 2;
}
.wrn-next--button-group[data-selectable="true"] > .wrn-btn[data-selected="true"] {
z-index: 1;
border-color: var(--wrn-component-color);
background: color-mix(in srgb, var(--wrn-component-color) 16%, var(--wrn-color-surface));
color: var(--wrn-component-color);
}
@media (max-width: 480px) {
.wrn-next--button-group[data-responsive="true"] > :is(.wrn-btn, .wrn-action) {
display: flex;
width: 100%;
flex-direction: column;
}
.wrn-next--button-group[data-responsive="true"] > .wrn-action > .wrn-btn {
width: 100%;
}
.wrn-next--button-group[data-responsive="true"][data-attached="true"]
> :is(.wrn-btn, .wrn-action) {
margin-top: -1px;
margin-inline-start: 0;
}
.wrn-next--button-group[data-responsive="true"][data-attached="true"]
> :is(.wrn-btn, .wrn-action):first-child {
margin-top: 0;
}
.wrn-next--button-group[data-responsive="true"][data-attached="true"] > .wrn-btn,
.wrn-next--button-group[data-responsive="true"][data-attached="true"]
> .wrn-action
> .wrn-btn {
border-radius: 0;
}
.wrn-next--button-group[data-responsive="true"][data-attached="true"] > .wrn-btn:first-child,
.wrn-next--button-group[data-responsive="true"][data-attached="true"]
> .wrn-action:first-child
> .wrn-btn {
border-start-start-radius: var(--wrn-radius-sm);
border-start-end-radius: var(--wrn-radius-sm);
}
.wrn-next--button-group[data-responsive="true"][data-attached="true"] > .wrn-btn:last-child,
.wrn-next--button-group[data-responsive="true"][data-attached="true"]
> .wrn-action:last-child
> .wrn-btn {
border-end-start-radius: var(--wrn-radius-sm);
border-end-end-radius: var(--wrn-radius-sm);
}
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+456 -22
View File
@@ -79,7 +79,7 @@ component Carousel {
if (!sourceEvent || !sourceEvent.currentTarget) {
return
}
root = sourceEvent.currentTarget.closest(".wire-next--carousel")
root = sourceEvent.currentTarget.closest(".wrn-next--carousel")
if (root) {
carouselRoot = root
}
@@ -92,8 +92,8 @@ component Carousel {
return
}
if (isSnap) {
viewport = root.querySelector(".wire-next__carousel-viewport")
slides = root.querySelectorAll(".wire-next__carousel-slide")
viewport = root.querySelector(".wrn-next__carousel-viewport")
slides = root.querySelectorAll(".wrn-next__carousel-slide")
slide = slides[index]
if (viewport && slide) {
viewport.scrollTo({
@@ -102,7 +102,7 @@ component Carousel {
})
}
}
thumbnails = root.querySelectorAll(".wire-next__carousel-thumbnails button")
thumbnails = root.querySelectorAll(".wrn-next__carousel-thumbnails button")
if (thumbnails[index]) {
thumbnail = thumbnails[index]
rail = thumbnail.parentElement
@@ -120,7 +120,7 @@ component Carousel {
}
rememberRoot(sourceEvent)
viewport = sourceEvent.currentTarget
slides = viewport.querySelectorAll(".wire-next__carousel-slide")
slides = viewport.querySelectorAll(".wrn-next__carousel-slide")
viewportCenter = viewport.scrollLeft + viewport.clientWidth / 2
closestIndex = currentIndex
closestDistance = 1000000000
@@ -142,7 +142,7 @@ component Carousel {
item: items[currentIndex],
reason: "snap"
})
slides = carouselRoot.querySelectorAll(".wire-next__carousel-thumbnails button")
slides = carouselRoot.querySelectorAll(".wrn-next__carousel-thumbnails button")
slide = slides[currentIndex]
if (slide) {
viewport = slide.parentElement
@@ -316,7 +316,7 @@ component Carousel {
view {
<section
{...attrs}
class="wire-next wire-next--carousel wire-next--color-{color} wire-next--size-{size} wire-next--variant-{variant} {class}"
class="wrn-component wrn-next--carousel wrn-component--color-{color} wrn-component--size-{size} wrn-next--variant-{variant} {class}"
data-rtl="{isRTL}"
data-centered="{isCentered}"
data-draggable="{isDraggable && !isSnap}"
@@ -335,15 +335,15 @@ component Carousel {
@focusout="startAutoplay()"
>
{#if title || description}
<header class="wire-next__carousel-header">
<header class="wrn-next__carousel-header">
{#if title}<h3>{title}</h3>{/if}
{#if description}<p>{description}</p>{/if}
</header>
{/if}
<div class="wire-next__carousel-layout">
<div class="wrn-next__carousel-layout">
{#if thumbnails === "vertical"}
<div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
<div class="wrn-next__carousel-thumbnails" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
@@ -359,10 +359,10 @@ component Carousel {
</div>
{/if}
<div class="wire-next__carousel-main">
<div class="wire-next__carousel-stage">
<div class="wrn-next__carousel-main">
<div class="wrn-next__carousel-stage">
<div
class="wire-next__carousel-viewport"
class="wrn-next__carousel-viewport"
tabindex="0"
@scroll="handleSnapScroll(event)"
@pointerdown="beginDrag(event)"
@@ -372,12 +372,12 @@ component Carousel {
@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"
class="wrn-next__carousel-track"
style="--wrn-carousel-index: {currentIndex}; --wrn-carousel-per-view: {slidesPerView}; --wrn-carousel-gap: {gap}; --wrn-carousel-drag-offset: {dragOffset}px"
>
{#each items as item, index}
<article
class="wire-next__carousel-slide"
class="wrn-next__carousel-slide"
data-active="{index === currentIndex}"
role="group"
aria-roledescription="slide"
@@ -387,7 +387,7 @@ component Carousel {
{#if item.imageSrc}
<img src="{item.imageSrc}" alt="{item.imageAlt || item.title || ''}" />
{/if}
<div class="wire-next__carousel-slide-content">
<div class="wrn-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}
@@ -402,7 +402,7 @@ component Carousel {
</div>
<button
class="wire-next__carousel-control wire-next__carousel-control--previous"
class="wrn-next__carousel-control wrn-next__carousel-control--previous"
type="button"
aria-label="Previous slide"
disabled="{!isInfiniteLoop && currentIndex === 0}"
@@ -411,7 +411,7 @@ component Carousel {
<span class="icon-[lucide--chevron-left]" aria-hidden="true"></span>
</button>
<button
class="wire-next__carousel-control wire-next__carousel-control--next"
class="wrn-next__carousel-control wrn-next__carousel-control--next"
type="button"
aria-label="Next slide"
disabled="{!isInfiniteLoop && currentIndex === maximumIndex()}"
@@ -421,14 +421,14 @@ component Carousel {
</button>
{#if showCounter}
<output class="wire-next__carousel-counter" aria-live="polite">
<output class="wrn-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">
<div class="wrn-next__carousel-pagination" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
@@ -442,7 +442,7 @@ component Carousel {
{/if}
{#if thumbnails === "horizontal"}
<div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
<div class="wrn-next__carousel-thumbnails" aria-label="Choose a slide">
{#each items as item, index}
<button
type="button"
@@ -461,4 +461,438 @@ component Carousel {
</div>
</section>
}
style {
.wrn-next--carousel {
--wrn-carousel-color: var(--wrn-component-color, var(--wrn-color-primary));
display: grid;
width: 100%;
min-width: 0;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
color: var(--wrn-color-text);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-1);
}
.wrn-next__carousel-header h3,
.wrn-next__carousel-header p {
margin: 0;
}
.wrn-next__carousel-header p {
margin-top: 0.35rem;
color: var(--wrn-color-text-muted);
}
.wrn-next__carousel-layout {
display: flex;
min-width: 0;
gap: 0.75rem;
}
.wrn-next__carousel-main {
display: grid;
flex: 1;
min-width: 0;
gap: 0.75rem;
}
.wrn-next__carousel-stage {
position: relative;
min-width: 0;
}
.wrn-next__carousel-viewport {
position: relative;
overflow: hidden;
min-width: 0;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface-subtle, var(--wrn-color-surface));
outline: none;
}
.wrn-next__carousel-viewport:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-carousel-color) 30%, transparent);
}
.wrn-next__carousel-track {
display: flex;
align-items: stretch;
gap: var(--wrn-carousel-gap);
transform: translateX(
calc(
var(--wrn-carousel-index) * -1 *
(
(100% - (var(--wrn-carousel-per-view) - 1) * var(--wrn-carousel-gap)) /
var(--wrn-carousel-per-view) + var(--wrn-carousel-gap)
)
)
);
transition: transform var(--wrn-motion-base) var(--wrn-ease-emphasized);
translate: var(--wrn-carousel-drag-offset, 0) 0;
will-change: transform;
}
.wrn-next--carousel[data-dragging="true"] .wrn-next__carousel-track {
transition: none;
}
.wrn-next--carousel[data-rtl="true"] .wrn-next__carousel-track {
transform: translateX(
calc(
var(--wrn-carousel-index) *
(
(100% - (var(--wrn-carousel-per-view) - 1) * var(--wrn-carousel-gap)) /
var(--wrn-carousel-per-view) + var(--wrn-carousel-gap)
)
)
);
}
.wrn-next__carousel-slide {
position: relative;
display: grid;
flex: 0 0
calc(
(100% - (var(--wrn-carousel-per-view) - 1) * var(--wrn-carousel-gap)) /
var(--wrn-carousel-per-view)
);
overflow: hidden;
min-width: 0;
min-height: 20rem;
place-items: center;
background: var(--wrn-color-surface-raised, var(--wrn-color-surface));
}
.wrn-next--carousel[data-auto-height="true"] .wrn-next__carousel-slide {
min-height: 0;
}
.wrn-next--carousel[data-centered="true"] .wrn-next__carousel-slide {
flex-basis: min(78%, 32rem);
}
.wrn-next--carousel[data-centered="true"] .wrn-next__carousel-track {
box-sizing: border-box;
transform: translateX(
calc(var(--wrn-carousel-index) * -1 * (min(78%, 32rem) + var(--wrn-carousel-gap)))
);
}
.wrn-next--carousel[data-centered="true"] .wrn-next__carousel-slide:first-child {
margin-inline-start: calc((100% - min(78%, 32rem)) / 2);
}
.wrn-next--carousel[data-centered="true"] .wrn-next__carousel-slide:last-child {
margin-inline-end: calc((100% - min(78%, 32rem)) / 2);
}
.wrn-next--carousel[data-centered="true"][data-rtl="true"] .wrn-next__carousel-track {
transform: translateX(
calc(var(--wrn-carousel-index) * (min(78%, 32rem) + var(--wrn-carousel-gap)))
);
}
.wrn-next__carousel-slide > img {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
inset: 0;
}
.wrn-next__carousel-slide-content {
position: relative;
z-index: 1;
width: min(100%, 38rem);
padding: clamp(2rem, 8vw, 5rem);
text-align: center;
}
.wrn-next__carousel-slide-content h4,
.wrn-next__carousel-slide-content p {
margin: 0;
}
.wrn-next__carousel-slide-content h4 {
font-size: clamp(1.5rem, 4vw, 2.4rem);
}
.wrn-next__carousel-slide-content p {
margin-top: 0.65rem;
color: var(--wrn-color-text-muted);
}
.wrn-next__carousel-control {
position: absolute;
z-index: 3;
top: 50%;
display: grid;
width: 2.75rem;
min-height: 2.75rem;
padding: 0;
border: 1px solid color-mix(in srgb, var(--wrn-color-border) 70%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--wrn-color-text) 76%, transparent);
color: var(--wrn-color-surface);
cursor: pointer;
place-items: center;
transform: translateY(-50%);
}
.wrn-next__carousel-control:focus-visible,
.wrn-next__carousel-pagination button:focus-visible,
.wrn-next__carousel-thumbnails button:focus-visible {
outline: 2px solid var(--wrn-carousel-color);
outline-offset: 2px;
}
.wrn-next__carousel-control:disabled {
cursor: not-allowed;
opacity: 0.35;
}
.wrn-next__carousel-control--previous {
left: 1rem;
}
.wrn-next__carousel-control--next {
right: 1rem;
}
.wrn-next--carousel[data-rtl="true"] .wrn-next__carousel-control--previous {
right: 1rem;
left: auto;
}
.wrn-next--carousel[data-rtl="true"] .wrn-next__carousel-control--next {
right: auto;
left: 1rem;
}
.wrn-next__carousel-pagination {
display: flex;
min-height: 1.5rem;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.wrn-next__carousel-pagination button {
width: 0.65rem;
min-height: 0.65rem;
padding: 0;
border: 1px solid var(--wrn-color-text-muted);
border-radius: 999px;
background: transparent;
cursor: pointer;
}
.wrn-next__carousel-pagination button[data-active="true"] {
border-color: var(--wrn-carousel-color);
background: var(--wrn-carousel-color);
}
.wrn-next__carousel-counter {
position: absolute;
z-index: 3;
bottom: 0.8rem;
left: 50%;
padding: 0.25rem 0.65rem;
border-radius: 999px;
background: color-mix(in srgb, var(--wrn-color-text) 82%, transparent);
color: var(--wrn-color-surface);
font-weight: 700;
transform: translateX(-50%);
}
.wrn-next__carousel-thumbnails {
display: flex;
overflow: auto;
gap: 0.55rem;
scrollbar-width: thin;
}
.wrn-next__carousel-thumbnails button {
display: grid;
flex: 0 0 8.5rem;
min-height: 3.75rem;
overflow: hidden;
padding: 0;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
cursor: pointer;
place-items: center;
}
.wrn-next__carousel-thumbnails button[data-active="true"] {
border-color: var(--wrn-carousel-color);
box-shadow: inset 0 0 0 1px var(--wrn-carousel-color);
}
.wrn-next__carousel-thumbnails img {
width: 100%;
height: 4rem;
object-fit: cover;
}
.wrn-next__carousel-thumbnails span {
padding: 0.5rem;
}
.wrn-next--carousel[data-thumbnails="vertical"] .wrn-next__carousel-thumbnails {
flex: 0 0 9rem;
flex-direction: column;
max-height: 28rem;
}
.wrn-next--carousel[data-thumbnails="vertical"] .wrn-next__carousel-thumbnails button {
flex-basis: 7.5rem;
width: 100%;
}
.wrn-next--carousel[data-draggable="true"] .wrn-next__carousel-viewport {
cursor: grab;
touch-action: pan-y;
user-select: none;
-webkit-user-select: none;
}
.wrn-next--carousel[data-draggable="true"] .wrn-next__carousel-viewport:active {
cursor: grabbing;
}
.wrn-next--carousel[data-draggable="true"] img,
.wrn-next--carousel[data-draggable="true"] a {
-webkit-user-drag: none;
}
.wrn-next--carousel[data-snap="true"] .wrn-next__carousel-viewport {
overflow-x: auto;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
scrollbar-width: thin;
}
.wrn-next--carousel[data-snap="true"] .wrn-next__carousel-track {
--wrn-carousel-snap-slide: calc(
(100% - (var(--wrn-carousel-per-view) - 1) * var(--wrn-carousel-gap)) /
var(--wrn-carousel-per-view)
);
display: grid;
width: 100%;
min-width: 100%;
box-sizing: content-box;
grid-auto-columns: var(--wrn-carousel-snap-slide);
grid-auto-flow: column;
padding-inline: calc((100% - var(--wrn-carousel-snap-slide)) / 2);
transform: none;
}
.wrn-next--carousel[data-snap="true"] .wrn-next__carousel-slide {
width: auto;
min-width: 0;
scroll-snap-align: center;
}
@media (max-width: 640px) {
.wrn-next__carousel-slide {
flex-basis: 100%;
min-height: 16rem;
}
.wrn-next--carousel[data-thumbnails="vertical"] .wrn-next__carousel-layout {
flex-direction: column-reverse;
}
.wrn-next--carousel[data-thumbnails="vertical"] .wrn-next__carousel-thumbnails {
flex: auto;
flex-direction: row;
max-height: none;
}
.wrn-next--carousel[data-thumbnails="vertical"] .wrn-next__carousel-thumbnails button {
flex-basis: 8.5rem;
width: auto;
}
}
@media (prefers-reduced-motion: reduce) {
.wrn-next__carousel-track {
transition-duration: 0.01ms;
}
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+93 -11
View File
@@ -1,25 +1,107 @@
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[])
select(payload: { item: object; index: number; sourceEvent?: Event })
dataPointClick(payload: { item: object; index: number; sourceEvent?: Event })
legendToggle(payload: { item: object; index: number; hidden: boolean; sourceEvent?: Event })
}
props {
size: string = "default"
color: string = "primary"
title: string = "Chart"
description: string = ""
items: unknown[] = []
variant: string = "default"
valueKey: string = "value"
labelKey: string = "label"
height: number = 240
showLegend: boolean = true
showValues: boolean = true
size: string = "default"
color: string = "primary"
variant: string = "bar"
class: string = ""
}
state hiddenIndexes = []
functions {
shared function numericValue(item) { return Math.max(0, Number(item[valueKey]) || 0) }
shared function maximumValue() {
return Math.max(1, ...items.map(function (item) { return numericValue(item) }))
}
shared function isHidden(index) { return hiddenIndexes.includes(index) }
shared function barWidth() { return items.length ? Math.max(4, 80 / items.length) : 80 }
shared function barX(index) { return 10 + index * (80 / Math.max(1, items.length)) }
shared function barHeight(item) { return numericValue(item) / maximumValue() * 72 }
shared function barY(item) { return 84 - barHeight(item) }
client function selectPoint(item, index, sourceEvent) {
output.dataPointClick({ item: item, index: index, sourceEvent: sourceEvent })
output.select({ item: item, index: index, sourceEvent: sourceEvent })
}
client function toggleLegend(item, index, sourceEvent, next) {
next = hiddenIndexes.slice()
if (next.includes(index)) {
next = next.filter(function (value) { return value !== index })
} else {
next.push(index)
}
hiddenIndexes = next
output.legendToggle({ item: item, index: index, hidden: next.includes(index), sourceEvent: sourceEvent })
}
}
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}
<figure {...attrs} data-ui-component="Chart" data-variant='{variant}' class='wrn-chart {class}'>
{#if title || description}
<figcaption class="wrn-chart__header">
{#if title}<strong>{title}</strong>{/if}
{#if description}<span>{description}</span>{/if}
</figcaption>
{/if}
<svg class="wrn-chart__plot" viewBox="0 0 100 100" role="img" aria-label='{title}' style='height:{height}px'>
<line x1="8" y1="84" x2="96" y2="84" class="wrn-chart__axis" />
{#each items as item, index}
<g data-hidden='{isHidden(index) ? "true" : "false"}'>
<rect
x='{barX(index)}' y='{barY(item)}' width='{barWidth()}' height='{barHeight(item)}'
rx="1.5" class="wrn-chart__bar" role="button" tabindex="0"
aria-label='{item[labelKey] + ": " + numericValue(item)}'
@click='selectPoint(item, index, event)'
@keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectPoint(item, index, event) }'
/>
{#if showValues}<text x='{barX(index) + barWidth() / 2}' y='{barY(item) - 3}' class="wrn-chart__value">{numericValue(item)}</text>{/if}
<text x='{barX(index) + barWidth() / 2}' y="94" class="wrn-chart__label">{item[labelKey]}</text>
</g>
{/each}
</svg>
{#if showLegend}
<div class="wrn-chart__legend" aria-label="Chart legend">
{#each items as item, index}
<button type="button" aria-pressed='{isHidden(index) ? "false" : "true"}' @click='toggleLegend(item, index, event)'>
<span aria-hidden="true"></span>{item[labelKey]}
</button>
{/each}
</div>
{/if}
<slot />
</section>
</figure>
}
style {
.wrn-chart { display: grid; gap: 1rem; min-width: 0; margin: 0; padding: 1rem; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); color: var(--wrn-color-text); background: var(--wrn-color-surface); box-shadow: var(--wrn-shadow-1); }
.wrn-chart__header { display: grid; gap: 0.2rem; }
.wrn-chart__header > span { color: var(--wrn-color-muted); font-size: 0.8rem; }
.wrn-chart__plot { display: block; width: 100%; min-height: 12rem; overflow: visible; }
.wrn-chart__axis { stroke: var(--wrn-color-border); stroke-width: 0.5; }
.wrn-chart__bar { fill: var(--wrn-color-primary); cursor: pointer; transition: opacity var(--wrn-motion-fast), transform var(--wrn-motion-fast); transform-box: fill-box; transform-origin: bottom; }
.wrn-chart__bar:hover { opacity: 0.82; transform: scaleY(1.02); }
.wrn-chart__bar:focus-visible { outline: 1.5px solid var(--wrn-color-focus); outline-offset: 1px; }
.wrn-chart__plot g[data-hidden="true"] { opacity: 0.18; pointer-events: none; }
.wrn-chart__value, .wrn-chart__label { fill: var(--wrn-color-text); font-size: 3.5px; text-anchor: middle; }
.wrn-chart__label { fill: var(--wrn-color-muted); }
.wrn-chart__legend { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.wrn-chart__legend button { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0.5rem; border: 1px solid var(--wrn-color-border); border-radius: 999px; color: var(--wrn-color-text); background: transparent; font: inherit; font-size: 0.75rem; cursor: pointer; }
.wrn-chart__legend button > span { width: 0.65rem; height: 0.65rem; border-radius: 0.2rem; background: var(--wrn-color-primary); }
.wrn-chart__legend button[aria-pressed="false"] { opacity: 0.5; }
}
}
+276 -8
View File
@@ -64,31 +64,31 @@ component ChatBubble {
view {
<section
{...attrs}
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chat-bubble wire-next--variant-{variant} {class}"
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--chat-bubble wrn-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">
<header class="wrn-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">
<div class="wrn-next__chat-thread">
{#each items as item, index}
<article
class="wire-next__chat-message"
class="wrn-next__chat-message"
data-direction="{messageDirection(item)}"
aria-label="{messageDirection(item) === 'outgoing' ? 'Sent message' : 'Received message'}"
>
<div class="wire-next__chat-row">
<div class="wrn-next__chat-row">
{#if showAvatars && (item.avatarSrc || item.avatarFallback)}
<button
class="wire-next__chat-avatar"
class="wrn-next__chat-avatar"
type="button"
aria-label="{item.avatarLabel || item.author || 'Message author'}"
@click="selectAvatar(event, item, index)"
@@ -102,7 +102,7 @@ component ChatBubble {
{/if}
<div
class="wire-next__chat-content"
class="wrn-next__chat-content"
role="button"
tabindex="0"
@click="selectMessage(item, index)"
@@ -129,7 +129,7 @@ component ChatBubble {
</div>
{#if showMetadata && (item.timestamp || item.status || item.actionLabel)}
<footer class="wire-next__chat-meta" data-tone="{item.statusTone || 'muted'}">
<footer class="wrn-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}
@@ -147,4 +147,272 @@ component ChatBubble {
<slot />
</section>
}
style {
.wrn-next--chat-bubble.wrn-component--size-sm {
--wrn-chat-padding: 0.75rem;
font-size: 0.875rem;
}
.wrn-next--chat-bubble.wrn-component--size-lg {
--wrn-chat-padding: 1.25rem;
font-size: 1.0625rem;
}
.wrn-next--chat-bubble {
--wrn-chat-color: var(--wrn-component-color, var(--wrn-color-primary));
--wrn-chat-padding: 1rem;
display: grid;
width: 100%;
min-width: 0;
gap: 1rem;
color: var(--wrn-color-text);
}
.wrn-next__chat-header h3,
.wrn-next__chat-header p,
.wrn-next__chat-content h4,
.wrn-next__chat-content p {
margin: 0;
}
.wrn-next__chat-header p {
margin-top: 0.35rem;
color: var(--wrn-color-text-muted);
}
.wrn-next__chat-thread {
display: grid;
min-width: 0;
gap: 1.25rem;
}
.wrn-next__chat-message {
display: grid;
width: min(78%, 34rem);
min-width: 0;
justify-self: start;
gap: 0.4rem;
}
.wrn-next__chat-message[data-direction="outgoing"] {
justify-self: end;
}
.wrn-next--chat-bubble[data-one-sided="true"] .wrn-next__chat-message {
justify-self: start;
}
.wrn-next__chat-row {
display: flex;
min-width: 0;
align-items: flex-start;
gap: 0.75rem;
}
.wrn-next__chat-message[data-direction="outgoing"] .wrn-next__chat-row {
flex-direction: row-reverse;
}
.wrn-next--chat-bubble[data-one-sided="true"]
.wrn-next__chat-message[data-direction="outgoing"]
.wrn-next__chat-row {
flex-direction: row;
}
.wrn-next__chat-content {
min-width: 0;
padding: var(--wrn-chat-padding);
border: 1px solid var(--wrn-color-border);
border-radius: 1rem;
background: var(--wrn-color-surface-raised, var(--wrn-color-surface));
color: var(--wrn-color-text);
cursor: pointer;
}
.wrn-next__chat-content:focus-visible {
outline: 2px solid var(--wrn-chat-color);
outline-offset: 2px;
}
.wrn-next__chat-message[data-direction="outgoing"] .wrn-next__chat-content {
border-color: var(--wrn-chat-color);
background: var(--wrn-chat-color);
color: var(--wrn-color-primary-contrast, #fff);
}
.wrn-next__chat-message[data-direction="outgoing"] .wrn-next__chat-content :is(h4, p, li, span) {
color: inherit;
}
.wrn-next__chat-content h4 {
margin-bottom: 0.65rem;
font-size: 1em;
}
.wrn-next__chat-content p + p,
.wrn-next__chat-content p + nav {
margin-top: 0.75rem;
}
.wrn-next__chat-content ul {
display: grid;
margin: 0.55rem 0 0;
padding-inline-start: 1.25rem;
gap: 0.35rem;
}
.wrn-next__chat-content nav {
display: grid;
margin-top: 0.75rem;
gap: 0.3rem;
}
.wrn-next__chat-content a {
color: var(--wrn-chat-color);
font-weight: 650;
text-decoration: none;
}
.wrn-next__chat-message[data-direction="outgoing"] .wrn-next__chat-content a {
color: inherit;
text-decoration: underline;
}
.wrn-next__chat-avatar {
display: grid;
flex: 0 0 2.5rem;
width: 2.5rem;
min-height: 2.5rem;
overflow: hidden;
padding: 0;
border: 1px solid var(--wrn-color-border);
border-radius: 999px;
background: var(--wrn-color-surface-raised, var(--wrn-color-surface));
color: var(--wrn-color-text);
cursor: pointer;
place-items: center;
}
.wrn-next__chat-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.wrn-next__chat-avatar:focus-visible {
outline: 2px solid var(--wrn-chat-color);
outline-offset: 2px;
}
.wrn-next__chat-meta {
display: flex;
align-items: center;
gap: 0.3rem;
padding-inline: calc(2.5rem + 0.75rem);
color: var(--wrn-color-text-muted);
font-size: 0.78em;
}
.wrn-next__chat-message[data-direction="outgoing"] .wrn-next__chat-meta {
justify-content: flex-end;
}
.wrn-next__chat-meta[data-tone="danger"] {
color: var(--wrn-color-danger);
}
.wrn-next__chat-meta button {
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: 700;
cursor: pointer;
text-decoration: underline;
}
@media (max-width: 640px) {
.wrn-next__chat-message {
width: min(92%, 34rem);
}
.wrn-next__chat-avatar {
flex-basis: 2rem;
width: 2rem;
min-height: 2rem;
}
.wrn-next__chat-meta {
padding-inline: 0;
}
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+83 -11
View File
@@ -1,25 +1,97 @@
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)
copy(payload: { value: string; sourceEvent?: Event })
success(payload: { value: string; sourceEvent?: Event })
error(payload: { error: Error | string; value: string; sourceEvent?: Event })
}
props {
size: string = "default"
color: string = "primary"
value: string = ""
label: string = "Copy"
copiedLabel: string = "Copied"
errorLabel: string = "Copy failed"
title: string = "Clipboard"
description: string = ""
items: unknown[] = []
variant: string = "default"
code: boolean = true
size: string = "default"
color: string = "primary"
disabled: boolean = false
class: string = ""
}
state status = "idle"
functions {
client function resetStatus() { status = "idle" }
client function copySucceeded() {
status = "success"
output.success({ value: value })
setTimeout(resetStatus, 1600)
}
client function copyFailed(error) {
status = "error"
output.error({ error: error, value: value })
setTimeout(resetStatus, 1600)
}
client function copyValue(sourceEvent) {
if (disabled || status === "copying") {
return
}
status = "copying"
output.copy({ value: value, sourceEvent: sourceEvent })
if (!window.navigator.clipboard || !window.navigator.clipboard.writeText) {
copyFailed(new Error("Clipboard API is unavailable"))
return
}
return window.navigator.clipboard.writeText(value).then(copySucceeded).catch(copyFailed)
}
}
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}
<section
{...attrs}
data-ui-component="Clipboard"
data-status='{status}'
data-size='{size}'
data-color='{color}'
class='wrn-clipboard {class}'
>
{#if title}<strong class="wrn-clipboard__title">{title}</strong>{/if}
{#if description}<p class="wrn-clipboard__description">{description}</p>{/if}
<div class="wrn-clipboard__control">
{#if code}
<code class="wrn-clipboard__value">{value}</code>
{:else}
<span class="wrn-clipboard__value">{value}</span>
{/if}
<button
type="button"
class="wrn-clipboard__button"
disabled='{disabled}'
aria-label='{status === "success" ? copiedLabel : status === "error" ? errorLabel : label}'
@click='copyValue(event)'
>
<span class="icon-[lucide--copy] wrn-clipboard__icon" data-show='status !== "success"' aria-hidden="true"></span>
<span class="icon-[lucide--check] wrn-clipboard__icon" data-show='status === "success"' aria-hidden="true"></span>
<span>{status === "success" ? copiedLabel : status === "error" ? errorLabel : label}</span>
</button>
</div>
<slot />
</section>
}
style {
.wrn-clipboard { display: grid; gap: 0.5rem; color: var(--wrn-color-text); }
.wrn-clipboard__title { font-size: 0.875rem; }
.wrn-clipboard__description { margin: 0; color: var(--wrn-color-muted); font-size: 0.8rem; }
.wrn-clipboard__control { display: flex; min-width: 0; align-items: stretch; overflow: hidden; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); background: var(--wrn-color-surface); box-shadow: var(--wrn-shadow-1); }
.wrn-clipboard__value { min-width: 0; flex: 1; overflow: auto; padding: 0.7rem 0.85rem; color: var(--wrn-color-text); background: var(--wrn-color-surface-2); font: inherit; font-family: var(--wrn-font-mono, ui-monospace, monospace); white-space: nowrap; }
.wrn-clipboard__button { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.6rem 0.85rem; border: 0; border-left: 1px solid var(--wrn-color-border); color: var(--wrn-color-primary); background: transparent; font: inherit; font-size: 0.8rem; font-weight: 700; cursor: pointer; }
.wrn-clipboard__button:hover { background: var(--wrn-color-primary-soft); }
.wrn-clipboard__button:focus-visible { outline: 2px solid var(--wrn-color-focus); outline-offset: -3px; }
.wrn-clipboard__button:disabled { cursor: not-allowed; opacity: 0.55; }
.wrn-clipboard[data-status="success"] .wrn-clipboard__button { color: var(--wrn-color-success); }
.wrn-clipboard[data-status="error"] .wrn-clipboard__button { color: var(--wrn-color-danger); }
.wrn-clipboard__icon { width: 1rem; height: 1rem; flex: none; }
}
}
+224 -7
View File
@@ -51,18 +51,18 @@ size: string = "default"
view {
<section
{...attrs}
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--collapse {class}"
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--collapse {class}"
data-mode="{mode}"
aria-label="{ariaLabel}"
>
{#each items as item, index}
<article class="wire-next__collapse-item" data-open="{isOpen(index)}">
<article class="wrn-next__collapse-item" data-open="{isOpen(index)}">
{#if mode === "inline" && item.preview}
<p class="wire-next__collapse-preview">{item.preview}</p>
<p class="wrn-next__collapse-preview">{item.preview}</p>
{/if}
<button
class="wire-next__collapse-trigger"
class="wrn-next__collapse-trigger"
type="button"
aria-expanded="{isOpen(index) ? 'true' : 'false'}"
aria-controls="{item.id || 'collapse-panel-' + index}"
@@ -70,17 +70,17 @@ size: string = "default"
@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>
<span class="icon-[lucide--chevron-down] wrn-next__collapse-chevron" aria-hidden="true"></span>
</button>
<div
id="{item.id || 'collapse-panel-' + index}"
class="wire-next__collapse-panel"
class="wrn-next__collapse-panel"
data-open="{isOpen(index)}"
aria-hidden="{isOpen(index) ? 'false' : 'true'}"
inert="{!isOpen(index)}"
>
<div class="wire-next__collapse-content">
<div class="wrn-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}
@@ -95,4 +95,221 @@ size: string = "default"
<slot />
</section>
}
style {
.wrn-next--collapse.wrn-component--size-sm {
font-size: 0.875rem;
}
.wrn-next--collapse.wrn-component--size-lg {
font-size: 1.0625rem;
}
.wrn-next--collapse {
--wrn-collapse-color: var(--wrn-component-color, var(--wrn-color-primary));
display: grid;
width: 100%;
min-width: 0;
gap: 0.75rem;
color: var(--wrn-color-text);
}
.wrn-next__collapse-item {
display: grid;
min-width: 0;
justify-items: start;
gap: 0;
}
.wrn-next__collapse-trigger {
display: inline-flex;
min-height: 2.75rem;
align-items: center;
justify-content: center;
gap: 0.55rem;
padding: 0.7rem 1rem;
border: 1px solid var(--wrn-collapse-color);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-collapse-color);
color: var(--wrn-color-primary-contrast, #fff);
font: inherit;
font-weight: 700;
cursor: pointer;
}
.wrn-next__collapse-trigger:focus-visible {
outline: 2px solid var(--wrn-collapse-color);
outline-offset: 3px;
}
.wrn-next__collapse-trigger:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.wrn-next__collapse-chevron {
transition: transform var(--wrn-motion-fast) var(--wrn-ease-standard);
}
.wrn-next__collapse-item[data-open="true"] .wrn-next__collapse-chevron {
transform: rotate(180deg);
}
.wrn-next__collapse-panel {
width: min(100%, 42rem);
max-height: 0;
overflow: hidden;
margin-top: 0;
opacity: 0;
transition:
max-height 350ms var(--wrn-ease-emphasized),
margin-top 300ms var(--wrn-ease-emphasized),
opacity 200ms var(--wrn-ease-standard);
}
.wrn-next__collapse-panel[data-open="true"] {
max-height: 40rem;
margin-top: 0.75rem;
opacity: 1;
}
.wrn-next__collapse-content {
display: grid;
overflow: hidden;
min-height: 0;
gap: 0.65rem;
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface-raised, var(--wrn-color-surface));
}
.wrn-next__collapse-content h4,
.wrn-next__collapse-content p,
.wrn-next__collapse-preview {
margin: 0;
}
.wrn-next__collapse-content nav {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.wrn-next__collapse-content a {
color: var(--wrn-collapse-color);
font-weight: 650;
text-decoration: none;
}
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-item {
gap: 0;
}
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-preview {
order: 1;
margin-bottom: 0.5rem;
}
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-panel {
order: 2;
width: 100%;
}
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-panel[data-open="true"] {
margin-top: 0;
}
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-trigger {
order: 3;
margin-top: 0.5rem;
min-height: auto;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
color: var(--wrn-collapse-color);
}
.wrn-next--collapse[data-mode="inline"] .wrn-next__collapse-content {
padding: 0;
border: 0;
background: transparent;
}
@media (prefers-reduced-motion: reduce) {
.wrn-next__collapse-chevron,
.wrn-next__collapse-panel {
transition: none;
}
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+570 -4
View File
@@ -32,10 +32,576 @@ component ColorPicker {
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 {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-component--color-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wrn-next__field-heading"><label class="{hiddenLabel ? 'wrn-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wrn-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wrn-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="wrn-next__field-help">{helperText}</small>{/if}<small class="wrn-next__field-error" data-error="{name}">{error}</small>
</div>
}
style {
.wrn-next__color-control input[type="color"] {
width: 3rem;
padding: 0.2rem;
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
/* Shared field foundation. */
/* Validation states set by the browser validation runtime. */
.wrn-invalid {
border-color: var(--wrn-color-danger) !important;
}
.wrn-field-error {
display: block;
min-height: 1em;
margin-top: 0.25rem;
color: var(--wrn-color-danger);
font-size: 0.8rem;
}
.wrn-next--choice {
display: inline-flex;
align-items: center;
gap: 0.55rem;
cursor: pointer;
}
.wrn-next--choice input {
width: 1.1rem;
height: 1.1rem;
accent-color: var(--wrn-color-primary);
}
.wrn-next--field {
display: grid;
width: min(100%, 28rem);
gap: 0.4rem;
font-size: 0.78rem;
font-weight: 700;
}
.wrn-next--field :where(input, textarea, select) {
width: 100%;
min-height: 2.65rem;
padding: 0.6rem 0.7rem;
border: 1px solid var(--wrn-color-border);
border-radius: 0.6rem;
outline: 0;
color: var(--wrn-color-text);
background: var(--wrn-color-bg);
font: inherit;
font-weight: 500;
}
.wrn-auth-form form > .wrn-next--field,
.wrn-auth-form form > .wrn-field {
width: 100%;
max-width: none;
}
.wrn-auth-form__submit > .wrn-btn {
width: 100%;
min-height: 3rem;
border-radius: 0.75rem;
box-shadow: 0 0.8rem 1.8rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
}
.wrn-next--field :where(input, textarea, select):focus-visible {
border-color: var(--wrn-color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 16%, transparent);
}
.wrn-next--field textarea {
resize: vertical;
}
.wrn-next--field,
.wrn-next--choice-field {
--wrn-field-color: var(--wrn-component-color, var(--wrn-color-primary));
width: min(100%, 34rem);
min-width: 0;
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 500;
}
.wrn-next--field.wrn-next--size-sm,
.wrn-next--choice-field.wrn-next--size-sm {
font-size: 0.8125rem;
}
.wrn-next--field.wrn-next--size-lg,
.wrn-next--choice-field.wrn-next--size-lg {
font-size: 1rem;
}
.wrn-next__field-heading {
display: flex;
min-width: 0;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-weight: 600;
}
.wrn-next__field-hint,
.wrn-next__field-help {
color: var(--wrn-color-muted);
font-size: 0.82em;
font-weight: 500;
}
.wrn-next__field-control {
position: relative;
min-width: 0;
}
.wrn-next__field-control > :is(input, textarea, select) {
padding-inline: 0.85rem;
border-color: var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
font-size: 0.8125rem;
}
.wrn-next__field-control[data-icon-position="start"] > :is(input, textarea, select) {
padding-inline-start: 2.5rem;
}
.wrn-next__field-control[data-icon-position="end"] > :is(input, textarea, select) {
padding-inline-end: 2.5rem;
}
.wrn-next__field-icon {
position: absolute;
z-index: 1;
top: 50%;
left: 0.85rem;
color: var(--wrn-color-muted);
pointer-events: none;
transform: translateY(-50%);
}
.wrn-next__field-control[data-icon-position="end"] > .wrn-next__field-icon {
right: 0.85rem;
left: auto;
}
.wrn-next--field[data-variant="outlined"]
.wrn-next__field-control
> :is(input, textarea, select) {
border-width: 2px;
}
.wrn-next--field[data-variant="underline"]
.wrn-next__field-control
> :is(input, textarea, select) {
padding-inline: 0;
border-width: 0 0 1px;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.wrn-next--field[data-variant="pill"] .wrn-next__field-control > :is(input, select) {
border-radius: 999px;
padding-inline: 1.15rem;
}
.wrn-next--field[data-variant="ghost"] .wrn-next__field-control > :is(input, textarea, select) {
border-color: transparent;
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
}
.wrn-next__field-floating-label {
position: absolute;
top: 50%;
left: 0.85rem;
color: var(--wrn-color-muted);
pointer-events: none;
transform: translateY(-50%);
transition: 150ms ease;
}
.wrn-next--textarea .wrn-next__field-floating-label {
top: 1.35rem;
}
.wrn-next--field[data-floating="true"] .wrn-next__field-control > :is(input, textarea, select) {
padding-top: 1.2rem;
padding-bottom: 0.3rem;
}
.wrn-next--field[data-floating="true"] .wrn-next__field-heading > label {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
margin: -1px !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
}
.wrn-next--field[data-floating="true"]
.wrn-next__field-heading:not(:has(.wrn-next__field-hint)) {
display: none;
}
.wrn-next--field[data-floating="true"]
.wrn-next__field-control
> :is(input, textarea):is(:focus, :not(:placeholder-shown))
+ .wrn-next__field-floating-label {
top: 0.42rem;
font-size: 0.7em;
transform: none;
}
.wrn-next__field-error {
min-height: 1em;
color: var(--wrn-color-danger);
font-size: 0.82em;
font-weight: 600;
}
.wrn-next__field-error:empty {
display: none;
}
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
.wrn-next--choice-field[data-invalid="true"] input {
border-color: var(--wrn-color-danger);
}
.wrn-next--field :is(input, textarea, select):disabled,
.wrn-next--choice-field input:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.wrn-next--field :is(input, textarea, select):read-only {
background: var(--wrn-color-surface-2);
}
.wrn-next--field[data-inline="true"],
.wrn-next--choice-field[data-inline="true"] {
display: grid;
width: 100%;
max-width: none;
grid-template-columns: minmax(7rem, max-content) minmax(12rem, 20rem) minmax(10rem, 1fr);
justify-content: center;
align-items: center;
gap: 0.75rem;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-heading,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading {
min-height: 2.65rem;
align-items: center;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-heading {
justify-content: flex-end;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-help,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-help {
display: flex;
min-height: 2.65rem;
align-items: center;
}
.wrn-next--choice-field[data-inline="true"] {
grid-template-columns: minmax(12rem, 20rem) minmax(10rem, 1fr);
}
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading:empty {
display: none;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
grid-column: 2 / -1;
}
.wrn-next__sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
border: 0 !important;
margin: -1px !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
}
.wrn-next__choice-field {
display: grid;
width: 100%;
max-width: none;
gap: 0.4rem;
}
.wrn-next__choice {
display: flex;
min-width: 0;
align-items: center;
gap: 0.65rem;
color: var(--wrn-color-text);
width: 100%;
font-size: 0.75rem;
font-weight: 500;
}
.wrn-next__checkbox-content > .wrn-next__choice {
width: 100%;
min-width: 0;
align-items: flex-start;
}
.wrn-next__choice input {
flex: 0 0 auto;
margin: 0;
accent-color: var(--wrn-field-color);
}
.wrn-next__choice-copy {
display: grid;
min-width: 0;
gap: 0.15rem;
line-height: 1.35;
}
.wrn-next__choice-copy small {
color: var(--wrn-color-muted);
font-weight: 500;
}
.wrn-next__radio-group,
.wrn-next__checkbox-group {
display: grid;
min-width: 0;
gap: 0.65rem;
}
.wrn-next--radio-field[data-orientation="horizontal"] .wrn-next__radio-group,
.wrn-next--checkbox-field[data-orientation="horizontal"] .wrn-next__checkbox-group {
grid-template-columns: repeat(auto-fit, minmax(8rem, max-content));
align-items: stretch;
}
.wrn-next--radio-field[data-card="true"] .wrn-next__radio-group,
.wrn-next--checkbox-field[data-card="true"] .wrn-next__checkbox-group {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
}
.wrn-next--radio-field[data-card="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox {
min-height: 4.5rem;
padding: 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
}
.wrn-next--radio-field[data-card="true"] .wrn-next--radio:has(input:checked),
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox:has(input:checked) {
border-color: var(--wrn-field-color);
box-shadow: 0 0 0 1px var(--wrn-field-color);
}
.wrn-next--radio-field[data-list="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-list="true"] .wrn-next--checkbox {
padding-block: 0.75rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox {
justify-content: space-between;
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio input,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox input {
order: 2;
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next__choice-copy,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next__choice-copy {
order: 1;
}
.wrn-next__choice[data-variant="outlined"],
.wrn-next__choice[data-variant="floating"] {
padding: 0.65rem 0.8rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
}
.wrn-next__choice[data-variant="pill"] {
padding: 0.65rem 1rem;
border-radius: 999px;
background: color-mix(in srgb, var(--wrn-field-color) 9%, var(--wrn-color-surface));
}
.wrn-next__choice[data-variant="ghost"] {
padding: 0.65rem 0.8rem;
border-radius: var(--wrn-radius-sm);
background: color-mix(in srgb, var(--wrn-field-color) 7%, transparent);
}
.wrn-next__choice[data-variant="underline"] {
padding-block: 0.5rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-next--select .wrn-next__field-control {
display: flex;
align-items: center;
}
.wrn-next__color-control,
.wrn-next__range-control,
.wrn-next__input-group-control,
.wrn-next__file-control {
display: flex;
min-width: 0;
align-items: center;
gap: 0.65rem;
}
.wrn-next__picker-control,
.wrn-next--time-picker .wrn-next__field-control {
position: relative;
display: flex;
min-width: 0;
align-items: center;
}
.wrn-next__picker-control > button:first-of-type,
.wrn-next__time-trigger {
display: flex;
width: 100%;
min-height: 2.65rem;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.7rem 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
font: inherit;
text-align: left;
}
.wrn-next__calendar,
.wrn-next__time-panel {
z-index: 20;
display: grid;
width: min(100%, 22rem);
gap: 0.75rem;
padding: 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-3);
}
.wrn-next--date-picker[data-expanded="false"] .wrn-next__calendar,
.wrn-next--time-picker[data-expanded="false"] .wrn-next__time-panel {
display: none;
}
.wrn-next__calendar button,
.wrn-next__time-panel button {
min-height: 2.25rem;
border: 0;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: inherit;
}
.wrn-next__calendar-weekdays,
.wrn-next__calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.2rem;
text-align: center;
}
.wrn-next__calendar-weekdays {
color: var(--wrn-color-muted);
font-size: 0.75rem;
}
.wrn-next__calendar-grid button:hover,
.wrn-next__time-panel button:hover {
background: var(--wrn-color-surface-2);
}
.wrn-next__calendar-grid button[aria-pressed="true"],
.wrn-next__time-panel button[aria-pressed="true"] {
background: var(--wrn-field-color);
color: var(--wrn-color-primary-contrast, #fff);
}
.wrn-next__time-period {
display: flex;
gap: 0.35rem;
}
.wrn-next--file-upload-progress .wrn-next__row,
.wrn-next__upload-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.wrn-next--file-upload-progress .wrn-next__row > span {
display: grid;
min-width: 0;
}
.wrn-next--field[data-variant="underline"]
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
border-width: 0 0 1px;
border-radius: 0;
background: transparent;
}
.wrn-next--field[data-variant="outlined"]
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
border-width: 2px;
}
.wrn-next--field[data-variant="pill"]
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
border-radius: 999px;
}
.wrn-next--field[data-variant="ghost"]
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
border-color: transparent;
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
}
@media (max-width: 640px) {
.wrn-next--field[data-inline="true"],
.wrn-next--choice-field[data-inline="true"] {
grid-template-columns: 1fr;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
grid-column: auto;
}
}
.wrn-next--toggle-password:has(input.wrn-invalid) .wrn-next__password-control > input,
.wrn-next--toggle-password.wrn-next--invalid .wrn-next__password-control > input {
border-color: var(--wrn-color-danger);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 14%, transparent);
}
.wrn-next--strong-password.wrn-next--invalid input,
.wrn-next--strong-password input.wrn-invalid {
border-color: var(--wrn-color-danger);
}
}
}
+88 -14
View File
@@ -1,3 +1,13 @@
// Columns -- a simple multi-column split for page content.
//
// <Columns columns={2} gap="lg">...</Columns>
//
// Columns and Grid overlap deliberately: Columns is the coarse two or three
// way split of a page, and its gaps are wider for that reason. Grid is the one
// to reach for when the items are cards and the count matters.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Columns {
props {
size: string = "default"
@@ -8,28 +18,92 @@ component Columns {
class: string = ""
}
functions {
shared function columnCount() {
var value = Number(columns)
if (!value || value < 1) {
return 1
}
return Math.min(4, value)
}
}
view {
<div
{...attrs}
data-ui-component="Columns"
class='wrn-columns {class}'
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"'
data-max-width='{maxWidth}'
data-columns='{columnCount()}'
>
<slot></slot>
</div>
}
style {
.wrn-columns {
--columns-gap: 1.5rem;
display: grid;
width: 100%;
min-width: 0;
gap: var(--columns-gap);
grid-template-columns: 1fr;
align-items: start;
}
.wrn-columns[data-max-width="md"] {
max-width: 48rem;
}
.wrn-columns[data-max-width="lg"] {
max-width: 64rem;
}
.wrn-columns[data-max-width="xl"] {
max-width: 80rem;
}
.wrn-columns[data-max-width="2xl"] {
max-width: 96rem;
}
.wrn-columns[data-max-width="full"] {
max-width: none;
}
.wrn-columns[data-gap="sm"] {
--columns-gap: 0.75rem;
}
.wrn-columns[data-gap="lg"] {
--columns-gap: 2.5rem;
}
.wrn-columns[data-gap="xl"] {
--columns-gap: 3.5rem;
}
/*
* Splits at the tablet breakpoint rather than the phone one: a page split
* is worth keeping single-column for longer than a card deck is.
*/
@media (min-width: 768px) {
.wrn-columns:not([data-columns="1"]) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.wrn-columns[data-columns="3"] {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.wrn-columns[data-columns="4"] {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
}
}
+119 -22
View File
@@ -1,3 +1,5 @@
import SelectStyles from "../styles/SelectStyles.wrn"
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)
@@ -225,7 +227,7 @@ component ComboBox {
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}"
class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--advanced-select wrn-next--combobox {open ? 'wrn-next--open' : ''} {fixed ? 'wrn-next--advanced-select-fixed' : ''} {invalid ? 'wrn-next--invalid' : ''} {disabled ? 'wrn-next--disabled' : ''} {class}"
data-placement="{placement}"
data-search-mode="{searchMode}"
data-remote="{remote ? 'true' : 'false'}"
@@ -241,12 +243,12 @@ component ComboBox {
>
<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>
<div class="wrn-next__select-control wrn-next__combobox-control">
<span class="wrn-next__search-icon icon-[lucide--search]" aria-hidden="true"></span>
<input
{...attrs}
id="{name}-input"
class="wire-next__select-trigger wire-next__combobox-input"
class="wrn-next__select-trigger wrn-next__combobox-input"
type="text"
role="combobox"
name="{allowCustomValue ? name : ''}"
@@ -269,7 +271,7 @@ component ComboBox {
<button
type="button"
class="wire-next__clear-select"
class="wrn-next__clear-select"
data-show="clearable && inputText() && !disabled"
aria-label="{clearLabel}"
title="{clearLabel}"
@@ -279,44 +281,44 @@ component ComboBox {
</button>
<button
type="button"
class="wire-next__combobox-toggle"
class="wrn-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>
<span class="wrn-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' : ''}"
class="wrn-next__select-dropdown {fixed ? 'wrn-next__select-dropdown--fixed' : ''}"
data-show="shouldShowDropdown()"
style="{shouldShowDropdown() ? '' : 'display: none'}"
>
<div
class="wire-next__select-message"
class="wrn-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>
<div class="wrn-next__select-message" data-show="loading" role="status">
<i class="wrn-spinner wrn-spinner--inline" aria-hidden="true"></i>
{loadingLabel}
</div>
<div
id="{name}-listbox"
class="wire-next__select-list"
class="wrn-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>
<div class="wrn-next__option-group" role="group" aria-label="{group.label}">
<div class="wrn-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' : ''}"
class="wrn-next__select-option {isSelected(option) ? 'wrn-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wrn-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
@@ -328,9 +330,9 @@ component ComboBox {
>
{#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}
{#if optionTemplate === "color" && option.color}<i class="wrn-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>
<i class="wrn-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
</button>
{/each}
</div>
@@ -340,7 +342,7 @@ component ComboBox {
{#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' : ''}"
class="wrn-next__select-option {isSelected(option) ? 'wrn-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wrn-next__select-option--active' : ''}"
data-option-value="{option.value}"
data-show="matches(option)"
role="option"
@@ -352,16 +354,16 @@ component ComboBox {
>
{#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}
{#if optionTemplate === "color" && option.color}<i class="wrn-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>
<i class="wrn-next__check icon-[lucide--check]" data-show="isSelected(option)" aria-hidden="true"></i>
</button>
{/each}
</div>
<button
type="button"
class="wire-next__load-more"
class="wrn-next__load-more"
data-wrn-select-load-more
data-show="infinite && hasMore"
>{loadMoreLabel}</button>
@@ -381,9 +383,104 @@ component ComboBox {
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
<small
id="{name}-validation"
class="wire-next__validation"
class="wrn-next__validation"
data-error="{name}"
>{validationMessage}</small>
</div>
<SelectStyles hidden />
}
style {
.wrn-next__combobox-toggle {
position: absolute;
top: 50%;
right: 0;
z-index: 1;
display: grid;
width: 2.25em;
height: 100%;
padding: 0;
place-items: center;
border: 0;
color: var(--wrn-color-muted);
background: transparent;
cursor: pointer;
transform: translateY(-50%);
}
.wrn-next--combobox .wrn-next__combobox-input {
padding-right: 5.25em;
padding-left: 2.5em;
cursor: text;
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+59 -11
View File
@@ -1,24 +1,72 @@
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 })
start(payload: { sourceEvent?: Event; duration: number })
complete(payload: { sourceEvent?: Event; duration: number })
}
props {
label: string = "Celebrate"
duration: number = 1200
count: number = 24
autoStart: boolean = false
disabled: boolean = false
size: string = "default"
color: string = "primary"
title: string = "Confetti"
description: string = ""
items: unknown[] = []
variant: string = "default"
class: string = ""
}
state running = false
functions {
shared function pieces() { return Array.from({ length: Math.max(1, Math.min(60, count)) }) }
shared function pieceStyle(index) {
return "--wrn-confetti-index:" + index + ";--wrn-confetti-x:" + ((index * 37) % 100) + ";--wrn-confetti-delay:" + ((index * 29) % 240) + "ms;"
}
client function finish(sourceEvent) {
running = false
output.complete({ sourceEvent: sourceEvent, duration: duration })
}
client function start(sourceEvent) {
if (disabled || running) {
return
}
running = true
output.start({ sourceEvent: sourceEvent, duration: duration })
setTimeout(finish, duration, sourceEvent)
}
}
lifecycle {
mount { if (autoStart) { start(null) } }
}
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}
<div {...attrs} data-ui-component="Confetti" data-running='{running ? "true" : "false"}' class='wrn-confetti {class}' style='--wrn-confetti-duration:{duration}ms'>
<button type="button" class="wrn-confetti__trigger" disabled='{disabled}' @click='start(event)'>
<span class="icon-[lucide--party-popper]" aria-hidden="true"></span><span>{label}</span>
</button>
<div class="wrn-confetti__burst" aria-hidden="true">
{#each pieces() as piece, index}<i style='{pieceStyle(index)}'></i>{/each}
</div>
<slot />
</section>
</div>
}
style {
.wrn-confetti { position: relative; display: inline-flex; }
.wrn-confetti__trigger { display: inline-flex; min-height: 2.5rem; align-items: center; gap: 0.5rem; padding: 0.55rem 0.85rem; border: 1px solid var(--wrn-color-primary); border-radius: var(--wrn-radius); color: var(--wrn-color-on-primary); background: var(--wrn-color-primary); font: inherit; font-weight: 700; cursor: pointer; }
.wrn-confetti__trigger > span:first-child { width: 1rem; height: 1rem; }
.wrn-confetti__trigger:focus-visible { outline: 2px solid var(--wrn-color-focus); outline-offset: 2px; }
.wrn-confetti__trigger:disabled { cursor: not-allowed; opacity: 0.55; }
.wrn-confetti__burst { position: absolute; inset: 50% 0 auto; height: 0; pointer-events: none; }
.wrn-confetti__burst > i { position: absolute; left: calc(var(--wrn-confetti-x) * 1%); width: 0.45rem; height: 0.7rem; border-radius: 0.1rem; opacity: 0; background: hsl(calc(var(--wrn-confetti-index) * 47deg) 80% 55%); transform: translate(-50%, 0) rotate(calc(var(--wrn-confetti-index) * 23deg)); }
.wrn-confetti[data-running="true"] .wrn-confetti__burst > i { animation: wrn-confetti-burst var(--wrn-confetti-duration) cubic-bezier(0.2, 0.7, 0.2, 1) var(--wrn-confetti-delay) both; }
@keyframes wrn-confetti-burst {
0% { opacity: 1; transform: translate(-50%, 0) rotate(0); }
100% { opacity: 0; transform: translate(calc(-50% + (var(--wrn-confetti-x) - 50) * 0.9px), 9rem) rotate(540deg); }
}
@media (prefers-reduced-motion: reduce) {
.wrn-confetti[data-running="true"] .wrn-confetti__burst > i { animation-duration: 1ms; animation-delay: 0ms; }
}
}
}
+94 -94
View File
@@ -102,7 +102,7 @@ component ContextMenu {
}
client function moveFocus(sourceEvent, direction) {
const root = sourceEvent.currentTarget.closest(".wire-context-menu") || sourceEvent.currentTarget
const root = sourceEvent.currentTarget.closest(".wrn-context-menu") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-context-menu-item]:not([disabled])")]
if (!options.length) {
return
@@ -137,12 +137,12 @@ component ContextMenu {
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};'
class='wrn-context-menu {class}'
style='--wrn-context-x:{positionX}px;--wrn-context-y:{positionY}px;--wrn-context-min-width:{minWidth};--wrn-context-max-width:{maxWidth};'
@keydown='handleKeydown(event)'
>
<div
class="wire-context-menu__trigger"
class="wrn-context-menu__trigger"
tabindex='{disabled ? "-1" : "0"}'
aria-haspopup="menu"
aria-expanded='{open || visible ? "true" : "false"}'
@@ -156,7 +156,7 @@ component ContextMenu {
{#if closeOnOutside}
<button
type="button"
class="wire-context-menu__dismiss-layer"
class="wrn-context-menu__dismiss-layer"
data-show='{open || visible}'
aria-label="Close context menu"
@click='hideMenu("outside", event)'
@@ -164,14 +164,14 @@ component ContextMenu {
{/if}
<div
class="wire-context-menu__panel"
class="wrn-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">
<div class="wrn-context-menu__header">
{#if title}
<strong>{title}</strong>
{/if}
@@ -183,12 +183,12 @@ component ContextMenu {
<slot name="header"></slot>
<div class="wire-context-menu__items">
<div class="wrn-context-menu__items">
{#each items as item, itemIndex}
{#if item.type === "divider"}
<div class="wire-context-menu__divider" role="separator"></div>
<div class="wrn-context-menu__divider" role="separator"></div>
{:else if item.type === "header"}
<div class="wire-context-menu__section-label">{item.label || item.title}</div>
<div class="wrn-context-menu__section-label">{item.label || item.title}</div>
{:else if item.href}
<a
href='{item.href}'
@@ -199,13 +199,13 @@ component ContextMenu {
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"
class="wrn-context-menu__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
<span class='wrn-context-menu__icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-context-menu__copy">
<span class="wrn-context-menu__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
@@ -214,9 +214,9 @@ component ContextMenu {
{#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>
<span class="icon-[lucide--check] wrn-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>
<span class="icon-[lucide--arrow-up-right] wrn-context-menu__status" aria-hidden="true"></span>
{/if}
</a>
{:else}
@@ -227,13 +227,13 @@ component ContextMenu {
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
disabled='{item.disabled}'
class="wire-context-menu__item"
class="wrn-context-menu__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
<span class='wrn-context-menu__icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-context-menu__copy">
<span class="wrn-context-menu__copy">
<strong>{item.label || item.title}</strong>
{#if item.description}
<small>{item.description}</small>
@@ -242,7 +242,7 @@ component ContextMenu {
{#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>
<span class="icon-[lucide--check] wrn-context-menu__status" aria-hidden="true"></span>
{/if}
</button>
{/if}
@@ -256,52 +256,52 @@ component ContextMenu {
}
style {
.wire-context-menu {
--context-accent: var(--wire-color-primary);
--context-soft: var(--wire-color-primary-soft);
.wrn-context-menu {
--context-accent: var(--wrn-color-primary);
--context-soft: var(--wrn-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);
.wrn-context-menu[data-color="secondary"] {
--context-accent: var(--wrn-color-secondary);
--context-soft: var(--wrn-color-secondary-soft);
}
.wire-context-menu[data-color="info"] {
--context-accent: var(--wire-color-info);
--context-soft: var(--wire-color-info-soft);
.wrn-context-menu[data-color="info"] {
--context-accent: var(--wrn-color-info);
--context-soft: var(--wrn-color-info-soft);
}
.wire-context-menu[data-color="success"] {
--context-accent: var(--wire-color-success);
--context-soft: var(--wire-color-success-soft);
.wrn-context-menu[data-color="success"] {
--context-accent: var(--wrn-color-success);
--context-soft: var(--wrn-color-success-soft);
}
.wire-context-menu[data-color="warning"] {
--context-accent: var(--wire-color-warning);
--context-soft: var(--wire-color-warning-soft);
.wrn-context-menu[data-color="warning"] {
--context-accent: var(--wrn-color-warning);
--context-soft: var(--wrn-color-warning-soft);
}
.wire-context-menu[data-color="danger"] {
--context-accent: var(--wire-color-danger);
--context-soft: var(--wire-color-danger-soft);
.wrn-context-menu[data-color="danger"] {
--context-accent: var(--wrn-color-danger);
--context-soft: var(--wrn-color-danger-soft);
}
.wire-context-menu__trigger {
.wrn-context-menu__trigger {
display: block;
min-width: 0;
outline: none;
}
.wire-context-menu__trigger:focus-visible {
.wrn-context-menu__trigger:focus-visible {
border-radius: 0.6rem;
outline: 2px solid var(--wire-color-focus);
outline: 2px solid var(--wrn-color-focus);
outline-offset: 3px;
}
.wire-context-menu__dismiss-layer {
.wrn-context-menu__dismiss-layer {
position: fixed;
inset: 0;
z-index: 1090;
@@ -311,20 +311,20 @@ component ContextMenu {
border: 0;
}
.wire-context-menu__panel {
.wrn-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));
min-width: var(--wrn-context-min-width);
max-width: min(var(--wrn-context-max-width), calc(100vw - 1.5rem));
padding: 0.45rem;
color: var(--wire-color-text);
color: var(--wrn-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));
color-mix(in srgb, var(--wrn-color-surface-raised) 96%, transparent);
border: 1px solid color-mix(in srgb, var(--context-accent) 18%, var(--wrn-color-border));
border-radius: 1rem;
box-shadow:
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
@@ -333,25 +333,25 @@ component ContextMenu {
transform-origin: top left;
}
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
.wrn-context-menu[data-placement="pointer"] .wrn-context-menu__panel {
position: fixed;
top: var(--wire-context-y);
left: var(--wire-context-x);
top: var(--wrn-context-y);
left: var(--wrn-context-x);
}
.wire-context-menu[data-placement="bottom-end"] .wire-context-menu__panel {
.wrn-context-menu[data-placement="bottom-end"] .wrn-context-menu__panel {
right: 0;
left: auto;
transform-origin: top right;
}
.wire-context-menu[data-placement="top-start"] .wire-context-menu__panel {
.wrn-context-menu[data-placement="top-start"] .wrn-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 {
.wrn-context-menu[data-placement="top-end"] .wrn-context-menu__panel {
top: auto;
right: 0;
bottom: calc(100% + 0.5rem);
@@ -359,58 +359,58 @@ component ContextMenu {
transform-origin: bottom right;
}
.wire-context-menu[data-variant="soft"] .wire-context-menu__panel {
.wrn-context-menu[data-variant="soft"] .wrn-context-menu__panel {
background:
linear-gradient(145deg, var(--context-soft), transparent 72%),
var(--wire-color-surface-raised);
var(--wrn-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);
.wrn-context-menu[data-variant="outline"] .wrn-context-menu__panel {
background: var(--wrn-color-surface-raised);
box-shadow: none;
}
.wire-context-menu__header {
.wrn-context-menu__header {
display: grid;
gap: 0.2rem;
padding: 0.65rem 0.75rem 0.75rem;
border-bottom: 1px solid var(--wire-color-border);
border-bottom: 1px solid var(--wrn-color-border);
}
.wire-context-menu__header strong {
.wrn-context-menu__header strong {
font-size: 0.86rem;
font-weight: 650;
}
.wire-context-menu__header span {
color: var(--wire-color-text-muted);
.wrn-context-menu__header span {
color: var(--wrn-color-text-muted);
font-size: 0.75rem;
line-height: 1.45;
}
.wire-context-menu__items {
.wrn-context-menu__items {
display: grid;
gap: 0.15rem;
padding-block: 0.25rem;
}
.wire-context-menu__section-label {
.wrn-context-menu__section-label {
padding: 0.55rem 0.75rem 0.3rem;
color: var(--wire-color-text-muted);
color: var(--wrn-color-text-muted);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.wire-context-menu__divider {
.wrn-context-menu__divider {
height: 1px;
margin: 0.3rem 0.45rem;
background: var(--wire-color-border);
background: var(--wrn-color-border);
}
.wire-context-menu__item {
.wrn-context-menu__item {
appearance: none;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
@@ -419,7 +419,7 @@ component ContextMenu {
width: 100%;
min-width: 0;
padding: 0.65rem 0.7rem;
color: var(--wire-color-text);
color: var(--wrn-color-text);
background: transparent;
border: 0;
border-radius: 0.72rem;
@@ -430,47 +430,47 @@ component ContextMenu {
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"] {
.wrn-context-menu__item:hover,
.wrn-context-menu__item:focus-visible,
.wrn-context-menu__item[data-selected="true"] {
color: var(--context-accent);
background: var(--context-soft);
outline: none;
}
.wire-context-menu__item:active {
.wrn-context-menu__item:active {
transform: scale(0.985);
}
.wire-context-menu__item[data-danger="true"] {
color: var(--wire-color-danger);
.wrn-context-menu__item[data-danger="true"] {
color: var(--wrn-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);
.wrn-context-menu__item[data-danger="true"]:hover,
.wrn-context-menu__item[data-danger="true"]:focus-visible {
background: var(--wrn-color-danger-soft);
}
.wire-context-menu__item[disabled],
.wire-context-menu__item[aria-disabled="true"] {
.wrn-context-menu__item[disabled],
.wrn-context-menu__item[aria-disabled="true"] {
opacity: 0.48;
pointer-events: none;
}
.wire-context-menu__icon,
.wire-context-menu__status {
.wrn-context-menu__icon,
.wrn-context-menu__status {
width: 1rem;
height: 1rem;
color: currentColor;
}
.wire-context-menu__copy {
.wrn-context-menu__copy {
display: grid;
gap: 0.12rem;
min-width: 0;
}
.wire-context-menu__copy strong {
.wrn-context-menu__copy strong {
overflow: hidden;
font-size: 0.82rem;
font-weight: 600;
@@ -479,35 +479,35 @@ component ContextMenu {
white-space: nowrap;
}
.wire-context-menu__copy small {
.wrn-context-menu__copy small {
overflow: hidden;
color: var(--wire-color-text-muted);
color: var(--wrn-color-text-muted);
font-size: 0.7rem;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-context-menu kbd {
.wrn-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);
color: var(--wrn-color-text-muted);
background: var(--wrn-color-surface-soft);
border: 1px solid var(--wrn-color-border);
border-radius: 0.36rem;
font-size: 0.64rem;
font-family: inherit;
}
.wire-context-menu[data-size="sm"] .wire-context-menu__item {
.wrn-context-menu[data-size="sm"] .wrn-context-menu__item {
padding: 0.52rem 0.6rem;
}
.wire-context-menu[data-size="lg"] .wire-context-menu__item {
.wrn-context-menu[data-size="lg"] .wrn-context-menu__item {
padding: 0.78rem 0.82rem;
}
@media (max-width: 639px) {
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
.wrn-context-menu[data-placement="pointer"] .wrn-context-menu__panel {
right: 0.75rem;
bottom: 0.75rem;
left: 0.75rem;
@@ -518,7 +518,7 @@ component ContextMenu {
}
@media (prefers-reduced-motion: reduce) {
.wire-context-menu__item {
.wrn-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>
}
}
+114 -9
View File
@@ -1,17 +1,122 @@
// CustomScrollbar -- a scrolling region with a themed scrollbar.
//
// <CustomScrollbar maxHeight="20rem">...long content...</CustomScrollbar>
//
// Scrollbar appearance is CSS, not script: scrollbar-width and scrollbar-color
// are the standard properties, and the ::-webkit-scrollbar rules cover the
// browsers that still need them.
//
// This component used to declare a scroll output it never emitted, and its
// props were columns, gap and maxWidth copied from a grid scaffold. The output
// is removed rather than left unimplemented -- a caller can listen for a plain
// scroll event on the element, which is what it would have been anyway.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
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"
size: string = "default"
axis: string = "vertical"
// Track thickness in pixels. Clamped, because it arrives as an attribute
// and a scrollbar wider than the content is not useful to anyone.
thickness: number = 8
maxHeight: string = "20rem"
radius: string = "999px"
class: string = ""
}
functions {
shared function trackSize() {
var value = Number(thickness)
if (!value || value < 2) {
return 8
}
return Math.min(24, value)
}
}
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>
<div
{...attrs}
data-ui-component="CustomScrollbar"
class='wrn-scrollbar {class}'
data-color='{color}'
data-size='{size}'
data-axis='{axis}'
style='--scrollbar-thickness:{trackSize()}px;--scrollbar-radius:{radius};max-height:{maxHeight};'
>
<slot />
</div>
}
style {
.wrn-scrollbar {
--scrollbar-accent: var(--wrn-color-primary);
min-width: 0;
overflow: auto;
/*
* The standard properties. Firefox and current Chrome honour these; the
* webkit rules below are only for the engines that still ignore them.
*/
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--scrollbar-accent) 55%, transparent) transparent;
overscroll-behavior: contain;
}
.wrn-scrollbar[data-color="secondary"] {
--scrollbar-accent: var(--wrn-color-secondary);
}
.wrn-scrollbar[data-color="success"] {
--scrollbar-accent: var(--wrn-color-success);
}
.wrn-scrollbar[data-color="danger"] {
--scrollbar-accent: var(--wrn-color-danger);
}
.wrn-scrollbar[data-color="info"] {
--scrollbar-accent: var(--wrn-color-info);
}
.wrn-scrollbar[data-axis="vertical"] {
overflow-x: hidden;
overflow-y: auto;
}
.wrn-scrollbar[data-axis="horizontal"] {
overflow-x: auto;
overflow-y: hidden;
}
.wrn-scrollbar::-webkit-scrollbar {
width: var(--scrollbar-thickness, 8px);
height: var(--scrollbar-thickness, 8px);
}
.wrn-scrollbar::-webkit-scrollbar-track {
background: var(--wrn-color-surface-soft);
border-radius: var(--scrollbar-radius, 999px);
}
.wrn-scrollbar::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--scrollbar-accent) 45%, transparent);
border-radius: var(--scrollbar-radius, 999px);
}
.wrn-scrollbar::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-accent);
}
/*
* A pointer-less device paints its own overlay scrollbar and ignores the
* width above, so the region keeps its own padding instead.
*/
@media (hover: none) {
.wrn-scrollbar {
scrollbar-width: auto;
}
}
}
}
+86 -2
View File
@@ -14,11 +14,95 @@ component DataMap {
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--data-map wire-next--variant-{variant} {class}">
<section class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--data-map wrn-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}
{#if items}<div class="wrn-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
style {
.wrn-next--data-map {
display: grid;
gap: 0.75rem;
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius);
color: var(--wrn-color-text);
background: var(--wrn-color-surface);
}
.wrn-next--data-map > p { margin: 0; color: var(--wrn-color-muted); }
.wrn-next--data-map > .wrn-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.wrn-next--data-map > .wrn-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wrn-radius-sm); background: var(--wrn-color-surface-2); }
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
File diff suppressed because it is too large Load Diff
+622 -6
View File
@@ -45,18 +45,634 @@ size: string = "default"
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 selectDay(sourceEvent, root, input, nextDate, trigger, detail) { root = sourceEvent.currentTarget.closest(".wrn-next--date-picker"); input = root.querySelector(".wrn-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(".wrn-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 {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--date-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}">
<div class="wrn-next__field-heading"><label for="{id || name}">{label}</label>{#if cornerHint}<span class="wrn-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wrn-next__picker-control"><input id="{id || name}" class="wrn-next__sr-only wrn-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" /><button type="button" class="wrn-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="wrn-next__picker-clear" aria-label="Clear date" @click="clearDate(event)">×</button>{/if}</div>
<div class="wrn-next__calendar" role="dialog" aria-label="{label}"><div class="wrn-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="wrn-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="wrn-next__date-done" disabled="{!currentValue}" @click="finishDate(event)">Done</button></div>
{#if helperText}<small class="wrn-next__field-help">{helperText}</small>{/if}<small class="wrn-next__field-error" data-error="{name}">{error}</small>
</div>
}
style {
.wrn-next__picker-clear {
position: absolute;
right: 2.5rem;
border: 0;
background: transparent;
color: var(--wrn-color-muted);
}
.wrn-next__date-selectors {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 0.65rem;
}
.wrn-next__date-selectors label {
display: grid;
gap: 0.3rem;
color: var(--wrn-color-muted);
font-size: 0.78rem;
font-weight: 600;
}
.wrn-next__date-selectors select {
width: 100%;
min-height: 2.35rem;
padding: 0.45rem 0.65rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
font: inherit;
}
.wrn-next__date-done {
background: var(--wrn-field-color) !important;
color: var(--wrn-color-primary-contrast, #fff) !important;
font-weight: 650;
}
.wrn-next__calendar > header {
display: grid;
grid-template-columns: 2.25rem 1fr 2.25rem;
align-items: center;
text-align: center;
}
.wrn-next__calendar-grid button[data-outside="true"] {
color: var(--wrn-color-muted);
opacity: 0.55;
}
.wrn-next__calendar-grid button[data-today="true"] {
box-shadow: inset 0 0 0 1px var(--wrn-field-color);
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
/* Shared field foundation. */
/* Validation states set by the browser validation runtime. */
.wrn-invalid {
border-color: var(--wrn-color-danger) !important;
}
.wrn-field-error {
display: block;
min-height: 1em;
margin-top: 0.25rem;
color: var(--wrn-color-danger);
font-size: 0.8rem;
}
.wrn-next--choice {
display: inline-flex;
align-items: center;
gap: 0.55rem;
cursor: pointer;
}
.wrn-next--choice input {
width: 1.1rem;
height: 1.1rem;
accent-color: var(--wrn-color-primary);
}
.wrn-next--field {
display: grid;
width: min(100%, 28rem);
gap: 0.4rem;
font-size: 0.78rem;
font-weight: 700;
}
.wrn-next--field :where(input, textarea, select) {
width: 100%;
min-height: 2.65rem;
padding: 0.6rem 0.7rem;
border: 1px solid var(--wrn-color-border);
border-radius: 0.6rem;
outline: 0;
color: var(--wrn-color-text);
background: var(--wrn-color-bg);
font: inherit;
font-weight: 500;
}
.wrn-auth-form form > .wrn-next--field,
.wrn-auth-form form > .wrn-field {
width: 100%;
max-width: none;
}
.wrn-auth-form__submit > .wrn-btn {
width: 100%;
min-height: 3rem;
border-radius: 0.75rem;
box-shadow: 0 0.8rem 1.8rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
}
.wrn-next--field :where(input, textarea, select):focus-visible {
border-color: var(--wrn-color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 16%, transparent);
}
.wrn-next--field textarea {
resize: vertical;
}
.wrn-next--field,
.wrn-next--choice-field {
--wrn-field-color: var(--wrn-component-color, var(--wrn-color-primary));
width: min(100%, 34rem);
min-width: 0;
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 500;
}
.wrn-next--field.wrn-next--size-sm,
.wrn-next--choice-field.wrn-next--size-sm {
font-size: 0.8125rem;
}
.wrn-next--field.wrn-next--size-lg,
.wrn-next--choice-field.wrn-next--size-lg {
font-size: 1rem;
}
.wrn-next__field-heading {
display: flex;
min-width: 0;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-weight: 600;
}
.wrn-next__field-hint,
.wrn-next__field-help {
color: var(--wrn-color-muted);
font-size: 0.82em;
font-weight: 500;
}
.wrn-next__field-control {
position: relative;
min-width: 0;
}
.wrn-next__field-control > :is(input, textarea, select) {
padding-inline: 0.85rem;
border-color: var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
font-size: 0.8125rem;
}
.wrn-next__field-control[data-icon-position="start"] > :is(input, textarea, select) {
padding-inline-start: 2.5rem;
}
.wrn-next__field-control[data-icon-position="end"] > :is(input, textarea, select) {
padding-inline-end: 2.5rem;
}
.wrn-next__field-icon {
position: absolute;
z-index: 1;
top: 50%;
left: 0.85rem;
color: var(--wrn-color-muted);
pointer-events: none;
transform: translateY(-50%);
}
.wrn-next__field-control[data-icon-position="end"] > .wrn-next__field-icon {
right: 0.85rem;
left: auto;
}
.wrn-next--field[data-variant="outlined"]
.wrn-next__field-control
> :is(input, textarea, select) {
border-width: 2px;
}
.wrn-next--field[data-variant="underline"]
.wrn-next__field-control
> :is(input, textarea, select) {
padding-inline: 0;
border-width: 0 0 1px;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.wrn-next--field[data-variant="pill"] .wrn-next__field-control > :is(input, select) {
border-radius: 999px;
padding-inline: 1.15rem;
}
.wrn-next--field[data-variant="ghost"] .wrn-next__field-control > :is(input, textarea, select) {
border-color: transparent;
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
}
.wrn-next__field-floating-label {
position: absolute;
top: 50%;
left: 0.85rem;
color: var(--wrn-color-muted);
pointer-events: none;
transform: translateY(-50%);
transition: 150ms ease;
}
.wrn-next--textarea .wrn-next__field-floating-label {
top: 1.35rem;
}
.wrn-next--field[data-floating="true"] .wrn-next__field-control > :is(input, textarea, select) {
padding-top: 1.2rem;
padding-bottom: 0.3rem;
}
.wrn-next--field[data-floating="true"] .wrn-next__field-heading > label {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
margin: -1px !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
}
.wrn-next--field[data-floating="true"]
.wrn-next__field-heading:not(:has(.wrn-next__field-hint)) {
display: none;
}
.wrn-next--field[data-floating="true"]
.wrn-next__field-control
> :is(input, textarea):is(:focus, :not(:placeholder-shown))
+ .wrn-next__field-floating-label {
top: 0.42rem;
font-size: 0.7em;
transform: none;
}
.wrn-next__field-error {
min-height: 1em;
color: var(--wrn-color-danger);
font-size: 0.82em;
font-weight: 600;
}
.wrn-next__field-error:empty {
display: none;
}
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
.wrn-next--choice-field[data-invalid="true"] input {
border-color: var(--wrn-color-danger);
}
.wrn-next--field :is(input, textarea, select):disabled,
.wrn-next--choice-field input:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.wrn-next--field :is(input, textarea, select):read-only {
background: var(--wrn-color-surface-2);
}
.wrn-next--field[data-inline="true"],
.wrn-next--choice-field[data-inline="true"] {
display: grid;
width: 100%;
max-width: none;
grid-template-columns: minmax(7rem, max-content) minmax(12rem, 20rem) minmax(10rem, 1fr);
justify-content: center;
align-items: center;
gap: 0.75rem;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-heading,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading {
min-height: 2.65rem;
align-items: center;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-heading {
justify-content: flex-end;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-help,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-help {
display: flex;
min-height: 2.65rem;
align-items: center;
}
.wrn-next--choice-field[data-inline="true"] {
grid-template-columns: minmax(12rem, 20rem) minmax(10rem, 1fr);
}
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading:empty {
display: none;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
grid-column: 2 / -1;
}
.wrn-next__sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
border: 0 !important;
margin: -1px !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
}
.wrn-next__choice-field {
display: grid;
width: 100%;
max-width: none;
gap: 0.4rem;
}
.wrn-next__choice {
display: flex;
min-width: 0;
align-items: center;
gap: 0.65rem;
color: var(--wrn-color-text);
width: 100%;
font-size: 0.75rem;
font-weight: 500;
}
.wrn-next__checkbox-content > .wrn-next__choice {
width: 100%;
min-width: 0;
align-items: flex-start;
}
.wrn-next__choice input {
flex: 0 0 auto;
margin: 0;
accent-color: var(--wrn-field-color);
}
.wrn-next__choice-copy {
display: grid;
min-width: 0;
gap: 0.15rem;
line-height: 1.35;
}
.wrn-next__choice-copy small {
color: var(--wrn-color-muted);
font-weight: 500;
}
.wrn-next__radio-group,
.wrn-next__checkbox-group {
display: grid;
min-width: 0;
gap: 0.65rem;
}
.wrn-next--radio-field[data-orientation="horizontal"] .wrn-next__radio-group,
.wrn-next--checkbox-field[data-orientation="horizontal"] .wrn-next__checkbox-group {
grid-template-columns: repeat(auto-fit, minmax(8rem, max-content));
align-items: stretch;
}
.wrn-next--radio-field[data-card="true"] .wrn-next__radio-group,
.wrn-next--checkbox-field[data-card="true"] .wrn-next__checkbox-group {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
}
.wrn-next--radio-field[data-card="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox {
min-height: 4.5rem;
padding: 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
}
.wrn-next--radio-field[data-card="true"] .wrn-next--radio:has(input:checked),
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox:has(input:checked) {
border-color: var(--wrn-field-color);
box-shadow: 0 0 0 1px var(--wrn-field-color);
}
.wrn-next--radio-field[data-list="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-list="true"] .wrn-next--checkbox {
padding-block: 0.75rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox {
justify-content: space-between;
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio input,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox input {
order: 2;
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next__choice-copy,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next__choice-copy {
order: 1;
}
.wrn-next__choice[data-variant="outlined"],
.wrn-next__choice[data-variant="floating"] {
padding: 0.65rem 0.8rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
}
.wrn-next__choice[data-variant="pill"] {
padding: 0.65rem 1rem;
border-radius: 999px;
background: color-mix(in srgb, var(--wrn-field-color) 9%, var(--wrn-color-surface));
}
.wrn-next__choice[data-variant="ghost"] {
padding: 0.65rem 0.8rem;
border-radius: var(--wrn-radius-sm);
background: color-mix(in srgb, var(--wrn-field-color) 7%, transparent);
}
.wrn-next__choice[data-variant="underline"] {
padding-block: 0.5rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-next--select .wrn-next__field-control {
display: flex;
align-items: center;
}
.wrn-next__color-control,
.wrn-next__range-control,
.wrn-next__input-group-control,
.wrn-next__file-control {
display: flex;
min-width: 0;
align-items: center;
gap: 0.65rem;
}
.wrn-next__picker-control,
.wrn-next--time-picker .wrn-next__field-control {
position: relative;
display: flex;
min-width: 0;
align-items: center;
}
.wrn-next__picker-control > button:first-of-type,
.wrn-next__time-trigger {
display: flex;
width: 100%;
min-height: 2.65rem;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.7rem 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
font: inherit;
text-align: left;
}
.wrn-next__calendar,
.wrn-next__time-panel {
z-index: 20;
display: grid;
width: min(100%, 22rem);
gap: 0.75rem;
padding: 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-3);
}
.wrn-next--date-picker[data-expanded="false"] .wrn-next__calendar,
.wrn-next--time-picker[data-expanded="false"] .wrn-next__time-panel {
display: none;
}
.wrn-next__calendar button,
.wrn-next__time-panel button {
min-height: 2.25rem;
border: 0;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: inherit;
}
.wrn-next__calendar-weekdays,
.wrn-next__calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.2rem;
text-align: center;
}
.wrn-next__calendar-weekdays {
color: var(--wrn-color-muted);
font-size: 0.75rem;
}
.wrn-next__calendar-grid button:hover,
.wrn-next__time-panel button:hover {
background: var(--wrn-color-surface-2);
}
.wrn-next__calendar-grid button[aria-pressed="true"],
.wrn-next__time-panel button[aria-pressed="true"] {
background: var(--wrn-field-color);
color: var(--wrn-color-primary-contrast, #fff);
}
.wrn-next__time-period {
display: flex;
gap: 0.35rem;
}
.wrn-next--file-upload-progress .wrn-next__row,
.wrn-next__upload-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.wrn-next--file-upload-progress .wrn-next__row > span {
display: grid;
min-width: 0;
}
.wrn-next--field[data-variant="underline"]
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
border-width: 0 0 1px;
border-radius: 0;
background: transparent;
}
.wrn-next--field[data-variant="outlined"]
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
border-width: 2px;
}
.wrn-next--field[data-variant="pill"]
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
border-radius: 999px;
}
.wrn-next--field[data-variant="ghost"]
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
border-color: transparent;
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
}
@media (max-width: 640px) {
.wrn-next--field[data-inline="true"],
.wrn-next--choice-field[data-inline="true"] {
grid-template-columns: 1fr;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
grid-column: auto;
}
}
.wrn-next--toggle-password:has(input.wrn-invalid) .wrn-next__password-control > input,
.wrn-next--toggle-password.wrn-next--invalid .wrn-next__password-control > input {
border-color: var(--wrn-color-danger);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 14%, transparent);
}
.wrn-next--strong-password.wrn-next--invalid input,
.wrn-next--strong-password input.wrn-invalid {
border-color: var(--wrn-color-danger);
}
}
}
+145 -3
View File
@@ -26,9 +26,151 @@ component DeviceFrame {
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 {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--device-frame wrn-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="wrn-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="wrn-next__device-shell">{#if src || srcdoc}<iframe title="{frameTitle}" src="{src}" srcdoc="{srcdoc}" allow="{allow}"></iframe>{:else}<div class="wrn-next__device-content"><slot /></div>{/if}</div>
</section>
}
style {
.wrn-next--device-frame {
display: grid;
gap: 0.75rem;
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-1);
}
.wrn-next--device-frame > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.wrn-next__device-actions {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.wrn-next__device-actions button {
min-height: 2.25rem;
padding-inline: 0.7rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface-2);
color: inherit;
}
.wrn-next__device-shell {
overflow: hidden;
width: min(100%, 24rem);
min-height: 28rem;
margin-inline: auto;
border: 0.65rem solid var(--wrn-color-text);
border-radius: 2rem;
background: var(--wrn-color-background);
transition: width var(--wrn-motion-normal) var(--wrn-ease-standard);
}
.wrn-next--device-frame[data-device="tablet"] .wrn-next__device-shell {
width: min(100%, 42rem);
}
.wrn-next--device-frame[data-device="desktop"] .wrn-next__device-shell {
width: 100%;
border-width: 0.4rem;
border-radius: var(--wrn-radius-md);
}
.wrn-next--device-frame[data-orientation="landscape"] .wrn-next__device-shell {
width: min(100%, 48rem);
min-height: 20rem;
}
.wrn-next__device-shell :is(iframe, .wrn-next__device-content) {
width: 100%;
min-height: inherit;
border: 0;
}
@media (max-width: 640px) {
.wrn-next--device-frame > header {
align-items: stretch;
flex-direction: column;
}
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+86 -2
View File
@@ -18,11 +18,95 @@ component DragAndDrop {
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}">
<section class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--drag-and-drop wrn-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}
{#if items}<div class="wrn-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
style {
.wrn-next--drag-and-drop {
display: grid;
gap: 0.75rem;
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius);
color: var(--wrn-color-text);
background: var(--wrn-color-surface);
}
.wrn-next--drag-and-drop > p { margin: 0; color: var(--wrn-color-muted); }
.wrn-next--drag-and-drop > .wrn-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.wrn-next--drag-and-drop > .wrn-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wrn-radius-sm); background: var(--wrn-color-surface-2); }
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+107 -107
View File
@@ -80,14 +80,14 @@ open: boolean = false
data-variant='{variant}'
data-overlay='{overlay ? "true" : "false"}'
data-scrollable='{scrollable ? "true" : "false"}'
class='wire-drawer {class}'
class='wrn-drawer {class}'
style='--drawer-duration: {duration}ms'
>
{#if triggerLabel}
<button
type="button"
class="wire-drawer__trigger"
class="wrn-drawer__trigger"
aria-haspopup="dialog"
aria-expanded='{open || visible ? "true" : "false"}'
@click='showDrawer(event)'
@@ -99,40 +99,40 @@ open: boolean = false
</button>
{/if}
<span class="wire-drawer__trigger-slot" @click='showDrawer(event)'>
<span class="wrn-drawer__trigger-slot" @click='showDrawer(event)'>
<slot name="trigger"></slot>
</span>
<div
class="wire-drawer__layer"
class="wrn-drawer__layer"
role="presentation"
@keydown='handleKeydown(event)'
>
{#if overlay}
<button
type="button"
class="wire-drawer__backdrop"
class="wrn-drawer__backdrop"
aria-label='{closeLabel}'
@click='if (closeOnBackdrop) { cancelDrawer(event) }'
></button>
{/if}
<section
class="wire-drawer__panel"
class="wrn-drawer__panel"
role="dialog"
aria-modal="true"
aria-label='{label || title}'
tabindex="-1"
>
<div class="wire-drawer__handle" aria-hidden="true"></div>
<div class="wrn-drawer__handle" aria-hidden="true"></div>
<header class="wire-drawer__header">
<div class="wire-drawer__heading">
<header class="wrn-drawer__header">
<div class="wrn-drawer__heading">
{#if icon}
<span class='wire-drawer__icon {icon}' aria-hidden="true"></span>
<span class='wrn-drawer__icon {icon}' aria-hidden="true"></span>
{/if}
<div class="wire-drawer__heading-copy">
<div class="wrn-drawer__heading-copy">
<slot name="header"></slot>
{#if title}
<h2>{title}</h2>
@@ -146,7 +146,7 @@ open: boolean = false
{#if showClose}
<button
type="button"
class="wire-drawer__close"
class="wrn-drawer__close"
aria-label='{closeLabel}'
@click='hideDrawer("close-button", event)'
>
@@ -155,11 +155,11 @@ open: boolean = false
{/if}
</header>
<div class="wire-drawer__body">
<div class="wrn-drawer__body">
<slot></slot>
</div>
<footer class="wire-drawer__footer">
<footer class="wrn-drawer__footer">
<slot name="footer"></slot>
</footer>
</section>
@@ -168,50 +168,50 @@ open: boolean = false
}
style {
.wire-drawer {
--drawer-accent: var(--wire-color-primary);
--drawer-soft: var(--wire-color-primary-soft);
.wrn-drawer {
--drawer-accent: var(--wrn-color-primary);
--drawer-soft: var(--wrn-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);
.wrn-drawer[data-color="secondary"] {
--drawer-accent: var(--wrn-color-secondary);
--drawer-soft: var(--wrn-color-secondary-soft);
}
.wire-drawer[data-color="info"] {
--drawer-accent: var(--wire-color-info);
--drawer-soft: var(--wire-color-info-soft);
.wrn-drawer[data-color="info"] {
--drawer-accent: var(--wrn-color-info);
--drawer-soft: var(--wrn-color-info-soft);
}
.wire-drawer[data-color="success"] {
--drawer-accent: var(--wire-color-success);
--drawer-soft: var(--wire-color-success-soft);
.wrn-drawer[data-color="success"] {
--drawer-accent: var(--wrn-color-success);
--drawer-soft: var(--wrn-color-success-soft);
}
.wire-drawer[data-color="warning"] {
--drawer-accent: var(--wire-color-warning);
--drawer-soft: var(--wire-color-warning-soft);
.wrn-drawer[data-color="warning"] {
--drawer-accent: var(--wrn-color-warning);
--drawer-soft: var(--wrn-color-warning-soft);
}
.wire-drawer[data-color="danger"] {
--drawer-accent: var(--wire-color-danger);
--drawer-soft: var(--wire-color-danger-soft);
.wrn-drawer[data-color="danger"] {
--drawer-accent: var(--wrn-color-danger);
--drawer-soft: var(--wrn-color-danger-soft);
}
.wire-drawer__trigger,
.wire-drawer__trigger-slot {
.wrn-drawer__trigger,
.wrn-drawer__trigger-slot {
display: inline-flex;
align-items: center;
gap: 0.55rem;
}
.wire-drawer__trigger {
.wrn-drawer__trigger {
appearance: none;
min-height: 2.55rem;
padding: 0.65rem 1rem;
color: var(--wire-color-primary-contrast);
color: var(--wrn-color-primary-contrast);
background: var(--drawer-accent);
border: 0;
border-radius: 0.8rem;
@@ -228,7 +228,7 @@ open: boolean = false
* 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 {
.wrn-drawer__layer {
position: fixed;
inset: 0;
z-index: 1200;
@@ -241,7 +241,7 @@ open: boolean = false
visibility 0s linear var(--drawer-duration, 260ms);
}
.wire-drawer[data-open="true"] .wire-drawer__layer {
.wrn-drawer[data-open="true"] .wrn-drawer__layer {
visibility: visible;
opacity: 1;
transition:
@@ -249,7 +249,7 @@ open: boolean = false
visibility 0s linear 0s;
}
.wire-drawer__backdrop {
.wrn-drawer__backdrop {
position: absolute;
inset: 0;
z-index: 0;
@@ -261,11 +261,11 @@ open: boolean = false
pointer-events: auto;
}
.wire-drawer[data-overlay="false"] .wire-drawer__backdrop {
.wrn-drawer[data-overlay="false"] .wrn-drawer__backdrop {
display: none;
}
.wire-drawer__panel {
.wrn-drawer__panel {
position: relative;
z-index: 1;
display: flex;
@@ -273,15 +273,15 @@ open: boolean = false
width: min(28rem, calc(100vw - 1rem));
max-height: 100%;
margin-left: auto;
color: var(--wire-color-text);
color: var(--wrn-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));
var(--wrn-color-surface-raised);
border-left: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wrn-color-border));
box-shadow: -28px 0 80px color-mix(in srgb, black 28%, transparent);
pointer-events: auto;
overflow: hidden;
@@ -290,66 +290,66 @@ open: boolean = false
transition: transform var(--drawer-duration, 260ms) cubic-bezier(0.32, 0.72, 0, 1);
}
.wire-drawer[data-open="true"] .wire-drawer__panel {
.wrn-drawer[data-open="true"] .wrn-drawer__panel {
transform: none;
}
.wire-drawer[data-placement="left"] .wire-drawer__panel {
.wrn-drawer[data-placement="left"] .wrn-drawer__panel {
transform: translateX(-100%);
}
.wire-drawer[data-placement="top"] .wire-drawer__panel {
.wrn-drawer[data-placement="top"] .wrn-drawer__panel {
transform: translateY(-100%);
}
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
.wrn-drawer[data-placement="bottom"] .wrn-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 {
.wrn-drawer[data-open="true"][data-placement="left"] .wrn-drawer__panel,
.wrn-drawer[data-open="true"][data-placement="top"] .wrn-drawer__panel,
.wrn-drawer[data-open="true"][data-placement="bottom"] .wrn-drawer__panel {
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.wire-drawer__layer,
.wire-drawer__panel {
.wrn-drawer__layer,
.wrn-drawer__panel {
transition: none;
}
}
.wire-drawer[data-size="sm"] .wire-drawer__panel {
.wrn-drawer[data-size="sm"] .wrn-drawer__panel {
width: min(22rem, calc(100vw - 1rem));
}
.wire-drawer[data-size="lg"] .wire-drawer__panel {
.wrn-drawer[data-size="lg"] .wrn-drawer__panel {
width: min(38rem, calc(100vw - 1rem));
}
.wire-drawer[data-size="xl"] .wire-drawer__panel {
.wrn-drawer[data-size="xl"] .wrn-drawer__panel {
width: min(52rem, calc(100vw - 1rem));
}
.wire-drawer[data-size="full"] .wire-drawer__panel {
.wrn-drawer[data-size="full"] .wrn-drawer__panel {
width: 100vw;
}
.wire-drawer[data-placement="left"] .wire-drawer__panel {
.wrn-drawer[data-placement="left"] .wrn-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-right: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wrn-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 {
.wrn-drawer[data-placement="top"] .wrn-drawer__layer,
.wrn-drawer[data-placement="bottom"] .wrn-drawer__layer {
align-items: flex-start;
}
.wire-drawer[data-placement="top"] .wire-drawer__panel,
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
.wrn-drawer[data-placement="top"] .wrn-drawer__panel,
.wrn-drawer[data-placement="bottom"] .wrn-drawer__panel {
width: 100%;
max-height: min(80vh, 44rem);
margin: 0;
@@ -357,61 +357,61 @@ open: boolean = false
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));
.wrn-drawer[data-placement="top"] .wrn-drawer__panel {
border-bottom: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wrn-color-border));
}
.wire-drawer[data-placement="bottom"] .wire-drawer__layer {
.wrn-drawer[data-placement="bottom"] .wrn-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));
.wrn-drawer[data-placement="bottom"] .wrn-drawer__panel {
border-top: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wrn-color-border));
border-radius: 1.3rem 1.3rem 0 0;
}
.wire-drawer[data-variant="soft"] .wire-drawer__panel {
.wrn-drawer[data-variant="soft"] .wrn-drawer__panel {
background:
linear-gradient(145deg, var(--drawer-soft), transparent 65%),
var(--wire-color-surface-raised);
var(--wrn-color-surface-raised);
}
.wire-drawer[data-variant="solid"] .wire-drawer__panel {
color: var(--wire-color-primary-contrast);
.wrn-drawer[data-variant="solid"] .wrn-drawer__panel {
color: var(--wrn-color-primary-contrast);
background: var(--drawer-accent);
border-color: color-mix(in srgb, white 18%, transparent);
}
.wire-drawer__handle {
.wrn-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);
background: color-mix(in srgb, var(--wrn-color-text-muted) 48%, transparent);
border-radius: 999px;
}
.wire-drawer[data-placement="bottom"] .wire-drawer__handle {
.wrn-drawer[data-placement="bottom"] .wrn-drawer__handle {
display: block;
}
.wire-drawer__header {
.wrn-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);
border-bottom: 1px solid var(--wrn-color-border);
}
.wire-drawer__heading {
.wrn-drawer__heading {
display: flex;
align-items: flex-start;
gap: 0.85rem;
min-width: 0;
}
.wire-drawer__icon {
.wrn-drawer__icon {
flex: 0 0 auto;
width: 1.25rem;
height: 1.25rem;
@@ -419,34 +419,34 @@ open: boolean = false
color: var(--drawer-accent);
}
.wire-drawer[data-variant="solid"] .wire-drawer__icon {
.wrn-drawer[data-variant="solid"] .wrn-drawer__icon {
color: currentColor;
}
.wire-drawer__heading-copy {
.wrn-drawer__heading-copy {
display: grid;
gap: 0.3rem;
min-width: 0;
}
.wire-drawer__heading-copy h2,
.wire-drawer__heading-copy p {
.wrn-drawer__heading-copy h2,
.wrn-drawer__heading-copy p {
margin: 0;
}
.wire-drawer__heading-copy h2 {
.wrn-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);
.wrn-drawer__heading-copy p {
color: var(--wrn-color-text-muted);
font-size: 0.8rem;
line-height: 1.55;
}
.wire-drawer[data-variant="solid"] .wire-drawer__heading-copy p {
.wrn-drawer[data-variant="solid"] .wrn-drawer__heading-copy p {
color: color-mix(in srgb, currentColor 76%, transparent);
}
@@ -456,7 +456,7 @@ open: boolean = false
* 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 {
.wrn-drawer__close {
appearance: none;
display: inline-flex;
align-items: center;
@@ -465,22 +465,22 @@ open: boolean = false
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);
color: var(--wrn-color-text-muted);
background: var(--wrn-color-surface-soft);
border: 1px solid var(--wrn-color-border);
border-radius: 0.75rem;
cursor: pointer;
}
.wire-drawer__close:hover,
.wire-drawer__close:focus-visible {
.wrn-drawer__close:hover,
.wrn-drawer__close:focus-visible {
color: var(--drawer-accent);
border-color: color-mix(in srgb, var(--drawer-accent) 34%, var(--wire-color-border));
border-color: color-mix(in srgb, var(--drawer-accent) 34%, var(--wrn-color-border));
outline: none;
}
/* Never let the glyph be shrunk by the flex container. */
.wire-drawer__close svg {
.wrn-drawer__close svg {
flex: 0 0 auto;
width: 1rem;
height: 1rem;
@@ -494,40 +494,40 @@ open: boolean = false
* :where() keeps the specificity low enough that any class the app puts on
* its own slot content still wins.
*/
.wire-drawer__body {
.wrn-drawer__body {
flex: 1 1 auto;
min-height: 0;
padding: 1.35rem;
color: var(--wire-color-text);
color: var(--wrn-color-text);
}
.wire-drawer__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
.wrn-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 {
.wrn-drawer[data-scrollable="true"] .wrn-drawer__body {
overflow: auto;
overscroll-behavior: contain;
}
.wire-drawer__footer {
.wrn-drawer__footer {
padding: 1rem 1.35rem 1.35rem;
border-top: 1px solid var(--wire-color-border);
border-top: 1px solid var(--wrn-color-border);
}
.wire-drawer__footer:empty {
.wrn-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 {
.wrn-drawer[data-placement="right"] .wrn-drawer__panel,
.wrn-drawer[data-placement="left"] .wrn-drawer__panel {
width: min(24rem, 100vw);
}
.wire-drawer__header,
.wire-drawer__body,
.wire-drawer__footer {
.wrn-drawer__header,
.wrn-drawer__body,
.wrn-drawer__footer {
padding-inline: 1rem;
}
}
+109 -109
View File
@@ -82,7 +82,7 @@ items: unknown[] = []
}
client function moveFocus(sourceEvent, direction) {
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
const root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
if (!options.length) {
return
@@ -110,14 +110,14 @@ items: unknown[] = []
moveFocus(sourceEvent, -1)
} else if (sourceEvent.key === "Home" && isOpen()) {
sourceEvent.preventDefault()
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
const root = sourceEvent.currentTarget.closest(".wrn-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 root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
if (options.length) {
options[options.length - 1].focus()
@@ -136,12 +136,12 @@ items: unknown[] = []
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
class='wire-dropdown {class}'
class='wrn-dropdown {class}'
@keydown='handleKeydown(event)'
>
<button
type="button"
class="wire-dropdown__trigger"
class="wrn-dropdown__trigger"
disabled='{disabled}'
aria-haspopup="menu"
aria-expanded='{open || visible ? "true" : "false"}'
@@ -149,20 +149,20 @@ items: unknown[] = []
>
<slot name="trigger"></slot>
{#if icon}
<span class='wire-dropdown__trigger-icon {icon}' aria-hidden="true"></span>
<span class='wrn-dropdown__trigger-icon {icon}' aria-hidden="true"></span>
{/if}
{#if label}
<span class="wire-dropdown__trigger-label">{label}</span>
<span class="wrn-dropdown__trigger-label">{label}</span>
{/if}
{#if showChevron}
<span class="icon-[lucide--chevron-down] wire-dropdown__chevron" aria-hidden="true"></span>
<span class="icon-[lucide--chevron-down] wrn-dropdown__chevron" aria-hidden="true"></span>
{/if}
</button>
{#if closeOnOutside}
<button
type="button"
class="wire-dropdown__dismiss-layer"
class="wrn-dropdown__dismiss-layer"
data-show='{open || visible}'
aria-label="Close menu"
@click='hideMenu("outside", event)'
@@ -170,7 +170,7 @@ items: unknown[] = []
{/if}
<div
class="wire-dropdown__panel"
class="wrn-dropdown__panel"
data-wrn-anchored="true"
data-show='{open || visible}'
role="menu"
@@ -178,12 +178,12 @@ items: unknown[] = []
>
<slot name="header"></slot>
<div class="wire-dropdown__items">
<div class="wrn-dropdown__items">
{#each items as item, itemIndex}
{#if item.type === "divider"}
<div class="wire-dropdown__divider" role="separator"></div>
<div class="wrn-dropdown__divider" role="separator"></div>
{:else if item.type === "header"}
<div class="wire-dropdown__section-label">{item.label || item.title}</div>
<div class="wrn-dropdown__section-label">{item.label || item.title}</div>
{:else if item.href}
<a
href='{item.href}'
@@ -194,26 +194,26 @@ items: unknown[] = []
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
class="wire-dropdown__item"
class="wrn-dropdown__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
<span class='wrn-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-dropdown__copy">
<span class="wrn-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>
<span class="wrn-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>
<span class="icon-[lucide--check] wrn-dropdown__status" aria-hidden="true"></span>
{:else if item.external}
<span class="icon-[lucide--arrow-up-right] wire-dropdown__status" aria-hidden="true"></span>
<span class="icon-[lucide--arrow-up-right] wrn-dropdown__status" aria-hidden="true"></span>
{/if}
</a>
{:else}
@@ -224,29 +224,29 @@ items: unknown[] = []
data-danger='{item.danger ? "true" : "false"}'
data-selected='{item.selected || item.checked ? "true" : "false"}'
disabled='{item.disabled}'
class="wire-dropdown__item"
class="wrn-dropdown__item"
@click='chooseItem(item, itemIndex, event)'
>
{#if item.icon}
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
<span class='wrn-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
{/if}
<span class="wire-dropdown__copy">
<span class="wrn-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>
<span class="wrn-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>
<span class="icon-[lucide--check] wrn-dropdown__status" aria-hidden="true"></span>
{/if}
</button>
{/if}
{:empty}
<div class="wire-dropdown__empty">{emptyLabel}</div>
<div class="wrn-dropdown__empty">{emptyLabel}</div>
{/each}
</div>
@@ -257,40 +257,40 @@ items: unknown[] = []
}
style {
.wire-dropdown {
--dropdown-accent: var(--wire-color-primary);
--dropdown-soft: var(--wire-color-primary-soft);
.wrn-dropdown {
--dropdown-accent: var(--wrn-color-primary);
--dropdown-soft: var(--wrn-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);
.wrn-dropdown[data-color="secondary"] {
--dropdown-accent: var(--wrn-color-secondary);
--dropdown-soft: var(--wrn-color-secondary-soft);
}
.wire-dropdown[data-color="info"] {
--dropdown-accent: var(--wire-color-info);
--dropdown-soft: var(--wire-color-info-soft);
.wrn-dropdown[data-color="info"] {
--dropdown-accent: var(--wrn-color-info);
--dropdown-soft: var(--wrn-color-info-soft);
}
.wire-dropdown[data-color="success"] {
--dropdown-accent: var(--wire-color-success);
--dropdown-soft: var(--wire-color-success-soft);
.wrn-dropdown[data-color="success"] {
--dropdown-accent: var(--wrn-color-success);
--dropdown-soft: var(--wrn-color-success-soft);
}
.wire-dropdown[data-color="warning"] {
--dropdown-accent: var(--wire-color-warning);
--dropdown-soft: var(--wire-color-warning-soft);
.wrn-dropdown[data-color="warning"] {
--dropdown-accent: var(--wrn-color-warning);
--dropdown-soft: var(--wrn-color-warning-soft);
}
.wire-dropdown[data-color="danger"] {
--dropdown-accent: var(--wire-color-danger);
--dropdown-soft: var(--wire-color-danger-soft);
.wrn-dropdown[data-color="danger"] {
--dropdown-accent: var(--wrn-color-danger);
--dropdown-soft: var(--wrn-color-danger-soft);
}
.wire-dropdown__trigger {
.wrn-dropdown__trigger {
appearance: none;
display: inline-flex;
align-items: center;
@@ -298,9 +298,9 @@ items: unknown[] = []
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);
color: var(--wrn-color-text);
background: var(--wrn-color-surface-raised);
border: 1px solid var(--wrn-color-border);
border-radius: 0.78rem;
font: inherit;
font-size: 0.84rem;
@@ -309,47 +309,47 @@ items: unknown[] = []
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 {
.wrn-dropdown__trigger:hover,
.wrn-dropdown__trigger:focus-visible,
.wrn-dropdown[data-open="true"] .wrn-dropdown__trigger {
color: var(--dropdown-accent);
background: var(--dropdown-soft);
border-color: color-mix(in srgb, var(--dropdown-accent) 38%, var(--wire-color-border));
border-color: color-mix(in srgb, var(--dropdown-accent) 38%, var(--wrn-color-border));
outline: none;
}
.wire-dropdown__trigger:disabled {
.wrn-dropdown__trigger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.wire-dropdown__trigger-icon,
.wire-dropdown__chevron {
.wrn-dropdown__trigger-icon,
.wrn-dropdown__chevron {
width: 1rem;
height: 1rem;
}
.wire-dropdown__chevron {
.wrn-dropdown__chevron {
transition: transform 160ms ease;
}
.wire-dropdown[data-open="true"] .wire-dropdown__chevron {
.wrn-dropdown[data-open="true"] .wrn-dropdown__chevron {
transform: rotate(180deg);
}
.wire-dropdown[data-size="sm"] .wire-dropdown__trigger {
.wrn-dropdown[data-size="sm"] .wrn-dropdown__trigger {
min-height: 2.2rem;
padding: 0.5rem 0.72rem;
font-size: 0.78rem;
}
.wire-dropdown[data-size="lg"] .wire-dropdown__trigger {
.wrn-dropdown[data-size="lg"] .wrn-dropdown__trigger {
min-height: 2.9rem;
padding: 0.78rem 1.05rem;
font-size: 0.9rem;
}
.wire-dropdown__dismiss-layer {
.wrn-dropdown__dismiss-layer {
position: fixed;
inset: 0;
z-index: 1100;
@@ -359,7 +359,7 @@ items: unknown[] = []
border: 0;
}
.wire-dropdown__panel {
.wrn-dropdown__panel {
position: absolute;
z-index: 1101;
top: calc(100% + 0.55rem);
@@ -368,11 +368,11 @@ items: unknown[] = []
min-width: 12rem;
max-width: min(22rem, calc(100vw - 1.5rem));
padding: 0.45rem;
color: var(--wire-color-text);
color: var(--wrn-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));
color-mix(in srgb, var(--wrn-color-surface-raised) 97%, transparent);
border: 1px solid color-mix(in srgb, var(--dropdown-accent) 18%, var(--wrn-color-border));
border-radius: 1rem;
box-shadow:
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
@@ -380,73 +380,73 @@ items: unknown[] = []
backdrop-filter: blur(18px);
}
.wire-dropdown[data-width="trigger"] .wire-dropdown__panel {
.wrn-dropdown[data-width="trigger"] .wrn-dropdown__panel {
width: 100%;
min-width: 100%;
}
.wire-dropdown[data-width="sm"] .wire-dropdown__panel {
.wrn-dropdown[data-width="sm"] .wrn-dropdown__panel {
width: 12rem;
}
.wire-dropdown[data-width="md"] .wire-dropdown__panel {
.wrn-dropdown[data-width="md"] .wrn-dropdown__panel {
width: 17rem;
}
.wire-dropdown[data-width="lg"] .wire-dropdown__panel {
.wrn-dropdown[data-width="lg"] .wrn-dropdown__panel {
width: 22rem;
}
.wire-dropdown[data-placement="bottom-end"] .wire-dropdown__panel {
.wrn-dropdown[data-placement="bottom-end"] .wrn-dropdown__panel {
right: 0;
left: auto;
}
.wire-dropdown[data-placement="top-start"] .wire-dropdown__panel {
.wrn-dropdown[data-placement="top-start"] .wrn-dropdown__panel {
top: auto;
bottom: calc(100% + 0.55rem);
}
.wire-dropdown[data-placement="top-end"] .wire-dropdown__panel {
.wrn-dropdown[data-placement="top-end"] .wrn-dropdown__panel {
top: auto;
right: 0;
bottom: calc(100% + 0.55rem);
left: auto;
}
.wire-dropdown[data-variant="soft"] .wire-dropdown__panel {
.wrn-dropdown[data-variant="soft"] .wrn-dropdown__panel {
background:
linear-gradient(145deg, var(--dropdown-soft), transparent 70%),
var(--wire-color-surface-raised);
var(--wrn-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);
.wrn-dropdown[data-variant="outline"] .wrn-dropdown__panel {
background: var(--wrn-color-surface-raised);
box-shadow: none;
}
.wire-dropdown__items {
.wrn-dropdown__items {
display: grid;
gap: 0.15rem;
}
.wire-dropdown__section-label {
.wrn-dropdown__section-label {
padding: 0.55rem 0.72rem 0.3rem;
color: var(--wire-color-text-muted);
color: var(--wrn-color-text-muted);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.wire-dropdown__divider {
.wrn-dropdown__divider {
height: 1px;
margin: 0.3rem 0.4rem;
background: var(--wire-color-border);
background: var(--wrn-color-border);
}
.wire-dropdown__item {
.wrn-dropdown__item {
appearance: none;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
@@ -455,7 +455,7 @@ items: unknown[] = []
width: 100%;
min-width: 0;
padding: 0.68rem 0.72rem;
color: var(--wire-color-text);
color: var(--wrn-color-text);
background: transparent;
border: 0;
border-radius: 0.72rem;
@@ -466,46 +466,46 @@ items: unknown[] = []
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"] {
.wrn-dropdown__item:hover,
.wrn-dropdown__item:focus-visible,
.wrn-dropdown__item[data-selected="true"] {
color: var(--dropdown-accent);
background: var(--dropdown-soft);
outline: none;
}
.wire-dropdown__item:active {
.wrn-dropdown__item:active {
transform: scale(0.985);
}
.wire-dropdown__item[data-danger="true"] {
color: var(--wire-color-danger);
.wrn-dropdown__item[data-danger="true"] {
color: var(--wrn-color-danger);
}
.wire-dropdown__item[data-danger="true"]:hover,
.wire-dropdown__item[data-danger="true"]:focus-visible {
background: var(--wire-color-danger-soft);
.wrn-dropdown__item[data-danger="true"]:hover,
.wrn-dropdown__item[data-danger="true"]:focus-visible {
background: var(--wrn-color-danger-soft);
}
.wire-dropdown__item[disabled],
.wire-dropdown__item[aria-disabled="true"] {
.wrn-dropdown__item[disabled],
.wrn-dropdown__item[aria-disabled="true"] {
opacity: 0.48;
pointer-events: none;
}
.wire-dropdown__item-icon,
.wire-dropdown__status {
.wrn-dropdown__item-icon,
.wrn-dropdown__status {
width: 1rem;
height: 1rem;
}
.wire-dropdown__copy {
.wrn-dropdown__copy {
display: grid;
gap: 0.12rem;
min-width: 0;
}
.wire-dropdown__copy strong {
.wrn-dropdown__copy strong {
overflow: hidden;
font-size: 0.82rem;
font-weight: 600;
@@ -514,16 +514,16 @@ items: unknown[] = []
white-space: nowrap;
}
.wire-dropdown__copy small {
.wrn-dropdown__copy small {
overflow: hidden;
color: var(--wire-color-text-muted);
color: var(--wrn-color-text-muted);
font-size: 0.7rem;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.wire-dropdown__badge {
.wrn-dropdown__badge {
padding: 0.18rem 0.45rem;
color: var(--dropdown-accent);
background: var(--dropdown-soft);
@@ -532,25 +532,25 @@ items: unknown[] = []
font-weight: 700;
}
.wire-dropdown kbd {
.wrn-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);
color: var(--wrn-color-text-muted);
background: var(--wrn-color-surface-soft);
border: 1px solid var(--wrn-color-border);
border-radius: 0.36rem;
font-size: 0.64rem;
font-family: inherit;
}
.wire-dropdown__empty {
.wrn-dropdown__empty {
padding: 1rem;
color: var(--wire-color-text-muted);
color: var(--wrn-color-text-muted);
font-size: 0.8rem;
text-align: center;
}
@media (max-width: 639px) {
.wire-dropdown__panel {
.wrn-dropdown__panel {
position: fixed;
right: 0.75rem;
bottom: 0.75rem;
@@ -563,9 +563,9 @@ items: unknown[] = []
}
@media (prefers-reduced-motion: reduce) {
.wire-dropdown__trigger,
.wire-dropdown__chevron,
.wire-dropdown__item {
.wrn-dropdown__trigger,
.wrn-dropdown__chevron,
.wrn-dropdown__item {
transition: none;
}
}
+581 -4
View File
@@ -46,10 +46,587 @@ size: string = "default"
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 {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--file-input {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wrn-next__field-heading"><label class="{hiddenLabel ? 'wrn-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wrn-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wrn-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="wrn-next__field-help">{helperText}</small>{/if}<small class="wrn-next__field-error" data-error="{name}">{error}</small>
</div>
}
style {
.wrn-next__file-control {
position: relative;
min-height: 2.75rem;
padding: 0.65rem 0.85rem;
border: 1px dashed var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
}
.wrn-next__file-control input {
position: absolute;
cursor: pointer;
opacity: 0;
inset: 0;
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
/* Shared field foundation. */
/* Validation states set by the browser validation runtime. */
.wrn-invalid {
border-color: var(--wrn-color-danger) !important;
}
.wrn-field-error {
display: block;
min-height: 1em;
margin-top: 0.25rem;
color: var(--wrn-color-danger);
font-size: 0.8rem;
}
.wrn-next--choice {
display: inline-flex;
align-items: center;
gap: 0.55rem;
cursor: pointer;
}
.wrn-next--choice input {
width: 1.1rem;
height: 1.1rem;
accent-color: var(--wrn-color-primary);
}
.wrn-next--field {
display: grid;
width: min(100%, 28rem);
gap: 0.4rem;
font-size: 0.78rem;
font-weight: 700;
}
.wrn-next--field :where(input, textarea, select) {
width: 100%;
min-height: 2.65rem;
padding: 0.6rem 0.7rem;
border: 1px solid var(--wrn-color-border);
border-radius: 0.6rem;
outline: 0;
color: var(--wrn-color-text);
background: var(--wrn-color-bg);
font: inherit;
font-weight: 500;
}
.wrn-auth-form form > .wrn-next--field,
.wrn-auth-form form > .wrn-field {
width: 100%;
max-width: none;
}
.wrn-auth-form__submit > .wrn-btn {
width: 100%;
min-height: 3rem;
border-radius: 0.75rem;
box-shadow: 0 0.8rem 1.8rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
}
.wrn-next--field :where(input, textarea, select):focus-visible {
border-color: var(--wrn-color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 16%, transparent);
}
.wrn-next--field textarea {
resize: vertical;
}
.wrn-next--field,
.wrn-next--choice-field {
--wrn-field-color: var(--wrn-component-color, var(--wrn-color-primary));
width: min(100%, 34rem);
min-width: 0;
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 500;
}
.wrn-next--field.wrn-next--size-sm,
.wrn-next--choice-field.wrn-next--size-sm {
font-size: 0.8125rem;
}
.wrn-next--field.wrn-next--size-lg,
.wrn-next--choice-field.wrn-next--size-lg {
font-size: 1rem;
}
.wrn-next__field-heading {
display: flex;
min-width: 0;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-weight: 600;
}
.wrn-next__field-hint,
.wrn-next__field-help {
color: var(--wrn-color-muted);
font-size: 0.82em;
font-weight: 500;
}
.wrn-next__field-control {
position: relative;
min-width: 0;
}
.wrn-next__field-control > :is(input, textarea, select) {
padding-inline: 0.85rem;
border-color: var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
font-size: 0.8125rem;
}
.wrn-next__field-control[data-icon-position="start"] > :is(input, textarea, select) {
padding-inline-start: 2.5rem;
}
.wrn-next__field-control[data-icon-position="end"] > :is(input, textarea, select) {
padding-inline-end: 2.5rem;
}
.wrn-next__field-icon {
position: absolute;
z-index: 1;
top: 50%;
left: 0.85rem;
color: var(--wrn-color-muted);
pointer-events: none;
transform: translateY(-50%);
}
.wrn-next__field-control[data-icon-position="end"] > .wrn-next__field-icon {
right: 0.85rem;
left: auto;
}
.wrn-next--field[data-variant="outlined"]
.wrn-next__field-control
> :is(input, textarea, select) {
border-width: 2px;
}
.wrn-next--field[data-variant="underline"]
.wrn-next__field-control
> :is(input, textarea, select) {
padding-inline: 0;
border-width: 0 0 1px;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.wrn-next--field[data-variant="pill"] .wrn-next__field-control > :is(input, select) {
border-radius: 999px;
padding-inline: 1.15rem;
}
.wrn-next--field[data-variant="ghost"] .wrn-next__field-control > :is(input, textarea, select) {
border-color: transparent;
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
}
.wrn-next__field-floating-label {
position: absolute;
top: 50%;
left: 0.85rem;
color: var(--wrn-color-muted);
pointer-events: none;
transform: translateY(-50%);
transition: 150ms ease;
}
.wrn-next--textarea .wrn-next__field-floating-label {
top: 1.35rem;
}
.wrn-next--field[data-floating="true"] .wrn-next__field-control > :is(input, textarea, select) {
padding-top: 1.2rem;
padding-bottom: 0.3rem;
}
.wrn-next--field[data-floating="true"] .wrn-next__field-heading > label {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
margin: -1px !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
}
.wrn-next--field[data-floating="true"]
.wrn-next__field-heading:not(:has(.wrn-next__field-hint)) {
display: none;
}
.wrn-next--field[data-floating="true"]
.wrn-next__field-control
> :is(input, textarea):is(:focus, :not(:placeholder-shown))
+ .wrn-next__field-floating-label {
top: 0.42rem;
font-size: 0.7em;
transform: none;
}
.wrn-next__field-error {
min-height: 1em;
color: var(--wrn-color-danger);
font-size: 0.82em;
font-weight: 600;
}
.wrn-next__field-error:empty {
display: none;
}
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
.wrn-next--choice-field[data-invalid="true"] input {
border-color: var(--wrn-color-danger);
}
.wrn-next--field :is(input, textarea, select):disabled,
.wrn-next--choice-field input:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.wrn-next--field :is(input, textarea, select):read-only {
background: var(--wrn-color-surface-2);
}
.wrn-next--field[data-inline="true"],
.wrn-next--choice-field[data-inline="true"] {
display: grid;
width: 100%;
max-width: none;
grid-template-columns: minmax(7rem, max-content) minmax(12rem, 20rem) minmax(10rem, 1fr);
justify-content: center;
align-items: center;
gap: 0.75rem;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-heading,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading {
min-height: 2.65rem;
align-items: center;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-heading {
justify-content: flex-end;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-help,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-help {
display: flex;
min-height: 2.65rem;
align-items: center;
}
.wrn-next--choice-field[data-inline="true"] {
grid-template-columns: minmax(12rem, 20rem) minmax(10rem, 1fr);
}
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading:empty {
display: none;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
grid-column: 2 / -1;
}
.wrn-next__sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
border: 0 !important;
margin: -1px !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
}
.wrn-next__choice-field {
display: grid;
width: 100%;
max-width: none;
gap: 0.4rem;
}
.wrn-next__choice {
display: flex;
min-width: 0;
align-items: center;
gap: 0.65rem;
color: var(--wrn-color-text);
width: 100%;
font-size: 0.75rem;
font-weight: 500;
}
.wrn-next__checkbox-content > .wrn-next__choice {
width: 100%;
min-width: 0;
align-items: flex-start;
}
.wrn-next__choice input {
flex: 0 0 auto;
margin: 0;
accent-color: var(--wrn-field-color);
}
.wrn-next__choice-copy {
display: grid;
min-width: 0;
gap: 0.15rem;
line-height: 1.35;
}
.wrn-next__choice-copy small {
color: var(--wrn-color-muted);
font-weight: 500;
}
.wrn-next__radio-group,
.wrn-next__checkbox-group {
display: grid;
min-width: 0;
gap: 0.65rem;
}
.wrn-next--radio-field[data-orientation="horizontal"] .wrn-next__radio-group,
.wrn-next--checkbox-field[data-orientation="horizontal"] .wrn-next__checkbox-group {
grid-template-columns: repeat(auto-fit, minmax(8rem, max-content));
align-items: stretch;
}
.wrn-next--radio-field[data-card="true"] .wrn-next__radio-group,
.wrn-next--checkbox-field[data-card="true"] .wrn-next__checkbox-group {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
}
.wrn-next--radio-field[data-card="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox {
min-height: 4.5rem;
padding: 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
}
.wrn-next--radio-field[data-card="true"] .wrn-next--radio:has(input:checked),
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox:has(input:checked) {
border-color: var(--wrn-field-color);
box-shadow: 0 0 0 1px var(--wrn-field-color);
}
.wrn-next--radio-field[data-list="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-list="true"] .wrn-next--checkbox {
padding-block: 0.75rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox {
justify-content: space-between;
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio input,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox input {
order: 2;
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next__choice-copy,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next__choice-copy {
order: 1;
}
.wrn-next__choice[data-variant="outlined"],
.wrn-next__choice[data-variant="floating"] {
padding: 0.65rem 0.8rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
}
.wrn-next__choice[data-variant="pill"] {
padding: 0.65rem 1rem;
border-radius: 999px;
background: color-mix(in srgb, var(--wrn-field-color) 9%, var(--wrn-color-surface));
}
.wrn-next__choice[data-variant="ghost"] {
padding: 0.65rem 0.8rem;
border-radius: var(--wrn-radius-sm);
background: color-mix(in srgb, var(--wrn-field-color) 7%, transparent);
}
.wrn-next__choice[data-variant="underline"] {
padding-block: 0.5rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-next--select .wrn-next__field-control {
display: flex;
align-items: center;
}
.wrn-next__color-control,
.wrn-next__range-control,
.wrn-next__input-group-control,
.wrn-next__file-control {
display: flex;
min-width: 0;
align-items: center;
gap: 0.65rem;
}
.wrn-next__picker-control,
.wrn-next--time-picker .wrn-next__field-control {
position: relative;
display: flex;
min-width: 0;
align-items: center;
}
.wrn-next__picker-control > button:first-of-type,
.wrn-next__time-trigger {
display: flex;
width: 100%;
min-height: 2.65rem;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.7rem 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
font: inherit;
text-align: left;
}
.wrn-next__calendar,
.wrn-next__time-panel {
z-index: 20;
display: grid;
width: min(100%, 22rem);
gap: 0.75rem;
padding: 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-3);
}
.wrn-next--date-picker[data-expanded="false"] .wrn-next__calendar,
.wrn-next--time-picker[data-expanded="false"] .wrn-next__time-panel {
display: none;
}
.wrn-next__calendar button,
.wrn-next__time-panel button {
min-height: 2.25rem;
border: 0;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: inherit;
}
.wrn-next__calendar-weekdays,
.wrn-next__calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.2rem;
text-align: center;
}
.wrn-next__calendar-weekdays {
color: var(--wrn-color-muted);
font-size: 0.75rem;
}
.wrn-next__calendar-grid button:hover,
.wrn-next__time-panel button:hover {
background: var(--wrn-color-surface-2);
}
.wrn-next__calendar-grid button[aria-pressed="true"],
.wrn-next__time-panel button[aria-pressed="true"] {
background: var(--wrn-field-color);
color: var(--wrn-color-primary-contrast, #fff);
}
.wrn-next__time-period {
display: flex;
gap: 0.35rem;
}
.wrn-next--file-upload-progress .wrn-next__row,
.wrn-next__upload-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.wrn-next--file-upload-progress .wrn-next__row > span {
display: grid;
min-width: 0;
}
.wrn-next--field[data-variant="underline"]
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
border-width: 0 0 1px;
border-radius: 0;
background: transparent;
}
.wrn-next--field[data-variant="outlined"]
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
border-width: 2px;
}
.wrn-next--field[data-variant="pill"]
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
border-radius: 999px;
}
.wrn-next--field[data-variant="ghost"]
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
border-color: transparent;
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
}
@media (max-width: 640px) {
.wrn-next--field[data-inline="true"],
.wrn-next--choice-field[data-inline="true"] {
grid-template-columns: 1fr;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
grid-column: auto;
}
}
.wrn-next--toggle-password:has(input.wrn-invalid) .wrn-next__password-control > input,
.wrn-next--toggle-password.wrn-next--invalid .wrn-next__password-control > input {
border-color: var(--wrn-color-danger);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 14%, transparent);
}
.wrn-next--strong-password.wrn-next--invalid input,
.wrn-next--strong-password input.wrn-invalid {
border-color: var(--wrn-color-danger);
}
}
}
-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>
}
}
+610 -4
View File
@@ -30,11 +30,617 @@ size: string = "default"
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>
<div {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--file-upload-progress {class}" data-status="{currentStatus}" aria-live="polite">
<div class="wrn-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 class="wrn-next__upload-status"><span class="wrn-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="wrn-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>
}
style {
.wrn-next__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.wrn-next--file-upload-progress progress {
width: 100%;
accent-color: var(--wrn-color-primary);
}
.wrn-next--file-upload-progress {
display: grid;
width: min(100%, 36rem);
gap: 0.75rem;
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-1);
}
.wrn-next--file-upload-progress progress {
width: 100%;
height: 0.55rem;
overflow: hidden;
border: 0;
border-radius: 999px;
accent-color: var(--wrn-component-color);
}
.wrn-next__upload-actions {
justify-content: flex-end;
}
.wrn-next__upload-actions button {
min-height: 2.25rem;
padding-inline: 0.8rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface-2);
color: inherit;
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
/* Shared field foundation. */
/* Validation states set by the browser validation runtime. */
.wrn-invalid {
border-color: var(--wrn-color-danger) !important;
}
.wrn-field-error {
display: block;
min-height: 1em;
margin-top: 0.25rem;
color: var(--wrn-color-danger);
font-size: 0.8rem;
}
.wrn-next--choice {
display: inline-flex;
align-items: center;
gap: 0.55rem;
cursor: pointer;
}
.wrn-next--choice input {
width: 1.1rem;
height: 1.1rem;
accent-color: var(--wrn-color-primary);
}
.wrn-next--field {
display: grid;
width: min(100%, 28rem);
gap: 0.4rem;
font-size: 0.78rem;
font-weight: 700;
}
.wrn-next--field :where(input, textarea, select) {
width: 100%;
min-height: 2.65rem;
padding: 0.6rem 0.7rem;
border: 1px solid var(--wrn-color-border);
border-radius: 0.6rem;
outline: 0;
color: var(--wrn-color-text);
background: var(--wrn-color-bg);
font: inherit;
font-weight: 500;
}
.wrn-auth-form form > .wrn-next--field,
.wrn-auth-form form > .wrn-field {
width: 100%;
max-width: none;
}
.wrn-auth-form__submit > .wrn-btn {
width: 100%;
min-height: 3rem;
border-radius: 0.75rem;
box-shadow: 0 0.8rem 1.8rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
}
.wrn-next--field :where(input, textarea, select):focus-visible {
border-color: var(--wrn-color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 16%, transparent);
}
.wrn-next--field textarea {
resize: vertical;
}
.wrn-next--field,
.wrn-next--choice-field {
--wrn-field-color: var(--wrn-component-color, var(--wrn-color-primary));
width: min(100%, 34rem);
min-width: 0;
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 500;
}
.wrn-next--field.wrn-next--size-sm,
.wrn-next--choice-field.wrn-next--size-sm {
font-size: 0.8125rem;
}
.wrn-next--field.wrn-next--size-lg,
.wrn-next--choice-field.wrn-next--size-lg {
font-size: 1rem;
}
.wrn-next__field-heading {
display: flex;
min-width: 0;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-weight: 600;
}
.wrn-next__field-hint,
.wrn-next__field-help {
color: var(--wrn-color-muted);
font-size: 0.82em;
font-weight: 500;
}
.wrn-next__field-control {
position: relative;
min-width: 0;
}
.wrn-next__field-control > :is(input, textarea, select) {
padding-inline: 0.85rem;
border-color: var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
font-size: 0.8125rem;
}
.wrn-next__field-control[data-icon-position="start"] > :is(input, textarea, select) {
padding-inline-start: 2.5rem;
}
.wrn-next__field-control[data-icon-position="end"] > :is(input, textarea, select) {
padding-inline-end: 2.5rem;
}
.wrn-next__field-icon {
position: absolute;
z-index: 1;
top: 50%;
left: 0.85rem;
color: var(--wrn-color-muted);
pointer-events: none;
transform: translateY(-50%);
}
.wrn-next__field-control[data-icon-position="end"] > .wrn-next__field-icon {
right: 0.85rem;
left: auto;
}
.wrn-next--field[data-variant="outlined"]
.wrn-next__field-control
> :is(input, textarea, select) {
border-width: 2px;
}
.wrn-next--field[data-variant="underline"]
.wrn-next__field-control
> :is(input, textarea, select) {
padding-inline: 0;
border-width: 0 0 1px;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.wrn-next--field[data-variant="pill"] .wrn-next__field-control > :is(input, select) {
border-radius: 999px;
padding-inline: 1.15rem;
}
.wrn-next--field[data-variant="ghost"] .wrn-next__field-control > :is(input, textarea, select) {
border-color: transparent;
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
}
.wrn-next__field-floating-label {
position: absolute;
top: 50%;
left: 0.85rem;
color: var(--wrn-color-muted);
pointer-events: none;
transform: translateY(-50%);
transition: 150ms ease;
}
.wrn-next--textarea .wrn-next__field-floating-label {
top: 1.35rem;
}
.wrn-next--field[data-floating="true"] .wrn-next__field-control > :is(input, textarea, select) {
padding-top: 1.2rem;
padding-bottom: 0.3rem;
}
.wrn-next--field[data-floating="true"] .wrn-next__field-heading > label {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
margin: -1px !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
}
.wrn-next--field[data-floating="true"]
.wrn-next__field-heading:not(:has(.wrn-next__field-hint)) {
display: none;
}
.wrn-next--field[data-floating="true"]
.wrn-next__field-control
> :is(input, textarea):is(:focus, :not(:placeholder-shown))
+ .wrn-next__field-floating-label {
top: 0.42rem;
font-size: 0.7em;
transform: none;
}
.wrn-next__field-error {
min-height: 1em;
color: var(--wrn-color-danger);
font-size: 0.82em;
font-weight: 600;
}
.wrn-next__field-error:empty {
display: none;
}
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
.wrn-next--choice-field[data-invalid="true"] input {
border-color: var(--wrn-color-danger);
}
.wrn-next--field :is(input, textarea, select):disabled,
.wrn-next--choice-field input:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.wrn-next--field :is(input, textarea, select):read-only {
background: var(--wrn-color-surface-2);
}
.wrn-next--field[data-inline="true"],
.wrn-next--choice-field[data-inline="true"] {
display: grid;
width: 100%;
max-width: none;
grid-template-columns: minmax(7rem, max-content) minmax(12rem, 20rem) minmax(10rem, 1fr);
justify-content: center;
align-items: center;
gap: 0.75rem;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-heading,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading {
min-height: 2.65rem;
align-items: center;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-heading {
justify-content: flex-end;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-help,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-help {
display: flex;
min-height: 2.65rem;
align-items: center;
}
.wrn-next--choice-field[data-inline="true"] {
grid-template-columns: minmax(12rem, 20rem) minmax(10rem, 1fr);
}
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading:empty {
display: none;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
grid-column: 2 / -1;
}
.wrn-next__sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
border: 0 !important;
margin: -1px !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
}
.wrn-next__choice-field {
display: grid;
width: 100%;
max-width: none;
gap: 0.4rem;
}
.wrn-next__choice {
display: flex;
min-width: 0;
align-items: center;
gap: 0.65rem;
color: var(--wrn-color-text);
width: 100%;
font-size: 0.75rem;
font-weight: 500;
}
.wrn-next__checkbox-content > .wrn-next__choice {
width: 100%;
min-width: 0;
align-items: flex-start;
}
.wrn-next__choice input {
flex: 0 0 auto;
margin: 0;
accent-color: var(--wrn-field-color);
}
.wrn-next__choice-copy {
display: grid;
min-width: 0;
gap: 0.15rem;
line-height: 1.35;
}
.wrn-next__choice-copy small {
color: var(--wrn-color-muted);
font-weight: 500;
}
.wrn-next__radio-group,
.wrn-next__checkbox-group {
display: grid;
min-width: 0;
gap: 0.65rem;
}
.wrn-next--radio-field[data-orientation="horizontal"] .wrn-next__radio-group,
.wrn-next--checkbox-field[data-orientation="horizontal"] .wrn-next__checkbox-group {
grid-template-columns: repeat(auto-fit, minmax(8rem, max-content));
align-items: stretch;
}
.wrn-next--radio-field[data-card="true"] .wrn-next__radio-group,
.wrn-next--checkbox-field[data-card="true"] .wrn-next__checkbox-group {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
}
.wrn-next--radio-field[data-card="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox {
min-height: 4.5rem;
padding: 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
}
.wrn-next--radio-field[data-card="true"] .wrn-next--radio:has(input:checked),
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox:has(input:checked) {
border-color: var(--wrn-field-color);
box-shadow: 0 0 0 1px var(--wrn-field-color);
}
.wrn-next--radio-field[data-list="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-list="true"] .wrn-next--checkbox {
padding-block: 0.75rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox {
justify-content: space-between;
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio input,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox input {
order: 2;
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next__choice-copy,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next__choice-copy {
order: 1;
}
.wrn-next__choice[data-variant="outlined"],
.wrn-next__choice[data-variant="floating"] {
padding: 0.65rem 0.8rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
}
.wrn-next__choice[data-variant="pill"] {
padding: 0.65rem 1rem;
border-radius: 999px;
background: color-mix(in srgb, var(--wrn-field-color) 9%, var(--wrn-color-surface));
}
.wrn-next__choice[data-variant="ghost"] {
padding: 0.65rem 0.8rem;
border-radius: var(--wrn-radius-sm);
background: color-mix(in srgb, var(--wrn-field-color) 7%, transparent);
}
.wrn-next__choice[data-variant="underline"] {
padding-block: 0.5rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-next--select .wrn-next__field-control {
display: flex;
align-items: center;
}
.wrn-next__color-control,
.wrn-next__range-control,
.wrn-next__input-group-control,
.wrn-next__file-control {
display: flex;
min-width: 0;
align-items: center;
gap: 0.65rem;
}
.wrn-next__picker-control,
.wrn-next--time-picker .wrn-next__field-control {
position: relative;
display: flex;
min-width: 0;
align-items: center;
}
.wrn-next__picker-control > button:first-of-type,
.wrn-next__time-trigger {
display: flex;
width: 100%;
min-height: 2.65rem;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.7rem 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
font: inherit;
text-align: left;
}
.wrn-next__calendar,
.wrn-next__time-panel {
z-index: 20;
display: grid;
width: min(100%, 22rem);
gap: 0.75rem;
padding: 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-3);
}
.wrn-next--date-picker[data-expanded="false"] .wrn-next__calendar,
.wrn-next--time-picker[data-expanded="false"] .wrn-next__time-panel {
display: none;
}
.wrn-next__calendar button,
.wrn-next__time-panel button {
min-height: 2.25rem;
border: 0;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: inherit;
}
.wrn-next__calendar-weekdays,
.wrn-next__calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.2rem;
text-align: center;
}
.wrn-next__calendar-weekdays {
color: var(--wrn-color-muted);
font-size: 0.75rem;
}
.wrn-next__calendar-grid button:hover,
.wrn-next__time-panel button:hover {
background: var(--wrn-color-surface-2);
}
.wrn-next__calendar-grid button[aria-pressed="true"],
.wrn-next__time-panel button[aria-pressed="true"] {
background: var(--wrn-field-color);
color: var(--wrn-color-primary-contrast, #fff);
}
.wrn-next__time-period {
display: flex;
gap: 0.35rem;
}
.wrn-next--file-upload-progress .wrn-next__row,
.wrn-next__upload-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.wrn-next--file-upload-progress .wrn-next__row > span {
display: grid;
min-width: 0;
}
.wrn-next--field[data-variant="underline"]
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
border-width: 0 0 1px;
border-radius: 0;
background: transparent;
}
.wrn-next--field[data-variant="outlined"]
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
border-width: 2px;
}
.wrn-next--field[data-variant="pill"]
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
border-radius: 999px;
}
.wrn-next--field[data-variant="ghost"]
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
border-color: transparent;
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
}
@media (max-width: 640px) {
.wrn-next--field[data-inline="true"],
.wrn-next--choice-field[data-inline="true"] {
grid-template-columns: 1fr;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
grid-column: auto;
}
}
.wrn-next--toggle-password:has(input.wrn-invalid) .wrn-next__password-control > input,
.wrn-next--toggle-password.wrn-next--invalid .wrn-next__password-control > input {
border-color: var(--wrn-color-danger);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 14%, transparent);
}
.wrn-next--strong-password.wrn-next--invalid input,
.wrn-next--strong-password input.wrn-invalid {
border-color: var(--wrn-color-danger);
}
}
}
+105 -25
View File
@@ -1,3 +1,13 @@
// Image -- a framed image with a reserved aspect ratio.
//
// <Image src="/hero.jpg" alt="Dashboard" size="video" rounded={true} />
//
// The frame keeps its ratio whether or not the image has loaded, so the page
// does not jump when it arrives. Without a src it renders a labelled
// placeholder of the same shape rather than collapsing.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Image {
props {
size: string = "default"
@@ -7,41 +17,111 @@ component Image {
width: string = ""
height: string = ""
loading: string = "lazy"
fit: string = "cover"
rounded: boolean = false
class: string = ""
}
view {
<figure
{...attrs}
data-ui-component="Image"
class='relative m-0 overflow-hidden bg-[var(--wire-color-surface-soft)] {class}'
class:rounded-2xl='rounded'
class='wrn-image {class}'
data-size='{size}'
data-color='{color}'
data-fit='{fit}'
data-rounded='{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}
<img
class="wrn-image__img"
data-show="src"
src='{src}'
alt='{alt}'
width='{width}'
height='{height}'
loading='{loading}'
decoding="async"
/>
<div
class="wrn-image__placeholder"
data-show="!src"
role="img"
aria-label='{alt || "Image placeholder"}'
>
<span class="icon-[lucide--image] wrn-image__placeholder-icon" aria-hidden="true"></span>
</div>
<slot></slot>
</figure>
}
style {
.wrn-image {
position: relative;
margin: 0;
overflow: hidden;
width: 100%;
min-width: 0;
background: var(--wrn-color-surface-soft);
}
.wrn-image[data-rounded="true"] {
border-radius: var(--wrn-radius-lg);
}
/*
* The ratio sits on the frame rather than the image, so the space is
* reserved before the file arrives and the page does not jump.
*/
.wrn-image[data-size="square"] {
aspect-ratio: 1 / 1;
}
.wrn-image[data-size="video"] {
aspect-ratio: 16 / 9;
}
.wrn-image[data-size="landscape"] {
aspect-ratio: 4 / 3;
}
.wrn-image[data-size="portrait"] {
aspect-ratio: 3 / 4;
}
.wrn-image__img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.wrn-image[data-fit="contain"] .wrn-image__img {
object-fit: contain;
}
.wrn-image[data-fit="fill"] .wrn-image__img {
object-fit: fill;
}
.wrn-image[data-size="default"] .wrn-image__img {
height: auto;
}
.wrn-image__placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 12rem;
height: 100%;
color: var(--wrn-color-text-muted);
}
.wrn-image__placeholder-icon {
width: 2rem;
height: 2rem;
}
}
}
+598 -7
View File
@@ -39,16 +39,607 @@ component InputGroup {
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}
<div {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--input-group {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
<div class="wrn-next__field-heading"><label class="{hiddenLabel ? 'wrn-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wrn-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wrn-next__input-group-control">
{#if startText}<span class="wrn-next__input-addon">{startText}</span>{/if}
{#if icon}<span class="{icon} wrn-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 endText}<span class="wrn-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>
{#if helperText}<small class="wrn-next__field-help">{helperText}</small>{/if}<small class="wrn-next__field-error" data-error="{name}">{error}</small>
</div>
}
style {
.wrn-next__input-group-control {
overflow: hidden;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
}
.wrn-next__input-group-control input {
flex: 1;
min-width: 0;
border: 0;
box-shadow: none;
}
.wrn-next__input-addon,
.wrn-next__input-group-control button {
padding: 0.7rem 0.85rem;
white-space: nowrap;
}
.wrn-next__input-group-control button {
align-self: stretch;
border: 0;
background: var(--wrn-field-color);
color: var(--wrn-color-primary-contrast, #fff);
font: inherit;
font-weight: 700;
cursor: pointer;
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
/* Shared field foundation. */
/* Validation states set by the browser validation runtime. */
.wrn-invalid {
border-color: var(--wrn-color-danger) !important;
}
.wrn-field-error {
display: block;
min-height: 1em;
margin-top: 0.25rem;
color: var(--wrn-color-danger);
font-size: 0.8rem;
}
.wrn-next--choice {
display: inline-flex;
align-items: center;
gap: 0.55rem;
cursor: pointer;
}
.wrn-next--choice input {
width: 1.1rem;
height: 1.1rem;
accent-color: var(--wrn-color-primary);
}
.wrn-next--field {
display: grid;
width: min(100%, 28rem);
gap: 0.4rem;
font-size: 0.78rem;
font-weight: 700;
}
.wrn-next--field :where(input, textarea, select) {
width: 100%;
min-height: 2.65rem;
padding: 0.6rem 0.7rem;
border: 1px solid var(--wrn-color-border);
border-radius: 0.6rem;
outline: 0;
color: var(--wrn-color-text);
background: var(--wrn-color-bg);
font: inherit;
font-weight: 500;
}
.wrn-auth-form form > .wrn-next--field,
.wrn-auth-form form > .wrn-field {
width: 100%;
max-width: none;
}
.wrn-auth-form__submit > .wrn-btn {
width: 100%;
min-height: 3rem;
border-radius: 0.75rem;
box-shadow: 0 0.8rem 1.8rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
}
.wrn-next--field :where(input, textarea, select):focus-visible {
border-color: var(--wrn-color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 16%, transparent);
}
.wrn-next--field textarea {
resize: vertical;
}
.wrn-next--field,
.wrn-next--choice-field {
--wrn-field-color: var(--wrn-component-color, var(--wrn-color-primary));
width: min(100%, 34rem);
min-width: 0;
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 500;
}
.wrn-next--field.wrn-next--size-sm,
.wrn-next--choice-field.wrn-next--size-sm {
font-size: 0.8125rem;
}
.wrn-next--field.wrn-next--size-lg,
.wrn-next--choice-field.wrn-next--size-lg {
font-size: 1rem;
}
.wrn-next__field-heading {
display: flex;
min-width: 0;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-weight: 600;
}
.wrn-next__field-hint,
.wrn-next__field-help {
color: var(--wrn-color-muted);
font-size: 0.82em;
font-weight: 500;
}
.wrn-next__field-control {
position: relative;
min-width: 0;
}
.wrn-next__field-control > :is(input, textarea, select) {
padding-inline: 0.85rem;
border-color: var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
font-size: 0.8125rem;
}
.wrn-next__field-control[data-icon-position="start"] > :is(input, textarea, select) {
padding-inline-start: 2.5rem;
}
.wrn-next__field-control[data-icon-position="end"] > :is(input, textarea, select) {
padding-inline-end: 2.5rem;
}
.wrn-next__field-icon {
position: absolute;
z-index: 1;
top: 50%;
left: 0.85rem;
color: var(--wrn-color-muted);
pointer-events: none;
transform: translateY(-50%);
}
.wrn-next__field-control[data-icon-position="end"] > .wrn-next__field-icon {
right: 0.85rem;
left: auto;
}
.wrn-next--field[data-variant="outlined"]
.wrn-next__field-control
> :is(input, textarea, select) {
border-width: 2px;
}
.wrn-next--field[data-variant="underline"]
.wrn-next__field-control
> :is(input, textarea, select) {
padding-inline: 0;
border-width: 0 0 1px;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.wrn-next--field[data-variant="pill"] .wrn-next__field-control > :is(input, select) {
border-radius: 999px;
padding-inline: 1.15rem;
}
.wrn-next--field[data-variant="ghost"] .wrn-next__field-control > :is(input, textarea, select) {
border-color: transparent;
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
}
.wrn-next__field-floating-label {
position: absolute;
top: 50%;
left: 0.85rem;
color: var(--wrn-color-muted);
pointer-events: none;
transform: translateY(-50%);
transition: 150ms ease;
}
.wrn-next--textarea .wrn-next__field-floating-label {
top: 1.35rem;
}
.wrn-next--field[data-floating="true"] .wrn-next__field-control > :is(input, textarea, select) {
padding-top: 1.2rem;
padding-bottom: 0.3rem;
}
.wrn-next--field[data-floating="true"] .wrn-next__field-heading > label {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
margin: -1px !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
}
.wrn-next--field[data-floating="true"]
.wrn-next__field-heading:not(:has(.wrn-next__field-hint)) {
display: none;
}
.wrn-next--field[data-floating="true"]
.wrn-next__field-control
> :is(input, textarea):is(:focus, :not(:placeholder-shown))
+ .wrn-next__field-floating-label {
top: 0.42rem;
font-size: 0.7em;
transform: none;
}
.wrn-next__field-error {
min-height: 1em;
color: var(--wrn-color-danger);
font-size: 0.82em;
font-weight: 600;
}
.wrn-next__field-error:empty {
display: none;
}
.wrn-next--field[data-invalid="true"] :is(input, textarea, select),
.wrn-next--choice-field[data-invalid="true"] input {
border-color: var(--wrn-color-danger);
}
.wrn-next--field :is(input, textarea, select):disabled,
.wrn-next--choice-field input:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.wrn-next--field :is(input, textarea, select):read-only {
background: var(--wrn-color-surface-2);
}
.wrn-next--field[data-inline="true"],
.wrn-next--choice-field[data-inline="true"] {
display: grid;
width: 100%;
max-width: none;
grid-template-columns: minmax(7rem, max-content) minmax(12rem, 20rem) minmax(10rem, 1fr);
justify-content: center;
align-items: center;
gap: 0.75rem;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-heading,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading {
min-height: 2.65rem;
align-items: center;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-heading {
justify-content: flex-end;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-help,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-help {
display: flex;
min-height: 2.65rem;
align-items: center;
}
.wrn-next--choice-field[data-inline="true"] {
grid-template-columns: minmax(12rem, 20rem) minmax(10rem, 1fr);
}
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-heading:empty {
display: none;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
grid-column: 2 / -1;
}
.wrn-next__sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
border: 0 !important;
margin: -1px !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
}
.wrn-next__choice-field {
display: grid;
width: 100%;
max-width: none;
gap: 0.4rem;
}
.wrn-next__choice {
display: flex;
min-width: 0;
align-items: center;
gap: 0.65rem;
color: var(--wrn-color-text);
width: 100%;
font-size: 0.75rem;
font-weight: 500;
}
.wrn-next__checkbox-content > .wrn-next__choice {
width: 100%;
min-width: 0;
align-items: flex-start;
}
.wrn-next__choice input {
flex: 0 0 auto;
margin: 0;
accent-color: var(--wrn-field-color);
}
.wrn-next__choice-copy {
display: grid;
min-width: 0;
gap: 0.15rem;
line-height: 1.35;
}
.wrn-next__choice-copy small {
color: var(--wrn-color-muted);
font-weight: 500;
}
.wrn-next__radio-group,
.wrn-next__checkbox-group {
display: grid;
min-width: 0;
gap: 0.65rem;
}
.wrn-next--radio-field[data-orientation="horizontal"] .wrn-next__radio-group,
.wrn-next--checkbox-field[data-orientation="horizontal"] .wrn-next__checkbox-group {
grid-template-columns: repeat(auto-fit, minmax(8rem, max-content));
align-items: stretch;
}
.wrn-next--radio-field[data-card="true"] .wrn-next__radio-group,
.wrn-next--checkbox-field[data-card="true"] .wrn-next__checkbox-group {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
}
.wrn-next--radio-field[data-card="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox {
min-height: 4.5rem;
padding: 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
}
.wrn-next--radio-field[data-card="true"] .wrn-next--radio:has(input:checked),
.wrn-next--checkbox-field[data-card="true"] .wrn-next--checkbox:has(input:checked) {
border-color: var(--wrn-field-color);
box-shadow: 0 0 0 1px var(--wrn-field-color);
}
.wrn-next--radio-field[data-list="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-list="true"] .wrn-next--checkbox {
padding-block: 0.75rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox {
justify-content: space-between;
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next--radio input,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next--checkbox input {
order: 2;
}
.wrn-next--radio-field[data-right-aligned="true"] .wrn-next__choice-copy,
.wrn-next--checkbox-field[data-right-aligned="true"] .wrn-next__choice-copy {
order: 1;
}
.wrn-next__choice[data-variant="outlined"],
.wrn-next__choice[data-variant="floating"] {
padding: 0.65rem 0.8rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
}
.wrn-next__choice[data-variant="pill"] {
padding: 0.65rem 1rem;
border-radius: 999px;
background: color-mix(in srgb, var(--wrn-field-color) 9%, var(--wrn-color-surface));
}
.wrn-next__choice[data-variant="ghost"] {
padding: 0.65rem 0.8rem;
border-radius: var(--wrn-radius-sm);
background: color-mix(in srgb, var(--wrn-field-color) 7%, transparent);
}
.wrn-next__choice[data-variant="underline"] {
padding-block: 0.5rem;
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-next--select .wrn-next__field-control {
display: flex;
align-items: center;
}
.wrn-next__color-control,
.wrn-next__range-control,
.wrn-next__input-group-control,
.wrn-next__file-control {
display: flex;
min-width: 0;
align-items: center;
gap: 0.65rem;
}
.wrn-next__picker-control,
.wrn-next--time-picker .wrn-next__field-control {
position: relative;
display: flex;
min-width: 0;
align-items: center;
}
.wrn-next__picker-control > button:first-of-type,
.wrn-next__time-trigger {
display: flex;
width: 100%;
min-height: 2.65rem;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.7rem 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
font: inherit;
text-align: left;
}
.wrn-next__calendar,
.wrn-next__time-panel {
z-index: 20;
display: grid;
width: min(100%, 22rem);
gap: 0.75rem;
padding: 0.85rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-3);
}
.wrn-next--date-picker[data-expanded="false"] .wrn-next__calendar,
.wrn-next--time-picker[data-expanded="false"] .wrn-next__time-panel {
display: none;
}
.wrn-next__calendar button,
.wrn-next__time-panel button {
min-height: 2.25rem;
border: 0;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: inherit;
}
.wrn-next__calendar-weekdays,
.wrn-next__calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.2rem;
text-align: center;
}
.wrn-next__calendar-weekdays {
color: var(--wrn-color-muted);
font-size: 0.75rem;
}
.wrn-next__calendar-grid button:hover,
.wrn-next__time-panel button:hover {
background: var(--wrn-color-surface-2);
}
.wrn-next__calendar-grid button[aria-pressed="true"],
.wrn-next__time-panel button[aria-pressed="true"] {
background: var(--wrn-field-color);
color: var(--wrn-color-primary-contrast, #fff);
}
.wrn-next__time-period {
display: flex;
gap: 0.35rem;
}
.wrn-next--file-upload-progress .wrn-next__row,
.wrn-next__upload-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.wrn-next--file-upload-progress .wrn-next__row > span {
display: grid;
min-width: 0;
}
.wrn-next--field[data-variant="underline"]
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
border-width: 0 0 1px;
border-radius: 0;
background: transparent;
}
.wrn-next--field[data-variant="outlined"]
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
border-width: 2px;
}
.wrn-next--field[data-variant="pill"]
:is(.wrn-next__input-group-control, .wrn-next__file-control) {
border-radius: 999px;
}
.wrn-next--field[data-variant="ghost"]
:is(.wrn-next__input-group-control, .wrn-next__file-control, .wrn-next__color-control) {
border-color: transparent;
background: color-mix(in srgb, var(--wrn-field-color) 8%, var(--wrn-color-surface));
}
@media (max-width: 640px) {
.wrn-next--field[data-inline="true"],
.wrn-next--choice-field[data-inline="true"] {
grid-template-columns: 1fr;
}
.wrn-next--field[data-inline="true"] .wrn-next__field-error,
.wrn-next--choice-field[data-inline="true"] .wrn-next__field-error {
grid-column: auto;
}
}
.wrn-next--toggle-password:has(input.wrn-invalid) .wrn-next__password-control > input,
.wrn-next--toggle-password.wrn-next--invalid .wrn-next__password-control > input {
border-color: var(--wrn-color-danger);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 14%, transparent);
}
.wrn-next--strong-password.wrn-next--invalid input,
.wrn-next--strong-password input.wrn-invalid {
border-color: var(--wrn-color-danger);
}
}
}
+104 -48
View File
@@ -81,26 +81,26 @@ component InputNumber {
shared function componentColor() {
if (color === "secondary") {
return "var(--wire-color-secondary)"
return "var(--wrn-color-secondary)"
}
if (color === "success") {
return "var(--wire-color-success)"
return "var(--wrn-color-success)"
}
if (color === "warning") {
return "var(--wire-color-warning)"
return "var(--wrn-color-warning)"
}
if (color === "danger") {
return "var(--wire-color-danger)"
return "var(--wrn-color-danger)"
}
if (color === "info") {
return "var(--wire-color-info)"
return "var(--wrn-color-info)"
}
return "var(--wire-color-primary)"
return "var(--wrn-color-primary)"
}
shared function hasMin() {
@@ -232,26 +232,17 @@ component InputNumber {
)
}
// Outputs are resolved by name, so each one is written out. Raw
// CustomEvents dispatched on the root -- what this did before -- never
// reach a parent @binding.
client function dispatchInputNumberEvent(
sourceEvent,
eventName,
action,
previousValue,
root,
customEvent
payload
) {
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, {
payload = {
component: "InputNumber",
name: name,
value: currentValue,
@@ -261,8 +252,17 @@ component InputNumber {
max: hasMax() ? Number(max) : null,
step: normalizedStep(),
valid: !isInvalid()
})
root.dispatchEvent(customEvent)
}
if (eventName === "input") {
output.input(payload)
} else if (eventName === "change") {
output.change(payload)
} else if (eventName === "increment") {
output.increment(payload)
} else if (eventName === "decrement") {
output.decrement(payload)
}
}
client function applyControlValue(nextValue, action, sourceEvent, previousValue) {
@@ -418,19 +418,20 @@ component InputNumber {
data-value='{currentValue}'
data-invalid='{isInvalid() ? "true" : "false"}'
data-disabled='{disabled ? "true" : "false"}'
data-full-width='{fullWidth ? "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}'
class='wrn-input-number {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)]'
class='wrn-input-number__label'
>
<span>{label}</span>
{#if required}
<span
aria-hidden="true"
class='text-[var(--wire-color-danger)]'
class='wrn-input-number__required'
>*</span>
{/if}
</label>
@@ -439,14 +440,14 @@ component InputNumber {
{#if description !== "" && variant !== "labeled" && variant !== "seat"}
<p
id='{descriptionId()}'
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
class='wrn-input-number__description'
>
{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)]" : ""}'
class='wrn-input-number__control'
>
{#if variant === "horizontal" && showButtons}
<button
@@ -455,30 +456,30 @@ component InputNumber {
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"}'
class='wrn-input-number__button wrn-input-number__button--start'
>
<span
aria-hidden="true"
class='icon-[lucide--minus] size-4'
class='icon-[lucide--minus] wrn-input-number__icon'
></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"}'
class='wrn-input-number__content'
>
{#if variant === "labeled" || variant === "seat"}
<div class='min-w-0 flex-1'>
<div class='wrn-input-number__copy'>
{#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"}'
class='wrn-input-number__inner-label'
>
{label}
{#if required}
<span
aria-hidden="true"
class='ml-0.5 text-[var(--wire-color-danger)]'
class='wrn-input-number__required'
>*</span>
{/if}
</label>
@@ -487,7 +488,7 @@ component InputNumber {
{#if description !== ""}
<span
id='{descriptionId()}'
class='block truncate text-xs leading-4 text-[var(--wire-color-muted)]'
class='wrn-input-number__inner-description'
>
{description}
</span>
@@ -496,12 +497,12 @@ component InputNumber {
{/if}
<div
class='flex min-w-0 items-center {variant === "seat" ? "w-auto shrink-0" : "w-full"}'
class='wrn-input-number__field'
>
{#if prefix !== ""}
<span
aria-hidden="true"
class='shrink-0 pr-1.5 text-[var(--wire-color-muted)]'
class='wrn-input-number__prefix'
>
{prefix}
</span>
@@ -531,13 +532,13 @@ component InputNumber {
@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"}'
class='wrn-input-number__input'
/>
{#if suffix !== ""}
<span
aria-hidden="true"
class='shrink-0 pl-1.5 text-[var(--wire-color-muted)]'
class='wrn-input-number__suffix'
>
{suffix}
</span>
@@ -552,11 +553,11 @@ component InputNumber {
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"}'
class='wrn-input-number__button wrn-input-number__button--end'
>
<span
aria-hidden="true"
class='icon-[lucide--plus] size-4'
class='icon-[lucide--plus] wrn-input-number__icon'
></span>
</button>
{:else}
@@ -564,7 +565,7 @@ component InputNumber {
<div
role="group"
aria-label='{controlsLabel}'
class='flex shrink-0 border-l border-[var(--wire-color-border)] {variant === "vertical" ? "flex-col" : "flex-row"}'
class='wrn-input-number__buttons'
>
<button
type="button"
@@ -572,11 +573,11 @@ component InputNumber {
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"}'
class='wrn-input-number__button wrn-input-number__button--decrement'
>
<span
aria-hidden="true"
class='icon-[lucide--minus] size-4'
class='icon-[lucide--minus] wrn-input-number__icon'
></span>
</button>
@@ -586,11 +587,11 @@ component InputNumber {
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"}'
class='wrn-input-number__button wrn-input-number__button--increment'
>
<span
aria-hidden="true"
class='icon-[lucide--plus] size-4'
class='icon-[lucide--plus] wrn-input-number__icon'
></span>
</button>
</div>
@@ -603,11 +604,11 @@ component InputNumber {
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)]'
class='wrn-input-number__message wrn-input-number__message--error'
>
<span
aria-hidden="true"
class='icon-[lucide--circle-alert] size-3.5 shrink-0'
class='icon-[lucide--circle-alert] wrn-input-number__error-icon'
></span>
<span>{validationMessage()}</span>
</p>
@@ -616,11 +617,66 @@ component InputNumber {
{#if (!showValidationMessage || !isInvalid() || validationMessage() === "") && helpText !== ""}
<p
id='{messageId()}'
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
class='wrn-input-number__message'
>
{helpText}
</p>
{/if}
</div>
}
style {
.wrn-input-number { position: relative; display: flex; width: 100%; max-width: 24rem; flex-direction: column; gap: 0.375rem; color: var(--wrn-color-text); }
.wrn-input-number[data-full-width="true"] { max-width: none; }
.wrn-input-number[data-variant="compact"] { width: fit-content; max-width: 100%; }
.wrn-input-number[data-disabled="true"] { opacity: 0.6; }
.wrn-input-number__label { display: inline-flex; align-items: center; gap: 0.25rem; color: var(--wrn-color-text); font-size: 0.875rem; font-weight: 600; line-height: 1.25rem; }
.wrn-input-number__required { margin-left: 0.125rem; color: var(--wrn-color-danger); }
.wrn-input-number__description, .wrn-input-number__message { margin: 0; color: var(--wrn-color-muted); font-size: 0.75rem; line-height: 1.25rem; }
.wrn-input-number__control { display: flex; min-width: 0; min-height: 2.5rem; overflow: hidden; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius-sm); color: var(--wrn-color-text); background: var(--wrn-color-surface); box-shadow: var(--wrn-shadow-1); transition: border-color var(--wrn-motion-base), box-shadow var(--wrn-motion-base), background var(--wrn-motion-base); }
.wrn-input-number__control:focus-within { border-color: var(--input-number-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--input-number-accent) 18%, transparent); }
.wrn-input-number[data-invalid="true"] .wrn-input-number__control { border-color: var(--wrn-color-danger); }
.wrn-input-number[data-invalid="true"] .wrn-input-number__control:focus-within { box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-danger) 18%, transparent); }
.wrn-input-number[data-disabled="true"] .wrn-input-number__control { cursor: not-allowed; background: var(--wrn-color-surface-2); }
.wrn-input-number[data-variant="compact"] .wrn-input-number__control { border-radius: 999px; }
.wrn-input-number[data-size="xs"] .wrn-input-number__control { min-height: 2rem; font-size: 0.75rem; }
.wrn-input-number[data-size="sm"] .wrn-input-number__control { min-height: 2.25rem; font-size: 0.875rem; }
.wrn-input-number[data-size="lg"] .wrn-input-number__control { min-height: 3rem; font-size: 1rem; }
.wrn-input-number[data-size="xl"] .wrn-input-number__control { min-height: 3.5rem; font-size: 1.125rem; }
.wrn-input-number__content { display: flex; min-width: 0; flex: 1; align-items: center; padding-inline: 0.75rem; }
.wrn-input-number[data-size="xs"] .wrn-input-number__content { padding-inline: 0.5rem; }
.wrn-input-number[data-size="sm"] .wrn-input-number__content { padding-inline: 0.625rem; }
.wrn-input-number[data-size="lg"] .wrn-input-number__content { padding-inline: 1rem; }
.wrn-input-number[data-size="xl"] .wrn-input-number__content { padding-inline: 1.25rem; }
.wrn-input-number[data-variant="labeled"] .wrn-input-number__content { flex-direction: column; align-items: stretch; justify-content: center; gap: 0.125rem; }
.wrn-input-number[data-variant="seat"] .wrn-input-number__content { justify-content: space-between; gap: 0.75rem; }
.wrn-input-number__copy { min-width: 0; flex: 1; }
.wrn-input-number__inner-label, .wrn-input-number__inner-description { display: block; overflow: hidden; color: var(--wrn-color-text); font-size: 0.875rem; font-weight: 600; line-height: 1rem; text-overflow: ellipsis; white-space: nowrap; }
.wrn-input-number__inner-description { color: var(--wrn-color-muted); font-size: 0.75rem; font-weight: 400; }
.wrn-input-number[data-variant="labeled"] .wrn-input-number__inner-label { color: var(--wrn-color-muted); font-size: 0.75rem; font-weight: 500; }
.wrn-input-number__field { display: flex; width: 100%; min-width: 0; align-items: center; }
.wrn-input-number[data-variant="seat"] .wrn-input-number__field { width: auto; flex: none; }
.wrn-input-number__prefix, .wrn-input-number__suffix { flex: none; color: var(--wrn-color-muted); }
.wrn-input-number__prefix { padding-right: 0.375rem; } .wrn-input-number__suffix { padding-left: 0.375rem; }
.wrn-input-number__input { width: 100%; min-width: 0; flex: 1; padding: 0; border: 0; outline: none; color: var(--wrn-color-text); background: transparent; font: inherit; font-weight: 500; line-height: 1; appearance: textfield; }
.wrn-input-number__input::-webkit-inner-spin-button, .wrn-input-number__input::-webkit-outer-spin-button { appearance: none; }
.wrn-input-number__input::placeholder { color: var(--wrn-color-muted); }
.wrn-input-number__input:read-only { cursor: default; }
.wrn-input-number__input:disabled { cursor: not-allowed; }
.wrn-input-number:is([data-variant="horizontal"], [data-variant="compact"], [data-variant="seat"]) .wrn-input-number__input { text-align: center; }
.wrn-input-number[data-variant="seat"] .wrn-input-number__input { width: 2.5rem; flex: none; }
.wrn-input-number__buttons { display: flex; flex: none; border-left: 1px solid var(--wrn-color-border); }
.wrn-input-number[data-variant="vertical"] .wrn-input-number__buttons { flex-direction: column; }
.wrn-input-number__button { display: inline-flex; width: 2.5rem; flex: none; align-items: center; justify-content: center; padding: 0; border: 0; color: var(--wrn-color-muted); background: transparent; transition: color var(--wrn-motion-fast), background var(--wrn-motion-fast); }
.wrn-input-number[data-size="xs"] .wrn-input-number__button { width: 2rem; } .wrn-input-number[data-size="sm"] .wrn-input-number__button { width: 2.25rem; } .wrn-input-number[data-size="lg"] .wrn-input-number__button { width: 3rem; } .wrn-input-number[data-size="xl"] .wrn-input-number__button { width: 3.5rem; }
.wrn-input-number__button--start { border-right: 1px solid var(--wrn-color-border); } .wrn-input-number__button--end { border-left: 1px solid var(--wrn-color-border); }
.wrn-input-number__button--decrement { border-right: 1px solid var(--wrn-color-border); }
.wrn-input-number[data-variant="vertical"] .wrn-input-number__button--decrement { border-right: 0; border-bottom: 1px solid var(--wrn-color-border); }
.wrn-input-number__button:hover { color: var(--input-number-accent); background: var(--wrn-color-surface-2); }
.wrn-input-number__button:focus-visible { z-index: 1; outline: 2px solid var(--input-number-accent); outline-offset: -2px; }
.wrn-input-number__button:disabled { cursor: not-allowed; opacity: 0.4; }
.wrn-input-number__icon { width: 1rem; height: 1rem; }
.wrn-input-number__message--error { display: flex; align-items: center; gap: 0.375rem; color: var(--wrn-color-danger); }
.wrn-input-number__error-icon { width: 0.875rem; height: 0.875rem; flex: none; }
}
}
+48 -2
View File
@@ -1,9 +1,55 @@
// Kbd -- a keyboard key.
//
// <Kbd label="Ctrl" />
//
// Deliberately small. A kbd element is a kbd element; the work here is making
// it look like a key and follow the theme.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Kbd {
props {
size: string = "default"
color: string = "primary"
label: string = "K"
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> }
view {
<kbd {...attrs} class='wrn-kbd {class}' data-size='{size}' data-color='{color}'>
<slot>{label}</slot>
</kbd>
}
style {
.wrn-kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.6rem;
padding: 0.15rem 0.4rem;
border: 1px solid var(--wrn-color-border);
/* The lower edge is what reads as a physical key. */
border-bottom-width: 2px;
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface-soft);
color: var(--wrn-color-text);
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
line-height: 1;
}
.wrn-kbd[data-size="sm"] {
min-width: 1.4rem;
padding: 0.1rem 0.3rem;
font-size: 0.7rem;
}
.wrn-kbd[data-size="lg"] {
min-width: 2rem;
padding: 0.3rem 0.55rem;
font-size: 0.9rem;
}
}
}
+214 -8
View File
@@ -1,19 +1,225 @@
// LayoutSplitter -- two panes with a divider the reader can move.
//
// <LayoutSplitter size={40} minSize={20}>
// <div data-slot="start">...</div>
// <div data-slot="end">...</div>
// </LayoutSplitter>
//
// The dragging lives in the reactive runtime behind data-wrn-splitter. Pointer
// moves fire far too often to route through a client function, and a state
// write made inside a pointermove callback is dropped, so the resolved size is
// held in the DOM as the --wrn-split custom property and these styles read it.
//
// This component previously declared resizeStart, resize and resizeEnd with no
// pointer handling whatsoever: a caller connected up @resize and received nothing,
// for ever, with no error.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
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[])
// Named sizeChange rather than resize so it cannot be confused with the
// native window event a caller may already be listening for. An earlier
// comment here claimed a natively-named output could never reach a parent
// binding; that was wrong, and the rename was never what fixed anything.
sizeChange(payload: { size: number })
}
props {
size: string = "default"
color: string = "primary"
columns: number = 2
gap: string = "md"
maxWidth: string = "xl"
size: number = 50
orientation: string = "horizontal"
// Smallest share either pane may take, as a percentage. It also fixes the
// upper bound at 100 - minSize, so neither pane can be dragged away to
// nothing and left unrecoverable by pointer.
minSize: number = 15
step: number = 5
label: string = "Resize panels"
class: string = ""
}
functions {
// The runtime resolves the size and announces it; this turns that into the
// declared output so a parent @resize binding receives it.
client function reportResize(sourceEvent) {
var detail = sourceEvent ? sourceEvent.detail : null
if (!detail) {
return
}
output.sizeChange({ size: detail.size })
}
shared function isVertical() {
return orientation === "vertical"
}
shared function lowerBound() {
var value = Number(minSize)
if (!value || value < 0) {
return 15
}
return Math.min(45, value)
}
shared function upperBound() {
return 100 - lowerBound()
}
// Sizes arrive as HTML attributes, so a value outside the bounds is
// routine rather than exceptional. Clamp instead of rendering something
// the reader cannot undo.
shared function currentSize() {
var value = Number(size)
if (!value || value < 0) {
return 50
}
return Math.min(upperBound(), Math.max(lowerBound(), value))
}
shared function stepSize() {
var value = Number(step)
return value && value > 0 ? value : 5
}
}
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>
<div
{...attrs}
data-ui-component="LayoutSplitter"
class='wrn-splitter {class}'
data-color='{color}'
data-orientation='{orientation}'
data-wrn-splitter='{isVertical() ? "vertical" : "horizontal"}'
data-wrn-splitter-min='{lowerBound()}'
data-wrn-splitter-step='{stepSize()}'
style='--wrn-split:{currentSize()}%;'
@wrnexus:splitter:resize='reportResize(event)'
>
<div class="wrn-splitter__pane wrn-splitter__pane--start">
<slot name="start"></slot>
</div>
<div
class="wrn-splitter__handle"
data-wrn-splitter-handle="true"
role="separator"
tabindex="0"
aria-label='{label}'
aria-orientation='{isVertical() ? "horizontal" : "vertical"}'
aria-valuenow='{currentSize()}'
aria-valuemin='{lowerBound()}'
aria-valuemax='{upperBound()}'
>
<span class="wrn-splitter__grip" aria-hidden="true"></span>
</div>
<div class="wrn-splitter__pane wrn-splitter__pane--end">
<slot name="end"></slot>
</div>
<slot />
</div>
}
style {
.wrn-splitter {
--splitter-accent: var(--wrn-color-primary);
display: grid;
/* The first track follows the size the runtime resolves. */
grid-template-columns: var(--wrn-split, 50%) auto minmax(0, 1fr);
align-items: stretch;
width: 100%;
min-width: 0;
}
.wrn-splitter[data-color="secondary"] {
--splitter-accent: var(--wrn-color-secondary);
}
.wrn-splitter[data-color="success"] {
--splitter-accent: var(--wrn-color-success);
}
.wrn-splitter[data-color="danger"] {
--splitter-accent: var(--wrn-color-danger);
}
.wrn-splitter[data-color="info"] {
--splitter-accent: var(--wrn-color-info);
}
.wrn-splitter[data-orientation="vertical"] {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: var(--wrn-split, 50%) auto minmax(0, 1fr);
}
.wrn-splitter__pane {
min-width: 0;
min-height: 0;
overflow: auto;
}
.wrn-splitter__handle {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
padding: 0 0.25rem;
border: 0;
background: transparent;
cursor: col-resize;
/* Without this the pointer drag selects the text in both panes. */
touch-action: none;
user-select: none;
}
.wrn-splitter[data-orientation="vertical"] .wrn-splitter__handle {
padding: 0.25rem 0;
cursor: row-resize;
}
.wrn-splitter__grip {
display: block;
width: 2px;
height: 100%;
min-height: 1.5rem;
border-radius: 999px;
background: var(--wrn-color-border);
transition: background 140ms ease;
}
.wrn-splitter[data-orientation="vertical"] .wrn-splitter__grip {
width: 100%;
min-width: 1.5rem;
height: 2px;
}
.wrn-splitter__handle:hover .wrn-splitter__grip,
.wrn-splitter[data-wrn-splitter-dragging="true"] .wrn-splitter__grip {
background: var(--splitter-accent);
}
.wrn-splitter__handle:focus-visible {
outline: 2px solid var(--splitter-accent);
outline-offset: -2px;
border-radius: var(--wrn-radius-sm);
}
/*
* Two panes side by side stop making sense on a phone. Stacking them keeps
* both readable, and the divider stops being draggable because the grid
* no longer has a second track to trade against.
*/
@media (max-width: 639px) {
.wrn-splitter,
.wrn-splitter[data-orientation="vertical"] {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto auto;
}
.wrn-splitter__handle {
cursor: default;
}
}
}
}
+86 -2
View File
@@ -14,11 +14,95 @@ component LegendIndicator {
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--legend-indicator wire-next--variant-{variant} {class}">
<section class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--legend-indicator wrn-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}
{#if items}<div class="wrn-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
style {
.wrn-next--legend-indicator {
display: grid;
gap: 0.75rem;
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius);
color: var(--wrn-color-text);
background: var(--wrn-color-surface);
}
.wrn-next--legend-indicator > p { margin: 0; color: var(--wrn-color-muted); }
.wrn-next--legend-indicator > .wrn-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.wrn-next--legend-indicator > .wrn-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wrn-radius-sm); background: var(--wrn-color-surface-2); }
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+114 -24
View File
@@ -1,3 +1,13 @@
// Link -- a text link that follows the theme.
//
// <Link href="/docs" label="Documentation" />
// <Link href="https://example.com" label="Spec" external={true} />
//
// external adds rel="noopener noreferrer" and an outbound icon, so the reader
// is told the link leaves the site before they follow it.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Link {
props {
size: string = "default"
@@ -6,42 +16,122 @@ component Link {
href: string = "#"
target: string = ""
rel: string = ""
underline: string = "hover"
external: boolean = false
class: string = ""
}
view {
<a
{...attrs}
data-ui-component="Link"
class='wrn-link {class}'
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'
data-size='{size}'
data-color='{color}'
data-underline='{underline}'
data-external='{external}'
>
<span class="truncate">{label}</span>
{#if external}
<span
class="icon-[lucide--external-link] size-3.5 shrink-0"
aria-hidden="true"
></span>
{/if}
<span class="wrn-link__label">{label}</span>
<span
class="icon-[lucide--external-link] wrn-link__icon"
data-show="external"
aria-hidden="true"
></span>
<slot></slot>
</a>
}
style {
.wrn-link {
--link-tone: var(--wrn-color-primary);
--link-tone-hover: var(--wrn-color-primary-hover);
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-width: 0;
border-radius: var(--wrn-radius-sm);
color: var(--link-tone);
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
text-underline-offset: 4px;
transition: color 140ms ease;
}
.wrn-link[data-color="secondary"] {
--link-tone: var(--wrn-color-secondary);
--link-tone-hover: var(--wrn-color-secondary-hover);
}
.wrn-link[data-color="success"] {
--link-tone: var(--wrn-color-success);
--link-tone-hover: var(--wrn-color-success);
}
.wrn-link[data-color="warning"] {
--link-tone: var(--wrn-color-warning-text);
--link-tone-hover: var(--wrn-color-warning-text);
}
.wrn-link[data-color="danger"] {
--link-tone: var(--wrn-color-danger);
--link-tone-hover: var(--wrn-color-danger);
}
.wrn-link[data-color="info"] {
--link-tone: var(--wrn-color-info);
--link-tone-hover: var(--wrn-color-info);
}
.wrn-link[data-color="neutral"] {
--link-tone: var(--wrn-color-text);
--link-tone-hover: var(--wrn-color-text);
}
.wrn-link[data-size="xs"] {
font-size: 0.75rem;
}
.wrn-link[data-size="md"] {
font-size: 1rem;
}
.wrn-link[data-size="lg"] {
font-size: 1.125rem;
}
.wrn-link[data-underline="always"] {
text-decoration: underline;
}
.wrn-link[data-underline="hover"]:hover {
text-decoration: underline;
}
.wrn-link:hover {
color: var(--link-tone-hover);
}
.wrn-link:focus-visible {
outline: 2px solid var(--wrn-color-focus);
outline-offset: 2px;
}
/* The label truncates; the outbound icon must not shrink with it. */
.wrn-link__label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wrn-link__icon {
flex: 0 0 auto;
width: 0.875rem;
height: 0.875rem;
}
}
}
+61 -40
View File
@@ -1,4 +1,8 @@
component List {
outputs {
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
@@ -13,89 +17,76 @@ component List {
<section
data-ui-component="List"
aria-label='{title}'
class='w-full {class}'
class='wrn-list {class}'
>
{#if title || description}
<header class="mb-4">
<header class="wrn-list__header">
{#if title}
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{title}</h3>
<h3 class="wrn-list__title">{title}</h3>
{/if}
{#if description}
<p class="mt-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
<p class="wrn-list__description">{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"'
class="wrn-list__items"
data-variant='{variant}'
>
{#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"'
class="wrn-list__item"
data-variant='{variant}'
@click='if (!item.href) { output.select({ item: item, index: index }) }'
>
{#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 } }))'
class="wrn-list__row"
data-size='{size}'
@click='output.select({ 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 class="wrn-list__leading-icon"><span class='{item.icon}' 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" />
<img src='{item.imageSrc}' alt='{item.imageAlt || ""}' class="wrn-list__image" 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>
<div class="wrn-list__body">
<div class="wrn-list__heading">
<p class="wrn-list__item-title">{item.title || item.label}</p>
{#if item.meta}
<span class="shrink-0 text-xs text-[var(--wire-color-text-muted)]">{item.meta}</span>
<span class="wrn-list__meta">{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>
<p class="wrn-list__item-description">{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>
<span class="wrn-list__badge">{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>
<span class="icon-[lucide--chevron-right] wrn-list__chevron" aria-hidden="true"></span>
</a>
{:else}
<div class="flex min-w-0 items-start gap-3 px-4 py-3">
<div class="wrn-list__row" data-size='{size}'>
{#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 class="wrn-list__leading-icon"><span class='{item.icon}' 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>
<div class="wrn-list__body">
<p class="wrn-list__item-title">{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>
<p class="wrn-list__item-description">{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)]">
<li class="wrn-list__empty">
No items available.
</li>
{/each}
@@ -104,4 +95,34 @@ component List {
<slot></slot>
</section>
}
style {
.wrn-list { width: 100%; }
.wrn-list__header { margin-bottom: 1rem; }
.wrn-list__title { color: var(--wrn-color-text); font-size: 1.125rem; font-weight: 700; }
.wrn-list__description { margin-top: 0.25rem; color: var(--wrn-color-text-muted); font-size: 0.875rem; line-height: 1.5rem; }
.wrn-list__items { overflow: hidden; }
.wrn-list__items:is([data-variant="default"], [data-variant="divided"]) > .wrn-list__item + .wrn-list__item { border-top: 1px solid var(--wrn-color-border); }
.wrn-list__items[data-variant="card"] { border: 1px solid var(--wrn-color-border); border-radius: calc(var(--wrn-radius) * 1.5); background: var(--wrn-color-surface-raised); }
.wrn-list__items[data-variant="separated"] { display: grid; gap: 0.75rem; }
.wrn-list__item { min-width: 0; }
.wrn-list__item[data-variant="separated"] { border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); background: var(--wrn-color-surface-raised); }
.wrn-list__row { display: flex; min-width: 0; align-items: flex-start; gap: 0.75rem; padding: 0.75rem 1rem; outline: none; transition: background var(--wrn-motion-base); }
a.wrn-list__row:hover { background: var(--wrn-color-surface-soft); }
.wrn-list__row:focus-visible { box-shadow: inset 0 0 0 2px var(--wrn-color-focus); }
.wrn-list__row[data-size="sm"] { padding: 0.625rem 0.75rem; }
.wrn-list__row[data-size="lg"] { padding: 1rem 1.25rem; }
.wrn-list__leading-icon { display: flex; width: 2.5rem; height: 2.5rem; flex: none; align-items: center; justify-content: center; border-radius: var(--wrn-radius); color: var(--wrn-color-primary); background: var(--wrn-color-primary-soft); }
.wrn-list__leading-icon > span { width: 1.25rem; height: 1.25rem; }
.wrn-list__image { width: 3rem; height: 3rem; flex: none; border-radius: var(--wrn-radius); object-fit: cover; }
.wrn-list__body { min-width: 0; flex: 1; }
.wrn-list__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; }
.wrn-list__item-title { overflow: hidden; color: var(--wrn-color-text); font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.wrn-list__meta { flex: none; color: var(--wrn-color-text-muted); font-size: 0.75rem; }
.wrn-list__item-description { display: -webkit-box; margin-top: 0.25rem; overflow: hidden; color: var(--wrn-color-text-muted); font-size: 0.875rem; line-height: 1.25rem; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
.wrn-list__badge { display: inline-flex; margin-top: 0.5rem; padding: 0.25rem 0.625rem; border-radius: 999px; color: var(--wrn-color-primary); background: var(--wrn-color-primary-soft); font-size: 0.75rem; font-weight: 600; }
.wrn-list__chevron { width: 1rem; height: 1rem; flex: none; margin-top: 0.25rem; color: var(--wrn-color-text-muted); transition: color var(--wrn-motion-base), transform var(--wrn-motion-base); }
.wrn-list__row:hover .wrn-list__chevron { color: var(--wrn-color-primary); transform: translateX(0.125rem); }
.wrn-list__empty { padding: 1.5rem; border: 1px dashed var(--wrn-color-border); border-radius: var(--wrn-radius); color: var(--wrn-color-text-muted); font-size: 0.875rem; text-align: center; }
}
}
+86 -2
View File
@@ -14,11 +14,95 @@ component ListGroup {
class: string = ""
}
view {
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--list-group wire-next--variant-{variant} {class}">
<section class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--list-group wrn-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}
{#if items}<div class="wrn-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
<slot />
</section>
}
style {
.wrn-next--list-group {
display: grid;
gap: 0.75rem;
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius);
color: var(--wrn-color-text);
background: var(--wrn-color-surface);
}
.wrn-next--list-group > p { margin: 0; color: var(--wrn-color-muted); }
.wrn-next--list-group > .wrn-next__items { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.wrn-next--list-group > .wrn-next__items > span { padding: 0.35rem 0.6rem; border-radius: var(--wrn-radius-sm); background: var(--wrn-color-surface-2); }
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+48 -22
View File
@@ -1,4 +1,12 @@
component Map {
outputs {
// markerClick and select both fire for a marker press; select is the
// generic name callers reach for, markerClick the explicit one.
markerClick(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
zoom(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
@@ -13,37 +21,34 @@ component Map {
<section
data-ui-component="Map"
aria-label='{title}'
class='overflow-hidden rounded-2xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] shadow-sm {class}'
class='wrn-map {class}'
>
{#if title || description}
<header class="flex items-start justify-between gap-4 border-b border-[var(--wire-color-border)] p-5">
<header class="wrn-map__header">
<div>
{#if title}
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{title}</h3>
<h3 class="wrn-map__title">{title}</h3>
{/if}
{#if description}
<p class="mt-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
<p class="wrn-map__description">{description}</p>
{/if}
</div>
<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="icon-[lucide--map] size-5" aria-hidden="true"></span>
<span class="wrn-map__header-icon"><span class="icon-[lucide--map]" aria-hidden="true"></span>
</span>
</header>
{/if}
<div
class="relative isolate overflow-hidden bg-[var(--wire-color-surface-soft)]"
class:h-64='size === "sm"'
class:h-80='size === "default" || size === "md"'
class:h-[28rem]='size === "lg"'
class="wrn-map__viewport"
data-size='{size}'
>
<div
class="pointer-events-none absolute inset-0 opacity-50"
style="background-image: linear-gradient(var(--wire-color-border) 1px, transparent 1px), linear-gradient(90deg, var(--wire-color-border) 1px, transparent 1px); background-size: 32px 32px;"
class="wrn-map__grid"
style="background-image: linear-gradient(var(--wrn-color-border) 1px, transparent 1px), linear-gradient(90deg, var(--wrn-color-border) 1px, transparent 1px); background-size: 32px 32px;"
aria-hidden="true"
></div>
<div class="absolute inset-0 flex items-center justify-center p-6">
<div class="wrn-map__content">
<slot></slot>
</div>
@@ -52,34 +57,55 @@ component Map {
<button
type="button"
aria-label='{item.label || item.title || "Map marker"}'
class="absolute inline-flex size-10 items-center justify-center rounded-full border-4 border-[var(--wire-color-surface-raised)] bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] shadow-lg transition hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
class="wrn-map__marker"
style='left: {item.x || (20 + index * 12)}%; top: {item.y || (30 + (index % 3) * 18)}%;'
@click='event.currentTarget.dispatchEvent(new CustomEvent("markerClick", { bubbles: true, detail: item })); event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: item }))'
@click='output.markerClick(item); output.select(item)'
>
<span class='{item.icon || "icon-[lucide--map-pin]"}' aria-hidden="true"></span>
</button>
{/each}
{/if}
<div class="absolute bottom-4 right-4 flex flex-col gap-2">
<div class="wrn-map__controls">
<button
type="button"
aria-label="Zoom in"
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
@click='event.currentTarget.dispatchEvent(new CustomEvent("zoom", { bubbles: true, detail: { direction: "in" } }))'
class="wrn-map__control"
@click='output.zoom({ direction: "in" })'
>
<span class="icon-[lucide--plus] size-4" aria-hidden="true"></span>
<span class="icon-[lucide--plus] wrn-map__control-icon" aria-hidden="true"></span>
</button>
<button
type="button"
aria-label="Zoom out"
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
@click='event.currentTarget.dispatchEvent(new CustomEvent("zoom", { bubbles: true, detail: { direction: "out" } }))'
class="wrn-map__control"
@click='output.zoom({ direction: "out" })'
>
<span class="icon-[lucide--minus] size-4" aria-hidden="true"></span>
<span class="icon-[lucide--minus] wrn-map__control-icon" aria-hidden="true"></span>
</button>
</div>
</div>
</section>
}
style {
.wrn-map { overflow: hidden; border: 1px solid var(--wrn-color-border); border-radius: calc(var(--wrn-radius) * 1.5); background: var(--wrn-color-surface-raised); box-shadow: var(--wrn-shadow-1); }
.wrn-map__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; padding: 1.25rem; border-bottom: 1px solid var(--wrn-color-border); }
.wrn-map__title { color: var(--wrn-color-text); font-size: 1.125rem; font-weight: 700; }
.wrn-map__description { margin-top: 0.25rem; color: var(--wrn-color-text-muted); font-size: 0.875rem; line-height: 1.5rem; }
.wrn-map__header-icon { display: flex; width: 2.5rem; height: 2.5rem; flex: none; align-items: center; justify-content: center; border-radius: var(--wrn-radius); color: var(--wrn-color-primary); background: var(--wrn-color-primary-soft); }
.wrn-map__header-icon > span { width: 1.25rem; height: 1.25rem; }
.wrn-map__viewport { position: relative; isolation: isolate; height: 20rem; overflow: hidden; background: var(--wrn-color-surface-soft); }
.wrn-map__viewport[data-size="sm"] { height: 16rem; }
.wrn-map__viewport[data-size="lg"] { height: 28rem; }
.wrn-map__grid { position: absolute; inset: 0; opacity: 0.5; pointer-events: none; }
.wrn-map__content { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 1.5rem; }
.wrn-map__marker { position: absolute; display: inline-flex; width: 2.5rem; height: 2.5rem; align-items: center; justify-content: center; padding: 0; border: 4px solid var(--wrn-color-surface-raised); border-radius: 50%; color: var(--wrn-color-on-primary); background: var(--wrn-color-primary); box-shadow: var(--wrn-shadow-3); transition: transform var(--wrn-motion-base) var(--wrn-ease-standard); }
.wrn-map__marker:hover { transform: scale(1.1); }
.wrn-map__controls { position: absolute; right: 1rem; bottom: 1rem; display: flex; flex-direction: column; gap: 0.5rem; }
.wrn-map__control { display: inline-flex; width: 2.5rem; height: 2.5rem; align-items: center; justify-content: center; padding: 0; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius); color: var(--wrn-color-text); background: var(--wrn-color-surface-raised); box-shadow: var(--wrn-shadow-1); transition: background var(--wrn-motion-base); }
.wrn-map__control:hover { background: var(--wrn-color-surface-soft); }
.wrn-map__marker:focus-visible, .wrn-map__control:focus-visible { outline: 2px solid var(--wrn-color-focus); outline-offset: 2px; }
.wrn-map__control-icon { width: 1rem; height: 1rem; }
}
}
+51 -28
View File
@@ -1,4 +1,10 @@
component Marquee {
outputs {
// Fired when the reader pauses the scroll, by hover, focus or the button.
pause(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
resume(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
@@ -15,46 +21,44 @@ component Marquee {
<section
data-ui-component="Marquee"
aria-label='{title}'
class='overflow-hidden border-y border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] {class}'
class='wrn-marquee {class}'
>
<div class="flex items-stretch">
<div class="wrn-marquee__layout">
{#if title}
<div class="relative z-10 flex shrink-0 items-center gap-2 border-r border-[var(--wire-color-border)] bg-[var(--wire-color-primary)] px-4 py-3 text-[var(--wire-color-on-primary)] sm:px-5">
<span class="icon-[lucide--megaphone] size-4" aria-hidden="true"></span>
<span class="text-sm font-bold">{title}</span>
<div class="wrn-marquee__title">
<span class="icon-[lucide--megaphone] wrn-marquee__icon" aria-hidden="true"></span>
<span>{title}</span>
</div>
{/if}
<div
class="group relative min-w-0 flex-1 overflow-hidden"
@mouseenter='paused = true; event.currentTarget.dispatchEvent(new CustomEvent("pause", { bubbles: true }))'
@mouseleave='paused = false; event.currentTarget.dispatchEvent(new CustomEvent("resume", { bubbles: true }))'
class="wrn-marquee__window"
@mouseenter='paused = true; output.pause({})'
@mouseleave='paused = false; output.resume({})'
@focusin='paused = true'
@focusout='paused = false'
>
<div
class="wire-marquee-track flex w-max min-w-full items-center"
class:wire-marquee-paused='paused'
class="wrn-marquee__track"
class:wrn-marquee-paused='paused'
>
{#each items as item}
<a
href='{item.href || "#"}'
class="flex shrink-0 items-center gap-3 px-5 py-3 text-sm font-medium text-[var(--wire-color-text)] transition hover:bg-[var(--wire-color-primary-soft)] hover:text-[var(--wire-color-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
class="wrn-marquee__item"
>
<span
class="size-2 shrink-0 rounded-full bg-[var(--wire-color-primary)]"
class:bg-[var(--wire-color-danger)]='item.color === "danger"'
class:bg-[var(--wire-color-warning)]='item.color === "warning"'
class:bg-[var(--wire-color-success)]='item.color === "success"'
class="wrn-marquee__dot"
data-color='{item.color || "primary"}'
aria-hidden="true"
></span>
<span>{item.label || item.title}</span>
{#if item.meta}
<span class="text-xs text-[var(--wire-color-text-muted)]">{item.meta}</span>
<span class="wrn-marquee__meta">{item.meta}</span>
{/if}
</a>
{:empty}
<p class="px-5 py-3 text-sm text-[var(--wire-color-text-muted)]">{description || "No announcements available."}</p>
<p class="wrn-marquee__empty">{description || "No announcements available."}</p>
{/each}
{#if items.length > 0}
@@ -63,9 +67,9 @@ component Marquee {
href='{item.href || "#"}'
aria-hidden="true"
tabindex="-1"
class="flex shrink-0 items-center gap-3 px-5 py-3 text-sm font-medium text-[var(--wire-color-text)]"
class="wrn-marquee__item"
>
<span class="size-2 shrink-0 rounded-full bg-[var(--wire-color-primary)]" aria-hidden="true"></span>
<span class="wrn-marquee__dot" aria-hidden="true"></span>
<span>{item.label || item.title}</span>
</a>
{/each}
@@ -76,11 +80,11 @@ component Marquee {
<button
type="button"
aria-label='{paused ? "Resume announcements" : "Pause announcements"}'
class="flex shrink-0 items-center justify-center border-l border-[var(--wire-color-border)] px-4 text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-soft)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
@click='paused = !paused; event.currentTarget.dispatchEvent(new CustomEvent(paused ? "pause" : "resume", { bubbles: true }))'
class="wrn-marquee__toggle"
@click='paused = !paused; paused ? output.pause({}) : output.resume({})'
>
<span class="icon-[lucide--pause] size-4" data-show='!paused' aria-hidden="true"></span>
<span class="icon-[lucide--play] size-4" data-show='paused' aria-hidden="true"></span>
<span class="icon-[lucide--pause] wrn-marquee__icon" data-show='!paused' aria-hidden="true"></span>
<span class="icon-[lucide--play] wrn-marquee__icon" data-show='paused' aria-hidden="true"></span>
</button>
</div>
@@ -89,21 +93,40 @@ component Marquee {
}
style {
.wire-marquee-track {
animation: wire-marquee 32s linear infinite;
.wrn-marquee { overflow: hidden; border-block: 1px solid var(--wrn-color-border); background: var(--wrn-color-surface-raised); }
.wrn-marquee__layout { display: flex; align-items: stretch; }
.wrn-marquee__title { position: relative; z-index: 1; display: flex; flex: none; align-items: center; gap: 0.5rem; padding: 0.75rem 1rem; border-right: 1px solid var(--wrn-color-border); color: var(--wrn-color-on-primary); background: var(--wrn-color-primary); font-size: 0.875rem; font-weight: 700; }
.wrn-marquee__window { position: relative; min-width: 0; flex: 1; overflow: hidden; }
.wrn-marquee__track {
display: flex; width: max-content; min-width: 100%; align-items: center;
animation: wrn-marquee 32s linear infinite;
}
.wrn-marquee-paused { animation-play-state: paused; }
.wrn-marquee__item { display: flex; flex: none; align-items: center; gap: 0.75rem; padding: 0.75rem 1.25rem; color: var(--wrn-color-text); font-size: 0.875rem; font-weight: 500; transition: color var(--wrn-motion-base), background var(--wrn-motion-base); }
.wrn-marquee__item:hover { color: var(--wrn-color-primary); background: var(--wrn-color-primary-soft); }
.wrn-marquee__item:focus-visible, .wrn-marquee__toggle:focus-visible { outline: 2px solid var(--wrn-color-focus); outline-offset: -2px; }
.wrn-marquee__dot { width: 0.5rem; height: 0.5rem; flex: none; border-radius: 50%; background: var(--wrn-color-primary); }
.wrn-marquee__dot[data-color="danger"] { background: var(--wrn-color-danger); }
.wrn-marquee__dot[data-color="warning"] { background: var(--wrn-color-warning); }
.wrn-marquee__dot[data-color="success"] { background: var(--wrn-color-success); }
.wrn-marquee__meta { color: var(--wrn-color-text-muted); font-size: 0.75rem; }
.wrn-marquee__empty { padding: 0.75rem 1.25rem; color: var(--wrn-color-text-muted); font-size: 0.875rem; }
.wrn-marquee__toggle { display: flex; flex: none; align-items: center; justify-content: center; padding-inline: 1rem; border: 0; border-left: 1px solid var(--wrn-color-border); color: var(--wrn-color-text-muted); background: transparent; transition: color var(--wrn-motion-base), background var(--wrn-motion-base); }
.wrn-marquee__toggle:hover { color: var(--wrn-color-text); background: var(--wrn-color-surface-soft); }
.wrn-marquee__icon { width: 1rem; height: 1rem; }
@media (min-width: 40rem) { .wrn-marquee__title { padding-inline: 1.25rem; } }
.wire-marquee-paused {
.wrn-marquee-paused {
animation-play-state: paused;
}
@keyframes wire-marquee {
@keyframes wrn-marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
.wire-marquee-track {
.wrn-marquee__track {
animation: none;
}
}
+333 -12
View File
@@ -1,23 +1,344 @@
// MegaMenu -- a trigger and a wide panel of grouped links.
//
// <MegaMenu label="Products" columns='[{"heading":"Platform",
// "items":[{"label":"Runtime","href":"/runtime"}]}]' />
//
// Deliberately one level deep. A mega menu exists to show breadth flat, so
// everything is one click away; nesting inside the panel buries content behind
// hover-within-hover and is close to unusable by keyboard and touch. Use Nav
// when you actually want cascading submenus.
//
// The panel carries data-wrn-anchored so the runtime clamp keeps it inside the
// viewport instead of hanging off the edge of a wide layout.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component MegaMenu {
outputs {
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(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)
open(payload: { sourceEvent: Event })
close(payload: { reason: string })
select(payload: { item: object; value: string })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Mega Menu"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
size: string = "default"
label: string = "Menu"
icon: string = ""
columns: unknown[] = []
footer: string = ""
defaultOpen: boolean = false
class: string = ""
}
state visible = defaultOpen
functions {
shared function columnList() {
return Array.isArray(columns) ? columns : []
}
shared function itemsOf(column) {
return column && Array.isArray(column.items) ? column.items : []
}
client function showPanel(sourceEvent) {
visible = true
output.open({ sourceEvent: sourceEvent })
}
client function hidePanel(reason) {
visible = false
output.close({ reason: reason })
}
client function togglePanel(sourceEvent) {
if (visible) {
hidePanel("toggle")
} else {
showPanel(sourceEvent)
}
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "Escape" && visible) {
sourceEvent.preventDefault()
hidePanel("escape")
}
}
client function choose(item) {
if (item.disabled) {
return
}
output.select({ item: item, value: item.value || item.label || "" })
hidePanel("select")
}
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--mega-menu wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<slot />
</nav>
<div
{...attrs}
data-ui-component="MegaMenu"
class='wrn-mega {class}'
data-color='{color}'
data-size='{size}'
data-open='{visible}'
@keydown='handleKeydown(event)'
@mouseleave='hidePanel("pointer-leave")'
>
<button
type="button"
class="wrn-mega__trigger"
aria-haspopup="true"
aria-expanded='{visible}'
@click='togglePanel(event)'
@mouseenter='showPanel(event)'
@focus='showPanel(event)'
>
<span class='wrn-mega__trigger-icon {icon}' data-show="icon" aria-hidden="true"></span>
<span class="wrn-mega__trigger-label">{label}</span>
<span class="wrn-mega__arrow" aria-hidden="true">&#8250;</span>
</button>
<div
class="wrn-mega__panel"
data-wrn-anchored="true"
data-show="visible"
data-wrn-roving="both"
>
<div class="wrn-mega__columns">
{#each columnList() as column}
<section class="wrn-mega__column">
<h3 class="wrn-mega__heading" data-show="column.heading">{column.heading}</h3>
<ul class="wrn-mega__list">
{#each itemsOf(column) as item}
<li>
<a
class="wrn-mega__link"
href='{item.href || "#"}'
data-wrn-roving-item="true"
aria-disabled='{item.disabled ? "true" : "false"}'
@click='choose(item)'
>
<span
class='wrn-mega__link-icon {item.icon}'
data-show="item.icon"
aria-hidden="true"
></span>
<span class="wrn-mega__link-body">
<span class="wrn-mega__link-label">{item.label}</span>
<span class="wrn-mega__link-description" data-show="item.description">
{item.description}
</span>
</span>
</a>
</li>
{/each}
</ul>
</section>
{/each}
</div>
<p class="wrn-mega__footer" data-show="footer">{footer}</p>
<slot />
</div>
</div>
}
style {
.wrn-mega {
--mega-accent: var(--wrn-color-primary);
position: relative;
display: inline-block;
max-width: 100%;
}
.wrn-mega[data-color="secondary"] {
--mega-accent: var(--wrn-color-secondary);
}
.wrn-mega[data-color="success"] {
--mega-accent: var(--wrn-color-success);
}
.wrn-mega[data-color="danger"] {
--mega-accent: var(--wrn-color-danger);
}
.wrn-mega[data-color="info"] {
--mega-accent: var(--wrn-color-info);
}
.wrn-mega[data-size="sm"] {
font-size: 0.82rem;
}
.wrn-mega[data-size="lg"] {
font-size: 1rem;
}
.wrn-mega__trigger {
appearance: none;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.45rem 0.7rem;
border: 1px solid transparent;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: var(--wrn-color-text-muted);
font: inherit;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
}
.wrn-mega__trigger:hover,
.wrn-mega[data-open="true"] .wrn-mega__trigger {
background: var(--wrn-color-surface-soft);
color: var(--wrn-color-text);
}
.wrn-mega__trigger:focus-visible {
outline: 2px solid var(--mega-accent);
outline-offset: 2px;
}
.wrn-mega__arrow {
display: inline-block;
transition: transform 160ms ease;
}
.wrn-mega[data-open="true"] .wrn-mega__arrow {
transform: rotate(90deg);
}
/*
* The panel is offset below the trigger for looks, which used to close the
* menu on the way to it: that offset is dead space belonging to neither
* element, so crossing it fired mouseleave on the root. The bridge below
* covers the gap with a descendant, so the pointer never actually leaves.
*/
.wrn-mega__panel::before {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 100%;
height: 0.6rem;
}
.wrn-mega__panel {
position: absolute;
z-index: 40;
top: calc(100% + 0.4rem);
left: 0;
width: max-content;
max-width: min(60rem, calc(100vw - 2rem));
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-lg);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-1);
}
.wrn-mega__columns {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
gap: 1.25rem;
}
.wrn-mega__column {
min-width: 0;
}
.wrn-mega__heading {
margin: 0 0 0.5rem;
color: var(--wrn-color-text-muted);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.wrn-mega__list {
display: grid;
gap: 0.15rem;
margin: 0;
padding: 0;
list-style: none;
}
.wrn-mega__link {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.45rem 0.5rem;
border-radius: var(--wrn-radius-sm);
color: var(--wrn-color-text);
text-decoration: none;
}
.wrn-mega__link:hover {
background: var(--wrn-color-surface-soft);
}
.wrn-mega__link:focus-visible {
outline: 2px solid var(--mega-accent);
outline-offset: 2px;
}
.wrn-mega__link[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
.wrn-mega__link-body {
display: flex;
flex-direction: column;
min-width: 0;
}
.wrn-mega__link-label {
font-size: 0.88rem;
font-weight: 600;
}
.wrn-mega__link-description {
color: var(--wrn-color-text-muted);
font-size: 0.78rem;
line-height: 1.5;
}
.wrn-mega__footer {
margin: 1rem 0 0;
padding-top: 0.75rem;
border-top: 1px solid var(--wrn-color-border);
color: var(--wrn-color-text-muted);
font-size: 0.8rem;
}
/*
* On a phone the panel stops floating and becomes part of the flow. A
* pointer-opened overlay pinned to a trigger is unreachable on touch, and
* a 60rem grid has nowhere to go on a 360px screen.
*/
@media (max-width: 767px) {
.wrn-mega {
display: block;
}
.wrn-mega__panel {
position: static;
width: auto;
max-width: 100%;
box-shadow: none;
}
.wrn-mega__columns {
grid-template-columns: 1fr;
}
}
}
}
+132 -132
View File
@@ -59,7 +59,7 @@ component Modal {
// )
//
// The listener is on the modal root, so the event only ever closes the
// modal the dispatching element is actually inside -- no ids to wire up
// modal the dispatching element is actually inside -- no ids to wrn up
// and no way to close somebody else's modal by accident.
client function hideModal(reason, sourceEvent) {
visible = false
@@ -106,13 +106,13 @@ component Modal {
data-scrollable='{scrollable ? "true" : "false"}'
data-scroll='{scrollBehavior}'
data-destructive='{destructive ? "true" : "false"}'
class='wire-modal {class}'
class='wrn-modal {class}'
@wrnexus:modal:close='hideModal("api", event)'
>
{#if triggerLabel}
<button
type="button"
class="wire-modal__trigger"
class="wrn-modal__trigger"
aria-haspopup="dialog"
aria-expanded='{isOpen() ? "true" : "false"}'
@click='showModal(event)'
@@ -129,7 +129,7 @@ component Modal {
{/if}
<span
class="wire-modal__trigger-slot"
class="wrn-modal__trigger-slot"
@click='showModal(event)'
>
<slot
@@ -139,42 +139,42 @@ component Modal {
</span>
<div
class="wire-modal__layer"
class="wrn-modal__layer"
data-show="isOpen()"
role="presentation"
@keydown='handleKeydown(event)'
>
<button
type="button"
class="wire-modal__backdrop"
class="wrn-modal__backdrop"
aria-label='{closeLabel}'
@click='if (closeOnBackdrop) { cancelModal(event) }'
>
</button>
<section
class="wire-modal__panel"
class="wrn-modal__panel"
role="dialog"
aria-modal="true"
aria-label='{label || title}'
tabindex="-1"
>
<header
class="wire-modal__header"
class="wrn-modal__header"
>
<div
class="wire-modal__heading"
class="wrn-modal__heading"
>
{#if icon}
<span
class='wire-modal__icon {icon}'
class='wrn-modal__icon {icon}'
aria-hidden="true"
>
</span>
{/if}
<div
class="wire-modal__heading-copy"
class="wrn-modal__heading-copy"
>
<slot
name="header"
@@ -192,7 +192,7 @@ component Modal {
{#if showClose}
<button
type="button"
class="wire-modal__close"
class="wrn-modal__close"
aria-label='{closeLabel}'
@click='hideModal("close-button", event)'
>
@@ -215,17 +215,17 @@ component Modal {
</header>
<div
class="wire-modal__body"
class="wrn-modal__body"
>
<slot></slot>
</div>
{#if showFooter}
<footer
class="wire-modal__footer"
class="wrn-modal__footer"
>
<div
class="wire-modal__custom-footer"
class="wrn-modal__custom-footer"
>
<slot
name="footer"
@@ -234,12 +234,12 @@ component Modal {
</div>
<div
class="wire-modal__actions"
class="wrn-modal__actions"
>
{#if cancelLabel}
<button
type="button"
class="wire-modal__button wire-modal__button--secondary"
class="wrn-modal__button wrn-modal__button--secondary"
@click='cancelModal(event)'
>
{#if cancelIcon}
@@ -256,13 +256,13 @@ component Modal {
{#if confirmLabel}
<button
type="button"
class="wire-modal__button wire-modal__button--primary"
class="wrn-modal__button wrn-modal__button--primary"
disabled='{confirmDisabled || confirmLoading}'
@click='confirmModal(event)'
>
{#if confirmLoading}
<span
class="wire-modal__spinner"
class="wrn-modal__spinner"
aria-hidden="true"
>
</span>
@@ -285,63 +285,63 @@ component Modal {
}
style {
.wire-modal {
--modal-accent: var(--wire-color-primary);
--modal-soft: var(--wire-color-primary-soft);
--modal-contrast: var(--wire-color-primary-contrast);
.wrn-modal {
--modal-accent: var(--wrn-color-primary);
--modal-soft: var(--wrn-color-primary-soft);
--modal-contrast: var(--wrn-color-primary-contrast);
position: relative;
display: inline-flex;
}
.wire-modal[data-color="secondary"] {
--modal-accent: var(--wire-color-secondary);
--modal-soft: var(--wire-color-secondary-soft);
--modal-contrast: var(--wire-color-secondary-contrast);
.wrn-modal[data-color="secondary"] {
--modal-accent: var(--wrn-color-secondary);
--modal-soft: var(--wrn-color-secondary-soft);
--modal-contrast: var(--wrn-color-secondary-contrast);
}
/*
* Fallbacks below (the second var() argument): the theme token generator
* (packages/styles/src/theme.ts) only emits a real -contrast token for
* primary and secondary. info, success, and danger have no contrast
* token defined at all, so var(--wire-color-info-contrast) with no
* token defined at all, so var(--wrn-color-info-contrast) with no
* fallback resolves to nothing, and --modal-contrast becomes invalid --
* which made confirm-button and solid-panel text unreadable. White is a
* safe default against these saturated colors until the theme package
* defines real tokens for them.
*/
.wire-modal[data-color="info"] {
--modal-accent: var(--wire-color-info);
--modal-soft: var(--wire-color-info-soft);
--modal-contrast: var(--wire-color-info-contrast, white);
.wrn-modal[data-color="info"] {
--modal-accent: var(--wrn-color-info);
--modal-soft: var(--wrn-color-info-soft);
--modal-contrast: var(--wrn-color-info-contrast, white);
}
.wire-modal[data-color="success"] {
--modal-accent: var(--wire-color-success);
--modal-soft: var(--wire-color-success-soft);
--modal-contrast: var(--wire-color-success-contrast, white);
.wrn-modal[data-color="success"] {
--modal-accent: var(--wrn-color-success);
--modal-soft: var(--wrn-color-success-soft);
--modal-contrast: var(--wrn-color-success-contrast, white);
}
.wire-modal[data-color="warning"] {
--modal-accent: var(--wire-color-warning);
--modal-soft: var(--wire-color-warning-soft);
--modal-contrast: var(--wire-color-warning-text);
.wrn-modal[data-color="warning"] {
--modal-accent: var(--wrn-color-warning);
--modal-soft: var(--wrn-color-warning-soft);
--modal-contrast: var(--wrn-color-warning-text);
}
.wire-modal[data-color="danger"],
.wire-modal[data-destructive="true"] {
--modal-accent: var(--wire-color-danger);
--modal-soft: var(--wire-color-danger-soft);
--modal-contrast: var(--wire-color-on-danger, white);
.wrn-modal[data-color="danger"],
.wrn-modal[data-destructive="true"] {
--modal-accent: var(--wrn-color-danger);
--modal-soft: var(--wrn-color-danger-soft);
--modal-contrast: var(--wrn-color-on-danger, white);
}
.wire-modal__trigger,
.wire-modal__trigger-slot {
.wrn-modal__trigger,
.wrn-modal__trigger-slot {
display: inline-flex;
align-items: center;
gap: 0.55rem;
}
.wire-modal__trigger {
.wrn-modal__trigger {
appearance: none;
min-height: 2.55rem;
padding: 0.65rem 1rem;
@@ -356,22 +356,22 @@ component Modal {
transition: opacity 150ms ease, transform 150ms ease, box-shadow 150ms ease;
}
.wire-modal__trigger:hover {
.wrn-modal__trigger:hover {
opacity: 0.92;
}
.wire-modal__trigger:active {
.wrn-modal__trigger:active {
transform: scale(0.97);
}
.wire-modal__trigger:focus-visible {
.wrn-modal__trigger:focus-visible {
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--modal-accent) 40%, transparent);
}
/*
* Hidden by default so the SSR-rendered HTML never paints the layer before
* hydration runs. data-open on the wire-modal root is evaluated and
* hydration runs. data-open on the wrn-modal root is evaluated and
* serialized to a real true or false string at render time -- a plain
* bind, not the raw-expression data-show directive -- so this selector
* is correct on first paint with zero flash, with no dependency on
@@ -382,7 +382,7 @@ component Modal {
* animate -- a box that starts at display: none has no prior frame to
* transition from.
*/
.wire-modal__layer {
.wrn-modal__layer {
position: fixed;
inset: 0;
z-index: 1250;
@@ -395,24 +395,24 @@ component Modal {
transition: opacity 180ms ease, visibility 0s linear 180ms;
}
.wire-modal[data-open="true"] .wire-modal__layer {
.wrn-modal[data-open="true"] .wrn-modal__layer {
visibility: visible;
opacity: 1;
transition: opacity 180ms ease, visibility 0s linear 0s;
}
.wire-modal[data-placement="top"] .wire-modal__layer {
.wrn-modal[data-placement="top"] .wrn-modal__layer {
align-items: flex-start;
padding-top: clamp(1rem, 8vh, 5rem);
}
.wire-modal[data-scroll="page"] .wire-modal__layer {
.wrn-modal[data-scroll="page"] .wrn-modal__layer {
align-items: flex-start;
overflow-y: auto;
padding: 2.5rem 1rem;
}
.wire-modal__backdrop {
.wrn-modal__backdrop {
position: absolute;
inset: 0;
z-index: 0;
@@ -423,22 +423,22 @@ component Modal {
backdrop-filter: blur(9px);
}
.wire-modal__panel {
.wrn-modal__panel {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: min(32rem, calc(100vw - 2rem));
max-height: min(88vh, 52rem);
color: var(--wire-color-text);
color: var(--wrn-color-text);
background:
radial-gradient(
circle at 100% 0%,
color-mix(in srgb, var(--modal-accent) 9%, transparent),
transparent 34%
),
var(--wire-color-surface-raised);
border: 1px solid color-mix(in srgb, var(--modal-accent) 18%, var(--wire-color-border));
var(--wrn-color-surface-raised);
border: 1px solid color-mix(in srgb, var(--modal-accent) 18%, var(--wrn-color-border));
border-radius: 1.35rem;
box-shadow:
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
@@ -449,71 +449,71 @@ component Modal {
transition: opacity 180ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
.wire-modal[data-open="true"] .wire-modal__panel {
.wrn-modal[data-open="true"] .wrn-modal__panel {
opacity: 1;
transform: none;
}
.wire-modal[data-size="xs"] .wire-modal__panel {
.wrn-modal[data-size="xs"] .wrn-modal__panel {
width: min(19rem, calc(100vw - 2rem));
}
.wire-modal[data-size="sm"] .wire-modal__panel {
.wrn-modal[data-size="sm"] .wrn-modal__panel {
width: min(25rem, calc(100vw - 2rem));
}
.wire-modal[data-size="lg"] .wire-modal__panel {
.wrn-modal[data-size="lg"] .wrn-modal__panel {
width: min(44rem, calc(100vw - 2rem));
}
.wire-modal[data-size="xl"] .wire-modal__panel {
.wrn-modal[data-size="xl"] .wrn-modal__panel {
width: min(64rem, calc(100vw - 2rem));
}
.wire-modal[data-size="full"] .wire-modal__panel {
.wrn-modal[data-size="full"] .wrn-modal__panel {
width: calc(100vw - 2rem);
height: calc(100vh - 2rem);
max-height: none;
}
.wire-modal[data-scroll="page"] .wire-modal__panel {
.wrn-modal[data-scroll="page"] .wrn-modal__panel {
max-height: none;
}
.wire-modal[data-variant="soft"] .wire-modal__panel {
.wrn-modal[data-variant="soft"] .wrn-modal__panel {
background:
linear-gradient(145deg, var(--modal-soft), transparent 68%),
var(--wire-color-surface-raised);
var(--wrn-color-surface-raised);
}
.wire-modal[data-variant="outline"] .wire-modal__panel {
background: var(--wire-color-surface-raised);
.wrn-modal[data-variant="outline"] .wrn-modal__panel {
background: var(--wrn-color-surface-raised);
box-shadow: 0 24px 72px color-mix(in srgb, black 24%, transparent);
}
.wire-modal[data-variant="solid"] .wire-modal__panel {
.wrn-modal[data-variant="solid"] .wrn-modal__panel {
color: var(--modal-contrast);
background: var(--modal-accent);
border-color: color-mix(in srgb, white 20%, transparent);
}
.wire-modal__header {
.wrn-modal__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.4rem 1.4rem 1.15rem;
border-bottom: 1px solid var(--wire-color-border);
border-bottom: 1px solid var(--wrn-color-border);
}
.wire-modal__heading {
.wrn-modal__heading {
display: flex;
align-items: flex-start;
gap: 0.9rem;
min-width: 0;
}
.wire-modal__icon {
.wrn-modal__icon {
flex: 0 0 auto;
width: 1.3rem;
height: 1.3rem;
@@ -521,35 +521,35 @@ component Modal {
color: var(--modal-accent);
}
.wire-modal[data-variant="solid"] .wire-modal__icon {
.wrn-modal[data-variant="solid"] .wrn-modal__icon {
color: currentColor;
}
.wire-modal__heading-copy {
.wrn-modal__heading-copy {
display: grid;
gap: 0.3rem;
min-width: 0;
}
.wire-modal__heading-copy h2,
.wire-modal__heading-copy p {
.wrn-modal__heading-copy h2,
.wrn-modal__heading-copy p {
margin: 0;
}
.wire-modal__heading-copy h2 {
.wrn-modal__heading-copy h2 {
color: inherit;
font-size: 1.08rem;
font-weight: 650;
line-height: 1.3;
}
.wire-modal__heading-copy p {
color: var(--wire-color-text-muted);
.wrn-modal__heading-copy p {
color: var(--wrn-color-text-muted);
font-size: 0.8rem;
line-height: 1.55;
}
.wire-modal[data-variant="solid"] .wire-modal__heading-copy p {
.wrn-modal[data-variant="solid"] .wrn-modal__heading-copy p {
color: color-mix(in srgb, currentColor 76%, transparent);
}
@@ -559,7 +559,7 @@ component Modal {
* 2.15rem button with a ~2px content box -- which squeezed the icon to
* 0.4px wide and read as "the close button has no icon".
*/
.wire-modal__close {
.wrn-modal__close {
appearance: none;
display: inline-flex;
align-items: center;
@@ -568,7 +568,7 @@ component Modal {
padding: 0;
width: 2.15rem;
height: 2.15rem;
color: var(--wire-color-text-muted);
color: var(--wrn-color-text-muted);
background: transparent;
border: 1px solid transparent;
border-radius: 9999px;
@@ -582,34 +582,34 @@ component Modal {
* missing rather than subtle. Derive it from the panel contrast color
* instead, and give it a faint ring so it is unmistakably a control.
*/
.wire-modal[data-variant="solid"] .wire-modal__close {
.wrn-modal[data-variant="solid"] .wrn-modal__close {
color: color-mix(in srgb, currentColor 82%, transparent);
border-color: color-mix(in srgb, currentColor 35%, transparent);
}
.wire-modal[data-variant="solid"] .wire-modal__close:hover {
.wrn-modal[data-variant="solid"] .wrn-modal__close:hover {
color: currentColor;
background: color-mix(in srgb, black 18%, transparent);
border-color: color-mix(in srgb, currentColor 55%, transparent);
}
.wire-modal__close:hover {
color: var(--wire-color-text);
background: var(--wire-color-surface-soft);
.wrn-modal__close:hover {
color: var(--wrn-color-text);
background: var(--wrn-color-surface-soft);
}
.wire-modal__close:focus-visible {
.wrn-modal__close:focus-visible {
color: var(--modal-accent);
border-color: color-mix(in srgb, var(--modal-accent) 45%, transparent);
outline: none;
}
.wire-modal__close:active {
.wrn-modal__close:active {
transform: scale(0.92);
}
/* Never let the glyph be shrunk by the flex container. */
.wire-modal__close svg {
.wrn-modal__close svg {
flex: 0 0 auto;
width: 1rem;
height: 1rem;
@@ -628,46 +628,46 @@ component Modal {
* block scanner treats a quote as a string delimiter while it counts
* braces, so a stray one breaks parsing of the whole component.
*/
.wire-modal__body {
.wrn-modal__body {
flex: 1 1 auto;
min-height: 0;
padding: 1.4rem;
color: var(--wire-color-text);
color: var(--wrn-color-text);
}
/*
* :where() keeps this at the specificity of .wire-modal__body alone, so it
* :where() keeps this at the specificity of .wrn-modal__body alone, so it
* outranks a global element selector but still yields to any class the app
* puts on its own slot content (an error message, a muted caption). A
* plain .wire-modal__body p list would have quietly overridden those.
* plain .wrn-modal__body p list would have quietly overridden those.
*/
.wire-modal__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
.wrn-modal__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
color: inherit;
}
.wire-modal[data-variant="solid"] .wire-modal__body {
.wrn-modal[data-variant="solid"] .wrn-modal__body {
color: var(--modal-contrast);
}
.wire-modal[data-scrollable="true"] .wire-modal__body {
.wrn-modal[data-scrollable="true"] .wrn-modal__body {
overflow: auto;
overscroll-behavior: contain;
}
.wire-modal__footer {
.wrn-modal__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.4rem 1.4rem;
border-top: 1px solid var(--wire-color-border);
border-top: 1px solid var(--wrn-color-border);
}
.wire-modal__custom-footer:empty {
.wrn-modal__custom-footer:empty {
display: none;
}
.wire-modal__actions {
.wrn-modal__actions {
display: flex;
align-items: center;
justify-content: flex-end;
@@ -675,7 +675,7 @@ component Modal {
margin-left: auto;
}
.wire-modal__button {
.wrn-modal__button {
appearance: none;
display: inline-flex;
align-items: center;
@@ -691,17 +691,17 @@ component Modal {
transition: transform 150ms ease, opacity 150ms ease, border-color 150ms ease;
}
.wire-modal__button:active {
.wrn-modal__button:active {
transform: scale(0.985);
}
.wire-modal__button--secondary {
color: var(--wire-color-text);
.wrn-modal__button--secondary {
color: var(--wrn-color-text);
background: transparent;
border: 1px solid var(--wire-color-border);
border: 1px solid var(--wrn-color-border);
}
.wire-modal__button--primary {
.wrn-modal__button--primary {
color: var(--modal-contrast);
background: var(--modal-accent);
border: 1px solid transparent;
@@ -713,73 +713,73 @@ component Modal {
* Darken the fill slightly and add a light border so the button still
* reads as a distinct, clickable pill instead of blending into the panel.
*/
.wire-modal[data-variant="solid"] .wire-modal__button--primary {
.wrn-modal[data-variant="solid"] .wrn-modal__button--primary {
background: color-mix(in srgb, black 18%, var(--modal-accent));
border-color: color-mix(in srgb, white 32%, transparent);
box-shadow: 0 1px 0 color-mix(in srgb, white 12%, transparent) inset;
}
.wire-modal__button:disabled {
.wrn-modal__button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.wire-modal__spinner {
.wrn-modal__spinner {
width: 0.95rem;
height: 0.95rem;
border: 2px solid color-mix(in srgb, currentColor 35%, transparent);
border-top-color: currentColor;
border-radius: 50%;
animation: wire-modal-spin 750ms linear infinite;
animation: wrn-modal-spin 750ms linear infinite;
}
@keyframes wire-modal-spin {
@keyframes wrn-modal-spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 639px) {
.wire-modal__layer {
.wrn-modal__layer {
align-items: flex-end;
padding: 0;
}
.wire-modal__panel,
.wire-modal[data-size="xs"] .wire-modal__panel,
.wire-modal[data-size="sm"] .wire-modal__panel,
.wire-modal[data-size="lg"] .wire-modal__panel,
.wire-modal[data-size="xl"] .wire-modal__panel {
.wrn-modal__panel,
.wrn-modal[data-size="xs"] .wrn-modal__panel,
.wrn-modal[data-size="sm"] .wrn-modal__panel,
.wrn-modal[data-size="lg"] .wrn-modal__panel,
.wrn-modal[data-size="xl"] .wrn-modal__panel {
width: 100%;
max-height: 92vh;
border-radius: 1.25rem 1.25rem 0 0;
}
.wire-modal__footer {
.wrn-modal__footer {
align-items: stretch;
flex-direction: column;
}
.wire-modal__actions {
.wrn-modal__actions {
width: 100%;
}
.wire-modal__button {
.wrn-modal__button {
flex: 1 1 0;
}
}
@media (prefers-reduced-motion: reduce) {
.wire-modal__button,
.wire-modal__spinner,
.wire-modal__layer,
.wire-modal__panel,
.wire-modal__close {
.wrn-modal__button,
.wrn-modal__spinner,
.wrn-modal__layer,
.wrn-modal__panel,
.wrn-modal__close {
animation: none;
transition: none;
}
.wire-modal__panel {
.wrn-modal__panel {
transform: none;
}
}
+338 -6
View File
@@ -1,22 +1,354 @@
// Nav -- a navigation link list, flat or with submenus.
//
// <Nav items='[{"label":"Home","href":"/","value":"home"}]' active="home" />
//
// An item carrying its own items array becomes a submenu. Depth is capped at
// three levels: this template language has no component recursion, so each
// level is written out, and three covers any realistic navigation.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Nav {
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)
select(payload: { item: object; value: string })
}
props {
size: string = "default"
color: string = "primary"
label: string = "Nav"
size: string = "default"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
label: string = "Main"
collapsible: boolean = true
toggleLabel: string = "Menu"
class: string = ""
}
state expanded = false
functions {
shared function itemList() {
return Array.isArray(items) ? items : []
}
shared function childrenOf(item) {
return item && Array.isArray(item.items) ? item.items : []
}
shared function isActive(item) {
return Boolean(item.value) && item.value === active
}
shared function rovingAxis() {
return orientation === "vertical" ? "vertical" : "horizontal"
}
client function choose(item) {
if (item.disabled) {
return
}
output.select({ item: item, value: item.value || "" })
}
client function toggleMenu() {
expanded = !expanded
}
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--nav wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<nav
{...attrs}
data-ui-component="Nav"
class='wrn-nav {class}'
data-orientation='{orientation}'
data-color='{color}'
data-size='{size}'
data-expanded='{expanded}'
role="navigation"
aria-label='{label}'
>
<button
type="button"
class="wrn-nav__toggle"
data-show="collapsible"
aria-expanded='{expanded}'
aria-label='{toggleLabel}'
@click='toggleMenu()'
>
<span class="wrn-nav__toggle-bar" aria-hidden="true"></span>
<span class="wrn-nav__toggle-text">{toggleLabel}</span>
</button>
<ul class="wrn-nav__list" data-wrn-roving='{rovingAxis()}'>
{#each itemList() as item}
<li class="wrn-nav__item" data-has-children='{childrenOf(item).length > 0}'>
<a
class="wrn-nav__link"
href='{item.href || "#"}'
data-wrn-roving-item="true"
data-active='{isActive(item)}'
aria-current='{isActive(item) ? "page" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
@click='choose(item)'
>
<span
class='wrn-nav__icon {item.icon}'
data-show="item.icon"
aria-hidden="true"
></span>
<span class="wrn-nav__label">{item.label}</span>
<span class="wrn-nav__badge" data-show="item.badge">{item.badge}</span>
<span
class="wrn-nav__arrow"
data-show="childrenOf(item).length > 0"
aria-hidden="true"
>&#8250;</span>
</a>
<ul
class="wrn-nav__submenu"
data-wrn-anchored="true"
data-show="childrenOf(item).length > 0"
>
{#each childrenOf(item) as child}
<li class="wrn-nav__item" data-has-children='{childrenOf(child).length > 0}'>
<a
class="wrn-nav__link"
href='{child.href || "#"}'
data-active='{isActive(child)}'
aria-current='{isActive(child) ? "page" : "false"}'
aria-disabled='{child.disabled ? "true" : "false"}'
@click='choose(child)'
>
<span
class='wrn-nav__icon {child.icon}'
data-show="child.icon"
aria-hidden="true"
></span>
<span class="wrn-nav__label">{child.label}</span>
<span class="wrn-nav__badge" data-show="child.badge">{child.badge}</span>
<span
class="wrn-nav__arrow"
data-show="childrenOf(child).length > 0"
aria-hidden="true"
>&#8250;</span>
</a>
<ul
class="wrn-nav__submenu wrn-nav__submenu--level3"
data-wrn-anchored="true"
data-show="childrenOf(child).length > 0"
>
{#each childrenOf(child) as leaf}
<li class="wrn-nav__item">
<a
class="wrn-nav__link"
href='{leaf.href || "#"}'
data-active='{isActive(leaf)}'
aria-current='{isActive(leaf) ? "page" : "false"}'
aria-disabled='{leaf.disabled ? "true" : "false"}'
@click='choose(leaf)'
>
<span
class='wrn-nav__icon {leaf.icon}'
data-show="leaf.icon"
aria-hidden="true"
></span>
<span class="wrn-nav__label">{leaf.label}</span>
</a>
</li>
{/each}
</ul>
</li>
{/each}
</ul>
</li>
{/each}
</ul>
<slot />
</nav>
}
style {
.wrn-nav {
--nav-accent: var(--wrn-color-primary);
max-width: 100%;
}
.wrn-nav[data-color="secondary"] {
--nav-accent: var(--wrn-color-secondary);
}
.wrn-nav[data-color="success"] {
--nav-accent: var(--wrn-color-success);
}
.wrn-nav[data-color="danger"] {
--nav-accent: var(--wrn-color-danger);
}
.wrn-nav[data-color="info"] {
--nav-accent: var(--wrn-color-info);
}
.wrn-nav[data-size="sm"] {
font-size: 0.82rem;
}
.wrn-nav[data-size="lg"] {
font-size: 1rem;
}
.wrn-nav__list {
display: flex;
align-items: center;
gap: 0.25rem;
margin: 0;
padding: 0;
list-style: none;
}
.wrn-nav[data-orientation="vertical"] .wrn-nav__list {
flex-direction: column;
align-items: stretch;
}
.wrn-nav__item {
position: relative;
}
.wrn-nav__link {
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.45rem 0.7rem;
border-radius: var(--wrn-radius-sm);
color: var(--wrn-color-text-muted);
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
}
.wrn-nav__link:hover {
background: var(--wrn-color-surface-soft);
color: var(--wrn-color-text);
}
.wrn-nav__link:focus-visible {
outline: 2px solid var(--nav-accent);
outline-offset: 2px;
}
.wrn-nav__link[data-active="true"] {
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
color: var(--nav-accent);
}
.wrn-nav__link[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
.wrn-nav__badge {
padding: 0.05rem 0.4rem;
border-radius: 999px;
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
color: var(--nav-accent);
font-size: 0.72rem;
}
.wrn-nav__arrow {
display: inline-block;
transition: transform 160ms ease;
}
.wrn-nav__item:hover > .wrn-nav__link > .wrn-nav__arrow,
.wrn-nav__item:focus-within > .wrn-nav__link > .wrn-nav__arrow {
transform: rotate(90deg);
}
.wrn-nav__submenu {
position: absolute;
z-index: 30;
top: 100%;
left: 0;
min-width: 12rem;
margin: 0;
padding: 0.35rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-1);
list-style: none;
opacity: 0;
visibility: hidden;
transition: opacity 160ms ease;
}
.wrn-nav__item:hover > .wrn-nav__submenu,
.wrn-nav__item:focus-within > .wrn-nav__submenu {
opacity: 1;
visibility: visible;
}
.wrn-nav__submenu--level3 {
top: 0;
left: 100%;
}
.wrn-nav__toggle {
display: none;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.7rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
font: inherit;
font-size: 0.9rem;
cursor: pointer;
}
.wrn-nav__toggle-bar {
width: 1rem;
height: 2px;
background: currentColor;
box-shadow:
0 -5px 0 currentColor,
0 5px 0 currentColor;
}
/*
* On a phone the bar becomes a disclosure: submenus stop being floating
* overlays and stack inline, because a hover-opened overlay is
* unreachable on touch.
*/
@media (max-width: 767px) {
.wrn-nav__toggle {
display: inline-flex;
}
.wrn-nav__list {
flex-direction: column;
align-items: stretch;
}
.wrn-nav[data-expanded="false"] .wrn-nav__list {
display: none;
}
.wrn-nav__submenu,
.wrn-nav__submenu--level3 {
position: static;
opacity: 1;
visibility: visible;
border: 0;
box-shadow: none;
padding-left: 1rem;
}
}
}
}
+527 -22
View File
@@ -60,56 +60,56 @@ size: string = "default"
}
view {
<header class="wire-navbar wire-navbar--width-{maxWidth} wire-next--color-{color} wire-next--size-{size} {sticky ? 'wire-navbar--sticky' : ''} {class}" data-wrn-navbar data-open-on-hover="{openOnHover}">
<div class="wire-navbar__topbar" aria-label="{topbarLabel}">
<header class="wrn-navbar wrn-navbar--width-{maxWidth} wrn-component--color-{color} wrn-component--size-{size} {sticky ? 'wrn-navbar--sticky' : ''} {class}" data-wrn-navbar data-open-on-hover="{openOnHover}">
<div class="wrn-navbar__topbar" aria-label="{topbarLabel}">
<slot name="topbar" />
</div>
<div class="wire-navbar__main">
<a class="wire-navbar__brand" href="{brand.href || '/'}" aria-label="{brand.ariaLabel || brand.label || label}">
<div class="wrn-navbar__main">
<a class="wrn-navbar__brand" href="{brand.href || '/'}" aria-label="{brand.ariaLabel || brand.label || label}">
{#if brand.logo}
<img class="wire-navbar__brand-logo" src="{brand.logo}" alt="{brand.logoAlt || brand.label || ''}" width="{brand.logoWidth || ''}" height="{brand.logoHeight || ''}" />
<img class="wrn-navbar__brand-logo" src="{brand.logo}" alt="{brand.logoAlt || brand.label || ''}" width="{brand.logoWidth || ''}" height="{brand.logoHeight || ''}" />
{/if}
{#if brand.icon}
<span class="wire-navbar__brand-icon {brand.icon}" aria-hidden="true"></span>
<span class="wrn-navbar__brand-icon {brand.icon}" aria-hidden="true"></span>
{/if}
{#if brand.label || brand.description}
<span class="wire-navbar__brand-copy">
<span class="wrn-navbar__brand-copy">
{#if brand.label}<strong>{brand.label}</strong>{/if}
{#if brand.description}<small>{brand.description}</small>{/if}
</span>
{/if}
</a>
<button type="button" class="wire-navbar__toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleNavigation()">
<button type="button" class="wrn-navbar__toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleNavigation()">
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
</button>
<div class="wire-navbar__collapse {mobileOpen ? 'is-open' : ''}">
<nav class="wire-navbar__menus" aria-label="{label}">
<div class="wrn-navbar__collapse {mobileOpen ? 'is-open' : ''}">
<nav class="wrn-navbar__menus" aria-label="{label}" data-wrn-roving="horizontal">
{#each items as item}
{#if item.children && item.children.length}
<details class="wire-navbar__dropdown wire-navbar__dropdown--{item.type || 'dropdown'}" name="wire-navbar-menu" @toggle="toggleDropdown(event, item)">
<summary aria-current="{isItemActive(item) ? 'page' : ''}">
<details class="wrn-navbar__dropdown wrn-navbar__dropdown--{item.type || 'dropdown'}" name="wrn-navbar-menu" @toggle="toggleDropdown(event, item)">
<summary data-wrn-roving-item="true" aria-current="{isItemActive(item) ? 'page' : 'false'}">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span>{item.label}</span>
<span class="wire-navbar__chevron" aria-hidden="true"></span>
<span class="wrn-navbar__chevron" aria-hidden="true"></span>
</summary>
<div class="wire-navbar__panel wire-navbar__panel--columns-{item.columns || 1}">
{#if item.description}<p class="wire-navbar__panel-intro">{item.description}</p>{/if}
<div class="wrn-navbar__panel wrn-navbar__panel--columns-{item.columns || 1}">
{#if item.description}<p class="wrn-navbar__panel-intro">{item.description}</p>{/if}
{#each item.children as child}
<div class="wire-navbar__group">
<div class="wrn-navbar__group">
{#if child.children && child.children.length}
{#if child.label}<strong class="wire-navbar__group-title">{child.label}</strong>{/if}
{#if child.label}<strong class="wrn-navbar__group-title">{child.label}</strong>{/if}
{#if child.description}<small>{child.description}</small>{/if}
{#each child.children as nested}
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : ''}" @click="selectItem(nested, 3)">
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @click="selectItem(nested, 3)">
{#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
<span><strong>{nested.label}</strong>{#if nested.description}<small>{nested.description}</small>{/if}</span>
</a>
{/each}
{:else}
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : ''}" @click="selectItem(child, 2)">
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : 'false'}" @click="selectItem(child, 2)">
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
<span><strong>{child.label}</strong>{#if child.description}<small>{child.description}</small>{/if}</span>
</a>
@@ -119,7 +119,7 @@ size: string = "default"
</div>
</details>
{:else}
<a class="wire-navbar__menu-link" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : ''}" @click="selectItem(item, 1)">
<a class="wrn-navbar__menu-link" data-wrn-roving-item="true" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : 'false'}" @click="selectItem(item, 1)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span>{item.label}</span>
</a>
@@ -127,9 +127,9 @@ size: string = "default"
{/each}
</nav>
<div class="wire-navbar__actions">
<div class="wrn-navbar__actions">
{#each actions as item}
<a class="wire-navbar__action wire-navbar__action--{item.variant || 'link'}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" @click="selectAction(item)">
<a class="wrn-navbar__action wrn-navbar__action--{item.variant || 'link'}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" @click="selectAction(item)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span>{item.label}</span>
</a>
@@ -140,4 +140,509 @@ size: string = "default"
</div>
</header>
}
style {
/* Full application navigation */
.wrn-navbar {
position: relative;
z-index: 40;
width: 100%;
color: var(--wrn-color-text);
background: color-mix(in srgb, var(--wrn-color-surface) 96%, transparent);
border-bottom: 1px solid var(--wrn-color-border);
}
.wrn-navbar--sticky {
position: sticky;
top: 0;
backdrop-filter: blur(16px);
}
.wrn-navbar__topbar:empty,
.wrn-navbar__topbar:not(:has(*)) {
display: none;
}
.wrn-navbar__topbar:not(:empty) {
display: flex;
min-height: 2.25rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.35rem clamp(1rem, 4vw, 3rem);
color: var(--wrn-color-muted);
background: var(--wrn-color-surface-2);
border-bottom: 1px solid var(--wrn-color-border);
font-size: 0.75rem;
font-weight: 400;
}
.wrn-navbar__main {
display: flex;
min-height: 4.25rem;
width: min(100%, 90rem);
margin-inline: auto;
padding: 0.75rem clamp(1rem, 4vw, 3rem);
align-items: center;
gap: 1.5rem;
}
.wrn-navbar--width-full .wrn-navbar__main {
width: 100%;
max-width: none;
}
.wrn-navbar--width-compact .wrn-navbar__main {
width: min(86%, 80rem);
}
.wrn-navbar__brand,
.wrn-navbar__menu-link,
.wrn-navbar__dropdown summary,
.wrn-navbar__action,
.wrn-navbar__panel a {
color: inherit;
text-decoration: none;
}
.wrn-navbar__brand {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 0.75rem;
flex: 0 0 auto;
}
.wrn-navbar__brand-logo {
display: block;
width: auto;
max-width: 11rem;
height: 2.5rem;
object-fit: contain;
}
.wrn-navbar__brand-icon {
width: 2.5rem;
height: 2.5rem;
color: var(--wrn-component-color, var(--wrn-color-primary));
}
.wrn-navbar__brand-copy {
display: grid;
min-width: 0;
line-height: 1.2;
}
.wrn-navbar__brand-copy strong {
font-size: 0.9rem;
font-weight: 600;
}
.wrn-navbar__brand-copy small {
margin-top: 0.2rem;
color: var(--wrn-color-muted);
font-size: 0.6875rem;
font-weight: 400;
}
.wrn-navbar__collapse,
.wrn-navbar__menus,
.wrn-navbar__actions {
display: flex;
align-items: center;
}
.wrn-navbar__collapse {
min-width: 0;
flex: 1;
gap: 1rem;
}
.wrn-navbar__menus {
justify-content: center;
flex: 1;
gap: 0.25rem;
}
.wrn-navbar__actions {
justify-content: flex-end;
gap: 0.5rem;
}
.wrn-navbar__menu-link,
.wrn-navbar__dropdown > summary,
.wrn-navbar__action {
display: inline-flex;
min-height: 2.75rem;
padding: 0.65rem 0.8rem;
align-items: center;
gap: 0.45rem;
border-radius: 0.65rem;
cursor: pointer;
font-size: 0.8125rem;
font-weight: 500;
white-space: nowrap;
}
.wrn-navbar__menu-link:hover,
.wrn-navbar__dropdown > summary:hover {
color: var(--wrn-color-text);
background: var(--wrn-color-surface-2);
}
.wrn-navbar__menu-link[aria-current="page"],
.wrn-navbar__dropdown > summary[aria-current="page"] {
color: var(--wrn-component-color, var(--wrn-color-primary));
font-weight: 600;
background: color-mix(
in srgb,
var(--wrn-component-color, var(--wrn-color-primary)) 12%,
transparent
);
box-shadow: inset 0 -2px 0 var(--wrn-component-color, var(--wrn-color-primary));
}
.wrn-navbar__menu-link[aria-current="page"]:hover,
.wrn-navbar__dropdown > summary[aria-current="page"]:hover {
color: var(--wrn-component-color, var(--wrn-color-primary));
background: color-mix(
in srgb,
var(--wrn-component-color, var(--wrn-color-primary)) 16%,
transparent
);
}
.wrn-navbar__dropdown {
position: relative;
}
.wrn-navbar__dropdown > summary {
list-style: none;
}
.wrn-navbar__dropdown > summary::-webkit-details-marker {
display: none;
}
.wrn-navbar__chevron {
width: 0.45rem;
height: 0.45rem;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: rotate(45deg) translateY(-0.15rem);
transition: transform var(--wrn-motion-fast, 150ms) ease;
}
.wrn-navbar__dropdown[open] .wrn-navbar__chevron {
transform: rotate(225deg) translate(-0.1rem, -0.1rem);
}
.wrn-navbar__panel {
position: absolute;
top: calc(100% + 0.55rem);
left: 50%;
display: grid;
width: max-content;
min-width: 15rem;
max-width: min(90vw, 64rem);
padding: 0.65rem;
gap: 0.45rem;
border: 1px solid var(--wrn-color-border);
border-radius: 0.9rem;
color: var(--wrn-color-text);
background: var(--wrn-color-surface);
box-shadow: 0 18px 48px rgb(15 23 42 / 0.16);
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
transform-origin: top center;
transition:
opacity 180ms ease,
transform 180ms cubic-bezier(0.22, 1, 0.36, 1),
display 180ms allow-discrete;
}
.wrn-navbar__panel::before {
position: absolute;
right: 0;
bottom: 100%;
left: 0;
height: 0.65rem;
content: "";
}
.wrn-navbar__dropdown:not([open]) > .wrn-navbar__panel {
display: none;
opacity: 0;
transform: translateX(-50%) translateY(-0.5rem) scale(0.98);
}
@starting-style {
.wrn-navbar__dropdown[open] > .wrn-navbar__panel {
opacity: 0;
transform: translateX(-50%) translateY(-0.5rem) scale(0.98);
}
}
.wrn-navbar__dropdown--mega {
position: static;
}
.wrn-navbar__dropdown--mega .wrn-navbar__panel {
right: auto;
left: 50%;
width: min(calc(100vw - 2rem), 64rem);
}
.wrn-navbar__panel--columns-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.wrn-navbar__panel--columns-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.wrn-navbar__panel--columns-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.wrn-navbar__panel-intro {
grid-column: 1 / -1;
margin: 0;
padding: 0.5rem 0.65rem;
color: var(--wrn-color-muted);
font-size: 0.8125rem;
font-weight: 400;
}
.wrn-navbar__group {
display: grid;
align-content: start;
gap: 0.25rem;
}
.wrn-navbar__group-title {
padding: 0.55rem 0.65rem 0.25rem;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.wrn-navbar__group > small {
padding: 0 0.65rem 0.4rem;
color: var(--wrn-color-muted);
font-size: 0.7rem;
font-weight: 400;
}
.wrn-navbar__panel a {
display: flex;
min-height: 2.75rem;
padding: 0.65rem;
align-items: flex-start;
gap: 0.65rem;
border-radius: 0.6rem;
font-size: 0.8rem;
font-weight: 450;
}
.wrn-navbar__panel a:hover {
background: var(--wrn-color-surface-2);
}
.wrn-navbar__panel a span:last-child {
display: grid;
gap: 0.15rem;
}
.wrn-navbar__panel a strong {
font-weight: 500;
}
.wrn-navbar__panel a small {
color: var(--wrn-color-muted);
font-size: 0.7rem;
font-weight: 400;
}
.wrn-navbar__action {
border: 1px solid transparent;
}
.wrn-navbar__action--primary {
color: var(--wrn-color-on-primary, #fff);
background: var(--wrn-component-color, var(--wrn-color-primary));
font-weight: 600;
}
.wrn-navbar__action--outline {
border-color: var(--wrn-color-border);
}
.wrn-navbar__toggle {
display: none;
width: 2.75rem;
height: 2.75rem;
margin-left: auto;
padding: 0.65rem;
border: 1px solid var(--wrn-color-border);
border-radius: 0.65rem;
color: inherit;
background: var(--wrn-color-surface);
}
.wrn-navbar__toggle span {
display: block;
height: 2px;
margin: 0.25rem 0;
background: currentColor;
}
@media (max-width: 900px) {
.wrn-navbar--width-compact .wrn-navbar__main {
width: 100%;
}
.wrn-navbar__toggle {
display: block;
}
.wrn-navbar__collapse {
position: absolute;
top: 100%;
right: 0;
left: 0;
display: none;
padding: 0.75rem 1rem 1rem;
align-items: stretch;
flex-direction: column;
background: var(--wrn-color-surface);
border-bottom: 1px solid var(--wrn-color-border);
box-shadow: 0 16px 32px rgb(15 23 42 / 0.12);
max-height: calc(100dvh - 4rem);
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
.wrn-navbar__collapse.is-open {
display: flex;
}
.wrn-navbar__menus,
.wrn-navbar__actions {
width: 100%;
align-items: stretch;
flex-direction: column;
}
.wrn-navbar__menu-link,
.wrn-navbar__dropdown > summary,
.wrn-navbar__action {
width: 100%;
}
.wrn-navbar__panel,
.wrn-navbar__dropdown--mega .wrn-navbar__panel {
position: static;
width: 100%;
max-width: none;
margin-top: 0.25rem;
box-shadow: none;
transform: none;
overflow: hidden;
animation: wrn-navbar-mobile-panel 180ms ease both;
}
.wrn-navbar__dropdown:not([open]) > .wrn-navbar__panel,
.wrn-navbar__dropdown[open] > .wrn-navbar__panel {
transform: none;
}
.wrn-navbar__panel[class*="wrn-navbar__panel--columns-"] {
grid-template-columns: 1fr;
}
.wrn-navbar__panel::before {
display: none;
}
}
@media (max-width: 600px) {
.wrn-navbar__brand-copy small {
display: none;
}
}
@keyframes wrn-navbar-mobile-panel {
from {
opacity: 0;
transform: translateY(-0.35rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Shared component foundation. */
.wrn-component {
--wrn-component-color: var(--wrn-color-primary);
--wrn-component-scale: 1;
margin: 0;
color: var(--wrn-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wrn-component--color-primary {
--wrn-component-color: var(--wrn-color-primary);
}
.wrn-component--color-secondary {
--wrn-component-color: var(--wrn-color-secondary);
}
.wrn-component--color-success {
--wrn-component-color: var(--wrn-color-success);
}
.wrn-component--color-warning {
--wrn-component-color: var(--wrn-color-warning);
}
.wrn-component--color-danger {
--wrn-component-color: var(--wrn-color-danger);
}
.wrn-component--color-info {
--wrn-component-color: var(--wrn-color-info);
}
.wrn-component--size-xs {
--wrn-component-scale: 0.78;
}
.wrn-component--size-sm {
--wrn-component-scale: 0.88;
}
.wrn-component--size-default,
.wrn-component--size-md {
--wrn-component-scale: 1;
}
.wrn-component--size-lg {
--wrn-component-scale: 1.14;
}
.wrn-component--size-xl {
--wrn-component-scale: 1.28;
}
.wrn-component:not(.wrn-btn) {
font-size: calc(1em * var(--wrn-component-scale));
}
.wrn-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wrn-component-color);
}
.wrn-component p {
margin: 0;
color: var(--wrn-color-muted);
line-height: 1.6;
}
}
}
+277 -9
View File
@@ -1,23 +1,291 @@
// Pagination -- page controls over a known total.
//
// <Pagination page={2} pageSize={10} total={137} variant="numbered" />
//
// The component owns no data. It reports the requested page through its
// change output and lets the caller fetch or slice.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component Pagination {
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)
previous(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
next(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
change(payload: { page: number; pageSize: number })
previous(payload: { page: number })
next(payload: { page: number })
}
props {
size: string = "default"
color: string = "primary"
size: string = "default"
page: number = 1
pageSize: number = 10
total: number = 0
variant: string = "compact"
siblingCount: number = 1
showSummary: boolean = true
label: string = "Pagination"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
previousLabel: string = "Previous"
nextLabel: string = "Next"
class: string = ""
}
functions {
shared function lastPage() {
var size = Number(pageSize) > 0 ? Number(pageSize) : 10
var count = Number(total) > 0 ? Number(total) : 0
return Math.max(1, Math.ceil(count / size))
}
// Out-of-range values arrive routinely: page travels as an HTML attribute
// and callers compute it from data that may have shrunk underneath them.
shared function currentPage() {
var value = Number(page)
if (!value || value < 1) {
return 1
}
return Math.min(value, lastPage())
}
shared function firstShown() {
if (Number(total) < 1) {
return 0
}
return (currentPage() - 1) * (Number(pageSize) || 10) + 1
}
shared function lastShown() {
return Math.min(currentPage() * (Number(pageSize) || 10), Number(total) || 0)
}
shared function pageNumbers() {
var last = lastPage()
var current = currentPage()
var siblings = Math.max(0, Number(siblingCount) || 0)
var start = Math.max(1, current - siblings)
var end = Math.min(last, current + siblings)
var pages = []
if (start > 1) {
pages.push({ value: 1, label: "1", gap: false })
if (start > 2) {
pages.push({ value: 0, label: "...", gap: true })
}
}
for (var index = start; index <= end; index += 1) {
pages.push({ value: index, label: String(index), gap: false })
}
if (end < last) {
if (end < last - 1) {
pages.push({ value: 0, label: "...", gap: true })
}
pages.push({ value: last, label: String(last), gap: false })
}
return pages
}
client function goToPage(target) {
var next = Math.min(Math.max(1, Number(target) || 1), lastPage())
output.change({ page: next, pageSize: Number(pageSize) || 10 })
}
client function goPrevious() {
var target = Math.max(1, currentPage() - 1)
output.previous({ page: target })
output.change({ page: target, pageSize: Number(pageSize) || 10 })
}
client function goNext() {
var target = Math.min(lastPage(), currentPage() + 1)
output.next({ page: target })
output.change({ page: target, pageSize: Number(pageSize) || 10 })
}
}
view {
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--pagination wire-next--{orientation} {class}" aria-label="{label}">
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
<nav
{...attrs}
data-ui-component="Pagination"
class='wrn-pagination {class}'
data-variant='{variant}'
data-color='{color}'
data-size='{size}'
role="navigation"
aria-label='{label}'
>
<p class="wrn-pagination__summary" data-show="showSummary">
{firstShown()} to {lastShown()} of {total}
</p>
<div class="wrn-pagination__controls">
<button
type="button"
class="wrn-pagination__step"
aria-label='{previousLabel}'
@click='goPrevious()'
>
<span class="wrn-pagination__step-icon" aria-hidden="true">&#8249;</span>
<span class="wrn-pagination__step-label">{previousLabel}</span>
</button>
<ol class="wrn-pagination__pages" data-show="variant === 'numbered'">
{#each pageNumbers() as entry}
<li class="wrn-pagination__slot">
<span class="wrn-pagination__gap" data-show="entry.gap">{entry.label}</span>
<button
type="button"
class="wrn-pagination__page"
data-show="!entry.gap"
data-active='{entry.value === currentPage()}'
aria-current='{entry.value === currentPage() ? "page" : "false"}'
@click='goToPage(entry.value)'
>
{entry.label}
</button>
</li>
{/each}
</ol>
<p class="wrn-pagination__compact" data-show="variant !== 'numbered'">
{currentPage()} / {lastPage()}
</p>
<button
type="button"
class="wrn-pagination__step"
aria-label='{nextLabel}'
@click='goNext()'
>
<span class="wrn-pagination__step-label">{nextLabel}</span>
<span class="wrn-pagination__step-icon" aria-hidden="true">&#8250;</span>
</button>
</div>
<slot />
</nav>
}
style {
.wrn-pagination {
--pagination-accent: var(--wrn-color-primary);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
max-width: 100%;
}
.wrn-pagination[data-color="secondary"] {
--pagination-accent: var(--wrn-color-secondary);
}
.wrn-pagination[data-color="success"] {
--pagination-accent: var(--wrn-color-success);
}
.wrn-pagination[data-color="danger"] {
--pagination-accent: var(--wrn-color-danger);
}
.wrn-pagination[data-color="info"] {
--pagination-accent: var(--wrn-color-info);
}
.wrn-pagination[data-size="sm"] {
font-size: 0.82rem;
}
.wrn-pagination[data-size="lg"] {
font-size: 1rem;
}
.wrn-pagination__summary {
margin: 0;
color: var(--wrn-color-text-muted);
font-size: 0.82rem;
}
.wrn-pagination__controls {
display: flex;
align-items: center;
gap: 0.35rem;
flex-wrap: wrap;
}
.wrn-pagination__pages {
display: flex;
align-items: center;
gap: 0.25rem;
margin: 0;
padding: 0;
list-style: none;
}
.wrn-pagination__slot {
display: inline-flex;
}
.wrn-pagination__page,
.wrn-pagination__step {
appearance: none;
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-width: 2.25rem;
justify-content: center;
padding: 0.4rem 0.6rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm);
background: var(--wrn-color-surface);
color: var(--wrn-color-text);
font: inherit;
font-size: 0.85rem;
cursor: pointer;
}
.wrn-pagination__page:hover,
.wrn-pagination__step:hover {
background: var(--wrn-color-surface-soft);
}
.wrn-pagination__page:focus-visible,
.wrn-pagination__step:focus-visible {
outline: 2px solid var(--pagination-accent);
outline-offset: 2px;
}
.wrn-pagination__page[data-active="true"] {
border-color: var(--pagination-accent);
background: var(--pagination-accent);
color: var(--wrn-color-primary-contrast);
font-weight: 700;
}
.wrn-pagination__gap {
padding: 0 0.35rem;
color: var(--wrn-color-text-muted);
}
.wrn-pagination__compact {
margin: 0;
padding: 0 0.5rem;
color: var(--wrn-color-text-muted);
font-size: 0.85rem;
}
/* Below the small breakpoint the word labels crowd the arrows out. */
@media (max-width: 639px) {
.wrn-pagination {
justify-content: center;
}
.wrn-pagination__step-label {
display: none;
}
.wrn-pagination__summary {
width: 100%;
text-align: center;
}
}
}
}

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