@flamingo-stack/openframe-frontend-core 0.0.720 → 0.0.721
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/assets/index.cjs +6 -0
- package/dist/assets/index.d.ts +1 -0
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +5 -1
- package/dist/assets/mobile-app-qr.d.ts +17 -0
- package/dist/assets/mobile-app-qr.d.ts.map +1 -0
- package/dist/{chunk-7IOU6BJE.js → chunk-2P2M27EW.js} +2393 -2367
- package/dist/{chunk-3HZ6XSZC.js → chunk-2TXGR7IK.js} +136 -59
- package/dist/{chunk-LO3KCLEF.js → chunk-3LY7V3RF.js} +3 -3
- package/dist/{chunk-6BLN5WDS.cjs → chunk-3R32ZNA2.cjs} +40 -40
- package/dist/{chunk-WSDT7LYL.js → chunk-3UBWDR65.js} +1 -1
- package/dist/{chunk-XW3Q623Q.js → chunk-44Q6MV7C.js} +2 -2
- package/dist/{chunk-CMZ3QTFQ.js → chunk-4I3K6LEH.js} +24 -0
- package/dist/{chunk-4SIZCHVZ.cjs → chunk-4LY6R4TN.cjs} +126 -49
- package/dist/{chunk-TORERAXD.js → chunk-57UTI7ZH.js} +6 -6
- package/dist/{chunk-K2BYVPGR.js → chunk-5QXE5HMV.js} +4 -4
- package/dist/{chunk-QF464PPL.cjs → chunk-6AJIOW3S.cjs} +57 -57
- package/dist/{chunk-B2N4YX66.js → chunk-6YBUQAOH.js} +1 -1
- package/dist/{chunk-6W46LVK4.js → chunk-74N742ZG.js} +6 -6
- package/dist/{chunk-4HJRHEXD.cjs → chunk-7Z5FMQYR.cjs} +22 -22
- package/dist/{chunk-WBNAEYTQ.cjs → chunk-BYUIFT7Q.cjs} +6 -6
- package/dist/{chunk-VF2CJXZ2.cjs → chunk-BYVVE7HP.cjs} +58 -58
- package/dist/{chunk-AZRI4XBP.js → chunk-CO3FOZIH.js} +1 -1
- package/dist/{chunk-XSM46FHD.cjs → chunk-D2DH6VZL.cjs} +3 -3
- package/dist/{chunk-KSGMZHVV.cjs → chunk-DFM572VH.cjs} +2 -2
- package/dist/{chunk-YDPKUAP4.js → chunk-DJ4IFQNQ.js} +2 -0
- package/dist/{chunk-VBHGJFA3.js → chunk-E6SMMMKV.js} +12 -12
- package/dist/{chunk-DHNJSQGX.cjs → chunk-F7ORKKEF.cjs} +103 -103
- package/dist/{chunk-RFUOIQSG.cjs → chunk-GCK75D7F.cjs} +9 -9
- package/dist/{chunk-ZHD3F2TH.cjs → chunk-GGBYZKMS.cjs} +11 -11
- package/dist/{chunk-YK3X74SF.js → chunk-GYAXV3MZ.js} +3 -3
- package/dist/{chunk-RDP5UZXQ.cjs → chunk-HJMSNOBT.cjs} +344 -323
- package/dist/{chunk-DFQYAYBH.cjs → chunk-HQIOMFJH.cjs} +2 -0
- package/dist/{chunk-PXAKSPLZ.js → chunk-HXB7SEYG.js} +27 -6
- package/dist/{chunk-HTS46FFY.cjs → chunk-I2254QN6.cjs} +16 -16
- package/dist/{chunk-ILNN6QRW.js → chunk-JDBZ4ZHU.js} +3 -3
- package/dist/{chunk-GQ7JJUIT.cjs → chunk-K2JUJ37D.cjs} +25 -1
- package/dist/{chunk-URW23QUM.js → chunk-K76ACBR3.js} +2 -2
- package/dist/{chunk-2NASBO7O.js → chunk-KFOAV77K.js} +2 -2
- package/dist/{chunk-Q5327DXK.js → chunk-LAXKEBDD.js} +10 -10
- package/dist/{chunk-O3MKJU6A.js → chunk-LGKMR7NZ.js} +4 -4
- package/dist/{chunk-5XDCCQ4A.cjs → chunk-LODMNSQY.cjs} +13 -13
- package/dist/{chunk-V6B2FBZK.js → chunk-NJ5TBTJG.js} +7 -7
- package/dist/{chunk-HFEYEPOG.cjs → chunk-NMPVK53I.cjs} +29 -29
- package/dist/{chunk-GANIUMRU.js → chunk-NPSJE7J3.js} +1 -1
- package/dist/{chunk-IL3ZWNM2.cjs → chunk-O5EHOJZD.cjs} +4 -4
- package/dist/{chunk-KT3U3ZRP.cjs → chunk-OKE5BIQH.cjs} +8 -8
- package/dist/{chunk-RZQGIWK3.cjs → chunk-QLTXT562.cjs} +2 -2
- package/dist/{chunk-PSMJYZMV.js → chunk-QQCFPMAA.js} +2 -2
- package/dist/{chunk-7LPR4RZ3.cjs → chunk-QYS4YVEP.cjs} +31 -31
- package/dist/{chunk-VYVPII4W.js → chunk-RCI3PALX.js} +6 -6
- package/dist/{chunk-P3JMAMHP.js → chunk-REETLEL5.js} +1 -1
- package/dist/{chunk-2C72EWQR.cjs → chunk-RJMWJAJO.cjs} +26 -26
- package/dist/{chunk-ISCZ6UNF.cjs → chunk-RQZQHLNS.cjs} +73 -73
- package/dist/{chunk-FFT56TYB.cjs → chunk-RUZIICXT.cjs} +2 -2
- package/dist/{chunk-7HYEQKN6.js → chunk-S63QBBT7.js} +5 -5
- package/dist/{chunk-B5LNX4GZ.js → chunk-UXU7Y5BH.js} +2 -2
- package/dist/{chunk-DYDDAA6O.cjs → chunk-UZTOI6FJ.cjs} +766 -740
- package/dist/{chunk-EVMM3JE6.cjs → chunk-VFICDEYJ.cjs} +43 -43
- package/dist/{chunk-DOPOHO6H.cjs → chunk-XE2K53GH.cjs} +18 -18
- package/dist/{chunk-UWJMDFDO.cjs → chunk-XXO52RZR.cjs} +26 -26
- package/dist/{chunk-VYZCKY5K.js → chunk-ZLUFEWZZ.js} +7 -7
- package/dist/{chunk-NYSMDM4J.cjs → chunk-ZSOFLPQN.cjs} +7 -7
- package/dist/{chunk-ZPHOQDAE.js → chunk-ZZEKXGFS.js} +1 -1
- package/dist/{cloud-tenants-UQZASXUB.cjs → cloud-tenants-HZHKRUIW.cjs} +13 -13
- package/dist/{cloud-tenants-RKQRSDNE.js → cloud-tenants-Q4TCJS5Y.js} +12 -12
- package/dist/components/calendar.cjs +4 -4
- package/dist/components/calendar.js +1 -1
- package/dist/components/case-studies/index.cjs +27 -27
- package/dist/components/case-studies/index.js +13 -13
- package/dist/components/chat/index.cjs +8 -8
- package/dist/components/chat/index.js +7 -7
- package/dist/components/contact/index.cjs +14 -14
- package/dist/components/contact/index.js +13 -13
- package/dist/components/docs/index.cjs +17 -17
- package/dist/components/docs/index.js +16 -16
- package/dist/components/embeds/index.cjs +14 -14
- package/dist/components/embeds/index.js +13 -13
- package/dist/components/faq/index.cjs +14 -14
- package/dist/components/faq/index.js +13 -13
- package/dist/components/features/index.cjs +13 -13
- package/dist/components/features/index.js +12 -12
- package/dist/components/help-center-pages/download-apps-page.d.ts +55 -0
- package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -0
- package/dist/components/help-center-pages/index.cjs +329 -114
- package/dist/components/help-center-pages/index.d.ts +1 -0
- package/dist/components/help-center-pages/index.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.js +243 -28
- package/dist/components/index.cjs +81 -79
- package/dist/components/index.js +28 -26
- package/dist/components/meeting-scheduler/index.cjs +51 -51
- package/dist/components/meeting-scheduler/index.js +14 -14
- package/dist/components/navigation/index.cjs +16 -16
- package/dist/components/navigation/index.js +15 -15
- package/dist/components/onboarding-guides/index.cjs +16 -16
- package/dist/components/onboarding-guides/index.js +15 -15
- package/dist/components/product-demo/index.cjs +8 -8
- package/dist/components/product-demo/index.js +8 -8
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +18 -18
- package/dist/components/tickets/index.js +17 -17
- package/dist/components/ui/button/button.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +11 -11
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +15 -13
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +14 -12
- package/dist/components/ui/mobile-app-qr.d.ts +23 -0
- package/dist/components/ui/mobile-app-qr.d.ts.map +1 -0
- package/dist/components/ui/store-badges.d.ts +7 -4
- package/dist/components/ui/store-badges.d.ts.map +1 -1
- package/dist/{devices-7Q2EUM7B.cjs → devices-6XHUFIC6.cjs} +14 -14
- package/dist/{devices-U3Y4IIW4.js → devices-NOUGVTES.js} +12 -12
- package/dist/hooks/index.cjs +9 -3
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +8 -2
- package/dist/hooks/use-downloads.d.ts +41 -0
- package/dist/hooks/use-downloads.d.ts.map +1 -0
- package/dist/index.cjs +57 -17
- package/dist/index.js +56 -16
- package/dist/{knowledge-NST53EXE.js → knowledge-2YTUXQ2Z.js} +12 -12
- package/dist/{knowledge-UYIZ74MA.cjs → knowledge-ANEKPMO2.cjs} +14 -14
- package/dist/{logs-HTH7X7KV.cjs → logs-U3IB3HUM.cjs} +22 -22
- package/dist/{logs-JVTRG232.js → logs-WVQIONUT.js} +12 -12
- package/dist/{policies-DTXTRK56.js → policies-66EGDW2H.js} +12 -12
- package/dist/{policies-MXK2HB4W.cjs → policies-UFRN2EQF.cjs} +17 -17
- package/dist/{remote-session-LAJZKQBN.js → remote-session-6WEUJZDC.js} +4 -4
- package/dist/{remote-session-SKKF4PBV.cjs → remote-session-A75X5NPC.cjs} +6 -6
- package/dist/{software-update-P6BSM6HX.cjs → software-update-2MHKL76D.cjs} +17 -17
- package/dist/{software-update-6RHMPM3W.js → software-update-3ZD3MXF3.js} +12 -12
- package/dist/{tickets-board-BHLPAVNJ.cjs → tickets-board-IFWMIZ35.cjs} +9 -9
- package/dist/{tickets-board-PYDNELSK.js → tickets-board-MYXG3V72.js} +4 -4
- package/dist/types/downloads.d.ts +67 -0
- package/dist/types/downloads.d.ts.map +1 -0
- package/dist/types/index.cjs +24 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +16 -1
- package/dist/utils/index.cjs +20 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +16 -1
- package/dist/utils/mobile-app.cjs +46 -0
- package/dist/utils/mobile-app.d.ts +40 -0
- package/dist/utils/mobile-app.d.ts.map +1 -0
- package/dist/utils/mobile-app.js +40 -0
- package/package.json +7 -1
- package/src/assets/index.ts +1 -0
- package/src/assets/mobile-app-qr.ts +19 -0
- package/src/components/help-center-pages/download-apps-page.tsx +364 -0
- package/src/components/help-center-pages/index.ts +1 -0
- package/src/components/ui/button/button.tsx +3 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/mobile-app-qr.tsx +40 -0
- package/src/components/ui/store-badges.tsx +17 -9
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-downloads.ts +138 -0
- package/src/types/downloads.ts +82 -0
- package/src/types/index.ts +1 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/mobile-app.ts +57 -0
package/dist/utils/index.js
CHANGED
|
@@ -2874,6 +2874,21 @@ function usesCommandKey(os) {
|
|
|
2874
2874
|
function shortcutLabel(os, key) {
|
|
2875
2875
|
return usesCommandKey(os) ? `\u2318${key}` : `Ctrl ${key}`;
|
|
2876
2876
|
}
|
|
2877
|
+
|
|
2878
|
+
// src/utils/mobile-app.ts
|
|
2879
|
+
var DOWNLOAD_PAGE_PATH = "/download";
|
|
2880
|
+
var DOWNLOAD_PAGE_STORE_PARAM = "store";
|
|
2881
|
+
var MOBILE_APP_INSTALL_URL = `https://www.flamingo.run${DOWNLOAD_PAGE_PATH}?${DOWNLOAD_PAGE_STORE_PARAM}=1`;
|
|
2882
|
+
var MOBILE_APP_INSTALL_HOST_PATH = MOBILE_APP_INSTALL_URL.replace(/^https:\/\/(www\.)?/, "").replace(
|
|
2883
|
+
/\?.*$/,
|
|
2884
|
+
""
|
|
2885
|
+
);
|
|
2886
|
+
function resolveMobileStoreUrl(links, userAgent, maxTouchPoints) {
|
|
2887
|
+
const os = detectVisitorOs(userAgent, { maxTouchPoints });
|
|
2888
|
+
if (os === "ios") return links.appStoreUrl;
|
|
2889
|
+
if (os === "android") return links.googlePlayUrl;
|
|
2890
|
+
return null;
|
|
2891
|
+
}
|
|
2877
2892
|
var PRIORITY_CODES = ["US", "CA", "GB", "AU"];
|
|
2878
2893
|
var COUNTRY_NAME_ALIASES = {
|
|
2879
2894
|
// United States
|
|
@@ -6452,4 +6467,4 @@ function pathMatchesAny(pathname, patterns) {
|
|
|
6452
6467
|
return (patterns ?? []).some((pattern) => matchesPathPattern(pathname, pattern));
|
|
6453
6468
|
}
|
|
6454
6469
|
|
|
6455
|
-
export { ACTIVE_ANCHOR_ATTRIBUTE, ANTHROPIC_SUPPORTED_IMAGE_MIME, AUTO_CONTINUATION_DIRECTIVE_PREFIX, BADGE_PALETTE, BITE_ID_RE, BRAND_MARK_SVGS, CHAT_ATTACHMENT_MARKDOWN_PATTERN, CHAT_ATTACHMENT_VIEW_TOKEN_QUERY_PARAM, CHAT_ATTACHMENT_VIEW_URL_PREFIX, CHAT_ATTACHMENT_VIEW_URL_PREFIX_REGEX_ESCAPED, COMPLIANCE_STANDARDS, CONTENT_REF_GROUPS, CSV_CONTENT_TYPE, CUSTOM_ITEM_ID, DEFAULT_CONTENT_SUFFIXES, DEFAULT_FOLDER_INDEX_FILE, DEFAULT_MIN_FILL_MS, DEFAULT_OS_PLATFORM, DELIVERY_TASK_TYPE_OPTIONS, DESIGN_DOC_READINESS, DESIGN_DOC_READINESS_DISPLAY, DESIGN_PALETTE, DESKTOP_OSES, DEV_SECTION_PARAM_KEYS, DOC_TABLE_TYPES, ELAPSED_MS_FIELD, EMBED_PROXY_OPTIONAL_HEADERS, FIRST_TOUCH_ATTRIBUTION_KEY, FORM_DRAFT_STATUSES, FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_DEBOUNCE_MS, FORM_RESCUE_EVENTS, FORM_RESCUE_MAX_FIELDS, FORM_RESCUE_MAX_VALUE_CHARS, FORM_RESCUE_RESUME_FIELD, FORM_RESCUE_RESUME_PARAM, FORM_RESCUE_SKIP_REASONS, FORM_RESCUE_SUBMIT_KEYS, FORM_RESCUE_VALUE_FIELDS, FullscreenSwitchController, GENERIC_EMAIL_DOMAINS, HEADING_EMOJI_RE, HEX_PATTERN, HONEYPOT_FIELD, HUBSPOT_DO_NOT_COLLECT_FORM_PROPS, HUB_HEADER_OFFSET_PX, HUMANITY_SIGNAL_KEYS, ICON_REGISTRY, IMAGE_FILE_ACCEPT, LOCATION_HASH_SYNC_EVENT, MAX_MONTH_OFFSET, MEDIA_FILE_ACCEPT, MEDIA_TYPE, NO_CLIENT_CACHE, ODS_SPACING_PROPS, ODS_SPACING_TOKENS, ODS_SPACING_TOKEN_PATTERN, ODS_STEM_CLASSES, ODS_STEM_TOKENS, OPENFRAME_CLASSIFICATIONS, OPENFRAME_DEV_SECTIONS, OS_PLATFORMS, PAGE_PARAM_LIMITS, PLATFORM_BRAND, PLATFORM_SURFACE, PLATFORM_THEME, PLAY_ICON_PATH, PROGRAM_META_RENDERERS, RESCUE_FORMS, ROADMAP_STATUS_OPTIONS, SCHEDULING_SLUG_SHAPE, SCROLL_ANCHOR, SCROLL_ANCHOR_WIRE_KEY, SECTION_HERO_ICON_CLASS, SEO_DESCRIPTION_MAX_LENGTH, SEO_TITLE_MAX_LENGTH, SOCIAL_PLATFORM_ALIASES, SOCIAL_PLATFORM_ALT_HOSTS, SOURCE_ICON_NAMES, SOURCE_LABELS_BY_TABLE, STICKY_HEADER_OFFSET_PX, TAILWIND_STEP_TO_ODS_TOKEN, TICKET_OPEN_PARAM, TICKET_STATUS_OPTIONS, TRIAL_DAYS, TRIAL_LABELS, VENDOR_CLASSIFICATIONS, VIEWER_TIMEZONE, WALKTHROUGH_OPEN_QUERY_PARAM, WALKTHROUGH_VIDEO_DISMISS_KEY, analyzeImageColor, announcementDismissCookieName, applyProxyAuth, brandMarkDataUri, buildAutoContinuationDirective, buildChatAttachmentViewUrl, buildDefaultHref, buildDiscussAddendum, buildDocumentTree, buildGroupedSource, buildListUrl, buildOgPlaceholderUrl, buildSuggestionUrl, buildTicketOpenHref, canonicalContentRefType, captureFirstTouchAttribution, captureFormRescueEvent, clamp, clampPageToTotal, classifySocialHost, cleanEmailDomain, clearAnnouncementDismissals, clearDismissCookie, clearEmbedProxyAuth, clearFirstTouchAttribution, clearLegacyAnnouncementCache, clearWalkthroughDismissal, clickupTaskUrl, cn, complianceStandardForName, consumeAccessCode, copyrightLine, createFenceTracker, createHeadingIdDeduper, createSearchParams, createSseSubscription, createUTCTimestamp, csvEscape, deepClone, defineParamSchema, defineRescueForm, delay, deriveActiveColor, deriveHoverColor, designDocReadiness, desktopOsOf, detectDesktopOs, detectVisitorOs, devSectionAnchorId, dismissAnnouncement, dismissWalkthrough, embedAuthedFetch, ensureBiteIds, errorMessage, escapeMarkdownInline, escapeRegExp, evaluateHumanitySignals, eventAudience, extractCardItems, extractDomainFromEmail, extractDominantColor, extractEntityIdFilter, extractHumanitySignals, extractImageEdgeColorAsync, extractItemId, extractItems, extractSections, faqItemAnchor, faqSectionSlug, fetchPriorityProp, fileContentKey, fileMediaType, filledRescueFields, findAnchorElementByNormalizedId, findDocNodeByPath, findHoneypotCopySource, firstParamValue, flattenAssistantContent, formatAbbreviatedNumber, formatAbsoluteDate, formatBioText, formatBytes, formatBytesShort, formatChatAttachmentMarkdownForBubble, formatCitationIndices, formatClassification, formatCompactMetric, formatCompletionLabel, formatCompletionLabelWithBlockers, formatCurrency, formatDate, formatDateRange, formatDateShort, formatDateSlashUTC, formatDateTime, formatDateTimeAt, formatDateTimeYmd, formatDateUTC, formatDateWithTimezone, formatDocName, formatDuration, formatDurationCompact, formatDurationFromMs, formatDurationFromRange, formatDurationMMSS, formatEntryMonthUTC, formatFileSize, formatLargeNumber, formatLegalDate, formatNumber, formatPercent, formatPhoneE164, formatPrice, formatPricingModel, formatProgramDate, formatProgramDateRange, formatProgramTimeRange, formatRelativeTime, formatReleaseDate, formatSingularLookupInvocation, formatTicketFullTimestamp, formatTicketRelativeTime, formatTimeWithTimezone, formatUnderscoreText, formatWebinarTimeMeta, formatWholeDollars, fromKeys, generateBiteId, generateImageSizes, generateRandomString, getAllPlatformBaseDomains, getBestContrastColor, getConfidenceBgClass, getConfidenceBorderClass, getConfidenceColorClass, getConfidenceLabel, getConfidenceLevel, getConfidenceTextClass, getContentRefLabel, getContentRefLabelOrTitleCase, getContrastRatio, getCountryByCode, getCountryByName, getCountryPhoneData, getCurrentPlatform, getDefaultColorForPlatform, getDefaultIconForPlatform, getDeploymentUrl, getDetailedTimeDifference, getDocAncestorNodeIds, getDocSourceDefaultPath, getDynamicIcon, getEmbedProxyAuth, getFirstTouchAttribution, getFlagFromCountryName, getIconComponent, getInitialExpandedNodeIds, getOSIcon, getOSLabel, getOSPlatformId, getOSTypeDefinition, getPersistedProxyEmail, getPlatformAccentColor, getPlatformBackgroundVarName, getPlatformBrand, getPlatformBrandClasses, getPlatformColor, getPlatformConfig, getPlatformDescription, getPlatformDisplayName, getPlatformIcon, getPlatformIconComponent, getPlatformIconComponent as getPlatformLogo, getPlatformProductionUrl, getPlatformSecondaryColor, getPlatformShortName, getPlatformSlogan, getPlatformTextVarName, getPlatformUrl, getProxiedImageUrl, getReadableTextColor, getRequestOrigin, getRescueForm, getSemanticColor, getShellIcon, getShellLabel, getSlackCommunityJoinUrl, getSmallPlatformIcon, getSourceIconName, getSourceLabel, getStatusColorScheme, getTaskTypeLabel, getToolLabel, getToolTypeAliases, getTrendColors, groupSourcesByTable, groupsByTable, hasGenericEmailDomain, hexToRgb2 as hexToRgb, hostMatches, isAnnouncementDismissed, isBadgePaletteKey, isBlankLine, isCrossOriginUrl, isDismissedCookieValue, isExcludedRescueField, isFigmaSlidesUrl, isFormAttemptId, isGenericDomain, isGenericWebsiteDomain, isHumanitySignalKey, isImageMedia, isListedSchedulingName, isLocalUrl, isOSPlatform, isOpenFrameClassification, isOrderedRange, isPublishableMediaType, isRescueEmail, isResumeToken, isScrollSyncedHash, isToday, isValidEmailDomain, isValidPathPattern, isValidSchedulingSlug, isValidToolType, isVendorClassification, isVideoMedia, isWalkthroughDismissed, isWithinMinutes, listRescueForms, makeComposeContentUrl, markdownToPlainText, matchBiteElement, matchesPathPattern, mediaNameKey, nameInitials, navigateSamePageHash, normalizeAnchorId, normalizeDocPath, normalizeDomain, normalizeHashFragment, normalizeIconKey, normalizeOSType, normalizeSchedulingSlug, normalizeSocialPlatform, normalizeToolType, normalizeToolTypeWithFallback, openInNewTab, orderContentRefTypes, pageCount, parseAttributionFromUrl, parseFaqHash, parseFormDraftSaveRequest, parseSchedulingLinkName, parseSchemaParams, parseScrollAnchor, parseWireCommandOverride, partitionMediaByType, pathMatchesAny, pathToNodeId, personFirstName, personInitials, pick, pickBadgePaletteColor, pickReadableTextColor, pickSocialLink, platformAccentToken, platformAccentVarName, platformColors, platformDescriptions, platformDisplayNames, platformHexColors, platformIconNames, platformIcons, platformSecondaryToken, platformSecondaryVarName, platformShortNames, platformSlogans, positiveInt, programDateInstant, programMetaFormatters, programMetaLine, programStr, readDismissCookie, readLeadingDecisionFrame, recordCountLabel, releaseTypeToBadgeColor, replaceLocationHash, rescueCompletionPct, resolveContentFetchPath, resolveRedirectTarget, resolveReleaseCover, rgbToHex, sanitizeLandingUrl, sanitizeRescueFieldName, sanitizeRescueValues, sanitizeTitleForChat, scanHeadings, schedulingAudienceKey, scrollElementIntoView, serializeJsonLd, serializeSchemaParams, setEmbedAuthAdapter, setEmbedProxyAuth, shortcutLabel, shouldIncludeInUrl, shouldProxyImage, singleInitial, skipsImageOptimizer, slugifyHeadingBase, slugifyHeadingText, socialPlatformHosts, sortTreeChildren, splitCsvEnv, stripChatAttachmentMarkdown, stripFolderIndexFromPath, stripHeadingEmojis, stripHtml, stripInlineMarkdown, switchPlatformTheme, titleCaseFromSlug, toClaudeEmbedUrl, toClaudeMirrorPath, toCsv, toDocSlug, toFigmaEmbedUrl, toFigmaOriginalUrl, toGoogleSheetsEmbedUrl, toGoogleSheetsOriginalUrl, toToolLabel, transformPlatformConfigsToOptions, truncateString, urlPathLooksLikeSvg, usePlatformColors, usesCommandKey, validateAccessCode, validateAndConsumeAccessCode, validateEmailDomain, validateEmailDomainList, validatePhoneNumber, walkthroughDismissCookieName, webinarTiming, withFirstTouchAttribution, withQuery, withWalkthroughOpenParam, writeDismissCookie };
|
|
6470
|
+
export { ACTIVE_ANCHOR_ATTRIBUTE, ANTHROPIC_SUPPORTED_IMAGE_MIME, AUTO_CONTINUATION_DIRECTIVE_PREFIX, BADGE_PALETTE, BITE_ID_RE, BRAND_MARK_SVGS, CHAT_ATTACHMENT_MARKDOWN_PATTERN, CHAT_ATTACHMENT_VIEW_TOKEN_QUERY_PARAM, CHAT_ATTACHMENT_VIEW_URL_PREFIX, CHAT_ATTACHMENT_VIEW_URL_PREFIX_REGEX_ESCAPED, COMPLIANCE_STANDARDS, CONTENT_REF_GROUPS, CSV_CONTENT_TYPE, CUSTOM_ITEM_ID, DEFAULT_CONTENT_SUFFIXES, DEFAULT_FOLDER_INDEX_FILE, DEFAULT_MIN_FILL_MS, DEFAULT_OS_PLATFORM, DELIVERY_TASK_TYPE_OPTIONS, DESIGN_DOC_READINESS, DESIGN_DOC_READINESS_DISPLAY, DESIGN_PALETTE, DESKTOP_OSES, DEV_SECTION_PARAM_KEYS, DOC_TABLE_TYPES, DOWNLOAD_PAGE_PATH, DOWNLOAD_PAGE_STORE_PARAM, ELAPSED_MS_FIELD, EMBED_PROXY_OPTIONAL_HEADERS, FIRST_TOUCH_ATTRIBUTION_KEY, FORM_DRAFT_STATUSES, FORM_RESCUE_ATTEMPT_FIELD, FORM_RESCUE_DEBOUNCE_MS, FORM_RESCUE_EVENTS, FORM_RESCUE_MAX_FIELDS, FORM_RESCUE_MAX_VALUE_CHARS, FORM_RESCUE_RESUME_FIELD, FORM_RESCUE_RESUME_PARAM, FORM_RESCUE_SKIP_REASONS, FORM_RESCUE_SUBMIT_KEYS, FORM_RESCUE_VALUE_FIELDS, FullscreenSwitchController, GENERIC_EMAIL_DOMAINS, HEADING_EMOJI_RE, HEX_PATTERN, HONEYPOT_FIELD, HUBSPOT_DO_NOT_COLLECT_FORM_PROPS, HUB_HEADER_OFFSET_PX, HUMANITY_SIGNAL_KEYS, ICON_REGISTRY, IMAGE_FILE_ACCEPT, LOCATION_HASH_SYNC_EVENT, MAX_MONTH_OFFSET, MEDIA_FILE_ACCEPT, MEDIA_TYPE, MOBILE_APP_INSTALL_HOST_PATH, MOBILE_APP_INSTALL_URL, NO_CLIENT_CACHE, ODS_SPACING_PROPS, ODS_SPACING_TOKENS, ODS_SPACING_TOKEN_PATTERN, ODS_STEM_CLASSES, ODS_STEM_TOKENS, OPENFRAME_CLASSIFICATIONS, OPENFRAME_DEV_SECTIONS, OS_PLATFORMS, PAGE_PARAM_LIMITS, PLATFORM_BRAND, PLATFORM_SURFACE, PLATFORM_THEME, PLAY_ICON_PATH, PROGRAM_META_RENDERERS, RESCUE_FORMS, ROADMAP_STATUS_OPTIONS, SCHEDULING_SLUG_SHAPE, SCROLL_ANCHOR, SCROLL_ANCHOR_WIRE_KEY, SECTION_HERO_ICON_CLASS, SEO_DESCRIPTION_MAX_LENGTH, SEO_TITLE_MAX_LENGTH, SOCIAL_PLATFORM_ALIASES, SOCIAL_PLATFORM_ALT_HOSTS, SOURCE_ICON_NAMES, SOURCE_LABELS_BY_TABLE, STICKY_HEADER_OFFSET_PX, TAILWIND_STEP_TO_ODS_TOKEN, TICKET_OPEN_PARAM, TICKET_STATUS_OPTIONS, TRIAL_DAYS, TRIAL_LABELS, VENDOR_CLASSIFICATIONS, VIEWER_TIMEZONE, WALKTHROUGH_OPEN_QUERY_PARAM, WALKTHROUGH_VIDEO_DISMISS_KEY, analyzeImageColor, announcementDismissCookieName, applyProxyAuth, brandMarkDataUri, buildAutoContinuationDirective, buildChatAttachmentViewUrl, buildDefaultHref, buildDiscussAddendum, buildDocumentTree, buildGroupedSource, buildListUrl, buildOgPlaceholderUrl, buildSuggestionUrl, buildTicketOpenHref, canonicalContentRefType, captureFirstTouchAttribution, captureFormRescueEvent, clamp, clampPageToTotal, classifySocialHost, cleanEmailDomain, clearAnnouncementDismissals, clearDismissCookie, clearEmbedProxyAuth, clearFirstTouchAttribution, clearLegacyAnnouncementCache, clearWalkthroughDismissal, clickupTaskUrl, cn, complianceStandardForName, consumeAccessCode, copyrightLine, createFenceTracker, createHeadingIdDeduper, createSearchParams, createSseSubscription, createUTCTimestamp, csvEscape, deepClone, defineParamSchema, defineRescueForm, delay, deriveActiveColor, deriveHoverColor, designDocReadiness, desktopOsOf, detectDesktopOs, detectVisitorOs, devSectionAnchorId, dismissAnnouncement, dismissWalkthrough, embedAuthedFetch, ensureBiteIds, errorMessage, escapeMarkdownInline, escapeRegExp, evaluateHumanitySignals, eventAudience, extractCardItems, extractDomainFromEmail, extractDominantColor, extractEntityIdFilter, extractHumanitySignals, extractImageEdgeColorAsync, extractItemId, extractItems, extractSections, faqItemAnchor, faqSectionSlug, fetchPriorityProp, fileContentKey, fileMediaType, filledRescueFields, findAnchorElementByNormalizedId, findDocNodeByPath, findHoneypotCopySource, firstParamValue, flattenAssistantContent, formatAbbreviatedNumber, formatAbsoluteDate, formatBioText, formatBytes, formatBytesShort, formatChatAttachmentMarkdownForBubble, formatCitationIndices, formatClassification, formatCompactMetric, formatCompletionLabel, formatCompletionLabelWithBlockers, formatCurrency, formatDate, formatDateRange, formatDateShort, formatDateSlashUTC, formatDateTime, formatDateTimeAt, formatDateTimeYmd, formatDateUTC, formatDateWithTimezone, formatDocName, formatDuration, formatDurationCompact, formatDurationFromMs, formatDurationFromRange, formatDurationMMSS, formatEntryMonthUTC, formatFileSize, formatLargeNumber, formatLegalDate, formatNumber, formatPercent, formatPhoneE164, formatPrice, formatPricingModel, formatProgramDate, formatProgramDateRange, formatProgramTimeRange, formatRelativeTime, formatReleaseDate, formatSingularLookupInvocation, formatTicketFullTimestamp, formatTicketRelativeTime, formatTimeWithTimezone, formatUnderscoreText, formatWebinarTimeMeta, formatWholeDollars, fromKeys, generateBiteId, generateImageSizes, generateRandomString, getAllPlatformBaseDomains, getBestContrastColor, getConfidenceBgClass, getConfidenceBorderClass, getConfidenceColorClass, getConfidenceLabel, getConfidenceLevel, getConfidenceTextClass, getContentRefLabel, getContentRefLabelOrTitleCase, getContrastRatio, getCountryByCode, getCountryByName, getCountryPhoneData, getCurrentPlatform, getDefaultColorForPlatform, getDefaultIconForPlatform, getDeploymentUrl, getDetailedTimeDifference, getDocAncestorNodeIds, getDocSourceDefaultPath, getDynamicIcon, getEmbedProxyAuth, getFirstTouchAttribution, getFlagFromCountryName, getIconComponent, getInitialExpandedNodeIds, getOSIcon, getOSLabel, getOSPlatformId, getOSTypeDefinition, getPersistedProxyEmail, getPlatformAccentColor, getPlatformBackgroundVarName, getPlatformBrand, getPlatformBrandClasses, getPlatformColor, getPlatformConfig, getPlatformDescription, getPlatformDisplayName, getPlatformIcon, getPlatformIconComponent, getPlatformIconComponent as getPlatformLogo, getPlatformProductionUrl, getPlatformSecondaryColor, getPlatformShortName, getPlatformSlogan, getPlatformTextVarName, getPlatformUrl, getProxiedImageUrl, getReadableTextColor, getRequestOrigin, getRescueForm, getSemanticColor, getShellIcon, getShellLabel, getSlackCommunityJoinUrl, getSmallPlatformIcon, getSourceIconName, getSourceLabel, getStatusColorScheme, getTaskTypeLabel, getToolLabel, getToolTypeAliases, getTrendColors, groupSourcesByTable, groupsByTable, hasGenericEmailDomain, hexToRgb2 as hexToRgb, hostMatches, isAnnouncementDismissed, isBadgePaletteKey, isBlankLine, isCrossOriginUrl, isDismissedCookieValue, isExcludedRescueField, isFigmaSlidesUrl, isFormAttemptId, isGenericDomain, isGenericWebsiteDomain, isHumanitySignalKey, isImageMedia, isListedSchedulingName, isLocalUrl, isOSPlatform, isOpenFrameClassification, isOrderedRange, isPublishableMediaType, isRescueEmail, isResumeToken, isScrollSyncedHash, isToday, isValidEmailDomain, isValidPathPattern, isValidSchedulingSlug, isValidToolType, isVendorClassification, isVideoMedia, isWalkthroughDismissed, isWithinMinutes, listRescueForms, makeComposeContentUrl, markdownToPlainText, matchBiteElement, matchesPathPattern, mediaNameKey, nameInitials, navigateSamePageHash, normalizeAnchorId, normalizeDocPath, normalizeDomain, normalizeHashFragment, normalizeIconKey, normalizeOSType, normalizeSchedulingSlug, normalizeSocialPlatform, normalizeToolType, normalizeToolTypeWithFallback, openInNewTab, orderContentRefTypes, pageCount, parseAttributionFromUrl, parseFaqHash, parseFormDraftSaveRequest, parseSchedulingLinkName, parseSchemaParams, parseScrollAnchor, parseWireCommandOverride, partitionMediaByType, pathMatchesAny, pathToNodeId, personFirstName, personInitials, pick, pickBadgePaletteColor, pickReadableTextColor, pickSocialLink, platformAccentToken, platformAccentVarName, platformColors, platformDescriptions, platformDisplayNames, platformHexColors, platformIconNames, platformIcons, platformSecondaryToken, platformSecondaryVarName, platformShortNames, platformSlogans, positiveInt, programDateInstant, programMetaFormatters, programMetaLine, programStr, readDismissCookie, readLeadingDecisionFrame, recordCountLabel, releaseTypeToBadgeColor, replaceLocationHash, rescueCompletionPct, resolveContentFetchPath, resolveMobileStoreUrl, resolveRedirectTarget, resolveReleaseCover, rgbToHex, sanitizeLandingUrl, sanitizeRescueFieldName, sanitizeRescueValues, sanitizeTitleForChat, scanHeadings, schedulingAudienceKey, scrollElementIntoView, serializeJsonLd, serializeSchemaParams, setEmbedAuthAdapter, setEmbedProxyAuth, shortcutLabel, shouldIncludeInUrl, shouldProxyImage, singleInitial, skipsImageOptimizer, slugifyHeadingBase, slugifyHeadingText, socialPlatformHosts, sortTreeChildren, splitCsvEnv, stripChatAttachmentMarkdown, stripFolderIndexFromPath, stripHeadingEmojis, stripHtml, stripInlineMarkdown, switchPlatformTheme, titleCaseFromSlug, toClaudeEmbedUrl, toClaudeMirrorPath, toCsv, toDocSlug, toFigmaEmbedUrl, toFigmaOriginalUrl, toGoogleSheetsEmbedUrl, toGoogleSheetsOriginalUrl, toToolLabel, transformPlatformConfigsToOptions, truncateString, urlPathLooksLikeSvg, usePlatformColors, usesCommandKey, validateAccessCode, validateAndConsumeAccessCode, validateEmailDomain, validateEmailDomainList, validatePhoneNumber, walkthroughDismissCookieName, webinarTiming, withFirstTouchAttribution, withQuery, withWalkthroughOpenParam, writeDismissCookie };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/utils/visitor-os.ts
|
|
4
|
+
var HINT_PLATFORMS = {
|
|
5
|
+
macos: "mac",
|
|
6
|
+
windows: "windows",
|
|
7
|
+
linux: "linux",
|
|
8
|
+
"chrome os": "linux",
|
|
9
|
+
"chromium os": "linux",
|
|
10
|
+
android: "android",
|
|
11
|
+
ios: "ios"
|
|
12
|
+
};
|
|
13
|
+
function detectVisitorOs(userAgent, hints = {}) {
|
|
14
|
+
const touchMac = (os) => os === "mac" && (hints.maxTouchPoints ?? 0) > 1 ? "ios" : os;
|
|
15
|
+
const hinted = HINT_PLATFORMS[(hints.platform ?? "").replace(/"/g, "").trim().toLowerCase()];
|
|
16
|
+
if (hinted) return touchMac(hinted);
|
|
17
|
+
const ua = userAgent ?? "";
|
|
18
|
+
if (!ua) return null;
|
|
19
|
+
if (/iPhone|iPad|iPod/i.test(ua)) return "ios";
|
|
20
|
+
if (/Android/i.test(ua)) return "android";
|
|
21
|
+
if (/Windows NT|Win64|Win32/i.test(ua)) return "windows";
|
|
22
|
+
if (/Macintosh|Mac OS X/i.test(ua)) return touchMac("mac");
|
|
23
|
+
if (/Linux|X11|CrOS/i.test(ua)) return "linux";
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/utils/mobile-app.ts
|
|
28
|
+
var DOWNLOAD_PAGE_PATH = "/download";
|
|
29
|
+
var DOWNLOAD_PAGE_STORE_PARAM = "store";
|
|
30
|
+
var MOBILE_APP_INSTALL_URL = `https://www.flamingo.run${DOWNLOAD_PAGE_PATH}?${DOWNLOAD_PAGE_STORE_PARAM}=1`;
|
|
31
|
+
var MOBILE_APP_INSTALL_HOST_PATH = MOBILE_APP_INSTALL_URL.replace(/^https:\/\/(www\.)?/, "").replace(
|
|
32
|
+
/\?.*$/,
|
|
33
|
+
""
|
|
34
|
+
);
|
|
35
|
+
function resolveMobileStoreUrl(links, userAgent, maxTouchPoints) {
|
|
36
|
+
const os = detectVisitorOs(userAgent, { maxTouchPoints });
|
|
37
|
+
if (os === "ios") return links.appStoreUrl;
|
|
38
|
+
if (os === "android") return links.googlePlayUrl;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exports.DOWNLOAD_PAGE_PATH = DOWNLOAD_PAGE_PATH;
|
|
43
|
+
exports.DOWNLOAD_PAGE_STORE_PARAM = DOWNLOAD_PAGE_STORE_PARAM;
|
|
44
|
+
exports.MOBILE_APP_INSTALL_HOST_PATH = MOBILE_APP_INSTALL_HOST_PATH;
|
|
45
|
+
exports.MOBILE_APP_INSTALL_URL = MOBILE_APP_INSTALL_URL;
|
|
46
|
+
exports.resolveMobileStoreUrl = resolveMobileStoreUrl;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The download page's own addresses: where it lives, the parameter that sends a
|
|
3
|
+
* phone on to its store, and the URL the install QR code encodes. The store
|
|
4
|
+
* listings and installer links are NOT here: they are the server's
|
|
5
|
+
* (`DownloadsPublic`, read through `useDownloads`).
|
|
6
|
+
*
|
|
7
|
+
* Pure and server-safe; also published as the granular subpath
|
|
8
|
+
* `./utils/mobile-app`, for an Edge proxy and other server-only consumers.
|
|
9
|
+
*/
|
|
10
|
+
import type { MobileAppLinks } from '../types/downloads';
|
|
11
|
+
/** The website's download page: every installer and the mobile app. */
|
|
12
|
+
export declare const DOWNLOAD_PAGE_PATH = "/download";
|
|
13
|
+
/**
|
|
14
|
+
* On the download page: "send a phone or a tablet on to its store". The install
|
|
15
|
+
* QR code carries it, because whoever scans a code is holding the phone the app
|
|
16
|
+
* goes on. Anyone who opens the page without it reads the page.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DOWNLOAD_PAGE_STORE_PARAM = "store";
|
|
19
|
+
/**
|
|
20
|
+
* The one URL the install QR code encodes: the website's download page, asking
|
|
21
|
+
* it to send a phone on to its store.
|
|
22
|
+
*
|
|
23
|
+
* Absolute and on the production host on purpose: a code must point at
|
|
24
|
+
* production whichever deployment rendered the page it came from. `MobileAppQr`
|
|
25
|
+
* draws this value as committed path data, so the two change together: see the
|
|
26
|
+
* regeneration note on that component.
|
|
27
|
+
*/
|
|
28
|
+
export declare const MOBILE_APP_INSTALL_URL = "https://www.flamingo.run/download?store=1";
|
|
29
|
+
/** The download page as it is printed for a human to read or type: no scheme, no `www`, no query. */
|
|
30
|
+
export declare const MOBILE_APP_INSTALL_HOST_PATH: string;
|
|
31
|
+
/**
|
|
32
|
+
* The store for a phone or tablet, or `null` for every other system and for a
|
|
33
|
+
* store the server names no listing for (`links` is the server's answer).
|
|
34
|
+
*
|
|
35
|
+
* The server passes the request's User-Agent; a browser also passes
|
|
36
|
+
* `navigator.maxTouchPoints`, the only signal that tells an iPad asking for the
|
|
37
|
+
* desktop site (it calls itself a Macintosh) from a real Mac.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveMobileStoreUrl(links: MobileAppLinks, userAgent: string | null | undefined, maxTouchPoints?: number): string | null;
|
|
40
|
+
//# sourceMappingURL=mobile-app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-app.d.ts","sourceRoot":"","sources":["../../src/utils/mobile-app.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,UAAU,CAAC;AAEjD;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,8CAAiF,CAAC;AAErH,qGAAqG;AACrG,eAAO,MAAM,4BAA4B,QAGxC,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,GAAG,IAAI,CAKf"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/utils/visitor-os.ts
|
|
2
|
+
var HINT_PLATFORMS = {
|
|
3
|
+
macos: "mac",
|
|
4
|
+
windows: "windows",
|
|
5
|
+
linux: "linux",
|
|
6
|
+
"chrome os": "linux",
|
|
7
|
+
"chromium os": "linux",
|
|
8
|
+
android: "android",
|
|
9
|
+
ios: "ios"
|
|
10
|
+
};
|
|
11
|
+
function detectVisitorOs(userAgent, hints = {}) {
|
|
12
|
+
const touchMac = (os) => os === "mac" && (hints.maxTouchPoints ?? 0) > 1 ? "ios" : os;
|
|
13
|
+
const hinted = HINT_PLATFORMS[(hints.platform ?? "").replace(/"/g, "").trim().toLowerCase()];
|
|
14
|
+
if (hinted) return touchMac(hinted);
|
|
15
|
+
const ua = userAgent ?? "";
|
|
16
|
+
if (!ua) return null;
|
|
17
|
+
if (/iPhone|iPad|iPod/i.test(ua)) return "ios";
|
|
18
|
+
if (/Android/i.test(ua)) return "android";
|
|
19
|
+
if (/Windows NT|Win64|Win32/i.test(ua)) return "windows";
|
|
20
|
+
if (/Macintosh|Mac OS X/i.test(ua)) return touchMac("mac");
|
|
21
|
+
if (/Linux|X11|CrOS/i.test(ua)) return "linux";
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/utils/mobile-app.ts
|
|
26
|
+
var DOWNLOAD_PAGE_PATH = "/download";
|
|
27
|
+
var DOWNLOAD_PAGE_STORE_PARAM = "store";
|
|
28
|
+
var MOBILE_APP_INSTALL_URL = `https://www.flamingo.run${DOWNLOAD_PAGE_PATH}?${DOWNLOAD_PAGE_STORE_PARAM}=1`;
|
|
29
|
+
var MOBILE_APP_INSTALL_HOST_PATH = MOBILE_APP_INSTALL_URL.replace(/^https:\/\/(www\.)?/, "").replace(
|
|
30
|
+
/\?.*$/,
|
|
31
|
+
""
|
|
32
|
+
);
|
|
33
|
+
function resolveMobileStoreUrl(links, userAgent, maxTouchPoints) {
|
|
34
|
+
const os = detectVisitorOs(userAgent, { maxTouchPoints });
|
|
35
|
+
if (os === "ios") return links.appStoreUrl;
|
|
36
|
+
if (os === "android") return links.googlePlayUrl;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { DOWNLOAD_PAGE_PATH, DOWNLOAD_PAGE_STORE_PARAM, MOBILE_APP_INSTALL_HOST_PATH, MOBILE_APP_INSTALL_URL, resolveMobileStoreUrl };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flamingo-stack/openframe-frontend-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.721",
|
|
4
4
|
"description": "Shared design system and components for all Flamingo platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -255,6 +255,12 @@
|
|
|
255
255
|
"require": "./dist/utils/form-rescue.cjs",
|
|
256
256
|
"default": "./dist/utils/form-rescue.js"
|
|
257
257
|
},
|
|
258
|
+
"./utils/mobile-app": {
|
|
259
|
+
"types": "./dist/utils/mobile-app.d.ts",
|
|
260
|
+
"import": "./dist/utils/mobile-app.js",
|
|
261
|
+
"require": "./dist/utils/mobile-app.cjs",
|
|
262
|
+
"default": "./dist/utils/mobile-app.js"
|
|
263
|
+
},
|
|
258
264
|
"./utils/platform-identity": {
|
|
259
265
|
"types": "./dist/utils/platform-identity.d.ts",
|
|
260
266
|
"import": "./dist/utils/platform-identity.js",
|
package/src/assets/index.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The install QR code's artwork: the modules of `MOBILE_APP_INSTALL_URL`
|
|
3
|
+
* (`utils/mobile-app`) at version 3, ECC level M, as one SVG path. Packaged with
|
|
4
|
+
* the library like the other assets, so every consumer draws the same code with
|
|
5
|
+
* no host-asset serving. `MobileAppQr` (components/ui) is the component that
|
|
6
|
+
* renders it.
|
|
7
|
+
*
|
|
8
|
+
* It changes WITH that URL. To regenerate: with the `qrcode` package,
|
|
9
|
+
* `QRCode.create(url, { errorCorrectionLevel: 'M' })` gives the modules; each
|
|
10
|
+
* row is one `M{x+4} {y+4}.5h{run}` followed by `m{gap} 0h{run}` per further
|
|
11
|
+
* run (the 4 is the quiet zone), and the viewBox side is `size + 8`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The side of the artwork's square viewBox: 29 modules plus a 4-module quiet zone on each side. */
|
|
15
|
+
export const MOBILE_APP_QR_VIEWBOX_SIZE = 37;
|
|
16
|
+
|
|
17
|
+
/** The dark modules, as horizontal strokes one module tall (`stroke` draws them). */
|
|
18
|
+
export const MOBILE_APP_QR_PATH =
|
|
19
|
+
'M4 4.5h7m2 0h2m2 0h1m2 0h1m2 0h2m1 0h7M4 5.5h1m5 0h1m2 0h2m2 0h3m6 0h1m5 0h1M4 6.5h1m1 0h3m1 0h1m1 0h3m1 0h1m1 0h3m2 0h1m2 0h1m1 0h3m1 0h1M4 7.5h1m1 0h3m1 0h1m1 0h1m1 0h1m1 0h6m2 0h1m1 0h1m1 0h3m1 0h1M4 8.5h1m1 0h3m1 0h1m1 0h3m1 0h1m1 0h1m1 0h5m1 0h1m1 0h3m1 0h1M4 9.5h1m5 0h1m1 0h1m1 0h2m1 0h1m1 0h3m4 0h1m5 0h1M4 10.5h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7M12 11.5h1m2 0h2m5 0h3M4 12.5h1m1 0h5m3 0h1m1 0h8m2 0h5M4 13.5h4m4 0h2m2 0h1m3 0h4m1 0h4m3 0h1M6 14.5h1m3 0h3m1 0h1m3 0h4m2 0h1M4 15.5h1m2 0h3m2 0h2m1 0h2m1 0h1m1 0h1m2 0h5m1 0h1m1 0h1M4 16.5h1m4 0h2m1 0h2m1 0h1m1 0h1m2 0h2m1 0h1m5 0h2M16 17.5h3m3 0h5m1 0h1m3 0h1M5 18.5h1m3 0h2m1 0h1m5 0h2m4 0h1m1 0h2m1 0h2M4 19.5h6m1 0h1m3 0h1m2 0h3m1 0h4m1 0h2m2 0h1M8 20.5h1m1 0h1m2 0h2m1 0h2m1 0h1m1 0h1m5 0h1m1 0h2M4 21.5h1m1 0h2m7 0h1m2 0h1m1 0h9m1 0h1m1 0h1M4 22.5h1m3 0h3m2 0h1m1 0h2m2 0h3m3 0h1m1 0h1m2 0h1M4 23.5h1m6 0h4m3 0h2m3 0h2m2 0h2m2 0h1M4 24.5h1m2 0h4m4 0h1m3 0h1m1 0h1m1 0h6m1 0h3M12 25.5h6m2 0h1m3 0h1m3 0h5M4 26.5h7m3 0h8m1 0h2m1 0h1m1 0h3M4 27.5h1m5 0h1m1 0h2m1 0h3m2 0h1m2 0h2m3 0h1M4 28.5h1m1 0h3m1 0h1m1 0h2m2 0h1m1 0h1m2 0h1m2 0h5m1 0h3M4 29.5h1m1 0h3m1 0h1m1 0h2m1 0h2m3 0h4m3 0h1m1 0h4M4 30.5h1m1 0h3m1 0h1m1 0h1m3 0h4m1 0h1m2 0h8M4 31.5h1m5 0h1m4 0h1m1 0h1m1 0h2m1 0h1m1 0h1m1 0h2m1 0h1m1 0h1M4 32.5h7m1 0h1m2 0h2m4 0h1m1 0h1m3 0h4';
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<DownloadAppsPage>`: every way to install OpenFrame, on one page.
|
|
5
|
+
*
|
|
6
|
+
* - The desktop app: every system at once, one card each (a download page
|
|
7
|
+
* lists its platforms; it does not hide them behind tabs, so nothing moves
|
|
8
|
+
* when a visitor looks for another system). In a card the installer is THE
|
|
9
|
+
* action (the other architecture is a quieter button) and each package
|
|
10
|
+
* manager is one line under it, with its copy button.
|
|
11
|
+
* - The mobile app: the store badges, and the install QR code beside them.
|
|
12
|
+
* - `footer`: the host's own last word (the website's "start on the web" card).
|
|
13
|
+
*
|
|
14
|
+
* DATA: all of it is the server's answer (`DownloadsPublic`), read by
|
|
15
|
+
* `useDownloads` against `endpoint` (default `DOWNLOADS_API_PATH`; embedders
|
|
16
|
+
* pass their `/content` proxy path), the one request every download button on
|
|
17
|
+
* the page shares. `initialData` (hub SSR) is the answer, with no fetch. The
|
|
18
|
+
* lib holds no link of its own and no fallback: the page ADAPTS, so a system
|
|
19
|
+
* with no installer and no command has no card, a store with no listing has no
|
|
20
|
+
* badge, and a section with nothing to offer is not drawn.
|
|
21
|
+
*
|
|
22
|
+
* A HOST DECIDES `showDesktop`: the website shows the desktop app only to a
|
|
23
|
+
* visitor whose main action is the download; the desktop shell hides it (it is
|
|
24
|
+
* the app already running).
|
|
25
|
+
*
|
|
26
|
+
* The chrome is the canonical `PageShell` + frozen `PageLayout`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useEffect, type ComponentType, type ReactNode } from 'react';
|
|
30
|
+
import { useRouter } from '../../embed-shims/next-navigation';
|
|
31
|
+
import { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard';
|
|
32
|
+
import { useDownloads } from '../../hooks/use-downloads';
|
|
33
|
+
import {
|
|
34
|
+
DESKTOP_OS_LABELS,
|
|
35
|
+
DOWNLOAD_ARCHITECTURE_LABELS,
|
|
36
|
+
DOWNLOADS_API_PATH,
|
|
37
|
+
DOWNLOADS_MOBILE_TAGLINE,
|
|
38
|
+
DOWNLOADS_MOBILE_TITLE,
|
|
39
|
+
DOWNLOADS_TAGLINE,
|
|
40
|
+
DOWNLOADS_TITLE,
|
|
41
|
+
type AppDownload,
|
|
42
|
+
type DownloadActionEvent,
|
|
43
|
+
type DownloadsPublic,
|
|
44
|
+
type MobileAppLinks,
|
|
45
|
+
} from '../../types/downloads';
|
|
46
|
+
import { cn } from '../../utils/cn';
|
|
47
|
+
import { DOWNLOAD_PAGE_STORE_PARAM, MOBILE_APP_INSTALL_HOST_PATH, resolveMobileStoreUrl } from '../../utils/mobile-app';
|
|
48
|
+
import { DESKTOP_OSES, type DesktopOs } from '../../utils/visitor-os';
|
|
49
|
+
import { CommandBox } from '../features/command-box';
|
|
50
|
+
import { AppleLogoIcon } from '../icons-v2-generated/brand-logos/apple-logo-icon';
|
|
51
|
+
import { WindowsLogoGreyIcon } from '../icons-v2-generated/brand-logos/windows-logo-grey-icon';
|
|
52
|
+
import { Download01Icon } from '../icons-v2-generated/interface/download-01-icon';
|
|
53
|
+
import { PageShell } from '../layout/article-detail-layout';
|
|
54
|
+
import { PageLayout } from '../layout/page-layout';
|
|
55
|
+
import { UnifiedSkeleton } from '../loading/unified-skeleton';
|
|
56
|
+
import { Button } from '../ui/button/button';
|
|
57
|
+
import { LoadError } from '../ui/error-state';
|
|
58
|
+
import { MobileAppQr } from '../ui/mobile-app-qr';
|
|
59
|
+
import { StoreBadgeLinks } from '../ui/store-badges';
|
|
60
|
+
|
|
61
|
+
export interface DownloadAppsPageProps {
|
|
62
|
+
/** GET endpoint for the public projection. Default `DOWNLOADS_API_PATH`. */
|
|
63
|
+
endpoint?: string;
|
|
64
|
+
/** Optional SSR hydrate (hub server-read): skips the initial client fetch. */
|
|
65
|
+
initialData?: DownloadsPublic;
|
|
66
|
+
/** Render the standalone `<PageShell>`. Default true. Pass false when the host
|
|
67
|
+
* layout already provides the page container (e.g. openframe-frontend). */
|
|
68
|
+
shell?: boolean;
|
|
69
|
+
/** Back-button config. Default: none. */
|
|
70
|
+
backButton?: { label?: string; href?: string } | false;
|
|
71
|
+
/** Page title. Default by what is offered (`DOWNLOADS_TITLE` / `DOWNLOADS_MOBILE_TITLE`). */
|
|
72
|
+
title?: string;
|
|
73
|
+
/** Page subtitle. Default by what is offered. */
|
|
74
|
+
subtitle?: string;
|
|
75
|
+
/** Offer the desktop app. Default true. False: the mobile app only. */
|
|
76
|
+
showDesktop?: boolean;
|
|
77
|
+
/** Open the store listings in a new tab. Default true. */
|
|
78
|
+
openStoresInNewTab?: boolean;
|
|
79
|
+
/** Called when a visitor downloads an installer or copies an install command (the host's analytics). */
|
|
80
|
+
onDownloadAction?: (event: DownloadActionEvent) => void;
|
|
81
|
+
/** The host's own closing block, under the mobile app. */
|
|
82
|
+
footer?: ReactNode;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const SECTIONS_CLASS = 'flex flex-col gap-[var(--spacing-system-xlf)]';
|
|
86
|
+
const SECTION_CLASS = 'flex flex-col gap-[var(--spacing-system-lf)]';
|
|
87
|
+
const CARDS_GRID_CLASS = 'grid grid-cols-1 gap-[var(--spacing-system-lf)] content-md:grid-cols-2';
|
|
88
|
+
/** One card takes the row; two or more share it. */
|
|
89
|
+
const cardsGridClass = (cards: number) => (cards > 1 ? CARDS_GRID_CLASS : 'grid grid-cols-1');
|
|
90
|
+
const CARD_CLASS =
|
|
91
|
+
'flex flex-col gap-[var(--spacing-system-lf)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-lf)]';
|
|
92
|
+
const CARD_HEADER_CLASS = 'flex items-center gap-[var(--spacing-system-sf)]';
|
|
93
|
+
const BUTTONS_CLASS = 'flex flex-wrap gap-[var(--spacing-system-sf)]';
|
|
94
|
+
const COMMANDS_CLASS = 'flex flex-col gap-[var(--spacing-system-mf)]';
|
|
95
|
+
|
|
96
|
+
const OS_ICONS: Partial<Record<DesktopOs, ComponentType<{ className?: string }>>> = {
|
|
97
|
+
mac: AppleLogoIcon,
|
|
98
|
+
windows: WindowsLogoGreyIcon,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** A card's heading row: a small logo, the name, and one quiet line of detail at the far end. */
|
|
102
|
+
function CardHeader({
|
|
103
|
+
icon: Icon,
|
|
104
|
+
title,
|
|
105
|
+
detail,
|
|
106
|
+
}: {
|
|
107
|
+
icon?: ComponentType<{ className?: string }>;
|
|
108
|
+
title: string;
|
|
109
|
+
detail?: string;
|
|
110
|
+
}) {
|
|
111
|
+
return (
|
|
112
|
+
<div className={CARD_HEADER_CLASS}>
|
|
113
|
+
{Icon && <Icon className="h-6 w-6 shrink-0 text-ods-text-secondary" />}
|
|
114
|
+
<h3 className="m-0 text-ods-text-primary text-h3">{title}</h3>
|
|
115
|
+
{detail && <p className="m-0 ml-auto text-right text-ods-text-secondary text-h6">{detail}</p>}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface SystemRows {
|
|
121
|
+
os: DesktopOs;
|
|
122
|
+
installers: AppDownload[];
|
|
123
|
+
commands: AppDownload[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The rows per system, in the order they arrived. The page adapts to what the
|
|
128
|
+
* deployment names: a system is offered when it has an installer OR a package
|
|
129
|
+
* manager command, and a system with neither has no card at all.
|
|
130
|
+
*/
|
|
131
|
+
function rowsBySystem(downloads: readonly AppDownload[]): SystemRows[] {
|
|
132
|
+
return DESKTOP_OSES.map(os => ({
|
|
133
|
+
os,
|
|
134
|
+
installers: downloads.filter(d => d.os === os && d.kind === 'binary' && d.url),
|
|
135
|
+
commands: downloads.filter(d => d.os === os && d.kind === 'command' && d.command),
|
|
136
|
+
})).filter(system => system.installers.length > 0 || system.commands.length > 0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function installerLabel(download: AppDownload, siblings: number): string {
|
|
140
|
+
if (download.label) return download.label;
|
|
141
|
+
const label = `Download for ${DESKTOP_OS_LABELS[download.os]}`;
|
|
142
|
+
const architecture = download.architecture ? DOWNLOAD_ARCHITECTURE_LABELS[download.architecture] : null;
|
|
143
|
+
// One installer for a system needs no qualifier; two are told apart by their architecture.
|
|
144
|
+
return siblings > 1 && architecture ? `${label} (${architecture})` : label;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** One system's card: its logo and name, its installers side by side, then each package manager as one line. */
|
|
148
|
+
function SystemCard({
|
|
149
|
+
system,
|
|
150
|
+
onDownloadAction,
|
|
151
|
+
}: {
|
|
152
|
+
system: SystemRows;
|
|
153
|
+
onDownloadAction?: DownloadAppsPageProps['onDownloadAction'];
|
|
154
|
+
}) {
|
|
155
|
+
const { copy } = useCopyToClipboard({
|
|
156
|
+
successTitle: 'Copied',
|
|
157
|
+
successDescription: 'Paste it into your terminal and press Return.',
|
|
158
|
+
});
|
|
159
|
+
const requirements = [...new Set(system.installers.map(d => d.minOsLabel).filter((l): l is string => !!l))];
|
|
160
|
+
const report = (action: DownloadActionEvent['action'], row: AppDownload) =>
|
|
161
|
+
onDownloadAction?.({ action, id: row.id, os: row.os, architecture: row.architecture });
|
|
162
|
+
return (
|
|
163
|
+
<div className={CARD_CLASS}>
|
|
164
|
+
<CardHeader
|
|
165
|
+
icon={OS_ICONS[system.os]}
|
|
166
|
+
title={DESKTOP_OS_LABELS[system.os]}
|
|
167
|
+
detail={requirements.length > 0 ? `Requires ${requirements.join(' or ')}` : undefined}
|
|
168
|
+
/>
|
|
169
|
+
{system.installers.length > 0 && (
|
|
170
|
+
<div className={BUTTONS_CLASS}>
|
|
171
|
+
{system.installers.map((download, index) => (
|
|
172
|
+
<Button
|
|
173
|
+
key={download.id}
|
|
174
|
+
variant={index === 0 ? 'accent' : 'outline'}
|
|
175
|
+
href={download.url ?? undefined}
|
|
176
|
+
download
|
|
177
|
+
leftIcon={<Download01Icon className="h-5 w-5" />}
|
|
178
|
+
onClick={() => report('installer', download)}
|
|
179
|
+
>
|
|
180
|
+
{installerLabel(download, system.installers.length)}
|
|
181
|
+
</Button>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
{system.commands.length > 0 && (
|
|
186
|
+
<div className={COMMANDS_CLASS}>
|
|
187
|
+
{system.commands.map(row =>
|
|
188
|
+
row.command ? (
|
|
189
|
+
<CommandBox
|
|
190
|
+
key={row.id}
|
|
191
|
+
title={row.label ?? undefined}
|
|
192
|
+
command={row.command}
|
|
193
|
+
maxLines={1}
|
|
194
|
+
commandClassName="text-ods-accent"
|
|
195
|
+
copyAriaLabel={`Copy the ${row.label ?? 'install'} command`}
|
|
196
|
+
onCopy={() => {
|
|
197
|
+
void copy(row.command as string);
|
|
198
|
+
report('command', row);
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
) : null,
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** A section's cards while the server's answer loads: the same grid and card frame, a button's box in each. */
|
|
210
|
+
function CardsSkeleton({ label }: { label: string }) {
|
|
211
|
+
return (
|
|
212
|
+
<div className={CARDS_GRID_CLASS} aria-busy="true" aria-label={label}>
|
|
213
|
+
{[0, 1].map(card => (
|
|
214
|
+
<div key={card} className={CARD_CLASS}>
|
|
215
|
+
<UnifiedSkeleton className="h-6 w-32 rounded" />
|
|
216
|
+
<UnifiedSkeleton className="h-12 w-56 rounded-md" />
|
|
217
|
+
<UnifiedSkeleton className="h-14 w-full rounded-md" />
|
|
218
|
+
</div>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function Section({ id, title, children }: { id: string; title: string; children: ReactNode }) {
|
|
225
|
+
return (
|
|
226
|
+
<section aria-labelledby={id} className={SECTION_CLASS}>
|
|
227
|
+
<h2 id={id} className="m-0 text-ods-text-primary text-h2">
|
|
228
|
+
{title}
|
|
229
|
+
</h2>
|
|
230
|
+
{children}
|
|
231
|
+
</section>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Which devices the store card names: only the stores the server has a listing for. */
|
|
236
|
+
function storeCardTitle(links: MobileAppLinks): string {
|
|
237
|
+
if (links.appStoreUrl && links.googlePlayUrl) return 'iPhone, iPad and Android';
|
|
238
|
+
return links.appStoreUrl ? 'iPhone and iPad' : 'Android';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function DownloadAppsPage({
|
|
242
|
+
endpoint = DOWNLOADS_API_PATH,
|
|
243
|
+
initialData,
|
|
244
|
+
shell = true,
|
|
245
|
+
backButton = false,
|
|
246
|
+
title,
|
|
247
|
+
subtitle,
|
|
248
|
+
showDesktop = true,
|
|
249
|
+
openStoresInNewTab = true,
|
|
250
|
+
onDownloadAction,
|
|
251
|
+
footer,
|
|
252
|
+
}: DownloadAppsPageProps) {
|
|
253
|
+
const { data, isLoading, error, reload } = useDownloads({ endpoint, initialData });
|
|
254
|
+
// The page adapts to the server's answer: a section exists only for what it names.
|
|
255
|
+
const systems = showDesktop && data ? rowsBySystem(data.desktop) : [];
|
|
256
|
+
const mobile = data?.mobile ?? null;
|
|
257
|
+
const hasMobile = !!mobile && (!!mobile.appStoreUrl || !!mobile.googlePlayUrl);
|
|
258
|
+
// Until the answer lands, a host that offers the desktop app is titled for it.
|
|
259
|
+
const offersDesktop = showDesktop && (!data || systems.length > 0);
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
// The install QR code arrives with this parameter: a phone or a tablet goes
|
|
263
|
+
// straight on to its store. `replace`, so Back returns to whatever sent the
|
|
264
|
+
// visitor here. Anyone else who opens the page on a phone reads it.
|
|
265
|
+
if (!mobile || !new URLSearchParams(window.location.search).has(DOWNLOAD_PAGE_STORE_PARAM)) return;
|
|
266
|
+
const storeUrl = resolveMobileStoreUrl(mobile, navigator.userAgent, navigator.maxTouchPoints);
|
|
267
|
+
if (storeUrl) window.location.replace(storeUrl);
|
|
268
|
+
}, [mobile]);
|
|
269
|
+
|
|
270
|
+
let body: ReactNode;
|
|
271
|
+
if (error && !data) {
|
|
272
|
+
body = <LoadError message="Could not load the downloads" onRetry={reload} />;
|
|
273
|
+
} else if (!data) {
|
|
274
|
+
body = isLoading ? (
|
|
275
|
+
<>
|
|
276
|
+
{showDesktop && (
|
|
277
|
+
<Section id="download-desktop" title="Desktop app">
|
|
278
|
+
<CardsSkeleton label="Loading the installers" />
|
|
279
|
+
</Section>
|
|
280
|
+
)}
|
|
281
|
+
<Section id="download-mobile" title="Mobile app">
|
|
282
|
+
<CardsSkeleton label="Loading the mobile app" />
|
|
283
|
+
</Section>
|
|
284
|
+
</>
|
|
285
|
+
) : null;
|
|
286
|
+
} else {
|
|
287
|
+
body = (
|
|
288
|
+
<>
|
|
289
|
+
{systems.length > 0 && (
|
|
290
|
+
<Section id="download-desktop" title="Desktop app">
|
|
291
|
+
<div className={cardsGridClass(systems.length)}>
|
|
292
|
+
{systems.map(system => (
|
|
293
|
+
<SystemCard key={system.os} system={system} onDownloadAction={onDownloadAction} />
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
</Section>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
{mobile && hasMobile && (
|
|
300
|
+
<Section id="download-mobile" title="Mobile app">
|
|
301
|
+
<div className={CARDS_GRID_CLASS}>
|
|
302
|
+
<div className={CARD_CLASS}>
|
|
303
|
+
<CardHeader title={storeCardTitle(mobile)} />
|
|
304
|
+
<p className="m-0 text-ods-text-secondary text-h4">Get alerts and respond to tickets on the go.</p>
|
|
305
|
+
<StoreBadgeLinks
|
|
306
|
+
appStoreUrl={mobile.appStoreUrl}
|
|
307
|
+
googlePlayUrl={mobile.googlePlayUrl}
|
|
308
|
+
openInNewTab={openStoresInNewTab}
|
|
309
|
+
/>
|
|
310
|
+
</div>
|
|
311
|
+
<div className={cn(CARD_CLASS, 'content-md:flex-row content-md:items-center content-md:justify-between')}>
|
|
312
|
+
<div className="flex flex-col gap-[var(--spacing-system-sf)]">
|
|
313
|
+
<CardHeader title="Scan with your phone" />
|
|
314
|
+
<p className="m-0 text-ods-text-secondary text-h4">
|
|
315
|
+
Point your camera at the code, or open {MOBILE_APP_INSTALL_HOST_PATH} on your phone.
|
|
316
|
+
</p>
|
|
317
|
+
</div>
|
|
318
|
+
{/* The plate is light on purpose: a QR code is read dark on light. */}
|
|
319
|
+
<div className="w-fit shrink-0 rounded-md bg-ods-bg-inverted p-[var(--spacing-system-sf)]">
|
|
320
|
+
<MobileAppQr className="h-[120px] w-[120px]" />
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</Section>
|
|
325
|
+
)}
|
|
326
|
+
</>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<DownloadAppsChrome
|
|
332
|
+
shell={shell}
|
|
333
|
+
backButton={backButton}
|
|
334
|
+
title={title ?? (offersDesktop ? DOWNLOADS_TITLE : DOWNLOADS_MOBILE_TITLE)}
|
|
335
|
+
subtitle={subtitle ?? (offersDesktop ? DOWNLOADS_TAGLINE : DOWNLOADS_MOBILE_TAGLINE)}
|
|
336
|
+
>
|
|
337
|
+
<div className={SECTIONS_CLASS}>
|
|
338
|
+
{body}
|
|
339
|
+
{footer}
|
|
340
|
+
</div>
|
|
341
|
+
</DownloadAppsChrome>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The page's chrome: the standalone `PageShell` or the host's padded box, and the frozen `PageLayout` header. */
|
|
346
|
+
function DownloadAppsChrome({
|
|
347
|
+
shell,
|
|
348
|
+
title,
|
|
349
|
+
subtitle,
|
|
350
|
+
backButton,
|
|
351
|
+
children,
|
|
352
|
+
}: Required<Pick<DownloadAppsPageProps, 'shell' | 'title' | 'subtitle' | 'backButton'>> & { children: ReactNode }) {
|
|
353
|
+
const router = useRouter();
|
|
354
|
+
const backCfg =
|
|
355
|
+
backButton === false
|
|
356
|
+
? undefined
|
|
357
|
+
: { label: backButton.label ?? 'Back to home', onClick: () => router.push(backButton.href ?? '/') };
|
|
358
|
+
const inner = (
|
|
359
|
+
<PageLayout title={title} subtitle={subtitle} backButton={backCfg} titleSize="h1" titleWrap>
|
|
360
|
+
{children}
|
|
361
|
+
</PageLayout>
|
|
362
|
+
);
|
|
363
|
+
return shell ? <PageShell>{inner}</PageShell> : <div className="page-shell-content">{inner}</div>;
|
|
364
|
+
}
|
|
@@ -22,6 +22,7 @@ export { ProductReleasesListPage, type ProductReleasesListPageProps } from './pr
|
|
|
22
22
|
export { DeliveryPage, type DeliveryPageProps } from './delivery-page';
|
|
23
23
|
export { OnboardingGuidesCatalogPage, type OnboardingGuidesCatalogPageProps } from './onboarding-guides-catalog-page';
|
|
24
24
|
export { TrustCenterPage, TrustCenterPageSkeleton, type TrustCenterPageProps } from './trust-center-page';
|
|
25
|
+
export { DownloadAppsPage, type DownloadAppsPageProps } from './download-apps-page';
|
|
25
26
|
|
|
26
27
|
// Existing full-pages re-exported for a single Help Center import site.
|
|
27
28
|
export { FaqDocumentPage, type FaqDocumentPageProps } from '../faq';
|
|
@@ -339,6 +339,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function ButtonI
|
|
|
339
339
|
download={download}
|
|
340
340
|
prefetch={false}
|
|
341
341
|
className={cn(shellClasses, isDisabled && 'pointer-events-none')}
|
|
342
|
+
// The click still reaches the caller (a download is what analytics counts).
|
|
343
|
+
onClick={onClick as unknown as React.MouseEventHandler<HTMLAnchorElement> | undefined}
|
|
342
344
|
aria-label={props['aria-label']}
|
|
343
345
|
>
|
|
344
346
|
{splitContent}
|
|
@@ -423,6 +425,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function ButtonI
|
|
|
423
425
|
download={download}
|
|
424
426
|
prefetch={false}
|
|
425
427
|
className={cn(classes, isDisabled && 'pointer-events-none')}
|
|
428
|
+
onClick={onClick as unknown as React.MouseEventHandler<HTMLAnchorElement> | undefined}
|
|
426
429
|
aria-label={props['aria-label']}
|
|
427
430
|
>
|
|
428
431
|
{content}
|
|
@@ -73,6 +73,7 @@ export * from './status-badge';
|
|
|
73
73
|
export * from './palette-badge';
|
|
74
74
|
export * from './department-badge';
|
|
75
75
|
export * from './store-badges';
|
|
76
|
+
export * from './mobile-app-qr';
|
|
76
77
|
export * from './status-indicator';
|
|
77
78
|
export * from './toaster';
|
|
78
79
|
// TODO: Add other UI components as they are moved to ui-kit
|