tmp/webapp-docs/frontend/components/DplInsertBar.js

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