diff --git a/src/firefly/html/images/Background_Firefly.jpg b/src/firefly/html/images/Background_Firefly.jpg
new file mode 100644
index 0000000000..c828ca8157
Binary files /dev/null and b/src/firefly/html/images/Background_Firefly.jpg differ
diff --git a/src/firefly/js/Firefly.js b/src/firefly/js/Firefly.js
index dbeb76636d..ceaf6006e0 100644
--- a/src/firefly/js/Firefly.js
+++ b/src/firefly/js/Firefly.js
@@ -13,12 +13,12 @@ import 'styles/global.css';
import {APP_LOAD, dispatchAppOptions, dispatchConnectionStatus, dispatchUpdateAppData, getConnectionStatus} from './core/AppDataCntlr.js';
import {FireflyViewer} from './templates/fireflyviewer/FireflyViewer.js';
import {FireflySlate} from './templates/fireflyslate/FireflySlate.jsx';
-import {LandingPage} from './templates/fireflyviewer/LandingPage.jsx';
+import {StandaloneFireflyLanding} from './templates/fireflyviewer/StandaloneFireflyLanding.jsx';
import {LcViewer} from './templates/lightcurve/LcViewer.jsx';
import {HydraViewer} from './templates/hydra/HydraViewer.jsx';
import {routeEntry, ROUTER} from './templates/router/RouteHelper.jsx';
import {initApi} from './api/ApiBuild.js';
-import {dispatchUpdateLayoutInfo} from './core/LayoutCntlr.js';
+import {dispatchShowDropDown, dispatchUpdateLayoutInfo} from './core/LayoutCntlr.js';
import {FireflyRoot} from './ui/FireflyRoot.jsx';
import {SIAv2SearchPanel} from './ui/tap/SIASearchRootPanel';
import {getSIAv2ServicesByName} from './ui/tap/SiaUtil';
@@ -45,7 +45,6 @@ import {loadAllJobs} from './core/background/BackgroundUtil.js';
import {
makeDefImageSearchActions, makeDefTableSearchActions, makeDefTapSearchActions, makeExternalSearchActions
} from './ui/DefaultSearchActions.js';
-import {useStoreConnector} from 'firefly/ui/SimpleComponent';
let initDone = false;
const logger = Logger('Firefly-init');
@@ -119,7 +118,7 @@ const defAppProps = {
showUserInfo: false,
showViewsSwitch: true,
rightButtons: undefined,
- landingPage: ,
+ landingPage: ,
fileDropEventAction: 'FileUploadDropDownCmd',
menu: [
@@ -340,22 +339,25 @@ function setupGatorProtocolPanel(installedOptions, appProps) {
*/
export function startAsAppFromApi(divId, overrideProps={template: 'FireflySlate'}) {
-
+ // TODO: test and improvise the look
const Message = ({}) => (
Welcome to Firefly Viewer for Python
);
- const landingPage= ( dispatchShowDropDown({view: 'TAPSearch'})},
+ {title: 'Upload a file', desc: 'drag & drop here',
+ onClick: () => dispatchShowDropDown({view: 'FileUploadDropDownCmd'})},
+ ],
+ chips: [],
+ },
}}/>);
const props = {
diff --git a/src/firefly/js/templates/fireflyviewer/LandingPage.jsx b/src/firefly/js/templates/fireflyviewer/LandingPage.jsx
index 05e2b6317a..f10abf74d5 100644
--- a/src/firefly/js/templates/fireflyviewer/LandingPage.jsx
+++ b/src/firefly/js/templates/fireflyviewer/LandingPage.jsx
@@ -11,6 +11,30 @@ import {FileDropZone} from '../../visualize/ui/FileUploadViewPanel.jsx';
import {APP_HINT_IDS, AppHint, HINT_TIP_PLACEMENTS} from 'firefly/ui/AppHint';
+/**
+ * Full-page landing screen shown before the user has any results to display.
+ * Wraps content in a {@link FileDropZone} so drag-and-drop file upload works everywhere.
+ * AppHint overlays (tabs menu, background monitor) are mounted automatically when relevant.
+ *
+ * All visual regions are customizable via `slotProps`. Each slot accepts an optional `component`
+ * key to replace the default renderer; all remaining keys are forwarded as props to that component.
+ *
+ * Slot layout:
+ * ```
+ * tabsMenuHint → AppHint (anchored to first tab after Results tab)
+ * bgMonitorHint → AppHint (anchored to last tab: Job Monitor)
+ * ┌─ bgContainer (Box) ──────────────────────────────────────┐
+ * │ ┌─ contentSection (Stack) ───────────────────────────┐ │
+ * │ │ topSection → DefaultAppBranding (title/desc) │ │
+ * │ │ bottomSection → EmptyResults (actions) │ │
+ * │ └────────────────────────────────────────────────────┘ │
+ * └──────────────────────────────────────────────────────────┘
+ * ```
+ *
+ * @param {object} props
+ * @param {object} [props.slotProps={}] - Per-slot overrides (see layout above).
+ * @param {object} [props.sx] - sx forwarded to the root Sheet.
+ */
export function LandingPage({slotProps={}, sx, ...props}) {
const {appTitle,footer,
fileDropEventAction='FileUploadDropDownCmd'} = useContext(AppPropertiesCtx);
diff --git a/src/firefly/js/templates/fireflyviewer/StandaloneFireflyLanding.jsx b/src/firefly/js/templates/fireflyviewer/StandaloneFireflyLanding.jsx
new file mode 100644
index 0000000000..8bc343eb30
--- /dev/null
+++ b/src/firefly/js/templates/fireflyviewer/StandaloneFireflyLanding.jsx
@@ -0,0 +1,353 @@
+import Flare from '@mui/icons-material/Flare';
+import ManageSearch from '@mui/icons-material/ManageSearch';
+import UploadFile from '@mui/icons-material/UploadFile';
+import {Box, Chip, Divider, Sheet, Stack, Typography} from '@mui/joy';
+import {cloneDeep, defaultsDeep} from 'lodash';
+import React from 'react';
+import {arrayOf, func, node, number, object, shape, string} from 'prop-types';
+import BG_IMAGE from 'images/Background_Firefly.jpg';
+import {dispatchShowDropDown} from '../../core/LayoutCntlr.js';
+import {joyVarColorWithAlpha} from 'firefly/util/Color.js';
+import {LandingPage} from './LandingPage.jsx';
+import {dispatchShowDialog, SIDE_BAR_ID} from 'firefly/core/ComponentCntlr';
+
+
+// ── Configurable text & data ─────────────────────────────────────────────────
+
+const DEFAULT_TITLE = 'Firefly';
+const DEFAULT_TAGLINE = 'Discover and Explore Astronomy Data — across missions and archives';
+const DEFAULT_TRUST_LINE = 'Developed by Caltech/IPAC for NASA and NSF';
+
+const FEATURE_BADGES = [
+ // <2-word feature>
+ '✦ FITS/HiPS Images',
+ '⊕ DS9/MOC Overlays',
+ '⋮⋮ Catalogs at Scale',
+ '⇄ Interlinked Views',
+ '∿ Spectra & Charts',
+];
+
+const DEFAULT_ACTION_ITEMS = [
+ {
+ icon: ,
+ title: 'Search for data',
+ desc: 'Images, TAP, SIAv2 & more',
+ sub: 'using the tabs above',
+ onClick: () => dispatchShowDropDown({view: 'TAPSearch'}),
+ },
+ {
+ icon: ,
+ title: 'Find more search options',
+ desc: 'VO SCS, HiPS search & more',
+ sub: 'in the side menu ☰',
+ onClick: () => dispatchShowDialog(SIDE_BAR_ID),
+ },
+ {
+ icon: ,
+ title: 'Upload a file',
+ desc: 'FITS, VOTable, Parquet & more',
+ sub: 'drag & drop anywhere on screen',
+ onClick: () => dispatchShowDropDown({view: 'FileUploadDropDownCmd'}),
+ },
+];
+
+const DEFAULT_CHIPS = [
+ {
+ label: '🔍 2MASS PSC cone search at M5 →',
+ url: '?api=tap&service=https://irsa.ipac.caltech.edu/TAP&schema=fp_2mass&table=fp_psc&ra=229.64&dec=2.08&sr=2m&execute=true',
+ },
+ {
+ label: '🌌 WISE Atlas image of M31 →',
+ url: '?api=image&service=WISE&SurveyKey=Atlas&SurveyKeyBand=2&WorldPt=10.68479;41.26906;EQ_J2000&sr=.12',
+ },
+ {
+ label: '🔭 GAIA DR2 sources near Orion →',
+ url: '?api=tap&service=https://gea.esac.esa.int/tap-server/tap&schema=gaiadr2&table=gaiadr2.gaia_source&WorldPt=83.63321237;22.01446012;EQ_J2000&sr=20s&execute=true',
+ },
+ {
+ label: '🌐 SDSS HiPS view of M81 →',
+ url: '?api=hips&uri=ivo://CDS/P/SDSS9/color&ra=148.88822&dec=69.06529&sr=40m',
+ },
+ {
+ label: '📈 Euclid Q1 spectrum from cloud →',
+ url: '?api=load&url=s3://nasa-irsa-euclid-q1/q1/SIR/102159776/EUC_SIR_W-COMBSPEC_102159776_2024-11-05T16:21:17.235160Z.fits',
+ },
+];
+
+const DEFAULT_RESULT_HINT = 'Results from your searches will appear here or try one of the examples above';
+
+
+// ── Sub-components ───────────────────────────────────────────────────────────
+
+function FireflyAppBranding({title, tagline, trustLine}) {
+ const textShadow = '0 1px 2px rgba(0,0,0,0.9), 0 2px 8px rgba(0,0,0,0.7)';
+ return (
+
+ ({
+ color: theme.colorSchemes.dark.palette.text.primary,
+ letterSpacing: '0.04em',
+ // add a glow micro-animation
+ animation: 'fireflyGlow 5s ease-in-out infinite',
+ '@keyframes fireflyGlow': {
+ '0%': { opacity: 1, textShadow: '0px -1px 2px rgba(255,255,255,0.1)' },
+ '50%': { opacity: 0.85, textShadow: '0px -1px 8px rgba(96,184,224,1), 1px -2px 16px rgba(96,184,224,0.8)' },
+ '100%': { opacity: 1, textShadow: '0px -1px 2px rgba(255,255,255,0.1)' },
+ },
+ })}>
+ {title}
+
+ {tagline && (
+ ({
+ color: theme.colorSchemes.dark.palette.text.secondary,
+ textShadow,
+ textAlign: 'center',
+ })}>
+ {tagline}
+
+ )}
+
+ {FEATURE_BADGES.map((badge, idx) => (
+ {
+ const dp = theme.colorSchemes.dark.palette;
+ return {
+ fontSize: theme.vars.fontSize.xs,
+ color: joyVarColorWithAlpha(dp.text.secondary, 0.9),
+ background: joyVarColorWithAlpha(dp.neutral[200], 0.1),
+ backdropFilter: 'blur(4px)',
+ border: `0.5px solid ${joyVarColorWithAlpha(dp.neutral[100], 0.25)}`,
+ borderRadius: '4px',
+ px: 0.75, py: 0.25,
+ letterSpacing: '0.02em',
+ whiteSpace: 'nowrap',
+ };
+ }}>
+ {badge}
+
+ ))}
+
+ {trustLine && (
+ ({
+ color: theme.colorSchemes.dark.palette.text.tertiary,
+ textShadow
+ })}>
+ {trustLine}
+
+ )}
+
+ );
+}
+
+FireflyAppBranding.propTypes = {
+ title: string,
+ tagline: string,
+ trustLine: string,
+};
+
+
+function ActionCard({icon, title, desc, sub, onClick}) {
+ return (
+ {
+ const dp = theme.colorSchemes.dark.palette;
+ return {
+ flex: 1,
+ p: 1.5,
+ textAlign: 'center',
+ cursor: onClick ? 'pointer' : 'default',
+ borderRadius: '8px',
+ transition: 'background 0.15s',
+ ...(onClick && {'&:hover': {background: joyVarColorWithAlpha(dp.text.primary, 0.1)}}),
+ };
+ }}>
+ {icon && (
+ ({
+ display: 'flex', alignItems: 'center', justifyContent: 'center', mb: 1,
+ color: theme.colorSchemes.dark.palette.text.icon,
+ '.MuiSvgIcon-root': {color: theme.colorSchemes.dark.palette.text.icon}
+ })}>
+ {icon}
+
+ )}
+ ({fontWeight: 500, color: theme.colorSchemes.dark.palette.text.primary, mb: 0.5})}>
+ {title}
+
+ {desc && (
+ ({color: theme.colorSchemes.dark.palette.text.secondary, lineHeight: 1.5})}>
+ {desc}
+
+ )}
+ {sub && (
+ ({color: theme.colorSchemes.dark.palette.text.tertiary, mt: 0.25})}>
+ {sub}
+
+ )}
+
+ );
+}
+
+ActionCard.propTypes = {
+ icon: node,
+ title: string,
+ desc: string,
+ sub: string,
+ onClick: func,
+};
+
+
+function FireflyActionsPanel({actionItems, chips, resultHint = DEFAULT_RESULT_HINT, slotProps}) {
+ // actionItems may arrive merged with LandingPage's old-format defaults (which use `text`/`subtext`);
+ // fall back to built-in defaults when none of the items carry the new `title` field.
+ const cardItems = actionItems?.some((i) => i.title) ? actionItems : DEFAULT_ACTION_ITEMS;
+ const cardChips = Array.isArray(chips) ? chips : DEFAULT_CHIPS;
+ const hasFooter = cardChips.length > 0 || Boolean(resultHint);
+
+ return (
+ {
+ const dp = theme.colorSchemes.dark.palette;
+ return {
+ background: joyVarColorWithAlpha(dp.neutral[500], 0.25),
+ backdropFilter: 'blur(8px)',
+ border: `0.5px solid ${joyVarColorWithAlpha(dp.neutral[100], 0.15)}`,
+ borderRadius: '12px',
+ p: 3,
+ width: '100%',
+ };
+ }} {...slotProps?.root}>
+
+ {cardItems.map((item, idx) => (
+
+ {idx > 0 && (
+ ({
+ background: joyVarColorWithAlpha(theme.colorSchemes.dark.palette.neutral[300], 0.1),
+ my: 1,
+ })}/>
+ )}
+
+
+ ))}
+
+ {cardChips.length > 0 && (
+ <>
+ ({
+ background: joyVarColorWithAlpha(theme.colorSchemes.dark.palette.neutral[300], 0.1),
+ mb: 2
+ })}/>
+
+ {cardChips.map((chip, idx) => (
+ { window.location.href = chip.url; }}
+ sx={(theme) => {
+ const dp = theme.colorSchemes.dark.palette;
+ return {
+ color: dp.text.secondary,
+ fontSize: 'xs',
+ cursor: 'pointer',
+ py: .5, px: 1.5,
+ '& .MuiChip-action': {
+ borderColor: joyVarColorWithAlpha(dp.neutral[300], 0.3),
+ backgroundColor: joyVarColorWithAlpha(dp.neutral[200], 0.15),
+ '&:hover': {
+ borderColor: joyVarColorWithAlpha(dp.neutral[200], 0.3),
+ backgroundColor: joyVarColorWithAlpha(dp.neutral[100], 0.3),
+ },
+ },
+ '&:hover': {
+ color: dp.text.primary,
+ textDecoration: 'underline',
+ textUnderlineOffset: '2px',
+ },
+ };
+ }}>
+ {chip.label}
+
+ ))}
+
+ >
+ )}
+ {resultHint && (
+ ({fontSize: 'xs', color: theme.colorSchemes.dark.palette.text.tertiary, textAlign: 'center'})}>
+ {resultHint}
+
+ )}
+
+ );
+}
+
+FireflyActionsPanel.propTypes = {
+ actionItems: arrayOf(shape({
+ icon: node,
+ title: string,
+ desc: string,
+ sub: string,
+ onClick: func,
+ })),
+ chips: arrayOf(shape({label: string, url: string})),
+ resultHint: string,
+ slotProps: object,
+};
+
+// ── Main component ───────────────────────────────────────────────────────────
+
+/**
+ * Landing page for the standalone Firefly web application (Firefly.js defAppProps).
+ * Renders a full-bleed dark hero section with an astronomy background image, feature badges,
+ * action cards, and clickable example query chips.
+ *
+ * Wraps {@link LandingPage} via the slot system to override visual elements (bgContainer, contentSection, topSection,
+ * bottomSection) while keeping the store connector logic, FileDropZone, and AppHints.
+ *
+ * @param {object} props
+ * @param {object} [props.bgImage=BG_IMAGE] - URL/import of the hero background image.
+ * @param {number} [props.bgDimOpacity=0.2] - Opacity (0–1) of the dark overlay on the bg image to dim it. Higher = darker.
+ * @param {object} [props.slotProps={}] - Per-slot overrides forwarded to LandingPage.
+ * User-provided values take precedence; defaults fill in missing keys.
+ */
+export function StandaloneFireflyLanding({bgImage = BG_IMAGE, bgDimOpacity = 0.2, slotProps = {}, ...rest}) {
+ const mSlotProps = cloneDeep(slotProps);
+ defaultsDeep(mSlotProps, {
+ bgContainer: {
+ sx: (theme) => {
+ const bodyColor = theme.colorSchemes.dark.palette.background.body;
+ const dimColor = joyVarColorWithAlpha(bodyColor, bgDimOpacity);
+ return {
+ display: 'flex',
+ alignItems: 'center',
+ flexGrow: 1,
+ backgroundColor: bodyColor,
+ ...(bgImage && {
+ background: `linear-gradient(${dimColor}, ${dimColor}), url(${bgImage}) center/cover`,
+ }),
+ };
+ },
+ },
+ contentSection: {
+ alignItems: 'center',
+ spacing: 4,
+ sx: (theme) => {
+ const dp = theme.colorSchemes.dark.palette;
+ return {
+ maxWidth: '52rem',
+ mx: 'auto',
+ };
+ },
+ },
+ topSection: {
+ component: FireflyAppBranding,
+ title: DEFAULT_TITLE,
+ tagline: DEFAULT_TAGLINE,
+ trustLine: DEFAULT_TRUST_LINE,
+ },
+ bottomSection: {
+ component: FireflyActionsPanel,
+ resultHint: DEFAULT_RESULT_HINT,
+ // actionItems and chips intentionally omitted — FireflyActionsPanel handles its own defaults
+ // so that callers can supply arrays without defaultsDeep merging them by index
+ },
+ });
+ return ;
+}
+
+StandaloneFireflyLanding.propTypes = {
+ bgImage: string,
+ bgDimOpacity: number,
+ ...LandingPage.propTypes,
+};
diff --git a/src/firefly/js/templates/hydra/HydraViewer.jsx b/src/firefly/js/templates/hydra/HydraViewer.jsx
index 31dd1dead5..553ced85de 100644
--- a/src/firefly/js/templates/hydra/HydraViewer.jsx
+++ b/src/firefly/js/templates/hydra/HydraViewer.jsx
@@ -205,6 +205,29 @@ const defaultPropsWithBgImage = ({bgImage}) => ({
});
+/**
+ * Landing page for HydraViewer-based applications.
+ * Layout and visual style differ significantly depending on whether `bgImage` is provided.
+ *
+ * Without `bgImage` — standard layout: `HydraAppBranding` replaces `topSection`, the given
+ * `icon` is passed to `bottomSection`, and content is constrained to 80 rem.
+ *
+ * With `bgImage` — the image fills `bgContainer`; `contentSection` becomes a dark overlay
+ * panel centred over the image; `bottomSection` drops its icon and background to
+ * blend in; all typography is locked to dark-theme colors to look good on dark overlay.
+ *
+ * Wraps {@link LandingPage} via the slot system to override visual elements (bgContainer,
+ * contentSection, topSection, bottomSection) while keeping the store connector logic,
+ * FileDropZone, and AppHints.
+ *
+ * @param {object} props
+ * @param {string} [props.title] - App title forwarded to topSection.
+ * @param {string} [props.desc] - App description forwarded to topSection.
+ * @param {node} [props.icon] - Icon shown in bottomSection (no-bgImage mode only).
+ * @param {object} [props.bgImage] - URL/import of the background image. Triggers the alternate layout when set.
+ * @param {object} [props.slotProps={}] - Per-slot overrides forwarded to LandingPage.
+ * User-provided values take precedence; defaults fill in missing keys.
+ */
export function HydraLanding({icon, title, desc, bgImage, slotProps={}, ...props} ) {
const mSlotProps = cloneDeep(slotProps);
defaultsDeep(mSlotProps, defaultCommonProps({title, desc}));