mnfst 0.5.214 → 0.5.215

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.
@@ -1070,57 +1070,6 @@ window.ManifestAppwriteAuth = {
1070
1070
 
1071
1071
  /* Auth frontend */
1072
1072
 
1073
- // Create a safe fallback proxy for undefined properties (similar to data proxies)
1074
- let authLoadingProxy = null;
1075
- function createAuthLoadingProxy() {
1076
- if (authLoadingProxy) {
1077
- return authLoadingProxy;
1078
- }
1079
-
1080
- const fallback = Object.create(null);
1081
- fallback[Symbol.toPrimitive] = function (hint) {
1082
- return hint === 'number' ? 0 : '';
1083
- };
1084
- fallback.valueOf = function () { return ''; };
1085
- fallback.toString = function () { return ''; };
1086
-
1087
- Object.defineProperty(fallback, 'length', {
1088
- value: 0,
1089
- writable: false,
1090
- enumerable: false,
1091
- configurable: false
1092
- });
1093
-
1094
- authLoadingProxy = new Proxy(fallback, {
1095
- get(target, key) {
1096
- if (key === Symbol.iterator) {
1097
- return function* () { };
1098
- }
1099
- if (key === 'then' || key === 'catch' || key === 'finally' ||
1100
- key === Symbol.toStringTag || key === Symbol.hasInstance ||
1101
- key === 'constructor' || key === '__proto__' || key === 'prototype') {
1102
- return undefined;
1103
- }
1104
- if (key in target || key === Symbol.toPrimitive) {
1105
- const value = target[key];
1106
- if (value !== undefined) {
1107
- return value;
1108
- }
1109
- }
1110
- // Return proxy itself for safe chaining (allows $auth.user.email even if user is undefined)
1111
- return authLoadingProxy;
1112
- },
1113
- has(target, key) {
1114
- if (typeof key === 'string') {
1115
- return true;
1116
- }
1117
- return key in target || key === Symbol.toPrimitive;
1118
- }
1119
- });
1120
-
1121
- return authLoadingProxy;
1122
- }
1123
-
1124
1073
  // Initialize $auth magic method
1125
1074
  function initializeAuthMagic() {
1126
1075
  if (typeof Alpine === 'undefined') {
@@ -1188,43 +1137,10 @@ function initializeAuthMagic() {
1188
1137
  return () => ({ success: false, error: 'Method not initialized' });
1189
1138
  }
1190
1139
  }
1191
- // Null/undefined: loading proxy keeps $auth.user.email chains safe
1192
- if (value === null || value === undefined) {
1193
- return createAuthLoadingProxy();
1194
- }
1195
- // If value is an array, return it as-is (arrays are already iterable and don't need proxying)
1196
- if (Array.isArray(value)) {
1197
- return value;
1198
- }
1199
- // If value is an object, wrap it in a proxy for safe nested property access
1200
- if (typeof value === 'object' && value !== null) {
1201
- // Recursive helper function for nested object proxying
1202
- function createNestedAuthProxy(objTarget) {
1203
- return new Proxy(objTarget, {
1204
- get(objTarget, key) {
1205
- // Handle special keys
1206
- if (key === Symbol.iterator || key === 'then' || key === 'catch' || key === 'finally') {
1207
- return undefined;
1208
- }
1209
- const nestedValue = objTarget[key];
1210
- // If nested value is undefined or null, return loading proxy for safe chaining
1211
- if (nestedValue === undefined || nestedValue === null) {
1212
- return createAuthLoadingProxy();
1213
- }
1214
- // If nested value is an array, return it as-is
1215
- if (Array.isArray(nestedValue)) {
1216
- return nestedValue;
1217
- }
1218
- // If nested value is an object, wrap recursively
1219
- if (typeof nestedValue === 'object' && nestedValue !== null) {
1220
- return createNestedAuthProxy(nestedValue);
1221
- }
1222
- return nestedValue;
1223
- }
1224
- });
1225
- }
1226
- return createNestedAuthProxy(value);
1227
- }
1140
+ // Hand back the real value — no placeholder. A signed-out
1141
+ // user is null, not a stand-in object: `?.`, `||` and typeof
1142
+ // must all behave. Templates use `$auth.user?.email`; Alpine
1143
+ // renders undefined as ''.
1228
1144
  return value;
1229
1145
  }
1230
1146
 
@@ -1285,8 +1201,8 @@ function initializeAuthMagic() {
1285
1201
  return store.getProvider();
1286
1202
  }
1287
1203
 
1288
- // Return loading proxy for undefined properties to allow safe chaining
1289
- return createAuthLoadingProxy();
1204
+ // Unknown property — undefined, same as any other object.
1205
+ return undefined;
1290
1206
  },
1291
1207
  set(target, prop, value) {
1292
1208
  // Forward assignments to the store for two-way binding (x-model)
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.214';
5
+ const MANIFEST_BUILD_VERSION = '0.5.215';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
@@ -17,11 +17,14 @@
17
17
  text-align: center;
18
18
  text-transform: uppercase;
19
19
  color: var(--color-field-inverse, oklch(43.9% 0 0));
20
- background-color: var(--color-field-surface, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent));
20
+ /* surface-3, not field-surface: an avatar is content, not a control.
21
+ Themes are free to make fields translucent, and a see-through avatar
22
+ bleeds the one stacked behind it. Same value in both modes today. */
23
+ background-color: var(--color-surface-3, oklch(87% 0 0));
21
24
  background-clip: content-box;
22
25
  background-size: cover;
23
26
  background-position: center;
24
- border-radius: var(--radius, 0.5rem);
27
+ border-radius: 50%;
25
28
 
26
29
  /* Icon */
27
30
  &[x-icon] {
@@ -49,7 +52,7 @@
49
52
  width: 9px;
50
53
  height: 9px;
51
54
  z-index: 1;
52
- background-color: var(--color-field-surface, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent));
55
+ background-color: var(--color-surface-3, oklch(87% 0 0));
53
56
  border: 1px solid var(--color-page, oklch(98.5% 0 0));
54
57
  border-radius: 50%
55
58
  }
@@ -61,7 +64,9 @@
61
64
  background-blend-mode: normal;
62
65
 
63
66
  &:hover {
64
- background-color: var(--color-field-surface-hover, color-mix(in oklch, oklch(20.5% 0 0) 15%, transparent));
67
+ /* Steps off the avatar's own surface, not a field token — darkens in
68
+ light mode, lightens in dark, and never goes translucent. */
69
+ background-color: color-mix(in oklch, var(--color-surface-3, oklch(87% 0 0)) 88%, var(--color-content-stark, oklch(20.5% 0 0)));
65
70
  background-blend-mode: multiply;
66
71
 
67
72
  & img {
@@ -113,7 +118,7 @@
113
118
  padding-inline-end: 1.5ch;
114
119
 
115
120
  &:hover .avatar {
116
- background-color: var(--color-field-surface-hover, color-mix(in oklch, oklch(20.5% 0 0) 15%, transparent));
121
+ background-color: color-mix(in oklch, var(--color-surface-3, oklch(87% 0 0)) 88%, var(--color-content-stark, oklch(20.5% 0 0)));
117
122
  transition: var(--transition, all .05s ease-in-out)
118
123
  }
119
124
  }
@@ -80,4 +80,22 @@
80
80
  mask-image: 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='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m6 9 6 6 6-6'/%3E%3C/svg%3E"))
81
81
  }
82
82
  }
83
+
84
+ /* Without base-select the reset's `appearance: button` leaves the select
85
+ natively rendered, and WebKit then ignores the author's border-radius and
86
+ imposes its own — so selects stop matching every other field. `appearance:
87
+ none` hands styling back; it also drops the native arrow, and ::picker-icon
88
+ only paints under base-select, so redraw the chevron as a background. It
89
+ can't be masked to currentColor there (an SVG loaded as an image can't see
90
+ it), hence the baked mid-grey, legible on the field surface in both modes. */
91
+ @supports not (appearance: base-select) {
92
+ :where(select):not(.unstyle) {
93
+ appearance: none;
94
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23808080' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
95
+ background-repeat: no-repeat;
96
+ background-position: right var(--spacing-field-padding, 0.625rem) center;
97
+ background-size: 1rem;
98
+ padding-inline-end: calc(var(--spacing-field-padding, 0.625rem) * 2 + 1rem)
99
+ }
100
+ }
83
101
  }
@@ -25,7 +25,7 @@
25
25
  const merged = JSON.parse(JSON.stringify(fallbacks || {}));
26
26
  try {
27
27
  if (!window.Alpine || typeof Alpine.evaluate !== 'function') return merged;
28
- try { Alpine.evaluate(document.body, '$locale && $locale.current'); } catch (_) { } // dep → re-resolve on locale switch
28
+ try { Alpine.evaluate(document.body, 'typeof $locale !== "undefined" && $locale.current'); } catch (_) { } // dep → re-resolve on locale switch
29
29
  for (const name of this._loadedSourceNames()) {
30
30
  let ui;
31
31
  try { ui = Alpine.evaluate(document.body, `$x['${name}'] && $x['${name}']._ui && $x['${name}']._ui['${component}']`); } catch (_) { ui = null; }
@@ -23,7 +23,7 @@ function initializeColorpickerPlugin() {
23
23
  const merged = JSON.parse(JSON.stringify(fallbacks || {}));
24
24
  try {
25
25
  if (!window.Alpine || typeof Alpine.evaluate !== 'function') return merged;
26
- try { Alpine.evaluate(document.body, '$locale && $locale.current'); } catch (_) { } // dep → re-resolve on locale switch
26
+ try { Alpine.evaluate(document.body, 'typeof $locale !== "undefined" && $locale.current'); } catch (_) { } // dep → re-resolve on locale switch
27
27
  for (const name of this._loadedSourceNames()) {
28
28
  let ui;
29
29
  try { ui = Alpine.evaluate(document.body, `$x['${name}'] && $x['${name}']._ui && $x['${name}']._ui['${component}']`); } catch (_) { ui = null; }
@@ -1689,7 +1689,7 @@ function initializeColorpickerPlugin() {
1689
1689
 
1690
1690
  // Reactive deps trigger re-runs on locale switch / manifest load.
1691
1691
  Alpine.effect(() => {
1692
- try { Alpine.evaluate(evalCtx, '$locale && $locale.current'); } catch {}
1692
+ try { Alpine.evaluate(evalCtx, 'typeof $locale !== "undefined" && $locale.current'); } catch {}
1693
1693
  try { Alpine.evaluate(evalCtx, '$x && $x.manifest && $x.manifest._loadedFrom'); } catch {}
1694
1694
  runDiscovery();
1695
1695
  // Re-localize the whole picker on locale switch.
@@ -23,7 +23,7 @@ if (!window.ManifestUI) {
23
23
  const merged = JSON.parse(JSON.stringify(fallbacks || {}));
24
24
  try {
25
25
  if (!window.Alpine || typeof Alpine.evaluate !== 'function') return merged;
26
- try { Alpine.evaluate(document.body, '$locale && $locale.current'); } catch (_) { }
26
+ try { Alpine.evaluate(document.body, 'typeof $locale !== "undefined" && $locale.current'); } catch (_) { }
27
27
  for (const name of this._loadedSourceNames()) {
28
28
  let ui;
29
29
  try { ui = Alpine.evaluate(document.body, `$x['${name}'] && $x['${name}']._ui && $x['${name}']._ui['${component}']`); } catch (_) { ui = null; }
@@ -102,9 +102,11 @@ window.ManifestComponentsRegistry = {
102
102
  window.ManifestComponentsLoader = {
103
103
  cache: {},
104
104
  _loading: {},
105
+ _missing: null,
105
106
  initialize() {
106
107
  this.cache = {};
107
108
  this._loading = {};
109
+ this._missing = new Set();
108
110
  // Preload components listed in registry.preloaded
109
111
  const registry = window.ManifestComponentsRegistry;
110
112
  if (registry && Array.isArray(registry.preloaded)) {
@@ -123,30 +125,42 @@ window.ManifestComponentsLoader = {
123
125
  return this._loading[name];
124
126
  }
125
127
  const registry = window.ManifestComponentsRegistry;
126
- if (!registry || !registry.manifest) {
127
- console.warn('[Manifest] Manifest not loaded, cannot load component:', name);
128
+ if (!registry) {
129
+ console.warn('[Manifest] Registry unavailable, cannot load component:', name);
128
130
  return null;
129
131
  }
130
- const path = (registry.manifest.preloadedComponents || []).concat(registry.manifest.components || [])
132
+ // No manifest.json is fine — the convention fallback below still resolves.
133
+ const mf = registry.manifest || {};
134
+ let path = (mf.preloadedComponents || []).concat(mf.components || [])
131
135
  .find(p => p.split('/').pop().replace('.html', '') === name);
136
+ // Convention fallback: unlisted names resolve to components/<name>.html.
137
+ let convention = false;
132
138
  if (!path) {
133
- console.warn('[Manifest] Component', name, 'not found in manifest.');
134
- return null;
139
+ if (!this._missing) this._missing = new Set();
140
+ if (this._missing.has(name)) return null;
141
+ path = 'components/' + name + '.html';
142
+ convention = true;
135
143
  }
136
144
  const base = (typeof window.getManifestBase === 'function' ? window.getManifestBase() : '') || '/';
137
145
  let url = path.startsWith('/') || path.startsWith('http') ? path : base + path;
138
146
  // Version stamp (publish-injected `deployment`, or authored `version`) busts browser-cached component HTML
139
- const stamp = registry.manifest.deployment || registry.manifest.version;
147
+ const stamp = mf.deployment || mf.version;
140
148
  if (stamp) url += (url.includes('?') ? '&' : '?') + 'v=' + encodeURIComponent(String(stamp));
141
149
  const promise = (async () => {
142
150
  try {
143
151
  const response = await fetch(url);
144
152
  if (!response.ok) {
145
- console.warn('[Manifest] HTML file not found for component', name, 'at path:', path, '(HTTP', response.status + ')');
153
+ if (convention) {
154
+ this._missing.add(name);
155
+ console.warn('[Manifest] Component', name, 'is not listed in manifest.json and', path, 'was not found (HTTP', response.status + ')');
156
+ } else {
157
+ console.warn('[Manifest] HTML file not found for component', name, 'at path:', path, '(HTTP', response.status + ')');
158
+ }
146
159
  return null;
147
160
  }
148
161
  const content = await response.text();
149
162
  this.cache[name] = content;
163
+ if (convention) registry.registered.add(name);
150
164
  return content;
151
165
  } catch (error) {
152
166
  console.warn('[Manifest] Failed to load component', name, 'from', path + ':', error.message);
@@ -176,6 +190,9 @@ function escapeForSingleQuotedJsString(s) {
176
190
  .replace(/\t/g, '\\t');
177
191
  }
178
192
 
193
+ // Framework web components (not project components) — never fetched.
194
+ const FRAMEWORK_COMPONENT_TAGS = new Set(['code', 'code-group']);
195
+
179
196
  window.ManifestComponentsProcessor = {
180
197
  async processComponent(element, instanceId) {
181
198
  const name = element.tagName.toLowerCase().replace('x-', '');
@@ -185,10 +202,7 @@ window.ManifestComponentsProcessor = {
185
202
  console.debug('[Manifest Components] skipped: registry or loader unavailable', element);
186
203
  return;
187
204
  }
188
- if (!registry.registered.has(name)) {
189
- console.debug('[Manifest Components] skipped: component not registered', element);
190
- return;
191
- }
205
+ if (FRAMEWORK_COMPONENT_TAGS.has(name)) return;
192
206
  if (element.hasAttribute('data-pre-rendered') || element.hasAttribute('data-processed')) {
193
207
  // Pre-rendered content skips re-fetch but still needs Alpine init.
194
208
  if (element.hasAttribute('data-pre-rendered') && window.Alpine && typeof window.Alpine.initTree === 'function') {
@@ -197,8 +211,15 @@ window.ManifestComponentsProcessor = {
197
211
  console.debug('[Manifest Components] skipped: already pre-rendered/processed', element);
198
212
  return;
199
213
  }
214
+ // Unregistered names still try the loader's components/<name>.html
215
+ // convention; a miss leaves the element alone (it may be someone else's).
216
+ const wasRegistered = registry.registered.has(name);
200
217
  const content = await loader.loadComponent(name);
201
218
  if (!content) {
219
+ if (!wasRegistered) {
220
+ console.debug('[Manifest Components] skipped: component not registered', element);
221
+ return;
222
+ }
202
223
  console.debug('[Manifest Components] skipped: failed to load component', element);
203
224
  element.replaceWith(document.createComment(` Failed to load component: ${name} `));
204
225
  return;
package/lib/manifest.css CHANGED
@@ -474,11 +474,14 @@
474
474
  text-align: center;
475
475
  text-transform: uppercase;
476
476
  color: var(--color-field-inverse, oklch(43.9% 0 0));
477
- background-color: var(--color-field-surface, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent));
477
+ /* surface-3, not field-surface: an avatar is content, not a control.
478
+ Themes are free to make fields translucent, and a see-through avatar
479
+ bleeds the one stacked behind it. Same value in both modes today. */
480
+ background-color: var(--color-surface-3, oklch(87% 0 0));
478
481
  background-clip: content-box;
479
482
  background-size: cover;
480
483
  background-position: center;
481
- border-radius: var(--radius, 0.5rem);
484
+ border-radius: 50%;
482
485
 
483
486
  /* Icon */
484
487
  &[x-icon] {
@@ -506,7 +509,7 @@
506
509
  width: 9px;
507
510
  height: 9px;
508
511
  z-index: 1;
509
- background-color: var(--color-field-surface, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent));
512
+ background-color: var(--color-surface-3, oklch(87% 0 0));
510
513
  border: 1px solid var(--color-page, oklch(98.5% 0 0));
511
514
  border-radius: 50%
512
515
  }
@@ -518,7 +521,9 @@
518
521
  background-blend-mode: normal;
519
522
 
520
523
  &:hover {
521
- background-color: var(--color-field-surface-hover, color-mix(in oklch, oklch(20.5% 0 0) 15%, transparent));
524
+ /* Steps off the avatar's own surface, not a field token — darkens in
525
+ light mode, lightens in dark, and never goes translucent. */
526
+ background-color: color-mix(in oklch, var(--color-surface-3, oklch(87% 0 0)) 88%, var(--color-content-stark, oklch(20.5% 0 0)));
522
527
  background-blend-mode: multiply;
523
528
 
524
529
  & img {
@@ -570,7 +575,7 @@
570
575
  padding-inline-end: 1.5ch;
571
576
 
572
577
  &:hover .avatar {
573
- background-color: var(--color-field-surface-hover, color-mix(in oklch, oklch(20.5% 0 0) 15%, transparent));
578
+ background-color: color-mix(in oklch, var(--color-surface-3, oklch(87% 0 0)) 88%, var(--color-content-stark, oklch(20.5% 0 0)));
574
579
  transition: var(--transition, all .05s ease-in-out)
575
580
  }
576
581
  }
@@ -670,6 +675,24 @@
670
675
  mask-image: 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='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m6 9 6 6 6-6'/%3E%3C/svg%3E"))
671
676
  }
672
677
  }
678
+
679
+ /* Without base-select the reset's `appearance: button` leaves the select
680
+ natively rendered, and WebKit then ignores the author's border-radius and
681
+ imposes its own — so selects stop matching every other field. `appearance:
682
+ none` hands styling back; it also drops the native arrow, and ::picker-icon
683
+ only paints under base-select, so redraw the chevron as a background. It
684
+ can't be masked to currentColor there (an SVG loaded as an image can't see
685
+ it), hence the baked mid-grey, legible on the field surface in both modes. */
686
+ @supports not (appearance: base-select) {
687
+ :where(select):not(.unstyle) {
688
+ appearance: none;
689
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23808080' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
690
+ background-repeat: no-repeat;
691
+ background-position: right var(--spacing-field-padding, 0.625rem) center;
692
+ background-size: 1rem;
693
+ padding-inline-end: calc(var(--spacing-field-padding, 0.625rem) * 2 + 1rem)
694
+ }
695
+ }
673
696
  }
674
697
 
675
698
  /* Manifest Charts */
@@ -3258,7 +3281,12 @@
3258
3281
  }
3259
3282
 
3260
3283
  & input,
3261
- & .combobox,
3284
+ & .combobox {
3285
+ width: fit-content
3286
+ }
3287
+
3288
+ /* Own rule: a selector list is not forgiving, so a browser that can't
3289
+ parse the :has() would drop the two plain selectors above with it. */
3262
3290
  & :where(label, .label):has(input:not([type=checkbox], [type=radio]), select, textarea) {
3263
3291
  width: fit-content
3264
3292
  }
@@ -4880,7 +4908,11 @@
4880
4908
  border-radius: var(--radius, 0.5rem)
4881
4909
  }
4882
4910
 
4883
- /* Badges */
4911
+ /* Badges. Leading is normal so descenders clear the bottom edge, padding
4912
+ comes off --spacing rather than ch (0.65ch crowds the first letter), and
4913
+ the radius follows --radius instead of a hard pill. The semantic surface
4914
+ stays solid: tinting it and keeping --color-field-inverse drops dark-mode
4915
+ contrast to ~1.1, and moving to --color-content-stark only reaches ~3. */
4884
4916
  :where(.badge) {
4885
4917
  display: inline-flex;
4886
4918
  justify-content: center;
@@ -4888,17 +4920,27 @@
4888
4920
  gap: 0.25rem;
4889
4921
  width: fit-content;
4890
4922
  height: fit-content;
4891
- padding: 0.35ch 0.65ch;
4923
+ padding: calc(var(--spacing, 0.25rem) * 0.5) calc(var(--spacing, 0.25rem) * 2);
4892
4924
  font-weight: 500;
4893
4925
  font-size: 0.75rem;
4894
- line-height: 1;
4926
+ line-height: 1.25;
4895
4927
  color: var(--color-field-inverse, oklch(43.9% 0 0));
4896
4928
  background-color: var(--color-field-surface, color-mix(in oklch, oklch(97% 0 0) 10%, transparent));
4897
- border-radius: 100px;
4929
+ border-radius: calc(var(--radius, 0.5rem) * 0.6);
4930
+
4931
+ /* Solo icons square up, as buttons do. (The old :only-child here keyed
4932
+ off the badge's siblings, so an icon badge next to anything got word
4933
+ padding.) */
4934
+ &[x-icon] {
4935
+ padding: calc(var(--spacing, 0.25rem) * 0.5);
4936
+ aspect-ratio: 1
4937
+ }
4898
4938
 
4899
- /* Solo icons */
4900
- &:only-child[x-icon] {
4901
- padding: 0.35ch
4939
+ /* Same for a badge wrapping a bare <svg>. Its own rule so the plain
4940
+ selector above still lands without :has(). */
4941
+ &:has(> svg:only-child) {
4942
+ padding: calc(var(--spacing, 0.25rem) * 0.5);
4943
+ aspect-ratio: 1
4902
4944
  }
4903
4945
  }
4904
4946
 
@@ -5845,7 +5887,12 @@
5845
5887
  margin-top: calc(3rem)
5846
5888
  }
5847
5889
 
5848
- &>details:last-child,
5890
+ &>details:last-child {
5891
+ margin-bottom: calc(3rem)
5892
+ }
5893
+
5894
+ /* Own rule — :not() is not forgiving either, so an unsupported :has()
5895
+ inside it would take :last-child above down with it. */
5849
5896
  &>details:not(:last-child):not(:has(+ details, + hr)) {
5850
5897
  margin-bottom: calc(3rem)
5851
5898
  }
package/lib/manifest.d.ts CHANGED
@@ -268,6 +268,16 @@ export type ManifestExport = (opts?: {
268
268
  [extra: string]: unknown;
269
269
  }) => Promise<unknown>;
270
270
 
271
+ /** `$import(opts)` from the import plugin — pick a local file and parse it (json/csv). Resolves the parsed data, or null when the dialog is dismissed. */
272
+ export type ManifestImport = (opts?: {
273
+ format?: 'json' | 'csv';
274
+ /** Data source to replace with the parsed content. */
275
+ source?: string;
276
+ /** Override the file input's accept list. */
277
+ accept?: string;
278
+ [extra: string]: unknown;
279
+ }) => Promise<unknown>;
280
+
271
281
  /** `$device` from the utilities plugin (enriched by the device plugin when present). */
272
282
  export interface ManifestDevice {
273
283
  readonly os: string;
@@ -424,7 +434,7 @@ export type ManifestDirective =
424
434
  | 'x-icon' | 'x-svg' | 'x-markdown' | 'x-code' | 'x-code-group'
425
435
  | 'x-toast' | 'x-tooltip' | 'x-carousel' | 'x-virtual' | 'x-resize' | 'x-computed' | 'x-defer'
426
436
  | 'x-chart' | 'x-date' | 'x-color' | 'x-colorpicker' | 'x-combobox'
427
- | 'x-export' | 'x-pay' | 'x-files' | 'x-data-files'
437
+ | 'x-export' | 'x-import' | 'x-pay' | 'x-files' | 'x-data-files'
428
438
  | 'x-edit' | 'x-text-edit';
429
439
 
430
440
  // ---------------------------------------------------------------------------
@@ -471,6 +481,8 @@ declare global {
471
481
  const $chart: ManifestChart;
472
482
  /** Export plugin. */
473
483
  const $export: ManifestExport;
484
+ /** Import plugin. */
485
+ const $import: ManifestImport;
474
486
  /** Element editor (edit plugin; opt-in). */
475
487
  const $edit: ManifestEdit;
476
488
  /** Text editor (text-edit plugin). */
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.214';
5
+ const MANIFEST_BUILD_VERSION = '0.5.215';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -74,7 +74,7 @@
74
74
  const merged = JSON.parse(JSON.stringify(fallbacks || {}));
75
75
  try {
76
76
  if (!window.Alpine || typeof Alpine.evaluate !== 'function') return merged;
77
- try { Alpine.evaluate(document.body, '$locale && $locale.current'); } catch (_) { } // dep → re-resolve on locale switch
77
+ try { Alpine.evaluate(document.body, 'typeof $locale !== "undefined" && $locale.current'); } catch (_) { } // dep → re-resolve on locale switch
78
78
  for (const name of this._loadedSourceNames()) {
79
79
  let ui;
80
80
  try { ui = Alpine.evaluate(document.body, `$x['${name}'] && $x['${name}']._ui && $x['${name}']._ui['${component}']`); } catch (_) { ui = null; }