tinacms 3.12.1 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -20,8 +20,7 @@ import { SlashInputPlugin, SlashPlugin } from "@udecode/plate-slash-command/reac
20
20
  import { TablePlugin, TableRowPlugin, useTableCellElement, TableProvider, useTableElement, useTableMergeState, useTableBordersDropdownMenuContentState, TableCellPlugin, TableCellHeaderPlugin } from "@udecode/plate-table/react";
21
21
  import { PlateElement, useReadOnly, useElement, PlateLeaf, useSelected, useFocused, useEditorRef, withRef as withRef$1, useComposedRef, useEditorPlugin, useElementSelector, withHOC, usePluginOption, useRemoveNodeButton, useEditorSelector, ParagraphPlugin, createPlatePlugin, PlateContent, PlateContainer, useEditorState, useMarkToolbarButtonState, useMarkToolbarButton, usePlateEditor, useFormInputProps, useEditorSelection, useEditorReadOnly, useOnClickOutside, useEventEditorValue, Plate } from "@udecode/plate/react";
22
22
  import { isLangSupported, formatCodeBlock, insertEmptyCodeBlock, isCodeBlockEmpty, unwrapCodeBlock, isSelectionAtCodeBlockStart } from "@udecode/plate-code-block";
23
- import { X, Search, ChevronDown, Check, AlertTriangle, BracesIcon, Heading6 as Heading6$1, Heading5 as Heading5$1, Heading4 as Heading4$1, Heading3 as Heading3$1, Heading2 as Heading2$1, Heading1 as Heading1$1, ChevronRight, Twitter, SunMedium, Moon, SeparatorHorizontal, Eye, Link2Off, Underline, Trash, Text, Table as Table$1, Superscript, Subscript, Strikethrough, Highlighter, Settings, RectangleHorizontal, RotateCcw, Pilcrow, Outdent, MoreHorizontal, Minus, WrapText, Keyboard, Indent, ExternalLink, Smile, Edit2, GripVertical, MessageSquarePlus, MessageSquare, Ungroup, Combine, RectangleVertical, Baseline, FileCode, PaintBucket, ChevronsUpDown, Quote, AlignRight as AlignRight$1, AlignLeft as AlignLeft$1, AlignJustify, AlignCenter as AlignCenter$1, Plus, PaintBucketIcon, CombineIcon, SquareSplitHorizontalIcon, Grid2X2Icon, Trash2Icon, ArrowUp, ArrowDown, XIcon, ArrowLeft, ArrowRight, EraserIcon, Info, CircleAlert, CircleCheck as CircleCheck$1, ChevronDownIcon as ChevronDownIcon$1, File as File$1, ChevronUp, Edit, Pencil, Clock, CalendarCheck, Calendar as Calendar$1, CalendarDays, RotateCw, ChevronLeft, WifiOff, TriangleAlert, RefreshCw, GitBranch, CircleHelp, Loader2, Lock, ArrowUpDown, GitFork, Globe, Copy, Folder as Folder$1, Clapperboard, LayoutGrid, List as List$1, CloudUpload, ArrowDownToLine, TextCursorInput, Images, AlertCircle, Image, Database, CheckCircle2, ImageOff, KeyRound, Cloud, LoaderCircle, ScrollText, RefreshCwOff, OctagonAlert, Menu, FilePlus, LogOut, FileStack, FolderOpen, History, Users, PanelLeft, Files, User, FileText, GitBranchIcon, Circle as Circle$1, ListOrdered, Grid3x3Icon, CircleX, Link, Unlink, SquarePen, Trash2, House, Unlock } from "lucide-react";
24
- import mermaid from "mermaid";
23
+ import { X, Search, ChevronDown, Check, AlertTriangle, BracesIcon, Heading6 as Heading6$1, Heading5 as Heading5$1, Heading4 as Heading4$1, Heading3 as Heading3$1, Heading2 as Heading2$1, Heading1 as Heading1$1, ChevronRight, Twitter, SunMedium, Moon, SeparatorHorizontal, Eye, Link2Off, Underline, Trash, Text, Table as Table$1, Superscript, Subscript, Strikethrough, Highlighter, Settings, RectangleHorizontal, RotateCcw, Pilcrow, Outdent, MoreHorizontal, Minus, WrapText, Keyboard, Indent, ExternalLink, Smile, Edit2, GripVertical, MessageSquarePlus, MessageSquare, Ungroup, Combine, RectangleVertical, Baseline, FileCode, PaintBucket, ChevronsUpDown, Quote, AlignRight as AlignRight$1, AlignLeft as AlignLeft$1, AlignJustify, AlignCenter as AlignCenter$1, Plus, PaintBucketIcon, CombineIcon, SquareSplitHorizontalIcon, Grid2X2Icon, Trash2Icon, ArrowUp, ArrowDown, XIcon, ArrowLeft, ArrowRight, EraserIcon, Info, CircleAlert, CircleCheck as CircleCheck$1, CircleIcon, ChevronDownIcon as ChevronDownIcon$1, File as File$1, ChevronUp, Edit, Pencil, Clock, CalendarCheck, Calendar as Calendar$1, CalendarDays, RotateCw, ChevronLeft, WifiOff, TriangleAlert, RefreshCw, GitBranch, CircleHelp, Loader2, Lock, ArrowUpDown, GitFork, Globe, Copy, Folder as Folder$1, Clapperboard, LayoutGrid, List as List$1, CloudUpload, ArrowDownToLine, TextCursorInput, Images, AlertCircle, Image, Database, CheckCircle2, ImageOff, KeyRound, Cloud, LoaderCircle, ScrollText, RefreshCwOff, OctagonAlert, Menu, FilePlus, LogOut, FileStack, FolderOpen, History, Users, PanelLeft, Files, User, FileText, GitBranchIcon, Circle as Circle$1, ListOrdered, Grid3x3Icon, CircleX, Link, Unlink, SquarePen, Trash2, House, Unlock } from "lucide-react";
25
24
  import { Slot } from "@radix-ui/react-slot";
26
25
  import { cva } from "class-variance-authority";
27
26
  import { Command as Command$2 } from "cmdk";
@@ -33,7 +32,7 @@ import { useHTMLInputCursorState, useComboboxInput } from "@udecode/plate-combob
33
32
  import { useComboboxContext, Combobox as Combobox$1, useComboboxStore, ComboboxProvider, Portal, ComboboxPopover, ComboboxItem } from "@ariakit/react";
34
33
  import { filterWords } from "@udecode/plate-combobox";
35
34
  import { toggleList, unwrapList, getListItemEntry } from "@udecode/plate-list";
36
- import { normalizeHeadingLevels, ALL_HEADING_LEVELS, ASYNC_POLLER_ERROR, canonicalPath, ERR_BRANCH_EXISTS, ERR_BRANCH_CONFLICT, isHeadingLevel, TinaSchema, addNamespaceToSchema, parseURL, ERR_HAS_REFERENCES, ERR_ALREADY_EXISTS, isValidRelativePath, RELATIVE_PATH_ALLOWED_CHARS_MESSAGE, resolveForm, RELATIVE_PATH_REGEX, normalizePath, ERR_NOT_INDEXED, validateSchema } from "@tinacms/schema-tools";
35
+ import { normalizeHeadingLevels, ALL_HEADING_LEVELS, MEDIA_MIME_TYPES, extensionOf, resolveMediaAccept, ASYNC_POLLER_ERROR, MEDIA_CATEGORIES, canonicalPath, ERR_BRANCH_EXISTS, ERR_BRANCH_CONFLICT, isHeadingLevel, TinaSchema, addNamespaceToSchema, parseURL, ERR_HAS_REFERENCES, ERR_ALREADY_EXISTS, isValidRelativePath, RELATIVE_PATH_ALLOWED_CHARS_MESSAGE, resolveForm, RELATIVE_PATH_REGEX, normalizePath, ERR_NOT_INDEXED, validateSchema } from "@tinacms/schema-tools";
37
36
  import { NAMER, resolveField } from "@tinacms/schema-tools";
38
37
  import { useBlockSelected, BlockSelectionPlugin } from "@udecode/plate-selection/react";
39
38
  import "@udecode/plate-dnd";
@@ -63,12 +62,12 @@ import { ExitBreakPlugin, SoftBreakPlugin } from "@udecode/plate-break/react";
63
62
  import { ResetNodePlugin } from "@udecode/plate-reset-node/react";
64
63
  import { TrailingBlockPlugin } from "@udecode/plate-trailing-block";
65
64
  import { BaseBlockquotePlugin } from "@udecode/plate-block-quote";
65
+ import { autoformatSmartQuotes, autoformatPunctuation, autoformatLegal, autoformatArrow, autoformatMath } from "@udecode/plate-autoformat";
66
66
  import { HeadingPlugin } from "@udecode/plate-heading/react";
67
67
  import { IndentListPlugin } from "@udecode/plate-indent-list/react";
68
68
  import { NodeIdPlugin } from "@udecode/plate-node-id";
69
69
  import { offset, flip, shift, limitShift, useVirtualFloating, getSelectionBoundingClientRect } from "@udecode/plate-floating";
70
70
  import { getLinkAttributes } from "@udecode/plate-link";
71
- import { autoformatSmartQuotes, autoformatPunctuation, autoformatLegal, autoformatArrow, autoformatMath } from "@udecode/plate-autoformat";
72
71
  import * as yup from "yup";
73
72
  import { format as format$3, formatDistanceToNow } from "date-fns";
74
73
  import { enUS } from "date-fns/locale";
@@ -1396,20 +1395,6 @@ function CodeBlockCombobox({
1396
1395
  )))))
1397
1396
  ));
1398
1397
  }
1399
- const MermaidElementWithRef = ({ config }) => {
1400
- const mermaidRef = useRef(null);
1401
- useEffect(() => {
1402
- if (mermaidRef.current) {
1403
- const renderMermaid = async () => {
1404
- await mermaid.run({
1405
- nodes: [mermaidRef.current.querySelector(".mermaid")]
1406
- });
1407
- };
1408
- renderMermaid();
1409
- }
1410
- }, [config]);
1411
- return /* @__PURE__ */ React__default.createElement("div", { contentEditable: false, className: "border-border border-b pt-10" }, /* @__PURE__ */ React__default.createElement("div", { ref: mermaidRef }, /* @__PURE__ */ React__default.createElement("pre", { className: "mermaid not-tina-prose" }, config)));
1412
- };
1413
1398
  function ErrorMessage$1({ error }) {
1414
1399
  if (!error) return null;
1415
1400
  return /* @__PURE__ */ React__default.createElement(
@@ -1423,6 +1408,52 @@ function ErrorMessage$1({ error }) {
1423
1408
  /* @__PURE__ */ React__default.createElement("div", { className: "ml-3 flex-1" }, /* @__PURE__ */ React__default.createElement("pre", { className: "m-0 font-mono text-sm text-red-700 whitespace-pre-wrap break-words" }, error))
1424
1409
  );
1425
1410
  }
1411
+ const RENDERER_FAILED = "The diagram renderer did not load.";
1412
+ const MermaidElementWithRef = ({ config }) => {
1413
+ const mermaidRef = useRef(null);
1414
+ const [status, setStatus] = useState("loading");
1415
+ const [error, setError] = useState(null);
1416
+ useEffect(() => {
1417
+ let isCancelled = false;
1418
+ setStatus("loading");
1419
+ const render = async () => {
1420
+ var _a;
1421
+ try {
1422
+ const node = (_a = mermaidRef.current) == null ? void 0 : _a.querySelector(".mermaid");
1423
+ if (!node) throw new Error(RENDERER_FAILED);
1424
+ const { default: mermaid } = await import("mermaid");
1425
+ await mermaid.run({ nodes: [node] });
1426
+ if (!isCancelled) {
1427
+ setError(null);
1428
+ setStatus("ready");
1429
+ }
1430
+ } catch (cause) {
1431
+ if (isCancelled) {
1432
+ return;
1433
+ }
1434
+ if (cause instanceof Error) {
1435
+ setError(cause.message || RENDERER_FAILED);
1436
+ } else {
1437
+ setError(RENDERER_FAILED);
1438
+ }
1439
+ setStatus("failed");
1440
+ }
1441
+ };
1442
+ render();
1443
+ return () => {
1444
+ isCancelled = true;
1445
+ };
1446
+ }, [config]);
1447
+ return /* @__PURE__ */ React__default.createElement("div", { contentEditable: false, className: "border-border border-b pt-10" }, /* @__PURE__ */ React__default.createElement("div", { className: "relative" }, /* @__PURE__ */ React__default.createElement(
1448
+ "div",
1449
+ {
1450
+ ref: mermaidRef,
1451
+ className: status === "loading" ? "opacity-0" : void 0
1452
+ },
1453
+ /* @__PURE__ */ React__default.createElement("pre", { className: "mermaid not-tina-prose" }, config)
1454
+ ), status === "loading" && /* @__PURE__ */ React__default.createElement("div", { className: "absolute inset-0 flex items-center justify-center text-sm text-muted-foreground" }, "Loading diagram…")), status === "failed" && /* @__PURE__ */ React__default.createElement(ErrorMessage$1, { error }));
1455
+ };
1456
+ const PARSE_FAILED = "An error occurred while parsing the diagram.";
1426
1457
  function codeLineToString(content) {
1427
1458
  return (content.children || []).map(
1428
1459
  (line) => (line.children || []).map((textNode) => textNode.text).join("")
@@ -1440,17 +1471,21 @@ const CodeBlockElement = withRef(
1440
1471
  let isCancelled = false;
1441
1472
  const validateMermaid = async () => {
1442
1473
  try {
1474
+ const { default: mermaid } = await import("mermaid");
1443
1475
  await mermaid.parse(
1444
1476
  codeLineToString(element)
1445
1477
  );
1446
1478
  if (!isCancelled) {
1447
1479
  setCodeBlockError(null);
1448
1480
  }
1449
- } catch (err) {
1450
- if (!isCancelled) {
1451
- setCodeBlockError(
1452
- String(err.message) || "An error occurred while parsing the diagram."
1453
- );
1481
+ } catch (cause) {
1482
+ if (isCancelled) {
1483
+ return;
1484
+ }
1485
+ if (cause instanceof Error) {
1486
+ setCodeBlockError(cause.message || PARSE_FAILED);
1487
+ } else {
1488
+ setCodeBlockError(PARSE_FAILED);
1454
1489
  }
1455
1490
  }
1456
1491
  };
@@ -2759,7 +2794,7 @@ const DropdownMenuTrigger$1 = DropdownMenuPrimitive.Trigger;
2759
2794
  const DropdownMenuGroup = DropdownMenuPrimitive.Group;
2760
2795
  const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
2761
2796
  const DropdownMenuSub = DropdownMenuPrimitive.Sub;
2762
- const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
2797
+ const DropdownMenuRadioGroup$1 = DropdownMenuPrimitive.RadioGroup;
2763
2798
  const DropdownMenuSubTrigger = withRef(({ children, className, inset, ...props }, ref) => /* @__PURE__ */ React__default.createElement(
2764
2799
  DropdownMenuPrimitive.SubTrigger,
2765
2800
  {
@@ -2818,7 +2853,7 @@ const DropdownMenuCheckboxItem = withRef(({ children, className, ...props }, ref
2818
2853
  /* @__PURE__ */ React__default.createElement("span", { className: "absolute left-2 flex size-3.5 items-center justify-center" }, /* @__PURE__ */ React__default.createElement(DropdownMenuPrimitive.ItemIndicator, null, /* @__PURE__ */ React__default.createElement(Icons.check, { className: "size-4" }))),
2819
2854
  children
2820
2855
  ));
2821
- const DropdownMenuRadioItem = withRef(({ children, className, hideIcon, ...props }, ref) => /* @__PURE__ */ React__default.createElement(
2856
+ const DropdownMenuRadioItem$1 = withRef(({ children, className, hideIcon, ...props }, ref) => /* @__PURE__ */ React__default.createElement(
2822
2857
  DropdownMenuPrimitive.RadioItem,
2823
2858
  {
2824
2859
  className: cn$1(
@@ -2842,7 +2877,7 @@ const dropdownMenuLabelVariants = cva(
2842
2877
  }
2843
2878
  }
2844
2879
  );
2845
- const DropdownMenuLabel = withVariants(
2880
+ const DropdownMenuLabel$1 = withVariants(
2846
2881
  DropdownMenuPrimitive.Label,
2847
2882
  dropdownMenuLabelVariants,
2848
2883
  ["inset"]
@@ -4166,6 +4201,17 @@ TinaField.propTypes = {
4166
4201
  Component: PropTypes.any.isRequired,
4167
4202
  children: PropTypes.any
4168
4203
  };
4204
+ const isErrorNamed = (error, name2) => error instanceof Error && error.name === name2;
4205
+ class SessionExpiredError extends Error {
4206
+ constructor() {
4207
+ super("Your session has ended. Please sign in again.");
4208
+ this.name = "SessionExpiredError";
4209
+ }
4210
+ }
4211
+ const isSessionExpiredError = (error) => error instanceof SessionExpiredError || isErrorNamed(error, "SessionExpiredError");
4212
+ const dispatchSessionExpired = (events) => {
4213
+ events.dispatch({ type: "cms:session-expired" });
4214
+ };
4169
4215
  const LoadingDots = ({
4170
4216
  dotSize = 8,
4171
4217
  color = "white"
@@ -4474,6 +4520,54 @@ function DropdownMenuItem({
4474
4520
  }
4475
4521
  );
4476
4522
  }
4523
+ function DropdownMenuRadioGroup({
4524
+ ...props
4525
+ }) {
4526
+ return /* @__PURE__ */ React.createElement(
4527
+ DropdownMenuPrimitive.RadioGroup,
4528
+ {
4529
+ "data-slot": "dropdown-menu-radio-group",
4530
+ ...props
4531
+ }
4532
+ );
4533
+ }
4534
+ function DropdownMenuRadioItem({
4535
+ className,
4536
+ children,
4537
+ ...props
4538
+ }) {
4539
+ return /* @__PURE__ */ React.createElement(
4540
+ DropdownMenuPrimitive.RadioItem,
4541
+ {
4542
+ "data-slot": "dropdown-menu-radio-item",
4543
+ className: cn(
4544
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
4545
+ className
4546
+ ),
4547
+ ...props
4548
+ },
4549
+ /* @__PURE__ */ React.createElement("span", { className: "pointer-events-none absolute left-2 flex size-3.5 items-center justify-center" }, /* @__PURE__ */ React.createElement(DropdownMenuPrimitive.ItemIndicator, null, /* @__PURE__ */ React.createElement(CircleIcon, { className: "size-2 fill-current" }))),
4550
+ children
4551
+ );
4552
+ }
4553
+ function DropdownMenuLabel({
4554
+ className,
4555
+ inset,
4556
+ ...props
4557
+ }) {
4558
+ return /* @__PURE__ */ React.createElement(
4559
+ DropdownMenuPrimitive.Label,
4560
+ {
4561
+ "data-slot": "dropdown-menu-label",
4562
+ "data-inset": inset,
4563
+ className: cn(
4564
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
4565
+ className
4566
+ ),
4567
+ ...props
4568
+ }
4569
+ );
4570
+ }
4477
4571
  function DropdownMenuSeparator({
4478
4572
  className,
4479
4573
  ...props
@@ -5557,12 +5651,38 @@ const dropzoneAcceptFromString = (str) => {
5557
5651
  ...(str || DEFAULT_MEDIA_UPLOAD_TYPES).split(",").map((x) => ({ [x]: [] }))
5558
5652
  );
5559
5653
  };
5560
- const isImage = (filename) => {
5561
- return /\.(gif|jpg|jpeg|tiff|png|svg|webp|avif)(\?.*)?$/i.test(filename);
5562
- };
5563
- const isVideo = (filename) => {
5564
- return /\.(mp4|webm|ogg|m4v|mov|avi|flv|mkv)(\?.*)?$/i.test(filename);
5654
+ const dropzoneAcceptFromExtensions = (extensions) => {
5655
+ if (!extensions.length) return void 0;
5656
+ const accept = {};
5657
+ for (const ext of extensions) {
5658
+ const mimeType = MEDIA_MIME_TYPES[ext];
5659
+ accept[mimeType] ?? (accept[mimeType] = []);
5660
+ accept[mimeType].push(`.${ext}`);
5661
+ }
5662
+ return accept;
5565
5663
  };
5664
+ const PREVIEWABLE_IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
5665
+ "gif",
5666
+ "jpg",
5667
+ "jpeg",
5668
+ "tiff",
5669
+ "png",
5670
+ "svg",
5671
+ "webp",
5672
+ "avif"
5673
+ ]);
5674
+ const PREVIEWABLE_VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
5675
+ "mp4",
5676
+ "webm",
5677
+ "ogg",
5678
+ "m4v",
5679
+ "mov",
5680
+ "avi",
5681
+ "flv",
5682
+ "mkv"
5683
+ ]);
5684
+ const isImage = (filename) => PREVIEWABLE_IMAGE_EXTENSIONS.has(extensionOf(filename ?? ""));
5685
+ const isVideo = (filename) => PREVIEWABLE_VIDEO_EXTENSIONS.has(extensionOf(filename ?? ""));
5566
5686
  const absoluteImgURL = (str) => {
5567
5687
  if (str.startsWith("http")) return str;
5568
5688
  return `${window.location.origin}${str}`;
@@ -5624,12 +5744,13 @@ const StyledImage = ({ src }) => {
5624
5744
  const StyledFile = ({ src }) => {
5625
5745
  return /* @__PURE__ */ React.createElement("div", { className: "max-w-full w-full flex-1 flex justify-start items-center gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "w-12 h-12 bg-white shadow border border-gray-100 rounded flex justify-center flex-none" }, /* @__PURE__ */ React.createElement(File$1, { className: "w-3/5 h-full text-gray-300" })), /* @__PURE__ */ React.createElement("span", { className: "text-base text-left flex-1 text-gray-500 w-full break-words truncate" }, src));
5626
5746
  };
5627
- const ImageUpload = React.forwardRef(({ onDrop, onClear, onClick, value, src, loading }, ref) => {
5747
+ const ImageUpload = React.forwardRef(({ onDrop, onClear, onClick, value, src, loading, accept }, ref) => {
5628
5748
  const cms = useCMS();
5629
5749
  const { getRootProps, getInputProps } = useDropzone$1({
5630
- accept: dropzoneAcceptFromString(
5631
- cms.media.accept || DEFAULT_MEDIA_UPLOAD_TYPES
5632
- ),
5750
+ // A field's `accept` overrides the global `media.accept`, so a file the
5751
+ // field would refuse is rejected here rather than uploaded and then
5752
+ // discarded by the insert guard.
5753
+ accept: dropzoneAcceptFromExtensions(resolveMediaAccept(accept)) ?? dropzoneAcceptFromString(cms.media.accept || DEFAULT_MEDIA_UPLOAD_TYPES),
5633
5754
  onDrop,
5634
5755
  noClick: !!onClick
5635
5756
  });
@@ -7349,6 +7470,16 @@ const ImageField = wrapFieldsWithMeta(
7349
7470
  var _a, _b;
7350
7471
  if (media) {
7351
7472
  const item = Array.isArray(media) ? media[0] : media;
7473
+ const allowed = resolveMediaAccept(
7474
+ props.field.accept
7475
+ );
7476
+ const ext = extensionOf(item.filename) || extensionOf(item.src ?? "");
7477
+ if (allowed.length && !allowed.includes(ext)) {
7478
+ cms.alerts.error(
7479
+ `${item.filename} is not an accepted file type. This field accepts ${allowed.join(", ")}.`
7480
+ );
7481
+ return;
7482
+ }
7352
7483
  const parsedValue = typeof ((_b = (_a = cms == null ? void 0 : cms.media) == null ? void 0 : _a.store) == null ? void 0 : _b.parse) === "function" ? cms.media.store.parse(item) : item.src || item;
7353
7484
  props.input.onChange(parsedValue);
7354
7485
  }
@@ -7361,12 +7492,14 @@ const ImageField = wrapFieldsWithMeta(
7361
7492
  value,
7362
7493
  src,
7363
7494
  loading: isImgUploading,
7495
+ accept: props.field.accept,
7364
7496
  onClick: () => {
7365
7497
  const directory = uploadDir(props.form.getState().values);
7366
7498
  cms.media.open({
7367
7499
  allowDelete: true,
7368
7500
  directory,
7369
- onSelect: onChange
7501
+ onSelect: onChange,
7502
+ accept: props.field.accept
7370
7503
  });
7371
7504
  },
7372
7505
  onDrop: async ([file], fileRejections) => {
@@ -9938,9 +10071,21 @@ let Alerts$1 = class Alerts {
9938
10071
  this.add(level, message, timeout);
9939
10072
  }
9940
10073
  });
10074
+ /**
10075
+ * While suppressed (the window between a session expiring and the user
10076
+ * signing back in), new alerts are dropped so nothing paints over the
10077
+ * login modal. Alerts already showing are left alone.
10078
+ */
10079
+ __publicField(this, "suppressed", false);
9941
10080
  this.events = events;
9942
10081
  this.map = map;
9943
10082
  this.events.subscribe("*", this.mapEventToAlert);
10083
+ this.events.subscribe("cms:session-expired", () => {
10084
+ this.suppressed = true;
10085
+ });
10086
+ this.events.subscribe("cms:login", () => {
10087
+ this.suppressed = false;
10088
+ });
9944
10089
  }
9945
10090
  setMap(eventsToAlerts) {
9946
10091
  this.map = {
@@ -9949,6 +10094,10 @@ let Alerts$1 = class Alerts {
9949
10094
  };
9950
10095
  }
9951
10096
  add(level, message, timeout = 4e3) {
10097
+ if (this.suppressed) {
10098
+ return () => {
10099
+ };
10100
+ }
9952
10101
  const alert = {
9953
10102
  level,
9954
10103
  message,
@@ -9989,219 +10138,6 @@ let Alerts$1 = class Alerts {
9989
10138
  return this.add("error", message, timeout);
9990
10139
  }
9991
10140
  };
9992
- class PluginTypeManager {
9993
- constructor(events) {
9994
- /**
9995
- * @ignore
9996
- */
9997
- __publicField(this, "plugins", {});
9998
- this.events = events;
9999
- }
10000
- /**
10001
- * Gets the [[PluginType|collection of plugins]] for the given type.
10002
- *
10003
- * #### Example: Basic Usage
10004
- *
10005
- * ```ts
10006
- * const colorPlugins = cms.plugins.get("color")
10007
- * ```
10008
- *
10009
- * #### Example: Advanced Types
10010
- *
10011
- * A type param can be added to specify the kind of Plugin
10012
- * that is being listed.
10013
- *
10014
- * ```ts
10015
- * const colorPlugins = cms.plugins.get<ColorPlugin>("color")
10016
- * ```
10017
- *
10018
- * @param type The type of plugins to be retrieved
10019
- * @typeparam P A subclass of Plugin. Optional.
10020
- */
10021
- getType(type2) {
10022
- return this.plugins[type2] = this.plugins[type2] || new PluginType(type2, this.events);
10023
- }
10024
- /**
10025
- * An alias to [[get]]
10026
- *
10027
- * ### !DEPRECATED!
10028
- *
10029
- * This name is unnecessarily verbose and weird.
10030
- */
10031
- findOrCreateMap(type2) {
10032
- return this.getType(type2);
10033
- }
10034
- /**
10035
- * Adds a Plugin to the CMS.
10036
- *
10037
- * #### Example: Basic Usage
10038
- *
10039
- * ```js
10040
- * cms.plugins.add({ __type: "color", name: "red" })
10041
- * ```
10042
- *
10043
- * #### Example: Advanced Types
10044
- *
10045
- * ```ts
10046
- * interface ColorPlugin extends Plugin {
10047
- * __type: "color"
10048
- * hex: string
10049
- * rgb: string
10050
- * }
10051
- *
10052
- * cms.plugins.add<ColorPlugin>({
10053
- * __type: "color",
10054
- * name: "red",
10055
- * hex: "#FF0000",
10056
- * rgb: "RGBA(255, 0, 0, 1)"
10057
- * })
10058
- * ```
10059
- *
10060
- * @typeparam P A subclass of Plugin. Optional.
10061
- * @param plugin
10062
- * @todo Consider returning the plugin which was just added.
10063
- */
10064
- add(plugin) {
10065
- this.findOrCreateMap(plugin.__type).add(plugin);
10066
- }
10067
- /**
10068
- * Removes the given plugin from the CMS.
10069
- *
10070
- * #### Example
10071
- *
10072
- * In this example a plugin is added to the CMS and removed
10073
- * 5 seconds later.
10074
- *
10075
- * ```ts
10076
- * const redPlugin = {
10077
- * __type: "color",
10078
- * name: "red",
10079
- * hex: "#FF0000",
10080
- * rgb: "RGBA(255, 0, 0, 1)"
10081
- * }
10082
- *
10083
- * cms.plugins.add(redPlugin)
10084
- *
10085
- * setTimeout(() => {
10086
- * cms.plugins.remove(redPlugin)
10087
- * }, 5000)
10088
- * ```
10089
- *
10090
- * @typeparam P A subclass of Plugin. Optional.
10091
- * @param plugin The plugin to be removed from the CMS.
10092
- */
10093
- remove(plugin) {
10094
- this.findOrCreateMap(plugin.__type).remove(plugin);
10095
- }
10096
- /**
10097
- * Returns a list of all the plugins of the given type.
10098
- *
10099
- * #### Example: Basic Usage
10100
- *
10101
- * ```ts
10102
- * cms.plugins.all("color").forEach(color => {
10103
- * console.log(color.name)
10104
- * })
10105
- * ```
10106
- *
10107
- * #### Example: Advanced Types
10108
- *
10109
- * A generic can be added to specify the type of plugins
10110
- * that are being retrieved.
10111
- *
10112
- * ```ts
10113
- * cms.plugins.all<ColorPlugin>("color").forEach(color => {
10114
- * console.log(color.name, color.hex)
10115
- * })
10116
- * ```
10117
- *
10118
- * @typeparam P A subclass of Plugin. Optional.
10119
- * @param type The name of the plugin
10120
- * @returns An array of all plugins of the given type.
10121
- */
10122
- all(type2) {
10123
- return this.findOrCreateMap(type2).all();
10124
- }
10125
- }
10126
- class PluginType {
10127
- /**
10128
- *
10129
- * @param __type The `__type` of plugin being managed.
10130
- */
10131
- constructor(__type, events) {
10132
- /**
10133
- * @ignore
10134
- */
10135
- __publicField(this, "__plugins", {});
10136
- this.__type = __type;
10137
- this.events = events;
10138
- }
10139
- /**
10140
- * Adds a new plugin to the collection.
10141
- *
10142
- * ### Example
10143
- *
10144
- * ```ts
10145
- * interface ColorPlugin extends Plugin {
10146
- * hex: string
10147
- * }
10148
- *
10149
- * const colorPlugins = new PluginType<ColorPlugin>("color")
10150
- *
10151
- * colorPlugins.add({ name: "red", hex: "#f00" })
10152
- * ```
10153
- *
10154
- * @param plugin A new plugin. The `__type` is optional and will be added if it's missing.
10155
- */
10156
- add(plugin) {
10157
- const p = plugin;
10158
- if (!p.__type) {
10159
- p.__type = this.__type;
10160
- }
10161
- this.__plugins[p.name] = p;
10162
- this.events.dispatch({ type: `plugin:add:${this.__type}` });
10163
- }
10164
- all() {
10165
- return Object.keys(this.__plugins).map((name2) => this.__plugins[name2]);
10166
- }
10167
- /**
10168
- *
10169
- * Looks up a plugin by it's `name`.
10170
- *
10171
- * ### Example
10172
- *
10173
- * ```ts
10174
- * const colorPlugins = new PluginType<ColorPlugin>("color")
10175
- *
10176
- * colorPlugins.add({ name: "red", hex: "#f00" })
10177
- *
10178
- * colorPlugins.find("red") // { __type: "color", name: "red", hex: "#f00" }
10179
- * colorPlugin.find("large") // undefined
10180
- * ```
10181
- *
10182
- * @param name The `name` of the plugin to be retrieved.
10183
- */
10184
- find(name2) {
10185
- return this.__plugins[name2];
10186
- }
10187
- /**
10188
- * Pass this function a plugin or the `name` of a plugin to have
10189
- * it be removed from the CMS.
10190
- *
10191
- * @param pluginOrName The `name` of a plugin, or the plugin itself.
10192
- * @returns The plugin that was removed, or `undefined` if it was not found.
10193
- */
10194
- remove(pluginOrName) {
10195
- const name2 = typeof pluginOrName === "string" ? pluginOrName : pluginOrName.name;
10196
- const plugin = this.__plugins[name2];
10197
- delete this.__plugins[name2];
10198
- this.events.dispatch({ type: `plugin:remove:${this.__type}` });
10199
- return plugin;
10200
- }
10201
- subscribe(cb) {
10202
- return this.events.subscribe(`plugin:*:${this.__type}`, cb);
10203
- }
10204
- }
10205
10141
  class EventBus {
10206
10142
  constructor() {
10207
10143
  __publicField(this, "listeners", /* @__PURE__ */ new Set());
@@ -10280,6 +10216,19 @@ class Listener {
10280
10216
  return !ignoresEvent;
10281
10217
  }
10282
10218
  }
10219
+ class Flags {
10220
+ constructor(events) {
10221
+ __publicField(this, "_flags", /* @__PURE__ */ new Map());
10222
+ this.events = events;
10223
+ }
10224
+ get(key) {
10225
+ return this._flags.get(key);
10226
+ }
10227
+ set(key, value) {
10228
+ this._flags.set(key, value);
10229
+ this.events.dispatch({ type: "flag:set", key, value });
10230
+ }
10231
+ }
10283
10232
  const EDITORIAL_WORKFLOW_STATUS = {
10284
10233
  QUEUED: "queued",
10285
10234
  PROCESSING: "processing",
@@ -10367,6 +10316,26 @@ const MEDIA_WORKFLOW_STATUS_TO_STEP = {
10367
10316
  [EDITORIAL_WORKFLOW_STATUS.COMPLETE]: MEDIA_WORKFLOW_STEP.COMPLETE
10368
10317
  };
10369
10318
  const s3ErrorRegex = /<Error>.*<Code>(.+)<\/Code>.*<Message>(.+)<\/Message>.*/;
10319
+ const CANONICAL_THUMBNAIL_SIZES = [
10320
+ { w: 75, h: 75 },
10321
+ { w: 400, h: 400 },
10322
+ { w: 1e3, h: 1e3 }
10323
+ ];
10324
+ const RENAME_ERROR_CODES = [
10325
+ "NOT_FOUND",
10326
+ "NAME_COLLISION",
10327
+ "INVALID_FILENAME",
10328
+ "INVALID_PATH",
10329
+ "UNAUTHORIZED",
10330
+ "UNSUPPORTED",
10331
+ "BACKEND_FAILURE"
10332
+ ];
10333
+ class MediaRenameCancelled extends Error {
10334
+ constructor() {
10335
+ super("Media rename cancelled.");
10336
+ __publicField(this, "ERR_TYPE", "MediaRenameCancelled");
10337
+ }
10338
+ }
10370
10339
  class DummyMediaStore {
10371
10340
  constructor() {
10372
10341
  __publicField(this, "accept", "*");
@@ -10408,10 +10377,9 @@ class TinaMediaStore {
10408
10377
  __publicField(this, "workflowBranchOverride");
10409
10378
  __publicField(this, "mediaWorkflowInProgress", false);
10410
10379
  /**
10411
- * Renaming is only implemented against the local dev server. This is an
10412
- * instance property rather than a prototype method on purpose: the media
10380
+ * An instance property rather than a prototype method on purpose: the media
10413
10381
  * manager decides whether to offer the action by checking whether the store
10414
- * defines `rename`, so a cloud-backed instance must not carry one — a
10382
+ * defines `rename`, so an instance that cannot rename must not carry one — a
10415
10383
  * prototype method would advertise support everywhere and surface an action
10416
10384
  * that always fails.
10417
10385
  */
@@ -10429,8 +10397,8 @@ class TinaMediaStore {
10429
10397
  this.isStatic = true;
10430
10398
  this.staticMedia = staticMedia;
10431
10399
  }
10432
- if (!this.isStatic && ((_b = (_a = cms == null ? void 0 : cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.isLocalMode)) {
10433
- this.rename = (from, to2) => this.rename_local(from, to2);
10400
+ if (!this.isStatic && !this.isUnsupportedSelfHostedRepoMedia()) {
10401
+ this.rename = ((_b = (_a = cms == null ? void 0 : cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.isLocalMode) ? (from, to2) => this.rename_local(from, to2) : (from, to2) => this.rename_cloud(from, to2);
10434
10402
  }
10435
10403
  }
10436
10404
  /**
@@ -10476,27 +10444,29 @@ class TinaMediaStore {
10476
10444
  return await this.api.authProvider.isAuthenticated();
10477
10445
  }
10478
10446
  /**
10479
- * Returns the workflow branch override or current branch as a single-encoded
10480
- * query-param value, or an empty string when no branch is set.
10481
- *
10482
- * `this.api.branch` is already URL-encoded by `Client.setBranch()`, so we
10483
- * decode then re-encode here to defend against double-encoding when this
10484
- * value is concatenated into a URL.
10485
- *
10486
- * `Client.setBranch()` runs the constructor's `options.branch` through
10487
- * `encodeURIComponent` without a guard, so an unset `options.branch`
10488
- * lands here as the literal string `"undefined"`. We treat that and the
10489
- * empty case as no-branch so we don't send `?branch=undefined` to the
10490
- * assets-api (which would route the call to a non-existent staging path).
10447
+ * The v2 cloud endpoint and the local dev server both filter by `ext` before
10448
+ * paginating. `staticMedia` is a build-time snapshot with no such pass, and
10449
+ * filtering a page client-side would leave a near-empty grid while matches
10450
+ * sit further down — so a static store reports no support and the control
10451
+ * stays hidden, the same way `searchable` is gated.
10491
10452
  */
10492
- encodedBranchParam() {
10493
- if (this.workflowBranchOverride) {
10494
- return encodeURIComponent(this.workflowBranchOverride);
10495
- }
10453
+ get extensionFilterable() {
10454
+ return !this.isStatic;
10455
+ }
10456
+ /**
10457
+ * `Client.setBranch()` encodes an unset branch to the literal `"undefined"`,
10458
+ * which the assets API would read as a real (non-existent) staging path.
10459
+ */
10460
+ currentBranch() {
10461
+ if (this.workflowBranchOverride) return this.workflowBranchOverride;
10496
10462
  if (!this.api.branch) return "";
10497
10463
  const decoded = decodeURIComponent(this.api.branch);
10498
- if (!decoded || decoded === "undefined") return "";
10499
- return encodeURIComponent(decoded);
10464
+ return decoded === "undefined" ? "" : decoded;
10465
+ }
10466
+ /** `this.api.branch` arrives encoded, so encode the decoded name once here. */
10467
+ encodedBranchParam() {
10468
+ const branch = this.currentBranch();
10469
+ return branch ? encodeURIComponent(branch) : "";
10500
10470
  }
10501
10471
  shortStableHash(input) {
10502
10472
  let hash = 2166136261;
@@ -10534,7 +10504,7 @@ class TinaMediaStore {
10534
10504
  const encodedBranch = this.encodedBranchParam();
10535
10505
  return encodedBranch ? `?branch=${encodedBranch}` : "";
10536
10506
  }
10537
- requestMediaBranchChoice(branchName, baseBranch, opType, repoPath) {
10507
+ requestMediaBranchChoice(branchName, baseBranch, request) {
10538
10508
  if (!this.cms.events.hasExplicitListenerFor("media:workflow:confirm-branch")) {
10539
10509
  throw new Error(
10540
10510
  "Cannot start a media editorial workflow: no branch prompt is mounted. Ensure <MediaWorkflowOverlay /> is rendered (TinaCloudProvider mounts it automatically)."
@@ -10545,12 +10515,13 @@ class TinaMediaStore {
10545
10515
  type: "media:workflow:confirm-branch",
10546
10516
  branchName,
10547
10517
  baseBranch,
10518
+ // A rename on a protected branch can only go through the workflow.
10519
+ allowSaveToProtectedBranch: request.opType !== "rename",
10548
10520
  onConfirm: async (selectedBranchName) => {
10549
10521
  const context = await this.prepareMediaBranch(
10550
10522
  selectedBranchName,
10551
10523
  baseBranch,
10552
- opType,
10553
- repoPath
10524
+ request
10554
10525
  );
10555
10526
  resolve({
10556
10527
  kind: "workflow",
@@ -10562,7 +10533,7 @@ class TinaMediaStore {
10562
10533
  });
10563
10534
  });
10564
10535
  }
10565
- async prepareMediaBranch(branchName, baseBranch, opType, repoPath) {
10536
+ async prepareMediaBranch(branchName, baseBranch, request) {
10566
10537
  if (this.mediaWorkflowInProgress) {
10567
10538
  throw new Error("A media workflow is already in progress.");
10568
10539
  }
@@ -10577,8 +10548,9 @@ class TinaMediaStore {
10577
10548
  branchName,
10578
10549
  baseBranch,
10579
10550
  prTitle: getEditorialWorkflowPrTitle(branchName),
10580
- operation: opType,
10581
- repoPath
10551
+ operation: request.opType,
10552
+ repoPath: request.repoPath,
10553
+ targetRepoPath: request.targetRepoPath
10582
10554
  });
10583
10555
  const branchContext = {
10584
10556
  branchName: workflow.branchName || branchName,
@@ -10654,12 +10626,10 @@ class TinaMediaStore {
10654
10626
  const branchName = `media-${opType}-${mediaSlug}`;
10655
10627
  const repoFilename = opType === "upload" && filename ? sanitizeFilename(filename) : filename;
10656
10628
  const repoPath = this.joinMediaPath(directory, repoFilename);
10657
- return this.requestMediaBranchChoice(
10658
- branchName,
10659
- baseBranch,
10629
+ return this.requestMediaBranchChoice(branchName, baseBranch, {
10660
10630
  opType,
10661
10631
  repoPath
10662
- );
10632
+ });
10663
10633
  }
10664
10634
  async waitForRequestStatus(requestId, timeoutMessage) {
10665
10635
  const startTime = Date.now();
@@ -10923,6 +10893,160 @@ class TinaMediaStore {
10923
10893
  lastSlash === -1 ? to2 : to2.slice(lastSlash + 1)
10924
10894
  );
10925
10895
  }
10896
+ /**
10897
+ * `from`/`to` are media-root-relative `directory/filename` paths. `to` is
10898
+ * already sanitised by `previewRename` and is passed through untouched.
10899
+ */
10900
+ async rename_cloud(from, to2) {
10901
+ this.setup();
10902
+ if (!await this.isAuthenticated()) {
10903
+ throw new MediaRenameError({
10904
+ code: "UNAUTHORIZED",
10905
+ message: "You don't have permission to rename this file."
10906
+ });
10907
+ }
10908
+ const decision = await this.prepareRenameBranch(from, to2);
10909
+ if (decision.kind === "cancelled") {
10910
+ throw new MediaRenameCancelled();
10911
+ }
10912
+ if (decision.kind === "workflow") {
10913
+ return this.renameCloudViaWorkflow(from, to2, decision.context);
10914
+ }
10915
+ return this.renameCloudDirect(from, to2);
10916
+ }
10917
+ /** `currentBranch()` is the workflow branch while one is active. */
10918
+ async postCloudRename(from, to2) {
10919
+ const branch = this.currentBranch() || void 0;
10920
+ const res = await this.api.authProvider.fetchWithToken(
10921
+ `${this.url}/rename`,
10922
+ {
10923
+ method: "POST",
10924
+ headers: { "Content-Type": "application/json" },
10925
+ body: JSON.stringify({ from, to: to2, branch })
10926
+ }
10927
+ );
10928
+ const body = await res.json().catch(() => null);
10929
+ if (res.status !== 200 || (body == null ? void 0 : body.success) !== true) {
10930
+ throw this.toCloudRenameError(res.status, body);
10931
+ }
10932
+ return body;
10933
+ }
10934
+ async renameCloudDirect(from, to2) {
10935
+ const result = await this.postCloudRename(from, to2);
10936
+ if (result.requestId) {
10937
+ try {
10938
+ await this.waitForRequestStatus(
10939
+ result.requestId,
10940
+ "Time out waiting for rename to complete"
10941
+ );
10942
+ } catch (error) {
10943
+ const detail = error instanceof Error && error.message ? `${error.message}. ` : "Failed to confirm the rename. ";
10944
+ throw new MediaRenameError({
10945
+ code: "BACKEND_FAILURE",
10946
+ message: `${detail}The file may still have been renamed — refresh the media library to check.`
10947
+ });
10948
+ }
10949
+ }
10950
+ return this.resolveRenamedMedia(result.path || to2, result.src);
10951
+ }
10952
+ /**
10953
+ * Completion is the workflow status, not the rename request status: only the
10954
+ * former waits for the branch, the media index and the pull request.
10955
+ */
10956
+ async renameCloudViaWorkflow(from, to2, branchContext) {
10957
+ let renamed;
10958
+ try {
10959
+ const result = await this.postCloudRename(from, to2);
10960
+ await this.finalizeMediaWorkflow(branchContext, async () => {
10961
+ renamed = await this.resolveRenamedMedia(result.path || to2, result.src);
10962
+ });
10963
+ } catch (err) {
10964
+ this.resetWorkflowState();
10965
+ this.cms.events.dispatch({ type: "media:workflow:finish" });
10966
+ throw err;
10967
+ }
10968
+ if (!renamed) {
10969
+ throw new MediaRenameError({
10970
+ code: "BACKEND_FAILURE",
10971
+ message: "The rename was staged but the branch workflow did not complete. Check the branch in TinaCloud before trying again."
10972
+ });
10973
+ }
10974
+ return renamed;
10975
+ }
10976
+ async prepareRenameBranch(from, to2) {
10977
+ if (!this.api.usingProtectedBranch()) return { kind: "direct" };
10978
+ const baseBranch = decodeURIComponent(this.api.branch || "");
10979
+ const { directory, filename } = this.splitMediaPath(from);
10980
+ const branchName = `media-rename-${this.branchSlugForMediaPath(
10981
+ directory,
10982
+ filename
10983
+ )}`;
10984
+ return this.requestMediaBranchChoice(branchName, baseBranch, {
10985
+ opType: "rename",
10986
+ repoPath: from,
10987
+ targetRepoPath: to2
10988
+ });
10989
+ }
10990
+ splitMediaPath(path) {
10991
+ const lastSlash = path.lastIndexOf("/");
10992
+ return lastSlash === -1 ? { directory: "", filename: path } : {
10993
+ directory: path.slice(0, lastSlash),
10994
+ filename: path.slice(lastSlash + 1)
10995
+ };
10996
+ }
10997
+ /**
10998
+ * The listing is the source of truth for `src` (staging path, per-stage CDN
10999
+ * host) and thumbnails; the rename response is the fallback when the entry
11000
+ * isn't on the first page.
11001
+ */
11002
+ async resolveRenamedMedia(targetPath, srcFromResponse) {
11003
+ const { directory, filename } = this.splitMediaPath(targetPath);
11004
+ try {
11005
+ const listed = await this.list({
11006
+ directory,
11007
+ limit: 100,
11008
+ thumbnailSizes: CANONICAL_THUMBNAIL_SIZES
11009
+ });
11010
+ const entry = listed.items.find(
11011
+ (item) => item.type === "file" && item.filename === filename
11012
+ );
11013
+ if (entry) return entry;
11014
+ } catch (error) {
11015
+ console.error("Failed to fetch the canonical media entry:", error);
11016
+ }
11017
+ const src = srcFromResponse || "";
11018
+ return {
11019
+ type: "file",
11020
+ id: filename,
11021
+ filename,
11022
+ directory,
11023
+ src,
11024
+ thumbnails: CANONICAL_THUMBNAIL_SIZES.reduce(
11025
+ (acc, size) => {
11026
+ acc[`${size.w}x${size.h}`] = this.genThumbnail(src, size);
11027
+ return acc;
11028
+ },
11029
+ {}
11030
+ )
11031
+ };
11032
+ }
11033
+ toCloudRenameError(status, body) {
11034
+ const message = typeof (body == null ? void 0 : body.message) === "string" && body.message ? body.message : "Failed to rename the file.";
11035
+ if (RENAME_ERROR_CODES.includes(body == null ? void 0 : body.code)) {
11036
+ return new MediaRenameError({ code: body.code, message });
11037
+ }
11038
+ let code;
11039
+ if (status === 401 || status === 403) {
11040
+ code = "UNAUTHORIZED";
11041
+ } else if (status === 404) {
11042
+ code = "NOT_FOUND";
11043
+ } else if (status === 409) {
11044
+ code = "NAME_COLLISION";
11045
+ } else {
11046
+ code = "BACKEND_FAILURE";
11047
+ }
11048
+ return new MediaRenameError({ code, message });
11049
+ }
10926
11050
  toRenameError(status, body) {
10927
11051
  if (body == null ? void 0 : body.code) {
10928
11052
  return new MediaRenameError({
@@ -10950,6 +11074,7 @@ class TinaMediaStore {
10950
11074
  return !this.isLocal ? `${src}?fit=crop&max-w=${dimensions.w}&max-h=${dimensions.h}` : src;
10951
11075
  }
10952
11076
  async list(options) {
11077
+ var _a, _b;
10953
11078
  this.setup();
10954
11079
  if (this.staticMedia) {
10955
11080
  const offset2 = options.offset || 0;
@@ -10981,13 +11106,16 @@ class TinaMediaStore {
10981
11106
  nextOffset: hasMore2 ? Number(offset2) + 20 : null
10982
11107
  };
10983
11108
  }
10984
- return { items: media, nextOffset: hasMore ? Number(offset2) + 20 : null };
11109
+ return {
11110
+ items: media,
11111
+ nextOffset: hasMore ? Number(offset2) + 20 : null
11112
+ };
10985
11113
  }
10986
11114
  let res;
10987
11115
  if (!this.isLocal) {
10988
11116
  const encodedBranch = this.encodedBranchParam();
10989
11117
  res = await this.api.authProvider.fetchWithToken(
10990
- `${this.listUrl}/list/${options.directory || ""}?limit=${options.limit || 20}${options.offset ? `&cursor=${options.offset}` : ""}${encodedBranch ? `&branch=${encodedBranch}` : ""}${options.search ? `&search=${encodeURIComponent(options.search)}` : ""}`
11118
+ `${this.listUrl}/list/${options.directory || ""}?limit=${options.limit || 20}${options.offset ? `&cursor=${encodeURIComponent(options.offset)}` : ""}${encodedBranch ? `&branch=${encodedBranch}` : ""}${options.search ? `&search=${encodeURIComponent(options.search)}` : ""}${((_a = options.ext) == null ? void 0 : _a.length) ? `&ext=${encodeURIComponent(options.ext.join(","))}` : ""}`
10991
11119
  );
10992
11120
  if (res.status == 401) {
10993
11121
  throw E_UNAUTHORIZED;
@@ -10997,7 +11125,7 @@ class TinaMediaStore {
10997
11125
  }
10998
11126
  } else {
10999
11127
  res = await this.fetchFunction(
11000
- `${this.url}/list/${options.directory || ""}?limit=${options.limit || 20}${options.offset ? `&cursor=${options.offset}` : ""}${options.search ? `&search=${encodeURIComponent(options.search)}` : ""}`
11128
+ `${this.url}/list/${options.directory || ""}?limit=${options.limit || 20}${options.offset ? `&cursor=${encodeURIComponent(options.offset)}` : ""}${options.search ? `&search=${encodeURIComponent(options.search)}` : ""}${((_b = options.ext) == null ? void 0 : _b.length) ? `&ext=${encodeURIComponent(options.ext.join(","))}` : ""}`
11001
11129
  );
11002
11130
  if (res.status == 404) {
11003
11131
  throw E_BAD_ROUTE;
@@ -11242,17 +11370,217 @@ const E_SELF_HOSTED_MEDIA = new MediaListError({
11242
11370
  message: "Self-hosted TinaCMS can't serve media from your repo through TinaCloud. Configure an external media store (e.g. S3, Cloudinary, DigitalOcean Spaces, or Azure) with media.loadCustomStore, or add media to your repo manually.",
11243
11371
  docsLink: "https://tina.io/docs/r/backend-media-handler/"
11244
11372
  });
11245
- class Flags {
11373
+ class PluginTypeManager {
11246
11374
  constructor(events) {
11247
- __publicField(this, "_flags", /* @__PURE__ */ new Map());
11375
+ /**
11376
+ * @ignore
11377
+ */
11378
+ __publicField(this, "plugins", {});
11248
11379
  this.events = events;
11249
11380
  }
11250
- get(key) {
11251
- return this._flags.get(key);
11381
+ /**
11382
+ * Gets the [[PluginType|collection of plugins]] for the given type.
11383
+ *
11384
+ * #### Example: Basic Usage
11385
+ *
11386
+ * ```ts
11387
+ * const colorPlugins = cms.plugins.get("color")
11388
+ * ```
11389
+ *
11390
+ * #### Example: Advanced Types
11391
+ *
11392
+ * A type param can be added to specify the kind of Plugin
11393
+ * that is being listed.
11394
+ *
11395
+ * ```ts
11396
+ * const colorPlugins = cms.plugins.get<ColorPlugin>("color")
11397
+ * ```
11398
+ *
11399
+ * @param type The type of plugins to be retrieved
11400
+ * @typeparam P A subclass of Plugin. Optional.
11401
+ */
11402
+ getType(type2) {
11403
+ return this.plugins[type2] = this.plugins[type2] || new PluginType(type2, this.events);
11252
11404
  }
11253
- set(key, value) {
11254
- this._flags.set(key, value);
11255
- this.events.dispatch({ type: "flag:set", key, value });
11405
+ /**
11406
+ * An alias to [[get]]
11407
+ *
11408
+ * ### !DEPRECATED!
11409
+ *
11410
+ * This name is unnecessarily verbose and weird.
11411
+ */
11412
+ findOrCreateMap(type2) {
11413
+ return this.getType(type2);
11414
+ }
11415
+ /**
11416
+ * Adds a Plugin to the CMS.
11417
+ *
11418
+ * #### Example: Basic Usage
11419
+ *
11420
+ * ```js
11421
+ * cms.plugins.add({ __type: "color", name: "red" })
11422
+ * ```
11423
+ *
11424
+ * #### Example: Advanced Types
11425
+ *
11426
+ * ```ts
11427
+ * interface ColorPlugin extends Plugin {
11428
+ * __type: "color"
11429
+ * hex: string
11430
+ * rgb: string
11431
+ * }
11432
+ *
11433
+ * cms.plugins.add<ColorPlugin>({
11434
+ * __type: "color",
11435
+ * name: "red",
11436
+ * hex: "#FF0000",
11437
+ * rgb: "RGBA(255, 0, 0, 1)"
11438
+ * })
11439
+ * ```
11440
+ *
11441
+ * @typeparam P A subclass of Plugin. Optional.
11442
+ * @param plugin
11443
+ * @todo Consider returning the plugin which was just added.
11444
+ */
11445
+ add(plugin) {
11446
+ this.findOrCreateMap(plugin.__type).add(plugin);
11447
+ }
11448
+ /**
11449
+ * Removes the given plugin from the CMS.
11450
+ *
11451
+ * #### Example
11452
+ *
11453
+ * In this example a plugin is added to the CMS and removed
11454
+ * 5 seconds later.
11455
+ *
11456
+ * ```ts
11457
+ * const redPlugin = {
11458
+ * __type: "color",
11459
+ * name: "red",
11460
+ * hex: "#FF0000",
11461
+ * rgb: "RGBA(255, 0, 0, 1)"
11462
+ * }
11463
+ *
11464
+ * cms.plugins.add(redPlugin)
11465
+ *
11466
+ * setTimeout(() => {
11467
+ * cms.plugins.remove(redPlugin)
11468
+ * }, 5000)
11469
+ * ```
11470
+ *
11471
+ * @typeparam P A subclass of Plugin. Optional.
11472
+ * @param plugin The plugin to be removed from the CMS.
11473
+ */
11474
+ remove(plugin) {
11475
+ this.findOrCreateMap(plugin.__type).remove(plugin);
11476
+ }
11477
+ /**
11478
+ * Returns a list of all the plugins of the given type.
11479
+ *
11480
+ * #### Example: Basic Usage
11481
+ *
11482
+ * ```ts
11483
+ * cms.plugins.all("color").forEach(color => {
11484
+ * console.log(color.name)
11485
+ * })
11486
+ * ```
11487
+ *
11488
+ * #### Example: Advanced Types
11489
+ *
11490
+ * A generic can be added to specify the type of plugins
11491
+ * that are being retrieved.
11492
+ *
11493
+ * ```ts
11494
+ * cms.plugins.all<ColorPlugin>("color").forEach(color => {
11495
+ * console.log(color.name, color.hex)
11496
+ * })
11497
+ * ```
11498
+ *
11499
+ * @typeparam P A subclass of Plugin. Optional.
11500
+ * @param type The name of the plugin
11501
+ * @returns An array of all plugins of the given type.
11502
+ */
11503
+ all(type2) {
11504
+ return this.findOrCreateMap(type2).all();
11505
+ }
11506
+ }
11507
+ class PluginType {
11508
+ /**
11509
+ *
11510
+ * @param __type The `__type` of plugin being managed.
11511
+ */
11512
+ constructor(__type, events) {
11513
+ /**
11514
+ * @ignore
11515
+ */
11516
+ __publicField(this, "__plugins", {});
11517
+ this.__type = __type;
11518
+ this.events = events;
11519
+ }
11520
+ /**
11521
+ * Adds a new plugin to the collection.
11522
+ *
11523
+ * ### Example
11524
+ *
11525
+ * ```ts
11526
+ * interface ColorPlugin extends Plugin {
11527
+ * hex: string
11528
+ * }
11529
+ *
11530
+ * const colorPlugins = new PluginType<ColorPlugin>("color")
11531
+ *
11532
+ * colorPlugins.add({ name: "red", hex: "#f00" })
11533
+ * ```
11534
+ *
11535
+ * @param plugin A new plugin. The `__type` is optional and will be added if it's missing.
11536
+ */
11537
+ add(plugin) {
11538
+ const p = plugin;
11539
+ if (!p.__type) {
11540
+ p.__type = this.__type;
11541
+ }
11542
+ this.__plugins[p.name] = p;
11543
+ this.events.dispatch({ type: `plugin:add:${this.__type}` });
11544
+ }
11545
+ all() {
11546
+ return Object.keys(this.__plugins).map((name2) => this.__plugins[name2]);
11547
+ }
11548
+ /**
11549
+ *
11550
+ * Looks up a plugin by it's `name`.
11551
+ *
11552
+ * ### Example
11553
+ *
11554
+ * ```ts
11555
+ * const colorPlugins = new PluginType<ColorPlugin>("color")
11556
+ *
11557
+ * colorPlugins.add({ name: "red", hex: "#f00" })
11558
+ *
11559
+ * colorPlugins.find("red") // { __type: "color", name: "red", hex: "#f00" }
11560
+ * colorPlugin.find("large") // undefined
11561
+ * ```
11562
+ *
11563
+ * @param name The `name` of the plugin to be retrieved.
11564
+ */
11565
+ find(name2) {
11566
+ return this.__plugins[name2];
11567
+ }
11568
+ /**
11569
+ * Pass this function a plugin or the `name` of a plugin to have
11570
+ * it be removed from the CMS.
11571
+ *
11572
+ * @param pluginOrName The `name` of a plugin, or the plugin itself.
11573
+ * @returns The plugin that was removed, or `undefined` if it was not found.
11574
+ */
11575
+ remove(pluginOrName) {
11576
+ const name2 = typeof pluginOrName === "string" ? pluginOrName : pluginOrName.name;
11577
+ const plugin = this.__plugins[name2];
11578
+ delete this.__plugins[name2];
11579
+ this.events.dispatch({ type: `plugin:remove:${this.__type}` });
11580
+ return plugin;
11581
+ }
11582
+ subscribe(cb) {
11583
+ return this.events.subscribe(`plugin:*:${this.__type}`, cb);
11256
11584
  }
11257
11585
  }
11258
11586
  const _CMS = class _CMS {
@@ -11365,14 +11693,28 @@ const _CMS = class _CMS {
11365
11693
  this.unsubscribeHooks[name2]();
11366
11694
  }
11367
11695
  if (api.events instanceof EventBus) {
11368
- const unsubscribeHost = api.events.subscribe(
11369
- "*",
11370
- this.events.dispatch
11371
- );
11372
- const unsubscribeGuest = this.events.subscribe(
11373
- "*",
11374
- (e) => api.events.dispatch(e)
11375
- );
11696
+ let forwardingToCms = null;
11697
+ let forwardingToApi = null;
11698
+ const unsubscribeHost = api.events.subscribe("*", (e) => {
11699
+ if (e === forwardingToApi) return;
11700
+ const previous = forwardingToCms;
11701
+ forwardingToCms = e;
11702
+ try {
11703
+ this.events.dispatch(e);
11704
+ } finally {
11705
+ forwardingToCms = previous;
11706
+ }
11707
+ });
11708
+ const unsubscribeGuest = this.events.subscribe("*", (e) => {
11709
+ if (e === forwardingToCms) return;
11710
+ const previous = forwardingToApi;
11711
+ forwardingToApi = e;
11712
+ try {
11713
+ api.events.dispatch(e);
11714
+ } finally {
11715
+ forwardingToApi = previous;
11716
+ }
11717
+ });
11376
11718
  this.unsubscribeHooks[name2] = () => {
11377
11719
  unsubscribeHost();
11378
11720
  unsubscribeGuest();
@@ -11715,6 +12057,10 @@ const RenameModal$1 = ({
11715
12057
  await renameFunc(sanitized);
11716
12058
  close2();
11717
12059
  } catch (e) {
12060
+ if ((e == null ? void 0 : e.ERR_TYPE) === "MediaRenameCancelled") {
12061
+ close2();
12062
+ return;
12063
+ }
11718
12064
  setError(renameErrorMessage(e, sanitized));
11719
12065
  setProcessing(false);
11720
12066
  }
@@ -11824,6 +12170,7 @@ function MediaPicker({
11824
12170
  allowDelete,
11825
12171
  onSelect,
11826
12172
  close: close2,
12173
+ accept: fieldAccept,
11827
12174
  ...props
11828
12175
  }) {
11829
12176
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
@@ -11860,6 +12207,7 @@ function MediaPicker({
11860
12207
  const [mediaFilter, setMediaFilter] = useState(
11861
12208
  "all"
11862
12209
  );
12210
+ const [typeFilter, setTypeFilter] = useState("all");
11863
12211
  const [offsetHistory, setOffsetHistory] = useState([]);
11864
12212
  const offset2 = offsetHistory[offsetHistory.length - 1];
11865
12213
  const resetOffset = () => setOffsetHistory([]);
@@ -11872,8 +12220,25 @@ function MediaPicker({
11872
12220
  resetList();
11873
12221
  setActiveItem(false);
11874
12222
  };
12223
+ const changeTypeFilter = (next) => {
12224
+ if (next === typeFilter) return;
12225
+ setTypeFilter(next);
12226
+ setLoadFolders(true);
12227
+ resetOffset();
12228
+ resetList();
12229
+ setActiveItem(false);
12230
+ };
11875
12231
  const listRequestRef = useRef(0);
11876
12232
  const newMediaSrcsRef = useRef(/* @__PURE__ */ new Set());
12233
+ const fieldExtensions = React__default.useMemo(
12234
+ () => resolveMediaAccept(fieldAccept),
12235
+ [fieldAccept]
12236
+ );
12237
+ const acceptedExtensions = React__default.useMemo(() => {
12238
+ if (!cms.media.store.extensionFilterable) return [];
12239
+ if (fieldExtensions.length) return fieldExtensions;
12240
+ return typeFilter === "all" ? [] : resolveMediaAccept(typeFilter);
12241
+ }, [fieldExtensions, typeFilter, cms.media.store.extensionFilterable]);
11877
12242
  async function loadMedia(loadFolders2 = true) {
11878
12243
  const requestId = ++listRequestRef.current;
11879
12244
  setListState("loading");
@@ -11888,7 +12253,8 @@ function MediaPicker({
11888
12253
  { w: 1e3, h: 1e3 }
11889
12254
  ],
11890
12255
  filesOnly: !loadFolders2,
11891
- search: debouncedSearch || void 0
12256
+ search: debouncedSearch || void 0,
12257
+ ext: acceptedExtensions.length ? acceptedExtensions : void 0
11892
12258
  });
11893
12259
  if (requestId !== listRequestRef.current) return;
11894
12260
  setList((prev) => {
@@ -11931,9 +12297,6 @@ function MediaPicker({
11931
12297
  }, [refreshing]);
11932
12298
  useEffect(() => {
11933
12299
  if (!cms.media.isConfigured) return;
11934
- if (refreshing) return;
11935
- loadMedia(loadFolders);
11936
- if (loadFolders) setLoadFolders(false);
11937
12300
  return cms.events.subscribe(
11938
12301
  ["media:delete:success", "media:rename:success", "media:pageSize"],
11939
12302
  (event) => {
@@ -11947,7 +12310,19 @@ function MediaPicker({
11947
12310
  resetList();
11948
12311
  }
11949
12312
  );
11950
- }, [offset2, directory, debouncedSearch, cms.media.isConfigured]);
12313
+ }, [cms.media.isConfigured]);
12314
+ useEffect(() => {
12315
+ if (!cms.media.isConfigured) return;
12316
+ if (refreshing) return;
12317
+ loadMedia(loadFolders);
12318
+ if (loadFolders) setLoadFolders(false);
12319
+ }, [
12320
+ offset2,
12321
+ directory,
12322
+ debouncedSearch,
12323
+ acceptedExtensions,
12324
+ cms.media.isConfigured
12325
+ ]);
11951
12326
  const onClickMediaItem = (item) => {
11952
12327
  if (!item) {
11953
12328
  setActiveItem(false);
@@ -11981,12 +12356,12 @@ function MediaPicker({
11981
12356
  };
11982
12357
  }
11983
12358
  const [uploading, setUploading] = useState(false);
11984
- const accept = Array.isArray(
12359
+ const globalAccept = Array.isArray(
11985
12360
  (_c = (_b = (_a = cms.api.tina.schema.schema) == null ? void 0 : _a.config) == null ? void 0 : _b.media) == null ? void 0 : _c.accept
11986
12361
  ) ? (_f = (_e = (_d = cms.api.tina.schema.schema) == null ? void 0 : _d.config) == null ? void 0 : _e.media) == null ? void 0 : _f.accept.join(",") : (_i = (_h = (_g = cms.api.tina.schema.schema) == null ? void 0 : _g.config) == null ? void 0 : _h.media) == null ? void 0 : _i.accept;
11987
12362
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
11988
- accept: dropzoneAcceptFromString(
11989
- accept || cms.media.accept || DEFAULT_MEDIA_UPLOAD_TYPES
12363
+ accept: dropzoneAcceptFromExtensions(fieldExtensions) ?? dropzoneAcceptFromString(
12364
+ globalAccept || cms.media.accept || DEFAULT_MEDIA_UPLOAD_TYPES
11990
12365
  ),
11991
12366
  maxSize: cms.media.maxSize,
11992
12367
  multiple: true,
@@ -12203,7 +12578,15 @@ function MediaPicker({
12203
12578
  setValue: setSearch,
12204
12579
  clear: () => setSearch("")
12205
12580
  }
12206
- ), /* @__PURE__ */ React__default.createElement("div", { className: "ml-auto" }, /* @__PURE__ */ React__default.createElement(
12581
+ ), /* @__PURE__ */ React__default.createElement("div", { className: "ml-auto flex items-center gap-3" }, cms.media.store.extensionFilterable && /* @__PURE__ */ React__default.createElement(
12582
+ MediaTypeFilter,
12583
+ {
12584
+ value: typeFilter,
12585
+ setValue: changeTypeFilter,
12586
+ lockedTo: fieldAccept,
12587
+ lockedExtensions: fieldExtensions
12588
+ }
12589
+ ), /* @__PURE__ */ React__default.createElement(
12207
12590
  MediaFilterToggle,
12208
12591
  {
12209
12592
  value: mediaFilter,
@@ -12468,6 +12851,62 @@ const MediaFilterToggle = ({
12468
12851
  option.label
12469
12852
  )));
12470
12853
  };
12854
+ const TYPE_FILTER_LABELS = {
12855
+ image: "Images",
12856
+ video: "Video",
12857
+ audio: "Audio",
12858
+ document: "Documents"
12859
+ };
12860
+ const acceptEntryLabel = (entry) => entry in MEDIA_CATEGORIES ? TYPE_FILTER_LABELS[entry] : entry.toUpperCase();
12861
+ const MediaTypeFilter = ({
12862
+ value,
12863
+ setValue,
12864
+ lockedTo,
12865
+ lockedExtensions
12866
+ }) => {
12867
+ if (lockedExtensions.length) {
12868
+ const entries = Array.isArray(lockedTo) ? lockedTo : [lockedTo];
12869
+ const label = entries.length > 3 ? `${entries.length} types only` : `${entries.map(acceptEntryLabel).join(", ")} only`;
12870
+ const description = `This field accepts ${lockedExtensions.join(", ")}`;
12871
+ return /* @__PURE__ */ React__default.createElement(
12872
+ "span",
12873
+ {
12874
+ className: "grow-0 flex items-center gap-1.5 whitespace-nowrap rounded-lg border border-dashed border-gray-200 bg-gray-100 px-3 py-1.5 text-sm text-gray-500",
12875
+ title: description
12876
+ },
12877
+ /* @__PURE__ */ React__default.createElement(Lock, { className: "w-3.5 h-3.5 opacity-80", "aria-hidden": "true" }),
12878
+ label,
12879
+ /* @__PURE__ */ React__default.createElement("span", { className: "sr-only" }, description)
12880
+ );
12881
+ }
12882
+ const active = value !== "all";
12883
+ return /* @__PURE__ */ React__default.createElement(DropdownMenu, null, /* @__PURE__ */ React__default.createElement(DropdownMenuTrigger, { asChild: true }, /* @__PURE__ */ React__default.createElement(
12884
+ "button",
12885
+ {
12886
+ type: "button",
12887
+ className: `grow-0 flex items-center gap-2 whitespace-nowrap rounded-lg border bg-white px-3 py-1.5 text-sm transition-colors ease-out duration-150 ${active ? "border-tina-orange text-tina-orange font-medium" : "border-gray-200 text-gray-700 hover:border-gray-300"}`
12888
+ },
12889
+ active ? TYPE_FILTER_LABELS[value] : "Any type",
12890
+ /* @__PURE__ */ React__default.createElement(
12891
+ ChevronDown,
12892
+ {
12893
+ className: `w-4 h-4 ${active ? "opacity-90" : "text-gray-400"}`
12894
+ }
12895
+ )
12896
+ )), /* @__PURE__ */ React__default.createElement(DropdownMenuContent, { align: "end", className: "min-w-52" }, /* @__PURE__ */ React__default.createElement(
12897
+ DropdownMenuRadioGroup,
12898
+ {
12899
+ value,
12900
+ onValueChange: (next) => setValue(next)
12901
+ },
12902
+ /* @__PURE__ */ React__default.createElement(DropdownMenuRadioItem, { value: "all" }, "Any type"),
12903
+ /* @__PURE__ */ React__default.createElement(DropdownMenuSeparator, null),
12904
+ /* @__PURE__ */ React__default.createElement(DropdownMenuLabel, null, "Category"),
12905
+ Object.keys(MEDIA_CATEGORIES).map(
12906
+ (category) => /* @__PURE__ */ React__default.createElement(DropdownMenuRadioItem, { key: category, value: category }, TYPE_FILTER_LABELS[category])
12907
+ )
12908
+ )));
12909
+ };
12471
12910
  const ViewModeToggle = ({ viewMode, setViewMode }) => {
12472
12911
  const base = "relative flex items-center justify-center w-9 py-1.5 transition-colors ease-out duration-150";
12473
12912
  const active = "bg-white text-tina-orange";
@@ -13566,6 +14005,70 @@ function createCloudConfig({
13566
14005
  ...options
13567
14006
  };
13568
14007
  }
14008
+ const LOADING_MESSAGES = [
14009
+ "Hang tight, TinaCMS is looking for llamas 🦙 🦙 🦙",
14010
+ "Reticulating llama splines 🦙",
14011
+ "Herding llamas into collections 🦙",
14012
+ "Brushing llama fur 🦙 🦙",
14013
+ "Teaching a llama to write frontmatter 🦙",
14014
+ "Feeding the GraphQL llama 🦙",
14015
+ "Counting llamas in your content folder 🦙",
14016
+ "Asking a llama what changed in git 🦙",
14017
+ "Llamas are proofreading your schema 🦙",
14018
+ "Untangling markdown with llama teeth 🦙",
14019
+ "Waking the llamas in the media library 🦙",
14020
+ "Alphabetising llamas by collection 🦙",
14021
+ "Convincing a llama that MDX is fine 🦙",
14022
+ "Saddling up the content layer 🦙",
14023
+ "Polishing the sidebar, llama style 🦙",
14024
+ "One more llama, almost there 🦙"
14025
+ ];
14026
+ const CYCLE_MS = 2e3;
14027
+ const FADE_MS = 200;
14028
+ const prefersReducedMotion = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
14029
+ const pickOther = (current, length) => {
14030
+ if (length < 2) return 0;
14031
+ const offset2 = 1 + Math.floor(Math.random() * (length - 1));
14032
+ return (current + offset2) % length;
14033
+ };
14034
+ const LoadingMessage = ({
14035
+ className,
14036
+ style,
14037
+ messages = LOADING_MESSAGES
14038
+ }) => {
14039
+ const [index, setIndex] = React__default.useState(0);
14040
+ const [visible, setVisible] = React__default.useState(false);
14041
+ const [reducedMotion] = React__default.useState(prefersReducedMotion);
14042
+ React__default.useEffect(() => {
14043
+ setIndex((current) => pickOther(current, messages.length));
14044
+ setVisible(true);
14045
+ }, [messages.length]);
14046
+ React__default.useEffect(() => {
14047
+ const fadeOut = setTimeout(() => setVisible(false), CYCLE_MS - FADE_MS);
14048
+ const next = setTimeout(() => {
14049
+ setIndex((current) => pickOther(current, messages.length));
14050
+ setVisible(true);
14051
+ }, CYCLE_MS);
14052
+ return () => {
14053
+ clearTimeout(fadeOut);
14054
+ clearTimeout(next);
14055
+ };
14056
+ }, [index, messages.length]);
14057
+ return /* @__PURE__ */ React__default.createElement(
14058
+ "p",
14059
+ {
14060
+ className,
14061
+ "aria-live": "polite",
14062
+ style: {
14063
+ ...style,
14064
+ opacity: reducedMotion || visible ? 1 : 0,
14065
+ transform: reducedMotion || visible ? "none" : "translateY(4px)",
14066
+ transition: reducedMotion ? void 0 : `opacity ${FADE_MS}ms ease-out, transform ${FADE_MS}ms ease-out`
14067
+ }
14068
+ },
14069
+ messages[index]
14070
+ );
14071
+ };
13569
14072
  const SidebarLoadingPlaceholder = () => /* @__PURE__ */ React.createElement(
13570
14073
  "div",
13571
14074
  {
@@ -13579,7 +14082,7 @@ const SidebarLoadingPlaceholder = () => /* @__PURE__ */ React.createElement(
13579
14082
  animationDuration: "150ms"
13580
14083
  }
13581
14084
  },
13582
- /* @__PURE__ */ React.createElement("p", { className: "block pb-5" }, "Please wait while TinaCMS", /* @__PURE__ */ React.createElement("br", null), "loads your content"),
14085
+ /* @__PURE__ */ React.createElement(LoadingMessage, { className: "block pb-5" }),
13583
14086
  /* @__PURE__ */ React.createElement(LoadingDots, { color: "var(--tina-color-primary)" })
13584
14087
  );
13585
14088
  class SidebarState {
@@ -13719,7 +14222,7 @@ const FormModal = ({ plugin, close: close2 }) => {
13719
14222
  const name = "tinacms";
13720
14223
  const type = "module";
13721
14224
  const typings = "dist/index.d.ts";
13722
- const version = "3.12.1";
14225
+ const version = "3.14.0";
13723
14226
  const main = "dist/index.js";
13724
14227
  const module$1 = "./dist/index.js";
13725
14228
  const exports$1 = { ".": "./dist/index.js", "./dist/client": "./dist/client.js", "./react": "./dist/react.js", "./dist/react": "./dist/react.js", "./tina-field": "./dist/tina-field.js", "./dist/tina-field": "./dist/tina-field.js", "./dist/rich-text": "./dist/rich-text/index.js", "./dist/rich-text/static": "./dist/rich-text/static.js", "./dist/rich-text/prism": "./dist/rich-text/prism.js" };
@@ -13827,7 +14330,11 @@ const useGetEvents = (cms, cursor, existingEvents) => {
13827
14330
  var _a, _b, _c, _d, _e;
13828
14331
  let doFetchEvents = false;
13829
14332
  if (!((_b = (_a = cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.isCustomContentApi)) {
13830
- doFetchEvents = await ((_e = (_d = (_c = cms.api) == null ? void 0 : _c.tina) == null ? void 0 : _d.authProvider) == null ? void 0 : _e.isAuthenticated());
14333
+ try {
14334
+ doFetchEvents = await ((_e = (_d = (_c = cms.api) == null ? void 0 : _c.tina) == null ? void 0 : _d.authProvider) == null ? void 0 : _e.isAuthenticated());
14335
+ } catch {
14336
+ doFetchEvents = false;
14337
+ }
13831
14338
  }
13832
14339
  if (doFetchEvents) {
13833
14340
  try {
@@ -13860,7 +14367,11 @@ function useSyncStatus(cms) {
13860
14367
  var _a2, _b2, _c, _d, _e;
13861
14368
  let doFetchEvents = false;
13862
14369
  if (!((_b2 = (_a2 = cms.api) == null ? void 0 : _a2.tina) == null ? void 0 : _b2.isCustomContentApi)) {
13863
- doFetchEvents = await ((_e = (_d = (_c = cms.api) == null ? void 0 : _c.tina) == null ? void 0 : _d.authProvider) == null ? void 0 : _e.isAuthenticated());
14370
+ try {
14371
+ doFetchEvents = await ((_e = (_d = (_c = cms.api) == null ? void 0 : _c.tina) == null ? void 0 : _d.authProvider) == null ? void 0 : _e.isAuthenticated());
14372
+ } catch {
14373
+ doFetchEvents = false;
14374
+ }
13864
14375
  }
13865
14376
  if (doFetchEvents) {
13866
14377
  const { events } = await cms.api.tina.fetchEvents();
@@ -16106,6 +16617,9 @@ const useFieldReference = (fieldName) => {
16106
16617
  } else {
16107
16618
  doc = document;
16108
16619
  }
16620
+ if (!doc) {
16621
+ return;
16622
+ }
16109
16623
  const fieldEle = doc.querySelector(
16110
16624
  `[data-tinafield="${fieldName}"]`
16111
16625
  );
@@ -17144,6 +17658,35 @@ const CreateBranchModal = ({
17144
17658
  }
17145
17659
  );
17146
17660
  };
17661
+ const getInitials = (name2) => {
17662
+ const atIndex = name2.indexOf("@");
17663
+ const localPart = atIndex === -1 ? name2 : name2.slice(0, atIndex);
17664
+ return localPart.split(/[\s._-]+/).filter(Boolean).slice(0, 2).map((part) => [...part][0].toUpperCase()).join("");
17665
+ };
17666
+ const CommittingAs = () => {
17667
+ var _a, _b, _c;
17668
+ const cms = useCMS$1();
17669
+ const user = (_b = (_a = cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.user;
17670
+ const author = typeof user === "object" && user !== null ? user : void 0;
17671
+ const mode = (_c = author == null ? void 0 : author.gitAuthoring) == null ? void 0 : _c.mode;
17672
+ if (mode !== "bot" && mode !== "user") {
17673
+ return null;
17674
+ }
17675
+ const authorName = mode === "bot" ? "TinaCloud bot" : author.fullName || author.email;
17676
+ if (!authorName) {
17677
+ return null;
17678
+ }
17679
+ return /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 mt-4 pt-4 border-t border-gray-100" }, /* @__PURE__ */ React.createElement("span", { className: "flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 text-gray-600 text-xs font-medium flex-shrink-0" }, getInitials(authorName)), /* @__PURE__ */ React.createElement("div", { className: "flex-1 min-w-0" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs text-gray-500" }, "Committing as"), /* @__PURE__ */ React.createElement("p", { className: "text-sm text-gray-700 font-medium truncate" }, authorName)), /* @__PURE__ */ React.createElement(
17680
+ "a",
17681
+ {
17682
+ className: "text-sm underline text-tina-orange-dark font-medium flex-shrink-0",
17683
+ href: cms.api.tina.gitSettingsLink,
17684
+ target: "_blank",
17685
+ rel: "noreferrer"
17686
+ },
17687
+ "Change"
17688
+ ));
17689
+ };
17147
17690
  const CreateBranchPromptModal = ({
17148
17691
  branchName,
17149
17692
  close: close2,
@@ -17153,7 +17696,8 @@ const CreateBranchPromptModal = ({
17153
17696
  onCreateBranch,
17154
17697
  onSaveToProtectedBranch,
17155
17698
  showSaveOptions = false,
17156
- disablePublish = false
17699
+ disablePublish = false,
17700
+ allowSaveToProtectedBranch = true
17157
17701
  }) => {
17158
17702
  const [lastChoice, setLastChoice] = useLocalStorage(
17159
17703
  SAVE_CHOICE_KEY,
@@ -17200,7 +17744,7 @@ const CreateBranchPromptModal = ({
17200
17744
  onBranchNameChange(e.target.value);
17201
17745
  }
17202
17746
  }
17203
- ))), /* @__PURE__ */ React.createElement(ModalActions, { align: "end" }, /* @__PURE__ */ React.createElement(
17747
+ ), /* @__PURE__ */ React.createElement(CommittingAs, null))), /* @__PURE__ */ React.createElement(ModalActions, { align: "end" }, /* @__PURE__ */ React.createElement(
17204
17748
  Button$2,
17205
17749
  {
17206
17750
  variant: "secondary",
@@ -17233,7 +17777,7 @@ const CreateBranchPromptModal = ({
17233
17777
  },
17234
17778
  /* @__PURE__ */ React.createElement(MainIcon, { className: "w-4 h-4 mr-1", style: { fill: "none" } }),
17235
17779
  mainChoice.label
17236
- ) : /* @__PURE__ */ React.createElement(
17780
+ ) : allowSaveToProtectedBranch ? /* @__PURE__ */ React.createElement(
17237
17781
  DropdownButton,
17238
17782
  {
17239
17783
  variant: "primary",
@@ -17257,6 +17801,25 @@ const CreateBranchPromptModal = ({
17257
17801
  }
17258
17802
  ),
17259
17803
  "Save to a new branch"
17804
+ ) : (
17805
+ // A plain button: the dropdown's only item is the one being dropped.
17806
+ /* @__PURE__ */ React.createElement(
17807
+ Button$2,
17808
+ {
17809
+ variant: "primary",
17810
+ className: "w-full sm:w-auto",
17811
+ disabled,
17812
+ onClick: () => onCreateBranch(false)
17813
+ },
17814
+ /* @__PURE__ */ React.createElement(
17815
+ GitBranchIcon,
17816
+ {
17817
+ className: "w-4 h-4 mr-1",
17818
+ style: { fill: "none" }
17819
+ }
17820
+ ),
17821
+ "Save to a new branch"
17822
+ )
17260
17823
  ))));
17261
17824
  };
17262
17825
  const PrefixedTextField = ({
@@ -17609,6 +18172,10 @@ const FormBuilder = ({
17609
18172
  setDeletedBranchModalOpen(true);
17610
18173
  return;
17611
18174
  }
18175
+ if (isSessionExpiredError(error)) {
18176
+ captureWorkflowChoice(false, errorMsg);
18177
+ return;
18178
+ }
17612
18179
  captureEvent(SaveContentErrorEvent, {
17613
18180
  documentPath: tinaForm.path,
17614
18181
  error: errorMsg
@@ -18709,7 +19276,7 @@ function HeadingsMenu(props) {
18709
19276
  className: "min-w-0 rounded-none rounded-bl rounded-br border-border"
18710
19277
  },
18711
19278
  /* @__PURE__ */ React__default.createElement(
18712
- DropdownMenuRadioGroup,
19279
+ DropdownMenuRadioGroup$1,
18713
19280
  {
18714
19281
  className: "flex flex-col gap-0.5",
18715
19282
  onValueChange: (type2) => {
@@ -18725,7 +19292,7 @@ function HeadingsMenu(props) {
18725
19292
  }
18726
19293
  return true;
18727
19294
  }).map(({ icon: Icon, label, value: itemValue }) => /* @__PURE__ */ React__default.createElement(
18728
- DropdownMenuRadioItem,
19295
+ DropdownMenuRadioItem$1,
18729
19296
  {
18730
19297
  className: "min-w-[180px]",
18731
19298
  key: itemValue,
@@ -19481,549 +20048,6 @@ const useCreateEditor = ({
19481
20048
  components
19482
20049
  });
19483
20050
  };
19484
- const popoverVariants = cva(
19485
- "z-[999999] w-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-hidden"
19486
- );
19487
- const inputVariants = cva(
19488
- "flex h-[28px] w-full rounded-md border-none bg-transparent px-1.5 py-1 text-base placeholder:text-muted-foreground focus-visible:ring-transparent focus-visible:outline-none md:text-sm"
19489
- );
19490
- function LinkFloatingToolbar({
19491
- state
19492
- }) {
19493
- const activeCommentId = usePluginOption({ key: "comment" }, "activeId");
19494
- const activeSuggestionId = usePluginOption({ key: "suggestion" }, "activeId");
19495
- const { api, editor } = useEditorPlugin(LinkPlugin);
19496
- const isUrlValidator = usePluginOption(LinkPlugin, "isUrl");
19497
- const [currentUrl, setCurrentUrl] = React.useState("");
19498
- const [isValidUrl, setIsValidUrl] = React.useState(true);
19499
- const handleUrlInput = React.useCallback(
19500
- (e) => {
19501
- const value = e.currentTarget.value;
19502
- setCurrentUrl(value);
19503
- if (value && isUrlValidator) {
19504
- const valid = isUrlValidator(value);
19505
- setIsValidUrl(valid);
19506
- } else {
19507
- setIsValidUrl(true);
19508
- }
19509
- },
19510
- [isUrlValidator]
19511
- );
19512
- const floatingOptions = React.useMemo(() => {
19513
- return {
19514
- middleware: [
19515
- offset(8),
19516
- flip({
19517
- fallbackPlacements: ["bottom-end", "top-start", "top-end"],
19518
- padding: 12
19519
- }),
19520
- shift({ padding: 8, limiter: limitShift() })
19521
- ],
19522
- placement: activeSuggestionId || activeCommentId ? "top-start" : "bottom-start"
19523
- };
19524
- }, [activeCommentId, activeSuggestionId]);
19525
- const insertState = useFloatingLinkInsertState({
19526
- ...state,
19527
- floatingOptions: {
19528
- ...floatingOptions,
19529
- ...state == null ? void 0 : state.floatingOptions
19530
- }
19531
- });
19532
- const {
19533
- hidden,
19534
- props: insertProps,
19535
- ref: insertRef,
19536
- textInputProps
19537
- } = useFloatingLinkInsert(insertState);
19538
- const editState = useFloatingLinkEditState({
19539
- ...state,
19540
- floatingOptions: {
19541
- ...floatingOptions,
19542
- ...state == null ? void 0 : state.floatingOptions
19543
- }
19544
- });
19545
- const {
19546
- editButtonProps,
19547
- props: editProps,
19548
- ref: editRef,
19549
- unlinkButtonProps
19550
- } = useFloatingLinkEdit(editState);
19551
- const inputProps = useFormInputProps({
19552
- preventDefaultOnEnterKeydown: true
19553
- });
19554
- if (hidden) return null;
19555
- const input = /* @__PURE__ */ React.createElement(
19556
- "div",
19557
- {
19558
- className: "z-[999999] flex w-[330px] flex-col relative",
19559
- ...inputProps
19560
- },
19561
- !isValidUrl && currentUrl && /* @__PURE__ */ React.createElement("div", { className: "absolute -top-16 left-0 right-0 z-[1000000] mb-2" }, /* @__PURE__ */ React.createElement(
19562
- "div",
19563
- {
19564
- className: "bg-red-50 border border-red-200 rounded-md p-2 shadow-lg",
19565
- role: "alert",
19566
- "aria-live": "polite"
19567
- },
19568
- /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement(CircleX, { className: "size-4 text-red-500 mr-2 flex-shrink-0" }), /* @__PURE__ */ React.createElement("span", { className: "text-sm text-red-700 text-wrap" }, "Invalid URL. Please prefix link with https:// or use a relative path like /about")),
19569
- /* @__PURE__ */ React.createElement("div", { className: "absolute -bottom-1 left-4 w-2 h-2 bg-red-50 border-r border-b border-red-200 transform rotate-45" })
19570
- )),
19571
- /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center pr-1 pl-2 text-muted-foreground" }, /* @__PURE__ */ React.createElement(Link, { className: "size-4" })), /* @__PURE__ */ React.createElement(
19572
- FloatingLinkUrlInput,
19573
- {
19574
- className: inputVariants(),
19575
- placeholder: "Paste link",
19576
- "data-plate-focus": true,
19577
- onInput: handleUrlInput
19578
- }
19579
- )),
19580
- /* @__PURE__ */ React.createElement(Separator, { className: "my-1" }),
19581
- /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center pr-1 pl-2 text-muted-foreground" }, /* @__PURE__ */ React.createElement(Text, { className: "size-4" })), /* @__PURE__ */ React.createElement(
19582
- "input",
19583
- {
19584
- className: inputVariants(),
19585
- placeholder: "Text to display",
19586
- "data-plate-focus": true,
19587
- ...textInputProps
19588
- }
19589
- )),
19590
- /* @__PURE__ */ React.createElement(Separator, { className: "my-1" }),
19591
- /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-end gap-2 px-2 py-1" }, /* @__PURE__ */ React.createElement(
19592
- "button",
19593
- {
19594
- type: "button",
19595
- className: buttonVariants$2({ size: "sm", variant: "ghost" }),
19596
- onClick: () => {
19597
- api.floatingLink.hide();
19598
- }
19599
- },
19600
- "Cancel"
19601
- ), /* @__PURE__ */ React.createElement(
19602
- "button",
19603
- {
19604
- type: "button",
19605
- className: buttonVariants$2({
19606
- size: "sm",
19607
- variant: "tinaPrimary"
19608
- }),
19609
- onClick: () => {
19610
- if (isValidUrl && currentUrl) {
19611
- submitFloatingLink(editor);
19612
- }
19613
- },
19614
- disabled: !isValidUrl && !!currentUrl
19615
- },
19616
- "OK"
19617
- ))
19618
- );
19619
- const editContent = editState.isEditing ? input : /* @__PURE__ */ React.createElement("div", { className: "box-content flex items-center" }, /* @__PURE__ */ React.createElement(
19620
- "button",
19621
- {
19622
- className: buttonVariants$2({ size: "sm", variant: "ghost" }),
19623
- type: "button",
19624
- ...editButtonProps
19625
- },
19626
- "Edit link"
19627
- ), /* @__PURE__ */ React.createElement(Separator, { orientation: "vertical" }), /* @__PURE__ */ React.createElement(LinkOpenButton, null), /* @__PURE__ */ React.createElement(Separator, { orientation: "vertical" }), /* @__PURE__ */ React.createElement(
19628
- "button",
19629
- {
19630
- className: buttonVariants$2({
19631
- size: "icon",
19632
- variant: "ghost"
19633
- }),
19634
- type: "button",
19635
- ...unlinkButtonProps
19636
- },
19637
- /* @__PURE__ */ React.createElement(Unlink, { width: 18 })
19638
- ));
19639
- return /* @__PURE__ */ React.createElement(PortalBody, null, /* @__PURE__ */ React.createElement(
19640
- "div",
19641
- {
19642
- ref: insertRef,
19643
- className: popoverVariants(),
19644
- ...insertProps,
19645
- style: {
19646
- ...insertProps.style,
19647
- zIndex: 999999
19648
- }
19649
- },
19650
- input
19651
- ), /* @__PURE__ */ React.createElement(
19652
- "div",
19653
- {
19654
- ref: editRef,
19655
- className: popoverVariants(),
19656
- ...editProps,
19657
- style: { ...editProps.style, zIndex: 999999 }
19658
- },
19659
- editContent
19660
- ));
19661
- }
19662
- function LinkOpenButton() {
19663
- const editor = useEditorRef();
19664
- const selection = useEditorSelection();
19665
- const attributes = React.useMemo(
19666
- () => {
19667
- const entry = editor.api.node({
19668
- match: { type: editor.getType(LinkPlugin) }
19669
- });
19670
- if (!entry) {
19671
- return {};
19672
- }
19673
- const [element] = entry;
19674
- return getLinkAttributes(editor, element);
19675
- },
19676
- // eslint-disable-next-line react-hooks/exhaustive-deps
19677
- [editor, selection]
19678
- );
19679
- return /* @__PURE__ */ React.createElement(
19680
- "a",
19681
- {
19682
- ...attributes,
19683
- className: buttonVariants$2({
19684
- size: "icon",
19685
- variant: "ghost"
19686
- }),
19687
- onMouseOver: (e) => {
19688
- e.stopPropagation();
19689
- },
19690
- "aria-label": "Open link in a new tab",
19691
- target: "_blank"
19692
- },
19693
- /* @__PURE__ */ React.createElement(ExternalLink, { width: 18 })
19694
- );
19695
- }
19696
- const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/;
19697
- const emailLintRE = /mailto:([^?\\]+)/;
19698
- const telLintRE = /tel:([\d-]+)/;
19699
- const localhostDomainRE = /^localhost[\d:?]*(?:[^\d:?]\S*)?$/;
19700
- const nonLocalhostDomainRE = /^[^\s.]+\.\S{2,}$/;
19701
- const localUrlRE = /^\/\S+/;
19702
- const isUrl = (string) => {
19703
- if (typeof string !== "string") {
19704
- return false;
19705
- }
19706
- if (string.startsWith("#")) {
19707
- return true;
19708
- }
19709
- const generalMatch = string.match(protocolAndDomainRE);
19710
- const emailLinkMatch = string.match(emailLintRE);
19711
- const telLinkMatch = string.match(telLintRE);
19712
- const localUrlMatch = string.match(localUrlRE);
19713
- if (emailLinkMatch || telLinkMatch || localUrlMatch) {
19714
- return true;
19715
- }
19716
- if (generalMatch) {
19717
- const everythingAfterProtocol = generalMatch[1];
19718
- if (!everythingAfterProtocol) {
19719
- return false;
19720
- }
19721
- try {
19722
- new URL(string);
19723
- } catch {
19724
- return false;
19725
- }
19726
- return localhostDomainRE.test(everythingAfterProtocol) || nonLocalhostDomainRE.test(everythingAfterProtocol);
19727
- }
19728
- return false;
19729
- };
19730
- const buildError = (element) => {
19731
- return {
19732
- message: element.message,
19733
- position: element.position && {
19734
- endColumn: element.position.end.column,
19735
- startColumn: element.position.start.column,
19736
- startLineNumber: element.position.start.line,
19737
- endLineNumber: element.position.end.line
19738
- }
19739
- };
19740
- };
19741
- const buildErrorMessage = (element) => {
19742
- if (!element) {
19743
- return "";
19744
- }
19745
- const errorMessage = buildError(element);
19746
- const message = errorMessage ? `${errorMessage.message}${errorMessage.position ? ` at line: ${errorMessage.position.startLineNumber}, column: ${errorMessage.position.startColumn}` : ""}` : null;
19747
- return message;
19748
- };
19749
- const ELEMENT_INVALID_MARKDOWN = "invalid_markdown";
19750
- const createInvalidMarkdownPlugin = createPlatePlugin({
19751
- key: ELEMENT_INVALID_MARKDOWN,
19752
- options: {
19753
- isElement: true,
19754
- isVoid: true,
19755
- isInline: false
19756
- },
19757
- node: {
19758
- component: InvalidMarkdownElement
19759
- }
19760
- });
19761
- function InvalidMarkdownElement({
19762
- attributes,
19763
- element,
19764
- children
19765
- }) {
19766
- return /* @__PURE__ */ React__default.createElement("div", { ...attributes }, /* @__PURE__ */ React__default.createElement(ErrorMessage, { error: element }), children);
19767
- }
19768
- function ErrorMessage({ error }) {
19769
- const message = buildErrorMessage(error);
19770
- const { setRawMode } = useEditorContext();
19771
- return /* @__PURE__ */ React__default.createElement("div", { contentEditable: false, className: "bg-red-50 sm:rounded-lg" }, /* @__PURE__ */ React__default.createElement("div", { className: "px-4 py-5 sm:p-6" }, /* @__PURE__ */ React__default.createElement("h3", { className: "text-lg leading-6 font-medium text-red-800" }, "❌ Error parsing markdown"), /* @__PURE__ */ React__default.createElement("div", { className: "mt-2 max-w-xl text-sm text-red-800 space-y-4" }, /* @__PURE__ */ React__default.createElement("p", null, message), /* @__PURE__ */ React__default.createElement("p", null, "To fix these errors, edit the content in raw-mode."), /* @__PURE__ */ React__default.createElement(
19772
- "button",
19773
- {
19774
- type: "button",
19775
- onClick: () => setRawMode(true),
19776
- className: "rounded-l border-r-0 shadow rounded bg-white cursor-pointer relative inline-flex items-center px-2 py-2 border border-gray-200 hover:text-white text-sm font-medium transition-all ease-out duration-150 hover:bg-gray-500 focus:z-10 focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"
19777
- },
19778
- "Switch to raw-mode"
19779
- ))));
19780
- }
19781
- const useCustomFloatingToolbarState = ({
19782
- editorId,
19783
- floatingOptions,
19784
- focusedEditorId,
19785
- hideToolbar,
19786
- showWhenReadOnly
19787
- }) => {
19788
- const editor = useEditorRef();
19789
- const selectionExpanded = useEditorSelector(
19790
- () => editor.api.isExpanded(),
19791
- []
19792
- );
19793
- const selectionText = useEditorSelector(() => editor.api.string(), []);
19794
- const readOnly = useEditorReadOnly();
19795
- const focused = useFocused();
19796
- const [open, setOpen] = React__default.useState(false);
19797
- const [waitForCollapsedSelection, setWaitForCollapsedSelection] = React__default.useState(false);
19798
- const [mousedown, setMousedown] = React__default.useState(false);
19799
- const floating = useVirtualFloating(
19800
- mergeProps(
19801
- {
19802
- open,
19803
- getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
19804
- onOpenChange: setOpen
19805
- },
19806
- floatingOptions
19807
- )
19808
- );
19809
- return {
19810
- editorId,
19811
- floating,
19812
- focused,
19813
- focusedEditorId,
19814
- hideToolbar,
19815
- mousedown,
19816
- open,
19817
- readOnly,
19818
- selectionExpanded,
19819
- selectionText,
19820
- setMousedown,
19821
- setOpen,
19822
- setWaitForCollapsedSelection,
19823
- showWhenReadOnly,
19824
- waitForCollapsedSelection
19825
- };
19826
- };
19827
- const useCustomFloatingToolbar = ({
19828
- editorId,
19829
- floating,
19830
- focusedEditorId,
19831
- hideToolbar,
19832
- mousedown,
19833
- open,
19834
- readOnly,
19835
- selectionExpanded,
19836
- selectionText,
19837
- setMousedown,
19838
- setOpen,
19839
- setWaitForCollapsedSelection,
19840
- showWhenReadOnly,
19841
- waitForCollapsedSelection
19842
- }) => {
19843
- React__default.useEffect(() => {
19844
- if (!(editorId === focusedEditorId)) {
19845
- setWaitForCollapsedSelection(true);
19846
- }
19847
- if (!selectionExpanded) {
19848
- setWaitForCollapsedSelection(false);
19849
- }
19850
- }, [
19851
- editorId,
19852
- focusedEditorId,
19853
- selectionExpanded,
19854
- setWaitForCollapsedSelection
19855
- ]);
19856
- React__default.useEffect(() => {
19857
- const mouseup = () => setMousedown(false);
19858
- const mousedown2 = () => setMousedown(true);
19859
- document.addEventListener("mouseup", mouseup);
19860
- document.addEventListener("mousedown", mousedown2);
19861
- return () => {
19862
- document.removeEventListener("mouseup", mouseup);
19863
- document.removeEventListener("mousedown", mousedown2);
19864
- };
19865
- }, []);
19866
- React__default.useEffect(() => {
19867
- if (!selectionExpanded || !selectionText || mousedown && !open || hideToolbar || readOnly && !showWhenReadOnly) {
19868
- setOpen(false);
19869
- } else if (selectionText && selectionExpanded && (!waitForCollapsedSelection || readOnly)) {
19870
- setOpen(true);
19871
- }
19872
- }, [
19873
- setOpen,
19874
- editorId,
19875
- focusedEditorId,
19876
- hideToolbar,
19877
- showWhenReadOnly,
19878
- selectionExpanded,
19879
- selectionText,
19880
- mousedown,
19881
- waitForCollapsedSelection,
19882
- open,
19883
- readOnly
19884
- ]);
19885
- const { update } = floating;
19886
- useEditorSelector(() => {
19887
- update == null ? void 0 : update();
19888
- }, [update]);
19889
- const clickOutsideRef = useOnClickOutside(
19890
- () => {
19891
- setOpen(false);
19892
- },
19893
- {
19894
- ignoreClass: "ignore-click-outside/toolbar"
19895
- }
19896
- );
19897
- return {
19898
- clickOutsideRef,
19899
- hidden: !open,
19900
- props: {
19901
- style: floating.style
19902
- },
19903
- ref: floating.refs.setFloating
19904
- };
19905
- };
19906
- const FloatingToolbar = withRef(({ children, state, ...props }, propRef) => {
19907
- const editorId = useEditorRef();
19908
- const focusedEditorId = useEventEditorValue("focus");
19909
- const test = useCustomFloatingToolbarState({
19910
- editorId: editorId.id,
19911
- focusedEditorId,
19912
- ...state,
19913
- floatingOptions: {
19914
- middleware: [
19915
- offset(12),
19916
- flip({
19917
- fallbackPlacements: [
19918
- "top-start",
19919
- "top-end",
19920
- "bottom-start",
19921
- "bottom-end"
19922
- ],
19923
- padding: 12
19924
- })
19925
- ],
19926
- placement: "top",
19927
- ...state == null ? void 0 : state.floatingOptions
19928
- }
19929
- });
19930
- const {
19931
- hidden,
19932
- props: rootProps,
19933
- ref: floatingRef
19934
- } = useCustomFloatingToolbar(test);
19935
- const ref = useComposedRef$1(propRef, floatingRef);
19936
- if (hidden) return null;
19937
- return /* @__PURE__ */ React__default.createElement(PortalBody, null, /* @__PURE__ */ React__default.createElement(
19938
- Toolbar,
19939
- {
19940
- className: cn$1(
19941
- "absolute z-[10799] whitespace-nowrap border bg-popover px-1 opacity-100 shadow-md print:hidden rounded-md"
19942
- ),
19943
- ...props,
19944
- ...rootProps,
19945
- ref
19946
- },
19947
- children
19948
- ));
19949
- });
19950
- function TurnIntoDropdownMenu(props) {
19951
- const { headingLevels } = useToolbarContext();
19952
- const items = [
19953
- paragraphItem,
19954
- ...headingLevels.map((level) => headingItemsByLevel[level])
19955
- ];
19956
- const value = useEditorSelector((editor2) => {
19957
- let initialNodeType = ParagraphPlugin.key;
19958
- let allNodesMatchInitialNodeType = false;
19959
- const codeBlockEntries = editor2.api.nodes({
19960
- match: (n) => editor2.api.isBlock(n),
19961
- mode: "highest"
19962
- });
19963
- const nodes = Array.from(codeBlockEntries);
19964
- if (nodes.length > 0) {
19965
- initialNodeType = nodes[0][0].type;
19966
- allNodesMatchInitialNodeType = nodes.every(([node]) => {
19967
- const type2 = (node == null ? void 0 : node.type) || ParagraphPlugin.key;
19968
- return type2 === initialNodeType;
19969
- });
19970
- }
19971
- return allNodesMatchInitialNodeType ? initialNodeType : ParagraphPlugin.key;
19972
- }, []);
19973
- const editor = useEditorRef();
19974
- const openState = useOpenState();
19975
- const selectedItem = getHeadingItem(value) ?? paragraphItem;
19976
- const { icon: SelectedItemIcon, label: selectedItemLabel } = selectedItem;
19977
- const editorState = useEditorState();
19978
- const userInTable = helpers.isNodeActive(editorState, TablePlugin.key);
19979
- if (userInTable) return null;
19980
- return /* @__PURE__ */ React__default.createElement(DropdownMenu$1, { modal: false, ...openState, ...props }, /* @__PURE__ */ React__default.createElement(DropdownMenuTrigger$1, { asChild: true }, /* @__PURE__ */ React__default.createElement(
19981
- ToolbarButton,
19982
- {
19983
- className: "lg:min-w-[130px]",
19984
- isDropdown: true,
19985
- showArrow: true,
19986
- pressed: openState.open,
19987
- tooltip: "Turn into"
19988
- },
19989
- /* @__PURE__ */ React__default.createElement("span", { className: "" }, selectedItemLabel)
19990
- )), /* @__PURE__ */ React__default.createElement(DropdownMenuContent$1, { align: "start", className: "min-w-0" }, /* @__PURE__ */ React__default.createElement(DropdownMenuLabel, null, "Turn into"), /* @__PURE__ */ React__default.createElement(
19991
- DropdownMenuRadioGroup,
19992
- {
19993
- className: "flex flex-col gap-0.5",
19994
- onValueChange: (type2) => {
19995
- if (type2 === "ul" || type2 === "ol") {
19996
- toggleList(editor, { type: type2 });
19997
- } else {
19998
- unwrapList(editor);
19999
- editor.tf.toggleBlock(type2);
20000
- }
20001
- editor.tf.collapse();
20002
- editor.tf.focus();
20003
- },
20004
- value
20005
- },
20006
- items.map(({ icon: Icon, label, value: itemValue }) => /* @__PURE__ */ React__default.createElement(
20007
- DropdownMenuRadioItem,
20008
- {
20009
- className: "min-w-[180px]",
20010
- key: itemValue,
20011
- value: itemValue
20012
- },
20013
- /* @__PURE__ */ React__default.createElement(Icon, { className: "mr-2 size-5" }),
20014
- label
20015
- ))
20016
- )));
20017
- }
20018
- function FloatingToolbarButtons() {
20019
- return /* @__PURE__ */ React__default.createElement("div", { className: "rounded-md" }, /* @__PURE__ */ React__default.createElement(TurnIntoDropdownMenu, null));
20020
- }
20021
- const FloatingToolbarPlugin = createPlatePlugin({
20022
- key: "floating-toolbar",
20023
- render: {
20024
- afterEditable: () => /* @__PURE__ */ React__default.createElement(FloatingToolbar, null, /* @__PURE__ */ React__default.createElement(FloatingToolbarButtons, null))
20025
- }
20026
- });
20027
20051
  function _1c(hljs) {
20028
20052
  const UNDERSCORE_IDENT_RE = "[A-Za-zА-Яа-яёЁ_][A-Za-zА-Яа-яёЁ_0-9]+";
20029
20053
  const v7_keywords = "далее ";
@@ -70669,6 +70693,252 @@ class HastEmitter {
70669
70693
  return "";
70670
70694
  }
70671
70695
  }
70696
+ const popoverVariants = cva(
70697
+ "z-[999999] w-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-hidden"
70698
+ );
70699
+ const inputVariants = cva(
70700
+ "flex h-[28px] w-full rounded-md border-none bg-transparent px-1.5 py-1 text-base placeholder:text-muted-foreground focus-visible:ring-transparent focus-visible:outline-none md:text-sm"
70701
+ );
70702
+ function LinkFloatingToolbar({
70703
+ state
70704
+ }) {
70705
+ const activeCommentId = usePluginOption({ key: "comment" }, "activeId");
70706
+ const activeSuggestionId = usePluginOption({ key: "suggestion" }, "activeId");
70707
+ const { api, editor } = useEditorPlugin(LinkPlugin);
70708
+ const isUrlValidator = usePluginOption(LinkPlugin, "isUrl");
70709
+ const [currentUrl, setCurrentUrl] = React.useState("");
70710
+ const [isValidUrl, setIsValidUrl] = React.useState(true);
70711
+ const handleUrlInput = React.useCallback(
70712
+ (e) => {
70713
+ const value = e.currentTarget.value;
70714
+ setCurrentUrl(value);
70715
+ if (value && isUrlValidator) {
70716
+ const valid = isUrlValidator(value);
70717
+ setIsValidUrl(valid);
70718
+ } else {
70719
+ setIsValidUrl(true);
70720
+ }
70721
+ },
70722
+ [isUrlValidator]
70723
+ );
70724
+ const floatingOptions = React.useMemo(() => {
70725
+ return {
70726
+ middleware: [
70727
+ offset(8),
70728
+ flip({
70729
+ fallbackPlacements: ["bottom-end", "top-start", "top-end"],
70730
+ padding: 12
70731
+ }),
70732
+ shift({ padding: 8, limiter: limitShift() })
70733
+ ],
70734
+ placement: activeSuggestionId || activeCommentId ? "top-start" : "bottom-start"
70735
+ };
70736
+ }, [activeCommentId, activeSuggestionId]);
70737
+ const insertState = useFloatingLinkInsertState({
70738
+ ...state,
70739
+ floatingOptions: {
70740
+ ...floatingOptions,
70741
+ ...state == null ? void 0 : state.floatingOptions
70742
+ }
70743
+ });
70744
+ const {
70745
+ hidden,
70746
+ props: insertProps,
70747
+ ref: insertRef,
70748
+ textInputProps
70749
+ } = useFloatingLinkInsert(insertState);
70750
+ const editState = useFloatingLinkEditState({
70751
+ ...state,
70752
+ floatingOptions: {
70753
+ ...floatingOptions,
70754
+ ...state == null ? void 0 : state.floatingOptions
70755
+ }
70756
+ });
70757
+ const {
70758
+ editButtonProps,
70759
+ props: editProps,
70760
+ ref: editRef,
70761
+ unlinkButtonProps
70762
+ } = useFloatingLinkEdit(editState);
70763
+ const inputProps = useFormInputProps({
70764
+ preventDefaultOnEnterKeydown: true
70765
+ });
70766
+ if (hidden) return null;
70767
+ const input = /* @__PURE__ */ React.createElement(
70768
+ "div",
70769
+ {
70770
+ className: "z-[999999] flex w-[330px] flex-col relative",
70771
+ ...inputProps
70772
+ },
70773
+ !isValidUrl && currentUrl && /* @__PURE__ */ React.createElement("div", { className: "absolute -top-16 left-0 right-0 z-[1000000] mb-2" }, /* @__PURE__ */ React.createElement(
70774
+ "div",
70775
+ {
70776
+ className: "bg-red-50 border border-red-200 rounded-md p-2 shadow-lg",
70777
+ role: "alert",
70778
+ "aria-live": "polite"
70779
+ },
70780
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement(CircleX, { className: "size-4 text-red-500 mr-2 flex-shrink-0" }), /* @__PURE__ */ React.createElement("span", { className: "text-sm text-red-700 text-wrap" }, "Invalid URL. Please prefix link with https:// or use a relative path like /about")),
70781
+ /* @__PURE__ */ React.createElement("div", { className: "absolute -bottom-1 left-4 w-2 h-2 bg-red-50 border-r border-b border-red-200 transform rotate-45" })
70782
+ )),
70783
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center pr-1 pl-2 text-muted-foreground" }, /* @__PURE__ */ React.createElement(Link, { className: "size-4" })), /* @__PURE__ */ React.createElement(
70784
+ FloatingLinkUrlInput,
70785
+ {
70786
+ className: inputVariants(),
70787
+ placeholder: "Paste link",
70788
+ "data-plate-focus": true,
70789
+ onInput: handleUrlInput
70790
+ }
70791
+ )),
70792
+ /* @__PURE__ */ React.createElement(Separator, { className: "my-1" }),
70793
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center pr-1 pl-2 text-muted-foreground" }, /* @__PURE__ */ React.createElement(Text, { className: "size-4" })), /* @__PURE__ */ React.createElement(
70794
+ "input",
70795
+ {
70796
+ className: inputVariants(),
70797
+ placeholder: "Text to display",
70798
+ "data-plate-focus": true,
70799
+ ...textInputProps
70800
+ }
70801
+ )),
70802
+ /* @__PURE__ */ React.createElement(Separator, { className: "my-1" }),
70803
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-end gap-2 px-2 py-1" }, /* @__PURE__ */ React.createElement(
70804
+ "button",
70805
+ {
70806
+ type: "button",
70807
+ className: buttonVariants$2({ size: "sm", variant: "ghost" }),
70808
+ onClick: () => {
70809
+ api.floatingLink.hide();
70810
+ }
70811
+ },
70812
+ "Cancel"
70813
+ ), /* @__PURE__ */ React.createElement(
70814
+ "button",
70815
+ {
70816
+ type: "button",
70817
+ className: buttonVariants$2({
70818
+ size: "sm",
70819
+ variant: "tinaPrimary"
70820
+ }),
70821
+ onClick: () => {
70822
+ if (isValidUrl && currentUrl) {
70823
+ submitFloatingLink(editor);
70824
+ }
70825
+ },
70826
+ disabled: !isValidUrl && !!currentUrl
70827
+ },
70828
+ "OK"
70829
+ ))
70830
+ );
70831
+ const editContent = editState.isEditing ? input : /* @__PURE__ */ React.createElement("div", { className: "box-content flex items-center" }, /* @__PURE__ */ React.createElement(
70832
+ "button",
70833
+ {
70834
+ className: buttonVariants$2({ size: "sm", variant: "ghost" }),
70835
+ type: "button",
70836
+ ...editButtonProps
70837
+ },
70838
+ "Edit link"
70839
+ ), /* @__PURE__ */ React.createElement(Separator, { orientation: "vertical" }), /* @__PURE__ */ React.createElement(LinkOpenButton, null), /* @__PURE__ */ React.createElement(Separator, { orientation: "vertical" }), /* @__PURE__ */ React.createElement(
70840
+ "button",
70841
+ {
70842
+ className: buttonVariants$2({
70843
+ size: "icon",
70844
+ variant: "ghost"
70845
+ }),
70846
+ type: "button",
70847
+ ...unlinkButtonProps
70848
+ },
70849
+ /* @__PURE__ */ React.createElement(Unlink, { width: 18 })
70850
+ ));
70851
+ return /* @__PURE__ */ React.createElement(PortalBody, null, /* @__PURE__ */ React.createElement(
70852
+ "div",
70853
+ {
70854
+ ref: insertRef,
70855
+ className: popoverVariants(),
70856
+ ...insertProps,
70857
+ style: {
70858
+ ...insertProps.style,
70859
+ zIndex: 999999
70860
+ }
70861
+ },
70862
+ input
70863
+ ), /* @__PURE__ */ React.createElement(
70864
+ "div",
70865
+ {
70866
+ ref: editRef,
70867
+ className: popoverVariants(),
70868
+ ...editProps,
70869
+ style: { ...editProps.style, zIndex: 999999 }
70870
+ },
70871
+ editContent
70872
+ ));
70873
+ }
70874
+ function LinkOpenButton() {
70875
+ const editor = useEditorRef();
70876
+ const selection = useEditorSelection();
70877
+ const attributes = React.useMemo(
70878
+ () => {
70879
+ const entry = editor.api.node({
70880
+ match: { type: editor.getType(LinkPlugin) }
70881
+ });
70882
+ if (!entry) {
70883
+ return {};
70884
+ }
70885
+ const [element] = entry;
70886
+ return getLinkAttributes(editor, element);
70887
+ },
70888
+ // eslint-disable-next-line react-hooks/exhaustive-deps
70889
+ [editor, selection]
70890
+ );
70891
+ return /* @__PURE__ */ React.createElement(
70892
+ "a",
70893
+ {
70894
+ ...attributes,
70895
+ className: buttonVariants$2({
70896
+ size: "icon",
70897
+ variant: "ghost"
70898
+ }),
70899
+ onMouseOver: (e) => {
70900
+ e.stopPropagation();
70901
+ },
70902
+ "aria-label": "Open link in a new tab",
70903
+ target: "_blank"
70904
+ },
70905
+ /* @__PURE__ */ React.createElement(ExternalLink, { width: 18 })
70906
+ );
70907
+ }
70908
+ const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/;
70909
+ const emailLintRE = /mailto:([^?\\]+)/;
70910
+ const telLintRE = /tel:([\d-]+)/;
70911
+ const localhostDomainRE = /^localhost[\d:?]*(?:[^\d:?]\S*)?$/;
70912
+ const nonLocalhostDomainRE = /^[^\s.]+\.\S{2,}$/;
70913
+ const localUrlRE = /^\/\S+/;
70914
+ const isUrl = (string) => {
70915
+ if (typeof string !== "string") {
70916
+ return false;
70917
+ }
70918
+ if (string.startsWith("#")) {
70919
+ return true;
70920
+ }
70921
+ const generalMatch = string.match(protocolAndDomainRE);
70922
+ const emailLinkMatch = string.match(emailLintRE);
70923
+ const telLinkMatch = string.match(telLintRE);
70924
+ const localUrlMatch = string.match(localUrlRE);
70925
+ if (emailLinkMatch || telLinkMatch || localUrlMatch) {
70926
+ return true;
70927
+ }
70928
+ if (generalMatch) {
70929
+ const everythingAfterProtocol = generalMatch[1];
70930
+ if (!everythingAfterProtocol) {
70931
+ return false;
70932
+ }
70933
+ try {
70934
+ new URL(string);
70935
+ } catch {
70936
+ return false;
70937
+ }
70938
+ return localhostDomainRE.test(everythingAfterProtocol) || nonLocalhostDomainRE.test(everythingAfterProtocol);
70939
+ }
70940
+ return false;
70941
+ };
70672
70942
  const createHTMLBlockPlugin = createPlatePlugin({
70673
70943
  key: "html",
70674
70944
  node: {
@@ -70685,18 +70955,26 @@ const createHTMLInlinePlugin = createPlatePlugin({
70685
70955
  isInline: true
70686
70956
  }
70687
70957
  });
70688
- const KEY_BLOCKQUOTE_ENTER_BREAK = "blockquote-enter-break";
70689
- const createBlockquoteEnterBreakPlugin = createPlatePlugin({
70690
- key: KEY_BLOCKQUOTE_ENTER_BREAK,
70958
+ const KEY_HARD_BREAK = "tina-hard-break";
70959
+ const NO_HARD_BREAK = [
70960
+ CodeBlockPlugin.key,
70961
+ TableCellPlugin.key,
70962
+ TableCellHeaderPlugin.key
70963
+ ];
70964
+ const createHardBreakPlugin = createPlatePlugin({
70965
+ key: KEY_HARD_BREAK,
70691
70966
  handlers: {
70692
70967
  onKeyDown: ({ editor, event }) => {
70693
70968
  var _a;
70694
- if (event.key !== "Enter") return;
70695
- const blockquoteEntry = editor.api.above({
70969
+ if (event.key !== "Enter" || event.metaKey || event.ctrlKey) return;
70970
+ const inBlockquote = editor.api.above({
70696
70971
  match: { type: BlockquotePlugin.key }
70697
70972
  });
70698
- if (!blockquoteEntry) return;
70973
+ if (!event.shiftKey && !inBlockquote) return;
70974
+ if (editor.api.some({ match: { type: NO_HARD_BREAK } })) return;
70975
+ if (!editor.selection) return;
70699
70976
  event.preventDefault();
70977
+ if (editor.api.isExpanded()) editor.tf.delete();
70700
70978
  const cursorPosition = (_a = editor.selection) == null ? void 0 : _a.focus;
70701
70979
  if (!cursorPosition) return;
70702
70980
  editor.tf.insertNodes(
@@ -70724,6 +71002,303 @@ const createBreakPlugin = createPlatePlugin({
70724
71002
  }
70725
71003
  }
70726
71004
  });
71005
+ const buildError = (element) => {
71006
+ return {
71007
+ message: element.message,
71008
+ position: element.position && {
71009
+ endColumn: element.position.end.column,
71010
+ startColumn: element.position.start.column,
71011
+ startLineNumber: element.position.start.line,
71012
+ endLineNumber: element.position.end.line
71013
+ }
71014
+ };
71015
+ };
71016
+ const buildErrorMessage = (element) => {
71017
+ if (!element) {
71018
+ return "";
71019
+ }
71020
+ const errorMessage = buildError(element);
71021
+ const message = errorMessage ? `${errorMessage.message}${errorMessage.position ? ` at line: ${errorMessage.position.startLineNumber}, column: ${errorMessage.position.startColumn}` : ""}` : null;
71022
+ return message;
71023
+ };
71024
+ const ELEMENT_INVALID_MARKDOWN = "invalid_markdown";
71025
+ const createInvalidMarkdownPlugin = createPlatePlugin({
71026
+ key: ELEMENT_INVALID_MARKDOWN,
71027
+ options: {
71028
+ isElement: true,
71029
+ isVoid: true,
71030
+ isInline: false
71031
+ },
71032
+ node: {
71033
+ component: InvalidMarkdownElement
71034
+ }
71035
+ });
71036
+ function InvalidMarkdownElement({
71037
+ attributes,
71038
+ element,
71039
+ children
71040
+ }) {
71041
+ return /* @__PURE__ */ React__default.createElement("div", { ...attributes }, /* @__PURE__ */ React__default.createElement(ErrorMessage, { error: element }), children);
71042
+ }
71043
+ function ErrorMessage({ error }) {
71044
+ const message = buildErrorMessage(error);
71045
+ const { setRawMode } = useEditorContext();
71046
+ return /* @__PURE__ */ React__default.createElement("div", { contentEditable: false, className: "bg-red-50 sm:rounded-lg" }, /* @__PURE__ */ React__default.createElement("div", { className: "px-4 py-5 sm:p-6" }, /* @__PURE__ */ React__default.createElement("h3", { className: "text-lg leading-6 font-medium text-red-800" }, "❌ Error parsing markdown"), /* @__PURE__ */ React__default.createElement("div", { className: "mt-2 max-w-xl text-sm text-red-800 space-y-4" }, /* @__PURE__ */ React__default.createElement("p", null, message), /* @__PURE__ */ React__default.createElement("p", null, "To fix these errors, edit the content in raw-mode."), /* @__PURE__ */ React__default.createElement(
71047
+ "button",
71048
+ {
71049
+ type: "button",
71050
+ onClick: () => setRawMode(true),
71051
+ className: "rounded-l border-r-0 shadow rounded bg-white cursor-pointer relative inline-flex items-center px-2 py-2 border border-gray-200 hover:text-white text-sm font-medium transition-all ease-out duration-150 hover:bg-gray-500 focus:z-10 focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500"
71052
+ },
71053
+ "Switch to raw-mode"
71054
+ ))));
71055
+ }
71056
+ const useCustomFloatingToolbarState = ({
71057
+ editorId,
71058
+ floatingOptions,
71059
+ focusedEditorId,
71060
+ hideToolbar,
71061
+ showWhenReadOnly
71062
+ }) => {
71063
+ const editor = useEditorRef();
71064
+ const selectionExpanded = useEditorSelector(
71065
+ () => editor.api.isExpanded(),
71066
+ []
71067
+ );
71068
+ const selectionText = useEditorSelector(() => editor.api.string(), []);
71069
+ const readOnly = useEditorReadOnly();
71070
+ const focused = useFocused();
71071
+ const [open, setOpen] = React__default.useState(false);
71072
+ const [waitForCollapsedSelection, setWaitForCollapsedSelection] = React__default.useState(false);
71073
+ const [mousedown, setMousedown] = React__default.useState(false);
71074
+ const floating = useVirtualFloating(
71075
+ mergeProps(
71076
+ {
71077
+ open,
71078
+ getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
71079
+ onOpenChange: setOpen
71080
+ },
71081
+ floatingOptions
71082
+ )
71083
+ );
71084
+ return {
71085
+ editorId,
71086
+ floating,
71087
+ focused,
71088
+ focusedEditorId,
71089
+ hideToolbar,
71090
+ mousedown,
71091
+ open,
71092
+ readOnly,
71093
+ selectionExpanded,
71094
+ selectionText,
71095
+ setMousedown,
71096
+ setOpen,
71097
+ setWaitForCollapsedSelection,
71098
+ showWhenReadOnly,
71099
+ waitForCollapsedSelection
71100
+ };
71101
+ };
71102
+ const useCustomFloatingToolbar = ({
71103
+ editorId,
71104
+ floating,
71105
+ focusedEditorId,
71106
+ hideToolbar,
71107
+ mousedown,
71108
+ open,
71109
+ readOnly,
71110
+ selectionExpanded,
71111
+ selectionText,
71112
+ setMousedown,
71113
+ setOpen,
71114
+ setWaitForCollapsedSelection,
71115
+ showWhenReadOnly,
71116
+ waitForCollapsedSelection
71117
+ }) => {
71118
+ React__default.useEffect(() => {
71119
+ if (!(editorId === focusedEditorId)) {
71120
+ setWaitForCollapsedSelection(true);
71121
+ }
71122
+ if (!selectionExpanded) {
71123
+ setWaitForCollapsedSelection(false);
71124
+ }
71125
+ }, [
71126
+ editorId,
71127
+ focusedEditorId,
71128
+ selectionExpanded,
71129
+ setWaitForCollapsedSelection
71130
+ ]);
71131
+ React__default.useEffect(() => {
71132
+ const mouseup = () => setMousedown(false);
71133
+ const mousedown2 = () => setMousedown(true);
71134
+ document.addEventListener("mouseup", mouseup);
71135
+ document.addEventListener("mousedown", mousedown2);
71136
+ return () => {
71137
+ document.removeEventListener("mouseup", mouseup);
71138
+ document.removeEventListener("mousedown", mousedown2);
71139
+ };
71140
+ }, []);
71141
+ React__default.useEffect(() => {
71142
+ if (!selectionExpanded || !selectionText || mousedown && !open || hideToolbar || readOnly && !showWhenReadOnly) {
71143
+ setOpen(false);
71144
+ } else if (selectionText && selectionExpanded && (!waitForCollapsedSelection || readOnly)) {
71145
+ setOpen(true);
71146
+ }
71147
+ }, [
71148
+ setOpen,
71149
+ editorId,
71150
+ focusedEditorId,
71151
+ hideToolbar,
71152
+ showWhenReadOnly,
71153
+ selectionExpanded,
71154
+ selectionText,
71155
+ mousedown,
71156
+ waitForCollapsedSelection,
71157
+ open,
71158
+ readOnly
71159
+ ]);
71160
+ const { update } = floating;
71161
+ useEditorSelector(() => {
71162
+ update == null ? void 0 : update();
71163
+ }, [update]);
71164
+ const clickOutsideRef = useOnClickOutside(
71165
+ () => {
71166
+ setOpen(false);
71167
+ },
71168
+ {
71169
+ ignoreClass: "ignore-click-outside/toolbar"
71170
+ }
71171
+ );
71172
+ return {
71173
+ clickOutsideRef,
71174
+ hidden: !open,
71175
+ props: {
71176
+ style: floating.style
71177
+ },
71178
+ ref: floating.refs.setFloating
71179
+ };
71180
+ };
71181
+ const FloatingToolbar = withRef(({ children, state, ...props }, propRef) => {
71182
+ const editorId = useEditorRef();
71183
+ const focusedEditorId = useEventEditorValue("focus");
71184
+ const test = useCustomFloatingToolbarState({
71185
+ editorId: editorId.id,
71186
+ focusedEditorId,
71187
+ ...state,
71188
+ floatingOptions: {
71189
+ middleware: [
71190
+ offset(12),
71191
+ flip({
71192
+ fallbackPlacements: [
71193
+ "top-start",
71194
+ "top-end",
71195
+ "bottom-start",
71196
+ "bottom-end"
71197
+ ],
71198
+ padding: 12
71199
+ })
71200
+ ],
71201
+ placement: "top",
71202
+ ...state == null ? void 0 : state.floatingOptions
71203
+ }
71204
+ });
71205
+ const {
71206
+ hidden,
71207
+ props: rootProps,
71208
+ ref: floatingRef
71209
+ } = useCustomFloatingToolbar(test);
71210
+ const ref = useComposedRef$1(propRef, floatingRef);
71211
+ if (hidden) return null;
71212
+ return /* @__PURE__ */ React__default.createElement(PortalBody, null, /* @__PURE__ */ React__default.createElement(
71213
+ Toolbar,
71214
+ {
71215
+ className: cn$1(
71216
+ "absolute z-[10799] whitespace-nowrap border bg-popover px-1 opacity-100 shadow-md print:hidden rounded-md"
71217
+ ),
71218
+ ...props,
71219
+ ...rootProps,
71220
+ ref
71221
+ },
71222
+ children
71223
+ ));
71224
+ });
71225
+ function TurnIntoDropdownMenu(props) {
71226
+ const { headingLevels } = useToolbarContext();
71227
+ const items = [
71228
+ paragraphItem,
71229
+ ...headingLevels.map((level) => headingItemsByLevel[level])
71230
+ ];
71231
+ const value = useEditorSelector((editor2) => {
71232
+ let initialNodeType = ParagraphPlugin.key;
71233
+ let allNodesMatchInitialNodeType = false;
71234
+ const codeBlockEntries = editor2.api.nodes({
71235
+ match: (n) => editor2.api.isBlock(n),
71236
+ mode: "highest"
71237
+ });
71238
+ const nodes = Array.from(codeBlockEntries);
71239
+ if (nodes.length > 0) {
71240
+ initialNodeType = nodes[0][0].type;
71241
+ allNodesMatchInitialNodeType = nodes.every(([node]) => {
71242
+ const type2 = (node == null ? void 0 : node.type) || ParagraphPlugin.key;
71243
+ return type2 === initialNodeType;
71244
+ });
71245
+ }
71246
+ return allNodesMatchInitialNodeType ? initialNodeType : ParagraphPlugin.key;
71247
+ }, []);
71248
+ const editor = useEditorRef();
71249
+ const openState = useOpenState();
71250
+ const selectedItem = getHeadingItem(value) ?? paragraphItem;
71251
+ const { icon: SelectedItemIcon, label: selectedItemLabel } = selectedItem;
71252
+ const editorState = useEditorState();
71253
+ const userInTable = helpers.isNodeActive(editorState, TablePlugin.key);
71254
+ if (userInTable) return null;
71255
+ return /* @__PURE__ */ React__default.createElement(DropdownMenu$1, { modal: false, ...openState, ...props }, /* @__PURE__ */ React__default.createElement(DropdownMenuTrigger$1, { asChild: true }, /* @__PURE__ */ React__default.createElement(
71256
+ ToolbarButton,
71257
+ {
71258
+ className: "lg:min-w-[130px]",
71259
+ isDropdown: true,
71260
+ showArrow: true,
71261
+ pressed: openState.open,
71262
+ tooltip: "Turn into"
71263
+ },
71264
+ /* @__PURE__ */ React__default.createElement("span", { className: "" }, selectedItemLabel)
71265
+ )), /* @__PURE__ */ React__default.createElement(DropdownMenuContent$1, { align: "start", className: "min-w-0" }, /* @__PURE__ */ React__default.createElement(DropdownMenuLabel$1, null, "Turn into"), /* @__PURE__ */ React__default.createElement(
71266
+ DropdownMenuRadioGroup$1,
71267
+ {
71268
+ className: "flex flex-col gap-0.5",
71269
+ onValueChange: (type2) => {
71270
+ if (type2 === "ul" || type2 === "ol") {
71271
+ toggleList(editor, { type: type2 });
71272
+ } else {
71273
+ unwrapList(editor);
71274
+ editor.tf.toggleBlock(type2);
71275
+ }
71276
+ editor.tf.collapse();
71277
+ editor.tf.focus();
71278
+ },
71279
+ value
71280
+ },
71281
+ items.map(({ icon: Icon, label, value: itemValue }) => /* @__PURE__ */ React__default.createElement(
71282
+ DropdownMenuRadioItem$1,
71283
+ {
71284
+ className: "min-w-[180px]",
71285
+ key: itemValue,
71286
+ value: itemValue
71287
+ },
71288
+ /* @__PURE__ */ React__default.createElement(Icon, { className: "mr-2 size-5" }),
71289
+ label
71290
+ ))
71291
+ )));
71292
+ }
71293
+ function FloatingToolbarButtons() {
71294
+ return /* @__PURE__ */ React__default.createElement("div", { className: "rounded-md" }, /* @__PURE__ */ React__default.createElement(TurnIntoDropdownMenu, null));
71295
+ }
71296
+ const FloatingToolbarPlugin = createPlatePlugin({
71297
+ key: "floating-toolbar",
71298
+ render: {
71299
+ afterEditable: () => /* @__PURE__ */ React__default.createElement(FloatingToolbar, null, /* @__PURE__ */ React__default.createElement(FloatingToolbarButtons, null))
71300
+ }
71301
+ });
70727
71302
  [
70728
71303
  HEADING_KEYS.h1,
70729
71304
  HEADING_KEYS.h2,
@@ -70788,7 +71363,7 @@ const createEditorPlugins = ({
70788
71363
  createImgPlugin,
70789
71364
  createHTMLBlockPlugin,
70790
71365
  createHTMLInlinePlugin,
70791
- createBlockquoteEnterBreakPlugin,
71366
+ createHardBreakPlugin,
70792
71367
  createInvalidMarkdownPlugin,
70793
71368
  CorrectNodeBehaviorPlugin,
70794
71369
  ClearHighlightOnEnterPlugin,
@@ -70887,6 +71462,8 @@ const createEditorPlugins = ({
70887
71462
  ]
70888
71463
  }
70889
71464
  }),
71465
+ // Only reached where `createHardBreakPlugin` bails (code blocks, table
71466
+ // cells) — everywhere else it has already inserted a `break` element.
70890
71467
  SoftBreakPlugin.configure({
70891
71468
  options: {
70892
71469
  rules: [
@@ -71067,6 +71644,7 @@ const MediaWorkflowOverlay = () => {
71067
71644
  phase: "confirming",
71068
71645
  branchName: event.branchName,
71069
71646
  baseBranch: event.baseBranch,
71647
+ allowSaveToProtectedBranch: event.allowSaveToProtectedBranch,
71070
71648
  onConfirm: event.onConfirm,
71071
71649
  onCancel: event.onCancel,
71072
71650
  onSaveToProtectedBranch: event.onSaveToProtectedBranch
@@ -71201,6 +71779,7 @@ const MediaWorkflowOverlay = () => {
71201
71779
  );
71202
71780
  },
71203
71781
  onCreateBranch: handleCreateBranch,
71782
+ allowSaveToProtectedBranch: state.allowSaveToProtectedBranch,
71204
71783
  onSaveToProtectedBranch: () => {
71205
71784
  abortPreflight();
71206
71785
  state.onSaveToProtectedBranch();
@@ -71390,6 +71969,14 @@ const DefaultSessionProvider = ({
71390
71969
  children
71391
71970
  }) => /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, children);
71392
71971
  class AbstractAuthProvider {
71972
+ constructor() {
71973
+ /**
71974
+ * Turns a tokened 401 into the auth wall re-arming. The Client assigns this
71975
+ * automatically; AbstractAuthProvider.fetchWithToken invokes it. A provider
71976
+ * that implements its own fetchWithToken must invoke it itself.
71977
+ */
71978
+ __publicField(this, "sessionExpiredListener");
71979
+ }
71393
71980
  /**
71394
71981
  * Wraps the normal fetch function with same API but adds the authorization header token.
71395
71982
  *
@@ -71400,15 +71987,20 @@ class AbstractAuthProvider {
71400
71987
  * @param init fetch function init
71401
71988
  */
71402
71989
  async fetchWithToken(input, init) {
71990
+ var _a;
71403
71991
  const headers = (init == null ? void 0 : init.headers) || {};
71404
71992
  const accessToken = await this.getAccessToken();
71405
71993
  if (accessToken) {
71406
71994
  headers["Authorization"] = "Bearer " + accessToken;
71407
71995
  }
71408
- return await fetch(input, {
71996
+ const res = await fetch(input, {
71409
71997
  ...init || {},
71410
71998
  headers: new Headers(headers)
71411
71999
  });
72000
+ if (res.status === 401 && accessToken) {
72001
+ (_a = this.sessionExpiredListener) == null ? void 0 : _a.call(this);
72002
+ }
72003
+ return res;
71412
72004
  }
71413
72005
  async getAccessToken() {
71414
72006
  const token = await this.getToken();
@@ -71529,9 +72121,14 @@ class TinaCloudAuthProvider extends AbstractAuthProvider {
71529
72121
  }
71530
72122
  return null;
71531
72123
  }
71532
- const res = await this.fetchWithToken(url, {
71533
- method: "GET"
71534
- });
72124
+ let res;
72125
+ try {
72126
+ res = await this.fetchWithToken(url, { method: "GET" });
72127
+ } catch (networkError) {
72128
+ console.warn("TinaCMS: session check failed, retrying.", networkError);
72129
+ await new Promise((resolve) => setTimeout(resolve, 300));
72130
+ res = await this.fetchWithToken(url, { method: "GET" });
72131
+ }
71535
72132
  const val = await res.json();
71536
72133
  if (!res.status.toString().startsWith("2")) {
71537
72134
  console.error(
@@ -71542,6 +72139,9 @@ class TinaCloudAuthProvider extends AbstractAuthProvider {
71542
72139
  }
71543
72140
  return val;
71544
72141
  } catch (e) {
72142
+ if (e instanceof TypeError || isErrorNamed(e, "AbortError")) {
72143
+ throw e;
72144
+ }
71545
72145
  console.error(e);
71546
72146
  return null;
71547
72147
  }
@@ -71658,6 +72258,8 @@ class Client {
71658
72258
  // automatically hooked into global event bus when attached via cms.
71659
72259
  __publicField(this, "protectedBranches", []);
71660
72260
  __publicField(this, "usingEditorialWorkflow", false);
72261
+ // Whatever the auth provider returned from getUser().
72262
+ __publicField(this, "user");
71661
72263
  __publicField(this, "addPendingContent", async (props) => {
71662
72264
  const mutation = `#graphql
71663
72265
  mutation addPendingDocumentMutation(
@@ -71757,13 +72359,24 @@ mutation addPendingDocumentMutation(
71757
72359
  }
71758
72360
  );
71759
72361
  this.clientId = options.clientId;
71760
- this.authProvider = ((_u = (_t = (_s = this.schema) == null ? void 0 : _s.config) == null ? void 0 : _t.config) == null ? void 0 : _u.authProvider) || new TinaCloudAuthProvider({
71761
- clientId: options.clientId,
71762
- identityApiUrl: this.identityApiUrl,
71763
- getTokenFn: options.getTokenFn,
71764
- tokenStorage,
71765
- frontendUrl: this.frontendUrl
71766
- });
72362
+ this.authProvider = this.adoptAuthProvider(
72363
+ ((_u = (_t = (_s = this.schema) == null ? void 0 : _s.config) == null ? void 0 : _t.config) == null ? void 0 : _u.authProvider) || new TinaCloudAuthProvider({
72364
+ clientId: options.clientId,
72365
+ identityApiUrl: this.identityApiUrl,
72366
+ getTokenFn: options.getTokenFn,
72367
+ tokenStorage,
72368
+ frontendUrl: this.frontendUrl
72369
+ })
72370
+ );
72371
+ }
72372
+ /**
72373
+ * Every provider swap must come through here: the listener is what turns a
72374
+ * REST 401 into the auth wall re-arming, and a bare field assignment would
72375
+ * silently drop it.
72376
+ */
72377
+ adoptAuthProvider(provider) {
72378
+ provider.sessionExpiredListener = () => dispatchSessionExpired(this.events);
72379
+ return provider;
71767
72380
  }
71768
72381
  get isLocalMode() {
71769
72382
  return false;
@@ -71808,6 +72421,17 @@ mutation addPendingDocumentMutation(
71808
72421
  variables
71809
72422
  })
71810
72423
  });
72424
+ if (res.status === 401) {
72425
+ res.json().catch(() => {
72426
+ });
72427
+ if (this.isCustomContentApi) {
72428
+ console.error(
72429
+ `TinaCMS: the content API at ${this.contentApiUrl} returned 401. If signing in again does not resolve this, check the backend's auth configuration (clientId: ${this.options.clientId}, branch: ${this.branch}).`
72430
+ );
72431
+ }
72432
+ dispatchSessionExpired(this.events);
72433
+ throw new SessionExpiredError();
72434
+ }
71811
72435
  if (res.status !== 200) {
71812
72436
  let errorMessage = `Unable to complete request, ${res.statusText}`;
71813
72437
  const resBody = await res.json();
@@ -72229,6 +72853,10 @@ mutation addPendingDocumentMutation(
72229
72853
  throw error;
72230
72854
  }
72231
72855
  }
72856
+ /**
72857
+ * The server normalises `branchName`, so follow-up calls must use the
72858
+ * `branchName` from the response rather than the one they sent.
72859
+ */
72232
72860
  async startMediaEditorialWorkflow(options) {
72233
72861
  const url = `${this.contentApiBase}/editorial-workflow/${this.clientId}/media`;
72234
72862
  return await this.postEditorialWorkflow(
@@ -72253,7 +72881,9 @@ class LocalClient extends Client {
72253
72881
  customContentApiUrl: props && props.customContentApiUrl ? props.customContentApiUrl : DEFAULT_LOCAL_TINA_GQL_SERVER_URL
72254
72882
  };
72255
72883
  super(clientProps);
72256
- this.authProvider = ((_c = (_b = (_a = this.schema) == null ? void 0 : _a.config) == null ? void 0 : _b.config) == null ? void 0 : _c.authProvider) || new LocalAuthProvider();
72884
+ this.authProvider = this.adoptAuthProvider(
72885
+ ((_c = (_b = (_a = this.schema) == null ? void 0 : _a.config) == null ? void 0 : _b.config) == null ? void 0 : _c.authProvider) || new LocalAuthProvider()
72886
+ );
72257
72887
  }
72258
72888
  get isLocalMode() {
72259
72889
  return true;
@@ -72544,6 +73174,7 @@ const AuthWallInner = ({
72544
73174
  if (!mounted) return;
72545
73175
  if (isAuthorized) {
72546
73176
  const user = await client.authProvider.getUser();
73177
+ client.user = user;
72547
73178
  if (user.passwordChangeRequired) {
72548
73179
  window.location.hash = "#/screens/change_password";
72549
73180
  }
@@ -72599,7 +73230,7 @@ const AuthWallInner = ({
72599
73230
  }
72600
73231
  return onAuthenticated();
72601
73232
  } catch (e) {
72602
- if (e instanceof AuthenticationCancelledError || (e == null ? void 0 : e.name) === "AuthenticationCancelledError") {
73233
+ if (e instanceof AuthenticationCancelledError || isErrorNamed(e, "AuthenticationCancelledError")) {
72603
73234
  return;
72604
73235
  }
72605
73236
  console.error(e);
@@ -73338,7 +73969,7 @@ const Loader = (props) => {
73338
73969
  ))
73339
73970
  ),
73340
73971
  /* @__PURE__ */ React__default.createElement(
73341
- "p",
73972
+ LoadingMessage,
73342
73973
  {
73343
73974
  style: {
73344
73975
  fontSize: "18px",
@@ -73348,8 +73979,7 @@ const Loader = (props) => {
73348
73979
  fontFamily: "'Inter', sans-serif",
73349
73980
  fontWeight: "normal"
73350
73981
  }
73351
- },
73352
- "Please wait, Tina is loading data..."
73982
+ }
73353
73983
  )
73354
73984
  )
73355
73985
  ), props.children);
@@ -73650,7 +74280,7 @@ const LoadingPage = () => /* @__PURE__ */ React__default.createElement(React__de
73650
74280
  ))
73651
74281
  ),
73652
74282
  /* @__PURE__ */ React__default.createElement(
73653
- "p",
74283
+ LoadingMessage,
73654
74284
  {
73655
74285
  style: {
73656
74286
  fontSize: "16px",
@@ -73660,8 +74290,7 @@ const LoadingPage = () => /* @__PURE__ */ React__default.createElement(React__de
73660
74290
  fontFamily: "'Inter', sans-serif",
73661
74291
  fontWeight: "normal"
73662
74292
  }
73663
- },
73664
- "Please wait, Tina is loading data..."
74293
+ }
73665
74294
  )
73666
74295
  )
73667
74296
  ));
@@ -73948,6 +74577,7 @@ const CollectionListPage = () => {
73948
74577
  );
73949
74578
  return (_b2 = (_a2 = doc == null ? void 0 : doc.document) == null ? void 0 : _a2._sys) == null ? void 0 : _b2.hasReferences;
73950
74579
  } catch (error) {
74580
+ if (isSessionExpiredError(error)) throw error;
73951
74581
  cms.alerts.error(
73952
74582
  "Document was not found, ask a developer for help or check the console for an error message"
73953
74583
  );
@@ -73963,6 +74593,7 @@ const CollectionListPage = () => {
73963
74593
  );
73964
74594
  reFetchCollection();
73965
74595
  } catch (error) {
74596
+ if (isSessionExpiredError(error)) throw error;
73966
74597
  if (error.message.includes(ERR_HAS_REFERENCES)) {
73967
74598
  cms.alerts.error(
73968
74599
  error.message.split("\n ").filter(Boolean)[1]
@@ -73992,6 +74623,7 @@ const CollectionListPage = () => {
73992
74623
  );
73993
74624
  reFetchCollection();
73994
74625
  } catch (error) {
74626
+ if (isSessionExpiredError(error)) throw error;
73995
74627
  cms.alerts.warn(
73996
74628
  "Document was not deleted, ask a developer for help or check the console for an error message"
73997
74629
  );
@@ -74024,6 +74656,7 @@ const CollectionListPage = () => {
74024
74656
  );
74025
74657
  reFetchCollection();
74026
74658
  } catch (error) {
74659
+ if (isSessionExpiredError(error)) throw error;
74027
74660
  if (error.message && error.message.includes(ERR_ALREADY_EXISTS)) {
74028
74661
  cms.alerts.error(
74029
74662
  `Document was not renamed. The filename "${vars.newRelativePath}" is already used by another document, please choose a different name.`
@@ -74070,6 +74703,7 @@ const CollectionListPage = () => {
74070
74703
  );
74071
74704
  cms.alerts.info("Folder was successfully created");
74072
74705
  } catch (error) {
74706
+ if (isSessionExpiredError(error)) throw error;
74073
74707
  if (error.message && error.message.includes(ERR_ALREADY_EXISTS)) {
74074
74708
  cms.alerts.error(
74075
74709
  `Folder was not created, folder with name "${vars.folderName}" already exists ${folder.name ? `in ${folder.name}` : ""}`
@@ -74633,6 +75267,7 @@ const DeleteModal = ({
74633
75267
  useEffect(() => {
74634
75268
  checkRefsFunc().then((result) => {
74635
75269
  setHasRefs(result);
75270
+ }).catch(() => {
74636
75271
  });
74637
75272
  }, [filename, checkRefsFunc]);
74638
75273
  return /* @__PURE__ */ React__default.createElement(Modal, null, /* @__PURE__ */ React__default.createElement(PopupModal, null, /* @__PURE__ */ React__default.createElement(ModalHeader, { close: close2 }, "Delete ", filename), /* @__PURE__ */ React__default.createElement(ModalBody, { padded: true }, /* @__PURE__ */ React__default.createElement("p", null, `Are you sure you want to delete ${filename}?${hasRefs ? " References to this document will also be deleted." : ""}`)), /* @__PURE__ */ React__default.createElement(ModalActions, null, /* @__PURE__ */ React__default.createElement(Button$2, { style: { flexGrow: 2 }, onClick: close2 }, "Cancel"), /* @__PURE__ */ React__default.createElement(
@@ -74641,7 +75276,10 @@ const DeleteModal = ({
74641
75276
  style: { flexGrow: 3 },
74642
75277
  variant: "danger",
74643
75278
  onClick: async () => {
74644
- await deleteFunc();
75279
+ try {
75280
+ await deleteFunc();
75281
+ } catch {
75282
+ }
74645
75283
  close2();
74646
75284
  }
74647
75285
  },
@@ -74849,16 +75487,18 @@ const useGetCollection = (cms, collectionName, includeDocuments = true, folder,
74849
75487
  );
74850
75488
  setCollection(collection2);
74851
75489
  } else if (!cancelled) {
74852
- cms.events.dispatch({ type: "cms:session-expired" });
75490
+ dispatchSessionExpired(cms.events);
74853
75491
  setCollection(void 0);
74854
75492
  }
74855
75493
  } catch (error2) {
74856
- cms.alerts.error(
74857
- `[${error2.name}] GetCollection failed: ${error2.message}`
74858
- );
74859
- console.error(error2);
74860
75494
  setCollection(void 0);
74861
- setError(error2);
75495
+ if (!isSessionExpiredError(error2)) {
75496
+ cms.alerts.error(
75497
+ `[${error2.name}] GetCollection failed: ${error2.message}`
75498
+ );
75499
+ console.error(error2);
75500
+ setError(error2);
75501
+ }
74862
75502
  }
74863
75503
  if (!cancelled) {
74864
75504
  setLoading(false);
@@ -74910,6 +75550,15 @@ const useSearchCollection = (cms, collectionName, includeDocuments = true, folde
74910
75550
  return api.fetchDocument(collection2, relativePath, false);
74911
75551
  })
74912
75552
  );
75553
+ if (docs.some(
75554
+ (p) => p.status === "rejected" && isSessionExpiredError(p.reason)
75555
+ )) {
75556
+ setCollection(void 0);
75557
+ if (!cancelled) {
75558
+ setLoading(false);
75559
+ }
75560
+ return;
75561
+ }
74913
75562
  const edges = docs.filter((p) => {
74914
75563
  var _a;
74915
75564
  return p.status === "fulfilled" && !!((_a = p.value) == null ? void 0 : _a.document);
@@ -74932,16 +75581,18 @@ const useSearchCollection = (cms, collectionName, includeDocuments = true, folde
74932
75581
  };
74933
75582
  setCollection(collectionData);
74934
75583
  } else if (!cancelled) {
74935
- cms.events.dispatch({ type: "cms:session-expired" });
75584
+ dispatchSessionExpired(cms.events);
74936
75585
  setCollection(void 0);
74937
75586
  }
74938
75587
  } catch (error2) {
74939
- cms.alerts.error(
74940
- `[${error2.name}] GetCollection failed: ${error2.message}`
74941
- );
74942
- console.error(error2);
74943
75588
  setCollection(void 0);
74944
- setError(error2);
75589
+ if (!isSessionExpiredError(error2)) {
75590
+ cms.alerts.error(
75591
+ `[${error2.name}] GetCollection failed: ${error2.message}`
75592
+ );
75593
+ console.error(error2);
75594
+ setError(error2);
75595
+ }
74945
75596
  }
74946
75597
  if (!cancelled) {
74947
75598
  setLoading(false);
@@ -75051,9 +75702,7 @@ const createDocument = async (cms, collection, template, mutationInfo, folder, v
75051
75702
  if (await api.isAuthenticated()) {
75052
75703
  await api.createDocument(collection, relativePath, params);
75053
75704
  } else {
75054
- const authMessage = `CreateDocument failed: User is no longer authenticated; please login and try again.`;
75055
- cms.alerts.error(authMessage);
75056
- console.error(authMessage);
75705
+ dispatchSessionExpired(cms.events);
75057
75706
  return false;
75058
75707
  }
75059
75708
  };
@@ -75236,7 +75885,7 @@ const RenderForm$1 = ({
75236
75885
  onSubmit: async (values) => {
75237
75886
  try {
75238
75887
  const folderName2 = folder.fullyQualifiedName ? folder.name : "";
75239
- await createDocument(
75888
+ const result = await createDocument(
75240
75889
  cms,
75241
75890
  collection,
75242
75891
  template,
@@ -75244,6 +75893,7 @@ const RenderForm$1 = ({
75244
75893
  folderName2,
75245
75894
  values
75246
75895
  );
75896
+ if (result === false) return;
75247
75897
  cms.alerts.success("Document created!");
75248
75898
  setTimeout(() => {
75249
75899
  navigate(
@@ -75251,6 +75901,7 @@ const RenderForm$1 = ({
75251
75901
  );
75252
75902
  }, 10);
75253
75903
  } catch (error) {
75904
+ if (isSessionExpiredError(error)) throw error;
75254
75905
  const defaultErrorText = "There was a problem saving your document.";
75255
75906
  if (error.message && error.message.includes(ERR_ALREADY_EXISTS)) {
75256
75907
  cms.alerts.error(
@@ -75323,17 +75974,19 @@ const useGetDocument = (cms, collectionName, relativePath) => {
75323
75974
  setDocument(response.document);
75324
75975
  }
75325
75976
  } else if (!isCancelled) {
75326
- cms.events.dispatch({ type: "cms:session-expired" });
75977
+ dispatchSessionExpired(cms.events);
75327
75978
  setDocument(void 0);
75328
75979
  }
75329
75980
  } catch (error2) {
75330
75981
  if (!isCancelled) {
75331
- cms.alerts.error(
75332
- `[${error2.name}] GetDocument failed: ${error2.message}`
75333
- );
75334
- console.error(error2);
75335
75982
  setDocument(void 0);
75336
- setError(error2);
75983
+ if (!isSessionExpiredError(error2)) {
75984
+ cms.alerts.error(
75985
+ `[${error2.name}] GetDocument failed: ${error2.message}`
75986
+ );
75987
+ console.error(error2);
75988
+ setError(error2);
75989
+ }
75337
75990
  }
75338
75991
  }
75339
75992
  if (!isCancelled) {
@@ -75419,9 +76072,7 @@ const updateDocument = async (cms, relativePath, collection, mutationInfo, value
75419
76072
  if (await api.isAuthenticated()) {
75420
76073
  await api.updateDocument(collection, relativePath, params);
75421
76074
  } else {
75422
- const authMessage = `UpdateDocument failed: User is no longer authenticated; please login and try again.`;
75423
- cms.alerts.error(authMessage);
75424
- console.error(authMessage);
76075
+ dispatchSessionExpired(cms.events);
75425
76076
  return false;
75426
76077
  }
75427
76078
  };
@@ -75499,15 +76150,17 @@ const RenderForm = ({
75499
76150
  initialValues: document2._values,
75500
76151
  onSubmit: async (values) => {
75501
76152
  try {
75502
- await updateDocument(
76153
+ const result = await updateDocument(
75503
76154
  cms,
75504
76155
  relativePath,
75505
76156
  collection,
75506
76157
  mutationInfo,
75507
76158
  values
75508
76159
  );
76160
+ if (result === false) return;
75509
76161
  cms.alerts.success("Document updated!");
75510
76162
  } catch (error) {
76163
+ if (isSessionExpiredError(error)) throw error;
75511
76164
  cms.alerts.error(
75512
76165
  () => ErrorDialog({
75513
76166
  title: "There was a problem saving your document",
@@ -75584,6 +76237,24 @@ const ScreenPage = () => {
75584
76237
  } })));
75585
76238
  });
75586
76239
  };
76240
+ function resolvePreviewPath(splat, baseOrigin = typeof window !== "undefined" ? window.location.origin : "") {
76241
+ if (!baseOrigin || !splat) {
76242
+ return { path: "/", offOrigin: false };
76243
+ }
76244
+ try {
76245
+ const resolved = new URL(`/${splat}`, baseOrigin);
76246
+ if (resolved.origin !== baseOrigin) {
76247
+ return { path: "/", offOrigin: true };
76248
+ }
76249
+ const path = `${resolved.pathname}${resolved.search}${resolved.hash}`;
76250
+ if (new URL(path, baseOrigin).origin !== baseOrigin) {
76251
+ return { path: "/", offOrigin: true };
76252
+ }
76253
+ return { path, offOrigin: false };
76254
+ } catch {
76255
+ return { path: "/", offOrigin: true };
76256
+ }
76257
+ }
75587
76258
  const TROUBLESHOOTING_URL = "https://tina.io/docs/tinacloud/troubleshooting";
75588
76259
  const ErrorModalContent = (props) => {
75589
76260
  const { title, message } = props;
@@ -75663,12 +76334,23 @@ const SetPreviewFlag = ({
75663
76334
  return null;
75664
76335
  };
75665
76336
  const PreviewInner = ({ preview, config }) => {
76337
+ const cms = useCMS$1();
75666
76338
  const params = useParams();
75667
76339
  const navigate = useNavigate();
75668
- const [url, setURL] = React__default.useState(`/${params["*"]}`);
76340
+ const splat = params["*"];
76341
+ const { path: paramURL, offOrigin } = resolvePreviewPath(splat);
76342
+ const [url, setURL] = React__default.useState(paramURL);
75669
76343
  const [reportedURL, setReportedURL] = useState(null);
75670
76344
  const ref = React__default.useRef(null);
75671
- const paramURL = `/${params["*"]}`;
76345
+ React__default.useEffect(() => {
76346
+ if (!offOrigin) {
76347
+ return;
76348
+ }
76349
+ cms.alerts.warn(
76350
+ "This preview link points to a different site, so it was not opened."
76351
+ );
76352
+ navigate(`/~${paramURL}`, { replace: true });
76353
+ }, [splat, offOrigin]);
75672
76354
  React__default.useEffect(() => {
75673
76355
  if (reportedURL !== paramURL && paramURL) {
75674
76356
  setURL(paramURL);
@@ -75717,6 +76399,9 @@ const CheckSchema = ({
75717
76399
  );
75718
76400
  }
75719
76401
  }).catch((error) => {
76402
+ if (isSessionExpiredError(error)) {
76403
+ return;
76404
+ }
75720
76405
  if (error.message.includes(ERR_NOT_INDEXED)) {
75721
76406
  setSchemaMissingError(true);
75722
76407
  } else {
@@ -76181,6 +76866,7 @@ export {
76181
76866
  Select$1 as Select,
76182
76867
  SelectField,
76183
76868
  SelectFieldPlugin,
76869
+ SessionExpiredError,
76184
76870
  SettingsIcon,
76185
76871
  StrikethroughIcon,
76186
76872
  StyledFile,
@@ -76233,6 +76919,7 @@ export {
76233
76919
  get,
76234
76920
  getFilteredBranchList,
76235
76921
  getStaticPropsForTina,
76922
+ isSessionExpiredError,
76236
76923
  passwordFieldClasses,
76237
76924
  resolveField,
76238
76925
  safeAssertShape,