Compare commits

..
48 Commits
Author SHA1 Message Date
Clintchiz 84e16ebc41 chore(release): publish ui 0.8.56
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:36:04 +05:30
Clintchiz 7993a303ac fix(ui): preserve viewport anchoring for centered navbar
Quality / quality (ubuntu-latest) (push) Failing after 9m57s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:35:43 +05:30
Clintchiz a99290a216 chore(release): publish ui 0.8.55
Quality / quality (ubuntu-latest) (push) Failing after 10m16s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:34:17 +05:30
Clintchiz c648efc240 fix(ui): contain long mega menu labels
Quality / quality (ubuntu-latest) (push) Failing after 10m41s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:33:59 +05:30
Clintchiz decbee2fd1 chore(release): publish ui 0.8.54
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:31:38 +05:30
Clintchiz e62e7f5d27 fix(ui): center desktop navbar menus
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:31:17 +05:30
Clintchiz b2c74a3a29 chore(release): publish ui 0.8.53
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:59:43 +05:30
Clintchiz fe86570f45 fix(ui): render dynamic tab panels and ids
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:59:10 +05:30
Clintchiz bc1211ea8b chore(release): publish ui 0.8.52
Quality / quality (ubuntu-latest) (push) Failing after 10m0s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:37:55 +05:30
Clintchiz d2e24eb089 feat(ui): refine timeline and blockquote layouts
Quality / quality (ubuntu-latest) (push) Failing after 10m2s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:37:29 +05:30
Clintchiz e71885d51c chore(release): publish ui 0.8.51
Quality / quality (ubuntu-latest) (push) Failing after 9m56s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:31:26 +05:30
Clintchiz 138b5d0950 fix(ui): prevent full bleed hero overflow
Quality / quality (ubuntu-latest) (push) Failing after 10m42s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:30:34 +05:30
Clintchiz 7e1eb899b3 fix(ui): align stats surface within max width
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 16:54:23 +05:30
Clintchiz 3f6612cfe7 fix(ui): honor stats bar desktop gutters
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 16:50:10 +05:30
Clintchiz 510b3ba777 fix(ui): keep breadcrumb separators visible
Quality / quality (ubuntu-latest) (push) Failing after 10m21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 16:34:44 +05:30
Clintchiz 1ff2c3339c fix(ui): render active breadcrumb pages
Quality / quality (ubuntu-latest) (push) Failing after 10m0s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 16:21:53 +05:30
Clintchiz 027873459d fix(ui): collapse empty page header slots
Quality / quality (ubuntu-latest) (push) Failing after 9m57s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 15:56:57 +05:30
Clintchiz d0bccfe37e fix(csr): suppress inactive theme token warnings
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 15:48:48 +05:30
Clintchiz 120889060a fix(ui): normalize form field label sizing
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 15:25:24 +05:30
Clintchiz 26ec18c24b chore(editor): release vscode extension 0.8.11
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 15:22:15 +05:30
Clintchiz 7658c5d499 fix(editor): type nested attribute expressions
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 15:16:38 +05:30
Clintchiz 3cb6ba1233 fix(ui): render navbar brand prop outside slot
Quality / quality (ubuntu-latest) (push) Failing after 10m32s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 14:00:46 +05:30
Clintchiz 64db7da08d fix(ui): refine responsive catalog navigation
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 13:46:19 +05:30
Clintchiz 9f6552ea62 fix(ui): separate catalog icon masks from tiles
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 23:30:33 +05:30
Clintchiz 5bc391e74f fix(ui): inherit catalog icon contrast
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 23:23:43 +05:30
Clintchiz 3327ec0d37 refactor(ui): clarify catalog navigation hierarchy
Quality / quality (ubuntu-latest) (push) Failing after 10m18s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 23:16:22 +05:30
Clintchiz ceb865f4f1 fix(ui): prevent navigation surface overflow
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 23:08:00 +05:30
Clintchiz 45309b3f72 refactor(ui): compact mega menu layouts
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 23:06:01 +05:30
Clintchiz 8f3a13c854 fix(ui): render only supplied mega menu metadata
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:57:13 +05:30
Clintchiz eca4447dcf fix(ui): stabilize catalog mega menu interaction
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:50:41 +05:30
Clintchiz 904bca21b2 refactor(ui): refine catalog mega menu navigation
Quality / quality (ubuntu-latest) (push) Failing after 10m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:40:58 +05:30
Clintchiz b98d417185 fix(ui): eliminate mega menu flash and lag
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:33:02 +05:30
Clintchiz 7d737ec0a0 fix(ui): make catalog mega menus interactive
Quality / quality (ubuntu-latest) (push) Failing after 6m15s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:18:30 +05:30
Clintchiz f5538ed621 fix(ui): keep mega menu open across hover gap
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:08:46 +05:30
Clintchiz d2d830e7e2 fix(ui): pin navbar mega panels to viewport
Quality / quality (ubuntu-latest) (push) Failing after 12m5s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:56:27 +05:30
Clintchiz 0618782355 fix(ui): make mega menus work under strict CSP
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:52:25 +05:30
Clintchiz 9067e68f69 fix(ui): import navbar mega menu dependency
Quality / quality (ubuntu-latest) (push) Failing after 10m39s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:37:26 +05:30
Clintchiz 9d64ec60c4 feat(ui): add typed navbar menu items
Quality / quality (ubuntu-latest) (push) Failing after 11m12s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:36:46 +05:30
Clintchiz fc1eea3939 chore(editor): refresh language server bundle hash
Quality / quality (ubuntu-latest) (push) Failing after 10m8s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:25:53 +05:30
Clintchiz 4ec5dad60a fix(formatter): expand large structured prop defaults
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:25:07 +05:30
Clintchiz a1a2947910 feat(ui): expand mega menu layout system
Quality / quality (ubuntu-latest) (push) Failing after 11m2s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:15:50 +05:30
Clintchiz 270040fb5f feat(ui): add in-flow floating navbar
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 19:47:47 +05:30
Clintchiz f9e285b182 feat(ui): own responsive marketing layout APIs
Quality / quality (ubuntu-latest) (push) Failing after 10m42s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 19:44:26 +05:30
Clintchiz 3b402b4e36 fix(ui): collapse empty navbar topbar and add CTA spacing
Quality / quality (ubuntu-latest) (push) Failing after 11m48s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 18:28:39 +05:30
Clintchiz fa535575c5 fix(cli): resolve package updates on Windows
Quality / quality (ubuntu-latest) (push) Failing after 9m56s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 17:23:35 +05:30
Clintchiz 46ef4b775b fix(ui): harden public page components
Quality / quality (ubuntu-latest) (push) Failing after 10m19s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 17:13:14 +05:30
Clintchiz 7a36c1905f fix(cli): handle version flags before workspace discovery
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 15:03:38 +05:30
Clintchiz 1a8c75a958 chore: refresh editor language server bundle 2026-08-24 14:58:03 +05:30
70 changed files with 2528 additions and 224 deletions
+6 -6
View File
@@ -292,7 +292,7 @@
},
"packages/cli": {
"name": "@wrnexus/cli",
"version": "0.8.62",
"version": "0.8.64",
"bin": {
"wrnexus": "src/index.ts",
},
@@ -320,7 +320,7 @@
},
"packages/compiler": {
"name": "@wrnexus/compiler",
"version": "0.8.21",
"version": "0.8.22",
"dependencies": {
"@wrnexus/csr": "workspace:*",
"@wrnexus/store": "workspace:*",
@@ -342,7 +342,7 @@
},
"packages/csr": {
"name": "@wrnexus/csr",
"version": "0.8.33",
"version": "0.8.35",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
@@ -668,7 +668,7 @@
},
"packages/syntax": {
"name": "@wrnexus/syntax",
"version": "0.8.16",
"version": "0.8.18",
},
"packages/test": {
"name": "@wrnexus/test",
@@ -680,7 +680,7 @@
},
"packages/typecheck": {
"name": "@wrnexus/typecheck",
"version": "0.8.15",
"version": "0.8.16",
"dependencies": {
"@wrnexus/syntax": "workspace:*",
"typescript": "^6.0.3",
@@ -688,7 +688,7 @@
},
"packages/ui": {
"name": "@wrnexus/ui",
"version": "0.8.23",
"version": "0.8.56",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
+30 -30
View File
@@ -4,22 +4,22 @@
"artifacts": {
"packages/ui/components/Accordion.wrn": "09251f76a2f9385e97eb67e06c0cf868e1771ac5f420647221bec5160287034b",
"packages/ui/components/AdvancedSelect.wrn": "b952b894a5b35050fc2f6e43c4182101eeee05c75662e2aca4abfe61512b305b",
"packages/ui/components/AnnouncementBar.wrn": "cb66b8d0619ee798a115b43f0ad93fda93dfb46571589e7a9f0a8d65ec3c84c9",
"packages/ui/components/AnnouncementBar.wrn": "5b3219db555e0fcdb94ec41dc18186ee1b7c0cf9c260469f7fb1fc43435c28c4",
"packages/ui/components/AuthForm.wrn": "46a3e1db1c9e69efc52473cbf2fababdcbfe7144e7571469638d2c3addd4d7f0",
"packages/ui/components/AuthSplitLayout.wrn": "724938921bc38cc5a0422de69c237b630f099f2df94c66977362b048e4a420a5",
"packages/ui/components/AvatarGroup.wrn": "30b702e06a1adff8b9bae32820206020e086a69560cbdcbadb19f04f25951f97",
"packages/ui/components/BackToTop.wrn": "5b760ec7941ffd8e58c5dedae1b322ee46e072e927533d7ec077b511a816e436",
"packages/ui/components/BackToTop.wrn": "d4b8d1fd37ff26c89b4276ce54ce6ac9581dc2ff8bbcbe160e072778bba76eab",
"packages/ui/components/Blockquote.wrn": "3761bf13babb6eb07ce7392258ee439ef5478064cf589eff63f945b950e00d2e",
"packages/ui/components/Breadcrumb.wrn": "9f6c23550ebfc635c36ca8660953775170b4a471c0cd0206b9a7ba760994980a",
"packages/ui/components/ButtonGroup.wrn": "fc23565ad56bd8483fcb777d9e02ccba2f9ea476c085754d42503bb83b718f72",
"packages/ui/components/CTASection.wrn": "d0665f2aa33eea95e6f84120f739b3dd9c8723c882138bcb1a7f80dcd7909904",
"packages/ui/components/Card.wrn": "3cc2ff238bda279c169026a462236144ad11ec0046deef00ab00e001a07e8a4c",
"packages/ui/components/CTASection.wrn": "f6872b980432d5ee9d88388250be9838ccce51bd706349b283ef472ee692b237",
"packages/ui/components/Card.wrn": "be8cd6a438d1ede4c284a302b76f0fbc235a2423b83a7ced21131763fecc8a90",
"packages/ui/components/Carousel.wrn": "e7d921f802aa19210f3f415b59b70750a2c8d78e30684ad334809741c70a6bf9",
"packages/ui/components/Chart.wrn": "07bc01247b5a1c2b82c2ba8386b7fcdbf480efead75d52b7da04d471072c7ee4",
"packages/ui/components/ChatBubble.wrn": "d71f7d6567d76ac0eb3ecc8e2e567fec12bb3236ea05f4d0a72286abc0b7cbb1",
"packages/ui/components/Clipboard.wrn": "5a93f2bae337d7ce364229b796bac673f60dde6dc135f0969f4e106e04854d30",
"packages/ui/components/Collapse.wrn": "26df087a61c82f1aa1749785f4c9be654c4f934859c1a47acfcb8cc67eecc75d",
"packages/ui/components/ColorPicker.wrn": "557e36adbdee66d8ba9dcdc2446c13f3385a243f2766777feef8504908006c99",
"packages/ui/components/ColorPicker.wrn": "ccac7eb49c0f443b404004f1298fa905fde45c52b51466194920939c920348c9",
"packages/ui/components/Columns.wrn": "8ccd9e53f6799b09f8b996740d12e4e73ee782f6fd3e57595c6c16f788b9b812",
"packages/ui/components/Combobox.wrn": "58488dbb60e41dacbf223afdb4a05abdbec53b7f54cd79a0db659add7057b1c1",
"packages/ui/components/Confetti.wrn": "0e81426a3ee8aff672385a5466cf96085c25ba17677a2fef432bcddf13f4d58e",
@@ -28,7 +28,7 @@
"packages/ui/components/CustomScrollbar.wrn": "2a3a2719580c21823d079c92ebb000c7a5a7d9d16e3fa9275b8fb7d4449cb437",
"packages/ui/components/DataMap.wrn": "338f0d0a3307289dd0387b7fe7ba631fa0b65b6be8c7b9ee3feb2203306f76a3",
"packages/ui/components/DataTable.wrn": "e929fdb4e59613878a03fb6a11e09c42f773c06ca7b20e43ae16a266835f2af4",
"packages/ui/components/DatePicker.wrn": "91bcfd3832d3bccc4de5402c0c06209947bde593bad4f869b4c29ee14a5c0bef",
"packages/ui/components/DatePicker.wrn": "cefbbb2abb57ad4c57864a0c65589592a20f9ea1695f9fc7693c3135dd82e71a",
"packages/ui/components/DeviceFrame.wrn": "c38d25419ee44b05d7c443cc4e854aeeb7a53a9289e374d71834de2dd2e4d979",
"packages/ui/components/Divider.wrn": "bfa36cf16ccd48c996bb0b08a438f1b281ea1811049ed69fc7680925b1eba019",
"packages/ui/components/DragAndDrop.wrn": "84305f07d43cdb9eff1f63c19458d88b0aec013424d05a49a2cafbecedc60513",
@@ -36,15 +36,15 @@
"packages/ui/components/Dropdown.wrn": "978367a9dde22448cc664d686e4dd9c579fe8ba0b2cf02d610d95a3501f8dbb0",
"packages/ui/components/FeatureCard.wrn": "1bc548c115f924a0c7ade4c557852757083755c60ca0ee747b150a7cca3ebced",
"packages/ui/components/FeatureGrid.wrn": "15b5d6f9ddb7e3697682e7247fdf3c2dd080b770335f84ba9fab2692166fe897",
"packages/ui/components/FeatureIconCard.wrn": "3b4f3fa62c6729e686886a5b848e26c255766684829cec2b715967b4e73d6f07",
"packages/ui/components/FileInput.wrn": "866a292a3280527bf429893469e7c50f7cf38965d8adb37b45a35ede1606d5b8",
"packages/ui/components/FileUploadProgress.wrn": "e5da29c562a521cdd6bb50b8f4d217aa1ab981d7b2a2432af47391472f0a8034",
"packages/ui/components/Footer.wrn": "f7c5a77064a09e244f689d42475c4c20143f4c8859cac451c10a50c14652db33",
"packages/ui/components/FeatureIconCard.wrn": "5f65d64235babc3e653a5695204412202015e6f3ffadca566fc18c7f9ce70428",
"packages/ui/components/FileInput.wrn": "611047c362458a4e2513b741cc3ef43fbe54c10ddafc13775e4ff39c9de34931",
"packages/ui/components/FileUploadProgress.wrn": "ba00557afebf60e6221385d6efcf08635789328deee6e551fc53fbb71e1f413b",
"packages/ui/components/Footer.wrn": "e3515e05ef3ef51e6dad71d67ddfe110c80fdcf69e952aecdc7973e69d4f08c7",
"packages/ui/components/Grid.wrn": "83d4f5f2656d539538723f5791ba3c238901432e9d8c55c74f068d3eb5a74517",
"packages/ui/components/Hero.wrn": "345478b212701817ff57060f87982a987baf01a7179c979768e1cd4218b50900",
"packages/ui/components/HeroActions.wrn": "67cd31400ccb6abdbbf16219267dee946b44b064b79f25276d20ceb7d6e8a790",
"packages/ui/components/Image.wrn": "361179c478d7674c3b998891a366d73de9514fc6cc8786440c6c50b9ef065357",
"packages/ui/components/InputGroup.wrn": "30e9f2e4364542179180b6d6e796002d0ffe1e30305146749874697cde6629ea",
"packages/ui/components/InputGroup.wrn": "9e259d15207500d07ff626f09c2e2f5eaf4e471f805a7bdbec154b1a23ce0f8d",
"packages/ui/components/InputNumber.wrn": "069195cc483f3f7d6f305d03fbe5a821906a030376fafb6f0d969e4c95b68fd0",
"packages/ui/components/Kbd.wrn": "6b402cb93f0fa76bb978d1ba483215f70ec7214b1c5e84b568fa3e26e87ed9d9",
"packages/ui/components/LayoutSplitter.wrn": "41d070e801a271552023e4392e4c0f67327c61e2e8eaee5f8b87af254b1d79c1",
@@ -55,38 +55,38 @@
"packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890",
"packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad",
"packages/ui/components/Marquee.wrn": "81cd80732be34c8d7e4bf28b00347e1a9273166afdf66589d9f5d5774b69403b",
"packages/ui/components/MegaMenu.wrn": "6a6db166259f8f2bd7ac8fbb21dba6d915c4b936d87b19805caa78189f54730a",
"packages/ui/components/MegaMenu.wrn": "a34a84f31f7a3016cf5a607a22f8f7b2322552c550278f6e8c5e6d6cb18848a8",
"packages/ui/components/MetricCard.wrn": "513a6e99be3d9db46d96af9942b51331a0a048b184603e77e814506f0a52bdc5",
"packages/ui/components/MetricGrid.wrn": "c71e53249835908833055b553e64a8ee55fdf11ac4605436a9581ebb87a0608b",
"packages/ui/components/MetricGrid.wrn": "1fcebf51dcbe8520fe16bda904c1ef206c25c3900438d53877492c64ea21a1a5",
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
"packages/ui/components/Nav.wrn": "c45b0ecb42250f4b3ede33c8932a025f789dda9ef48dbf332459ae458163e69a",
"packages/ui/components/Navbar.wrn": "01903230210cd2e5e6d9325a0708f85af9623d4118e3a53f1adb770fd545425d",
"packages/ui/components/PageHeader.wrn": "321cde36ce8d521a57901033d46e8b437f42e558cca27f49ca26f7172aff1d54",
"packages/ui/components/Navbar.wrn": "b68a824e479ce6957afc3e9710442281cdc1a8d02cd96501fe060271f54368e4",
"packages/ui/components/PageHeader.wrn": "29a93cdca7285548b79ebe5b6421eef6dbb4d5ae8be2508fa2d681e1068ba07f",
"packages/ui/components/Pagination.wrn": "d54226705d4556f76ee5f0d6ae82d101ca6d006397756d547a9aa5954415ba39",
"packages/ui/components/PinInput.wrn": "4dc398456f6392d7925db941debb484c7cb0358ecef527f696fe5ec4800a7602",
"packages/ui/components/Popover.wrn": "1246b99b4236bc7109a5d1427d013bb07bbea7a2da4f821517d2545ee0b5c019",
"packages/ui/components/PortalDashboard.wrn": "f04171eb1f8cabef06de3c20e4694b766def3407e1e20d2c9aaebc9bb7da03ee",
"packages/ui/components/PreferenceSwitcher.wrn": "77229e15449182563ebf49159507139928eb74e799e4438e00e9aa81fd60f982",
"packages/ui/components/PreferenceSwitcher.wrn": "519cf911390f529fff3fd16e11fb58e79d618fe851902202dc37552ee1182395",
"packages/ui/components/PublicPageShell.wrn": "3d3f25c03d48f3d3d7e3c567e79cb137c7fce1867e054fd6d440fa1d2e961077",
"packages/ui/components/RangeSlider.wrn": "bbf646c5d5a89a62986290db811d77b8f635ebcaea9ca2d334b569aa5edc785b",
"packages/ui/components/RangeSlider.wrn": "e8ab41782c9d6e58638f0bfccf6e3172af4ebf552189d2199cc6c96bd1652067",
"packages/ui/components/Rating.wrn": "902b55c64e3798f3097f09b1558d1a2372f66d4f760662de085783108ee04587",
"packages/ui/components/Scrollspy.wrn": "b9c6400ee92a7c228362610a2051f37bff1d3baba1dbacd66d58397dfda5d856",
"packages/ui/components/Scrollspy.wrn": "4b452fcbd524feda9c9f2c81894ee01747a51888e3b86fafb37c5d4d192a3a51",
"packages/ui/components/SearchBox.wrn": "3f88340e0b69c0cd552023c72bd22dc511bc00422d46060097bcf0777e683675",
"packages/ui/components/Section.wrn": "45b7a1977c25a6020d5cc8bab2ce9c80730e7ec2d51ad53e8c9f64cd9b5b64cd",
"packages/ui/components/Section.wrn": "78da13eb8bc1acafe8e6151cbaeed4baaac2ab860e3209f1d13c467f279d2e68",
"packages/ui/components/SectionHeader.wrn": "aca2df8eb35806096f5b9af466b018e5c22a19422e8665f488380da577a0ceb5",
"packages/ui/components/Sidebar.wrn": "e0250a973aea589a4d7dcc8b30dd01f22592bf22ba012e54a672b2a2288d8af6",
"packages/ui/components/SplitHero.wrn": "843387dc7b920b3cf6a1b2b785b8653c281982e524479edc2638a89381ff3577",
"packages/ui/components/StatsBar.wrn": "c273bd6ca417a0a20e5e6f1214414f678741b73bedd885fea1037575fa8c5b95",
"packages/ui/components/SplitHero.wrn": "7127cb869d1228522d066e028818dd9e512de8b6dd8f99e3dec500e6da5a3cb0",
"packages/ui/components/StatsBar.wrn": "6fabcb4374b1e074f58ebcaba968f235c8a3409762163aea51298e2515a14435",
"packages/ui/components/Stepper.wrn": "7ec0902acd7cb74b44d490215b6421dae9a41731735ad1e9aec17e8bfc3fa142",
"packages/ui/components/StrongPassword.wrn": "407eabcc13affc9323a346ddba8e39c81b885a59ef7043d2bd5567d4b7553282",
"packages/ui/components/StrongPassword.wrn": "cc31cb080c4b4116c544b4340b8d740d08fec1b05563372b3280c50759416c87",
"packages/ui/components/StyledIcon.wrn": "0874f9a234b053a7c8b9c6983be5d664453a64c4576dff1a049101e249a2866d",
"packages/ui/components/Tabs.wrn": "f85214960e62da2fac41228deba511a50c57fd94f2830a8233b75048f6a542e9",
"packages/ui/components/TextLink.wrn": "9d561631c5f65a2bcdc59f5c0b166a5dd794a396f42093f2cd26388386f6f954",
"packages/ui/components/TimePicker.wrn": "ec0860a4a82a042a65acf9aa81519a5c815b61e0b8080cf6f1c986d26ce7f0e8",
"packages/ui/components/TimePicker.wrn": "33bd7a7069b7abe858cf1617b5b950733b37a8d522469ed673cff00b4dfcad0e",
"packages/ui/components/Timeline.wrn": "fb553f69763840532eef1c0aafa79013f57411dc5335e7cb7e7c20c5a3753008",
"packages/ui/components/Toaster.wrn": "5e4c9b2c1bfd5675a81a32e57684b99393ac7fe45d8420c7409a4e95d47d080c",
"packages/ui/components/ToggleCount.wrn": "e577195ee05ddb43cceec47145f6b90c0149f2f17055733d58911fcd02de2c7f",
"packages/ui/components/TogglePassword.wrn": "6bd4d9fa82b0c8e80178430d5a42b1b3ec9725fb62be158585f68af9338c901a",
"packages/ui/components/TogglePassword.wrn": "caa58479e5c8b1e3e504181a56a7d6bcf71d0a230010733710961c31e1cc58ab",
"packages/ui/components/TreeView.wrn": "f54d8495730203b923629bcfb167d5ca41236af0101ea2723d33354413555b4b",
"packages/ui/components/Typography.wrn": "9676af57f2a937b21f45512e0a290b5bee4a16267fbf404850297d29645d88b5",
"packages/ui/components/WysiwygEditor.wrn": "a858ebd4e6e2672463ba57d4d049176ca1cc98dac32ab3df3638d6a9cdcd0f2d",
@@ -94,15 +94,15 @@
"packages/ui/components/avatar.wrn": "8bc705e459b13c7f063c57e1506ce540e50fa8ef9020ae85b6e8b6a3579fc5dc",
"packages/ui/components/badge.wrn": "e44e33633fb34e897696cd9290f210108e35a3e2dc3b2a4a367411f45c69a1e1",
"packages/ui/components/button.wrn": "65570c58cb2547f1e17cd158eec7cf7b1ffc721e6976c6486c9df564627b1219",
"packages/ui/components/checkbox.wrn": "18046396b75d0b9c6bb2bdb09dbff1846c84fb07490f390a25ca7b5367fd2ef4",
"packages/ui/components/input.wrn": "e4c14527f009b610b4aa96d8d7b7d5ac3ca1ffd34237bab348ee9dc4ddc55847",
"packages/ui/components/checkbox.wrn": "da735354b57f33946591e1bd018b521ff709bae1325e2317735824673188377a",
"packages/ui/components/input.wrn": "2f17353cbf0711ca50a935ae03ee9ee53594d7e35eb73f5df573fadb7958fac2",
"packages/ui/components/progress.wrn": "6307a90585197d7aab19a8710b2430f5d4ed27ce77e9b90b1414ea0eed876492",
"packages/ui/components/radio.wrn": "09425a78358de5bbd2f47482f313e80065135335b969ce5ccdd5c7cc3ea5232c",
"packages/ui/components/select.wrn": "1d8d47a74d5ab9ed58d0ba11f46910897233b96652d17f5962f5dda60bd4cf8a",
"packages/ui/components/radio.wrn": "df8e562324b6d99f5a157b0971d63bd715daa0cf6189e7b0d4df04d6f0411388",
"packages/ui/components/select.wrn": "747321d6292b8052e44e8014e87ce25f4b56d8e07f86f1061e0ed0cec7651356",
"packages/ui/components/skeleton.wrn": "4fc5e0846eeefd7830c038e1789be995c4f9d833aa913ff079eb4863baa65648",
"packages/ui/components/spinner.wrn": "2322645da7ef53f7c06035ff071d9a2f6ffe2901ff9338daed84037369367105",
"packages/ui/components/switch.wrn": "874504e4828e9db6a570d78984c4d3a76d0ceb39d70baa877649cb3798c82c85",
"packages/ui/components/textarea.wrn": "9870d37664471a103d44435a3977bf40b087a743acd76ee4d77df9b56cccbb2b",
"packages/ui/components/switch.wrn": "243f521431ffb644089df7f629bace0c07094638dfdd27e75b2017efa9cbba4f",
"packages/ui/components/textarea.wrn": "aa3a75214e998a0b57c50e416476d6f85fa0313aaf97262f2f9222150c6c9e26",
"packages/ui/components/tooltip.wrn": "f3dfdfa5cd9661fe5e95ef3580eef4c7437c069726421370c2d7328fbe7d840d",
"packages/ui/styles/SelectStyles.wrn": "074fe0d67de4ef5e9f9cfe879beb72fd5c83352888687d3a1a4b7722c87af3ca",
"packages/ui/ui.css": "9ea591404e9cf675bbf1003e15e9fad00327094ff217dc20733f0fc87c0f4a62"
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "wrnexus",
"displayName": "WRNexus Language Support",
"description": "Complete WRNexus v0.8.3 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
"version": "0.8.9",
"version": "0.8.11",
"publisher": "wrnexus",
"private": true,
"license": "SEE LICENSE IN LICENSE",
+80 -7
View File
@@ -1,6 +1,6 @@
"use strict";
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
// WRN editor compiler source hash: 884c9faffb5517c907321484014e00562654ea0ba96e432bec319e0001934446
// WRN editor compiler source hash: 3d346a264807ba7b107584dc9a7b505b1d14057b2ad1aa85c5ea890f9b09c272
// WRN editor compiler generator hash: f8c1094b47e695151205899de28cd63d574356d26ac49219c4a43db0f2391ef5
// Generated with TypeScript: 6.0.3
const __nodeRequire = require;
@@ -4067,6 +4067,7 @@ function islandNamesFrom(imports) {
Object.defineProperty(exports, "__esModule", { value: true });
exports.NativeCompileError = void 0;
exports.generateNative = generateNative;
const syntax_1 = require("@wrnexus/syntax");
class NativeCompileError extends Error {
constructor(message) {
super(message);
@@ -4132,12 +4133,12 @@ function expression(value) {
function textJsx(value) {
const pieces = [];
let last = 0;
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
if (match.index > last)
pieces.push(value.slice(last, match.index));
const expr = match[1].trim();
for (const segment of (0, syntax_1.extractInterpolations)(value)) {
if (segment.start > last)
pieces.push(value.slice(last, segment.start));
const expr = segment.expression;
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
last = match.index + match[0].length;
last = segment.end;
}
pieces.push(value.slice(last));
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "&lt;" : "&gt;"));
@@ -6227,6 +6228,26 @@ function expandStructuredStateDeclarations(lines, unit) {
}
});
}
function formatStructuredPropDeclaration(value, unit, depth, printWidth) {
const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec(value);
if (!match)
return null;
try {
const parsed = JSON.parse(match[2].trim());
const compact = JSON.stringify(parsed);
const indentation = unit.repeat(depth);
if (indentation.length + match[1].length + compact.length <= printWidth)
return null;
const jsonLines = JSON.stringify(parsed, null, unit).split("\n");
return [
`${indentation}${match[1]}${jsonLines[0]}`,
...jsonLines.slice(1).map((jsonLine) => `${indentation}${jsonLine}`),
];
}
catch {
return null;
}
}
function formatWrnPass(source, options = {}) {
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
const printWidth = options.printWidth ?? 100;
@@ -6282,6 +6303,12 @@ function formatWrnPass(source, options = {}) {
index += 1;
continue;
}
const structuredProp = formatStructuredPropDeclaration(value, unit, codeDepth + htmlDepth + controlDepth, printWidth);
if (structuredProp) {
output.push(...structuredProp);
index += 1;
continue;
}
const leadingClosingBraces = countLeadingClosingBraces(value);
const closesControlBlock = isControlBlockClose(value);
const continuesControlBlock = isControlBlockMiddle(value);
@@ -6362,7 +6389,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.stripRuntimeFunctionModifiers = exports.parseStructuredImports = exports.parseStoreLifecycle = exports.parseStateDeclarations = exports.parseRuntimeFunctions = exports.parsePersist = exports.parseOutputs = exports.parseComputedDeclarations = exports.supportsSyntaxFeature = exports.diagnosticSummary = exports.sliceSource = exports.createSourceRange = exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = exports.positionAt = exports.isRuntimeTarget = exports.isHydrationStrategy = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.containsReadonlyPropMutation = exports.classifyParseError = exports.assertValidAst = exports.validateTypedInitializer = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.formatWrn = exports.skipLiteralOrComment = exports.LexError = exports.Lexer = void 0;
exports.stripRuntimeFunctionModifiers = exports.parseStructuredImports = exports.parseStoreLifecycle = exports.parseStateDeclarations = exports.parseRuntimeFunctions = exports.parsePersist = exports.parseOutputs = exports.parseComputedDeclarations = exports.supportsSyntaxFeature = exports.diagnosticSummary = exports.sliceSource = exports.createSourceRange = exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = exports.positionAt = exports.isRuntimeTarget = exports.isHydrationStrategy = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.containsReadonlyPropMutation = exports.classifyParseError = exports.assertValidAst = exports.validateTypedInitializer = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.extractInterpolations = exports.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.formatWrn = exports.skipLiteralOrComment = exports.LexError = exports.Lexer = void 0;
var tokenizer_ts_1 = require("./tokenizer.js");
Object.defineProperty(exports, "Lexer", { enumerable: true, get: function () { return tokenizer_ts_1.Lexer; } });
Object.defineProperty(exports, "LexError", { enumerable: true, get: function () { return tokenizer_ts_1.LexError; } });
@@ -6374,6 +6401,8 @@ Object.defineProperty(exports, "parse", { enumerable: true, get: function () { r
Object.defineProperty(exports, "parseHtmlView", { enumerable: true, get: function () { return parser_ts_1.parseHtmlView; } });
Object.defineProperty(exports, "ParseError", { enumerable: true, get: function () { return parser_ts_1.ParseError; } });
Object.defineProperty(exports, "VOID_ELEMENTS", { enumerable: true, get: function () { return parser_ts_1.VOID_ELEMENTS; } });
var interpolation_ts_1 = require("./interpolation.js");
Object.defineProperty(exports, "extractInterpolations", { enumerable: true, get: function () { return interpolation_ts_1.extractInterpolations; } });
var types_ts_1 = require("./types.js");
Object.defineProperty(exports, "eraseFunctionTypes", { enumerable: true, get: function () { return types_ts_1.eraseFunctionTypes; } });
Object.defineProperty(exports, "inferredRuntimeType", { enumerable: true, get: function () { return types_ts_1.inferredRuntimeType; } });
@@ -6407,6 +6436,50 @@ Object.defineProperty(exports, "parseStoreLifecycle", { enumerable: true, get: f
Object.defineProperty(exports, "parseStructuredImports", { enumerable: true, get: function () { return v060_ts_1.parseStructuredImports; } });
Object.defineProperty(exports, "stripRuntimeFunctionModifiers", { enumerable: true, get: function () { return v060_ts_1.stripRuntimeFunctionModifiers; } });
},
"packages/syntax/src/interpolation.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractInterpolations = extractInterpolations;
/** Extract top-level `{...}` expressions while preserving nested object/array literals. */
function extractInterpolations(value) {
const segments = [];
let index = 0;
while (index < value.length) {
if (value[index] !== "{") {
index++;
continue;
}
const start = index++;
let depth = 1;
let quote = null;
while (index < value.length && depth > 0) {
const char = value[index];
if (quote) {
if (char === "\\" && index + 1 < value.length)
index += 2;
else {
if (char === quote)
quote = null;
index++;
}
continue;
}
if (char === '"' || char === "'" || char === "`")
quote = char;
else if (char === "{")
depth++;
else if (char === "}")
depth--;
index++;
}
if (depth === 0) {
segments.push({ expression: value.slice(start + 1, index - 1).trim(), start, end: index });
}
}
return segments;
}
},
"packages/syntax/src/parser.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
+1 -1
View File
@@ -1,4 +1,4 @@
// WRN editor extension source hash: 8e634610c27521e6ccbf4e7293eb4320ecaac29944bde5236bb286ff84422203
// WRN editor extension source hash: d07c535bd69332e90f207cbc254aae06982b40dd863fbc8de63fb386ecf31f50
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
"use strict";
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
+67 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// WRN editor language server source hash: 8e23fe3a501243ea96b8620142ed0187bae8de43cb9eb4e2bee17cf24777b31b
// WRN editor language server source hash: 5258b3e76cec47df752d4bb886fbd57a3f3c4af9ca9b6b2bf6742c8d78025ff1
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
// @bun @bun-cjs
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
@@ -170722,6 +170722,26 @@ function expandStructuredStateDeclarations(lines, unit) {
}
});
}
function formatStructuredPropDeclaration(value, unit, depth, printWidth) {
const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec(value);
if (!match)
return null;
try {
const parsed = JSON.parse(match[2].trim());
const compact = JSON.stringify(parsed);
const indentation = unit.repeat(depth);
if (indentation.length + match[1].length + compact.length <= printWidth)
return null;
const jsonLines = JSON.stringify(parsed, null, unit).split(`
`);
return [
`${indentation}${match[1]}${jsonLines[0]}`,
...jsonLines.slice(1).map((jsonLine) => `${indentation}${jsonLine}`)
];
} catch {
return null;
}
}
function formatWrnPass(source, options = {}) {
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
const printWidth = options.printWidth ?? 100;
@@ -170778,6 +170798,12 @@ function formatWrnPass(source, options = {}) {
index += 1;
continue;
}
const structuredProp = formatStructuredPropDeclaration(value, unit, codeDepth + htmlDepth + controlDepth, printWidth);
if (structuredProp) {
output.push(...structuredProp);
index += 1;
continue;
}
const leadingClosingBraces = countLeadingClosingBraces(value);
const closesControlBlock = isControlBlockClose(value);
const continuesControlBlock = isControlBlockMiddle(value);
@@ -172393,6 +172419,44 @@ function parseHtmlView(src, pos) {
const nodes = parseNodeList("root");
return { nodes, endPos: i };
}
// packages/syntax/src/interpolation.ts
function extractInterpolations(value) {
const segments = [];
let index = 0;
while (index < value.length) {
if (value[index] !== "{") {
index++;
continue;
}
const start = index++;
let depth = 1;
let quote = null;
while (index < value.length && depth > 0) {
const char = value[index];
if (quote) {
if (char === "\\" && index + 1 < value.length)
index += 2;
else {
if (char === quote)
quote = null;
index++;
}
continue;
}
if (char === '"' || char === "'" || char === "`")
quote = char;
else if (char === "{")
depth++;
else if (char === "}")
depth--;
index++;
}
if (depth === 0) {
segments.push({ expression: value.slice(start + 1, index - 1).trim(), start, end: index });
}
}
return segments;
}
// packages/syntax/src/diagnostics.ts
function stripAsciiControlAndSpace(value) {
let result = "";
@@ -173126,6 +173190,7 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
append(`declare const api: { ${apiType} };`);
append(`declare const props: Readonly<${ast.name}Props>;`);
append("declare const refs: Record<string, Element | null>;");
append("declare const toast: ((message: string, options?: Record<string, unknown>) => unknown) & { success(message: string, options?: Record<string, unknown>): unknown; error(message: string, options?: Record<string, unknown>): unknown; danger(message: string, options?: Record<string, unknown>): unknown; warning(message: string, options?: Record<string, unknown>): unknown; info(message: string, options?: Record<string, unknown>): unknown; dismiss(id: string | number): void; clear(): void };");
append("declare function useFetch(path: string, method?: string | { method?: string; query?: unknown; params?: unknown; body?: unknown; data?: unknown }, input?: unknown): Promise<any>;");
append(ast.kind === "global-store" || ast.kind === "page-store" ? storeContract(ast) : componentContract(ast));
append(importedWrnDeclarations(ast, filePath, appRoot));
@@ -173157,7 +173222,7 @@ ${sharedAliases}`);
for (const node of nodes) {
if (node.type === "element") {
for (const attr of node.attrs) {
const expressions = attr.event ? [attr.value] : [...attr.value.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1].trim());
const expressions = attr.event ? [attr.value] : extractInterpolations(attr.value).map((segment) => segment.expression);
for (const expression of expressions) {
if (!expression.trim())
continue;
+14
View File
@@ -20,6 +20,20 @@ test("preserves nested prop defaults while formatting", () => {
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2 }), formatted);
});
test("formats large typed JSON prop defaults with the bundled formatter", () => {
const source = `component Catalog {
props {
categories: unknown[] = [{"label":"Platform & Core","apps":[{"label":"Home","href":"/home"},{"label":"SSO","href":"/sso"}]},{"label":"Sales","apps":[{"label":"CRM","href":"/crm"}]}]
}
view { <div></div> }
}`;
const formatted = formatWrn(source, { insertSpaces: true, tabSize: 2, printWidth: 70 });
assert.match(formatted, /categories: unknown\[\] = \[\n \{/);
assert.match(formatted, /"apps": \[\n \{/);
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2, printWidth: 70 }), formatted);
});
test("formats native JSON state values with readable indentation", () => {
const source = `component Footer {
state footerItems = [{"label":"Accessibility","href":"/accessibility","value":"accessibility"},{"label":"Privacy","href":"/privacy","value":"privacy"}]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.8.62",
"version": "0.8.64",
"type": "module",
"main": "src/index.ts",
"exports": {
+5
View File
@@ -113,6 +113,11 @@ Update options: --dry-run previews changes; --no-verify skips post-update check/
async function main(): Promise<void> {
const [command, ...rest] = process.argv.slice(2);
if (command === "--version" || command === "-v" || command === "version") {
console.log(currentCliVersion());
return;
}
notifyIfUpdateAvailable(currentCliVersion(), command);
switch (command) {
+13 -4
View File
@@ -63,14 +63,16 @@ const execFileAsync = promisify(execFile);
/** Resolve every installed WRNexus dependency independently and in parallel. */
export async function latestPackageVersions(names: string[]): Promise<Map<string, string>> {
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const unique = [...new Set(names.filter((name) => name.startsWith("@wrnexus/")))];
const entries = await Promise.all(
unique.map(async (name) => {
try {
const { stdout } = await execFileAsync(npm, ["view", name, "version", "--json"], {
encoding: "utf8",
});
const command = process.platform === "win32" ? (process.env.ComSpec ?? "cmd.exe") : "npm";
const args =
process.platform === "win32"
? ["/d", "/s", "/c", "npm", "view", name, "version", "--json"]
: ["view", name, "version", "--json"];
const { stdout } = await execFileAsync(command, args, { encoding: "utf8" });
const parsed = JSON.parse(stdout.trim()) as string | string[];
const version = Array.isArray(parsed) ? parsed.at(-1) : parsed;
return typeof version === "string" && /^\d+\.\d+\.\d+/.test(version)
@@ -1274,6 +1276,13 @@ export async function runUpdate(dir: string, args: string[]): Promise<void> {
? new Map<string, string>()
: await latestPackageVersions(dependencyNames);
if (!versionArg && dependencyNames.length > 0 && packageVersions.size === 0) {
throw new Error(
`Unable to resolve any of ${new Set(dependencyNames).size} installed WRNexus package ` +
`version(s) from npm. Check the @wrnexus registry and authentication before retrying.`,
);
}
console.log(`\n ⚡ wrnexus update → migrations ${target}${dryRun ? " (dry run)" : ""}`);
if (!versionArg) {
console.log(
+24 -2
View File
@@ -1,8 +1,12 @@
import { afterEach, expect, test } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { compareVersions, notifyIfUpdateAvailable } from "../src/update-notifier.ts";
import { join, resolve } from "node:path";
import {
compareVersions,
currentCliVersion,
notifyIfUpdateAvailable,
} from "../src/update-notifier.ts";
const originalCache = process.env.WRNEXUS_UPDATE_CACHE;
@@ -35,3 +39,21 @@ test("cached update checks suggest the exact upgrade command", () => {
rmSync(root, { recursive: true, force: true });
}
});
test("--version prints the package version outside a WrNexus workspace", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-version-"));
try {
const result = Bun.spawnSync({
cmd: [process.execPath, resolve(import.meta.dir, "../src/index.ts"), "--version"],
cwd: root,
env: { ...process.env, WRNEXUS_NO_UPDATE_CHECK: "1" },
stdout: "pipe",
stderr: "pipe",
});
expect(result.exitCode).toBe(0);
expect(result.stdout.toString().trim()).toBe(currentCliVersion());
expect(result.stderr.toString()).toBe("");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.8.21",
"version": "0.8.22",
"type": "module",
"main": "src/index.ts",
"exports": {
+5 -5
View File
@@ -1,4 +1,4 @@
import type { Attr, PageAst, ViewNode } from "@wrnexus/syntax";
import { extractInterpolations, type Attr, type PageAst, type ViewNode } from "@wrnexus/syntax";
export class NativeCompileError extends Error {
constructor(message: string) {
@@ -68,11 +68,11 @@ function expression(value: string): string | null {
function textJsx(value: string): string {
const pieces: string[] = [];
let last = 0;
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
if (match.index! > last) pieces.push(value.slice(last, match.index));
const expr = match[1]!.trim();
for (const segment of extractInterpolations(value)) {
if (segment.start > last) pieces.push(value.slice(last, segment.start));
const expr = segment.expression;
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
last = match.index! + match[0].length;
last = segment.end;
}
pieces.push(value.slice(last));
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "&lt;" : "&gt;"));
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.8.33",
"version": "0.8.35",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -90,7 +90,7 @@ const CONTROLLER_LOADER = String.raw`
if (!componentControllerPromise) {
componentControllerPromise = new Promise(function (resolve, reject) {
var script = document.createElement("script");
script.src = componentControllerUrl;
script.src = trustedScriptURL(componentControllerUrl);
script.defer = true;
script.onload = resolve;
script.onerror = reject;
+53 -6
View File
@@ -26,6 +26,40 @@ export const REACTIVE_RUNTIME = String.raw`
var behaviorObserver;
var clientModuleCache = new Map();
/*
* Production enables require-trusted-types-for 'script'. Every HTML write
* below uses compiler-emitted template markup (or the explicit data-html
* escape hatch), and controller URLs are generated by the framework. Route
* those known sinks through the policy allowed by the default security
* preset instead of letting the browser reject hydration midway through a
* state update.
*/
var wrnexusTrustedTypesPolicy = null;
if (window.trustedTypes && typeof window.trustedTypes.createPolicy === "function") {
try {
wrnexusTrustedTypesPolicy = window.trustedTypes.createPolicy("wrnexus", {
createHTML: function (value) { return value; },
createScriptURL: function (value) { return value; }
});
} catch (_) {
wrnexusTrustedTypesPolicy = window.trustedTypes.defaultPolicy || null;
}
}
function trustedHTML(value) {
var source = value == null ? "" : String(value);
return wrnexusTrustedTypesPolicy
? wrnexusTrustedTypesPolicy.createHTML(source)
: source;
}
function trustedScriptURL(value) {
var source = value == null ? "" : String(value);
return wrnexusTrustedTypesPolicy
? wrnexusTrustedTypesPolicy.createScriptURL(source)
: source;
}
/*
* Two builtin chains the runtime reaches for constantly. Aliasing them is
* not only shorter: hasOwn keeps prototype keys from reading as data, and
@@ -234,8 +268,21 @@ export const REACTIVE_RUNTIME = String.raw`
Array.prototype.forEach.call(rules || [], function (rule) {
var cssText = rule.cssText || "";
var match;
var pattern = /var\(\s*(--wrn-[A-Za-z0-9_-]+)/g;
while ((match = pattern.exec(cssText))) referenced[match[1]] = true;
// Only inspect declarations that can affect the current document. A
// component stylesheet may contain optional variants that are not
// rendered on this page, and variables with fallbacks are intentionally
// optional rather than missing theme tokens.
if (rule.selectorText) {
try {
if (!document.querySelector(rule.selectorText)) return;
} catch (_) {
// Keep inspecting selectors that the DOM implementation cannot parse.
}
}
var pattern = /var\(\s*(--wrn-[A-Za-z0-9_-]+)\s*(,)?/g;
while ((match = pattern.exec(cssText))) {
if (!match[2]) referenced[match[1]] = true;
}
if (rule.style) {
Array.prototype.forEach.call(rule.style, function (property) {
if (String(property).indexOf("--wrn-") === 0) declared[property] = true;
@@ -1818,7 +1865,7 @@ export const REACTIVE_RUNTIME = String.raw`
var next = markup == null ? "" : String(markup);
if (htmlNode.innerHTML !== next) {
htmlNode.innerHTML = next;
htmlNode.innerHTML = trustedHTML(next);
}
});
runHtml();
@@ -2002,7 +2049,7 @@ export const REACTIVE_RUNTIME = String.raw`
}
function clearControlContent() {
if (!rangeEnd) { block.innerHTML = ""; return; }
if (!rangeEnd) { block.innerHTML = trustedHTML(""); return; }
while (block.nextSibling && block.nextSibling !== rangeEnd) {
block.parentNode.removeChild(block.nextSibling);
}
@@ -2010,7 +2057,7 @@ export const REACTIVE_RUNTIME = String.raw`
function appendControlContent(markup, locals) {
var template = document.createElement("template");
template.innerHTML = markup || "";
template.innerHTML = trustedHTML(markup || "");
var fragment = template.content;
var elements = toArray(fragment.childNodes).filter(function (node) {
return node.nodeType === 1;
@@ -2399,7 +2446,7 @@ export const REACTIVE_RUNTIME = String.raw`
var markup = value == null ? "" : String(value);
if (node.innerHTML !== markup) {
node.innerHTML = markup;
node.innerHTML = trustedHTML(markup);
}
});
});
+39
View File
@@ -62,6 +62,13 @@ test("split runtime hydrates a controller only from the controller asset", () =>
expect(win.document.activeElement).toBe(buttons[1]);
});
test("production hydration uses Trusted Types for HTML and controller URLs", () => {
const runtime = getReactiveRuntime();
expect(runtime).toContain('createPolicy("wrnexus"');
expect(runtime).toContain('template.innerHTML = trustedHTML(markup || "")');
expect(runtime).toContain("script.src = trustedScriptURL(componentControllerUrl)");
});
test("a page without controller markers does not request the controller asset", () => {
const win = mount(
`<main data-scope="count: 1"><span>{count}</span></main>`,
@@ -1081,6 +1088,38 @@ test("development runtime accepts component-local and inline wrn variables", ()
expect(warnings).toHaveLength(0);
});
test("development runtime ignores fallback and inactive component variables", () => {
const win = new Window() as unknown as Window & Record<string, unknown>;
win.document.head.innerHTML =
`<style>.visible { color: var(--wrn-optional, red); } ` +
`.not-rendered { color: var(--wrn-inactive); }</style>`;
win.document.body.innerHTML = `<div class="visible" data-scope=""></div>`;
(globalThis as Record<string, unknown>).window = win;
(globalThis as Record<string, unknown>).document = win.document;
(globalThis as Record<string, unknown>).location = win.location;
(globalThis as Record<string, unknown>).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record<string, unknown>).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(globalThis as Record<string, unknown>).CustomEvent = (
win as unknown as { CustomEvent: unknown }
).CustomEvent;
const warnings: unknown[][] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => warnings.push(args);
try {
(0, eval)(getReactiveRuntime(true));
(
win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }
).__wrnexusHydrateScopes?.(win.document);
} finally {
console.warn = originalWarn;
}
expect(warnings).toHaveLength(0);
});
test("production runtime strips development diagnostics", () => {
const production = getReactiveRuntime();
expect(production).not.toContain("WRN-DEV-");
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/syntax",
"version": "0.8.16",
"version": "0.8.18",
"type": "module",
"main": "src/index.ts",
"exports": {
+34
View File
@@ -767,6 +767,28 @@ function expandStructuredStateDeclarations(lines, unit) {
});
}
function formatStructuredPropDeclaration(value, unit, depth, printWidth) {
const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec(
value,
);
if (!match) return null;
try {
const parsed = JSON.parse(match[2].trim());
const compact = JSON.stringify(parsed);
const indentation = unit.repeat(depth);
if (indentation.length + match[1].length + compact.length <= printWidth) return null;
const jsonLines = JSON.stringify(parsed, null, unit).split("\n");
return [
`${indentation}${match[1]}${jsonLines[0]}`,
...jsonLines.slice(1).map((jsonLine) => `${indentation}${jsonLine}`),
];
} catch {
return null;
}
}
function formatWrnPass(source: string, options: FormatWrnOptions = {}): string {
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
@@ -844,6 +866,18 @@ function formatWrnPass(source: string, options: FormatWrnOptions = {}): string {
continue;
}
const structuredProp = formatStructuredPropDeclaration(
value,
unit,
codeDepth + htmlDepth + controlDepth,
printWidth,
);
if (structuredProp) {
output.push(...structuredProp);
index += 1;
continue;
}
const leadingClosingBraces = countLeadingClosingBraces(value);
const closesControlBlock = isControlBlockClose(value);
+2
View File
@@ -2,6 +2,8 @@ export { Lexer, LexError, skipLiteralOrComment } from "./tokenizer.ts";
export { formatWrn } from "./formatter.ts";
export type { FormatWrnOptions } from "./formatter.ts";
export { parse, parseHtmlView, ParseError, VOID_ELEMENTS } from "./parser.ts";
export { extractInterpolations } from "./interpolation.ts";
export type { InterpolationSegment } from "./interpolation.ts";
export type {
ActionBlock,
ApiBlock,
+39
View File
@@ -0,0 +1,39 @@
export interface InterpolationSegment {
expression: string;
start: number;
end: number;
}
/** Extract top-level `{...}` expressions while preserving nested object/array literals. */
export function extractInterpolations(value: string): InterpolationSegment[] {
const segments: InterpolationSegment[] = [];
let index = 0;
while (index < value.length) {
if (value[index] !== "{") {
index++;
continue;
}
const start = index++;
let depth = 1;
let quote: string | null = null;
while (index < value.length && depth > 0) {
const char = value[index]!;
if (quote) {
if (char === "\\" && index + 1 < value.length) index += 2;
else {
if (char === quote) quote = null;
index++;
}
continue;
}
if (char === '"' || char === "'" || char === "`") quote = char;
else if (char === "{") depth++;
else if (char === "}") depth--;
index++;
}
if (depth === 0) {
segments.push({ expression: value.slice(start + 1, index - 1).trim(), start, end: index });
}
}
return segments;
}
+15
View File
@@ -83,3 +83,18 @@ test("preserves balanced compact sibling markup without indentation drift", () =
expect(formatted).toContain('<div class="strip"><span>Page</span><b>→</b><span>API</span></div>');
expect(formatWrn(formatted, { tabSize: 2, printWidth: 80 })).toBe(formatted);
});
test("formats large typed JSON prop defaults and remains idempotent", () => {
const source = `component Catalog {
props {
categories: unknown[] = [{"label":"Platform & Core","value":"platform","apps":[{"label":"WRNexus Home / Business Command Center","href":"/products/home","number":"01"},{"label":"SSO & Account","href":"/products/sso","number":"02"}]},{"label":"Sales & Customer","value":"sales","apps":[{"label":"CRM","href":"/products/crm","number":"11"}]}]
}
view { <div></div> }
}`;
const formatted = formatWrn(source, options);
expect(formatted).toContain(`categories: unknown[] = [\n {\n "label": "Platform & Core"`);
expect(formatted).toContain(`"apps": [\n {`);
expect(formatWrn(formatted, options)).toBe(formatted);
expect(() => parse(formatted)).not.toThrow();
});
+12 -1
View File
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
import { diagnose, formatDiagnostic, parse } from "../src/index.ts";
import { diagnose, extractInterpolations, formatDiagnostic, parse } from "../src/index.ts";
test("parses explicit rendering modes and the never hydration alias", () => {
const ast = parse(`page Marketing {
@@ -337,3 +337,14 @@ test("a block comment inside props is still refused with its own message", () =>
`;
expect(() => parse(source)).toThrow("Block comments are not allowed inside props");
});
test("extracts nested object and array interpolation as one expression", () => {
const value = `prefix {[{ label: 'One', meta: { count: 1 } }, { label: "Two" }]} suffix`;
expect(extractInterpolations(value)).toEqual([
{
expression: `[{ label: 'One', meta: { count: 1 } }, { label: "Two" }]`,
start: 7,
end: value.length - 7,
},
]);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/typecheck",
"version": "0.8.15",
"version": "0.8.16",
"type": "module",
"main": "src/index.ts",
"exports": {
+5 -1
View File
@@ -3,6 +3,7 @@ import { dirname, join, normalize, resolve } from "node:path";
import ts from "typescript";
import {
containsReadonlyPropMutation,
extractInterpolations,
inferredRuntimeType,
parse,
runtimeTypeOf,
@@ -332,6 +333,9 @@ export function virtualTypeScriptModule(
if (ast.dataApis.length) append(`declare const api: { ${apiType} };`);
append(`declare const props: Readonly<${ast.name}Props>;`);
append("declare const refs: Record<string, Element | null>;");
append(
"declare const toast: ((message: string, options?: Record<string, unknown>) => unknown) & { success(message: string, options?: Record<string, unknown>): unknown; error(message: string, options?: Record<string, unknown>): unknown; danger(message: string, options?: Record<string, unknown>): unknown; warning(message: string, options?: Record<string, unknown>): unknown; info(message: string, options?: Record<string, unknown>): unknown; dismiss(id: string | number): void; clear(): void };",
);
append(
"declare function useFetch(path: string, method?: string | { method?: string; query?: unknown; params?: unknown; body?: unknown; data?: unknown }, input?: unknown): Promise<any>;",
);
@@ -382,7 +386,7 @@ export function virtualTypeScriptModule(
for (const attr of node.attrs) {
const expressions = attr.event
? [attr.value]
: [...attr.value.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]!.trim());
: extractInterpolations(attr.value).map((segment) => segment.expression);
for (const expression of expressions) {
if (!expression.trim()) continue;
const declarations = [
+29
View File
@@ -112,3 +112,32 @@ test("accepts handler payload, loop locals, and the public useFetch helper", ()
);
expect(diagnostics.filter((diagnostic) => diagnostic.code === "WRN-TYPE-2304")).toEqual([]);
});
test("accepts multiline array props containing nested object literals", () => {
const root = app();
const diagnostics = checkWrnSource(
`page Contact {
view {
<div data-component="StatsBar" items="{[
{ label: 'Product catalog', value: '81', suffix: ' apps' },
{ label: 'Starting point', value: 'One workflow' }
]}"></div>
}
}`,
{ appRoot: root, filePath: join(root, "app", "pages", "contact.wrn") },
);
expect(diagnostics.filter((diagnostic) => diagnostic.code.startsWith("WRN-TYPE-"))).toEqual([]);
});
test("provides the toast API to view event expressions", () => {
const root = app();
const diagnostics = checkWrnSource(
`page Contact {
view {
<form @wrn:success="toast.success('Sent', { title: 'Request sent' })"></form>
}
}`,
{ appRoot: root, filePath: join(root, "app", "pages", "contact.wrn") },
);
expect(diagnostics.filter((diagnostic) => diagnostic.code.startsWith("WRN-TYPE-"))).toEqual([]);
});
+15 -15
View File
@@ -45,7 +45,7 @@ Secure multi-cell PIN and verification-code input with regex and paste support.
Theme-aware, responsive strong password component.
- Mount: `data-component="StrongPassword"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Create a strong password"`, `autocomplete: string = "new-password"`, `minLength: number = 8`, `specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"`, `requireLowercase: boolean = true`, `requireUppercase: boolean = true`, `requireNumber: boolean = true`, `requireSpecialCharacter: boolean = true`, `showRequirements: boolean = true`, `presentation: string = "inline"`, `hintText: string = "Use a unique password you do not use elsewhere."`, `emptyLabel: string = "Enter a password"`, `weakLabel: string = "Weak"`, `fairLabel: string = "Fair"`, `goodLabel: string = "Good"`, `strongLabel: string = "Strong"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Create a strong password"`, `autocomplete: string = "new-password"`, `minLength: number = 8`, `specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"`, `requireLowercase: boolean = true`, `requireUppercase: boolean = true`, `requireNumber: boolean = true`, `requireSpecialCharacter: boolean = true`, `showRequirements: boolean = true`, `presentation: string = "inline"`, `hintText: string = "Use a unique password you do not use elsewhere."`, `emptyLabel: string = "Enter a password"`, `weakLabel: string = "Weak"`, `fairLabel: string = "Fair"`, `goodLabel: string = "Good"`, `strongLabel: string = "Strong"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `showLabel: string = "Show password"`, `hideLabel: string = "Hide password"`, `class: string = ""`
- Slots: None
- Outputs: `input({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `change({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `strength({ value: string; score: number; maximumScore: number; percent: number; level: string })`
@@ -146,7 +146,7 @@ Theme-aware, responsive button group component.
Group related content in a responsive themed surface with title, description, content, and supporting slots.
- Mount: `data-component="Card"`
- Props: `title: string = "Card title"`, `subtitle: string = ""`, `description: string = ""`, `header: string = ""`, `footer: string = ""`, `imageSrc: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `actionLabel: string = ""`, `actionHref: string = ""`, `headerActions: unknown[] = []`, `navigation: unknown[] = []`, `activeNav: string = ""`, `mobileNavigation: boolean = false`, `alertTitle: string = ""`, `alertDescription: string = ""`, `empty: boolean = false`, `emptyTitle: string = "No data to show"`, `emptyIcon: string = "icon-[lucide--inbox]"`, `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "vertical"`, `align: string = "left"`, `hover: string = "none"`, `scrollable: boolean = false`, `maxHeight: string = "18rem"`, `dismissible: boolean = false`, `ariaLabel: string = ""`, `class: string = ""`
- Props: `title: string = ""`, `icon: string = ""`, `titleKey: string = ""`, `subtitle: string = ""`, `subtitleKey: string = ""`, `description: string = ""`, `descriptionKey: string = ""`, `header: string = ""`, `footer: string = ""`, `imageSrc: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `actionLabel: string = ""`, `actionLabelKey: string = ""`, `actionHref: string = ""`, `headerActions: unknown[] = []`, `navigation: unknown[] = []`, `activeNav: string = ""`, `mobileNavigation: boolean = false`, `alertTitle: string = ""`, `alertDescription: string = ""`, `empty: boolean = false`, `emptyTitle: string = "No data to show"`, `emptyIcon: string = "icon-[lucide--inbox]"`, `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "vertical"`, `align: string = "left"`, `hover: string = "none"`, `radius: string = "default"`, `shadow: string = "default"`, `equalHeight: boolean = false`, `scrollable: boolean = false`, `maxHeight: string = "18rem"`, `dismissible: boolean = false`, `ariaLabel: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `navigate({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
@@ -337,7 +337,7 @@ Reusable portal dashboard component.
Reusable preference switcher component.
- Mount: `data-component="PreferenceSwitcher"`
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `iconOnly: boolean = true`, `themeIcon: string = "icon-[lucide--sun-moon]"`, `colorIcon: string = "icon-[lucide--palette]"`, `languageIcon: string = "icon-[lucide--languages]"`, `panelPlacement: string = "bottom-end"`, `mobilePanel: string = "dropdown"`, `showCurrentLanguage: boolean = true`, `class: string = ""`
- Slots: None
- Outputs: `theme({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `color({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `language({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
@@ -375,7 +375,7 @@ Arrange operational metrics, KPIs, public statistics, or service indicators in a
Present a compact responsive strip of key facts, counts, performance indicators, or trust signals.
- Mount: `data-component="StatsBar"`
- Props: `items: unknown[] = []`, `columns: number = 4`, `compact: boolean = true`, `dividers: boolean = true`, `icons: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `maxWidth: string = "xl"`, `class: string = ""`
- Props: `items: unknown[] = []`, `columns: number = 4`, `compact: boolean = true`, `dividers: boolean = true`, `icons: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `maxWidth: string = "xl"`, `radius: string = "default"`, `shadow: string = "default"`, `gutter: string = "md"`, `outerSpacing: string = "none"`, `mobileColumns: number = 1`, `class: string = ""`
- Slots: None
- Outputs: None
@@ -606,7 +606,7 @@ Arrange feature cards, services, solutions, or benefits in a responsive equal-he
Render structured responsive footer navigation, pre and post content, copyright content, links, and public events.
- Mount: `data-component="Footer"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: unknown[] = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `copyright: string = ""`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: unknown[] = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `mobileColumns: number = 2`, `mobileCopyrightAlign: string = "center"`, `copyright: string = ""`, `copyrightKey: string = ""`, `translationPrefix: string = ""`, `class: string = ""`
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
@@ -698,7 +698,7 @@ Apply consistent readable responsive typography, widths, columns, spacing, and e
Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls.
- Mount: `data-component="AnnouncementBar"`
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `width: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `flush: boolean = false`, `mobileCompact: boolean = false`, `mobileMessageLines: number = 2`, `hideBadgeOnMobile: boolean = false`, `hideDescriptionOnMobile: boolean = false`, `mobileActionSize: string = "default"`, `gutter: string = "md"`, `contentGutter: string = "md"`, `flushMobile: boolean = false`, `size: string = "default"`, `width: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
- Slots: None
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
@@ -707,7 +707,7 @@ Publish a responsive notice with badge, icon, supporting copy, action, dismiss b
Close a page or major section with conversion-focused copy, actions, and optional supporting visual content.
- Mount: `data-component="CTASection"`
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualPosition: string = "right"`, `maxWidth: string = "xl"`, `fullBleed: boolean = false`, `class: string = ""`
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `spacing: string = "none"`, `radius: string = "default"`, `titleMaxWidth: string = ""`, `contentPadding: string = "default"`, `mobilePadding: string = "default"`, `mobileActions: string = "stack"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualPosition: string = "right"`, `maxWidth: string = "xl"`, `fullBleed: boolean = false`, `class: string = ""`
- Slots: `default`, `actions`, `visual`, `footer`
- Outputs: None
@@ -725,7 +725,7 @@ Present one linked feature or service with media, icon, badge, description, and
Present a compact feature or benefit with a styled icon, title, description, badge, and optional link.
- Mount: `data-component="FeatureIconCard"`
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `class: string = ""`
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `radius: string = "default"`, `shadow: string = "default"`, `clickableSurface: boolean = false`, `descriptionTone: string = "default"`, `class: string = ""`
- Slots: `default`, `footer`
- Outputs: None
@@ -770,7 +770,7 @@ Introduce an internal or public page with breadcrumbs, icon, title, description,
Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel.
- Mount: `data-component="SplitHero"`
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = "icon-[lucide--sparkles]"`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "xl"`, `fullBleed: boolean = true`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualStyle: string = "panel"`, `trustItems: unknown[] = []`, `class: string = ""`
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = "icon-[lucide--sparkles]"`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "xl"`, `fullBleed: boolean = true`, `gutter: string = "md"`, `contentGutter: string = "md"`, `flushMobile: boolean = false`, `titleSize: string = "default"`, `titleMaxWidth: string = ""`, `descriptionWeight: string = "default"`, `mobileTitleSize: string = "default"`, `mobileStack: boolean = true`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualStyle: string = "panel"`, `trustItems: unknown[] = []`, `class: string = ""`
- Slots: `default`, `actions`, `trust`, `visual`
- Outputs: None
@@ -799,7 +799,7 @@ Provide a responsive floating control that returns long pages to the top and can
Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events.
- Mount: `data-component="Breadcrumb"`
- Props: `label: string = "Breadcrumb"`, `items: unknown[] = []`, `active: string = ""`, `separator: string = "chevron"`, `showHome: boolean = false`, `homeLabel: string = "Home"`, `homeHref: string = "/"`, `homeIcon: string = "icon-[lucide--house]"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "minimal"`, `class: string = ""`
- Props: `label: string = "Breadcrumb"`, `items: unknown[] = []`, `active: string = ""`, `activeIcon: string = "icon-[lucide--file-text]"`, `separator: string = "chevron"`, `showHome: boolean = false`, `homeLabel: string = "Home"`, `homeHref: string = "/"`, `homeIcon: string = "icon-[lucide--house]"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "minimal"`, `class: string = ""`
- Slots: None
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
@@ -808,9 +808,9 @@ Show responsive hierarchical navigation with home support, separators, current-p
Theme-aware, responsive mega menu component.
- Mount: `data-component="MegaMenu"`
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `footer: string = ""`, `defaultOpen: boolean = false`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `rail: unknown[] = []`, `featured: Record<string, unknown> = {}`, `variant: string = "default"`, `density: string = "default"`, `panelWidth: string = "xl"`, `panelAlign: string = "start"`, `columnCount: number = 0`, `fullWidth: boolean = false`, `openOnHover: boolean = true`, `activeCategory: string = ""`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = "icon-[lucide--arrow-right]"`, `footer: string = ""`, `defaultOpen: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`, `category({ item: object; value: string; index: number })`
### Nav
@@ -826,8 +826,8 @@ Theme-aware, responsive nav component.
Theme-aware, responsive navbar component.
- Mount: `data-component="Navbar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record<string, unknown> = {}`, `items: unknown[] = []`, `actions: unknown[] = []`, `active: string = ""`, `sticky: boolean = false`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `class: string = ""`
- Slots: `topbar`, `actions`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record<string, unknown> = {}`, `items: unknown[] = []`, `actions: unknown[] = []`, `active: string = ""`, `sticky: boolean = false`, `floating: boolean = false`, `startFloating: string = "0px"`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `collapseAt: string = "900px"`, `mobilePanel: string = "content"`, `mobileUtilitiesPosition: string = "header"`, `gutter: string = "md"`, `contentGutter: string = "md"`, `flushMobile: boolean = false`, `translationPrefix: string = ""`, `class: string = ""`
- Slots: `topbar`, `actions`, `utilities`
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ item: string | number | boolean | null | object; value: string | number | boolean; level: string })`, `action({ item: string | number | boolean | null | object; value: string | number | boolean })`
### Pagination
@@ -844,7 +844,7 @@ Theme-aware, responsive pagination component.
Theme-aware, responsive scrollspy component.
- Mount: `data-component="Scrollspy"`
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `label: string = "On this page"`, `heading: string = ""`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `label: string = "On this page"`, `heading: string = ""`, `sticky: boolean = false`, `topOffset: string = "6rem"`, `sectionOffset: string = "7rem"`, `collapseAt: string = "800px"`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ href: string; label: string })`
+56
View File
@@ -90,6 +90,62 @@ the component directory at runtime.
For the full catalog, see [`COMPONENTS.md`](./COMPONENTS.md). The machine-readable
equivalent is exported as `@wrnexus/ui/component-reference.json`.
### Typed Navbar items
Keep navigation data in a shared TypeScript module and validate it with the
package contract. `NavbarItem` is a discriminated union of `link`, `dropdown`,
and `mega`; a mega item accepts every `MegaMenu` layout option.
```ts
import type { NavbarItem } from "@wrnexus/ui";
export const navigation = [
{ type: "link", label: "Pricing", href: "/pricing" },
{
type: "dropdown",
label: "Company",
items: [
{ label: "About", href: "/about" },
{ label: "Contact", href: "/contact" },
],
},
{
type: "mega",
label: "Products",
mega: {
variant: "icon-grid",
panelWidth: "2xl",
columnCount: 3,
rail: [{ label: "All products", href: "/products" }],
columns: [
{
heading: "Work",
items: [
{
label: "Projects",
href: "/products/projects",
icon: "icon-[lucide--folders]",
description: "Plan and deliver work",
badge: "New",
},
],
},
],
featured: {
title: "What's new",
actionLabel: "Explore",
actionHref: "/new",
},
actionLabel: "View all products",
actionHref: "/products",
},
},
] satisfies NavbarItem[];
```
Importing from `@wrnexus/ui/navigation` is also supported. Existing untyped
Navbar data using `children` remains accepted at runtime for migration.
## API
The JS module (`@wrnexus/ui`) exposes five helpers used by the build tooling to
+560 -3
View File
@@ -806,6 +806,69 @@
"default": "false",
"options": []
},
{
"name": "flush",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "mobileCompact",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "mobileMessageLines",
"type": "number",
"required": false,
"default": "2",
"options": []
},
{
"name": "hideBadgeOnMobile",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "hideDescriptionOnMobile",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "mobileActionSize",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "gutter",
"type": "string",
"required": false,
"default": "\"md\"",
"options": []
},
{
"name": "contentGutter",
"type": "string",
"required": false,
"default": "\"md\"",
"options": []
},
{
"name": "flushMobile",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "size",
"type": "string",
@@ -2180,6 +2243,48 @@
"default": "\"solid\"",
"options": []
},
{
"name": "spacing",
"type": "string",
"required": false,
"default": "\"none\"",
"options": []
},
{
"name": "radius",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "titleMaxWidth",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "contentPadding",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "mobilePadding",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "mobileActions",
"type": "string",
"required": false,
"default": "\"stack\"",
"options": []
},
{
"name": "primaryLabel",
"type": "string",
@@ -2315,7 +2420,21 @@
"name": "title",
"type": "string",
"required": false,
"default": "\"Card title\"",
"default": "\"\"",
"options": []
},
{
"name": "icon",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "titleKey",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
@@ -2325,6 +2444,13 @@
"default": "\"\"",
"options": []
},
{
"name": "subtitleKey",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "description",
"type": "string",
@@ -2332,6 +2458,13 @@
"default": "\"\"",
"options": []
},
{
"name": "descriptionKey",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "header",
"type": "string",
@@ -2374,6 +2507,13 @@
"default": "\"\"",
"options": []
},
{
"name": "actionLabelKey",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "actionHref",
"type": "string",
@@ -2493,6 +2633,27 @@
"default": "\"none\"",
"options": []
},
{
"name": "radius",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "shadow",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "equalHeight",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "scrollable",
"type": "boolean",
@@ -5833,6 +5994,34 @@
"default": "\"lift\"",
"options": []
},
{
"name": "radius",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "shadow",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "clickableSurface",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "descriptionTone",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -6198,6 +6387,20 @@
"default": "\"compact\"",
"options": []
},
{
"name": "mobileColumns",
"type": "number",
"required": false,
"default": "2",
"options": []
},
{
"name": "mobileCopyrightAlign",
"type": "string",
"required": false,
"default": "\"center\"",
"options": []
},
{
"name": "copyright",
"type": "string",
@@ -6205,6 +6408,20 @@
"default": "\"\"",
"options": []
},
{
"name": "copyrightKey",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "translationPrefix",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -8128,6 +8345,97 @@
"default": "[]",
"options": []
},
{
"name": "rail",
"type": "unknown[]",
"required": false,
"default": "[]",
"options": []
},
{
"name": "featured",
"type": "Record<string, unknown>",
"required": false,
"default": "{}",
"options": []
},
{
"name": "variant",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "density",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "panelWidth",
"type": "string",
"required": false,
"default": "\"xl\"",
"options": []
},
{
"name": "panelAlign",
"type": "string",
"required": false,
"default": "\"start\"",
"options": []
},
{
"name": "columnCount",
"type": "number",
"required": false,
"default": "0",
"options": []
},
{
"name": "fullWidth",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "openOnHover",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "activeCategory",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "actionLabel",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "actionHref",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "actionIcon",
"type": "string",
"required": false,
"default": "\"icon-[lucide--arrow-right]\"",
"options": []
},
{
"name": "footer",
"type": "string",
@@ -8163,9 +8471,13 @@
{
"name": "select",
"payloadType": "{ item: object; value: string }"
},
{
"name": "category",
"payloadType": "{ item: object; value: string; index: number }"
}
],
"events": ["open", "close", "select"],
"events": ["open", "close", "select", "category"],
"source": "components/MegaMenu.wrn"
},
{
@@ -8878,6 +9190,20 @@
"default": "false",
"options": []
},
{
"name": "floating",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "startFloating",
"type": "string",
"required": false,
"default": "\"0px\"",
"options": []
},
{
"name": "openOnHover",
"type": "boolean",
@@ -8899,6 +9225,55 @@
"default": "\"Toggle navigation\"",
"options": []
},
{
"name": "collapseAt",
"type": "string",
"required": false,
"default": "\"900px\"",
"options": []
},
{
"name": "mobilePanel",
"type": "string",
"required": false,
"default": "\"content\"",
"options": []
},
{
"name": "mobileUtilitiesPosition",
"type": "string",
"required": false,
"default": "\"header\"",
"options": []
},
{
"name": "gutter",
"type": "string",
"required": false,
"default": "\"md\"",
"options": []
},
{
"name": "contentGutter",
"type": "string",
"required": false,
"default": "\"md\"",
"options": []
},
{
"name": "flushMobile",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "translationPrefix",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -8907,7 +9282,7 @@
"options": []
}
],
"slots": ["topbar", "actions"],
"slots": ["topbar", "actions", "utilities"],
"outputs": [
{
"name": "toggle",
@@ -9826,6 +10201,55 @@
"default": "true",
"options": []
},
{
"name": "iconOnly",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "themeIcon",
"type": "string",
"required": false,
"default": "\"icon-[lucide--sun-moon]\"",
"options": []
},
{
"name": "colorIcon",
"type": "string",
"required": false,
"default": "\"icon-[lucide--palette]\"",
"options": []
},
{
"name": "languageIcon",
"type": "string",
"required": false,
"default": "\"icon-[lucide--languages]\"",
"options": []
},
{
"name": "panelPlacement",
"type": "string",
"required": false,
"default": "\"bottom-end\"",
"options": []
},
{
"name": "mobilePanel",
"type": "string",
"required": false,
"default": "\"dropdown\"",
"options": []
},
{
"name": "showCurrentLanguage",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "class",
"type": "string",
@@ -10532,6 +10956,34 @@
"default": "\"\"",
"options": []
},
{
"name": "sticky",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "topOffset",
"type": "string",
"required": false,
"default": "\"6rem\"",
"options": []
},
{
"name": "sectionOffset",
"type": "string",
"required": false,
"default": "\"7rem\"",
"options": []
},
{
"name": "collapseAt",
"type": "string",
"required": false,
"default": "\"800px\"",
"options": []
},
{
"name": "class",
"type": "string",
@@ -11346,6 +11798,62 @@
"default": "true",
"options": []
},
{
"name": "gutter",
"type": "string",
"required": false,
"default": "\"md\"",
"options": []
},
{
"name": "contentGutter",
"type": "string",
"required": false,
"default": "\"md\"",
"options": []
},
{
"name": "flushMobile",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "titleSize",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "titleMaxWidth",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "descriptionWeight",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "mobileTitleSize",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "mobileStack",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "backgroundImage",
"type": "string",
@@ -11498,6 +12006,41 @@
"default": "\"xl\"",
"options": []
},
{
"name": "radius",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "shadow",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "gutter",
"type": "string",
"required": false,
"default": "\"md\"",
"options": []
},
{
"name": "outerSpacing",
"type": "string",
"required": false,
"default": "\"none\"",
"options": []
},
{
"name": "mobileColumns",
"type": "number",
"required": false,
"default": "1",
"options": []
},
{
"name": "class",
"type": "string",
@@ -11844,6 +12387,20 @@
"default": "\"\"",
"options": []
},
{
"name": "showLabel",
"type": "string",
"required": false,
"default": "\"Show password\"",
"options": []
},
{
"name": "hideLabel",
"type": "string",
"required": false,
"default": "\"Hide password\"",
"options": []
},
{
"name": "class",
"type": "string",
+24 -3
View File
@@ -16,6 +16,15 @@ badge: string = ""
dismissLabel: string = "Dismiss announcement"
sticky: boolean = false
compact: boolean = false
flush: boolean = false
mobileCompact: boolean = false
mobileMessageLines: number = 2
hideBadgeOnMobile: boolean = false
hideDescriptionOnMobile: boolean = false
mobileActionSize: string = "default"
gutter: string = "md"
contentGutter: string = "md"
flushMobile: boolean = false
size: string = "default"
width: string = "default"
color: string = "primary"
@@ -36,6 +45,7 @@ badge: string = ""
data-color='{color}'
data-variant='{variant}'
data-compact='{compact}'
data-flush='{flush ? "true" : "false"}' data-mobile-compact='{mobileCompact ? "true" : "false"}' data-mobile-message-lines='{mobileMessageLines}' data-hide-badge-mobile='{hideBadgeOnMobile ? "true" : "false"}' data-hide-description-mobile='{hideDescriptionOnMobile ? "true" : "false"}' data-mobile-action-size='{mobileActionSize}' data-gutter='{gutter}' data-content-gutter='{contentGutter}' data-flush-mobile='{flushMobile ? "true" : "false"}'
data-sticky='{sticky}'
data-show='{!dismissed}'
aria-hidden='{dismissed ? "true" : "false"}'
@@ -152,9 +162,9 @@ badge: string = ""
position: relative;
z-index: 40;
display: block;
width: 100vw;
max-width: 100vw;
margin-inline: calc(50% - 50vw);
width: 100%;
max-width: 100%;
margin-inline: 0;
color: var(--wrn-color-text);
}
@@ -163,6 +173,8 @@ badge: string = ""
margin-inline: auto;
}
.wrn-announcement[data-flush="true"] .wrn-announcement__surface { border-radius: 0; box-shadow: none; }
.wrn-announcement[data-width="wide"] .wrn-announcement__surface {
width: min(calc(100% - 2rem), 90rem);
}
@@ -573,6 +585,15 @@ badge: string = ""
}
@media (max-width: 639px) {
.wrn-announcement[data-mobile-compact="true"] .wrn-announcement__inner,
.wrn-announcement[data-compact="true"] .wrn-announcement__inner { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 0.65rem; }
.wrn-announcement[data-hide-badge-mobile="true"] .wrn-announcement__badge,
.wrn-announcement[data-mobile-compact="true"] .wrn-announcement__badge { display: none; }
.wrn-announcement[data-hide-description-mobile="true"] .wrn-announcement__description,
.wrn-announcement[data-mobile-compact="true"] .wrn-announcement__description { display: none; }
.wrn-announcement[data-mobile-message-lines="2"] .wrn-announcement__message { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
.wrn-announcement[data-mobile-action-size="sm"] .wrn-announcement__action { min-height: 2.25rem; padding-inline: 0.7rem; }
.wrn-announcement[data-flush-mobile="false"] .wrn-announcement__inner { padding-inline: 1rem; }
.wrn-announcement__surface,
.wrn-announcement[data-width="wide"] .wrn-announcement__surface {
width: calc(100% - 1rem);
+2 -2
View File
@@ -120,11 +120,11 @@ component BackToTop {
}
.wrn-back-to-top[data-position="right"] {
right: 1.5rem;
right: max(1.5rem, env(safe-area-inset-right));
}
.wrn-back-to-top[data-position="left"] {
left: 1.5rem;
left: max(1.5rem, env(safe-area-inset-left));
}
.wrn-back-to-top[data-position="center"] {
+20
View File
@@ -12,6 +12,7 @@ component Blockquote {
variant: string = "default"
quoteMark: boolean = true
italic: boolean = true
maxWidth: string = "xl"
class: string = ""
}
@@ -24,6 +25,7 @@ component Blockquote {
data-align="{align}"
data-variant="{variant}"
data-italic="{italic}"
data-max-width="{maxWidth}"
>
<blockquote cite="{citationUrl}">
{#if quoteMark}
@@ -73,10 +75,17 @@ component Blockquote {
display: grid;
gap: 1.25rem;
width: 100%;
max-width: var(--wrn-blockquote-max-width, 48rem);
color: var(--wrn-color-text);
text-align: left;
}
.wrn-next--blockquote[data-max-width="sm"] { --wrn-blockquote-max-width: 32rem; }
.wrn-next--blockquote[data-max-width="md"] { --wrn-blockquote-max-width: 40rem; }
.wrn-next--blockquote[data-max-width="lg"] { --wrn-blockquote-max-width: 48rem; }
.wrn-next--blockquote[data-max-width="xl"] { --wrn-blockquote-max-width: 64rem; }
.wrn-next--blockquote[data-max-width="full"] { --wrn-blockquote-max-width: 100%; }
.wrn-next--blockquote[data-size="xs"] {
--wrn-blockquote-font-size: 0.875rem;
--wrn-blockquote-mark-size: 3rem;
@@ -189,8 +198,19 @@ component Blockquote {
.wrn-next--blockquote[data-align="center"] {
text-align: center;
margin-inline: auto;
}
.wrn-next--blockquote:is([data-variant="contained"], [data-variant="spotlight"], [data-variant="card"]) {
padding: clamp(1.5rem, 4vw, 3rem);
border: 1px solid color-mix(in srgb, var(--wrn-blockquote-color) 24%, var(--wrn-color-border));
border-radius: calc(var(--wrn-radius) * 2);
background: linear-gradient(145deg, color-mix(in srgb, var(--wrn-blockquote-color) 8%, var(--wrn-color-surface-raised)), var(--wrn-color-surface-raised));
box-shadow: var(--wrn-shadow-1);
}
.wrn-next--blockquote[data-variant="spotlight"] > blockquote { padding-top: calc(var(--wrn-blockquote-mark-size) * 0.55); }
.wrn-next--blockquote[data-align="right"],
.wrn-next--blockquote[data-align="end"] {
text-align: right;
+32 -1
View File
@@ -7,6 +7,7 @@ component Breadcrumb {
label: string = "Breadcrumb"
items: unknown[] = []
active: string = ""
activeIcon: string = "icon-[lucide--file-text]"
separator: string = "chevron"
showHome: boolean = false
homeLabel: string = "Home"
@@ -61,7 +62,7 @@ label: string = "Breadcrumb"
{:else if separator === "arrow"}
<span>→</span>
{:else}
<span class="icon-[lucide--chevron-right] wrn-breadcrumb__separator-icon"></span>
<span class="wrn-breadcrumb__separator-icon"></span>
{/if}
</span>
{/if}
@@ -99,6 +100,30 @@ label: string = "Breadcrumb"
{/if}
</li>
{/each}
{#if active && !items.some((item) => active === (item.value || item.label || item.title))}
<li class="wrn-breadcrumb__item" data-current="true">
{#if showHome || items.length > 0}
<span class="wrn-breadcrumb__separator" aria-hidden="true">
{#if separator === "slash"}
<span>/</span>
{:else if separator === "dot"}
<span>•</span>
{:else if separator === "arrow"}
<span>→</span>
{:else}
<span class="wrn-breadcrumb__separator-icon"></span>
{/if}
</span>
{/if}
<span class="wrn-breadcrumb__current" aria-current="page">
{#if activeIcon}
<span class='{activeIcon} wrn-breadcrumb__icon' aria-hidden="true"></span>
{/if}
<span class="wrn-breadcrumb__label">{active}</span>
</span>
</li>
{/if}
</ol>
</nav>
}
@@ -207,8 +232,14 @@ label: string = "Breadcrumb"
}
.wrn-breadcrumb__separator-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 0.9rem;
height: 0.9rem;
font-size: 1rem;
font-weight: 700;
line-height: 1;
}
.wrn-breadcrumb__link,
+42 -3
View File
@@ -8,6 +8,12 @@ component CTASection {
size: string = "default"
color: string = "primary"
variant: string = "solid"
spacing: string = "none"
radius: string = "default"
titleMaxWidth: string = ""
contentPadding: string = "default"
mobilePadding: string = "default"
mobileActions: string = "stack"
primaryLabel: string = "Get started"
primaryHref: string = "#"
primaryIcon: string = ""
@@ -35,6 +41,9 @@ component CTASection {
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
data-spacing='{spacing}'
data-radius='{radius}' data-content-padding='{contentPadding}' data-mobile-padding='{mobilePadding}' data-mobile-actions='{mobileActions}'
style='--wrn-cta-title-max-width: {titleMaxWidth || "18ch"}'
data-max-width='{maxWidth}'
data-full-bleed='{fullBleed ? "true" : "false"}'
data-visual-position='{visualPosition}'
@@ -186,6 +195,18 @@ component CTASection {
--cta-contrast: var(--wrn-color-secondary-contrast);
}
.wrn-cta-section[data-spacing="sm"] {
padding-block: 2rem;
}
.wrn-cta-section[data-spacing="md"] {
padding-block: 3rem;
}
.wrn-cta-section[data-spacing="lg"] {
padding-block: 4rem;
}
.wrn-cta-section[data-color="info"] {
--cta-accent: var(--wrn-color-info);
--cta-accent-hover: var(--wrn-color-info-hover);
@@ -223,6 +244,20 @@ component CTASection {
margin-inline: auto;
}
@media (max-width: 640px) {
.wrn-cta-section[data-spacing="sm"] {
padding-block: 1.5rem;
}
.wrn-cta-section[data-spacing="md"] {
padding-block: 2rem;
}
.wrn-cta-section[data-spacing="lg"] {
padding-block: 2.5rem;
}
}
.wrn-cta-section[data-max-width="compact"] .wrn-cta-section__outer {
width: min(calc(100% - 2rem), 64rem);
}
@@ -260,6 +295,8 @@ component CTASection {
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
0 24px 70px color-mix(in srgb, black 15%, transparent);
}
.wrn-cta-section[data-radius="xl"] .wrn-cta-section__surface { border-radius: 1.5rem; }
.wrn-cta-section[data-content-padding="lg"] .wrn-cta-section__surface { padding: 3.5rem clamp(1.5rem, 5vw, 4rem); }
.wrn-cta-section[data-size="compact"] .wrn-cta-section__surface,
.wrn-cta-section[data-size="sm"] .wrn-cta-section__surface {
@@ -401,7 +438,7 @@ component CTASection {
}
.wrn-cta-section__title {
max-width: 18ch;
max-width: var(--wrn-cta-title-max-width);
margin: 0.75rem 0 0;
color: var(--wrn-color-text);
font-size: clamp(2rem, 5vw, 4rem);
@@ -637,11 +674,11 @@ component CTASection {
line-height: 1.5;
}
.wrn-cta-section__footer:empty {
.wrn-cta-section__footer:not(:has(> *)) {
display: none;
}
.wrn-cta-section__footer:not(:empty) {
.wrn-cta-section__footer:has(> *) {
margin-top: 2rem;
padding-top: 1.25rem;
border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
@@ -671,6 +708,8 @@ component CTASection {
}
@media (max-width: 639px) {
.wrn-cta-section[data-mobile-padding="md"] .wrn-cta-section__surface { padding: 2.5rem 1.25rem; }
.wrn-cta-section[data-mobile-actions="inline"] .wrn-cta-section__action { width: auto; }
.wrn-cta-section__surface {
padding: 1.6rem;
border-radius: 1.2rem;
+13 -1
View File
@@ -9,7 +9,8 @@ component Card {
}
props {
title: string = "Card title"
title: string = ""
icon: string = ""
titleKey: string = ""
subtitle: string = ""
subtitleKey: string = ""
@@ -39,6 +40,9 @@ component Card {
layout: string = "vertical"
align: string = "left"
hover: string = "none"
radius: string = "default"
shadow: string = "default"
equalHeight: boolean = false
scrollable: boolean = false
maxHeight: string = "18rem"
dismissible: boolean = false
@@ -84,6 +88,7 @@ component Card {
data-layout='{layout}'
data-hover='{hover}'
data-align='{align}'
data-radius='{radius}' data-shadow='{shadow}' data-equal-height='{equalHeight ? "true" : "false"}'
class='wrn-component wrn-next--card wrn-component--color-{color} wrn-component--size-{size} wrn-next--variant-{variant} {class}'
>
{#if items.length > 0}
@@ -271,6 +276,7 @@ component Card {
<p>{emptyTitle}</p>
</div>
{:else}
{#if icon}<span class='wrn-next__card-icon {icon}' aria-hidden="true"></span>{/if}
{#if description}
<p class="wrn-next__card-description" data-t='{descriptionKey}'>{description}</p>
{/if}
@@ -343,6 +349,12 @@ component Card {
.wrn-next--card[data-size="sm"] {
--wrn-card-padding: 0.85rem;
}
.wrn-next--card[data-size="compact"] .wrn-next__card-body { padding: 1rem; }
.wrn-next--card[data-radius="lg"] > .wrn-next__card-panel { border-radius: 1rem; }
.wrn-next--card[data-shadow="none"] > .wrn-next__card-panel { box-shadow: none; }
.wrn-next--card[data-equal-height="true"], .wrn-next--card[data-equal-height="true"] > .wrn-next__card-panel { height: 100%; }
.wrn-next--card[data-layout="horizontal"] .wrn-next__card-body { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 0.8rem; }
.wrn-next__card-icon { display: grid; width: 2.35rem; height: 2.35rem; place-items: center; border-radius: 0.7rem; color: var(--wrn-color-primary); background: color-mix(in srgb, var(--wrn-color-primary) 12%, transparent); font-size: 1.15rem; }
.wrn-next--card[data-size="lg"] {
--wrn-card-padding: 1.75rem;
+2
View File
@@ -205,7 +205,9 @@ component ColorPicker {
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+2
View File
@@ -276,7 +276,9 @@ size: string = "default"
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+10 -1
View File
@@ -15,13 +15,17 @@ component FeatureIconCard {
variant: string = "default"
align: string = "left"
hover: string = "lift"
radius: string = "default"
shadow: string = "default"
clickableSurface: boolean = false
descriptionTone: string = "default"
class: string = ""
}
view {
<article
data-ui-component="FeatureIconCard"
data-size='{size}' data-variant='{variant}' data-hover='{hover}' data-align='{align}'
data-size='{size}' data-variant='{variant}' data-hover='{hover}' data-align='{align}' data-radius='{radius}' data-shadow='{shadow}' data-clickable-surface='{clickableSurface ? "true" : "false"}' data-description-tone='{descriptionTone}'
class='wrn-feature-icon-card {class}'
>
<div
@@ -62,6 +66,11 @@ component FeatureIconCard {
style {
.wrn-feature-icon-card { position: relative; display: flex; height: 100%; flex-direction: column; padding: 1.5rem; border: 1px solid var(--wrn-color-border); border-radius: calc(var(--wrn-radius) * 2); background: var(--wrn-color-surface-raised); box-shadow: var(--wrn-shadow-1); transition: transform var(--wrn-motion-base), border-color var(--wrn-motion-base), box-shadow var(--wrn-motion-base); }
.wrn-feature-icon-card[data-size="sm"] { padding: 1.25rem; } .wrn-feature-icon-card[data-size="lg"] { padding: 2rem; }
.wrn-feature-icon-card[data-size="compact"] { min-height: 0; padding: 1.25rem; }
.wrn-feature-icon-card[data-radius="lg"] { border-radius: 1rem; }
.wrn-feature-icon-card[data-shadow="sm"] { box-shadow: 0 5px 18px rgb(15 23 42 / 0.06); }
.wrn-feature-icon-card[data-description-tone="muted"] .wrn-feature-icon-card__description { color: var(--wrn-color-text-muted); font-weight: 500; }
.wrn-feature-icon-card[data-clickable-surface="true"] .wrn-feature-icon-card__action::after { position: absolute; inset: 0; content: ""; }
.wrn-feature-icon-card[data-variant="soft"] { background: var(--wrn-color-surface-soft); }
.wrn-feature-icon-card[data-variant="ghost"] { border-color: transparent; background: transparent; box-shadow: none; }
.wrn-feature-icon-card[data-hover="lift"]:hover { transform: translateY(-0.25rem); }
+2
View File
@@ -230,7 +230,9 @@ size: string = "default"
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
@@ -244,7 +244,9 @@ size: string = "default"
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+6
View File
@@ -12,6 +12,8 @@ component Footer {
items: unknown[] = []
columns: number = 3
maxWidth: string = "compact"
mobileColumns: number = 2
mobileCopyrightAlign: string = "center"
copyright: string = ""
copyrightKey: string = ""
translationPrefix: string = ""
@@ -36,6 +38,7 @@ component Footer {
data-ui-component="Footer"
data-size='{size}'
data-color='{color}'
data-mobile-columns='{mobileColumns}' data-mobile-copyright-align='{mobileCopyrightAlign}'
class='wrn-footer {maxWidth === "full" ? "wrn-footer--width-full" : ""} {class}'
>
<div
@@ -531,6 +534,9 @@ component Footer {
.wrn-footer__links {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.wrn-footer[data-mobile-columns="1"] .wrn-footer__links { grid-template-columns: 1fr; }
.wrn-footer[data-mobile-columns="2"] .wrn-footer__links { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.wrn-footer[data-mobile-copyright-align="start"] :is(.wrn-footer__copyright-left, .wrn-footer__copyright-right) { justify-content: flex-start; text-align: left; }
.wrn-footer__bottom,
.wrn-footer__bottom--slots {
grid-template-columns: 1fr;
+3 -11
View File
@@ -280,22 +280,14 @@ component Hero {
}
.wrn-hero[data-full-bleed="true"] {
width: 100vw;
max-width: 100vw;
margin-inline: calc(50% - 50vw);
width: 100%;
max-width: 100%;
margin-inline: 0;
border-right-width: 0;
border-left-width: 0;
border-radius: 0;
}
@supports (width: 100dvw) {
.wrn-hero[data-full-bleed="true"] {
width: 100dvw;
max-width: 100dvw;
margin-inline: calc(50% - 50dvw);
}
}
.wrn-hero[data-color="secondary"] {
--wrn-hero-accent: var(--wrn-color-secondary);
--wrn-hero-accent-soft: var(--wrn-color-secondary-soft, var(--wrn-color-primary-soft));
+2
View File
@@ -243,7 +243,9 @@ component InputGroup {
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+296 -39
View File
@@ -18,6 +18,7 @@ component MegaMenu {
open(payload: { sourceEvent: Event })
close(payload: { reason: string })
select(payload: { item: object; value: string })
category(payload: { item: object; value: string; index: number })
}
props {
@@ -26,12 +27,27 @@ component MegaMenu {
label: string = "Menu"
icon: string = ""
columns: unknown[] = []
rail: unknown[] = []
featured: Record<string, unknown> = {}
variant: string = "default"
density: string = "default"
panelWidth: string = "xl"
panelAlign: string = "start"
columnCount: number = 0
fullWidth: boolean = false
openOnHover: boolean = true
activeCategory: string = ""
actionLabel: string = ""
actionHref: string = ""
actionIcon: string = "icon-[lucide--arrow-right]"
footer: string = ""
defaultOpen: boolean = false
class: string = ""
}
state visible = defaultOpen
state closeTimer = null
state selectedCategory = activeCategory
functions {
shared function columnList() {
@@ -42,16 +58,66 @@ component MegaMenu {
return column && Array.isArray(column.items) ? column.items : []
}
shared function railList() {
return Array.isArray(rail) ? rail : []
}
shared function categoryValue(item) {
return item ? String(item.value || item.href || item.label || "") : ""
}
shared function activeCategoryIndex() {
const entries = railList()
if (!entries.length) {
return 0
}
if (!selectedCategory) {
return 0
}
for (let index = 0; index < entries.length; index += 1) {
if (categoryValue(entries[index]) === selectedCategory) {
return index
}
}
return 0
}
shared function visibleColumns() {
const entries = columnList()
if (variant !== "catalog") {
return entries
}
const selected = entries[activeCategoryIndex()]
return selected ? [selected] : []
}
client function showPanel(sourceEvent) {
cancelScheduledHide()
visible = true
output.open({ sourceEvent: sourceEvent })
}
client function hidePanel(reason) {
cancelScheduledHide()
visible = false
output.close({ reason: reason })
}
client function cancelScheduledHide() {
if (closeTimer) {
window.clearTimeout(closeTimer)
closeTimer = null
}
}
client function scheduleHide() {
cancelScheduledHide()
closeTimer = window.setTimeout(function () {
closeTimer = null
hidePanel("pointer-leave")
}, 220)
}
client function togglePanel(sourceEvent) {
if (visible) {
hidePanel("toggle")
@@ -60,6 +126,34 @@ component MegaMenu {
}
}
client function activatePanel(sourceEvent) {
if (openOnHover) {
showPanel(sourceEvent)
} else {
togglePanel(sourceEvent)
}
}
client function hoverPanel(sourceEvent) {
if (openOnHover) {
showPanel(sourceEvent)
}
}
client function chooseCategory(item, index, sourceEvent) {
const root = sourceEvent.currentTarget.closest(".wrn-mega")
const rail = sourceEvent.currentTarget.closest(".wrn-mega__rail")
const scrollTop = rail ? rail.scrollTop : 0
selectedCategory = categoryValue(item)
output.category({ item: item, value: selectedCategory, index: index })
window.requestAnimationFrame(function () {
const nextRail = root ? root.querySelector(".wrn-mega__rail") : null
if (nextRail) {
nextRail.scrollTop = scrollTop
}
})
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "Escape" && visible) {
sourceEvent.preventDefault()
@@ -84,20 +178,27 @@ component MegaMenu {
data-color='{color}'
data-size='{size}'
data-open='{visible}'
data-variant='{variant}'
data-density='{density}'
data-panel-width='{panelWidth}'
data-panel-align='{panelAlign}'
data-full-width='{fullWidth ? "true" : "false"}'
data-column-count='{columnCount}'
@keydown='handleKeydown(event)'
@mouseleave='hidePanel("pointer-leave")'
@mouseenter='cancelScheduledHide()'
@mouseleave='scheduleHide()'
>
<button
type="button"
class="wrn-mega__trigger"
aria-haspopup="true"
aria-expanded='{visible}'
@click='togglePanel(event)'
@mouseenter='showPanel(event)'
@focus='showPanel(event)'
aria-label='{label}'
@click='activatePanel(event)'
@mouseenter='hoverPanel(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__trigger-label" data-label='{label}' aria-hidden="true"></span>
<span class="wrn-mega__arrow" aria-hidden="true">&#8250;</span>
</button>
@@ -105,42 +206,76 @@ component MegaMenu {
class="wrn-mega__panel"
data-wrn-anchored="true"
data-show="visible"
style='{visible ? "" : "display:none"}'
data-wrn-roving="both"
@mouseenter='cancelScheduledHide()'
>
<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}
<div class="wrn-mega__layout">
{#if railList().length > 0}
<aside class="wrn-mega__rail">
{#each railList() as item, index}
{#if variant === "catalog"}
<button type="button" class="wrn-mega__rail-link" data-active='{activeCategoryIndex() === index ? "true" : "false"}' aria-pressed='{activeCategoryIndex() === index ? "true" : "false"}' @click='chooseCategory(item, index, event)'>
{#if item.icon}<span class="wrn-mega__rail-icon" aria-hidden="true"><span class='{item.icon}'></span></span>{/if}
<span><strong>{item.label}</strong>{#if item.description}<small>{item.description}</small>{/if}</span>
</button>
{:else}
<a class="wrn-mega__rail-link" href='{item.href || "#"}' @click='choose(item)'>
{#if item.icon}<span class="wrn-mega__rail-icon" aria-hidden="true"><span class='{item.icon}'></span></span>{/if}
<span><strong>{item.label}</strong>{#if item.description}<small>{item.description}</small>{/if}</span>
</a>
{/if}
{/each}
</aside>
{/if}
<div class="wrn-mega__columns">
{#each visibleColumns() as column}
<section class="wrn-mega__column">
<header class="wrn-mega__column-header">
{#if column.eyebrow}<p class="wrn-mega__eyebrow">{column.eyebrow}</p>{/if}
<h3 class="wrn-mega__heading" data-show="column.heading">{column.heading}</h3>
{#if column.description}<p class="wrn-mega__column-description">{column.description}</p>{/if}
</header>
{#if column.image}<img class="wrn-mega__column-image" src='{column.image}' alt='{column.imageAlt || ""}' loading="lazy" />{/if}
<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)'>
{#if item.image}<img class="wrn-mega__link-image" src='{item.image}' alt="" loading="lazy" />{/if}
{#if item.icon}
<span class='wrn-mega__link-icon {item.icon}' aria-hidden="true"></span>
{:else if variant === "catalog"}
<span class="wrn-mega__link-icon wrn-mega__link-icon--fallback" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"></rect><path d="M3 9h18"></path><path d="M9 21V9"></path></svg>
</span>
{/if}
<span class="wrn-mega__link-body">
{#if item.eyebrow}<span class="wrn-mega__link-eyebrow">{item.eyebrow}</span>{/if}
<span class="wrn-mega__link-label">{item.label}{#if item.badge}<span class="wrn-mega__badge">{item.badge}</span>{/if}</span>
{#if item.description}<span class="wrn-mega__link-description">{item.description}</span>{/if}
</span>
</span>
</a>
</li>
{/each}
</ul>
</section>
{/each}
</a>
</li>
{/each}
</ul>
{#if column.actionLabel}<a class="wrn-mega__column-action" href='{column.actionHref || "#"}'>{column.actionLabel}<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span></a>{/if}
</section>
{/each}
</div>
{#if featured.title || featured.image}
<aside class="wrn-mega__featured">
{#if featured.eyebrow}<p class="wrn-mega__eyebrow">{featured.eyebrow}</p>{/if}
{#if featured.image}<img src='{featured.image}' alt='{featured.imageAlt || ""}' loading="lazy" />{/if}
{#if featured.title}<strong>{featured.title}</strong>{/if}
{#if featured.description}<p>{featured.description}</p>{/if}
{#if featured.actionLabel}<a href='{featured.actionHref || "#"}'>{featured.actionLabel}<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span></a>{/if}
</aside>
{/if}
</div>
<p class="wrn-mega__footer" data-show="footer">{footer}</p>
{#if footer || actionLabel}<div class="wrn-mega__footer"><span>{footer}</span>{#if actionLabel}<a href='{actionHref || "#"}'>{actionLabel}<span class='{actionIcon}' aria-hidden="true"></span></a>{/if}</div>{/if}
<slot />
</div>
</div>
@@ -187,7 +322,7 @@ component MegaMenu {
border: 1px solid transparent;
border-radius: var(--wrn-radius-sm);
background: transparent;
color: var(--wrn-color-text-muted);
color: var(--wrn-color-text);
font: inherit;
font-size: 0.9rem;
font-weight: 600;
@@ -200,6 +335,10 @@ component MegaMenu {
color: var(--wrn-color-text);
}
.wrn-mega__trigger-label::before {
content: attr(data-label);
}
.wrn-mega__trigger:focus-visible {
outline: 2px solid var(--mega-accent);
outline-offset: 2px;
@@ -226,29 +365,78 @@ component MegaMenu {
left: 0;
right: 0;
bottom: 100%;
height: 0.6rem;
height: 0.85rem;
}
.wrn-mega__panel {
box-sizing: border-box;
position: absolute;
z-index: 40;
z-index: 1200;
top: calc(100% + 0.4rem);
left: 0;
width: max-content;
max-width: min(60rem, calc(100vw - 2rem));
max-height: calc(100dvh - 5.5rem);
padding: 1rem;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-lg);
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-1);
}
/* Keep SSR output closed before the reactive runtime hydrates it. */
.wrn-mega__panel[data-show="false"] { display: none; }
.wrn-mega__panel[data-show="true"] { display: flex; }
.wrn-mega[data-panel-width="lg"] .wrn-mega__panel { width: min(48rem, calc(100vw - 2rem)); }
.wrn-mega[data-panel-width="xl"] .wrn-mega__panel { width: min(72rem, calc(100vw - 2rem)); }
.wrn-mega[data-panel-width="2xl"] .wrn-mega__panel { width: min(88rem, calc(100vw - 2rem)); }
.wrn-mega[data-full-width="true"] .wrn-mega__panel { width: calc(100vw - 2rem); max-width: none; }
.wrn-mega[data-panel-align="center"] .wrn-mega__panel { left: 50%; transform: translateX(-50%); }
.wrn-mega[data-panel-align="end"] .wrn-mega__panel { right: 0; left: auto; }
.wrn-mega[data-variant="default"] .wrn-mega__panel {
width: max-content;
min-width: 15rem;
max-width: min(24rem, calc(100dvw - 2rem));
height: auto;
}
.wrn-mega[data-variant="default"] .wrn-mega__layout,
.wrn-mega[data-variant="default"] .wrn-mega__columns,
.wrn-mega[data-variant="default"][data-column-count] .wrn-mega__columns {
grid-template-columns: minmax(0, 1fr);
}
.wrn-mega[data-variant="default"] .wrn-mega__columns { overflow: visible; }
.wrn-mega[data-variant="default"] .wrn-mega__featured { width: auto; padding: 1rem 0 0; border-top: 1px solid var(--wrn-color-border); border-left: 0; }
.wrn-mega__layout { display: grid; min-height: 0; max-height: calc(100dvh - 8rem); flex: 1; grid-template-columns: auto minmax(0, 1fr) auto; gap: 1.25rem; overflow: hidden; }
.wrn-mega__columns {
display: grid;
min-height: 0;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
gap: 1.25rem;
overflow-y: auto;
overscroll-behavior: contain;
}
.wrn-mega[data-column-count="2"] .wrn-mega__columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.wrn-mega[data-column-count="3"] .wrn-mega__columns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.wrn-mega[data-column-count="4"] .wrn-mega__columns { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.wrn-mega[data-column-count="5"] .wrn-mega__columns { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.wrn-mega__rail { display: grid; min-width: 13rem; min-height: 0; padding: 0.75rem; align-content: start; gap: 0.25rem; overflow-y: auto; overscroll-behavior: contain; border-radius: var(--wrn-radius-lg); background: var(--wrn-color-surface-soft); }
.wrn-mega__rail-link { appearance: none; display: flex; width: 100%; padding: 0.65rem; align-items: flex-start; gap: 0.65rem; color: var(--wrn-color-text); border: 0; border-radius: var(--wrn-radius-sm); background: transparent; font: inherit; text-align: left; text-decoration: none; cursor: pointer; }
.wrn-mega__rail-link:hover { background: var(--wrn-color-surface); }
.wrn-mega__rail-link[data-active="true"] { color: var(--mega-accent); background: var(--wrn-color-surface); box-shadow: inset 3px 0 0 var(--mega-accent); }
.wrn-mega__rail-link > span:last-child { display: grid; gap: 0.2rem; }
.wrn-mega__rail-link strong { font-size: 0.82rem; }
.wrn-mega__rail-link small { max-width: 18ch; color: var(--wrn-color-text-muted); font-size: 0.72rem; line-height: 1.45; }
.wrn-mega__rail-icon { display: grid; width: 2.4rem; height: 2.4rem; flex: none; place-items: center; color: var(--mega-accent); border-radius: 0.7rem; background: color-mix(in srgb, var(--mega-accent) 12%, transparent); font-size: 1.15rem; }
.wrn-mega__column {
min-width: 0;
}
@@ -262,6 +450,10 @@ component MegaMenu {
text-transform: uppercase;
}
.wrn-mega__eyebrow, .wrn-mega__link-eyebrow { margin: 0 0 0.35rem; color: var(--wrn-color-text-muted); font-size: 0.65rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
.wrn-mega__column-description { margin: -0.25rem 0 0.75rem; color: var(--wrn-color-text-muted); font-size: 0.75rem; line-height: 1.5; }
.wrn-mega__column-image { width: 100%; aspect-ratio: 16 / 7; margin-bottom: 0.65rem; object-fit: cover; border-radius: var(--wrn-radius-sm); }
.wrn-mega__list {
display: grid;
gap: 0.15rem;
@@ -270,8 +462,14 @@ component MegaMenu {
list-style: none;
}
.wrn-mega__list > li {
min-width: 0;
}
.wrn-mega__link {
display: flex;
width: 100%;
min-width: 0;
align-items: flex-start;
gap: 0.5rem;
padding: 0.45rem 0.5rem;
@@ -280,6 +478,43 @@ component MegaMenu {
text-decoration: none;
}
.wrn-mega__link-icon { display: grid; width: 2rem; height: 2rem; flex: none; place-items: center; color: var(--mega-accent); border-radius: 999px; background: color-mix(in srgb, var(--mega-accent) 10%, var(--wrn-color-surface-soft)); }
.wrn-mega__link-image { width: 2.4rem; height: 2.4rem; flex: none; object-fit: cover; border-radius: 0.65rem; }
.wrn-mega__badge { display: inline-flex; margin-left: 0.4rem; padding: 0.12rem 0.35rem; align-items: center; color: var(--mega-accent); border: 1px solid color-mix(in srgb, var(--mega-accent) 35%, transparent); border-radius: 999px; font-size: 0.6rem; font-weight: 700; }
.wrn-mega__column-action, .wrn-mega__featured a, .wrn-mega__footer a { display: inline-flex; margin-top: 0.7rem; align-items: center; gap: 0.35rem; color: var(--mega-accent); font-size: 0.78rem; font-weight: 700; text-decoration: none; }
.wrn-mega__featured { display: grid; width: 14rem; padding-left: 1.25rem; align-content: start; gap: 0.55rem; border-left: 1px solid var(--wrn-color-border); }
.wrn-mega__featured img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: var(--wrn-radius-lg); }
.wrn-mega__featured strong { font-size: 0.85rem; line-height: 1.35; }
.wrn-mega__featured p { margin: 0; color: var(--wrn-color-text-muted); font-size: 0.75rem; line-height: 1.5; }
.wrn-mega[data-density="compact"] :is(.wrn-mega__panel, .wrn-mega__rail) { padding: 0.7rem; }
.wrn-mega[data-density="compact"] .wrn-mega__columns { gap: 0.65rem; }
.wrn-mega[data-variant="icon-grid"] .wrn-mega__list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.35rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__panel { height: min(36rem, calc(100dvh - 6.5rem)); }
.wrn-mega[data-variant="catalog"] .wrn-mega__layout { grid-template-columns: minmax(14rem, 17rem) minmax(0, 1fr); gap: 0; }
.wrn-mega[data-variant="catalog"] .wrn-mega__rail { min-width: 0; padding: 0.55rem; gap: 0.3rem; border-radius: calc(var(--wrn-radius-lg) - 0.25rem); }
.wrn-mega[data-variant="catalog"] .wrn-mega__rail-link { min-height: 2.5rem; padding: 0.45rem 0.55rem; align-items: center; gap: 0.7rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__rail-link strong { font-size: 0.76rem; font-weight: 400; line-height: 1.25; }
.wrn-mega[data-variant="catalog"] .wrn-mega__rail-link[data-active="true"] strong { font-weight: 600; }
.wrn-mega[data-variant="catalog"] .wrn-mega__rail-icon { display: grid; width: 1.65rem; height: 1.65rem; flex: 0 0 1.65rem; place-items: center; color: currentColor; border: 1px solid color-mix(in srgb, currentColor 35%, transparent); border-radius: 0.45rem; background: color-mix(in srgb, currentColor 8%, transparent); opacity: 0.9; }
.wrn-mega[data-variant="catalog"] .wrn-mega__rail-icon > span { display: block; width: 0.95rem; height: 0.95rem; color: currentColor !important; font-size: 0.95rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__rail-link:hover .wrn-mega__rail-icon,
.wrn-mega[data-variant="catalog"] .wrn-mega__rail-link[data-active="true"] .wrn-mega__rail-icon { opacity: 1; }
.wrn-mega[data-variant="catalog"] .wrn-mega__columns { padding: 0.35rem 0.5rem 0.75rem 1.25rem; grid-template-columns: minmax(0, 1fr); }
.wrn-mega[data-variant="catalog"] .wrn-mega__column { display: flex; min-height: 100%; flex-direction: column; }
.wrn-mega[data-variant="catalog"] .wrn-mega__column-header { width: 100%; min-width: 0; margin: 0 0 0.85rem; padding: 0 0 0.65rem; border: 0; border-bottom: 1px solid var(--wrn-color-border); border-radius: 0; background: transparent; }
.wrn-mega[data-variant="catalog"] .wrn-mega__heading { margin: 0; color: var(--wrn-color-text-muted); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }
.wrn-mega[data-variant="catalog"] .wrn-mega__column-description { margin: 0.3rem 0 0; }
.wrn-mega[data-variant="catalog"] .wrn-mega__list { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 0.3rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link { min-height: 2.7rem; padding: 0.45rem 0.55rem; align-items: center; gap: 0.55rem; border: 1px solid transparent; background: transparent; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link:hover { border-color: color-mix(in srgb, var(--mega-accent) 28%, var(--wrn-color-border)); background: color-mix(in srgb, var(--mega-accent) 7%, var(--wrn-color-surface)); transform: translateY(-1px); }
.wrn-mega[data-variant="catalog"] .wrn-mega__column-action { margin-top: auto; padding-top: 1rem; }
.wrn-mega[data-variant="dark"] .wrn-mega__panel { color: #f8fafc; border-color: #2b2b2b; background: #151515; }
.wrn-mega[data-variant="dark"] :is(.wrn-mega__heading, .wrn-mega__link, .wrn-mega__featured strong) { color: #f8fafc; }
.wrn-mega[data-variant="dark"] :is(.wrn-mega__link-description, .wrn-mega__column-description, .wrn-mega__eyebrow, .wrn-mega__featured p) { color: #a3a3a3; }
.wrn-mega[data-variant="dark"] .wrn-mega__link:hover { background: #242424; }
.wrn-mega__link:hover {
background: var(--wrn-color-surface-soft);
}
@@ -300,9 +535,15 @@ component MegaMenu {
min-width: 0;
}
.wrn-mega[data-variant="catalog"] .wrn-mega__link-icon { width: 1.65rem; height: 1.65rem; flex: 0 0 1.65rem; border: 1px solid color-mix(in srgb, var(--mega-accent) 22%, var(--wrn-color-border)); border-radius: 0.45rem; font-size: 0.9rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link-icon--fallback svg { width: 0.9rem; height: 0.9rem; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link-label { font-size: 0.78rem; line-height: 1.25; }
.wrn-mega[data-variant="catalog"] .wrn-mega__link-description { font-size: 0.7rem; line-height: 1.35; }
.wrn-mega__link-label {
font-size: 0.88rem;
font-weight: 600;
overflow-wrap: anywhere;
}
.wrn-mega__link-description {
@@ -312,8 +553,12 @@ component MegaMenu {
}
.wrn-mega__footer {
display: flex;
margin: 1rem 0 0;
padding-top: 0.75rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
border-top: 1px solid var(--wrn-color-border);
color: var(--wrn-color-text-muted);
font-size: 0.8rem;
@@ -333,12 +578,24 @@ component MegaMenu {
position: static;
width: auto;
max-width: 100%;
max-height: none;
overflow: visible;
box-shadow: none;
}
.wrn-mega__panel[data-show="false"] { display: none; }
.wrn-mega__columns {
grid-template-columns: 1fr;
}
.wrn-mega__layout { max-height: none; grid-template-columns: 1fr; overflow: visible; }
.wrn-mega__rail, .wrn-mega__columns { overflow: visible; }
.wrn-mega__featured { width: auto; padding: 1rem 0 0; border-top: 1px solid var(--wrn-color-border); border-left: 0; }
.wrn-mega[data-variant="icon-grid"] .wrn-mega__list { grid-template-columns: 1fr; }
.wrn-mega[data-variant="catalog"] .wrn-mega__panel { height: auto; }
.wrn-mega[data-variant="catalog"] .wrn-mega__layout { grid-template-columns: 1fr; }
.wrn-mega[data-variant="catalog"] .wrn-mega__columns { padding: 1rem 0 0; }
.wrn-mega[data-variant="catalog"] .wrn-mega__list { grid-template-columns: 1fr; }
}
}
}
+12 -1
View File
@@ -92,7 +92,9 @@ component MetricGrid {
@action='forwardAction(item, itemIndex, event)'
/>
{:empty}
<slot></slot>
<div class="wrn-metric-grid__empty" role="status">
<slot>No metrics are available.</slot>
</div>
{/each}
</div>
</div>
@@ -100,6 +102,15 @@ component MetricGrid {
}
style {
.wrn-metric-grid__empty {
grid-column: 1 / -1;
padding: 1.25rem;
color: var(--wrn-color-text-muted);
background: var(--wrn-color-surface-soft);
border: 1px dashed var(--wrn-color-border);
border-radius: 0.9rem;
text-align: center;
}
.wrn-metric-grid__frame {
width: 100%;
min-width: 0;
+290 -35
View File
@@ -1,3 +1,5 @@
import MegaMenu from "./MegaMenu.wrn"
component Navbar {
outputs {
toggle(payload: { open: boolean })
@@ -17,9 +19,17 @@ size: string = "default"
actions: unknown[] = []
active: string = ""
sticky: boolean = false
floating: boolean = false
startFloating: string = "0px"
openOnHover: boolean = false
maxWidth: string = "full"
mobileLabel: string = "Toggle navigation"
collapseAt: string = "900px"
mobilePanel: string = "content"
mobileUtilitiesPosition: string = "header"
gutter: string = "md"
contentGutter: string = "md"
flushMobile: boolean = false
translationPrefix: string = ""
class: string = ""
}
@@ -36,27 +46,112 @@ size: string = "default"
const slug = String(source).replace(/^\/+|\/+$/g, "").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase()
return slug ? translationPrefix + "." + slug + (field === "description" ? ".description" : "") : ""
}
client function toggleNavigation() {
mobileOpen = !mobileOpen
output.toggle({ open: mobileOpen })
output[mobileOpen ? "open" : "close"]({ source: "mobile" })
shared function itemType(item) {
if (item && item.type) {
return item.type
}
return item && ((item.items && item.items.length) || (item.children && item.children.length)) ? "dropdown" : "link"
}
client function selectItem(item, level) {
mobileOpen = false
shared function dropdownItems(item) {
if (!item) {
return []
}
if (Array.isArray(item.items)) {
return item.items
}
return Array.isArray(item.children) ? item.children : []
}
shared function dropdownGroupItems(item) {
if (!item) {
return []
}
if (Array.isArray(item.items)) {
return item.items
}
return Array.isArray(item.children) ? item.children : []
}
shared function megaData(item) {
return item && item.mega && typeof item.mega === "object" ? item.mega : item || {}
}
shared function megaColumns(item) {
const data = megaData(item)
if (Array.isArray(data.columns)) {
return data.columns
}
const legacy = dropdownItems(item)
if (!legacy.length) {
return []
}
const grouped = legacy.some(function (entry) {
return dropdownGroupItems(entry).length > 0
})
if (grouped) {
return legacy.map(function (entry) {
return {
heading: entry.label || "",
description: entry.description || "",
items: dropdownGroupItems(entry)
}
})
}
return [{ heading: item.description || "", items: legacy }]
}
client function setMobileNavigation(open, trigger) {
const root = trigger && trigger.closest ? trigger.closest("[data-wrn-navbar]") : null
if (!root) return
const collapse = root.querySelector(".wrn-navbar__collapse")
const toggle = root.querySelector(".wrn-navbar__toggle")
if (collapse) collapse.classList.toggle("is-open", open)
if (toggle) toggle.setAttribute("aria-expanded", open ? "true" : "false")
}
client function toggleNavigation(event) {
const open = event.currentTarget.getAttribute("aria-expanded") !== "true"
setMobileNavigation(open, event.currentTarget)
output.toggle({ open: open })
output[open ? "open" : "close"]({ source: "mobile" })
}
client function selectItem(item, level, event) {
setMobileNavigation(false, event.currentTarget)
output.select({ item: item, value: item.value || "", level: level })
}
client function selectAction(item) {
mobileOpen = false
client function selectAction(item, event) {
setMobileNavigation(false, event.currentTarget)
output.action({ item: item, value: item.value || "" })
}
shared function isItemActive(item) {
if (!item) return false
if (item.value && item.value === active) return true
if (!item.children || !item.children.length) return false
return item.children.some(function (child) {
if (!item) {
return false
}
if (item.value && item.value === active) {
return true
}
if (itemType(item) === "mega") {
const data = megaData(item)
const links = Array.isArray(data.rail) ? data.rail.slice() : []
megaColumns(item).forEach(function (column) {
if (column && Array.isArray(column.items)) {
column.items.forEach(function (child) {
links.push(child)
})
}
})
return links.some(function (child) {
return child.value && child.value === active
})
}
const entries = dropdownItems(item)
if (!entries.length) {
return false
}
return entries.some(function (child) {
return isItemActive(child)
})
}
@@ -70,12 +165,13 @@ size: string = "default"
}
view {
<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}">
<header class="wrn-navbar wrn-navbar--width-{maxWidth} wrn-component--color-{color} wrn-component--size-{size} {sticky ? 'wrn-navbar--sticky' : ''} {floating ? 'wrn-navbar--floating' : ''} {class}" style="--wrn-navbar-floating-start: {startFloating};" data-wrn-navbar data-open-on-hover="{openOnHover}" data-floating="{floating ? 'true' : 'false'}" data-collapse-at="{collapseAt}" data-mobile-panel="{mobilePanel}" data-mobile-utilities-position="{mobileUtilitiesPosition}" data-gutter="{gutter}" data-content-gutter="{contentGutter}" data-flush-mobile="{flushMobile ? 'true' : 'false'}">
<div class="wrn-navbar__topbar" aria-label="{topbarLabel}">
<slot name="topbar" />
</div>
<div class="wrn-navbar__main">
{#if brand.logo || brand.icon || brand.label || brand.description}
<a class="wrn-navbar__brand" href="{brand.href || '/'}" aria-label="{brand.ariaLabel || brand.label || label}">
{#if brand.logo}
<img class="wrn-navbar__brand-logo" src="{brand.logo}" alt="{brand.logoAlt || brand.label || ''}" width="{brand.logoWidth || ''}" height="{brand.logoHeight || ''}" />
@@ -90,15 +186,39 @@ size: string = "default"
</span>
{/if}
</a>
<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>
{/if}
<slot name="brand" />
<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}
{#if itemType(item) === "mega"}
<MegaMenu
class="wrn-navbar__mega"
data-active="{isItemActive(item) ? 'true' : 'false'}"
label="{item.label}"
icon="{item.icon || ''}"
columns={megaColumns(item)}
rail={megaData(item).rail || []}
featured={megaData(item).featured || {}}
variant="{megaData(item).variant || 'default'}"
density="{megaData(item).density || 'default'}"
panelWidth="{megaData(item).panelWidth || 'xl'}"
panelAlign="{megaData(item).panelAlign || 'start'}"
columnCount={megaData(item).columnCount || 0}
fullWidth={megaData(item).fullWidth || false}
openOnHover={openOnHover}
activeCategory="{megaData(item).activeCategory || ''}"
actionLabel="{megaData(item).actionLabel || ''}"
actionHref="{megaData(item).actionHref || ''}"
actionIcon="{megaData(item).actionIcon || 'icon-[lucide--arrow-right]'}"
footer="{megaData(item).footer || ''}"
/>
<a class="wrn-navbar__menu-link wrn-navbar__mobile-mega-link" data-wrn-roving-item="true" href="{item.href || megaData(item).actionHref || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{isItemActive(item) ? 'page' : 'false'}" @click="selectItem(item, 1, event)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span data-t="{translationKey(item, 'label')}">{item.label}</span>
</a>
{:else if itemType(item) === "dropdown"}
<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}
@@ -107,19 +227,19 @@ size: string = "default"
</summary>
<div class="wrn-navbar__panel wrn-navbar__panel--columns-{item.columns || 1}">
{#if item.description}<p class="wrn-navbar__panel-intro" data-t="{translationKey(item, 'description')}">{item.description}</p>{/if}
{#each item.children as child}
{#each dropdownItems(item) as child}
<div class="wrn-navbar__group">
{#if child.children && child.children.length}
{#if dropdownGroupItems(child).length}
{#if child.label}<strong class="wrn-navbar__group-title" data-t="{translationKey(child, 'label')}">{child.label}</strong>{/if}
{#if child.description}<small data-t="{translationKey(child, 'description')}">{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' : 'false'}" @click="selectItem(nested, 3)">
{#each dropdownGroupItems(child) as nested}
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @click="selectItem(nested, 3, event)">
{#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
<span><strong data-t="{translationKey(nested, 'label')}">{nested.label}</strong>{#if nested.description}<small data-t="{translationKey(nested, 'description')}">{nested.description}</small>{/if}</span>
</a>
{/each}
{:else}
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : 'false'}" @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, event)">
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
<span><strong data-t="{translationKey(child, 'label')}">{child.label}</strong>{#if child.description}<small data-t="{translationKey(child, 'description')}">{child.description}</small>{/if}</span>
</a>
@@ -129,7 +249,7 @@ size: string = "default"
</div>
</details>
{:else}
<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)">
<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, event)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span data-t="{translationKey(item, 'label')}">{item.label}</span>
</a>
@@ -138,15 +258,21 @@ size: string = "default"
</nav>
<div class="wrn-navbar__actions">
<slot name="actions" />
{#each actions as item}
<a class="wrn-navbar__action wrn-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, event)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span data-t="{translationKey(item, 'label')}">{item.label}</span>
</a>
{/each}
<slot name="actions" />
</div>
</div>
<div class="wrn-navbar__utilities"><slot name="utilities" /></div>
<button type="button" class="wrn-navbar__toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleNavigation(event)">
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
</button>
</div>
</header>
}
@@ -155,7 +281,7 @@ size: string = "default"
/* Full application navigation */
.wrn-navbar {
position: relative;
z-index: 40;
z-index: 1000;
width: 100%;
color: var(--wrn-color-text);
background: color-mix(in srgb, var(--wrn-color-surface) 96%, transparent);
@@ -168,12 +294,19 @@ size: string = "default"
backdrop-filter: blur(16px);
}
.wrn-navbar__topbar:empty,
.wrn-navbar__topbar:not(:has(*)) {
.wrn-navbar--floating {
position: sticky;
z-index: 1000;
top: var(--wrn-navbar-floating-start, 0px);
box-shadow: 0 10px 30px rgb(15 23 42 / 0.08);
backdrop-filter: blur(16px);
}
.wrn-navbar__topbar:not(:has(> *)) {
display: none;
}
.wrn-navbar__topbar:not(:empty) {
.wrn-navbar__topbar:has(> *) {
display: flex;
min-height: 2.25rem;
align-items: center;
@@ -188,6 +321,7 @@ size: string = "default"
}
.wrn-navbar__main {
position: relative;
display: flex;
min-height: 4.25rem;
width: min(100%, 90rem);
@@ -197,6 +331,17 @@ size: string = "default"
gap: 1.5rem;
}
.wrn-navbar[data-gutter="sm"] .wrn-navbar__main { padding-inline: 1rem; }
.wrn-navbar[data-gutter="lg"] .wrn-navbar__main { padding-inline: clamp(1.5rem, 5vw, 4rem); }
.wrn-navbar__utilities {
display: inline-flex;
align-items: center;
flex: none;
}
.wrn-navbar__utilities:not(:has(> *)) { display: none; }
.wrn-navbar--width-full .wrn-navbar__main {
width: 100%;
max-width: none;
@@ -265,20 +410,59 @@ size: string = "default"
.wrn-navbar__collapse {
min-width: 0;
flex: 1;
justify-content: flex-end;
gap: 1rem;
}
.wrn-navbar__menus {
position: absolute;
right: 0;
left: 0;
justify-content: center;
flex: 1;
pointer-events: none;
gap: 0.25rem;
}
.wrn-navbar__menus > * {
pointer-events: auto;
}
.wrn-navbar__actions {
justify-content: flex-end;
gap: 0.5rem;
}
.wrn-navbar__mega {
flex: 0 0 auto;
}
.wrn-navbar__mega .wrn-mega__trigger {
min-height: 2.75rem;
padding: 0.65rem 0.8rem;
border-radius: 0.65rem;
font-size: 0.8125rem;
font-weight: 500;
}
.wrn-navbar__mega[data-full-width="true"] .wrn-mega__panel {
position: fixed;
top: calc(var(--wrn-navbar-floating-start, 0px) + 4.25rem);
right: 1rem;
left: 1rem;
width: auto;
max-width: none;
}
.wrn-navbar__mega[data-variant="default"][data-full-width="true"] .wrn-mega__panel {
position: absolute;
top: calc(100% + 0.4rem);
right: auto;
left: 0;
width: max-content;
min-width: 15rem;
max-width: min(24rem, calc(100dvw - 2rem));
}
.wrn-navbar__menu-link,
.wrn-navbar__dropdown > summary,
.wrn-navbar__action {
@@ -295,13 +479,15 @@ size: string = "default"
}
.wrn-navbar__menu-link:hover,
.wrn-navbar__dropdown > summary:hover {
.wrn-navbar__dropdown > summary:hover,
.wrn-navbar__mega .wrn-mega__trigger: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"] {
.wrn-navbar__dropdown > summary[aria-current="page"],
.wrn-navbar__mega[data-active="true"] .wrn-mega__trigger {
color: var(--wrn-component-color, var(--wrn-color-primary));
font-weight: 600;
background: color-mix(
@@ -313,7 +499,8 @@ size: string = "default"
}
.wrn-navbar__menu-link[aria-current="page"]:hover,
.wrn-navbar__dropdown > summary[aria-current="page"]:hover {
.wrn-navbar__dropdown > summary[aria-current="page"]:hover,
.wrn-navbar__mega[data-active="true"] .wrn-mega__trigger:hover {
color: var(--wrn-component-color, var(--wrn-color-primary));
background: color-mix(
in srgb,
@@ -349,6 +536,7 @@ size: string = "default"
.wrn-navbar__panel {
position: absolute;
z-index: 1100;
top: calc(100% + 0.55rem);
left: 50%;
display: grid;
@@ -508,7 +696,13 @@ size: string = "default"
background: currentColor;
}
.wrn-navbar__mobile-mega-link { display: none; }
@media (max-width: 900px) {
.wrn-navbar__main {
min-height: 3.5rem;
padding-block: 0.4rem;
}
.wrn-navbar--width-compact .wrn-navbar__main {
width: 100%;
}
@@ -527,20 +721,39 @@ size: string = "default"
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);
height: auto;
max-height: calc(100dvh - 3.5rem);
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
.wrn-navbar[data-mobile-panel="sheet"] .wrn-navbar__collapse {
height: calc(100dvh - 3.5rem);
}
.wrn-navbar__collapse.is-open {
display: flex;
}
.wrn-navbar:has(.wrn-navbar__collapse.is-open)::after {
position: fixed;
z-index: -1;
inset: 0;
background: rgb(15 23 42 / 0.42);
content: "";
}
.wrn-navbar__menus,
.wrn-navbar__actions {
position: static;
transform: none;
width: 100%;
align-items: stretch;
flex-direction: column;
}
.wrn-navbar__mega,
.wrn-navbar__mega .wrn-mega__trigger {
width: 100%;
}
.wrn-navbar__mega { display: none; }
.wrn-navbar__mobile-mega-link { display: flex; }
.wrn-navbar__menu-link,
.wrn-navbar__dropdown > summary,
.wrn-navbar__action {
@@ -557,6 +770,12 @@ size: string = "default"
overflow: hidden;
animation: wrn-navbar-mobile-panel 180ms ease both;
}
.wrn-navbar__mega[data-full-width="true"] .wrn-mega__panel {
position: static;
width: 100%;
max-width: 100%;
margin-top: 0.25rem;
}
.wrn-navbar__dropdown:not([open]) > .wrn-navbar__panel,
.wrn-navbar__dropdown[open] > .wrn-navbar__panel {
transform: none;
@@ -569,7 +788,43 @@ size: string = "default"
}
}
@media (max-width: 1100px) and (min-width: 901px) {
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__toggle { display: block; }
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__collapse {
position: absolute;
top: 100%;
right: 0;
left: 0;
display: none;
max-height: calc(100dvh - 3.5rem);
padding: 0.75rem 1rem 1rem;
align-items: stretch;
flex-direction: column;
overflow-y: auto;
background: var(--wrn-color-surface);
border-bottom: 1px solid var(--wrn-color-border);
box-shadow: 0 16px 32px rgb(15 23 42 / 0.12);
}
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__collapse.is-open { display: flex; }
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__menus,
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__actions {
position: static;
transform: none;
width: 100%;
align-items: stretch;
flex: none;
flex-direction: column;
}
.wrn-navbar[data-collapse-at="1100px"] :is(.wrn-navbar__menu-link, .wrn-navbar__dropdown > summary, .wrn-navbar__action) { width: 100%; }
.wrn-navbar[data-collapse-at="1100px"] :is(.wrn-navbar__mega, .wrn-navbar__mega .wrn-mega__trigger) { width: 100%; }
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__mega { display: none; }
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__mobile-mega-link { display: flex; }
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__panel { position: static; width: 100%; max-width: none; transform: none; }
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__mega[data-full-width="true"] .wrn-mega__panel { position: static; width: 100%; max-width: 100%; margin-top: 0.25rem; }
}
@media (max-width: 600px) {
.wrn-navbar[data-flush-mobile="false"] .wrn-navbar__main { padding-inline: 1rem; }
.wrn-navbar__brand-copy small {
display: none;
}
+8
View File
@@ -576,6 +576,14 @@ current: true
display: none;
}
/* Compiled empty slots retain inert template markers, so :empty alone
cannot collapse their spacing in the rendered document. */
.wrn-page-header__meta:not(:has(> :not(template))),
.wrn-page-header__body:not(:has(> :not(template))),
.wrn-page-header__actions--slot-only:not(:has(> :not(template))) {
display: none;
}
.wrn-page-header__meta {
margin-top: 1.1rem;
}
+28 -9
View File
@@ -25,6 +25,13 @@ size: string = "default"
{"label": "Amber", "value": "amber", "hex": "#d97706"}
]
compact: boolean = true
iconOnly: boolean = true
themeIcon: string = "icon-[lucide--sun-moon]"
colorIcon: string = "icon-[lucide--palette]"
languageIcon: string = "icon-[lucide--languages]"
panelPlacement: string = "bottom-end"
mobilePanel: string = "dropdown"
showCurrentLanguage: boolean = true
class: string = ""
}
@@ -37,11 +44,11 @@ size: string = "default"
}
view {
<div class="wrn-preferences wrn-component--color-{color} wrn-component--size-{size} {compact ? 'wrn-preferences--compact' : ''} {class}" data-wrn-preferences aria-label="Display and language preferences">
<div class="wrn-preferences wrn-component--color-{color} wrn-component--size-{size} {compact ? 'wrn-preferences--compact' : ''} {class}" data-wrn-preferences data-icon-only="{iconOnly ? 'true' : 'false'}" data-panel-placement="{panelPlacement}" data-mobile-panel="{mobilePanel}" aria-label="Display and language preferences">
<details class="wrn-preferences__menu" name="wrn-preference-menu">
<summary aria-label="{themeLabel}" title="{themeLabel}">
<span class="icon-[lucide--sun-moon]" aria-hidden="true"></span>
{#if !compact}<span>{themeLabel}</span>{/if}
<span class="{themeIcon}" aria-hidden="true"></span>
{#if !iconOnly && !compact}<span>{themeLabel}</span>{/if}
</summary>
<div class="wrn-preferences__panel">
<strong>{themeLabel}</strong>
@@ -54,8 +61,8 @@ size: string = "default"
<details class="wrn-preferences__menu" name="wrn-preference-menu">
<summary aria-label="{colorLabel}" title="{colorLabel}">
<span class="icon-[lucide--palette]" aria-hidden="true"></span>
{#if !compact}<span>{colorLabel}</span>{/if}
<span class="{colorIcon}" aria-hidden="true"></span>
{#if !iconOnly && !compact}<span>{colorLabel}</span>{/if}
</summary>
<div class="wrn-preferences__panel wrn-preferences__panel--colors">
<strong>{colorLabel}</strong>
@@ -69,9 +76,9 @@ size: string = "default"
<details class="wrn-preferences__menu wrn-preferences__menu--language" name="wrn-preference-menu">
<summary aria-label="{languageLabel}" title="{languageLabel}">
<span class="icon-[lucide--languages]" aria-hidden="true"></span>
<span class="wrn-preferences__current-language" aria-hidden="true">EN</span>
{#if !compact}<span>{languageLabel}</span>{/if}
<span class="{languageIcon}" aria-hidden="true"></span>
{#if showCurrentLanguage}<span class="wrn-preferences__current-language" aria-hidden="true">EN</span>{/if}
{#if !iconOnly && !compact}<span>{languageLabel}</span>{/if}
</summary>
<div class="wrn-preferences__panel wrn-preferences__panel--language">
<strong>{languageLabel}</strong>
@@ -157,6 +164,9 @@ size: string = "default"
animation: wrn-component-enter 160ms ease both;
}
.wrn-preferences[data-panel-placement="bottom-start"] .wrn-preferences__panel { right: auto; left: 0; }
.wrn-preferences[data-panel-placement="bottom-center"] .wrn-preferences__panel { right: auto; left: 50%; transform: translateX(-50%); }
.wrn-preferences__panel > strong {
padding-inline: 0.25rem;
font-size: 0.7rem;
@@ -242,7 +252,7 @@ size: string = "default"
}
@media (max-width: 640px) {
.wrn-preferences__panel {
.wrn-preferences[data-mobile-panel="sheet"] .wrn-preferences__panel {
position: fixed;
top: auto;
right: 1rem;
@@ -250,6 +260,15 @@ size: string = "default"
left: 1rem;
width: auto;
}
.wrn-preferences[data-mobile-panel="dropdown"] .wrn-preferences__panel {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
bottom: auto;
left: auto;
width: max-content;
max-width: min(22rem, calc(100vw - 2rem));
}
}
/* Shared component foundation. */
+2
View File
@@ -365,7 +365,9 @@ component RangeSlider {
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+12
View File
@@ -24,6 +24,10 @@ component Scrollspy {
active: string = ""
label: string = "On this page"
heading: string = ""
sticky: boolean = false
topOffset: string = "6rem"
sectionOffset: string = "7rem"
collapseAt: string = "800px"
class: string = ""
}
@@ -44,6 +48,7 @@ component Scrollspy {
class='wrn-scrollspy {class}'
data-color='{color}'
data-size='{size}'
data-sticky='{sticky ? "true" : "false"}' data-collapse-at='{collapseAt}' style='--wrn-scrollspy-top: {topOffset}; --wrn-scrollspy-section-offset: {sectionOffset};'
data-wrn-scrollspy="true"
role="navigation"
aria-label='{label}'
@@ -74,6 +79,12 @@ component Scrollspy {
--scrollspy-accent: var(--wrn-color-primary);
max-width: 100%;
}
.wrn-scrollspy[data-sticky="true"] { position: sticky; top: var(--wrn-scrollspy-top); height: max-content; }
.wrn-scrollspy__link { scroll-margin-top: var(--wrn-scrollspy-section-offset); }
.wrn-scrollspy:not(:has(.wrn-scrollspy__item)):not(:has(slot > *)) {
display: none;
}
.wrn-scrollspy[data-color="secondary"] {
--scrollspy-accent: var(--wrn-color-secondary);
@@ -148,6 +159,7 @@ component Scrollspy {
* line rather than a screenful.
*/
@media (max-width: 767px) {
.wrn-scrollspy[data-collapse-at="800px"] { position: static; }
.wrn-scrollspy__list {
grid-auto-flow: column;
grid-auto-columns: max-content;
+1
View File
@@ -54,6 +54,7 @@ component Section {
padding-block: 3rem;
background: var(--section-tint);
color: var(--section-ink);
scroll-margin-top: var(--wrn-sticky-header-offset, 5rem);
}
.wrn-section[data-spacing="sm"] {
+16
View File
@@ -20,6 +20,14 @@ component SplitHero {
variant: string = "default"
maxWidth: string = "xl"
fullBleed: boolean = true
gutter: string = "md"
contentGutter: string = "md"
flushMobile: boolean = false
titleSize: string = "default"
titleMaxWidth: string = ""
descriptionWeight: string = "default"
mobileTitleSize: string = "default"
mobileStack: boolean = true
backgroundImage: string = ""
visualImage: string = ""
visualAlt: string = ""
@@ -45,6 +53,8 @@ component SplitHero {
data-variant='{variant}'
data-max-width='{maxWidth}'
data-full-bleed='{fullBleed ? "true" : "false"}'
data-gutter='{gutter}' data-content-gutter='{contentGutter}' data-flush-mobile='{flushMobile ? "true" : "false"}' data-title-size='{titleSize}' data-description-weight='{descriptionWeight}' data-mobile-title-size='{mobileTitleSize}' data-mobile-stack='{mobileStack ? "true" : "false"}'
style='--wrn-split-title-max-width: {titleMaxWidth || "18ch"}'
data-visual-style='{visualStyle}'
class='wrn-split-hero {class}'
>
@@ -413,6 +423,7 @@ component SplitHero {
}
.wrn-split-hero__title {
max-width: var(--wrn-split-title-max-width);
max-width: 15ch;
margin: 1.45rem 0 0;
color: var(--wrn-color-text);
@@ -421,6 +432,8 @@ component SplitHero {
line-height: 1.02;
letter-spacing: -0.05em;
}
.wrn-split-hero[data-title-size="xl"] .wrn-split-hero__title { font-size: clamp(3rem, 5.5vw, 5rem); }
.wrn-split-hero[data-description-weight="medium"] .wrn-split-hero__description { font-weight: 500; }
.wrn-split-hero[data-size="compact"] .wrn-split-hero__title,
.wrn-split-hero[data-size="sm"] .wrn-split-hero__title {
@@ -771,6 +784,7 @@ component SplitHero {
.wrn-split-hero[data-ratio="content"] .wrn-split-hero__layout {
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
}
.wrn-split-hero[data-ratio="wide-copy"] .wrn-split-hero__layout { grid-template-columns: minmax(0, 1.12fr) minmax(20rem, 0.88fr); }
.wrn-split-hero[data-ratio="visual"] .wrn-split-hero__layout {
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
@@ -786,6 +800,8 @@ component SplitHero {
}
@media (max-width: 639px) {
.wrn-split-hero[data-flush-mobile="false"] .wrn-split-hero__inner { width: calc(100% - 2rem); }
.wrn-split-hero[data-mobile-title-size="lg"] .wrn-split-hero__title { font-size: clamp(2.5rem, 12vw, 3.5rem); }
.wrn-split-hero__inner {
padding-block: 3.25rem;
}
+47 -4
View File
@@ -9,6 +9,11 @@ component StatsBar {
color: string = "primary"
variant: string = "raised"
maxWidth: string = "xl"
radius: string = "default"
shadow: string = "default"
gutter: string = "md"
outerSpacing: string = "none"
mobileColumns: number = 1
class: string = ""
}
@@ -24,6 +29,7 @@ component StatsBar {
data-color='{color}'
data-variant='{variant}'
data-max-width='{maxWidth}'
data-radius='{radius}' data-shadow='{shadow}' data-gutter='{gutter}' data-outer-spacing='{outerSpacing}' data-mobile-columns='{mobileColumns}'
class='wrn-stats-bar {class}'
aria-label="Key statistics"
>
@@ -91,6 +97,8 @@ component StatsBar {
</div>
{/if}
</article>
{:empty}
<div class="wrn-stats-bar__empty" role="status">No statistics are available.</div>
{/each}
</div>
</div>
@@ -98,6 +106,15 @@ component StatsBar {
}
style {
.wrn-stats-bar__empty {
grid-column: 1 / -1;
padding: 1.25rem;
color: var(--wrn-color-text-muted);
background: var(--wrn-color-surface-soft);
border: 1px dashed var(--wrn-color-border);
border-radius: 0.9rem;
text-align: center;
}
.wrn-stats-bar {
--stats-accent: var(--wrn-color-primary);
--stats-contrast: var(--wrn-color-primary-contrast);
@@ -135,21 +152,43 @@ component StatsBar {
}
.wrn-stats-bar__inner {
width: min(calc(100% - 2rem), 80rem);
--stats-max-width: 80rem;
--stats-gutter-width: 0rem;
width: min(
calc(100% - var(--stats-gutter-width)),
calc(var(--stats-max-width) - var(--stats-gutter-width))
);
margin-inline: auto;
}
.wrn-stats-bar[data-gutter="sm"] .wrn-stats-bar__inner {
--stats-gutter-width: 2rem;
}
.wrn-stats-bar[data-gutter="md"] .wrn-stats-bar__inner {
--stats-gutter-width: 4rem;
}
.wrn-stats-bar[data-gutter="lg"] .wrn-stats-bar__inner {
--stats-gutter-width: 6rem;
}
.wrn-stats-bar[data-outer-spacing="md"] { margin-block: 2rem; }
.wrn-stats-bar[data-radius="xl"] .wrn-stats-bar__surface { overflow: hidden; border-radius: 1.25rem; }
.wrn-stats-bar[data-shadow="sm"] .wrn-stats-bar__surface { box-shadow: 0 10px 30px rgb(15 23 42 / 0.05); }
.wrn-stats-bar[data-variant="card"] .wrn-stats-bar__surface { border: 1px solid var(--wrn-color-border); background: var(--wrn-color-surface); }
.wrn-stats-bar[data-max-width="compact"] .wrn-stats-bar__inner {
width: min(calc(100% - 2rem), 64rem);
--stats-max-width: 64rem;
}
.wrn-stats-bar[data-max-width="lg"] .wrn-stats-bar__inner {
width: min(calc(100% - 2rem), 72rem);
--stats-max-width: 72rem;
}
.wrn-stats-bar[data-max-width="wide"] .wrn-stats-bar__inner,
.wrn-stats-bar[data-max-width="2xl"] .wrn-stats-bar__inner {
width: min(calc(100% - 2rem), 90rem);
--stats-max-width: 90rem;
}
.wrn-stats-bar[data-max-width="full"] .wrn-stats-bar__inner {
@@ -469,6 +508,10 @@ component StatsBar {
}
@media (max-width: 639px) {
.wrn-stats-bar[data-mobile-columns="1"] .wrn-stats-bar__surface { grid-template-columns: 1fr; }
.wrn-stats-bar[data-gutter="sm"] .wrn-stats-bar__inner,
.wrn-stats-bar[data-gutter="md"] .wrn-stats-bar__inner { --stats-gutter-width: 2rem; }
.wrn-stats-bar[data-gutter="lg"] .wrn-stats-bar__inner { --stats-gutter-width: 3rem; }
.wrn-stats-bar[data-max-width="full"] .wrn-stats-bar__surface {
border-inline: 0;
border-radius: 0;
@@ -649,7 +649,9 @@ component StrongPassword {
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+17 -5
View File
@@ -41,6 +41,18 @@ component Tabs {
return String(item.value || item.id || index)
}
shared function tabId(item, index) {
return "wrn-tab-" + valueOf(item, index)
}
shared function panelId(item, index) {
return "wrn-panel-" + valueOf(item, index)
}
shared function panelSlot(item, index) {
return "panel-" + valueOf(item, index)
}
/*
* In url mode the query parameter is the source of truth, not local state.
*
@@ -130,8 +142,8 @@ component Tabs {
role="tab"
data-value='{valueOf(item, index)}'
data-wrn-roving-item="true"
id='wrn-tab-{valueOf(item, index)}'
aria-controls='wrn-panel-{valueOf(item, index)}'
id='{tabId(item, index)}'
aria-controls='{panelId(item, index)}'
aria-selected='{isSelected(item, index) ? "true" : "false"}'
aria-disabled='{item.disabled ? "true" : "false"}'
@click='selectTab(item, index, event)'
@@ -148,8 +160,8 @@ component Tabs {
<div
class="wrn-tabs__panel"
role="tabpanel"
id='wrn-panel-{valueOf(item, index)}'
aria-labelledby='wrn-tab-{valueOf(item, index)}'
id='{panelId(item, index)}'
aria-labelledby='{tabId(item, index)}'
tabindex="0"
data-show='isSelected(item, index)'
>
@@ -158,7 +170,7 @@ component Tabs {
</h3>
<p class="wrn-tabs__description" data-show="item.description">{item.description}</p>
<div class="wrn-tabs__content" data-show="item.content">{item.content}</div>
<slot name="panel-{valueOf(item, index)}"></slot>
<slot name='{panelSlot(item, index)}'></slot>
</div>
{/each}
<slot />
+2
View File
@@ -249,7 +249,9 @@ size: string = "default"
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+28 -7
View File
@@ -10,6 +10,7 @@ component Timeline {
description: string = ""
items: unknown[] = []
variant: string = "default"
numbered: boolean = false
class: string = ""
}
@@ -18,6 +19,9 @@ component Timeline {
data-ui-component="Timeline"
aria-label='{title}'
class='wrn-timeline {class}'
data-size='{size}'
data-color='{color}'
data-variant='{variant}'
>
{#if title || description}
<header class="wrn-timeline__header">
@@ -34,13 +38,18 @@ component Timeline {
{#each items as item, index}
<li
class="wrn-timeline__item"
data-interactive='{item.href ? "true" : "false"}'
@click='output.select({ item: item, index: index })'
>
<span
class="wrn-timeline__marker"
data-status='{item.status || "default"}'
>
<span class='{item.icon || "icon-[lucide--circle]"}' aria-hidden="true"></span>
{#if numbered}
<span class="wrn-timeline__number" aria-hidden="true">{index + 1}</span>
{:else}
<span class='{item.icon || "icon-[lucide--circle-check]"}' aria-hidden="true"></span>
{/if}
</span>
<article
@@ -81,22 +90,28 @@ component Timeline {
}
style {
.wrn-timeline { width: 100%; }
.wrn-timeline { --wrn-timeline-accent: var(--wrn-color-primary); width: 100%; min-width: 0; color: var(--wrn-color-text); }
.wrn-timeline[data-color="success"] { --wrn-timeline-accent: var(--wrn-color-success); }
.wrn-timeline[data-color="info"] { --wrn-timeline-accent: var(--wrn-color-info); }
.wrn-timeline__header { margin-bottom: 1.5rem; }
.wrn-timeline__title { color: var(--wrn-color-text); font-size: 1.25rem; font-weight: 700; }
.wrn-timeline__description { max-width: 42rem; margin-top: 0.5rem; color: var(--wrn-color-text-muted); line-height: 1.75rem; }
.wrn-timeline__list { position: relative; margin-left: 1rem; border-left: 1px solid var(--wrn-color-border); }
.wrn-timeline__item { position: relative; padding: 0 0 2rem 2rem; cursor: pointer; }
.wrn-timeline__list { position: relative; display: grid; gap: 1rem; margin: 0; padding: 0 0 0 3rem; list-style: none; }
.wrn-timeline__list::before { position: absolute; inset-block: 1rem; inset-inline-start: 1rem; width: 2px; border-radius: 999px; background: color-mix(in srgb, var(--wrn-timeline-accent) 30%, var(--wrn-color-border)); content: ""; }
.wrn-timeline__item { position: relative; min-width: 0; }
.wrn-timeline__item:last-child { padding-bottom: 0; }
.wrn-timeline__marker {
position: absolute; top: 0; left: -1.05rem; display: flex; width: 2rem; height: 2rem;
position: absolute; z-index: 1; top: 1rem; inset-inline-start: -3rem; display: flex; width: 2rem; height: 2rem;
align-items: center; justify-content: center; border: 4px solid var(--wrn-color-background);
border-radius: 50%; color: var(--wrn-color-on-primary); background: var(--wrn-color-primary); box-shadow: var(--wrn-shadow-1);
border-radius: 50%; color: var(--wrn-color-on-primary); background: var(--wrn-timeline-accent); box-shadow: var(--wrn-shadow-1);
}
.wrn-timeline__marker:is([data-status="complete"], [data-status="success"]) { background: var(--wrn-color-success); }
.wrn-timeline__marker:is([data-status="warning"], [data-status="pending"]) { background: var(--wrn-color-warning); }
.wrn-timeline__marker:is([data-status="danger"], [data-status="failed"]) { background: var(--wrn-color-danger); }
.wrn-timeline__card { padding: 1.25rem; 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-timeline__number { font-size: 0.75rem; font-weight: 800; }
.wrn-timeline__card { min-width: 0; padding: 1.25rem; 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); transition: border-color 160ms ease, transform 160ms ease, box-shadow 160ms ease; }
.wrn-timeline__item[data-interactive="true"] { cursor: pointer; }
.wrn-timeline__item[data-interactive="true"]:hover .wrn-timeline__card { border-color: color-mix(in srgb, var(--wrn-timeline-accent) 45%, var(--wrn-color-border)); transform: translateY(-2px); box-shadow: var(--wrn-shadow-2); }
.wrn-timeline__card[data-variant="minimal"] { padding: 0; border-color: transparent; background: transparent; box-shadow: none; }
.wrn-timeline__card-header { display: flex; flex-direction: column; gap: 0.5rem; }
.wrn-timeline__item-title { color: var(--wrn-color-text); font-weight: 700; }
@@ -110,5 +125,11 @@ component Timeline {
@media (min-width: 40rem) {
.wrn-timeline__card-header { flex-direction: row; align-items: flex-start; justify-content: space-between; }
}
@media (max-width: 39.99rem) {
.wrn-timeline__list { padding-inline-start: 2.5rem; }
.wrn-timeline__list::before { inset-inline-start: 0.75rem; }
.wrn-timeline__marker { inset-inline-start: -2.5rem; }
.wrn-timeline__card { padding: 1rem; }
}
}
}
@@ -492,7 +492,9 @@ component TogglePassword {
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+2
View File
@@ -360,7 +360,9 @@ size: string = "default"
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+2
View File
@@ -315,7 +315,9 @@ component Input {
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+2
View File
@@ -221,7 +221,9 @@ size: string = "default"
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+2
View File
@@ -244,7 +244,9 @@ size: string = "default"
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+2
View File
@@ -243,7 +243,9 @@ component Switch {
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+2
View File
@@ -223,7 +223,9 @@ size: string = "default"
}
.wrn-next__field-heading label {
color: var(--wrn-color-text);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.35;
}
.wrn-next__field-hint,
.wrn-next__field-help {
+2 -1
View File
@@ -1,11 +1,12 @@
{
"name": "@wrnexus/ui",
"version": "0.8.23",
"version": "0.8.56",
"private": true,
"type": "module",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./navigation": "./src/navigation.ts",
"./registry": "./src/registry.ts",
"./components/*": "./components/*",
"./component-catalog.json": "./component-catalog.json",
+19 -1
View File
@@ -24,4 +24,22 @@
* server-only subpath. Import from there instead if you need them.
*/
export {};
export type {
MegaMenuColumn,
MegaMenuDensity,
MegaMenuFeatured,
MegaMenuPanelAlign,
MegaMenuPanelWidth,
MegaMenuRailItem,
MegaMenuVariant,
NavbarAction,
NavbarBrand,
NavbarDropdownGroup,
NavbarDropdownItem,
NavbarItem,
NavbarItemType,
NavbarLinkItem,
NavbarMegaMenuData,
NavbarMegaMenuItem,
NavigationLink,
} from "./navigation";
+121
View File
@@ -0,0 +1,121 @@
/** Shared, serializable navigation contracts for Navbar and application configs. */
export type NavbarItemType = "link" | "dropdown" | "mega";
export type MegaMenuVariant = "default" | "icon-grid" | "catalog" | "dark";
export type MegaMenuDensity = "compact" | "default" | "comfortable";
export type MegaMenuPanelWidth = "sm" | "md" | "lg" | "xl" | "2xl" | "full";
export type MegaMenuPanelAlign = "start" | "center" | "end";
export interface NavigationLink {
label: string;
href: string;
value?: string;
labelKey?: string;
description?: string;
descriptionKey?: string;
icon?: string;
image?: string;
imageAlt?: string;
eyebrow?: string;
badge?: string;
target?: string;
rel?: string;
disabled?: boolean;
}
export interface NavbarLinkItem extends NavigationLink {
type?: "link";
}
export interface NavbarDropdownGroup {
label?: string;
labelKey?: string;
description?: string;
descriptionKey?: string;
items: NavigationLink[];
/** Compatibility alias accepted by Navbar. Prefer `items`. */
children?: NavigationLink[];
}
export interface NavbarDropdownItem {
type: "dropdown";
label: string;
value?: string;
labelKey?: string;
description?: string;
descriptionKey?: string;
icon?: string;
columns?: number;
items: Array<NavigationLink | NavbarDropdownGroup>;
/** Compatibility alias accepted by Navbar. Prefer `items`. */
children?: Array<NavigationLink | NavbarDropdownGroup>;
}
export interface MegaMenuColumn {
heading?: string;
eyebrow?: string;
description?: string;
image?: string;
imageAlt?: string;
items: NavigationLink[];
actionLabel?: string;
actionHref?: string;
}
export interface MegaMenuRailItem extends NavigationLink {}
export interface MegaMenuFeatured {
eyebrow?: string;
image?: string;
imageAlt?: string;
title?: string;
description?: string;
actionLabel?: string;
actionHref?: string;
}
export interface NavbarMegaMenuData {
variant?: MegaMenuVariant;
density?: MegaMenuDensity;
panelWidth?: MegaMenuPanelWidth;
panelAlign?: MegaMenuPanelAlign;
fullWidth?: boolean;
activeCategory?: string;
columnCount?: number;
columns: MegaMenuColumn[];
rail?: MegaMenuRailItem[];
featured?: MegaMenuFeatured;
actionLabel?: string;
actionHref?: string;
actionIcon?: string;
footer?: string;
}
export interface NavbarMegaMenuItem {
type: "mega";
label: string;
value?: string;
labelKey?: string;
icon?: string;
mega: NavbarMegaMenuData;
}
export type NavbarItem = NavbarLinkItem | NavbarDropdownItem | NavbarMegaMenuItem;
export interface NavbarAction extends NavigationLink {
variant?: "link" | "outline" | "primary";
}
export interface NavbarBrand {
label?: string;
labelKey?: string;
description?: string;
descriptionKey?: string;
href?: string;
ariaLabel?: string;
icon?: string;
logo?: string;
logoAlt?: string;
logoWidth?: number | string;
logoHeight?: number | string;
}
+51
View File
@@ -0,0 +1,51 @@
import { expect, test } from "bun:test";
import type { NavbarItem } from "../src/navigation";
const items = [
{ type: "link", label: "Pricing", href: "/pricing" },
{
type: "dropdown",
label: "Company",
items: [{ label: "About", href: "/about" }],
},
{
type: "mega",
label: "Products",
mega: {
variant: "icon-grid",
density: "compact",
panelWidth: "2xl",
panelAlign: "center",
columnCount: 3,
fullWidth: true,
rail: [{ label: "All products", href: "/products", icon: "icon-products" }],
columns: [
{
heading: "Work",
items: [
{
label: "Projects",
href: "/projects",
icon: "icon-projects",
description: "Plan and deliver work",
badge: "New",
},
],
},
],
featured: {
title: "What's new",
image: "/new.png",
actionLabel: "Explore",
actionHref: "/new",
},
footer: "One connected platform",
actionLabel: "View all products",
actionHref: "/products",
},
},
] satisfies NavbarItem[];
test("navbar item contract covers links, dropdowns, and complete mega menus", () => {
expect(items.map((item) => item.type)).toEqual(["link", "dropdown", "mega"]);
});
@@ -78,6 +78,28 @@ function openingTags(source: string): string[] {
}
describe("@wrnexus/ui redesign component pack", () => {
test("Hero full bleed does not use scrollbar-sensitive viewport units", async () => {
const source = await readFile(join(componentsDir, "Hero.wrn"), "utf8");
expect(source).not.toContain("width: 100vw");
expect(source).not.toContain("width: 100dvw");
expect(source).toContain('.wrn-hero[data-full-bleed="true"]');
});
test("Timeline supports numbered data items without styling every row as interactive", async () => {
const source = await readFile(join(componentsDir, "Timeline.wrn"), "utf8");
expect(source).toContain("numbered: boolean = false");
expect(source).toContain("data-interactive");
expect(source).toContain('data-interactive="true"');
expect(source).toContain('.wrn-timeline__item[data-interactive="true"] { cursor: pointer; }');
});
test("Blockquote provides constrained contained presentation", async () => {
const source = await readFile(join(componentsDir, "Blockquote.wrn"), "utf8");
expect(source).toContain('maxWidth: string = "xl"');
expect(source).toContain('data-variant="spotlight"');
expect(source).toContain('data-max-width="full"');
});
test("contains every replacement and new component", async () => {
const files = new Set(await readdir(componentsDir));
@@ -118,6 +140,77 @@ describe("@wrnexus/ui redesign component pack", () => {
}
});
test("Navbar only displays its utility bar when the topbar slot has rendered content", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
expect(source).toContain(".wrn-navbar__topbar:not(:has(> *))");
expect(source).toContain(".wrn-navbar__topbar:has(> *)");
expect(source).not.toContain(".wrn-navbar__topbar:not(:empty)");
});
test("Navbar renders its brand prop independently from the optional brand slot", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
expect(source).toContain('<slot name="brand" />');
expect(source).toContain("brand.logo || brand.icon || brand.label || brand.description");
expect(source.indexOf("brand.logo || brand.icon || brand.label || brand.description")).toBeLessThan(
source.indexOf('<slot name="brand" />'),
);
});
test("Navbar renders slotted utilities before its built-in actions", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
const actionsStart = source.indexOf('<div class="wrn-navbar__actions">');
const slotIndex = source.indexOf('<slot name="actions" />', actionsStart);
const actionsLoop = source.indexOf("{#each actions as item}", actionsStart);
expect(slotIndex).toBeGreaterThan(actionsStart);
expect(slotIndex).toBeLessThan(actionsLoop);
});
test("Navbar passes structured MegaMenu props as expressions", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
expect(source).toContain("columns={megaColumns(item)}");
expect(source).toContain("rail={megaData(item).rail || []}");
expect(source).toContain("featured={megaData(item).featured || {}}");
expect(source).toContain("fullWidth={megaData(item).fullWidth || false}");
expect(source).not.toContain('columns="{megaColumns(item)}"');
});
test("Navbar replaces mega panels with direct links in collapsed navigation", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
expect(source).toContain("wrn-navbar__mobile-mega-link");
expect(source).toContain("item.href || megaData(item).actionHref");
expect(source).toContain('.wrn-navbar__mega { display: none; }');
expect(source).toContain('.wrn-navbar__mobile-mega-link { display: flex; }');
});
test("Navbar supports in-flow floating and elevated menu panels", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
const ast = parse(source);
const defaults = new Map(ast.props.map((prop) => [prop.name, prop.default]));
expect(defaults.get("floating")).toBe("false");
expect(defaults.get("startFloating")).toBe('"0px"');
expect(source).toContain("wrn-navbar--floating");
expect(source).toContain("--wrn-navbar-floating-start");
expect(source).toContain("z-index: 1100");
});
test("CTASection exposes responsive section spacing", async () => {
const source = await readFile(join(componentsDir, "CTASection.wrn"), "utf8");
const ast = parse(source);
const defaults = new Map(ast.props.map((prop) => [prop.name, prop.default]));
expect(defaults.get("spacing")).toBe('"none"');
expect(source).toContain("data-spacing='{spacing}'");
expect(source).toContain('.wrn-cta-section[data-spacing="sm"]');
expect(source).toContain('.wrn-cta-section[data-spacing="md"]');
expect(source).toContain('.wrn-cta-section[data-spacing="lg"]');
});
test("existing public defaults remain release compatible", async () => {
const expectations: Record<string, string[]> = {
Container: [
+194 -10
View File
@@ -32,6 +32,19 @@ test("Textarea binds its value without rendering hydration markup as user conten
expect(source).not.toContain(">{value}</textarea>");
});
test("form field labels use a stable control-label size instead of inherited heading scale", () => {
const fieldComponents = uiComponentNames().filter((name) =>
readFileSync(uiComponentPath(name), "utf8").includes(".wrn-next__field-heading label"),
);
expect(fieldComponents.length).toBeGreaterThan(0);
for (const name of fieldComponents) {
const source = readFileSync(uiComponentPath(name), "utf8");
expect(source).toMatch(
/\.wrn-next__field-heading label\s*\{[^}]*font-size: 0\.8125rem;[^}]*line-height: 1\.35;/s,
);
}
});
test("global UI CSS stays below its migration ratchet", () => {
const css = uiCss();
// Component selectors belong to their owning .wrn files. Keep this asset to
@@ -239,7 +252,9 @@ test("navbar supports brands, nested menus, actions, utility slots, and public e
expect(defaults.get("openOnHover")).toBe("false");
expect(defaults.get("maxWidth")).toBe('"full"');
expect(source).toContain("item.children");
expect(source).toContain("child.children");
expect(source).toContain("dropdownGroupItems(child)");
expect(source).toContain('itemType(item) === "mega"');
expect(source).toContain("megaData(item).featured");
expect(source).toContain("wrn-navbar__dropdown--{item.type || 'dropdown'}");
const outputs = new Set(contract.outputs.map((output) => output.name));
for (const event of ["toggle", "open", "close", "select", "action"]) {
@@ -304,13 +319,19 @@ test("navbar renders a brand, nested dropdowns, mega groups, and actions", async
{
label: "Safety",
type: "mega",
columns: 2,
children: [
{
label: "Resources",
children: [{ label: "Guidance", href: "/guidance" }],
},
],
mega: {
variant: "icon-grid",
columnCount: 2,
rail: [{ label: "Overview", href: "/safety" }],
columns: [
{
heading: "Resources",
items: [{ label: "Guidance", href: "/guidance", badge: "New" }],
},
],
featured: { title: "Safety report", actionLabel: "Read report" },
actionLabel: "All safety resources",
},
},
],
actions: [{ label: "Sign in", href: "/login", variant: "primary" }],
@@ -320,7 +341,9 @@ test("navbar renders a brand, nested dropdowns, mega groups, and actions", async
expect(html).toContain("Public portal");
expect(html).toContain("Reports");
expect(html).toContain("Guidance");
expect(html).toContain("wrn-navbar__dropdown--mega");
expect(html).toContain("wrn-navbar__mega");
expect(html).toContain("Safety report");
expect(html).toContain("All safety resources");
expect(html).toContain("data-wrn-navbar");
expect(html).toContain('name="wrn-navbar-menu"');
expect(html).toContain("Sign in");
@@ -2856,8 +2879,13 @@ test("tabs use wrn classes and declare real outputs instead of raw events", asyn
expect(tabs).toHaveLength(2);
expect(tabs[0]!.getAttribute("aria-selected")).toBe("true");
expect(tabs[1]!.getAttribute("aria-selected")).toBe("false");
expect(tabs[0]!.id).toBe("wrn-tab-overview");
expect(tabs[0]!.getAttribute("aria-controls")).toBe("wrn-panel-overview");
expect(dom.querySelector(".wrn-tabs")).not.toBeNull();
expect(dom.querySelectorAll('[role="tabpanel"]')).toHaveLength(2);
const panels = dom.querySelectorAll('[role="tabpanel"]');
expect(panels).toHaveLength(2);
expect(panels[0]!.id).toBe("wrn-panel-overview");
expect(panels[0]!.getAttribute("aria-labelledby")).toBe("wrn-tab-overview");
});
test("tabs in url mode declare the query parameter they sync to", async () => {
@@ -2918,6 +2946,10 @@ test("mega menu renders column groups in an anchored panel", async () => {
const trigger = dom.querySelector(".wrn-mega__trigger") as HTMLElement;
expect(trigger.getAttribute("aria-expanded")).toBe("false");
expect(trigger.getAttribute("aria-haspopup")).toBe("true");
expect(trigger.getAttribute("aria-label")).toBe("Products");
expect(dom.querySelector(".wrn-mega__trigger-label")?.getAttribute("data-label")).toBe(
"Products",
);
const panel = dom.querySelector(".wrn-mega__panel") as HTMLElement;
expect(panel.getAttribute("data-wrn-anchored")).toBe("true");
@@ -2927,6 +2959,14 @@ test("mega menu renders column groups in an anchored panel", async () => {
// Single level by design: a mega panel shows breadth flat, so there is no
// nested submenu markup to find.
expect(source).not.toContain("wrn-mega__submenu");
expect(source).toContain('.wrn-mega[data-variant="default"] .wrn-mega__panel');
expect(source).toContain("box-sizing: border-box");
});
test("announcement full-width surface does not create viewport scrollbar overflow", () => {
const source = readFileSync(uiComponentPath("AnnouncementBar"), "utf8");
expect(source).not.toContain("width: 100vw");
expect(source).not.toContain("calc(50% - 50vw)");
});
test("mega menu columns take roving focus and survive an empty column list", async () => {
@@ -2938,6 +2978,76 @@ test("mega menu columns take roving focus and survive an empty column list", asy
expect(dom.querySelector('[data-wrn-roving="both"]')).not.toBeNull();
});
test("catalog mega menu renders only the active rail category", async () => {
const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
expect(source).toContain(".wrn-mega__list > li");
expect(source).toContain("overflow-wrap: anywhere;");
const html = await renderComponent(source, {
label: "Products",
variant: "catalog",
activeCategory: "sales",
rail: [
{ label: "Platform", value: "platform" },
{ label: "Sales", value: "sales" },
],
columns: [
{ heading: "Platform", items: [{ label: "Home", href: "/home" }] },
{ heading: "Sales", items: [{ label: "CRM", href: "/crm" }] },
],
});
const dom = mountHtml(html);
expect(dom.querySelectorAll(".wrn-mega__rail-link")).toHaveLength(2);
expect(dom.querySelectorAll(".wrn-mega__column")).toHaveLength(1);
expect(dom.querySelector(".wrn-mega__heading")?.textContent).toContain("Sales");
expect(dom.querySelector('[aria-pressed="true"]')?.textContent).toContain("Sales");
expect(source).toContain("overflow-y: auto");
expect(source).not.toContain("@mouseenter='chooseCategory(item, index)'");
expect(dom.querySelector(".wrn-mega__column-header")?.textContent).toContain("Sales");
expect(dom.querySelectorAll(".wrn-mega__link-icon")).toHaveLength(1);
expect(dom.querySelector(".wrn-mega__link-icon--fallback svg")).not.toBeNull();
expect(dom.querySelectorAll(".wrn-mega__link-description")).toHaveLength(0);
expect(source).toContain("border-bottom: 1px solid var(--wrn-color-border)");
expect(source).toContain(".wrn-mega__rail-icon > span");
expect(source).toContain("color: currentColor !important");
});
test("mega menu supports rails, icons, badges, media, featured content and footer actions", async () => {
const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
const html = await renderComponent(source, {
label: "Explore",
variant: "icon-grid",
panelWidth: "2xl",
columnCount: 3,
rail: [{ label: "Enterprise", description: "For larger teams", icon: "icon-enterprise" }],
columns: [
{
heading: "Products",
description: "Choose a workspace",
image: "/products.png",
items: [{ label: "CRM", description: "Customer records", icon: "icon-crm", badge: "New" }],
actionLabel: "All products",
},
],
featured: {
eyebrow: "What's new",
title: "Release notes",
image: "/release.png",
actionLabel: "See more",
},
footer: "One connected platform",
actionLabel: "View everything",
});
const dom = mountHtml(html);
expect(dom.querySelector(".wrn-mega")?.getAttribute("data-variant")).toBe("icon-grid");
expect(dom.querySelector(".wrn-mega__rail-link")?.textContent).toContain("Enterprise");
expect(dom.querySelector(".wrn-mega__badge")?.textContent).toContain("New");
expect(dom.querySelector(".wrn-mega__column-image")).not.toBeNull();
expect(dom.querySelector(".wrn-mega__featured")?.textContent).toContain("Release notes");
expect(dom.querySelector(".wrn-mega__footer")?.textContent).toContain("View everything");
});
test("sidebar renders single items, labelled groups and nested levels", async () => {
const source = readFileSync(uiComponentPath("Sidebar"), "utf8");
const html = await renderComponent(source, {
@@ -3027,6 +3137,8 @@ test("navbar uses a valid aria-current and takes roving focus on its menu", asyn
*/
expect(source).not.toContain("? 'page' : ''");
expect(source).toContain("data-wrn-roving");
expect(source).toContain("pointer-events: none;");
expect(source).toContain(".wrn-navbar__menus > *");
const html = await renderComponent(source, {
label: "Main",
@@ -3066,6 +3178,38 @@ test("breadcrumb marks the trail end without emitting an empty aria-current", as
expect(dom.querySelectorAll(".wrn-breadcrumb__item").length).toBeGreaterThanOrEqual(3);
});
test("breadcrumb renders a standalone active page with its icon", async () => {
const source = readFileSync(uiComponentPath("Breadcrumb"), "utf8");
const html = await renderComponent(source, {
showHome: true,
items: [{ label: "Products", href: "/products", icon: "icon-[lucide--blocks]" }],
active: "CRM",
activeIcon: "icon-[lucide--contact-round]",
});
expect(html).toContain("CRM");
expect(html).toContain("icon-[lucide--contact-round]");
expect(html).toMatch(/aria-current="page"/);
const dom = mountHtml(html);
expect(dom.querySelectorAll(".wrn-breadcrumb__item").length).toBe(3);
});
test("breadcrumb default separators remain visible without generated icon CSS", async () => {
const source = readFileSync(uiComponentPath("Breadcrumb"), "utf8");
const html = await renderComponent(source, {
showHome: true,
items: [{ label: "Legal" }, { label: "Terms", current: true }],
});
expect(html).toContain("");
expect(html).not.toContain("icon-[lucide--chevron-right]");
});
test("stats bar desktop gutters align its surface with container content", () => {
const source = readFileSync(uiComponentPath("StatsBar"), "utf8");
expect(source).toContain("--stats-max-width: 80rem");
expect(source).toContain("--stats-gutter-width: 4rem");
expect(source).toContain("calc(var(--stats-max-width) - var(--stats-gutter-width))");
});
test("mega menu bridges the gap between its trigger and panel", async () => {
const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
/*
@@ -3078,6 +3222,39 @@ test("mega menu bridges the gap between its trigger and panel", async () => {
expect(source).toMatch(/\.wrn-mega__panel::before \{[^}]*bottom: 100%/s);
});
test("mega menu keeps hover activation and click activation from cancelling each other", () => {
const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
expect(source).toContain("openOnHover: boolean = true");
expect(source).toContain("@click='activatePanel(event)'");
expect(source).toContain("@mouseenter='hoverPanel(event)'");
expect(source).toContain("showPanel(sourceEvent)");
expect(source).not.toContain("function scheduleOpen");
expect(source).toContain('.wrn-mega__panel[data-show="false"] { display: none; }');
expect(source).toContain('style=\'{visible ? "" : "display:none"}\'');
expect(source).not.toContain("@focus='showPanel(event)'");
expect(source).toContain("max-height: calc(100dvh - 5.5rem)");
});
test("mega menu gives the pointer time to cross from its trigger into a floating panel", () => {
const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
expect(source).toContain("state closeTimer = null");
expect(source).toContain("@mouseleave='scheduleHide()'");
expect(source).toContain("@mouseenter='cancelScheduledHide()'");
expect(source).toContain("}, 220)");
expect(source).not.toContain("@mouseleave='hidePanel(\"pointer-leave\")'");
});
test("navbar pins full-width mega panels to desktop viewport gutters", () => {
const source = readFileSync(uiComponentPath("Navbar"), "utf8");
expect(source).toMatch(
/\.wrn-navbar__mega\[data-full-width="true"\] \.wrn-mega__panel \{[^}]*position: fixed/s,
);
expect(source).toMatch(/right: 1rem;\s+left: 1rem;/s);
expect(source).toMatch(
/data-collapse-at="1100px"[^}]+\.wrn-navbar__mega\[data-full-width="true"\][^}]+position: static/s,
);
});
test("stepper shows only the active step content and offers back, next and skip", async () => {
const source = readFileSync(uiComponentPath("Stepper"), "utf8");
const html = await renderComponent(source, {
@@ -3213,6 +3390,13 @@ test("custom scrollbar styles the scrollbar and drops its fake output", async ()
expect(style).toContain("18rem");
});
test("page header collapses compiled empty slot markers", () => {
const source = readFileSync(uiComponentPath("PageHeader"), "utf8");
expect(source).toContain(".wrn-page-header__meta:not(:has(> :not(template)))");
expect(source).toContain(".wrn-page-header__body:not(:has(> :not(template)))");
expect(source).toContain(".wrn-page-header__actions--slot-only:not(:has(> :not(template)))");
});
test("layout components ship their own styles instead of Tailwind utilities", () => {
/*
* These were built from utility classes and class: conditionals. That works