function apologyEditorSections(model) {
    const fallback = model.id === 'the-us-archive'
        ? [['opening', 'The camera opening', 'opening'], ['hero', 'The first words', '#home', 'headline'], ['memories', 'Your memories', '#developing', 'photos'], ['apology', 'Your apology', '#the-words', 'message'], ['rewrite', 'Kinder words', '#say-it-better'], ['promises', 'Your promises', '#same-side', 'promises'], ['reasons', 'Little things you love', '#little-things-i-love'], ['quiet', 'The quiet note', '#what-stays'], ['keepsake', 'Your keepsake', '#our-next-picture'], ['fireflies', 'A little light', '#a-light-for-you'], ['closing', 'The last note', '#one-more-moment', 'closing']]
        : [['opening', 'The ribbon opening', 'opening'], ['hero', 'The first words', '#home', 'headline'], ['delivery', 'Your letter', '#delivery', 'message'], ['tea', 'A cup of chai', '.tea-pour-section'], ['promises', 'Your promises', '#little-promises', 'promises'], ['memories', 'Your memories', '#memory-section', 'photos'], ['quiet', 'The unsaid words', '.almost-said'], ['closing', 'The last note', '#with-love', 'closing']];
    const story = Array.isArray(model.storySections) && model.storySections.length ? model.storySections : fallback.map(([id, label, target, core]) => ({ id, label, target, core, fields: [] }));
    return [{ id: 'about', label: 'About you', hint: 'Start with the two names that make this story yours.', fields: [] }, ...story];
}
function apologyCoreFields(section, archive) {
    const fields = {
        about: [{ path: ['from'], label: 'Your name', maxLength: 70, placeholder: 'Your name' }, { path: ['to'], label: 'Their name', maxLength: 70, placeholder: 'The name you call them' }],
        headline: [{ path: ['headline'], label: 'The first words they see', multiline: true, maxLength: 1200, hint: 'What would you want them to feel in the first moment? Keep this short.' }],
        message: [{ path: ['message'], label: 'What you want to say', multiline: true, rows: 7, maxLength: 8000, hint: 'Name what hurt. Say what you understand now, and what you will do differently.' }],
        promises: [0, 1, 2].map(index => ({ path: ['promises', index], label: archive ? 'Promise ' + (index + 1) : ['Time — how you will listen', 'Softness — how you will speak', 'Care — how you will show up'][index], multiline: true, rows: 4, maxLength: 1500, hint: 'Choose one small action you can actually keep.' })),
        closing: [{ path: ['closing'], label: 'Your last note', multiline: true, rows: 5, maxLength: 1200, hint: 'Leave a warm invitation to talk, with room for them to take their time.' }]
    };
    return section.id === 'about' ? fields.about : (Array.isArray(section.core) ? section.core : [section.core]).flatMap(key => fields[key] || []);
}
function apologyFreshDraft(model) {
    const draft = typeof model.createAuthorDraft === 'function' ? model.createAuthorDraft() : model.create({});
    return { ...draft, music: window.EverafterMusicConfig ? window.EverafterMusicConfig.template() : { source: 'template', enabled: true } };
}
function apologyInitialDraft(model, projectId) {
    if (projectId)
        return model.create({});
    const saved = readBuilderConfig(model.key);
    return saved ? model.create(saved) : apologyFreshDraft(model);
}
function apologyKeepFocusedFieldVisible(root, view = window, doc = document) {
    const field = doc.activeElement;
    if (!root?.contains(field) || !field?.matches('input:not([type="file"]),textarea,select'))
        return;
    const viewport = view.visualViewport;
    const viewportTop = viewport?.offsetTop || 0;
    const viewportBottom = viewportTop + (viewport?.height || view.innerHeight);
    const top = Math.max(viewportTop, doc.querySelector('.nav')?.getBoundingClientRect().bottom || 0) + 12;
    const bottom = Math.min(viewportBottom, root.querySelector('.ap-builder-savebar')?.getBoundingClientRect().top ?? viewportBottom) - 12;
    const available = bottom - top;
    if (available <= 0)
        return;
    const rect = field.getBoundingClientRect();
    if (rect.top >= top && (rect.bottom <= bottom || (rect.height > available && rect.top <= top + 1)))
        return;
    const targetTop = rect.height > available ? top : top + (available - rect.height) / 2;
    view.scrollBy({ top: rect.top - targetTop, left: 0, behavior: 'instant' });
}
function ApologyBuilder({ template, goTo, user, openSignIn, projectId, model }) {
    const resolved = model || window.ApologyModels?.[template?.id];
    if (!resolved)
        return React.createElement("main", { className: "section builder-load-state" },
            React.createElement("div", { className: "wrap" },
                React.createElement("h1", null, "This design could not open"),
                React.createElement("p", null, "Please refresh the page and try again.")));
    return React.createElement(ApologyEditor, { key: resolved.id + ':' + (projectId || 'new'), template: template, goTo: goTo, user: user, openSignIn: openSignIn, projectId: projectId, model: resolved });
}
function ApologyEditor({ template, goTo, user, openSignIn, projectId, model: M, mode = 'apology' }) {
    const api = typeof useBuilderApi === 'function' ? useBuilderApi() : window.EA;
    const wedding = mode === 'wedding';
    const previewProtocol = wedding ? 'keepsake' : 'apology';
    const ownerId = user?.id || null;
    const templateId = M.id;
    const isArchive = templateId === 'the-us-archive';
    const root = React.useRef(null);
    const previewFrame = React.useRef(null);
    const session = React.useRef(templateId + '-' + Date.now().toString(36));
    const alive = React.useRef(true);
    const latestScope = React.useRef('');
    const scope = [templateId, projectId || 'new', ownerId || 'anonymous'].join(':');
    latestScope.current = scope;
    const [scopeOwner, setScopeOwner] = React.useState(ownerId);
    const [draft, setDraft] = React.useState(() => apologyInitialDraft(M, projectId));
    const [activeProjectId, setActiveProjectId] = React.useState(null);
    const [loadedProjectId, setLoadedProjectId] = React.useState(null);
    const [loadedOwnerId, setLoadedOwnerId] = React.useState(null);
    const [loading, setLoading] = React.useState(Boolean(projectId));
    const [loadError, setLoadError] = React.useState('');
    const [loadVersion, setLoadVersion] = React.useState(0);
    const [recovery, setRecovery] = React.useState(null);
    const sections = React.useMemo(() => [...(wedding ? M.sections : apologyEditorSections(M)), { id: 'music', label: 'Music', hint: 'Add a personal soundtrack, keep the original sound or turn music off.' }], [M, wedding]);
    const [tab, setTab] = React.useState(wedding ? 'couple' : 'about');
    const [device, setDevice] = React.useState('phone');
    const [inlinePreview, setInlinePreview] = React.useState(() => !window.matchMedia || window.matchMedia('(min-width: 821px)').matches);
    const [status, setStatus] = React.useState('Your changes stay on this device until you save a draft.');
    const [statusError, setStatusError] = React.useState(false);
    const [busy, setBusy] = React.useState('');
    const [pendingAction, setPendingAction] = React.useState('');
    const [shareSlug, setShareSlug] = React.useState(null);
    const [uploads, setUploads] = React.useState({});
    const [photoEditors, setPhotoEditors] = React.useState({});
    const photoRequests = React.useRef({});
    const photoError = React.useRef(false);
    const [previewReady, setPreviewReady] = React.useState(false);
    const [previewError, setPreviewError] = React.useState('');
    const [previewVersion, setPreviewVersion] = React.useState(0);
    const [isPreviewMode, setIsPreviewMode] = React.useState(false);
    const [wholePreviewSrc, setWholePreviewSrc] = React.useState('');
    const { enterPreview, returnToEditor } = useBuilderPreviewNavigation(isPreviewMode, setIsPreviewMode);
    const saveSequence = React.useRef(0);
    const skipAutosave = React.useRef(false);
    const operation = React.useRef(false);
    const config = React.useMemo(() => M.create(draft), [M, draft]);
    const latestConfig = React.useRef(config);
    latestConfig.current = config;
    const blocked = scopeOwner !== ownerId || loading || Boolean(loadError) || Boolean(recovery) || Boolean(projectId && (loadedOwnerId !== ownerId || (loadedProjectId !== projectId && activeProjectId !== projectId)));
    const uploading = Object.values(uploads).some(Boolean);
    const canAct = !blocked && !busy && !uploading;
    const tabs = sections.map(section => [section.id, section.label]);
    const currentSection = sections.find(section => section.id === tab) || sections[0];
    const sectionOptions = (M.sectionOptions || M.storySections || []).map(section => ({ id: section.id, label: section.label, defaultEnabled: M.isSectionEnabled ? M.isSectionEnabled(draft, section.id) : true }));
    const sectionIndex = sections.indexOf(currentSection);
    const sectionIsEnabled = id => !sectionOptions.some(section => section.id === id && (draft.sections?.[id] ?? section.defaultEnabled) === false);
    const sectionHidden = !sectionIsEnabled(currentSection.id);
    const requestedPreviewSection = ['about', 'music'].includes(currentSection.id) ? sections[1]?.id : currentSection.id;
    const previewSectionId = sectionIsEnabled(requestedPreviewSection) ? requestedPreviewSection : sectionOptions.find(section => sectionIsEnabled(section.id))?.id;
    const modalSection = React.useRef(null);
    const notify = (text, error = false) => { setStatus(text); setStatusError(error); };
    const isCurrent = captured => alive.current && latestScope.current === captured;
    React.useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []);
    React.useEffect(() => { photoRequests.current = {}; photoError.current = false; setPhotoEditors({}); return () => { photoRequests.current = {}; }; }, [scope]);
    React.useEffect(() => {
        if (!window.matchMedia)
            return;
        const query = window.matchMedia('(min-width: 821px)');
        const sync = () => setInlinePreview(query.matches);
        sync();
        if (query.addEventListener)
            query.addEventListener('change', sync);
        else
            query.addListener?.(sync);
        return () => {
            if (query.removeEventListener)
                query.removeEventListener('change', sync);
            else
                query.removeListener?.(sync);
        };
    }, []);
    React.useEffect(() => {
        const editor = root.current;
        if (inlinePreview || isPreviewMode || !editor)
            return;
        let frame;
        const keepVisible = () => {
            if (frame)
                window.cancelAnimationFrame(frame);
            frame = window.requestAnimationFrame(() => apologyKeepFocusedFieldVisible(editor));
        };
        editor.addEventListener('focusin', keepVisible);
        window.addEventListener('resize', keepVisible);
        window.visualViewport?.addEventListener('resize', keepVisible);
        return () => {
            if (frame)
                window.cancelAnimationFrame(frame);
            editor.removeEventListener('focusin', keepVisible);
            window.removeEventListener('resize', keepVisible);
            window.visualViewport?.removeEventListener('resize', keepVisible);
        };
    }, [inlinePreview, isPreviewMode]);
    const writeConfig = React.useCallback(value => {
        if (latestScope.current !== scope)
            throw new Error('Your account changed. Open this design again before saving.');
        writeBuilderConfig(M.key, value);
    }, [M, scope]);
    React.useEffect(() => {
        if (scopeOwner === ownerId)
            return;
        const arrivingFromAnonymous = scopeOwner === null && ownerId !== null && !projectId;
        if (!arrivingFromAnonymous) {
            setDraft(apologyInitialDraft(M, projectId));
            setActiveProjectId(null);
            setLoadedProjectId(null);
            setLoadedOwnerId(null);
            setPendingAction('');
            setShareSlug(null);
            setRecovery(null);
            setUploads({});
            setLoadError('');
            setLoading(Boolean(projectId));
        }
        setScopeOwner(ownerId);
    }, [ownerId, scopeOwner, projectId, M]);
    const acceptProject = React.useCallback((project, source, restored = false) => {
        const value = M.create(source);
        writeConfig(value);
        skipAutosave.current = true;
        if (!restored)
            markBuilderDraftSaved(templateId, project.id, value);
        setDraft(value);
        setActiveProjectId(project.id);
        setLoadedProjectId(project.id);
        setLoadedOwnerId(ownerId);
        setRecovery(null);
        setLoading(false);
        setLoadError('');
        notify(restored ? 'Your latest edits are restored. Save draft to update your account.' : 'Your saved design is ready.');
    }, [M, ownerId, templateId, writeConfig]);
    React.useEffect(() => {
        if (!projectId || (loadedProjectId === projectId && loadedOwnerId === ownerId))
            return;
        let cancelled = false;
        const captured = scope;
        setLoading(true);
        setLoadError('');
        setRecovery(null);
        if (!api?.isLoggedIn()) {
            setLoading(false);
            setLoadError('Sign in to open your saved design.');
            return;
        }
        api.getProject(projectId).then(({ project }) => {
            if (cancelled || !isCurrent(captured))
                return;
            if (!project || project.template_id !== templateId)
                throw new Error('This saved project does not use ' + M.name + '.');
            const local = readBuilderRecovery(templateId, project);
            if (local) {
                setRecovery({ project, local, ownerId });
                setLoading(false);
                return;
            }
            acceptProject(project, project.config);
        }).catch(error => {
            if (!cancelled && isCurrent(captured)) {
                setLoading(false);
                setLoadError(error.message || 'Your saved design could not load.');
            }
        });
        return () => { cancelled = true; };
    }, [projectId, ownerId, templateId, M.name, scope, loadVersion, acceptProject, loadedProjectId, loadedOwnerId]);
    const sendPreview = React.useCallback(target => {
        if (blocked || !target)
            return;
        const frame = Array.from(root.current?.querySelectorAll('iframe') || []).find(item => item.contentWindow === target);
        const draftKey = frame?.src ? new URL(frame.src, location.origin).searchParams.get('draftKey') : null;
        target.postMessage({ type: 'everafter:' + previewProtocol + '-config', templateId, draftKey, config: latestConfig.current }, location.origin);
    }, [blocked, templateId, previewProtocol]);
    const sendSection = React.useCallback((target, sectionId) => {
        if (!blocked && target && sectionId)
            target.postMessage({ type: 'everafter:' + previewProtocol + '-section', templateId, sectionId }, location.origin);
    }, [blocked, templateId, previewProtocol]);
    React.useEffect(() => {
        const ready = event => {
            if (event.origin !== location.origin || event.data?.type !== 'everafter:' + previewProtocol + '-ready' || event.data?.templateId !== templateId)
                return;
            const frames = Array.from(root.current?.querySelectorAll('iframe') || []);
            if (!frames.some(frame => frame.contentWindow === event.source))
                return;
            sendPreview(event.source);
            if (previewFrame.current?.contentWindow === event.source) {
                setPreviewReady(true);
                setPreviewError('');
            }
            else if (modalSection.current)
                sendSection(event.source, modalSection.current);
        };
        window.addEventListener('message', ready);
        return () => window.removeEventListener('message', ready);
    }, [templateId, previewProtocol, sendPreview, sendSection]);
    React.useEffect(() => {
        if (previewReady && !isPreviewMode)
            sendSection(previewFrame.current?.contentWindow, previewSectionId);
    }, [previewSectionId, previewReady, isPreviewMode, sendSection]);
    React.useEffect(() => {
        if (!blocked)
            root.current?.querySelectorAll('iframe').forEach(frame => sendPreview(frame.contentWindow));
    }, [config, blocked, sendPreview]);
    React.useEffect(() => {
        if (blocked || busy || uploading)
            return;
        if (skipAutosave.current) {
            skipAutosave.current = false;
            return;
        }
        const sequence = ++saveSequence.current, captured = scope;
        let cancelled = false;
        try {
            writeConfig(config);
            markBuilderDraftPending(templateId, activeProjectId);
            if (!photoError.current)
                notify('Saved on this device.');
        }
        catch (error) {
            notify('Could not save on this device. ' + error.message, true);
            return;
        }
        const timer = setTimeout(async () => {
            if (!activeProjectId || !api?.isLoggedIn() || window.EA_UI_PREVIEW === true || !isCurrent(captured))
                return;
            try {
                validateBuilderMedia(config);
                await api.saveProject(activeProjectId, config, builderProjectName(config, templateId));
                if (!isCurrent(captured))
                    return;
                markBuilderDraftSaved(templateId, activeProjectId, config);
                if (!cancelled && sequence === saveSequence.current && !photoError.current)
                    notify(api.isAdminEditor ? 'Saved for the customer.' : 'Saved to your account.');
            }
            catch (error) {
                if (!cancelled && isCurrent(captured) && sequence === saveSequence.current)
                    notify('Your account could not be updated. ' + error.message, true);
            }
        }, 900);
        return () => { cancelled = true; clearTimeout(timer); };
    }, [config, activeProjectId, blocked, busy, uploading, scope, templateId, writeConfig]);
    const previewSrc = React.useMemo(() => {
        const url = new URL(buildBuilderPreviewSrc(resolveWeddingAssetUrl(template.builderTemplatePath), templateId, previewVersion, session.current));
        url.searchParams.set('skipIntro', '1');
        url.searchParams.set('silentPreview', '1');
        return url.href;
    }, [template.builderTemplatePath, templateId, previewVersion, ownerId, activeProjectId]);
    React.useEffect(() => { setPreviewReady(false); setPreviewError(''); }, [previewSrc, isPreviewMode, inlinePreview]);
    React.useEffect(() => {
        if (blocked || previewReady || isPreviewMode || !inlinePreview)
            return;
        const timer = setTimeout(() => setPreviewError('The preview is taking a little longer. Your words are still here.'), 20000);
        return () => clearTimeout(timer);
    }, [blocked, previewReady, previewSrc, isPreviewMode, inlinePreview]);
    const update = (path, value) => {
        if (blocked || busy || !isCurrent(scope))
            return;
        photoError.current = false;
        setDraft(previous => setDeepDraft(previous, path, value));
    };
    const act = React.useCallback(async (action) => {
        if (blocked || uploading || operation.current)
            return;
        const issues = typeof M.validate === 'function' ? M.validate(latestConfig.current, { required: action === 'publish' }) : [];
        if (issues.length) {
            if (wedding) {
                notify(issues[0], true);
                return;
            }
            const value = latestConfig.current;
            const missingName = !value.from?.trim() || !value.to?.trim();
            const missingCore = ['headline', 'message', 'closing'].find(key => !value[key]?.trim() && sections.some(section => section.core === key && value.sections?.[section.id] !== false));
            const missingSection = missingName ? 'about' : value.sections?.opening !== false && !value.copy?.openingTitle?.trim() ? 'opening' : sections.find(section => section.core === missingCore)?.id;
            if (action === 'publish' && missingSection)
                setTab(missingSection);
            const guidance = missingName ? 'Add both names before publishing.' : missingSection === 'opening' ? 'Write an opening title before publishing.' : missingCore ? 'Write your ' + ({ headline: 'main headline', message: 'personal message', closing: 'closing note' })[missingCore] + ' before publishing.' : issues[0];
            notify(action === 'publish' ? guidance : issues[0], true);
            return;
        }
        if (!api?.isLoggedIn() && window.EA_UI_PREVIEW !== true) {
            try {
                writeConfig(latestConfig.current);
            }
            catch (_) { }
            setPendingAction(action);
            notify('Sign in to save this design to your account.');
            openSignIn?.('builder');
            return;
        }
        const captured = scope;
        operation.current = true;
        setBusy(action);
        setPendingAction('');
        try {
            const args = { api, templateId, config: latestConfig.current, writeConfig, projectId: activeProjectId, setProjectId: value => { if (isCurrent(captured))
                    setActiveProjectId(value); } };
            const result = action === 'publish' ? await runPublish(args) : await runSaveDraft(args);
            if (!isCurrent(captured))
                return;
            if (!result.ok)
                throw new Error(result.reason === 'project_not_found' ? 'This saved project is no longer available. Reopen it from your dashboard.' : result.reason || 'Please try again.');
            if (result.projectId) {
                setLoadedProjectId(result.projectId);
                setLoadedOwnerId(ownerId);
            }
            if (result.slug)
                setShareSlug(result.slug);
            skipAutosave.current = true;
            notify(result.local ? 'Draft saved on this device for this UI preview.' : action === 'publish' ? 'Your personal link is ready to share.' : 'Draft saved to your account.');
        }
        catch (error) {
            if (isCurrent(captured)) {
                skipAutosave.current = true;
                notify('Could not ' + (action === 'publish' ? 'publish' : 'save') + '. ' + error.message, true);
            }
        }
        finally {
            operation.current = false;
            if (isCurrent(captured))
                setBusy('');
        }
    }, [M, wedding, blocked, uploading, scope, templateId, activeProjectId, writeConfig, ownerId, openSignIn, sections]);
    React.useEffect(() => {
        if (pendingAction && api?.isLoggedIn() && !blocked)
            act(pendingAction);
    }, [pendingAction, user, blocked, act]);
    const upload = async (index, event, group = 'photos') => {
        const file = event.target.files?.[0];
        event.target.value = '';
        const uploadKey = group + ':' + index;
        if (!file || blocked || busy || uploads[uploadKey] || photoRequests.current[uploadKey]?.scope === scope)
            return;
        if (!api?.isLoggedIn()) {
            notify('Sign in, then choose your photograph again.', true);
            openSignIn?.('builder');
            return;
        }
        if (window.EA_UI_PREVIEW === true) {
            notify('Photo uploads are unavailable in this UI preview.', true);
            return;
        }
        if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
            notify('Choose a JPG, PNG or WebP photograph.', true);
            return;
        }
        if (!file.size || file.size > 20000000) {
            notify('Choose a photograph smaller than 20 MB.', true);
            return;
        }
        if (group === 'filmPhotos') {
            if (file.size > 3000000) {
                notify('Choose a film photograph smaller than 3 MB.', true);
                return;
            }
        }
        const captured = scope, capturedOwner = ownerId, token = { scope };
        photoRequests.current[uploadKey] = token;
        photoError.current = false;
        const current = () => isCurrent(captured) && photoRequests.current[uploadKey] === token && api?.isLoggedIn() && (!api.currentUser || api.currentUser()?.id === capturedOwner);
        setUploads(previous => ({ ...previous, [uploadKey]: true }));
        try {
            const result = await api.uploadImage(file, activeProjectId || undefined);
            if (!current())
                return;
            const url = result?.url || result?.upload?.url;
            let secure = false;
            try {
                const parsed = new URL(url);
                secure = typeof url === 'string' && parsed.protocol === 'https:' && !parsed.username && !parsed.password;
            }
            catch (_) { }
            if (!secure)
                throw new Error('The photograph could not be confirmed. Please try again.');
            setDraft(previous => setDeepDraft(previous, [group, index, 'src'], url));
            notify((group === 'filmPhotos' ? 'Film photograph ' : 'Photograph ') + (index + 1) + ' is ready.');
        }
        catch (error) {
            if (current()) {
                photoError.current = true;
                notify(error.message || 'Your photograph could not upload. Please try again.', true);
            }
        }
        finally {
            if (photoRequests.current[uploadKey] === token) {
                delete photoRequests.current[uploadKey];
                if (isCurrent(captured))
                    setUploads(previous => ({ ...previous, [uploadKey]: false }));
            }
        }
    };
    const changePhotoList = (group, index, role) => {
        if (!canAct || Object.keys(photoRequests.current).length)
            return;
        const count = (latestConfig.current[group] || []).length;
        if (index === null && count >= (M.maxPhotos || 20))
            return;
        setPhotoEditors(previous => {
            if (index === null)
                return { ...previous, [group + ':' + count]: true };
            const next = {};
            Object.entries(previous).forEach(([key, value]) => {
                const [collection, position] = key.split(':');
                if (collection !== group)
                    next[key] = value;
                else if (Number(position) !== index)
                    next[collection + ':' + (Number(position) > index ? Number(position) - 1 : position)] = value;
            });
            return next;
        });
        setDraft(previous => {
            const items = [...(previous[group] || [])];
            if (index === null) {
                if (items.length >= (M.maxPhotos || 20))
                    return previous;
                items.push(group === 'photos' ? { src: '', caption: '', ...(role ? { role } : {}) } : { src: '' });
            }
            else
                items.splice(index, 1);
            const next = { ...previous, [group]: items };
            if (group === 'photos' && Array.isArray(previous.memoryNotes)) {
                next.memoryNotes = [...previous.memoryNotes];
                if (index === null)
                    next.memoryNotes.push('');
                else
                    next.memoryNotes.splice(index, 1);
            }
            return next;
        });
    };
    const openStoryPreview = (event, sectionId) => {
        if (!canAct)
            return;
        try {
            writeConfig(latestConfig.current);
            const url = new URL(buildBuilderPreviewSrc(resolveWeddingAssetUrl(template.builderTemplatePath), templateId, Date.now(), session.current));
            modalSection.current = sectionId || null;
            if (sectionId)
                url.searchParams.set('previewSection', sectionId);
            if (sectionId && sectionId !== sections[1]?.id)
                url.searchParams.set('skipIntro', '1');
            else
                url.searchParams.set('intro', '1');
            url.searchParams.set('silentPreview', '1');
            setWholePreviewSrc(url.href);
            enterPreview(event);
        }
        catch (error) {
            notify('Preview could not open. ' + error.message, true);
        }
    };
    const showWholePreview = event => openStoryPreview(event, null);
    const showSectionPreview = event => openStoryPreview(event, previewSectionId);
    const photoDisclosure = (key, photo) => ({
        open: photoEditors[key] ?? !photo.src,
        onToggle: event => { const open = event.currentTarget.open; setPhotoEditors(previous => previous[key] === open ? previous : { ...previous, [key]: open }); }
    });
    const photoSummary = (photo, label, uploadingPhoto) => React.createElement("summary", { className: "ap-builder-photo-summary" },
        React.createElement("span", { className: 'ap-builder-photo-thumb' + (photo.src ? ' has-photo' : ''), "aria-hidden": "true" }, photo.src ? React.createElement("img", { src: photo.src, alt: "", loading: "lazy" }) : React.createElement("svg", { viewBox: "0 0 48 40" },
            React.createElement("rect", { x: "3", y: "3", width: "42", height: "34", rx: "4" }),
            React.createElement("circle", { cx: "15", cy: "13", r: "4" }),
            React.createElement("path", { d: "m5 31 12-11 8 7 8-12 10 16" }))),
        React.createElement("span", { className: "ap-builder-photo-summary-copy" },
            React.createElement("strong", null, label),
            React.createElement("span", null, uploadingPhoto ? 'Uploading photograph…' : photo.src ? (photo.caption?.trim() || 'Ready to edit') : 'Choose a photograph')),
        React.createElement("svg", { className: "ap-builder-photo-chevron", viewBox: "0 0 20 20", "aria-hidden": "true" },
            React.createElement("path", { d: "m6 8 4 4 4-4" })));
    const field = (label, path, { multiline = false, limit = 1200, maxLength, hint = '', rows = 3, placeholder = '', type = 'text' } = {}) => {
        const value = path.reduce((item, key) => item?.[key], draft) ?? '';
        const key = path.join('-');
        const id = 'apology-' + templateId + '-' + key;
        const labeling = { id, 'aria-labelledby': id + '-label', 'aria-describedby': hint ? id + '-hint' : undefined };
        return React.createElement("label", { className: "builder-field ap-builder-field", key: key, htmlFor: id },
            React.createElement("span", { id: id + '-label' }, label),
            type === 'checkbox' ? React.createElement("input", { ...labeling, type: "checkbox", checked: Boolean(value), onChange: event => update(path, event.target.checked) }) : multiline ? React.createElement("textarea", { ...labeling, rows: rows, value: value, maxLength: maxLength || limit, placeholder: placeholder, onChange: event => update(path, event.target.value) }) : React.createElement("input", { ...labeling, type: type, value: value, maxLength: maxLength || limit, placeholder: placeholder, onInput: type === 'date' || type === 'time' ? event => update(path, event.target.value) : undefined, onChange: event => update(path, event.target.value) }),
            hint && React.createElement("small", { id: id + '-hint' }, hint));
    };
    const copyFields = !wedding && Array.isArray(currentSection.fields) ? currentSection.fields.filter(item => templateId !== 'the-moonlit-post' || !/^memory(?:Note|Label)[1-3]$/.test(item.key)) : [];
    const primaryFields = copyFields.filter(item => !item.secondary);
    const secondaryFields = copyFields.filter(item => item.secondary);
    const coreFields = wedding ? [] : apologyCoreFields(currentSection, isArchive);
    const moonlitPhotos = templateId === 'the-moonlit-post';
    const photoRole = moonlitPhotos && ['opening', 'hero', 'photos'].includes(currentSection.id) ? currentSection.id === 'photos' ? 'postcard' : 'painting' : null;
    const hasPhotos = Boolean(photoRole) || currentSection.core === 'photos' || currentSection.core?.includes?.('photos');
    const matchingPhotos = (draft.photos || []).map((photo, index) => ({ photo, index })).filter(({ photo, index }) => !photoRole || (photo.role || (index < 3 ? 'painting' : 'postcard')) === photoRole);
    const visiblePhotos = moonlitPhotos && currentSection.id === 'opening' ? matchingPhotos.slice(0, 1) : matchingPhotos;
    const filmPhotos = Array.isArray(draft.filmPhotos) ? draft.filmPhotos : [];
    const filmHelpId = 'apology-' + templateId + '-film-photos-help';
    const renderCopyField = item => field(item.label, ['copy', item.key], { ...item, multiline: item.multiline ?? (item.maxLength > 120 || /\n/.test(item.defaultValue || '')), rows: item.rows || (/title/i.test(item.key) ? 2 : 3), placeholder: item.placeholder || item.defaultValue || '', hint: item.hint || '' });
    const renderCoreField = item => field(item.label, item.path, { ...item, placeholder: item.placeholder || item.path.reduce((value, key) => value?.[key], M.defaults) || '' });
    const weddingFields = wedding && currentSection.id !== 'music' ? WeddingKeepsakeFields({ model: M, section: currentSection, draft, field, update, disabled: !canAct }) : null;
    const hasWording = primaryFields.length > 0 || coreFields.length > 0 || Boolean(weddingFields);
    const renderedFields = hasWording && React.createElement("div", { className: 'builder-fields' + (currentSection.id === 'about' ? ' ap-builder-name-pair' : '') },
        primaryFields.map(renderCopyField),
        coreFields.map(renderCoreField),
        weddingFields);
    const fillSectionExamples = () => {
        if (!canAct || currentSection.id === 'about')
            return;
        setDraft(previous => {
            let next = previous;
            const fill = (path, example) => { const value = path.reduce((node, key) => node?.[key], next); if ((typeof value !== 'string' || !value.trim()) && typeof example === 'string' && example)
                next = setDeepDraft(next, path, example); };
            copyFields.forEach(item => fill(['copy', item.key], item.defaultValue));
            coreFields.forEach(item => fill(item.path, item.path.reduce((value, key) => value?.[key], M.defaults)));
            if (hasPhotos)
                visiblePhotos.forEach(({ index }) => { fill(['photos', index, 'caption'], M.defaults.photos[index]?.caption); if (isArchive && M.defaults.memoryNotes)
                    fill(['memoryNotes', index], M.defaults.memoryNotes[index]); });
            return next;
        });
    };
    const changeTab = id => {
        if (blocked || busy)
            return;
        setTab(id);
        root.current?.querySelector('.saas-builder-form')?.scrollTo({ top: 0, behavior: 'instant' });
        if (!inlinePreview)
            root.current?.querySelector('.builder-section-picker')?.scrollIntoView({ block: 'start', behavior: 'instant' });
    };
    return React.createElement("div", { ref: root, className: 'saas-template-builder apology-builder' + (wedding ? ' wedding-keepsake-builder' : '') + (isArchive ? ' apology-builder--archive' : '') },
        shareSlug && React.createElement(ShareModal, { slug: shareSlug, onClose: () => setShareSlug(null) }),
        React.createElement("aside", { className: "saas-builder-side" },
            React.createElement("div", { className: "saas-template-switch" },
                React.createElement("div", null,
                    React.createElement("span", null, wedding ? 'Your wedding invitation' : 'A little more you'),
                    React.createElement("strong", null, M.name)),
                React.createElement("button", { type: "button", className: "ap-builder-change-design", disabled: Boolean(busy), onClick: () => goTo('builder', { projectId: null }) }, api.isAdminEditor ? 'Administration' : 'Change design')),
            React.createElement("label", { className: "builder-section-picker" },
                React.createElement("span", null, wedding ? 'Your invitation, section by section' : 'Your story, section by section'),
                React.createElement("select", { "aria-label": "Editing section", value: currentSection.id, disabled: blocked || Boolean(busy), onChange: event => changeTab(event.target.value) }, tabs.map(([id, label], index) => React.createElement("option", { key: id, value: id },
                    String(index + 1).padStart(2, '0'),
                    " \u00B7 ",
                    label)))),
            React.createElement("div", { className: "saas-builder-form" },
                loading && React.createElement("p", { role: "status" }, "Opening your saved words and memories\u2026"),
                recovery?.ownerId === ownerId && React.createElement(BuilderRecoveryNotice, { onRestore: () => acceptProject(recovery.project, recovery.local, true), onUseSaved: () => acceptProject(recovery.project, recovery.project.config) }),
                loadError && React.createElement("div", { className: "ap-builder-load-error", role: "alert" },
                    React.createElement("p", null, loadError),
                    React.createElement("button", { type: "button", className: "btn btn-secondary", onClick: () => api?.isLoggedIn() ? setLoadVersion(value => value + 1) : openSignIn?.('builder') }, api?.isLoggedIn() ? 'Try again' : 'Sign in')),
                React.createElement("fieldset", { className: "ap-builder-fieldset", disabled: blocked || Boolean(busy) },
                    React.createElement(TemplateSectionsPanel, { sections: sectionOptions, value: draft.sections, onChange: value => update(['sections'], value), disabled: !canAct }),
                    React.createElement("section", { className: "builder-panel ap-builder-story-section", key: currentSection.id },
                        React.createElement("div", { className: "ap-builder-step" },
                            React.createElement("span", null,
                                "Section ",
                                sectionIndex + 1,
                                " of ",
                                sections.length),
                            sectionIndex > 0 && React.createElement("button", { type: "button", onClick: () => changeTab(sections[sectionIndex - 1].id) }, "\u2190 Previous")),
                        React.createElement("div", { className: "builder-panel-title" },
                            React.createElement("h3", { tabIndex: "-1" }, currentSection.label)),
                        React.createElement("p", { className: "ap-builder-guidance" }, hasPhotos ? (photoRole === 'postcard' ? 'Add photographs with a caption and a note on the back.' : photoRole === 'painting' ? 'Choose photos for your painted gallery. The first becomes your opening portrait.' : 'Add only the photographs you want to share. Photos are optional.') : currentSection.hint || 'Write the words you want them to find in this moment.'),
                        sectionHidden && React.createElement("aside", { className: "ap-builder-section-hidden", role: "note" },
                            React.createElement("strong", null, "Hidden from guests"),
                            React.createElement("p", null, "Your content is kept. Turn this section on in the section settings above to show it.")),
                        currentSection.id !== 'music' && React.createElement("div", { className: "ap-builder-section-tools" },
                            React.createElement("button", { type: "button", className: "btn btn-secondary", disabled: !canAct, onClick: sectionHidden ? showWholePreview : showSectionPreview }, sectionHidden ? 'Preview guest view' : 'Preview this section'),
                            !wedding && currentSection.id !== 'about' && React.createElement("button", { type: "button", className: "ap-builder-example", disabled: !canAct, onClick: fillSectionExamples }, "Use example wording")),
                        !hasPhotos && !wedding && !['about', 'music'].includes(currentSection.id) && React.createElement("p", { className: "ap-builder-example-hint" }, "Examples fill empty fields only."),
                        !hasPhotos && renderedFields,
                        currentSection.id === 'hero' && M.filmPhotoCount > 0 && React.createElement("section", { className: "ap-builder-film-photos", "aria-labelledby": filmHelpId + '-title' },
                            React.createElement("div", { className: "ap-builder-film-heading" },
                                React.createElement("h4", { id: filmHelpId + '-title' }, "Your first-screen photos"),
                                React.createElement("span", null, "Optional")),
                            React.createElement("p", { id: filmHelpId },
                                "These photos play in order",
                                isArchive ? ', after the camera opening and on the main screen' : ', on the main screen',
                                ". Leave this empty to keep the original film."),
                            React.createElement("div", { className: "ap-builder-film-list" }, filmPhotos.map((photo, index) => {
                                const uploadKey = 'filmPhotos:' + index;
                                const uploadingPhoto = Boolean(uploads[uploadKey]);
                                const disabled = blocked || Boolean(busy) || uploadingPhoto;
                                return React.createElement("details", { className: "ap-builder-photo-editor ap-builder-film-photo", key: index, ...photoDisclosure(uploadKey, photo) },
                                    photoSummary(photo, 'Film photo ' + (index + 1), uploadingPhoto),
                                    React.createElement("div", { className: "ap-builder-photo-editor-body" },
                                        React.createElement("div", { className: "ap-builder-film-controls" },
                                            React.createElement("label", { className: 'ap-builder-upload' + (uploadingPhoto ? ' is-busy' : '') },
                                                React.createElement("input", { type: "file", accept: "image/jpeg,image/png,image/webp", "aria-label": (photo.src ? 'Replace' : 'Choose') + ' film photo ' + (index + 1), "aria-describedby": filmHelpId + '-formats', disabled: disabled || window.EA_UI_PREVIEW === true, onChange: event => upload(index, event, 'filmPhotos') }),
                                                React.createElement("span", null, uploadingPhoto ? 'Uploading…' : photo.src ? 'Replace photo' : 'Choose photo')),
                                            React.createElement("button", { type: "button", className: "ap-builder-film-remove", "aria-label": 'Remove film photo ' + (index + 1), disabled: !canAct, onClick: () => changePhotoList('filmPhotos', index) }, "Remove"))));
                            })),
                            React.createElement("button", { type: "button", className: "btn btn-secondary", disabled: !canAct || filmPhotos.length >= (M.maxPhotos || 20), onClick: () => changePhotoList('filmPhotos', null) }, "Add film photo"),
                            React.createElement("small", { id: filmHelpId + '-formats', className: "ap-builder-upload-note" }, "JPG, PNG or WebP \u00B7 Up to 3 MB each. These are separate from your memory photos."),
                            window.EA_UI_PREVIEW === true && React.createElement("p", { className: "ap-builder-upload-note" }, "Photo uploads are unavailable in this design preview.")),
                        hasPhotos && React.createElement("div", { className: "builder-fields ap-builder-memories" },
                            React.createElement("h4", null, photoRole === 'painting' ? (currentSection.id === 'opening' ? 'Your painted portrait' : 'Your painted gallery') : photoRole === 'postcard' ? 'Your memory postcards' : 'Your photographs'),
                            React.createElement("p", { className: "ap-builder-quiet-note" }, photoRole === 'painting' ? 'The first painting becomes your opening pencil portrait, then fills with colour.' : photoRole === 'postcard' ? 'Each postcard has a photograph, caption and personal note on the back.' : 'Photos are optional. Add the moments you want to share.'),
                            !visiblePhotos.length && React.createElement("p", { className: "ap-builder-photo-empty-note" }, "No photos yet. Add one below, or leave this area out."),
                            React.createElement("div", { className: "ap-builder-photo-list" }, visiblePhotos.map(({ photo, index }, position) => React.createElement("details", { className: "ap-builder-photo-editor ap-builder-memory", key: index, ...photoDisclosure('photos:' + index, photo) },
                                photoSummary(photo, photoRole === 'painting' ? (position === 0 ? 'Opening portrait' : 'Painting ' + (position + 1)) : photoRole === 'postcard' ? 'Postcard ' + (position + 1) : 'Photo ' + (position + 1), Boolean(uploads['photos:' + index])),
                                React.createElement("div", { className: "ap-builder-photo-editor-body" },
                                    photo.src && React.createElement("img", { className: "ap-builder-photo", src: photo.src, alt: 'Photo ' + (position + 1) + ' preview', loading: "lazy" }),
                                    React.createElement("div", { className: "ap-builder-photo-actions" },
                                        React.createElement("label", { className: 'ap-builder-upload' + (uploads['photos:' + index] ? ' is-busy' : '') },
                                            React.createElement("input", { type: "file", accept: "image/jpeg,image/png,image/webp", "aria-label": 'Choose photograph ' + (position + 1), disabled: blocked || Boolean(busy) || Boolean(uploads['photos:' + index]) || window.EA_UI_PREVIEW === true, onChange: event => upload(index, event) }),
                                            React.createElement("span", null, uploads['photos:' + index] ? 'Uploading photograph...' : photo.src ? 'Replace photograph' : 'Choose photograph')),
                                        React.createElement("button", { type: "button", className: "ap-builder-photo-remove", disabled: !canAct, onClick: () => changePhotoList('photos', index) },
                                            "Remove photo ",
                                            position + 1)),
                                    field(photoRole === 'postcard' ? 'Postcard caption' : 'Photo caption', ['photos', index, 'caption'], { limit: 500, placeholder: 'A moment worth keeping' }),
                                    photoRole === 'postcard' && field('The note on the back', ['photos', index, 'note'], { multiline: true, limit: 800, placeholder: 'What would you like them to find?' }),
                                    photoRole === 'postcard' && field('A small label on the back', ['photos', index, 'label'], { limit: 140, placeholder: 'A little memory' }),
                                    M.memoryNoteCount > 0 && Array.isArray(draft.memoryNotes) && field('The little note behind this photograph', ['memoryNotes', index], { multiline: true, limit: 800, placeholder: 'Something you want to remember' }))))),
                            !(moonlitPhotos && currentSection.id === 'opening' && visiblePhotos.length > 0) && React.createElement("button", { type: "button", className: "btn btn-secondary", disabled: !canAct || (draft.photos || []).length >= (M.maxPhotos || 20), onClick: () => changePhotoList('photos', null, photoRole) },
                                "Add ",
                                photoRole === 'postcard' ? 'postcard' : photoRole === 'painting' ? 'painting' : 'photo'),
                            React.createElement("small", { className: "ap-builder-upload-note" },
                                (draft.photos || []).filter(photo => photo.src).length,
                                " photos selected. Up to ",
                                M.maxPhotos || 20,
                                moonlitPhotos ? ' across paintings and postcards' : '',
                                ". JPG, PNG or WebP; up to 20 MB each."),
                            (draft.photos || []).length >= (M.maxPhotos || 20) && (draft.photos || []).some(photo => !photo.src) && React.createElement("small", { className: "ap-builder-upload-note" }, "Choose a photograph in an empty entry, or remove that entry to add another.")),
                        hasPhotos && hasWording && React.createElement("details", { className: "ap-builder-secondary ap-builder-wording" },
                            React.createElement("summary", null, "Headings & wording"),
                            !wedding && React.createElement("p", { className: "ap-builder-example-hint" }, "Examples fill empty fields only."),
                            renderedFields),
                        secondaryFields.length > 0 && React.createElement("details", { className: "ap-builder-secondary" },
                            React.createElement("summary", null,
                                React.createElement("span", null, "Small captions & buttons"),
                                React.createElement("small", null,
                                    secondaryFields.length,
                                    " optional details")),
                            React.createElement("p", null, "Adjust the little labels and instructions around this scene. You can keep the examples or make them your own."),
                            React.createElement("div", { className: "builder-fields" }, secondaryFields.map(renderCopyField))),
                        currentSection.core === 'closing' && React.createElement("div", { className: "ap-builder-endnote" },
                            React.createElement("span", { "aria-hidden": "true" }, "\u2661"),
                            React.createElement("p", null, "Read it once as if you were receiving it."),
                            React.createElement("button", { type: "button", className: "btn btn-secondary", disabled: !canAct, onClick: showWholePreview }, "Watch from the beginning"))),
                    React.createElement("div", { hidden: currentSection.id !== 'music' },
                        React.createElement(MusicUploadPanel, { value: draft.music, templateId: templateId, projectId: activeProjectId || projectId, scopeKey: projectId || 'new', user: user, disabled: blocked || Boolean(busy), onChange: value => update(['music'], value), onBusyChange: value => setUploads(previous => ({ ...previous, music: value })), openSignIn: openSignIn, active: currentSection.id === 'music' })),
                    React.createElement(BuilderProgressActions, { tabs: tabs, tab: currentSection.id, onTabChange: changeTab, onPreview: showWholePreview, disabled: !canAct }))),
            React.createElement("div", { className: "ap-builder-savebar" },
                React.createElement("p", { role: "status", className: statusError ? 'is-error' : '' }, status),
                React.createElement("div", null,
                    React.createElement("button", { type: "button", className: "btn btn-secondary", disabled: !canAct, onClick: () => act('save') }, busy === 'save' ? 'Saving…' : 'Save draft'),
                    React.createElement("button", { type: "button", className: "btn btn-primary", disabled: !canAct || window.EA_UI_PREVIEW === true, onClick: () => act('publish') }, busy === 'publish' ? 'Preparing your link…' : 'Publish & share')),
                window.EA_UI_PREVIEW === true && React.createElement(BuilderPreviewNotice, { compact: true }))),
        React.createElement("main", { className: "saas-builder-preview ap-builder-preview" },
            React.createElement("div", { className: "ap-builder-preview-toolbar" },
                React.createElement("div", null,
                    React.createElement("span", { className: "ap-builder-preview-eyebrow" }, wedding ? 'Your invitation, taking shape' : 'Your story, taking shape'),
                    React.createElement("strong", null, sectionHidden ? 'Guest preview' : currentSection.id === 'about' ? 'Live preview' : currentSection.label)),
                React.createElement("label", { className: "ap-builder-devices" },
                    React.createElement("span", null, "Preview size"),
                    React.createElement("select", { "aria-label": "Preview size", value: device, onChange: event => setDevice(event.target.value) },
                        React.createElement("option", { value: "phone" }, "Phone"),
                        React.createElement("option", { value: "desktop" }, "Desktop")))),
            React.createElement("div", { className: 'ap-builder-preview-stage ap-builder-preview-stage--' + device },
                !blocked && !isPreviewMode && inlinePreview && React.createElement("iframe", { ref: previewFrame, title: M.name + ' live preview', src: previewSrc, allow: "autoplay 'none'", onLoad: event => sendPreview(event.currentTarget.contentWindow) }),
                !blocked && !isPreviewMode && inlinePreview && !previewReady && React.createElement("div", { className: "ap-builder-preview-loading", role: "status" },
                    React.createElement("p", null, previewError || 'Opening your story…'),
                    previewError && React.createElement("button", { type: "button", className: "btn btn-secondary", onClick: () => setPreviewVersion(value => value + 1) }, "Reload preview")),
                blocked && React.createElement("div", { className: "ap-builder-preview-loading" },
                    React.createElement("p", null, "Your saved design will appear here."))),
            React.createElement("div", { className: "ap-builder-preview-footer" },
                React.createElement("p", null, "Changes appear here as you write."),
                React.createElement("button", { type: "button", className: "btn btn-secondary", disabled: !canAct, onClick: showWholePreview }, "Watch the whole experience \u2197"),
                React.createElement("small", null, "Preview only. Publish to create your personal link."))),
        React.createElement(BuilderPreviewWindow, { open: isPreviewMode, onClose: returnToEditor, activeTemplate: template, fallbackName: M.name, previewSrc: wholePreviewSrc }));
}
window.ApologyBuilder = ApologyBuilder;
