/**
* The header links menu: the project's GitHub repo, the live web app's home (the fairyfox hub),
* and the project docs, followed (below a separator) by the app's own legal pages (Privacy, Terms,
* Cookies), plus the display-language picker. Every item opens in a new tab with
* `rel="noopener noreferrer"`; the legal pages are same-origin static pages under `/legal/`, opened
* in a new tab too so the app keeps its in-memory state.
*
* Two presentations, chosen by width (the hydration-safe {@link useCompact}; desktop renders from
* first paint, unchanged):
* • Desktop (>768px): a single icon button that opens a small dropdown popover.
* • Compact (<=768px): the links fold DIRECTLY into the header overflow menu as inline rows — no
* nested trigger, no separate bottom sheet (which read as a stray, differently-shaped panel).
* @module gui/components/LinksMenu
*/
import { useEffect, useState } from "react";
import { useIntl, defineMessages } from "react-intl";
import { Select } from "./Field.jsx";
import { useCompact } from "../lib/useCompact.js";
import { APP_VERSION } from "../lib/version.js";
import { AUTO_LOCALE, SUPPORTED_LOCALES, LOCALES } from "../i18n/index.js";
import { MenuIcon, GitHubIcon, BookIcon, HomeIcon, ExternalLinkIcon, ShieldIcon, FileTextIcon, CookieIcon, DownloadIcon, ServerIcon } from "./icons.jsx";
// External destinations. Kept here (not in `online.js`) so the menu owns its own link set.
import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime";
const LINKS = {
github: "https://github.com/1fairyfox/random-ai-prompt",
releases: "https://github.com/1fairyfox/random-ai-prompt/releases",
selfhost: "https://github.com/1fairyfox/random-ai-prompt#how-to-get-it",
docs: "https://fairyfox.io/random-ai-prompt/",
home: "https://fairyfox.io"
};
// The app's own legal pages — same-origin static pages served from `gui/public/legal/`.
const LEGAL = {
privacy: "/legal/privacy.html",
terms: "/legal/terms.html",
cookies: "/legal/cookies.html"
};
const msgs = defineMessages({
menu: {
id: "linksMenu.menu",
defaultMessage: "Menu"
},
links: {
id: "linksMenu.links",
defaultMessage: "Links"
},
github: {
id: "linksMenu.github",
defaultMessage: "GitHub repository"
},
githubDesc: {
id: "linksMenu.githubDesc",
defaultMessage: "Source code, issues, and releases"
},
docs: {
id: "linksMenu.docs",
defaultMessage: "Project docs"
},
docsDesc: {
id: "linksMenu.docsDesc",
defaultMessage: "API reference and developer guide"
},
home: {
id: "linksMenu.home",
defaultMessage: "fairyfox.io"
},
homeDesc: {
id: "linksMenu.homeDesc",
defaultMessage: "The fairyfox project home"
},
download: {
id: "linksMenu.download",
defaultMessage: "Get the desktop app"
},
downloadDesc: {
id: "linksMenu.downloadDesc",
defaultMessage: "Pre-built downloads for Windows, macOS, and Linux"
},
selfhost: {
id: "linksMenu.selfhost",
defaultMessage: "Run it yourself"
},
selfhostDesc: {
id: "linksMenu.selfhostDesc",
defaultMessage: "Self-host the online edition or build from source"
},
privacy: {
id: "linksMenu.privacy",
defaultMessage: "Privacy Policy"
},
privacyDesc: {
id: "linksMenu.privacyDesc",
defaultMessage: "What we collect (almost nothing)"
},
terms: {
id: "linksMenu.terms",
defaultMessage: "Terms & Conditions"
},
termsDesc: {
id: "linksMenu.termsDesc",
defaultMessage: "The rules for using the app"
},
cookies: {
id: "linksMenu.cookies",
defaultMessage: "Cookies Policy"
},
cookiesDesc: {
id: "linksMenu.cookiesDesc",
defaultMessage: "We don't use cookies"
},
language: {
id: "linksMenu.language",
defaultMessage: "Language"
},
localeAuto: {
id: "linksMenu.localeAuto",
defaultMessage: "Auto (browser)",
description: "Locale option that follows the browser's language"
}
});
/**
* @returns {JSX.Element} The header links menu.
*/
export default function LinksMenu({
settings,
setSettings
}) {
const intl = useIntl();
const compact = useCompact();
const [open, setOpen] = useState(false);
// App-wide display-language picker lives in this menu (alongside the project links + legal pages).
const localeOptions = [{
value: AUTO_LOCALE,
label: intl.formatMessage(msgs.localeAuto)
}, ...SUPPORTED_LOCALES.map(code => ({
value: code,
label: LOCALES[code].label
}))];
// Close on Escape (desktop dropdown only).
useEffect(() => {
if (!open) return undefined;
const onKey = e => {
if (e.key === "Escape") setOpen(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);
// `external: true` items get the little outbound-link glyph; the legal pages are part of the app
// (same origin), so they omit it even though they also open in a new tab.
const groups = [[{
href: LINKS.github,
Icon: GitHubIcon,
label: msgs.github,
desc: msgs.githubDesc,
external: true
}, {
href: LINKS.docs,
Icon: BookIcon,
label: msgs.docs,
desc: msgs.docsDesc,
external: true
}, {
href: LINKS.home,
Icon: HomeIcon,
label: msgs.home,
desc: msgs.homeDesc,
external: true
}], [{
href: LINKS.releases,
Icon: DownloadIcon,
label: msgs.download,
desc: msgs.downloadDesc,
external: true
}, {
href: LINKS.selfhost,
Icon: ServerIcon,
label: msgs.selfhost,
desc: msgs.selfhostDesc,
external: true
}], [{
href: LEGAL.privacy,
Icon: ShieldIcon,
label: msgs.privacy,
desc: msgs.privacyDesc
}, {
href: LEGAL.terms,
Icon: FileTextIcon,
label: msgs.terms,
desc: msgs.termsDesc
}, {
href: LEGAL.cookies,
Icon: CookieIcon,
label: msgs.cookies,
desc: msgs.cookiesDesc
}]];
const langGroup = setSettings && /*#__PURE__*/_jsxDEV("div", {
className: "links-group links-lang",
role: "group",
children: /*#__PURE__*/_jsxDEV(Select, {
label: intl.formatMessage(msgs.language),
value: settings?.locale ?? AUTO_LOCALE,
onChange: v => setSettings(s => ({
...s,
locale: v
})),
options: localeOptions
}, void 0, false)
}, void 0, false);
const linkGroups = groups.map((items, gi) => /*#__PURE__*/_jsxDEV("div", {
className: "links-group",
role: "group",
children: [gi > 0 && /*#__PURE__*/_jsxDEV("div", {
className: "links-sep",
role: "separator"
}, void 0, false), items.map(({
href,
Icon,
label,
desc,
external
}) => /*#__PURE__*/_jsxDEV("a", {
className: "links-item",
role: "menuitem",
href: href,
target: "_blank",
rel: "noopener noreferrer",
onClick: () => setOpen(false),
children: [/*#__PURE__*/_jsxDEV("span", {
className: "links-item-icon",
"aria-hidden": "true",
children: /*#__PURE__*/_jsxDEV(Icon, {}, void 0, false)
}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
className: "links-item-text",
children: [/*#__PURE__*/_jsxDEV("span", {
className: "links-item-label",
children: intl.formatMessage(label)
}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
className: "links-item-desc",
children: intl.formatMessage(desc)
}, void 0, false)]
}, void 0, true), external && /*#__PURE__*/_jsxDEV("span", {
className: "links-item-ext",
"aria-hidden": "true",
children: /*#__PURE__*/_jsxDEV(ExternalLinkIcon, {}, void 0, false)
}, void 0, false)]
}, href, true))]
}, gi, true));
// Compact (<=768px): fold the links straight into the header overflow menu — the project links,
// then legal pages, then the language picker — instead of a nested trigger that opened a separate
// bottom sheet. No trigger button, no popover, no scrim.
if (compact) {
return /*#__PURE__*/_jsxDEV("div", {
className: "links-menu links-inline",
role: "group",
"aria-label": intl.formatMessage(msgs.links),
children: [langGroup && /*#__PURE__*/_jsxDEV(_Fragment, {
children: [langGroup, /*#__PURE__*/_jsxDEV("div", {
className: "links-sep",
role: "separator"
}, void 0, false)]
}, void 0, true), linkGroups, /*#__PURE__*/_jsxDEV("div", {
className: "links-sep",
role: "separator"
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "links-version",
title: `Version ${APP_VERSION}`,
children: ["v", APP_VERSION]
}, void 0, true)]
}, void 0, true);
}
// Desktop (>768px): the icon-button dropdown (unchanged).
return /*#__PURE__*/_jsxDEV("div", {
className: "links-menu",
children: [/*#__PURE__*/_jsxDEV("button", {
className: `links-trigger${open ? " on" : ""}`,
onClick: () => setOpen(o => !o),
title: intl.formatMessage(msgs.menu),
"aria-label": intl.formatMessage(msgs.menu),
"aria-haspopup": "menu",
"aria-expanded": open,
children: [/*#__PURE__*/_jsxDEV(MenuIcon, {}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
className: "ctl-label",
children: intl.formatMessage(msgs.links)
}, void 0, false)]
}, void 0, true), open && /*#__PURE__*/_jsxDEV(_Fragment, {
children: [/*#__PURE__*/_jsxDEV("div", {
className: "links-scrim",
onClick: () => setOpen(false),
"aria-hidden": "true"
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "links-pop",
role: "menu",
"aria-label": intl.formatMessage(msgs.links),
children: [langGroup && /*#__PURE__*/_jsxDEV(_Fragment, {
children: [langGroup, /*#__PURE__*/_jsxDEV("div", {
className: "links-sep",
role: "separator"
}, void 0, false)]
}, void 0, true), linkGroups]
}, void 0, true)]
}, void 0, true)]
}, void 0, true);
}