/**
* The DPL insert control above the prompt box. It teaches + inserts the lax line grammar (conditions,
* choices, calls, weights, …) that's hard to discover by typing: each construct shows its name, a
* one-line description, the literal syntax, and a live re-rolling example; picking one drops its
* snippet at the editor's cursor (via the editor's imperative `insertSnippet`).
*
* Two presentations, chosen by width (a hydration-safe media-query hook — the DESKTOP row renders
* from first paint, unchanged):
* • Desktop (>768px): the original slim ROW of per-category buttons, each opening its own popover.
* • Compact (<=768px): a single **Insert ▾** button opening a menu — category list → drill into a
* category's constructs with a Back row — so it never wraps on a narrow screen.
* @module gui/components/DplInsertBar
*/
import { useEffect, useMemo, useRef, useState } from "react";
import { useIntl, defineMessages } from "react-intl";
import { getDplInserts } from "../lib/dpl/dplInserts.js";
import { expandPrompt } from "../lib/promptEngine.js";
import { useCompact } from "../lib/useCompact.js";
import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime";
const msgs = defineMessages({
toolbar: {
id: "dplInsertBar.toolbar",
defaultMessage: "Insert DPL syntax"
},
insert: {
id: "dplInsertBar.insert",
defaultMessage: "Insert"
},
example: {
id: "dplInsertBar.example",
defaultMessage: "example"
}
});
/** Roll one item's `example` DPL into a concrete string (no auto-FX/artist noise). */
const rollExample = (dpl, settings) => {
try {
const out = expandPrompt(dpl, {
...settings,
autoAddFx: false,
autoAddArtists: false
});
return out && out.trim() ? out : "—";
} catch {
return "—";
}
};
/** Live-rolling examples for the currently-open category (keyed by item id). */
function useExamples(openCat, settings) {
const settingsRef = useRef(settings);
settingsRef.current = settings;
const [examples, setExamples] = useState({});
useEffect(() => {
if (!openCat) {
setExamples({});
return undefined;
}
const withEx = openCat.items.filter(it => it.example);
if (!withEx.length) return undefined;
const roll = () => {
const next = {};
for (const it of withEx) next[it.id] = rollExample(it.example, settingsRef.current);
setExamples(next);
};
roll();
const id = setInterval(roll, 1000);
return () => clearInterval(id);
}, [openCat]);
return examples;
}
/** One construct row (name + description + syntax + live example). Shared by both presentations. */
function ItemButton({
item,
example,
intl,
onPick
}) {
return /*#__PURE__*/_jsxDEV("button", {
type: "button",
className: "dpl-ins-item",
role: "menuitem",
onClick: () => onPick(item),
children: [/*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-item-head",
children: /*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-item-name",
children: item.label
}, void 0, false)
}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-item-desc",
children: item.desc
}, void 0, false), /*#__PURE__*/_jsxDEV("code", {
className: "dpl-ins-item-syntax",
children: item.syntax
}, void 0, false), item.example && /*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-item-ex",
children: [/*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-item-ex-label",
children: intl.formatMessage(msgs.example)
}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-item-ex-text",
children: example ?? "…"
}, void 0, false)]
}, void 0, true)]
}, void 0, true);
}
/** Desktop (>768px): the original row of per-category buttons, each with its own popover. */
function InsertRow({
inserts,
intl,
settings,
onPick
}) {
const [open, setOpen] = useState(null); // open category key, or null
const openCat = inserts.find(c => c.key === open) || null;
const examples = useExamples(openCat, settings);
useEffect(() => {
if (!open) return undefined;
const onKey = e => e.key === "Escape" && setOpen(null);
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);
const pick = item => {
onPick(item);
setOpen(null);
};
return /*#__PURE__*/_jsxDEV("div", {
className: "dpl-insert-bar",
role: "toolbar",
"aria-label": intl.formatMessage(msgs.toolbar),
children: [/*#__PURE__*/_jsxDEV("span", {
className: "dpl-insert-lead",
children: intl.formatMessage(msgs.insert)
}, void 0, false), inserts.map((cat, idx) => /*#__PURE__*/_jsxDEV("div", {
className: "dpl-ins-wrap",
children: [/*#__PURE__*/_jsxDEV("button", {
type: "button",
className: `dpl-ins-btn${open === cat.key ? " on" : ""}`,
onClick: () => setOpen(o => o === cat.key ? null : cat.key),
"aria-haspopup": "menu",
"aria-expanded": open === cat.key,
title: cat.hint,
children: [cat.label, /*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-caret",
"aria-hidden": "true",
children: "▾"
}, void 0, false)]
}, void 0, true), open === cat.key && /*#__PURE__*/_jsxDEV("div", {
className: `dpl-ins-pop${idx >= inserts.length - 2 ? " dpl-ins-pop-right" : ""}`,
role: "menu",
"aria-label": cat.label,
children: [/*#__PURE__*/_jsxDEV("div", {
className: "dpl-ins-pop-hint",
children: cat.hint
}, void 0, false), cat.items.map(it => /*#__PURE__*/_jsxDEV(ItemButton, {
item: it,
example: examples[it.id],
intl: intl,
onPick: pick
}, it.id, false))]
}, void 0, true)]
}, cat.key, true)), open && /*#__PURE__*/_jsxDEV("div", {
className: "dpl-ins-scrim",
onClick: () => setOpen(null),
"aria-hidden": "true"
}, void 0, false)]
}, void 0, true);
}
/** Compact (<=768px): a single Insert button opening a category list → drill-into-items menu. */
function InsertMenu({
inserts,
intl,
settings,
onPick
}) {
const [open, setOpen] = useState(false);
const [activeCat, setActiveCat] = useState(null);
const openCat = inserts.find(c => c.key === activeCat) || null;
const examples = useExamples(openCat, settings);
const close = () => {
setOpen(false);
setActiveCat(null);
};
useEffect(() => {
if (!open) return undefined;
const onKey = e => {
if (e.key !== "Escape") return;
if (activeCat) setActiveCat(null);else setOpen(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, activeCat]);
const pick = item => {
onPick(item);
close();
};
return /*#__PURE__*/_jsxDEV("div", {
className: "dpl-insert-bar",
role: "toolbar",
"aria-label": intl.formatMessage(msgs.toolbar),
children: /*#__PURE__*/_jsxDEV("div", {
className: "dpl-ins-wrap",
children: [/*#__PURE__*/_jsxDEV("button", {
type: "button",
className: `dpl-ins-btn dpl-ins-main${open ? " on" : ""}`,
onClick: () => open ? close() : setOpen(true),
"aria-haspopup": "menu",
"aria-expanded": open,
children: [intl.formatMessage(msgs.insert), /*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-caret",
"aria-hidden": "true",
children: "▾"
}, void 0, false)]
}, void 0, true), open && /*#__PURE__*/_jsxDEV("div", {
className: "dpl-ins-pop",
role: "menu",
"aria-label": intl.formatMessage(msgs.insert),
children: !openCat ? inserts.map(cat => /*#__PURE__*/_jsxDEV("button", {
type: "button",
className: "dpl-ins-cat",
role: "menuitem",
onClick: () => setActiveCat(cat.key),
"aria-haspopup": "menu",
children: [/*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-cat-text",
children: [/*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-cat-name",
children: cat.label
}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-cat-hint",
children: cat.hint
}, void 0, false)]
}, void 0, true), /*#__PURE__*/_jsxDEV("span", {
className: "dpl-ins-cat-arrow",
"aria-hidden": "true",
children: "›"
}, void 0, false)]
}, cat.key, true)) : /*#__PURE__*/_jsxDEV(_Fragment, {
children: [/*#__PURE__*/_jsxDEV("button", {
type: "button",
className: "dpl-ins-back",
onClick: () => setActiveCat(null),
children: [/*#__PURE__*/_jsxDEV("span", {
"aria-hidden": "true",
children: "‹"
}, void 0, false), " ", openCat.label]
}, void 0, true), openCat.items.map(it => /*#__PURE__*/_jsxDEV(ItemButton, {
item: it,
example: examples[it.id],
intl: intl,
onPick: pick
}, it.id, false))]
}, void 0, true)
}, void 0, false), open && /*#__PURE__*/_jsxDEV("div", {
className: "dpl-ins-scrim",
onClick: close,
"aria-hidden": "true"
}, void 0, false)]
}, void 0, true)
}, void 0, false);
}
/**
* @param {object} props
* @param {import("react").RefObject} props.editorRef Ref to the prompt {@link DplEditor} (for insertSnippet).
* @param {object} props.settings Generation settings (for SFW/NSFW-correct live examples).
* @returns {JSX.Element}
*/
export default function DplInsertBar({
editorRef,
settings
}) {
const intl = useIntl();
const inserts = useMemo(() => getDplInserts(intl), [intl]);
const compact = useCompact();
const pick = item => editorRef?.current?.insertSnippet?.(item.template, {
line: item.line,
wrap: item.wrap
});
const shared = {
inserts,
intl,
settings,
onPick: pick
};
return compact ? /*#__PURE__*/_jsxDEV(InsertMenu, {
...shared
}, void 0, false) : /*#__PURE__*/_jsxDEV(InsertRow, {
...shared
}, void 0, false);
}