tinacms 3.6.3 → 3.7.1

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.
Files changed (21) hide show
  1. package/dist/index.js +1218 -119
  2. package/dist/lib/posthog/posthog.d.ts +6 -0
  3. package/dist/rich-text/index.d.ts +4 -0
  4. package/dist/rich-text/index.js +14 -0
  5. package/dist/toolkit/components/dashboard/dashboard-ui.d.ts +16 -0
  6. package/dist/toolkit/components/dashboard/media-usage-dashboard/index.d.ts +6 -0
  7. package/dist/toolkit/components/dashboard/media-usage-dashboard/media-lightbox.d.ts +6 -0
  8. package/dist/toolkit/components/dashboard/media-usage-dashboard/media-usage-scanner.d.ts +21 -0
  9. package/dist/toolkit/components/dashboard/media-usage-dashboard/media-usage-table.d.ts +7 -0
  10. package/dist/toolkit/components/dashboard/media-usage-dashboard/media-usage-thumbnails.d.ts +5 -0
  11. package/dist/toolkit/components/dashboard/media-usage-dashboard/useMediaUsageScanner.d.ts +8 -0
  12. package/dist/toolkit/components/ui/dialog.d.ts +16 -0
  13. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/components/plate-ui/icons.d.ts +1 -0
  14. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/components/plate-ui/mark-toolbar-button.d.ts +1 -0
  15. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/plugins/editor-plugins.d.ts +3 -3
  16. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/plugins/ui/components.d.ts +2 -0
  17. package/dist/toolkit/fields/plugins/mdx-field-plugin/plate/toolbar/toolbar-overrides.d.ts +1 -1
  18. package/dist/toolkit/form-builder/create-branch-modal.d.ts +3 -1
  19. package/dist/toolkit/plugin-screens/media-usage-dashboard-screen.d.ts +1 -0
  20. package/dist/toolkit/react-screens/screen-plugin.d.ts +2 -2
  21. package/package.json +7 -4
package/dist/index.js CHANGED
@@ -16,13 +16,14 @@ import { HEADING_KEYS as HEADING_KEYS$1, HEADING_LEVELS as HEADING_LEVELS$1 } fr
16
16
  import { isHotkey } from "is-hotkey";
17
17
  import { Slot } from "@radix-ui/react-slot";
18
18
  import { isLangSupported, formatCodeBlock, insertEmptyCodeBlock, unwrapCodeBlock, isCodeBlockEmpty, isSelectionAtCodeBlockStart } from "@udecode/plate-code-block";
19
- import { X, Search, ChevronDown, Check, AlertTriangle, BracesIcon, Plus, AlignCenter as AlignCenter$1, AlignJustify, AlignLeft as AlignLeft$1, AlignRight as AlignRight$1, PaintBucket, Quote, ChevronRight, ChevronsUpDown, FileCode, Baseline, RectangleVertical, Combine, Ungroup, MessageSquare, MessageSquarePlus, Trash, GripVertical, Edit2, Smile, ExternalLink, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Indent, Keyboard, WrapText, Minus, MoreHorizontal, Outdent, Pilcrow, RotateCcw, RectangleHorizontal, Settings, Strikethrough, Subscript, Superscript, Table as Table$1, Text as Text$2, Underline, Link2Off, Eye, SeparatorHorizontal, Moon, SunMedium, Twitter, PaintBucketIcon, CombineIcon, SquareSplitHorizontalIcon, Grid2X2Icon, Trash2Icon, ArrowUp, ArrowDown, XIcon, ArrowLeft, ArrowRight, EraserIcon, ChevronDownIcon as ChevronDownIcon$1, ChevronUp, Clock, CalendarCheck, Calendar as Calendar$1, CalendarDays, RotateCw, ChevronLeft, ArrowUpDown, LoaderCircle, TriangleAlert, FileStack, History, GitBranchIcon, List as List$1, ListOrdered, Grid3x3Icon, CircleX, Link, Unlink } from "lucide-react";
19
+ import { X, Search, ChevronDown, Check, AlertTriangle, BracesIcon, Plus, AlignCenter as AlignCenter$1, AlignJustify, AlignLeft as AlignLeft$1, AlignRight as AlignRight$1, PaintBucket, Quote, ChevronRight, ChevronsUpDown, FileCode, Baseline, RectangleVertical, Combine, Ungroup, MessageSquare, MessageSquarePlus, Trash, GripVertical, Edit2, Smile, ExternalLink, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Indent, Keyboard, WrapText, Minus, MoreHorizontal, Outdent, Pilcrow, RotateCcw, RectangleHorizontal, Settings, Highlighter, Strikethrough, Subscript, Superscript, Table as Table$1, Text as Text$2, Underline, Link2Off, Eye, SeparatorHorizontal, Moon, SunMedium, Twitter, PaintBucketIcon, CombineIcon, SquareSplitHorizontalIcon, Grid2X2Icon, Trash2Icon, ArrowUp, ArrowDown, XIcon, ArrowLeft, ArrowRight, EraserIcon, ChevronDownIcon as ChevronDownIcon$1, ChevronUp, Clock, CalendarCheck, Calendar as Calendar$1, CalendarDays, RotateCw, ChevronLeft, ArrowUpDown, AlertCircle, Image, RefreshCw, Database, CheckCircle2, ImageOff, LoaderCircle, TriangleAlert, FileStack, History, GitBranchIcon, List as List$1, ListOrdered, Grid3x3Icon, CircleX, Link, Unlink } from "lucide-react";
20
20
  import mermaid from "mermaid";
21
21
  import { cva } from "class-variance-authority";
22
22
  import { Command as Command$2 } from "cmdk";
23
23
  import * as DialogPrimitive from "@radix-ui/react-dialog";
24
24
  import * as PopoverPrimitive from "@radix-ui/react-popover";
25
25
  import { PopoverAnchor } from "@radix-ui/react-popover";
26
+ import posthog from "posthog-js";
26
27
  import { createSlatePlugin as createSlatePlugin$1, someHtmlElement, findHtmlParentElement, createTSlatePlugin as createTSlatePlugin$1, RangeApi as RangeApi$1, TextApi as TextApi$1, HtmlPlugin as HtmlPlugin$1, NodeApi as NodeApi$1, ElementApi as ElementApi$1, Hotkeys, isHotkey as isHotkey$1, isUrl as isUrl$1, getEditorPlugin as getEditorPlugin$1, bindFirst as bindFirst$1, sanitizeUrl, PathApi as PathApi$1, isDefined as isDefined$1, PointApi as PointApi$1, BaseParagraphPlugin as BaseParagraphPlugin$1, match as match$2, deleteMerge, getPluginTypes, queryNode as queryNode$1, isType, getInjectMatch as getInjectMatch$1, traverseHtmlElements, isHtmlBlockElement as isHtmlBlockElement$1, postCleanHtml, mergeProps } from "@udecode/plate";
27
28
  import { useComboboxContext, Combobox as Combobox$1, useComboboxStore, ComboboxProvider, Portal, ComboboxPopover, ComboboxItem } from "@ariakit/react";
28
29
  import { withTriggerCombobox, filterWords } from "@udecode/plate-combobox";
@@ -35,6 +36,7 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
35
36
  import * as ToolbarPrimitive from "@radix-ui/react-toolbar";
36
37
  import * as SeparatorPrimitive from "@radix-ui/react-separator";
37
38
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
39
+ import colorString, { get as get$6, to as to$1 } from "color-string";
38
40
  import { createForm, FORM_ERROR, getIn } from "final-form";
39
41
  import arrayMutators from "final-form-arrays";
40
42
  import setFieldData from "final-form-set-field-data";
@@ -45,8 +47,6 @@ import { sortableKeyboardCoordinates, useSortable, SortableContext, verticalList
45
47
  import { CSS } from "@dnd-kit/utilities";
46
48
  import { buildSchema, print, getIntrospectionQuery, buildClientSchema, parse as parse$4 } from "graphql";
47
49
  import { diff as diff$1 } from "@graphql-inspector/core";
48
- import posthog from "posthog-js";
49
- import { get as get$6, to as to$1 } from "color-string";
50
50
  import { HexColorPicker } from "react-colorful";
51
51
  import * as dropzone from "react-dropzone";
52
52
  import { Command as Command$3 } from "@udecode/cmdk";
@@ -63,7 +63,7 @@ import "moment-timezone";
63
63
  import { DayPicker } from "react-day-picker";
64
64
  import * as SelectPrimitive from "@radix-ui/react-select";
65
65
  import { formatDistanceToNow } from "date-fns";
66
- import { useReactTable, getCoreRowModel, getSortedRowModel, flexRender } from "@tanstack/react-table";
66
+ import { useReactTable, getCoreRowModel, getSortedRowModel, flexRender, getFilteredRowModel, getExpandedRowModel } from "@tanstack/react-table";
67
67
  import { TinaSchema, addNamespaceToSchema, parseURL, resolveForm, normalizePath, canonicalPath, validateSchema } from "@tinacms/schema-tools";
68
68
  import { NAMER, resolveField } from "@tinacms/schema-tools";
69
69
  import gql from "graphql-tag";
@@ -27490,6 +27490,35 @@ var CodeBlockPlugin = toPlatePlugin(BaseCodeBlockPlugin, {
27490
27490
  }
27491
27491
  }
27492
27492
  }));
27493
+ var BaseHighlightPlugin = createSlatePlugin$1({
27494
+ key: "highlight",
27495
+ node: { isLeaf: true },
27496
+ parsers: {
27497
+ html: {
27498
+ deserializer: {
27499
+ rules: [
27500
+ {
27501
+ validNodeName: ["MARK"]
27502
+ }
27503
+ ]
27504
+ }
27505
+ }
27506
+ }
27507
+ });
27508
+ var HighlightPlugin = toPlatePlugin(
27509
+ BaseHighlightPlugin,
27510
+ ({ editor, type: type2 }) => ({
27511
+ shortcuts: {
27512
+ toggleHighlight: {
27513
+ keys: [[Key2.Mod, Key2.Shift, "h"]],
27514
+ preventDefault: true,
27515
+ handler: () => {
27516
+ editor.tf.toggleMark(type2);
27517
+ }
27518
+ }
27519
+ }
27520
+ })
27521
+ );
27493
27522
  var BaseHorizontalRulePlugin = createSlatePlugin$1({
27494
27523
  key: "hr",
27495
27524
  node: { isElement: true, isVoid: true },
@@ -33620,13 +33649,13 @@ const Button$3 = withRef$1(({ asChild = false, className, isMenu, size: size2, v
33620
33649
  }
33621
33650
  );
33622
33651
  });
33623
- const DialogPortal = DialogPrimitive.Portal;
33624
- const DialogOverlay = withCn(
33652
+ const DialogPortal$1 = DialogPrimitive.Portal;
33653
+ const DialogOverlay$1 = withCn(
33625
33654
  DialogPrimitive.Overlay,
33626
33655
  "fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0"
33627
33656
  );
33628
33657
  withRef$1(
33629
- ({ children, className, ...props }, ref) => /* @__PURE__ */ React.createElement(DialogPortal, null, /* @__PURE__ */ React.createElement(DialogOverlay, null), /* @__PURE__ */ React.createElement(
33658
+ ({ children, className, ...props }, ref) => /* @__PURE__ */ React.createElement(DialogPortal$1, null, /* @__PURE__ */ React.createElement(DialogOverlay$1, null), /* @__PURE__ */ React.createElement(
33630
33659
  DialogPrimitive.Content,
33631
33660
  {
33632
33661
  ref,
@@ -34458,6 +34487,7 @@ const Icons = {
34458
34487
  row: RectangleHorizontal,
34459
34488
  search: Search,
34460
34489
  settings: Settings,
34490
+ highlight: Highlighter,
34461
34491
  strikethrough: Strikethrough,
34462
34492
  subscript: Subscript,
34463
34493
  superscript: Superscript,
@@ -34724,6 +34754,101 @@ function ItalicIcon(props) {
34724
34754
  ))
34725
34755
  );
34726
34756
  }
34757
+ const BranchSwitchedEvent = "branch-switched";
34758
+ const BranchSwitcherOpenedEvent = "branch-switcher-opened";
34759
+ const BranchSwitcherSearchEvent = "branch-switcher-search";
34760
+ const BranchSwitcherDropDownEvent = "branch-switcher-dropdown";
34761
+ const BranchSwitcherPRClickedEvent = "branch-switcher-pr-clicked";
34762
+ const SavedContentEvent = "saved-content";
34763
+ const SaveContentErrorEvent = "save-content-error";
34764
+ const FormResetEvent = "form-reset";
34765
+ const MediaManagerContentUploadedEvent = "media-manager-content-uploaded";
34766
+ const MediaManagerContentDeletedEvent = "media-manager-content-deleted";
34767
+ const RichTextEditorSwitchedEvent = "rich-text-editor-switched";
34768
+ const EventLogPageViewedEvent = "event-log-page-viewed";
34769
+ const TinaCMSStartedEvent = "tina-cms-started";
34770
+ const CollectionListPageItemClickedEvent = "collection-list-page-item-clicked";
34771
+ const CollectionListPageSortEvent = "collection-list-page-sort";
34772
+ const CollectionListPageSearchEvent = "collection-list-page-search";
34773
+ const CloudConfigNavComponentClickedEvent = "cloud-config-nav-component-clicked";
34774
+ const SlashCommandOpenedEvent = "slash-command-opened";
34775
+ const SlashCommandUsedEvent = "slash-command-used";
34776
+ let posthogClient = null;
34777
+ let isInitialized = false;
34778
+ let initializationPromise = null;
34779
+ const POSTHOG_CONFIG_ENDPOINT = "https://identity-v2.tinajs.io/v2/posthog-token";
34780
+ async function fetchPostHogConfig() {
34781
+ try {
34782
+ const response = await fetch(POSTHOG_CONFIG_ENDPOINT, {
34783
+ method: "GET",
34784
+ headers: {
34785
+ "Content-Type": "application/json"
34786
+ }
34787
+ });
34788
+ if (!response.ok) {
34789
+ console.warn(`Failed to fetch PostHog config: ${response.statusText}`);
34790
+ return {};
34791
+ }
34792
+ return await response.json();
34793
+ } catch (error2) {
34794
+ console.warn(
34795
+ "Failed to fetch PostHog config:",
34796
+ error2 instanceof Error ? error2.message : "Unknown error"
34797
+ );
34798
+ return {};
34799
+ }
34800
+ }
34801
+ async function initializePostHog(mode = "anonymous") {
34802
+ if (isInitialized) {
34803
+ return posthogClient;
34804
+ }
34805
+ if (initializationPromise) {
34806
+ return initializationPromise;
34807
+ }
34808
+ if (mode === "disabled") {
34809
+ isInitialized = true;
34810
+ return null;
34811
+ }
34812
+ if (process.env.TINA_DEV === "true") {
34813
+ isInitialized = true;
34814
+ return null;
34815
+ }
34816
+ initializationPromise = (async () => {
34817
+ const config = await fetchPostHogConfig();
34818
+ if (!config.api_key) {
34819
+ console.warn(
34820
+ "PostHog API key not found. PostHog tracking will be disabled."
34821
+ );
34822
+ isInitialized = true;
34823
+ return null;
34824
+ }
34825
+ posthog.init(config.api_key, {
34826
+ api_host: config.host || "https://us.i.posthog.com",
34827
+ persistence: "localStorage",
34828
+ autocapture: false,
34829
+ capture_pageview: false,
34830
+ disable_session_recording: true,
34831
+ disable_compression: true
34832
+ });
34833
+ posthogClient = posthog;
34834
+ isInitialized = true;
34835
+ return posthogClient;
34836
+ })();
34837
+ return initializationPromise;
34838
+ }
34839
+ function captureEvent(event, properties2) {
34840
+ if (!posthogClient) {
34841
+ return;
34842
+ }
34843
+ try {
34844
+ posthogClient.capture(event, {
34845
+ ...properties2,
34846
+ system: "tinacms/tinacms"
34847
+ });
34848
+ } catch (error2) {
34849
+ console.error("Error capturing PostHog event:", error2);
34850
+ }
34851
+ }
34727
34852
  var useComboboxInput = ({
34728
34853
  autoFocus = true,
34729
34854
  cancelInputOnArrowLeftRight = true,
@@ -34873,7 +34998,7 @@ const InlineCombobox = ({
34873
34998
  },
34874
34999
  [setValueProp, hasValueProp]
34875
35000
  );
34876
- const [insertPoint, setInsertPoint] = useState(null);
35001
+ const insertPoint = React__default.useRef(null);
34877
35002
  useEffect(() => {
34878
35003
  const path3 = editor.api.findPath(element);
34879
35004
  if (!path3)
@@ -34882,7 +35007,7 @@ const InlineCombobox = ({
34882
35007
  if (!point3)
34883
35008
  return;
34884
35009
  const pointRef3 = editor.api.pointRef(point3);
34885
- setInsertPoint(pointRef3);
35010
+ insertPoint.current = pointRef3.current;
34886
35011
  return () => {
34887
35012
  pointRef3.unref();
34888
35013
  };
@@ -34896,12 +35021,6 @@ const InlineCombobox = ({
34896
35021
  at: (insertPoint == null ? void 0 : insertPoint.current) ?? void 0
34897
35022
  });
34898
35023
  }
34899
- if (cause === "arrowLeft" || cause === "arrowRight") {
34900
- editor.tf.move({
34901
- distance: 1,
34902
- reverse: cause === "arrowLeft"
34903
- });
34904
- }
34905
35024
  },
34906
35025
  ref: inputRef
34907
35026
  });
@@ -35099,6 +35218,9 @@ const rules = [
35099
35218
  const SlashInputElement = withRef$1(
35100
35219
  ({ className, ...props }, ref) => {
35101
35220
  const { children, editor, element } = props;
35221
+ useEffect(() => {
35222
+ captureEvent(SlashCommandOpenedEvent);
35223
+ }, []);
35102
35224
  return /* @__PURE__ */ React__default.createElement(
35103
35225
  PlateElement,
35104
35226
  {
@@ -35112,7 +35234,12 @@ const SlashInputElement = withRef$1(
35112
35234
  {
35113
35235
  key: value,
35114
35236
  keywords: keywords2,
35115
- onClick: () => onSelect(editor),
35237
+ onClick: () => {
35238
+ onSelect(editor);
35239
+ captureEvent(SlashCommandUsedEvent, {
35240
+ command: value
35241
+ });
35242
+ },
35116
35243
  value
35117
35244
  },
35118
35245
  /* @__PURE__ */ React__default.createElement(Icon, { "aria-hidden": true, className: "mr-2 size-4" }),
@@ -37561,6 +37688,30 @@ const uuid = () => {
37561
37688
  };
37562
37689
  const blockClasses = "mt-0.5";
37563
37690
  const headerClasses = "font-normal";
37691
+ function getContrastColor(color) {
37692
+ const parsed = colorString.get.rgb(color);
37693
+ if (!parsed)
37694
+ return "#000000";
37695
+ const [r2, g, b] = parsed;
37696
+ const luminance = (0.299 * r2 + 0.587 * g + 0.114 * b) / 255;
37697
+ return luminance > 0.5 ? "#000000" : "#ffffff";
37698
+ }
37699
+ const HighlightLeaf = ({
37700
+ leaf: leaf3,
37701
+ ...props
37702
+ }) => {
37703
+ const backgroundColor = leaf3.highlightColor || "#FEF08A";
37704
+ return /* @__PURE__ */ React__default.createElement(
37705
+ PlateLeaf,
37706
+ {
37707
+ as: "mark",
37708
+ className: "rounded-sm",
37709
+ style: { backgroundColor, color: getContrastColor(backgroundColor) },
37710
+ leaf: leaf3,
37711
+ ...props
37712
+ }
37713
+ );
37714
+ };
37564
37715
  const Components = () => {
37565
37716
  return {
37566
37717
  [SlashInputPlugin.key]: SlashInputElement,
@@ -37719,6 +37870,7 @@ const Components = () => {
37719
37870
  [ListItemPlugin.key]: withProps(PlateElement, { as: "li" }),
37720
37871
  [LinkPlugin.key]: LinkElement,
37721
37872
  [CodePlugin.key]: CodeLeaf,
37873
+ [HighlightPlugin.key]: HighlightLeaf,
37722
37874
  [UnderlinePlugin.key]: withProps(PlateLeaf, { as: "u" }),
37723
37875
  [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: "s" }),
37724
37876
  [ItalicPlugin.key]: withProps(PlateLeaf, { as: "em" }),
@@ -39939,6 +40091,9 @@ function MdAccessTime(props) {
39939
40091
  function MdKeyboardArrowDown(props) {
39940
40092
  return GenIcon({ "tag": "svg", "attr": { "viewBox": "0 0 24 24" }, "child": [{ "tag": "path", "attr": { "fill": "none", "d": "M0 0h24v24H0V0z" }, "child": [] }, { "tag": "path", "attr": { "d": "M7.41 8.59 12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" }, "child": [] }] })(props);
39941
40093
  }
40094
+ function MdImage(props) {
40095
+ return GenIcon({ "tag": "svg", "attr": { "viewBox": "0 0 24 24" }, "child": [{ "tag": "path", "attr": { "fill": "none", "d": "M0 0h24v24H0z" }, "child": [] }, { "tag": "path", "attr": { "d": "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }, "child": [] }] })(props);
40096
+ }
39942
40097
  function MdArrowForward(props) {
39943
40098
  return GenIcon({ "tag": "svg", "attr": { "viewBox": "0 0 24 24" }, "child": [{ "tag": "path", "attr": { "fill": "none", "d": "M0 0h24v24H0z" }, "child": [] }, { "tag": "path", "attr": { "d": "m12 4-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z" }, "child": [] }] })(props);
39944
40099
  }
@@ -41458,7 +41613,15 @@ const Blocks = ({
41458
41613
  meta,
41459
41614
  index
41460
41615
  }) => {
41461
- const addItem = React.useCallback(
41616
+ const [newObjects, setNewObjects] = useState(/* @__PURE__ */ new Set());
41617
+ const prevPristine = useRef(meta.pristine);
41618
+ useEffect(() => {
41619
+ if (!prevPristine.current && meta.pristine) {
41620
+ setNewObjects(/* @__PURE__ */ new Set());
41621
+ }
41622
+ prevPristine.current = meta.pristine;
41623
+ }, [meta.pristine]);
41624
+ const addItem = useCallback(
41462
41625
  (name2, template) => {
41463
41626
  let obj = {};
41464
41627
  if (typeof template.defaultItem === "function") {
@@ -41467,6 +41630,7 @@ const Blocks = ({
41467
41630
  obj = template.defaultItem || {};
41468
41631
  }
41469
41632
  obj._template = name2;
41633
+ setNewObjects((prev) => new Set(prev).add(obj));
41470
41634
  form.mutators.push(field.name, obj);
41471
41635
  },
41472
41636
  [field.name, form.mutators]
@@ -41529,6 +41693,7 @@ const Blocks = ({
41529
41693
  tinaForm,
41530
41694
  isMin,
41531
41695
  fixedLength,
41696
+ isNew: newObjects.has(block2),
41532
41697
  ...itemProps(block2)
41533
41698
  }
41534
41699
  );
@@ -41543,7 +41708,8 @@ const BlockListItem = ({
41543
41708
  index,
41544
41709
  template,
41545
41710
  isMin,
41546
- fixedLength
41711
+ fixedLength,
41712
+ isNew
41547
41713
  }) => {
41548
41714
  const cms = useCMS$1();
41549
41715
  const removeItem = React.useCallback(() => {
@@ -41587,6 +41753,7 @@ const BlockListItem = ({
41587
41753
  onMouseOut: () => setHoveredField({ id: null, fieldName: null })
41588
41754
  },
41589
41755
  /* @__PURE__ */ React.createElement(GroupLabel, null, label || template.label),
41756
+ isNew && /* @__PURE__ */ React.createElement("span", { className: "mr-1.5 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] border-[0.5px] border-tina-orange/50 font-semibold bg-tina-orange/10 text-tina-orange leading-none" }, "NEW"),
41590
41757
  /* @__PURE__ */ React.createElement(BiPencil, { className: "h-5 w-auto fill-current text-gray-200 group-hover:text-inherit transition-colors duration-150 ease-out" })
41591
41758
  ), (!fixedLength || fixedLength && !isMin) && /* @__PURE__ */ React.createElement(ItemDeleteButton, { disabled: isMin, onClick: removeItem }))));
41592
41759
  };
@@ -43958,99 +44125,6 @@ const TableCaption = React.forwardRef(({ className, ...props }, ref) => /* @__PU
43958
44125
  }
43959
44126
  ));
43960
44127
  TableCaption.displayName = "TableCaption";
43961
- let posthogClient = null;
43962
- let isInitialized = false;
43963
- let initializationPromise = null;
43964
- const POSTHOG_CONFIG_ENDPOINT = "https://identity-v2.tinajs.io/v2/posthog-token";
43965
- async function fetchPostHogConfig() {
43966
- try {
43967
- const response = await fetch(POSTHOG_CONFIG_ENDPOINT, {
43968
- method: "GET",
43969
- headers: {
43970
- "Content-Type": "application/json"
43971
- }
43972
- });
43973
- if (!response.ok) {
43974
- console.warn(`Failed to fetch PostHog config: ${response.statusText}`);
43975
- return {};
43976
- }
43977
- return await response.json();
43978
- } catch (error2) {
43979
- console.warn(
43980
- "Failed to fetch PostHog config:",
43981
- error2 instanceof Error ? error2.message : "Unknown error"
43982
- );
43983
- return {};
43984
- }
43985
- }
43986
- async function initializePostHog(mode = "anonymous") {
43987
- if (isInitialized) {
43988
- return posthogClient;
43989
- }
43990
- if (initializationPromise) {
43991
- return initializationPromise;
43992
- }
43993
- if (mode === "disabled") {
43994
- isInitialized = true;
43995
- return null;
43996
- }
43997
- if (process.env.TINA_DEV === "true") {
43998
- isInitialized = true;
43999
- return null;
44000
- }
44001
- initializationPromise = (async () => {
44002
- const config = await fetchPostHogConfig();
44003
- if (!config.api_key) {
44004
- console.warn(
44005
- "PostHog API key not found. PostHog tracking will be disabled."
44006
- );
44007
- isInitialized = true;
44008
- return null;
44009
- }
44010
- posthog.init(config.api_key, {
44011
- api_host: config.host || "https://us.i.posthog.com",
44012
- persistence: "localStorage",
44013
- autocapture: false,
44014
- capture_pageview: false,
44015
- disable_session_recording: true,
44016
- disable_compression: true
44017
- });
44018
- posthogClient = posthog;
44019
- isInitialized = true;
44020
- return posthogClient;
44021
- })();
44022
- return initializationPromise;
44023
- }
44024
- function captureEvent(event, properties2) {
44025
- if (!posthogClient) {
44026
- return;
44027
- }
44028
- try {
44029
- posthogClient.capture(event, {
44030
- ...properties2,
44031
- system: "tinacms/tinacms"
44032
- });
44033
- } catch (error2) {
44034
- console.error("Error capturing PostHog event:", error2);
44035
- }
44036
- }
44037
- const BranchSwitchedEvent = "branch-switched";
44038
- const BranchSwitcherOpenedEvent = "branch-switcher-opened";
44039
- const BranchSwitcherSearchEvent = "branch-switcher-search";
44040
- const BranchSwitcherDropDownEvent = "branch-switcher-dropdown";
44041
- const BranchSwitcherPRClickedEvent = "branch-switcher-pr-clicked";
44042
- const SavedContentEvent = "saved-content";
44043
- const SaveContentErrorEvent = "save-content-error";
44044
- const FormResetEvent = "form-reset";
44045
- const MediaManagerContentUploadedEvent = "media-manager-content-uploaded";
44046
- const MediaManagerContentDeletedEvent = "media-manager-content-deleted";
44047
- const RichTextEditorSwitchedEvent = "rich-text-editor-switched";
44048
- const EventLogPageViewedEvent = "event-log-page-viewed";
44049
- const TinaCMSStartedEvent = "tina-cms-started";
44050
- const CollectionListPageItemClickedEvent = "collection-list-page-item-clicked";
44051
- const CollectionListPageSortEvent = "collection-list-page-sort";
44052
- const CollectionListPageSearchEvent = "collection-list-page-search";
44053
- const CloudConfigNavComponentClickedEvent = "cloud-config-nav-component-clicked";
44054
44128
  const IndexStatus$1 = ({ indexingStatus }) => {
44055
44129
  const styles = {
44056
44130
  complete: {
@@ -46796,6 +46870,882 @@ const PasswordScreenPlugin = createScreen({
46796
46870
  layout: "fullscreen",
46797
46871
  navCategory: "Account"
46798
46872
  });
46873
+ const DashboardLoadingState = ({
46874
+ message,
46875
+ progress
46876
+ }) => /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center justify-center p-12" }, /* @__PURE__ */ React__default.createElement("div", { className: "text-center text-gray-500" }, /* @__PURE__ */ React__default.createElement("h3", { className: "text-lg font-semibold text-gray-700 animate-pulse" }, "Loading Dashboard"), /* @__PURE__ */ React__default.createElement("p", { className: "mt-2 text-sm" }, message), progress ? /* @__PURE__ */ React__default.createElement("div", { className: "mt-4 w-72 max-w-full mx-auto" }, /* @__PURE__ */ React__default.createElement("div", { className: "h-2 rounded-full bg-gray-200 overflow-hidden" }, /* @__PURE__ */ React__default.createElement(
46877
+ "div",
46878
+ {
46879
+ className: "h-full bg-tina-orange transition-[width] duration-300 ease-out",
46880
+ style: {
46881
+ width: `${Math.max(0, Math.min(100, progress.value))}%`
46882
+ }
46883
+ }
46884
+ )), /* @__PURE__ */ React__default.createElement("div", { className: "mt-2 flex items-center justify-between text-xs text-gray-500" }, /* @__PURE__ */ React__default.createElement("span", null, progress.label || "In progress"), /* @__PURE__ */ React__default.createElement("span", null, Math.round(progress.value), "%"))) : /* @__PURE__ */ React__default.createElement("div", { className: "mt-4 flex justify-center" }, /* @__PURE__ */ React__default.createElement(LoadingDots, { color: "var(--tina-color-primary)" }))));
46885
+ const DashboardErrorState = ({ message }) => /* @__PURE__ */ React__default.createElement("div", { className: "p-8 text-red-500 bg-red-50 rounded-lg m-8" }, /* @__PURE__ */ React__default.createElement("h3", { className: "text-lg font-bold mb-2 flex items-center gap-2" }, /* @__PURE__ */ React__default.createElement(AlertCircle, { className: "text-xl" }), " Dashboard Error"), /* @__PURE__ */ React__default.createElement("p", null, message));
46886
+ const DashboardTitleBar = ({
46887
+ title,
46888
+ icon,
46889
+ controls
46890
+ }) => /* @__PURE__ */ React__default.createElement("div", { className: "mb-6 flex items-center justify-between gap-4" }, /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ React__default.createElement("span", { className: "text-3xl text-tina-orange" }, icon), /* @__PURE__ */ React__default.createElement("h2", { className: "text-2xl font-bold text-gray-800" }, title)), controls);
46891
+ const MEDIA_USAGE_THUMBNAIL_SIZE = { w: 75, h: 75 };
46892
+ const MEDIA_USAGE_THUMBNAIL_KEY = `${MEDIA_USAGE_THUMBNAIL_SIZE.w}x${MEDIA_USAGE_THUMBNAIL_SIZE.h}`;
46893
+ const getDocumentEditUrl = async (cms, document2) => {
46894
+ var _a2;
46895
+ const collectionName = document2._sys.collection.name;
46896
+ const breadcrumbsDerivedPath = document2._sys.breadcrumbs.join("/");
46897
+ let editUrl = `#/collections/edit/${collectionName}/~/${breadcrumbsDerivedPath}`;
46898
+ try {
46899
+ const collection = cms.api.tina.schema.getCollection(collectionName);
46900
+ if ((_a2 = collection == null ? void 0 : collection.ui) == null ? void 0 : _a2.router) {
46901
+ let customPath = await collection.ui.router({
46902
+ document: document2,
46903
+ collection
46904
+ });
46905
+ if (customPath) {
46906
+ customPath = customPath.startsWith("/") ? customPath.slice(1) : customPath;
46907
+ const tinaBasePath = cms.flags.get("tina-basepath");
46908
+ const tinaPreviewIsSet = cms.flags.get("tina-preview");
46909
+ if (tinaPreviewIsSet) {
46910
+ if (tinaBasePath) {
46911
+ editUrl = `#/~/${tinaBasePath}/${customPath}`;
46912
+ } else {
46913
+ editUrl = `#/~/${customPath}`;
46914
+ }
46915
+ }
46916
+ }
46917
+ }
46918
+ } catch (e3) {
46919
+ console.error(
46920
+ "Unable to determine custom edit URL for document. Falling back to default edit URL.",
46921
+ e3
46922
+ );
46923
+ }
46924
+ return editUrl;
46925
+ };
46926
+ const scanDocumentForMedia = (documentContent, mediaUsages) => {
46927
+ const matchedSrcs = /* @__PURE__ */ new Set();
46928
+ for (const mediaUsage of mediaUsages) {
46929
+ const src = JSON.stringify(mediaUsage.media.src).slice(1, -1);
46930
+ let index = documentContent.indexOf(src);
46931
+ while (index !== -1) {
46932
+ const prevChar = documentContent[index - 1];
46933
+ const nextChar = documentContent[index + src.length];
46934
+ const isPrevValid = !prevChar || !/[a-zA-Z0-9_.%~+-]/.test(prevChar);
46935
+ const isNextValid = !nextChar || !/[a-zA-Z0-9_.%~+-]/.test(nextChar);
46936
+ if (isPrevValid && isNextValid) {
46937
+ matchedSrcs.add(mediaUsage.media.src);
46938
+ break;
46939
+ }
46940
+ index = documentContent.indexOf(src, index + 1);
46941
+ }
46942
+ }
46943
+ return matchedSrcs;
46944
+ };
46945
+ const THUMBNAIL_SIZES = [MEDIA_USAGE_THUMBNAIL_SIZE];
46946
+ const BATCH_SIZE = 100;
46947
+ const UI_YIELD_INTERVAL = 50;
46948
+ const collectAllMedia = async (cms, mediaSrcToUsageMap, currentDirectory) => {
46949
+ const subDirectories = [];
46950
+ let currentOffset = void 0;
46951
+ let moreOffsetsAvailable = true;
46952
+ while (moreOffsetsAvailable) {
46953
+ const list = await cms.media.list({
46954
+ directory: currentDirectory,
46955
+ offset: currentOffset,
46956
+ thumbnailSizes: THUMBNAIL_SIZES
46957
+ });
46958
+ for (const item of list.items) {
46959
+ if (item.type === "file") {
46960
+ if (item.src) {
46961
+ const isImg = isImage(item.filename);
46962
+ const isVid = isVideo(item.filename);
46963
+ const type2 = isImg ? "image" : isVid ? "video" : "other";
46964
+ mediaSrcToUsageMap[item.src] = {
46965
+ media: item,
46966
+ type: type2,
46967
+ usedIn: []
46968
+ };
46969
+ }
46970
+ } else if (item.type === "dir") {
46971
+ const directorySegment = item.filename.replace(/^\/+|\/+$/g, "");
46972
+ subDirectories.push(
46973
+ currentDirectory ? `${currentDirectory}/${directorySegment}` : directorySegment
46974
+ );
46975
+ }
46976
+ }
46977
+ currentOffset = list.nextOffset;
46978
+ if (!currentOffset) {
46979
+ moreOffsetsAvailable = false;
46980
+ }
46981
+ }
46982
+ if (subDirectories.length > 0) {
46983
+ await Promise.all(
46984
+ subDirectories.map((dir) => collectAllMedia(cms, mediaSrcToUsageMap, dir))
46985
+ );
46986
+ }
46987
+ };
46988
+ const scanCollectionForMediaUsage = async (cms, collectionMeta, mediaSrcToUsageMap) => {
46989
+ var _a2, _b;
46990
+ let hasNextPage = true;
46991
+ let after3 = void 0;
46992
+ const mediaUsages = Object.values(mediaSrcToUsageMap);
46993
+ const listQuery = `
46994
+ query($after: String, $first: Float) {
46995
+ collectionConnection: ${collectionMeta.name}Connection(first: $first, after: $after) {
46996
+ pageInfo { hasNextPage, endCursor }
46997
+ edges {
46998
+ node {
46999
+ ... on Document {
47000
+ _sys {
47001
+ relativePath
47002
+ filename
47003
+ basename
47004
+ path
47005
+ breadcrumbs
47006
+ extension
47007
+ template
47008
+ title
47009
+ hasReferences
47010
+ collection {
47011
+ name
47012
+ label
47013
+ path
47014
+ format
47015
+ }
47016
+ }
47017
+ _values
47018
+ }
47019
+ }
47020
+ }
47021
+ }
47022
+ }
47023
+ `;
47024
+ while (hasNextPage) {
47025
+ try {
47026
+ const res = await cms.api.tina.request(listQuery, {
47027
+ variables: { after: after3, first: BATCH_SIZE }
47028
+ });
47029
+ const connection = res.collectionConnection;
47030
+ const edges2 = (connection == null ? void 0 : connection.edges) || [];
47031
+ hasNextPage = ((_a2 = connection == null ? void 0 : connection.pageInfo) == null ? void 0 : _a2.hasNextPage) || false;
47032
+ after3 = (_b = connection == null ? void 0 : connection.pageInfo) == null ? void 0 : _b.endCursor;
47033
+ for (let i2 = 0; i2 < edges2.length; i2++) {
47034
+ if (i2 > 0 && i2 % UI_YIELD_INTERVAL === 0) {
47035
+ await new Promise((resolve) => setTimeout(resolve, 0));
47036
+ }
47037
+ const edge = edges2[i2];
47038
+ const matchedSrcs = scanDocumentForMedia(
47039
+ JSON.stringify(edge.node._values),
47040
+ mediaUsages
47041
+ );
47042
+ if (matchedSrcs.size === 0)
47043
+ continue;
47044
+ const editUrl = await getDocumentEditUrl(cms, edge.node);
47045
+ matchedSrcs.forEach((mediaSrc) => {
47046
+ const usage = mediaSrcToUsageMap[mediaSrc];
47047
+ if (usage) {
47048
+ usage.usedIn.push({
47049
+ collectionName: collectionMeta.name,
47050
+ breadcrumbs: edge.node._sys.breadcrumbs,
47051
+ collectionLabel: collectionMeta.label || collectionMeta.name,
47052
+ // label is optional in the schema
47053
+ editUrl
47054
+ });
47055
+ }
47056
+ });
47057
+ }
47058
+ } catch (e3) {
47059
+ console.error(`Error processing collection ${collectionMeta.name}:`, e3);
47060
+ throw e3;
47061
+ }
47062
+ }
47063
+ };
47064
+ const scanAllMedia = async (cms, onProgress) => {
47065
+ const mediaSrcToUsageMap = {};
47066
+ await collectAllMedia(cms, mediaSrcToUsageMap, "");
47067
+ const collectionMetas = cms.api.tina.schema.getCollections();
47068
+ for (let i2 = 0; i2 < collectionMetas.length; i2++) {
47069
+ await scanCollectionForMediaUsage(
47070
+ cms,
47071
+ collectionMetas[i2],
47072
+ mediaSrcToUsageMap
47073
+ );
47074
+ onProgress == null ? void 0 : onProgress((i2 + 1) / collectionMetas.length * 100);
47075
+ }
47076
+ return Object.values(mediaSrcToUsageMap);
47077
+ };
47078
+ const useMediaUsageScanner = () => {
47079
+ const cms = useCMS$1();
47080
+ const [mediaItems, setMediaItems] = useState([]);
47081
+ const [isLoading, setIsLoading] = useState(true);
47082
+ const [errorOccurred, setErrorOccurred] = useState(false);
47083
+ const [progress, setProgress] = useState(0);
47084
+ const activeRef = useRef(true);
47085
+ const scanMedia = useCallback(async () => {
47086
+ setIsLoading(true);
47087
+ setErrorOccurred(false);
47088
+ setProgress(0);
47089
+ try {
47090
+ const updatedMediaItems = await scanAllMedia(cms, setProgress);
47091
+ if (activeRef.current)
47092
+ setMediaItems(updatedMediaItems);
47093
+ } catch (e3) {
47094
+ console.error("Error scanning media usage:", e3);
47095
+ if (activeRef.current)
47096
+ setErrorOccurred(true);
47097
+ } finally {
47098
+ if (activeRef.current)
47099
+ setIsLoading(false);
47100
+ }
47101
+ }, [cms]);
47102
+ useEffect(() => {
47103
+ activeRef.current = true;
47104
+ scanMedia().catch((e3) => {
47105
+ console.error("Unhandled rejection from scanMedia:", e3);
47106
+ });
47107
+ return () => {
47108
+ activeRef.current = false;
47109
+ };
47110
+ }, [scanMedia]);
47111
+ return {
47112
+ mediaItems,
47113
+ isLoading,
47114
+ errorOccurred,
47115
+ progress,
47116
+ refresh: scanMedia
47117
+ };
47118
+ };
47119
+ function Dialog({
47120
+ ...props
47121
+ }) {
47122
+ return /* @__PURE__ */ React.createElement(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
47123
+ }
47124
+ function DialogPortal({
47125
+ ...props
47126
+ }) {
47127
+ return /* @__PURE__ */ React.createElement(DialogPrimitive.Portal, { "data-slot": "dialog-portal", ...props });
47128
+ }
47129
+ const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ React.createElement(
47130
+ DialogPrimitive.Overlay,
47131
+ {
47132
+ ref,
47133
+ "data-slot": "dialog-overlay",
47134
+ className: cn(
47135
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-[100000]",
47136
+ className
47137
+ ),
47138
+ ...props
47139
+ }
47140
+ ));
47141
+ function DialogContent({
47142
+ className,
47143
+ children,
47144
+ showCloseButton = true,
47145
+ ...props
47146
+ }) {
47147
+ return /* @__PURE__ */ React.createElement(DialogPortal, null, /* @__PURE__ */ React.createElement(DialogOverlay, null), /* @__PURE__ */ React.createElement(
47148
+ DialogPrimitive.Content,
47149
+ {
47150
+ "data-slot": "dialog-content",
47151
+ className: cn(
47152
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-[100000] w-full -translate-x-1/2 -translate-y-1/2 outline-none",
47153
+ className
47154
+ ),
47155
+ ...props
47156
+ },
47157
+ children,
47158
+ showCloseButton && /* @__PURE__ */ React.createElement(DialogPrimitive.Close, { "data-slot": "dialog-close", asChild: true }, /* @__PURE__ */ React.createElement(
47159
+ Button,
47160
+ {
47161
+ variant: "ghost",
47162
+ size: "icon",
47163
+ className: "absolute top-2 right-2 h-8 w-8"
47164
+ },
47165
+ /* @__PURE__ */ React.createElement(XIcon, null),
47166
+ /* @__PURE__ */ React.createElement("span", { className: "sr-only" }, "Close")
47167
+ ))
47168
+ ));
47169
+ }
47170
+ function DialogTitle({
47171
+ className,
47172
+ ...props
47173
+ }) {
47174
+ return /* @__PURE__ */ React.createElement(
47175
+ DialogPrimitive.Title,
47176
+ {
47177
+ "data-slot": "dialog-title",
47178
+ className: cn("text-base leading-none font-medium", className),
47179
+ ...props
47180
+ }
47181
+ );
47182
+ }
47183
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
47184
+ function DialogDescription({
47185
+ className,
47186
+ ...props
47187
+ }) {
47188
+ return /* @__PURE__ */ React.createElement(
47189
+ DialogPrimitive.Description,
47190
+ {
47191
+ "data-slot": "dialog-description",
47192
+ className: cn(
47193
+ "text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3",
47194
+ className
47195
+ ),
47196
+ ...props
47197
+ }
47198
+ );
47199
+ }
47200
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
47201
+ const MediaLightbox = ({
47202
+ item,
47203
+ onClose
47204
+ }) => {
47205
+ if (!item)
47206
+ return null;
47207
+ const usageCount = item.usedIn.length;
47208
+ const mediaSrc = item.media.src;
47209
+ const directory = item.media.directory || "/";
47210
+ return /* @__PURE__ */ React__default.createElement(Dialog, { open: true, onOpenChange: (isOpen) => !isOpen && onClose() }, /* @__PURE__ */ React__default.createElement(DialogContent, { className: "w-auto max-w-[95vw] border border-gray-200 bg-white px-4 pt-12 pb-4 shadow-xl sm:max-w-fit" }, /* @__PURE__ */ React__default.createElement(DialogTitle, { className: "sr-only" }, "Preview: ", item.media.filename), /* @__PURE__ */ React__default.createElement(DialogDescription, { className: "sr-only" }, usageCount > 0 ? `Used in ${usageCount} ${usageCount === 1 ? "document" : "documents"}` : "Unused media file"), /* @__PURE__ */ React__default.createElement("div", { className: "mx-auto w-fit max-w-full rounded-lg border border-gray-200 bg-gray-50 p-2 sm:p-3" }, item.type === "video" ? /* @__PURE__ */ React__default.createElement(
47211
+ VideoLightboxContent,
47212
+ {
47213
+ src: mediaSrc,
47214
+ filename: item.media.filename
47215
+ }
47216
+ ) : /* @__PURE__ */ React__default.createElement(
47217
+ ImageLightboxContent,
47218
+ {
47219
+ src: mediaSrc,
47220
+ filename: item.media.filename
47221
+ }
47222
+ )), /* @__PURE__ */ React__default.createElement("div", { className: "mt-3 space-y-1 text-center" }, /* @__PURE__ */ React__default.createElement("div", { className: "text-sm font-medium text-gray-700" }, /* @__PURE__ */ React__default.createElement("span", { className: "break-all" }, item.media.filename)), /* @__PURE__ */ React__default.createElement("div", { className: "text-xs text-gray-500" }, "Directory:", " ", /* @__PURE__ */ React__default.createElement("span", { className: "break-all font-medium" }, directory)), /* @__PURE__ */ React__default.createElement("div", { className: "text-sm font-medium text-gray-700" }, usageCount > 0 ? /* @__PURE__ */ React__default.createElement("span", { className: "text-tina-orange font-semibold" }, "Used in ", usageCount, " ", usageCount === 1 ? "doc" : "docs") : /* @__PURE__ */ React__default.createElement("span", { className: "font-semibold text-gray-500" }, "Unused")))));
47223
+ };
47224
+ const ImageLightboxContent = ({
47225
+ src,
47226
+ filename
47227
+ }) => /* @__PURE__ */ React__default.createElement(
47228
+ "img",
47229
+ {
47230
+ src,
47231
+ alt: filename,
47232
+ className: "mx-auto block max-w-full max-h-[75vh] object-contain rounded-md shadow-sm"
47233
+ }
47234
+ );
47235
+ const VideoLightboxContent = ({
47236
+ src,
47237
+ filename
47238
+ }) => {
47239
+ const [playbackFailed, setPlaybackFailed] = React__default.useState(false);
47240
+ React__default.useEffect(() => {
47241
+ setPlaybackFailed(false);
47242
+ return () => {
47243
+ setPlaybackFailed(false);
47244
+ };
47245
+ }, [src]);
47246
+ if (playbackFailed) {
47247
+ return /* @__PURE__ */ React__default.createElement("div", { className: "flex min-h-[12rem] min-w-[16rem] flex-col items-center justify-center gap-3 rounded-md border border-dashed border-gray-300 bg-white px-6 py-8 text-center" }, /* @__PURE__ */ React__default.createElement(BiMovie, { className: "h-10 w-10 text-gray-400" }), /* @__PURE__ */ React__default.createElement("div", { className: "text-sm font-medium text-gray-700" }, filename), /* @__PURE__ */ React__default.createElement("div", { className: "max-w-sm text-sm text-gray-500" }, "This video format is recognized, but this browser could not preview it."));
47248
+ }
47249
+ return /* @__PURE__ */ React__default.createElement(
47250
+ "video",
47251
+ {
47252
+ src,
47253
+ controls: true,
47254
+ playsInline: true,
47255
+ preload: "metadata",
47256
+ className: "mx-auto block h-auto w-[min(80vw,720px)] max-w-full max-h-[75vh] rounded-md bg-black shadow-sm",
47257
+ onError: () => setPlaybackFailed(true)
47258
+ }
47259
+ );
47260
+ };
47261
+ const INFINITE_SCROLL_PAGE_SIZE = 10;
47262
+ const DEFAULT_COLUMN_FILTERS = [
47263
+ { id: "type", value: "all" },
47264
+ { id: "usage", value: "all" }
47265
+ ];
47266
+ const getUsageCount = (item) => item.usedIn.length;
47267
+ const SortIcon = ({ sorted }) => {
47268
+ if (sorted === "asc")
47269
+ return /* @__PURE__ */ React__default.createElement(ArrowUp, { className: "ml-2 h-4 w-4" });
47270
+ if (sorted === "desc")
47271
+ return /* @__PURE__ */ React__default.createElement(ArrowDown, { className: "ml-2 h-4 w-4" });
47272
+ return /* @__PURE__ */ React__default.createElement(ArrowUpDown, { className: "ml-2 h-4 w-4" });
47273
+ };
47274
+ const mediaTypeFilterFn = (row, _columnId, filterValue) => {
47275
+ if (filterValue === "all")
47276
+ return true;
47277
+ return row.original.type === filterValue;
47278
+ };
47279
+ const usageFilterFn = (row, _columnId, filterValue) => {
47280
+ if (filterValue === "all")
47281
+ return true;
47282
+ const count = getUsageCount(row.original);
47283
+ if (filterValue === "used")
47284
+ return count > 0;
47285
+ if (filterValue === "unused")
47286
+ return count === 0;
47287
+ return true;
47288
+ };
47289
+ const getMediaColumns = (onPreview) => [
47290
+ {
47291
+ // Hidden behavior-only column so TanStack can filter by media type.
47292
+ id: "type",
47293
+ accessorFn: (row) => row.type,
47294
+ filterFn: mediaTypeFilterFn,
47295
+ enableSorting: false,
47296
+ header: () => null,
47297
+ cell: () => null
47298
+ },
47299
+ {
47300
+ id: "preview",
47301
+ header: () => /* @__PURE__ */ React__default.createElement("span", { className: "font-medium text-muted-foreground" }, "Preview"),
47302
+ cell: ({ row }) => {
47303
+ var _a2;
47304
+ const media = row.original.media;
47305
+ const type2 = row.original.type;
47306
+ const usageCount = getUsageCount(row.original);
47307
+ const isExpanded = row.getIsExpanded();
47308
+ const thumbnailSrc = (_a2 = media.thumbnails) == null ? void 0 : _a2[MEDIA_USAGE_THUMBNAIL_KEY];
47309
+ return /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center gap-2" }, usageCount > 0 ? /* @__PURE__ */ React__default.createElement(
47310
+ "button",
47311
+ {
47312
+ type: "button",
47313
+ onClick: (event) => {
47314
+ event.stopPropagation();
47315
+ row.toggleExpanded();
47316
+ },
47317
+ className: `flex h-5 w-5 shrink-0 items-center justify-center rounded-full border transition-colors ${isExpanded ? "border-tina-orange bg-white text-tina-orange" : "border-tina-orange bg-tina-orange text-white"}`,
47318
+ title: "View where this file is used",
47319
+ "aria-label": `Toggle usage references for ${row.original.media.filename}`
47320
+ },
47321
+ /* @__PURE__ */ React__default.createElement(
47322
+ ChevronDown,
47323
+ {
47324
+ className: `h-3.5 w-3.5 transition-transform ${isExpanded ? "rotate-180" : ""}`
47325
+ }
47326
+ )
47327
+ ) : /* @__PURE__ */ React__default.createElement("span", { className: "block h-5 w-5 shrink-0", "aria-hidden": "true" }), /* @__PURE__ */ React__default.createElement("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded border border-gray-200 bg-gray-100 text-gray-400" }, type2 === "image" && thumbnailSrc ? /* @__PURE__ */ React__default.createElement(
47328
+ "img",
47329
+ {
47330
+ src: thumbnailSrc,
47331
+ alt: media.filename,
47332
+ loading: "lazy",
47333
+ decoding: "async",
47334
+ className: "h-full w-full cursor-pointer object-cover transition-opacity hover:opacity-80",
47335
+ onClick: (event) => {
47336
+ event.stopPropagation();
47337
+ onPreview == null ? void 0 : onPreview(row.original);
47338
+ }
47339
+ }
47340
+ ) : type2 === "video" ? /* @__PURE__ */ React__default.createElement(
47341
+ "button",
47342
+ {
47343
+ type: "button",
47344
+ title: "Preview video",
47345
+ className: "flex h-full w-full cursor-pointer items-center justify-center text-gray-400 transition-opacity hover:opacity-80",
47346
+ onClick: (event) => {
47347
+ event.stopPropagation();
47348
+ onPreview == null ? void 0 : onPreview(row.original);
47349
+ }
47350
+ },
47351
+ /* @__PURE__ */ React__default.createElement(BiMovie, { className: "text-3xl" })
47352
+ ) : /* @__PURE__ */ React__default.createElement(BiFile, { className: "text-3xl" })));
47353
+ },
47354
+ enableSorting: false
47355
+ },
47356
+ {
47357
+ id: "filename",
47358
+ accessorFn: (row) => row.media.filename,
47359
+ header: ({ column }) => {
47360
+ const sorted = column.getIsSorted();
47361
+ return /* @__PURE__ */ React__default.createElement(
47362
+ "button",
47363
+ {
47364
+ type: "button",
47365
+ className: "font-medium text-muted-foreground flex items-center gap-1",
47366
+ onClick: column.getToggleSortingHandler()
47367
+ },
47368
+ "Filename",
47369
+ /* @__PURE__ */ React__default.createElement(SortIcon, { sorted })
47370
+ );
47371
+ },
47372
+ cell: ({ row }) => /* @__PURE__ */ React__default.createElement(TooltipProvider$1, null, /* @__PURE__ */ React__default.createElement(Tooltip$1, { delayDuration: 300 }, /* @__PURE__ */ React__default.createElement(TooltipTrigger$1, { asChild: true }, /* @__PURE__ */ React__default.createElement("span", { className: "block w-full truncate font-medium text-gray-800" }, row.original.media.filename)), /* @__PURE__ */ React__default.createElement(TooltipPortal, null, /* @__PURE__ */ React__default.createElement(TooltipContent$1, { side: "top", className: "max-w-sm break-all shadow-md" }, row.original.media.filename))))
47373
+ },
47374
+ {
47375
+ id: "directory",
47376
+ accessorFn: (row) => row.media.directory || "/",
47377
+ header: ({ column }) => {
47378
+ const sorted = column.getIsSorted();
47379
+ return /* @__PURE__ */ React__default.createElement(
47380
+ "button",
47381
+ {
47382
+ type: "button",
47383
+ className: "font-medium text-muted-foreground flex items-center gap-1",
47384
+ onClick: column.getToggleSortingHandler()
47385
+ },
47386
+ "Directory",
47387
+ /* @__PURE__ */ React__default.createElement(SortIcon, { sorted })
47388
+ );
47389
+ },
47390
+ cell: ({ getValue: getValue2 }) => {
47391
+ const directory = getValue2();
47392
+ return /* @__PURE__ */ React__default.createElement(TooltipProvider$1, null, /* @__PURE__ */ React__default.createElement(Tooltip$1, { delayDuration: 300 }, /* @__PURE__ */ React__default.createElement(TooltipTrigger$1, { asChild: true }, /* @__PURE__ */ React__default.createElement("span", { className: "block w-full truncate text-sm text-gray-500" }, directory)), /* @__PURE__ */ React__default.createElement(TooltipPortal, null, /* @__PURE__ */ React__default.createElement(
47393
+ TooltipContent$1,
47394
+ {
47395
+ side: "top",
47396
+ className: "max-w-sm break-all shadow-md"
47397
+ },
47398
+ directory
47399
+ ))));
47400
+ }
47401
+ },
47402
+ {
47403
+ id: "usage",
47404
+ accessorFn: getUsageCount,
47405
+ header: ({ column }) => {
47406
+ const sorted = column.getIsSorted();
47407
+ return /* @__PURE__ */ React__default.createElement(
47408
+ "button",
47409
+ {
47410
+ type: "button",
47411
+ className: "font-medium text-muted-foreground ml-auto flex items-center gap-1",
47412
+ onClick: column.getToggleSortingHandler()
47413
+ },
47414
+ "Usage",
47415
+ /* @__PURE__ */ React__default.createElement(SortIcon, { sorted })
47416
+ );
47417
+ },
47418
+ cell: ({ row }) => {
47419
+ const count = getUsageCount(row.original);
47420
+ return /* @__PURE__ */ React__default.createElement(
47421
+ "span",
47422
+ {
47423
+ className: `inline-block min-w-[3rem] text-right ${count > 0 ? "font-medium text-gray-800" : "text-gray-500"}`
47424
+ },
47425
+ count
47426
+ );
47427
+ },
47428
+ filterFn: usageFilterFn
47429
+ }
47430
+ ];
47431
+ const MediaUsageTable = ({
47432
+ mediaItems,
47433
+ onClose,
47434
+ onPreview
47435
+ }) => {
47436
+ var _a2, _b;
47437
+ const [columnFilters, setColumnFilters] = useState(
47438
+ DEFAULT_COLUMN_FILTERS
47439
+ );
47440
+ const [sorting, setSorting] = useState([]);
47441
+ const [expanded, setExpanded] = useState({});
47442
+ const [visibleCount, setVisibleCount] = useState(INFINITE_SCROLL_PAGE_SIZE);
47443
+ const sentinelRef = useRef(null);
47444
+ const scrollContainerRef = useRef(null);
47445
+ const columns = useMemo(() => getMediaColumns(onPreview), [onPreview]);
47446
+ const typeFilter = ((_a2 = columnFilters.find((filter2) => filter2.id === "type")) == null ? void 0 : _a2.value) ?? "all";
47447
+ const usageFilter = ((_b = columnFilters.find((filter2) => filter2.id === "usage")) == null ? void 0 : _b.value) ?? "all";
47448
+ const table = useReactTable({
47449
+ data: mediaItems,
47450
+ columns,
47451
+ getRowId: (row) => row.media.src,
47452
+ initialState: { columnVisibility: { type: false } },
47453
+ state: { sorting, columnFilters, expanded },
47454
+ onColumnFiltersChange: setColumnFilters,
47455
+ onSortingChange: setSorting,
47456
+ onExpandedChange: setExpanded,
47457
+ getCoreRowModel: getCoreRowModel(),
47458
+ getSortedRowModel: getSortedRowModel(),
47459
+ getFilteredRowModel: getFilteredRowModel(),
47460
+ getExpandedRowModel: getExpandedRowModel(),
47461
+ getRowCanExpand: (row) => getUsageCount(row.original) > 0
47462
+ });
47463
+ const handleTypeFilterChange = (value) => {
47464
+ var _a3;
47465
+ setExpanded({});
47466
+ setVisibleCount(INFINITE_SCROLL_PAGE_SIZE);
47467
+ (_a3 = table.getColumn("type")) == null ? void 0 : _a3.setFilterValue(value);
47468
+ };
47469
+ const handleUsageFilterChange = (value) => {
47470
+ var _a3;
47471
+ setExpanded({});
47472
+ setVisibleCount(INFINITE_SCROLL_PAGE_SIZE);
47473
+ (_a3 = table.getColumn("usage")) == null ? void 0 : _a3.setFilterValue(value);
47474
+ };
47475
+ useEffect(() => {
47476
+ setVisibleCount(INFINITE_SCROLL_PAGE_SIZE);
47477
+ }, [sorting]);
47478
+ const allRows = table.getRowModel().rows;
47479
+ const visibleRows = allRows.slice(0, visibleCount);
47480
+ const hasMore = visibleCount < allRows.length;
47481
+ const loadMore = useCallback(() => {
47482
+ setVisibleCount((prev) => prev + INFINITE_SCROLL_PAGE_SIZE);
47483
+ }, []);
47484
+ useEffect(() => {
47485
+ const sentinel = sentinelRef.current;
47486
+ const scrollContainer = scrollContainerRef.current;
47487
+ if (!sentinel || !scrollContainer)
47488
+ return;
47489
+ const observer = new IntersectionObserver(
47490
+ (entries) => {
47491
+ var _a3;
47492
+ if ((_a3 = entries[0]) == null ? void 0 : _a3.isIntersecting) {
47493
+ loadMore();
47494
+ }
47495
+ },
47496
+ { root: scrollContainer, rootMargin: "200px" }
47497
+ );
47498
+ observer.observe(sentinel);
47499
+ return () => observer.disconnect();
47500
+ }, [loadMore, hasMore]);
47501
+ const filteredRowCount = table.getFilteredRowModel().rows.length;
47502
+ return /* @__PURE__ */ React__default.createElement("div", { className: "bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden" }, /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center gap-3 border-b border-gray-200 bg-gray-50/50 px-6 py-4" }, /* @__PURE__ */ React__default.createElement("h3", { className: "whitespace-nowrap text-lg font-semibold text-gray-800" }, "Media Inventory"), /* @__PURE__ */ React__default.createElement(
47503
+ MediaFilters,
47504
+ {
47505
+ filteredRowCount,
47506
+ typeFilter,
47507
+ usageFilter,
47508
+ setTypeFilter: handleTypeFilterChange,
47509
+ setUsageFilter: handleUsageFilterChange,
47510
+ className: "ml-auto"
47511
+ }
47512
+ )), /* @__PURE__ */ React__default.createElement("div", { ref: scrollContainerRef, className: "max-h-[45vh] overflow-auto" }, /* @__PURE__ */ React__default.createElement(
47513
+ Table,
47514
+ {
47515
+ className: "border-separate border-spacing-0",
47516
+ wrapperClassName: "overflow-visible"
47517
+ },
47518
+ /* @__PURE__ */ React__default.createElement(TableHeader, null, table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ React__default.createElement(TableRow, { key: headerGroup.id }, headerGroup.headers.map((header) => /* @__PURE__ */ React__default.createElement(
47519
+ TableHead,
47520
+ {
47521
+ key: header.id,
47522
+ className: `sticky top-0 z-20 border-b border-gray-200 bg-white ${header.column.id === "preview" ? "w-[4rem] pl-9" : header.column.id === "filename" ? "w-[20rem]" : header.column.id === "directory" ? "w-[14rem]" : header.column.id === "usage" ? "w-[5.5rem]" : ""}`
47523
+ },
47524
+ header.isPlaceholder ? null : flexRender(
47525
+ header.column.columnDef.header,
47526
+ header.getContext()
47527
+ )
47528
+ ))))),
47529
+ /* @__PURE__ */ React__default.createElement(TableBody, null, allRows.length === 0 ? /* @__PURE__ */ React__default.createElement(TableRow, null, /* @__PURE__ */ React__default.createElement(
47530
+ TableCell,
47531
+ {
47532
+ colSpan: columns.length,
47533
+ className: "!py-8 text-center text-gray-500"
47534
+ },
47535
+ mediaItems.length === 0 ? "No media files found." : "No media files match the current filters."
47536
+ )) : visibleRows.map((row) => /* @__PURE__ */ React__default.createElement(React__default.Fragment, { key: row.id }, /* @__PURE__ */ React__default.createElement(
47537
+ TableRow,
47538
+ {
47539
+ style: { contentVisibility: "auto" },
47540
+ onClick: getUsageCount(row.original) > 0 ? row.getToggleExpandedHandler() : void 0,
47541
+ className: getUsageCount(row.original) > 0 ? row.getIsExpanded() ? "cursor-pointer bg-[#FFF8F6] hover:bg-[#FFF8F6]" : "cursor-pointer" : ""
47542
+ },
47543
+ row.getVisibleCells().map((cell) => /* @__PURE__ */ React__default.createElement(
47544
+ TableCell,
47545
+ {
47546
+ key: cell.id,
47547
+ className: cell.column.id === "preview" ? "!px-2 !py-1.5 w-[4rem]" : cell.column.id === "filename" ? "!px-3 !py-1.5 w-[20rem] max-w-0 overflow-hidden" : cell.column.id === "directory" ? "!px-3 !py-1.5 w-[14rem] max-w-0 overflow-hidden" : cell.column.id === "usage" ? "!pl-2 !pr-4 !py-1.5 w-[5.5rem] text-sm font-medium text-right whitespace-nowrap" : "!px-3 !py-1.5"
47548
+ },
47549
+ flexRender(
47550
+ cell.column.columnDef.cell,
47551
+ cell.getContext()
47552
+ )
47553
+ ))
47554
+ ), row.getIsExpanded() && /* @__PURE__ */ React__default.createElement(TableRow, null, /* @__PURE__ */ React__default.createElement(
47555
+ TableCell,
47556
+ {
47557
+ colSpan: columns.length,
47558
+ className: "border-b border-gray-150 bg-[#FFEBE5]"
47559
+ },
47560
+ /* @__PURE__ */ React__default.createElement(
47561
+ ExpandedRowContent,
47562
+ {
47563
+ usedIn: row.original.usedIn,
47564
+ onClose
47565
+ }
47566
+ )
47567
+ )))))
47568
+ ), hasMore && /* @__PURE__ */ React__default.createElement("div", { ref: sentinelRef, className: "h-px", "aria-hidden": "true" })));
47569
+ };
47570
+ const MediaFilters = ({
47571
+ filteredRowCount,
47572
+ typeFilter,
47573
+ usageFilter,
47574
+ setTypeFilter,
47575
+ setUsageFilter,
47576
+ className
47577
+ }) => /* @__PURE__ */ React__default.createElement("div", { className: `flex items-center gap-3 ${className ?? ""}` }, /* @__PURE__ */ React__default.createElement("span", { className: "text-xs text-gray-500 whitespace-nowrap" }, filteredRowCount, " ", filteredRowCount === 1 ? "result" : "results"), /* @__PURE__ */ React__default.createElement(
47578
+ Select,
47579
+ {
47580
+ value: typeFilter,
47581
+ onValueChange: (value) => setTypeFilter(value)
47582
+ },
47583
+ /* @__PURE__ */ React__default.createElement(
47584
+ SelectTrigger,
47585
+ {
47586
+ "aria-label": "Filter by media type",
47587
+ className: "w-[130px] bg-white border-gray-200 text-gray-700 h-9"
47588
+ },
47589
+ /* @__PURE__ */ React__default.createElement(SelectValue, { placeholder: "All Types" })
47590
+ ),
47591
+ /* @__PURE__ */ React__default.createElement(SelectContent, null, /* @__PURE__ */ React__default.createElement(SelectItem, { value: "all" }, "All Types"), /* @__PURE__ */ React__default.createElement(SelectItem, { value: "image" }, "Images"), /* @__PURE__ */ React__default.createElement(SelectItem, { value: "video" }, "Videos"), /* @__PURE__ */ React__default.createElement(SelectItem, { value: "other" }, "Other"))
47592
+ ), /* @__PURE__ */ React__default.createElement(
47593
+ Select,
47594
+ {
47595
+ value: usageFilter,
47596
+ onValueChange: (value) => setUsageFilter(value)
47597
+ },
47598
+ /* @__PURE__ */ React__default.createElement(
47599
+ SelectTrigger,
47600
+ {
47601
+ "aria-label": "Filter by usage status",
47602
+ className: "w-[130px] bg-white border-gray-200 text-gray-700 h-9"
47603
+ },
47604
+ /* @__PURE__ */ React__default.createElement(SelectValue, { placeholder: "All Usage" })
47605
+ ),
47606
+ /* @__PURE__ */ React__default.createElement(SelectContent, null, /* @__PURE__ */ React__default.createElement(SelectItem, { value: "all" }, "All Usage"), /* @__PURE__ */ React__default.createElement(SelectItem, { value: "used" }, "Used"), /* @__PURE__ */ React__default.createElement(SelectItem, { value: "unused" }, "Unused"))
47607
+ ));
47608
+ const ExpandedRowContent = ({
47609
+ usedIn,
47610
+ onClose
47611
+ }) => {
47612
+ const documentCount = usedIn.length;
47613
+ const sortedDocs = [...usedIn].sort(
47614
+ (a2, b) => a2.collectionLabel.localeCompare(b.collectionLabel)
47615
+ );
47616
+ return /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, /* @__PURE__ */ React__default.createElement("p", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3" }, "Used in ", documentCount, " document", documentCount !== 1 ? "s" : "", ":"), /* @__PURE__ */ React__default.createElement("table", { className: "w-full text-xs border-collapse" }, /* @__PURE__ */ React__default.createElement("thead", null, /* @__PURE__ */ React__default.createElement("tr", { className: "text-left text-[10px] font-bold uppercase tracking-tight text-gray-400" }, /* @__PURE__ */ React__default.createElement("th", { className: "pb-1.5 pr-6 w-40" }, "Collection"), /* @__PURE__ */ React__default.createElement("th", { className: "pb-1.5" }, "Document"))), /* @__PURE__ */ React__default.createElement("tbody", null, sortedDocs.map((doc) => {
47617
+ const breadcrumb = doc.breadcrumbs.join("/");
47618
+ return /* @__PURE__ */ React__default.createElement(
47619
+ "tr",
47620
+ {
47621
+ key: doc.editUrl || breadcrumb,
47622
+ className: "border-t border-gray-150"
47623
+ },
47624
+ /* @__PURE__ */ React__default.createElement("td", { className: "py-1.5 pr-6 text-gray-500" }, /* @__PURE__ */ React__default.createElement(
47625
+ "a",
47626
+ {
47627
+ href: `#/collections/${doc.collectionName}/~`,
47628
+ onClick: () => onClose == null ? void 0 : onClose(),
47629
+ className: "underline hover:text-tina-orange-dark transition-colors"
47630
+ },
47631
+ doc.collectionLabel
47632
+ )),
47633
+ /* @__PURE__ */ React__default.createElement("td", { className: "py-1.5 text-gray-700" }, /* @__PURE__ */ React__default.createElement("span", { className: "flex items-center gap-1.5" }, /* @__PURE__ */ React__default.createElement(BiFile, { className: "text-gray-400 flex-shrink-0" }), doc.editUrl ? /* @__PURE__ */ React__default.createElement(
47634
+ "a",
47635
+ {
47636
+ href: doc.editUrl,
47637
+ onClick: () => onClose == null ? void 0 : onClose(),
47638
+ className: "underline hover:text-tina-orange-dark transition-colors break-all"
47639
+ },
47640
+ breadcrumb
47641
+ ) : /* @__PURE__ */ React__default.createElement("span", { className: "break-all" }, breadcrumb)))
47642
+ );
47643
+ }))));
47644
+ };
47645
+ const MediaUsageDashboard = ({
47646
+ close: onClose
47647
+ }) => {
47648
+ const { mediaItems, isLoading, errorOccurred, progress, refresh } = useMediaUsageScanner();
47649
+ const [lightboxImage, setLightboxImage] = useState(null);
47650
+ const stats = useMemo(() => {
47651
+ const unusedCount = mediaItems.filter(
47652
+ (usage) => usage.usedIn.length === 0
47653
+ ).length;
47654
+ const usedCount = mediaItems.length - unusedCount;
47655
+ return {
47656
+ totalFiles: mediaItems.length,
47657
+ unusedCount,
47658
+ usedCount
47659
+ };
47660
+ }, [mediaItems]);
47661
+ if (isLoading) {
47662
+ return /* @__PURE__ */ React__default.createElement(
47663
+ DashboardLoadingState,
47664
+ {
47665
+ message: "Scanning Media Usage...",
47666
+ progress: { value: progress, label: "Scanning collections" }
47667
+ }
47668
+ );
47669
+ }
47670
+ if (errorOccurred) {
47671
+ return /* @__PURE__ */ React__default.createElement(DashboardErrorState, { message: "Something went wrong, unable to collect media usage statistics" });
47672
+ }
47673
+ return /* @__PURE__ */ React__default.createElement("div", { className: "p-8 w-full max-w-6xl mx-auto font-sans" }, /* @__PURE__ */ React__default.createElement(
47674
+ DashboardTitleBar,
47675
+ {
47676
+ title: "Media Usage Dashboard",
47677
+ icon: /* @__PURE__ */ React__default.createElement(Image, null),
47678
+ controls: /* @__PURE__ */ React__default.createElement(
47679
+ Button,
47680
+ {
47681
+ variant: "outline",
47682
+ onClick: refresh,
47683
+ disabled: isLoading,
47684
+ className: "flex items-center gap-2 shadow-sm font-medium transition-colors"
47685
+ },
47686
+ /* @__PURE__ */ React__default.createElement(RefreshCw, { className: "w-4 h-4" }),
47687
+ "Refresh"
47688
+ )
47689
+ }
47690
+ ), /* @__PURE__ */ React__default.createElement("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-6 mb-8" }, /* @__PURE__ */ React__default.createElement(
47691
+ StatCard,
47692
+ {
47693
+ label: "Total Media Files",
47694
+ icon: /* @__PURE__ */ React__default.createElement(Database, { className: "text-2xl text-gray-700" }),
47695
+ iconClassName: "bg-gray-100",
47696
+ value: stats.totalFiles
47697
+ }
47698
+ ), /* @__PURE__ */ React__default.createElement(
47699
+ StatCard,
47700
+ {
47701
+ label: "In Use",
47702
+ icon: /* @__PURE__ */ React__default.createElement(CheckCircle2, { className: "text-2xl text-tina-orange-dark" }),
47703
+ iconClassName: "bg-tina-orange/10",
47704
+ value: stats.usedCount
47705
+ }
47706
+ ), /* @__PURE__ */ React__default.createElement(
47707
+ StatCard,
47708
+ {
47709
+ label: "Unused",
47710
+ icon: /* @__PURE__ */ React__default.createElement(
47711
+ ImageOff,
47712
+ {
47713
+ className: `text-2xl ${stats.unusedCount > 0 ? "text-orange-600" : "text-gray-400"}`
47714
+ }
47715
+ ),
47716
+ iconClassName: stats.unusedCount > 0 ? "bg-orange-100" : "bg-gray-100",
47717
+ value: stats.unusedCount,
47718
+ valueClassName: stats.unusedCount > 0 ? "text-orange-600" : "text-gray-800"
47719
+ }
47720
+ )), /* @__PURE__ */ React__default.createElement(
47721
+ MediaUsageTable,
47722
+ {
47723
+ mediaItems,
47724
+ onClose,
47725
+ onPreview: setLightboxImage
47726
+ }
47727
+ ), /* @__PURE__ */ React__default.createElement(
47728
+ MediaLightbox,
47729
+ {
47730
+ item: lightboxImage,
47731
+ onClose: () => setLightboxImage(null)
47732
+ }
47733
+ ));
47734
+ };
47735
+ const StatCard = ({
47736
+ label,
47737
+ icon,
47738
+ iconClassName,
47739
+ value,
47740
+ valueClassName = "text-gray-800"
47741
+ }) => /* @__PURE__ */ React__default.createElement("div", { className: "bg-white rounded-xl border border-gray-200 shadow-sm flex items-center gap-4 p-6" }, /* @__PURE__ */ React__default.createElement("div", { className: `${iconClassName} p-3 rounded-lg flex-shrink-0` }, icon), /* @__PURE__ */ React__default.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React__default.createElement("p", { className: "text-sm text-gray-500 font-medium" }, label), /* @__PURE__ */ React__default.createElement("p", { className: `text-3xl font-bold ${valueClassName}` }, value)));
47742
+ const MediaUsageDashboardScreenPlugin = createScreen({
47743
+ name: "Media Usage",
47744
+ Component: MediaUsageDashboard,
47745
+ Icon: MdImage,
47746
+ layout: "popup",
47747
+ navCategory: "Dashboard"
47748
+ });
46799
47749
  function createCloudConfig({
46800
47750
  ...options
46801
47751
  }) {
@@ -46999,7 +47949,7 @@ const NavProvider = ({
46999
47949
  const name = "tinacms";
47000
47950
  const type = "module";
47001
47951
  const typings = "dist/index.d.ts";
47002
- const version$1 = "3.6.3";
47952
+ const version$1 = "3.7.1";
47003
47953
  const main = "dist/index.js";
47004
47954
  const module = "./dist/index.js";
47005
47955
  const exports = {
@@ -47071,6 +48021,7 @@ const dependencies = {
47071
48021
  "@udecode/plate-dnd": "catalog:",
47072
48022
  "@udecode/plate-floating": "catalog:",
47073
48023
  "@udecode/plate-heading": "catalog:",
48024
+ "@udecode/plate-highlight": "catalog:",
47074
48025
  "@udecode/plate-horizontal-rule": "catalog:",
47075
48026
  "@udecode/plate-indent-list": "catalog:",
47076
48027
  "@udecode/plate-link": "catalog:",
@@ -47108,6 +48059,8 @@ const dependencies = {
47108
48059
  "react-colorful": "catalog:",
47109
48060
  "react-datetime": "catalog:",
47110
48061
  "react-day-picker": "^9.13.0",
48062
+ "react-dnd": "catalog:",
48063
+ "react-dnd-html5-backend": "catalog:",
47111
48064
  "react-dropzone": "catalog:",
47112
48065
  "react-final-form": "catalog:",
47113
48066
  "react-icons": "^5.4.0",
@@ -48045,11 +48998,11 @@ const FormLists = (props) => {
48045
48998
  "input",
48046
48999
  {
48047
49000
  type: "checkbox",
48048
- checked: !showReferences,
48049
- onChange: (e3) => setShowReferences(!e3.target.checked),
49001
+ checked: showReferences,
49002
+ onChange: (e3) => setShowReferences(e3.target.checked),
48050
49003
  className: "w-4 h-4 text-orange-500 border-gray-300 rounded focus:ring-orange-500"
48051
49004
  }
48052
- ), /* @__PURE__ */ React.createElement("span", null, "Direct references only"))), /* @__PURE__ */ React.createElement("div", { className: "flex-1 overflow-x-auto overflow-y-auto min-h-0" }, cms.state.formLists.map((formList, index) => /* @__PURE__ */ React.createElement("div", { key: `${formList.id}-${index}` }, /* @__PURE__ */ React.createElement(
49005
+ ), /* @__PURE__ */ React.createElement("span", null, "Show all references"))), /* @__PURE__ */ React.createElement("div", { className: "flex-1 overflow-x-auto overflow-y-auto min-h-0" }, cms.state.formLists.map((formList, index) => /* @__PURE__ */ React.createElement("div", { key: `${formList.id}-${index}` }, /* @__PURE__ */ React.createElement(
48053
49006
  FormList,
48054
49007
  {
48055
49008
  setActiveFormId: (id2) => {
@@ -48821,6 +49774,9 @@ class TinaCMS extends CMS {
48821
49774
  });
48822
49775
  this.plugins.add(MediaManagerScreenPlugin);
48823
49776
  this.plugins.add(PasswordScreenPlugin);
49777
+ if (isLocalClient) {
49778
+ this.plugins.add(MediaUsageDashboardScreenPlugin);
49779
+ }
48824
49780
  if (isLocalClient !== true) {
48825
49781
  if (clientId) {
48826
49782
  this.plugins.add(
@@ -65044,7 +66000,8 @@ const CreateBranchModal = ({
65044
66000
  safeSubmit,
65045
66001
  path: path3,
65046
66002
  values,
65047
- crudType
66003
+ crudType,
66004
+ tinaForm
65048
66005
  }) => {
65049
66006
  const cms = useCMS$1();
65050
66007
  const tinaApi = cms.api.tina;
@@ -65094,6 +66051,7 @@ const CreateBranchModal = ({
65094
66051
  }
65095
66052
  ];
65096
66053
  const executeEditorialWorkflow = async () => {
66054
+ var _a2;
65097
66055
  try {
65098
66056
  const branchName = `tina/${newBranchName}`;
65099
66057
  setDisabled(true);
@@ -65108,7 +66066,21 @@ const CreateBranchModal = ({
65108
66066
  graphql2 = UPDATE_DOCUMENT_GQL;
65109
66067
  }
65110
66068
  const collection = tinaApi.schema.getCollectionByFullPath(path3);
65111
- const params = tinaApi.schema.transformPayload(collection.name, values);
66069
+ let submittedValues = values;
66070
+ if ((_a2 = collection == null ? void 0 : collection.ui) == null ? void 0 : _a2.beforeSubmit) {
66071
+ const valOverride = await collection.ui.beforeSubmit({
66072
+ cms,
66073
+ values,
66074
+ form: tinaForm
66075
+ });
66076
+ if (valOverride) {
66077
+ submittedValues = valOverride;
66078
+ }
66079
+ }
66080
+ const params = tinaApi.schema.transformPayload(
66081
+ collection.name,
66082
+ submittedValues
66083
+ );
65112
66084
  const relativePath = pathRelativeToCollection(collection.path, path3);
65113
66085
  const result = await tinaApi.executeEditorialWorkflow({
65114
66086
  branchName,
@@ -65242,7 +66214,7 @@ const CreateBranchModal = ({
65242
66214
  ),
65243
66215
  /* @__PURE__ */ React.createElement("div", { className: "text-center max-w-24" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold leading-tight" }, step.name), /* @__PURE__ */ React.createElement("div", { className: "text-xs text-gray-400 mt-1 leading-tight" }, step.description))
65244
66216
  );
65245
- })), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs text-gray-500" }, "Estimated time: 1-2 min "), isExecuting && currentStep > 0 && /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1 text-sm text-gray-500" }, /* @__PURE__ */ React.createElement("svg", { className: "w-4 h-4", fill: "currentColor", viewBox: "0 0 20 20" }, /* @__PURE__ */ React.createElement(
66217
+ })), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs text-gray-500" }, "Estimated time: 1-2 min "), isExecuting && currentStep > 0 && /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1 text-sm text-gray-500 tabular-nums" }, /* @__PURE__ */ React.createElement("svg", { className: "w-4 h-4", fill: "currentColor", viewBox: "0 0 20 20" }, /* @__PURE__ */ React.createElement(
65246
66218
  "path",
65247
66219
  {
65248
66220
  fillRule: "evenodd",
@@ -65481,6 +66453,7 @@ const FormBuilder = ({
65481
66453
  crudType: tinaForm.crudType,
65482
66454
  path: tinaForm.path,
65483
66455
  values: tinaForm.values,
66456
+ tinaForm,
65484
66457
  close: () => setCreateBranchModalOpen(false)
65485
66458
  }
65486
66459
  ), /* @__PURE__ */ React.createElement(DragDropContext, { onDragEnd: moveArrayItem }, /* @__PURE__ */ React.createElement(FormKeyBindings, { onSubmit: safeHandleSubmit }), /* @__PURE__ */ React.createElement(FormPortalProvider, null, /* @__PURE__ */ React.createElement(FormWrapper, { id: tinaForm.id }, (tinaForm == null ? void 0 : tinaForm.fields.length) ? /* @__PURE__ */ React.createElement(
@@ -67379,10 +68352,105 @@ const MarkToolbarButton = withRef$1(({ clear, nodeType, ...rest }, ref) => {
67379
68352
  const { props } = useMarkToolbarButton(state);
67380
68353
  return /* @__PURE__ */ React__default.createElement(ToolbarButton, { ref, ...props, ...rest });
67381
68354
  });
68355
+ const highlightColors = [
68356
+ { label: "Yellow", value: "#FEF08A" },
68357
+ { label: "Green", value: "#BBF7D0" },
68358
+ { label: "Blue", value: "#BFDBFE" },
68359
+ { label: "Red", value: "#CC4141" }
68360
+ ];
67382
68361
  const BoldToolbarButton = () => /* @__PURE__ */ React__default.createElement(MarkToolbarButton, { tooltip: "Bold (⌘+B)", nodeType: BoldPlugin.key }, /* @__PURE__ */ React__default.createElement(Icons.bold, null));
67383
68362
  const StrikethroughToolbarButton = () => /* @__PURE__ */ React__default.createElement(MarkToolbarButton, { tooltip: "Strikethrough", nodeType: StrikethroughPlugin.key }, /* @__PURE__ */ React__default.createElement(Icons.strikethrough, null));
67384
68363
  const ItalicToolbarButton = () => /* @__PURE__ */ React__default.createElement(MarkToolbarButton, { tooltip: "Italic (⌘+I)", nodeType: ItalicPlugin.key }, /* @__PURE__ */ React__default.createElement(Icons.italic, null));
67385
68364
  const CodeToolbarButton = () => /* @__PURE__ */ React__default.createElement(MarkToolbarButton, { tooltip: "Code (⌘+E)", nodeType: CodePlugin.key }, /* @__PURE__ */ React__default.createElement(Icons.code, null));
68365
+ const HighlightToolbarButton = () => /* @__PURE__ */ React__default.createElement(HighlightColorToolbarButton, null);
68366
+ const useHighlightToolbar = () => {
68367
+ const editor = useEditorRef();
68368
+ const openState = useOpenState();
68369
+ const savedSelection = React__default.useRef(editor.selection);
68370
+ const inlineCodeActive = useMarkToolbarButtonState({
68371
+ nodeType: CodePlugin.key
68372
+ }).pressed;
68373
+ const rememberSelection = React__default.useCallback(() => {
68374
+ if (editor.selection) {
68375
+ savedSelection.current = structuredClone(editor.selection);
68376
+ }
68377
+ }, [editor]);
68378
+ React__default.useEffect(() => {
68379
+ if (openState.open) {
68380
+ rememberSelection();
68381
+ }
68382
+ }, [openState.open, rememberSelection]);
68383
+ const applyHighlight = React__default.useCallback(
68384
+ (highlightColor) => {
68385
+ if (inlineCodeActive) {
68386
+ openState.onOpenChange(false);
68387
+ return;
68388
+ }
68389
+ if (savedSelection.current) {
68390
+ editor.tf.select(structuredClone(savedSelection.current));
68391
+ }
68392
+ if (highlightColor) {
68393
+ editor.tf.addMark("highlight", true);
68394
+ editor.tf.addMark("highlightColor", highlightColor);
68395
+ } else {
68396
+ editor.tf.removeMark("highlight");
68397
+ editor.tf.removeMark("highlightColor");
68398
+ }
68399
+ editor.tf.setNodes(
68400
+ highlightColor ? {
68401
+ highlight: true,
68402
+ highlightColor
68403
+ } : {
68404
+ highlight: void 0,
68405
+ highlightColor: void 0
68406
+ },
68407
+ {
68408
+ at: editor.selection ?? void 0,
68409
+ match: (node3) => editor.api.isText(node3),
68410
+ split: true
68411
+ }
68412
+ );
68413
+ editor.tf.focus();
68414
+ openState.onOpenChange(false);
68415
+ },
68416
+ [editor, inlineCodeActive, openState]
68417
+ );
68418
+ return {
68419
+ applyHighlight,
68420
+ inlineCodeActive,
68421
+ openState,
68422
+ rememberSelection
68423
+ };
68424
+ };
68425
+ const HighlightColorToolbarButton = () => {
68426
+ const { applyHighlight, inlineCodeActive, openState, rememberSelection } = useHighlightToolbar();
68427
+ return /* @__PURE__ */ React__default.createElement(DropdownMenu$1, { modal: false, ...openState }, /* @__PURE__ */ React__default.createElement(DropdownMenuTrigger$1, { asChild: true }, /* @__PURE__ */ React__default.createElement(
68428
+ ToolbarButton,
68429
+ {
68430
+ isDropdown: true,
68431
+ showArrow: true,
68432
+ pressed: openState.open,
68433
+ tooltip: "Highlight color",
68434
+ disabled: inlineCodeActive,
68435
+ onMouseDown: rememberSelection
68436
+ },
68437
+ /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center gap-1.5" }, /* @__PURE__ */ React__default.createElement(Icons.highlight, null), /* @__PURE__ */ React__default.createElement("span", { className: "sr-only" }, "Highlight color"))
68438
+ )), /* @__PURE__ */ React__default.createElement(DropdownMenuContent$1, { align: "start", className: "min-w-[180px]" }, /* @__PURE__ */ React__default.createElement(DropdownMenuItem$1, { onSelect: () => applyHighlight() }, "Clear highlight"), highlightColors.map((color) => /* @__PURE__ */ React__default.createElement(
68439
+ DropdownMenuItem$1,
68440
+ {
68441
+ key: color.value,
68442
+ onSelect: () => applyHighlight(color.value)
68443
+ },
68444
+ /* @__PURE__ */ React__default.createElement(
68445
+ "span",
68446
+ {
68447
+ className: "mr-2 inline-block size-4 rounded border border-gray-300",
68448
+ style: { backgroundColor: color.value }
68449
+ }
68450
+ ),
68451
+ color.label
68452
+ ))));
68453
+ };
67386
68454
  const ListToolbarButton = withRef$1(({ nodeType = BulletedListPlugin.key, ...rest }, ref) => {
67387
68455
  const state = useListToolbarButtonState({ nodeType });
67388
68456
  const { props } = useListToolbarButton(state);
@@ -67616,6 +68684,11 @@ const toolbarItems = {
67616
68684
  width: () => STANDARD_ICON_WIDTH,
67617
68685
  Component: /* @__PURE__ */ React__default.createElement(StrikethroughToolbarButton, null)
67618
68686
  },
68687
+ highlight: {
68688
+ label: "Highlight",
68689
+ width: () => STANDARD_ICON_WIDTH,
68690
+ Component: /* @__PURE__ */ React__default.createElement(HighlightToolbarButton, null)
68691
+ },
67619
68692
  italic: {
67620
68693
  label: "Italic",
67621
68694
  width: () => STANDARD_ICON_WIDTH,
@@ -68803,7 +69876,7 @@ const FloatingToolbar = withRef$1(({ children, state, ...props }, propRef) => {
68803
69876
  Toolbar,
68804
69877
  {
68805
69878
  className: cn$1(
68806
- "absolute z-[999999] whitespace-nowrap border bg-popover px-1 opacity-100 shadow-md print:hidden rounded-md"
69879
+ "absolute z-[10799] whitespace-nowrap border bg-popover px-1 opacity-100 shadow-md print:hidden rounded-md"
68807
69880
  ),
68808
69881
  ...props,
68809
69882
  ...rootProps,
@@ -119698,6 +120771,7 @@ const resetBlockTypesCodeBlockRule = {
119698
120771
  const viewPlugins = [
119699
120772
  BasicMarksPlugin,
119700
120773
  UnderlinePlugin,
120774
+ HighlightPlugin,
119701
120775
  HeadingPlugin.configure({ options: { levels: 6 } }),
119702
120776
  ParagraphPlugin,
119703
120777
  CodeBlockPlugin.configure({
@@ -119708,6 +120782,30 @@ const viewPlugins = [
119708
120782
  const CorrectNodeBehaviorPlugin = createSlatePlugin$1({
119709
120783
  key: "WITH_CORRECT_NODE_BEHAVIOR"
119710
120784
  });
120785
+ const ClearHighlightOnEnterPlugin = createSlatePlugin$1({
120786
+ key: "CLEAR_HIGHLIGHT_ON_ENTER"
120787
+ }).overrideEditor(({ editor, tf: { insertBreak: insertBreak2 } }) => ({
120788
+ transforms: {
120789
+ insertBreak() {
120790
+ const keyboardEvent = editor.currentKeyboardEvent;
120791
+ const isPlainEnter = (keyboardEvent == null ? void 0 : keyboardEvent.key) === "Enter" && !keyboardEvent.shiftKey && !keyboardEvent.metaKey && !keyboardEvent.ctrlKey && !keyboardEvent.altKey;
120792
+ const activeMarks = editor.api.marks();
120793
+ const hasHighlight = Boolean(
120794
+ (activeMarks == null ? void 0 : activeMarks.highlight) || (activeMarks == null ? void 0 : activeMarks.highlightColor)
120795
+ );
120796
+ insertBreak2();
120797
+ if (!isPlainEnter || !hasHighlight) {
120798
+ return;
120799
+ }
120800
+ editor.tf.removeMark("highlight");
120801
+ editor.tf.removeMark("highlightColor");
120802
+ editor.tf.unsetNodes(["highlight", "highlightColor"], {
120803
+ at: editor.selection ?? void 0,
120804
+ match: (node3) => editor.api.isText(node3)
120805
+ });
120806
+ }
120807
+ }
120808
+ }));
119711
120809
  const editorPlugins = [
119712
120810
  createMdxBlockPlugin,
119713
120811
  createMdxInlinePlugin,
@@ -119717,6 +120815,7 @@ const editorPlugins = [
119717
120815
  createBlockquoteEnterBreakPlugin,
119718
120816
  createInvalidMarkdownPlugin,
119719
120817
  CorrectNodeBehaviorPlugin,
120818
+ ClearHighlightOnEnterPlugin,
119720
120819
  LinkPlugin.configure({
119721
120820
  options: {
119722
120821
  // Custom validation function to allow relative links, e.g., /about