mnfst 0.5.145 → 0.5.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -799,6 +799,158 @@ window.ManifestComponentsMutation = {
799
799
  }
800
800
  };
801
801
 
802
+ // Components — route-level prefetch.
803
+ //
804
+ // Two enhancements that run on top of the existing on-encounter loader:
805
+ //
806
+ // 1. Parallel batch on route change. When manifest:route-change fires,
807
+ // scan the [x-route] subtrees that match the new route and call
808
+ // loadComponent() on every <x-*> tag inside them. The loader
809
+ // deduplicates fetches, so calling it for components that the
810
+ // regular swapping logic is already mounting is harmless — but
811
+ // pre-issuing in parallel saves 50–200 ms vs. one-by-one fetches.
812
+ //
813
+ // 2. Prefetch on hover. When the pointer enters an internal <a href>,
814
+ // derive the target pathname, find the [x-route] subtree(s) that
815
+ // would match it, and prefetch their components. By the time the
816
+ // user clicks the link, the components are warm in the loader's
817
+ // cache and navigation feels instant.
818
+ //
819
+ // Both phases require zero author configuration. Manifest auto-discovers
820
+ // what to prefetch from the existing [x-route] DOM structure.
821
+
822
+ (function () {
823
+ 'use strict';
824
+
825
+ // <x-*> tag pattern — lowercase, hyphenated.
826
+ const TAG_RE = /^x-[a-z][a-z0-9-]*$/;
827
+
828
+ // Framework-provided web components (registered by Manifest plugins
829
+ // themselves, not as project components in manifest.json). Skip these
830
+ // when scanning for project components to prefetch.
831
+ const FRAMEWORK_TAGS = new Set(['code', 'code-group']);
832
+
833
+ // Anchors we've already issued a hover-prefetch for. WeakSet so DOM
834
+ // garbage-collects naturally as elements leave the tree.
835
+ const prefetchedAnchors = new WeakSet();
836
+
837
+ function loader() { return window.ManifestComponentsLoader; }
838
+
839
+ // Match a single route pattern against a normalized pathname (no
840
+ // leading/trailing slashes, '/' represented as '/'). Mirrors the
841
+ // router visibility logic so prefetch targets the same subtrees.
842
+ function routeMatches(routeValue, pathname) {
843
+ const pieces = String(routeValue || '').split(',').map((s) => s.trim()).filter(Boolean);
844
+ let matched = false;
845
+ let negated = false;
846
+ for (const piece of pieces) {
847
+ if (piece === '!*') continue; // catch-all only handled by visibility plugin
848
+ if (piece.startsWith('!')) {
849
+ if (piece.slice(1) === pathname) negated = true;
850
+ continue;
851
+ }
852
+ if (piece.startsWith('=')) {
853
+ if (piece.slice(1) === pathname) matched = true;
854
+ continue;
855
+ }
856
+ if (piece.endsWith('/*')) {
857
+ const prefix = piece.slice(0, -2);
858
+ if (pathname === prefix || pathname.startsWith(prefix + '/')) matched = true;
859
+ continue;
860
+ }
861
+ if (piece === pathname) { matched = true; continue; }
862
+ if (pathname.startsWith(piece + '/')) matched = true;
863
+ }
864
+ return matched && !negated;
865
+ }
866
+
867
+ function findRouteSubtrees(pathname) {
868
+ const normalized = (pathname || '/') === '/' ? '/' : pathname.replace(/^\/|\/$/g, '');
869
+ const out = [];
870
+ document.querySelectorAll('[x-route]').forEach((el) => {
871
+ const value = el.getAttribute('x-route') || '';
872
+ if (routeMatches(value, normalized)) out.push(el);
873
+ });
874
+ return out;
875
+ }
876
+
877
+ function discoverComponentNames(root) {
878
+ const names = new Set();
879
+ if (!root || !root.querySelectorAll) return names;
880
+ // querySelectorAll('*') is the fastest path for "every descendant".
881
+ // We filter by tag name in JS — there's no CSS selector for "tag
882
+ // name starts with x-". A page typically has a few thousand nodes,
883
+ // which scans in well under a millisecond.
884
+ root.querySelectorAll('*').forEach((el) => {
885
+ const tag = el.tagName.toLowerCase();
886
+ if (!tag.startsWith('x-') || !TAG_RE.test(tag)) return;
887
+ const name = tag.slice(2);
888
+ if (!FRAMEWORK_TAGS.has(name)) names.add(name);
889
+ });
890
+ return names;
891
+ }
892
+
893
+ function prefetchForRoute(pathname) {
894
+ const L = loader();
895
+ if (!L || typeof L.loadComponent !== 'function') return;
896
+ const subtrees = findRouteSubtrees(pathname);
897
+ if (!subtrees.length) return;
898
+ const names = new Set();
899
+ for (const subtree of subtrees) {
900
+ discoverComponentNames(subtree).forEach((n) => names.add(n));
901
+ }
902
+ names.forEach((name) => {
903
+ try { L.loadComponent(name); } catch { /* swallow — dedup is internal */ }
904
+ });
905
+ }
906
+
907
+ function hrefToPathname(href) {
908
+ if (!href) return null;
909
+ if (/^(#|mailto:|tel:|javascript:)/i.test(href)) return null;
910
+ try {
911
+ const url = new URL(href, window.location.href);
912
+ if (url.origin !== window.location.origin) return null;
913
+ return url.pathname || '/';
914
+ } catch {
915
+ return null;
916
+ }
917
+ }
918
+
919
+ function initialize() {
920
+ // 1) Parallel batch on route change.
921
+ window.addEventListener('manifest:route-change', (event) => {
922
+ const detail = (event && event.detail) || {};
923
+ const path = detail.normalizedPath || detail.to || '/';
924
+ const pathname = String(path).startsWith('/') ? String(path) : '/' + String(path);
925
+ prefetchForRoute(pathname);
926
+ });
927
+
928
+ // 2) Hover prefetch. Use pointerover (bubbles) and check the closest
929
+ // anchor on each event so we get a single trigger per anchor entry
930
+ // without needing pointerenter (which doesn't bubble). Dedup via
931
+ // a WeakSet so repeat moves within the anchor don't re-scan.
932
+ document.addEventListener('pointerover', (e) => {
933
+ if (!e.target || !e.target.closest) return;
934
+ const a = e.target.closest('a[href]');
935
+ if (!a || prefetchedAnchors.has(a)) return;
936
+ // Author opt-out: `data-no-prefetch` skips this anchor.
937
+ if (a.hasAttribute('data-no-prefetch')) return;
938
+ const href = a.getAttribute('href');
939
+ const pathname = hrefToPathname(href);
940
+ if (!pathname) return;
941
+ prefetchedAnchors.add(a);
942
+ prefetchForRoute(pathname);
943
+ });
944
+ }
945
+
946
+ if (document.readyState === 'loading') {
947
+ document.addEventListener('DOMContentLoaded', initialize);
948
+ } else {
949
+ initialize();
950
+ }
951
+ })();
952
+
953
+
802
954
  // Main initialization for Manifest Components
803
955
  async function initializeComponents() {
804
956
  // Registry.initialize() may fetch manifest.json (async) when the loader
package/lib/manifest.css CHANGED
@@ -1456,6 +1456,215 @@
1456
1456
  }
1457
1457
  }
1458
1458
 
1459
+ /* Manifest Combobox */
1460
+
1461
+ @layer components {
1462
+
1463
+ /* Field shell — inherits input tokens so it reads as a normal field */
1464
+ :where(.combobox):not(.unstyle) {
1465
+ display: flex;
1466
+ flex-wrap: wrap;
1467
+ align-items: center;
1468
+ gap: var(--spacing, 0.25rem);
1469
+ width: 100%;
1470
+ max-width: 100%;
1471
+ min-height: var(--spacing-field-height, 2.25rem);
1472
+ padding: calc(var(--spacing, 0.25rem) * 1.25) var(--spacing-field-padding, 0.625rem);
1473
+ color: var(--color-field-inverse, oklch(43.9% 0 0));
1474
+ background-color: var(--color-field-surface, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent));
1475
+ border-radius: var(--radius, 0.5rem);
1476
+ transition: var(--transition);
1477
+ cursor: text;
1478
+
1479
+ &:hover:has(> :where(input:not([type=hidden]), textarea, button):not([hidden])) {
1480
+ background-color: var(--color-field-surface-hover, color-mix(in oklch, oklch(20.5% 0 0) 15%, transparent))
1481
+ }
1482
+
1483
+ &:not(:has(> :where(input:not([type=hidden]), textarea, button):not([hidden]))) {
1484
+ cursor: default
1485
+ }
1486
+
1487
+ &:focus-within {
1488
+ background-color: var(--color-field-surface, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent));
1489
+ box-shadow: 0 0 0 2px color-mix(in oklch, var(--color-content-stark, oklch(43.9% 0 0)) 30%, transparent)
1490
+ }
1491
+
1492
+ &:has(.combobox-chip) {
1493
+ padding-inline: calc(var(--spacing, 0.25rem) * 1.25)
1494
+ }
1495
+
1496
+ &:has(> :where(input, textarea):disabled) {
1497
+ opacity: .5;
1498
+ pointer-events: none
1499
+ }
1500
+
1501
+ /* Inner typing surface */
1502
+ &> :where(input:not([type=hidden]), textarea) {
1503
+ flex: 1 0 auto;
1504
+ width: auto;
1505
+ min-width: 7rem;
1506
+ height: auto;
1507
+ margin: 0;
1508
+ padding: 0;
1509
+ color: inherit;
1510
+ background: transparent;
1511
+ field-sizing: content;
1512
+
1513
+ &:hover,
1514
+ &:focus-visible {
1515
+ background: transparent;
1516
+ box-shadow: none
1517
+ }
1518
+ }
1519
+
1520
+ &>textarea {
1521
+ resize: none
1522
+ }
1523
+ }
1524
+
1525
+ /* Chips */
1526
+ :where(.combobox-chip):not(.unstyle) {
1527
+ display: inline-flex;
1528
+ align-items: center;
1529
+ gap: var(--spacing, 0.25rem);
1530
+ max-width: 100%;
1531
+ height: calc(var(--spacing-field-height, 2.25rem) - var(--spacing, 0.25rem) * 2.5);
1532
+ padding-inline-start: calc(var(--spacing, 0.25rem) * 2);
1533
+ font-size: 0.875rem;
1534
+ color: var(--color-content-stark, oklch(43.9% 0 0));
1535
+ background-color: var(--color-popover-surface, oklch(100% 0 0));
1536
+ border-radius: calc(var(--radius, 0.5rem) * 0.7);
1537
+ overflow: hidden;
1538
+ user-select: none;
1539
+
1540
+ /* Label */
1541
+ &>span {
1542
+ overflow: hidden;
1543
+ text-overflow: ellipsis;
1544
+ white-space: nowrap
1545
+ }
1546
+
1547
+ /* Remove button */
1548
+ &>button {
1549
+ display: inline-flex;
1550
+ align-items: center;
1551
+ justify-content: center;
1552
+ min-width: 0;
1553
+ height: 100%;
1554
+ aspect-ratio: 1 / 1;
1555
+ padding: 0;
1556
+ font-size: 1em;
1557
+ line-height: 1;
1558
+ color: var(--color-content-neutral, oklch(43.9% 0 0));
1559
+ background: transparent;
1560
+ border-radius: 0;
1561
+ cursor: pointer;
1562
+
1563
+ &:hover {
1564
+ color: var(--color-content-stark, oklch(43.9% 0 0));
1565
+ background: color-mix(in oklch, var(--color-content-stark, oklch(43.9% 0 0)) 12%, transparent)
1566
+ }
1567
+ }
1568
+
1569
+ /* Failed validation */
1570
+ &[aria-invalid="true"] {
1571
+ color: var(--color-negative-inverse, oklch(44.4% 0.177 26.899));
1572
+ background-color: var(--color-negative-surface, oklch(80.8% 0.114 19.571))
1573
+ }
1574
+ }
1575
+
1576
+ /* Trigger (editor:none button mode) — transparent; the shell is the field.
1577
+ Fills the row like the input editor, with a select-style caret. */
1578
+ :where(.combobox) > button:not(.unstyle) {
1579
+ flex: 1 1 auto;
1580
+ align-self: stretch;
1581
+ min-width: 7rem;
1582
+ justify-content: flex-start;
1583
+ gap: var(--spacing, 0.25rem);
1584
+ height: auto;
1585
+ min-height: calc(var(--spacing-field-height, 2.25rem) - var(--spacing, 0.25rem) * 2.5);
1586
+ padding: 0;
1587
+ color: inherit;
1588
+ background: transparent;
1589
+ cursor: pointer;
1590
+
1591
+ &:hover {
1592
+ background: transparent
1593
+ }
1594
+
1595
+ /* Caret */
1596
+ &::after {
1597
+ content: '';
1598
+ flex: none;
1599
+ width: 1em;
1600
+ height: 1em;
1601
+ margin-inline-start: auto;
1602
+ background-color: var(--color-content-subtle, oklch(55.6% 0 0));
1603
+ mask: var(--icon-chevron-down, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m6 9 6 6 6-6'/%3E%3C/svg%3E")) center / contain no-repeat
1604
+ }
1605
+ }
1606
+
1607
+ /* Button-only field (no chips): the button IS the field — it fills the whole
1608
+ wrapper and carries the padding, so the entire area is the click target. */
1609
+ :where(.combobox):has(> button:not(.unstyle)):not(:has(.combobox-chip)) {
1610
+ padding: 0;
1611
+
1612
+ &>button:not(.unstyle) {
1613
+ min-height: var(--spacing-field-height, 2.25rem);
1614
+ padding: calc(var(--spacing, 0.25rem) * 1.25) var(--spacing-field-padding, 0.625rem)
1615
+ }
1616
+ }
1617
+
1618
+ /* Listbox — base look comes from menu[popover]; these are combobox-only extras */
1619
+ :where(menu[role=listbox]):not(.unstyle) {
1620
+
1621
+ /* Active descendant (mirrors hover) */
1622
+ & :where([role=option][aria-current="true"]) {
1623
+ color: var(--color-field-inverse, oklch(43.9% 0 0));
1624
+ background-color: var(--color-field-surface, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent))
1625
+ }
1626
+
1627
+ /* Filtered out */
1628
+ & :where([role=option][hidden]) {
1629
+ display: none
1630
+ }
1631
+
1632
+ /* Selected (multiple) */
1633
+ & :where([role=option][aria-selected="true"]) {
1634
+ font-weight: 600
1635
+ }
1636
+
1637
+ /* "Add …" create row */
1638
+ & :where(.combobox-create) {
1639
+ color: var(--color-content-neutral, oklch(43.9% 0 0))
1640
+ }
1641
+
1642
+ /* Empty state */
1643
+ & :where(.combobox-empty) {
1644
+ padding: 0.5rem;
1645
+ color: var(--color-content-subtle, oklch(55.6% 0 0));
1646
+ cursor: default;
1647
+
1648
+ &:hover {
1649
+ background: transparent
1650
+ }
1651
+ }
1652
+ }
1653
+
1654
+ /* Screen-reader-only live region */
1655
+ :where(.combobox)> :where([role=status]) {
1656
+ position: absolute;
1657
+ width: 1px;
1658
+ height: 1px;
1659
+ margin: -1px;
1660
+ padding: 0;
1661
+ overflow: hidden;
1662
+ clip: rect(0 0 0 0);
1663
+ white-space: nowrap;
1664
+ border: 0
1665
+ }
1666
+ }
1667
+
1459
1668
  /* Manifest Date Picker */
1460
1669
 
1461
1670
  @layer utilities {
@@ -6,7 +6,8 @@
6
6
  "manifest.code.js": "sha384-nP6DncLx/UuJtloyVKMCOXwIBAq32DshTb/Lc0vVRBWX7kSbxiBnY5aEyqqvK8Kg",
7
7
  "manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
8
8
  "manifest.colorpicker.js": "sha384-Wqz0ZIbeIi7KarqqqSLsQk+7E/fMaKhb32hrq5/eWzX1yjqMrpPZKH8y+jZ3mfg+",
9
- "manifest.components.js": "sha384-73CB1A+LAGfNexkd7aT69APFSHMzix8irse9uzOkYehHHio4px3oR8JHJeaMH+jI",
9
+ "manifest.combobox.js": "sha384-AEEtOVCcoGgCyKPbZSzKtRhz8G4VbE8BD5LC/urGqzxjMXNP9NrExeWrENXHCLsg",
10
+ "manifest.components.js": "sha384-mzPFoM0vqL9dnTVLMN3OrmO+KCgSqGknM1fd7bM1xzYeCco5OaZi56IMR5RS5oad",
10
11
  "manifest.data.js": "sha384-bCYTYyAYNVkg5pSwGcoe07Dgf5B7JDN7GtOIQdS+BtrBStQwvjZtskiQ38Bncvrf",
11
12
  "manifest.datepicker.js": "sha384-NEb/H4vuR3CFtRcodHsm3jJjrcYW2JMpDlQKlgwTrzpMMTcDkFKYXzAYJD0gZ7Ov",
12
13
  "manifest.dropdowns.js": "sha384-a2j9Z1LWolyZANlfeBc1aIFQ0kf0UDeOZ8TetulYbpRS6T/zaD/OWSMGvz+gRehX",
@@ -26,5 +27,6 @@
26
27
  "manifest.tooltips.js": "sha384-ADzAx9D0HWq2b46mvNG05iOwPmEWdiFZNpEOXONSbBxs4xj1B/bzNL7S3x2R9cS1",
27
28
  "manifest.url.parameters.js": "sha384-FIufiClqDx1rJpU/QUc9z/D43qClQ6Qm8rBahipbJl9BDHUvhrOsUDegmTWW7Tuf",
28
29
  "manifest.utilities.js": "sha384-HWyVkjQoDRlWFKDBQw4RQOYODkBcU72NHW6l1p4bhQv1RtN0/XtnjwIb+lQK6+zv",
29
- "manifest.js": "sha384-SrHYQWIDaJ9A8m6FFwqruve5PTSnVbRaDiBB6+Yx/4ab2Ax7bGgnt5KM7HaJEN8X"
30
+ "manifest.virtual.js": "sha384-J2+MpRskVH39R2DmHUib2bItuGsvSLEhPM+83iznrdfQFMZ63Ea6xwLeCsG04jOl",
31
+ "manifest.js": "sha384-IsNGBIHGuUF5ABO58nkkZJQq1Ilt0wIsz+X2qR5hx4cGEhZez7GCmB8rAowJmrHS"
30
32
  }
package/lib/manifest.js CHANGED
@@ -235,6 +235,7 @@
235
235
  'toasts',
236
236
  'tooltips',
237
237
  'dropdowns',
238
+ 'combobox',
238
239
  'tabs',
239
240
  'slides',
240
241
  'resize',
@@ -242,6 +243,7 @@
242
243
  'datepicker',
243
244
  'charts',
244
245
  'url-parameters',
246
+ 'virtual',
245
247
  'export',
246
248
  'status'
247
249
  ];