From 2a313f28fcb8675223708b0657de7517a3281095 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 17 Apr 2024 12:27:21 -0400
Subject: restoring eslint - updates not complete yet

---
 src/client/views/ObservableReactComponent.tsx | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

(limited to 'src/client/views/ObservableReactComponent.tsx')

diff --git a/src/client/views/ObservableReactComponent.tsx b/src/client/views/ObservableReactComponent.tsx
index 394d1eae2..266411770 100644
--- a/src/client/views/ObservableReactComponent.tsx
+++ b/src/client/views/ObservableReactComponent.tsx
@@ -14,7 +14,10 @@ export abstract class ObservableReactComponent<T> extends React.Component<T, {}>
         makeObservable(this);
     }
     componentDidUpdate(prevProps: Readonly<T>): void {
-        Object.keys(prevProps).filter(pkey => (prevProps as any)[pkey] !== (this.props as any)[pkey]).forEach(action(pkey =>
-                ((this._props as any)[pkey] = (this.props as any)[pkey]))); // prettier-ignore
+        Object.keys(prevProps)
+            .filter(pkey => (prevProps as any)[pkey] !== (this.props as any)[pkey])
+            .forEach(action(pkey => {
+                (this._props as any)[pkey] = (this.props as any)[pkey];
+            })); // prettier-ignore
     }
 }
-- 
cgit v1.2.3-70-g09d2


From b858bd3cad81da41e63b9f8e807e41421ca4aa34 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 8 May 2024 21:03:08 -0400
Subject: lots of api cleanup and cycle removal

---
 .../apis/google_docs/GooglePhotosClientUtils.ts    |   2 +-
 src/client/documents/DocFromField.ts               |  59 +++------
 src/client/util/CaptureManager.tsx                 |   5 +-
 src/client/util/DocumentManager.ts                 |  15 ++-
 src/client/util/ReplayMovements.ts                 |  51 +++++---
 src/client/util/SnappingManager.ts                 |   3 +
 src/client/util/UndoManager.ts                     |  10 +-
 src/client/views/DashboardView.tsx                 |   5 -
 src/client/views/DocComponent.tsx                  |  62 +---------
 src/client/views/DocumentDecorations.tsx           |   5 +-
 src/client/views/GestureOverlay.tsx                |  17 +--
 src/client/views/GlobalKeyHandler.ts               |   7 +-
 src/client/views/InkingStroke.tsx                  |   3 +-
 src/client/views/LightboxView.tsx                  |  33 +++--
 src/client/views/Main.tsx                          |  99 +++++++++++++--
 src/client/views/MainView.tsx                      |  29 +++--
 src/client/views/ObservableReactComponent.tsx      |  11 ++
 src/client/views/PropertiesDocContextSelector.tsx  |   4 +-
 src/client/views/StyleProvider.tsx                 |  12 +-
 src/client/views/TemplateMenu.tsx                  |   4 +-
 src/client/views/ViewBoxInterface.ts               |  60 +++++++++
 .../views/collections/CollectionDockingView.tsx    |  13 +-
 src/client/views/collections/CollectionMenu.tsx    |   4 +-
 .../views/collections/CollectionNoteTakingView.tsx |   3 +-
 .../collections/CollectionNoteTakingViewColumn.tsx |  54 +--------
 .../collections/CollectionStackedTimeline.tsx      |   5 +-
 .../views/collections/CollectionStackingView.tsx   |   3 +-
 .../views/collections/CollectionTreeView.tsx       |   2 +-
 src/client/views/collections/CollectionView.tsx    |   4 +-
 src/client/views/collections/TabDocView.tsx        |  16 ++-
 .../CollectionFreeFormPannableContents.tsx         |   5 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  25 ++--
 .../collectionSchema/SchemaTableCell.tsx           |  70 +++++------
 src/client/views/linking/LinkPopup.tsx             |   3 +-
 .../views/newlightbox/ButtonMenu/ButtonMenu.tsx    |  22 ++--
 .../views/newlightbox/ExploreView/ExploreView.tsx  |  30 ++---
 .../views/newlightbox/Header/LightboxHeader.tsx    |   8 +-
 src/client/views/newlightbox/NewLightboxView.tsx   |  62 ++--------
 .../RecommendationList/RecommendationList.tsx      | 134 ++++++++++-----------
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   2 +-
 src/client/views/nodes/ComparisonBox.tsx           |   2 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |   4 +-
 src/client/views/nodes/DocumentContentsView.tsx    |  96 +++------------
 src/client/views/nodes/DocumentView.tsx            | 120 ++++++++++--------
 src/client/views/nodes/EquationBox.tsx             |   4 +-
 src/client/views/nodes/FieldView.tsx               |   2 +-
 src/client/views/nodes/KeyValuePair.tsx            |   3 +-
 src/client/views/nodes/LinkBox.tsx                 |   5 +-
 src/client/views/nodes/MapBox/MapBox.tsx           |   4 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |  13 +-
 src/client/views/nodes/VideoBox.tsx                |  31 ++---
 src/client/views/nodes/WebBox.tsx                  |   6 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  35 ++----
 src/client/views/nodes/trails/PresBox.tsx          |  21 ++--
 src/client/views/search/SearchBox.tsx              |  54 ++++-----
 src/client/views/topbar/TopBar.tsx                 |   4 +-
 src/fields/Doc.ts                                  |   2 +-
 src/fields/InkField.ts                             |  11 ++
 src/fields/Proxy.ts                                |   2 +-
 src/fields/RichTextUtils.ts                        |   2 +-
 src/fields/util.ts                                 |   2 +-
 61 files changed, 664 insertions(+), 720 deletions(-)
 create mode 100644 src/client/views/ViewBoxInterface.ts

(limited to 'src/client/views/ObservableReactComponent.tsx')

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index fdc185a8e..b238f07e9 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -331,7 +331,7 @@ export namespace GooglePhotos {
             if (typeof target === 'string') {
                 description = target;
             } else if (target instanceof RichTextField) {
-                description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data)));
+                description = RichTextUtils.ToPlainText(EditorState.fromJSON(new FormattedTextBox({} as any).config, JSON.parse(target.Data)));
             }
             return description;
         };
diff --git a/src/client/documents/DocFromField.ts b/src/client/documents/DocFromField.ts
index 1c0a9755b..b65bbbdf5 100644
--- a/src/client/documents/DocFromField.ts
+++ b/src/client/documents/DocFromField.ts
@@ -1,50 +1,31 @@
-/* eslint-disable prefer-destructuring */
-/* eslint-disable default-param-last */
-/* eslint-disable no-use-before-define */
 import { Doc, DocListCast } from '../../fields/Doc';
 import { InkField } from '../../fields/InkField';
 import { List } from '../../fields/List';
+import { StrCast } from '../../fields/Types';
 import { AudioField, ImageField, PdfField, VideoField } from '../../fields/URLField';
-import { InkingStroke } from '../views/InkingStroke';
-import { CollectionView } from '../views/collections/CollectionView';
-import { AudioBox } from '../views/nodes/AudioBox';
-import { ImageBox } from '../views/nodes/ImageBox';
-import { PDFBox } from '../views/nodes/PDFBox';
-import { VideoBox } from '../views/nodes/VideoBox';
-import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox';
 import { Docs, DocumentOptions } from './Documents';
 
+/**
+ * Changes the field key in the doc's layout string to be the specified field
+ */
+export function ResetLayoutFieldKey(doc: Doc, fieldKey: string) {
+    doc.layout = StrCast(doc.layout).replace(/={'.*'}/, `={'${fieldKey}'}`);
+    return doc;
+}
 export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
-    let created: Doc | undefined;
     const field = target[fieldKey];
     const resolved = options ?? {};
-    if (field instanceof ImageField) {
-        created = Docs.Create.ImageDocument(field.url.href, resolved);
-        created.layout = ImageBox.LayoutString(fieldKey);
-    } else if (field instanceof Doc) {
-        created = field;
-    } else if (field instanceof VideoField) {
-        created = Docs.Create.VideoDocument(field.url.href, resolved);
-        created.layout = VideoBox.LayoutString(fieldKey);
-    } else if (field instanceof PdfField) {
-        created = Docs.Create.PdfDocument(field.url.href, resolved);
-        created.layout = PDFBox.LayoutString(fieldKey);
-    } else if (field instanceof AudioField) {
-        created = Docs.Create.AudioDocument(field.url.href, resolved);
-        created.layout = AudioBox.LayoutString(fieldKey);
-    } else if (field instanceof InkField) {
-        created = Docs.Create.InkDocument(field.inkData, resolved);
-        created.layout = InkingStroke.LayoutString(fieldKey);
-    } else if (field instanceof List && field[0] instanceof Doc) {
-        created = Docs.Create.StackingDocument(DocListCast(field), resolved);
-        created.layout = CollectionView.LayoutString(fieldKey);
-    } else {
-        created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _layout_autoHeight: true }, ...resolved });
-        created.layout = FormattedTextBox.LayoutString(fieldKey);
-    }
-    if (created) {
-        created.title = fieldKey;
-        proto && created.proto && (created.proto = Doc.GetProto(proto));
-    }
+    const nonDocFieldToDoc = () => {
+        if (field instanceof ImageField) return Docs.Create.ImageDocument(field.url.href, resolved);
+        if (field instanceof VideoField) return Docs.Create.VideoDocument(field.url.href, resolved);
+        if (field instanceof PdfField) return Docs.Create.PdfDocument(field.url.href, resolved);
+        if (field instanceof AudioField) return Docs.Create.AudioDocument(field.url.href, resolved);
+        if (field instanceof InkField) return Docs.Create.InkDocument(field.inkData, resolved);
+        if (field instanceof List && field[0] instanceof Doc) return Docs.Create.StackingDocument(DocListCast(field), resolved);
+        return Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _layout_autoHeight: true }, ...resolved });
+    };
+    const created = field instanceof Doc ? field : ResetLayoutFieldKey(nonDocFieldToDoc(), fieldKey);
+    created.title = fieldKey;
+    proto && created.proto && (created.proto = Doc.GetProto(proto));
     return created;
 }
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index 4fd934774..253cdd8b5 100644
--- a/src/client/util/CaptureManager.tsx
+++ b/src/client/util/CaptureManager.tsx
@@ -7,10 +7,9 @@ import * as React from 'react';
 import { addStyleSheet } from '../../ClientUtils';
 import { Doc } from '../../fields/Doc';
 import { DocCast, StrCast } from '../../fields/Types';
-import { LightboxView } from '../views/LightboxView';
 import { MainViewModal } from '../views/MainViewModal';
-import './CaptureManager.scss';
 import { DocumentView } from '../views/nodes/DocumentView';
+import './CaptureManager.scss';
 
 @observer
 export class CaptureManager extends React.Component<{}> {
@@ -79,7 +78,7 @@ export class CaptureManager extends React.Component<{}> {
                     <div
                         className="save"
                         onClick={() => {
-                            LightboxView.Instance.SetLightboxDoc(this._document);
+                            DocumentView.SetLightboxDoc(this._document);
                             this.close();
                         }}>
                         Save
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 5bcac7330..97051207b 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -7,7 +7,6 @@ import { listSpec } from '../../fields/Schema';
 import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { AudioField } from '../../fields/URLField';
 import { CollectionViewType } from '../documents/DocumentTypes';
-import { LightboxView } from '../views/LightboxView';
 import { DocumentView, DocumentViewInternal } from '../views/nodes/DocumentView';
 import { FocusViewOptions } from '../views/nodes/FocusViewOptions';
 import { OpenWhere } from '../views/nodes/OpenWhere';
@@ -25,7 +24,7 @@ export class DocumentManager {
     // global holds all of the nodes (regardless of which collection they're in)
     @observable private _documentViews = new Set<DocumentView>();
     @computed public get DocumentViews() {
-        return Array.from(this._documentViews).filter(view => (!view.ComponentView?.dontRegisterView?.() && !LightboxView.LightboxDoc) || LightboxView.Contains(view));
+        return Array.from(this._documentViews).filter(view => (!view.ComponentView?.dontRegisterView?.() && !DocumentView.LightboxDoc()) || DocumentView.LightboxContains(view));
     }
     public AddDocumentView(dv: DocumentView) {
         this._documentViews.add(dv);
@@ -68,7 +67,7 @@ export class DocumentManager {
     private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = [];
     public AddViewRenderedCb = (doc: Opt<Doc>, func: (dv: DocumentView) => any) => {
         if (doc) {
-            const dv = LightboxView.LightboxDoc ? this.getLightboxDocumentView(doc) : this.getDocumentView(doc);
+            const dv = DocumentView.LightboxDoc() ? this.getLightboxDocumentView(doc) : this.getDocumentView(doc);
             this._viewRenderedCbs.push({ doc, func });
             if (dv) {
                 this.callAddViewFuncs(dv);
@@ -141,18 +140,18 @@ export class DocumentManager {
 
     public getLightboxDocumentView = (toFind: Doc): DocumentView | undefined => {
         const views: DocumentView[] = [];
-        DocumentManager.Instance.DocumentViews.forEach(view => LightboxView.Contains(view) && Doc.AreProtosEqual(view.Document, toFind) && views.push(view));
+        DocumentManager.Instance.DocumentViews.forEach(view => DocumentView.LightboxContains(view) && Doc.AreProtosEqual(view.Document, toFind) && views.push(view));
         return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /* && view._props.focus !== returnFalse) || views?.find(view => view._props.focus !== returnFalse */) || (views.length ? views[0] : undefined);
     };
     public getFirstDocumentView = (toFind: Doc): DocumentView | undefined => {
-        if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind);
+        if (DocumentView.LightboxDoc()) return DocumentManager.Instance.getLightboxDocumentView(toFind);
         const views = this.getDocumentViews(toFind); // .filter(view => view.Document !== originatingDoc);
         return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /* && view._props.focus !== returnFalse) || views?.find(view => view._props.focus !== returnFalse */) || (views.length ? views[0] : undefined);
     };
     public getDocumentViews(toFind: Doc): DocumentView[] {
         const toReturn: DocumentView[] = [];
-        const docViews = DocumentManager.Instance.DocumentViews.filter(view => !LightboxView.Contains(view));
-        const lightViews = DocumentManager.Instance.DocumentViews.filter(view => LightboxView.Contains(view));
+        const docViews = DocumentManager.Instance.DocumentViews.filter(view => !DocumentView.LightboxContains(view));
+        const lightViews = DocumentManager.Instance.DocumentViews.filter(view => DocumentView.LightboxContains(view));
 
         // heuristic to return the "best" documents first:
         //   choose a document in the lightbox first
@@ -262,7 +261,7 @@ export class DocumentManager {
                     return;
                 }
                 options.didMove = true;
-                (!LightboxView.LightboxDoc && docContextPath.some(doc => DocumentView.activateTabView(doc))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
+                (!DocumentView.LightboxDoc() && docContextPath.some(doc => DocumentView.activateTabView(doc))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
                 this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
             }));
         if (options.openLocation === OpenWhere.lightbox) {
diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts
index 530fcf211..c5afe549c 100644
--- a/src/client/util/ReplayMovements.ts
+++ b/src/client/util/ReplayMovements.ts
@@ -1,16 +1,17 @@
 import { IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { Doc, IdToDoc } from '../../fields/Doc';
-import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
+import { DocumentView } from '../views/nodes/DocumentView';
 import { OpenWhereMod } from '../views/nodes/OpenWhere';
-import { VideoBox } from '../views/nodes/VideoBox';
+import { SnappingManager } from './SnappingManager';
 import { Movement, Presentation } from './TrackMovements';
-import { DocumentView } from '../views/nodes/DocumentView';
+import { ViewBoxInterface } from '../views/ViewBoxInterface';
+import { StrCast } from '../../fields/Types';
 
 export class ReplayMovements {
     private timers: NodeJS.Timeout[] | null;
     private videoBoxDisposeFunc: IReactionDisposer | null;
-    private videoBox: VideoBox | null;
+    private videoBox: ViewBoxInterface<any> | null;
     private isPlaying: boolean;
 
     // create static instance and getter for global use
@@ -29,6 +30,22 @@ export class ReplayMovements {
         this.videoBoxDisposeFunc = null;
         this.videoBox = null;
         this.isPlaying = false;
+
+        reaction(
+            () => SnappingManager.UserPanned,
+            () => {
+                if (Doc.UserDoc()?.presentationMode === 'watching') this.pauseFromInteraction();
+            }
+        );
+        reaction(
+            () => DocumentView.Selected().slice(),
+            selviews => {
+                const selVideo = selviews.find(dv => dv.ComponentView?.playFrom);
+                if (selVideo?.ComponentView?.Play) {
+                    this.setVideoBox(selVideo.ComponentView);
+                } else this.removeVideoBox();
+            }
+        );
     }
 
     // pausing movements will dispose all timers that are planned to replay the movements
@@ -45,18 +62,16 @@ export class ReplayMovements {
         this.timers?.map(timer => clearTimeout(timer));
     };
 
-    setVideoBox = async (videoBox: VideoBox) => {
-        // console.info('setVideoBox', videoBox);
+    setVideoBox = async (videoBox: ViewBoxInterface<any>) => {
         if (this.videoBox !== null) {
             console.warn('setVideoBox on already videoBox');
         }
-        if (this.videoBoxDisposeFunc !== null) {
-            console.warn('setVideoBox on already videoBox dispose func');
-            this.videoBoxDisposeFunc();
-        }
+        this.videoBoxDisposeFunc?.();
+
+        const data = StrCast(videoBox.dataDoc?.[videoBox.fieldKey + '_presentation']);
+        const presentation = data ? JSON.parse(data) : null;
 
-        const { presentation } = videoBox;
-        if (presentation == null) {
+        if (presentation === null) {
             console.warn('setVideoBox on null videoBox presentation');
             return;
         }
@@ -64,18 +79,14 @@ export class ReplayMovements {
         this.loadPresentation(presentation);
 
         this.videoBoxDisposeFunc = reaction(
-            () => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
+            () => ({ playing: videoBox.IsPlaying?.(), timeViewed: videoBox.PlayerTime?.() || 0 }),
             ({ playing, timeViewed }) => (playing ? this.playMovements(presentation, timeViewed) : this.pauseMovements())
         );
         this.videoBox = videoBox;
     };
 
     removeVideoBox = () => {
-        if (this.videoBoxDisposeFunc == null) {
-            console.warn('removeVideoBox on null videoBox');
-            return;
-        }
-        this.videoBoxDisposeFunc();
+        this.videoBoxDisposeFunc?.();
 
         this.videoBox = null;
         this.videoBoxDisposeFunc = null;
@@ -83,7 +94,7 @@ export class ReplayMovements {
 
     // should be called from interacting with the screen
     pauseFromInteraction = () => {
-        this.videoBox?.Pause();
+        this.videoBox?.Pause?.();
 
         this.pauseMovements();
     };
@@ -117,7 +128,7 @@ export class ReplayMovements {
             return undefined;
         }
         // console.log('openTab', docId, doc);
-        CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
+        DocumentView.addSplit(doc, OpenWhereMod.right);
         const docView = DocumentView.getDocumentView(doc);
         // BUG - this returns undefined if the doc is already open
         return docView?.ComponentView as CollectionFreeFormView;
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 6789c2ab8..1337d271f 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -26,6 +26,7 @@ export class SnappingManager {
     @observable _serverVersion: string = '';
     @observable _lastBtnId: string = '';
     @observable _propertyWid: number = 0;
+    @observable _printToConsole: boolean = false;
 
     private constructor() {
         SnappingManager._manager = this;
@@ -55,6 +56,7 @@ export class SnappingManager {
     public static get ServerVersion()  { return this.Instance._serverVersion; } // prettier-ignore
     public static get LastPressedBtn() { return this.Instance._lastBtnId; } // prettier-ignore
     public static get PropertiesWidth(){ return this.Instance._propertyWid; } // prettier-ignore
+    public static get PrintToConsole() { return this.Instance._printToConsole; } // prettier-ignore
 
     public static SetShiftKey       = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
     public static SetCtrlKey        = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore
@@ -69,6 +71,7 @@ export class SnappingManager {
     public static SetServerVersion  = (version:string) =>runInAction(() => {this.Instance._serverVersion = version}); // prettier-ignore
     public static SetLastPressedBtn = (id:string)      =>runInAction(() => {this.Instance._lastBtnId = id}); // prettier-ignore
     public static SetPropertiesWidth= (wid:number)     =>runInAction(() => {this.Instance._propertyWid = wid}); // prettier-ignore
+    public static SetPrintToConsole = (state:boolean)  =>runInAction(() => {this.Instance._printToConsole = state}); // prettier-ignore
 
     public static userColor: string | undefined;
     public static userVariantColor: string | undefined;
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 956c0e674..534ffd2c8 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -3,9 +3,7 @@
 import { action, observable, runInAction } from 'mobx';
 import { Without } from '../../Utils';
 import { RichTextField } from '../../fields/RichTextField';
-
-// eslint-disable-next-line prefer-const
-let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen
+import { SnappingManager } from './SnappingManager';
 
 function getBatchName(target: any, key: string | symbol): string {
     const keyName = key.toString();
@@ -108,7 +106,7 @@ export namespace UndoManager {
 
     export function AddEvent(event: UndoEvent, value?: any): void {
         if (currentBatch && batchCounter.get() && !undoing) {
-            printToConsole &&
+            SnappingManager.PrintToConsole &&
                 console.log(
                     '                         '.slice(0, batchCounter.get()) +
                         'UndoEvent : ' +
@@ -183,7 +181,7 @@ export namespace UndoManager {
     }
 
     export function StartBatch(batchName: string): Batch {
-        printToConsole && console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
+        SnappingManager.PrintToConsole && console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
         runInAction(() => batchCounter.set(batchCounter.get() + 1));
         if (currentBatch === undefined) {
             currentBatch = [];
@@ -193,7 +191,7 @@ export namespace UndoManager {
 
     const EndBatch = action((batchName: string, cancel: boolean = false) => {
         runInAction(() => batchCounter.set(batchCounter.get() - 1));
-        printToConsole && console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + (currentBatch?.length ?? 0) + ')');
+        SnappingManager.PrintToConsole && console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + (currentBatch?.length ?? 0) + ')');
         if (batchCounter.get() === 0 && currentBatch?.length) {
             if (!cancel) {
                 undoStack.push(currentBatch);
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index fc6a330f7..b7383a37e 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -305,11 +305,6 @@ export class DashboardView extends ObservableReactComponent<{}> {
                 });
             if (state.readonly === true || state.readonly === null) {
                 DocServer.Control.makeReadOnly();
-                // } else if (state.safe) {
-                //     if (!state.nro) {
-                //         DocServer.Control.makeReadOnly();
-                //     }
-                //     CollectionView.SetSafeMode(true);
             } else if (state.nro || state.nro === null || state.readonly === false) {
                 /* empty */
             } else if (doc.readOnly) {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index f01666d62..94e84e647 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -2,66 +2,16 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import * as React from 'react';
 import { returnFalse } from '../../ClientUtils';
 import { DateField } from '../../fields/DateField';
-import { Doc, DocListCast, FieldType, Opt } from '../../fields/Doc';
-import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData, DocViews } from '../../fields/DocSymbols';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData } from '../../fields/DocSymbols';
 import { List } from '../../fields/List';
-import { RefField } from '../../fields/RefField';
 import { toList } from '../../fields/Types';
 import { GetEffectiveAcl, inheritParentAcls } from '../../fields/util';
 import { DocumentType } from '../documents/DocumentTypes';
-import { DragManager } from '../util/DragManager';
 import { ObservableReactComponent } from './ObservableReactComponent';
-import { PinProps } from './PinFuncs';
-import { DocumentView } from './nodes/DocumentView';
-import { FocusViewOptions } from './nodes/FocusViewOptions';
+import { ViewBoxInterface } from './ViewBoxInterface';
 import { FieldViewProps } from './nodes/FieldView';
-import { OpenWhere } from './nodes/OpenWhere';
-// import { DocUtils } from '../documents/Documents';
 
-/**
- * Shared interface among all viewBox'es (ie, react classes that render the contents of a Doc)
- * Many of these methods only make sense for specific viewBox'es, but they should be written to
- * be as general as possible
- */
-export class ViewBoxInterface<P> extends ObservableReactComponent<React.PropsWithChildren<P>> {
-    promoteCollection?: () => void; // moves contents of collection to parent
-    updateIcon?: () => 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)
-    restoreView?: (viewSpec: Doc) => boolean;
-    scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus
-    brushView?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number) => void; // highlight a region of a view (used by freeforms)
-    getView?: (doc: Doc, options: FocusViewOptions) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
-    addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
-    addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections)
-    removeDocument?: (doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean) => boolean; // add a document (used only by collections)
-    select?: (ctrlKey: boolean, shiftKey: boolean) => void;
-    focus?: (textAnchor: Doc, options: FocusViewOptions) => Opt<number>;
-    viewTransition?: () => Opt<string>; // duration of a view transition animation
-    isAnyChildContentActive?: () => boolean; // is any child content of the document active
-    onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
-    getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
-    setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
-    playTrail?: (docs: Doc[]) => void;
-    playFrom?: (time: number, endTime?: number) => void;
-    Pause?: () => void; // pause a media document (eg, audio/video)
-    IsPlaying?: () => boolean; // is a media document playing
-    TogglePause?: (keep?: boolean) => void; // toggle media document playing state
-    setFocus?: () => void; // sets input focus to the componentView
-    setData?: (data: FieldType | Promise<RefField | undefined>) => boolean;
-    componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
-    dragStarting?: (snapToDraggedDoc: boolean, showGroupDragTarget: boolean, visited: Set<Doc>) => void;
-    dragConfig?: (dragData: DragManager.DocumentDragData) => void; // function to setup dragData in custom way (see TreeViews which add a tree view flag)
-    incrementalRendering?: () => void;
-    infoUI?: () => JSX.Element | null;
-    contentBounds?: () => undefined | { bounds: { x: number; y: number; r: number; b: number }; cx: number; cy: number; width: number; height: number }; // bounds of contents in collection coordinate space (used by TabDocViewThumb)
-    screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }>;
-    ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
-    ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
-    snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number };
-    search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
-    dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views
-    isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations
-}
 /**
  * DocComponent returns a React base class used by Doc views with accessors for unpacking the Document,layoutDoc, and dataDoc's
  *  (note: this should not be used for the 'Box' views that render the contents of Doc views)
@@ -238,11 +188,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
                         Doc.RemoveEmbedding(rdoc, rdoc);
                     }
                 });
-                if (targetDataDoc.isGroup && DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]).length < 2) {
-                    Array.from(targetDataDoc[DocViews])[0]?.ComponentView?.promoteCollection?.();
-                } else {
-                    this.isAnyChildContentActive() && this._props.select(false);
-                }
+                this.isAnyChildContentActive() && this._props.select(false);
                 return true;
             }
 
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4262c2d57..fd44909c0 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -25,7 +25,6 @@ import { DocumentButtonBar } from './DocumentButtonBar';
 import './DocumentDecorations.scss';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { InkingStroke } from './InkingStroke';
-import { LightboxView } from './LightboxView';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
@@ -95,7 +94,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     @computed get ClippedBounds() {
         const bounds = { ...this.Bounds };
         const leftBounds = this._props.boundsLeft;
-        const topBounds = LightboxView.LightboxDoc ? 0 : this._props.boundsTop;
+        const topBounds = DocumentView.LightboxDoc() ? 0 : this._props.boundsTop;
         bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
         bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight;
         const borderRadiusDraggerWidth = 15;
@@ -277,7 +276,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                     openDoc = Doc.GetEmbeddings(openDoc).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc);
                     Doc.deiconifyView(openDoc);
                 }
-                LightboxView.Instance.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1));
+                DocumentView.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1));
             }
         }
         DocumentView.DeselectAll();
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 7246f0ba0..2f26bdaef 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -21,7 +21,7 @@ import {
     SetActiveInkColor,
     SetActiveInkWidth,
 } from '../../fields/Doc';
-import { InkData, InkTool } from '../../fields/InkField';
+import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { NumCast } from '../../fields/Types';
 // import MobileInkOverlay from '../../mobile/MobileInkOverlay';
 import { Gestures } from '../../pen-gestures/GestureTypes';
@@ -362,7 +362,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                     detail: {
                         points,
                         gesture,
-                        bounds: GestureOverlay.getBounds(points),
+                        bounds: InkField.getBounds(points),
                         text,
                     },
                 })
@@ -370,19 +370,8 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         );
     };
 
-    public static getBounds = (stroke: InkData, pad?: boolean) => {
-        const padding = pad ? [-20000, 20000] : [];
-        const xs = [...padding, ...stroke.map(p => p.X)];
-        const ys = [...padding, ...stroke.map(p => p.Y)];
-        const right = Math.max(...xs);
-        const left = Math.min(...xs);
-        const bottom = Math.max(...ys);
-        const top = Math.min(...ys);
-        return { right, left, bottom, top, width: right - left, height: bottom - top };
-    };
-
     @computed get svgBounds() {
-        return GestureOverlay.getBounds(this._points);
+        return InkField.getBounds(this._points);
     }
 
     get elements() {
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index c73da35bc..18ec0b6a9 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -15,7 +15,6 @@ import { UndoManager, undoable } from '../util/UndoManager';
 import { ContextMenu } from './ContextMenu';
 import { DocumentDecorations } from './DocumentDecorations';
 import { InkStrokeProperties } from './InkStrokeProperties';
-import { LightboxView } from './LightboxView';
 import { MainView } from './MainView';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
@@ -146,7 +145,7 @@ export class KeyManager {
                     }
                     if (doDeselect) {
                         DocumentView.DeselectAll();
-                        LightboxView.Instance.SetLightboxDoc(undefined);
+                        DocumentView.SetLightboxDoc(undefined);
                     }
                     // DictationManager.Controls.stop();
                     GoogleAuthenticationManager.Instance.cancel();
@@ -163,8 +162,8 @@ export class KeyManager {
             case 'delete':
             case 'backspace':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
-                    if (LightboxView.LightboxDoc) {
-                        LightboxView.Instance.SetLightboxDoc(undefined);
+                    if (DocumentView.LightboxDoc()) {
+                        DocumentView.SetLightboxDoc(undefined);
                         DocumentView.DeselectAll();
                     } else if (!window.getSelection()?.toString()) DocumentDecorations.Instance.onCloseClick(true);
                     return { stopPropagation: true, preventDefault: true };
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index f497ca447..55f28f415 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -35,7 +35,8 @@ import { InteractionUtils } from '../util/InteractionUtils';
 import { SnappingManager } from '../util/SnappingManager';
 import { UndoManager } from '../util/UndoManager';
 import { ContextMenu } from './ContextMenu';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from './DocComponent';
+import { ViewBoxInterface } from './ViewBoxInterface';
+import { ViewBoxAnnotatableComponent } from './DocComponent';
 import { Colors } from './global/globalEnums';
 import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
 import './InkStroke.scss';
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 12d899388..269f4fa83 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -19,14 +19,16 @@ import { GestureOverlay } from './GestureOverlay';
 import './LightboxView.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
-import { CollectionDockingView } from './collections/CollectionDockingView';
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
+import { ScriptingGlobals } from '../util/ScriptingGlobals';
+import { OverlayView } from './OverlayView';
 
 interface LightboxViewProps {
     PanelWidth: number;
     PanelHeight: number;
     maxBorder: number[];
+    addSplit: (document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string | undefined, keyValue?: boolean | undefined) => boolean;
 }
 
 const savedKeys = ['freeform_panX', 'freeform_panY', 'freeform_scale', 'layout_scrollTop', 'layout_fieldKey'];
@@ -39,7 +41,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
      * @returns true if a DocumentView is descendant of the lightbox view
      */
     public static Contains(view?:DocumentView) { return view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView); } // prettier-ignore
-    public static get LightboxDoc() { return LightboxView.Instance?._doc; } // prettier-ignore
+    public static LightboxDoc = () => LightboxView.Instance?._doc;
     // eslint-disable-next-line no-use-before-define
     static Instance: LightboxView;
     private _path: {
@@ -65,10 +67,20 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         super(props);
         makeObservable(this);
         LightboxView.Instance = this;
+        DocumentView._setLightboxDoc = this.SetLightboxDoc;
+        DocumentView._lightboxContains = LightboxView.Contains;
+        DocumentView._lightboxDoc = LightboxView.LightboxDoc;
     }
-
+    /**
+     * Sets the root Doc to render in the lightbox view.
+     * @param doc
+     * @param target a Doc within 'doc' to focus on (useful for freeform collections)
+     * @param future a list of Docs to step through with the arrow buttons of the lightbox
+     * @param layoutTemplate a template to apply to 'doc' to render it.
+     * @returns success flag which is currently always true
+     */
     @action
-    public SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) {
+    public SetLightboxDoc = (doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) => {
         const lightDoc = this._doc;
         lightDoc &&
             lightDoc !== doc &&
@@ -110,7 +122,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         this._docTarget = target ?? doc;
 
         return true;
-    }
+    };
 
     public AddDocTab = (docs: Doc | Doc[], location: OpenWhere, layoutTemplate?: Doc | string) => {
         const doc = toList(docs).lastElement();
@@ -183,7 +195,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         const lightDoc = this._docTarget ?? this._doc;
         if (lightDoc) {
             Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', lightDoc);
-            CollectionDockingView.AddSplit(lightDoc, OpenWhereMod.none);
+            this._props.addSplit(lightDoc, OpenWhereMod.none);
             this.SetLightboxDoc(undefined);
         }
     };
@@ -243,7 +255,9 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                 />
             </div>
         );
-        return !this._doc ? null : (
+        return !this._doc ? (
+            <OverlayView />
+        ) : (
             <div
                 className="lightboxView-frame"
                 style={{ background: SnappingManager.userBackgroundColor }}
@@ -324,3 +338,8 @@ export class LightboxTourBtn extends React.Component<LightboxTourBtnProps> {
         return this.props.navBtn('50%', 0, 0, 'chevron-down', this.props.lightboxDoc(), this.props.stepInto, '');
     }
 }
+
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) {
+    LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); // , 0);
+});
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 259ffbbc5..8968acbbb 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -7,7 +7,6 @@ import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
 import { AssignAllExtensions } from '../../extensions/Extensions';
-import { Doc } from '../../fields/Doc';
 import { FieldLoader } from '../../fields/FieldLoader';
 import { BranchingTrailManager } from '../util/BranchingTrailManager';
 import { CurrentUserUtils } from '../util/CurrentUserUtils';
@@ -16,11 +15,49 @@ import { PingManager } from '../util/PingManager';
 import { ReplayMovements } from '../util/ReplayMovements';
 import { TrackMovements } from '../util/TrackMovements';
 import { KeyManager } from './GlobalKeyHandler';
+import { InkingStroke } from './InkingStroke';
 import { MainView } from './MainView';
+import { CollectionCalendarView } from './collections/CollectionCalendarView';
+import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionView } from './collections/CollectionView';
+import { TabDocView } from './collections/TabDocView';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { CollectionFreeFormInfoUI } from './collections/collectionFreeForm/CollectionFreeFormInfoUI';
+import { CollectionSchemaView } from './collections/collectionSchema/CollectionSchemaView';
+import { SchemaRowBox } from './collections/collectionSchema/SchemaRowBox';
 import './global/globalScripts';
+import { AudioBox } from './nodes/AudioBox';
+import { ComparisonBox } from './nodes/ComparisonBox';
+import { DataVizBox } from './nodes/DataVizBox/DataVizBox';
+import { DocumentContentsView, HTMLtag } from './nodes/DocumentContentsView';
+import { EquationBox } from './nodes/EquationBox';
+import { FieldView } from './nodes/FieldView';
+import { FontIconBox } from './nodes/FontIconBox/FontIconBox';
+import { FunctionPlotBox } from './nodes/FunctionPlotBox';
+import { ImageBox } from './nodes/ImageBox';
 import { KeyValueBox } from './nodes/KeyValueBox';
+import { LabelBox } from './nodes/LabelBox';
+import { LinkBox } from './nodes/LinkBox';
+import { LoadingBox } from './nodes/LoadingBox';
+import { MapBox } from './nodes/MapBox/MapBox';
+import { MapPushpinBox } from './nodes/MapBox/MapPushpinBox';
+import { PDFBox } from './nodes/PDFBox';
+import { PhysicsSimulationBox } from './nodes/PhysicsBox/PhysicsSimulationBox';
+import { RecordingBox } from './nodes/RecordingBox';
+import { ScreenshotBox } from './nodes/ScreenshotBox';
+import { ScriptingBox } from './nodes/ScriptingBox';
+import { VideoBox } from './nodes/VideoBox';
+import { WebBox } from './nodes/WebBox';
+import { DashDocCommentView } from './nodes/formattedText/DashDocCommentView';
+import { DashDocView } from './nodes/formattedText/DashDocView';
+import { DashFieldView } from './nodes/formattedText/DashFieldView';
+import { EquationView } from './nodes/formattedText/EquationView';
+import { FootnoteView } from './nodes/formattedText/FootnoteView';
+import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { SummaryView } from './nodes/formattedText/SummaryView';
+import { ImportElementBox } from './nodes/importBox/ImportElementBox';
+import { PresBox, PresElementBox } from './nodes/trails';
+import { SearchBox } from './search/SearchBox';
 
 dotenv.config();
 
@@ -36,22 +73,14 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
     root.render(<FieldLoader />);
     window.location.search.includes('safe') && CollectionView.SetSafeMode(true);
     const info = await CurrentUserUtils.loadCurrentUser();
-    // if (info.email === 'guest') DocServer.Control.makeReadOnly();
     if (!info.userDocumentId) {
         alert('Fatal Error: user not found in database');
         return;
     }
     await CurrentUserUtils.loadUserDocument(info);
     setTimeout(() => {
-        document.getElementById('root')!.addEventListener(
-            'wheel',
-            event => {
-                if (event.ctrlKey) {
-                    event.preventDefault();
-                }
-            },
-            true
-        );
+        // prevent zooming browser
+        document.getElementById('root')!.addEventListener('wheel', event => event.ctrlKey && event.preventDefault(), true);
         const startload = (document as any).startLoad;
         const loading = Date.now() - (startload ? Number(startload) : Date.now() - 3000);
         console.log('Loading Time = ' + loading);
@@ -65,10 +94,56 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
         new PingManager();
         new KeyManager();
 
-        // iniitialize plugin apis
+        // initialize plugins and classes that require plugins
+        CollectionDockingView.Init(TabDocView);
+        FormattedTextBox.Init((tbox: FormattedTextBox) => ({
+            dashComment(node: any, view: any, getPos: any) { return new DashDocCommentView(node, view, getPos); }, // prettier-ignore
+            dashDoc(node: any, view: any, getPos: any)     { return new DashDocView(node, view, getPos, tbox); }, // prettier-ignore
+            dashField(node: any, view: any, getPos: any)   { return new DashFieldView(node, view, getPos, tbox); }, // prettier-ignore
+            equation(node: any, view: any, getPos: any)    { return new EquationView(node, view, getPos, tbox); }, // prettier-ignore
+            summary(node: any, view: any, getPos: any)     { return new SummaryView(node, view, getPos); }, // prettier-ignore
+            footnote(node: any, view: any, getPos: any)    { return new FootnoteView(node, view, getPos); }, // prettier-ignore
+        }));
         CollectionFreeFormInfoUI.Init();
         LinkFollower.Init();
         KeyValueBox.Init();
+        PresBox.Init(TabDocView.AllTabDocs);
+        DocumentContentsView.Init(KeyValueBox.LayoutString(), {
+            FormattedTextBox,
+            ImageBox,
+            FontIconBox,
+            LabelBox,
+            EquationBox,
+            FieldView,
+            CollectionFreeFormView,
+            CollectionDockingView,
+            CollectionSchemaView,
+            CollectionCalendarView,
+            CollectionView,
+            WebBox,
+            KeyValueBox,
+            PDFBox,
+            VideoBox,
+            AudioBox,
+            RecordingBox,
+            PresBox,
+            PresElementBox,
+            SearchBox,
+            FunctionPlotBox,
+            InkingStroke,
+            LinkBox,
+            ScriptingBox,
+            MapBox,
+            ScreenshotBox,
+            DataVizBox,
+            HTMLtag,
+            ComparisonBox,
+            LoadingBox,
+            PhysicsSimulationBox,
+            SchemaRowBox,
+            ImportElementBox,
+            MapPushpinBox,
+        });
         root.render(<MainView />);
     }, 0);
 })();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 637824c31..e4a18dcea 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -12,7 +12,7 @@ import '../../../node_modules/browndash-components/dist/styles/global.min.css';
 import { ClientUtils, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
 import { Doc, DocListCast, GetDocFromUrl, Opt } from '../../fields/Doc';
-import { DocData, DocViews } from '../../fields/DocSymbols';
+import { DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { DocCast, StrCast, toList } from '../../fields/Types';
 import { DocServer } from '../DocServer';
@@ -45,10 +45,9 @@ import { GestureOverlay } from './GestureOverlay';
 import { LightboxView } from './LightboxView';
 import './MainView.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
-import { OverlayView } from './OverlayView';
 import { PreviewCursor } from './PreviewCursor';
 import { PropertiesView } from './PropertiesView';
-import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider';
+import { DashboardStyleProvider, DefaultStyleProvider, returnEmptyDocViewList } from './StyleProvider';
 import { TimelineMenu } from './animationtimeline/TimelineMenu';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionMenu } from './collections/CollectionMenu';
@@ -61,7 +60,7 @@ import { LinkMenu } from './linking/LinkMenu';
 import { AudioBox } from './nodes/AudioBox';
 import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp';
 import { DocButtonState } from './nodes/DocumentLinksButton';
-import { DocumentView, DocumentViewInternal, returnEmptyDocViewList } from './nodes/DocumentView';
+import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
 import { ImageEditorData as ImageEditor } from './nodes/ImageBox';
 import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
 import { LinkDocPreview, LinkInfo } from './nodes/LinkDocPreview';
@@ -102,7 +101,7 @@ export class MainView extends ObservableReactComponent<{}> {
         return this._hideUI ? 0 : 27;
     } //  27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js
     @computed private get topOfDashUI() {
-        return this._hideUI || LightboxView.LightboxDoc ? 0 : Number(TOPBAR_HEIGHT.replace('px', ''));
+        return this._hideUI || DocumentView.LightboxDoc() ? 0 : Number(TOPBAR_HEIGHT.replace('px', ''));
     }
     @computed private get topOfHeaderBarDoc() {
         return this.topOfDashUI;
@@ -171,6 +170,10 @@ export class MainView extends ObservableReactComponent<{}> {
             () => DocumentView.Selected().slice(),
             views => views.length > 1 && (document.activeElement as any)?.blur !== undefined && (document.activeElement as any)!.blur()
         );
+        reaction(
+            () => Doc.MyDockedBtns.linearView_IsOpen,
+            open => SnappingManager.SetPrintToConsole(!!open)
+        );
         const scriptTag = document.createElement('script');
         scriptTag.setAttribute('type', 'text/javascript');
         scriptTag.setAttribute('src', 'https://www.bing.com/api/maps/mapcontrol?callback=makeMap');
@@ -252,7 +255,6 @@ export class MainView extends ObservableReactComponent<{}> {
     constructor(props: any) {
         super(props);
         makeObservable(this);
-        CollectionDockingView.setTabJSXComponent(TabDocView);
         DocumentViewInternal.addDocTabFunc = MainView.addDocTabFunc_impl;
         MainView.Instance = this;
         DashboardView._urlState = HistoryUtil.parseUrl(window.location) || ({} as any);
@@ -686,7 +688,7 @@ export class MainView extends ObservableReactComponent<{}> {
 
     @computed get dockingContent() {
         return (
-            <GestureOverlay isActive={!LightboxView.LightboxDoc}>
+            <GestureOverlay isActive={!DocumentView.LightboxDoc()}>
                 <div
                     key="docking"
                     className={`mainView-dockingContent${this._leftMenuFlyoutWidth ? '-flyout' : ''}`}
@@ -697,7 +699,7 @@ export class MainView extends ObservableReactComponent<{}> {
                     style={{
                         width: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
                         minWidth: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
-                        transform: LightboxView.LightboxDoc ? 'scale(0.0001)' : undefined,
+                        transform: DocumentView.LightboxDoc() ? 'scale(0.0001)' : undefined,
                     }}>
                     {!this.mainContainer ? null : this.mainDocView}
                 </div>
@@ -791,7 +793,7 @@ export class MainView extends ObservableReactComponent<{}> {
 
     @computed get leftMenuPanel() {
         return (
-            <div key="menu" className="mainView-leftMenuPanel" style={{ background: SnappingManager.userBackgroundColor, display: LightboxView.LightboxDoc ? 'none' : undefined }}>
+            <div key="menu" className="mainView-leftMenuPanel" style={{ background: SnappingManager.userBackgroundColor, display: DocumentView.LightboxDoc() ? 'none' : undefined }}>
                 <DocumentView
                     Document={Doc.MyLeftSidebarMenu}
                     addDocument={undefined}
@@ -957,7 +959,7 @@ export class MainView extends ObservableReactComponent<{}> {
     }
     @computed get snapLines() {
         const dragged = DragManager.docsBeingDragged.lastElement() ?? DocumentView.SelectedDocs().lastElement();
-        const dragPar = dragged ? CollectionFreeFormView.from(Array.from(dragged[DocViews]).lastElement()) : undefined;
+        const dragPar = dragged ? CollectionFreeFormView.from(DocumentView.getViews(dragged).lastElement()) : undefined;
         return !dragPar?.layoutDoc.freeform_snapLines ? null : (
             <div className="mainView-snapLines">
                 <svg style={{ width: '100%', height: '100%' }}>
@@ -1066,7 +1068,7 @@ export class MainView extends ObservableReactComponent<{}> {
                         case 'home': return <DashboardView />;
                         case 'dashboard':
                         default: return (<>
-                                        <div key="dashdiv" style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}>
+                                        <div key="dashdiv" style={{ position: 'relative', display: this._hideUI || DocumentView.LightboxDoc() ? 'none' : undefined, zIndex: 2001 }}>
                                             <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} togglePropertiesFlyout={this.togglePropertiesFlyout} toggleTopBar={this.toggleTopBar} topBarHeight={this.headerBarHeightFunc}/>
                                         </div>
                                         {this.mainDashboardArea}
@@ -1083,14 +1085,11 @@ export class MainView extends ObservableReactComponent<{}> {
                 <MarqueeOptionsMenu />
                 <TimelineMenu />
                 <RichTextMenu />
-                {/* <InkTranscription /> */}
                 {this.snapLines}
-                <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} />
-                {LightboxView.LightboxDoc ? null : <OverlayView />}
+                <LightboxView key="lightbox" PanelWidth={this._windowWidth} addSplit={CollectionDockingView.AddSplit} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} />
                 <GPTPopup key="gptpopup" />
                 <SchemaCSVPopUp key="schemacsvpopup" />
                 <GenerativeFill imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
-                {/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */}
             </div>
         );
     }
diff --git a/src/client/views/ObservableReactComponent.tsx b/src/client/views/ObservableReactComponent.tsx
index 266411770..34da82b6c 100644
--- a/src/client/views/ObservableReactComponent.tsx
+++ b/src/client/views/ObservableReactComponent.tsx
@@ -1,6 +1,8 @@
 import { action, makeObservable, observable } from 'mobx';
 import * as React from 'react';
 import './AntimodeMenu.scss';
+import { observer } from 'mobx-react';
+import JsxParser from 'react-jsx-parser';
 
 /**
  * This is an abstract class that serves as the base for a PDF-style or Marquee-style
@@ -21,3 +23,12 @@ export abstract class ObservableReactComponent<T> extends React.Component<T, {}>
             })); // prettier-ignore
     }
 }
+
+class ObserverJsxParser1 extends JsxParser {
+    constructor(props: any) {
+        super(props);
+        observer(this as any);
+    }
+}
+
+export const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 7465c727a..1fea36d16 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -10,7 +10,7 @@ import { Cast, StrCast } from '../../fields/Types';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import './PropertiesDocContextSelector.scss';
 import { CollectionDockingView } from './collections/CollectionDockingView';
-import { DocFocusOrOpen, DocumentView } from './nodes/DocumentView';
+import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere } from './nodes/OpenWhere';
 
 type PropertiesDocContextSelectorProps = {
@@ -58,7 +58,7 @@ export class PropertiesDocContextSelector extends ObservableReactComponent<Prope
     getOnClick = (clickCol: Doc) => {
         if (!this._props.DocView) return;
         const col = Doc.IsDataProto(clickCol) ? Doc.MakeDelegate(clickCol) : clickCol;
-        DocFocusOrOpen(Doc.GetProto(this._props.DocView.Document), undefined, col);
+        DocumentView.FocusOrOpen(Doc.GetProto(this._props.DocView.Document), undefined, col);
     };
 
     render() {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index e6e95e86c..0b8201903 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -12,7 +12,6 @@ import { BsArrowDown, BsArrowDownUp, BsArrowUp } from 'react-icons/bs';
 import { FaFilter } from 'react-icons/fa';
 import { ClientUtils, DashColor, lightOrDark } from '../../ClientUtils';
 import { Doc, Opt, StrListCast } from '../../fields/Doc';
-import { DocViews } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
@@ -23,10 +22,11 @@ import { SnappingManager } from '../util/SnappingManager';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { TreeSort } from './collections/TreeSort';
 import { Colors } from './global/globalEnums';
-import { DocFocusOrOpen, DocumentView, DocumentViewProps } from './nodes/DocumentView';
+import { DocumentView, DocumentViewProps } from './nodes/DocumentView';
 import { FieldViewProps } from './nodes/FieldView';
 import { StyleProp } from './StyleProp';
 import './StyleProvider.scss';
+import { emptyPath } from '../../Utils';
 
 function toggleLockedPosition(doc: Doc) {
     UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -134,7 +134,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
         case StyleProp.Highlighting:
             if (doc && (Doc.IsSystem(doc) || doc.type === DocumentType.FONTICON)) return undefined;            
             if (doc && !doc.layout_disableBrushing && !disableBrushing) {
-                const selected = Array.from(doc?.[DocViews]??[]).filter(dv => dv.IsSelected).length;
+                const selected = DocumentView.getViews(doc).filter(dv => dv.IsSelected).length;
                 const highlightIndex = Doc.GetBrushHighlightStatus(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0);
                 const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? "black" : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
                 const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
@@ -403,7 +403,11 @@ export function DashboardStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps>
     if (doc && property.split(':')[0] === StyleProp.Decorations) {
         return doc._type_collection === CollectionViewType.Docking || Doc.IsSystem(doc)
             ? null
-            : DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => DocFocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.embedContainer ?? doc?.annotationOn)));
+            : DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => DocumentView.FocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.embedContainer ?? doc?.annotationOn)));
     }
     return DefaultStyleProvider(doc, props, property);
 }
+
+export function returnEmptyDocViewList() {
+    return emptyPath;
+}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 5a7c2ef5b..cff32a557 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -13,8 +13,8 @@ import { DocUtils } from '../documents/DocUtils';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { Transform } from '../util/Transform';
 import { CollectionTreeView } from './collections/CollectionTreeView';
-import { DocumentView, returnEmptyDocViewList } from './nodes/DocumentView';
-import { DefaultStyleProvider } from './StyleProvider';
+import { DocumentView } from './nodes/DocumentView';
+import { DefaultStyleProvider, returnEmptyDocViewList } from './StyleProvider';
 import './TemplateMenu.scss';
 
 @observer
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
new file mode 100644
index 000000000..c633f34fb
--- /dev/null
+++ b/src/client/views/ViewBoxInterface.ts
@@ -0,0 +1,60 @@
+import * as React from 'react';
+import { Doc, FieldType, Opt } from '../../fields/Doc';
+import { RefField } from '../../fields/RefField';
+import { DragManager } from '../util/DragManager';
+import { ObservableReactComponent } from './ObservableReactComponent';
+import { PinProps } from './PinFuncs';
+import { DocumentView } from './nodes/DocumentView';
+import { FocusViewOptions } from './nodes/FocusViewOptions';
+import { OpenWhere } from './nodes/OpenWhere';
+// import { DocUtils } from '../documents/Documents';
+
+/**
+ * Shared interface among all viewBox'es (ie, react classes that render the contents of a Doc)
+ * Many of these methods only make sense for specific viewBox'es, but they should be written to
+ * be as general as possible
+ */
+export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React.PropsWithChildren<P>> {
+    abstract get Document(): Doc;
+    abstract get dataDoc(): Doc;
+    abstract get fieldKey(): string;
+    promoteCollection?: () => void; // moves contents of collection to parent
+    updateIcon?: () => 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)
+    restoreView?: (viewSpec: Doc) => boolean;
+    scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus
+    brushView?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number) => void; // highlight a region of a view (used by freeforms)
+    getView?: (doc: Doc, options: FocusViewOptions) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
+    addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
+    addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections)
+    removeDocument?: (doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean) => boolean; // add a document (used only by collections)
+    select?: (ctrlKey: boolean, shiftKey: boolean) => void;
+    focus?: (textAnchor: Doc, options: FocusViewOptions) => Opt<number>;
+    viewTransition?: () => Opt<string>; // duration of a view transition animation
+    isAnyChildContentActive?: () => boolean; // is any child content of the document active
+    onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
+    getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
+    setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
+    playTrail?: (docs: Doc[]) => void;
+    playFrom?: (time: number, endTime?: number, fullPlay?: boolean) => void; // play a range of a media document
+    Play?: () => void; // play a media documents
+    Pause?: () => void; // pause a media document (eg, audio/video)
+    IsPlaying?: () => boolean; // is a media document playing
+    PlayerTime?: () => number | undefined; // current timecode of player
+    TogglePause?: (keep?: boolean) => void; // toggle media document playing state
+    setFocus?: () => void; // sets input focus to the componentView
+    setData?: (data: FieldType | Promise<RefField | undefined>) => boolean;
+    componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
+    dragStarting?: (snapToDraggedDoc: boolean, showGroupDragTarget: boolean, visited: Set<Doc>) => void;
+    dragConfig?: (dragData: DragManager.DocumentDragData) => void; // function to setup dragData in custom way (see TreeViews which add a tree view flag)
+    incrementalRendering?: () => void;
+    infoUI?: () => JSX.Element | null;
+    contentBounds?: () => undefined | { bounds: { x: number; y: number; r: number; b: number }; cx: number; cy: number; width: number; height: number }; // bounds of contents in collection coordinate space (used by TabDocViewThumb)
+    screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }>;
+    ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
+    ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
+    snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number };
+    search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
+    dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views
+    isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations
+}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 0ee3575f3..8fb2b30f1 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -22,7 +22,6 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
 import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
-import { LightboxView } from '../LightboxView';
 import { DocumentView } from '../nodes/DocumentView';
 import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 import { OverlayView } from '../OverlayView';
@@ -37,11 +36,13 @@ const _global = (window /* browser */ || global) /* node */ as any;
 export class CollectionDockingView extends CollectionSubView() {
     static tabClass: JSX.Element | null = null;
     /**
-     * Configure golden layout to render its documents using the specified React component
+     * Initialize by assigning the add split method to DocumentView and by
+     * configuring golden layout to render its documents using the specified React component
      * @param ele - typically would be set to TabDocView
      */
-    static setTabJSXComponent(ele: any) {
+    public static Init(ele: any) {
         this.tabClass = ele;
+        DocumentView.addSplit = CollectionDockingView.AddSplit;
     }
     // eslint-disable-next-line no-use-before-define
     @observable public static Instance: CollectionDockingView | undefined = undefined;
@@ -336,7 +337,7 @@ export class CollectionDockingView extends CollectionSubView() {
         SetPropSetterCb('title', this.titleChanged); // this overrides any previously assigned callback for the property
         if (this._containerRef.current) {
             this._lightboxReactionDisposer = reaction(
-                () => LightboxView.LightboxDoc,
+                () => DocumentView.LightboxDoc(),
                 doc => setTimeout(() => !doc && this.onResize())
             );
             new _global.ResizeObserver(this.onResize).observe(this._containerRef.current);
@@ -391,7 +392,7 @@ export class CollectionDockingView extends CollectionSubView() {
     onResize = () => {
         const cur = this._containerRef.current;
         // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
-        !LightboxView.LightboxDoc && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
+        !DocumentView.LightboxDoc() && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
     };
 
     endUndoBatch = () => {
@@ -633,7 +634,7 @@ export class CollectionDockingView extends CollectionSubView() {
 ScriptingGlobals.add(
     // eslint-disable-next-line prefer-arrow-callback
     function openInLightbox(doc: any) {
-        LightboxView.Instance.AddDocTab(doc, OpenWhere.lightbox);
+        CollectionDockingView.Instance?._props.addDocTab(doc, OpenWhere.lightbox);
     },
     'opens up document in a lightbox',
     '(doc: any)'
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index e53071584..3eb3008c4 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -26,8 +26,8 @@ import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
 import { AntimodeMenu } from '../AntimodeMenu';
 import { EditableView } from '../EditableView';
-import { DefaultStyleProvider } from '../StyleProvider';
-import { DocumentView, DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import './CollectionMenu.scss';
 import { CollectionLinearView } from './collectionLinear';
 
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 53211be77..16c474996 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -22,7 +22,6 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { FieldsDropdown } from '../FieldsDropdown';
 import { Colors } from '../global/globalEnums';
-import { LightboxView } from '../LightboxView';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
@@ -177,7 +176,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         );
 
         this._disposers.refList = reaction(
-            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }),
+            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
             ({ refList, autoHeight }) => {
                 if (autoHeight) {
                     refList.forEach(r => this.observer.observe(r));
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index c098c033b..44ab1968d 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -4,14 +4,11 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { lightOrDark, returnEmptyString } from '../../../ClientUtils';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { RichTextField } from '../../../fields/RichTextField';
+import { Doc, Opt } from '../../../fields/Doc';
 import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { Cast, NumCast } from '../../../fields/Types';
-import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { DocumentFromField } from '../../documents/DocFromField';
 import { DocUtils } from '../../documents/DocUtils';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
@@ -19,11 +16,10 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch, undoable } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import './CollectionNoteTakingView.scss';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import './CollectionNoteTakingView.scss';
 
 interface CSVFieldColumnProps {
     Document: Doc;
@@ -171,9 +167,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
 
     menuCallback = (x: number, y: number) => {
         ContextMenu.Instance.clearItems();
-        const layoutItems: ContextMenuProps[] = [];
-        const docItems: ContextMenuProps[] = [];
-        const dataDoc = this._props.TemplateDataDocument || this._props.Document;
+        const { pivotField } = this._props;
         const pivotValue = this.getValue(this._props.heading);
 
         DocUtils.addDocumentCreatorMenuItems(
@@ -187,50 +181,10 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
             x,
             y,
             true,
-            this._props.pivotField,
+            pivotField, // when created, the new doc's pivotField will be set to pivotValue
             pivotValue
         );
 
-        Array.from(Object.keys(Doc.GetProto(dataDoc)))
-            .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string')
-            .map(fieldKey =>
-                docItems.push({
-                    description: ':' + fieldKey,
-                    event: () => {
-                        const created = DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this._props.Document));
-                        if (created) {
-                            if (this._props.Document.isTemplateDoc) {
-                                Doc.MakeMetadataFieldTemplate(created, this._props.Document);
-                            }
-                            return this._props.addDocument?.(created);
-                        }
-                        return undefined;
-                    },
-                    icon: 'compress-arrows-alt',
-                })
-            );
-        Array.from(Object.keys(Doc.GetProto(dataDoc)))
-            .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length)
-            .map(fieldKey =>
-                docItems.push({
-                    description: ':' + fieldKey,
-                    event: () => {
-                        const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey });
-                        if (created) {
-                            const container = this._props.Document.resolvedDataDoc ? Doc.GetProto(this._props.Document) : this._props.Document;
-                            if (container.isTemplateDoc) {
-                                Doc.MakeMetadataFieldTemplate(created, container);
-                                return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created);
-                            }
-                            return this._props.addDocument?.(created) || false;
-                        }
-                        return undefined;
-                    },
-                    icon: 'compress-arrows-alt',
-                })
-            );
-        !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' });
-        !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' });
         ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
             Doc.GetProto(this._props.Document)[name] = '';
             const created = Docs.Create.TextDocument('', { title: name, _width: 250, _layout_autoHeight: true });
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 1604920f6..fac885300 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -26,7 +26,6 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { VideoThumbnails } from '../global/globalEnums';
-import { LightboxView } from '../LightboxView';
 import { AudioWaveform } from '../nodes/audio/AudioWaveform';
 import { DocumentView } from '../nodes/DocumentView';
 import { FocusFuncType, StyleProviderFuncType } from '../nodes/FieldView';
@@ -752,10 +751,10 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
                 // const dictationDoc = Cast(this._props.layoutDoc.data_dictation, Doc, null);
                 // const isDictation = dictationDoc && LinkManager.Links(this._props.mark).some(link => Cast(link.link_anchor_1, Doc, null)?.annotationOn === dictationDoc);
                 if (
-                    !LightboxView.LightboxDoc &&
+                    !DocumentView.LightboxDoc() &&
                     // bcz: when should links be followed?  we don't want to move away from the video to follow a link but we can open it in a sidebar/etc.  But we don't know that upfront.
                     // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
-                    /* (isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this._props.layoutDoc)) */
+                    /* (isDictation || !Doc.AreProtosEqual(DocumentView.LightboxDoc(), this._props.layoutDoc)) */
                     !this._props.layoutDoc.dontAutoFollowLinks &&
                     Doc.Links(this._props.mark).length &&
                     time > NumCast(this._props.mark[this._props.startTag]) &&
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 07aa0f4f0..56d2a6c9c 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -26,7 +26,6 @@ import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from '../EditableView';
-import { LightboxView } from '../LightboxView';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
@@ -229,7 +228,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             layoutAutoHeight => layoutAutoHeight && this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0)))
         );
         this._disposers.refList = reaction(
-            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }),
+            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
             ({ refList, autoHeight }) => {
                 this.observer.disconnect();
                 if (autoHeight) refList.forEach(r => this.observer.observe(r));
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index c1247f5b0..beb8c0666 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -209,7 +209,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     onContextMenu = (): void => {
         // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
         const layoutItems: ContextMenuProps[] = [];
-        const menuDoc = ScriptCast(Cast(this.layoutDoc.layout_headerButton, Doc, null)?.onClick).script.originalScript === CollectionTreeView.AddTreeFunc;
+        const menuDoc = ScriptCast(Cast(this.layoutDoc.layout_headerButton, Doc, null)?.onClick)?.script.originalScript === CollectionTreeView.AddTreeFunc;
         menuDoc && layoutItems.push({ description: 'Create new folder', event: () => CollectionTreeView.addTreeFolder(this.Document), icon: 'paint-brush' });
         if (!Doc.noviceMode) {
             layoutItems.push({
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7cadd072b..b52c7c54c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -13,7 +13,7 @@ import { Docs } from '../../documents/Documents';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { FieldView } from '../nodes/FieldView';
 import { OpenWhere } from '../nodes/OpenWhere';
 import { CollectionCalendarView } from './CollectionCalendarView';
@@ -35,7 +35,7 @@ import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultir
 import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView';
 
 @observer
-export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewProps>() implements ViewBoxInterface {
+export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewProps>() {
     public static LayoutString(fieldStr: string) {
         return FieldView.LayoutString(CollectionView, fieldStr);
     }
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 008ef6ab4..afd584154 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -9,7 +9,7 @@ import * as ReactDOM from 'react-dom/client';
 import { ClientUtils, DashColor, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
-import { DocData, DocViews } from '../../../fields/DocSymbols';
+import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { FieldId } from '../../../fields/RefField';
@@ -28,13 +28,14 @@ import { LightboxView } from '../LightboxView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
-import { DefaultStyleProvider } from '../StyleProvider';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
 import { Colors } from '../global/globalEnums';
-import { DocumentView, returnEmptyDocViewList } from '../nodes/DocumentView';
+import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { KeyValueBox } from '../nodes/KeyValueBox';
 import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
-import { PresBox, PresMovement } from '../nodes/trails';
+import { PresBox } from '../nodes/trails';
+import { PresMovement } from '../nodes/trails/PresEnums';
 import { CollectionDockingView } from './CollectionDockingView';
 import { CollectionView } from './CollectionView';
 import './TabDocView.scss';
@@ -187,6 +188,11 @@ interface TabDocViewProps {
 @observer
 export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     static _allTabs = new ObservableSet<TabDocView>();
+    public static AllTabDocs() {
+        return Array.from(TabDocView._allTabs)
+            .filter(tv => tv._document)
+            .map(tv => tv._document!);
+    }
     _mainCont: HTMLDivElement | null = null;
     _tabReaction: IReactionDisposer | undefined;
 
@@ -294,7 +300,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     @observable _isActive: boolean = false;
     @observable _isAnyChildContentActive = false;
     public static IsSelected = (doc?: Doc) => {
-        if (Array.from(doc?.[DocViews] ?? []).some(dv => dv?.IsSelected)) {
+        if (DocumentView.getViews(doc).some(dv => dv?.IsSelected)) {
             return true;
         }
         return false;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
index 65a2fe0aa..e543b4008 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx
@@ -20,6 +20,11 @@ export interface CollectionFreeFormPannableContentsProps {
 @observer
 export class CollectionFreeFormPannableContents extends ObservableReactComponent<CollectionFreeFormPannableContentsProps> {
     static _overlayPlugin: ((fform: Doc) => React.JSX.Element) | null = null;
+    /**
+     * Setup a plugin function that returns components to display on a layer above the collection
+     *     See PresBox which renders presenstation paths over the collection
+     * @param plugin a function that receives the collection Doc and returns JSX Elements
+     */
     public static SetOverlayPlugin(plugin: ((fform: Doc) => React.JSX.Element) | null) {
         CollectionFreeFormPannableContents._overlayPlugin = plugin;
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 74de6524b..dbd9fb11f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -28,7 +28,6 @@ import { CollectionViewType, DocumentType } from '../../../documents/DocumentTyp
 import { DocUtils } from '../../../documents/DocUtils';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
-import { ReplayMovements } from '../../../util/ReplayMovements';
 import { CompileScript } from '../../../util/Scripting';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { freeformScrollMode, SnappingManager } from '../../../util/SnappingManager';
@@ -36,9 +35,7 @@ import { Transform } from '../../../util/Transform';
 import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
 import { Timeline } from '../../animationtimeline/Timeline';
 import { ContextMenu } from '../../ContextMenu';
-import { GestureOverlay } from '../../GestureOverlay';
 import { InkingStroke } from '../../InkingStroke';
-import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
 import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
 import { DocumentView } from '../../nodes/DocumentView';
@@ -367,7 +364,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) && !LightboxView.LightboxDoc);
+        const cantTransform = this.fitContentsToBox || ((this.Document.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
@@ -620,7 +617,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return false;
     };
     forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => {
-        this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, GestureOverlay.getBounds(points), text));
+        this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text));
     };
 
     onPointerMove = (e: PointerEvent) => {
@@ -838,8 +835,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     setPan(panXIn: number, panYIn: number, panTime: number = 0, allowScroll = false) {
         let [panX, panY] = [panXIn, panYIn];
-        // this is the easiest way to do this -> will talk with Bob about using mobx to do this to remove this line of code.
-        if (Doc.UserDoc()?.presentationMode === 'watching') ReplayMovements.Instance.pauseFromInteraction();
 
         if (!this.isAnnotationOverlay && this.childDocs.length) {
             // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
@@ -849,7 +844,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             panX = clamp(panX, xrangeMin - widScaling / 2, xrangeMax + widScaling / 2);
             panY = clamp(panY, yrangeMin - hgtScaling / 2, yrangeMax + hgtScaling / 2);
         }
-        if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc) {
+        if (!this.layoutDoc._lockedTransform || DocumentView.LightboxDoc()) {
             this.setPanZoomTransition(panTime);
             const minScale = NumCast(this.dataDoc._freeform_scale_min, 1);
             const scale = 1 - minScale / this.zoomScaling();
@@ -882,6 +877,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     nudge = (x: number, y: number, nudgeTime: number = 500) => {
         const collectionDoc = this.Document;
         if (collectionDoc?._type_collection !== CollectionViewType.Freeform) {
+            SnappingManager.TriggerUserPanned();
             this.setPan(
                 NumCast(this.layoutDoc[this.panXFieldKey]) + ((this._props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale
                 NumCast(this.layoutDoc[this.panYFieldKey]) + ((this._props.PanelHeight() / 2) * -y) / this.zoomScaling(),
@@ -1008,6 +1004,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         return undefined;
     };
+
+    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) {
+            this.promoteCollection();
+        }
+        return ret;
+    };
     childPointerEventsFunc = () => this._childPointerEvents;
     childContentsActive = () => (this._props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)();
     getChildDocView(entry: PoolData) {
@@ -1050,7 +1055,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 focus={this.Document.isGroup ? this.groupFocus : this.isAnnotationOverlay ? this._props.focus : this.focus}
                 addDocTab={this.addDocTab}
                 addDocument={this._props.addDocument}
-                removeDocument={this._props.removeDocument}
+                removeDocument={this.removeDocument}
                 moveDocument={this._props.moveDocument}
                 pinToPres={this._props.pinToPres}
                 whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
@@ -1525,7 +1530,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     incrementalRendering = () => this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])).length !== 0;
 
     incrementalRender = action(() => {
-        if (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())) {
+        if (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())) {
             const layoutUnrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
             const loadIncrement = this.Document.isTemplateDoc ? Number.MAX_VALUE : 5;
             for (let i = 0; i < Math.min(layoutUnrendered.length, loadIncrement); i++) {
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index b017eb62b..5874364e0 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -25,9 +25,9 @@ import { Transform } from '../../../util/Transform';
 import { undoBatch, undoable } from '../../../util/UndoManager';
 import { EditableView } from '../../EditableView';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
-import { DefaultStyleProvider } from '../../StyleProvider';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider';
 import { Colors } from '../../global/globalEnums';
-import { DocFocusOrOpen, returnEmptyDocViewList } from '../../nodes/DocumentView';
+import { DocumentView } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { FInfotoColType } from './CollectionSchemaView';
@@ -59,6 +59,14 @@ export interface SchemaTableCellProps {
     rootSelected?: () => boolean;
 }
 
+function selectedCell(props: SchemaTableCellProps) {
+    return (
+        props.isRowActive() &&
+        props.selectedCol() === props.col && //
+        props.selectedCells()?.filter(d => d === props.Document)?.length
+    );
+}
+
 @observer
 export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellProps> {
     constructor(props: SchemaTableCellProps) {
@@ -67,7 +75,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
     }
 
     static addFieldDoc = (docs: Doc | Doc[] /* , where: OpenWhere */) => {
-        DocFocusOrOpen(toList(docs)[0]);
+        DocumentView.FocusOrOpen(toList(docs)[0]);
         return true;
     };
     public static renderProps(props: SchemaTableCellProps) {
@@ -114,11 +122,6 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
         return { color, textDecoration, fieldProps, cursor, pointerEvents };
     }
 
-    @computed get selected() {
-        const selectedDocs: Doc[] | undefined = this._props.selectedCells();
-        return this._props.isRowActive() && selectedDocs?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col;
-    }
-
     @computed get defaultCellContent() {
         const { color, textDecoration, fieldProps, pointerEvents } = SchemaTableCell.renderProps(this._props);
 
@@ -132,12 +135,12 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
                     pointerEvents,
                 }}>
                 <EditableView
-                    ref={r => this.selected && this._props.autoFocus && r?.setIsFocused(true)}
+                    ref={r => selectedCell(this._props) && this._props.autoFocus && r?.setIsFocused(true)}
                     oneLine={this._props.oneLine}
                     allowCRs={this._props.allowCRs}
                     contents={undefined}
                     fieldContents={fieldProps}
-                    editing={this.selected ? undefined : false}
+                    editing={selectedCell(this._props) ? undefined : false}
                     GetValue={() => Field.toKeyValueString(fieldProps.Document, this._props.fieldKey, SnappingManager.MetaKey)}
                     SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => {
                         if (shiftDown && enterKey) {
@@ -157,30 +160,27 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
     get getCellType() {
         const columnTypeStr = this._props.getFinfo(this._props.fieldKey)?.fieldType;
         const cellValue = this._props.Document[this._props.fieldKey];
+
         if (cellValue instanceof ImageField) return ColumnType.Image;
         if (cellValue instanceof DateField) return ColumnType.Date;
         if (cellValue instanceof RichTextField) return ColumnType.RTF;
         if (typeof cellValue === 'number') return ColumnType.Any;
         if (typeof cellValue === 'string' && columnTypeStr !== FInfoFieldType.enumeration) return ColumnType.Any;
         if (typeof cellValue === 'boolean') return ColumnType.Boolean;
-
-        if (columnTypeStr && columnTypeStr in FInfotoColType) {
-            return FInfotoColType[columnTypeStr];
-        }
+        if (columnTypeStr && columnTypeStr in FInfotoColType) return FInfotoColType[columnTypeStr];
 
         return ColumnType.Any;
     }
 
     get content() {
-        const cellType: ColumnType = this.getCellType;
         // prettier-ignore
-        switch (cellType) {
-            case ColumnType.Image: return <SchemaImageCell {...this._props} />;
-            case ColumnType.Boolean: return <SchemaBoolCell {...this._props} />;
-            case ColumnType.RTF: return <SchemaRTFCell {...this._props} />;
+        switch (this.getCellType) {
+            case ColumnType.Image:       return <SchemaImageCell {...this._props} />;
+            case ColumnType.Boolean:     return <SchemaBoolCell {...this._props} />;
+            case ColumnType.RTF:         return <SchemaRTFCell {...this._props} />;
             case ColumnType.Enumeration: return <SchemaEnumerationCell {...this._props} options={this._props.getFinfo(this._props.fieldKey)?.values?.map(val => Field.toString(val))} />;
-            case ColumnType.Date: return <SchemaDateCell {...this._props} />;
-            default:  return this.defaultCellContent;
+            case ColumnType.Date:        return <SchemaDateCell {...this._props} />;
+            default:                     return this.defaultCellContent;
         }
     }
 
@@ -193,13 +193,13 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
                     const shift: boolean = e.shiftKey;
                     const ctrl: boolean = e.ctrlKey;
                     if (this._props.isRowActive?.() !== false) {
-                        if (this.selected && ctrl) {
+                        if (selectedCell(this._props) && ctrl) {
                             this._props.selectCell(this._props.Document, this._props.col, shift, ctrl);
                             e.stopPropagation();
-                        } else !this.selected && this._props.selectCell(this._props.Document, this._props.col, shift, ctrl);
+                        } else !selectedCell(this._props) && this._props.selectCell(this._props.Document, this._props.col, shift, ctrl);
                     }
                 })}
-                style={{ padding: this._props.padding, maxWidth: this._props.maxWidth?.(), width: this._props.columnWidth() || undefined, border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}>
+                style={{ padding: this._props.padding, maxWidth: this._props.maxWidth?.(), width: this._props.columnWidth() || undefined, border: selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}>
                 {this.content}
             </div>
         );
@@ -329,20 +329,14 @@ export class SchemaRTFCell extends ObservableReactComponent<SchemaTableCellProps
         makeObservable(this);
     }
 
-    @computed get selected() {
-        const selected = this._props.selectedCells();
-        return this._props.isRowActive() && selected && selected?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col;
-        // return this._props.isRowActive() && selected?.[0] === this._props.Document && selected[1] === this._props.col;
-    }
-
     // if the text box blurs and none of its contents are focused(), then the edit finishes
-    selectedFunc = () => this.selected;
+    selectedFunc = () => !!selectedCell(this._props);
     render() {
         const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
         fieldProps.isContentActive = this.selectedFunc;
         return (
-            <div className="schemaRTFCell" style={{ fontStyle: this.selected ? undefined : 'italic', color, textDecoration, cursor, pointerEvents }}>
-                {this.selected ? <FormattedTextBox {...fieldProps} autoFocus onBlur={() => this._props.finishEdit?.()} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
+            <div className="schemaRTFCell" style={{ fontStyle: selectedCell(this._props) ? undefined : 'italic', color, textDecoration, cursor, pointerEvents }}>
+                {selectedCell(this._props) ? <FormattedTextBox {...fieldProps} autoFocus onBlur={() => this._props.finishEdit?.()} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
             </div>
         );
     }
@@ -354,10 +348,6 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp
         makeObservable(this);
     }
 
-    @computed get selected() {
-        const selected = this._props.selectedCells();
-        return this._props.isRowActive() && selected && selected?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col;
-    }
     render() {
         const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
         return (
@@ -375,7 +365,7 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp
                 <EditableView
                     contents={undefined}
                     fieldContents={fieldProps}
-                    editing={this.selected ? undefined : false}
+                    editing={selectedCell(this._props) ? undefined : false}
                     GetValue={() => Field.toKeyValueString(this._props.Document, this._props.fieldKey)}
                     SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => {
                         if (shiftDown && enterKey) {
@@ -399,10 +389,6 @@ export class SchemaEnumerationCell extends ObservableReactComponent<SchemaTableC
         makeObservable(this);
     }
 
-    @computed get selected() {
-        const selected = this._props.selectedCells();
-        return this._props.isRowActive() && selected && selected?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col;
-    }
     render() {
         const { color, textDecoration, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
         const options = this._props.options?.map(facet => ({ value: facet, label: facet }));
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index 9fb1c0fdc..76a8396ff 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -5,8 +5,7 @@ import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '
 import { emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
 import { Transform } from '../../util/Transform';
-import { DefaultStyleProvider } from '../StyleProvider';
-import { returnEmptyDocViewList } from '../nodes/DocumentView';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
 import { SearchBox } from '../search/SearchBox';
 import './LinkPopup.scss';
 
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
index e48e993cf..3eb99f47a 100644
--- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
@@ -1,24 +1,27 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { action } from 'mobx';
 import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
 import { InkTool } from '../../../../fields/InkField';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
+import { DocumentView } from '../../nodes/DocumentView';
 import { OpenWhereMod } from '../../nodes/OpenWhere';
 import { NewLightboxView } from '../NewLightboxView';
 import './ButtonMenu.scss';
-import { IButtonMenu } from './utils';
 
-export const ButtonMenu = (props: IButtonMenu) => {
+export function ButtonMenu() {
     return (
-        <div className={`newLightboxButtonMenu-container`}>
+        <div className="newLightboxButtonMenu-container">
             <div
                 className="newLightboxView-navBtn"
                 title="toggle fit width"
                 onClick={e => {
                     e.stopPropagation();
                     NewLightboxView.LightboxDoc!._fitWidth = !NewLightboxView.LightboxDoc!._fitWidth;
-                }}></div>
+                }}
+            />
             <div
                 className="newLightboxView-tabBtn"
                 title="open in tab"
@@ -27,7 +30,8 @@ export const ButtonMenu = (props: IButtonMenu) => {
                     CollectionDockingView.AddSplit(NewLightboxView.LightboxDoc || NewLightboxView.LightboxDoc!, OpenWhereMod.none);
                     DocumentView.DeselectAll();
                     NewLightboxView.SetNewLightboxDoc(undefined);
-                }}></div>
+                }}
+            />
             <div
                 className="newLightboxView-penBtn"
                 title="toggle pen annotation"
@@ -35,7 +39,8 @@ export const ButtonMenu = (props: IButtonMenu) => {
                 onClick={e => {
                     e.stopPropagation();
                     Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
-                }}></div>
+                }}
+            />
             <div
                 className="newLightboxView-exploreBtn"
                 title="toggle explore mode to navigate among documents only"
@@ -43,7 +48,8 @@ export const ButtonMenu = (props: IButtonMenu) => {
                 onClick={action(e => {
                     e.stopPropagation();
                     SnappingManager.SetExploreMode(!SnappingManager.ExploreMode);
-                })}></div>
+                })}
+            />
         </div>
     );
-};
+}
diff --git a/src/client/views/newlightbox/ExploreView/ExploreView.tsx b/src/client/views/newlightbox/ExploreView/ExploreView.tsx
index a1d6375c4..f8c07cc43 100644
--- a/src/client/views/newlightbox/ExploreView/ExploreView.tsx
+++ b/src/client/views/newlightbox/ExploreView/ExploreView.tsx
@@ -1,32 +1,34 @@
-import './ExploreView.scss';
-import { IBounds, IExploreView, emptyBounds } from './utils';
-import { IRecommendation } from '../components';
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import * as React from 'react';
-import { NewLightboxView } from '../NewLightboxView';
 import { StrCast } from '../../../../fields/Types';
+import { NewLightboxView } from '../NewLightboxView';
+import './ExploreView.scss';
+import { IExploreView, emptyBounds } from './utils';
 
-export const ExploreView = (props: IExploreView) => {
+export function ExploreView(props: IExploreView) {
     const { recs, bounds = emptyBounds } = props;
 
     return (
-        <div className={`exploreView-container`}>
+        <div className="exploreView-container">
             {recs &&
                 recs.map(rec => {
-                    const x_bound: number = Math.max(Math.abs(bounds.max_x), Math.abs(bounds.min_x));
-                    const y_bound: number = Math.max(Math.abs(bounds.max_y), Math.abs(bounds.min_y));
+                    const xBound: number = Math.max(Math.abs(bounds.max_x), Math.abs(bounds.min_x));
+                    const yBound: number = Math.max(Math.abs(bounds.max_y), Math.abs(bounds.min_y));
                     if (rec.embedding) {
-                        const x = (rec.embedding.x / x_bound) * 50;
-                        const y = (rec.embedding.y / y_bound) * 50;
+                        const x = (rec.embedding.x / xBound) * 50;
+                        const y = (rec.embedding.y / yBound) * 50;
                         return (
-                            <div className={`exploreView-doc`} onClick={() => {}} style={{ top: `calc(50% + ${y}%)`, left: `calc(50% + ${x}%)` }}>
+                            <div className="exploreView-doc" onClick={() => {}} style={{ top: `calc(50% + ${y}%)`, left: `calc(50% + ${x}%)` }}>
                                 {rec.title}
                             </div>
                         );
-                    } else return null;
+                    }
+                    return null;
                 })}
-            <div className={`exploreView-doc`} style={{ top: `calc(50% + ${0}%)`, left: `calc(50% + ${0}%)`, background: '#073763', color: 'white' }}>
+            <div className="exploreView-doc" style={{ top: `calc(50% + ${0}%)`, left: `calc(50% + ${0}%)`, background: '#073763', color: 'white' }}>
                 {StrCast(NewLightboxView.LightboxDoc?.title)}
             </div>
         </div>
     );
-};
+}
diff --git a/src/client/views/newlightbox/Header/LightboxHeader.tsx b/src/client/views/newlightbox/Header/LightboxHeader.tsx
index 51bfaa4e5..882d28fba 100644
--- a/src/client/views/newlightbox/Header/LightboxHeader.tsx
+++ b/src/client/views/newlightbox/Header/LightboxHeader.tsx
@@ -4,8 +4,8 @@ import { BsBookmark, BsBookmarkFill } from 'react-icons/bs';
 import { MdTravelExplore } from 'react-icons/md';
 import { Doc } from '../../../../fields/Doc';
 import { StrCast } from '../../../../fields/Types';
-import { LightboxView } from '../../LightboxView';
 import { Colors } from '../../global/globalEnums';
+import { DocumentView } from '../../nodes/DocumentView';
 import { NewLightboxView } from '../NewLightboxView';
 import { EditableText } from '../components/EditableText';
 import { getType } from '../utils';
@@ -14,11 +14,11 @@ import { INewLightboxHeader } from './utils';
 
 export function NewLightboxHeader(props: INewLightboxHeader) {
     const { height = 100, width } = props;
-    const [doc, setDoc] = React.useState<Doc | undefined>(LightboxView.LightboxDoc);
+    const [doc, setDoc] = React.useState<Doc | undefined>(DocumentView.LightboxDoc());
     const [editing, setEditing] = React.useState<boolean>(false);
     const [title, setTitle] = React.useState<JSX.Element | null>(null);
     React.useEffect(() => {
-        const lbDoc = LightboxView.LightboxDoc;
+        const lbDoc = DocumentView.LightboxDoc();
         setDoc(lbDoc);
         if (lbDoc) {
             setTitle(
@@ -32,7 +32,7 @@ export function NewLightboxHeader(props: INewLightboxHeader) {
                 />
             );
         }
-    }, [LightboxView.LightboxDoc]);
+    }, [DocumentView.LightboxDoc()]);
 
     const [saved, setSaved] = React.useState<boolean>(false);
 
diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx
index 558ce7e38..c86ddb745 100644
--- a/src/client/views/newlightbox/NewLightboxView.tsx
+++ b/src/client/views/newlightbox/NewLightboxView.tsx
@@ -1,6 +1,5 @@
 /* eslint-disable jsx-a11y/no-static-element-interactions */
 /* eslint-disable jsx-a11y/click-events-have-key-events */
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -12,7 +11,6 @@ import { Cast, NumCast, StrCast, toList } from '../../../fields/Types';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { GestureOverlay } from '../GestureOverlay';
-import { LightboxView } from '../LightboxView';
 import { DefaultStyleProvider } from '../StyleProvider';
 import { DocumentView } from '../nodes/DocumentView';
 import { OpenWhere } from '../nodes/OpenWhere';
@@ -108,7 +106,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
             DocumentView.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.());
             // DocumentView.PinDoc(doc, { hidePresBox: true });
             this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]);
-            if (doc !== LightboxView.LightboxDoc) {
+            if (doc !== DocumentView.LightboxDoc()) {
                 this._savedState = {
                     layout_fieldKey: StrCast(doc.layout_fieldKey),
                     panX: Cast(doc.freeform_panX, 'number', null),
@@ -151,11 +149,12 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
             if (NewLightboxView._history?.lastElement().target !== target) NewLightboxView._history?.push({ doc, target });
         } else if (!target && NewLightboxView.path.length) {
             const saved = NewLightboxView._savedState;
-            if (LightboxView.LightboxDoc && saved) {
-                LightboxView.LightboxDoc._freeform_panX = saved.panX;
-                LightboxView.LightboxDoc._freeform_panY = saved.panY;
-                LightboxView.LightboxDoc._freeform_scale = saved.scale;
-                LightboxView.LightboxDoc._layout_scrollTop = saved.scrollTop;
+            const lightboxDoc = DocumentView.LightboxDoc();
+            if (lightboxDoc && saved) {
+                lightboxDoc._freeform_panX = saved.panX;
+                lightboxDoc._freeform_panY = saved.panY;
+                lightboxDoc._freeform_scale = saved.scale;
+                lightboxDoc._layout_scrollTop = saved.scrollTop;
             }
             const pop = NewLightboxView.path.pop();
             if (pop) {
@@ -176,7 +175,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
             NewLightboxView.SetNewLightboxDoc(undefined);
             return;
         }
-        const { doc, target } = NewLightboxView._history?.lastElement();
+        const { doc, target } = NewLightboxView._history?.lastElement() ?? { doc: undefined, target: undefined };
         const docView = DocumentView.getLightboxDocumentView(target || doc);
         if (docView) {
             NewLightboxView._docTarget = target;
@@ -248,14 +247,15 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
 
     @computed
     get documentView() {
-        if (!LightboxView.LightboxDoc) return null;
+        const lightboxDoc = DocumentView.LightboxDoc();
+        if (!lightboxDoc) return null;
         return (
             <GestureOverlay isActive>
                 <DocumentView
                     ref={action((r: DocumentView | null) => {
                         NewLightboxView._docView = r !== null ? r : undefined;
                     })}
-                    Document={LightboxView.LightboxDoc}
+                    Document={lightboxDoc}
                     PanelWidth={this.newLightboxWidth}
                     PanelHeight={this.newLightboxHeight}
                     LayoutTemplate={NewLightboxView.LightboxDocTemplate}
@@ -281,50 +281,14 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
     newLightboxWidth = () => this.props.PanelWidth - 420;
     newLightboxHeight = () => this.props.PanelHeight - 140;
     newLightboxScreenToLocal = () => new Transform(-this.leftBorder, -this.topBorder, 1);
-    navBtn = (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => (
-        <div
-            className="newLightboxView-navBtn-frame"
-            style={{
-                display: display(),
-                left,
-                width: bottom !== undefined ? undefined : Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]),
-                bottom,
-            }}>
-            <div className="newLightboxView-navBtn" title={color} style={{ top, color: color ? 'red' : 'white', background: color ? 'white' : undefined }} onClick={click}>
-                <div style={{ height: 10 }}>{color}</div>
-                <FontAwesomeIcon icon={icon as any} size="3x" />
-            </div>
-        </div>
-    );
 
     docFilters = () => NewLightboxView._docFilters || [];
-    @action
-    stepInto = () => {
-        NewLightboxView.path.push({
-            doc: LightboxView.LightboxDoc,
-            target: NewLightboxView._docTarget,
-            future: NewLightboxView._future,
-            history: NewLightboxView._history,
-            saved: NewLightboxView._savedState,
-        });
-        const coll = NewLightboxView._docTarget;
-        if (coll) {
-            const fieldKey = Doc.LayoutFieldKey(coll);
-            const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '_annotations'])];
-            const links = Doc.Links(coll)
-                .map(link => Doc.getOppositeAnchor(link, coll))
-                .filter(doc => doc)
-                .map(doc => doc!);
-            NewLightboxView.SetNewLightboxDoc(coll, undefined, contents.length ? contents : links);
-        }
-    };
 
-    future = () => NewLightboxView._future;
     render() {
         const newLightboxHeaderHeight = 100;
         let downx = 0;
         let downy = 0;
-        return !LightboxView.LightboxDoc ? null : (
+        return !DocumentView.LightboxDoc() ? null : (
             <div
                 className="newLightboxView-frame"
                 onPointerDown={e => {
@@ -376,7 +340,7 @@ export class NewLightboxTourBtn extends React.Component<NewLightboxTourBtnProps>
             0,
             0,
             'chevron-down',
-            () => (LightboxView.LightboxDoc /* && this.props.future()?.length */ ? '' : 'none'),
+            () => (DocumentView.LightboxDoc() /* && this.props.future()?.length */ ? '' : 'none'),
             e => {
                 e.stopPropagation();
                 this.props.stepInto();
diff --git a/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
index 1d502b73f..dc3339cd3 100644
--- a/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
+++ b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
@@ -1,3 +1,7 @@
+/* eslint-disable react/jsx-props-no-spreading */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable guard-for-in */
 import { IconButton, Size, Type } from 'browndash-components';
 import * as React from 'react';
 import { FaCaretDown, FaCaretUp } from 'react-icons/fa';
@@ -5,17 +9,15 @@ import { GrClose } from 'react-icons/gr';
 import { DocListCast, StrListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { StrCast } from '../../../../fields/Types';
-import { LightboxView } from '../../LightboxView';
 import { Colors } from '../../global/globalEnums';
+import { DocumentView } from '../../nodes/DocumentView';
 import { IBounds } from '../ExploreView/utils';
 import { NewLightboxView } from '../NewLightboxView';
 import { IRecommendation, Recommendation } from '../components';
 import { IDocRequest, fetchKeywords, fetchRecommendations } from '../utils';
 import './RecommendationList.scss';
-import { IRecommendationList } from './utils';
 
-export const RecommendationList = (props: IRecommendationList) => {
-    const { loading, keywords } = props;
+export function RecommendationList() {
     const [loadingKeywords, setLoadingKeywords] = React.useState<boolean>(true);
     const [showMore, setShowMore] = React.useState<boolean>(false);
     const [keywordsLoc, setKeywordsLoc] = React.useState<string[]>([]);
@@ -25,21 +27,22 @@ export const RecommendationList = (props: IRecommendationList) => {
 
     React.useEffect(() => {
         const getKeywords = async () => {
-            let text = StrCast(LightboxView.LightboxDoc?.text);
+            const text = StrCast(DocumentView.LightboxDoc()?.text);
             console.log('[1] fetching keywords');
             const response = await fetchKeywords(text, 5, true);
             console.log('[2] response:', response);
             const kw = response.keywords;
             console.log(kw);
             NewLightboxView.SetKeywords(kw);
-            if (LightboxView.LightboxDoc) {
+            const lightboxDoc = DocumentView.LightboxDoc();
+            if (lightboxDoc) {
                 console.log('setting keywords on doc');
-                LightboxView.LightboxDoc.keywords = new List<string>(kw);
+                lightboxDoc.keywords = new List<string>(kw);
                 setKeywordsLoc(NewLightboxView.Keywords);
             }
             setLoadingKeywords(false);
         };
-        let keywordsList = StrListCast(LightboxView.LightboxDoc!.keywords);
+        const keywordsList = StrListCast(DocumentView.LightboxDoc()!.keywords);
         if (!keywordsList || keywordsList.length < 2) {
             setLoadingKeywords(true);
             getKeywords();
@@ -57,14 +60,14 @@ export const RecommendationList = (props: IRecommendationList) => {
             console.log('fetching recommendations');
             let query = 'undefined';
             if (keywordsLoc) query = keywordsLoc.join(',');
-            let src = StrCast(NewLightboxView.LightboxDoc?.text);
-            let dashDocs: IDocRequest[] = [];
+            const src = StrCast(NewLightboxView.LightboxDoc?.text);
+            const dashDocs: IDocRequest[] = [];
             // get linked docs
-            let linkedDocs = DocListCast(NewLightboxView.LightboxDoc?.links);
+            const linkedDocs = DocListCast(NewLightboxView.LightboxDoc?.links);
             console.log('linked docs', linkedDocs);
             // get context docs (docs that are also in the collection)
-            // let contextDocs: Doc[] = DocListCast(DocCast(LightboxView.LightboxDoc?.context).data)
-            // let docId = LightboxView.LightboxDoc && LightboxView.LightboxDoc[Id]
+            // let contextDocs: Doc[] = DocListCast(DocCast(DocumentView.LightboxDoc()?.context).data)
+            // let docId = DocumentView.LightboxDoc() && DocumentView.LightboxDoc()[Id]
             // console.log("context docs", contextDocs)
             // contextDocs.forEach((doc: Doc) => {
             //     if (docId !== doc[Id]){
@@ -79,10 +82,8 @@ export const RecommendationList = (props: IRecommendationList) => {
             console.log('dash docs', dashDocs);
             if (query !== undefined) {
                 const response = await fetchRecommendations(src, query, [], true);
-                const num_recs = response.num_recommendations;
-                const recs = response.recommendations;
-                const keywords = response.keywords;
-                const response_bounds: IBounds = {
+                const theRecs = response.recommendations;
+                const responseBounds: IBounds = {
                     max_x: response.max_x,
                     max_y: response.max_y,
                     min_x: response.min_x,
@@ -93,22 +94,23 @@ export const RecommendationList = (props: IRecommendationList) => {
                 //     setKeywordsLoc(NewLightboxView.Keywords);
                 // }
                 // console.log(response_bounds)
-                NewLightboxView.SetBounds(response_bounds);
+                NewLightboxView.SetBounds(responseBounds);
                 const recommendations: IRecommendation[] = [];
-                for (const key in recs) {
+                // eslint-disable-next-line no-restricted-syntax
+                for (const key in theRecs) {
                     console.log(key);
-                    const title = recs[key].title;
-                    const url = recs[key].url;
-                    const type = recs[key].type;
-                    const text = recs[key].text;
-                    const transcript = recs[key].transcript;
-                    const previewUrl = recs[key].previewUrl;
-                    const embedding = recs[key].embedding;
-                    const distance = recs[key].distance;
-                    const source = recs[key].source;
-                    const related_concepts = recs[key].related_concepts;
-                    const docId = recs[key].doc_id;
-                    related_concepts.length >= 1 &&
+                    const { title } = theRecs[key];
+                    const { url } = theRecs[key];
+                    const { type } = theRecs[key];
+                    const { text } = theRecs[key];
+                    const { transcript } = theRecs[key];
+                    const { previewUrl } = theRecs[key];
+                    const { embedding } = theRecs[key];
+                    const { distance } = theRecs[key];
+                    const { source } = theRecs[key];
+                    const { related_concepts: relatedConcepts } = theRecs[key];
+                    const docId = theRecs[key].doc_id;
+                    relatedConcepts.length >= 1 &&
                         recommendations.push({
                             title: title,
                             data: url,
@@ -119,14 +121,15 @@ export const RecommendationList = (props: IRecommendationList) => {
                             embedding: embedding,
                             distance: Math.round(distance * 100) / 100,
                             source: source,
-                            related_concepts: related_concepts,
+                            related_concepts: relatedConcepts,
                             docId: docId,
                         });
                 }
                 recommendations.sort((a, b) => {
                     if (a.distance && b.distance) {
                         return a.distance - b.distance;
-                    } else return 0;
+                    }
+                    return 0;
                 });
                 console.log('[rec]: ', recommendations);
                 NewLightboxView.SetRecs(recommendations);
@@ -138,12 +141,12 @@ export const RecommendationList = (props: IRecommendationList) => {
 
     return (
         <div
-            className={`recommendationlist-container`}
+            className="recommendationlist-container"
             onPointerDown={e => {
                 e.stopPropagation();
             }}>
-            <div className={`header`}>
-                <div className={`title`}>Recommendations</div>
+            <div className="header">
+                <div className="title">Recommendations</div>
                 {NewLightboxView.LightboxDoc && (
                     <div style={{ fontSize: 10 }}>
                         The recommendations are produced based on the text in the document{' '}
@@ -153,65 +156,58 @@ export const RecommendationList = (props: IRecommendationList) => {
                         . The following keywords are used to fetch the recommendations.
                     </div>
                 )}
-                <div className={`lb-label`}>Keywords</div>
+                <div className="lb-label">Keywords</div>
                 {loadingKeywords ? (
-                    <div className={`keywords`}>
+                    <div className="keywords">
                         <div className={`keyword ${loadingKeywords && 'loading'}`} />
                         <div className={`keyword ${loadingKeywords && 'loading'}`} />
                         <div className={`keyword ${loadingKeywords && 'loading'}`} />
                         <div className={`keyword ${loadingKeywords && 'loading'}`} />
                     </div>
                 ) : (
-                    <div className={`keywords`}>
+                    <div className="keywords">
                         {keywordsLoc &&
-                            keywordsLoc.map((word, ind) => {
-                                return (
-                                    <div className={`keyword`}>
-                                        {word}
-                                        <IconButton
-                                            type={Type.PRIM}
-                                            size={Size.XSMALL}
-                                            color={Colors.DARK_GRAY}
-                                            icon={<GrClose />}
-                                            onClick={() => {
-                                                let kw = keywordsLoc;
-                                                kw.splice(ind);
-                                                NewLightboxView.SetKeywords(kw);
-                                            }}
-                                        />
-                                    </div>
-                                );
-                            })}
+                            keywordsLoc.map((word, ind) => (
+                                <div className="keyword">
+                                    {word}
+                                    <IconButton
+                                        type={Type.PRIM}
+                                        size={Size.XSMALL}
+                                        color={Colors.DARK_GRAY}
+                                        icon={<GrClose />}
+                                        onClick={() => {
+                                            const kw = keywordsLoc;
+                                            kw.splice(ind);
+                                            NewLightboxView.SetKeywords(kw);
+                                        }}
+                                    />
+                                </div>
+                            ))}
                     </div>
                 )}
                 {!showMore ? (
                     <div
-                        className={`lb-caret`}
+                        className="lb-caret"
                         onClick={() => {
                             setShowMore(true);
                         }}>
                         More <FaCaretDown />
                     </div>
                 ) : (
-                    <div className={`more`}>
+                    <div className="more">
                         <div
-                            className={`lb-caret`}
+                            className="lb-caret"
                             onClick={() => {
                                 setShowMore(false);
                             }}>
                             Less <FaCaretUp />
                         </div>
-                        <div className={`lb-label`}>Type</div>
-                        <div className={`lb-label`}>Sources</div>
+                        <div className="lb-label">Type</div>
+                        <div className="lb-label">Sources</div>
                     </div>
                 )}
             </div>
-            <div className={`recommendations`}>
-                {recs &&
-                    recs.map((rec: IRecommendation) => {
-                        return <Recommendation {...rec} />;
-                    })}
-            </div>
+            <div className="recommendations">{recs && recs.map((rec: IRecommendation) => <Recommendation {...rec} />)}</div>
         </div>
     );
-};
+}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 685a5aca4..62c4cc61a 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -296,9 +296,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} />
                 ) : (
                     <DocumentView
-                        parent={this}
                         // eslint-disable-next-line react/jsx-props-no-spreading
                         {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
+                        parent={this}
                         DataTransition={this.DataTransition}
                         LocalRotation={this.localRotation}
                         CollectionFreeFormDocumentView={this.returnThis}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 474d54119..e1d16549c 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -13,7 +13,7 @@ import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { undoBatch } from '../../util/UndoManager';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
 import './ComparisonBox.scss';
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 15187b4e4..9ca63194c 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -18,7 +18,7 @@ import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
 import { UndoManager, undoable } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
 import { MarqueeAnnotator } from '../../MarqueeAnnotator';
 import { PinProps } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
@@ -41,7 +41,7 @@ export enum DataVizView {
 }
 
 @observer
-export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
+export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _marqueeref = React.createRef<MarqueeAnnotator>();
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index ec9db8480..18529a429 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -2,7 +2,6 @@
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import JsxParser from 'react-jsx-parser';
 import * as XRegExp from 'xregexp';
 import { OmitKeys } from '../../../ClientUtils';
 import { Without, emptyPath } from '../../../Utils';
@@ -11,56 +10,15 @@ import { AclPrivate, DocData } from '../../../fields/DocSymbols';
 import { ScriptField } from '../../../fields/ScriptField';
 import { Cast, DocCast, StrCast } from '../../../fields/Types';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
-import { InkingStroke } from '../InkingStroke';
-import { ObservableReactComponent } from '../ObservableReactComponent';
-import { CollectionCalendarView } from '../collections/CollectionCalendarView';
-import { CollectionDockingView } from '../collections/CollectionDockingView';
-import { CollectionView } from '../collections/CollectionView';
-import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView';
-import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox';
-import { SearchBox } from '../search/SearchBox';
-import { AudioBox } from './AudioBox';
-import { ComparisonBox } from './ComparisonBox';
-import { DataVizBox } from './DataVizBox/DataVizBox';
+import { ObservableReactComponent, ObserverJsxParser } from '../ObservableReactComponent';
 import './DocumentView.scss';
-import { EquationBox } from './EquationBox';
-import { FieldView, FieldViewProps } from './FieldView';
-import { FontIconBox } from './FontIconBox/FontIconBox';
-import { FunctionPlotBox } from './FunctionPlotBox';
-import { ImageBox } from './ImageBox';
-import { KeyValueBox } from './KeyValueBox';
-import { LabelBox } from './LabelBox';
-import { LinkBox } from './LinkBox';
-import { LoadingBox } from './LoadingBox';
-import { MapBox } from './MapBox/MapBox';
-import { MapPushpinBox } from './MapBox/MapPushpinBox';
-import { PDFBox } from './PDFBox';
-import { PhysicsSimulationBox } from './PhysicsBox/PhysicsSimulationBox';
-import { RecordingBox } from './RecordingBox';
-import { ScreenshotBox } from './ScreenshotBox';
-import { ScriptingBox } from './ScriptingBox';
-import { VideoBox } from './VideoBox';
-import { WebBox } from './WebBox';
-import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { ImportElementBox } from './importBox/ImportElementBox';
-import { PresBox } from './trails/PresBox';
-import { PresElementBox } from './trails/PresElementBox';
+import { FieldViewProps } from './FieldView';
 
 type BindingProps = Without<FieldViewProps, 'fieldKey'>;
 export interface JsxBindings {
     props: BindingProps;
 }
 
-class ObserverJsxParser1 extends JsxParser {
-    constructor(props: any) {
-        super(props);
-        observer(this as any);
-    }
-}
-
-export const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
-
 interface HTMLtagProps {
     Document: Doc;
     htmltag: string;
@@ -116,6 +74,15 @@ export interface DocumentContentsViewProps extends FieldViewProps {
 }
 @observer
 export class DocumentContentsView extends ObservableReactComponent<DocumentContentsViewProps> {
+    private static DefaultLayoutString: string;
+    /**
+     * Set of all available rendering componets for Docs (e.g., ImageBox, CollectionFreeFormView, etc)
+     */
+    private static Components: { [key: string]: any };
+    public static Init(defaultLayoutString: string, components:{ [key: string]: any}) {
+        DocumentContentsView.DefaultLayoutString = defaultLayoutString;
+        DocumentContentsView.Components = components;
+    }
     constructor(props: any) {
         super(props);
         makeObservable(this);
@@ -124,11 +91,11 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
         TraceMobx();
         if (this._props.LayoutTemplateString) return this._props.LayoutTemplateString;
         if (!this.layoutDoc) return '<p>awaiting layout</p>';
-        if (this._props.layoutFieldKey === 'layout_keyValue') return StrCast(this._props.Document.layout_keyValue, KeyValueBox.LayoutString());
+        if (this._props.layoutFieldKey === 'layout_keyValue') return StrCast(this._props.Document.layout_keyValue, DocumentContentsView.DefaultLayoutString);
         const tempLayout = DocCast(this.layoutDoc[this.layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')]);
         const layoutDoc = tempLayout ?? this.layoutDoc;
         const layout = Cast(layoutDoc[layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(layoutDoc.layout_fieldKey, 'layout')], 'string');
-        if (layout === undefined) return this._props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString();
+        if (layout === undefined) return this._props.Document.data ? "<FieldView {...props} fieldKey='data' />" : DocumentContentsView.DefaultLayoutString;
         if (typeof layout === 'string') return layout;
         return '<p>Loading layout</p>';
     }
@@ -223,42 +190,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
                 key={42}
                 blacklistedAttrs={emptyPath}
                 renderInWrapper={false}
-                components={{
-                    FormattedTextBox,
-                    ImageBox,
-                    FontIconBox,
-                    LabelBox,
-                    EquationBox,
-                    FieldView,
-                    CollectionFreeFormView,
-                    CollectionDockingView,
-                    CollectionSchemaView,
-                    CollectionCalendarView,
-                    CollectionView,
-                    WebBox,
-                    KeyValueBox,
-                    PDFBox,
-                    VideoBox,
-                    AudioBox,
-                    RecordingBox,
-                    PresBox,
-                    PresElementBox,
-                    SearchBox,
-                    FunctionPlotBox,
-                    InkingStroke,
-                    LinkBox,
-                    ScriptingBox,
-                    MapBox,
-                    ScreenshotBox,
-                    DataVizBox,
-                    HTMLtag,
-                    ComparisonBox,
-                    LoadingBox,
-                    PhysicsSimulationBox,
-                    SchemaRowBox,
-                    ImportElementBox,
-                    MapPushpinBox,
-                }}
+                components={DocumentContentsView.Components}
                 bindings={bindings}
                 jsx={layoutFrame}
                 showWarnings
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index dc597e5ff..8df28a770 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -8,7 +8,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal';
 import { ClientUtils, DivWidth, isTargetChildOf as isParentOf, lightOrDark, returnFalse, returnVal, simulateMouseClick } from '../../../ClientUtils';
-import { Utils, emptyFunction, emptyPath } from '../../../Utils';
+import { Utils, emptyFunction } from '../../../Utils';
 import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc';
 import { AclAdmin, AclEdit, AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -35,20 +35,21 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager, undoBatch, undoable } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { DocComponent, ViewBoxInterface } from '../DocComponent';
+import { DocComponent } from '../DocComponent';
 import { EditableView } from '../EditableView';
 import { FieldsDropdown } from '../FieldsDropdown';
-import { LightboxView } from '../LightboxView';
+import { ObserverJsxParser } from '../ObservableReactComponent';
 import { PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
-import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
+import { ViewBoxInterface } from '../ViewBoxInterface';
+import { DocumentContentsView } from './DocumentContentsView';
 import { DocumentLinksButton } from './DocumentLinksButton';
 import './DocumentView.scss';
 import { FieldViewProps, FieldViewSharedProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
-import { OpenWhere } from './OpenWhere';
+import { OpenWhere, OpenWhereMod } from './OpenWhere';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { PresEffect, PresEffectDirection } from './trails';
+import { PresEffect, PresEffectDirection } from './trails/PresEnums';
 
 export interface DocumentViewProps extends FieldViewSharedProps {
     hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
@@ -77,7 +78,7 @@ export interface DocumentViewProps extends FieldViewSharedProps {
     dragStarting?: () => void;
     dragEnding?: () => void;
 
-    parent?: any;
+    parent?: any; // parent React component view (see CollectionFreeFormDocumentView)
 }
 @observer
 export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps>() {
@@ -320,7 +321,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 if (this.onDoubleClickHandler?.script) {
                     UndoManager.RunInBatch(() => this.onDoubleClickHandler.script.run(scriptProps, console.log).result?.select && this._props.select(false), 'on double click: ' + this.Document.title);
                 } else if (!Doc.IsSystem(this.Document) && defaultDblclick !== 'ignore') {
-                    UndoManager.RunInBatch(() => LightboxView.Instance.AddDocTab(this.Document, OpenWhere.lightbox), 'double tap');
+                    UndoManager.RunInBatch(() => this._props.addDocTab(this.Document, OpenWhere.lightbox), 'double tap');
                     DocumentView.DeselectAll();
                     Doc.UnBrushDoc(this.Document);
                 } else {
@@ -561,7 +562,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];
 
             if (this._props.renderDepth === 0) {
-                appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => LightboxView.Instance.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
+                appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'eye' });
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
@@ -980,6 +981,29 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 @observer
 export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static ROOT_DIV = 'documentView-effectsWrapper';
+    public static addSplit: (Doc: Doc, where: OpenWhereMod) => void;
+    // Lightbox
+    public static _lightboxDoc: () => Doc | undefined;
+    public static _lightboxContains: (view?: DocumentView) => boolean | undefined;
+    public static _setLightboxDoc: (doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) => boolean;
+    /**
+     * @returns The Doc, if any, being displayed in the lightbox
+     */
+    public static readonly LightboxDoc = () => DocumentView._lightboxDoc?.();
+    /**
+     * @param view
+     * @returns whether 'view' is anywhere in the rendering hierarchy of the lightbox
+     */
+    public static readonly LightboxContains = (view?: DocumentView) => DocumentView._lightboxContains?.(view);
+    /**
+     * Sets the root Doc to render in the lightbox view.
+     * @param doc
+     * @param target a Doc within 'doc' to focus on (useful for freeform collections)
+     * @param future a list of Docs to step through with the arrow buttons of the lightbox
+     * @param layoutTemplate a template to apply to 'doc' to render it.
+     * @returns success flag which is currently always true
+     */
+    public static readonly SetLightboxDoc = (doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) => DocumentView._setLightboxDoc(doc, target, future, layoutTemplate);
     // Sharing Manager
     public static ShareOpen: (target?: DocumentView, targetDoc?: Doc) => void;
     // LinkFollower
@@ -998,6 +1022,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static addView: (dv: DocumentView) => void | undefined;
     public static removeView: (dv: DocumentView) => void | undefined;
     public static addViewRenderedCb: (doc: Opt<Doc>, func: (dv: DocumentView) => any) => boolean;
+    public static getViews = (doc?: Doc) => Array.from(doc?.[DocViews] ?? []) as DocumentView[];
     public static getFirstDocumentView: (toFind: Doc) => DocumentView | undefined;
     public static getDocumentView: (target: Doc | undefined, preferredCollection?: DocumentView) => Opt<DocumentView>;
     public static getContextPath: (doc: Opt<Doc>, includeExistingViews?: boolean) => Doc[];
@@ -1032,8 +1057,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static UniquifyId(inLightbox: boolean | undefined, id: string) {
         return (inLightbox ? 'lightbox-' : '') + id;
     }
-    public ViewGuid = DocumentView.UniquifyId(LightboxView.Contains(this), Utils.GenerateGuid()); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations.
-    public DocUniqueId = DocumentView.UniquifyId(LightboxView.Contains(this), this.Document[Id]);
+    public ViewGuid = DocumentView.UniquifyId(DocumentView.LightboxContains(this), Utils.GenerateGuid()); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations.
+    public DocUniqueId = DocumentView.UniquifyId(DocumentView.LightboxContains(this), this.Document[Id]);
 
     constructor(props: DocumentViewProps) {
         super(props);
@@ -1401,6 +1426,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                         }}>
                         <DocumentViewInternal
                             {...this._props}
+                            parent={undefined}
                             fieldKey={this.LayoutFieldKey}
                             DataTransition={this.DataTransition}
                             DocumentView={this.selfView}
@@ -1451,58 +1477,52 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         const docId = ClientUtils.CurrentUserEmail() + Doc.GetProto(linkAnchor)[Id] + '-pivotish';
         // prettier-ignore
         DocServer.GetRefField(docId).then(docx =>
-            LightboxView.Instance.SetLightboxDoc(
+            DocumentView.SetLightboxDoc(
                 (docx as Doc) ?? // reuse existing pivot view of documents, or else create a new collection
                 Docs.Create.StackingDocument([], { title: linkAnchor.title + '-pivot', _width: 500, _height: 500, target: linkAnchor, onViewMounted: ScriptField.MakeScript('updateLinkCollection(this, this.target)') }, docId)
             )
         );
     }
-}
-
-export function returnEmptyDocViewList() {
-    return emptyPath;
-}
-
-// eslint-disable-next-line default-param-last
-export function DocFocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) {
-    let doc = docIn;
-    const options = optionsIn;
-    const func = () => {
-        const cv = DocumentView.getDocumentView(containingDoc);
-        const dv = DocumentView.getDocumentView(doc, cv);
-        if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) {
-            DocumentView.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document));
-        } else {
-            const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc));
-            const showDoc = !Doc.IsSystem(container) && !cv ? container : doc;
-            options.toggleTarget = undefined;
-            DocumentView.showDocument(showDoc, options, () => DocumentView.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
-                const cvFound = DocumentView.getDocumentView(containingDoc);
-                const dvFound = DocumentView.getDocumentView(doc, cvFound);
-                dvFound && Doc.linkFollowHighlight(dvFound.Document);
-            });
+    // eslint-disable-next-line default-param-last
+    public static FocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) {
+        let doc = docIn;
+        const options = optionsIn;
+        const func = () => {
+            const cv = DocumentView.getDocumentView(containingDoc);
+            const dv = DocumentView.getDocumentView(doc, cv);
+            if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) {
+                DocumentView.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document));
+            } else {
+                const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc));
+                const showDoc = !Doc.IsSystem(container) && !cv ? container : doc;
+                options.toggleTarget = undefined;
+                DocumentView.showDocument(showDoc, options, () => DocumentView.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
+                    const cvFound = DocumentView.getDocumentView(containingDoc);
+                    const dvFound = DocumentView.getDocumentView(doc, cvFound);
+                    dvFound && Doc.linkFollowHighlight(dvFound.Document);
+                });
+            }
+        };
+        if (Doc.IsDataProto(doc) && Doc.GetEmbeddings(doc).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) {
+            doc = Doc.GetEmbeddings(doc).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!;
         }
-    };
-    if (Doc.IsDataProto(doc) && Doc.GetEmbeddings(doc).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) {
-        doc = Doc.GetEmbeddings(doc).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!;
+        if (doc.hidden) {
+            doc.hidden = false;
+            options.toggleTarget = false;
+            setTimeout(func);
+        } else func();
     }
-    if (doc.hidden) {
-        doc.hidden = false;
-        options.toggleTarget = false;
-        setTimeout(func);
-    } else func();
 }
-ScriptingGlobals.add(DocFocusOrOpen);
 
 // eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
-    documentView.iconify();
-    documentView.select(false);
+ScriptingGlobals.add(function DocFocusOrOpen(docIn: Doc, optionsIn?: FocusViewOptions, containingDoc?: Doc) {
+    return DocumentView.FocusOrOpen(docIn, optionsIn, containingDoc);
 });
 
 // eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) {
-    LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); // , 0);
+ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
+    documentView.iconify();
+    documentView.select(false);
 });
 
 // eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 32d08fbe7..1f5c9b84b 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -11,7 +11,7 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
-import { LightboxView } from '../LightboxView';
+import { DocumentView } from './DocumentView';
 import './EquationBox.scss';
 import { FieldView, FieldViewProps } from './FieldView';
 import EquationEditor from './formattedText/EquationEditor';
@@ -30,7 +30,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        if (Doc.SelectOnLoad === this.Document && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()))) {
+        if (Doc.SelectOnLoad === this.Document && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) {
             this._props.select(false);
 
             this._ref.current!.mathField.focus();
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 66b134889..3f351a072 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -10,8 +10,8 @@ import { ScriptField } from '../../../fields/ScriptField';
 import { WebField } from '../../../fields/URLField';
 import { dropActionType } from '../../util/DropActionTypes';
 import { Transform } from '../../util/Transform';
-import { ViewBoxInterface } from '../DocComponent';
 import { PinProps } from '../PinFuncs';
+import { ViewBoxInterface } from '../ViewBoxInterface';
 import { DocumentView } from './DocumentView';
 import { FocusViewOptions } from './FocusViewOptions';
 import { OpenWhere } from './OpenWhere';
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 397cc15ed..0956be3e9 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -13,8 +13,7 @@ import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { DefaultStyleProvider } from '../StyleProvider';
-import { returnEmptyDocViewList } from './DocumentView';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
 import './KeyValueBox.scss';
 import './KeyValuePair.scss';
 import { OpenWhere } from './OpenWhere';
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 6caa38a7f..8d6ae9f73 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -16,7 +16,6 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { SnappingManager } from '../../util/SnappingManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { EditableView } from '../EditableView';
-import { LightboxView } from '../LightboxView';
 import { StyleProp } from '../StyleProp';
 import { ComparisonBox } from './ComparisonBox';
 import { DocumentView } from './DocumentView';
@@ -52,7 +51,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
     componentDidMount() {
         this._props.setContentViewBox?.(this);
         this._disposers.deleting = reaction(
-            () => !this.anchor1 && !this.anchor2 && this.DocumentView?.() && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView!())),
+            () => !this.anchor1 && !this.anchor2 && this.DocumentView?.() && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView!())),
             empty => {
                 if (empty) {
                     this._hackToSeeIfDeleted = setTimeout(() => {
@@ -110,7 +109,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const getAnchor = (field: FieldResult): Element[] => {
                 const docField = DocCast(field);
                 const doc = docField?.layout_unrendered ? DocCast(docField.annotationOn, docField) : docField;
-                const ele = document.getElementById(DocumentView.UniquifyId(LightboxView.Contains(this.DocumentView?.()), doc[Id]));
+                const ele = document.getElementById(DocumentView.UniquifyId(DocumentView.LightboxContains(this.DocumentView?.()), doc[Id]));
                 if (ele?.className === 'linkBox-label') foundParent = true;
                 if (ele?.getBoundingClientRect().width) return [ele];
                 const eles = Array.from(document.getElementsByClassName(doc[Id])).filter(el => el?.getBoundingClientRect().width);
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index ac8010f11..d7687e03e 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -23,7 +23,7 @@ import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { UndoManager, undoable } from '../../../util/UndoManager';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
 import { PinDocView, PinProps } from '../../PinFuncs';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
@@ -63,7 +63,7 @@ type PopupInfo = {
 };
 
 @observer
-export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
+export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(MapBox, fieldKey);
     }
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index e46e40bfe..07381c7d0 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -50,9 +50,9 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.result = info;
         this.dataDoc.type = DocumentType.VID;
 
-        this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
+        this.dataDoc[this.fieldKey + '_recorded'] = this.dataDoc.layout; // save the recording layout to allow re-recording later
+        this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey); // then convert the recording box to a video
         this.dataDoc[this._props.fieldKey] = new VideoField(this.result.accessPaths.client);
-        this.dataDoc[this.fieldKey + '_recorded'] = true;
         // stringify the presentation and store it
         if (presentation?.movements) {
             const presCopy = { ...presentation };
@@ -143,18 +143,13 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     public static resumeWorkspaceReplaying(doc: Doc) {
         const docView = DocumentView.getDocumentView(doc);
-        if (docView?.ComponentView instanceof VideoBox) {
-            docView.ComponentView.Play();
-        }
+        docView?.ComponentView?.Play?.();
         Doc.UserDoc().workspaceReplayingState = mediaState.Playing;
     }
 
     public static pauseWorkspaceReplaying(doc: Doc) {
         const docView = DocumentView.getDocumentView(doc);
-        const videoBox = docView?.ComponentView as VideoBox;
-        if (videoBox) {
-            videoBox.Pause();
-        }
+        docView?.ComponentView?.Pause?.();
         Doc.UserDoc().workspaceReplayingState = mediaState.Paused;
     }
 
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 3b1815f8a..fe7600fa3 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -17,7 +17,6 @@ import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
 import { dropActionType } from '../../util/DropActionTypes';
-import { ReplayMovements } from '../../util/ReplayMovements';
 import { undoBatch } from '../../util/UndoManager';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
@@ -32,7 +31,6 @@ import { StyleProp } from '../StyleProp';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
-import { RecordingBox } from './RecordingBox';
 import './VideoBox.scss';
 
 /**
@@ -101,18 +99,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return field?.url.href ?? vfield?.url.href ?? '';
     }
 
-    // returns the presentation data if it exists, null otherwise
-    @computed get presentation() {
-        const data = this.dataDoc[this.fieldKey + '_presentation'];
-        return data ? JSON.parse(StrCast(data)) : null;
-    }
-
     @computed private get timeline() {
         return this._stackedTimeline;
     }
     private get transition() {
         return this._clicking ? 'left 0.5s, width 0.5s, height 0.5s' : '';
     } // css transition for hiding/showing timeline
+
     public get player(): HTMLVideoElement | null {
         return this._videoRef;
     }
@@ -122,10 +115,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         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.
         this.player && this.setPlayheadTime(this.timeline?.clipStart || 0);
         document.addEventListener('keydown', this.keyEvents, true);
-
-        if (this.presentation) {
-            ReplayMovements.Instance.setVideoBox(this);
-        }
     }
 
     componentWillUnmount() {
@@ -134,12 +123,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this.Pause();
         Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
         document.removeEventListener('keydown', this.keyEvents, true);
-
-        if (this.presentation) {
-            ReplayMovements.Instance.removeVideoBox();
-        }
     }
 
+    override PlayerTime = () => this.player?.currentTime;
+    override Pause = (update: boolean = true) => {
+        this.pause(update);
+        !this._keepCurrentlyPlaying && this.removeCurrentlyPlaying();
+    };
+
     // handles key events, when timeline scrubs fade controls
     @action
     keyEvents = (e: KeyboardEvent) => {
@@ -230,10 +221,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
         this._playRegionTimer = undefined;
     };
-    @action Pause = (update: boolean = true) => {
-        this.pause(update);
-        !this._keepCurrentlyPlaying && this.removeCurrentlyPlaying();
-    };
 
     // toggles video full screen
     @action public FullScreen = () => {
@@ -518,11 +505,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 icon: 'expand-arrows-alt',
             });
             // if the videobox was turned from a recording box
-            if (this.dataDoc[this.fieldKey + '_recorded'] === true) {
+            if (this.dataDoc[this.fieldKey + '_recorded']) {
                 subitems.push({
                     description: 'Recreate recording',
                     event: () => {
-                        this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
+                        this.dataDoc.layout = StrCast(this.dataDoc[this.fieldKey + '_recorded']); // restore the saed recording layout
                         // delete assoicated video data
                         this.dataDoc[this._props.fieldKey] = '';
                         this.dataDoc[this.fieldKey + '_duration'] = '';
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index b6ef36974..8835ea5e7 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -28,9 +28,8 @@ import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { Colors } from '../global/globalEnums';
-import { LightboxView } from '../LightboxView';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { Annotation } from '../pdf/Annotation';
@@ -38,6 +37,7 @@ import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { StyleProp } from '../StyleProp';
+import { ViewBoxInterface } from '../ViewBoxInterface';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
@@ -623,7 +623,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             clearStyleSheetRules(WebBox.webStyleSheet);
             this._scrollTimer = undefined;
             const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop;
-            if (!LinkInfo.Instance?.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()))) {
+            if (!LinkInfo.Instance?.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) {
                 this.layoutDoc.thumb = undefined;
                 this.layoutDoc.thumbScrollTop = undefined;
                 this.layoutDoc.thumbNativeWidth = undefined;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 321fdbb91..e354aedb7 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable no-use-before-define */
 /* eslint-disable jsx-a11y/no-static-element-interactions */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -10,7 +11,7 @@ import { inputRules } from 'prosemirror-inputrules';
 import { keymap } from 'prosemirror-keymap';
 import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
 import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transaction } from 'prosemirror-state';
-import { EditorView } from 'prosemirror-view';
+import { EditorView, NodeViewConstructor } from 'prosemirror-view';
 import * as React from 'react';
 import { BsMarkdownFill } from 'react-icons/bs';
 import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, StopEvent } from '../../../../ClientUtils';
@@ -45,7 +46,6 @@ import { ContextMenu } from '../../ContextMenu';
 import { ContextMenuProps } from '../../ContextMenuItem';
 import { ViewBoxAnnotatableComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
-import { LightboxView } from '../../LightboxView';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup';
 import { PinDocView, PinProps } from '../../PinFuncs';
@@ -58,11 +58,6 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { FocusViewOptions } from '../FocusViewOptions';
 import { LinkInfo } from '../LinkDocPreview';
 import { OpenWhere } from '../OpenWhere';
-import { DashDocCommentView } from './DashDocCommentView';
-import { DashDocView } from './DashDocView';
-import { DashFieldView } from './DashFieldView';
-import { EquationView } from './EquationView';
-import { FootnoteView } from './FootnoteView';
 import './FormattedTextBox.scss';
 import { findLinkMark, FormattedTextBoxComment } from './FormattedTextBoxComment';
 import { buildKeymap, updateBullets } from './ProsemirrorExampleTransfer';
@@ -70,7 +65,6 @@ import { removeMarkWithAttrs } from './prosemirrorPatches';
 import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
 import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
-import { SummaryView } from './SummaryView';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 
 export interface FormattedTextBoxProps extends FieldViewProps {
@@ -82,10 +76,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     public static LayoutString(fieldStr: string) {
         return FieldView.LayoutString(FormattedTextBox, fieldStr);
     }
-    public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
-    // eslint-disable-next-line no-use-before-define
-    public static Instance: FormattedTextBox;
-    public static LiveTextUndo: UndoManager.Batch | undefined;
+    private static nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor };
+    /**
+     * Initialize the class with all the plugin node view components
+     * @param nodeViews prosemirror plugins that render a custom UI for specific node types
+     */
+    public static Init(nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }) {
+        FormattedTextBox.nodeViews = nodeViews;
+    }
+    public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
     static _globalHighlightsCache: string = '';
     static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']);
     static _highlightStyleSheet: any = addStyleSheet();
@@ -189,7 +188,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     constructor(props: FormattedTextBoxProps) {
         super(props);
         makeObservable(this);
-        FormattedTextBox.Instance = this;
         this._recordingStart = Date.now();
     }
 
@@ -1435,14 +1433,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     return true;
                 },
                 dispatchTransaction: this.dispatchTransaction,
-                nodeViews: {
-                    dashComment(node: any, view: any, getPos: any) { return new DashDocCommentView(node, view, getPos); }, // prettier-ignore
-                    dashDoc(node: any, view: any, getPos: any)     { return new DashDocView(node, view, getPos, self); }, // prettier-ignore
-                    dashField(node: any, view: any, getPos: any)   { return new DashFieldView(node, view, getPos, self); }, // prettier-ignore
-                    equation(node: any, view: any, getPos: any)    { return new EquationView(node, view, getPos, self); }, // prettier-ignore
-                    summary(node: any, view: any, getPos: any)     { return new SummaryView(node, view, getPos); }, // prettier-ignore
-                    footnote(node: any, view: any, getPos: any)    { return new FootnoteView(node, view, getPos); }, // prettier-ignore
-                },
+                nodeViews: FormattedTextBox.nodeViews(this),
                 clipboardTextSerializer: this.clipboardTextSerializer,
                 handlePaste: this.handlePaste,
             });
@@ -1463,7 +1454,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             (this._editorView as any).TextView = this;
         }
 
-        const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, Doc.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()));
+        const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, Doc.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()));
         const selLoadChar = FormattedTextBox.SelectOnLoadChar;
         if (selectOnLoad) {
             Doc.SetSelectOnLoad(undefined);
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 6b4f5e073..75492d2f9 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -24,14 +24,12 @@ import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { SerializationHelper } from '../../../util/SerializationHelper';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
-import { CollectionDockingView } from '../../collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
 import { CollectionFreeFormPannableContents } from '../../collections/collectionFreeForm/CollectionFreeFormPannableContents';
 import { CollectionView } from '../../collections/CollectionView';
 import { TreeView } from '../../collections/TreeView';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
-import { LightboxView } from '../../LightboxView';
 import { pinDataTypes as dataTypes } from '../../PinFuncs';
 import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
@@ -46,6 +44,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(PresBox, fieldKey);
     }
+    private static _getTabDocs: () => Doc[];
+    public static Init(tabDocs: () => Doc[]) {
+        PresBox._getTabDocs = tabDocs;
+    }
     static navigateToDocScript: ScriptField;
 
     constructor(props: FieldViewProps) {
@@ -679,16 +681,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (activeItem.presentation_openInLightbox) {
             const context = DocCast(targetDoc.annotationOn) ?? targetDoc;
             if (!DocumentView.getLightboxDocumentView(context)) {
-                LightboxView.Instance.SetLightboxDoc(context);
+                DocumentView.SetLightboxDoc(context);
             }
         }
         if (targetDoc) {
             if (activeItem.presentation_targetDoc instanceof Doc) activeItem.presentation_targetDoc[Animation] = undefined;
 
-            DocumentView.addViewRenderedCb(LightboxView.LightboxDoc, () => {
+            DocumentView.addViewRenderedCb(DocumentView.LightboxDoc(), () => {
                 // if target or the doc it annotates is not in the lightbox, then close the lightbox
                 if (!DocumentView.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) {
-                    LightboxView.Instance.SetLightboxDoc(undefined);
+                    DocumentView.SetLightboxDoc(undefined);
                 }
                 DocumentView.showDocument(targetDoc, options, finished);
             });
@@ -789,7 +791,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         default:
                     }
                 });
-            LightboxView.Instance.SetLightboxDoc(undefined);
+            DocumentView.SetLightboxDoc(undefined);
             Doc.RemFromMyOverlay(this.Document);
             return PresStatus.Edit;
         };
@@ -892,7 +894,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     exitMinimize = () => {
         if (Doc.IsInMyOverlay(this.layoutDoc)) {
             Doc.RemFromMyOverlay(this.Document);
-            CollectionDockingView.AddSplit(this.Document, OpenWhereMod.right);
+            DocumentView.addSplit(this.Document, OpenWhereMod.right);
         }
         return PresStatus.Edit;
     };
@@ -2202,9 +2204,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (freeform && layout) doc = this.createTemplate(layout, title);
         if (!freeform && !layout) doc = Docs.Create.TextDocument('', { _nativeWidth: 400, _width: 225, title: title });
         if (doc) {
-            const tabMap = CollectionDockingView.Instance?.tabMap;
-            const docTab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc.type === DocumentType.COL)?.DashDoc;
-            const presCollection = DocumentView.getContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? docTab;
+            const docTab = PresBox._getTabDocs().find(tdoc => tdoc.type === DocumentType.COL);
+            const presCollection = DocCast(DocumentView.getContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc, docTab);
             const data = Cast(presCollection?.data, listSpec(Doc));
             const configData = Cast(this.Document.data, listSpec(Doc));
             if (data && configData) {
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 56552c952..ae0838dd5 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -18,8 +18,8 @@ import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { CollectionDockingView } from '../collections/CollectionDockingView';
-import { IRecommendation, Recommendation } from '../newlightbox/components';
-import { fetchRecommendations } from '../newlightbox/utils';
+// import { IRecommendation, Recommendation } from '../newlightbox/components';
+// import { fetchRecommendations } from '../newlightbox/utils';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import './SearchBox.scss';
@@ -142,7 +142,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
     @observable _searchString = '';
     @observable _docTypeString = 'all';
     @observable _results: Map<Doc, string[]> = new Map<Doc, string[]>();
-    @observable _recommendations: IRecommendation[] = [];
+    // @observable _recommendations: IRecommendation[] = [];
     @observable _pageRanks: Map<Doc, number> = new Map<Doc, number>();
     @observable _linkedDocsOut: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>();
     @observable _linkedDocsIn: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>();
@@ -375,29 +375,29 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
 
         if (query) {
             this.searchCollection(query);
-            const response = await fetchRecommendations('', query, [], true);
-            const recs = response.recommendations as any[];
-            const recommendations: IRecommendation[] = [];
-            recs.forEach(rec => {
-                const { title, url, type, text, transcript, previewUrl, embedding, distance, source, related_concepts: relatedConcepts, doc_id: docId } = rec;
-                recommendations.push({
-                    title,
-                    data: url,
-                    type,
-                    text,
-                    transcript,
-                    previewUrl,
-                    embedding,
-                    distance: Math.round(distance * 100) / 100,
-                    source: source,
-                    related_concepts: relatedConcepts,
-                    docId,
-                });
-            });
-            const setRecommendations = action(() => {
-                this._recommendations = recommendations;
-            });
-            setRecommendations();
+            // const response = await fetchRecommendations('', query, [], true);
+            // const recs = response.recommendations as any[];
+            // const recommendations: IRecommendation[] = [];
+            // recs.forEach(rec => {
+            //     const { title, url, type, text, transcript, previewUrl, embedding, distance, source, related_concepts: relatedConcepts, doc_id: docId } = rec;
+            //     recommendations.push({
+            //         title,
+            //         data: url,
+            //         type,
+            //         text,
+            //         transcript,
+            //         previewUrl,
+            //         embedding,
+            //         distance: Math.round(distance * 100) / 100,
+            //         source: source,
+            //         related_concepts: relatedConcepts,
+            //         docId,
+            //     });
+            // });
+            // const setRecommendations = action(() => {
+            //     this._recommendations = recommendations;
+            // });
+            // setRecommendations();
         }
     };
 
@@ -470,7 +470,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
         });
 
         // eslint-disable-next-line react/jsx-props-no-spreading
-        const recommendationsJSX: JSX.Element[] = this._recommendations.map(props => <Recommendation {...props} />);
+        const recommendationsJSX: JSX.Element[] = []; // this._recommendations.map(props => <Recommendation {...props} />);
 
         return (
             <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}>
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 9f97efcce..e558e14e3 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -23,9 +23,9 @@ import { CollectionDockingView } from '../collections/CollectionDockingView';
 import { CollectionLinearView } from '../collections/collectionLinear';
 import { DashboardView } from '../DashboardView';
 import { Colors } from '../global/globalEnums';
-import { DocumentView, DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { DefaultStyleProvider } from '../StyleProvider';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
 import './TopBar.scss';
 
 /**
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 725221a66..fe044c035 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -102,7 +102,7 @@ export namespace Field {
     export function Copy(field: any) {
         return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field;
     }
-    UndoManager.SetFieldPrinter(toJavascriptString);
+    UndoManager.SetFieldPrinter(toString);
 }
 export type FieldType = number | string | boolean | ObjectField | RefField;
 export type Opt<T> = T | undefined;
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index c4f5f7a24..46704eb2b 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -89,6 +89,17 @@ export class InkField extends ObjectField {
     [ToString]() {
         return 'InkField';
     }
+
+    public static getBounds(stroke: InkData, pad?: boolean) {
+        const padding = pad ? [-20000, 20000] : [];
+        const xs = [...padding, ...stroke.map(p => p.X)];
+        const ys = [...padding, ...stroke.map(p => p.Y)];
+        const right = Math.max(...xs);
+        const left = Math.min(...xs);
+        const bottom = Math.max(...ys);
+        const top = Math.min(...ys);
+        return { right, left, bottom, top, width: right - left, height: bottom - top };
+    }
 }
 
 ScriptingGlobals.add('InkField', InkField);
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index 4f8058ce4..83b5672b3 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -45,7 +45,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
         return Field.toScriptString(this[ToValue]()?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ?
     }
     [ToString]() {
-        return 'ProxyField';
+        return Field.toString(this[ToValue]()?.value);
     }
 
     @serializable(primitive())
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index 5eb60a2f8..3763dcd2c 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -164,7 +164,7 @@ export namespace RichTextUtils {
             const inlineObjectMap = await parseInlineObjects(document);
             const title = document.title!;
             const { text, paragraphs } = GoogleApiClientUtils.Docs.Utils.extractText(document);
-            let state = FormattedTextBox.blankState();
+            let state = EditorState.create(new FormattedTextBox({} as any).config);
             const structured = parseLists(paragraphs);
 
             let position = 3;
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 9361430cb..a6499c3e3 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -443,7 +443,7 @@ export function containedFieldChangedHandler(container: List<FieldType> | Doc, p
                             });
                             lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
                         },
-                        prop: 'remove ' + (diff.items?.length ?? 0) + ' items from list',
+                        prop: 'remove ' + (diff.items?.length ?? 0) + ' items from list(' + ((container as any)?.title ?? '') + ':' + prop + ')',
                     },
                     diff?.items
                 );
-- 
cgit v1.2.3-70-g09d2

