mnfst 0.5.130 → 0.5.133

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.
package/lib/manifest.css CHANGED
@@ -2142,16 +2142,17 @@
2142
2142
  border-radius: 6px;
2143
2143
  cursor: pointer;
2144
2144
  user-select: none;
2145
+ transition: none;
2145
2146
 
2146
2147
  &:hover {
2147
2148
  color: var(--color-field-inverse, oklch(43.9% 0 0));
2148
2149
  text-decoration: inherit;
2149
- background-color: var(--color-field-surface, color-mix(oklch(20.5% 0 0) 10%, transparent));
2150
+ background-color: var(--color-field-surface, color-mix(oklch(20.5% 0 0) 10%, transparent))
2150
2151
  }
2151
2152
 
2152
2153
  &:active {
2153
2154
  color: var(--color-field-inverse, oklch(43.9% 0 0));
2154
- background-color: var(--color-field-surface, color-mix(oklch(20.5% 0 0) 10%, transparent));
2155
+ background-color: var(--color-field-surface, color-mix(oklch(20.5% 0 0) 10%, transparent))
2155
2156
  }
2156
2157
 
2157
2158
  & span,
@@ -2168,7 +2169,7 @@
2168
2169
  /* Titles */
2169
2170
  & small {
2170
2171
  padding: 0.25rem 0.5rem;
2171
- color: var(--color-content-neutral, oklch(43.9% 0 0));
2172
+ color: var(--color-content-neutral, oklch(43.9% 0 0))
2172
2173
  }
2173
2174
 
2174
2175
  /* Horizontal rules (offset to ignore menu padding) */
@@ -2178,7 +2179,7 @@
2178
2179
  margin-inline-start: calc(0.25rem * -1);
2179
2180
  margin-top: 0.25rem;
2180
2181
  margin-bottom: 0.25rem;
2181
- background-color: var(--color-line, color-mix(oklch(20.5% 0 0) 10%, transparent));
2182
+ background-color: var(--color-line, color-mix(oklch(20.5% 0 0) 10%, transparent))
2182
2183
  }
2183
2184
 
2184
2185
  /* Labels */
@@ -2226,7 +2227,7 @@
2226
2227
 
2227
2228
  /* Dark theme */
2228
2229
  :where(.dark menu[popover]) :where(li, a, button, label):hover {
2229
- background-color: var(--color-field-surface-hover, color-mix(oklch(20.5% 0 0) 15%, transparent));
2230
+ background-color: var(--color-field-surface-hover, color-mix(oklch(20.5% 0 0) 15%, transparent))
2230
2231
  }
2231
2232
 
2232
2233
  /* Nested menu alignment */
@@ -2237,7 +2238,7 @@
2237
2238
 
2238
2239
  /* Center alignment */
2239
2240
  :where(menu.center) {
2240
- position-area: center;
2241
+ position-area: center
2241
2242
  }
2242
2243
 
2243
2244
  /* Top alignment */
@@ -2374,6 +2375,7 @@
2374
2375
  z-index: 1
2375
2376
  }
2376
2377
 
2378
+ /* Equal-width children (give the wrapper a width if children need more room) */
2377
2379
  &.even>* {
2378
2380
  flex-shrink: initial;
2379
2381
  width: 100%
@@ -2415,21 +2417,24 @@
2415
2417
  /* Concentric corners */
2416
2418
  &>* {
2417
2419
  flex-grow: 1;
2420
+ width: fit-content;
2418
2421
  min-height: 0;
2419
2422
  height: 100%;
2423
+ color: var(--color-content-neutral, oklch(43.9% 0 0));
2420
2424
  background-color: transparent;
2421
2425
  border-radius: max(calc(var(--radius, 0.5rem) - var(--spacing, 0.25rem) / 2), 0px);
2422
2426
 
2423
2427
  &:hover:not(.selected, [aria-selected=true], [aria-current]) {
2424
- background-color: color-mix(in oklch, var(--color-field-surface-hover, oklch(37.1% 0 0)) 40%, transparent)
2428
+ color: var(--color-field-inverse, oklch(20.5% 0 0));
2429
+ background-color: color-mix(in oklch, var(--color-field-surface-hover, oklch(70.8% 0 0)) 40%, transparent)
2425
2430
  }
2426
2431
  }
2427
2432
 
2428
2433
  /* Selected tab */
2429
2434
  &>:is(.selected, [aria-selected=true], [aria-current]) {
2430
- color: var(--color-page, oklch(98.5% 0 0));
2431
- background-color: var(--color-content-stark, oklch(20.5% 0 0));
2432
- box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
2435
+ color: var(--color-field-inverse, oklch(20.5% 0 0));
2436
+ background-color: var(--color-field-surface, oklch(87% 0 0));
2437
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)
2433
2438
  }
2434
2439
 
2435
2440
  /* Background slider for selected tab) */
@@ -2440,7 +2445,7 @@
2440
2445
  anchor-name: --selected-tab;
2441
2446
  --co-anchor: --selected-tab;
2442
2447
  background-color: transparent;
2443
- box-shadow: none;
2448
+ box-shadow: none
2444
2449
  }
2445
2450
 
2446
2451
  &:has(>:is(.selected, [aria-selected=true], [aria-current]))::before {
@@ -2452,10 +2457,14 @@
2452
2457
  left: anchor(left);
2453
2458
  width: anchor-size(width);
2454
2459
  height: anchor-size(height);
2455
- background-color: var(--color-content-stark, oklch(20.5% 0 0));
2460
+ background-color: var(--color-page, oklch(98.5% 0 0));
2456
2461
  border-radius: max(calc(var(--radius, 0.5rem) - var(--spacing, 0.25rem) / 2), 0px);
2457
2462
  box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
2458
- transition: var(--tab-slide-transition, all 0.15s ease-in-out);
2463
+ transition: var(--tab-slide-transition, all 0.15s ease-in-out)
2464
+ }
2465
+
2466
+ .dark &:has(>:is(.selected, [aria-selected=true], [aria-current]))::before {
2467
+ background-color: var(--color-field-surface, oklch(87% 0 0))
2459
2468
  }
2460
2469
 
2461
2470
  /* Track the tab rigidly during continuous reflow (drag-to-resize, window resize) */
@@ -2482,7 +2491,7 @@
2482
2491
  width: 100%;
2483
2492
 
2484
2493
  &:has([type=radio], [type=checkbox]) {
2485
- gap: calc(var(--spacing, 0.25rem) * 2);
2494
+ gap: calc(var(--spacing, 0.25rem) * 2)
2486
2495
  }
2487
2496
  }
2488
2497
 
@@ -2496,7 +2505,7 @@
2496
2505
  & :where(legend) {
2497
2506
  padding: 0 1.5ch;
2498
2507
  font-size: 0.875rem;
2499
- color: var(--color-content-subtle, oklch(55.6% 0 0));
2508
+ color: var(--color-content-subtle, oklch(55.6% 0 0))
2500
2509
  }
2501
2510
  }
2502
2511
 
@@ -3550,7 +3559,7 @@
3550
3559
  /* Entry animation */
3551
3560
  :where(.toast-entry) {
3552
3561
  opacity: 1;
3553
- transform: translateY(0);
3562
+ transform: translateY(0)
3554
3563
  }
3555
3564
 
3556
3565
  /* Exit animation */
@@ -27,19 +27,38 @@ async function ensureManifest() {
27
27
  }
28
28
  }
29
29
 
30
- // Helper to interpolate environment variables
30
+ // Interpolate ${VAR} placeholders in a string against window.env. Only
31
+ // PUBLIC_-prefixed vars reach window.env (the mnfst-run dev server filters
32
+ // .env by that prefix to keep server-side secrets like MANIFEST_API_KEY out
33
+ // of the browser). Misses warn once per name so a missing var doesn't fail
34
+ // silently downstream as an empty URL / endpoint.
35
+ const _warnedMissingEnv = new Set();
31
36
  function interpolateEnvVars(str) {
32
37
  if (typeof str !== 'string') return str;
33
38
  return str.replace(/\$\{([^}]+)\}/g, (match, varName) => {
34
- // Check for environment variables (in browser, these would be set by build process)
35
39
  if (typeof process !== 'undefined' && process.env && process.env[varName]) {
36
40
  return process.env[varName];
37
41
  }
38
- // Check for window.env (common pattern for client-side env vars)
39
42
  if (typeof window !== 'undefined' && window.env && window.env[varName]) {
40
43
  return window.env[varName];
41
44
  }
42
- // Return original if not found
45
+ if (!_warnedMissingEnv.has(varName)) {
46
+ _warnedMissingEnv.add(varName);
47
+ if (!varName.startsWith('PUBLIC_')) {
48
+ console.warn(
49
+ `[Manifest Data] data source references \${${varName}}, but only ` +
50
+ `PUBLIC_-prefixed env vars are injected into window.env by mnfst-run. ` +
51
+ `Rename to PUBLIC_${varName}, hardcode the value, or supply it via ` +
52
+ `<script>window.env = {…}</script>. Leaving placeholder literal.`
53
+ );
54
+ } else {
55
+ console.warn(
56
+ `[Manifest Data] data source references \${${varName}}, but it is not ` +
57
+ `present in window.env. Add ${varName}=… to .env (read by mnfst-run) ` +
58
+ `or set it via <script>window.env = {…}</script>. Leaving placeholder literal.`
59
+ );
60
+ }
61
+ }
43
62
  return match;
44
63
  });
45
64
  }
@@ -1297,7 +1316,11 @@ async function loadYamlLibrary() {
1297
1316
 
1298
1317
  yamlLoadingPromise = new Promise((resolve, reject) => {
1299
1318
  const script = document.createElement('script');
1300
- script.src = 'https://cdn.jsdelivr.net/npm/js-yaml/dist/js-yaml.min.js';
1319
+ // Pinned + Subresource Integrity — a floating version or tampered CDN
1320
+ // file can't inject arbitrary JS into the user's page.
1321
+ script.src = 'https://cdn.jsdelivr.net/npm/js-yaml@4.2.0/dist/js-yaml.min.js';
1322
+ script.integrity = 'sha384-hyhT0yrWXjngXhDa5wpJTU0pt/Djmlx4KtZECqqS6ZHf3ah4fWkFeW/2eWX9cX5J';
1323
+ script.crossOrigin = 'anonymous';
1301
1324
  script.onload = () => {
1302
1325
  if (typeof window.jsyaml !== 'undefined') {
1303
1326
  jsyaml = window.jsyaml;
@@ -1332,7 +1355,11 @@ async function loadCSVParser() {
1332
1355
 
1333
1356
  csvLoadingPromise = new Promise((resolve, reject) => {
1334
1357
  const script = document.createElement('script');
1335
- script.src = 'https://cdn.jsdelivr.net/npm/papaparse@latest/papaparse.min.js';
1358
+ // Pinned + Subresource Integrity — a floating version or tampered CDN
1359
+ // file can't inject arbitrary JS into the user's page.
1360
+ script.src = 'https://cdn.jsdelivr.net/npm/papaparse@5.5.3/papaparse.min.js';
1361
+ script.integrity = 'sha384-Jd2/X5FXVKahwaY2nivvw4LfSTg9idSj8yNXWtT4qef0fHSYr6M7M8bJAfbFYoMc';
1362
+ script.crossOrigin = 'anonymous';
1336
1363
  script.onload = () => {
1337
1364
  if (typeof window.Papa !== 'undefined') {
1338
1365
  papaparse = window.Papa;
@@ -6,19 +6,19 @@
6
6
 
7
7
  @layer base {
8
8
 
9
- @keyframes mnfst-popover-in {
9
+ @keyframes popover-in {
10
10
  from {
11
- transform: scale(.9);
11
+ transform: scale(.9)
12
12
  }
13
13
 
14
14
  to {
15
- transform: none;
15
+ transform: none
16
16
  }
17
17
  }
18
18
 
19
19
  :where([popover]):not(.unstyle):popover-open {
20
20
  display: flex;
21
- animation: mnfst-popover-in .15s ease-in both;
21
+ animation: popover-in .15s ease-in both
22
22
  }
23
23
  }
24
24
 
@@ -66,16 +66,17 @@
66
66
  border-radius: 6px;
67
67
  cursor: pointer;
68
68
  user-select: none;
69
+ transition: none;
69
70
 
70
71
  &:hover {
71
72
  color: var(--color-field-inverse, oklch(43.9% 0 0));
72
73
  text-decoration: inherit;
73
- background-color: var(--color-field-surface, color-mix(oklch(20.5% 0 0) 10%, transparent));
74
+ background-color: var(--color-field-surface, color-mix(oklch(20.5% 0 0) 10%, transparent))
74
75
  }
75
76
 
76
77
  &:active {
77
78
  color: var(--color-field-inverse, oklch(43.9% 0 0));
78
- background-color: var(--color-field-surface, color-mix(oklch(20.5% 0 0) 10%, transparent));
79
+ background-color: var(--color-field-surface, color-mix(oklch(20.5% 0 0) 10%, transparent))
79
80
  }
80
81
 
81
82
  & span,
@@ -92,7 +93,7 @@
92
93
  /* Titles */
93
94
  & small {
94
95
  padding: 0.25rem 0.5rem;
95
- color: var(--color-content-neutral, oklch(43.9% 0 0));
96
+ color: var(--color-content-neutral, oklch(43.9% 0 0))
96
97
  }
97
98
 
98
99
  /* Horizontal rules (offset to ignore menu padding) */
@@ -102,7 +103,7 @@
102
103
  margin-inline-start: calc(0.25rem * -1);
103
104
  margin-top: 0.25rem;
104
105
  margin-bottom: 0.25rem;
105
- background-color: var(--color-line, color-mix(oklch(20.5% 0 0) 10%, transparent));
106
+ background-color: var(--color-line, color-mix(oklch(20.5% 0 0) 10%, transparent))
106
107
  }
107
108
 
108
109
  /* Labels */
@@ -150,7 +151,7 @@
150
151
 
151
152
  /* Dark theme */
152
153
  :where(.dark menu[popover]) :where(li, a, button, label):hover {
153
- background-color: var(--color-field-surface-hover, color-mix(oklch(20.5% 0 0) 15%, transparent));
154
+ background-color: var(--color-field-surface-hover, color-mix(oklch(20.5% 0 0) 15%, transparent))
154
155
  }
155
156
 
156
157
  /* Nested menu alignment */
@@ -161,7 +162,7 @@
161
162
 
162
163
  /* Center alignment */
163
164
  :where(menu.center) {
164
- position-area: center;
165
+ position-area: center
165
166
  }
166
167
 
167
168
  /* Top alignment */
@@ -175,6 +175,17 @@ function initializeDropdownPlugin() {
175
175
  el.style.setProperty('--trigger-anchor', anchorName);
176
176
  menu.style.setProperty('position-anchor', anchorName);
177
177
 
178
+ // Re-point the shared menu at whichever trigger opens it (multi-trigger)
179
+ const pointMenuHere = () => {
180
+ const a = el.style.getPropertyValue('--trigger-anchor');
181
+ if (a) menu.style.setProperty('position-anchor', a);
182
+ };
183
+ if (!modifiers.includes('context') && !el.__mnfstAnchorSwapBound) {
184
+ el.__mnfstAnchorSwapBound = true;
185
+ el.addEventListener('pointerdown', pointMenuHere);
186
+ el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') pointMenuHere(); });
187
+ }
188
+
178
189
  // ----- A11y wiring (WAI-ARIA Menu Button pattern) -----
179
190
  // The trigger needs `aria-haspopup="menu"`, `aria-controls`, and a
180
191
  // dynamic `aria-expanded` that follows the popover's open state.
@@ -192,11 +203,12 @@ function initializeDropdownPlugin() {
192
203
  menu.querySelectorAll('li').forEach((li) => {
193
204
  if (!li.hasAttribute('role')) li.setAttribute('role', 'menuitem');
194
205
  });
195
- // Keep aria-expanded in sync with the popover's state.
206
+ // Keep aria-expanded in sync across every trigger of this menu.
196
207
  if (!menu.__mnfstAriaToggleBound) {
197
208
  menu.__mnfstAriaToggleBound = true;
198
209
  menu.addEventListener('toggle', (e) => {
199
- el.setAttribute('aria-expanded', e.newState === 'open' ? 'true' : 'false');
210
+ const expanded = e.newState === 'open' ? 'true' : 'false';
211
+ document.querySelectorAll(`[aria-controls="${menu.id}"]`).forEach(t => t.setAttribute('aria-expanded', expanded));
200
212
  });
201
213
  }
202
214
  }
@@ -208,6 +220,7 @@ function initializeDropdownPlugin() {
208
220
  clearTimeout(hoverTimeout);
209
221
  clearTimeout(autoCloseTimeout);
210
222
 
223
+ pointMenuHere();
211
224
  menu.showPopover();
212
225
  }
213
226
  };
@@ -244,46 +257,63 @@ function initializeDropdownPlugin() {
244
257
  if (menu.matches(':popover-open')) menu.hidePopover();
245
258
  };
246
259
 
247
- el.addEventListener('contextmenu', (e) => {
248
- e.preventDefault();
249
- e.stopPropagation();
250
-
251
- // Stash the actual right-clicked element so menu items can act on it.
252
- // (e.target is the deepest hit; el is the directive-bearing ancestor.)
253
- menu._triggerEl = e.target.closest('[data-cp-value], [x-data]') || el;
254
- menu._triggerHost = el;
255
-
256
- // Position at cursor, overriding anchor positioning
257
- menu.style.position = 'fixed';
258
- menu.style.positionAnchor = 'unset';
259
- menu.style.positionArea = 'unset';
260
- menu.style.inset = 'auto';
261
- menu.style.left = e.clientX + 'px';
262
- menu.style.top = e.clientY + 'px';
263
- menu.style.margin = '0';
264
-
265
- if (!menu.matches(':popover-open')) menu.showPopover();
266
-
267
- // Adjust if menu overflows viewport
268
- requestAnimationFrame(() => {
269
- const rect = menu.getBoundingClientRect();
270
- if (rect.right > window.innerWidth) menu.style.left = (window.innerWidth - rect.width) + 'px';
271
- if (rect.bottom > window.innerHeight) menu.style.top = (window.innerHeight - rect.height) + 'px';
260
+ // Bind the right-click trigger once per element. Without
261
+ // the guard, reprocessing (router re-walks, re-init) would
262
+ // stack duplicate handlers.
263
+ if (!el.__mnfstContextTriggerBound) {
264
+ el.__mnfstContextTriggerBound = true;
265
+ el.addEventListener('contextmenu', (e) => {
266
+ e.preventDefault();
267
+ e.stopPropagation();
268
+
269
+ // Stash the actual right-clicked element so menu items can act on it.
270
+ // (e.target is the deepest hit; el is the directive-bearing ancestor.)
271
+ menu._triggerEl = e.target.closest('[data-cp-value], [x-data]') || el;
272
+ menu._triggerHost = el;
273
+
274
+ // Position at cursor, overriding anchor positioning
275
+ menu.style.position = 'fixed';
276
+ menu.style.positionAnchor = 'unset';
277
+ menu.style.positionArea = 'unset';
278
+ menu.style.inset = 'auto';
279
+ menu.style.left = e.clientX + 'px';
280
+ menu.style.top = e.clientY + 'px';
281
+ menu.style.margin = '0';
282
+
283
+ if (!menu.matches(':popover-open')) menu.showPopover();
284
+
285
+ // Adjust if menu overflows viewport
286
+ requestAnimationFrame(() => {
287
+ const rect = menu.getBoundingClientRect();
288
+ if (rect.right > window.innerWidth) menu.style.left = (window.innerWidth - rect.width) + 'px';
289
+ if (rect.bottom > window.innerHeight) menu.style.top = (window.innerHeight - rect.height) + 'px';
290
+ });
272
291
  });
273
- });
274
-
275
- // Manual dismiss: click outside or Escape
276
- document.addEventListener('pointerdown', (e) => {
277
- if (menu.matches(':popover-open') && !menu.contains(e.target)) closeContext();
278
- });
279
- document.addEventListener('keydown', (e) => {
280
- if (e.key === 'Escape' && menu.matches(':popover-open')) { closeContext(); el.focus(); }
281
- });
292
+ }
282
293
 
283
- // Close after clicking a menu item
284
- menu.addEventListener('click', (e) => {
285
- if (e.target.closest('li, a, button')) closeContext();
286
- });
294
+ // Document-level dismiss listeners are GLOBAL — re-binding
295
+ // them on every reprocess leaks listeners on `document`.
296
+ // Bind once per menu, tied to an AbortController so they
297
+ // share one removable signal.
298
+ if (!menu.__mnfstContextDismissBound) {
299
+ menu.__mnfstContextDismissBound = true;
300
+ const ctxAbort = new AbortController();
301
+ menu.__mnfstContextAbort = ctxAbort;
302
+ const { signal } = ctxAbort;
303
+
304
+ // Manual dismiss: click outside or Escape
305
+ document.addEventListener('pointerdown', (e) => {
306
+ if (menu.matches(':popover-open') && !menu.contains(e.target)) closeContext();
307
+ }, { signal });
308
+ document.addEventListener('keydown', (e) => {
309
+ if (e.key === 'Escape' && menu.matches(':popover-open')) { closeContext(); el.focus(); }
310
+ }, { signal });
311
+
312
+ // Close after clicking a menu item
313
+ menu.addEventListener('click', (e) => {
314
+ if (e.target.closest('li, a, button')) closeContext();
315
+ }, { signal });
316
+ }
287
317
  }
288
318
 
289
319
  // Add keyboard navigation handling
@@ -16,6 +16,7 @@
16
16
  z-index: 1
17
17
  }
18
18
 
19
+ /* Equal-width children (give the wrapper a width if children need more room) */
19
20
  &.even>* {
20
21
  flex-shrink: initial;
21
22
  width: 100%
@@ -57,21 +58,24 @@
57
58
  /* Concentric corners */
58
59
  &>* {
59
60
  flex-grow: 1;
61
+ width: fit-content;
60
62
  min-height: 0;
61
63
  height: 100%;
64
+ color: var(--color-content-neutral, oklch(43.9% 0 0));
62
65
  background-color: transparent;
63
66
  border-radius: max(calc(var(--radius, 0.5rem) - var(--spacing, 0.25rem) / 2), 0px);
64
67
 
65
68
  &:hover:not(.selected, [aria-selected=true], [aria-current]) {
66
- background-color: color-mix(in oklch, var(--color-field-surface-hover, oklch(37.1% 0 0)) 40%, transparent)
69
+ color: var(--color-field-inverse, oklch(20.5% 0 0));
70
+ background-color: color-mix(in oklch, var(--color-field-surface-hover, oklch(70.8% 0 0)) 40%, transparent)
67
71
  }
68
72
  }
69
73
 
70
74
  /* Selected tab */
71
75
  &>:is(.selected, [aria-selected=true], [aria-current]) {
72
- color: var(--color-page, oklch(98.5% 0 0));
73
- background-color: var(--color-content-stark, oklch(20.5% 0 0));
74
- box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
76
+ color: var(--color-field-inverse, oklch(20.5% 0 0));
77
+ background-color: var(--color-field-surface, oklch(87% 0 0));
78
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)
75
79
  }
76
80
 
77
81
  /* Background slider for selected tab) */
@@ -82,7 +86,7 @@
82
86
  anchor-name: --selected-tab;
83
87
  --co-anchor: --selected-tab;
84
88
  background-color: transparent;
85
- box-shadow: none;
89
+ box-shadow: none
86
90
  }
87
91
 
88
92
  &:has(>:is(.selected, [aria-selected=true], [aria-current]))::before {
@@ -94,10 +98,14 @@
94
98
  left: anchor(left);
95
99
  width: anchor-size(width);
96
100
  height: anchor-size(height);
97
- background-color: var(--color-content-stark, oklch(20.5% 0 0));
101
+ background-color: var(--color-page, oklch(98.5% 0 0));
98
102
  border-radius: max(calc(var(--radius, 0.5rem) - var(--spacing, 0.25rem) / 2), 0px);
99
103
  box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
100
- transition: var(--tab-slide-transition, all 0.15s ease-in-out);
104
+ transition: var(--tab-slide-transition, all 0.15s ease-in-out)
105
+ }
106
+
107
+ .dark &:has(>:is(.selected, [aria-selected=true], [aria-current]))::before {
108
+ background-color: var(--color-field-surface, oklch(87% 0 0))
101
109
  }
102
110
 
103
111
  /* Track the tab rigidly during continuous reflow (drag-to-resize, window resize) */
@@ -124,7 +132,7 @@
124
132
  width: 100%;
125
133
 
126
134
  &:has([type=radio], [type=checkbox]) {
127
- gap: calc(var(--spacing, 0.25rem) * 2);
135
+ gap: calc(var(--spacing, 0.25rem) * 2)
128
136
  }
129
137
  }
130
138
 
@@ -138,7 +146,7 @@
138
146
  & :where(legend) {
139
147
  padding: 0 1.5ch;
140
148
  font-size: 0.875rem;
141
- color: var(--color-content-subtle, oklch(55.6% 0 0));
149
+ color: var(--color-content-subtle, oklch(55.6% 0 0))
142
150
  }
143
151
  }
144
152
 
@@ -7,24 +7,24 @@
7
7
  "manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
8
8
  "manifest.colorpicker.js": "sha384-Wqz0ZIbeIi7KarqqqSLsQk+7E/fMaKhb32hrq5/eWzX1yjqMrpPZKH8y+jZ3mfg+",
9
9
  "manifest.components.js": "sha384-73CB1A+LAGfNexkd7aT69APFSHMzix8irse9uzOkYehHHio4px3oR8JHJeaMH+jI",
10
- "manifest.data.js": "sha384-PZiaRsDV4nnRGvmT8y2jEZV+BolcEZGJXbMJU75YqWGXI8b3y/L93zhhllovf4Go",
10
+ "manifest.data.js": "sha384-XDL6cgiej9q+11HT8cp/feCcq1ir0rq3FZYgWngW627DPQOSdmMQVy4sDK+j1qAm",
11
11
  "manifest.datepicker.js": "sha384-NEb/H4vuR3CFtRcodHsm3jJjrcYW2JMpDlQKlgwTrzpMMTcDkFKYXzAYJD0gZ7Ov",
12
- "manifest.dropdowns.js": "sha384-9vRdtkgheqvl/2PEaAFJhQ/NCqrRO8NFOjCBNKSu1CDA+RXkIbUNBIZRST96asLe",
12
+ "manifest.dropdowns.js": "sha384-a2j9Z1LWolyZANlfeBc1aIFQ0kf0UDeOZ8TetulYbpRS6T/zaD/OWSMGvz+gRehX",
13
13
  "manifest.export.js": "sha384-RsTGzsPCBw3yO4+TdAGd4F+o3FnzUNlqnMBqtnn/kUfv7axpzRdPc2AnsExV/93c",
14
14
  "manifest.icons.js": "sha384-uOkboYrovjCpl22eey3Jaxpey+pOnot5NDnRRumcRxiR7IOVaRh1i20gYnWXR5dW",
15
15
  "manifest.localization.js": "sha384-M3HRb2Ma8PemfFeqq9rgWgw/+Vdb/8d5LGW2MFbVsXaWUPqr/mPuxWtl5Pv8wolL",
16
- "manifest.markdown.js": "sha384-I3fa8r0OU1eHaD/1EmlzRd31HaezxisSABApJohZ74o9MrO4EA286WQChvmGUw0c",
16
+ "manifest.markdown.js": "sha384-IrtsTsNgCsKmrNQgnvrh5ETHdnrrsJeOg8r6zD5zfxEgXVUu+HZ6ai7e2iM9fXIM",
17
17
  "manifest.payments.js": "sha384-Ng5y5hZfMqrSLo1AbQ8ucOni/BaaYsbEj16WW7rwsgRKEaflE+6lQRoZSlLA4PMd",
18
18
  "manifest.resize.js": "sha384-S3v4RAJoA77LUH6MddYG9bx//dZX//up7XWeH69Ql2ge3bVHgC6mR/CJBe6y4nQI",
19
19
  "manifest.router.js": "sha384-Kpl5k/pWOrMJhkgFZ9UAtGXuH7FDEv24WGMACpNqV8DW5ZjcbC67SnVY42qUSn3X",
20
20
  "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
21
21
  "manifest.status.js": "sha384-7cEl+Nh729ncqy5GtRYMqo5R4d257QPsoFm/hx9Znp9uV/D85pjxVzQ1fhiD+sO6",
22
- "manifest.svg.js": "sha384-wPfasscODIO6pyMFNIqZ7/C12cR4QYDnVl/wYNhwBO7gFNBGrhimNzL18VTpMPIL",
22
+ "manifest.svg.js": "sha384-yAUSl5sTwyMSerR0zyuimuRNzySvKaGN9KuEBgu81MOjF1n5Y8PMtDfxUO6uHMHu",
23
23
  "manifest.tabs.js": "sha384-7Kb1EHIbqh1NOl8J1NMp087lcF1gVmwm55QNM3s7JamV6sYiH/WZbdnknAZFtsfW",
24
24
  "manifest.tailwind.js": "sha384-aHLvl2oSuUgy06VaBqhhByn5wWxqvnqxw6KCwehakKUS00F/s/Nb62umeASS6Y4P",
25
25
  "manifest.toasts.js": "sha384-ytd5rDbax/Ou9z23uedFXPZbxDPsk2E/pxCTq4WLvfv+os1qTI6kELp0kPp07g24",
26
- "manifest.tooltips.js": "sha384-GaBa+oORH0y6LC0NyLy+RjAyh1pRglud0U5oZKZAQEaYSsrm/G0ir+RUNSnW0Geb",
26
+ "manifest.tooltips.js": "sha384-ADzAx9D0HWq2b46mvNG05iOwPmEWdiFZNpEOXONSbBxs4xj1B/bzNL7S3x2R9cS1",
27
27
  "manifest.url.parameters.js": "sha384-FIufiClqDx1rJpU/QUc9z/D43qClQ6Qm8rBahipbJl9BDHUvhrOsUDegmTWW7Tuf",
28
- "manifest.utilities.js": "sha384-o6LAk/jJGclXUpC6nzReSRCaAVxaMrgAMhCNYCLz/H4HNdITpmmWk25Fzag4GSHI",
29
- "manifest.js": "sha384-LosPV9wWLItaJan0+SjFzLGe3W2kVRORdy+gpr6YkQ7AKJkasmtAf6rbbvk3G37O"
28
+ "manifest.utilities.js": "sha384-Hl2tlhHgIJLEr3CFiEq79nlWCgx6nSVJCyThPCZi3wOFBLv/KXm/VrIZ0vRZqBi7",
29
+ "manifest.js": "sha384-vc0GN2LMS0zDdT2Oj4a7NwyR498zKDgyiLiBl4VjDFJB+XDMcrdBRBBY5yn7ixXK"
30
30
  }
package/lib/manifest.js CHANGED
@@ -586,14 +586,38 @@
586
586
  // the loader rather than borrowed from the data plugin because the
587
587
  // data plugin's script may not have finished executing yet at the
588
588
  // point we cache the manifest. window.env is populated by either
589
- // the mnfst-run dev server (which reads .env at startup) or a
590
- // developer-supplied <script>window.env = {…}</script> block.
589
+ // the mnfst-run dev server (which reads PUBLIC_-prefixed vars from
590
+ // .env at startup) or a developer-supplied
591
+ // <script>window.env = {…}</script> block.
592
+ //
593
+ // Misses are warned, not silently dropped: a missing var almost
594
+ // always means the dev forgot the PUBLIC_ prefix or hasn't set the
595
+ // var at all, and an empty substitution downstream (e.g. an empty
596
+ // API URL) tends to fail far from the cause.
597
+ const warnedMissingEnv = new Set();
591
598
  const interpolateManifestEnv = (obj) => {
592
599
  if (obj === null || typeof obj !== 'object') return;
593
600
  const subst = (str) => str.replace(/\$\{([^}]+)\}/g, (m, name) => {
594
601
  if (typeof window !== 'undefined' && window.env && window.env[name] !== undefined) {
595
602
  return window.env[name];
596
603
  }
604
+ if (!warnedMissingEnv.has(name)) {
605
+ warnedMissingEnv.add(name);
606
+ if (!name.startsWith('PUBLIC_')) {
607
+ console.warn(
608
+ `[Manifest] manifest.json references \${${name}}, but only PUBLIC_-prefixed ` +
609
+ `env vars are injected into window.env by mnfst-run. Rename to ` +
610
+ `PUBLIC_${name}, hardcode the value, or supply it via ` +
611
+ `<script>window.env = {…}</script>. Leaving placeholder literal.`
612
+ );
613
+ } else {
614
+ console.warn(
615
+ `[Manifest] manifest.json references \${${name}}, but it is not present ` +
616
+ `in window.env. Add ${name}=… to .env (read by mnfst-run) or ` +
617
+ `set it via <script>window.env = {…}</script>. Leaving placeholder literal.`
618
+ );
619
+ }
620
+ }
597
621
  return m;
598
622
  });
599
623
  const walk = (o) => {
@@ -43,7 +43,13 @@ if (!window.ManifestDOMPurify) {
43
43
  if (this._promise) return this._promise;
44
44
  this._promise = new Promise((resolve, reject) => {
45
45
  const script = document.createElement('script');
46
- script.src = 'https://cdn.jsdelivr.net/npm/dompurify@latest/dist/purify.min.js';
46
+ // Pinned + Subresource Integrity: a moving `@latest` (or a
47
+ // tampered CDN file) would otherwise run arbitrary JS in the
48
+ // user's page. The browser rejects the script if the bytes don't
49
+ // match the hash. Bump version AND integrity together.
50
+ script.src = 'https://cdn.jsdelivr.net/npm/dompurify@3.4.10/dist/purify.min.js';
51
+ script.integrity = 'sha384-eguRoJERj8ghOpzO//Rl7+ScQsQIR1cH+ajll7+fG+IpbNPlkZsQn9h8ccr+wPXx';
52
+ script.crossOrigin = 'anonymous';
47
53
  script.onload = () => {
48
54
  if (typeof window.DOMPurify !== 'undefined') {
49
55
  resolve(window.DOMPurify);
@@ -96,7 +102,11 @@ async function loadMarkedJS() {
96
102
 
97
103
  markedPromise = new Promise((resolve, reject) => {
98
104
  const script = document.createElement('script');
99
- script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
105
+ // Pinned + Subresource Integrity (see DOMPurify loader above) — a
106
+ // floating version or tampered CDN file can't inject arbitrary JS.
107
+ script.src = 'https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js';
108
+ script.integrity = 'sha384-948ahk4ZmxYVYOc+rxN1H2gM1EJ2Duhp7uHtZ4WSLkV4Vtx5MUqnV+l7u9B+jFv+';
109
+ script.crossOrigin = 'anonymous';
100
110
  script.onload = () => {
101
111
  // Initialize marked.js
102
112
  if (typeof marked !== 'undefined') {