From 609ec6d37b5abf94f3ab84784544ebe47804cdf5 Mon Sep 17 00:00:00 2001
From: sharkiecodes <lanyi_stroud@brown.edu>
Date: Tue, 29 Apr 2025 23:01:11 -0400
Subject: Autotag

---
 .../views/nodes/formattedText/FormattedTextBox.tsx     | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 98e461a52..f4cbbcc9e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -64,6 +64,7 @@ import { removeMarkWithAttrs } from './prosemirrorPatches';
 import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
 import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
+import { tickStep } from 'd3';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 
 export interface FormattedTextBoxProps extends FieldViewProps {
@@ -304,6 +305,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
     };
 
+    autoTag = async () => {
+        this.Document.$tags_chat = new List<string>();
+        gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]),
+        GPTCallType.CLASSIFYTEXT).then(desc => (this.Document.$tags_chat as List<string>).push(desc));
+        this.Document._layout_showTags = true;
+                //or... then(desc => this.Document.$tags_chat = desc);
+    }
+
     leafText = (node: Node) => {
         if (node.type === this.EditorView?.state.schema.nodes.dashField) {
             const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
@@ -1236,6 +1245,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             },
             { fireImmediately: true }
         );
+        
 
         this._disposers.search = reaction(
             () => Doc.IsSearchMatch(this.Document),
@@ -1269,6 +1279,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             { fireImmediately: true }
         );
 
+        this._disposers.tagger = reaction(
+            () => ({ title: this.Document.title, sel: this.props.isSelected() }),
+            action(() => {
+                this.autoTag();
+            }),
+            { fireImmediately: true }
+        );
+    
         if (!this._props.dontRegisterView) {
             this._disposers.record = reaction(
                 () => this.recordingDictation,
-- 
cgit v1.2.3-70-g09d2


From a2195058135bfac873aba2a5e8677baf8d90b201 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 9 May 2025 23:09:35 -0400
Subject: fixed reaction exception from accessing props and not _props

---
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 97049d0eb..10becc00b 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -307,11 +307,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     autoTag = async () => {
         this.Document.$tags_chat = new List<string>();
-        gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]),
-        GPTCallType.CLASSIFYTEXT).then(desc => (this.Document.$tags_chat as List<string>).push(desc));
+        gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), GPTCallType.CLASSIFYTEXT).then(desc => (this.Document.$tags_chat as List<string>).push(desc));
         this.Document._layout_showTags = true;
-                //or... then(desc => this.Document.$tags_chat = desc);
-    }
+        //or... then(desc => this.Document.$tags_chat = desc);
+    };
 
     leafText = (node: Node) => {
         if (node.type === this.EditorView?.state.schema.nodes.dashField) {
@@ -1246,7 +1245,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             },
             { fireImmediately: true }
         );
-        
 
         this._disposers.search = reaction(
             () => Doc.IsSearchMatch(this.Document),
@@ -1281,13 +1279,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         );
 
         this._disposers.tagger = reaction(
-            () => ({ title: this.Document.title, sel: this.props.isSelected() }),
+            () => ({ title: this.Document.title, sel: this._props.isSelected() }),
             action(() => {
                 this.autoTag();
             }),
             { fireImmediately: true }
         );
-    
+
         if (!this._props.dontRegisterView) {
             this._disposers.record = reaction(
                 () => this.recordingDictation,
-- 
cgit v1.2.3-70-g09d2


From 87484331f805f583c98226c5a8c6b1ec5fc88925 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 13 May 2025 13:37:54 -0400
Subject: fixed setting up link anchors for video to be added to Doc. fixed
 imageTemplate to work with templates in tabs.  fixed link following from link
 menu.  cleaned up getView with sidebars to open them when needed properly..

---
 src/client/documents/DocUtils.ts                   |  2 +-
 src/client/util/DocumentManager.ts                 |  2 +-
 src/client/views/MarqueeAnnotator.tsx              |  3 +-
 src/client/views/SidebarAnnos.tsx                  | 29 +++++++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  7 +--
 src/client/views/linking/LinkMenuItem.tsx          | 18 +++----
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/MapBox/MapBox.tsx           | 59 +++++++---------------
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx   | 51 ++++++++++---------
 src/client/views/nodes/PDFBox.tsx                  | 17 +++----
 src/client/views/nodes/VideoBox.tsx                | 17 ++++---
 src/client/views/nodes/WebBox.tsx                  | 26 +++++-----
 .../views/nodes/formattedText/FormattedTextBox.tsx | 17 +++----
 13 files changed, 124 insertions(+), 126 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 36e03daed..41a61b154 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -724,7 +724,7 @@ export namespace DocUtils {
                                 _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200,
                                 _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35,
                                 _layout_autoHeight: true,
-                                backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor),
+                                backgroundColor: backgroundColor ?? StrCast(Doc.UserDoc().textBackgroundColor),
                                 borderColor: Doc.UserDoc().borderColor as string,
                                 borderWidth: Doc.UserDoc().borderWidth as number,
                                 text_centered: BoolCast(Doc.UserDoc().textCentered),
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index fc8f22cf3..422613968 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -250,7 +250,7 @@ export class DocumentManager {
         const options = optionsIn;
         Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc);
         const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
-        if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
+        if (docContextPath.some(doc => doc !== targetDoc && doc.hidden)) options.toggleTarget = false;
         let activatedTab = false;
         if (DocumentView.activateTabView(docContextPath[0])) {
             options.toggleTarget = false;
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index b2e42652d..d354dd2c0 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -194,14 +194,13 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         AnchorMenu.Instance.StartDrag = action((e: PointerEvent, ele: HTMLElement) => {
             e.preventDefault();
             e.stopPropagation();
-            const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color
-
             const targetCreator = (annotationOn: Doc | undefined) => {
                 const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
                 target.layout_fitWidth = true;
                 DocumentView.SetSelectOnLoad(target);
                 return target;
             };
+            const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
                 dragComplete: dragEv => {
                     if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) {
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 573c28ccf..71b479a22 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, returnFalse, returnOne, returnZero } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
-import { Doc, DocListCast, Field, FieldResult, FieldType, StrListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, FieldType, Opt, StrListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
 import { RichTextField } from '../../fields/RichTextField';
@@ -19,6 +19,7 @@ import { StyleProp } from './StyleProp';
 import { CollectionStackingView } from './collections/CollectionStackingView';
 import { DocumentView } from './nodes/DocumentView';
 import { FieldViewProps } from './nodes/FieldView';
+import { FocusViewOptions } from './nodes/FocusViewOptions';
 
 interface ExtraProps {
     fieldKey: string;
@@ -149,15 +150,33 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
     };
     makeDocUnfiltered = (doc: Doc) => {
         if (DocListCast(this._props.Doc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
-            if (this.childFilters()) {
+            if (this.childFilters().length) {
                 // if any child filters exist, get rid of them
                 this._props.layoutDoc._childFilters = new List<string>();
+                return true;
             }
-            return true;
         }
         return false;
     };
 
+    public static getView(sidebar: SidebarAnnos | null, sidebarShown: boolean, toggleSidebar: () => void, doc: Doc, options: FocusViewOptions) {
+        if (!sidebarShown) {
+            options.didMove = true;
+            toggleSidebar();
+        }
+        options.didMove = sidebar?.makeDocUnfiltered(doc) || options.didMove;
+
+        if (!doc.hidden) {
+            if (!options.didMove && options.toggleTarget) {
+                options.toggleTarget = false;
+                options.didMove = doc.hidden = true;
+            }
+        } else {
+            options.didMove = !(doc.hidden = false);
+        }
+        return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
+    }
+
     get sidebarKey() {
         return this._props.fieldKey + '_sidebar';
     }
@@ -181,8 +200,8 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
     layout_showTitle = () => 'title';
     setHeightCallback = (height: number) => this._props.setHeight?.(height + this.filtersHeight());
     sortByLinkAnchorY = (a: Doc, b: Doc) => {
-        const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1).y;
-        const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1).y;
+        const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1)?.y;
+        const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1)?.y;
         return NumCast(ay) - NumCast(by);
     };
     render() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bb3c59eae..77615c650 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1738,10 +1738,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
 
         if (addAsAnnotation) {
-            if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
-                Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), [])?.push(anchor);
+            const fieldKey = this._props.isAnnotationOverlay ? this._props.fieldKey : this._props.fieldKey + '_annotations';
+            if (Cast(this.dataDoc[fieldKey], listSpec(Doc), null) !== undefined) {
+                Cast(this.dataDoc[fieldKey], listSpec(Doc), [])?.push(anchor);
             } else {
-                this.dataDoc[this._props.fieldKey + '_annotations'] = new List<Doc>([anchor]);
+                this.dataDoc[fieldKey] = new List<Doc>([anchor]);
             }
         }
         return anchor;
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index c984a7053..6c1ad568d 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -105,11 +105,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                     LinkManager.Instance.currentLinkAnchor = LinkManager.Instance.currentLink ? this.sourceAnchor : undefined;
 
                     if ((SnappingManager.PropertiesWidth ?? 0) < 100) {
-                        setTimeout(
-                            action(() => {
-                                SnappingManager.SetPropertiesWidth(250);
-                            })
-                        );
+                        setTimeout(action(() => SnappingManager.SetPropertiesWidth(250)));
                     }
                 }
             })
@@ -136,14 +132,14 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                     this._props.itemHandler?.(this._props.linkDoc);
                 } else {
                     const focusDoc =
-                        Cast(this._props.linkDoc.link_anchor_1, Doc, null)?.annotationOn === this._props.sourceDoc
-                            ? Cast(this._props.linkDoc.link_anchor_1, Doc, null)
-                            : Cast(this._props.linkDoc.link_anchor_2, Doc, null)?.annotationOn === this._props.sourceDoc
-                              ? Cast(this._props.linkDoc.link_anchor_12, Doc, null)
-                              : undefined;
+                        DocCast(this._props.linkDoc.link_anchor_1)?.annotationOn === this._props.sourceDoc
+                            ? DocCast(this._props.linkDoc.link_anchor_1)
+                      : DocCast(this._props.linkDoc.link_anchor_2)?.annotationOn === this._props.sourceDoc
+                            ? DocCast(this._props.linkDoc.link_anchor_2)
+                      : undefined; // prettier-ignore
 
                     if (focusDoc) this._props.docView._props.focus(focusDoc, { instant: true });
-                    DocumentView.FollowLink(this._props.linkDoc, this._props.sourceDoc, false);
+                    DocumentView.FollowLink(this._props.linkDoc, focusDoc ?? this._props.sourceDoc, false);
                 }
             }
         );
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 05706fe6b..b0415ef18 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1412,7 +1412,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     }
     public static setDefaultImageTemplate(checkResult?: boolean) {
         if (checkResult) return Doc.UserDoc().defaultImageLayout;
-        const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined;
+        const view = DocumentView.Selected()[0]?._props.renderDepth > 0 || DocumentView.Selected()[0]?.Document.isTemplateDoc ? DocumentView.Selected()[0] : undefined;
         undoable(() => {
             const tempDoc = DocumentView.getTemplate(view);
             Doc.UserDoc().defaultImageLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined;
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index a563b7c1b..a279ccc48 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -13,7 +13,7 @@ import { CirclePicker, ColorResult } from 'react-color';
 import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl/mapbox';
 import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils';
 import { emptyFunction } from '../../../../Utils';
-import { Doc, DocListCast, Field, LinkedTo, Opt, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, LinkedTo, StrListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types';
@@ -111,12 +111,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     // this list contains pushpins and configs
     @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } // prettier-ignore
-    @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } // prettier-ignore
+    @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.sidebarKey]); } // prettier-ignore
     @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } // prettier-ignore
     @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } // prettier-ignore
     @computed get SidebarShown() { return !!this.layoutDoc._layout_showSidebar; } // prettier-ignore
     @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore
-    @computed get SidebarKey() {  return this.fieldKey + '_sidebar'; } // prettier-ignore
+    @computed get sidebarKey() {  return this.fieldKey + '_sidebar'; } // prettier-ignore
     @computed get sidebarColor() {
         return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4'));
     }
@@ -260,7 +260,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => {
         this.allAnnotations
-            .filter(anno => toList(doc).includes(DocCast(anno.mapPin)))
+            .filter(anno => toList(doc).includes(DocCast(anno.mapPin)!))
             .forEach(anno => {
                 anno.mapPin = undefined;
             });
@@ -339,27 +339,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => {
         e.preventDefault();
         e.stopPropagation();
-
-        const sourceAnchorCreator = action(() => {
-            const note = this.getAnchor(true);
-            if (note && this._selectedPinOrRoute) {
-                note.latitude = this._selectedPinOrRoute.latitude;
-                note.longitude = this._selectedPinOrRoute.longitude;
-                note.map = this._selectedPinOrRoute.map;
-            }
-            return note as Doc;
-        });
-
         const targetCreator = (annotationOn: Doc | undefined) => {
             const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
+            target.layout_fitWidth = true;
             DocumentView.SetSelectOnLoad(target);
             return target;
         };
+
+        const sourceAnchorCreator = () => this.getAnchor(true);
         const docView = this.DocumentView?.();
         docView &&
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
                 dragComplete: dragEv => {
-                    if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) {
+                    if (!dragEv.aborted && dragEv.annoDragData?.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) {
                         dragEv.annoDragData.linkSourceDoc.followLinkToggle = dragEv.annoDragData.dropDocument.annotationOn === this.Document;
                         dragEv.annoDragData.linkSourceDoc.followLinkZoom = false;
                     }
@@ -368,17 +360,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     createNoteAnnotation = () => {
-        const createFunc = undoable(
-            action(() => {
-                const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]);
-                if (note && this._selectedPinOrRoute) {
-                    note.latitude = this._selectedPinOrRoute.latitude;
-                    note.longitude = this._selectedPinOrRoute.longitude;
-                    note.map = this._selectedPinOrRoute.map;
-                }
-            }),
-            'create note annotation'
-        );
+        const createFunc = undoable(() => this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]), 'create note annotation');
         if (!this.layoutDoc.layout_showSidebar) {
             this.toggleSidebar();
             setTimeout(createFunc);
@@ -428,14 +410,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
-    getView = (doc: Doc, options: FocusViewOptions) => {
-        if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
-            this.toggleSidebar();
-            options.didMove = true;
+    getView = async (doc: Doc, options: FocusViewOptions) => {
+        if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+            SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, this.toggleSidebar, doc, options);
         }
-        return new Promise<Opt<DocumentView>>(res => {
-            DocumentView.addViewRenderedCb(doc, dv => res(dv));
-        });
+        return undefined;
     };
 
     /*
@@ -476,13 +455,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     @action
     deleteSelectedPinOrRoute = undoable(() => {
-        if (this._selectedPinOrRoute) {
+        const selPin = DocCast(this._selectedPinOrRoute);
+        if (selPin) {
             // Removes filter
-            Doc.setDocFilter(this.Document, 'latitude', NumCast(this._selectedPinOrRoute.latitude), 'remove');
-            Doc.setDocFilter(this.Document, 'longitude', NumCast(this._selectedPinOrRoute.longitude), 'remove');
-            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this._selectedPinOrRoute))}`, 'remove');
+            Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove');
+            Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove');
+            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove');
 
-            this.removePushpinOrRoute(this._selectedPinOrRoute);
+            this.removePushpinOrRoute(selPin);
         }
         MapAnchorMenu.Instance.fadeOut(true);
         document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
@@ -1299,7 +1279,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     <SidebarAnnos
                         ref={this._sidebarRef}
                         {...this._props}
-                        fieldKey={this.fieldKey}
                         Doc={this.Document}
                         layoutDoc={this.layoutDoc}
                         dataDoc={this.dataDoc}
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index e0efab576..0beefcb67 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -145,7 +145,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
     removeMapDocument = (docsIn: Doc | Doc[], annotationKey?: string) => {
         const docs = toList(docsIn);
         this.allAnnotations
-            .filter(anno => docs.includes(DocCast(anno.mapPin)))
+            .filter(anno => docs.includes(DocCast(anno.mapPin)!))
             .forEach(anno => {
                 anno.mapPin = undefined;
             });
@@ -224,6 +224,12 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
     startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => {
         e.preventDefault();
         e.stopPropagation();
+        const targetCreator = (annotationOn: Doc | undefined) => {
+            const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
+            target.layout_fitWidth = true;
+            DocumentView.SetSelectOnLoad(target);
+            return target;
+        };
 
         const sourceAnchorCreator = action(() => {
             const note = this.getAnchor(true);
@@ -235,11 +241,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
             return note as Doc;
         });
 
-        const targetCreator = (annotationOn: Doc | undefined) => {
-            const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
-            DocumentView.SetSelectOnLoad(target);
-            return target;
-        };
         const docView = this.DocumentView?.();
         docView &&
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
@@ -362,22 +363,22 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
 
     @action
     deselectPin = () => {
-        if (this.selectedPin) {
+        const selPin = DocCast(this.selectedPin);
+        if (selPin) {
             // Removes filter
-            Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove');
-            Doc.setDocFilter(this.Document, 'longitude', NumCast(this.selectedPin.longitude), 'remove');
-            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove');
+            Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove');
+            Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove');
+            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove');
 
-            const temp = this.selectedPin;
             if (!this._unmounting) {
-                this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp));
+                this._bingMap.current.entities.remove(this.map_docToPinMap.get(selPin));
             }
-            const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude));
-            this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(temp as Doc));
+            const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(selPin.latitude, selPin.longitude));
+            this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(selPin));
             if (!this._unmounting) {
                 this._bingMap.current.entities.push(newpin);
             }
-            this.map_docToPinMap.set(temp, newpin);
+            this.map_docToPinMap.set(selPin, newpin);
             this.selectedPin = undefined;
             this.bingSearchBarContents = this.Document.map;
         }
@@ -388,9 +389,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
             this.toggleSidebar();
             options.didMove = true;
         }
-        return new Promise<Opt<DocumentView>>(res => {
-            DocumentView.addViewRenderedCb(doc, dv => res(dv));
-        });
+        return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
     };
     /*
      * Pushpin onclick
@@ -535,13 +534,14 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
 
     @action
     deleteSelectedPin = undoable(() => {
-        if (this.selectedPin) {
+        const selPin = this.selectedPin;
+        if (selPin) {
             // Removes filter
-            Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove');
-            Doc.setDocFilter(this.Document, 'longitude', NumCast(this.selectedPin.longitude), 'remove');
-            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove');
+            Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove');
+            Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove');
+            Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove');
 
-            this.removePushpin(this.selectedPin);
+            this.removePushpin(selPin);
         }
         MapAnchorMenu.Instance.fadeOut(true);
         document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
@@ -638,7 +638,10 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
         this._disposers.highlight = reaction(
             () => this.allAnnotations.map(doc => doc[Highlight]),
             () => {
-                const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin);
+                const allConfigPins = this.allAnnotations
+                    .map(doc => ({ doc, pushpin: DocCast(doc.mapPin) }))
+                    .filter(pair => pair.pushpin)
+                    .map(pair => ({ doc: pair.doc, pushpin: pair.pushpin! }));
                 allConfigPins.forEach(({ pushpin }) => {
                     if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) {
                         this.recolorPin(pushpin);
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 55e6d5596..e4050a3c3 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -10,7 +10,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types';
+import { Cast, DocCast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types';
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
@@ -27,7 +27,6 @@ import { Colors } from '../global/globalEnums';
 import { PDFViewer } from '../pdf/PDFViewer';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { SidebarAnnos } from '../SidebarAnnos';
-import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import { ImageBox } from './ImageBox';
@@ -56,6 +55,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy> = undefined;
     @observable private _pageControls = false;
 
+    @computed get sidebarKey() {
+        return this.fieldKey + '_sidebar';
+    }
     @computed get pdfUrl() {
         return Cast(this.dataDoc[this._props.fieldKey], PdfField);
     }
@@ -232,14 +234,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options);
     };
 
-    getView = (doc: Doc, options: FocusViewOptions) => {
-        if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
-            options.didMove = true;
-            this.toggleSidebar(false);
+    getView = async (doc: Doc, options: FocusViewOptions) => {
+        if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+            SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options);
         }
-        return new Promise<Opt<DocumentView>>(res => {
-            DocumentView.addViewRenderedCb(doc, dv => res(dv));
-        });
+        return undefined;
     };
 
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index fa099178c..b3cb0e1db 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -338,12 +338,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null);
         const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
+        const docAnchor = () =>
+            Docs.Create.ConfigDocument({
+                title: '#' + timecode,
+                _timecodeToShow: timecode,
+                annotationOn: this.Document,
+            });
         if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent';
-        const anchor =
-            addAsAnnotation && marquee
-                ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document
-                : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document });
+        const visibleAnchor = () => addAsAnnotation && marquee && (CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document);
+        const anchor = visibleAnchor() || docAnchor();
         PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document);
+        addAsAnnotation && this.addDocument(anchor);
         return anchor;
     };
 
@@ -376,9 +381,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             }
             return this._stackedTimeline.getView(doc, options);
         }
-        return new Promise<Opt<DocumentView>>(res => {
-            DocumentView.addViewRenderedCb(doc, dv => res(dv));
-        });
+        return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
     };
 
     // extracts video thumbnails and saves them as field of doc
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 838dbea9d..779ccab40 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -13,7 +13,7 @@ import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { RefField } from '../../../fields/RefField';
 import { listSpec } from '../../../fields/Schema';
-import { Cast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types';
+import { Cast, DocCast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, stringHash } from '../../../Utils';
@@ -104,6 +104,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @computed get webField() {
         return Cast(this.Document[this._props.fieldKey], WebField)?.url;
     }
+    @computed get sidebarKey() {
+        return this.fieldKey + '_sidebar';
+    }
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -308,18 +311,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     @action
-    getView = (doc: Doc /* , options: FocusViewOptions */) => {
-        if (Doc.AreProtosEqual(doc, this.Document))
-            return new Promise<Opt<DocumentView>>(res => {
-                res(this.DocumentView?.());
-            });
+    getView = async (doc: Doc, options: FocusViewOptions) => {
+        if (Doc.AreProtosEqual(doc, this.Document)) return new Promise<Opt<DocumentView>>(res => res(this.DocumentView?.()));
+
         if (this.Document.layout_fieldKey === 'layout_icon') this.DocumentView?.().iconify();
         const webUrl = WebCast(doc.config_data)?.url;
         if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href);
-        if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
-        return new Promise<Opt<DocumentView>>(res => {
-            DocumentView.addViewRenderedCb(doc, dv => res(dv));
-        });
+
+        if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+            return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options);
+        }
+        return undefined;
     };
 
     sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
@@ -454,7 +456,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     iframeDown = (e: PointerEvent) => {
         this._textAnnotationCreator = undefined;
         const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection();
-        if (sel?.empty && !(e.target as any).textContent)
+        if (sel?.empty && !(e.target as HTMLElement).textContent)
             sel.empty(); // Chrome
         else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
 
@@ -565,7 +567,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 'click',
                 undoable(
                     action((e: MouseEvent) => {
-                        let eleHref = (e.target as any)?.outerHTML?.split('"="')[1]?.split('"')[0];
+                        let eleHref = (e.target as HTMLElement)?.outerHTML?.split('"="')[1]?.split('"')[0];
                         for (let ele = e.target as HTMLElement | Element | null; ele; ele = ele.parentElement) {
                             if ('href' in ele) {
                                 eleHref = (typeof ele.href === 'string' ? ele.href : eleHref) || (ele.parentElement && 'href' in ele.parentElement ? (ele.parentElement.href as string) : eleHref);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 57720baae..bab97f681 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -270,13 +270,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             e.preventDefault();
             e.stopPropagation();
             const targetCreator = (annotationOn?: Doc) => {
-                const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn);
+                const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
+                target.layout_fitWidth = true;
                 DocumentView.SetSelectOnLoad(target);
                 return target;
             };
 
+            const sourceAnchorCreator = () => this.getAnchor(true);
+
             const docView = this.DocumentView?.();
-            docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY);
+            docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY);
         });
 
         AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => {
@@ -1070,15 +1073,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     getView = (doc: Doc, options: FocusViewOptions) => {
         if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
-            if (!this.SidebarShown) {
-                this.toggleSidebar(false);
-                options.didMove = true;
-            }
-            setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc));
+            return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options);
         }
-        return new Promise<Opt<DocumentView>>(res => {
-            DocumentView.addViewRenderedCb(doc, dv => res(dv));
-        });
+        return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
     };
     focus = (textAnchor: Doc, options: FocusViewOptions) => {
         const focusSpeed = options.zoomTime ?? 500;
-- 
cgit v1.2.3-70-g09d2


From 2c26d7c96e56344a1a73a7c6f6dec3d6b0f0ee7e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 16 May 2025 11:46:22 -0400
Subject: changed isGroup to freeform_isGroup to fix issues with groups that
 are changed into other collection types.  fixed toggleTarget links to work
 when opening in a light box (previously they flickered on, then off).  fixed
 text boxes with a number field as data to act like a number input box.

---
 src/client/documents/Documents.ts                  |  2 +-
 src/client/util/DocumentManager.ts                 |  1 +
 src/client/views/DocumentDecorations.tsx           |  8 ++--
 src/client/views/PropertiesButtons.tsx             |  4 +-
 src/client/views/PropertiesView.tsx                |  6 +--
 src/client/views/StyleProvider.tsx                 |  8 ++--
 src/client/views/collections/CollectionView.tsx    |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 42 +++++++++--------
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |  1 -
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/KeyValueBox.tsx             |  8 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx | 12 ++---
 src/client/views/nodes/trails/PresBox.tsx          |  2 +-
 src/fields/Doc.ts                                  | 54 +++++++++++++---------
 15 files changed, 85 insertions(+), 71 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2df6f3e23..9aa1d53c2 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -361,7 +361,6 @@ export class DocumentOptions {
     isBaseProto?: BOOLt = new BoolInfo('is doc a base level prototype for data documents as opposed to data documents which are prototypes for layout documents.  base protos are not cloned during a deep');
     isTemplateForField?: string; // the field key for which the containing document is a rendering template
     isTemplateDoc?: BOOLt = new BoolInfo('is the document a template for creating other documents');
-    isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior');
     isFolder?: BOOLt = new BoolInfo('is document a tree view folder');
     _isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label');
     isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target');
@@ -401,6 +400,7 @@ export class DocumentOptions {
 
     // freeform properties
     freeform?: STRt = new StrInfo('');
+    freeform_isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior');
     _freeform_backgroundGrid?: BOOLt = new BoolInfo('whether background grid is shown on freeform collections');
     _freeform_scale_min?: NUMt = new NumInfo('how far out a view can zoom (used by image/videoBoxes that are clipped');
     _freeform_scale_max?: NUMt = new NumInfo('how far in a view can zoom (used by sidebar freeform views');
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 422613968..1c2ffab1b 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -274,6 +274,7 @@ export class DocumentManager {
             // even if we found the document view, if the target is a lightbox, we try to open it in the lightbox to preserve lightbox semantics (eg, there's only one active doc in the lightbox)
             const target = DocCast(targetDoc.annotationOn, targetDoc)!;
             const compView = this.getDocumentView(DocCast(target.embedContainer))?.ComponentView;
+            options.didMove = target.hidden || options.didMove ? true : false;
             if ((compView?.addDocTab ?? compView?._props.addDocTab)?.(target, options.openLocation)) {
                 await new Promise<void>(waitres => {
                     setTimeout(() => waitres());
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index ab665e984..1e7f4fb52 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -524,7 +524,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     resizeViewForOutpainting = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; shiftKey: boolean }) => {
         const doc = docView.Document;
 
-        if (doc.isGroup) {
+        if (Doc.IsFreeformGroup(doc)) {
             DocListCast(doc.data)
                 .map(member => DocumentView.getDocumentView(member, docView)!)
                 .forEach(member => this.resizeViewForOutpainting(member, refPt, scale, opts));
@@ -598,14 +598,14 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     //
     // determines if anything being dragged directly or via a group has a fixed aspect ratio (in which case we resize uniformly)
     //
-    hasFixedAspect = (doc: Doc): boolean => (doc.isGroup ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable));
+    hasFixedAspect = (doc: Doc): boolean => (Doc.IsFreeformGroup(doc) ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable));
 
     //
     // resize a single DocumentView about the specified reference point, possibly setting/updating the native dimensions of the Doc
     //
     resizeView = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; freezeNativeDims: boolean }) => {
         const doc = docView.Document;
-        if (doc.isGroup) {
+        if (Doc.IsFreeformGroup(doc)) {
             DocListCast(doc.data)
                 .map(member => DocumentView.getDocumentView(member, docView)!)
                 .forEach(member => this.resizeView(member, refPt, scale, opts));
@@ -737,7 +737,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             hideDecorations ||
             seldocview._props.hideOpenButton ||
             seldocview.Document.layout_hideOpenButton ||
-            DocumentView.Selected().some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.isGroup || docView.Document.layout_hideOpenButton) ||
+            DocumentView.Selected().some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.layout_hideOpenButton) ||
             this._isRounding ||
             this._isRotating;
         const hideDeleteButton =
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index ded342df0..2c84d7fe7 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -238,8 +238,8 @@ export class PropertiesButtons extends React.Component {
     //         on => `Display collection as a Group`,
     //         on => 'object-group',
     //         (dv, doc) => {
-    //             doc.isGroup = !doc.isGroup;
-    //             doc.forceActive = doc.isGroup;
+    //             docfreeform_isGroup = !docfreeform_isGroup;
+    //             doc.forceActive = docfreeform_isGroup;
     //         }
     //     );
     // }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index acf6f928a..06463b2a2 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -81,7 +81,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get selectedDoc() {
-        return DocumentView.SelectedSchemaDoc() || this.selectedDocumentView?.Document || Doc.ActiveDashboard;
+        return DocumentView.SelectedSchemaDoc() ?? this.selectedDocumentView?.Document ?? Doc.ActiveDashboard;
     }
 
     @computed get selectedLink() {
@@ -89,7 +89,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get selectedLayoutDoc() {
-        return DocumentView.SelectedSchemaDoc() || this.selectedDocumentView?.layoutDoc || Doc.ActiveDashboard;
+        return DocumentView.SelectedSchemaDoc() ?? this.selectedDocumentView?.layoutDoc ?? Doc.ActiveDashboard;
     }
     @computed get selectedDocumentView() {
         return DocumentView.Selected().lastElement();
@@ -149,7 +149,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         return this.selectedDoc?.type === DocumentType.INK;
     }
     @computed get isGroup() {
-        return this.selectedDoc?.isGroup;
+        return Doc.IsFreeformGroup(this.selectedDoc);
     }
     @computed get isStack() {
         return [
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 9110e198f..b52f17102 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -154,7 +154,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
                 if (highlightIndex) {
                     return {
-                        highlightStyle: doc.isGroup ? "dotted": highlightStyle,
+                        highlightStyle: Doc.IsFreeformGroup(doc) ? "dotted": highlightStyle,
                         highlightColor,
                         highlightIndex,
                         highlightStroke: BoolCast(layoutDoc?.layout_isSvg),
@@ -284,7 +284,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                             ? SnappingManager.userBackgroundColor
                             : doc?.annotationOn
                             ? '#00000010' // faint interior for collections on PDFs, images, etc
-                            : doc?.isGroup
+                            : doc && Doc.IsFreeformGroup(doc)
                             ? undefined
                             : doc?._type_collection === CollectionViewType.Stacking ?
                             (Colors.DARK_GRAY)
@@ -308,7 +308,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         doc?.layout_boxShadow,
                         doc?._type_collection === CollectionViewType.Pile
                             ? '4px 4px 10px 2px'
-                            : lockedPosition() || doc?.isGroup || LayoutTemplateString
+                            : lockedPosition() || (doc && Doc.IsFreeformGroup(doc)) || LayoutTemplateString
                             ? undefined // groups have no drop shadow -- they're supposed to be "invisible".  LayoutString's imply collection is being rendered as something else (e.g., title of a Slide)
                             : `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}`
                     );
@@ -338,7 +338,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
             if (SnappingManager.ExploreMode || doc?.layout_unrendered) return isInk() ? 'visiblePainted' : 'all';
             if (pointerEvents?.() === 'none') return 'none';
             if (opacity() === 0) return 'none';
-            if (isGroupActive?.() ) return isInk()  ? 'visiblePainted':  (doc?.isGroup ) ? undefined: 'all';
+            if (isGroupActive?.()) return isInk()  ? 'visiblePainted':  doc && Doc.IsFreeformGroup(doc) ? undefined: 'all';
             if (isDocumentActive?.()) return isInk() ? 'visiblePainted' : 'all';
             return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
         case StyleProp.Decorations: {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index eb9caf29d..72c8c3f8c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -113,7 +113,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
     };
 
     setupViewTypes(category: string, func: (type_collection: CollectionViewType) => Doc) {
-        if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking && !this.dataDoc.isGroup && !this.Document.annotationOn) {
+        if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking && !this.Document.annotationOn) {
             // prettier-ignore
             const subItems: ContextMenuProps[] = [
                 { description: 'Freeform',    event: () => func(CollectionViewType.Freeform), icon: 'signature' },
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 77615c650..3571dab1a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -176,7 +176,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return renderableEles;
     }
     @computed get fitContentsToBox() {
-        return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox) && !this.isAnnotationOverlay;
+        return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox || this.Document.freeform_isGroup) && !this.isAnnotationOverlay;
     }
     @computed get nativeWidth() {
         return this._props.NativeWidth?.() || Doc.NativeWidth(this.Document);
@@ -342,7 +342,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      */
     focusOnPoint = (options: FocusViewOptions) => {
         const { pointFocus, zoomTime, didMove } = options;
-        if (!this.Document.isGroup && pointFocus && !didMove) {
+        if (!this.Document.freeform_isGroup && pointFocus && !didMove) {
             const dfltScale = this.isAnnotationOverlay ? 1 : 0.25;
             if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) {
                 this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(pointFocus.X, pointFocus.Y), dfltScale, zoomTime);
@@ -380,7 +380,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      * @returns
      */
     focus = (anchor: Doc, options: FocusViewOptions) => {
-        if (anchor.isGroup && !options.docTransform && options.contextPath?.length) {
+        if (Doc.IsFreeformGroup(anchor) && !options.docTransform && options.contextPath?.length) {
             // don't focus on group if there's a context path because we're about to focus on a group item
             // which will override any group focus. (If we allowed the group to focus, it would mark didMove even if there were no net movement)
             return undefined;
@@ -395,7 +395,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         const xfToCollection = options?.docTransform ?? Transform.Identity();
         const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
-        const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc());
+        const cantTransform = this.fitContentsToBox || ((this.Document.freeform_isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc());
         const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? (options?.zoomScale ?? 0.75) : undefined);
 
         // focus on the document in the collection
@@ -514,7 +514,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._downTime = Date.now();
         const scrollMode = e.altKey ? (Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? freeformScrollMode.Zoom : freeformScrollMode.Pan) : Doc.UserDoc().freeformScrollMode;
         if (e.button === 0 && (!(e.ctrlKey && !e.metaKey) || scrollMode !== freeformScrollMode.Pan) && this._props.isContentActive()) {
-            if (!this.Document.isGroup) {
+            if (!this.Document.freeform_isGroup) {
                 // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
                 // prettier-ignore
                 const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
@@ -1271,7 +1271,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     zoom = (pointX: number, pointY: number, deltaY: number): void => {
-        if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
+        if (this.Document.freeform_isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
         let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05;
         if (deltaScale < 0) deltaScale = -deltaScale;
         const [x, y] = this.screenToFreeformContentsXf.transformPoint(pointX, pointY);
@@ -1298,7 +1298,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     onPointerWheel = (e: React.WheelEvent): void => {
-        if (this.Document.isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
+        if (this.Document.freeform_isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
         SnappingManager.TriggerUserPanned();
         if (this.layoutDoc._Transform || this.Document.treeView_OutlineMode === TreeViewType.outline) return;
         e.stopPropagation();
@@ -1422,7 +1422,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) {
-        if (this.Document.isGroup) return;
+        if (this.Document.freeform_isGroup) return;
         this.setPanZoomTransition(transitionTime);
         const screenXY = this.screenToFreeformContentsXf.inverse().transformPoint(docpt[0], docpt[1]);
         this.layoutDoc[this.scaleFieldKey] = scale;
@@ -1505,7 +1505,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     removeDocument = (docs: Doc | Doc[], annotationKey?: string | undefined) => {
         const ret = !!this._props.removeDocument?.(docs, annotationKey);
         // if this is a group and we have fewer than 2 Docs, then just promote what's left to our parent and get rid of the group.
-        if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.isGroup) {
+        if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.freeform_isGroup) {
             this.promoteCollection();
         }
         return ret;
@@ -1548,7 +1548,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 searchFilterDocs={this.searchFilterDocs}
                 isDocumentActive={childLayout.pointerEvents === 'none' ? returnFalse : this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive}
                 isContentActive={this.childContentsActive}
-                focus={this.Document.isGroup ? this.groupFocus : this.isAnnotationOverlay ? this._props.focus : this.focus}
+                focus={this.Document.freeform_isGroup ? this.groupFocus : this.isAnnotationOverlay ? this._props.focus : this.focus}
                 addDocTab={this.addDocTab}
                 addDocument={this._props.addDocument}
                 removeDocument={this.removeDocument}
@@ -1733,7 +1733,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         });
         PinDocView(
             anchor,
-            { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, collectionType: true, filters: true } },
+            { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.freeform_isGroup, collectionType: true, filters: true } },
             this.Document
         );
 
@@ -1767,7 +1767,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 this._firstRender = false;
                 this._disposers.groupBounds = reaction(
                     () => {
-                        if (this.Document.isGroup && this.childDocs.length === this.childDocList?.length) {
+                        if (this.Document.freeform_isGroup && this.childDocs.length === this.childDocList?.length) {
                             const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: NumCast(cd._width), height: NumCast(cd._height) }));
                             return aggregateBounds(clist, NumCast(this.layoutDoc._xMargin), NumCast(this.layoutDoc._yMargin));
                         }
@@ -1929,8 +1929,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
         const appearance = ContextMenu.Instance.findByDescription('Appearance...');
         const appearanceItems = appearance?.subitems ?? [];
-        !this.Document.isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' });
-        !this.Document.isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' });
+        !this.Document.freeform_isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' });
+        !this.Document.freeform_isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' });
         if (this._props.setContentViewBox === emptyFunction) {
             !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
             return;
@@ -1948,7 +1948,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: () => this.updateIcon(), icon: 'compress-arrows-alt' });
         this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
 
-        this.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
+        this.Document.freeform_isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
 
         !Doc.noviceMode ? appearanceItems.push({ description: 'Arrange contents in grid', event: this.layoutDocsInGrid, icon: 'table' }) : null;
         !Doc.noviceMode ? appearanceItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this._clusters.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null;
@@ -2007,7 +2007,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     transcribeStrokes = undoable(() => {
-        if (this.Document.isGroup && this.Document.transcription) {
+        if (this.Document.freeform_isGroup && this.Document.transcription) {
             const text = StrCast(this.Document.transcription);
             const lines = text.split('\n');
             const height = 30 + 15 * lines.length;
@@ -2025,7 +2025,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     dragStarting = (snapToDraggedDoc: boolean = false, showGroupDragTarget: boolean = true, visited = new Set<Doc>()) => {
         if (visited.has(this.Document)) return;
         visited.add(this.Document);
-        showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.isGroup));
+        showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.freeform_isGroup));
         const activeDocs = this.getActiveDocuments();
         const size = this.screenToFreeformContentsXf.transformDirection(this._props.PanelWidth(), this._props.PanelHeight());
         const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] };
@@ -2033,13 +2033,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const isDocInView = (doc: Doc, rect: { left: number; top: number; width: number; height: number }) => intersectRect(docDims(doc), rect);
 
         const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to
-        activeDocs.filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)).forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited));
+        activeDocs
+            .filter(doc => Doc.IsFreeformGroup(doc) && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))
+            .forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited));
 
         const horizLines: number[] = [];
         const vertLines: number[] = [];
         const invXf = this.screenToFreeformContentsXf.inverse();
         snappableDocs
-            .filter(doc => !doc.isGroup && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))))
+            .filter(doc => !Doc.IsFreeformGroup(doc) && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))))
             .forEach(doc => {
                 const { left, top, width, height } = docDims(doc);
                 const topLeftInScreen = invXf.transformPoint(left, top);
@@ -2131,7 +2133,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 {...this._props}
                 ref={this._marqueeViewRef}
                 Doc={this.Document}
-                ungroup={this.Document.isGroup ? this.promoteCollection : undefined}
+                ungroup={this.Document.freeform_isGroup ? this.promoteCollection : undefined}
                 nudge={this.isAnnotationOverlay || this._props.renderDepth > 0 ? undefined : this.nudge}
                 addDocTab={this.addDocTab}
                 slowLoadDocuments={this.slowLoadDocuments}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3cc7c0f2d..37959a5f0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -372,7 +372,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             ? creator(selected, { title: 'nested stack' })
             : ((doc: Doc) => {
                   doc.$data = new List<Doc>(selected);
-                  doc.$isGroup = makeGroup;
+                  doc.$freeform_isGroup = makeGroup;
                   doc.$title = makeGroup ? 'grouping' : 'nested freeform';
                   doc._freeform_panX = doc._freeform_panY = 0;
                   return doc;
@@ -508,7 +508,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             _layout_showSidebar: true,
             title: 'overview',
         });
-        const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
+        const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, freeform_isGroup: true, backgroundColor: 'transparent' });
         DocUtils.MakeLink(summary, portal, { link_relationship: 'summary of:summarized by' });
 
         portal.hidden = true;
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index c35a329c9..d30f00829 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -38,7 +38,6 @@ export class DocButtonState {
     // eslint-disable-next-line no-use-before-define
     public static _instance: DocButtonState | undefined;
     public static get Instance() {
-        // eslint-disable-next-line no-return-assign
         return DocButtonState._instance ?? (DocButtonState._instance = new DocButtonState());
     }
     constructor() {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b0415ef18..9b73cc073 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -744,7 +744,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
-        const noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
+        const noBackground = Doc.IsFreeformGroup(this.Document) && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
         return (
             <>
                 <div
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index aa66b5ba9..606f63d6d 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -273,15 +273,15 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     createFieldView = (templateDoc: Doc, row: KeyValuePair) => {
-        const metaKey = row._props.keyName;
-        const fieldTempDoc = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc);
-        fieldTempDoc.title = metaKey;
+        const keyName = row._props.keyName;
+        const fieldTempDoc = Doc.IsDelegateField(templateDoc, keyName) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc);
+        fieldTempDoc.title = keyName;
         fieldTempDoc.layout_fitWidth = true;
         fieldTempDoc._xMargin = 10;
         fieldTempDoc._yMargin = 10;
         fieldTempDoc._width = 100;
         fieldTempDoc._height = 40;
-        fieldTempDoc.layout = this.inferType(templateDoc[metaKey], metaKey);
+        fieldTempDoc.layout = this.inferType(templateDoc[keyName], keyName);
         return fieldTempDoc;
     };
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index bab97f681..4189fcc0c 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -428,10 +428,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const newAutoLinks = new Set<Doc>();
         const oldAutoLinks = Doc.Links(this.Document).filter(
             link => 
-                ((!Doc.isTemplateForField(this.Document) &&
-                    ((DocCast(link.link_anchor_1) && !Doc.isTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) &&
-                    ((DocCast(link.link_anchor_2) && !Doc.isTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) ||
-                 (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
+                ((!Doc.IsTemplateForField(this.Document) &&
+                    ((DocCast(link.link_anchor_1) && !Doc.IsTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) &&
+                    ((DocCast(link.link_anchor_2) && !Doc.IsTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) ||
+                 (Doc.IsTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
                 link.link_relationship === LinkManager.AutoKeywords
         ); // prettier-ignore
         if (this.EditorView?.state.doc.textContent) {
@@ -1216,7 +1216,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)?.[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0;
                 const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData;
                 const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData;
-                return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
+                return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? NumCast(whichData)?.toString() ?? StrCast(whichData)) };
             },
             incomingValue => {
                 if (this.EditorView && this.ApplyingChange !== this.fieldKey) {
@@ -1767,7 +1767,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
         if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) {
             const stordMarks = this.EditorView?.state.storedMarks?.slice();
-            if (!(this.EditorView?.state.selection instanceof NodeSelection)) {
+            if (!(this.EditorView?.state.selection instanceof NodeSelection) && typeof this.dataDoc[this.fieldKey] !== 'number') {
                 this.autoLink();
                 if (this.EditorView?.state.tr) {
                     const tr = stordMarks?.reduce((tr2, m) => {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 11f35b8ef..cb2a1f13f 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -702,7 +702,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 transTime + 10
             );
         }
-        if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !bestTarget.isGroup) {
+        if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !Doc.IsFreeformGroup(bestTarget)) {
             const contentBounds = Cast(activeItem.config_viewBounds, listSpec('number'));
             if (contentBounds) {
                 const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] };
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 0d11b9743..e110550e6 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -536,23 +536,35 @@ export namespace Doc {
     export function GetT<T extends FieldType>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
         return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
     }
-    export function isTemplateDoc(doc: Doc) {
-        return GetT(doc, 'isTemplateDoc', 'boolean', true);
-    }
-    export function isTemplateForField(doc: Doc) {
-        return GetT(doc, 'isTemplateForField', 'string', true);
-    }
-    export function IsDataProto(doc: Doc) {
-        return GetT(doc, 'isDataDoc', 'boolean', true);
-    }
-    export function IsBaseProto(doc: Doc) {
-        return GetT(doc, 'isBaseProto', 'boolean', true);
-    }
-    export function IsSystem(doc: Doc) {
-        return GetT(doc, 'isSystem', 'boolean', true);
-    }
-    export function IsDelegateField(doc: Doc, fieldKey: string) {
-        return doc && Get(doc, fieldKey, true) !== undefined;
+    /**
+     * Tests whether the Doc is flagged as being a template.
+     * Templates can be set as a layout for another target Doc. When rendered by the target Doc, the template
+     * creates an instance of itself that holds rendering data specific to the target
+     * @param doc
+     */
+    export function IsTemplateDoc(doc: Doc)      { return GetT(doc, 'isTemplateDoc', 'boolean', true); } // prettier-ignore
+    /**
+     * Tests whether the Doc is flagged as being a template for rendering a specific field  of a Doc
+     * When flagged as field template and rendered, the template will redirect its componentView to write to the
+     * specified template field.  In general, a compound Doc template will contain multiple field templates, one for each of the
+     * data fields rendered by the compound template.
+     * @param doc
+     * @returns
+     */
+    export function IsTemplateForField(doc: Doc) { return GetT(doc, 'isTemplateForField', 'string', true); } // prettier-ignore
+    export function IsDataProto(doc: Doc)        { return GetT(doc, 'isDataDoc', 'boolean', true); } // prettier-ignore
+    export function IsBaseProto(doc: Doc)        { return GetT(doc, 'isBaseProto', 'boolean', true); } // prettier-ignore
+    export function IsSystem(doc: Doc)           { return GetT(doc, 'isSystem', 'boolean', true); } // prettier-ignore
+    export function IsDelegateField(doc: Doc, fieldKey: string) { return doc && Get(doc, fieldKey, true) !== undefined; } // prettier-ignore
+    /**
+     * Tests whether a doc is a freeform collection that renders as a group.
+     * The group variant of a collection automatically resizes so that none of its contents
+     * are ever hidden.
+     * @param doc doc to test for being a freeform group
+     * @returns boolean
+     */
+    export function IsFreeformGroup(doc: Doc) {
+        return doc.freeform_isGroup && doc.type_collection === CollectionViewType.Freeform;
     }
     //
     // this will write the value to the key on either the data doc or the embedding doc.  The choice
@@ -734,7 +746,7 @@ export namespace Doc {
     const FindDocsInRTF = new RegExp(/(audioId|textId|anchorId|docId)"\s*:\s*"(.*?)"/g);
 
     export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], pruneDocs: Doc[], cloneLinks: boolean, cloneTemplates: boolean): Doc {
-        if (Doc.IsBaseProto(doc) || ((Doc.isTemplateDoc(doc) || Doc.isTemplateForField(doc)) && !cloneTemplates)) {
+        if (Doc.IsBaseProto(doc) || ((Doc.IsTemplateDoc(doc) || Doc.IsTemplateForField(doc)) && !cloneTemplates)) {
             return doc;
         }
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
@@ -808,7 +820,7 @@ export namespace Doc {
                 const docAtKey = DocCast(clone[key]);
                 if (docAtKey && !Doc.IsSystem(docAtKey)) {
                     if (!Array.from(cloneMap.values()).includes(docAtKey)) {
-                        clone[key] = !cloneTemplates && (Doc.isTemplateDoc(docAtKey) || Doc.isTemplateForField(docAtKey)) ? docAtKey : cloneMap.get(docAtKey[Id]);
+                        clone[key] = !cloneTemplates && (Doc.IsTemplateDoc(docAtKey) || Doc.IsTemplateForField(docAtKey)) ? docAtKey : cloneMap.get(docAtKey[Id]);
                     } else {
                         repairClone(docAtKey, cloneMap, cloneTemplates, visited);
                     }
@@ -859,7 +871,7 @@ export namespace Doc {
      */
     export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, layoutFieldKey?: string) {
         // nothing to do if the layout isn't a template or we don't have a target that's different than the template
-        if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.isTemplateForField(templateLayoutDoc) && !Doc.isTemplateDoc(templateLayoutDoc))) {
+        if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.IsTemplateForField(templateLayoutDoc) && !Doc.IsTemplateDoc(templateLayoutDoc))) {
             return templateLayoutDoc;
         }
 
@@ -922,7 +934,7 @@ export namespace Doc {
             console.log('Warning: GetLayoutDataDocPair childDoc not defined');
             return { layout: childDoc, data: childDoc };
         }
-        const data = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc;
+        const data = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.IsTemplateDoc(childDoc) && !Doc.IsTemplateForField(childDoc)) ? undefined : containerDataDoc;
         const templateRoot = DocCast(containerDoc?.rootDocument);
         return { layout: Doc.expandTemplateLayout(childDoc, templateRoot, layoutFieldKey), data };
     }
-- 
cgit v1.2.3-70-g09d2


From 51b4b64eae4749542590785bca9d33d3ccdded69 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 19 May 2025 19:43:28 -0400
Subject: fixed text boxes to calculate height correctly.  removed usertag
 modification time in text.  fixed masonry and stacking views with pivots.

---
 src/ClientUtils.ts                                 |   3 +-
 .../collections/CollectionMasonryViewFieldRow.tsx  |  42 +++---
 .../views/collections/CollectionStackingView.scss  |   5 +-
 .../views/collections/CollectionStackingView.tsx   | 154 ++++++++-------------
 .../CollectionStackingViewFieldColumn.tsx          |  30 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  25 ++--
 src/client/views/nodes/formattedText/marks_rts.ts  |  14 --
 7 files changed, 111 insertions(+), 162 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index cc8b715b4..8eb1a39ec 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -670,7 +670,8 @@ export function setupMoveUpEvents(
 }
 
 export function DivHeight(ele: HTMLElement | null): number {
-    return ele ? Number(getComputedStyle(ele).height.replace('px', '')) : 0;
+    const hgt = ele ? Number(getComputedStyle(ele).height.replace('px', '')) : 0;
+    return Number.isNaN(hgt) ? 0 : hgt;
 }
 export function DivWidth(ele: HTMLElement | null): number {
     return ele ? Number(getComputedStyle(ele).width.replace('px', '')) : 0;
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 89ccf5a0f..d1f7971d4 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -28,12 +28,14 @@ interface CMVFieldRowProps {
     headingObject: SchemaHeaderField | undefined;
     docList: Doc[];
     parent: CollectionStackingView;
+    panelWidth: () => number;
+    columnWidth: () => number;
     pivotField: string;
     type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
     createDropTarget: (ele: HTMLDivElement) => void;
     screenToLocalTransform: () => Transform;
     setDocHeight: (key: string, thisHeight: number) => void;
-    refList: Element[];
+    sectionRefs: Element[];
     showHandle: boolean;
 }
 
@@ -74,7 +76,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     createRowDropRef = (ele: HTMLDivElement | null) => {
         this._dropDisposer?.();
         if (ele) this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this), this._props.Doc);
-        else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        else if (this._ele) this.props.sectionRefs.splice(this.props.sectionRefs.indexOf(this._ele), 1);
         this._ele = ele;
     };
     @action
@@ -82,10 +84,10 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         this.heading = this._props.headingObject?.heading || '';
         this.color = this._props.headingObject?.color || '#f1efeb';
         this.collapsed = this._props.headingObject?.collapsed || false;
-        this._ele && this.props.refList.push(this._ele);
+        this._ele && this.props.sectionRefs.push(this._ele);
     }
     componentWillUnmount() {
-        this._ele && this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        this._ele && this.props.sectionRefs.splice(this.props.sectionRefs.indexOf(this._ele), 1);
         this._ele = null;
     }
 
@@ -128,10 +130,8 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         const key = this._props.pivotField;
         const castedValue = this.getValue(value);
         if (castedValue) {
-            if (this._props.parent.colHeaderData) {
-                if (this._props.parent.colHeaderData.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
-                    return false;
-                }
+            if (this._props.parent.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) || 0 > -1) {
+                return false;
             }
             key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true));
             this._heading = castedValue.toString();
@@ -251,20 +251,11 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     textCallback = (/* char: string */) => this.addDocument('', false);
 
     @computed get contentLayout() {
-        const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor((this._props.parent._props.PanelWidth() - 2 * this._props.parent.xMargin) / (this._props.parent.columnWidth + this._props.parent.gridGap))));
-        const showChrome = !this._props.chromeHidden;
-        const stackPad = showChrome ? `0px ${this._props.parent.xMargin}px` : `${this._props.parent.yMargin}px ${this._props.parent.xMargin}px 0px ${this._props.parent.xMargin}px `;
+        const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor(this._props.panelWidth() / this._props.columnWidth())));
         return this.collapsed ? null : (
             <div style={{ position: 'relative' }}>
-                {showChrome ? (
-                    <div
-                        className="collectionStackingView-addDocumentButton"
-                        style={
-                            {
-                                // width: style.columnWidth / style.numGroupColumns,
-                                // padding: `${NumCast(this._props.parent.layoutDoc._yPadding, this._props.parent.yMargin)}px 0px 0px 0px`,
-                            }
-                        }>
+                {!this._props.chromeHidden ? (
+                    <div className="collectionStackingView-addDocumentButton">
                         <EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents="+ NEW" />
                     </div>
                 ) : null}
@@ -272,11 +263,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
                     className="collectionStackingView-masonryGrid"
                     ref={this._contRef}
                     style={{
-                        padding: stackPad,
                         minHeight: this._props.showHandle && this._props.parent._props.isContentActive() ? '10px' : undefined,
-                        width: this._props.parent.NodeWidth,
                         gridGap: this._props.parent.gridGap,
-                        gridTemplateColumns: numberRange(rows).reduce(list => list + ` ${this._props.parent.columnWidth}px`, ''),
+                        gridTemplateColumns: numberRange(rows).reduce(list => list + ` ${this._props.columnWidth()}px`, ''),
                     }}>
                     {this._props.parent.children(this._props.docList)}
                 </div>
@@ -339,7 +328,12 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     render() {
         const background = this._background;
         return (
-            <div className="collectionStackingView-masonrySection" style={{ width: this._props.parent.NodeWidth, background }} ref={this.createRowDropRef} onPointerEnter={this.pointerEnteredRow} onPointerLeave={this.pointerLeaveRow}>
+            <div
+                className="collectionStackingView-masonrySection"
+                style={{ width: this._props.pivotField ? this._props.panelWidth() : '100%', background }}
+                ref={this.createRowDropRef}
+                onPointerEnter={this.pointerEnteredRow}
+                onPointerLeave={this.pointerLeaveRow}>
                 {this.headingView}
                 {this.contentLayout}
             </div>
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 05ac52ff9..d6e4943ff 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -2,10 +2,12 @@
 
 .collectionMasonryView {
     display: inline;
+    flex-wrap: wrap;
 }
 
 .collectionStackingView {
     display: flex;
+    justify-content: space-between;
 }
 
 .collectionStackingMasonry-cont {
@@ -56,7 +58,6 @@
         top: 0;
         overflow-y: auto;
         overflow-x: hidden;
-        flex-wrap: wrap;
         transition: top 0.5s;
 
         > div {
@@ -210,7 +211,6 @@
     .collectionStackingView-sectionHeader {
         text-align: center;
         margin: auto;
-        margin-bottom: 10px;
         background: global.$medium-gray;
         // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
 
@@ -367,7 +367,6 @@
     .collectionStackingView-addGroupButton {
         display: flex;
         overflow: hidden;
-        margin: auto;
         width: 90%;
         overflow: ellipses;
 
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 25a222cbb..cc514f262 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, Observ
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, DivHeight, returnNone, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, Field, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
@@ -35,6 +35,7 @@ import { CollectionStackingViewFieldColumn } from './CollectionStackingViewField
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { computedFn } from 'mobx-utils';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { FieldsDropdown } from '../FieldsDropdown';
 
 export type collectionStackingViewProps = {
     sortFunc?: (a: Doc, b: Doc) => number;
@@ -48,15 +49,15 @@ export type collectionStackingViewProps = {
 @observer
 export class CollectionStackingView extends CollectionSubView<Partial<collectionStackingViewProps>>() {
     _disposers: { [key: string]: IReactionDisposer } = {};
+    _addGroupRef = React.createRef<HTMLDivElement>();
     _masonryGridRef: HTMLDivElement | null = null;
     // used in a column dragger, likely due for the masonry grid view. We want to use this
     _draggerRef = React.createRef<HTMLDivElement>();
     // keeping track of documents. Updated on internal and external drops. What's the difference?
     _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = [];
-    // Doesn't look like this field is being used anywhere. Obsolete?
-    _columnStart: number = 0;
 
-    @observable _refList: HTMLElement[] = [];
+    @observable _colStackRefs: HTMLElement[] = [];
+    @observable _colHdrRefs: HTMLElement[] = [];
     // map of node headers to their heights. Used in Masonry
     @observable _heightMap = new Map<string, number>();
     // Assuming that this is the current css cursor style
@@ -67,10 +68,25 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     @computed get chromeHidden() {
         return this._props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
     }
-    // it looks like this gets the column headers that Mehek was showing just now
     @computed get colHeaderData() {
         return Cast(this.dataDoc['_' + this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
     }
+
+    @computed get Sections() {
+        return this.filteredChildren.reduce(
+            (map, d) => {
+                const docHeader = d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`;
+                const docHeaderString = docHeader !== undefined ? Field.toString(docHeader) : `NO ${this.pivotField.toUpperCase()} VALUE`;
+
+                // find existing header or create
+                const existingHeader = Array.from(map.keys()).find(sh => sh.heading === docHeaderString);
+                if (!existingHeader) map.set(new SchemaHeaderField(docHeaderString), [d]);
+                else map.get(existingHeader)!.push(d);
+                return map;
+            },
+            new ObservableMap<SchemaHeaderField, Doc[]>(this.colHeaderData?.map(hdata => [hdata, []] as [SchemaHeaderField, Doc[]]) ?? [])
+        );
+    }
     // Still not sure what a pivot is, but it appears that we can actually filter docs somehow?
     @computed get pivotField() {
         return StrCast(this.layoutDoc._pivotField);
@@ -107,9 +123,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     @computed get showAddAGroup() {
         return this.pivotField && !this.chromeHidden;
     }
+    @computed get availableWidth() {
+        return this._props.PanelWidth() - 2 * this.xMargin - (this.isStackingView ? this.gridGap * ((this.numGroupColumns || 1) - 1) : 0);
+    }
     // columnWidth handles the margin on the left and right side of the documents
     @computed get columnWidth() {
-        const availableWidth = this._props.PanelWidth() - 2 * this.xMargin;
+        const availableWidth = this.availableWidth;
         const cwid = availableWidth / (NumCast(this.Document._layout_columnCount) || this._props.PanelWidth() / NumCast(this.Document._layout_columnWidth, this._props.PanelWidth() / 4));
         return Math.min(availableWidth, this.isStackingView ? availableWidth / (this.numGroupColumns || 1) : cwid - this.gridGap);
     }
@@ -121,28 +140,17 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     constructor(props: SubCollectionViewProps) {
         super(props);
         makeObservable(this);
-        if (this.colHeaderData === undefined) {
-            // TODO: what is a layout doc? Is it literally how this document is supposed to be layed out?
-            // here we're making an empty list of column headers (again, what Mehek showed us)
-            this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
-        }
     }
 
+    availableWidthFn = () => this.availableWidth;
     columnWidthFn = () => this.columnWidth;
     columnDocHeightFn = (doc: Doc) => () => (this.isStackingView ? this.getDocHeight(doc)() : Math.min(this.getDocHeight(doc)(), this._props.PanelHeight()));
 
-    // TODO: plj - these are the children
     children = (docs: Doc[]) => {
-        // TODO: can somebody explain me to what exactly TraceMobX is?
         TraceMobx();
-        // appears that we are going to reset the _docXfs. TODO: what is Xfs?
         this._docXfs.length = 0;
-        this._renderCount < docs.length &&
-            setTimeout(
-                action(() => {
-                    this._renderCount = Math.min(docs.length, this._renderCount + 5);
-                })
-            );
+        this._renderCount < docs.length && 
+            setTimeout(action(() => (this._renderCount = Math.min(docs.length, this._renderCount + 5)))); // prettier-ignore
         return docs.map((d, i) => {
             // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns
             const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap);
@@ -153,76 +161,28 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                       margin: undefined,
                       transition: this.getDocTransition(d)(),
                       width: this.columnWidth,
-                      marginTop: i ? this.gridGap : 0,
                       height: this.getDocHeight(d)(),
                       zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0,
                   }
                 : { gridRowEnd: `span ${rowSpan}`, zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0 };
             // So we're choosing whether we're going to render a column or a masonry doc
             return (
-                <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
+                <div className={`collectionStackingView${this.isStackingView ? '-columnDoc' : '-masonryDoc'}`} key={d[Id]} style={style}>
                     {this.getDisplayDoc(d, this.getDocTransition(d), i)}
                 </div>
             );
         });
     };
     @action
-    setDocHeight = (key: string, sectionHeight: number) => {
-        this._heightMap.set(key, sectionHeight);
+    setDocHeight = (key: string, sectionHeight: number) => this._heightMap.set(key, sectionHeight);
+
+    setAutoHeight = () => {
+        const maxHeader = this.isStackingView ? this._colHdrRefs.reduce((p, r) => Math.max(p, DivHeight(r)), 0) + (this._colHdrRefs.length ? this.gridGap : 0) : 0;
+        const maxCol = this.isStackingView
+            ? this._colStackRefs.reduce((p, r) => Math.max(p, DivHeight(r)), 0) + this.gridGap
+            : this._colStackRefs.reduce((p, r) => p + DivHeight(r), this._addGroupRef.current ? DivHeight(this._addGroupRef.current) : 0);
+        this._props.setHeight?.(this.headerMargin + 2 * this.yMargin + maxCol + maxHeader);
     };
-
-    // is sections that all collections inherit? I think this is how we show the masonry/columns
-    // TODO: this seems important
-    get Sections() {
-        // appears that pivot field IS actually for sorting
-        if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
-
-        if (this.colHeaderData === undefined) {
-            setTimeout(() => {
-                this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
-            });
-            return new Map<SchemaHeaderField, Doc[]>();
-        }
-        const colHeaderData = Array.from(this.colHeaderData);
-        const fields = new Map<SchemaHeaderField, Doc[]>(colHeaderData.map(sh => [sh, []] as [SchemaHeaderField, []]));
-        let changed = false;
-        this.filteredChildren.forEach(d => {
-            const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object;
-            // the next five lines ensures that floating point rounding errors don't create more than one section -syip
-            const parsed = parseInt(sectionValue.toString());
-            const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue;
-
-            // look for if header exists already
-            const existingHeader = colHeaderData.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`));
-            if (existingHeader) {
-                fields.get(existingHeader)!.push(d);
-            } else {
-                const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`);
-                fields.set(newSchemaHeader, [d]);
-                colHeaderData.push(newSchemaHeader);
-                changed = true;
-            }
-        });
-        // remove all empty columns if hideHeadings is set
-        // we will want to have something like this, so that we can hide columns and add them back in
-        if (this.layoutDoc._columnsHideIfEmpty) {
-            Array.from(fields.keys())
-                .filter(key => !fields.get(key)!.length)
-                .forEach(header => {
-                    fields.delete(header);
-                    colHeaderData.splice(colHeaderData.indexOf(header), 1);
-                    changed = true;
-                });
-        }
-        changed &&
-            setTimeout(
-                action(() => this.colHeaderData?.splice(0, this.colHeaderData.length, ...colHeaderData)),
-                0
-            );
-        return fields;
-    }
-
-    setAutoHeight = () => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : 2 * this.yMargin + this._refList.reduce((p, r) => p + DivHeight(r), 0)));
     observer = new ResizeObserver(this.setAutoHeight);
 
     componentDidMount() {
@@ -232,9 +192,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         // reset section headers when a new filter is inputted
         this._disposers.pivotField = reaction(
             () => this.pivotField,
-            () => {
-                this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List();
-            }
+            () => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List())
         );
         // reset section headers when a new filter is inputted
         this._disposers.width = reaction(
@@ -252,7 +210,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         );
 
         this._disposers.refList = reaction(
-            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
+            () => ({ refList: this._colStackRefs.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
             ({ refList, autoHeight }) => {
                 this.observer.disconnect();
                 if (autoHeight) refList.forEach(r => this.observer.observe(r));
@@ -409,11 +367,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     getDocWidth = computedFn((d?: Doc) => () => {
         if (!d) return 0;
         const childLayoutDoc = Doc.LayoutDoc(d, this._props.childLayoutTemplate?.());
-        const maxWidth = this.columnWidth / this.numGroupColumns;
         if (!this.layoutDoc._columnsFill && !this.childFitWidth(childLayoutDoc)) {
-            return Math.min(NumCast(d._width), maxWidth);
+            return Math.min(NumCast(d._width), this.columnWidth);
         }
-        return maxWidth;
+        return this.columnWidth;
     });
     getDocTransition = computedFn((d?: Doc) => () => StrCast(d?.dataTransition));
     getDocHeight = computedFn((d?: Doc) => () => {
@@ -424,7 +381,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._width) : 0);
         const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._height) : 0);
         if (nw && nh) {
-            const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
+            const colWid = this.columnWidth;
             const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d)(), colWid);
             return Math.min(maxHeight, (docWid * nh) / nw);
         }
@@ -571,7 +528,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         }
         return (
             <CollectionStackingViewFieldColumn
-                refList={this._refList}
+                colStackRefs={this._colStackRefs}
+                colHeaderRefs={this._colHdrRefs}
                 addDocument={this.addDocument}
                 chromeHidden={this.chromeHidden}
                 colHeaderData={this.colHeaderData}
@@ -613,8 +571,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                         showHandle={first}
                         Doc={this.Document}
                         chromeHidden={this.chromeHidden}
+                        panelWidth={this.availableWidthFn}
+                        columnWidth={this.columnWidthFn}
                         pivotField={this.pivotField}
-                        refList={this._refList}
+                        sectionRefs={this._colStackRefs}
                         key={heading ? heading.heading : ''}
                         rows={rows}
                         headings={this.headings}
@@ -636,8 +596,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     @action
     addGroup = (value: string) => {
         if (value && this.colHeaderData) {
-            const schemaHdrField = new SchemaHeaderField(value);
-            this.colHeaderData.push(schemaHdrField);
+            this.colHeaderData.push(new SchemaHeaderField(value));
             return true;
         }
         return false;
@@ -745,7 +704,11 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                             this.fixWheelEvents(ele, this._props.isContentActive);
                         }}
                         style={{
-                            overflowY: this.isContentActive() ? 'auto' : 'hidden',
+                            paddingBottom: this.yMargin,
+                            paddingTop: this.yMargin,
+                            paddingLeft: this.xMargin,
+                            paddingRight: this.xMargin,
+                            overflowY: this.isContentActive() && !this.layoutDoc._layout_autoHeight ? 'auto' : 'hidden',
                             background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string,
                             pointerEvents: this._props.pointerEvents?.() ?? this.backgroundEvents,
                         }}
@@ -756,11 +719,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                         onContextMenu={this.onContextMenu}
                         onWheel={e => this.isContentActive() && e.stopPropagation()}>
                         {this.renderedSections}
-                        {!this.showAddAGroup ? null : (
-                            <div key={`${this.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? '100%' : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
-                                <EditableView {...editableViewProps} />
-                            </div>
-                        )}
+                        <div className="collectionStackingView-addGroupButton" ref={this._addGroupRef} style={{ width: !this.isStackingView ? '100%' : this.columnWidth, display: this.showAddAGroup ? undefined : 'none' }}>
+                            <EditableView {...editableViewProps} />
+                        </div>
+                        <div style={{ right: 0, top: 0, position: 'absolute', display: !this.layoutDoc._pivotField ? 'none' : undefined }}>
+                            <FieldsDropdown Doc={this.Document} selectFunc={fieldKey => (this.layoutDoc._pivotField = fieldKey)} placeholder={StrCast(this.layoutDoc._pivotField)} />
+                        </div>
                     </div>
                 </div>
             </>
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 345f60e75..8c7cb8276 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { DivHeight, DivWidth, returnEmptyString, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
@@ -50,14 +50,14 @@ interface CSVFieldColumnProps {
     addDocument: (doc: Doc | Doc[]) => boolean;
     createDropTarget: (ele: HTMLDivElement) => void;
     screenToLocalTransform: () => Transform;
-    refList: HTMLElement[];
+    colStackRefs: HTMLElement[];
+    colHeaderRefs: HTMLElement[];
 }
 
 @observer
 export class CollectionStackingViewFieldColumn extends ObservableReactComponent<CSVFieldColumnProps> {
     private dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [name: string]: IReactionDisposer } = {};
-    private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
     @observable _background = 'inherit';
     @observable _paletteOn = false;
     @observable _heading = '';
@@ -72,6 +72,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
 
     _ele: HTMLElement | null = null;
     _eleMasonrySingle = React.createRef<HTMLDivElement>();
+    _headerRef: HTMLDivElement | null = null;
 
     protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => {
         const dragData = de.complete.docDragData;
@@ -93,13 +94,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     createColumnDropRef = (ele: HTMLDivElement | null) => {
         this.dropDisposer?.();
         if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Doc, this.onInternalPreDrop.bind(this));
-        else if (this._eleMasonrySingle.current) this.props.refList.splice(this.props.refList.indexOf(this._eleMasonrySingle.current), 1);
+        else if (this._eleMasonrySingle.current) runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._eleMasonrySingle.current!), 1));
         this._ele = ele;
     };
 
     @action
     componentDidMount() {
-        this._eleMasonrySingle.current && this.props.refList.push(this._eleMasonrySingle.current);
+        this._eleMasonrySingle.current && this.props.colStackRefs.push(this._eleMasonrySingle.current);
         this._disposers.collapser = reaction(
             () => this._props.headingObject?.collapsed,
             collapsed => { this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false; }, // prettier-ignore
@@ -108,7 +109,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     }
     componentWillUnmount() {
         this._disposers.collapser?.();
-        this._ele && this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        this._ele && runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._ele!), 1));
         this._ele = null;
     }
 
@@ -192,7 +193,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         value = typeof value === 'string' ? `"${value}"` : value;
         embedding.viewSpecScript = ScriptField.MakeFunction(`doc.${this._props.pivotField} === ${value}`, { doc: Doc.name });
         if (embedding.viewSpecScript) {
-            DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY);
+            DragManager.StartDocumentDrag([this._headerRef!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY);
             return true;
         }
         return false;
@@ -314,9 +315,14 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
             <div
                 key={heading}
                 className="collectionStackingView-sectionHeader"
-                ref={this._headerRef}
+                ref={r => {
+                    if (this._headerRef && this._props.colHeaderRefs.includes(this._headerRef)) this._props.colHeaderRefs.splice(this._props.colStackRefs.indexOf(this._headerRef), 1);
+                    r && this._props.colHeaderRefs.push(r);
+                    this._headerRef = r;
+                }}
                 style={{
-                    marginTop: this._props.yMargin,
+                    marginTop: 0,
+                    marginBottom: this._props.gridGap,
                     width: this._props.columnWidth,
                 }}>
                 {/* the default bucket (no key value) has a tooltip that describes what it is.
@@ -367,7 +373,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                             ref={this._eleMasonrySingle}
                             className="collectionStackingView-masonrySingle"
                             style={{
-                                padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`,
+                                padding: `${columnYMargin}px ${0}px ${0}px ${0}px`,
                                 margin: this._props.dontCenter.includes('x') ? undefined : 'auto',
                                 height: 'max-content',
                                 position: 'relative',
@@ -400,15 +406,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
 
     render() {
         TraceMobx();
-        const headings = this._props.headings();
         const heading = this._heading;
-        const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
         return (
             <div
                 className="collectionStackingViewFieldColumn"
                 key={heading}
                 style={{
-                    width: `${100 / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1)}%`,
+                    width: this._props.columnWidth,
                     height: undefined,
                     background: this._background,
                 }}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 4189fcc0c..fea2986c7 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1522,7 +1522,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
         if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
             const { $from } = this.EditorView.state.selection;
-            const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
+            const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() });
             const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
             const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
             if (selLoadChar === 'Enter') {
@@ -1549,7 +1549,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const { text, paragraph } = schema.nodes;
             const selNode = this.EditorView.state.selection.$anchor.node();
             if (this.EditorView.state.selection.from === 1 && this.EditorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
-                const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })];
+                const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() })];
                 this.EditorView.state.selection.empty && this.EditorView.state.selection.from === 1 && this.EditorView?.dispatch(this.EditorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor));
             }
         }
@@ -1685,7 +1685,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 if (e.clientX > boundsRect.left && e.clientX < boundsRect.right && e.clientY > boundsRect.bottom) {
                     // if we clicked below the last prosemirror div, then set the selection to be the end of the document
                     editorView.focus();
-                    editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size)));
+                    // editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size)));
                 }
             } else if (node && [editorView.state.schema.nodes.ordered_list, editorView.state.schema.nodes.listItem].includes(node.type) && node !== (editorView.state.selection as NodeSelection)?.node && pcords) {
                 editorView.dispatch(editorView.state.tr.setSelection(NodeSelection.create(editorView.state.doc, pcords.pos)));
@@ -1814,7 +1814,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             return;
         }
         if (this._enteringStyle && 'tix!'.includes(e.key)) {
-            const tag = e.key === 't' ? 'todo' : e.key === 'i' ? 'ignore' : e.key === 'x' ? 'disagree' : e.key === '!' ? 'important' : '??';
             const node = state.selection.$from.nodeAfter;
             const start = state.selection.from;
             const end = state.selection.to;
@@ -1823,9 +1822,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 StopEvent(e);
                 _editorView.dispatch(
                     state.tr
-                        .removeMark(start, end, schema.marks.user_mark)
-                        .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }))
-                        .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag, modified: Math.round(Date.now() / 1000 / 60) }))
+                        .removeMark(start, end, schema.marks.user_mark) //
+                        .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }))
                 );
                 return;
             }
@@ -1858,9 +1856,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 break;
             default:
                 if ([AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document))) {
-                    const modified = Math.floor(Date.now() / 1000);
-                    const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark && m.attrs.modified === modified);
-                    _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified })));
+                    const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark);
+                    _editorView.dispatch(
+                        state.tr
+                            .removeStoredMark(schema.marks.user_mark) //
+                            .addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }))
+                    );
                 }
                 break;
         }
@@ -1884,14 +1885,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yMargin || 0);
         const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
         if (this.EditorView && children && !SnappingManager.IsDragging) {
-            const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
+            const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), 0) ?? 0;
             const toNum = (val: string) => Number(val.replace('px', ''));
             const toHgt = (node: Element): number => {
                 const { height, marginTop, marginBottom } = getComputedStyle(node);
                 const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height);
                 return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom));
             };
-            const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children);
+            const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children) + margins;
             const scrollHeight = this.ProseRef && proseHeight;
             if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) {
                 // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index b7dae1ca3..333ee6be8 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -330,20 +330,6 @@ export const marks: { [index: string]: MarkSpec } = {
             return ['span', { class: 'UM-' + uid + remote + ' UM-min-' + min + ' UM-hr-' + hr + ' UM-day-' + day }, 0];
         },
     },
-    // the id of the user who entered the text
-    user_tag: {
-        attrs: {
-            userid: { default: '' },
-            modified: { default: 'when?' }, // 1 second intervals since 1970
-            tag: { default: '' },
-        },
-        group: 'inline',
-        inclusive: false,
-        toDOM: node => {
-            const uid = node.attrs.userid.replace('.', '').replace('@', '');
-            return ['span', { class: 'UT-' + uid + ' UT-' + node.attrs.tag }, 0];
-        },
-    },
 
     // :: MarkSpec Code font mark. Represented as a `<code>` element.
     code: {
-- 
cgit v1.2.3-70-g09d2


From 3efe20d11332df95237c13a11fa99918552ed290 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 27 May 2025 11:29:47 -0400
Subject: fixed resetting autoheight by dragging up to work when nativewidth's
 are set.  fixed autoheight on text to work properly when native dims are set.
  fixed templates to support native witdths and heights.

---
 src/client/views/DocumentDecorations.tsx           | 20 ++++++++---------
 .../views/nodes/formattedText/FormattedTextBox.tsx | 25 ++++++++++++----------
 src/fields/Doc.ts                                  |  5 ++---
 3 files changed, 26 insertions(+), 24 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index a94bee7cb..69c2467a3 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -607,8 +607,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             if (nwidth && nheight && !cornerReflow && !horizontalReflow && !verticalReflow) {
                 scale.x === 1 ? (scale.x = scale.y) : (scale.y = scale.x);
             }
+            if (NumCast(doc._height) * scale.y < NumCast(doc._height_min, 10)) scale.y = NumCast(doc._height_min, 10) / NumCast(doc._height);
+            if (NumCast(doc._width) * scale.x < NumCast(doc._width_min, 25)) scale.x = NumCast(doc._width_min, 25) / NumCast(doc._width);
 
-            if ((horizontalReflow || cornerReflow) && Doc.NativeWidth(doc)) {
+            if ((horizontalReflow || cornerReflow) && Doc.NativeWidth(doc) && scale.x > 0) {
                 const setData = Doc.NativeWidth(doc[DocData]) === doc.nativeWidth;
                 doc._nativeWidth = scale.x * Doc.NativeWidth(doc);
                 if (setData) Doc.SetNativeWidth(doc[DocData], NumCast(doc.nativeWidth));
@@ -616,25 +618,23 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                     doc._nativeHeight = (initHeight / initWidth) * nwidth; // initializes the nativeHeight for a PDF
                 }
             }
-            if ((verticalReflow || cornerReflow) && Doc.NativeHeight(doc)) {
+            if ((verticalReflow || cornerReflow) && Doc.NativeHeight(doc) && scale.y > 0) {
                 const setData = Doc.NativeHeight(doc[DocData]) === doc.nativeHeight && !doc.layout_reflowVertical;
                 doc._nativeHeight = scale.y * Doc.NativeHeight(doc);
                 if (setData) Doc.SetNativeHeight(doc[DocData], NumCast(doc._nativeHeight));
             }
 
-            doc._width = Math.max(NumCast(doc._width_min, 25), NumCast(doc._width) * scale.x);
-            doc._height = Math.max(NumCast(doc._height_min, 10), NumCast(doc._height) * scale.y);
+            doc._width = NumCast(doc._width) * scale.x;
+            doc._height = NumCast(doc._height) * scale.y;
             const { deltaX, deltaY } = this.realignRefPt(doc, refCent, initWidth || 1, initHeight || 1);
             doc.x = NumCast(doc.x) + deltaX;
             doc.y = NumCast(doc.y) + deltaY;
 
             doc._layout_modificationDate = new DateField();
-            if (scale.y !== 1) {
-                const docLayout = docView.layoutDoc;
-                docLayout._layout_autoHeight = undefined;
-                if (docView.layoutDoc._layout_autoHeight) {
-                    // if autoHeight is still on because of a prototype
-                    docLayout._layout_autoHeight = false; // then don't inherit, but explicitly set it to false
+            if (scale.y !== 1 && !opts.freezeNativeDims) {
+                doc._layout_autoHeight = undefined;
+                if (doc._layout_autoHeight) {
+                    doc._layout_autoHeight = false; // set explicitly to false if inherited from parent of layout
                 }
             }
         }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fea2986c7..1827cd2dc 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1142,13 +1142,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         return undefined;
     };
 
-    // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc.
-    // Since we also monitor all component height changes, this will update the document's height.
-    resetNativeHeight = action((scrollHeight: number) => {
-        this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight;
-        if (!this.layoutDoc.isTemplateForField && NumCast(this.layoutDoc._nativeHeight)) this.layoutDoc._nativeHeight = scrollHeight;
-    });
-
     addPlugin = (plugin: Plugin) => {
         const editorView = this.EditorView;
         if (editorView) {
@@ -1179,21 +1172,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         this._disposers.width = reaction(this._props.PanelWidth, this.tryUpdateScrollHeight);
         this._disposers.scrollHeight = reaction(
             () => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }),
-            ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && this.resetNativeHeight(scrollHeight),
+            ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && (this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight),
             { fireImmediately: true }
         );
         this._disposers.componentHeights = reaction(
             // set the document height when one of the component heights changes and layout_autoHeight is on
-            () => ({ border: this._props.PanelHeight(), sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
-            ({ border, sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
+            () => ({
+                border: this._props.PanelHeight(),
+                scrollHeight: NumCast(this.layoutDoc['_' + this.fieldKey + '_height']),
+                sidebarHeight: this.sidebarHeight,
+                textHeight: this.textHeight,
+                layoutAutoHeight: this.layout_autoHeight,
+                marginsHeight: this.layout_autoHeightMargins,
+            }),
+            ({ border, sidebarHeight, scrollHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
                 const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
                 if (
                     (!Array.from(this._curHighlights).includes('Bold Text') || this._props.isSelected()) && //
                     layoutAutoHeight &&
                     newHeight &&
-                    (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height)) &&
+                    (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height) || this.layoutDoc._nativeHeight !== scrollHeight) &&
                     !this._props.dontRegisterView
                 ) {
+                    if (NumCast(this.layoutDoc.nativeHeight)) {
+                        this.layoutDoc._nativeHeight = scrollHeight;
+                    }
                     this._props.setHeight?.(newHeight);
                 }
             },
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index e110550e6..366dc6a20 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1202,14 +1202,14 @@ export namespace Doc {
     }
     export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
         // if this is a field template, then don't use the doc's nativeWidth/height
-        return !doc ? 0 : NumCast(doc.isTemplateForField ? undefined : doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], !doc.isTemplateForField && useWidth ? NumCast(doc._width) : 0));
+        return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], useWidth ? NumCast(doc._width) : 0));
     }
     export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
         if (!doc) return 0;
         const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); // divide before multiply to avoid floating point errrorin case nativewidth = width
         const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0);
         // if this is a field template, then don't use the doc's nativeWidth/height
-        return NumCast(doc.isTemplateForField ? undefined : doc._nativeHeight, nheight || dheight);
+        return NumCast(doc._nativeHeight, nheight || dheight);
     }
 
     export function OutpaintingWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
@@ -1479,7 +1479,6 @@ export namespace Doc {
                 layoutDoc._nativeWidth = undefined;
                 layoutDoc._nativeHeight = undefined;
             } else {
-                layoutDoc._layout_autoHeight = false;
                 if (!Doc.NativeWidth(layoutDoc)) {
                     layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth);
                     layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight);
-- 
cgit v1.2.3-70-g09d2


From 76a21badf70c82388872ec5485858ab06e303bca Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 30 May 2025 09:57:46 -0400
Subject: fixed using empty images in templates to support assigning a new
 image and getting native dimensions right.  fixed native dimensions of images
 within templates to not use native dims of template. fixed undo for text
 templates that don't have a text box.  fixed image dropping on images that
 are empty (to assign the dropped image).

---
 src/client/documents/DocUtils.ts                   | 20 +++---
 .../collections/collectionFreeForm/MarqueeView.tsx |  1 +
 src/client/views/nodes/EquationBox.tsx             |  9 +--
 src/client/views/nodes/ImageBox.tsx                | 84 +++++++++++++---------
 src/client/views/nodes/LabelBox.tsx                |  8 ++-
 .../views/nodes/formattedText/FormattedTextBox.tsx | 11 +--
 src/fields/Doc.ts                                  |  4 +-
 7 files changed, 80 insertions(+), 57 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 41a61b154..5aae50ee8 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -642,27 +642,27 @@ export namespace DocUtils {
         return dd;
     }
 
-    export function assignUploadInfo(result: Upload.FileInformation, protoIn: Doc) {
+    export function assignUploadInfo(result: Upload.FileInformation, protoIn: Doc, fieldKey: string = 'data') {
         const proto = protoIn;
 
         if (Upload.isTextInformation(result)) {
             proto.text = result.rawText;
         }
         if (Upload.isVideoInformation(result)) {
-            proto.data_duration = result.duration;
+            proto[`${fieldKey}_duration`] = result.duration;
         }
         if (Upload.isImageInformation(result)) {
             const maxNativeDim = Math.max(result.nativeHeight, result.nativeWidth);
             const exifRotation = StrCast(result.exifData?.data?.Orientation).toLowerCase();
-            proto.data_nativeOrientation = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined);
-            proto.data_nativeWidth = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
-            proto.data_nativeHeight = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
-            if (NumCast(proto.data_nativeOrientation) >= 5) {
-                proto.data_nativeHeight = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
-                proto.data_nativeWidth = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
+            proto[`${fieldKey}_nativeOrientation`] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined);
+            proto[`${fieldKey}_nativeWidth`] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
+            proto[`${fieldKey}_nativeHeight`] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
+            if (NumCast(proto[`${fieldKey}_nativeOrientation`]) >= 5) {
+                proto[`${fieldKey}_nativeHeight`] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
+                proto[`${fieldKey}_nativeWidth`] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
             }
-            proto.data_exif = JSON.stringify(result.exifData?.data);
-            proto.data_contentSize = result.contentSize;
+            proto[`${fieldKey}_exif`] = JSON.stringify(result.exifData?.data);
+            proto[`${fieldKey}_contentSize`] = result.contentSize;
             // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates
             const latitude = result.exifData?.data?.GPSLatitude;
             const latitudeDirection = result.exifData?.data?.GPSLatitudeRef;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 37959a5f0..c120cddf0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -190,6 +190,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this._props.childLayoutString ? e.key : '';
             FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note');
             this._props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100));
+            setTimeout(() => FormattedTextBox.LiveTextUndo?.end(), 100);
             e.stopPropagation();
         }
     };
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 3cacb6692..8e48a0b54 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -6,7 +6,7 @@ import { TraceMobx } from '../../../fields/util';
 import { DocUtils } from '../../documents/DocUtils';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
-import { undoBatch } from '../../util/UndoManager';
+import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { StyleProp } from '../StyleProp';
 import { DocumentView } from './DocumentView';
@@ -21,6 +21,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return FieldView.LayoutString(EquationBox, fieldKey);
     }
     _ref: React.RefObject<EquationEditor> = React.createRef();
+    _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -30,6 +31,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
     componentDidMount() {
         this._props.setContentViewBox?.(this);
         if (DocumentView.SelectOnLoad === this.Document && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) {
+            this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+            FormattedTextBox.LiveTextUndo = undefined;
             this._props.select(false);
 
             this._ref.current?.mathField.focus();
@@ -113,9 +116,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 className="equationBox-cont"
                 onKeyDown={e => e.stopPropagation()}
                 onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
-                onBlur={() => {
-                    FormattedTextBox.LiveTextUndo?.end();
-                }}
+                onBlur={() => this._liveTextUndo?.end()}
                 style={{
                     transform: `scale(${scale})`,
                     minWidth: `${100 / scale}%`,
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 6380dec17..30fc44f62 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -112,6 +112,28 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._props.setContentViewBox?.(this);
     }
 
+    @computed get oupaintOriginalSize(): { width: number; height: number } {
+        return {
+            width: NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']),
+            height: NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']),
+        };
+    }
+    set outpaintOriginalSize(prop: { width: number; height: number } | undefined) {
+        this.Document[this.fieldKey + '_outpaintOriginalWidth'] = prop?.width;
+        this.Document[this.fieldKey + '_outpaintOriginalHeight'] = prop?.height;
+    }
+
+    @computed get imgNativeSize() {
+        return {
+            nativeWidth: NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)),
+            nativeHeight: NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)),
+        };
+    }
+    set imgNativeSize(prop: { nativeWidth: number; nativeHeight: number }) {
+        this.dataDoc[this.fieldKey + '_nativeWidth'] = prop.nativeWidth;
+        this.dataDoc[this.fieldKey + '_nativeHeight'] = prop.nativeHeight;
+    }
+
     protected createDropTarget = (ele: HTMLDivElement) => {
         this._mainCont = ele;
         this._dropDisposer?.();
@@ -153,7 +175,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }),
             ({ nativeSize, width, height }) => {
                 if (!this.layoutDoc._layout_nativeDimEditable || !height || this.layoutDoc.layout_resetNativeDim) {
-                    this.layoutDoc.layout_resetNativeDim = undefined; // template images need to reset their dimensions when they are rendered with content.  afterwards, remove this flag.
+                    this.layoutDoc.layout_resetNativeDim = undefined; // reset dimensions of templates rendered with content or if image changes.  afterwards, remove this flag.
                     this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
                 }
             },
@@ -170,7 +192,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             { fireImmediately: true }
         );
         this._disposers.outpaint = reaction(
-            () => this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined && !SnappingManager.ShiftKey,
+            () => this.outpaintOriginalSize?.width && !SnappingManager.ShiftKey,
             complete => complete && this.openOutpaintPrompt(),
             { fireImmediately: true }
         );
@@ -231,14 +253,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     DrawingFillHandler.drawingToImage(this.Document, 90, newPrompt(descText), drag)?.then(action(() => (this._regenerateLoading = false)));
                     added = false;
                 } else if (de.altKey || !this.dataDoc[this.fieldKey]) {
-                    const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
-                    const targetField = Doc.LayoutDataKey(layoutDoc);
-                    const targetDoc = layoutDoc[DocData];
-                    if (targetDoc[targetField] instanceof ImageField) {
+                    const dropDoc = de.complete.docDragData?.draggedDocuments[0];
+                    const dropDocFieldKey = Doc.LayoutDataKey(dropDoc);
+                    const dropDataDoc = dropDoc[DocData];
+                    if (dropDataDoc[dropDocFieldKey] instanceof ImageField) {
                         added = true;
-                        this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
-                        Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey);
-                        Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey);
+                        this.dataDoc.layout_resetNativeDim = true;
+                        this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(dropDataDoc[dropDocFieldKey] as ImageField);
+                        Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(dropDataDoc), this.fieldKey);
+                        Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(dropDataDoc), this.fieldKey);
                     }
                 }
                 added === false && e.preventDefault();
@@ -257,18 +280,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     @undoBatch
     setNativeSize = action(() => {
-        const oldnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+        const oldnativeWidth = this.imgNativeSize.nativeWidth;
         const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1);
         const nw = nscale / oldnativeWidth;
-        this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw;
-        this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw;
+        this.imgNativeSize = { nativeWidth: this.imgNativeSize.nativeWidth * nw, nativeHeight: this.imgNativeSize.nativeHeight * nw };
         this.dataDoc.freeform_panX = nw * NumCast(this.dataDoc.freeform_panX);
         this.dataDoc.freeform_panY = nw * NumCast(this.dataDoc.freeform_panY);
         this.dataDoc.freeform_panX_max = this.dataDoc.freeform_panX_max ? nw * NumCast(this.dataDoc.freeform_panX_max) : undefined;
         this.dataDoc.freeform_panX_min = this.dataDoc.freeform_panX_min ? nw * NumCast(this.dataDoc.freeform_panX_min) : undefined;
         this.dataDoc.freeform_panY_max = this.dataDoc.freeform_panY_max ? nw * NumCast(this.dataDoc.freeform_panY_max) : undefined;
         this.dataDoc.freeform_panY_min = this.dataDoc.freeform_panY_min ? nw * NumCast(this.dataDoc.freeform_panY_min) : undefined;
-        const newnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+        const newnativeWidth = this.imgNativeSize.nativeWidth;
         DocListCast(this.dataDoc[this.annotationKey]).forEach(doc => {
             doc.x = (NumCast(doc.x) / oldnativeWidth) * newnativeWidth;
             doc.y = (NumCast(doc.y) / oldnativeWidth) * newnativeWidth;
@@ -280,13 +302,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     });
     @undoBatch
     rotate = action(() => {
-        const nw = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
-        const nh = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']);
+        const nativeSize = this.imgNativeSize;
         const w = this.layoutDoc._width;
         const h = this.layoutDoc._height;
         this.dataDoc[this.fieldKey + '_rotation'] = (NumCast(this.dataDoc[this.fieldKey + '_rotation']) + 90) % 360;
-        this.dataDoc[this.fieldKey + '_nativeWidth'] = nh;
-        this.dataDoc[this.fieldKey + '_nativeHeight'] = nw;
+        this.imgNativeSize = { nativeWidth: nativeSize.nativeHeight, nativeHeight: nativeSize.nativeWidth }; // swap width and height
         this.layoutDoc._width = h;
         this.layoutDoc._height = w;
     });
@@ -302,7 +322,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const anchy = NumCast(cropping.y);
         const anchw = NumCast(cropping._width);
         const anchh = NumCast(cropping._height);
-        const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw;
+        const viewScale = this.nativeSize.nativeWidth / anchw;
         cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width);
         cropping.y = NumCast(this.Document.y);
         cropping.onClick = undefined;
@@ -364,12 +384,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     @action
     cancelOutpaintPrompt = () => {
-        const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']);
-        const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']);
-        this.Document._width = origWidth;
-        this.Document._height = origHeight;
+        [this.Document._width, this.Document._height] = [this.oupaintOriginalSize.width, this.oupaintOriginalSize.height];
         this._outpaintingInProgress = false;
-        this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined;
+        this.outpaintOriginalSize = undefined;
         this.closeOutpaintPrompt();
     };
 
@@ -417,8 +434,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>';
             this._mainCont?.appendChild(loadingOverlay);
 
-            const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']);
-            const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']);
+            const { width: origWidth, height: origHeight } = this.oupaintOriginalSize;
             const response = await Networking.PostToServer('/outpaintImage', {
                 imageUrl: currentPath,
                 prompt: customPrompt,
@@ -455,8 +471,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     this.Document.$ai = true;
                     this.Document.$ai_outpainted = true;
                     this.Document.$ai_outpaint_prompt = customPrompt;
-                    this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined;
-                    this.Document[this.fieldKey + '_outpaintOriginalHeight'] = undefined;
+                    this.outpaintOriginalSize = undefined;
                 } else {
                     this.cancelOutpaintPrompt();
                     alert('Failed to receive a valid image URL from server.');
@@ -669,8 +684,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @computed get nativeSize() {
         TraceMobx();
         if (this.paths.length && this.paths[0].includes(DefaultPath)) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 };
-        const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500));
-        const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500));
+        const { nativeWidth, nativeHeight } = this.imgNativeSize;
         const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1);
         return { nativeWidth, nativeHeight, nativeOrientation };
     }
@@ -821,7 +835,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             transform,
                             transformOrigin,
                             width: this._outpaintAlign ? 'max-content' : this._outpaintAlign ? '100%' : undefined,
-                            height: this._outpaintVAlign ? 'max-content' : this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined ? '100%' : undefined,
+                            height: this._outpaintVAlign ? 'max-content' : this.outpaintOriginalSize?.width ? '100%' : undefined,
                         }}
                         onError={action(e => (this._error = e.toString()))}
                         draggable={false}
@@ -944,6 +958,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return { width, height };
     };
     savedAnnotations = () => this._savedAnnotations;
+    rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView | undefined) => (this.dataDoc[this.fieldKey] === undefined ? true : (this._props.rejectDrop?.(de, subView) ?? false));
     render() {
         TraceMobx();
         const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
@@ -987,7 +1002,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         ScreenToLocalTransform={this.screenToLocalTransform}
                         select={emptyFunction}
                         focus={this.focus}
-                        rejectDrop={this._props.rejectDrop}
+                        rejectDrop={this.rejectDrop}
                         getScrollHeight={this.getScrollHeight}
                         NativeDimScaling={returnOne}
                         isAnyChildContentActive={returnFalse}
@@ -1049,8 +1064,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 if (result instanceof Error) {
                     alert('Error uploading files - possibly due to unsupported file types');
                 } else {
-                    this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
-                    !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc);
+                    runInAction(() => {
+                        this.dataDoc.layout_resetNativeDim = true;
+                        !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc, this.fieldKey);
+                        this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
+                    });
                 }
                 disposer();
             } else {
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 4cbe01b82..e1ecc2018 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -9,7 +9,7 @@ import { TraceMobx } from '../../../fields/util';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
-import { undoable } from '../../util/UndoManager';
+import { undoable, UndoManager } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
@@ -28,6 +28,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
     private _timeout: NodeJS.Timeout | undefined;
     private _divRef: HTMLDivElement | null = null;
     private _disposers: { [key: string]: IReactionDisposer } = {};
+    private _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -220,8 +221,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             this.setText(this._divRef?.innerText ?? '');
                             if (!FormattedTextBox.tryKeepingFocus(e.relatedTarget, () => this._divRef?.focus())) {
                                 RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
-                                FormattedTextBox.LiveTextUndo?.end();
-                                FormattedTextBox.LiveTextUndo = undefined;
+                                this._liveTextUndo?.end();
                             }
                         }}
                         dangerouslySetInnerHTML={{
@@ -236,6 +236,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
                                 if (DocumentView.SelectOnLoad === this.Document) {
                                     DocumentView.SetSelectOnLoad(undefined);
+                                    this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+                                    FormattedTextBox.LiveTextUndo = undefined;
                                     this._divRef.focus();
                                 }
                                 this.fitTextToBox(this._divRef);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 1827cd2dc..c8df6e50f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -98,7 +98,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     public static PasteOnLoad: ClipboardEvent | undefined;
     public static SelectOnLoadChar = '';
-    public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
+    public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch request when typing a new text note into a collection
+    private _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection
 
     private static _nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor };
     private _curHighlights = new ObservableSet<string>(['Audio Tags']);
@@ -1536,6 +1537,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
         }
         if (selectOnLoad) {
+            this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+            FormattedTextBox.LiveTextUndo = undefined;
             this.EditorView!.focus();
         }
         if (this._props.isContentActive()) this.prepareForTyping();
@@ -1565,8 +1568,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         removeStyleSheet(this._userStyleSheetElement);
         Object.values(this._disposers).forEach(disposer => disposer?.());
         this.endUndoTypingBatch();
-        FormattedTextBox.LiveTextUndo?.end();
-        FormattedTextBox.LiveTextUndo = undefined;
+        this._liveTextUndo?.end();
         this.unhighlightSearchTerms();
         this.EditorView?.destroy();
         RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined);
@@ -1795,8 +1797,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
         this.endUndoTypingBatch();
 
-        FormattedTextBox.LiveTextUndo?.end();
-        FormattedTextBox.LiveTextUndo = undefined;
+        this._liveTextUndo?.end();
 
         // if the text box blurs and none of its contents are focused(), then pass the blur along
         setTimeout(() => !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.());
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 366dc6a20..25e70d950 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1202,12 +1202,12 @@ export namespace Doc {
     }
     export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
         // if this is a field template, then don't use the doc's nativeWidth/height
-        return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], useWidth ? NumCast(doc._width) : 0));
+        return !doc ? 0 : NumCast(doc._nativeWidth, doc[DocLayout].isTemplateDoc ? 0 : NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], useWidth ? NumCast(doc._width) : 0));
     }
     export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
         if (!doc) return 0;
         const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); // divide before multiply to avoid floating point errrorin case nativewidth = width
-        const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0);
+        const dheight = doc[DocLayout].isTemplateDoc ? 0 : NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0);
         // if this is a field template, then don't use the doc's nativeWidth/height
         return NumCast(doc._nativeHeight, nheight || dheight);
     }
-- 
cgit v1.2.3-70-g09d2


From 7626527799c0606fa9c4fd4d26a19189dc7e7a0e Mon Sep 17 00:00:00 2001
From: sharkiecodes <lanyi_stroud@brown.edu>
Date: Sun, 1 Jun 2025 20:24:04 -0400
Subject: reactive backgrounds, tagging of pdfs, group-select and suggested
 templates, text box content influences backgrounds

---
 src/client/apis/gpt/GPT.ts                         |  41 +++-
 .../collections/collectionFreeForm/MarqueeView.tsx |  70 ++++--
 src/client/views/nodes/ImageBox.tsx                |   2 +-
 src/client/views/nodes/PDFBox.tsx                  |  44 ++++
 src/client/views/nodes/VideoBox.tsx                |  51 ++++
 .../views/nodes/formattedText/FormattedTextBox.tsx |  52 +++-
 .../views/nodes/scrapbook/AIPresetGenerator.ts     |  31 +++
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx  | 264 ++++++---------------
 .../views/nodes/scrapbook/ScrapbookPreset.tsx      |   8 +-
 .../nodes/scrapbook/ScrapbookPresetRegistry.ts     |   3 +
 10 files changed, 347 insertions(+), 219 deletions(-)
 create mode 100644 src/client/views/nodes/scrapbook/AIPresetGenerator.ts

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 4dd30f8b3..03fce21f7 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -30,7 +30,6 @@ enum GPTCallType {
     DRAW = 'draw',
     COLOR = 'color',
     TEMPLATE = 'template',
-    SCRAPBOOK = 'scrapbook',
     VIZSUM = 'vizsum',
     VIZSUM2 = 'vizsum2',
     FILL = 'fill',
@@ -41,7 +40,9 @@ enum GPTCallType {
     SUBSETDOCS = 'subset_docs', // select a subset of documents based on their descriptions
     DOCINFO = 'doc_info', // provide information about a document
     SORTDOCS = 'sort_docs',
-    CLASSIFYTEXT = 'classify_text', // classify text into one of the three categories: title, caption, lengthy description
+    CLASSIFYTEXTMINIMAL = 'classify_text_minimal', // classify text into one of the three categories: title, caption, lengthy description
+    CLASSIFYTEXTFULL = 'classify_text_full', //tags pdf content
+    GENERATESCRAPBOOK = 'generate_scrapbook'
 }
 
 type GPTCallOpts = {
@@ -93,7 +94,7 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
         prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please only return a JSON with a correlation column 1 propert, a correlation column 2 property, and an analysis property. ",
     },
     //new
-    classify_text: {
+    classify_text_minimal: {
         model: 'gpt-4o',
         maxTokens: 2048,
         temp: 0.25,
@@ -101,6 +102,13 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
                 most appropriate category: '${TextClassifications.Title}', '${TextClassifications.Caption}', or '${TextClassifications.LengthyDescription}'. Output exclusively the classification in your response.
                 `
     },
+    classify_text_full: {
+        model: 'gpt-4o',
+        maxTokens: 2048,
+        temp: 0.25,
+        prompt: `Based on the content of the text, provide six descriptive tags (single words) separated by spaces. 
+            Finally, include a seventh more detailed summary phrase using underscores.`
+    },
     describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
     flashcard: {
         model: 'gpt-4-turbo',
@@ -160,12 +168,29 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
         prompt: 'You will be coloring drawings. You will be given what the drawing is, then a list of descriptions for parts of the drawing. Based on each description, respond with the stroke and fill color that it should be. Follow the rules: 1. Avoid using black for stroke color 2. Make the stroke color 1-3 shades darker than the fill color 3. Use the same colors when possible. Format as {#abcdef #abcdef}, making sure theres a color for each description, and do not include any additional text.',
     },
 
-    scrapbook: {
-        model: 'gpt-4-turbo',
-        maxTokens: 512,
+    generate_scrapbook: {
+        model: 'gpt-4o',
+        maxTokens: 2048,
         temp: 0.5,
-        prompt: 'You will be given a list of field descriptions for one or more templates in the format {field #0: “description”}{field #1: “description”}{...}, and a list of column descriptions in the format {“title”: “description”}{...}. Your job is to match columns with fields based on their descriptions. Your output should be in the following JSON format: {“template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}, “template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}} where “template_title” is the templates title as specified in the description provided,  # represents the field # and “title” the title of the column assigned to it. A filled out example might look like {“fivefield2”:{“0”:”Name”, “1”:”Image”, “2”:”Caption”, “3”:”Position”, “4”:”Stats”}, “fivefield3”:{0:”Image”, 1:”Name”, 2:”Caption”, 3:”Stats”, 4:”Position”}. Include one object for each template. IT IS VERY IMPORTANT THAT YOU ONLY INCLUDE TEXT IN THE FORMAT ABOVE, WITH NO ADDITIONS WHATSOEVER. Do not include extraneous ‘#’ characters, ‘column’ for columns, or ‘template’ for templates: ONLY THE TITLES AND NUMBERS. There should never be one column assigned to more than one field (ie. if the “name” column is assigned to field 1, it can’t be assigned to any other fields) . Do this for each template whose fields are described. The descriptions are as follows:',
-    },
+         prompt: `Generate an aesthetically pleasing scrapbook layout preset based on these items.
+    Return your response as JSON in the format:
+    [{
+        "type": DocumentType.RTF or DocumentType.IMG or DocumentType.PDF
+        "tag": a singular tag summarizing the document
+        "x": number,
+        "y": number,
+        "width": number,
+        "height": number
+    }, ...]
+    If there are mutliple documents, you may include 
+    "children": [
+                    { type: 
+                      tag: 
+                      x: , y: , width: , height:
+                    }
+                  ] `
+
+      },
 
     command_type: {
         model: 'gpt-4-turbo',
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index f5e699d3e..0b91d628b 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -28,6 +28,8 @@ import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { SubCollectionViewProps } from '../CollectionSubView';
 import { ImageLabelBoxData } from './ImageLabelBox';
 import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
+import { StrListCast } from '../../../../fields/Doc';
+import { requestAiGeneratedPreset, DocumentDescriptor } from '../../nodes/scrapbook/AIPresetGenerator';
 import './MarqueeView.scss';
 
 interface MarqueeViewProps {
@@ -519,35 +521,71 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     });
 
 
+   
+    
+    generateScrapbook = action(async () => {
+   
+        const selectedDocs = this.marqueeSelect(false);
+        if (!selectedDocs.length) return;
 
-    @undoBatch
-    generateScrapbook = action(() => {
-        let docs = new Array<Doc>();
+       const descriptors: DocumentDescriptor[] = selectedDocs.map(doc => ({
+            type: typeof doc.$type === 'string' ? doc.$type : 'UNKNOWN',
+            tags: (() => {
+                const internalTagsSet = new Set<string>();
+                StrListCast(doc.$tags_chat ?? new List<string>()).forEach(tag => {
+                internalTagsSet.add(tag);
+                });
+                return Array.from(internalTagsSet);
+            })()
+            }));
+
+
+        const aiPreset = await requestAiGeneratedPreset(descriptors);
+        if (!aiPreset.length) {
+            alert("Failed to generate preset");
+            return;
+        }
+
+        const scrapbookPlaceholders: Doc[] = aiPreset.map(cfg => {
+            const placeholderDoc = Docs.Create.TextDocument(cfg.tag);
+            placeholderDoc.accepts_docType = cfg.type as DocumentType;
+            placeholderDoc.accepts_tagType = cfg.acceptTag ?? cfg.tag;
+
+            const placeholder = new Doc();
+            placeholder.proto = placeholderDoc;
+            placeholder.original = placeholderDoc;
+            placeholder.x = cfg.x;
+            placeholder.y = cfg.y;
+            if (cfg.width != null) placeholder._width = cfg.width;
+            if (cfg.height != null) placeholder._height = cfg.height;
+
+            return placeholder;
+        });
+
+        const scrapbook = Docs.Create.ScrapbookDocument(scrapbookPlaceholders, {
+            backgroundColor: '#e2ad32',
+            x: this.Bounds.left,
+            y: this.Bounds.top,
+            _width: 500,
+            _height: 500,
+            title: 'AI-generated Scrapbook'
+        });
         const selected = this.marqueeSelect(false).map(d => {
             this._props.removeDocument?.(d);
             d.x = NumCast(d.x) - this.Bounds.left;
             d.y = NumCast(d.y) - this.Bounds.top;
-            docs.push(d);
             return d;
         });
-        const scrapbook = Docs.Create.ScrapbookDocument(docs, {
-            backgroundColor: '#e2ad32',
-            x: this.Bounds.left,
-            y: this.Bounds.top,
-            followLinkToggle: true,
-            _width: 200,
-            _height: 200,
-            _layout_showSidebar: true,
-            title: 'overview',
-        });
+
+        this._props.addDocument?.(scrapbook);
+        selectedDocs.forEach(doc => this._props.removeDocument?.(doc));
         const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
         DocUtils.MakeLink(scrapbook, portal, { link_relationship: 'summary of:summarized by' });
 
         portal.hidden = true;
         this._props.addDocument?.(portal);
-        //this._props.addLiveTextDocument(summary);
-        this._props.addDocument?.(scrapbook);
         MarqueeOptionsMenu.Instance.fadeOut(true);
+        this.hideMarquee();
     });
 
 
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index bd612d04f..9067f7e0c 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -132,7 +132,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
       const base64 = await imageUrlToBase64(url);
       if (!base64) throw new Error('Failed to load image data');
 
-      // 3) ask GPT for exactly one label: PERSON or LANDSCAPE
+      // 3) ask GPT for labels one label: PERSON or LANDSCAPE
       const raw = await gptImageLabel(
         base64,
         `Classify this image as PERSON or LANDSCAPE.  You may only respond with one of these two options. Then
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 55e6d5596..282b06215 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -34,6 +34,9 @@ import { ImageBox } from './ImageBox';
 import { OpenWhere } from './OpenWhere';
 import './PDFBox.scss';
 import { CreateImage } from './WebBoxRenderer';
+import { gptAPICall } from '../../apis/gpt/GPT';
+import { List } from '../../../fields/List';
+import { GPTCallType } from '../../apis/gpt/GPT';
 
 @observer
 export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -76,6 +79,47 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     }
 
+     autoTag = async () => {
+     try {
+        if (!this._pdf) {
+            throw new Error('PDF not loaded');
+        }
+
+        // 1) Extract text from the first few pages (e.g., first 2 pages)
+        const maxPages = Math.min(2, this._pdf.numPages);
+        let textContent = '';
+        for (let pageNum = 1; pageNum <= maxPages; pageNum++) {
+            const page = await this._pdf.getPage(pageNum);
+            const text = await page.getTextContent();
+            const pageText = text.items.map(item => ('str' in item ? item.str : '')).join(' ');
+            textContent += ` ${pageText}`;
+        }
+
+        if (!textContent.trim()) {
+            throw new Error('No text found in PDF');
+        }
+
+        // 2) Ask GPT to classify and provide descriptive tags
+        const raw = await gptAPICall(
+            `"${textContent.trim().slice(0, 2000)}"`, 
+            GPTCallType.CLASSIFYTEXTFULL
+        );
+
+        // 3) Normalize and store the labels
+        const label = raw.trim().toUpperCase();
+
+        const tokens = label.split(/\s+/);
+        this.Document.$tags_chat = new List<string>();
+        tokens.forEach(tok => (this.Document.$tags_chat as List<string>).push(tok));
+
+        // 4) Show tags in layout
+        this.Document._layout_showTags = true;
+
+    } catch (err) {
+        console.error('PDF autoTag failed:', err);
+    }
+};
+
     replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => {
         if (oldDiv.childNodes) {
             for (let i = 0; i < oldDiv.childNodes.length; i++) {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index fa099178c..0e7afbab1 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -30,6 +30,7 @@ import { StyleProp } from '../StyleProp';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
+import { gptImageLabel } from '../../apis/gpt/GPT';
 import './VideoBox.scss';
 
 /**
@@ -109,6 +110,56 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return this._videoRef;
     }
 
+
+     autoTag = async () => {
+    try {
+        if (!this.player) throw new Error('Video element not available.');
+
+        // 1) Extract a frame at the video's midpoint
+        const videoDuration = this.player.duration;
+        const snapshotTime = videoDuration / 2;
+
+        // Seek the video element to the midpoint
+        await new Promise<void>((resolve, reject) => {
+            const onSeeked = () => {
+                this.player!.removeEventListener('seeked', onSeeked);
+                resolve();
+            };
+            this.player!.addEventListener('seeked', onSeeked);
+            this.player!.currentTime = snapshotTime;
+        });
+
+        // 2) Draw the frame onto a canvas and get a base64 representation
+        const canvas = document.createElement('canvas');
+        canvas.width = this.player.videoWidth;
+        canvas.height = this.player.videoHeight;
+        const ctx = canvas.getContext('2d');
+        if (!ctx) throw new Error('Failed to create canvas context.');
+        ctx.drawImage(this.player, 0, 0, canvas.width, canvas.height);
+        const base64Image = canvas.toDataURL('image/png');
+
+        // 3) Send the image data to GPT for classification and descriptive tags
+        const raw = await gptImageLabel(
+            base64Image,
+            `Classify this video frame as either a PERSON or LANDSCAPE.
+            Then provide five additional descriptive tags (single words) separated by spaces.
+            Finally, add one detailed summary phrase using underscores.`
+        );
+
+        // 4) Normalize and store labels in the Document's tags
+        const label = raw.trim().toUpperCase();
+        const tokens = label.split(/\s+/);
+        this.Document.$tags_chat = new List<string>();
+        tokens.forEach(tok => (this.Document.$tags_chat as List<string>).push(tok));
+
+        // 5) Turn on tag display in layout
+        this.Document._layout_showTags = true;
+
+    } catch (err) {
+        console.error('Video autoTag failed:', err);
+    }
+};
+
     componentDidMount() {
         this.unmounting = false;
         this._props.setContentViewBox?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 10becc00b..04a14a15f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -8,6 +8,7 @@ import { baseKeymap, selectAll, splitBlock } from 'prosemirror-commands';
 import { history } from 'prosemirror-history';
 import { inputRules } from 'prosemirror-inputrules';
 import { keymap } from 'prosemirror-keymap';
+import { runInAction } from 'mobx';
 import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
 import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transaction } from 'prosemirror-state';
 import { EditorView, NodeViewConstructor } from 'prosemirror-view';
@@ -305,12 +306,53 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
     };
 
-    autoTag = async () => {
+  autoTag = async () => {
+
+    const layoutKey = Doc.LayoutDataKey(this.Document);
+    const rawText = RTFCast(this.Document[layoutKey])?.Text ?? StrCast(this.Document[layoutKey]);
+
+    const callType = rawText.includes("[placeholder]")
+        ? GPTCallType.CLASSIFYTEXTMINIMAL
+        : GPTCallType.CLASSIFYTEXTFULL;
+
+    gptAPICall(rawText, callType).then(desc => {
+        runInAction(() => {
+            // Clear existing tags
+            this.Document.$tags_chat = new List<string>();
+
+            // Split GPT response into tokens and push individually
+            const tokens = desc.trim().split(/\s+/);
+            tokens.forEach(tok => {
+                (this.Document.$tags_chat as List<string>).push(tok);
+            });
+
+            this.Document._layout_showTags = true;
+        });
+    });
+        /*this.Document.$tags_chat = new List<string>();
+        gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), GPTCallType.CLASSIFYTEXTMINIMAL).then(desc => (this.Document.$tags_chat as List<string>).push(desc));
+        this.Document._layout_showTags = true;*/
+
+
+    // 2) grab whatever’s actually in the field (either RTF or plain string)
+/*
+    const rawText = RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)])
+
+    // 3) pick minimal vs. full classification based on "[placeholder]" substring
+    if (rawText.includes("[placeholder]")) {
         this.Document.$tags_chat = new List<string>();
-        gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), GPTCallType.CLASSIFYTEXT).then(desc => (this.Document.$tags_chat as List<string>).push(desc));
-        this.Document._layout_showTags = true;
-        //or... then(desc => this.Document.$tags_chat = desc);
-    };
+        gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), GPTCallType.CLASSIFYTEXTMINIMAL).then(desc => {
+            (this.Document.$tags_chat as List<string>).push(desc);
+        });
+    } else {
+        this.Document.$tags_chat = new List<string>();
+        gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), GPTCallType.CLASSIFYTEXTFULL).then(desc => {
+            (this.Document.$tags_chat as List<string>).push(desc);
+        })};
+    // 4) make sure the UI will show tags
+    this.Document._layout_showTags = true;*/
+    
+};
 
     leafText = (node: Node) => {
         if (node.type === this.EditorView?.state.schema.nodes.dashField) {
diff --git a/src/client/views/nodes/scrapbook/AIPresetGenerator.ts b/src/client/views/nodes/scrapbook/AIPresetGenerator.ts
new file mode 100644
index 000000000..1f159222b
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/AIPresetGenerator.ts
@@ -0,0 +1,31 @@
+import { ScrapbookItemConfig } from './ScrapbookPreset';
+import { GPTCallType, gptAPICall } from '../../../apis/gpt/GPT';
+
+// Represents the descriptor for each document
+export interface DocumentDescriptor {
+  type: string;
+  tags: string[];
+}
+
+// Main function to request AI-generated presets
+export async function requestAiGeneratedPreset(descriptors: DocumentDescriptor[]): Promise<ScrapbookItemConfig[]> {
+    const prompt = createPrompt(descriptors);
+    let aiResponse = await gptAPICall(prompt, GPTCallType.GENERATESCRAPBOOK);
+            // Strip out ```json and ``` if the model wrapped its answer in fences
+    aiResponse = aiResponse
+            .trim()
+            .replace(/^```(?:json)?\s*/, "")   // remove leading ``` or ```json
+            .replace(/\s*```$/, "");           // remove trailing ```
+    const parsedPreset = JSON.parse(aiResponse) as ScrapbookItemConfig[];
+    return parsedPreset;
+}
+
+// Helper to generate prompt text for AI
+function createPrompt(descriptors: DocumentDescriptor[]): string {
+    let prompt = "";
+    descriptors.forEach((desc, index) => {
+        prompt += `${index + 1}. Type: ${desc.type}, Tags: ${desc.tags.join(', ')}\n`;
+    });
+
+    return prompt;
+}
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index 391dcb83d..5dd02295c 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -24,20 +24,72 @@ import { ImageCast } from '../../../../fields/Types';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { IReactionDisposer } from 'mobx';
 import { observer } from 'mobx-react';
-import { ImageField } from '../../../../fields/URLField';
 import { runInAction } from 'mobx';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faRedoAlt }   from '@fortawesome/free-solid-svg-icons';
 import { getPresetNames, createPreset } from './ScrapbookPresetRegistry';
 
 
-enum ScrapbookPresetType {
-    Classic = 'Classic',
-    Default = 'Default',
-    Collage = 'Collage',
-    Spotlight = 'Spotlight',
-}
+export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]): Doc[] {
+  const placeholders: Doc[] = [];
+
+  for (const cfg of configs) {
+    if (cfg.children && cfg.children.length) {
+      // --- nested container ---
+      const childDocs = cfg.children.map(child => {
+        const doc = Docs.Create.TextDocument("[placeholder] " + child.tag);
+        doc.accepts_docType = child.type;
+
+        const ph = new Doc();
+        ph.proto    = doc;
+        ph.original = doc;
+        ph.x        = child.x;
+        ph.y        = child.y;
+        if (child.width  != null) ph._width  = child.width;
+        if (child.height != null) ph._height = child.height;
+        return ph;
+      });
+
+      // wrap those children in a stacking container
+      const protoW = cfg.containerWidth ?? cfg.width;
+      const protoH = cfg.containerHeight ?? cfg.height;
+      const containerProto = Docs.Create.StackingDocument(
+        childDocs,
+        {
+          ...(protoW != null ? { _width:  protoW } : {}),
+          ...(protoH != null ? { _height: protoH } : {}),
+          title: cfg.tag
+        }
+      );
+
+      const ph = new Doc();
+      ph.proto    = containerProto;
+      ph.original = containerProto;
+      ph.x        = cfg.x;
+      ph.y        = cfg.y;
+      if (cfg.width  != null) ph._width  = cfg.width;
+      if (cfg.height != null) ph._height = cfg.height;
+      placeholders.push(ph);
+
+    } else {
+      // --- flat placeholder ---
+      const doc = Docs.Create.TextDocument("[placeholder] " + cfg.tag);
+      doc.accepts_docType = cfg.type;
+      doc.accepts_tagType = cfg.acceptTag ?? cfg.tag;
+
+      const ph = new Doc();
+      ph.proto    = doc;
+      ph.original = doc;
+      ph.x        = cfg.x;
+      ph.y        = cfg.y;
+      if (cfg.width  != null) ph._width  = cfg.width;
+      if (cfg.height != null) ph._height = cfg.height;
+      placeholders.push(ph);
+    }
+  }
 
+  return placeholders;
+}
 // Scrapbook view: a container that lays out its child items in a grid/template
 @observer
 export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -55,14 +107,22 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
-    // whenever the preset changes, rebuild the layout
-    reaction(
+        const existingItems = DocListCast(this.dataDoc[this.fieldKey] as List<Doc>);
+    if (!existingItems || existingItems.length === 0) {
+      // Only wire up reaction/setTitle if it's truly a brand-new, empty Scrapbook
+      reaction(
         () => this.selectedPreset,
         presetName => this.initScrapbook(presetName),
         { fireImmediately: true }
-    );
-        this.createdDate = this.getFormattedDate();
-
+      );
+
+      this.createdDate = this.getFormattedDate();
+      this.setTitle();
+    } else {
+      // If items are already present, just preserve whatever was injected.
+      // We still want `createdDate` set so that the UI title bar can show it if needed.
+      this.createdDate = this.getFormattedDate();
+    }
         //this.configs = 
         //ScrapbookPreset.createPreset(presetType);
 
@@ -70,9 +130,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         if (!this.dataDoc[this.fieldKey]) {
             this.dataDoc[this.fieldKey] = new List<Doc>();
         }
-        this.createdDate = this.getFormattedDate();
         //this.initScrapbook(ScrapbookPresetType.Default);
-        this.setTitle();
         //this.setLayout(ScrapbookPreset.Spotlight);
     }
 
@@ -99,175 +157,11 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
       }
     
       // 2) build placeholders from the preset
+      const placeholders = buildPlaceholdersFromConfigs(configs);
 
-      const placeholders: Doc[] = [];
-    
-      for (const cfg of configs) {
-        if (cfg.children) {
-          // --- nested container ---
-          const childDocs = cfg.children.map(child => {
-            const doc = Docs.Create.TextDocument(child.tag);
-            doc.accepts_docType = child.type;
-            doc.accepts_tagType = child.acceptTag ?? child.tag;
-    
-            const ph = new Doc();
-            ph.proto    = doc;
-            ph.original = doc;
-            ph.x        = child.x;
-            ph.y        = child.y;
-            if (child.width  != null) ph._width  = child.width;
-            if (child.height != null) ph._height = child.height;
-            return ph;
-          });
-    
-          const protoW = cfg.containerWidth ?? cfg.width;
-          const protoH = cfg.containerHeight ?? cfg.height;
-          const containerProto = Docs.Create.StackingDocument(
-            childDocs,
-            {
-              ...(protoW != null ? { _width:  protoW } : {}),
-              ...(protoH != null ? { _height: protoH } : {}),
-              title: cfg.tag
-            }
-          );
-    
-          const ph = new Doc();
-          ph.proto    = containerProto;
-          ph.original = containerProto;
-          ph.x        = cfg.x;
-          ph.y        = cfg.y;
-          if (cfg.width  != null) ph._width  = cfg.width;
-          if (cfg.height != null) ph._height = cfg.height;
-          placeholders.push(ph);
-    
-        } else {
-          // --- flat placeholder ---
-          const doc = Docs.Create.TextDocument(cfg.tag);
-          doc.accepts_docType = cfg.type;
-          doc.accepts_tagType = cfg.acceptTag ?? cfg.tag;
-    
-          const ph = new Doc();
-          ph.proto    = doc;
-          ph.original = doc;
-          ph.x        = cfg.x;
-          ph.y        = cfg.y;
-          if (cfg.width  != null) ph._width  = cfg.width;
-          if (cfg.height != null) ph._height = cfg.height;
-          placeholders.push(ph);
-        }
-      }
-    
       // 3) commit them into the field
       this.dataDoc[this.fieldKey] = new List<Doc>(placeholders);
     }
-    @action
-    //INACTIVE VER ignore!! not in use rn, implementation ver 1
-    setLayout(preset: ScrapbookPreset) {
-        // helper to wrap a TextDocument proto in a Doc with positioning
-        function makePlaceholder(
-            proto: Doc, x: number, y: number,
-            width: number, height: number
-        ): Doc {
-            const d = new Doc();
-            d.proto = proto;
-            d.original = proto;
-            d.x = x;
-            d.y = y;
-            d._width = width;
-            d._height = height;
-            return d;
-        }
-    
-        let placeholders: Doc[];
-    
-        switch (preset) {
-            case ScrapbookPresetType.Classic:
-                // One large landscape image on top, caption below, sidebar at right
-                const imgClassic = Docs.Create.TextDocument('image');
-                imgClassic.accepts_docType = DocumentType.IMG;
-                imgClassic.accepts_tagType = 'LANDSCAPE';
-                const phImageClassic = makePlaceholder(imgClassic, 0, -120, 300, 180);
-    
-                const captionClassic = Docs.Create.TextDocument('caption');
-                captionClassic.accepts_docType = DocumentType.RTF;
-                captionClassic.accepts_tagType = 'caption';
-                const phCaptionClassic = makePlaceholder(captionClassic, 0, 80, 300, 60);
-    
-                const sidebarClassic = Docs.Create.TextDocument('sidebar');
-                sidebarClassic.accepts_docType = DocumentType.RTF;
-                sidebarClassic.accepts_tagType = 'lengthy description';
-                const phSidebarClassic = makePlaceholder(sidebarClassic, 320, -50, 80, 200);
-    
-                placeholders = [phImageClassic, phCaptionClassic, phSidebarClassic];
-                break;
-    
-            case ScrapbookPresetType.Collage:
-                // Grid of four person images, small captions under each
-                const personDocs: Doc[] = [];
-                for (let i = 0; i < 4; i++) {
-                    const img = Docs.Create.TextDocument(`person ${i+1}`);
-                    img.accepts_docType = DocumentType.IMG;
-                    img.accepts_tagType = 'PERSON';
-                    // position in 2x2 grid
-                    const x = (i % 2) * 160 - 80;
-                    const y = Math.floor(i / 2) * 160 - 80;
-                    personDocs.push(makePlaceholder(img, x, y, 150, 120));
-    
-                    const cap = Docs.Create.TextDocument(`caption ${i+1}`);
-                    cap.accepts_docType = DocumentType.RTF;
-                    cap.accepts_tagType = 'caption';
-                    personDocs.push(makePlaceholder(cap, x, y + 70, 150, 30));
-                }
-                placeholders = personDocs;
-                break;
-    
-            case ScrapbookPresetType.Spotlight:
-                // Full-width title, then a stacking of an internal person image + landscape, then description
-                const titleSpot = Docs.Create.TextDocument('title');
-                titleSpot.accepts_docType = DocumentType.RTF;
-                titleSpot.accepts_tagType = 'title';
-                const phTitleSpot = makePlaceholder(titleSpot, 0, -180, 400, 60);
-    
-                const internalImg = Docs.Create.TextDocument('<person>');
-                internalImg.accepts_docType = DocumentType.IMG;
-                internalImg.accepts_tagType = 'PERSON';
-                const phInternal = makePlaceholder(internalImg, -100, -120, 120, 160);
-    
-                const landscapeImg = Docs.Create.TextDocument('<landscape>');
-                landscapeImg.accepts_docType = DocumentType.IMG;
-                landscapeImg.accepts_tagType = 'LANDSCAPE';
-                const phLandscape = makePlaceholder(landscapeImg, 50, 0, 200, 160);
-    
-                const stack = Docs.Create.StackingDocument(
-                    [phInternal, phLandscape],
-                    { _width: 360, _height: 180, title: 'spotlight stack' }
-                );
-                const phStack = (() => {
-                    const d = new Doc();
-                    d.proto = stack;
-                    d.original = stack;
-                    d.x = 8;
-                    d.y = -84;
-                    d._width = 360;
-                    d._height = 180;
-                    return d;
-                })();
-    
-                const descSpot = Docs.Create.TextDocument('description');
-                descSpot.accepts_docType = DocumentType.RTF;
-                descSpot.accepts_tagType = 'lengthy description';
-                const phDescSpot = makePlaceholder(descSpot, 0, 140, 400, 100);
-    
-                placeholders = [phTitleSpot, phStack, phDescSpot];
-                break;
-    
-            default:
-                placeholders = [];
-        }
-    
-        // finally assign into the dataDoc
-        this.dataDoc[this.fieldKey] = new List<Doc>(placeholders);
-    }
 
     
 
@@ -276,8 +170,8 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         const title = `Scrapbook - ${this.createdDate}`;
         if (this.dataDoc.title !== title) {
             this.dataDoc.title = title;
-
-            const image = Docs.Create.TextDocument('person image');
+            if (!this.dataDoc[this.fieldKey]){
+            const image = Docs.Create.TextDocument('[placeholder] person image');
             image.accepts_docType = DocumentType.IMG;
             image.accepts_tagType = 'PERSON' //should i be writing fields on this doc? clarify diff between this and proto, original
             const placeholder = new Doc();
@@ -289,7 +183,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             placeholder.y = -100;
             //placeholder.overrideFields = new List<string>(['x', 'y']);  // shouldn't need to do this for layout fields since the placeholder already overrides its protos
             
-            const summary = Docs.Create.TextDocument('long summary');
+            const summary = Docs.Create.TextDocument('[placeholder] long summary');
             summary.accepts_docType = DocumentType.RTF;
             summary.accepts_tagType = 'lengthy description';
             //summary.$tags_chat = new List<string>(['lengthy description']); //we need to go back and set this
@@ -301,7 +195,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             placeholder2._width = 250;
             //placeholder2.overrideFields = new List<string>(['x', 'y', '_width']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos
             
-            const sidebar = Docs.Create.TextDocument('brief sidebar');
+            const sidebar = Docs.Create.TextDocument('[placeholder] brief sidebar');
             sidebar.accepts_docType = DocumentType.RTF;
             sidebar.accepts_tagType = 'title'; //accepts_textType = 'lengthy description'
             const placeholder3 = new Doc();
@@ -314,7 +208,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
           
 
-            const internalImg = Docs.Create.TextDocument('landscape internal');
+            const internalImg = Docs.Create.TextDocument('[placeholder] landscape internal');
             internalImg.accepts_docType = DocumentType.IMG;
             internalImg.accepts_tagType = 'LANDSCAPE' //should i be writing fields on this doc? clarify diff between this and proto, original
             const placeholder5 = new Doc();
@@ -350,7 +244,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
            difference between passing a new List<Doc> versus just the raw array?
            */
            this.dataDoc[this.fieldKey] = new List<Doc>([placeholder, placeholder2, placeholder3, placeholder4]);
-
+            }
           //this.dataDoc[this.fieldKey] = this.dataDoc[this.fieldKey] ?? new List<Doc>([placeholder, placeholder2, placeholder3, placeholder4]);
 
           
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
index fc69552c0..87821c7bf 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
@@ -47,22 +47,22 @@ export class ScrapbookPreset {
     private static createClassicPreset(): ScrapbookItemConfig[] {
         return [
             { type: DocumentType.IMG,
-              tag: 'LANDSCAPE',
+              tag: '[placeholder] LANDSCAPE',
               acceptTag: 'LANDSCAPE',
               x: 0, y: -100, width: 250, height: 200
             },
             { type: DocumentType.RTF,
-              tag: 'caption',
+              tag: '[placeholder] caption',
               acceptTag: 'caption',
               x: 0, y: 200, width: 250, height: 50
             },
             { type: DocumentType.RTF,
-              tag: 'lengthy description',
+              tag: '[placeholder] lengthy description',
               acceptTag: 'lengthy description',
               x: 280, y: -50, width: 50, height: 200
             },
             { type: DocumentType.IMG,
-              tag: 'PERSON',
+              tag: '[placeholder] PERSON',
               acceptTag: 'PERSON',
               x: -200, y: -100, width: 100, height: 200
             },
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts
index f7ddd70ab..d6fd3620c 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts
+++ b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts
@@ -6,6 +6,9 @@ type PresetGenerator = () => ScrapbookItemConfig[];
 // Internal map of preset name to generator
 const presetRegistry = new Map<string, PresetGenerator>();
 
+
+
+
 /**
  * Register a new scrapbook preset under the given name.
  */
-- 
cgit v1.2.3-70-g09d2


From 08b40f21d9295f36277827ac33085f82c9ee1646 Mon Sep 17 00:00:00 2001
From: sharkiecodes <lanyi_stroud@brown.edu>
Date: Fri, 6 Jun 2025 11:07:59 -0400
Subject: clean-up

---
 .../collections/collectionFreeForm/MarqueeView.tsx |   4 +-
 src/client/views/nodes/ImageBox.tsx                |   8 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  23 ---
 src/client/views/nodes/scrapbook/ScrapbookBox.scss |  63 ++++++
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx  | 216 +++++----------------
 .../nodes/scrapbook/ScrapbookSettingsPanel.tsx     |  60 ------
 .../views/nodes/scrapbook/ScrapbookSlot.scss       |  85 --------
 src/client/views/nodes/scrapbook/ScrapbookSlot.tsx |  28 ---
 .../views/nodes/scrapbook/ScrapbookSlotTypes.ts    |  25 ---
 .../views/nodes/scrapbook/scrapbookleftover.ts     |   6 +-
 10 files changed, 114 insertions(+), 404 deletions(-)
 create mode 100644 src/client/views/nodes/scrapbook/ScrapbookBox.scss
 delete mode 100644 src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx
 delete mode 100644 src/client/views/nodes/scrapbook/ScrapbookSlot.scss
 delete mode 100644 src/client/views/nodes/scrapbook/ScrapbookSlot.tsx
 delete mode 100644 src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b8d203cf6..9f96163cd 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -16,7 +16,7 @@ import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs, DocumentOptions } from '../../../documents/Documents';
 import { SnappingManager, freeformScrollMode } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
-import { UndoManager, undoBatch } from '../../../util/UndoManager';
+import { UndoManager, undoBatch, undoable } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { MarqueeViewBounds } from '../../PinFuncs';
@@ -548,7 +548,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     }));
   }
    
-    
+
     generateScrapbook = action(async () => {
    
         const selectedDocs = this.marqueeSelect(false);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 2473f1c0a..79a218e45 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -46,7 +46,6 @@ import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
 import { gptImageLabel } from '../../apis/gpt/GPT';
-import { ImageLabelBox } from '../collections/collectionFreeForm/ImageLabelBox';
 
 const DefaultPath = '/assets/unknown-file-icon-hi.png';
 export class ImageEditorData {
@@ -157,13 +156,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
       this.Document.$tags_chat = new List<string>();
       tokens.forEach(tok => {
         (this.Document.$tags_chat as List<string>).push(tok)});
-        (this.Document.$tags_chat as List<string>).push(`ASPECT_${aspectRatio}`);  
-      //!!! changed may 11 (this.Document.$tags_chat as List<string>).push(label);
+        (this.Document.$tags_chat as List<string>).push(`ASPECT_${aspectRatio}`); 
 
       // 6) flip on “show tags” in the layout 
       //    (same flag that ImageLabelBox.toggleDisplayInformation uses)
-      //note to self: What if i used my own field (ex: Document.$auto_description or something
-      //Would i still have to toggle it on for it to show in the metadata? 
       this.Document._layout_showTags = true;
 
     } catch (err) {
@@ -172,8 +168,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     }
   };
       
-        //Doc.getDescription(this.Document).then(desc => this.desc = desc)
-    
 
 
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 04a14a15f..ad75d320a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -329,29 +329,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             this.Document._layout_showTags = true;
         });
     });
-        /*this.Document.$tags_chat = new List<string>();
-        gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), GPTCallType.CLASSIFYTEXTMINIMAL).then(desc => (this.Document.$tags_chat as List<string>).push(desc));
-        this.Document._layout_showTags = true;*/
-
-
-    // 2) grab whatever’s actually in the field (either RTF or plain string)
-/*
-    const rawText = RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)])
-
-    // 3) pick minimal vs. full classification based on "[placeholder]" substring
-    if (rawText.includes("[placeholder]")) {
-        this.Document.$tags_chat = new List<string>();
-        gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), GPTCallType.CLASSIFYTEXTMINIMAL).then(desc => {
-            (this.Document.$tags_chat as List<string>).push(desc);
-        });
-    } else {
-        this.Document.$tags_chat = new List<string>();
-        gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), GPTCallType.CLASSIFYTEXTFULL).then(desc => {
-            (this.Document.$tags_chat as List<string>).push(desc);
-        })};
-    // 4) make sure the UI will show tags
-    this.Document._layout_showTags = true;*/
-    
 };
 
     leafText = (node: Node) => {
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.scss b/src/client/views/nodes/scrapbook/ScrapbookBox.scss
new file mode 100644
index 000000000..8dc93df60
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.scss
@@ -0,0 +1,63 @@
+
+.scrapbook-box {
+  /* Make sure the container fills its parent, and set a base background */
+  position: relative; /* so that absolute children (loading overlay, etc.) are positioned relative to this */
+  width: 100%;
+  height: 100%;
+  background: beige;
+  overflow: hidden; /* prevent scrollbars if children overflow */
+}
+
+/* Loading overlay that covers the entire scrapbook while AI-generation is in progress */
+.scrapbook-box-loading-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: rgba(255, 255, 255, 0.8);
+  z-index: 10; /* sits above the ImageBox and other content */
+}
+
+/* The <select> dropdown for choosing presets */
+.scrapbook-box-preset-select {
+  position: absolute;
+  top: 8px;
+  left: 8px;
+  z-index: 20;
+  padding: 4px 8px;
+  font-size: 14px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  background: white;
+}
+
+/* Container for the “Regenerate Background” button */
+.scrapbook-box-ui {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  z-index: 20;
+}
+
+/* The button itself */
+.scrapbook-box-ui-button {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 4px 8px;
+  font-size: 14px;
+  color: black;
+  background: white;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  cursor: pointer;
+  white-space: nowrap;
+}
+
+.scrapbook-box-ui-button:hover {
+  background: #f5f5f5;
+}
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index eb997024b..e910f2fa7 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -27,6 +27,7 @@ import { runInAction } from 'mobx';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faRedoAlt }   from '@fortawesome/free-solid-svg-icons';
 import { getPresetNames, createPreset } from './ScrapbookPresetRegistry';
+import './ScrapbookBox.scss';
 
 
 export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]): Doc[] {
@@ -294,30 +295,6 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         }
     }
 
-    async generateAiImage() {
-        this.setState({ loading: true });
-    
-        const prompt = 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background';
-        //'A serene mountain landscape at sunrise, ultra-wide, pastel sky';
-        const dimensions = FireflyImageDimensions.Square; // or whichever suits your scenario
-    
-        SmartDrawHandler.CreateWithFirefly(prompt, dimensions)
-            .then(action(doc => {
-                if (doc instanceof Doc) {
-                    const imgField = ImageCast(doc.data);
-                    this.setState({ src: imgField?.url.href });
-                } else {
-                    alert('Failed to generate the image.');
-                }
-            }))
-            .catch(e => {
-                alert(`Image generation error: ${e}`);
-            })
-            .finally(() => {
-                this.setState({ loading: false });
-            });
-    }
-
     componentDidMount() {
         //this.initScrapbook(ScrapbookPresetType.Default);
         this.setTitle();
@@ -346,38 +323,9 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 ;(this.imageBoxRef.current as any).layoutDoc._width  = dims.w
                 ;(this.imageBoxRef.current as any).layoutDoc._height = dims.h
           
-                // tell the imageDoc to resize itself to the *new* scrapbook size
-                //this.imgDoc._width  = dims.w;
-                //this.imgDoc._height = dims.h;
-                //Doc.SetNativeWidth(this.imgDoc,  dims.w);
-                //Doc.SetNativeHeight(this.imgDoc, dims.h);
               });
             }
         );
-        /*
-        this._disposers.propagateResize = reaction(
-            () => ({ w: this.layoutDoc._width, h: this.layoutDoc._height }),
-            ({ w, h }, prev) => {
-              // only when shift is held (i.e. outpaint mode)
-              if (SnappingManager.ShiftKey && this.imgDoc) {
-                const key = this.imageBoxRef.current!.props.fieldKey; // “data”
-                // record original size on the *image* doc:
-                this.imgDoc[key + '_outpaintOriginalWidth']  = this.imgDoc._width;
-                this.imgDoc[key + '_outpaintOriginalHeight'] = this.imgDoc._height;
-              }
-            }
-          );*/
-        
-
-        //   this._disposers.outpaint = reaction(
-        //     () => this.imgDoc?.[this.imgDoc.fieldKey + '_outpaintOriginalWidth'],
-        //     originalWidth => {
-        //         if (originalWidth !== undefined && !SnappingManager.ShiftKey) {
-        //             this.imageBoxRef.current?.openOutpaintPrompt(); // ✅ CORRECT!
-        //         }
-        //     }
-        // );
-        
     }
 
 
@@ -462,9 +410,6 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 };
      
 
-
-
-
     @computed get regenPrompt() {
         const slots = DocListCast(this.dataDoc[this.fieldKey]);
         
@@ -495,129 +440,62 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             : 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background';
     }
        
-    render() {
-
-        /*const internalTags = DocListCast(this.dataDoc[this.fieldKey])
-        .flatMap(ph => StrListCast(ph.$tags_chat))
-        .join(' ');*/
-
-        
-        return (
-        <div style={{ background: 'beige', width: '100%', height: '100%' }}>
-          {this.loading && (
-          <div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', 
-                        display: 'flex', justifyContent: 'center', alignItems: 'center',
-                        background: 'rgba(255,255,255,0.8)' }}>
+   render() {
+    return (
+      <div className="scrapbook-box">
+        {this.loading && (
+          <div className="scrapbook-box-loading-overlay">
             <ReactLoading type="spin" width={50} height={50} />
           </div>
         )}
-          {this.loading && (
-                <div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', 
-                              display: 'flex', justifyContent: 'center', alignItems: 'center',
-                              background: 'rgba(255,255,255,0.8)', zIndex: 10 }}>
-                    <ReactLoading type="spin" width={50} height={50} />
-                </div>
-            )}
-            {/* Render AI-generated background */}
-            {this.src && this.imgDoc && (
-        <ImageBox
-        ref={this.imageBoxRef}
-          {...this._props}
-          Document={this.imgDoc}
-          fieldKey="data"
-        />)}
-          
-        
-                <select
-        value={this.selectedPreset}
-        onChange={e => (this.selectedPreset = e.currentTarget.value)}
-        style={{ position: 'absolute', top: 8, left: 8, zIndex: 20 }}
+
+        {this.src && this.imgDoc && (
+          <ImageBox
+            ref={this.imageBoxRef}
+            {...this._props}
+            Document={this.imgDoc}
+            fieldKey="data"
+          />
+        )}
+
+        <select
+          className="scrapbook-box-preset-select"
+          value={this.selectedPreset}
+          onChange={e => (this.selectedPreset = e.currentTarget.value)}
         >
-        {getPresetNames().map(name => (
-            <option key={name} value={name}>{name}</option>
-        ))}
-        </select>  
+          {getPresetNames().map(name => (
+            <option key={name} value={name}>
+              {name}
+            </option>
+          ))}
+        </select>
+
         {this._props.isContentActive() && (
-            <div
-              className="scrapbookBox-ui"
-              style={{
-                position: 'absolute',
-                top: 8,
-                right: 8,
-                zIndex: 20,
-              }}
-              onPointerDown={e => e.stopPropagation()}
-            >
+          <div className="scrapbook-box-ui">
             <button
-            type="button"
-            title="Regenerate Background"
-            onClick={() => this.generateAiImageCorrect(this.regenPrompt)}
-            style={{
-                color: 'black',
-                background: 'white',
-                border: '1px solid #ccc',
-                borderRadius: 4,
-                padding: '4px 8px',
-                cursor: 'pointer',
-                display: 'flex',
-                alignItems: 'center',
-                gap: '6px',
-                whiteSpace: 'nowrap',
-                overflow: 'visible', // important
-                fontSize: '14px',
-            }}
+              type="button"
+              title="Regenerate Background"
+              onClick={() => this.generateAiImageCorrect(this.regenPrompt)}
+              className="scrapbook-box-ui-button"
             >
-            <FontAwesomeIcon icon={faRedoAlt} />
-            <span>Regenerate Background</span>
+              <FontAwesomeIcon icon={faRedoAlt} />
+              <span>Regenerate Background</span>
             </button>
-            </div>
-          )}
-
-                    <CollectionView
-                    {...this._props} //
-                    setContentViewBox={emptyFunction}
-                    rejectDrop={this.rejectDrop}
-                    childRejectDrop={this.childRejectDrop}
-                    filterAddDocument={this.filterAddDocument}
-                />
-                {/* <div style={{ border: '1px black', borderStyle: 'dotted', position: 'absolute', top: '50%', width: '100%', textAlign: 'center' }}>Drop an image here</div> */}
-            </div>
-        );
-    }
-}
-
-
-
-//function extractScrapbookConfigs(docs: Doc[]): ScrapbookItemConfig[] {
-    //return docs.map(doc => extractConfig(doc));
-//}
-
-// function extractConfig(doc: Doc): ScrapbookItemConfig {
-//     const layoutKey = Doc.LayoutDataKey(doc);
-//     const childDocs = doc[layoutKey] ? DocListCast(doc[layoutKey]) : [];
-
-//     const isContainer = childDocs.length > 0;
-
-//     const cfg: ScrapbookItemConfig = {
-//         type: isContainer ? DocumentType.COL : doc.$type,
-//         tag: 
-//         acceptTag: doc.accepts_tagType,
-//         x: doc.x || 0,
-//         y: doc.y || 0,
-//         width: doc._width,
-//         height: doc._height,
-//     };
-
-//     if (isContainer) {
-//         cfg.containerWidth = doc.proto._width;
-//         cfg.containerHeight = doc.proto._height;
-//         cfg.children = childDocs.map(child => extractConfig(child));
-//     }
+          </div>
+        )}
 
-//     return cfg;
-// }
+        <CollectionView
+          {...this._props}
+          setContentViewBox={emptyFunction}
+          rejectDrop={this.rejectDrop}
+          childRejectDrop={this.childRejectDrop}
+          filterAddDocument={this.filterAddDocument}
+        />
+      </div>
+    );
+  }
+}
 
-// Register scrapbook
 
 
 Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, {
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx b/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx
deleted file mode 100644
index 5808ab4d1..000000000
--- a/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import React from 'react';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faRedoAlt } from '@fortawesome/free-solid-svg-icons';
-import { action } from 'mobx';
-
-export default class ScrapbookSettingsPanel extends React.Component {
-    
-    constructor(props) {
-        super(props);
-        this.state = { regenerating: false };
-    }
-
-    regenerateScrapbook = async () => {
-        this.setState({ regenerating: true });
-        try {
-            // Example API call or method invoking ChatGPT for JSON
-            const newLayout = await fetch('/api/generate-scrapbook-layout', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ currentLayout: this.props.currentLayout })
-            }).then(res => res.json());
-
-            action(() => {
-                // Apply new layout
-                this.props.applyNewLayout(newLayout);
-            })();
-        } catch (err) {
-            console.error('Failed to regenerate layout:', err);
-        } finally {
-            this.setState({ regenerating: false });
-        }
-    };
-
-    render() {
-        const { regenerating } = this.state;
-
-        return (
-            <div className="scrapbook-settings-panel" style={{ display: 'flex', alignItems: 'center', padding: '8px', backgroundColor: '#f0f0f0', borderRadius: '8px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)' }}>
-                <button
-                    className="regenerate-scrapbook-btn"
-                    title="Regenerate Scrapbook"
-                    onClick={this.regenerateScrapbook}
-                    disabled={regenerating}
-                    style={{
-                        padding: '8px 12px',
-                        background: regenerating ? '#ccc' : '#007bff',
-                        color: 'white',
-                        border: 'none',
-                        borderRadius: '4px',
-                        cursor: regenerating ? 'default' : 'pointer',
-                        display: 'flex',
-                        alignItems: 'center'
-                    }}>
-                    <FontAwesomeIcon icon={faRedoAlt} style={{ marginRight: '6px' }} />
-                    {regenerating ? 'Regenerating...' : 'Regenerate Scrapbook'}
-                </button>
-            </div>
-        );
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss
deleted file mode 100644
index ae647ad36..000000000
--- a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss
+++ /dev/null
@@ -1,85 +0,0 @@
-//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
-.scrapbook-slot {
-  position: absolute;
-  background-color: rgba(245, 245, 245, 0.7);
-  border: 2px dashed #ccc;
-  border-radius: 5px;
-  box-sizing: border-box;
-  transition: all 0.2s ease;
-  overflow: hidden;
-
-  &.scrapbook-slot-over {
-    border-color: #4a90e2;
-    background-color: rgba(74, 144, 226, 0.1);
-  }
-
-  &.scrapbook-slot-filled {
-    border-style: solid;
-    border-color: rgba(0, 0, 0, 0.1);
-    background-color: transparent;
-
-    &.scrapbook-slot-over {
-      border-color: #4a90e2;
-      background-color: rgba(74, 144, 226, 0.1);
-    }
-  }
-
-  .scrapbook-slot-empty {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    width: 100%;
-    height: 100%;
-  }
-
-  .scrapbook-slot-placeholder {
-    text-align: center;
-    color: #888;
-  }
-
-  .scrapbook-slot-title {
-    font-weight: bold;
-    margin-bottom: 5px;
-  }
-
-  .scrapbook-slot-instruction {
-    font-size: 0.9em;
-    font-style: italic;
-  }
-
-  .scrapbook-slot-content {
-    width: 100%;
-    height: 100%;
-    position: relative;
-  }
-
-  .scrapbook-slot-controls {
-    position: absolute;
-    top: 5px;
-    right: 5px;
-    z-index: 10;
-    opacity: 0;
-    transition: opacity 0.2s ease;
-
-    .scrapbook-slot-remove-btn {
-      background-color: rgba(255, 255, 255, 0.8);
-      border: 1px solid #ccc;
-      border-radius: 50%;
-      width: 20px;
-      height: 20px;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      cursor: pointer;
-      font-size: 10px;
-      
-      &:hover {
-        background-color: rgba(255, 0, 0, 0.1);
-      }
-    }
-  }
-
-  &:hover .scrapbook-slot-controls {
-    opacity: 1;
-  }
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx
deleted file mode 100644
index 2c8f93778..000000000
--- a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-
-//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
-export interface SlotDefinition {
-    id: string;
-    x: number; y: number;
-    defaultWidth: number;
-    defaultHeight: number;
-  }
-  
-  export interface SlotContentMap {
-    slotId: string;
-    docId?: string;
-  }
-  
-  export interface ScrapbookConfig {
-    slots: SlotDefinition[];
-    contents?: SlotContentMap[];
-  }
-  
-  export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = {
-    slots: [
-      { id: "slot1", x: 10, y: 10, defaultWidth: 180, defaultHeight: 120 },
-      { id: "slot2", x: 200, y: 10, defaultWidth: 180, defaultHeight: 120 },
-      // …etc
-    ],
-    contents: []
-  };
-  
\ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts
deleted file mode 100644
index 686917d9a..000000000
--- a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// ScrapbookSlotTypes.ts
-export interface SlotDefinition {
-    id: string;
-    title: string;
-    x: number;
-    y: number;
-    defaultWidth: number;
-    defaultHeight: number;
-  }
-  
-  export interface ScrapbookConfig {
-    slots: SlotDefinition[];
-    contents?: { slotId: string; docId: string }[];
-  }
-  
-  // give it three slots by default:
-  export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = {
-    slots: [
-      { id: "main",      title: "Main Content", x: 20,  y: 20,  defaultWidth: 360, defaultHeight: 200 },
-      { id: "notes",     title: "Notes",        x: 20,  y: 240, defaultWidth: 360, defaultHeight: 160 },
-      { id: "resources", title: "Resources",    x: 400, y: 20,  defaultWidth: 320, defaultHeight: 380 },
-    ],
-    contents: [],
-  };
-  
\ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/scrapbookleftover.ts b/src/client/views/nodes/scrapbook/scrapbookleftover.ts
index 2f381ab95..ee7a858e4 100644
--- a/src/client/views/nodes/scrapbook/scrapbookleftover.ts
+++ b/src/client/views/nodes/scrapbook/scrapbookleftover.ts
@@ -1,8 +1,4 @@
-
-   
-
-
-            //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)])
+ //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)])
 
             if (placeholder) {
                 /**Look at the autotags and see what matches*RTFCast(d[Doc.LayoutDataKey(d)])?.Text*/
-- 
cgit v1.2.3-70-g09d2


From 403dcfb5e8b659f62ed51212ede3f5807caa58c6 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 10 Jun 2025 19:06:31 -0400
Subject: cleanup and streamlining of scrapbook code.

---
 src/client/documents/Documents.ts                  |   3 +
 .../collections/collectionFreeForm/MarqueeView.tsx | 112 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  44 +-
 src/client/views/nodes/scrapbook/ScrapbookBox.scss |  87 +--
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx  | 645 +++++++--------------
 .../views/nodes/scrapbook/ScrapbookPreset.tsx      | 169 ++----
 .../nodes/scrapbook/ScrapbookPresetRegistry.ts     |  20 +-
 7 files changed, 353 insertions(+), 727 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4ad9c9bd8..aea636040 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -331,6 +331,9 @@ export class DocumentOptions {
     toolType?: string; // type of pen tool
     expertMode?: BOOLt = new BoolInfo('something available only in expert (not novice) mode');
 
+    placeholder_docType?: STRt = new StrInfo('type of document that this document accepts as a child', false, false, Array.from(Object.keys(DocumentType)));
+    placeholder_acceptTags?: List<string>;
+
     contextMenuFilters?: List<ScriptField>;
     contextMenuScripts?: List<ScriptField>;
     contextMenuLabels?: List<string>;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b2b904509..12515a72c 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -81,14 +81,11 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     @observable _labelsVisibile: boolean = false;
     @observable _lassoPts: [number, number][] = [];
     @observable _lassoFreehand: boolean = false;
-      // ─── New Observables for “Pick 1 of N AI Scrapbook” ───
-    @observable aiChoices: Doc[] = [];      // temporary hidden Scrapbook docs
-    @observable pickerX = 0;                // popup x coordinate
-    @observable pickerY = 0;                // popup y coordinate
-    @observable pickerVisible = false;      // show/hide ScrapbookPicker
-
-
-    
+    // ─── New Observables for “Pick 1 of N AI Scrapbook” ───
+    @observable aiChoices: Doc[] = []; // temporary hidden Scrapbook docs
+    @observable pickerX = 0; // popup x coordinate
+    @observable pickerY = 0; // popup y coordinate
+    @observable pickerVisible = false; // show/hide ScrapbookPicker
 
     @computed get Transform() {
         return this._props.getTransform();
@@ -532,42 +529,39 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         MarqueeOptionsMenu.Instance.fadeOut(true);
     });
 
-
     getAiPresetsDescriptors(): DocumentDescriptor[] {
-    const selected = this.marqueeSelect(false);
-    return selected.map((doc) => ({
-      type: typeof doc.$type === 'string' ? doc.$type : 'UNKNOWN',
-      tags: (() => {
-        const s = new Set<string>();
-        StrListCast(doc.$tags_chat ?? new List<string>()).forEach((t) => s.add(t));
-        return Array.from(s);
-      })(),
-    }));
-  }
-   
+        const selected = this.marqueeSelect(false);
+        return selected.map(doc => ({
+            type: typeof doc.$type === 'string' ? doc.$type : 'UNKNOWN',
+            tags: (() => {
+                const s = new Set<string>();
+                StrListCast(doc.$tags_chat ?? new List<string>()).forEach(t => s.add(t));
+                return Array.from(s);
+            })(),
+        }));
+    }
 
     generateScrapbook = action(async () => {
-   
         const selectedDocs = this.marqueeSelect(false);
         if (!selectedDocs.length) return;
 
-    const descriptors = this.getAiPresetsDescriptors();
-    if (descriptors.length === 0) {
-      alert('No documents selected to generate a scrapbook from!');
-      return;
-    }
+        const descriptors = this.getAiPresetsDescriptors();
+        if (descriptors.length === 0) {
+            alert('No documents selected to generate a scrapbook from!');
+            return;
+        }
 
         const aiPreset = await requestAiGeneratedPreset(descriptors);
         if (!aiPreset.length) {
-            alert("Failed to generate preset");
+            alert('Failed to generate preset');
             return;
         }
         const scrapbookPlaceholders: Doc[] = buildPlaceholdersFromConfigs(aiPreset);
         /*
         const scrapbookPlaceholders: Doc[] = aiPreset.map(cfg => {
             const placeholderDoc = Docs.Create.TextDocument(cfg.tag);
-            placeholderDoc.accepts_docType = cfg.type as DocumentType;
-            placeholderDoc.accepts_tagType = new List<string>(cfg.acceptTags ?? [cfg.tag]);
+            placeholderDoc.placeholder_docType = cfg.type as DocumentType;
+            placeholderDoc.placeholder_acceptTags = new List<string>(cfg.acceptTags ?? [cfg.tag]);
 
             const placeholder = new Doc();
             placeholder.proto = placeholderDoc;
@@ -586,25 +580,17 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             y: this.Bounds.top,
             _width: 500,
             _height: 500,
-            title: 'AI-generated Scrapbook'
+            title: 'AI-generated Scrapbook',
         });
 
-
-
         // 3) Now grab that new scrapbook’s flat placeholders
         const flatPl = DocListCast(scrapbook[Doc.LayoutDataKey(scrapbook)]) as Doc[];
-        const unwrap = (items: Doc[]): Doc[] =>
-            items.flatMap(d =>
-            d.$type === DocumentType.COL
-                ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)]))
-                : [d]
-            );
+        const unwrap = (items: Doc[]): Doc[] => items.flatMap(d => (d.$type === DocumentType.COL ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)])) : [d]));
         const allPlaceholders = unwrap(flatPl);
 
         // 4) Slot each selectedDocs[i] into the first matching placeholder
         selectedDocs.forEach(realDoc => {
-            slotRealDocIntoPlaceholders(realDoc, allPlaceholders
-            );
+            slotRealDocIntoPlaceholders(realDoc, allPlaceholders);
         });
 
         const selected = this.marqueeSelect(false).map(d => {
@@ -625,9 +611,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         this.hideMarquee();
     });
 
-
-
-
     @action
     marqueeCommand = (e: KeyboardEvent) => {
         const ee = e as unknown as KeyboardEvent & { propagationIsStopped?: boolean };
@@ -649,7 +632,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             if (e.key === 'g') this.collection(e, true);
             if (e.key === 'c' || e.key === 't') this.collection(e);
             if (e.key === 's' || e.key === 'S') this.summary();
-            if (e.key === 'g' || e.key === 'G') this.generateScrapbook();    // ← scrapbook shortcut
+            if (e.key === 'g' || e.key === 'G') this.generateScrapbook(); // ← scrapbook shortcut
             if (e.key === 'p') this.pileup();
             this.cleanupInteractions(false);
         }
@@ -795,27 +778,26 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     render() {
         return (
             <>
-            <div
-                className="marqueeView"
-                ref={r => {
-                    r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject);
-                    this.MarqueeRef = r;
-                }}
-                style={{
-                    overflow: StrCast(this._props.Document._overflow),
-                    cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
-                }}
-                onDragOver={e => e.preventDefault()}
-                onScroll={e => {
-                    e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0;
-                }}
-                onClick={this.onClick}
-                onPointerDown={this.onPointerDown}>
-                {this._visible ? this.marqueeDiv : null}
-                {this.props.children}
-            </div>
-        </>
-    
+                <div
+                    className="marqueeView"
+                    ref={r => {
+                        r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject);
+                        this.MarqueeRef = r;
+                    }}
+                    style={{
+                        overflow: StrCast(this._props.Document._overflow),
+                        cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
+                    }}
+                    onDragOver={e => e.preventDefault()}
+                    onScroll={e => {
+                        e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0;
+                    }}
+                    onClick={this.onClick}
+                    onPointerDown={this.onPointerDown}>
+                    {this._visible ? this.marqueeDiv : null}
+                    {this.props.children}
+                </div>
+            </>
         );
     }
 }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 0c3179173..1768eb08d 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -8,7 +8,6 @@ import { baseKeymap, selectAll, splitBlock } from 'prosemirror-commands';
 import { history } from 'prosemirror-history';
 import { inputRules } from 'prosemirror-inputrules';
 import { keymap } from 'prosemirror-keymap';
-import { runInAction } from 'mobx';
 import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
 import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transaction } from 'prosemirror-state';
 import { EditorView, NodeViewConstructor } from 'prosemirror-view';
@@ -65,7 +64,6 @@ import { removeMarkWithAttrs } from './prosemirrorPatches';
 import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
 import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
-import { tickStep } from 'd3';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 
 export interface FormattedTextBoxProps extends FieldViewProps {
@@ -310,30 +308,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
     };
 
-  autoTag = async () => {
+    autoTag = () => {
+        const rawText = RTFCast(this.Document[this.fieldKey])?.Text ?? StrCast(this.Document[this.fieldKey]);
+        const callType = rawText.includes('[placeholder]') ? GPTCallType.CLASSIFYTEXTMINIMAL : GPTCallType.CLASSIFYTEXTFULL;
 
-    const layoutKey = Doc.LayoutDataKey(this.Document);
-    const rawText = RTFCast(this.Document[layoutKey])?.Text ?? StrCast(this.Document[layoutKey]);
-
-    const callType = rawText.includes("[placeholder]")
-        ? GPTCallType.CLASSIFYTEXTMINIMAL
-        : GPTCallType.CLASSIFYTEXTFULL;
-
-    gptAPICall(rawText, callType).then(desc => {
-        runInAction(() => {
-            // Clear existing tags
-            this.Document.$tags_chat = new List<string>();
-
-            // Split GPT response into tokens and push individually
-            const tokens = desc.trim().split(/\s+/);
-            tokens.forEach(tok => {
-                (this.Document.$tags_chat as List<string>).push(tok);
-            });
-
-            this.Document._layout_showTags = true;
-        });
-    });
-};
+        gptAPICall(rawText, callType).then(
+            action(desc => {
+                // Split GPT response into tokens and push individually & clear existing tags
+                this.Document.$tags_chat = new List<string>(desc.trim().split(/\s+/));
+                this.Document._layout_showTags = true;
+            })
+        );
+    };
 
     leafText = (node: Node) => {
         if (node.type === this.EditorView?.state.schema.nodes.dashField) {
@@ -1298,13 +1284,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             { fireImmediately: true }
         );
 
-        this._disposers.tagger = reaction(
-            () => ({ title: this.Document.title, sel: this._props.isSelected() }),
-            action(() => {
-                this.autoTag();
-            }),
-            { fireImmediately: true }
-        );
+        this._disposers.tagger = reaction(() => ({ title: this.Document.title, sel: this._props.isSelected() }), this.autoTag, { fireImmediately: true });
 
         if (!this._props.dontRegisterView) {
             this._disposers.record = reaction(
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.scss b/src/client/views/nodes/scrapbook/ScrapbookBox.scss
index 8dc93df60..6ac2220f9 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.scss
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.scss
@@ -1,63 +1,66 @@
-
 .scrapbook-box {
-  /* Make sure the container fills its parent, and set a base background */
-  position: relative; /* so that absolute children (loading overlay, etc.) are positioned relative to this */
-  width: 100%;
-  height: 100%;
-  background: beige;
-  overflow: hidden; /* prevent scrollbars if children overflow */
+    /* Make sure the container fills its parent, and set a base background */
+    position: relative; /* so that absolute children (loading overlay, etc.) are positioned relative to this */
+    width: 100%;
+    height: 100%;
+    background: beige;
+    overflow: hidden; /* prevent scrollbars if children overflow */
 }
 
 /* Loading overlay that covers the entire scrapbook while AI-generation is in progress */
 .scrapbook-box-loading-overlay {
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background: rgba(255, 255, 255, 0.8);
-  z-index: 10; /* sits above the ImageBox and other content */
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background: rgba(255, 255, 255, 0.8);
+    z-index: 10; /* sits above the ImageBox and other content */
 }
 
 /* The <select> dropdown for choosing presets */
 .scrapbook-box-preset-select {
-  position: absolute;
-  top: 8px;
-  left: 8px;
-  z-index: 20;
-  padding: 4px 8px;
-  font-size: 14px;
-  border: 1px solid #ccc;
-  border-radius: 4px;
-  background: white;
+    position: relative;
+    top: 8px;
+    left: 8px;
+    z-index: 20;
+    padding: 4px 8px;
+    font-size: 14px;
+    border: 1px solid #ccc;
+    border-radius: 4px;
+    background: white;
 }
 
 /* Container for the “Regenerate Background” button */
 .scrapbook-box-ui {
-  position: absolute;
-  top: 8px;
-  right: 8px;
-  z-index: 20;
+    position: relative;
+    top: 8px;
+    right: 8px;
+    z-index: 20;
+    background: white;
+    width: 40px;
+    display: flex;
+    justify-content: center;
 }
 
 /* The button itself */
 .scrapbook-box-ui-button {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  padding: 4px 8px;
-  font-size: 14px;
-  color: black;
-  background: white;
-  border: 1px solid #ccc;
-  border-radius: 4px;
-  cursor: pointer;
-  white-space: nowrap;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 4px 8px;
+    font-size: 14px;
+    color: black;
+    background: white;
+    border: 1px solid #ccc;
+    border-radius: 4px;
+    cursor: pointer;
+    white-space: nowrap;
 }
 
 .scrapbook-box-ui-button:hover {
-  background: #f5f5f5;
+    background: #f5f5f5;
 }
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index 52e3c26dc..fcb82a6ba 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -1,510 +1,259 @@
-import { action, makeObservable, observable, reaction, computed } from 'mobx';
+import { IconButton, Size } from '@dash/components';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
+import ReactLoading from 'react-loading';
+import { Doc, DocListCast, Opt, StrListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
+import { DateCast, DocCast, NumCast, toList } from '../../../../fields/Types';
 import { emptyFunction } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { DocumentType } from '../../../documents/DocumentTypes';
+import { DragManager } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { undoable } from '../../../util/UndoManager';
 import { CollectionView } from '../../collections/CollectionView';
 import { ViewBoxAnnotatableComponent } from '../../DocComponent';
-import { AspectRatioLimits } from '../../smartdraw/FireflyConstants';
+import { AspectRatioLimits, FireflyImageDimensions } from '../../smartdraw/FireflyConstants';
+import { SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
 import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
-import { DragManager } from '../../../util/DragManager';
-import { toList } from '../../../../fields/Types';
-import { undoable } from '../../../util/UndoManager';
-import ReactLoading from 'react-loading';
-import { NumCast } from '../../../../fields/Types';
-import { ScrapbookItemConfig } from './ScrapbookPreset';
 import { ImageBox } from '../ImageBox';
-import { FireflyImageDimensions } from '../../smartdraw/FireflyConstants';
-import { SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
-import { ImageCast } from '../../../../fields/Types';
-import { SnappingManager } from '../../../util/SnappingManager';
-import { IReactionDisposer } from 'mobx';
-import { observer } from 'mobx-react';
-import { runInAction } from 'mobx';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faRedoAlt }   from '@fortawesome/free-solid-svg-icons';
-import { getPresetNames, createPreset } from './ScrapbookPresetRegistry';
 import './ScrapbookBox.scss';
-import { isDestArraysEqual } from 'pdfjs-dist/types/web/pdf_history';
-
-
-export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]): Doc[] {
-  const placeholders: Doc[] = [];
-
-  for (const cfg of configs) {
-    if (cfg.children && cfg.children.length) {
-      const childDocs = cfg.children.map(child => {
-        const doc = Docs.Create.TextDocument("[placeholder] " + child.tag);
-        doc.accepts_docType = child.type;
-        doc.accepts_tagType = new List<string>(child.acceptTags ?? [child.tag]);
-
-        const ph = new Doc();
-        ph.proto = doc;
-        ph.original = doc;
-        ph.x = child.x;
-        ph.y = child.y;
-        if (child.width != null) ph._width = child.width;
-        if (child.height != null) ph._height = child.height;
-        return ph;
-      });
-
-      const protoW = cfg.containerWidth ?? cfg.width;
-      const protoH = cfg.containerHeight ?? cfg.height;
-      // Create a stacking document with the child placeholders
-      const containerProto = Docs.Create.StackingDocument(childDocs, {
-        ...(protoW != null ? { _width: protoW } : {}),
-        ...(protoH != null ? { _height: protoH } : {}),
-        title: cfg.tag,
-      });
-
-      const ph = new Doc();
-      ph.proto = containerProto;
-      ph.original = containerProto;
-      ph.x = cfg.x;
-      ph.y = cfg.y;
-      if (cfg.width != null) ph._width = cfg.width;
-      if (cfg.height != null) ph._height = cfg.height;
-      placeholders.push(ph);
-    } 
-    
-    else {
-      const doc = Docs.Create.TextDocument("[placeholder] " + cfg.tag);
-      doc.accepts_docType = cfg.type;
-      doc.accepts_tagType = new List<string>(cfg.acceptTags ?? [cfg.tag]);
-
-      const ph = new Doc();
-      ph.proto = doc;
-      ph.original = doc;
-      ph.x = cfg.x;
-      ph.y = cfg.y;
-      if (cfg.width != null) ph._width = cfg.width;
-      if (cfg.height != null) ph._height = cfg.height;
-      placeholders.push(ph);
-    }
-  }
+import { ScrapbookItemConfig } from './ScrapbookPreset';
+import { createPreset, getPresetNames } from './ScrapbookPresetRegistry';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 
-  return placeholders;
+function createPlaceholder(cfg: ScrapbookItemConfig, doc: Doc) {
+    const placeholder = new Doc();
+    placeholder.proto = doc;
+    placeholder.original = doc;
+    placeholder.x = cfg.x;
+    placeholder.y = cfg.y;
+    if (cfg.width !== null) placeholder._width = cfg.width;
+    if (cfg.height !== null) placeholder._height = cfg.height;
+    return placeholder;
 }
-export function slotRealDocIntoPlaceholders(
-  realDoc: Doc,
-  placeholders: Doc[]
-): boolean {
- const realTags = new Set<string>(
-  StrListCast(realDoc.$tags_chat ?? new List<string>())
-    .map(t => t.toLowerCase())
-);
 
-  // Find placeholder with most matching tags
-  let bestMatch: Doc | null = null;
-  let maxMatches = 0;
-/*
-                 (d.accepts_docType === docs[0].$type ||   // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type
-                  RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type)))*/
-
-  placeholders.forEach(ph => {
-    if (ph.accepts_docType !== realDoc.$type) {
-      // Skip this placeholder entirely if types do not match.
-      return;
-    };
-    const phTagTypes = StrListCast(ph.accepts_tagType ?? new List<string>())
-  .map(t => t.toLowerCase());
-    console.log({ realTags, phTagTypes });
-    const matches = phTagTypes.filter(tag => realTags.has(tag));
+function createMessagePlaceholder(cfg: ScrapbookItemConfig) {
+    return createPlaceholder(cfg, 
+        Docs.Create.TextDocument(cfg.message ?? ('[placeholder] ' + cfg.acceptTags?.[0]), { placeholder: "", placeholder_docType: cfg.type, placeholder_acceptTags: new List<string>(cfg.acceptTags) })
+    ); // prettier-ignore
+}
+export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]) {
+    return configs.map(cfg => {
+        if (cfg.children?.length) {
+            const childDocs = cfg.children.map(createMessagePlaceholder);
+            const protoW = cfg.containerWidth ?? cfg.width;
+            const protoH = cfg.containerHeight ?? cfg.height;
+            // Create a stacking document with the child placeholders
+            const containerProto = Docs.Create.StackingDocument(childDocs, {
+                ...(protoW !== null ? { _width: protoW } : {}),
+                ...(protoH !== null ? { _height: protoH } : {}),
+                title: cfg.message,
+            });
+            return createPlaceholder(cfg, containerProto);
+        }
+        return createMessagePlaceholder(cfg);
+    });
+}
+export function slotRealDocIntoPlaceholders(realDoc: Doc, placeholders: Doc[]): boolean {
+    const realTags = new Set<string>(StrListCast(realDoc.$tags_chat).map(t => t.toLowerCase?.() ?? ''));
+
+    // Find placeholder with most matching tags
+    let bestMatch: Doc | null = null;
+    let maxMatches = 0;
+
+    // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type
+    placeholders
+        .filter(ph => ph.placeholder_docType === realDoc.$type) // Skip this placeholder entirely if types do not match.
+        .forEach(ph => {
+            const matches = StrListCast(ph.placeholder_acceptTags)
+                .map(t => t.toLowerCase?.())
+                .filter(tag => realTags.has(tag));
+
+            if (matches.length > maxMatches) {
+                maxMatches = matches.length;
+                bestMatch = ph;
+            }
+        });
 
-    if (matches.length > maxMatches) {
-      maxMatches = matches.length;
-      bestMatch = ph;
+    if (bestMatch && maxMatches > 0) {
+        setTimeout(undoable(() => (bestMatch!.proto = realDoc), 'Scrapbook add'));
+        return true;
     }
 
-  });
-
-  if (bestMatch && maxMatches > 0) {
-    setTimeout(
-      undoable(() => {
-        bestMatch!.proto = realDoc;
-      }, 'Scrapbook add'),
-      0
-    );
-    return true;
-  }
-
-  return false;
+    return false;
 }
 
 // Scrapbook view: a container that lays out its child items in a template
 @observer
 export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
-    @observable selectedPreset = getPresetNames()[0];
-
-    @observable createdDate: string;
-    @observable loading = false;
-    @observable src = '';
-    @observable imgDoc: Doc | undefined;
+    public static LayoutString(fieldStr: string) {
+        return FieldView.LayoutString(ScrapbookBox, fieldStr);
+    }
     private _disposers: { [name: string]: IReactionDisposer } = {};
-    private imageBoxRef = React.createRef<ImageBox>();
-
+    private _imageBoxRef = React.createRef<ImageBox>();
 
     constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
-        const existingItems = DocListCast(this.dataDoc[this.fieldKey] as List<Doc>);
-    if (!existingItems || existingItems.length === 0) {
-      // Only wire up reaction/setTitle if it's truly a brand-new, empty Scrapbook
-      reaction(
-        () => this.selectedPreset,
-        presetName => this.initScrapbook(presetName),
-        { fireImmediately: true }
-      );
-
-      this.createdDate = this.getFormattedDate();
-      this.setTitle();
-    } else {
-      // If items are already present, just preserve whatever was injected.
-      // We still want `createdDate` set so that the UI title bar can show it if needed.
-      this.createdDate = this.getFormattedDate();
     }
-     
 
-        // ensure we always have a List<Doc> in dataDoc['items']
-        if (!this.dataDoc[this.fieldKey]) {
-            this.dataDoc[this.fieldKey] = new List<Doc>();
-        }
-  
-    }
-
-    public static LayoutString(fieldStr: string) {
-        return FieldView.LayoutString(ScrapbookBox, fieldStr);
-    }
+    @observable _selectedPreset = getPresetNames()[0];
+    @observable _loading = false;
 
-    getFormattedDate(): string {
-        return new Date().toLocaleDateString(undefined, {
+    @computed get createdDate() {
+        return DateCast(this.dataDoc.$author_date)?.date.toLocaleDateString(undefined, {
             year: 'numeric',
             month: 'short',
             day: 'numeric',
         });
     }
-
-    
-    @action
-    initScrapbook(name: string) {
-        const configs = createPreset(name); 
-      // 1) ensure title is set
-      const title = `Scrapbook - ${this.createdDate}`;
-      if (this.dataDoc.title !== title) {
-        this.dataDoc.title = title;
-      }
-    
-      // 2) build placeholders from the preset
-      const placeholders = buildPlaceholdersFromConfigs(configs);
-
-      // 3) commit them into the field
-      this.dataDoc[this.fieldKey] = new List<Doc>(placeholders);
-    }
-
-    
+    @computed get ScrapbookLayoutDocs()  { return DocListCast(this.dataDoc[this.fieldKey]); } // prettier-ignore
+    @computed get BackgroundDoc()        { return DocCast(this.dataDoc[this.fieldKey + '_background']); } // prettier-ignore
+    set ScrapbookLayoutDocs(doc: Doc[])  { this.dataDoc[this.fieldKey] = new List(doc); } // prettier-ignore
+    set BackgroundDoc(doc: Opt<Doc>)     { this.dataDoc[this.fieldKey + '_background'] = doc; } // prettier-ignore
 
     @action
-    setTitle() {
-        const title = `Scrapbook - ${this.createdDate}`;
-        if (this.dataDoc.title !== title) {
-            this.dataDoc.title = title;
-            if (!this.dataDoc[this.fieldKey]){
-            const image = Docs.Create.TextDocument('[placeholder] person image');
-            image.accepts_docType = DocumentType.IMG;
-            image.accepts_tagType = 'PERSON' 
-            const placeholder = new Doc();
-            placeholder.proto = image;
-            placeholder.original = image;
-            placeholder._width = 250;
-            placeholder._height = 200;
-            placeholder.x = 0;
-            placeholder.y = -100;
-          
-            
-            const summary = Docs.Create.TextDocument('[placeholder] long summary');
-            summary.accepts_docType = DocumentType.RTF;
-            summary.accepts_tagType = 'lengthy description';
-            const placeholder2 = new Doc();
-            placeholder2.proto = summary;
-            placeholder2.original = summary;
-            placeholder2.x = 0;
-            placeholder2.y = 200;
-            placeholder2._width = 250;
-        
-            
-            const sidebar = Docs.Create.TextDocument('[placeholder] brief sidebar');
-            sidebar.accepts_docType = DocumentType.RTF;
-            sidebar.accepts_tagType = 'title'; 
-            const placeholder3 = new Doc();
-            placeholder3.proto = sidebar;
-            placeholder3.original = sidebar;
-            placeholder3.x = 280;
-            placeholder3.y = -50;
-            placeholder3._width = 50; 
-            placeholder3._height = 200;
-
-          
-
-            const internalImg = Docs.Create.TextDocument('[placeholder] landscape internal');
-            internalImg.accepts_docType = DocumentType.IMG;
-            internalImg.accepts_tagType = 'LANDSCAPE'
-            const placeholder5 = new Doc();
-            placeholder5.proto = internalImg;
-            placeholder5.original = internalImg;
-            placeholder5._width = 50;
-            placeholder5._height = 100;
-            placeholder5.x = 0;
-            placeholder5.y = -100;
-
-            const collection = Docs.Create.StackingDocument([placeholder5], { _width: 300, _height: 300, title: "internal coll" });
-            //collection.accepts_docType = DocumentType.COL; don't mark this field
-            const placeholder4 = new Doc();
-            placeholder4.proto = collection;
-            placeholder4.original = collection;
-            placeholder4.x = -200;
-            placeholder4.y = -100;
-            placeholder4._width = 100;
-            placeholder4._height = 200;
-
-            const starter = Docs.Create.TextDocument('To create a scrapbook from existing documents, marquee select. For existing scrapbook arrangements, select a preset from the dropdown.');
-            starter.accepts_docType = DocumentType.RTF;
-            starter.accepts_tagType = 'n/a'
-            const starterplaceholder = new Doc();
-            starterplaceholder.proto = summary;
-            starterplaceholder.original = summary;
-            starterplaceholder.x = 0;
-            starterplaceholder.y = 0;
-            starterplaceholder._width = 250;
-        
-
-            
-
-
-            /*note-to-self
-            would doing:
-
-            const collection = Docs.Create.ScrapbookDocument([placeholder, placeholder2, placeholder3]);
-            create issues with references to the same object?*/
-
-            /*note-to-self
-            Should we consider that there are more collections than just COL type collections?
-            when spreading*/
-
-           /*note-to-self
-           difference between passing a new List<Doc> versus just the raw array?
-           */
-           this.dataDoc[this.fieldKey] = new List<Doc>([starterplaceholder]);
-            }
+    setDefaultPlaceholder = () => {
+        this.ScrapbookLayoutDocs = [
+            createMessagePlaceholder({
+                message: 'To create a scrapbook from existing documents, marquee select. For existing scrapbook arrangements, select a preset from the dropdown.',
+                type: DocumentType.RTF,
+                width: 250,
+                height: 200,
+                x: 0,
+                y: 0,
+            }),
+        ];
+
+        const placeholder1 = createMessagePlaceholder({ acceptTags: ['PERSON'], type: DocumentType.IMG, width: 250, height: 200, x: 0, y: -100 });
+        const placeholder2 = createMessagePlaceholder({ acceptTags: ['lengthy description'], type: DocumentType.RTF, width: 250, height: undefined, x: 0, y: 200 });
+        const placeholder3 = createMessagePlaceholder({ acceptTags: ['title'], type: DocumentType.RTF, width: 50, height: 200, x: 280, y: -50 });
+        const placeholder4 = createPlaceholder( { width: 100, height: 200, x: -200, y: -100 }, Docs.Create.StackingDocument([
+                             createMessagePlaceholder({ acceptTags: ['LANDSCAPE'], type: DocumentType.IMG, width: 50, height: 100, x: 0, y: -100 })
+                             ], { _width: 300, _height: 300, title: 'internal coll' })); // prettier-ignore
+        console.log('UNUSED', placeholder4, placeholder3, placeholder2, placeholder1);
+        /* note-to-self
+                    would doing:
+                        const collection = Docs.Create.ScrapbookDocument([placeholder, placeholder2, placeholder3]);
+                        create issues with references to the same object? */
+
+        /*note-to-self
+                    Should we consider that there are more collections than just COL type collections?
+                    when spreading */
+
+        /*note-to-self
+                    difference between passing a new List<Doc> versus just the raw array? */
+    };
 
-          
-        }
-    }
+    selectPreset = action((presetName: string) => (this.ScrapbookLayoutDocs = buildPlaceholdersFromConfigs(createPreset(presetName))));
 
     componentDidMount() {
-        this.setTitle();
-        this.generateAiImage();
+        const title = `Scrapbook - ${this.createdDate}`;
+        if (!this.ScrapbookLayoutDocs.length) this.setDefaultPlaceholder();
+        if (!this.BackgroundDoc) this.generateAiImage(this.regenPrompt);
+        if (this.dataDoc.title !== title) this.dataDoc.title = title; // ensure title is set
 
         this._disposers.propagateResize = reaction(
             () => ({ w: this.layoutDoc._width, h: this.layoutDoc._height }),
             (dims, prev) => {
-              // prev is undefined on the first run, so bail early
-              if (!prev || !SnappingManager.ShiftKey || !this.imgDoc) return;
-          
-              // either guard the ref…
-              const imageBox = this.imageBoxRef.current;
-              if (!imageBox) return;
-          
-              // …or just hard-code the fieldKey if you know it’s always `"data"`
-              const key = imageBox.props.fieldKey;
-          
-              runInAction(() => {
-                if(!this.imgDoc){
-                    return
+                const imageBox = this._imageBoxRef.current;
+                // prev is undefined on the first run
+                if (prev && SnappingManager.ShiftKey && this.BackgroundDoc && imageBox) {
+                    this.BackgroundDoc[imageBox.fieldKey + '_outpaintOriginalWidth'] = prev.w;
+                    this.BackgroundDoc[imageBox.fieldKey + '_outpaintOriginalHeight'] = prev.h;
+                    imageBox.layoutDoc._width = dims.w;
+                    imageBox.layoutDoc._height = dims.h;
                 }
-                // use prev.w/h (the *old* size) as your orig dims
-                this.imgDoc[key + '_outpaintOriginalWidth']  = prev.w;
-                this.imgDoc[key + '_outpaintOriginalHeight'] = prev.h;
-                ;(this.imageBoxRef.current as any).layoutDoc._width  = dims.w
-                ;(this.imageBoxRef.current as any).layoutDoc._height = dims.h
-          
-              });
             }
         );
     }
 
-
     @action
-    async generateAiImage(prompt?: string) {
-          this.loading = true;
-          try {
-            // 1) Default to regenPrompt if none provided
-            if (!prompt) prompt = this.regenPrompt;
-        
-            // 2) Measure the scrapbook’s current size
-            const w = NumCast(this.layoutDoc._width, 1);
-            const h = NumCast(this.layoutDoc._height, 1);
-            const ratio = w / h;
-        
-            // 3) Pick the Firefly preset that best matches the aspect ratio
-            let preset = FireflyImageDimensions.Square;
-            if (ratio > AspectRatioLimits[FireflyImageDimensions.Widescreen]) {
-              preset = FireflyImageDimensions.Widescreen;
-            } else if (ratio > AspectRatioLimits[FireflyImageDimensions.Landscape]) {
-              preset = FireflyImageDimensions.Landscape;
-            } else if (ratio < AspectRatioLimits[FireflyImageDimensions.Portrait]) {
-              preset = FireflyImageDimensions.Portrait;
-            }
-        
-            // 4) Call exactly the same CreateWithFirefly that ImageBox uses
-            const doc = await SmartDrawHandler.CreateWithFirefly(prompt, preset);
-        
-            if (doc instanceof Doc) {
-              // 5) Hook it into your state
-              this.imgDoc = doc;
-              const imgField = ImageCast(doc.data);
-              this.src      = imgField?.url.href ?? '';
-            } else {
-              alert('Failed to generate document.');
-              this.src = '';
-            }
-          } catch (e) {
-            alert(`Generation error: ${e}`);
-          } finally {
-            runInAction(() => {
-              this.loading = false;
-            });
-          }
-        }
-      
-    childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => {
-        return true; // disable dropping documents onto any child of the scrapbook.
-    };
-    rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => {
-        // Test to see if the dropped doc is dropped on an acceptable location (anywerhe? on a specific box).
-        // const draggedDocs = de.complete.docDragData?.draggedDocuments;
-        return false; // allow all Docs to be dropped onto scrapbook -- let filterAddDocument make the final decision.
+    generateAiImage = (prompt: string) => {
+        this._loading = true;
+
+        const ratio = NumCast(this.layoutDoc._width, 1) / NumCast(this.layoutDoc._height, 1); // Measure the scrapbook’s current aspect
+        const choosePresetForDimensions = (() => { // Pick the Firefly preset that best matches the aspect ratio
+            if (ratio > AspectRatioLimits[FireflyImageDimensions.Widescreen]) return FireflyImageDimensions.Widescreen;
+            if (ratio > AspectRatioLimits[FireflyImageDimensions.Landscape]) return FireflyImageDimensions.Landscape;
+            if (ratio < AspectRatioLimits[FireflyImageDimensions.Portrait]) return FireflyImageDimensions.Portrait;
+            return FireflyImageDimensions.Square;
+        })(); // prettier-ignore
+
+        SmartDrawHandler.CreateWithFirefly(prompt, choosePresetForDimensions)  // Call exactly the same CreateWithFirefly that ImageBox uses
+            .then(action(doc => {
+                if (doc instanceof Doc) {
+                    this.BackgroundDoc = doc; // set the background image directly on the scrapbook 
+                } else {
+                    alert('Failed to generate document.');
+                }
+            }))
+            .catch(e => alert(`Generation error: ${e}`))
+            .finally(action(() => (this._loading = false))); // prettier-ignore
     };
 
-    filterAddDocument = (docIn: Doc | Doc[]) => {
-        const docs = toList(docIn); //The docs being added to the scrapbook
-
-        // 1) Grab all template slots:
-        const slots = DocListCast(this.dataDoc[this.fieldKey]);
-
-        // 2) recursive unwrap:
-        const unwrap = (items: Doc[]): Doc[] =>
-            items.flatMap(d =>
-            d.$type === DocumentType.COL
-                ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)]))
-                : [d]
-            );
-
-        // 3) produce a flat list of every doc, unwrapping any number of nested COLs
-        const allDocs: Doc[] = unwrap(slots);
-          if (docs?.length === 1) {
-        return slotRealDocIntoPlaceholders(
-            docs[0],
-            allDocs,
-        )
-            ? false
-            : false;
-    }
-
-    return false;
-};
-     
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => true; // disable dropping documents onto any child of the scrapbook.
+
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => false; // allow all Docs to be dropped onto scrapbook -- let filterAddDocument make the final decision.
+
+    /**
+     * iterate through all items and their children and return a flat list of leaf placeholder content Docs
+     * @param items
+     * @returns list of placeholder content Docs
+     */
+    unwrapPlaceholders = (items: Doc[]): Doc[] => items.flatMap(d => (d.$type === DocumentType.COL ? this.unwrapPlaceholders(DocListCast(d[Doc.LayoutDataKey(d)])) : [d]));
+
+    /**
+     * Filter function to determine if a document can be added to the scrapbook.
+     * This checks if the document matches any of the placeholder slots in the scrapbook.
+     * @param docs - The document(s) being added to the scrapbook.
+     * @returns true if the document can be added, false otherwise.
+     */
+    filterAddDocument = (docs: Doc | Doc[]) => {
+        toList(docs).forEach(doc => slotRealDocIntoPlaceholders(doc, this.unwrapPlaceholders(this.ScrapbookLayoutDocs)));
+        return false;
+    };
 
     @computed get regenPrompt() {
-        const slots = DocListCast(this.dataDoc[this.fieldKey]);
-        
-        const unwrap = (items: Doc[]): Doc[] =>
-            items.flatMap(d =>
-                d.$type === DocumentType.COL
-                    ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)]))
-                    : [d]
-            );
-    
-        const allDocs: Doc[] = unwrap(slots);
-        const internalTagsSet = new Set<string>();
-        
-        allDocs.forEach(doc => {
-            const tags = StrListCast(doc.$tags_chat ?? new List<string>());
-            tags.forEach(tag => 
-                 {if (!tag.startsWith("ASPECT_")) {
-            internalTagsSet.add(tag);
-            }
-    });
-        });
-    
+        const allDocs = this.unwrapPlaceholders(this.ScrapbookLayoutDocs); // find all non-collections in scrapbook (e.g., placeholder content docs)
+        const internalTagsSet = new Set<string>(allDocs.flatMap(doc => StrListCast(doc.$tags_chat).filter(tag => !tag.startsWith?.('ASPECT_'))));
         const internalTags = Array.from(internalTagsSet).join(', ');
-      
-    
-        return internalTags
-            ? `Create a new scrapbook background featuring: ${internalTags}`
-            : 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background';
-    }
-       
-   render() {
-    return (
-      <div className="scrapbook-box">
-        {this.loading && (
-          <div className="scrapbook-box-loading-overlay">
-            <ReactLoading type="spin" width={50} height={50} />
-          </div>
-        )}
-
-        {this.src && this.imgDoc && (
-          <ImageBox
-            ref={this.imageBoxRef}
-            {...this._props}
-            Document={this.imgDoc}
-            fieldKey="data"
-          />
-        )}
-
-        <select
-          className="scrapbook-box-preset-select"
-          value={this.selectedPreset}
-          onChange={e => (this.selectedPreset = e.currentTarget.value)}
-        >
-          {getPresetNames().map(name => (
-            <option key={name} value={name}>
-              {name}
-            </option>
-          ))}
-        </select>
 
-        {this._props.isContentActive() && (
-          <div className="scrapbook-box-ui">
-            <button
-              type="button"
-              title="Regenerate Background"
-              onClick={() => this.generateAiImage(this.regenPrompt)}
-              className="scrapbook-box-ui-button"
-            >
-              <FontAwesomeIcon icon={faRedoAlt} />
-              <span>Regenerate Background</span>
-            </button>
-          </div>
-        )}
+        return internalTags ? `Create a new scrapbook background featuring: ${internalTags}` : 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background';
+    }
 
-        <CollectionView
-          {...this._props}
-          setContentViewBox={emptyFunction}
-          rejectDrop={this.rejectDrop}
-          childRejectDrop={this.childRejectDrop}
-          filterAddDocument={this.filterAddDocument}
-        />
-      </div>
-    );
-  }
+    render() {
+        return (
+            <div className="scrapbook-box">
+                <div style={{ display: this._loading ? undefined : 'none' }} className="scrapbook-box-loading-overlay">
+                    <ReactLoading type="spin" width={50} height={50} />
+                </div>
+
+                {this.BackgroundDoc && <ImageBox ref={this._imageBoxRef} {...this._props} Document={this.BackgroundDoc} fieldKey="data" />}
+                <div style={{ display: this._props.isContentActive() ? 'flex' : 'none', alignItems: 'center', justifyContent: 'space-between', padding: '0 10px' }}>
+                    <select className="scrapbook-box-preset-select" value={this._selectedPreset} onChange={action(e => this.selectPreset((this._selectedPreset = e.currentTarget.value)))}>
+                        {getPresetNames().map(name => (
+                            <option key={name} value={name}>
+                                {name}
+                            </option>
+                        ))}
+                    </select>
+                    <div className="scrapbook-box-ui" style={{ opacity: this._loading ? 0.5 : 1 }}>
+                        <IconButton size={Size.SMALL} tooltip="regenerate a new background" label="back-ground" icon={<FontAwesomeIcon icon="redo-alt" size="sm" />} onClick={() => !this._loading && this.generateAiImage(this.regenPrompt)} />
+                    </div>
+                </div>
+
+                <CollectionView {...this._props} setContentViewBox={emptyFunction} rejectDrop={this.rejectDrop} childRejectDrop={this.childRejectDrop} filterAddDocument={this.filterAddDocument} />
+            </div>
+        );
+    }
 }
 
-
-
 Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, {
     layout: { view: ScrapbookBox, dataField: 'items' },
     options: {
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
index 706b9dafd..fe33741af 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
@@ -1,23 +1,22 @@
 import { DocumentType } from '../../../documents/DocumentTypes';
 
 export enum ScrapbookPresetType {
-    Default   = 'Default',
-    Classic   = 'Classic',
+    Default = 'Default',
+    Classic = 'Classic',
     None = 'Select Template',
-    Collage   = 'Collage',
+    Collage = 'Collage',
     Spotlight = 'Spotlight',
-    Gallery   = 'Gallery'
+    Gallery = 'Gallery',
 }
 
 export interface ScrapbookItemConfig {
-    type: DocumentType;
-    /** text shown in the placeholder bubble */
-    tag: string;
-    /** what this slot actually accepts (defaults to `tag`) */
-    acceptTags?: string[];
-
     x: number;
     y: number;
+
+    message?: string; // optional text to display instead of [placeholder] + acceptTags[0]
+    type?: DocumentType;
+    /** what this slot actually accepts (defaults to `tag`) */
+    acceptTags?: string[];
     /** the frame this placeholder occupies */
     width?: number;
     height?: number;
@@ -30,147 +29,65 @@ export interface ScrapbookItemConfig {
 export class ScrapbookPreset {
     static createPreset(presetType: ScrapbookPresetType): ScrapbookItemConfig[] {
         switch (presetType) {
-             case ScrapbookPresetType.None:
-                return ScrapbookPreset.createNonePreset();     
-            case ScrapbookPresetType.Classic:
-                return ScrapbookPreset.createClassicPreset();
-            case ScrapbookPresetType.Collage:
-                return ScrapbookPreset.createCollagePreset();
-            case ScrapbookPresetType.Spotlight:
-                return ScrapbookPreset.createSpotlightPreset();
-            case ScrapbookPresetType.Default:
-                return ScrapbookPreset.createDefaultPreset();   
-            case ScrapbookPresetType.Gallery:
-                return ScrapbookPreset.createGalleryPreset();     
+            case ScrapbookPresetType.None:      return ScrapbookPreset.createNonePreset();     
+            case ScrapbookPresetType.Classic:   return ScrapbookPreset.createClassicPreset();
+            case ScrapbookPresetType.Collage:   return ScrapbookPreset.createCollagePreset();
+            case ScrapbookPresetType.Spotlight: return ScrapbookPreset.createSpotlightPreset();
+            case ScrapbookPresetType.Default:   return ScrapbookPreset.createDefaultPreset();   
+            case ScrapbookPresetType.Gallery:   return ScrapbookPreset.createGalleryPreset();     
             default:
                 throw new Error(`Unknown preset type: ${presetType}`);
-        }
+        } // prettier-ignore
     }
 
-     private static createNonePreset(): ScrapbookItemConfig[] {
-        return [
-       
-            { type: DocumentType.RTF,
-              tag: 'To create a scrapbook from existing documents, marquee select. For existing scrapbook arrangements, select a preset from the dropdown.',
-              acceptTags: ['n/a'],
-              x: 0, y: 0, width: 250, height: 200
-            },
-          
-        ];
+    private static createNonePreset(): ScrapbookItemConfig[] {
+        return [{ message: 'To create a scrapbook from existing documents, marquee select. For existing scrapbook arrangements, select a preset from the dropdown.', type: DocumentType.RTF, acceptTags: [], x: 0, y: 0, width: 250, height: 200 }];
     }
 
     private static createClassicPreset(): ScrapbookItemConfig[] {
         return [
-            { type: DocumentType.IMG,
-              tag: '[placeholder] LANDSCAPE',
-              acceptTags: ['LANDSCAPE'],
-              x: 0, y: -100, width: 250, height: 200
-            },
-            { type: DocumentType.RTF,
-              tag: '[placeholder] caption',
-              acceptTags: ['sentence'],
-              x: 0, y: 200, width: 250, height: 50
-            },
-            { type: DocumentType.RTF,
-              tag: '[placeholder] lengthy description',
-              acceptTags: ['paragraphs'],
-              x: 280, y: -50, width: 50, height: 200
-            },
-            { type: DocumentType.IMG,
-              tag: '[placeholder] PERSON',
-              acceptTags: ['PERSON'],
-              x: -200, y: -100, width: 100, height: 200
-            },
+            { type: DocumentType.IMG, message: '[placeholder] LANDSCAPE', acceptTags: ['LANDSCAPE'], x: 0, y: -100, width: 250, height: 200 },
+            { type: DocumentType.RTF, message: '[placeholder] caption', acceptTags: ['sentence'], x: 0, y: 200, width: 250, height: 50 },
+            { type: DocumentType.RTF, message: '[placeholder] lengthy description', acceptTags: ['paragraphs'], x: 280, y: -50, width: 50, height: 200 },
+            { type: DocumentType.IMG, message: '[placeholder] PERSON', acceptTags: ['PERSON'], x: -200, y: -100, width: 100, height: 200 },
         ];
     }
 
     private static createGalleryPreset(): ScrapbookItemConfig[] {
         return [
-          { type: DocumentType.IMG, tag: 'Gallery 1', acceptTags: ['LANDSCAPE'], x: -150, y: -150, width: 150, height: 150 },
-          { type: DocumentType.IMG, tag: 'Gallery 2', acceptTags: ['LANDSCAPE'], x:   0, y: -150, width: 150, height: 150 },
-          { type: DocumentType.IMG, tag: 'Gallery 3', acceptTags: ['LANDSCAPE'], x: 150, y: -150, width: 150, height: 150 },
-          { type: DocumentType.IMG, tag: 'Gallery 4', acceptTags: ['LANDSCAPE'], x: -150, y:   0, width: 150, height: 150 },
-          { type: DocumentType.IMG, tag: 'Gallery 5', acceptTags: ['LANDSCAPE'], x:   0, y:   0, width: 150, height: 150 },
-          { type: DocumentType.IMG, tag: 'Gallery 6', acceptTags: ['LANDSCAPE'], x: 150, y:   0, width: 150, height: 150 },
+            { type: DocumentType.IMG, message: 'Gallery 1', acceptTags: ['LANDSCAPE'], x: -150, y: -150, width: 150, height: 150 },
+            { type: DocumentType.IMG, message: 'Gallery 2', acceptTags: ['LANDSCAPE'], x: 0, y: -150, width: 150, height: 150 },
+            { type: DocumentType.IMG, message: 'Gallery 3', acceptTags: ['LANDSCAPE'], x: 150, y: -150, width: 150, height: 150 },
+            { type: DocumentType.IMG, message: 'Gallery 4', acceptTags: ['LANDSCAPE'], x: -150, y: 0, width: 150, height: 150 },
+            { type: DocumentType.IMG, message: 'Gallery 5', acceptTags: ['LANDSCAPE'], x: 0, y: 0, width: 150, height: 150 },
+            { type: DocumentType.IMG, message: 'Gallery 6', acceptTags: ['LANDSCAPE'], x: 150, y: 0, width: 150, height: 150 },
         ];
-      }
-      
+    }
 
     private static createDefaultPreset(): ScrapbookItemConfig[] {
         return [
-            { type: DocumentType.IMG,
-              tag: 'image',
-              acceptTags: ['LANDSCAPE'],
-              x: 0, y: -100, width: 250, height: 200
-            },
-            { type: DocumentType.RTF,
-              tag: 'summary',
-              acceptTags: ['sentence'],
-              x: 0, y: 200, width: 250
-            },
-            { type: DocumentType.RTF,
-              tag: 'sidebar',
-              acceptTags: ['paragraphs'],
-              x: 280, y: -50, width: 50, height: 200
-            },
-            {
-              type: DocumentType.COL,
-              tag: 'internal coll',
-              x: -200, y: -100, width: 100, height: 200,
-              containerWidth: 300, containerHeight: 300,
-              children: [
-                { type: DocumentType.IMG,
-                  tag: 'image internal',
-                  acceptTags: ['PERSON'],
-                  x: 0, y: 0, width: 50, height: 100
-                }
-              ]
-            }
-        ];
+            { type: DocumentType.IMG, message: 'image',   acceptTags: ['LANDSCAPE'],  x:    0, y: -100, width: 250, height: 200 },
+            { type: DocumentType.RTF, message: 'summary', acceptTags: ['sentence'],   x:    0, y: 200,  width: 250 },
+            { type: DocumentType.RTF, message: 'sidebar', acceptTags: ['paragraphs'], x:  280, y: -50,  width:  50, height: 200 },
+            { containerWidth: 300, containerHeight: 300,                              x: -200, y: -100, width: 100, height: 200,
+              children: [{ type: DocumentType.IMG, message: 'image internal', acceptTags: ['PERSON'], x: 0, y: 0, width: 50, height: 100 }], },
+        ]; // prettier-ignore
     }
 
     private static createCollagePreset(): ScrapbookItemConfig[] {
         return [
-            { type: DocumentType.IMG,
-              tag: 'LANDSCAPE',
-              acceptTags: ['LANDSCAPE'],
-              x: -150, y: -150, width: 150, height: 150
-            },
-            { type: DocumentType.IMG,
-              tag: 'PERSON',
-              acceptTags: ['PERSON'],
-              x: 0, y: -150, width: 150, height: 150
-            },
-            { type: DocumentType.RTF,
-              tag: 'caption',
-              acceptTags: ['sentence'],
-              x: -150, y: 0, width: 300, height: 100
-            },
-            { type: DocumentType.RTF,
-              tag: 'lengthy description',
-              acceptTags: ['paragraphs'],
-              x: 0, y: 100, width: 300, height: 100
-            }
-        ];
+            { type: DocumentType.IMG, message: 'LANDSCAPE',           acceptTags: ['LANDSCAPE'],  x: -150, y: -150, width: 150, height: 150 },
+            { type: DocumentType.IMG, message: 'PERSON',              acceptTags: ['PERSON'],     x:    0, y: -150, width: 150, height: 150 },
+            { type: DocumentType.RTF, message: 'caption',             acceptTags: ['sentence'],   x: -150, y:    0, width: 300, height: 100 },
+            { type: DocumentType.RTF, message: 'lengthy description', acceptTags: ['paragraphs'], x:   0,  y:  100, width: 300, height: 100 },
+        ]; // prettier-ignore
     }
 
     private static createSpotlightPreset(): ScrapbookItemConfig[] {
         return [
-            { type: DocumentType.RTF,
-              tag: 'title',
-              acceptTags: ['word'],
-              x: 0, y: -180, width: 300, height: 40
-            },
-            { type: DocumentType.IMG,
-              tag: 'LANDSCAPE',
-              acceptTags: ['LANDSCAPE'],
-              x: 0, y: 0, width: 300, height: 200
-            },
-            { type: DocumentType.RTF,
-              tag: 'caption',
-              acceptTags: ['sentence'],
-              x: 0, y: 230, width: 300, height: 50
-            }
+            { type: DocumentType.RTF, message: 'title', acceptTags: ['word'], x: 0, y: -180, width: 300, height: 40 },
+            { type: DocumentType.IMG, message: 'LANDSCAPE', acceptTags: ['LANDSCAPE'], x: 0, y: 0, width: 300, height: 200 },
+            { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: 0, y: 230, width: 300, height: 50 },
         ];
     }
 }
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts
index c6d67ab73..3a2189d00 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts
+++ b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts
@@ -6,39 +6,31 @@ type PresetGenerator = () => ScrapbookItemConfig[];
 // Internal map of preset name to generator
 const presetRegistry = new Map<string, PresetGenerator>();
 
-
-
-
 /**
  * Register a new scrapbook preset under the given name.
  */
 export function registerPreset(name: string, gen: PresetGenerator) {
-  presetRegistry.set(name, gen);
+    presetRegistry.set(name, gen);
 }
 
 /**
  * List all registered preset names.
  */
 export function getPresetNames(): string[] {
-  return Array.from(presetRegistry.keys());
+    return Array.from(presetRegistry.keys());
 }
 
 /**
  * Create the config array for the named preset.
  */
 export function createPreset(name: string): ScrapbookItemConfig[] {
-  const gen = presetRegistry.get(name);
-  if (!gen) throw new Error(`Unknown scrapbook preset: ${name}`);
-  return gen();
+    const gen = presetRegistry.get(name);
+    if (!gen) throw new Error(`Unknown scrapbook preset: ${name}`);
+    return gen();
 }
 
 // ------------------------
 // Register built-in presets
 import { ScrapbookPreset } from './ScrapbookPreset';
 
-registerPreset(ScrapbookPresetType.None, () => ScrapbookPreset.createPreset(ScrapbookPresetType.None));
-registerPreset(ScrapbookPresetType.Classic, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Classic));
-registerPreset(ScrapbookPresetType.Collage, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Collage));
-registerPreset(ScrapbookPresetType.Spotlight, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Spotlight));
-registerPreset(ScrapbookPresetType.Default, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Default));
-registerPreset(ScrapbookPresetType.Gallery, () => ScrapbookPreset.createPreset(ScrapbookPresetType.Gallery));
+Object.keys(ScrapbookPresetType).forEach(key => registerPreset(key, () => ScrapbookPreset.createPreset(key as ScrapbookPresetType))); // pretter-ignore
-- 
cgit v1.2.3-70-g09d2


From 6c011f502118e5246aabd29a30494c669d917fb1 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 11 Jun 2025 12:15:39 -0400
Subject: don't autoTag text docs until they're dropped onto a scrapbook. 
 don't re-tag a text box that has not changed.

---
 .../collections/collectionFreeForm/MarqueeView.tsx |  8 ++------
 .../views/nodes/formattedText/FormattedTextBox.tsx | 23 +++++++++++-----------
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx  |  5 ++++-
 src/client/views/search/FaceRecognitionHandler.tsx |  2 +-
 4 files changed, 19 insertions(+), 19 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 12515a72c..7a456c46f 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -16,7 +16,7 @@ import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs, DocumentOptions } from '../../../documents/Documents';
 import { SnappingManager, freeformScrollMode } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
-import { UndoManager, undoBatch, undoable } from '../../../util/UndoManager';
+import { UndoManager, undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { MarqueeViewBounds } from '../../PinFuncs';
@@ -33,8 +33,6 @@ import { requestAiGeneratedPreset, DocumentDescriptor } from '../../nodes/scrapb
 import { buildPlaceholdersFromConfigs, slotRealDocIntoPlaceholders } from '../../nodes/scrapbook/ScrapbookBox';
 import './MarqueeView.scss';
 
-import { build } from 'xregexp';
-
 interface MarqueeViewProps {
     Doc: Doc;
     getContainerTransform: () => Transform;
@@ -589,9 +587,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         const allPlaceholders = unwrap(flatPl);
 
         // 4) Slot each selectedDocs[i] into the first matching placeholder
-        selectedDocs.forEach(realDoc => {
-            slotRealDocIntoPlaceholders(realDoc, allPlaceholders);
-        });
+        selectedDocs.forEach(realDoc => slotRealDocIntoPlaceholders(realDoc, allPlaceholders));
 
         const selected = this.marqueeSelect(false).map(d => {
             this._props.removeDocument?.(d);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 1768eb08d..d700ce9f8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -310,15 +310,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     autoTag = () => {
         const rawText = RTFCast(this.Document[this.fieldKey])?.Text ?? StrCast(this.Document[this.fieldKey]);
-        const callType = rawText.includes('[placeholder]') ? GPTCallType.CLASSIFYTEXTMINIMAL : GPTCallType.CLASSIFYTEXTFULL;
-
-        gptAPICall(rawText, callType).then(
-            action(desc => {
-                // Split GPT response into tokens and push individually & clear existing tags
-                this.Document.$tags_chat = new List<string>(desc.trim().split(/\s+/));
-                this.Document._layout_showTags = true;
-            })
-        );
+        if (rawText && !this.Document.$tags_chat) {
+            const callType = rawText.includes('[placeholder]') ? GPTCallType.CLASSIFYTEXTMINIMAL : GPTCallType.CLASSIFYTEXTFULL;
+
+            gptAPICall(rawText, callType).then(
+                action(desc => {
+                    // Split GPT response into tokens and push individually & clear existing tags
+                    this.Document.$tags_chat = new List<string>(desc.trim().split(/\s+/));
+                    this.Document._layout_showTags = true;
+                })
+            );
+        }
     };
 
     leafText = (node: Node) => {
@@ -382,6 +384,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                             dataDoc[this.fieldKey] =
                                 numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined;
                             textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText });
+                            if (textChange) this.dataDoc.$tags_chat = undefined;
                             this.ApplyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false
                             unchanged = false;
                         }
@@ -1284,8 +1287,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             { fireImmediately: true }
         );
 
-        this._disposers.tagger = reaction(() => ({ title: this.Document.title, sel: this._props.isSelected() }), this.autoTag, { fireImmediately: true });
-
         if (!this._props.dontRegisterView) {
             this._disposers.record = reaction(
                 () => this.recordingDictation,
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index fcb82a6ba..94522c979 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -57,7 +57,10 @@ export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]) {
         return createMessagePlaceholder(cfg);
     });
 }
-export function slotRealDocIntoPlaceholders(realDoc: Doc, placeholders: Doc[]): boolean {
+export async function slotRealDocIntoPlaceholders(realDoc: Doc, placeholders: Doc[]) {
+    if (!realDoc.$tags_chart) {
+        await DocumentView.getFirstDocumentView(realDoc)?.ComponentView?.autoTag?.();
+    }
     const realTags = new Set<string>(StrListCast(realDoc.$tags_chat).map(t => t.toLowerCase?.() ?? ''));
 
     // Find placeholder with most matching tags
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index dac91b89a..84404d65a 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -209,9 +209,9 @@ export class FaceRecognitionHandler {
         } else if (imgDoc.type === DocumentType.LOADING && !imgDoc.loadingError) {
             setTimeout(() => this.classifyFacesInImage(imgDocView), 1000);
         } else {
-            imgDocView.ComponentView?.autoTag?.();
             const imgUrl = ImageCast(imgDoc[Doc.LayoutDataKey(imgDoc)]);
             if (imgUrl && !DocListCast(Doc.MyFaceCollection?.examinedFaceDocs).includes(imgDoc[DocData])) {
+                imgDocView.ComponentView?.autoTag?.();
                 // only examine Docs that have an image and that haven't already been examined.
                 Doc.MyFaceCollection && Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc[DocData]);
                 FaceRecognitionHandler.loadImage(imgUrl).then(
-- 
cgit v1.2.3-70-g09d2


From dd84952de3381049aca6742054f28bf502df2e16 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 19 Jun 2025 15:21:53 -0400
Subject: removing references to Box types from documentdecorations - added
 showBorderRounding to viewBoxInterface.

---
 src/client/views/ViewBoxInterface.ts                                  | 1 +
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx   | 3 ++-
 src/client/views/nodes/ImageBox.tsx                                   | 3 ++-
 src/client/views/nodes/formattedText/FormattedTextBox.tsx             | 3 ++-
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx                     | 4 +++-
 5 files changed, 10 insertions(+), 4 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index 83e395867..514dc4ae8 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -26,6 +26,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
     docEditorView?: () => void;
     autoTag?: () => void; // auto tag the document
     isOutpaintable?: () => boolean; // can document be resized and outpainted
+    showBorderRounding?: () => boolean; // can document borders be rounded
     showSmartDraw?: (x: number, y: number, regenerate?: boolean) => void;
     updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
     getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3571dab1a..32ace463d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -8,7 +8,7 @@ import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
 import ReactLoading from 'react-loading';
-import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
+import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnTrue, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
 import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
 import { DocData, DocLayout, Height, Width } from '../../../../fields/DocSymbols';
@@ -2065,6 +2065,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
     });
+    showBorderRounding = returnTrue;
     showPresPaths = () => SnappingManager.ShowPresPaths;
     brushedView = () => this._brushedView;
     gridColor = () => DashColor(lightOrDark(this.backgroundColor)).fade(0.5).toString(); // prettier-ignore
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 356f9e13f..4d8d486b1 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -8,7 +8,7 @@ import { extname } from 'path';
 import * as React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
 import ReactLoading from 'react-loading';
-import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
+import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon, returnTrue } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -994,6 +994,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return { width, height };
     };
     savedAnnotations = () => this._savedAnnotations;
+    showBorderRounding = returnTrue;
     rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView | undefined) => (this.dataDoc[this.fieldKey] === undefined ? true : (this._props.rejectDrop?.(de, subView) ?? false));
     render() {
         TraceMobx();
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d700ce9f8..07cb795f1 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti
 import { EditorView, NodeViewConstructor } from 'prosemirror-view';
 import * as React from 'react';
 import { BsMarkdownFill } from 'react-icons/bs';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
 import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
@@ -1088,6 +1088,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         return anchorDoc ?? this.Document;
     }
 
+    showBorderRounding = returnTrue;
     getView = (doc: Doc, options: FocusViewOptions) => {
         if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
             return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options);
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index ff757af88..d0ae6194f 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -24,6 +24,7 @@ import { ScrapbookItemConfig } from './ScrapbookPreset';
 import { createPreset, getPresetNames } from './ScrapbookPresetRegistry';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { DocUtils } from '../../../documents/DocUtils';
+import { returnTrue } from '../../../../ClientUtils';
 
 function createPlaceholder(cfg: ScrapbookItemConfig, doc: Doc) {
     const placeholder = new Doc();
@@ -175,7 +176,8 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     }
 
-    isOutpaintable = () => true;
+    isOutpaintable = returnTrue;
+    showBorderRounding = returnTrue;
 
     @action
     generateAiImage = (prompt: string) => {
-- 
cgit v1.2.3-70-g09d2


From 35bd9e51f7cef551382025a5459d68eddd8f028b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 23 Jun 2025 13:26:59 -0400
Subject: fixed invalidations to not trigger creating new refs when ref= was
 assigned to an anonymous function.  fixed scribble erase to not delete
 everything it overlaps, just things it intersects with or contains.  fixed
 ink to have a Math mode and fixed math recognition myscript calls.

---
 src/Utils.ts                                       |   3 +
 src/client/util/CurrentUserUtils.ts                |   4 +
 src/client/views/EditableView.tsx                  |   5 +-
 src/client/views/GestureOverlay.tsx                |   4 +-
 src/client/views/InkTranscription.tsx              | 182 +++++++++++----------
 src/client/views/InkingStroke.tsx                  |   2 +-
 src/client/views/LightboxView.tsx                  |   3 +-
 src/client/views/MainView.tsx                      |  36 ++--
 src/client/views/UndoStack.tsx                     |   3 +-
 src/client/views/animationtimeline/Timeline.tsx    |   3 +-
 .../views/collections/CollectionNoteTakingView.tsx |   3 +-
 .../views/collections/CollectionStackingView.tsx   |  10 +-
 .../CollectionStackingViewFieldColumn.tsx          |  12 +-
 .../views/collections/CollectionTreeView.tsx       |   3 +-
 src/client/views/collections/TreeView.tsx          |   3 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  53 +++---
 .../collectionFreeForm/FaceCollectionBox.tsx       |   3 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  39 ++---
 .../collectionLinear/CollectionLinearView.tsx      |   5 +-
 .../collectionSchema/CollectionSchemaView.tsx      |  24 ++-
 .../collectionSchema/SchemaCellField.tsx           |   4 +-
 .../collectionSchema/SchemaColumnHeader.tsx        |   9 +-
 .../collectionSchema/SchemaTableCell.tsx           |   3 +-
 src/client/views/global/globalScripts.ts           |   2 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |   7 +-
 .../DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx   |   3 +-
 .../nodes/DataVizBox/components/Histogram.tsx      |  11 +-
 .../nodes/DataVizBox/components/LineChart.tsx      |  11 +-
 .../views/nodes/DataVizBox/components/PieChart.tsx |  15 +-
 src/client/views/nodes/DiagramBox.tsx              |   6 +-
 src/client/views/nodes/DocumentContentsView.tsx    |   4 +-
 src/client/views/nodes/DocumentView.tsx            |  22 +--
 src/client/views/nodes/EquationBox.tsx             |   3 +-
 src/client/views/nodes/FunctionPlotBox.tsx         |   3 +-
 src/client/views/nodes/LabelBox.tsx                |  38 +++--
 src/client/views/nodes/LinkBox.tsx                 |  23 +--
 src/client/views/nodes/LinkDocPreview.tsx          |   9 +-
 src/client/views/nodes/ScreenshotBox.tsx           |  18 +-
 src/client/views/nodes/calendarBox/CalendarBox.tsx |  35 ++--
 .../nodes/chatbot/chatboxcomponents/ChatBox.tsx    |  14 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   8 +-
 src/client/views/nodes/trails/PresBox.tsx          |  51 +++---
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         |   3 +-
 src/fields/InkField.ts                             |   1 +
 44 files changed, 358 insertions(+), 345 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 724725c23..e96b8e231 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -202,6 +202,9 @@ export function aggregateBounds(boundsList: { x: number; y: number; width?: numb
 export function intersectRect(r1: { left: number; top: number; width: number; height: number }, r2: { left: number; top: number; width: number; height: number }) {
     return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
 }
+export function rectContains(r1: { left: number; top: number; width: number; height: number }, r2: { left: number; top: number; width: number; height: number }) {
+    if (r1.left < r2.left && r1.left + r1.width > r2.left + r2.width && r1.top < r2.top && r1.top + r1.height > r2.top + r2.height) return true;
+}
 
 export function stringHash(s?: string) {
     return !s ? undefined : Math.abs(s.split('').reduce((a, b) => (n => n & n)((a << 5) - a + b.charCodeAt(0)), 0));
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 45feb0b2f..9fbc82bef 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -796,6 +796,7 @@ pie title Minerals in my tap water
                     { title: "Pen",      toolTip: "Pen (Ctrl+P)",           btnType: ButtonType.ToggleButton, icon: "pen-nib",     toolType: InkInkTool.Pen,         ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
                     { title: "Highlight",toolTip: "Highlight (Ctrl+H)",     btnType: ButtonType.ToggleButton, icon: "highlighter", toolType: InkInkTool.Highlight,   ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
                     { title: "Write",    toolTip: "Write (Ctrl+Shift+P)",   btnType: ButtonType.ToggleButton, icon: "pen",         toolType: InkInkTool.Write,       ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' },  funcs: {hidden:"IsNoviceMode()" }},      
+                    { title: "Math",     toolTip: "Math",                   btnType: ButtonType.ToggleButton, icon: "calculator",  toolType: InkInkTool.Math,        ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' },  funcs: {hidden:"IsNoviceMode()" }},      
                 ]},
             { title: "Width",      toolTip: "Stroke width",                 btnType: ButtonType.NumberSliderButton,                toolType: InkProperty.StrokeWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}, numBtnMin: 1, linearView_btnWidth:40},
             { title: "Color",      toolTip: "Stroke color",                 btnType: ButtonType.ColorButton,  icon: "pen",         toolType: InkProperty.StrokeColor,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}},
@@ -1032,6 +1033,9 @@ pie title Minerals in my tap water
         doc[`active${InkInkTool.Write}Color`]      ?? (doc[`active${InkInkTool.Write}Color`] = "rgb(255, 0, 0)");
         doc[`active${InkInkTool.Write}Width`]      ?? (doc[`active${InkInkTool.Write}Width`] = 1);
         doc[`active${InkInkTool.Write}Bezier`]     ?? (doc[`active${InkInkTool.Write}Bezier`] = "0");
+        doc[`active${InkInkTool.Math}Color`]       ?? (doc[`active${InkInkTool.Math}Color`] = "rgb(0, 0, 255)");
+        doc[`active${InkInkTool.Math}Width`]       ?? (doc[`active${InkInkTool.Math}Width`] = 1);
+        doc[`active${InkInkTool.Math}Bezier`]      ?? (doc[`active${InkInkTool.Math}Bezier`] = "0");
         doc[`active${InkInkTool.Highlight}Color`]  ?? (doc[`active${InkInkTool.Highlight}Color`] = 'transparent');
         doc[`active${InkInkTool.Highlight}Width`]  ?? (doc[`active${InkInkTool.Highlight}Width`] = 20);
         doc[`active${InkInkTool.Highlight}Bezier`] ?? (doc[`active${InkInkTool.Highlight}Bezier`] = "0");
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index d9447b7ec..deeabaa28 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -236,6 +236,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
         return this._editing;
     };
 
+    setInputRef = (r: HTMLInputElement | HTMLTextAreaElement | null) => (this._inputref = r);
     renderEditor() {
         return this._props.autosuggestProps ? (
             <Autosuggest
@@ -255,7 +256,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
         ) : this._props.oneLine !== false && this._props.GetValue()?.toString().indexOf('\n') === -1 ? (
             <input
                 className="editableView-input"
-                ref={r => { this._inputref = r; }} // prettier-ignore
+                ref={this.setInputRef}
                 style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minWidth: 20, background: this._props.background }}
                 placeholder={this._props.placeholder}
                 onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
@@ -270,7 +271,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
         ) : (
             <textarea
                 className="editableView-input"
-                ref={r => { this._inputref = r; }} // prettier-ignore
+                ref={this.setInputRef}
                 style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minHeight: `min(100%, ${(this._props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, background: this._props.background }}
                 placeholder={this._props.placeholder}
                 onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 777a34ebc..8488c5293 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -3,7 +3,7 @@ import { action, computed, makeObservable, observable, runInAction } from 'mobx'
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { setupMoveUpEvents } from '../../ClientUtils';
-import { emptyFunction, intersectRect } from '../../Utils';
+import { emptyFunction, intersectRect, rectContains } from '../../Utils';
 import { Doc } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { NumCast } from '../../fields/Types';
@@ -166,7 +166,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
      */
     docsInBoundingBox = (boundingBox: { topLeft: number[]; bottomRight: number[] }, childDocs: Doc[]): Doc[] => {
         const rect = { left: boundingBox.topLeft[0], top: boundingBox.topLeft[1], width: boundingBox.bottomRight[0] - boundingBox.topLeft[0], height: boundingBox.bottomRight[1] - boundingBox.topLeft[1] };
-        return childDocs.filter(doc => intersectRect(rect, { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }));
+        return childDocs.filter(doc => rectContains(rect, { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }));
     };
     /**
      * Determines if what the array of cusp/intersection data corresponds to a scribble.
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 6220033d4..32ca5f56b 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -32,7 +32,7 @@ export class InkTranscription extends React.Component {
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     @observable _iinkEditor: any = undefined;
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    private lastJiix: any;
+    @observable _iinkMathEditor: any = undefined;
     private currGroup?: Doc;
     private collectionFreeForm?: CollectionFreeFormView;
 
@@ -44,7 +44,7 @@ export class InkTranscription extends React.Component {
     @action
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     setMathRef = async (r: any) => {
-        if (!this._textRegister && r) {
+        if (!this._mathRegister && r) {
             const options = {
                 configuration: {
                     server: {
@@ -55,19 +55,17 @@ export class InkTranscription extends React.Component {
                         protocol: 'WEBSOCKET',
                     },
                     recognition: {
-                        type: 'TEXT',
-                        lang: 'en_US',
-                        text: {
-                            mimeTypes: ['application/vnd.myscript.jiix'] as 'application/vnd.myscript.jiix'[],
+                        type: 'MATH',
+                        math: {
+                            mimeTypes: ['application/x-latex'] as unknown as 'application/vnd.myscript.jiix'[],
                         },
                     },
                 },
             };
-            await iink.Editor.load(r, 'INKV2', options);
-
-            this._textRegister = r;
+            this._iinkMathEditor = await iink.Editor.load(r, 'INKV2', options);
+            this._mathRegister = r;
             // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+            r?.addEventListener('exported', (e: any) => this.exportInk(e));
 
             return (this._textRef = r);
         }
@@ -97,12 +95,14 @@ export class InkTranscription extends React.Component {
             this._iinkEditor = await iink.Editor.load(r, 'INKV2', options);
             this._textRegister = r;
             // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+            r?.addEventListener('exported', (e: any) => this.exportInk(e));
 
             return (this._textRef = r);
         }
     };
 
+    _ffview: CollectionFreeFormView | undefined;
+
     /**
      * Handles processing Dash Doc data for ink transcription.
      *
@@ -110,7 +110,7 @@ export class InkTranscription extends React.Component {
      * @param inkDocs the ink docs contained within the selected group
      * @param math boolean whether to do math transcription or not
      */
-    transcribeInk = (groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean) => {
+    transcribeInk = (ffview: CollectionFreeFormView, groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean) => {
         if (!groupDoc) return;
         const validInks = inkDocs.filter(s => s.type === DocumentType.INK);
 
@@ -126,13 +126,10 @@ export class InkTranscription extends React.Component {
                 strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y })));
                 times.push(authorTime);
             });
+        this._ffview = ffview;
         this.currGroup = groupDoc;
         const pointerData = strokes.map((stroke, i) => this.inkJSON(stroke, times[i]));
-        if (math) {
-            this._iinkEditor.importPointEvents(pointerData);
-        } else {
-            this._iinkEditor.importPointEvents(pointerData);
-        }
+        (math ? this._iinkMathEditor : this._iinkEditor).importPointEvents(pointerData);
     };
     convertPointsToString(points: InkData[]): string {
         return points[0].map(point => `new Point(${point.X}, ${point.Y})`).join(',');
@@ -204,83 +201,92 @@ export class InkTranscription extends React.Component {
      * @param ref the ref to the editor
      */
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    exportInk = async (e: any, ref: any) => {
-        const exports = e.detail['application/vnd.myscript.jiix'];
-        if (exports) {
-            if (exports['type'] == 'Math') {
-                const latex = exports['application/x-latex'];
-                if (this.currGroup) {
-                    this.currGroup.text = latex;
-                    this.currGroup.title = latex;
-                }
+    exportInk = (e: { detail: { [key: string]: string } }) => {
+        const exports = e.detail;
+        if (exports['application/x-latex']) {
+            const latex = exports['application/x-latex'];
+            if (this.currGroup) {
+                this.currGroup.text = latex;
+                this.currGroup.title = latex;
+            }
 
-                ref.editor.clear();
-            } else if (exports['type'] == 'Text') {
-                if (exports['application/vnd.myscript.jiix']) {
-                    this.lastJiix = JSON.parse(exports['application/vnd.myscript.jiix']);
-                    // map timestamp to strokes
-                    const timestampWord = new Map<number, string>();
-                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-                    this.lastJiix.words.map((word: any) => {
-                        if (word.items) {
-                            word.items.forEach((i: { id: string; timestamp: string; X: Array<number>; Y: Array<number>; F: Array<number> }) => {
-                                const ms = Date.parse(i.timestamp);
-                                timestampWord.set(ms, word.label);
-                            });
-                        }
+            this._ffview?.addDocument(
+                Docs.Create.EquationDocument(latex, {
+                    title: '',
+                    x: this.currGroup?.x as number,
+                    y: (this.currGroup?.y as number) + (this.currGroup?.height as number),
+                    nativeHeight: 40,
+                    _height: 50,
+                    nativeWidth: 40,
+                    _width: 50,
+                })
+            );
+            // this.showRecogBox(latex as string);
+            this._iinkMathEditor.clear();
+        } else if (exports['application/vnd.myscript.jiix']) {
+            const lastJiix = exports['application/vnd.myscript.jiix'] as unknown as { words: string[]; label: string };
+            // map timestamp to strokes
+            const timestampWord = new Map<number, string>();
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            lastJiix.words.map((word: any) => {
+                if (word.items) {
+                    word.items.forEach((i: { id: string; timestamp: string; X: Array<number>; Y: Array<number>; F: Array<number> }) => {
+                        const ms = Date.parse(i.timestamp);
+                        timestampWord.set(ms, word.label);
                     });
-
-                    const wordInkDocMap = new Map<string, Doc[]>();
-                    if (this.currGroup) {
-                        const docList = DocListCast(this.currGroup.data);
-                        docList.forEach((inkDoc: Doc) => {
-                            // just having the times match up and be a unique value (actual timestamp doesn't matter)
-                            const ms = (DateCast(inkDoc.author_date)?.getDate().getTime() ?? 0) + 14400000;
-                            const word = timestampWord.get(ms);
-                            if (!word) {
-                                return;
-                            }
-                            const entry = wordInkDocMap.get(word);
-                            if (entry) {
-                                entry.push(inkDoc);
-                                wordInkDocMap.set(word, entry);
-                            } else {
-                                const newEntry = [inkDoc];
-                                wordInkDocMap.set(word, newEntry);
-                            }
-                        });
-                        if (this.lastJiix.words.length > 1) this.subgroupsTranscriptions(wordInkDocMap);
-                    }
                 }
-                const text = exports['label'];
+            });
 
-                if (this.currGroup && text) {
-                    DocumentView.getDocumentView(this.currGroup)?.ComponentView?.updateIcon?.();
-                    const image = await this.getIcon();
-                    const { href } = (image as URLField).url;
-                    const hrefParts = href.split('.');
-                    const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
-                    let response;
-                    try {
-                        const hrefBase64 = await imageUrlToBase64(hrefComplete);
-                        response = await gptHandwriting(hrefBase64);
-                    } catch {
-                        console.error('Error getting image');
-                    }
-                    const textBoxText = 'iink: ' + text + '\n' + '\n' + 'ChatGPT: ' + response;
-                    this.currGroup.transcription = response;
-                    this.currGroup.title = response;
-                    if (!this.currGroup.hasTextBox) {
-                        const newDoc = Docs.Create.TextDocument(textBoxText, { title: '', x: this.currGroup.x as number, y: (this.currGroup.y as number) + (this.currGroup.height as number) });
-                        newDoc.height = 200;
-                        this.collectionFreeForm?.addDocument(newDoc);
-                        this.currGroup.hasTextBox = true;
+            const wordInkDocMap = new Map<string, Doc[]>();
+            if (this.currGroup) {
+                DocListCast(this.currGroup.data).forEach((inkDoc: Doc) => {
+                    // just having the times match up and be a unique value (actual timestamp doesn't matter)
+                    const ms = (DateCast(inkDoc.author_date)?.getDate().getTime() ?? 0) + 14400000;
+                    const word = timestampWord.get(ms);
+                    if (word) {
+                        const entry = wordInkDocMap.get(word);
+                        if (entry) {
+                            entry.push(inkDoc);
+                            wordInkDocMap.set(word, entry);
+                        } else {
+                            const newEntry = [inkDoc];
+                            wordInkDocMap.set(word, newEntry);
+                        }
                     }
-                    ref.editor.clear();
-                }
+                });
+                if (lastJiix.words.length > 1) this.subgroupsTranscriptions(wordInkDocMap);
             }
+            this.showRecogBox(lastJiix.label);
+            this._iinkEditor.clear();
         }
     };
+    private showRecogBox(text: string) {
+        if (this.currGroup) {
+            let response;
+            // DocumentView.getDocumentView(this.currGroup)?.ComponentView?.updateIcon?.();
+            // const image = await this.getIcon();
+            // const { href } = (image as URLField).url;
+            // const hrefParts = href.split('.');
+            // const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+            // try {
+            //     const hrefBase64 = await imageUrlToBase64(hrefComplete);
+            //     response = await gptHandwriting(hrefBase64);
+            // } catch {
+            //     console.error('Error getting image');
+            // }
+            const textBoxText = 'iink: ' + text + '\n' + '\n' + 'ChatGPT: ' + response;
+
+            this.currGroup.transcription = response;
+            this.currGroup.title = response;
+            if (!this.currGroup.hasTextBox) {
+                const newDoc = Docs.Create.TextDocument(textBoxText, { title: '', x: this.currGroup.x as number, y: (this.currGroup.y as number) + (this.currGroup.height as number) });
+                newDoc.height = 200;
+                this.collectionFreeForm?.addDocument(newDoc);
+                this.currGroup.hasTextBox = true;
+            }
+        }
+    }
+
     /**
      * gets the icon of the collection that was just made
      * @returns the image of the collection
@@ -299,7 +305,7 @@ export class InkTranscription extends React.Component {
      */
     createInkGroup() {
         // TODO nda - if document being added to is a inkGrouping then we can just add to that group
-        if (Doc.ActiveTool === InkTool.Ink && Doc.ActiveInk === InkInkTool.Write) {
+        if (Doc.ActiveTool === InkTool.Ink && [InkInkTool.Write, InkInkTool.Math].includes(Doc.ActiveInk)) {
             CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
                 // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
                 const selected = ffView.unprocessedDocs;
@@ -309,7 +315,7 @@ export class InkTranscription extends React.Component {
                 );
                 ffView.unprocessedDocs = [];
 
-                InkTranscription.Instance.transcribeInk(newCollection, selected, false);
+                InkTranscription.Instance.transcribeInk(ffView, newCollection, selected, Doc.ActiveInk === InkInkTool.Math);
             });
         }
         CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 253db08de..4b651af7d 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -124,7 +124,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
             );
             ffView.unprocessedDocs = [];
 
-            InkTranscription.Instance.transcribeInk(newCollection, selected, false);
+            InkTranscription.Instance.transcribeInk(ffView, newCollection, selected, false);
         }
     };
 
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 0eb21b943..62b1180ec 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -244,6 +244,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
             </div>
         </div>
     );
+    setStickerRef = (r: StickerPalette | null) => (this._annoPaletteView = r);
     render() {
         let downx = 0;
         let downy = 0;
@@ -317,7 +318,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                             </GestureOverlay>
                         </div>
 
-                        {this._showPalette && <StickerPalette ref={r => (this._annoPaletteView = r)} Doc={DocCast(Doc.UserDoc().myLightboxDrawings)} />}
+                        {this._showPalette && DocCast(Doc.UserDoc().myLightboxDrawings) && <StickerPalette ref={this.setStickerRef} Doc={DocCast(Doc.UserDoc().myLightboxDrawings)!} />}
                         {this.renderNavBtn(0, undefined, this._props.PanelHeight / 2 - 12.5, 'chevron-left', this._doc && this._history.length ? true : false, this.previous)}
                         {this.renderNavBtn(
                             this._props.PanelWidth - Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]),
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 13b14617c..867a5a304 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -921,19 +921,19 @@ export class MainView extends ObservableReactComponent<object> {
         );
     }
 
+    setMainDashRef = (r: HTMLDivElement | null) =>
+        r &&
+        new ResizeObserver(
+            action(() => {
+                this._dashUIWidth = r.getBoundingClientRect().width;
+                this._dashUIHeight = r.getBoundingClientRect().height;
+            })
+        ).observe(r);
     @computed get mainDashboardArea() {
         return !this.userDoc ? null : (
             <div
                 className="mainView-dashboardArea"
-                ref={r => {
-                    r &&
-                        new ResizeObserver(
-                            action(() => {
-                                this._dashUIWidth = r.getBoundingClientRect().width;
-                                this._dashUIHeight = r.getBoundingClientRect().height;
-                            })
-                        ).observe(r);
-                }}
+                ref={this.setMainDashRef}
                 style={{
                     color: 'black',
                     height: `calc(100% - ${this.topOfDashUI + this.topMenuHeight()}px)`,
@@ -1072,6 +1072,14 @@ export class MainView extends ObservableReactComponent<object> {
     };
 
     lightboxMaxBorder = [200, 50];
+    setMainViewRef = (r: HTMLDivElement | null) =>
+        r &&
+        new ResizeObserver(
+            action(() => {
+                this._windowWidth = r.getBoundingClientRect().width;
+                this._windowHeight = r.getBoundingClientRect().height;
+            })
+        ).observe(r);
     render() {
         return (
             <div
@@ -1085,15 +1093,7 @@ export class MainView extends ObservableReactComponent<object> {
                         ele.scrollTop = ele.scrollLeft = 0;
                     })(document.getElementById('root')!)
                 }
-                ref={r => {
-                    r &&
-                        new ResizeObserver(
-                            action(() => {
-                                this._windowWidth = r.getBoundingClientRect().width;
-                                this._windowHeight = r.getBoundingClientRect().height;
-                            })
-                        ).observe(r);
-                }}>
+                ref={this.setMainViewRef}>
                 {this.inkResources}
                 <DictationOverlay />
                 <SharingManager />
diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx
index 067020a62..3755b1ec1 100644
--- a/src/client/views/UndoStack.tsx
+++ b/src/client/views/UndoStack.tsx
@@ -9,6 +9,7 @@ import './UndoStack.scss';
 
 @observer
 export class UndoStack extends React.Component<object> {
+    setRef = (r: HTMLDivElement | null) => r?.scroll({ behavior: 'auto', top: (r?.scrollHeight ?? 0) + 20 });
     render() {
         const background = UndoManager.batchCounter.get() ? 'yellow' : SettingsManager.userVariantColor;
         const color = UndoManager.batchCounter.get() ? 'black' : SettingsManager.userColor;
@@ -25,7 +26,7 @@ export class UndoStack extends React.Component<object> {
                             popup={
                                 <div
                                     className="undoStack-commandsContainer"
-                                    ref={r => r?.scroll({ behavior: 'auto', top: (r?.scrollHeight ?? 0) + 20 })}
+                                    ref={this.setRef}
                                     style={{
                                         background,
                                         color,
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index 814e9a7a0..c2bd01334 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -533,6 +533,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps & { Doc: D
         this._totalLength = RegionHelpers.convertPixelTime(this._time, 'mili', 'pixel', this._tickSpacing, this._tickIncrement);
     };
 
+    setTrackRef = (r: Track) => this.mapOfTracks.push(r);
     /**
      * if you have any question here, just shoot me an email or text.
      * basically the only thing you need to edit besides render methods in track (individual track lines) and keyframe (green region)
@@ -554,7 +555,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps & { Doc: D
                                 {[...this.children, this._props.Doc].map(doc => (
                                     <Track
                                         key={doc[Id]}
-                                        ref={ref => this.mapOfTracks.push(ref)}
+                                        ref={this.setTrackRef}
                                         timeline={this}
                                         animatedDoc={doc}
                                         currentBarX={this._currentBarX}
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 7f639a11e..7a4408931 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -256,9 +256,10 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const height = () => this.getDocHeight(doc);
         let dref: Opt<DocumentView>;
         const noteTakingDocTransform = () => this.getDocTransform(doc, dref);
+        const setRef = (r: DocumentView | null) => (dref = r || undefined);
         return (
             <DocumentView
-                ref={r => (dref = r || undefined)}
+                ref={setRef}
                 Document={doc}
                 TemplateDataDocument={doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined}
                 pointerEvents={this.blockPointerEventsWhenDragging}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index bdeb7d944..fbdd23315 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import * as CSS from 'csstype';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, DivHeight, returnNone, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils';
@@ -391,10 +391,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     });
 
     // This following three functions must be from the view Mehek showed
-    columnDividerDown = (e: React.PointerEvent) => {
-        runInAction(() => {
-            this._cursor = 'grabbing';
-        });
+    columnDividerDown = action((e: React.PointerEvent) => {
+        this._cursor = 'grabbing';
         const batch = UndoManager.StartBatch('stacking width');
         setupMoveUpEvents(
             this,
@@ -406,7 +404,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             }),
             emptyFunction
         );
-    };
+    });
     @action
     onDividerMove = (e: PointerEvent) => {
         this.Document._layout_columnWidth = Math.max(10, (this._props.DocumentView?.().screenToViewTransform().transformPoint(e.clientX, 0)[0] ?? 0) - this.xMargin);
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 8c535534a..b5efa7a72 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -304,6 +304,12 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         ContextMenu.Instance.displayMenu(pt[0], pt[1], undefined, true);
     };
 
+    setRef = (r: HTMLDivElement | null) => {
+        if (this._headerRef && this._props.colHeaderRefs.includes(this._headerRef)) this._props.colHeaderRefs.splice(this._props.colStackRefs.indexOf(this._headerRef), 1);
+        r && this._props.colHeaderRefs.push(r);
+        this._headerRef = r;
+    };
+
     @computed get innards() {
         TraceMobx();
         const key = this._props.pivotField;
@@ -315,11 +321,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
             <div
                 key={heading}
                 className="collectionStackingView-sectionHeader"
-                ref={r => {
-                    if (this._headerRef && this._props.colHeaderRefs.includes(this._headerRef)) this._props.colHeaderRefs.splice(this._props.colStackRefs.indexOf(this._headerRef), 1);
-                    r && this._props.colHeaderRefs.push(r);
-                    this._headerRef = r;
-                }}
+                ref={this.setRef}
                 style={{
                     marginTop: 0,
                     marginBottom: this._props.gridGap,
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index bee5d016d..4625965b4 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -407,6 +407,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     remAnnotationDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, `${this._props.fieldKey}_annotations`) || false;
     moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) => this.moveDocument(doc, targetCollection, addDocument) || false;
 
+    setRef = (r: HTMLDivElement | null) => !this.Document.treeView_HasOverlay && r && this.createTreeDropTarget(r);
     @observable _headerHeight = 0;
     @computed get content() {
         const background = () => this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string;
@@ -428,7 +429,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                 <div
                     className="collectionTreeView-contents"
                     key="tree"
-                    ref={r => !this.Document.treeView_HasOverlay && r && this.createTreeDropTarget(r)}
+                    ref={this.setRef}
                     style={{
                         ...(!titleBar ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
                         color: color(),
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 5b2f1ff81..f84c7d3c0 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1294,6 +1294,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                     return null;
                 }
 
+                const setRef = (r: TreeView | null) => treeViewRefs.set(child, r || undefined);
                 const dentDoc = (editTitle: boolean, newParent: Doc, addAfter: Doc | undefined, parent: TreeView | CollectionTreeView | undefined) => {
                     if (parent instanceof TreeView && parent._props.treeView.fileSysMode && !newParent.isFolder) return;
                     const fieldKey = Doc.LayoutDataKey(newParent);
@@ -1317,7 +1318,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                 return (
                     <TreeView
                         key={child[Id]}
-                        ref={r => treeViewRefs.set(child, r || undefined)}
+                        ref={setRef}
                         Document={pair.layout}
                         dataDoc={pair.data}
                         treeViewParent={treeViewParent}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 32ace463d..6e9e503f4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -562,7 +562,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
                 const inkDoc = this.createInkDoc(points, B);
                 if (Doc.ActiveInk === InkInkTool.Highlight) inkDoc.$backgroundColor = 'transparent';
-                if (Doc.ActiveInk === InkInkTool.Write) {
+                if ([InkInkTool.Write, InkInkTool.Math].includes(Doc.ActiveInk)) {
                     this.unprocessedDocs.push(inkDoc);
                     CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
                 }
@@ -2220,18 +2220,41 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
     };
 
+    @computed get inkEraser() {
+        return (
+            Doc.ActiveTool === InkTool.Eraser &&
+            Doc.ActiveEraser === InkEraserTool.Radius &&
+            this._showEraserCircle && (
+                <div
+                    onPointerMove={this.onCursorMove}
+                    style={{
+                        position: 'fixed',
+                        left: this._eraserX,
+                        top: this._eraserY,
+                        width: (ActiveEraserWidth() + 5) * 2,
+                        height: (ActiveEraserWidth() + 5) * 2,
+                        borderRadius: '50%',
+                        border: '1px solid gray',
+                        transform: 'translate(-50%, -50%)',
+                    }}
+                />
+            )
+        );
+    }
+
+    setRef = (r: HTMLDivElement | null) => {
+        this.createDashEventsTarget(r);
+        this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel);
+        r?.addEventListener('mouseleave', this.onMouseLeave);
+        r?.addEventListener('mouseenter', this.onMouseEnter);
+    };
     render() {
         TraceMobx();
         return (
             <div
                 className="collectionfreeformview-container"
                 id={this._paintedId}
-                ref={r => {
-                    this.createDashEventsTarget(r);
-                    this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel);
-                    r?.addEventListener('mouseleave', this.onMouseLeave);
-                    r?.addEventListener('mouseenter', this.onMouseEnter);
-                }}
+                ref={this.setRef}
                 onWheel={this.onPointerWheel}
                 onClick={this.onClick}
                 onPointerDown={this.onPointerDown}
@@ -2246,21 +2269,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     width: `${100 / this.nativeDimScaling}%`,
                     height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
                 }}>
-                {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
-                    <div
-                        onPointerMove={this.onCursorMove}
-                        style={{
-                            position: 'fixed',
-                            left: this._eraserX,
-                            top: this._eraserY,
-                            width: (ActiveEraserWidth() + 5) * 2,
-                            height: (ActiveEraserWidth() + 5) * 2,
-                            borderRadius: '50%',
-                            border: '1px solid gray',
-                            transform: 'translate(-50%, -50%)',
-                        }}
-                    />
-                )}
+                {this.inkEraser}
                 {this.paintFunc ? (
                     <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
                 ) : this._lightboxDoc ? (
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index 142085e14..c31558dff 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -150,6 +150,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
         FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, this.Document);
     }, 'remove doc from face');
 
+    setRef = (r: HTMLDivElement | null) => this.fixWheelEvents(r, this._props.isContentActive);
     render() {
         return (
             <div className="face-document-item" ref={ele => this.createDropTarget(ele!)}>
@@ -176,7 +177,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         style={{
                             pointerEvents: this._props.isContentActive() ? undefined : 'none',
                         }}
-                        ref={r => this.fixWheelEvents(r, this._props.isContentActive)}>
+                        ref={this.setRef}>
                         {FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => {
                             const [name, type] = ImageCastToNameType(doc?.[Doc.LayoutDataKey(doc)]) ?? ['-missing-', '.png'];
                             return (
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index ff78b332a..4191aaca8 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -762,29 +762,26 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         }
         e.stopPropagation();
     };
+    setRef = (r: HTMLDivElement | null) => {
+        r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject);
+        this.MarqueeRef = r;
+    };
     render() {
         return (
-            <>
-                <div
-                    className="marqueeView"
-                    ref={r => {
-                        r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject);
-                        this.MarqueeRef = r;
-                    }}
-                    style={{
-                        overflow: StrCast(this._props.Document._overflow),
-                        cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
-                    }}
-                    onDragOver={e => e.preventDefault()}
-                    onScroll={e => {
-                        e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0;
-                    }}
-                    onClick={this.onClick}
-                    onPointerDown={this.onPointerDown}>
-                    {this._visible ? this.marqueeDiv : null}
-                    {this.props.children}
-                </div>
-            </>
+            <div
+                className="marqueeView"
+                ref={this.setRef}
+                style={{
+                    overflow: StrCast(this._props.Document._overflow),
+                    cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
+                }}
+                onDragOver={e => e.preventDefault()}
+                onScroll={e => (e.currentTarget.scrollTop = (e.currentTarget.scrollLeft = 0))} // prettier-ignore
+                onClick={this.onClick}
+                onPointerDown={this.onPointerDown}>
+                {this._visible ? this.marqueeDiv : null}
+                {this.props.children}
+            </div>
         );
     }
 }
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index d0a1e6f0d..435f618d9 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -158,14 +158,13 @@ export class CollectionLinearView extends CollectionSubView() {
 
         let dref: Opt<HTMLDivElement>;
         const docXf = () => this.getTransform(dref);
+        const setRef = (r: HTMLDivElement | null) => (dref = r || undefined);
         // const scalable = pair.layout.onClick || pair.layout.onDragStart;
         return hidden ? null : (
             <div
                 className={preview ? 'preview' : `collectionLinearView-docBtn`}
                 key={doc[Id]}
-                ref={r => {
-                    dref = r || undefined;
-                }}
+                ref={setRef}
                 style={{
                     pointerEvents: 'all',
                     width: NumCast(doc._width),
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 6442385c0..2576bdf9b 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1043,17 +1043,16 @@ export class CollectionSchemaView extends CollectionSubView() {
         if (!this._oldKeysWheel?.scrollTop && e.deltaY <= 0) e.preventDefault();
         e.stopPropagation();
     };
+    setRef = (r: HTMLDivElement | null) => {
+        this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel);
+        this._oldKeysWheel = r;
+        r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false });
+    };
     _oldKeysWheel: HTMLDivElement | null = null;
     @computed get keysDropdown() {
         return (
             <div className="schema-key-search">
-                <div
-                    className="schema-key-list"
-                    ref={r => {
-                        this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel);
-                        this._oldKeysWheel = r;
-                        r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false });
-                    }}>
+                <div className="schema-key-list" ref={this.setRef}>
                     {this._menuKeys.map(key => (
                         <div
                             key={key}
@@ -1294,6 +1293,9 @@ export class CollectionSchemaView extends CollectionSubView() {
     screenToLocal = () => this.ScreenToLocalBoxXf().translate(-this.tableWidth, 0);
     previewWidthFunc = () => this.previewWidth;
     displayedDocsFunc = () => this.docsWithDrag.docs;
+    setColHdrRef = (r: SchemaColumnHeader | null) => r && this._headerRefs.push(r);
+    setPreviewRef = (r: HTMLDivElement | null) => (this._previewRef = r);
+
     render() {
         return (
             <div
@@ -1331,7 +1333,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                         {this.columnKeys.map((key, index) => (
                             <SchemaColumnHeader
                                 //cleanupField={this.cleanupComputedField}
-                                ref={r => r && this._headerRefs.push(r)}
+                                ref={this.setColHdrRef}
                                 keysDropdown={this.keysDropdown}
                                 schemaView={this}
                                 columnWidth={() => CollectionSchemaView._minColWidth} //TODO: update
@@ -1379,11 +1381,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                 </div>
                 {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown} />}
                 {this.previewWidth > 0 && (
-                    <div
-                        style={{ width: `${this.previewWidth}px` }}
-                        ref={ref => {
-                            this._previewRef = ref;
-                        }}>
+                    <div style={{ width: `${this.previewWidth}px` }} ref={this.setPreviewRef}>
                         {Array.from(this._selectedDocs).lastElement() && (
                             <DocumentView
                                 Document={Array.from(this._selectedDocs).lastElement()}
diff --git a/src/client/views/collections/collectionSchema/SchemaCellField.tsx b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
index 9ad94cb31..412daa105 100644
--- a/src/client/views/collections/collectionSchema/SchemaCellField.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
@@ -341,12 +341,14 @@ export class SchemaCellField extends ObservableReactComponent<SchemaCellFieldPro
         return <span className="editableView-static">{this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : ''}</span>;
     };
 
+    setRef = (r: HTMLDivElement | null) => (this._inputref = r);
+
     renderEditor = () => {
         return (
             <div
                 contentEditable
                 className="schemaField-editing"
-                ref={r => (this._inputref = r)}
+                ref={this.setRef}
                 style={{ minHeight: `min(100%, ${(this._props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20 }}
                 onBlur={() => (this._props.refSelectModeInfo.enabled ? setTimeout(() => this.setIsFocused(true), 1000) : this.finalizeEdit(false, true, false))}
                 onInput={this.onChange}
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
index 134f2ed31..64bfab856 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -118,6 +118,10 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea
         return { color, fieldProps, cursor };
     };
 
+    setRef = (r: EditableView | null) => {
+        this._inputRef = r;
+        this._props.autoFocus && r?.setIsFocused(true);
+    };
     @computed get editableView() {
         const { color, fieldProps } = this.renderProps(this._props);
 
@@ -133,10 +137,7 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea
                     width: '100%',
                 }}>
                 <EditableView
-                    ref={r => {
-                        this._inputRef = r;
-                        this._props.autoFocus && r?.setIsFocused(true);
-                    }}
+                    ref={this.setRef}
                     oneLine={true}
                     allowCRs={false}
                     contents={''}
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 8b34b4139..02e0d8100 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -185,6 +185,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
         return eqSymbol + modField;
     };
 
+    setRef = (r: SchemaCellField | null) => selectedCell(this._props) && this._props.autoFocus && r?.setIsFocused(true);
     @computed get defaultCellContent() {
         const { color, textDecoration, fieldProps, pointerEvents } = SchemaTableCell.renderProps(this._props);
 
@@ -204,7 +205,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
                     Doc={this._props.Doc}
                     highlightCells={(text: string) => this._props.highlightCells(this.adjustSelfReference(text))}
                     getCells={(text: string) => this._props.eqHighlightFunc(this.adjustSelfReference(text))}
-                    ref={r => selectedCell(this._props) && this._props.autoFocus && r?.setIsFocused(true)}
+                    ref={this.setRef}
                     oneLine={this._props.oneLine}
                     contents={undefined}
                     fieldContents={fieldProps}
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index cb3adae10..e098d50d8 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -408,7 +408,7 @@ function setActiveTool(tool: InkTool | InkEraserTool | InkInkTool | Gestures, ke
     }
     runInAction(() => {
         const eraserTool = tool === InkTool.Eraser ? Doc.ActiveEraser : [InkEraserTool.Stroke, InkEraserTool.Radius, InkEraserTool.Segment].includes(tool as InkEraserTool) ? (tool as InkEraserTool) : undefined;
-        const inkTool = tool === InkTool.Ink ? Doc.ActiveInk : [InkInkTool.Pen, InkInkTool.Write, InkInkTool.Highlight].includes(tool as InkInkTool) ? (tool as InkInkTool) : undefined;
+        const inkTool = tool === InkTool.Ink ? Doc.ActiveInk : [InkInkTool.Pen, InkInkTool.Write, InkInkTool.Math, InkInkTool.Highlight].includes(tool as InkInkTool) ? (tool as InkInkTool) : undefined;
         if (GestureOverlay.Instance) {
             SnappingManager.SetKeepGestureMode(keepPrim);
         }
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 9369ff98a..0b7033e57 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -420,6 +420,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
+    setRef = (r: LineChart | Histogram | PieChart | null) => (this._vizRenderer = r ?? undefined);
     // toggles for user to decide which chart type to view the data in
     @computed get renderVizView() {
         const scale = this._props.NativeDimScaling?.() || 1;
@@ -437,9 +438,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         if (!this.records.length) return 'no data/visualization';
         switch (this.dataVizView) {
             case DataVizView.TABLE:     return <TableBox  {...sharedProps} Doc={this.Document} specHighlightedRow={this._specialHighlightedRow} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>;
-            case DataVizView.LINECHART: return <LineChart {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} vizBox={this} />;
-            case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} />;
-            case DataVizView.PIECHART:  return <PieChart  {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} 
+            case DataVizView.LINECHART: return <LineChart {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={this.setRef} vizBox={this} />;
+            case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={this.setRef} />;
+            case DataVizView.PIECHART:  return <PieChart  {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={this.setRef} 
             margin={{ top: 10, right: 15, bottom: 15, left: 15 }} />;
             default:
         } // prettier-ignore
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
index fb083ea75..8f6ecab57 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
@@ -540,6 +540,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
         ]; //prettier-ignore
     }
 
+    setRef = (r: HTMLDivElement) => (this._ref = r);
     render() {
         const topButton = (icon: string, opt: string, func: () => void, tag: string) => (
             <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}>
@@ -558,7 +559,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
                 {!this._shouldDisplay ? undefined : (
                     <div
                         className="docCreatorMenu-cont"
-                        ref={r => (this._ref = r)}
+                        ref={this.setRef}
                         style={{
                             display: '',
                             left: this._pageX,
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index a7c4a00b0..f51683991 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -413,6 +413,10 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
         });
     };
 
+    setChartRef = (r: HTMLDivElement | null) => {
+        this._histogramRef = r;
+        r && this.drawChart(this._histogramData, this.width, this.height);
+    };
     render() {
         if (!this.selectedBins) this.layoutDoc.dataViz_histogram_selectedBins = new List<string>();
 
@@ -446,12 +450,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
                             size={Size.XSMALL}
                         />
                     </div>
-                    <div
-                        ref={r => {
-                            this._histogramRef = r;
-                            r && this.drawChart(this._histogramData, this.width, this.height);
-                        }}
-                    />
+                    <div ref={this.setChartRef} />
                     {selected !== 'none' ? (
                         <div className="selected-data">
                             Selected: {selected}
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 80fadf178..732681e05 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -347,6 +347,10 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
             .style('pointer-events', 'none')
             .style('transform', `translate(${xScale(d0.x) - this.width}px,${yScale(d0.y)}px)`);
     }
+    setLineRef = (r: HTMLDivElement | null) => {
+        this._lineChartRef = r;
+        this.drawChart([this._lineChartData], this.rangeVals, this.width, this.height);
+    };
 
     render() {
         const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none';
@@ -378,12 +382,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
                             fillWidth
                         />
                     </div>
-                    <div
-                        ref={r => {
-                            this._lineChartRef = r;
-                            this.drawChart([this._lineChartData], this.rangeVals, this.width, this.height);
-                        }}
-                    />
+                    <div ref={this.setLineRef} />
                     {selectedPt !== 'none' ? (
                         <div className="selected-data">
                             {`Selected: ${selectedPt}`}
diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
index 0ae70786f..cf476b8d0 100644
--- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
@@ -373,10 +373,10 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
         const sliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle;
 
         const sliceColors = Cast(this._props.layoutDoc.dataViz_pie_sliceColors, listSpec('string'), null);
-        sliceColors.forEach(each => {
+        sliceColors?.forEach(each => {
             if (each.split('::')[0] === sliceName) sliceColors.splice(sliceColors.indexOf(each), 1);
         });
-        sliceColors.push(StrCast(sliceName + '::' + color));
+        sliceColors?.push(StrCast(sliceName + '::' + color));
     };
 
     @action changeHistogramCheckBox = () => {
@@ -384,6 +384,10 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
         this.drawChart(this._pieChartData, this.width, this.height);
     };
 
+    setChartRef = (r: HTMLDivElement | null) => {
+        this._piechartRef = r;
+        this.drawChart(this._pieChartData, this.width, this.height);
+    };
     render() {
         let titleAccessor = 'dataViz_pie_title';
         if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1];
@@ -443,12 +447,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
                             Organize data as histogram
                         </div>
                     ) : null}
-                    <div
-                        ref={r => {
-                            this._piechartRef = r;
-                            this.drawChart(this._pieChartData, this.width, this.height);
-                        }}
-                    />
+                    <div ref={this.setChartRef} />
                     {selected !== 'none' ? (
                         <div className="selected-data">
                             Selected: {selected}
diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx
index 7cfccf0dc..6a31f64ce 100644
--- a/src/client/views/nodes/DiagramBox.tsx
+++ b/src/client/views/nodes/DiagramBox.tsx
@@ -185,6 +185,8 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return '( )';
     };
 
+    setRef = (r: HTMLDivElement | null) => this.fixWheelEvents(r, this._props.isContentActive);
+    setDiagramBoxRef = (r: HTMLDivElement | null) => r && this.renderMermaidAsync.call(this, this.removeWords(this.mermaidcode), r);
     render() {
         return (
             <div
@@ -192,7 +194,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 style={{
                     pointerEvents: this._props.isContentActive() ? undefined : 'none',
                 }}
-                ref={r => this.fixWheelEvents(r, this._props.isContentActive)}>
+                ref={this.setRef}>
                 <div className="DIYNodeBox-searchbar">
                     <input type="text" value={this._inputValue} onKeyDown={action(e => e.key === 'Enter' && this.generateMermaidCode())} onChange={action(e => (this._inputValue = e.target.value))} />
                     <button type="button" onClick={this.generateMermaidCode}>
@@ -208,7 +210,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     ) : this._generating ? (
                         <div className="loading-circle" />
                     ) : (
-                        <div className="diagramBox" ref={r => r && this.renderMermaidAsync.call(this, this.removeWords(this.mermaidcode), r)}>
+                        <div className="diagramBox" ref={this.setDiagramBoxRef}>
                             {this._errorMessage || 'Type a prompt to generate a diagram'}
                         </div>
                     )}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 504c1491e..32741a0fe 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -261,9 +261,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
                 jsx={layoutFrame}
                 showWarnings
                 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-                onError={(test: any) => {
-                    console.log('DocumentContentsView:' + test, bindings, layoutFrame);
-                }}
+                onError={(test: any) => console.log('DocumentContentsView:' + test, bindings, layoutFrame)}
             />
         );
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fe95f15af..bd71115db 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -701,16 +701,18 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field
 
     aiContentsWidth = () => (this.aiContentsHeight() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
     aiContentsHeight = () => Math.max(10, this._props.PanelHeight() - (this._aiWinHeight + (this.tagsOverlayFunc() ? 22 : 0)) * this.uiBtnScaling);
+    setAiRef = action((r: HTMLDivElement | null) => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r)));
+
     @computed get aiEditor() {
         return (
             <div
                 className="documentView-editorView"
+                ref={this.setAiRef}
                 style={{
                     background: SnappingManager.userVariantColor,
                     width: `${100 / this.uiBtnScaling}%`, //
                     transform: `scale(${this.uiBtnScaling})`,
-                }}
-                ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}>
+                }}>
                 <div className="documentView-editorView-resizer" />
                 {this._componentView?.componentAIView?.() ?? null}
                 {this._props.DocumentView?.() ? <TagsView background={this.backgroundBoxColor} Views={[this._props.DocumentView?.()]} /> : null}
@@ -1507,18 +1509,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
             .translate(-(this._docViewInternal?.aiShift() ?? 0), 0)
             .scale((this._docViewInternal?.aiScale() ?? 1) / this.nativeScaling);
 
+    setHtmlOverlayRef = (r: HTMLDivElement | null) => {
+        const val = r?.style.display !== 'none'; // if the outer overlay has been displayed, trigger the innner div to start it's opacity fade in transition
+        if (r && val !== this._enableHtmlOverlayTransitions) {
+            setTimeout(action(() => (this._enableHtmlOverlayTransitions = val)));
+        }
+    };
     htmlOverlay = () => {
         const effect = StrCast(this._htmlOverlayEffect?.presentation_effect, StrCast(this._htmlOverlayEffect?.followLinkAnimEffect));
         return (
-            <div
-                className="documentView-htmlOverlay"
-                ref={r => {
-                    const val = r?.style.display !== 'none'; // if the outer overlay has been displayed, trigger the innner div to start it's opacity fade in transition
-                    if (r && val !== this._enableHtmlOverlayTransitions) {
-                        setTimeout(action(() => (this._enableHtmlOverlayTransitions = val)));
-                    }
-                }}
-                style={{ display: !this._htmlOverlayText ? 'none' : undefined }}>
+            <div className="documentView-htmlOverlay" ref={this.setHtmlOverlayRef} style={{ display: !this._htmlOverlayText ? 'none' : undefined }}>
                 <div className="documentView-htmlOverlayInner" style={{ transition: `all 500ms`, opacity: this._enableHtmlOverlayTransitions ? 0.9 : 0 }}>
                     {DocumentViewInternal.AnimationEffect(
                         <div className="webBox-textHighlight">
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index e0ab04692..2ce24b688 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -116,12 +116,13 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.layoutDoc._nativeHeight = mathHeight;
         }
     };
+    setRef = (r: HTMLDivElement) => r && this._ref.current?.element.current && this.updateSize(this._ref.current?.element.current);
     render() {
         TraceMobx();
         const scale = this._props.NativeDimScaling?.() || 1;
         return (
             <div
-                ref={r => r && this._ref.current?.element.current && this.updateSize(this._ref.current?.element.current)}
+                ref={this.setRef}
                 className="equationBox-cont"
                 onKeyDown={e => e.stopPropagation()}
                 onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 8e4b64851..e4d37e006 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -121,11 +121,12 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         }
         // if (this.layout_autoHeight) this.tryUpdateScrollHeight();
     };
+    setRef = (r: HTMLDivElement | null) => r && this.createGraph(r);
     @computed get theGraph() {
         return (
             <div
                 id={`${this._plotId}`}
-                ref={r => r && this.createGraph(r)}
+                ref={this.setRef}
                 style={{ position: 'absolute', width: '100%', height: '100%' }}
                 onPointerDown={e => {
                     e.stopPropagation();
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index e1ecc2018..c5948cbbd 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -169,6 +169,25 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     };
 
+    setRef = (r: HTMLDivElement | null) => {
+        this._divRef?.removeEventListener('beforeinput', this.beforeInput);
+        this._divRef = r;
+        if (this._divRef) {
+            this._divRef.addEventListener('beforeinput', this.beforeInput);
+
+            if (DocumentView.SelectOnLoad === this.Document) {
+                DocumentView.SetSelectOnLoad(undefined);
+                this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+                FormattedTextBox.LiveTextUndo = undefined;
+                this._divRef.focus();
+            }
+            this.fitTextToBox(this._divRef);
+            if (this.Title) {
+                this.resetCursor();
+            }
+        } else this._timeout && clearTimeout(this._timeout);
+    };
+
     render() {
         TraceMobx();
         const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
@@ -228,24 +247,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             __html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title?.startsWith('#') ? '' : (this.Title ?? '')}</span>`,
                         }}
                         contentEditable={this._props.onClickScript?.() ? undefined : true}
-                        ref={r => {
-                            this._divRef?.removeEventListener('beforeinput', this.beforeInput);
-                            this._divRef = r;
-                            if (this._divRef) {
-                                this._divRef.addEventListener('beforeinput', this.beforeInput);
-
-                                if (DocumentView.SelectOnLoad === this.Document) {
-                                    DocumentView.SetSelectOnLoad(undefined);
-                                    this._liveTextUndo = FormattedTextBox.LiveTextUndo;
-                                    FormattedTextBox.LiveTextUndo = undefined;
-                                    this._divRef.focus();
-                                }
-                                this.fitTextToBox(this._divRef);
-                                if (this.Title) {
-                                    this.resetCursor();
-                                }
-                            } else this._timeout && clearTimeout(this._timeout);
-                        }}
+                        ref={this.setRef}
                     />
                 </div>
             </div>
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 78c8a686c..d31fadf77 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -99,6 +99,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     };
 
+    setRef = (r: HTMLDivElement | null) => (this._divRef = r);
     render() {
         TraceMobx();
 
@@ -149,28 +150,18 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const aid = targetAhyperlinks?.find(alink => container?.contains(alink))?.id ?? targetAhyperlinks?.lastElement()?.id;
             const bid = targetBhyperlinks?.find(blink => container?.contains(blink))?.id ?? targetBhyperlinks?.lastElement()?.id;
             if (!aid || !bid) {
-                setTimeout(
-                    action(() => {
-                        this._forceAnimate += 0.01;
-                    })
-                );
+                setTimeout(action(() => (this._forceAnimate += 0.01)));
                 return null;
             }
             if (foundParent) {
                 setTimeout(
-                    action(() => {
-                        this._forceAnimate += 0.01;
-                    }),
+                    action(() => (this._forceAnimate += 0.01)),
                     1
                 );
             }
-
-            if (at || bt)
-                setTimeout(
-                    action(() => {
-                        this._forceAnimate += 0.01;
-                    })
-                ); // this forces an update during a transition animation
+            if (at || bt) {
+                setTimeout(action(() => (this._forceAnimate += 0.01))); // this forces an update during a transition animation
+            }
             const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting) as { highlightStyle: string; highlightColor: string; highlightIndex: number; highlightStroke: boolean };
             const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined;
             const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
@@ -217,7 +208,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 id={this.DocumentView?.().DocUniqueId}
                                 className="linkBox-label"
                                 tabIndex={-1}
-                                ref={r => (this._divRef = r)}
+                                ref={this.setRef}
                                 onPointerDown={e => e.stopPropagation()}
                                 onFocus={() => {
                                     RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, this.dataDoc);
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 5b07b303a..1d6f41c65 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -250,6 +250,10 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
         );
     }
 
+    setDocViewRef = (r: DocumentView | null) => {
+        const targetanchor = this._linkDoc && this._linkSrc && Doc.getOppositeAnchor(this._linkDoc, this._linkSrc);
+        targetanchor && this._targetDoc !== targetanchor && r?._props.focus?.(targetanchor, {});
+    };
     @computed get docPreview() {
         return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? null : (
             <div className="linkDocPreview-inner">
@@ -279,10 +283,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
                         this._toolTipText
                     ) : (
                         <DocumentView
-                            ref={r => {
-                                const targetanchor = this._linkDoc && this._linkSrc && Doc.getOppositeAnchor(this._linkDoc, this._linkSrc);
-                                targetanchor && this._targetDoc !== targetanchor && r?._props.focus?.(targetanchor, {});
-                            }}
+                            ref={this.setDocViewRef}
                             Document={this._targetDoc!}
                             moveDocument={returnFalse}
                             styleProvider={this._props.styleProvider}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 603dcad5c..4677e0e61 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -171,19 +171,21 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         ContextMenu.Instance.addItem({ description: 'Options...', subitems, icon: 'video' });
     };
 
+    setRef = (r: HTMLVideoElement | null) => {
+        this._videoRef = r;
+        setTimeout(() => {
+            if (this.layoutDoc.mediaState === mediaState.PendingRecording && this._videoRef) {
+                this.toggleRecording();
+            }
+        }, 100);
+    };
+
     @computed get content() {
         return (
             <video
                 className="videoBox-content"
                 key="video"
-                ref={r => {
-                    this._videoRef = r;
-                    setTimeout(() => {
-                        if (this.layoutDoc.mediaState === mediaState.PendingRecording && this._videoRef) {
-                            this.toggleRecording();
-                        }
-                    }, 100);
-                }}
+                ref={this.setRef}
                 autoPlay={this._screenCapture}
                 style={{ width: this._screenCapture ? '100%' : undefined, height: this._screenCapture ? '100%' : undefined }}
                 onCanPlay={this.videoLoad}
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx
index a4183a11a..a2fa83b5a 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.tsx
+++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx
@@ -298,10 +298,9 @@ export class CalendarBox extends CollectionSubView() {
                         ev.preventDefault();
                     });
                 }}
-
                 // for dragging and dropping (mirror)
 
-                eventDragStart={(arg) => {
+                eventDragStart={arg => {
                     const mirror = arg.el.cloneNode(true) as HTMLElement;
                     const rect = arg.el.getBoundingClientRect();
 
@@ -312,25 +311,24 @@ export class CalendarBox extends CollectionSubView() {
                     mirror.classList.add('custom-drag-mirror');
                     mirror.style.width = `${rect.width}px`;
                     mirror.style.height = `${rect.height}px`;
-                  
+
                     document.body.appendChild(mirror);
-                  
+
                     const moveListener = (ev: MouseEvent) => {
-                      mirror.style.left = `${ev.clientX}px`;
-                      mirror.style.top = `${ev.clientY}px`;
+                        mirror.style.left = `${ev.clientX}px`;
+                        mirror.style.top = `${ev.clientY}px`;
                     };
-                  
+
                     window.addEventListener('mousemove', moveListener);
 
                     // hide the actual box
                     arg.el.style.visibility = 'hidden';
                     arg.el.style.opacity = '0';
-                  
+
                     (arg.el as any)._mirrorElement = mirror;
                     (arg.el as any)._moveListener = moveListener;
                 }}
-                
-                eventDragStop={(arg) => {
+                eventDragStop={arg => {
                     const el = arg.el as any;
                     const mirror = el._mirrorElement;
                     const moveListener = el._moveListener;
@@ -338,15 +336,18 @@ export class CalendarBox extends CollectionSubView() {
                     // show the actual box
                     el.style.visibility = 'visible';
                     el.style.opacity = '1';
-                    
+
                     if (mirror) document.body.removeChild(mirror);
                     if (moveListener) window.removeEventListener('mousemove', moveListener);
                 }}
-                  
             />
         );
     }
 
+    setRef = (r: HTMLDivElement | null) => {
+        this.createDashEventsTarget(r);
+        this.fixWheelEvents(r, this._props.isContentActive);
+    };
     render() {
         const scale = this._props.ScreenToLocalTransform().Scale;
         const scaledWidth = this._props.PanelWidth();
@@ -361,11 +362,8 @@ export class CalendarBox extends CollectionSubView() {
                     height: scaledHeight,
                     overflow: 'hidden',
                     position: 'relative',
-                  }}
-                ref={r => {
-                    this.createDashEventsTarget(r);
-                    this.fixWheelEvents(r, this._props.isContentActive);
                 }}
+                ref={this.setRef}
                 onPointerDown={e => {
                     setTimeout(
                         action(() => {
@@ -383,9 +381,8 @@ export class CalendarBox extends CollectionSubView() {
                         transformOrigin: 'top left',
                         width: scaledWidth / scale,
                         height: scaledHeight / scale,
-                    }}
-                >
-                {this.renderCalendar}
+                    }}>
+                    {this.renderCalendar}
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 6c3da8977..8043111b9 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -974,6 +974,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     _dictation: DictationButton | null = null;
+    setInputRef = (r: HTMLInputElement) => (this._textInputRef = r);
+    setDictationRef = (r: DictationButton) => (this._dictation = r);
     /**
      * Renders the chat interface, including the message list, input field, and other UI elements.
      */
@@ -1002,9 +1004,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
                 <form onSubmit={this.askGPT} className="chat-input">
                     <input
-                        ref={r => {
-                            this._textInputRef = r;
-                        }}
+                        ref={this.setInputRef}
                         type="text"
                         name="messageInput"
                         autoComplete="off"
@@ -1023,13 +1023,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             </svg>
                         )}
                     </button>
-                    <DictationButton
-                        ref={r => {
-                            this._dictation = r;
-                        }}
-                        setInput={this.setChatInput}
-                        inputRef={this._textInputRef}
-                    />
+                    <DictationButton ref={this.setDictationRef} setInput={this.setChatInput} inputRef={this._textInputRef} />
                 </form>
                 {/* Popup for citation */}
                 {this._citationPopup.visible && (
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 07cb795f1..255ee1afe 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -2129,6 +2129,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
     };
 
+    setRef = (r: HTMLDivElement | null) => this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel);
+    setScrollRef = (r: HTMLDivElement | null) => (this._scrollRef = r);
     render() {
         TraceMobx();
         const scale = this.nativeScaling();
@@ -2143,7 +2145,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         ) : styleFromLayout?.height === '0px' ? null : (
             <div
                 className="formattedTextBox"
-                ref={r => this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel)}
+                ref={this.setRef}
                 style={{
                     ...(this._props.dontScale
                         ? {}
@@ -2181,9 +2183,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     onDoubleClick={this.onDoubleClick}>
                     <div
                         className="formattedTextBox-outer"
-                        ref={r => {
-                            this._scrollRef = r;
-                        }}
+                        ref={this.setScrollRef}
                         style={{
                             width: this.noSidebar ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`,
                             overflow: this.layoutDoc._createDocOnCR || this.layoutDoc._layout_hideScroll ? 'hidden' : this.layout_autoHeight ? 'visible' : undefined,
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index cb2a1f13f..04b312ca5 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1743,6 +1743,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return <div />;
     }
 
+    setAiEffectsRef = (r: HTMLTextAreaElement | null) =>
+        setTimeout(() => {
+            if (r && !r.textContent) {
+                r.style.height = '';
+                r.style.height = r.scrollHeight + 'px';
+            }
+        });
+
+    setAnimDictationRef = (r: DictationButton | null) => (this._animationDictation = r);
     /**
      * This chatbox is for getting slide effect transition suggestions from gpt and visualizing them
      */
@@ -1755,14 +1764,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <ReactTextareaAutosize
                             placeholder="Use AI to suggest effects. Leave blank for random results."
                             className="pres-chatbox"
-                            ref={r => {
-                                setTimeout(() => {
-                                    if (r && !r.textContent) {
-                                        r.style.height = '';
-                                        r.style.height = r.scrollHeight + 'px';
-                                    }
-                                });
-                            }}
+                            ref={this.setAiEffectsRef}
                             value={this._animationChat}
                             onChange={e => {
                                 e.currentTarget.style.height = '';
@@ -1784,12 +1786,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         color={SnappingManager.userVariantColor}
                         onClick={this.customizeAnimations}
                     />
-                    <DictationButton
-                        ref={r => {
-                            this._animationDictation = r;
-                        }}
-                        setInput={this.setAnimationChat}
-                    />
+                    <DictationButton ref={this.setAnimDictationRef} setInput={this.setAnimationChat} />
                 </div>
                 <div style={{ alignItems: 'center' }}>
                     Click a box to use the effect.
@@ -1821,6 +1818,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 
+    setPropertiesRef = (r: HTMLTextAreaElement | null) =>
+        setTimeout(() => {
+            if (r && !r.textContent) {
+                r.style.height = '';
+                r.style.height = r.scrollHeight + 'px';
+            }
+        });
+
+    setSlideDictationRef = (r: DictationButton | null) => (this._slideDictation = r);
+
     @computed get transitionDropdown() {
         const { activeItem } = this;
         // Retrieving spring timing properties
@@ -1855,14 +1862,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 <ReactTextareaAutosize
                                     placeholder="Describe how to modify the slide properties."
                                     className="pres-chatbox"
-                                    ref={r => {
-                                        setTimeout(() => {
-                                            if (r && !r.textContent) {
-                                                r.style.height = '';
-                                                r.style.height = r.scrollHeight + 'px';
-                                            }
-                                        });
-                                    }}
+                                    ref={this.setPropertiesRef}
                                     value={this._chatInput}
                                     onChange={e => {
                                         e.currentTarget.style.height = '';
@@ -1874,12 +1874,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         e.stopPropagation();
                                     }}
                                 />
-                                <DictationButton
-                                    ref={r => {
-                                        this._slideDictation = r;
-                                    }}
-                                    setInput={this.setChatInput}
-                                />
+                                <DictationButton ref={this.setSlideDictationRef} setInput={this.setChatInput} />
                             </div>
                             <Button
                                 style={{ alignSelf: 'flex-end' }}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 568e48edf..660284397 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -515,6 +515,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
         </div>
     );
 
+    setDictationRef = (r: DictationButton | null) => (this._askDictation = r);
     promptBox = (heading: string, value: string, onChange: (e: string) => string, placeholder: string) => (
         <>
             <div className="gptPopup-sortBox">
@@ -541,7 +542,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
                     background={SettingsManager.userVariantColor}
                     onClick={() => this.callGpt(this._mode)}
                 />
-                <DictationButton ref={r => (this._askDictation = r)} setInput={onChange} />
+                <DictationButton ref={this.setDictationRef} setInput={onChange} />
             </div>
         </>
     );
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index d1dda106a..f2c09b6b7 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -18,6 +18,7 @@ export enum InkInkTool {
     Pen = 'Pen',
     Highlight = 'Highlight',
     Write = 'Write',
+    Math = 'Math',
 }
 
 export enum InkEraserTool {
-- 
cgit v1.2.3-70-g09d2

