mnfst 0.5.165 → 0.5.167

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
@@ -945,6 +945,217 @@
945
945
  }
946
946
  }
947
947
 
948
+ /* Manifest Chat */
949
+
950
+ @layer utilities {
951
+
952
+ :where(.chat-wrapper) {
953
+ display: flex;
954
+ flex-direction: column;
955
+ gap: 1rem;
956
+ width: 100%;
957
+ height: 100%;
958
+ box-shadow: var(--shadow);
959
+ overflow: hidden;
960
+
961
+ /* Chat log scroll container. column-reverse pins the view to the
962
+ bottom natively: new messages and a growing composer never hide
963
+ the latest message, and reading history isn't yanked on updates. */
964
+ :where([role="log"]) {
965
+ display: flex;
966
+ flex-direction: column-reverse;
967
+ flex: 1;
968
+ font-size: 0.875rem;
969
+ overflow-y: auto;
970
+
971
+ /* Message column */
972
+ > :where(div) {
973
+ display: flex;
974
+ flex-direction: column;
975
+ justify-content: flex-end;
976
+ gap: 2rem;
977
+ min-height: 100%;
978
+ width: calc(var(--spacing-content-width, 74rem) - 4rem);
979
+ max-width: 100%;
980
+ margin-inline-start: auto;
981
+ margin-inline-end: auto;
982
+ padding-inline-start: 1rem;
983
+ padding-inline-end: 1rem;
984
+ padding-block-start: 1.5rem;
985
+ padding-block-end: 5rem;
986
+ }
987
+
988
+ /* Messages */
989
+ :where(div div) {
990
+
991
+ /* Markdown spacing */
992
+ :where(p) {
993
+ margin-block: 0.5rem;
994
+ }
995
+
996
+ :where(p:first-child) {
997
+ margin-block-start: 0;
998
+ }
999
+
1000
+ :where(p:last-child) {
1001
+ margin-block-end: 0;
1002
+ }
1003
+ }
1004
+
1005
+ /* Own messages */
1006
+ :where([data-own]) {
1007
+ width: fit-content;
1008
+ max-width: calc(var(--spacing-content-width, 74rem) - 10rem);
1009
+ margin-inline-start: auto;
1010
+ padding-block: 0.5rem;
1011
+ padding-inline: 0.75rem;
1012
+ background: color-mix(in oklch, var(--color-surface-2) 50%, transparent);
1013
+ border-radius: var(--radius);
1014
+ }
1015
+
1016
+ /* Attachments */
1017
+ :where(img, video) {
1018
+ display: block;
1019
+ max-width: 20rem;
1020
+ max-height: 15rem;
1021
+ margin-block: 0.5rem;
1022
+ border-radius: var(--radius);
1023
+ }
1024
+
1025
+ :where(audio) {
1026
+ display: block;
1027
+ width: 18rem;
1028
+ max-width: 100%;
1029
+ margin-block: 0.5rem;
1030
+ }
1031
+
1032
+ :where(a[download]) {
1033
+ display: inline-flex;
1034
+ align-items: center;
1035
+ gap: 0.4rem;
1036
+ margin-block: 0.5rem;
1037
+ padding-block: 0.35rem;
1038
+ padding-inline: 0.6rem;
1039
+ background: var(--color-surface-2);
1040
+ border-radius: var(--radius);
1041
+ }
1042
+ }
1043
+
1044
+ /* Chat input */
1045
+ :where(form) {
1046
+ position: relative;
1047
+ width: var(--spacing-content-width, 74rem);
1048
+ max-width: 100%;
1049
+ margin-inline-start: auto;
1050
+ margin-inline-end: auto;
1051
+
1052
+ :where(textarea) {
1053
+ padding-inline-end: 2.5rem;
1054
+ field-sizing: content;
1055
+ resize: none;
1056
+ }
1057
+
1058
+ &:has(button[data-action="attach"]) :where(textarea) {
1059
+ padding-inline-start: 2.5rem;
1060
+ }
1061
+
1062
+ /* Submit button */
1063
+ :where(button[type="submit"]) {
1064
+ position: absolute;
1065
+ bottom: var(--spacing-field-padding, 0.625rem);
1066
+ inset-inline-end: calc(var(--spacing-field-padding, 0.625rem) * 1.3);
1067
+ }
1068
+
1069
+ /* Toolbar buttons (data-action) — attach, formatting, voice, … */
1070
+ :where(button[data-action="attach"]) {
1071
+ position: absolute;
1072
+ bottom: var(--spacing-field-padding, 0.625rem);
1073
+ inset-inline-start: calc(var(--spacing-field-padding, 0.625rem) * 1.3);
1074
+ }
1075
+
1076
+ /* Composer variant — the form itself is the field (.textarea):
1077
+ [ attachment thumbnails ]
1078
+ [ attach | growing textarea | send ] */
1079
+ &:where(.textarea) {
1080
+ display: grid;
1081
+ grid-template-columns: auto 1fr auto;
1082
+ align-items: end;
1083
+ gap: 0.25rem;
1084
+ padding: 0.375rem;
1085
+
1086
+ /* Buttons sit in-flow at the row's bottom corners */
1087
+ :where(button[type="submit"], button[data-action]) {
1088
+ position: static;
1089
+ }
1090
+
1091
+ /* The inner textarea is a bare well inside the field */
1092
+ :where(textarea) {
1093
+ padding-inline: 0.25rem;
1094
+ padding-block: 0.45rem;
1095
+
1096
+ &,
1097
+ &:hover,
1098
+ &:active,
1099
+ &:focus-visible {
1100
+ background: transparent;
1101
+ }
1102
+ }
1103
+
1104
+ /* Attachment thumbnails — removable, X-scrollable */
1105
+ :where(menu) {
1106
+ grid-column: 1 / -1;
1107
+ display: flex;
1108
+ gap: 0.5rem;
1109
+ margin: 0;
1110
+ padding: 0.375rem 0.25rem 0.125rem;
1111
+ list-style: none;
1112
+ overflow-x: auto;
1113
+
1114
+ :where(li) {
1115
+ position: relative;
1116
+ flex: 0 0 auto;
1117
+ }
1118
+
1119
+ :where(img) {
1120
+ display: block;
1121
+ width: 3.5rem;
1122
+ height: 3.5rem;
1123
+ object-fit: cover;
1124
+ border-radius: calc(var(--radius, 0.5rem) * 0.75);
1125
+ }
1126
+
1127
+ /* Non-image attachments — file chip */
1128
+ :where(li > span) {
1129
+ display: flex;
1130
+ align-items: center;
1131
+ gap: 0.3rem;
1132
+ height: 3.5rem;
1133
+ padding-inline: 0.6rem;
1134
+ font-size: 0.75rem;
1135
+ white-space: nowrap;
1136
+ background: var(--color-surface-2);
1137
+ border-radius: calc(var(--radius, 0.5rem) * 0.75);
1138
+ }
1139
+
1140
+ /* Remove button — revealed on hover/focus */
1141
+ :where(button) {
1142
+ position: absolute;
1143
+ inset-block-start: -0.35rem;
1144
+ inset-inline-end: -0.35rem;
1145
+ opacity: 0;
1146
+ transition: var(--transition, all .05s ease-in-out);
1147
+ }
1148
+
1149
+ :where(li:hover, li:focus-within) :where(button) {
1150
+ opacity: 1;
1151
+ }
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ }
1158
+
948
1159
  /* Manifest Checkboxes */
949
1160
 
950
1161
  @layer components {
@@ -1582,16 +1793,25 @@
1582
1793
  gap: var(--spacing, 0.25rem);
1583
1794
  max-width: 100%;
1584
1795
  height: calc(var(--spacing-field-height, 2.25rem) - var(--spacing, 0.25rem) * 2.5);
1796
+ margin: 0;
1797
+ padding: 0;
1585
1798
  padding-inline-start: calc(var(--spacing, 0.25rem) * 2);
1586
1799
  font-size: 0.875rem;
1800
+ line-height: normal;
1587
1801
  color: var(--color-content-stark, oklch(43.9% 0 0));
1588
1802
  background-color: var(--color-popover-surface, oklch(100% 0 0));
1803
+ border: 0;
1589
1804
  border-radius: calc(var(--radius, 0.5rem) * 0.7);
1590
1805
  overflow: hidden;
1591
1806
  user-select: none;
1592
1807
 
1593
1808
  /* Label */
1594
1809
  &>span {
1810
+ min-width: 0;
1811
+ margin: 0;
1812
+ padding: 0;
1813
+ border: 0;
1814
+ font: inherit;
1595
1815
  overflow: hidden;
1596
1816
  text-overflow: ellipsis;
1597
1817
  white-space: nowrap
@@ -1602,15 +1822,23 @@
1602
1822
  display: inline-flex;
1603
1823
  align-items: center;
1604
1824
  justify-content: center;
1825
+ box-sizing: border-box;
1826
+ flex: none;
1827
+ width: auto !important;
1605
1828
  min-width: 0;
1829
+ max-width: none;
1606
1830
  height: 100%;
1831
+ min-height: 0;
1607
1832
  aspect-ratio: 1 / 1;
1833
+ margin: 0;
1608
1834
  padding: 0;
1835
+ border: 0;
1836
+ border-radius: 0;
1609
1837
  font-size: 1em;
1610
1838
  line-height: 1;
1611
1839
  color: var(--color-content-neutral, oklch(43.9% 0 0));
1612
1840
  background: transparent;
1613
- border-radius: 0;
1841
+ box-shadow: none;
1614
1842
  cursor: pointer;
1615
1843
 
1616
1844
  &:hover {
@@ -3107,7 +3335,7 @@
3107
3335
 
3108
3336
  @layer components {
3109
3337
 
3110
- :where(input:not([type=range], [type=color]), textarea, label:has([type=search], [type=file]), .label:has([type=search], [type=file])):not(.unstyle) {
3338
+ :where(input:not([type=range], [type=color]), textarea, .textarea, label:has([type=search], [type=file]), .label:has([type=search], [type=file])):not(.unstyle) {
3111
3339
  width: 100%;
3112
3340
  max-width: 100%;
3113
3341
  color: var(--color-field-inverse, oklch(43.9% 0 0));
@@ -3226,7 +3454,7 @@
3226
3454
  }
3227
3455
 
3228
3456
  /* Textarea-specific styles */
3229
- :where(textarea):not(.unstyle) {
3457
+ :where(textarea, .textarea):not(.unstyle) {
3230
3458
  display: block;
3231
3459
  height: auto;
3232
3460
  padding: var(--spacing-field-padding, 0.625rem) calc(var(--spacing-field-padding, 0.625rem) * 1.3)
@@ -2,7 +2,7 @@
2
2
 
3
3
  @layer components {
4
4
 
5
- :where(input:not([type=range], [type=color]), textarea, label:has([type=search], [type=file]), .label:has([type=search], [type=file])):not(.unstyle) {
5
+ :where(input:not([type=range], [type=color]), textarea, .textarea, label:has([type=search], [type=file]), .label:has([type=search], [type=file])):not(.unstyle) {
6
6
  width: 100%;
7
7
  max-width: 100%;
8
8
  color: var(--color-field-inverse, oklch(43.9% 0 0));
@@ -121,7 +121,7 @@
121
121
  }
122
122
 
123
123
  /* Textarea-specific styles */
124
- :where(textarea):not(.unstyle) {
124
+ :where(textarea, .textarea):not(.unstyle) {
125
125
  display: block;
126
126
  height: auto;
127
127
  padding: var(--spacing-field-padding, 0.625rem) calc(var(--spacing-field-padding, 0.625rem) * 1.3)
@@ -1,9 +1,9 @@
1
1
  {
2
- "manifest.appwrite.auth.js": "sha384-+4T3vdzf2NsDrSpatkIqg7cO2HUnY3tNNwSgfyXCCU0NZ7K9eqX2dzhDJIlochlz",
2
+ "manifest.appwrite.auth.js": "sha384-IFEn8+AMicdf5DkS+ytkPls8CpW2kDelRWrL3M61cu1HqaTJvHmABQUPgIMJC/tc",
3
3
  "manifest.appwrite.data.js": "sha384-KFKVo5TQESLJB7wgk+C8Zb/sAl43QAMdioWW3tQRWYq5umfdpGCZ5Hld9y9f6/Np",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
5
  "manifest.charts.js": "sha384-wTQlB3hJ9V9lmisCgm8l6YkpJV+nA4+Vci3dI0x1nBtWnS9TFr+eXaJelP7AusyD",
6
- "manifest.chat.js": "sha384-HDyzPvNxqUOKVcLC3HNSFd5w83Flgm58LHy1BDLB9ejUsbYHAmr34G4X3FwKXokW",
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",
@@ -29,5 +29,5 @@
29
29
  "manifest.url.parameters.js": "sha384-FIufiClqDx1rJpU/QUc9z/D43qClQ6Qm8rBahipbJl9BDHUvhrOsUDegmTWW7Tuf",
30
30
  "manifest.utilities.js": "sha384-MtzNaVOrgyepjlY5nz38pAJk67yiFPU1H5WoPNfcNbUZTXy+oin1lrtKCIcobEOJ",
31
31
  "manifest.virtual.js": "sha384-c194pXD0Ld48QJ+HPVNibvbn54/ETJRuYhUOWesEBjtOIQcQMIKo0DnCyQMvDlLg",
32
- "manifest.js": "sha384-VshJgT9QAxsLpwNmQZfrxw/EAxoyLIklo0DsmhxLhjVmmT70fLmn5eNRuq98o6zs"
32
+ "manifest.js": "sha384-RusFaByZu31LkLZCAhjgtJC8iWksvmjTKS8D5Xj9/4N7B/Xr4DDyFGmiL23kajgK"
33
33
  }
package/lib/manifest.js CHANGED
@@ -202,11 +202,29 @@
202
202
 
203
203
  // Configuration
204
204
  const DEFAULT_VERSION = 'latest';
205
- const ALPINE_CDN_URL = 'https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js';
205
+
206
+ // CDN fallback chain (first-party first). Each origin serves the npm scheme
207
+ // `<origin>/<pkg>@<version>/<path>`. Override order with `data-cdn`
208
+ // (comma-separated origins).
209
+ let CDN_HOSTS = [
210
+ 'https://cdn.manifestx.dev/npm',
211
+ 'https://cdn.jsdelivr.net/npm',
212
+ 'https://unpkg.com'
213
+ ];
214
+ function setCdnHosts(value) {
215
+ if (!value) return;
216
+ const hosts = value.split(',').map(s => s.trim().replace(/\/$/, '')).filter(Boolean);
217
+ if (hosts.length) CDN_HOSTS = hosts;
218
+ }
219
+
220
+ // unpkg serves packages as published (no auto-minify); mnfst ships unminified .js.
221
+ function hostFile(host, file) {
222
+ return host.includes('unpkg.com') ? file.replace(/\.min\.js$/, '.js') : file;
223
+ }
206
224
 
207
225
  // Get base URL for a given version
208
- function getBaseUrl(version = DEFAULT_VERSION) {
209
- return `https://cdn.jsdelivr.net/npm/mnfst@${version}/lib`;
226
+ function getBaseUrl(version = DEFAULT_VERSION, host = CDN_HOSTS[0]) {
227
+ return `${host}/mnfst@${version}/lib`;
210
228
  }
211
229
 
212
230
  // Available core plugins (auto-loaded if no data-plugins specified)
@@ -279,26 +297,29 @@
279
297
  });
280
298
  }
281
299
 
282
- // Plugin URL: `data-plugin-base` override → unminified `.js`; else the
283
- // jsDelivr CDN as `.min.js`.
300
+ // Plugin URLs: `data-plugin-base` override → single unminified `.js` (no
301
+ // fallback an explicit base, e.g. local dev, must fail loudly); else one
302
+ // candidate per CDN host, tried in order.
284
303
  let _pluginBase = null;
285
304
  function setPluginBase(b) { _pluginBase = b || null; }
286
- function getPluginUrl(pluginName, version = DEFAULT_VERSION) {
305
+ function getPluginUrlCandidates(pluginName, version = DEFAULT_VERSION) {
287
306
  // Hyphenated API name → dotted file name (`appwrite-auth` → appwrite.auth).
288
307
  const fileName = pluginName.replace(/-/g, '.');
289
308
  if (_pluginBase) {
290
309
  const base = _pluginBase.replace(/\/$/, '');
291
- return `${base}/manifest.${fileName}.js`;
310
+ return [`${base}/manifest.${fileName}.js`];
292
311
  }
293
- const base = getBaseUrl(version);
294
- return `${base}/manifest.${fileName}.min.js`;
312
+ return CDN_HOSTS.map(h => `${getBaseUrl(version, h)}/${hostFile(h, `manifest.${fileName}.min.js`)}`);
313
+ }
314
+ function getPluginUrl(pluginName, version = DEFAULT_VERSION) {
315
+ return getPluginUrlCandidates(pluginName, version)[0];
295
316
  }
296
317
 
297
- // Resolve Alpine CDN URL from a data-alpine value (version tag or full URL)
298
- function resolveAlpineUrl(dataAlpine) {
299
- if (!dataAlpine) return ALPINE_CDN_URL;
300
- if (dataAlpine.startsWith('http')) return dataAlpine;
301
- return `https://cdn.jsdelivr.net/npm/alpinejs@${dataAlpine}/dist/cdn.min.js`;
318
+ // Alpine URL candidates from a data-alpine value (version tag or full URL)
319
+ function alpineUrlCandidates(dataAlpine) {
320
+ if (dataAlpine && dataAlpine.startsWith('http')) return [dataAlpine];
321
+ const v = dataAlpine || '3';
322
+ return CDN_HOSTS.map(h => `${h}/alpinejs@${v}/dist/cdn.min.js`);
302
323
  }
303
324
 
304
325
  // Has DOMContentLoaded fired? readyState can't tell ('interactive' spans
@@ -331,7 +352,7 @@
331
352
  // fire `alpine:init` before the page's registrations exist. DCL fires only
332
353
  // after every deferred script runs, making the order deterministic (and cold
333
354
  // cache is already past DCL, so no added delay).
334
- function loadAlpine(alpineUrl = ALPINE_CDN_URL) {
355
+ function loadAlpine(alpineUrls) {
335
356
  whenDomReady(() => {
336
357
  if (window.Alpine) {
337
358
  return;
@@ -343,24 +364,30 @@
343
364
  return;
344
365
  }
345
366
 
346
- const script = document.createElement('script');
347
- script.src = alpineUrl;
348
- // No `defer` — past DCL, so execute as soon as it arrives.
349
- document.head.appendChild(script);
367
+ // Past DCL, so each candidate executes as soon as it arrives; fall
368
+ // through the CDN chain on error.
369
+ (async () => {
370
+ for (const url of alpineUrls) {
371
+ try {
372
+ return await injectScript(url);
373
+ } catch (_) {
374
+ console.warn(`[Manifest Loader] Alpine failed from ${url} — trying fallback CDN`);
375
+ }
376
+ }
377
+ console.error('[Manifest Loader] Alpine.js failed to load from all CDNs.');
378
+ })();
350
379
  });
351
380
  }
352
381
 
353
- // Add a script tag to the head and wait for it to load and execute
354
- function addScript(pluginName, version = DEFAULT_VERSION) {
382
+ // Inject one script URL and wait for it to load and execute
383
+ function injectScript(url) {
355
384
  return new Promise((resolve, reject) => {
356
- const url = getPluginUrl(pluginName, version);
357
-
358
385
  // Skip if script with same src already in DOM (e.g. prerendered HTML or second loader run)
359
386
  const existing = document.querySelector(`script[src="${url}"]`);
360
387
  if (existing) {
361
388
  if (existing.complete) return resolve();
362
389
  existing.addEventListener('load', () => resolve());
363
- existing.addEventListener('error', () => reject(new Error(`Failed to load ${pluginName} from ${url}`)));
390
+ existing.addEventListener('error', () => reject(new Error(`Failed to load ${url}`)));
364
391
  return;
365
392
  }
366
393
 
@@ -368,11 +395,28 @@
368
395
  script.src = url;
369
396
  script.async = false; // Ensure scripts execute in order
370
397
  script.onload = () => resolve();
371
- script.onerror = () => reject(new Error(`Failed to load ${pluginName} from ${url}`));
398
+ script.onerror = () => { script.remove(); reject(new Error(`Failed to load ${url}`)); };
372
399
  document.head.appendChild(script);
373
400
  });
374
401
  }
375
402
 
403
+ // Load a plugin, falling through the CDN chain on error. A fallback insert
404
+ // lands after already-inserted scripts, so strict cross-plugin execution
405
+ // order is traded for availability in the (already degraded) fallback case.
406
+ async function addScript(pluginName, version = DEFAULT_VERSION) {
407
+ const urls = getPluginUrlCandidates(pluginName, version);
408
+ let lastErr = null;
409
+ for (const url of urls) {
410
+ try {
411
+ return await injectScript(url);
412
+ } catch (e) {
413
+ lastErr = e;
414
+ if (urls.length > 1) console.warn(`[Manifest Loader] ${url} failed — trying fallback CDN`);
415
+ }
416
+ }
417
+ throw lastErr || new Error(`Failed to load ${pluginName}`);
418
+ }
419
+
376
420
  // Resolve plugin dependencies (auto-inject required dependencies)
377
421
  function resolveDependencies(pluginList) {
378
422
  const resolved = [];
@@ -435,6 +479,15 @@
435
479
  return [];
436
480
  }
437
481
 
482
+ // Detect the chat plugin from manifest.json content.
483
+ // Opt-in / auto-loaded when an `ai` (or `chat`) config block is present.
484
+ function detectChatPlugins(manifest) {
485
+ if (!manifest || typeof manifest !== 'object') return [];
486
+ if ((manifest.ai && typeof manifest.ai === 'object') ||
487
+ (manifest.chat && typeof manifest.chat === 'object')) return ['chat'];
488
+ return [];
489
+ }
490
+
438
491
  // Parse data attributes
439
492
  function parseDataAttributes() {
440
493
  // Try to get current script first, then fall back to querySelector
@@ -455,6 +508,8 @@
455
508
  // Override: resolve plugin URLs against this base (dir serving
456
509
  // `manifest.<name>.js`, relative or absolute) instead of the CDN.
457
510
  const pluginBase = script.getAttribute('data-plugin-base');
511
+ // Override: custom CDN fallback chain (comma-separated origins).
512
+ const cdn = script.getAttribute('data-cdn');
458
513
 
459
514
  let pluginList = [];
460
515
  const deriveFromManifest = !plugins;
@@ -483,37 +538,29 @@
483
538
  version,
484
539
  alpine,
485
540
  pluginBase,
541
+ cdn,
486
542
  };
487
543
  }
488
544
 
489
- // Load custom Tailwind CDN script
490
- function loadTailwind(version = DEFAULT_VERSION) {
491
- return new Promise((resolve, reject) => {
492
- const base = getBaseUrl(version);
493
- const tailwindUrl = `${base}/manifest.tailwind.min.js`;
494
-
495
- // Check if already loaded
496
- const existing = document.querySelector(`script[src="${tailwindUrl}"]`);
497
- if (existing && existing.complete) {
498
- return resolve();
545
+ // Load custom Tailwind CDN script, falling through the CDN chain on error
546
+ async function loadTailwind(version = DEFAULT_VERSION) {
547
+ const urls = CDN_HOSTS.map(h => `${getBaseUrl(version, h)}/${hostFile(h, 'manifest.tailwind.min.js')}`);
548
+ let lastErr = null;
549
+ for (const url of urls) {
550
+ try {
551
+ return await injectScript(url);
552
+ } catch (e) {
553
+ lastErr = e;
499
554
  }
500
-
501
- const script = document.createElement('script');
502
- script.src = tailwindUrl;
503
- script.async = false;
504
- script.onload = () => resolve();
505
- script.onerror = () => {
506
- console.warn(`[Manifest Loader] Tailwind plugin not yet published to CDN. Load it directly: <script src="/scripts/tailwind.v4.3.1.js"></script>`);
507
- reject(new Error(`Tailwind plugin not available from CDN. Load it directly from your project.`));
508
- };
509
- document.head.appendChild(script);
510
- });
555
+ }
556
+ console.warn(`[Manifest Loader] Tailwind plugin not available from any CDN. Load it directly: <script src="/scripts/tailwind.v4.3.1.js"></script>`);
557
+ throw lastErr || new Error(`Tailwind plugin not available from CDN.`);
511
558
  }
512
559
 
513
560
  // Expose API
514
561
  window.Manifest = {
515
562
  loadPlugin: function (pluginName, version = DEFAULT_VERSION) {
516
- const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, 'payments'];
563
+ const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, 'payments', 'chat'];
517
564
  if (!allPlugins.includes(pluginName)) {
518
565
  console.warn(`[Manifest Loader] Unknown plugin: ${pluginName}`);
519
566
  return Promise.reject(new Error(`Unknown plugin: ${pluginName}`));
@@ -532,6 +579,7 @@
532
579
  // Parse config and load plugins
533
580
  const config = parseDataAttributes();
534
581
  if (config && config.pluginBase) setPluginBase(config.pluginBase);
582
+ if (config && config.cdn) setCdnHosts(config.cdn);
535
583
 
536
584
  if (config && config.plugins.length > 0) {
537
585
  if (window.__manifestLoaderStarted) {
@@ -605,7 +653,8 @@
605
653
  const corePlugins = getDefaultPluginsFromManifest(manifest);
606
654
  const appwritePlugins = detectAppwritePlugins(manifest);
607
655
  const paymentsPlugins = detectPaymentsPlugins(manifest);
608
- pluginsToLoad = resolveDependencies([...corePlugins, ...appwritePlugins, ...paymentsPlugins]);
656
+ const chatPlugins = detectChatPlugins(manifest);
657
+ pluginsToLoad = resolveDependencies([...corePlugins, ...appwritePlugins, ...paymentsPlugins, ...chatPlugins]);
609
658
  } else {
610
659
  const needsManifest = config.plugins.some(p => MANIFEST_DEPENDENT_PLUGINS.includes(p));
611
660
  if (needsManifest) {
@@ -634,7 +683,7 @@
634
683
  window.ManifestComponentsRegistry.manifest = manifest;
635
684
  }
636
685
  }
637
- loadAlpine(resolveAlpineUrl(config.alpine));
686
+ loadAlpine(alpineUrlCandidates(config.alpine));
638
687
  };
639
688
 
640
689
  loadPlugins();