diff --git a/internal/_tree-name.mjs b/internal/_tree-name.mjs index 9f4a6d352..f51d447b1 100644 --- a/internal/_tree-name.mjs +++ b/internal/_tree-name.mjs @@ -50,6 +50,19 @@ function readGated() { } export const GATED_PROFILES = readGated() +// The PUBLIC address of the tree for the Share menu's Embed tab (the config's shareBaseUrl): an https URL with no +// trailing slash, or '' (unset). vite.config.js bakes it as __DOXA_SHARE_BASE__; library/utils/embedBase.js reads it. +function readShareBase() { + let v = '' + try { + const cfg = JSON.parse(readFileSync(BUILD_CONFIG_FILE, 'utf8')) + v = typeof cfg.shareBaseUrl === 'string' ? cfg.shareBaseUrl.trim().replace(/\/+$/, '') : '' + } catch { return '' } + if (v && !/^https:\/\/[^\s"'<>]+$/i.test(v)) throw new Error(`[_tree-name] build config "shareBaseUrl" must be an https URL, got ${JSON.stringify(v)}`) + return v +} +export const SHARE_BASE = readShareBase() + // The name escaped for use inside a RegExp source (a '.' in a future name must not match any char). export const TREE_NAME_RE = TREE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') diff --git a/internal/check-i18n-parity.mjs b/internal/check-i18n-parity.mjs index 32f6e2493..7e7759908 100644 --- a/internal/check-i18n-parity.mjs +++ b/internal/check-i18n-parity.mjs @@ -71,7 +71,8 @@ const DEBT = { // 2026-10-03, the Share menu's 10 new share.json keys (shareable pages P3): localization is English-only for now. // tStatic falls back to the English text, so these locales show English until the translations arrive; // lower each to 0 then, and empty the map again. - ar: 10, de: 10, es: 10, fr: 10, hi: 10, it: 10, pt: 10, ro: 10, ru: 10, zh: 10, + // + the Embed tab's 9 keys (URL / Iframe / JavaScript, P3b), the same English-only rule. + ar: 19, de: 19, es: 19, fr: 19, hi: 19, it: 19, pt: 19, ro: 19, ru: 19, zh: 19, } const fails = [] diff --git a/library/components/map-controls/ShareButton.vue b/library/components/map-controls/ShareButton.vue index c5bb319bb..72037713e 100644 --- a/library/components/map-controls/ShareButton.vue +++ b/library/components/map-controls/ShareButton.vue @@ -33,7 +33,8 @@ import { tStatic } from '@map/i18n/index.js' const T = (key, en) => tStatic(key) || en import { useI18n } from 'vue-i18n' import { useShadowStyles } from '../../composables/useShadowStyles.js' -import { resolveShareUrl } from '../../utils/embedBase.js' +import { resolveShareUrl, resolvePublicBase, resolveEmbedBase } from '../../utils/embedBase.js' +import { publicLeafUrl, publicScriptCode } from '../../utils/publicEmbed.js' import { urlOwnerOf } from '../../composables/useViewAddress.js' import MapControlButton from './MapControlButton.vue' import MapControlMenu from './MapControlMenu.vue' @@ -357,6 +358,8 @@ const pageUrl = ref('') // "Share this page": read when the menu const linkCopied = ref(false) const embedCopied = ref(false) const embedSize = ref('medium') // 'small' | 'medium' | 'large' +const embedKind = ref('url') // 'url' | 'iframe' | 'script': the Embed tab's three codes +const scriptState = ref({ loading: false }) // the JavaScript code from the public manifest: { code } | { missing } | { failed } const sizePresets = { small: { label: 'Small', height: 400 }, @@ -390,10 +393,35 @@ const shareUrl = computed(() => resolveShareUrl({ profileConfig: unref(profileConfig) || null, })) +/* ★ EMBED THIS PLUGIN, FROM THE PUBLIC SITE (shareable pages P3b). The Embed tab's three codes are for the plugin + whose Share was pressed (its own page; its own bundle + element), even nested in Plugin Studio, Deep Research or + Posterverse, and they point at the tree's PUBLIC address (resolvePublicBase: the build's shareBaseUrl), never a dev + server or the page's own host. With no public address configured, the tree this bundle loaded from stands in. */ +const embedBase = computed(() => resolvePublicBase(unref(profileConfig) || null) || resolveEmbedBase(unref(profileConfig) || null)) +const embedPathOf = () => props.embedPath || (unref(profileConfig) || {}).shareEmbedPath || '' +// URL: the plugin's own generated page on the public site; an absolute or root path keeps the old resolution. +const leafUrl = computed(() => publicLeafUrl(embedBase.value, embedPathOf()) || shareUrl.value) const embedSnippet = computed(() => { const h = sizePresets[embedSize.value].height - return '' + return '' }) +// JavaScript: the plugin's embed.script from the public manifest, read once the Embed tab opens. "missing" means the +// public site does not carry this plugin yet, so the tab shows a plain note instead of codes that would 404. +function loadScriptCode() { + if (scriptState.value.code || scriptState.value.loading) return + scriptState.value = { loading: true } + publicScriptCode(embedBase.value, embedPathOf()).then((r) => { scriptState.value = r }) +} +const embedText = computed(() => embedKind.value === 'url' ? leafUrl.value + : embedKind.value === 'iframe' ? embedSnippet.value : (scriptState.value.code || '')) +const EMBED_KINDS = ['url', 'iframe', 'script'] +const kindLabel = (k) => ({ url: T('share.kind_url', 'URL'), iframe: T('share.kind_iframe', 'Iframe'), script: T('share.kind_script', 'JavaScript') })[k] +// One plain sentence per code, for a reader who has never embedded anything. +const kindHint = (k) => ({ + url: T('share.url_hint', 'A link to open the map on its own page.'), + iframe: T('share.iframe_hint', 'Put the map in your page, in a frame.'), + script: T('share.script_hint', 'Load the map straight into your page, with no frame.'), +})[k] /* ★ SHARE THIS PAGE. Where the page OWNS its address (useViewAddress: urlOwner 'app' on our own pages, 'host' on DX Life), the address bar already names the exact view (the tab, and the filters), so the @@ -433,6 +461,7 @@ function toggle() { pageUrl.value = pageLink() linkCopied.value = false embedCopied.value = false + embedKind.value = 'url' } } @@ -472,7 +501,7 @@ async function copyLink() { } async function copyEmbed() { - if (await copyText(embedSnippet.value)) { + if (embedText.value && await copyText(embedText.value)) { embedCopied.value = true setTimeout(() => { embedCopied.value = false }, 2000) } @@ -578,7 +607,7 @@ function bindAnchor(el) { @@ -612,7 +641,7 @@ function bindAnchor(el) { - +
@@ -622,43 +651,57 @@ function bindAnchor(el) { - diff --git a/library/i18n/locales/en/share.json b/library/i18n/locales/en/share.json index 71692cb15..07c946b7c 100644 --- a/library/i18n/locales/en/share.json +++ b/library/i18n/locales/en/share.json @@ -11,5 +11,14 @@ "copied": "Copied!", "size_small": "Small", "size_medium": "Medium", - "size_large": "Large" + "size_large": "Large", + "kind_url": "URL", + "kind_iframe": "Iframe", + "kind_script": "JavaScript", + "url_hint": "A link to open the map on its own page.", + "iframe_hint": "Put the map in your page, in a frame.", + "script_hint": "Load the map straight into your page, with no frame.", + "script_loading": "Reading the public site…", + "not_public": "This map isn't on the public site yet. Its embed codes will appear here once it's released.", + "public_failed": "Couldn't reach the public site just now. Try again in a moment." } diff --git a/library/utils/embedBase.js b/library/utils/embedBase.js index 3f1c1ed7a..d0231a7e2 100644 --- a/library/utils/embedBase.js +++ b/library/utils/embedBase.js @@ -212,6 +212,25 @@ export function absoluteEmbedUrl(leaf, profileConfig = null) { return rel } +/** + * The tree's PUBLIC address, for the Share menu's Embed tab (no trailing slash). Its URL, Iframe and JavaScript codes + * point here, so an embed follows the stable releases served there, never a dev server or the page's own host. + * Ladder: profileConfig.shareBaseUrl → window.MAP_SHARE_BASE_URL → the build's shareBaseUrl (vite.doxa-maps-build- + * config.json, baked as __DOXA_SHARE_BASE__) → '' (unset: the Embed tab keeps the tree the bundle was loaded from). + * @param {object|null} [profileConfig] the parsed profile-config (already unref'd) + * @returns {string} + */ +export function resolvePublicBase(profileConfig) { + const fromConfig = profileConfig?.shareBaseUrl + if (typeof fromConfig === 'string' && /^https?:\/\//i.test(fromConfig.trim())) return stripSlashes(fromConfig) + if (typeof window !== 'undefined') { + const fromHost = window.MAP_SHARE_BASE_URL + if (typeof fromHost === 'string' && /^https?:\/\//i.test(fromHost.trim())) return stripSlashes(fromHost) + } + // typeof-guarded: a build or a test that defines no __DOXA_SHARE_BASE__ reads '' instead of throwing + return stripSlashes(typeof __DOXA_SHARE_BASE__ === 'string' ? __DOXA_SHARE_BASE__ : '') +} + export function resolveShareUrl({ shareUrl, embedPath, profileConfig } = {}) { const cfg = profileConfig || null const hasWindow = typeof window !== 'undefined' diff --git a/library/utils/publicEmbed.js b/library/utils/publicEmbed.js new file mode 100644 index 000000000..6f2912949 --- /dev/null +++ b/library/utils/publicEmbed.js @@ -0,0 +1,71 @@ +/** + * publicEmbed.js: the Share menu's Embed codes for THIS plugin, from the tree's PUBLIC address. + * + * Three codes, all for the plugin itself (its own leaf page, its own bundle + element), never for the app it is + * nested in (Plugin Studio, Deep Research, Posterverse): + * URL ///index.html, the plugin's generated page; + * Iframe