mnfst 0.5.171 → 0.5.172

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.
@@ -649,25 +649,31 @@
649
649
  const thickness = Math.max(8, r * 0.22);
650
650
  const cx = width / 2, cy = Math.max(PAD, (height - (r + labelBand)) / 2) + r;
651
651
  const g = svg('g', { transform: `translate(${cx},${cy})` }, root);
652
- const arc = d3.arc().innerRadius(r - thickness).outerRadius(r).cornerRadius(cssRadius(state.el));
652
+ // Bands are drawn with square (butt) ends so zone/value joins stay flush;
653
+ // the whole dial is then clipped to a single rounded full-sweep arc, so
654
+ // only its two outer corners are rounded — not each interior segment.
655
+ const arc = d3.arc().innerRadius(r - thickness).outerRadius(r);
656
+ const clipId = 'mnfst-gauge-' + (++_uid);
657
+ svg('path', { d: d3.arc().innerRadius(r - thickness).outerRadius(r).cornerRadius(cssRadius(state.el))({ startAngle: START, endAngle: END }) }, svg('clipPath', { id: clipId }, svg('defs', null, root)));
658
+ const ring = svg('g', { 'clip-path': `url(#${clipId})` }, g);
653
659
 
654
660
  // Track, or threshold zone bands when `zones` is given.
655
661
  if (Array.isArray(cfg.zones) && cfg.zones.length) {
656
662
  let from = min;
657
663
  cfg.zones.forEach((z, i) => {
658
664
  const to = num(z.to, max);
659
- svg('path', { class: 'gauge-track', d: arc({ startAngle: scale(from), endAngle: scale(to) }), style: `--color-chart-color:${z.color || seriesColorVar(i)}`, opacity: 0.35 }, g);
665
+ svg('path', { class: 'gauge-track', d: arc({ startAngle: scale(from), endAngle: scale(to) }), style: `--color-chart-color:${z.color || seriesColorVar(i)}`, opacity: 0.35 }, ring);
660
666
  from = to;
661
667
  });
662
668
  } else {
663
- svg('path', { class: 'gauge-track', d: arc({ startAngle: START, endAngle: END }) }, g);
669
+ svg('path', { class: 'gauge-track', d: arc({ startAngle: START, endAngle: END }) }, ring);
664
670
  }
665
671
 
666
672
  // Value arc — final geometry set unconditionally, so a background tab that
667
673
  // skips the fade reveal still renders correctly.
668
674
  const color = (cfg.series[0] && cfg.series[0].color) || 'var(--color-chart-1)';
669
675
  const valueAngle = scale(value);
670
- const vArc = svg('path', { class: 'gauge-value', d: arc({ startAngle: START, endAngle: valueAngle }), style: `--color-chart-color:${color}` }, g);
676
+ const vArc = svg('path', { class: 'gauge-value', d: arc({ startAngle: START, endAngle: valueAngle }), style: `--color-chart-color:${color}` }, ring);
671
677
  applyTip(vArc, (cfg.title ? cfg.title + ': ' : '') + value + unit, cfg);
672
678
  animate(vArc, [{ opacity: 0 }, { opacity: 1 }], { duration: 500 });
673
679
 
@@ -67,22 +67,34 @@ function initializeComboboxPlugin() {
67
67
  const rand = () => Math.random().toString(36).slice(2, 9);
68
68
  const ui = () => window.ManifestUI ? window.ManifestUI.resolve('combobox', UI_FALLBACK) : UI_FALLBACK;
69
69
 
70
+ // Alpine binding attributes — never carried onto combobox-owned clones
71
+ const isBinding = (n) => n[0] === ':' || n[0] === '@' || n.indexOf('x-') === 0;
72
+
70
73
  // Read options from a source element (datalist/select → option, menu → li)
71
74
  function readOptions(src) {
72
75
  if (!src) return [];
73
76
  if (src.tagName === 'MENU') {
74
- return Array.from(src.querySelectorAll('li')).map(li => ({
75
- value: li.dataset.value != null ? li.dataset.value : li.textContent.trim(),
76
- label: li.dataset.label || li.textContent.trim(),
77
- pattern: li.dataset.pattern || null,
78
- locked: li.hasAttribute('data-locked'),
79
- html: li.innerHTML,
80
- // Carry the row's own attributes into the option, minus combobox-managed
81
- // ones and Alpine bindings (x-/:/@ reference the source's scope, not the clone).
82
- attrs: Array.from(li.attributes)
83
- .filter(a => { const n = a.name; return n !== 'role' && n !== 'id' && n !== 'aria-selected' && n[0] !== ':' && n[0] !== '@' && n.indexOf('x-') !== 0; })
84
- .map(a => [a.name, a.value])
85
- }));
77
+ return Array.from(src.querySelectorAll('li')).map(li => {
78
+ // The clone is a rendered snapshot: strip Alpine bindings (x-/:/@) from the
79
+ // WHOLE subtree, not just the row — they reference the source's x-for scope
80
+ // and would throw when Alpine walks the combobox-owned copy. The computed
81
+ // results (style, class, text) are already baked in and survive.
82
+ const copy = li.cloneNode(true);
83
+ copy.querySelectorAll('*').forEach(n => {
84
+ Array.from(n.attributes).forEach(a => { if (isBinding(a.name)) n.removeAttribute(a.name); });
85
+ });
86
+ return {
87
+ value: li.dataset.value != null ? li.dataset.value : li.textContent.trim(),
88
+ label: li.dataset.label || li.textContent.trim(),
89
+ pattern: li.dataset.pattern || null,
90
+ locked: li.hasAttribute('data-locked'),
91
+ html: copy.innerHTML,
92
+ // Carry the row's own attributes into the option, minus combobox-managed ones.
93
+ attrs: Array.from(li.attributes)
94
+ .filter(a => { const n = a.name; return n !== 'role' && n !== 'id' && n !== 'aria-selected' && !isBinding(n); })
95
+ .map(a => [a.name, a.value])
96
+ };
97
+ });
86
98
  }
87
99
  return Array.from(src.querySelectorAll('option')).map(o => ({
88
100
  value: o.value || o.textContent.trim(),
@@ -308,7 +320,10 @@ function initializeComboboxPlugin() {
308
320
  // stays a data layer x-for/$x can own (reread below mirrors it), the combobox
309
321
  // owns the listbox. readOptions keeps each row's innerHTML, preserving markup.
310
322
  menu = document.createElement('menu');
311
- document.body.appendChild(menu); // anchor positioning needs it out of overflow contexts
323
+ menu.setAttribute('x-ignore', ''); // plugin-owned rows Alpine must never walk them
324
+ // Nearest popover host, so an outer auto popover isn't light-dismissed
325
+ // by listbox clicks; body otherwise (out of overflow contexts either way).
326
+ (el.closest('[popover]') || document.body).appendChild(menu);
312
327
  generatedMenu = menu; // tracked so cleanup() can remove it on re-render
313
328
  if (src) src.style.setProperty('display', 'none', 'important');
314
329
  menu.setAttribute('popover', 'manual');
package/lib/manifest.css CHANGED
@@ -28,7 +28,13 @@
28
28
  overflow-wrap: break-word;
29
29
  -moz-tab-size: 4;
30
30
  -o-tab-size: 4;
31
- tab-size: 4
31
+ tab-size: 4;
32
+
33
+ /* Device safe-area insets (0 unless viewport-fit=cover on a notched screen) */
34
+ --safe-top: env(safe-area-inset-top, 0px);
35
+ --safe-right: env(safe-area-inset-right, 0px);
36
+ --safe-bottom: env(safe-area-inset-bottom, 0px);
37
+ --safe-left: env(safe-area-inset-left, 0px)
32
38
  }
33
39
 
34
40
  :where(html),
@@ -2640,6 +2646,63 @@
2640
2646
  }
2641
2647
  }
2642
2648
 
2649
+ /* Manifest Dock */
2650
+
2651
+ @layer components {
2652
+
2653
+ /* Fixed bottom navigation bar. Best practice: <nav dock aria-label="Primary">.
2654
+ Physics (fixed + safe-area + item layout) baked in; skin is themeable via
2655
+ the --dock-* variables. Edge-to-edge by default; set --dock-inset (+ radius)
2656
+ to float it. */
2657
+ :where([dock]) {
2658
+ position: fixed;
2659
+ z-index: 40;
2660
+ inset-inline: var(--dock-inset, 0px);
2661
+ bottom: var(--dock-inset, 0px);
2662
+ display: flex;
2663
+ align-items: stretch;
2664
+ justify-content: space-around;
2665
+ gap: var(--dock-gap, 0px);
2666
+ /* Clear the home indicator in both modes: total bottom clearance == --safe-bottom */
2667
+ padding-bottom: max(calc(var(--safe-bottom, 0px) - var(--dock-inset, 0px)), 0px);
2668
+ background: var(--dock-surface, var(--color-surface-1, oklch(96.7% 0 0)));
2669
+ border-top: var(--dock-border-top, 1px solid var(--color-line, color-mix(in oklch, currentColor 11%, transparent)));
2670
+ border-radius: var(--dock-radius, 0px);
2671
+ box-shadow: var(--dock-shadow, none)
2672
+ }
2673
+
2674
+ /* Dock items */
2675
+ :where([dock] > a, [dock] > button) {
2676
+ flex: 1 1 0;
2677
+ display: flex;
2678
+ flex-direction: column;
2679
+ align-items: center;
2680
+ justify-content: center;
2681
+ gap: 0.125rem;
2682
+ min-height: 3.25rem;
2683
+ padding: 0.375rem 0.25rem;
2684
+ background: transparent;
2685
+ color: var(--color-content-subtle, oklch(55.6% 0 0));
2686
+ font-size: 0.6875rem;
2687
+ line-height: 1.1;
2688
+ text-decoration: none
2689
+ }
2690
+
2691
+ /* Dock icons */
2692
+ :where([dock] :is([x-icon], svg, img)) {
2693
+ width: 1.375rem;
2694
+ height: 1.375rem;
2695
+ font-size: 1.375rem
2696
+ }
2697
+
2698
+ /* Active item — a hook, not a look (bind :aria-current="$route === '/x' ? 'page' : null') */
2699
+ [dock] > a[aria-current="page"],
2700
+ [dock] > button[aria-current="page"],
2701
+ [dock] > .selected {
2702
+ color: var(--color-content-stark, oklch(14.5% 0 0))
2703
+ }
2704
+ }
2705
+
2643
2706
  /* Manifest Dropdowns */
2644
2707
  [popover]:not(.unstyle):not(:popover-open) {
2645
2708
  display: none;
@@ -2669,8 +2732,8 @@
2669
2732
  transform-origin: top center;
2670
2733
  overflow-x: hidden;
2671
2734
 
2672
- /* Options */
2673
- & :where(li, a, button, [role=button], label):not(.unstyle) {
2735
+ /* Options (labels only when flat items — field wrappers keep form layout) */
2736
+ & :where(li, a, button, [role=button], label:not(:has(input:not([type=checkbox], [type=radio], [type=hidden]), select, textarea, .combobox, .label, label))):not(.unstyle) {
2674
2737
  display: inline-flex;
2675
2738
  justify-content: start;
2676
2739
  align-items: center;
@@ -2730,13 +2793,14 @@
2730
2793
  background-color: var(--color-line, color-mix(oklch(20.5% 0 0) 10%, transparent))
2731
2794
  }
2732
2795
 
2733
- /* Labels */
2734
- & label {
2796
+ /* Labels (not inner field-surface wrappers) */
2797
+ & label:not(.unstyle):not(:has(> input[type=search], > input[type=file], > button[command="--copy"])) {
2735
2798
  padding-inline-start: 0.5rem;
2736
2799
  padding-inline-end: 0.5rem;
2737
2800
  cursor: default;
2738
2801
 
2739
- &:hover {
2802
+ &:hover,
2803
+ &:active {
2740
2804
  background-color: transparent
2741
2805
  }
2742
2806
 
@@ -2745,8 +2809,9 @@
2745
2809
  }
2746
2810
  }
2747
2811
 
2748
- /* Inputs and textareas */
2749
- & :where(input, textarea) {
2812
+ /* Inputs and textareas placed as direct items */
2813
+ &>:where(input, textarea),
2814
+ & :where(li)>:where(input, textarea) {
2750
2815
  flex-shrink: 0;
2751
2816
 
2752
2817
  &:not(:first-child) {
@@ -2774,12 +2839,12 @@
2774
2839
  }
2775
2840
 
2776
2841
  /* Dark theme */
2777
- :where(.dark menu[popover]) :where(li, a, button, label):hover {
2842
+ :where(.dark menu[popover]) :where(li, a, button, label:not(:has(input:not([type=checkbox], [type=radio], [type=hidden]), select, textarea, .combobox, .label, label))):hover {
2778
2843
  background-color: var(--color-field-surface-hover, color-mix(oklch(20.5% 0 0) 15%, transparent))
2779
2844
  }
2780
2845
 
2781
- /* Nested menu alignment */
2782
- :where(menu menu):where(:not(.unstyle)) {
2846
+ /* Nested menu alignment (not combobox listboxes, which anchor to their field) */
2847
+ :where(menu menu:not([role=listbox])):where(:not(.unstyle)) {
2783
2848
  position-area: span-end end;
2784
2849
  margin: 0 var(--spacing-popover-offset, 0.5rem)
2785
2850
  }
@@ -5649,4 +5714,31 @@
5649
5714
  html[data-os="ios"] .no-ios { display: none !important }
5650
5715
  html[data-os="android"] .no-android { display: none !important }
5651
5716
  html[data-os="macos"] .no-apple, html[data-os="ios"] .no-apple { display: none !important }
5717
+
5718
+ /* Visibility — connectivity & container (html[data-*] set by the $device signal) */
5719
+ html:not([data-online="false"]) .offline-only { display: none !important }
5720
+ html[data-online="false"] .online-only { display: none !important }
5721
+ html:not([data-standalone]) .standalone-only { display: none !important }
5722
+ html:not([data-native]) .native-only { display: none !important }
5723
+ html[data-native] .web-only { display: none !important }
5724
+
5725
+ /* Safe-area padding (var --safe-* is 0 unless viewport-fit=cover on a notched screen) */
5726
+ :where(.pt-safe) { padding-top: var(--safe-top) }
5727
+ :where(.pr-safe) { padding-right: var(--safe-right) }
5728
+ :where(.pb-safe) { padding-bottom: var(--safe-bottom) }
5729
+ :where(.pl-safe) { padding-left: var(--safe-left) }
5730
+ :where(.px-safe) { padding-left: var(--safe-left); padding-right: var(--safe-right) }
5731
+ :where(.py-safe) { padding-top: var(--safe-top); padding-bottom: var(--safe-bottom) }
5732
+ :where(.p-safe) { padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left) }
5733
+
5734
+ /* Dynamic viewport height (avoids the mobile 100vh browser-chrome jump) */
5735
+ :where(.min-h-dvh) { min-height: 100dvh }
5736
+ :where(.h-dvh) { height: 100dvh }
5737
+
5738
+ /* Native — suppress web tells (opt-in) */
5739
+ :where(.no-callout) { -webkit-touch-callout: none }
5740
+ :where(.no-select) { -webkit-user-select: none; user-select: none }
5741
+ :where(.no-tap-zoom) { touch-action: manipulation }
5742
+ :where(.no-overscroll) { overscroll-behavior: none }
5743
+ :where(.no-overscroll-y) { overscroll-behavior-y: none }
5652
5744
  }
@@ -45,8 +45,8 @@
45
45
  transform-origin: top center;
46
46
  overflow-x: hidden;
47
47
 
48
- /* Options */
49
- & :where(li, a, button, [role=button], label):not(.unstyle) {
48
+ /* Options (labels only when flat items — field wrappers keep form layout) */
49
+ & :where(li, a, button, [role=button], label:not(:has(input:not([type=checkbox], [type=radio], [type=hidden]), select, textarea, .combobox, .label, label))):not(.unstyle) {
50
50
  display: inline-flex;
51
51
  justify-content: start;
52
52
  align-items: center;
@@ -106,13 +106,14 @@
106
106
  background-color: var(--color-line, color-mix(oklch(20.5% 0 0) 10%, transparent))
107
107
  }
108
108
 
109
- /* Labels */
110
- & label {
109
+ /* Labels (not inner field-surface wrappers) */
110
+ & label:not(.unstyle):not(:has(> input[type=search], > input[type=file], > button[command="--copy"])) {
111
111
  padding-inline-start: 0.5rem;
112
112
  padding-inline-end: 0.5rem;
113
113
  cursor: default;
114
114
 
115
- &:hover {
115
+ &:hover,
116
+ &:active {
116
117
  background-color: transparent
117
118
  }
118
119
 
@@ -121,8 +122,9 @@
121
122
  }
122
123
  }
123
124
 
124
- /* Inputs and textareas */
125
- & :where(input, textarea) {
125
+ /* Inputs and textareas placed as direct items */
126
+ &>:where(input, textarea),
127
+ & :where(li)>:where(input, textarea) {
126
128
  flex-shrink: 0;
127
129
 
128
130
  &:not(:first-child) {
@@ -150,12 +152,12 @@
150
152
  }
151
153
 
152
154
  /* Dark theme */
153
- :where(.dark menu[popover]) :where(li, a, button, label):hover {
155
+ :where(.dark menu[popover]) :where(li, a, button, label:not(:has(input:not([type=checkbox], [type=radio], [type=hidden]), select, textarea, .combobox, .label, label))):hover {
154
156
  background-color: var(--color-field-surface-hover, color-mix(oklch(20.5% 0 0) 15%, transparent))
155
157
  }
156
158
 
157
- /* Nested menu alignment */
158
- :where(menu menu):where(:not(.unstyle)) {
159
+ /* Nested menu alignment (not combobox listboxes, which anchor to their field) */
160
+ :where(menu menu:not([role=listbox])):where(:not(.unstyle)) {
159
161
  position-area: span-end end;
160
162
  margin: 0 var(--spacing-popover-offset, 0.5rem)
161
163
  }
@@ -2,12 +2,12 @@
2
2
  "manifest.appwrite.auth.js": "sha384-mHMJ2sVgZ82AdmLXK6ZyUpkWyX6KOtmIf5P/ribCUb2ejtvu/lwMhB68IgAcy9po",
3
3
  "manifest.appwrite.data.js": "sha384-KFKVo5TQESLJB7wgk+C8Zb/sAl43QAMdioWW3tQRWYq5umfdpGCZ5Hld9y9f6/Np",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
- "manifest.charts.js": "sha384-wTQlB3hJ9V9lmisCgm8l6YkpJV+nA4+Vci3dI0x1nBtWnS9TFr+eXaJelP7AusyD",
5
+ "manifest.charts.js": "sha384-qQrjhWkIDi4oU5uQrpy8DdlMjY+gfiivBuX6mssNiFdkawgkVBvKneGgp+nd3PYJ",
6
6
  "manifest.chat.js": "sha384-3+eva9YYz5DrdS3DeRGw9oXH/CreuYX+ZHHtYP25vmnYDHiEpFf9nEGLvwSHBXjz",
7
7
  "manifest.code.js": "sha384-G3MNfvQRWWY+gm4SGGrSsgQbDeil9MP+ON2DTk6Rcq9Nsex8woHMDt5EJHrFH0Nj",
8
8
  "manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
9
9
  "manifest.colorpicker.js": "sha384-N5ezEwgKrDodV4YdURxhdyZKmg1n/QhqvmAC4SEG03mw1pEAZAA8Gphp+NLAOxts",
10
- "manifest.combobox.js": "sha384-ZPOkx4hKGlcrOqeb3MmWLyBfjZXlnkP47Yc9VLE7yHds6dXRqMeB5DPFeJl3NZS/",
10
+ "manifest.combobox.js": "sha384-iJpm6Ufqrmm+zdl1YjPUeBkv60gMUxxe/P4Da9rNBuLyo04wkijU7KHxS/W7IG3Q",
11
11
  "manifest.components.js": "sha384-eZDib+RFdwd9mOUdi6o53eNS0LBlEEkZPDegxtkAcE+GrGIFxsuB4O0ymfixGNfM",
12
12
  "manifest.data.js": "sha384-3lLvGnqcQr4Ium80MdaQTr8RytjVbs5rnxd0q41BF3ppcoQccMFAcvgFSyZP/0sl",
13
13
  "manifest.datepicker.js": "sha384-e6rJI0BxuE+AyI96/u6XoeOstsxLuQTO6+wYSr6rm2aKhOP2APM2xJqTz//0v4Pw",
@@ -16,6 +16,7 @@
16
16
  "manifest.icons.js": "sha384-kKOINpX70YzNO3FvkV6umrmO3f0KIu0DNbwB+ej55a7y/lhU7v/myQtXL0bwCTbt",
17
17
  "manifest.localization.js": "sha384-MUetuW8PllIM+9JDMw/tx+qGem8t+ZUN5WZpycsTDKtz+LaX4DJehsfh/YZt9J7L",
18
18
  "manifest.markdown.js": "sha384-qlhYOzX0nl8HzuaiMVTkIhsma9mv3PlEvHO2S0AzshgmIcdZFvth6/sbHPCe5Zwr",
19
+ "manifest.native.js": "sha384-4WfuciRjqrfDLpHXp91AKQUTt8FDHaupSuVErRgdeaeoHf5d1CfHWqBd/aCOduDk",
19
20
  "manifest.payments.js": "sha384-vhG1ZL3wGzb0vt0ygJJQF3jVQumMuvc2smGjYezuyQ+auN+MN9HY2k5NW2bdClxl",
20
21
  "manifest.resize.js": "sha384-S3v4RAJoA77LUH6MddYG9bx//dZX//up7XWeH69Ql2ge3bVHgC6mR/CJBe6y4nQI",
21
22
  "manifest.router.js": "sha384-oV0lwFkeevEWjRCC/VV2OzkjNdE70owBz+pwKqZzx5bqbx5/1PORW+ndCClEqSgS",
@@ -27,7 +28,7 @@
27
28
  "manifest.toasts.js": "sha384-ytd5rDbax/Ou9z23uedFXPZbxDPsk2E/pxCTq4WLvfv+os1qTI6kELp0kPp07g24",
28
29
  "manifest.tooltips.js": "sha384-HeJ0BGK3ebut1t8lm+MGeUUxd1pOZDyX8e6HFLsa4kAiRL+LwGDr67ckkRqv7to1",
29
30
  "manifest.url.parameters.js": "sha384-FIufiClqDx1rJpU/QUc9z/D43qClQ6Qm8rBahipbJl9BDHUvhrOsUDegmTWW7Tuf",
30
- "manifest.utilities.js": "sha384-MtzNaVOrgyepjlY5nz38pAJk67yiFPU1H5WoPNfcNbUZTXy+oin1lrtKCIcobEOJ",
31
+ "manifest.utilities.js": "sha384-yLEqEbd1FncqswrtrcTO7KrIEK4iP0ZvqCi64XLI7D77/L0YiJbjLdLGGg6l7rgN",
31
32
  "manifest.virtual.js": "sha384-c194pXD0Ld48QJ+HPVNibvbn54/ETJRuYhUOWesEBjtOIQcQMIKo0DnCyQMvDlLg",
32
- "manifest.js": "sha384-RusFaByZu31LkLZCAhjgtJC8iWksvmjTKS8D5Xj9/4N7B/Xr4DDyFGmiL23kajgK"
33
+ "manifest.js": "sha384-FLz68HEE8hEVLlAsoAX2+5cWBrhS5+B24rdJy5obXtP5U5bJKCswjbIQL8MpDUFZ"
33
34
  }
package/lib/manifest.js CHANGED
@@ -265,7 +265,8 @@
265
265
  // Plugin dependencies: plugins that require other plugins to be loaded first
266
266
  const PLUGIN_DEPENDENCIES = {
267
267
  'appwrite-data': ['data'],
268
- 'appwrite-presences': ['data', 'appwrite-auth']
268
+ 'appwrite-presences': ['data', 'appwrite-auth'],
269
+ 'native': ['utilities']
269
270
  };
270
271
 
271
272
  // Derive default plugin list from manifest (only load data/localization/components when manifest needs them)
@@ -488,6 +489,14 @@
488
489
  return [];
489
490
  }
490
491
 
492
+ // Detect the native umbrella plugin. Auto-loaded inside a Capacitor container
493
+ // (window.Capacitor present) or when a `native` config block opts in on the web.
494
+ function detectNativePlugins(manifest) {
495
+ if (typeof window !== 'undefined' && window.Capacitor) return ['native'];
496
+ if (manifest && typeof manifest === 'object' && manifest.native && typeof manifest.native === 'object') return ['native'];
497
+ return [];
498
+ }
499
+
491
500
  // Parse data attributes
492
501
  function parseDataAttributes() {
493
502
  // Try to get current script first, then fall back to querySelector
@@ -560,7 +569,7 @@
560
569
  // Expose API
561
570
  window.Manifest = {
562
571
  loadPlugin: function (pluginName, version = DEFAULT_VERSION) {
563
- const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, 'payments', 'chat'];
572
+ const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, 'payments', 'chat', 'native'];
564
573
  if (!allPlugins.includes(pluginName)) {
565
574
  console.warn(`[Manifest Loader] Unknown plugin: ${pluginName}`);
566
575
  return Promise.reject(new Error(`Unknown plugin: ${pluginName}`));
@@ -654,12 +663,18 @@
654
663
  const appwritePlugins = detectAppwritePlugins(manifest);
655
664
  const paymentsPlugins = detectPaymentsPlugins(manifest);
656
665
  const chatPlugins = detectChatPlugins(manifest);
657
- pluginsToLoad = resolveDependencies([...corePlugins, ...appwritePlugins, ...paymentsPlugins, ...chatPlugins]);
666
+ const nativePlugins = detectNativePlugins(manifest);
667
+ pluginsToLoad = resolveDependencies([...corePlugins, ...appwritePlugins, ...paymentsPlugins, ...chatPlugins, ...nativePlugins]);
658
668
  } else {
659
669
  const needsManifest = config.plugins.some(p => MANIFEST_DEPENDENT_PLUGINS.includes(p));
660
670
  if (needsManifest) {
661
671
  manifestPromise = fetch(manifestUrl).then(r => r.ok ? r.json() : null).catch(() => null);
662
672
  }
673
+ // Inside a Capacitor container, ensure the native umbrella loads even on
674
+ // the explicit data-plugins path (matches the derive-path auto-inject).
675
+ if (typeof window !== 'undefined' && window.Capacitor && !pluginsToLoad.includes('native')) {
676
+ pluginsToLoad = resolveDependencies([...pluginsToLoad, 'native']);
677
+ }
663
678
  }
664
679
 
665
680
  const pluginPromises = pluginsToLoad.map(pluginName => {