mindoodb-app-sdk 0.0.51 → 0.0.54

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/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  releaseMindooDBAppBridgeSessions,
24
24
  sanitizeHostShortcutBindings,
25
25
  snapshotElementToPng
26
- } from "./chunk-R76XXRJ6.js";
26
+ } from "./chunk-6NTVTL6G.js";
27
27
  import {
28
28
  MINDOODB_APP_BUNDLE_ARCHIVE_FILE_NAME,
29
29
  MINDOODB_APP_BUNDLE_MANIFEST_FILE_NAME,
@@ -61,6 +61,9 @@ var MINDOODB_APP_DEFINITION_PERMISSIONS = [
61
61
  "proposeapps"
62
62
  ];
63
63
  var MINDOODB_APP_DEFINITION_REGISTRATION_PERMISSIONS = ["proposeapps"];
64
+ var MINDOODB_APP_LISTING_SUMMARY_MAX = 300;
65
+ var MINDOODB_APP_LISTING_DESCRIPTION_MAX = 4e3;
66
+ var MINDOODB_APP_LISTING_MAX_SCREENSHOTS = 8;
64
67
  var LOGICAL_DATABASE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
65
68
  function isPlainObject(value) {
66
69
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -200,6 +203,163 @@ function readDatabases(value, errors) {
200
203
  });
201
204
  return result.length ? result : void 0;
202
205
  }
206
+ function readLocalizedText(value, label, maxLength, errors) {
207
+ if (value === void 0 || value === null) {
208
+ return void 0;
209
+ }
210
+ if (typeof value === "string") {
211
+ const trimmed = value.trim();
212
+ if (trimmed.length > maxLength) {
213
+ errors.push(`${label} must not be longer than ${maxLength} characters.`);
214
+ return void 0;
215
+ }
216
+ return trimmed || void 0;
217
+ }
218
+ if (!isPlainObject(value)) {
219
+ errors.push(`${label} must be a string or an object of per-locale strings.`);
220
+ return void 0;
221
+ }
222
+ const result = {};
223
+ for (const [locale, text] of Object.entries(value)) {
224
+ if (typeof text !== "string") {
225
+ errors.push(`${label}.${locale} must be a string.`);
226
+ continue;
227
+ }
228
+ const trimmed = text.trim();
229
+ if (trimmed.length > maxLength) {
230
+ errors.push(`${label}.${locale} must not be longer than ${maxLength} characters.`);
231
+ continue;
232
+ }
233
+ if (trimmed) {
234
+ result[locale] = trimmed;
235
+ }
236
+ }
237
+ if (!Object.keys(result).length) {
238
+ return void 0;
239
+ }
240
+ if (!result.en) {
241
+ errors.push(`${label} must include an "en" entry as the fallback for other locales.`);
242
+ return void 0;
243
+ }
244
+ return result;
245
+ }
246
+ function readListingAssetPath(value, label, errors) {
247
+ if (value === void 0 || value === null) {
248
+ return void 0;
249
+ }
250
+ if (typeof value !== "string" || !value.trim()) {
251
+ errors.push(`${label} must be a non-empty string.`);
252
+ return void 0;
253
+ }
254
+ const trimmed = value.trim();
255
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed)?.[1]?.toLowerCase();
256
+ if (scheme && scheme !== "https") {
257
+ errors.push(`${label} must be a path on the app origin or an https: URL.`);
258
+ return void 0;
259
+ }
260
+ if (trimmed.startsWith("//")) {
261
+ errors.push(`${label} must be a path on the app origin or an https: URL.`);
262
+ return void 0;
263
+ }
264
+ return trimmed;
265
+ }
266
+ function readListing(value, errors) {
267
+ if (value === void 0 || value === null) {
268
+ return void 0;
269
+ }
270
+ if (!isPlainObject(value)) {
271
+ errors.push("App definition listing must be an object.");
272
+ return void 0;
273
+ }
274
+ const listing = {};
275
+ const summary = readLocalizedText(
276
+ value.summary,
277
+ "App definition listing.summary",
278
+ MINDOODB_APP_LISTING_SUMMARY_MAX,
279
+ errors
280
+ );
281
+ if (summary) listing.summary = summary;
282
+ const description = readLocalizedText(
283
+ value.description,
284
+ "App definition listing.description",
285
+ MINDOODB_APP_LISTING_DESCRIPTION_MAX,
286
+ errors
287
+ );
288
+ if (description) listing.description = description;
289
+ const descriptionMarkdown = readLocalizedText(
290
+ value.descriptionMarkdown,
291
+ "App definition listing.descriptionMarkdown",
292
+ MINDOODB_APP_LISTING_DESCRIPTION_MAX,
293
+ errors
294
+ );
295
+ if (descriptionMarkdown) listing.descriptionMarkdown = descriptionMarkdown;
296
+ const icon = readListingAssetPath(value.icon, "App definition listing.icon", errors);
297
+ if (icon) listing.icon = icon;
298
+ if (value.screenshots !== void 0 && value.screenshots !== null) {
299
+ if (!Array.isArray(value.screenshots)) {
300
+ errors.push("App definition listing.screenshots must be an array.");
301
+ } else if (value.screenshots.length > MINDOODB_APP_LISTING_MAX_SCREENSHOTS) {
302
+ errors.push(
303
+ `App definition listing.screenshots must not list more than ${MINDOODB_APP_LISTING_MAX_SCREENSHOTS} images.`
304
+ );
305
+ } else {
306
+ const screenshots = [];
307
+ value.screenshots.forEach((entry, index) => {
308
+ const label = `App definition listing.screenshots[${index}]`;
309
+ const raw = typeof entry === "string" ? { file: entry } : entry;
310
+ if (!isPlainObject(raw)) {
311
+ errors.push(`${label} must be a path or an object with a file.`);
312
+ return;
313
+ }
314
+ const file = readListingAssetPath(raw.file, `${label}.file`, errors);
315
+ if (!file) {
316
+ return;
317
+ }
318
+ const caption = readLocalizedText(raw.caption, `${label}.caption`, MINDOODB_APP_LISTING_SUMMARY_MAX, errors);
319
+ screenshots.push(caption ? { file, caption } : { file });
320
+ });
321
+ if (screenshots.length) listing.screenshots = screenshots;
322
+ }
323
+ }
324
+ if (value.publisher !== void 0 && value.publisher !== null) {
325
+ const publisher = isPlainObject(value.publisher) ? value.publisher : {};
326
+ const name = typeof publisher.name === "string" ? publisher.name.trim() : "";
327
+ if (!name) {
328
+ errors.push("App definition listing.publisher must be an object with a non-empty name.");
329
+ } else {
330
+ const url = readListingAssetPath(publisher.url, "App definition listing.publisher.url", errors);
331
+ if (url && !/^https:\/\//i.test(url)) {
332
+ errors.push("App definition listing.publisher.url must be an absolute https: URL.");
333
+ } else {
334
+ listing.publisher = url ? { name, url } : { name };
335
+ }
336
+ }
337
+ }
338
+ return Object.keys(listing).length ? listing : void 0;
339
+ }
340
+ function resolveMindooDBAppLocalizedText(text, locale) {
341
+ if (text === void 0) {
342
+ return "";
343
+ }
344
+ if (typeof text === "string") {
345
+ return text;
346
+ }
347
+ const normalized = locale.trim();
348
+ const language = normalized.split(/[-_]/)[0]?.toLowerCase() ?? "";
349
+ return text[normalized] ?? text[normalized.toLowerCase()] ?? (language ? text[language] : void 0) ?? text.en;
350
+ }
351
+ function resolveMindooDBAppListingAssetUrl(path, originUrl) {
352
+ try {
353
+ const base = new URL(originUrl.endsWith("/") ? originUrl : `${originUrl}/`);
354
+ const resolved = new URL(path, base);
355
+ if (resolved.protocol !== "https:" && resolved.protocol !== base.protocol) {
356
+ return null;
357
+ }
358
+ return resolved.toString();
359
+ } catch {
360
+ return null;
361
+ }
362
+ }
203
363
  function validateMindooDBAppDefinition(raw) {
204
364
  const errors = [];
205
365
  if (!isPlainObject(raw)) {
@@ -251,6 +411,7 @@ function validateMindooDBAppDefinition(raw) {
251
411
  const allowGeolocation = readOptionalBoolean(raw, "allowGeolocation", errors);
252
412
  const allowWebRtc = readOptionalBoolean(raw, "allowWebRtc", errors);
253
413
  const allowWorkers = readOptionalBoolean(raw, "allowWorkers", errors);
414
+ const listing = readListing(raw.listing, errors);
254
415
  if (defaultLaunchDatabaseId && databases && !databases.some((database) => database.logicalDatabaseId === defaultLaunchDatabaseId)) {
255
416
  errors.push(
256
417
  `App definition defaultLaunchDatabaseId "${defaultLaunchDatabaseId}" is not one of the declared databases.`
@@ -279,7 +440,8 @@ function validateMindooDBAppDefinition(raw) {
279
440
  permissions,
280
441
  launchParameters,
281
442
  defaultLaunchDatabaseId,
282
- databases
443
+ databases,
444
+ listing
283
445
  },
284
446
  errors: []
285
447
  };
@@ -389,6 +551,22 @@ function expandAbbreviatedName(value) {
389
551
 
390
552
  // src/client/hosting.ts
391
553
  var MINDOODB_APP_HOSTING_QUERY_PARAM = "mindoodbAppHosting";
554
+ var MINDOODB_APP_LAUNCH_ID_QUERY_PARAM = "mindoodbAppLaunchId";
555
+ function readMindooDBAppLaunchId(search) {
556
+ const raw = search ?? (typeof window === "undefined" ? "" : window.location.search);
557
+ if (!raw) {
558
+ return null;
559
+ }
560
+ try {
561
+ const launchId = new URLSearchParams(raw).get(MINDOODB_APP_LAUNCH_ID_QUERY_PARAM)?.trim();
562
+ return launchId || null;
563
+ } catch {
564
+ return null;
565
+ }
566
+ }
567
+ function isLaunchedByHaven(search) {
568
+ return readMindooDBAppLaunchId(search) !== null;
569
+ }
392
570
  function readMindooDBAppHostingMode(search) {
393
571
  const raw = search ?? (typeof window === "undefined" ? "" : window.location.search);
394
572
  if (!raw) {
@@ -404,6 +582,451 @@ function isHostedBundleRuntime(search) {
404
582
  return readMindooDBAppHostingMode(search) === "hosted";
405
583
  }
406
584
 
585
+ // src/listingMarkdown.ts
586
+ var MAX_INLINE_DEPTH = 4;
587
+ var HEADING = /^#{1,6}\s+(.*?)\s*#*\s*$/;
588
+ var BULLET = /^[-*+]\s+(.*)$/;
589
+ var ORDERED = /^\d{1,9}[.)]\s+(.*)$/;
590
+ var ESCAPABLE = new Set("\\`*_[]()#+-.!<>".split(""));
591
+ var BARE_URL_TRAILING_PUNCTUATION = /[.,;:!?)\]'"]+$/;
592
+ function sanitizeListingHref(raw) {
593
+ const cleaned = raw.replace(/[\t\n\r]/g, "").trim();
594
+ if (!cleaned) {
595
+ return null;
596
+ }
597
+ try {
598
+ const url = new URL(cleaned);
599
+ if (url.protocol === "https:") {
600
+ return url.toString();
601
+ }
602
+ if (url.protocol === "mailto:" && url.pathname) {
603
+ return url.toString();
604
+ }
605
+ } catch {
606
+ }
607
+ return null;
608
+ }
609
+ function isWordCharacter(char) {
610
+ return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
611
+ }
612
+ function pushText(nodes, text) {
613
+ if (!text) {
614
+ return;
615
+ }
616
+ const last = nodes[nodes.length - 1];
617
+ if (last?.type === "text") {
618
+ last.text += text;
619
+ } else {
620
+ nodes.push({ type: "text", text });
621
+ }
622
+ }
623
+ function findClosingDelimiter(text, from, delimiter) {
624
+ let index = text.indexOf(delimiter, from);
625
+ while (index !== -1) {
626
+ const before = text[index - 1];
627
+ const after = text[index + delimiter.length];
628
+ const closesAfterContent = index > from && before !== " " && before !== "\\";
629
+ const underscoreBoundary = delimiter[0] !== "_" || !isWordCharacter(after);
630
+ if (closesAfterContent && underscoreBoundary) {
631
+ return index;
632
+ }
633
+ index = text.indexOf(delimiter, index + 1);
634
+ }
635
+ return -1;
636
+ }
637
+ function findLinkTargetEnd(text, from) {
638
+ let open = 0;
639
+ for (let index = from; index < text.length; index += 1) {
640
+ const char = text[index];
641
+ if (char === "(") {
642
+ open += 1;
643
+ } else if (char === ")") {
644
+ if (open === 0) return index;
645
+ open -= 1;
646
+ } else if (char === " ") {
647
+ return -1;
648
+ }
649
+ }
650
+ return -1;
651
+ }
652
+ function parseInline(text, depth = 0) {
653
+ const nodes = [];
654
+ let index = 0;
655
+ while (index < text.length) {
656
+ const char = text[index];
657
+ if (char === "\\" && ESCAPABLE.has(text[index + 1] ?? "")) {
658
+ pushText(nodes, text[index + 1]);
659
+ index += 2;
660
+ continue;
661
+ }
662
+ if (char === "`") {
663
+ const end = text.indexOf("`", index + 1);
664
+ if (end > index + 1) {
665
+ nodes.push({ type: "code", text: text.slice(index + 1, end) });
666
+ index = end + 1;
667
+ continue;
668
+ }
669
+ }
670
+ if (depth < MAX_INLINE_DEPTH && (char === "*" || char === "_")) {
671
+ const strong = text[index + 1] === char;
672
+ const delimiter = strong ? char + char : char;
673
+ const next = text[index + delimiter.length];
674
+ const opens = next !== void 0 && next !== " " && (char !== "_" || !isWordCharacter(text[index - 1]));
675
+ if (opens) {
676
+ const end = findClosingDelimiter(text, index + delimiter.length, delimiter);
677
+ if (end !== -1) {
678
+ const children = parseInline(text.slice(index + delimiter.length, end), depth + 1);
679
+ nodes.push(strong ? { type: "strong", children } : { type: "em", children });
680
+ index = end + delimiter.length;
681
+ continue;
682
+ }
683
+ }
684
+ }
685
+ if (char === "[" && depth < MAX_INLINE_DEPTH) {
686
+ const labelEnd = text.indexOf("](", index + 1);
687
+ const urlEnd = labelEnd === -1 ? -1 : findLinkTargetEnd(text, labelEnd + 2);
688
+ if (labelEnd > index + 1 && urlEnd !== -1) {
689
+ const label = parseInline(text.slice(index + 1, labelEnd), depth + 1);
690
+ const href = sanitizeListingHref(text.slice(labelEnd + 2, urlEnd));
691
+ if (href) {
692
+ nodes.push({ type: "link", href, children: label });
693
+ } else {
694
+ nodes.push(...label);
695
+ }
696
+ index = urlEnd + 1;
697
+ continue;
698
+ }
699
+ }
700
+ if (char === "h" && text.startsWith("https://", index) && !isWordCharacter(text[index - 1])) {
701
+ let end = index;
702
+ while (end < text.length && !/\s/.test(text[end])) end += 1;
703
+ const candidate = text.slice(index, end).replace(BARE_URL_TRAILING_PUNCTUATION, "");
704
+ const href = sanitizeListingHref(candidate);
705
+ if (href) {
706
+ nodes.push({ type: "link", href, children: [{ type: "text", text: candidate }] });
707
+ index += candidate.length;
708
+ continue;
709
+ }
710
+ }
711
+ pushText(nodes, char);
712
+ index += 1;
713
+ }
714
+ return nodes;
715
+ }
716
+ function parseListingMarkdown(markdown) {
717
+ const blocks = [];
718
+ let paragraph = [];
719
+ let list = null;
720
+ const flushParagraph = () => {
721
+ if (paragraph.length) {
722
+ blocks.push({ type: "paragraph", children: parseInline(paragraph.join(" ")) });
723
+ paragraph = [];
724
+ }
725
+ };
726
+ const flushList = () => {
727
+ if (list) {
728
+ blocks.push({ type: "list", ordered: list.ordered, items: list.items.map((item) => parseInline(item)) });
729
+ list = null;
730
+ }
731
+ };
732
+ for (const rawLine of markdown.split(/\r?\n/)) {
733
+ const line = rawLine.trim();
734
+ if (!line) {
735
+ flushParagraph();
736
+ flushList();
737
+ continue;
738
+ }
739
+ const heading = HEADING.exec(line);
740
+ const bullet = BULLET.exec(line);
741
+ const ordered = bullet ? null : ORDERED.exec(line);
742
+ if (heading) {
743
+ flushParagraph();
744
+ flushList();
745
+ blocks.push({ type: "heading", children: parseInline(heading[1]) });
746
+ } else if (bullet || ordered) {
747
+ flushParagraph();
748
+ const isOrdered = Boolean(ordered);
749
+ if (list && list.ordered !== isOrdered) {
750
+ flushList();
751
+ }
752
+ list ??= { ordered: isOrdered, items: [] };
753
+ list.items.push((bullet ?? ordered)[1]);
754
+ } else if (list) {
755
+ list.items[list.items.length - 1] += ` ${line}`;
756
+ } else {
757
+ paragraph.push(line);
758
+ }
759
+ }
760
+ flushParagraph();
761
+ flushList();
762
+ return blocks;
763
+ }
764
+ function inlineToPlainText(nodes) {
765
+ return nodes.map((node) => {
766
+ if (node.type === "text" || node.type === "code") {
767
+ return node.text;
768
+ }
769
+ const label = inlineToPlainText(node.children);
770
+ if (node.type !== "link") {
771
+ return label;
772
+ }
773
+ const target = node.href.replace(/^mailto:/, "").replace(/^https:\/\//, "").replace(/\/$/, "");
774
+ return label.includes(target) ? label : `${label} (${node.href.replace(/^mailto:/, "")})`;
775
+ }).join("");
776
+ }
777
+ function listingMarkdownToPlainText(markdown) {
778
+ const paragraphs = [];
779
+ for (const block of parseListingMarkdown(markdown)) {
780
+ if (block.type === "list") {
781
+ block.items.forEach((item, index) => {
782
+ paragraphs.push(`${block.ordered ? `${index + 1}.` : "\u2022"} ${inlineToPlainText(item)}`);
783
+ });
784
+ } else {
785
+ paragraphs.push(inlineToPlainText(block.children));
786
+ }
787
+ }
788
+ return paragraphs.filter((paragraph) => paragraph.trim()).join("\n\n");
789
+ }
790
+
791
+ // src/landing.ts
792
+ var DEFAULT_HAVEN_URL = "https://haven.mindoodb.com";
793
+ var APP_ICON_FALLBACKS = ["appicon.png", "appicon.webp", "appicon.svg"];
794
+ var STRINGS = {
795
+ en: {
796
+ eyebrow: "App for MindooDB Haven",
797
+ cta: "Open in MindooDB Haven",
798
+ ctaHint: "Haven sets up your encrypted workspace and installs {label}. Already using Haven? Then the app is simply added.",
799
+ from: "from {origin}",
800
+ by: "by {publisher}",
801
+ screenshots: "Screenshots",
802
+ footer: "{label} runs inside MindooDB Haven, which keeps its data end-to-end encrypted and local-first. Opening this address on its own shows only this page.",
803
+ learnMore: "What is MindooDB?",
804
+ fallbackSummary: "This app runs inside MindooDB Haven."
805
+ },
806
+ de: {
807
+ eyebrow: "App f\xFCr MindooDB Haven",
808
+ cta: "In MindooDB Haven \xF6ffnen",
809
+ ctaHint: "Haven richtet deine verschl\xFCsselte Umgebung ein und installiert {label}. Du nutzt Haven schon? Dann wird die App einfach hinzugef\xFCgt.",
810
+ from: "von {origin}",
811
+ by: "von {publisher}",
812
+ screenshots: "Screenshots",
813
+ footer: "{label} l\xE4uft in MindooDB Haven, das ihre Daten Ende-zu-Ende-verschl\xFCsselt und lokal h\xE4lt. Direkt aufgerufen zeigt diese Adresse nur diese Seite.",
814
+ learnMore: "Was ist MindooDB?",
815
+ fallbackSummary: "Diese App l\xE4uft in MindooDB Haven."
816
+ }
817
+ };
818
+ function stringsFor(locale) {
819
+ return locale.toLowerCase().startsWith("de") ? STRINGS.de : STRINGS.en;
820
+ }
821
+ function format(template, values) {
822
+ return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
823
+ }
824
+ function resolveCurrentHavenAppUrl(href = window.location.href) {
825
+ const url = new URL(href);
826
+ url.search = "";
827
+ url.hash = "";
828
+ url.pathname = url.pathname.replace(/\/index\.html?$/i, "/");
829
+ return url.toString();
830
+ }
831
+ function buildHavenAppInstallUrl(appUrl, havenUrl = DEFAULT_HAVEN_URL) {
832
+ const url = new URL(havenUrl);
833
+ url.searchParams.set("app", appUrl);
834
+ return url.toString();
835
+ }
836
+ async function loadDefinition(appUrl) {
837
+ try {
838
+ const definitionUrl = new URL(MINDOODB_APP_DEFINITION_FILE_NAME, appUrl.endsWith("/") ? appUrl : `${appUrl}/`);
839
+ const response = await fetch(definitionUrl, { headers: { Accept: "application/json" } });
840
+ if (!response.ok) {
841
+ return null;
842
+ }
843
+ return validateMindooDBAppDefinition(await response.json()).definition;
844
+ } catch {
845
+ return null;
846
+ }
847
+ }
848
+ function el(tag, className, text) {
849
+ const node = document.createElement(tag);
850
+ if (className) node.className = className;
851
+ if (text !== void 0) node.textContent = text;
852
+ return node;
853
+ }
854
+ function externalLink(href) {
855
+ const link = el("a");
856
+ link.href = href;
857
+ link.rel = "noopener noreferrer";
858
+ link.target = "_blank";
859
+ return link;
860
+ }
861
+ function appendInline(parent, nodes) {
862
+ for (const node of nodes) {
863
+ if (node.type === "text") {
864
+ parent.append(node.text);
865
+ } else if (node.type === "code") {
866
+ parent.append(el("code", void 0, node.text));
867
+ } else {
868
+ const child = node.type === "link" ? externalLink(node.href) : el(node.type);
869
+ appendInline(child, node.children);
870
+ parent.append(child);
871
+ }
872
+ }
873
+ }
874
+ function renderMarkdownDescription(block, markdown) {
875
+ for (const entry of parseListingMarkdown(markdown)) {
876
+ if (entry.type === "list") {
877
+ const list = el(entry.ordered ? "ol" : "ul");
878
+ for (const item of entry.items) {
879
+ const li = el("li");
880
+ appendInline(li, item);
881
+ list.append(li);
882
+ }
883
+ block.append(list);
884
+ } else {
885
+ const node = el(entry.type === "heading" ? "h2" : "p");
886
+ appendInline(node, entry.children);
887
+ block.append(node);
888
+ }
889
+ }
890
+ }
891
+ var STYLE_ID = "mdb-landing-style";
892
+ var STYLE = `
893
+ .mdb-landing{--bg:#f4f6fb;--card:#fff;--text:#172033;--muted:#5b6478;--line:#e2e6ef;--accent:#1f3a8a;--accent-text:#fff;
894
+ min-height:100vh;box-sizing:border-box;margin:0;padding:48px 16px;background:var(--bg);color:var(--text);
895
+ font:16px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
896
+ @media (prefers-color-scheme:dark){.mdb-landing{--bg:#0e1320;--card:#161d2e;--text:#e8ecf5;--muted:#9aa3b8;--line:#263049;--accent:#8fb0ff;--accent-text:#0e1320}}
897
+ .mdb-landing *{box-sizing:border-box}
898
+ .mdb-landing__card{max-width:880px;margin:0 auto;background:var(--card);border:1px solid var(--line);border-radius:24px;padding:40px;box-shadow:0 20px 60px rgba(15,23,42,.08)}
899
+ .mdb-landing__head{display:flex;gap:24px;align-items:center}
900
+ .mdb-landing__icon{width:96px;height:96px;border-radius:22px;object-fit:cover;flex:none;box-shadow:0 10px 30px rgba(15,23,42,.18)}
901
+ .mdb-landing__eyebrow{margin:0 0 4px;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
902
+ .mdb-landing__title{margin:0;font-size:clamp(26px,4vw,36px);line-height:1.15}
903
+ .mdb-landing__meta{margin:6px 0 0;color:var(--muted);font-size:14px}
904
+ .mdb-landing__summary{margin:28px 0 0;font-size:19px}
905
+ .mdb-landing__cta{display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin:28px 0 0}
906
+ .mdb-landing__button{display:inline-block;padding:14px 22px;border-radius:12px;background:var(--accent);color:var(--accent-text);font-weight:600;text-decoration:none}
907
+ .mdb-landing__button:focus-visible{outline:3px solid var(--accent);outline-offset:3px}
908
+ .mdb-landing__hint{margin:0;max-width:420px;color:var(--muted);font-size:14px}
909
+ .mdb-landing__description{margin:32px 0 0}
910
+ .mdb-landing__description p,.mdb-landing__description ul,.mdb-landing__description ol{margin:0 0 12px}
911
+ .mdb-landing__description ul,.mdb-landing__description ol{padding-left:22px}
912
+ .mdb-landing__description li+li{margin-top:4px}
913
+ .mdb-landing__description h2{margin:20px 0 8px;font-size:17px}
914
+ .mdb-landing__description code{font:.92em ui-monospace,SFMono-Regular,Menlo,monospace;padding:1px 5px;border-radius:5px;background:var(--line)}
915
+ .mdb-landing__description a,.mdb-landing__meta a{color:var(--accent)}
916
+ .mdb-landing__shots-title{margin:32px 0 12px;font-size:15px;color:var(--muted);font-weight:600}
917
+ .mdb-landing__shots{display:grid;grid-auto-flow:column;grid-auto-columns:min(78%,420px);gap:16px;overflow-x:auto;padding-bottom:8px;scroll-snap-type:x mandatory}
918
+ .mdb-landing__shot{margin:0;scroll-snap-align:start}
919
+ .mdb-landing__shot img{width:100%;border-radius:14px;border:1px solid var(--line);display:block}
920
+ .mdb-landing__shot figcaption{margin-top:8px;font-size:13px;color:var(--muted)}
921
+ .mdb-landing__footer{margin:36px 0 0;padding-top:20px;border-top:1px solid var(--line);color:var(--muted);font-size:13px}
922
+ .mdb-landing__footer a{color:inherit}
923
+ @media (max-width:560px){.mdb-landing{padding:16px}.mdb-landing__card{padding:24px;border-radius:18px}.mdb-landing__head{flex-direction:column;align-items:flex-start}.mdb-landing__icon{width:72px;height:72px}}
924
+ `;
925
+ async function renderHavenAppLandingPage(options = {}) {
926
+ const appUrl = options.appUrl ?? resolveCurrentHavenAppUrl();
927
+ const locale = options.locale ?? (typeof navigator === "undefined" ? "en" : navigator.language || "en");
928
+ const strings = stringsFor(locale);
929
+ const definition = options.definition ?? await loadDefinition(appUrl);
930
+ const installUrl = buildHavenAppInstallUrl(appUrl, options.havenUrl);
931
+ const listing = definition?.listing;
932
+ const label = definition?.label ?? (document.title || new URL(appUrl).host);
933
+ const origin = new URL(appUrl).host;
934
+ if (!document.getElementById(STYLE_ID)) {
935
+ const style = el("style");
936
+ style.id = STYLE_ID;
937
+ style.textContent = STYLE;
938
+ document.head.append(style);
939
+ }
940
+ document.title = label;
941
+ const root = el("main", "mdb-landing");
942
+ root.dataset.testid = "haven-app-landing";
943
+ const card = el("article", "mdb-landing__card");
944
+ root.append(card);
945
+ const head = el("header", "mdb-landing__head");
946
+ const iconCandidates = (listing?.icon ? [listing.icon] : [...APP_ICON_FALLBACKS]).map((path) => resolveMindooDBAppListingAssetUrl(path, appUrl)).filter((url) => Boolean(url));
947
+ if (iconCandidates.length) {
948
+ const icon = el("img", "mdb-landing__icon");
949
+ let next = 0;
950
+ icon.alt = "";
951
+ icon.addEventListener("error", () => {
952
+ next += 1;
953
+ if (next < iconCandidates.length) {
954
+ icon.src = iconCandidates[next];
955
+ } else {
956
+ icon.remove();
957
+ }
958
+ });
959
+ icon.src = iconCandidates[0];
960
+ head.append(icon);
961
+ }
962
+ const titles = el("div");
963
+ titles.append(el("p", "mdb-landing__eyebrow", strings.eyebrow), el("h1", "mdb-landing__title", label));
964
+ const meta = el("p", "mdb-landing__meta");
965
+ if (listing?.publisher) {
966
+ const [before, after = ""] = strings.by.split("{publisher}");
967
+ const name = listing.publisher.url ? Object.assign(externalLink(listing.publisher.url), { textContent: listing.publisher.name }) : listing.publisher.name;
968
+ meta.append(before ?? "", name, after, " \xB7 ");
969
+ }
970
+ meta.append(format(strings.from, { origin }));
971
+ titles.append(meta);
972
+ head.append(titles);
973
+ card.append(head);
974
+ const summary = resolveMindooDBAppLocalizedText(listing?.summary, locale) || definition?.description || strings.fallbackSummary;
975
+ card.append(el("p", "mdb-landing__summary", summary));
976
+ const cta = el("div", "mdb-landing__cta");
977
+ const button = el("a", "mdb-landing__button", strings.cta);
978
+ button.href = installUrl;
979
+ button.rel = "noopener";
980
+ button.dataset.testid = "haven-app-landing-install";
981
+ cta.append(button, el("p", "mdb-landing__hint", format(strings.ctaHint, { label })));
982
+ card.append(cta);
983
+ const descriptionMarkdown = resolveMindooDBAppLocalizedText(listing?.descriptionMarkdown, locale);
984
+ const description = resolveMindooDBAppLocalizedText(listing?.description, locale);
985
+ if (descriptionMarkdown || description) {
986
+ const block = el("section", "mdb-landing__description");
987
+ if (descriptionMarkdown) {
988
+ renderMarkdownDescription(block, descriptionMarkdown);
989
+ } else {
990
+ for (const paragraph of description.split(/\n\s*\n/)) {
991
+ if (paragraph.trim()) block.append(el("p", void 0, paragraph.trim()));
992
+ }
993
+ }
994
+ card.append(block);
995
+ }
996
+ const screenshots = (listing?.screenshots ?? []).map((shot) => ({
997
+ url: resolveMindooDBAppListingAssetUrl(shot.file, appUrl),
998
+ caption: resolveMindooDBAppLocalizedText(shot.caption, locale)
999
+ })).filter((shot) => Boolean(shot.url));
1000
+ if (screenshots.length) {
1001
+ card.append(el("h2", "mdb-landing__shots-title", strings.screenshots));
1002
+ const strip = el("div", "mdb-landing__shots");
1003
+ for (const shot of screenshots) {
1004
+ const figure = el("figure", "mdb-landing__shot");
1005
+ const image = el("img");
1006
+ image.src = shot.url;
1007
+ image.alt = shot.caption;
1008
+ image.loading = "lazy";
1009
+ figure.append(image);
1010
+ if (shot.caption) figure.append(el("figcaption", void 0, shot.caption));
1011
+ strip.append(figure);
1012
+ }
1013
+ card.append(strip);
1014
+ }
1015
+ const footer = el("footer", "mdb-landing__footer");
1016
+ footer.append(format(strings.footer, { label }), " ");
1017
+ const learnMore = el("a", void 0, strings.learnMore);
1018
+ learnMore.href = "https://mindoodb.com";
1019
+ learnMore.rel = "noopener";
1020
+ footer.append(learnMore);
1021
+ card.append(footer);
1022
+ const container = options.container ?? document.body;
1023
+ container.replaceChildren(root);
1024
+ if (container === document.body) {
1025
+ document.body.style.margin = "0";
1026
+ }
1027
+ return { definition, installUrl };
1028
+ }
1029
+
407
1030
  // src/client/havenStorageShim.ts
408
1031
  var DEFAULT_QUOTA_BYTES = 256 * 1024;
409
1032
  var DEFAULT_MAX_VALUE_BYTES = 64 * 1024;
@@ -731,11 +1354,11 @@ var MindooDBRichTextHandle = class {
731
1354
  this.pollTimer = null;
732
1355
  }
733
1356
  }
734
- reconcile(document, snapshot) {
1357
+ reconcile(document2, snapshot) {
735
1358
  const previous = JSON.stringify(this.spansInternal);
736
1359
  this.spansInternal = cloneSpans(snapshot.spans);
737
1360
  this.baseMaterializedText = spansToPlainText(snapshot.spans);
738
- this.heads = snapshot.heads ?? document.heads ?? [];
1361
+ this.heads = snapshot.heads ?? document2.heads ?? [];
739
1362
  this.dirtyInternal = false;
740
1363
  const changed = JSON.stringify(this.spansInternal) !== previous;
741
1364
  if (changed) {
@@ -754,14 +1377,14 @@ var MindooDBRichTextHandle = class {
754
1377
  if (this.dirtyInternal) {
755
1378
  return false;
756
1379
  }
757
- const document = await this.options.database.documents.get(
1380
+ const document2 = await this.options.database.documents.get(
758
1381
  this.options.document.id
759
1382
  );
760
- if (!document) {
1383
+ if (!document2) {
761
1384
  return false;
762
1385
  }
763
1386
  const snapshot = await this.refresh();
764
- return this.reconcile(document, snapshot);
1387
+ return this.reconcile(document2, snapshot);
765
1388
  }
766
1389
  async flush(updateSpansConfig) {
767
1390
  if (!this.dirtyInternal) {
@@ -778,7 +1401,7 @@ var MindooDBRichTextHandle = class {
778
1401
  const localSpansAtFlush = cloneSpans(this.spansInternal);
779
1402
  const localTextAtFlush = spansToPlainText(localSpansAtFlush);
780
1403
  const textChanged = localTextAtFlush !== this.baseMaterializedText;
781
- const document = textChanged ? await this.flushTextSteps(localTextAtFlush) : await this.options.database.documents.update(
1404
+ const document2 = textChanged ? await this.flushTextSteps(localTextAtFlush) : await this.options.database.documents.update(
782
1405
  this.options.document.id,
783
1406
  {
784
1407
  richText: [{
@@ -796,13 +1419,13 @@ var MindooDBRichTextHandle = class {
796
1419
  const reconciled = JSON.stringify(snapshot.spans) !== JSON.stringify(localSpansAtFlush);
797
1420
  this.spansInternal = cloneSpans(snapshot.spans);
798
1421
  this.baseMaterializedText = spansToPlainText(snapshot.spans);
799
- this.heads = snapshot.heads ?? document.heads ?? [];
1422
+ this.heads = snapshot.heads ?? document2.heads ?? [];
800
1423
  this.dirtyInternal = false;
801
1424
  if (reconciled) {
802
1425
  this.emitChange();
803
1426
  }
804
1427
  return {
805
- document,
1428
+ document: document2,
806
1429
  snapshot,
807
1430
  reconciled
808
1431
  };
@@ -940,11 +1563,11 @@ var MindooDBTextBuffer = class {
940
1563
  * Returns whether the text value changed. Use this after an external refresh
941
1564
  * or after receiving a document update through another app-level mechanism.
942
1565
  */
943
- reconcile(document) {
944
- const nextValue = readTextAtPath(document.data, this.options.path);
1566
+ reconcile(document2) {
1567
+ const nextValue = readTextAtPath(document2.data, this.options.path);
945
1568
  const changed = nextValue !== this.valueInternal;
946
1569
  this.valueInternal = nextValue;
947
- this.heads = document.heads ? [...document.heads] : [];
1570
+ this.heads = document2.heads ? [...document2.heads] : [];
948
1571
  this.pendingEdits = [];
949
1572
  return changed;
950
1573
  }
@@ -966,20 +1589,20 @@ var MindooDBTextBuffer = class {
966
1589
  };
967
1590
  }
968
1591
  const localValueAtFlush = this.valueInternal;
969
- const document = await this.options.database.documents.update(this.options.document.id, {
1592
+ const document2 = await this.options.database.documents.update(this.options.document.id, {
970
1593
  text: [{
971
1594
  path: this.options.path,
972
1595
  baseHeads: this.heads,
973
1596
  edits: this.pendingEdits.map((edit) => ({ ...edit }))
974
1597
  }]
975
1598
  });
976
- const nextValue = readTextAtPath(document.data, this.options.path);
1599
+ const nextValue = readTextAtPath(document2.data, this.options.path);
977
1600
  const reconciled = nextValue !== localValueAtFlush;
978
1601
  this.valueInternal = nextValue;
979
- this.heads = document.heads ? [...document.heads] : [];
1602
+ this.heads = document2.heads ? [...document2.heads] : [];
980
1603
  this.pendingEdits = [];
981
1604
  return {
982
- document,
1605
+ document: document2,
983
1606
  value: this.valueInternal,
984
1607
  reconciled
985
1608
  };
@@ -1055,6 +1678,7 @@ export {
1055
1678
  DEFAULT_DRAG_LONG_PRESS_MS,
1056
1679
  DEFAULT_DRAG_POINTER_THRESHOLD_PX,
1057
1680
  DEFAULT_HAVEN_HOST_SHORTCUTS,
1681
+ DEFAULT_HAVEN_URL,
1058
1682
  MINDOODB_APP_BUNDLE_ARCHIVE_FILE_NAME,
1059
1683
  MINDOODB_APP_BUNDLE_MANIFEST_FILE_NAME,
1060
1684
  MINDOODB_APP_BUNDLE_MANIFEST_FORMAT,
@@ -1065,6 +1689,10 @@ export {
1065
1689
  MINDOODB_APP_DEFINITION_REGISTRATION_PERMISSIONS,
1066
1690
  MINDOODB_APP_DEFINITION_VERSION,
1067
1691
  MINDOODB_APP_HOSTING_QUERY_PARAM,
1692
+ MINDOODB_APP_LAUNCH_ID_QUERY_PARAM,
1693
+ MINDOODB_APP_LISTING_DESCRIPTION_MAX,
1694
+ MINDOODB_APP_LISTING_MAX_SCREENSHOTS,
1695
+ MINDOODB_APP_LISTING_SUMMARY_MAX,
1068
1696
  MINDOODB_APP_MAX_DRAG_PAYLOAD_BYTES,
1069
1697
  MINDOODB_APP_MAX_DRAG_PREVIEW_BYTES,
1070
1698
  MINDOODB_APP_MAX_DRAG_PREVIEW_EDGE,
@@ -1080,6 +1708,7 @@ export {
1080
1708
  abbreviateCanonicalName,
1081
1709
  applyHostedDocumentOverscrollContain,
1082
1710
  bindDragSource,
1711
+ buildHavenAppInstallUrl,
1083
1712
  buildMindooDBAppBundleContentHashInput,
1084
1713
  canPreviewAttachment,
1085
1714
  createMindooDBAppBridge,
@@ -1092,18 +1721,27 @@ export {
1092
1721
  installHostShortcutCapture,
1093
1722
  isHostShortcutAction,
1094
1723
  isHostedBundleRuntime,
1724
+ isLaunchedByHaven,
1095
1725
  isMindooDBAppBundleHash,
1096
1726
  isMindooDBAppDragType,
1727
+ listingMarkdownToPlainText,
1097
1728
  matchHostShortcut,
1098
1729
  normalizeDragAccepts,
1099
1730
  normalizeDragPointerInput,
1100
1731
  normalizeDragStartInput,
1101
1732
  normalizeMindooDBAppBundlePath,
1733
+ parseListingMarkdown,
1102
1734
  queryDocuments,
1103
1735
  readMindooDBAppHostingMode,
1736
+ readMindooDBAppLaunchId,
1104
1737
  releaseMindooDBAppBridgeSessions,
1738
+ renderHavenAppLandingPage,
1739
+ resolveCurrentHavenAppUrl,
1105
1740
  resolveMindooDBAppDefinitionUrl,
1741
+ resolveMindooDBAppListingAssetUrl,
1742
+ resolveMindooDBAppLocalizedText,
1106
1743
  sanitizeHostShortcutBindings,
1744
+ sanitizeListingHref,
1107
1745
  snapshotElementToPng,
1108
1746
  validateMindooDBAppBundleManifest,
1109
1747
  validateMindooDBAppDefinition,