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}));