From 6900008b1bb89cca1eab7b95f17ee33fa335282f Mon Sep 17 00:00:00 2001
From: sharkiecodes <lanyi_stroud@brown.edu>
Date: Mon, 28 Apr 2025 20:18:26 -0400
Subject: adding autotagging

---
 src/client/views/nodes/ImageBox.tsx | 7 +++++++
 1 file changed, 7 insertions(+)

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

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 999d7089b..38f0390db 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -119,6 +119,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._dropDisposer?.();
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document));
     };
+
+    autoTag = () => {
+        //Doc.getDescription(this.Document).then(desc => this.desc = desc)
+    }
+
+
+
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const visibleAnchor = this._getAnchor?.(this._savedAnnotations, true); // use marquee anchor, otherwise, save zoom/pan as anchor
         const anchor =
-- 
cgit v1.2.3-70-g09d2


From 2a36216359054532084be24ab9034cd08c1c8798 Mon Sep 17 00:00:00 2001
From: sharkiecodes <lanyi_stroud@brown.edu>
Date: Mon, 28 Apr 2025 21:13:31 -0400
Subject: working on autotagging

---
 src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx | 6 ++++--
 src/client/views/nodes/ImageBox.tsx                               | 6 +++++-
 2 files changed, 9 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
index 28d3ccd48..bf674c025 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
@@ -158,12 +158,14 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this._currentLabel = e.target.value;
     });
 
-    classifyImagesInBox = async (prompt? : string) => {
+    classifyImagesInBox = async (selectedImages? : Doc[], prompt? : string) => {
         this.startLoading();
 
+        selectedImages ??= this._selectedImages;
+
         // Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them.
 
-        const imageInfos = this._selectedImages.map(async doc => {
+        const imageInfos = selectedImages.map(async doc => {
             if (!doc.$tags_chat) {
                 const url = ImageCastWithSuffix(doc[Doc.LayoutDataKey(doc)], '_o') ?? '';
                 return imageUrlToBase64(url).then(hrefBase64 =>
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 87168d17d..6e6ca3f73 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -46,6 +46,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
+import { ImageLabelBox } from '../collections/collectionFreeForm/ImageLabelBox';
 
 const DefaultPath = '/assets/unknown-file-icon-hi.png';
 export class ImageEditorData {
@@ -120,7 +121,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document));
     };
 
-    autoTag = () => {
+    autoTag = async () => {
+       ImageLabelBox.Instance.classifyImagesInBox([this.Document], "Classify this image as a PERSON or LANDSCAPE. You may only respond with one of these two options");
+        
+
         //Doc.getDescription(this.Document).then(desc => this.desc = desc)
     }
 
-- 
cgit v1.2.3-70-g09d2


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

---
 src/client/apis/gpt/GPT.ts                         | 35 +++++++++++----
 .../collectionFreeForm/ImageLabelBox.tsx           |  2 +-
 src/client/views/nodes/ImageBox.tsx                | 48 ++++++++++++++++++---
 .../views/nodes/formattedText/FormattedTextBox.tsx | 18 ++++++++
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx  | 50 ++++++++++++++++++++--
 src/client/views/search/FaceRecognitionHandler.tsx |  4 +-
 6 files changed, 137 insertions(+), 20 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 9cb47995c..1956fef0c 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -11,6 +11,10 @@ export enum GPTDocCommand {
 
 export const DescriptionSeperator = '======';
 export const DocSeperator = '------';
+export enum TextClassifications {
+    Title = 'title', //a few words 
+    Caption = 'caption', //few sentences
+    LengthyDescription = 'lengthy description' }
 
 enum GPTCallType {
     SUMMARY = 'summary',
@@ -36,6 +40,7 @@ enum GPTCallType {
     SUBSETDOCS = 'subset_docs', // select a subset of documents based on their descriptions
     DOCINFO = 'doc_info', // provide information about a document
     SORTDOCS = 'sort_docs',
+    CLASSIFYTEXT = 'classify_text', // classify text into one of the three categories: title, caption, lengthy description
 }
 
 type GPTCallOpts = {
@@ -48,6 +53,23 @@ type GPTCallOpts = {
 const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
     // newest model: gpt-4
     summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' },
+    
+    
+    sort_docs: {
+        model: 'gpt-4o',
+        maxTokens: 2048,
+        temp: 0.25,
+        prompt: 
+            `The user is going to give you a list of descriptions. 
+                Each one is separated by '${DescriptionSeperator}' on either side. 
+                Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'. 
+                Sort them by the user's specifications. 
+                Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'. 
+                Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). 
+                It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`,
+    },
+
+    
     edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' },
     stack: {
         model: 'gpt-4o',
@@ -69,17 +91,14 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
         temp: 0.5,
         prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please only return a JSON with a correlation column 1 propert, a correlation column 2 property, and an analysis property. ",
     },
-    sort_docs: {
+    //new
+    classify_text: {
         model: 'gpt-4o',
         maxTokens: 2048,
         temp: 0.25,
-        prompt: `The user is going to give you a list of descriptions. 
-                Each one is separated by '${DescriptionSeperator}' on either side. 
-                Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'. 
-                Sort them by the user's specifications. 
-                Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'. 
-                Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). 
-                It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`,
+        prompt: `Based on the content of the the text, classify it into the  
+                most appropriate category: '${TextClassifications.Title}', '${TextClassifications.Caption}', or '${TextClassifications.LengthyDescription}'. Output exclusively the classification in your response.
+                `
     },
     describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
     flashcard: {
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
index bf674c025..038b1c6f9 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
@@ -160,7 +160,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     classifyImagesInBox = async (selectedImages? : Doc[], prompt? : string) => {
         this.startLoading();
-
+        alert('Classifying images...');
         selectedImages ??= this._selectedImages;
 
         // Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them.
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 6e6ca3f73..0b6814c01 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -7,8 +7,9 @@ import { observer } from 'mobx-react';
 import { extname } from 'path';
 import * as React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
+import { ImageLabelBoxData } from '../collections/collectionFreeForm/ImageLabelBox';
 import ReactLoading from 'react-loading';
-import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
+import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -16,7 +17,7 @@ import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
+import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast, ImageCastWithSuffix } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
@@ -46,6 +47,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
+import { gptImageLabel } from '../../apis/gpt/GPT';
 import { ImageLabelBox } from '../collections/collectionFreeForm/ImageLabelBox';
 
 const DefaultPath = '/assets/unknown-file-icon-hi.png';
@@ -122,11 +124,47 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     autoTag = async () => {
-       ImageLabelBox.Instance.classifyImagesInBox([this.Document], "Classify this image as a PERSON or LANDSCAPE. You may only respond with one of these two options");
-        
 
-        //Doc.getDescription(this.Document).then(desc => this.desc = desc)
+    try {
+      // 1) grab the full-size URL
+      const layoutKey = Doc.LayoutDataKey(this.Document);
+      const url = ImageCastWithSuffix(this.Document[layoutKey], '_o') ?? '';
+      if (!url) throw new Error('No image URL found');
+
+      // 2) convert to base64
+      const base64 = await imageUrlToBase64(url);
+      if (!base64) throw new Error('Failed to load image data');
+
+      // 3) ask GPT for exactly one label: PERSON or LANDSCAPE
+      const raw = await gptImageLabel(
+        base64,
+        'Classify this image as PERSON or LANDSCAPE.  You may only respond with one of these two options.'
+      );
+
+      // 4) normalize and prefix
+      const label = raw
+        .trim()
+        .toUpperCase()
+
+      // 5) stash it on the Doc
+      //    overwrite any old tags so re-runs still work
+      this.Document.$tags_chat = new List<string>();
+      (this.Document.$tags_chat as List<string>).push(label);
+
+      // 6) flip on “show tags” in the layout 
+      //    (same flag that ImageLabelBox.toggleDisplayInformation uses)
+      //note to self: What if i used my own field (ex: Document.$auto_description or something
+      //Would i still have to toggle it on for it to show in the metadata? 
+      this.Document._layout_showTags = true;
+
+    } catch (err) {
+      console.error('autoTag failed:', err);
+    } finally {
     }
+  };
+      
+        //Doc.getDescription(this.Document).then(desc => this.desc = desc)
+    
 
 
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 98e461a52..f4cbbcc9e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -64,6 +64,7 @@ import { removeMarkWithAttrs } from './prosemirrorPatches';
 import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
 import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
+import { tickStep } from 'd3';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 
 export interface FormattedTextBoxProps extends FieldViewProps {
@@ -304,6 +305,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
     };
 
+    autoTag = async () => {
+        this.Document.$tags_chat = new List<string>();
+        gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]),
+        GPTCallType.CLASSIFYTEXT).then(desc => (this.Document.$tags_chat as List<string>).push(desc));
+        this.Document._layout_showTags = true;
+                //or... then(desc => this.Document.$tags_chat = desc);
+    }
+
     leafText = (node: Node) => {
         if (node.type === this.EditorView?.state.schema.nodes.dashField) {
             const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
@@ -1236,6 +1245,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             },
             { fireImmediately: true }
         );
+        
 
         this._disposers.search = reaction(
             () => Doc.IsSearchMatch(this.Document),
@@ -1269,6 +1279,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             { fireImmediately: true }
         );
 
+        this._disposers.tagger = reaction(
+            () => ({ title: this.Document.title, sel: this.props.isSelected() }),
+            action(() => {
+                this.autoTag();
+            }),
+            { fireImmediately: true }
+        );
+    
         if (!this._props.dontRegisterView) {
             this._disposers.record = reaction(
                 () => this.recordingDictation,
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index 55d0df585..39729a1c5 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -1,6 +1,6 @@
 import { action, makeObservable, observable } from 'mobx';
 import * as React from 'react';
-import { Doc, DocListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { emptyFunction } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
@@ -100,20 +100,62 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     };
 
     filterAddDocument = (docIn: Doc | Doc[]) => {
-        const docs = toList(docIn);
+        const docs = toList(docIn); //The docs being added to the scrapbook
+
+        // 1) Grab all template slots:
+        const slots = DocListCast(this.dataDoc[this.fieldKey]);
+
+        // 2) recursive unwrap:
+        const unwrap = (items: Doc[]): Doc[] =>
+            items.flatMap(d =>
+            d.$type === DocumentType.COL
+                ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)]))
+                : [d]
+            );
+
+        // 3) produce a flat list of every doc, unwrapping any number of nested COLs
+        const allDocs: Doc[] = unwrap(slots);
+     
+
         if (docs?.length === 1) {
-            const placeholder = DocListCast(this.dataDoc[this.fieldKey]).find(d =>
+            const placeholder = allDocs.filter(d =>
+
                  (d.accepts_docType === docs[0].$type ||   // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type
                   RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type)))
             ); // prettier-ignore
 
+            //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)])
+
             if (placeholder) {
+                /**Look at the autotags and see what matches*RTFCast(d[Doc.LayoutDataKey(d)])?.Text*/
                 // ugh.  we have to tell the underlying view not to add the Doc so that we can add it where we want it.
                 // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo.
                 setTimeout(
                     undoable(() => {
+
+                        const slotTagsList: Set<string>[] = placeholder.map(doc =>
+                            new Set<string>(StrListCast(doc.$tags_chat))
+                          );
+                          // turn docs[0].$tags_chat into a Set
+                        const targetTags = new Set(StrListCast(docs[0].$tags_chat));
+
                         //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field])); // // shouldn't need to do this for layout fields since the placeholder already overrides its protos
-                        placeholder.proto = docs[0];
+                        
+                            // find the first placeholder that shares *any* tag
+                        const match = placeholder.find(ph =>
+                            StrListCast(ph.$tags_chat).some(tag => targetTags.has(tag))
+                        );
+                        if (match) {
+                            match.proto = docs[0];
+                          }
+
+                        /*const chosenPlaceholder = placeholder.find(d => 
+                            pl = new Set<string>(StrListCast(d.$tags_chat)
+                            
+                            d.$tags_chat && d.$tags_chat[0].equals(docs[0].$tags_chat)); //why [0]
+                        if (chosenPlaceholder){
+                            chosenPlaceholder.proto = docs[0];}*/
+                         //excess if statement??   
                     }, 'Scrapbook add')
                 );
                 return false;
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index 60744588f..256e68afd 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -210,8 +210,8 @@ export class FaceRecognitionHandler {
         } else if (imgDoc.type === DocumentType.LOADING && !imgDoc.loadingError) {
             setTimeout(() => this.classifyFacesInImage(imgDocView), 1000);
         } else {
-            reaction(() => ({sel:imgDocView.isSelected()}), ({sel}) => !sel && imgDoc.type == 'text'
-            && imgDocView.ComponentView?.autoTag?.(), {fireImmediately: true} 
+            reaction(() => ({sel:imgDocView.isSelected()}), ({sel}) => !sel &&
+             imgDocView.ComponentView?.autoTag?.(), {fireImmediately: true} 
         )
             const imgUrl = ImageCast(imgDoc[Doc.LayoutDataKey(imgDoc)]);
             if (imgUrl && !DocListCast(Doc.MyFaceCollection?.examinedFaceDocs).includes(imgDoc[DocData])) {
-- 
cgit v1.2.3-70-g09d2


From 64a9a1a982ec3f11adfed68cc0f18eb61059aff8 Mon Sep 17 00:00:00 2001
From: sharkiecodes <lanyi_stroud@brown.edu>
Date: Sun, 11 May 2025 22:58:07 -0400
Subject: integrated outpainting with scrapbooks

---
 src/client/views/DocumentDecorations.tsx          |   7 +-
 src/client/views/nodes/ImageBox.tsx               |   1 -
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 136 ++++++++++++++++++----
 3 files changed, 118 insertions(+), 26 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index ab665e984..94e5e662c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -36,6 +36,7 @@ import { ImageBox } from './nodes/ImageBox';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { TagsView } from './TagsView';
+import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox';
 
 interface DocumentDecorationsProps {
     PanelWidth: number;
@@ -446,7 +447,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     onPointerDown = (e: React.PointerEvent): void => {
         SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
         DocumentView.Selected()
-            .filter(dv => e.shiftKey && dv.ComponentView instanceof ImageBox)
+            .filter(dv => e.shiftKey && dv.ComponentView instanceof ImageBox || dv.ComponentView instanceof ScrapbookBox)
             .forEach(dv => {
                 dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalWidth'] = NumCast(dv.Document._width);
                 dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalHeight'] = NumCast(dv.Document._height);
@@ -502,7 +503,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 this._interactionLock = true;
                 this._snapPt = thisPt;
 
-                const outpainted = e.shiftKey ? DocumentView.Selected().filter(dv => dv.ComponentView instanceof ImageBox) : [];
+                const outpainted = e.shiftKey ? DocumentView.Selected().filter(dv => dv.ComponentView instanceof ImageBox || dv.ComponentView instanceof ScrapbookBox) : [];
                 const notOutpainted = e.shiftKey ? DocumentView.Selected().filter(dv => !outpainted.includes(dv)) : DocumentView.Selected();
 
                 // Special handling for shift-drag resize (outpainting of Images by resizing without scaling content - fill in with firefly GAI)
@@ -765,7 +766,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         const rotation = DocumentView.Selected().length === 1 ? seldocview.screenToContentsTransform().inverse().RotateDeg : 0;
 
         // Radius constants
-        const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView;
+        const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof ScrapbookBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView;
         const borderRadius = numberValue(Cast(seldocview.Document.layout_borderRounding, 'string', null));
         const docMax = Math.min(NumCast(seldocview.Document._width) / 2, NumCast(seldocview.Document._height) / 2);
         const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9d459d7eb..0eb74740f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -7,7 +7,6 @@ import { observer } from 'mobx-react';
 import { extname } from 'path';
 import * as React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
-import { ImageLabelBoxData } from '../collections/collectionFreeForm/ImageLabelBox';
 import ReactLoading from 'react-loading';
 import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index ced2df6c5..ad3bfa7ad 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -1,4 +1,4 @@
-import { action, makeObservable, observable } from 'mobx';
+import { action, makeObservable, observable, reaction } from 'mobx';
 import * as React from 'react';
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
@@ -18,7 +18,11 @@ import { ImageBox } from '../ImageBox';
 import { FireflyImageDimensions } from '../../smartdraw/FireflyConstants';
 import { SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
 import { ImageCast } from '../../../../fields/Types';
-import { lengthToDegrees } from '@turf/turf';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { IReactionDisposer } from 'mobx';
+import { observer } from 'mobx-react';
+import { ImageField } from '../../../../fields/URLField';
+import { runInAction } from 'mobx';
 
 enum ScrapbookPresetType {
     Classic = 'Classic',
@@ -28,16 +32,17 @@ enum ScrapbookPresetType {
 }
 
 // Scrapbook view: a container that lays out its child items in a grid/template
+@observer
 export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
 
-    state = {
-        loading: false,
-        src: '',
-      };
-
-
     @observable createdDate: string;
+    @observable loading = false;
+    @observable src = '';
+    @observable imgDoc: Doc | undefined;
+    private _disposers: { [name: string]: IReactionDisposer } = {};
+    private imageBoxRef = React.createRef<ImageBox>();
+
     // @observable configs : ScrapbookItemConfig[]
 
     constructor(props: FieldViewProps) {
@@ -337,7 +342,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
           
         }
     }
-    
+
     async generateAiImage() {
         this.setState({ loading: true });
     
@@ -365,10 +370,98 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     componentDidMount() {
         //this.initScrapbook(ScrapbookPresetType.Default);
         this.setTitle();
-        this.generateAiImage();
+        this.generateAiImageCorrect();
+
+        this._disposers.propagateResize = reaction(
+            () => ({ w: this.layoutDoc._width, h: this.layoutDoc._height }),
+            (dims, prev) => {
+              // prev is undefined on the first run, so bail early
+              if (!prev || !SnappingManager.ShiftKey || !this.imgDoc) return;
+          
+              // either guard the ref…
+              const imageBox = this.imageBoxRef.current;
+              if (!imageBox) return;
+          
+              // …or just hard-code the fieldKey if you know it’s always `"data"`
+              const key = imageBox.props.fieldKey;
+          
+              runInAction(() => {
+                if(!this.imgDoc){
+                    return
+                }
+                // use prev.w/h (the *old* size) as your orig dims
+                this.imgDoc[key + '_outpaintOriginalWidth']  = prev.w;
+                this.imgDoc[key + '_outpaintOriginalHeight'] = prev.h;
+                ;(this.imageBoxRef.current as any).layoutDoc._width  = dims.w
+                ;(this.imageBoxRef.current as any).layoutDoc._height = dims.h
+          
+                // tell the imageDoc to resize itself to the *new* scrapbook size
+                //this.imgDoc._width  = dims.w;
+                //this.imgDoc._height = dims.h;
+                //Doc.SetNativeWidth(this.imgDoc,  dims.w);
+                //Doc.SetNativeHeight(this.imgDoc, dims.h);
+              });
+            }
+        );
+        /*
+        this._disposers.propagateResize = reaction(
+            () => ({ w: this.layoutDoc._width, h: this.layoutDoc._height }),
+            ({ w, h }, prev) => {
+              // only when shift is held (i.e. outpaint mode)
+              if (SnappingManager.ShiftKey && this.imgDoc) {
+                const key = this.imageBoxRef.current!.props.fieldKey; // “data”
+                // record original size on the *image* doc:
+                this.imgDoc[key + '_outpaintOriginalWidth']  = this.imgDoc._width;
+                this.imgDoc[key + '_outpaintOriginalHeight'] = this.imgDoc._height;
+              }
+            }
+          );*/
+        
+
+        //   this._disposers.outpaint = reaction(
+        //     () => this.imgDoc?.[this.imgDoc.fieldKey + '_outpaintOriginalWidth'],
+        //     originalWidth => {
+        //         if (originalWidth !== undefined && !SnappingManager.ShiftKey) {
+        //             this.imageBoxRef.current?.openOutpaintPrompt(); // ✅ CORRECT!
+        //         }
+        //     }
+        // );
         
     }
 
+    async generateAiImageCorrect() {
+        this.loading = true;
+      
+        const prompt = 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background';
+        const dimensions = FireflyImageDimensions.Square;
+      
+        SmartDrawHandler.CreateWithFirefly(prompt, dimensions)
+          .then(action(doc => {
+            if (doc instanceof Doc) {
+              const imgField = ImageCast(doc.data);
+              if (imgField?.url.href) {
+                this.src = imgField.url.href;
+                const url = new ImageField(this.src);
+                this.imgDoc = Docs.Create.ImageDocument(url, { title: 'Generated Background', _width: 1792, _height: 2304,
+                    _nativeWidth: 1792, _nativeHeight: 1792
+                 }, );
+              } else {
+                alert('Image URL missing.');
+                this.src = '';
+              }
+              
+            } else {
+              alert('Failed to generate document.');
+            }
+          }))
+          .catch(e => {
+            alert(`Generation error: ${e}`);
+          })
+          .finally(action(() => {
+            this.loading = false;
+          }));
+      }
+      
     childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => {
         return true; // disable dropping documents onto any child of the scrapbook.
     };
@@ -446,18 +539,18 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     };
 
     render() {
-        const { loading, src } = this.state;
+
         
         return (
         <div style={{ background: 'beige', width: '100%', height: '100%' }}>
-          {loading && (
+          {this.loading && (
           <div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', 
                         display: 'flex', justifyContent: 'center', alignItems: 'center',
                         background: 'rgba(255,255,255,0.8)' }}>
             <ReactLoading type="spin" width={50} height={50} />
           </div>
         )}
-          {loading && (
+          {this.loading && (
                 <div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', 
                               display: 'flex', justifyContent: 'center', alignItems: 'center',
                               background: 'rgba(255,255,255,0.8)', zIndex: 10 }}>
@@ -465,15 +558,14 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 </div>
             )}
             {/* Render AI-generated background */}
-            {src && (
-                <ImageBox
-                    {...this._props}
-                    Document={Docs.Create.ImageDocument(src, { title: 'Generated Background' })}
-                    fieldKey="data"
-                />
-            )}
-
-                <CollectionView
+            {this.src && this.imgDoc && (
+        <ImageBox
+        ref={this.imageBoxRef}
+          {...this._props}
+          Document={this.imgDoc}
+          fieldKey="data"
+        />
+      )}                <CollectionView
                     {...this._props} //
                     setContentViewBox={emptyFunction}
                     rejectDrop={this.rejectDrop}
-- 
cgit v1.2.3-70-g09d2


From f2642f08d49b787c462a2f34166ab545d0a5110a Mon Sep 17 00:00:00 2001
From: sharkiecodes <lanyi_stroud@brown.edu>
Date: Sun, 11 May 2025 23:28:12 -0400
Subject: outpainting scrapbooks

---
 src/client/views/nodes/ImageBox.tsx | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0eb74740f..89fa9942d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -135,7 +135,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
       // 3) ask GPT for exactly one label: PERSON or LANDSCAPE
       const raw = await gptImageLabel(
         base64,
-        'Classify this image as PERSON or LANDSCAPE.  You may only respond with one of these two options.'
+        `Classify this image as PERSON or LANDSCAPE.  You may only respond with one of these two options. Then
+        provide five additional descriptive tags to describe the image for a total of 6 words outputted, 
+        delimited by spaces and commas. Then add one final summary tag (separated by underscores)
+        that describes the image.`
       );
 
       // 4) normalize and prefix
-- 
cgit v1.2.3-70-g09d2


From a5744b79733654cdaa4ed732b48e2d86f570e31d Mon Sep 17 00:00:00 2001
From: sharkiecodes <lanyi_stroud@brown.edu>
Date: Mon, 12 May 2025 00:43:44 -0400
Subject: scrapbooks with tags

---
 src/client/views/nodes/ImageBox.tsx               |  7 ++-
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 66 +++++++++++++++++++++--
 2 files changed, 66 insertions(+), 7 deletions(-)

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

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 89fa9942d..bd612d04f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -137,7 +137,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         base64,
         `Classify this image as PERSON or LANDSCAPE.  You may only respond with one of these two options. Then
         provide five additional descriptive tags to describe the image for a total of 6 words outputted, 
-        delimited by spaces and commas. Then add one final summary tag (separated by underscores)
+        delimited by spaces. For example: "LANDSCAPE BUNNY NATURE FOREST PEACEFUL OUTDOORS". Then add one final lengthier summary tag (separated by underscores)
         that describes the image.`
       );
 
@@ -148,8 +148,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
       // 5) stash it on the Doc
       //    overwrite any old tags so re-runs still work
+      const tokens = label.split(/\s+/);
       this.Document.$tags_chat = new List<string>();
-      (this.Document.$tags_chat as List<string>).push(label);
+      tokens.forEach(tok => {
+        (this.Document.$tags_chat as List<string>).push(tok)});
+      //!!! changed may 11 (this.Document.$tags_chat as List<string>).push(label);
 
       // 6) flip on “show tags” in the layout 
       //    (same flag that ImageLabelBox.toggleDisplayInformation uses)
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index ad3bfa7ad..9ffc13e6e 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -23,6 +23,8 @@ import { IReactionDisposer } from 'mobx';
 import { observer } from 'mobx-react';
 import { ImageField } from '../../../../fields/URLField';
 import { runInAction } from 'mobx';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faRedoAlt }   from '@fortawesome/free-solid-svg-icons';
 
 enum ScrapbookPresetType {
     Classic = 'Classic',
@@ -429,10 +431,12 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         
     }
 
-    async generateAiImageCorrect() {
+    async generateAiImageCorrect(prompt? : string) {
         this.loading = true;
-      
-        const prompt = 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background';
+    
+        if(!prompt){
+        prompt = 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background';
+        }
         const dimensions = FireflyImageDimensions.Square;
       
         SmartDrawHandler.CreateWithFirefly(prompt, dimensions)
@@ -538,8 +542,31 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         return false;
     };
 
+       
     render() {
 
+        /*const internalTags = DocListCast(this.dataDoc[this.fieldKey])
+        .flatMap(ph => StrListCast(ph.$tags_chat))
+        .join(' ');*/
+        const slots = DocListCast(this.dataDoc[this.fieldKey]);
+
+        // 2) recursive unwrap:
+        const unwrap = (items: Doc[]): Doc[] =>
+            items.flatMap(d =>
+            d.$type === DocumentType.COL
+                ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)]))
+                : [d]
+            );
+
+        // 3) produce a flat list of every doc, unwrapping any number of nested COLs
+        const allDocs: Doc[] = unwrap(slots);
+       const internalTags = ''
+
+        const regenPrompt = internalTags
+            ? `Create a new background using these tags: ${internalTags}`
+            : 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background';
+        
+     
         
         return (
         <div style={{ background: 'beige', width: '100%', height: '100%' }}>
@@ -564,8 +591,37 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
           {...this._props}
           Document={this.imgDoc}
           fieldKey="data"
-        />
-      )}                <CollectionView
+        />)}
+          
+        {this._props.isContentActive() && (
+            <div
+              className="scrapbookBox-ui"
+              style={{
+                position: 'absolute',
+                top: 8,
+                right: 8,
+                zIndex: 20,
+              }}
+              onPointerDown={e => e.stopPropagation()}
+            >
+              <button
+                type="button"
+                title="Regenerate Background"
+                onClick={() => this.generateAiImageCorrect(regenPrompt)}
+                style={{
+                  background: 'white',
+                  border: '1px solid #ccc',
+                  borderRadius: 4,
+                  padding: '4px 8px',
+                  cursor: 'pointer',
+                }}
+              >
+                <FontAwesomeIcon icon={faRedoAlt} /> Regenerate
+              </button>
+            </div>
+          )}
+
+                    <CollectionView
                     {...this._props} //
                     setContentViewBox={emptyFunction}
                     rejectDrop={this.rejectDrop}
-- 
cgit v1.2.3-70-g09d2


From b9f72afbc1a4238e1f29a1dd2b26a1cdf971e7e7 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 27 May 2025 11:37:51 -0400
Subject: fixed image rengerate to hide ui buttons first.

---
 src/client/views/nodes/ImageBox.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index f7ad5c7e2..7d130b4fb 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -689,7 +689,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     @computed get overlayImageIcon() {
         const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`];
-        return (
+        return this._regenerateLoading ? null : (
             <Tooltip
                 title={
                     <div className="dash-tooltip">
@@ -731,7 +731,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
     @computed get regenerateImageIcon() {
-        return (
+        return this._regenerateLoading ? null : (
             <Tooltip title={'click to show AI generations.  Drop an image on to create a new generation'}>
                 <div
                     className="imageBox-regenerateDropTarget"
-- 
cgit v1.2.3-70-g09d2


From 49fb76f1c54fb8fc4e76bdcf675719d41bfc36aa Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 27 May 2025 14:27:51 -0400
Subject: canceling image outpaiting cancels outpaint ui from showing up now.

---
 src/client/views/nodes/ImageBox.tsx | 1 +
 1 file changed, 1 insertion(+)

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

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 7d130b4fb..6380dec17 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -369,6 +369,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this.Document._width = origWidth;
         this.Document._height = origHeight;
         this._outpaintingInProgress = false;
+        this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined;
         this.closeOutpaintPrompt();
     };
 
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From 215f874c5f7f28964a3428909fcee0f667388bae Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 1 Jun 2025 16:07:30 -0400
Subject: a number of fixes to document creator to fix preview of templates and
 templates with images.

---
 src/client/views/collections/TabDocView.tsx        |   4 +-
 .../DocCreatorMenu/Backend/TemplateManager.ts      |  79 ++----
 .../DocCreatorMenu/Backend/TemplateMenuAIUtils.ts  |  34 ++-
 .../DataVizBox/DocCreatorMenu/DocCreatorMenu.scss  | 183 +++++++-------
 .../DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx   | 206 ++++++---------
 .../DocCreatorMenu/Menu/TemplateEditingWindow.tsx  | 277 ++++++++++-----------
 .../Menu/TemplateMenuFieldOptions.tsx              | 252 +++++++++----------
 .../DocCreatorMenu/Menu/TemplatePreviewBox.tsx     | 113 ++++-----
 .../Menu/TemplateRenderPreviewWindow.tsx           |  97 ++++----
 .../nodes/DataVizBox/DocCreatorMenu/Template.ts    | 132 ++++------
 .../TemplateFieldTypes/TemplateField.ts            |   7 +-
 src/client/views/nodes/ImageBox.tsx                |   1 +
 12 files changed, 621 insertions(+), 764 deletions(-)

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

diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index c4373aaa7..897a86e7f 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -584,9 +584,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     miniMapColor = () => Colors.MEDIUM_GRAY;
     tabView = () => this._view;
     disableMinimap = () => !this._document;
-    whenChildContentActiveChanges = (isActive: boolean) => {
-        this._isAnyChildContentActive = isActive;
-    };
+    whenChildContentActiveChanges = (isActive: boolean) => (this._isAnyChildContentActive = isActive);
     isContentActiveFunc = () => this.isContentActive;
     waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined);
     renderDocView = (doc: Doc) => (
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts
index 78235d000..526fcf9c4 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts
@@ -14,87 +14,64 @@ export type Conditional = {
     target: string;
     attribute: string;
     value: string;
-}
+};
 
 export class TemplateManager {
+    _templates: Template[] = [];
 
-    templates: Template[] = [];
-
-    conditionalFieldLogic: Record<string, Conditional[]> = {};
+    _conditionalFieldLogic: Record<string, Conditional[]> = {};
 
     constructor(templateSettings: FieldSettings[]) {
         makeAutoObservable(this);
-        this.templates = this.initializeTemplates(templateSettings);
+        this._templates = templateSettings.map(settings => new Template(settings));
     }
 
-    initializeTemplates = (templateSettings: FieldSettings[]) => templateSettings.map(settings => {
-        return new Template(settings)});
-
-    getValidTemplates = (cols: Col[]) => this.templates.filter(template => template.isValidTemplate(cols));
+    getValidTemplates = (cols: Col[]) => this._templates.filter(template => template.isValidTemplate(cols));
 
-    addTemplate = (newTemplate: Template) => this.templates.push(newTemplate);
+    addTemplate = (newTemplate: Template) => this._templates.push(newTemplate);
 
     removeTemplate = (template: Template) => {
-        this.templates.splice(this.templates.indexOf(template), 1);
+        if (this._templates.includes(template)) {
+            this._templates.splice(this._templates.indexOf(template), 1);
+        }
         template.cleanup();
     };
 
     addFieldCondition = (fieldTitle: string, condition: Conditional) => {
-        if (this.conditionalFieldLogic[fieldTitle] === undefined) {
-            this.conditionalFieldLogic[fieldTitle] = [condition];
+        if (this._conditionalFieldLogic[fieldTitle] === undefined) {
+            this._conditionalFieldLogic[fieldTitle] = [condition];
         } else {
-            this.conditionalFieldLogic[fieldTitle].push(condition);
+            this._conditionalFieldLogic[fieldTitle].push(condition);
         }
-    }
+    };
 
-    removeFieldCondition = (fieldTitle: string, condition: Conditional) => {
-        if (this.conditionalFieldLogic[fieldTitle]) {
-            this.conditionalFieldLogic[fieldTitle] = this.conditionalFieldLogic[fieldTitle].filter(cond => cond !== condition);
-        }
-    }
+    removeFieldCondition = (fieldTitle: string, condition: Conditional) => (this._conditionalFieldLogic[fieldTitle] = this._conditionalFieldLogic[fieldTitle]?.filter(cond => cond !== condition));
 
-    addDataField = (title: string) => {
-        this.templates.forEach(template => template.addDataField(title));
-    }
+    addDataField = (title: string) => this._templates.forEach(template => template.addDataField(title));
 
-    removeDataField = (title: string) => {
-        this.templates.forEach(template => template.removeDataField(title));
-    }
+    removeDataField = (title: string) => this._templates.forEach(template => template.removeDataField(title));
 
     createDocsFromTemplate = action((dv: DataVizBox, template: Template, cols: Col[], debug: boolean = false) => {
         const csvFields = Array.from(Object.keys(dv.records[0]));
 
-        const processContent = async (content: { [title: string]: string }) => {
+        const processContent = (content: { [title: string]: string }) => {
             const templateCopy = template.clone();
 
             csvFields
                 .filter(title => title)
                 .forEach(title => {
                     const field = templateCopy.getFieldByTitle(title);
-                    field && field.setContent(content[title], field.viewType);
+                    field?.setContent(content[title], field.viewType);
                 });
 
             const gptFunc = (type: TemplateFieldType) => (type === TemplateFieldType.VISUAL ? TemplateMenuAIUtils.renderGPTImageCall : TemplateMenuAIUtils.renderGPTTextCall);
-            const applyGPTContent = async () => {
-                const promises = cols
-                    .filter(field => field.AIGenerated)
-                    .map(field => {
-                        const templateField: TemplateField = templateCopy.getFieldByTitle(field.title) as TemplateField;
-                        if (templateField !== undefined) {
-                            return gptFunc(field.type)(templateCopy, field, templateField.getID);
-                        }
-                        return null; 
-                    })
-                    .filter(p => p !== null);
-            
-                await Promise.all(promises);
-            };
-
-            await applyGPTContent();
-
-            templateCopy.applyConditionalLogic(this.conditionalFieldLogic);
-
-            return templateCopy.getRenderedDoc();
+
+            const generateGptContent = cols
+                .map(field => ({ field, templateField: field?.AIGenerated && templateCopy.getFieldByTitle(field.title) }))
+                .filter(({ templateField }) => templateField instanceof TemplateField)
+                .map(({ field, templateField }) => gptFunc(field.type)(templateCopy, field, (templateField as TemplateField).getID));
+
+            return Promise.all(generateGptContent).then(() => templateCopy.applyConditionalLogic(this._conditionalFieldLogic));
         };
 
         const rowContents = debug
@@ -109,10 +86,6 @@ export class TemplateManager {
                   )
               );
 
-        return Promise.all(rowContents.map(processContent)).then(
-            action(renderedDocs => {
-                return renderedDocs;
-            })
-        );
+        return Promise.all(rowContents.map(processContent));
     });
 }
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts
index 162b7a1b1..08818dd6c 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts
@@ -1,13 +1,13 @@
-import { ClientUtils } from "../../../../../../ClientUtils";
-import { Networking } from "../../../../../Network";
-import { gptImageCall, gptAPICall, GPTCallType } from "../../../../../apis/gpt/GPT";
-import { Col } from "../DocCreatorMenu";
-import { TemplateFieldSize, TemplateFieldType } from "../TemplateBackend";
-import { TemplateField, ViewType } from "../TemplateFieldTypes/TemplateField";
-import { Template } from "../Template";
+import { ClientUtils } from '../../../../../../ClientUtils';
+import { Networking } from '../../../../../Network';
+import { gptImageCall, gptAPICall, GPTCallType } from '../../../../../apis/gpt/GPT';
+import { Col } from '../DocCreatorMenu';
+import { TemplateFieldSize, TemplateFieldType } from '../TemplateBackend';
+import { ViewType } from '../TemplateFieldTypes/TemplateField';
+import { Template } from '../Template';
+import { Upload } from '../../../../../../server/SharedMediaTypes';
 
 export class TemplateMenuAIUtils {
-
     public static generateGPTImage = async (prompt: string): Promise<string | undefined> => {
         try {
             const res = await gptImageCall(prompt);
@@ -24,13 +24,10 @@ export class TemplateMenuAIUtils {
 
     public static renderGPTImageCall = async (template: Template, col: Col, fieldNumber: number): Promise<boolean> => {
         const generateAndLoadImage = async (id: number, prompt: string) => {
+            const field = template.getFieldByID(id);
             const url = await this.generateGPTImage(prompt);
-            // eslint-disable-next-line
-            var field: TemplateField = template.getFieldByID(id);
-
-            field.setContent(url ?? '', ViewType.IMG);
-            field = template.getFieldByID(id);
-            field.setTitle(col.title);
+            field?.setContent(url ?? '', ViewType.IMG);
+            field?.setTitle(col.title);
         };
 
         const fieldContent: string = template.compiledContent;
@@ -81,10 +78,10 @@ export class TemplateMenuAIUtils {
             if (res) {
                 const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res);
                 Object.entries(assignments).forEach(([, /* title */ info]) => {
-                    const field: TemplateField = template.getFieldByID(Number(info.number));
+                    const field = template.getFieldByID(Number(info.number));
 
-                    field.setContent(info.content ?? '', ViewType.TEXT);
-                    field.setTitle(col.title);
+                    field?.setContent(info.content ?? '', ViewType.TEXT);
+                    field?.setTitle(col.title);
                 });
             }
         } catch (err) {
@@ -122,5 +119,4 @@ export class TemplateMenuAIUtils {
 
         return template;
     };
-
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
index 463e69c67..6eb7fa96a 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
@@ -9,10 +9,10 @@
     position: absolute;
     z-index: 1000;
     // box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
-    // background: whitesmoke; 
+    // background: whitesmoke;
     // color: black;
     border-radius: 3px;
-}   
+}
 
 .docCreatorMenu-menu {
     display: flex;
@@ -24,7 +24,7 @@
 .docCreatorMenu-menu-button {
     width: 25px;
     height: 25px;
-    background: whitesmoke; 
+    background: whitesmoke;
     background-color: rgb(50, 50, 50);
     border-radius: 5px;
     padding: 0px;
@@ -53,8 +53,8 @@
         position: absolute;
         right: 0px;
     }
-     
-    &.right{
+
+    &.right {
         margin-left: 0px;
         font-size: 12px;
     }
@@ -103,12 +103,12 @@
         }
     }
 
-    &:hover::before{
+    &:hover::before {
         border-bottom: 20px solid rgb(82, 82, 82);
     }
 
     &::before {
-        content: "";
+        content: '';
         position: absolute;
         top: 0;
         left: 0;
@@ -120,7 +120,7 @@
     }
 
     &::after {
-        content: "";    
+        content: '';
         position: absolute;
         top: -1px;
         left: -1px;
@@ -143,7 +143,7 @@
     color: white;
 }
 
-.docCreatorMenu-menu-hr{
+.docCreatorMenu-menu-hr {
     margin-top: 0px;
     margin-bottom: 0px;
     color: rgb(180, 180, 180);
@@ -173,14 +173,14 @@
     align-items: center;
     width: 40px;
     height: 40px;
-    background-color: rgb(99, 148, 238); 
+    background-color: rgb(99, 148, 238);
     border: 2px solid rgb(80, 107, 152);
     border-radius: 5px;
     margin-bottom: 20px;
     font-size: 25px;
 
-    &:hover{
-        background-color: rgb(59, 128, 255);  
+    &:hover {
+        background-color: rgb(59, 128, 255);
         border: 2px solid rgb(53, 80, 127);
     }
 }
@@ -188,7 +188,7 @@
 .docCreatorMenu-create-docs-button {
     width: 40px;
     height: 40px;
-    background-color: rgb(176, 229, 149); 
+    background-color: rgb(176, 229, 149);
     border: 2px solid rgb(126, 219, 80);
     border-radius: 5px;
     padding: 0px;
@@ -199,7 +199,7 @@
 
     &:hover {
         background-color: rgb(129, 223, 83);
-        border: 2px solid rgb(80, 185, 28); 
+        border: 2px solid rgb(80, 185, 28);
     }
 }
 
@@ -230,17 +230,22 @@
     position: absolute;
     background-color: none;
 
-    &.top, &.bottom {
+    &.top,
+    &.bottom {
         height: 10px;
         cursor: ns-resize;
     }
 
-    &.right, &.left {
+    &.right,
+    &.left {
         width: 10px;
         cursor: ew-resize;
     }
 
-    &.topRight, &.topLeft, &.bottomRight, &.bottomLeft {
+    &.topRight,
+    &.topLeft,
+    &.bottomRight,
+    &.bottomLeft {
         height: 15px;
         width: 15px;
         background-color: none;
@@ -263,7 +268,7 @@
     height: calc(100% - 30px);
     border: 1px solid rgb(180, 180, 180);
     border-radius: 5px;
-    -ms-overflow-style: none; 
+    -ms-overflow-style: none;
     scrollbar-width: none;
 }
 
@@ -277,7 +282,7 @@
     height: 100%;
     flex-grow: 1;
 
-    .top-panel{
+    .top-panel {
         width: 100%;
         height: 10px;
     }
@@ -302,13 +307,14 @@
     justify-content: center;
     align-items: center;
     height: 100%;
+    width: 100%;
     aspect-ratio: 1;
     color: none;
     border: 1px solid rgb(163, 163, 163);
     border-radius: 5px;
     box-shadow: 5px 5px rgb(29, 29, 31);
 
-    &:hover{
+    &:hover {
         background-color: rgb(72, 72, 73);
     }
 
@@ -353,20 +359,18 @@
     &:hover .option-button {
         display: block;
     }
-    
 }
 
-.docCreatorMenu-preview-image{ 
+.docCreatorMenu-preview-image {
     background-color: transparent;
     height: 100%;
     display: block;
     object-fit: contain;
     border-radius: 5px;
-
 }
 
 .docCreatorMenu-variations-tab {
-    flex-grow: .5;
+    flex-grow: 0.5;
 }
 
 .docCreatorMenu-section {
@@ -400,7 +404,7 @@
     display: grid;
     justify-content: space-evenly;
     row-gap: 2rem;
-    grid-template-columns: repeat(auto-fill, minmax(150px, 30%));
+    grid-template-columns: repeat(auto-fill, minmax(150px, 50%));
     margin: 5px;
     width: calc(100% - 10px);
     height: 100%;
@@ -413,7 +417,7 @@
     }
 }
 
-.divvv{
+.div {
     width: 200;
     height: 200;
     border: solid 1px white;
@@ -448,7 +452,7 @@
 .docCreatorMenu-GPT-generate {
     height: 30px;
     width: 30px;
-    background-color: rgb(176, 229, 149); 
+    background-color: rgb(176, 229, 149);
     border: 1px solid rgb(126, 219, 80);
     border-radius: 5px;
     padding: 0px;
@@ -459,7 +463,7 @@
 
     &:hover {
         background-color: rgb(129, 223, 83);
-        border: 2px solid rgb(80, 185, 28); 
+        border: 2px solid rgb(80, 185, 28);
     }
 }
 
@@ -477,7 +481,7 @@
 // DocCreatorMenu options CSS
 //--------------------------------------------------------------------------------------------------------------------------------------------
 
-.docCreatorMenu-option-container{
+.docCreatorMenu-option-container {
     display: flex;
     width: 180px;
     height: 30px;
@@ -487,16 +491,16 @@
     margin-top: 10px;
     margin-bottom: 10px;
 
-    &.layout{
+    &.layout {
         z-index: 5;
     }
 }
 
-.docCreatorMenu-option-title{
+.docCreatorMenu-option-title {
     display: flex;
     width: 140px;
     height: 30px;
-    background: whitesmoke; 
+    background: whitesmoke;
     background-color: rgb(34, 34, 37);
     border-radius: 5px;
     border: 1px solid rgb(180, 180, 180);
@@ -513,7 +517,7 @@
         border-radius: 0px;
         width: auto;
         text-transform: none;
-         
+
         &.small {
             height: 20px;
             transform: translateY(-5px);
@@ -607,38 +611,38 @@
     height: calc(100% - 30px);
     border: 1px solid rgb(180, 180, 180);
     border-radius: 5px;
-    -ms-overflow-style: none; 
+    -ms-overflow-style: none;
     scrollbar-width: none;
 
-    .docCreatorMenu-option-container{
+    .docCreatorMenu-option-container {
         width: 180px;
         height: 30px;
 
         .docCreatorMenu-dropdown-hoverable {
             width: 140px;
             height: 30px;
-    
+
             &:hover .docCreatorMenu-dropdown-content {
                 display: block;
             }
-    
+
             &:hover .docCreatorMenu-option-title {
                 border-bottom-left-radius: 0px;
                 border-bottom-right-radius: 0px;
             }
-        
+
             .docCreatorMenu-dropdown-content {
                 display: none;
                 min-width: 100px;
                 height: 75px;
                 overflow-y: scroll;
-                -ms-overflow-style: none; 
+                -ms-overflow-style: none;
                 scrollbar-width: none;
                 border-bottom: 1px solid rgb(180, 180, 180);
                 border-bottom-left-radius: 5px;
                 border-bottom-right-radius: 5px;
-        
-                .docCreatorMenu-dropdown-option{
+
+                .docCreatorMenu-dropdown-option {
                     display: flex;
                     background-color: rgb(42, 42, 46);
                     border-left: 1px solid rgb(180, 180, 180);
@@ -649,13 +653,13 @@
                     justify-content: center;
                     justify-items: center;
                     padding-top: 3px;
-    
+
                     &:hover {
                         background-color: rgb(68, 68, 74);
                         cursor: pointer;
                     }
                 }
-            }    
+            }
         }
     }
 }
@@ -710,7 +714,7 @@
         border: 1px solid rgb(180, 180, 180);
         border-radius: 5px;
         background-color: rgb(34, 34, 37);
-        -ms-overflow-style: none; 
+        -ms-overflow-style: none;
         scrollbar-width: none;
 
         &.small {
@@ -742,10 +746,10 @@
         z-index: 999;
     }
 
-    .docCreatorMenu-zoom-button{ 
+    .docCreatorMenu-zoom-button {
         width: 15px;
         height: 15px;
-        background: whitesmoke; 
+        background: whitesmoke;
         background-color: rgb(34, 34, 37);
         border-radius: 3px;
         border: 1px solid rgb(180, 180, 180);
@@ -759,8 +763,6 @@
     }
 }
 
-
-
 //------------------------------------------------------------------------------------------------------------------------------------------
 // DocCreatorMenu dashboard CSS
 //--------------------------------------------------------------------------------------------------------------------------------------------
@@ -778,7 +780,7 @@
     height: calc(100% - 30px);
     border: 1px solid rgb(180, 180, 180);
     border-radius: 5px;
-    -ms-overflow-style: none; 
+    -ms-overflow-style: none;
     scrollbar-width: none;
 
     .panels-container {
@@ -836,8 +838,8 @@
                 background-color: rgb(72, 72, 72);
                 cursor: pointer;
             }
-        }  
-        
+        }
+
         .opts-bar {
             display: flex;
             flex-direction: row;
@@ -880,11 +882,11 @@
                     flex-direction: row;
                     align-items: flex-start;
                 }
-        
+
                 &:hover .type-display {
                     display: none;
                 }
-        
+
                 .bubble {
                     margin: 3px;
                 }
@@ -919,7 +921,7 @@
                     flex-direction: row;
                     align-items: center;
                 }
-        
+
                 .bubble {
                     margin: 3px;
                     margin-right: 4px;
@@ -942,10 +944,8 @@
                 border-bottom-right-radius: 5px;
                 border-bottom-left-radius: 5px;
                 resize: none;
-
             }
         }
-
     }
 
     .conditionals-section {
@@ -972,7 +972,7 @@
         flex-direction: row;
         justify-content: flex-start;
         align-items: flex-start;
-        color: whitesmoke; 
+        color: whitesmoke;
         width: 100%;
         height: fit-content;
         margin-bottom: 15px;
@@ -1041,7 +1041,6 @@
             overflow-y: scroll;
             white-space: nowrap;
         }
-
     }
 
     .form {
@@ -1057,10 +1056,8 @@
             align-items: center;
             margin: 3px;
             cursor: pointer;
-
         }
     }
-
 }
 
 //------------------------------------------------------------------------------------------------------------------------------------------
@@ -1113,38 +1110,38 @@
     display: flex;
     align-items: center;
     justify-content: center;
-    gap: 2rem;                 
+    gap: 2rem;
     padding: 0.5rem 1rem;
     background: rgb(50, 50, 50);
     color: whitesmoke;
     font-family: system-ui, sans-serif;
     font-size: 0.9rem;
-    flex-wrap: wrap;         
-  }
-  
-  .menu‑item {
+    flex-wrap: wrap;
+}
+
+.menu‑item {
     display: flex;
     align-items: center;
     gap: 0.5rem;
     white-space: nowrap;
-  }
-  
-  .menu‑item input[type="range"] {
+}
+
+.menu‑item input[type='range'] {
     width: 7rem;
-    accent-color: whitesmoke;      
-  }
-  
-  .value {
+    accent-color: whitesmoke;
+}
+
+.value {
     min-width: 2ch;
     text-align: right;
-  }
-  
-  .switch {
+}
+
+.switch {
     gap: 0.75rem;
     margin-bottom: 0px;
-  }
-  
-  .switch .slider {
+}
+
+.switch .slider {
     position: relative;
     width: 2.2rem;
     height: 1.1rem;
@@ -1152,9 +1149,9 @@
     border-radius: 1rem;
     cursor: pointer;
     transition: background 0.2s;
-  }
-  
-  .switch .slider::before {
+}
+
+.switch .slider::before {
     content: '';
     position: absolute;
     top: 0.1rem;
@@ -1164,27 +1161,25 @@
     background: rgb(50, 50, 50);
     border-radius: 50%;
     transition: transform 0.2s;
-  }
-  
-  .switch input {
+}
+
+.switch input {
     display: none;
-  }
-  
-  .switch input:checked + .slider {
+}
+
+.switch input:checked + .slider {
     background: #78c2f1;
-  }
-  
-  .switch input:checked + .slider::before {
+}
+
+.switch input:checked + .slider::before {
     transform: translateX(1.1rem);
-  }
+}
 
 .firefly-option-label {
-    padding: .2em .6em .3em;
+    padding: 0.2em 0.6em 0.3em;
     font-size: 100%;
     color: whitesmoke;
     text-align: center;
     margin-bottom: 0px;
     font-weight: 500;
 }
-  
-
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
index ebfa3fc65..fb083ea75 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
@@ -1,4 +1,3 @@
-
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
@@ -22,7 +21,7 @@ import { ViewType } from './TemplateFieldTypes/TemplateField';
 import { Template } from './Template';
 import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from './TemplateBackend';
 import { TemplateManager } from './Backend/TemplateManager';
-import { TemplateMenuAIUtils } from './Backend/TemplateMenuAIUtils'
+import { TemplateMenuAIUtils } from './Backend/TemplateMenuAIUtils';
 import { TemplatePreviewGrid } from './Menu/TemplatePreviewGrid';
 import { FireflyStructureOptions, TemplateEditingWindow } from './Menu/TemplateEditingWindow';
 import { DocCreatorMenuButton } from './Menu/DocCreatorMenuButton';
@@ -158,18 +157,12 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
         return bounds;
     }
 
-    setUpButtonClick = (e: React.PointerEvent, func: (...args: any) => void) => {
-        setupMoveUpEvents(
-            this,
-            e,
-            returnFalse,
-            emptyFunction,
-            undoable(clickEv => {
-                clickEv.stopPropagation();
-                clickEv.preventDefault();
-                func();
-            }, 'create docs')
-        );
+    setUpButtonClick = (e: React.PointerEvent, func: () => void) => {
+        setupMoveUpEvents(this, e, returnFalse, emptyFunction, clickEv => {
+            clickEv.stopPropagation();
+            clickEv.preventDefault();
+            undoable(func, 'create docs')();
+        });
     };
 
     @action
@@ -301,16 +294,13 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
         this._pageX = x + translation.x;
         this._pageY = y + translation.y;
     };
-    
-    async createDocsForPreview(): Promise<Doc[]> { return this._dataViz && this._selectedTemplate ? ((await this.templateManager.createDocsFromTemplate(this._dataViz, this._selectedTemplate, this.fieldsInfos, this.DEBUG_MODE)).filter(doc => doc).map(doc => doc!) ?? []) as unknown as Doc[] : []; }
 
-    @action updateSelectedTemplate = async (template: Template) => {
-        if (this._selectedTemplate === template) {
-            this._selectedTemplate = undefined;
-            return;
-        } else {
-            this._selectedTemplate = template;
-        }
+    async createDocsForPreview() {
+        return this._dataViz && this._selectedTemplate ? ((await this.templateManager.createDocsFromTemplate(this._dataViz, this._selectedTemplate, this.fieldsInfos, this.DEBUG_MODE)).filter(doc => doc).map(doc => doc!) ?? []) : [];
+    }
+
+    @action updateSelectedTemplate = (template: Template) => {
+        this._selectedTemplate = this._selectedTemplate === template ? undefined : template; // toggle selection
     };
 
     // testTemplate = async () => {
@@ -318,27 +308,16 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
     // };
 
     @action addField = () => {
-        const newFields: Col[] = this._userCreatedFields.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [], AIGenerated: true }]);
-        this._userCreatedFields = newFields;
+        this._userCreatedFields = this._userCreatedFields.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [], AIGenerated: true }]);
     };
 
     @action removeField = (field: { title: string; type: string; desc: string }) => {
         if (this._dataViz?.axes.includes(field.title)) {
             this._dataViz.selectAxes(this._dataViz.axes.filter(col => col !== field.title));
         } else {
-            const toRemove = this._userCreatedFields.filter(f => f === field);
-            if (!toRemove) return;
-
-            if (toRemove.length > 1) {
-                while (toRemove.length > 1) {
-                    toRemove.pop();
-                }
-            }
-
-            if (this._userCreatedFields.length === 1) {
-                this._userCreatedFields = [];
-            } else {
-                this._userCreatedFields.splice(this._userCreatedFields.indexOf(toRemove[0]), 1);
+            const toRemove = this._userCreatedFields.findIndex(f => f === field);
+            if (toRemove !== -1) {
+                this._userCreatedFields.splice(toRemove, 1);
             }
         }
     };
@@ -390,35 +369,22 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
         this.forceUpdate();
     };
 
-    compileFieldDescriptions = (templates: Template[]): string => {
-        let descriptions: string = '';
-        templates.forEach(template => {
-            descriptions += `----------  NEW TEMPLATE TO INCLUDE: The title is: ${template.title}. Its fields are: `;
-            descriptions += template.descriptionSummary;
-        });
-
-        return descriptions;
-    };
-
-    compileColDescriptions = (cols: Col[]): string => {
-        let descriptions: string = ' ------------- COL DESCRIPTIONS START HERE:';
-        cols.forEach(col => (descriptions += `{title: ${col.title}, sizes: ${String(col.sizes)}, type: ${col.type}, descreiption: ${col.desc}} `));
+    compileFieldDescriptions = (templates: Template[]) => 
+        templates.map(template => `----------  NEW TEMPLATE TO INCLUDE: The title is: ${template.title}. Its fields are: ` + template.descriptionSummary).join(''); // prettier-ignore
 
-        return descriptions;
-    };
+    compileColDescriptions = (cols: Col[]) => 
+        ' ------------- COL DESCRIPTIONS START HERE:' + cols.map(col => `{title: ${col.title}, sizes: ${String(col.sizes)}, type: ${col.type}, descreiption: ${col.desc}} `).join(''); // prettier-ignore
 
-    getColByTitle = (title: string) => {
-        return this.fieldsInfos.filter(col => col.title === title)[0];
-    };
+    getColByTitle = (title: string): Col | undefined => this.fieldsInfos.filter(col => col.title === title)[0];
 
     @action
     assignColsToFields = async (templates: Template[], cols: Col[]): Promise<[Template, { [field: number]: Col }][]> => {
-        const fieldDescriptions: string = this.compileFieldDescriptions(templates);
-        const colDescriptions: string = this.compileColDescriptions(cols);
+        const fieldDescriptions = this.compileFieldDescriptions(templates);
+        const colDescriptions = this.compileColDescriptions(cols);
 
         const inputText = fieldDescriptions.concat(colDescriptions);
 
-        const prompt: string = `(${Math.random() * 100000}) ${inputText}`;
+        const prompt = `(${Math.random() * 100000}) ${inputText}`;
 
         this._GPTLoading = true;
 
@@ -431,23 +397,25 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
 
                 Object.entries(assignments).forEach(([tempTitle, assignment]) => {
                     const template = templates.filter(temp => temp.title === tempTitle)[0];
-                    if (!template) return;
-                    const toObj = Object.entries(assignment).reduce(
-                        (a, [fieldID, colTitle]) => {
-                            const col = this.getColByTitle(colTitle);
-                            if (!col.AIGenerated) {
-                                var field = template.getFieldByID(Number(fieldID));
-                                field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? ViewType.IMG : ViewType.TEXT);
-                                field = template.getFieldByID(Number(fieldID));
-                                field.setTitle(col.title);
-                            } else {
-                                a[Number(fieldID)] = this.getColByTitle(colTitle);
-                            }
-                            return a;
-                        },
-                        {} as { [field: number]: Col }
-                    );
-                    brokenDownAssignments.push([template, toObj]);
+                    if (template) {
+                        const toObj = Object.entries(assignment).reduce(
+                            (a, [fieldID, colTitle]) => {
+                                const col = this.getColByTitle(colTitle);
+                                if (col) {
+                                    if (!col.AIGenerated) {
+                                        const field = template.getFieldByID(Number(fieldID));
+                                        field?.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? ViewType.IMG : ViewType.TEXT);
+                                        field?.setTitle(col.title);
+                                    } else {
+                                        a[Number(fieldID)] = col;
+                                    }
+                                }
+                                return a;
+                            },
+                            {} as { [field: number]: Col }
+                        );
+                        brokenDownAssignments.push([template, toObj]);
+                    }
                 });
 
                 return brokenDownAssignments;
@@ -459,39 +427,32 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
         return [];
     };
 
-    generatePresetTemplates = async () => {
-        const templates: Template[] = [];
-
+    generatePresetTemplates = action(() => {
         if (this.DEBUG_MODE) {
-            templates.push(...this.templateManager.templates);
+            this.setSuggestedTemplates(this.templateManager._templates);
+            this._GPTLoading = false;
         } else {
             this._dataViz?.updateColDefaults();
-
             const contentFields = this.fieldsInfos.filter(field => field.type !== TemplateFieldType.DATA);
-
-            templates.push(...this.templateManager.getValidTemplates(contentFields));
-
-            const assignments = await this.assignColsToFields(templates, contentFields);
-
-            const renderedTemplatePromises = assignments.map(([template, assgns]) => TemplateMenuAIUtils.applyGPTContentToTemplate(template, assgns));
-
-            await Promise.all(renderedTemplatePromises);
+            const templates = this.templateManager.getValidTemplates(contentFields);
+
+            return this.assignColsToFields(templates, contentFields)
+                .then(pairs => 
+                    Promise.all(pairs.map(([templ, assgns]) => TemplateMenuAIUtils.applyGPTContentToTemplate(templ, assgns))))
+                .then(action(() => {
+                    this.setSuggestedTemplates(templates);
+                    this._GPTLoading = false;
+                })); // prettier-ignore
         }
+    });
 
-        setTimeout(
-            action(() => {
-                this.setSuggestedTemplates(templates);
-                this._GPTLoading = false;
-            })
-        );
-    };
-
-    generateVariations = async (onDoc: Doc, prompt: string, options: FireflyStructureOptions): Promise<string[]> => {
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    generateVariations = async (onDoc: Doc, prompt: string, options: FireflyStructureOptions) => {
         // const { numVariations, temperature, useStyleRef } = options;
         this.variations = [];
         const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
 
-        const clone: Doc = (await Doc.MakeClone(onDoc)).clone;
+        const clone = Doc.MakeClone(onDoc).clone;
         mainCollection.addDocument(clone);
         clone.x = 10000;
         clone.y = 10000;
@@ -499,13 +460,13 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
         // await DrawingFillHandler.drawingToImage(clone, 100 - temperature, prompt, useStyleRef ? clone : undefined, this, numVariations)
 
         return this.variations;
-    }
+    };
 
-    variations: string[] = []
+    variations: string[] = [];
 
     @action addVariation = (url: string) => {
         this.variations.push(url);
-    }
+    };
 
     addRenderedCollectionToMainview = (collection: Doc) => {
         if (collection) {
@@ -517,10 +478,11 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
         }
     };
 
-    @action editLastTemplate = () => { if (this._editedTemplateTrail.length) this._currEditingTemplate = this._editedTemplateTrail.pop()}
+    @action editLastTemplate = () => {
+        if (this._editedTemplateTrail.length) this._currEditingTemplate = this._editedTemplateTrail.pop();
+    };
 
     @action setExpandedView = (template: Template | undefined) => {
-
         if (template) {
             this._menuContent = 'templateEditing';
             this._currEditingTemplate && this._editedTemplateTrail.push(this._currEditingTemplate);
@@ -533,27 +495,23 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
         //Docs.Create.FreeformDocument([doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''});
     };
 
-    @computed 
-    get templatesView() { return (
-        <div className='docCreatorMenu-templates-view'>
-            <div className="docCreatorMenu-templates-displays">
-                <TemplatePreviewGrid 
-                    title={'Suggested Templates'} 
-                    menu={this} 
-                    loading={this._GPTLoading}
-                    optionsButtonOpts={this.optionsButtonOpts}
-                    templates={this._suggestedTemplates}
-                />
-                <div className="docCreatorMenu-GPT-options">
-                    <div className="docCreatorMenu-GPT-options-container">
-                        <DocCreatorMenuButton icon={'arrows-rotate'} styles={'border'} function={this.generatePresetTemplates}/>
+    @computed
+    get templatesView() {
+        return (
+            <div className="docCreatorMenu-templates-view">
+                <div className="docCreatorMenu-templates-displays">
+                    <TemplatePreviewGrid title={'Suggested Templates'} menu={this} loading={this._GPTLoading} optionsButtonOpts={this.optionsButtonOpts} templates={this._suggestedTemplates} />
+                    <div className="docCreatorMenu-GPT-options">
+                        <div className="docCreatorMenu-GPT-options-container">
+                            <DocCreatorMenuButton icon={'arrows-rotate'} styles={'border'} function={this.generatePresetTemplates} />
+                        </div>
                     </div>
                 </div>
             </div>
-        </div>
-    )};
-    
-    private optionsButtonOpts: [IconProp, () => any] = ['gear', () => (this._menuContent = 'dashboard')];
+        );
+    }
+
+    private optionsButtonOpts: [IconProp, () => void] = ['gear', () => (this._menuContent = 'dashboard')];
 
     get renderSelectedViewType() {
         switch (this._menuContent) {
@@ -634,7 +592,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
                                 {topButton('magnifying-glass', 'options', onOptionsSelected, 'middle')}
                                 {topButton('bars', 'saved', onSavedSelected, 'right')}
                             </div>
-                            <DocCreatorMenuButton icon={'minus'} styles={'float-right'} function={this.closeMenu}/>
+                            <DocCreatorMenuButton icon={'minus'} styles={'float-right'} function={this.closeMenu} />
                         </div>
                         {this.renderSelectedViewType}
                     </div>
@@ -642,4 +600,4 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
             </div>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx
index b50fff9e0..c35099e82 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx
@@ -1,25 +1,25 @@
-import { action, makeObservable, observable, reaction, runInAction } from "mobx";
-import React from "react";
-import { returnFalse, returnEmptyFilter } from "../../../../../../ClientUtils";
-import { emptyFunction } from "../../../../../../Utils";
-import { Doc, returnEmptyDoclist } from "../../../../../../fields/Doc";
-import { DefaultStyleProvider } from "../../../../StyleProvider";
-import { DocumentView, DocumentViewInternal } from "../../../DocumentView";
-import { DocCreatorMenu } from "../DocCreatorMenu";
-import { TemplatePreviewGrid } from "./TemplatePreviewGrid";
-import { observer } from "mobx-react";
-import { Transform } from "../../../../../util/Transform";
-import { Template } from "../Template";
-import { ObservableReactComponent } from "../../../../ObservableReactComponent";
-import { IDisposer } from "mobx-utils";
-import { DocCreatorMenuButton } from "./DocCreatorMenuButton";
-import { IconProp } from "@fortawesome/fontawesome-svg-core";
+import { action, makeObservable, observable, reaction } from 'mobx';
+import React from 'react';
+import { returnFalse, returnEmptyFilter } from '../../../../../../ClientUtils';
+import { emptyFunction } from '../../../../../../Utils';
+import { returnEmptyDoclist } from '../../../../../../fields/Doc';
+import { DefaultStyleProvider } from '../../../../StyleProvider';
+import { DocumentView, DocumentViewInternal } from '../../../DocumentView';
+import { DocCreatorMenu } from '../DocCreatorMenu';
+import { TemplatePreviewGrid } from './TemplatePreviewGrid';
+import { observer } from 'mobx-react';
+import { Transform } from '../../../../../util/Transform';
+import { Template } from '../Template';
+import { ObservableReactComponent } from '../../../../ObservableReactComponent';
+import { IDisposer } from 'mobx-utils';
+import { DocCreatorMenuButton } from './DocCreatorMenuButton';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
 
 export type FireflyStructureOptions = {
     numVariations: number;
     temperature: number;
     useStyleRef: boolean;
-}
+};
 
 interface FireflyVariationsTabProps {
     menu: DocCreatorMenu;
@@ -28,112 +28,93 @@ interface FireflyVariationsTabProps {
 
 @observer
 export class FireflyVariationsTab extends ObservableReactComponent<FireflyVariationsTabProps> {
+    private _prompt: string = 'Use this template to generate an empty baseball card template.';
+    private _optionsButtonOpts: [IconProp, () => void] = ['gear', emptyFunction];
+    private _previewBoxRightButtonOpts: [IconProp, () => void] = ['gear', () => this.forceUpdate()];
 
-    private prompt: string = 'Use this template to generate an empty baseball card template.';
-
-    @observable private promptInput: HTMLTextAreaElement | null = null;
-
+    @observable _fireflyOptions: FireflyStructureOptions = { numVariations: 3, temperature: 0, useStyleRef: false };
+    @observable _promptInput: HTMLTextAreaElement | null = null;
     @observable _loading: boolean = false;
     @observable _variationsTabOpen: boolean = false;
     @observable _variationURLs: string[] = [];
 
-    @observable private fireflyOptions: FireflyStructureOptions = {numVariations: 3, temperature: 0, useStyleRef: false};
-
     constructor(props: FireflyVariationsTabProps) {
         super(props);
         makeObservable(this);
     }
 
-    generateVariations = async () => {
+    generateVariations = action(async () => {
         this._props.menu._variations = [];
         this._loading = true;
         const cloneTemplate = this._props.template.clone(false);
         cloneTemplate.setMatteBackground();
-        const doc: Doc = cloneTemplate.getRenderedDoc()!;
-        this._variationURLs = await this._props.menu.generateVariations(doc, this.prompt, this.fireflyOptions);
-        this._variationURLs.forEach(() => {
-            const newTemplate: Template = this._props.template.clone(true);
-            this._props.menu._variations.push(newTemplate);
-        });
-        setTimeout(() => {
-            this._variationURLs.forEach((url, i) => {
-                this._props.menu._variations[i].setImageAsBackground(url, true);
-            });
-            this._loading = false;
-        });
-    }
-
-    setPromptInputRef: React.LegacyRef<HTMLTextAreaElement> = (node) => {
-        this.promptInput = node;
-    }
-
-    // eslint-disable-next-line
-    private optionsButtonOpts: [IconProp, () => any] = ['gear', () => {}];
-    // eslint-disable-next-line
-    private previewBoxRightButtonOpts: [IconProp, () => any] = ['gear', () => this.forceUpdate()];
+        const doc = cloneTemplate.getRenderedDoc()!;
+        this._props.menu.generateVariations(doc, this._prompt, this._fireflyOptions).then(
+            action((urls: string[]) => {
+                (this._variationURLs = urls).forEach(url => {
+                    const template = this._props.template.clone(true);
+                    template.setImageAsBackground(url, true);
+                    this._props.menu._variations.push(template);
+                });
+                this._loading = false;
+            })
+        );
+    });
 
     render() {
         return (
-            <div className='docCreatorMenu-editing-firefly-section'>
-                <div className="docCreatorMenu-option-divider full no-margin-bottom"/>
+            <div className="docCreatorMenu-editing-firefly-section">
+                <div className="docCreatorMenu-option-divider full no-margin-bottom" />
                 <TemplatePreviewGrid
                     menu={this._props.menu}
-                    title={'Generate Variations'}
+                    title="Generate Variations"
                     loading={this._loading}
-                    styles={'scrolling'}
+                    styles="scrolling"
                     templates={this._props.menu._variations}
-                    optionsButtonOpts={this.optionsButtonOpts}
-                    previewBoxRightButtonOpts={this.previewBoxRightButtonOpts}
+                    optionsButtonOpts={this._optionsButtonOpts}
+                    previewBoxRightButtonOpts={this._previewBoxRightButtonOpts}
                 />
                 <div className="docCreatorMenu-firefly-options">
                     <div className="docCreatorMenu-variation-prompt-row">
                         <textarea
                             className="docCreatorMenu-variation-prompt-input-textbox"
-                            ref={this.setPromptInputRef}
-                            onChange={e => { this.prompt = e.target.value }}
+                            ref={action((node: HTMLTextAreaElement | null) => (this._promptInput = node))}
+                            onChange={e => (this._prompt = e.target.value)}
                             onInput={() => {
-                                if (this.promptInput !== null) {
-                                    this.promptInput.style.height = 'auto';
-                                    this.promptInput.style.height = this.promptInput.scrollHeight + 'px';
+                                if (this._promptInput !== null) {
+                                    this._promptInput.style.height = 'auto';
+                                    this._promptInput.style.height = this._promptInput.scrollHeight + 'px';
                                 }
                             }}
-                            defaultValue={''}
-                            placeholder={'Enter a custom prompt here (optional)'}
+                            defaultValue=""
+                            placeholder="Enter a custom prompt here (optional)"
                         />
-                        <DocCreatorMenuButton icon={'arrows-rotate'} styles={'border'} function={this.generateVariations}/>
+                        <DocCreatorMenuButton icon={'arrows-rotate'} styles={'border'} function={this.generateVariations} />
                     </div>
-                        <nav className="options‑menu">
-                            <label className="menu‑item switch">
-                                <input type="checkbox" checked={this.fireflyOptions.useStyleRef} 
-                                    onChange={(e) => runInAction(() => { this.fireflyOptions.useStyleRef = e.target.checked })}
-                                />
-                                <span className="slider round"></span>
-                                <span className="firefly-option-label">Use template as style guide</span>
-                            </label>
-                            <div className="menu‑item">
-                                <span className="firefly-option-label">Variations</span>
-                                <input type="range" id="variations" 
-                                    min="1" 
-                                    max="5" 
-                                    value={this.fireflyOptions.numVariations} 
-                                    onChange={(e) => runInAction(() => { this.fireflyOptions.numVariations = Number(e.target.value) })}
-                                />
-                                <span className="value" id="varVal">{this.fireflyOptions.numVariations}</span>
-                            </div>
-                            <div className="menu‑item">
-                                <span className="firefly-option-label">Temperature</span>
-                                <input type="range" id="temperature" 
-                                    min="1" 
-                                    max="100" 
-                                    value={this.fireflyOptions.temperature} 
-                                    onChange={(e) => runInAction(() => { this.fireflyOptions.temperature = Number(e.target.value) })}
-                                />
-                                <span className="value" id="tempVal">{this.fireflyOptions.temperature}</span>
-                            </div>
-                        </nav>
+                    <nav className="options‑menu">
+                        <label className="menu‑item switch">
+                            <input type="checkbox" checked={this._fireflyOptions.useStyleRef} onChange={action(e => (this._fireflyOptions.useStyleRef = e.target.checked))} />
+                            <span className="slider round"></span>
+                            <span className="firefly-option-label">Use template as style guide</span>
+                        </label>
+                        <div className="menu‑item">
+                            <span className="firefly-option-label">Variations</span>
+                            <input type="range" id="variations" min="1" max="5" value={this._fireflyOptions.numVariations} onChange={action(e => (this._fireflyOptions.numVariations = Number(e.target.value)))} />
+                            <span className="value" id="varVal">
+                                {this._fireflyOptions.numVariations}
+                            </span>
+                        </div>
+                        <div className="menu‑item">
+                            <span className="firefly-option-label">Temperature</span>
+                            <input type="range" id="temperature" min="1" max="100" value={this._fireflyOptions.temperature} onChange={action(e => (this._fireflyOptions.temperature = Number(e.target.value)))} />
+                            <span className="value" id="tempVal">
+                                {this._fireflyOptions.temperature}
+                            </span>
+                        </div>
+                    </nav>
                 </div>
             </div>
-        )
+        );
     }
 }
 
@@ -144,11 +125,9 @@ interface TemplateEditingWindowProps {
 
 @observer
 export class TemplateEditingWindow extends ObservableReactComponent<TemplateEditingWindowProps> {
-
     private disposers: { [name: string]: IDisposer } = {};
 
-    @observable private previewWindow: HTMLDivElement | null = null;
-
+    @observable private _previewWindow: HTMLDivElement | null = null;
     @observable _variationsTabOpen: boolean = false;
 
     constructor(props: TemplateEditingWindowProps) {
@@ -157,83 +136,85 @@ export class TemplateEditingWindow extends ObservableReactComponent<TemplateEdit
     }
 
     componentDidMount(): void {
-        this.disposers.windowDimensions = reaction(() => 
-            this._props.menu._resizing, 
-            () => { this.forceUpdate() }, 
+        this.disposers.windowDimensions = reaction(
+            () => this._props.menu._resizing,
+            () => this.forceUpdate(),
             { fireImmediately: true }
-            );
+        );
     }
 
     componentWillUnmount() {
         Object.values(this.disposers).forEach(disposer => disposer?.());
     }
 
-    setContainerRef: React.LegacyRef<HTMLDivElement> = (node) => {
-        this.previewWindow = node;
-    }
-
     @action setVariationTab = (open: boolean) => {
         this._variationsTabOpen = open;
-        if (this.previewWindow && open) {
-            this.previewWindow.style.height = String(Number(this.previewWindow.clientHeight) * .6);
-        } else if (this.previewWindow && !open) {
-            this.previewWindow.style.height = String(Number(this.previewWindow.clientHeight) * 5/3);
+        if (this._previewWindow && open) {
+            this._previewWindow.style.height = String(Number(this._previewWindow.clientHeight) * 0.6);
+        } else if (this._previewWindow && !open) {
+            this._previewWindow.style.height = String((Number(this._previewWindow.clientHeight) * 5) / 3);
         }
-    }
-   
-    get renderedDocPreview(){
-        const doc: Doc = this._props.template.getRenderedDoc() as unknown as Doc;
+    };
 
+    previewPanelWidth = () => this._previewWindow?.clientWidth ?? 500;
+    previewPanelHeight = () => this._previewWindow?.clientHeight ?? 500;
+    previewScreenToLocalXf = () => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1);
+    get renderedDocPreview() {
+        const doc = this._props.template.getRenderedDoc();
         return (
-            <div className="docCreatorMenu-expanded-template-preview" ref={this.setContainerRef}>
-                {this.previewWindow ? <DocumentView
-                    Document={doc}
-                    isContentActive={emptyFunction}
-                    addDocument={returnFalse}
-                    moveDocument={returnFalse}
-                    removeDocument={returnFalse}
-                    PanelWidth={() => this.previewWindow?.clientWidth ?? 500}
-                    PanelHeight={() => this.previewWindow?.clientHeight ?? 500}
-                    ScreenToLocalTransform={() => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1)}
-                    renderDepth={5}
-                    whenChildContentsActiveChanged={emptyFunction}
-                    focus={emptyFunction}
-                    styleProvider={DefaultStyleProvider}
-                    addDocTab={DocumentViewInternal.addDocTabFunc}
-                    pinToPres={() => undefined}
-                    childFilters={returnEmptyFilter}
-                    childFiltersByRanges={returnEmptyFilter}
-                    searchFilterDocs={returnEmptyDoclist}
-                    fitContentsToBox={returnFalse}
-                    fitWidth={returnFalse}
-                /> : null}
+            <div className="docCreatorMenu-expanded-template-preview" ref={action((node: HTMLDivElement | null) => (this._previewWindow = node))}>
+                {this._previewWindow && doc ? (
+                    <DocumentView
+                        Document={doc}
+                        isContentActive={emptyFunction}
+                        addDocument={returnFalse}
+                        moveDocument={returnFalse}
+                        removeDocument={returnFalse}
+                        PanelWidth={this.previewPanelWidth}
+                        PanelHeight={this.previewPanelHeight}
+                        ScreenToLocalTransform={this.previewScreenToLocalXf}
+                        renderDepth={5}
+                        whenChildContentsActiveChanged={emptyFunction}
+                        focus={emptyFunction}
+                        styleProvider={DefaultStyleProvider}
+                        addDocTab={DocumentViewInternal.addDocTabFunc}
+                        pinToPres={emptyFunction}
+                        childFilters={returnEmptyFilter}
+                        childFiltersByRanges={returnEmptyFilter}
+                        searchFilterDocs={returnEmptyDoclist}
+                        // fitContentsToBox={returnFalse}
+                        // fitWidth={returnFalse}
+                    />
+                ) : null}
             </div>
-        )
+        );
     }
 
+    expandFunc = () => {
+        // if (this._props.template === this._props.menu._selectedTemplate) {
+        //     this._props.menu.updateRenderedPreviewCollection(this._props.template);
+        // }
+        this._props.menu.setExpandedView(undefined);
+    };
+    lastFunc = () => {
+        this._props.menu.editLastTemplate();
+        this.forceUpdate();
+    };
+    variationFunc = () => this.setVariationTab(!this._variationsTabOpen);
     render() {
         return (
-            <div className='docCreatorMenu-templates-view'>
+            <div className="docCreatorMenu-templates-view">
                 <div className="docCreatorMenu-expanded-template-preview">
-                    <div className="top-panel"/>
+                    <div className="top-panel" />
                     {this.renderedDocPreview}
-                    {this._variationsTabOpen ? <FireflyVariationsTab
-                        menu={this._props.menu}
-                        template={this._props.template}
-                    />
-                     : null}
+                    {this._variationsTabOpen ? <FireflyVariationsTab menu={this._props.menu} template={this._props.template} /> : null}
                     <div className="right-buttons-panel">
-                        <DocCreatorMenuButton icon={'minimize'} function={() => {
-                                // if (this._props.template === this._props.menu._selectedTemplate) {
-                                //     this._props.menu.updateRenderedPreviewCollection(this._props.template);
-                                // }
-                                this._props.menu.setExpandedView(undefined);
-                            }}/>
-                        <DocCreatorMenuButton icon={'lightbulb'} function={() => this.setVariationTab(!this._variationsTabOpen)}/>
-                        <DocCreatorMenuButton icon={'arrow-rotate-backward'} function={() => { this._props.menu.editLastTemplate(); this.forceUpdate(); }}/>
+                        <DocCreatorMenuButton icon="minimize" function={this.expandFunc} />
+                        <DocCreatorMenuButton icon="lightbulb" function={this.variationFunc} />
+                        <DocCreatorMenuButton icon="arrow-rotate-backward" function={this.lastFunc} />
                     </div>
                 </div>
             </div>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx
index a4da54392..f0e20837c 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx
@@ -1,11 +1,11 @@
-import { makeObservable, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { ObservableReactComponent } from "../../../../ObservableReactComponent";
-import { Col, DocCreatorMenu } from "../DocCreatorMenu";
-import React from "react";
-import { Conditional, TemplateManager } from "../Backend/TemplateManager";
-import { TemplateFieldType, TemplateFieldSize } from "../TemplateBackend";
-import { DocCreatorMenuButton } from "./DocCreatorMenuButton";
+import { action, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { ObservableReactComponent } from '../../../../ObservableReactComponent';
+import { Col, DocCreatorMenu } from '../DocCreatorMenu';
+import React from 'react';
+import { Conditional, TemplateManager } from '../Backend/TemplateManager';
+import { TemplateFieldType, TemplateFieldSize } from '../TemplateBackend';
+import { DocCreatorMenuButton } from './DocCreatorMenuButton';
 
 interface TemplateMenuFieldOptionsProps {
     menu: DocCreatorMenu;
@@ -14,7 +14,6 @@ interface TemplateMenuFieldOptionsProps {
 
 @observer
 export class TemplateMenuFieldOptions extends ObservableReactComponent<TemplateMenuFieldOptionsProps> {
-
     @observable _collapsedCols: string[] = []; //any columns whose options panels are hidden
 
     constructor(props: TemplateMenuFieldOptionsProps) {
@@ -34,160 +33,153 @@ export class TemplateMenuFieldOptions extends ObservableReactComponent<TemplateM
                 condition: '',
                 target: 'Own',
                 attribute: '',
-                value: ''
+                value: '',
             });
         }
         return this._newCondCache[title];
     };
 
     conditionForm = (title: string, parameters?: Conditional, empty: boolean = false) => {
-
-        const contentFieldTitles = this._props.menu.fieldsInfos.filter(field => field.type !== TemplateFieldType.DATA).map(field => field.title).concat('Template');
+        const contentFieldTitles = this._props.menu.fieldsInfos
+            .filter(field => field.type !== TemplateFieldType.DATA)
+            .map(field => field.title)
+            .concat('Template');
         const params: Conditional = this.getParams(title, parameters);
 
         return (
-        <div className='form'>
-            <div className='form-row'>
-                <div className='form-row-plain-text'>If</div>
-                <div className='form-row-plain-text'>{title}</div>
-                <div className="operator-options-dropdown">
-                    <span className="operator-dropdown-current">{params.operator ?? '='}</span>
-                    <div className='operator-dropdown-option' onPointerDown={() => {params.operator = '='}}>{'='}</div>
-                </div>
-                <input
-                    className="form-row-textarea"
-                    onChange={e => runInAction(() => { params.condition = e.target.value })}
-                    placeholder='value'
-                    value={params.condition}
-                />
-                <div className='form-row-plain-text'>then</div>
-                <div className="operator-options-dropdown">
-                    <span className="operator-dropdown-current">{params.target ?? 'Own'}</span>
-                    {contentFieldTitles.map((fieldTitle, i) => 
-                        <div className='operator-dropdown-option' key={i} onPointerDown={() => {params.target = fieldTitle}}>{fieldTitle === title ? 'Own' : fieldTitle}</div>
-                    )}
+            <div className="form">
+                <div className="form-row">
+                    <div className="form-row-plain-text">If</div>
+                    <div className="form-row-plain-text">{title}</div>
+                    <div className="operator-options-dropdown">
+                        <span className="operator-dropdown-current">{params.operator ?? '='}</span>
+                        <div className="operator-dropdown-option" onPointerDown={() => (params.operator = '=')}>
+                            {'='}
+                        </div>
+                    </div>
+                    <input className="form-row-textarea" onChange={action(e => (params.condition = e.target.value))} placeholder="value" value={params.condition} />
+                    <div className="form-row-plain-text">then</div>
+                    <div className="operator-options-dropdown">
+                        <span className="operator-dropdown-current">{params.target ?? 'Own'}</span>
+                        {contentFieldTitles.map((fieldTitle, i) => (
+                            <div className="operator-dropdown-option" key={i} onPointerDown={() => (params.target = fieldTitle)}>
+                                {fieldTitle === title ? 'Own' : fieldTitle}
+                            </div>
+                        ))}
+                    </div>
+                    <input className="form-row-textarea" onChange={action(e => (params.attribute = e.target.value))} placeholder="attribute" value={params.attribute} />
+                    <div className="form-row-plain-text">{'becomes'}</div>
+                    <input className="form-row-textarea" onChange={action(e => (params.value = e.target.value))} placeholder="value" value={params.value} />
                 </div>
-                <input
-                    className="form-row-textarea"
-                    onChange={e => runInAction(() => { params.attribute = e.target.value })}
-                    placeholder='attribute'
-                    value={params.attribute}
-                />
-                <div className='form-row-plain-text'>{'becomes'}</div>
-                <input
-                    className="form-row-textarea"
-                    onChange={e => runInAction(() => { params.value = e.target.value })}
-                    placeholder='value'
-                    value={params.value}
-                />
+                {empty ? (
+                    <DocCreatorMenuButton
+                        icon={'plus'}
+                        styles={'float-right border'}
+                        function={() => {
+                            this._newCondCache[title] = observable<Conditional>({
+                                field: title,
+                                operator: '=',
+                                condition: '',
+                                target: 'Own',
+                                attribute: '',
+                                value: '',
+                            });
+                            this._props.templateManager.addFieldCondition(title, params);
+                        }}
+                    />
+                ) : (
+                    <DocCreatorMenuButton icon={'minus'} styles={'float-right border'} function={() => this._props.templateManager.removeFieldCondition(title, params)} />
+                )}
             </div>
-            {empty ? 
-            <DocCreatorMenuButton icon={'plus'} styles={'float-right border'} function={() => {
-                this._newCondCache[title] = observable<Conditional>({
-                    field: title,
-                    operator: '=',
-                    condition: '',
-                    target: 'Own',
-                    attribute: '',
-                    value: ''
-                });
-                this._props.templateManager.addFieldCondition(title, params);
-            }}/>
-            :
-            <DocCreatorMenuButton icon={'minus'} styles={'float-right border'} function={() => this._props.templateManager.removeFieldCondition(title, params)}/>
-            }
-        </div>
-        )
-    }
+        );
+    };
 
     fieldPanel = (field: Col, id: number) => (
         <div className="field-panel" key={id}>
-            <div className="top-bar" onPointerDown={e => this._props.menu.setUpButtonClick(e, runInAction(() => () => {
-                if (this._collapsedCols.includes(field.title)) {
-                    this._collapsedCols = this._collapsedCols.filter(col => col !== field.title);
-                } else {
-                    this._collapsedCols.push(field.title);
-                }
-            }))}>
+            <div
+                className="top-bar"
+                onPointerDown={e =>
+                    this._props.menu.setUpButtonClick(
+                        e,
+                        action(() => {
+                            if (this._collapsedCols.includes(field.title)) {
+                                this._collapsedCols = this._collapsedCols.filter(col => col !== field.title);
+                            } else {
+                                this._collapsedCols.push(field.title);
+                            }
+                        })
+                    )
+                }>
                 <span className="field-title">{`${field.title} Field`}</span>
-                <DocCreatorMenuButton icon={'minus'} styles={'no-margin absolute-right'} function={() => this._props.menu.removeField(field)}/>
+                <DocCreatorMenuButton icon={'minus'} styles={'no-margin absolute-right'} function={() => this._props.menu.removeField(field)} />
             </div>
-            { this._collapsedCols.includes(field.title) ? null :
+            {this._collapsedCols.includes(field.title) ? null : (
                 <>
-                <div className="opts-bar">
-                    <div className="opt-box">
-                        <div className="top-bar"> Title </div>
-                        <textarea className="content" style={{ width: '100%', height: 'calc(100% - 20px)' }} value={field.title} placeholder={'Enter title'} onChange={e => this._props.menu.setColTitle(field, e.target.value)} />
-                    </div>
-                    <div className="opt-box">
-                        <div className="top-bar"> Type </div>
-                        <div className="content">
-                            <span className="type-display">{
-                                field.type === TemplateFieldType.TEXT ? 'Text Field' 
-                                : field.type === TemplateFieldType.VISUAL ? 'File Field' 
-                                : field.type === TemplateFieldType.DATA ? 'Data Field' 
-                                : ''
-                            }</span>
-                            <div className="bubbles">
-                                <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.TEXT)} />
-                                <div className="text">Text</div>
-                                <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.VISUAL)} />
-                                <div className="text">File</div>
-                                <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.DATA)} />
-                                <div className="text">Data</div>
-                            </div>
+                    <div className="opts-bar">
+                        <div className="opt-box">
+                            <div className="top-bar"> Title </div>
+                            <textarea className="content" style={{ width: '100%', height: 'calc(100% - 20px)' }} value={field.title} placeholder={'Enter title'} onChange={e => this._props.menu.setColTitle(field, e.target.value)} />
                         </div>
-                    </div>
-                </div>
-                { field.type === TemplateFieldType.DATA ? null :
-                    (<> 
-                        <div className="sizes-box">
-                            <div className="top-bar"> Valid Sizes </div>
+                        <div className="opt-box">
+                            <div className="top-bar"> Type </div>
                             <div className="content">
+                                <span className="type-display">{field.type === TemplateFieldType.TEXT ? 'Text Field' : field.type === TemplateFieldType.VISUAL ? 'File Field' : field.type === TemplateFieldType.DATA ? 'Data Field' : ''}</span>
                                 <div className="bubbles">
-                                    {Object.values(TemplateFieldSize).map(size => (
-                                        <div key={field + size}>
-                                            <input className="bubble" type="checkbox" name="type" checked={field.sizes.includes(size)} onChange={e => this._props.menu.modifyColSizes(field, size, e.target.checked)} />
-                                            <div className="text">{size}</div>
-                                        </div>
-                                    ))}
+                                    <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.TEXT)} />
+                                    <div className="text">Text</div>
+                                    <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.VISUAL)} />
+                                    <div className="text">File</div>
+                                    <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.DATA)} />
+                                    <div className="text">Data</div>
                                 </div>
                             </div>
                         </div>
-                        <div className="desc-box">
-                            <div className="top-bar"> Prompt </div>
-                            <textarea
-                                className="content"
-                                onChange={e => this._props.menu.setColDesc(field, e.target.value)}
-                                defaultValue={field.desc === this._props.menu._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc}
-                                placeholder={this._props.menu._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description/prompt to help with template generation.'}
-                            />
-                        </div> 
-                    </>)
-                }
-                <div className="conditionals-section">
-                    <span className="conditionals-title">Conditional Logic</span>
-                    {this.conditionForm(field.title, undefined, true)}
-                    {this._props.templateManager.conditionalFieldLogic[field.title]?.map(condition => this.conditionForm(condition.field, condition))}
-                </div>
+                    </div>
+                    {field.type === TemplateFieldType.DATA ? null : (
+                        <>
+                            <div className="sizes-box">
+                                <div className="top-bar"> Valid Sizes </div>
+                                <div className="content">
+                                    <div className="bubbles">
+                                        {Object.values(TemplateFieldSize).map(size => (
+                                            <div key={field + size}>
+                                                <input className="bubble" type="checkbox" name="type" checked={field.sizes.includes(size)} onChange={e => this._props.menu.modifyColSizes(field, size, e.target.checked)} />
+                                                <div className="text">{size}</div>
+                                            </div>
+                                        ))}
+                                    </div>
+                                </div>
+                            </div>
+                            <div className="desc-box">
+                                <div className="top-bar"> Prompt </div>
+                                <textarea
+                                    className="content"
+                                    onChange={e => this._props.menu.setColDesc(field, e.target.value)}
+                                    defaultValue={field.desc === this._props.menu._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc}
+                                    placeholder={this._props.menu._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description/prompt to help with template generation.'}
+                                />
+                            </div>
+                        </>
+                    )}
+                    <div className="conditionals-section">
+                        <span className="conditionals-title">Conditional Logic</span>
+                        {this.conditionForm(field.title, undefined, true)}
+                        {this._props.templateManager._conditionalFieldLogic[field.title]?.map(condition => this.conditionForm(condition.field, condition))}
+                    </div>
                 </>
-            }
+            )}
         </div>
     );
 
-
-
     render() {
         return (
             <div className="docCreatorMenu-dashboard-view">
                 <div className="topbar">
-                    <DocCreatorMenuButton icon={'plus'} function={this._props.menu.addField}/>
-                    <DocCreatorMenuButton icon={'arrow-left'} styles={'float-right'} function={() => runInAction(() => (this._props.menu._menuContent = 'templates'))}/>
+                    <DocCreatorMenuButton icon="plus" function={this._props.menu.addField} />
+                    <DocCreatorMenuButton icon="arrow-left" styles="float-right" function={action(() => (this._props.menu._menuContent = 'templates'))} />
                 </div>
                 <div className="panels-container">{this._props.menu.fieldsInfos.map((field, i) => this.fieldPanel(field, i))}</div>
             </div>
         );
     }
-
-
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx
index dc4c35789..7d02fff12 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx
@@ -1,35 +1,30 @@
-import { Colors } from "@dash/components/src";
-import { FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import { Template } from "../Template";
-import { makeObservable, observable } from "mobx";
-import React from "react";
-import { ObservableReactComponent } from "../../../../ObservableReactComponent";
-import { DocCreatorMenu } from "../DocCreatorMenu";
-import { IconProp } from "@fortawesome/fontawesome-svg-core";
-import { DocumentView } from "../../../DocumentView";
-import { emptyFunction } from "../../../../../../Utils";
-import { returnEmptyFilter, returnFalse } from "../../../../../../ClientUtils";
-import { Transform } from "../../../../../util/Transform";
-import { DefaultStyleProvider } from "../../../../StyleProvider";
-import { Doc, returnEmptyDoclist } from "../../../../../../fields/Doc";
-import { observer } from "mobx-react";
+import { Colors } from '@dash/components/src';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Template } from '../Template';
+import { action, makeObservable, observable } from 'mobx';
+import React from 'react';
+import { ObservableReactComponent } from '../../../../ObservableReactComponent';
+import { DocCreatorMenu } from '../DocCreatorMenu';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { DocumentView } from '../../../DocumentView';
+import { emptyFunction } from '../../../../../../Utils';
+import { returnEmptyFilter, returnFalse } from '../../../../../../ClientUtils';
+import { Transform } from '../../../../../util/Transform';
+import { DefaultStyleProvider } from '../../../../StyleProvider';
+import { Doc, returnEmptyDoclist } from '../../../../../../fields/Doc';
+import { observer } from 'mobx-react';
 
 export interface TemplatePreviewBoxProps {
     template: Template;
     menu: DocCreatorMenu; // eslint-disable-next-line
-    leftButtonOpts?: [icon: IconProp, func: (...args: any) => void] // eslint-disable-next-line
-    rightButtonOpts?: [icon: IconProp, func: (...args: any) => void]
+    leftButtonOpts?: [icon: IconProp, func: (...args: any) => void]; // eslint-disable-next-line
+    rightButtonOpts?: [icon: IconProp, func: (...args: any) => void];
 }
 
 @observer
 export class TemplatePreviewBox extends ObservableReactComponent<TemplatePreviewBoxProps> {
-
     @observable private previewWindow: HTMLDivElement | null = null;
 
-    setContainerRef: React.LegacyRef<HTMLDivElement> = (node) => {
-        this.previewWindow = node;
-    }
-
     constructor(props: TemplatePreviewBoxProps) {
         super(props);
         makeObservable(this);
@@ -39,6 +34,10 @@ export class TemplatePreviewBox extends ObservableReactComponent<TemplatePreview
         return this._props.template.getRenderedDoc() as Doc;
     }
 
+    docPanelWidth = () => this.previewWindow?.clientWidth ?? this._props.menu._menuDimensions.height * 0.3;
+    docPanelHeight = () => this.previewWindow?.clientHeight ?? this._props.menu._menuDimensions.height * 0.3;
+    docScreenToLocalXf = () => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1);
+
     render() {
         const template = this._props.template;
 
@@ -46,49 +45,45 @@ export class TemplatePreviewBox extends ObservableReactComponent<TemplatePreview
             <div
                 key={template.title}
                 className="docCreatorMenu-preview-window"
-                ref={this.setContainerRef}
+                ref={action((node: HTMLDivElement | null) => (this.previewWindow = node))}
                 style={{
                     border: this._props.menu._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
                     boxShadow: this._props.menu._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '',
                 }}
-                onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.menu.updateSelectedTemplate(template))}
-                >
-                    { this._props.leftButtonOpts ? 
-                    <button
-                        className="option-button left"
-                        onPointerDown={e =>
-                            this._props.menu.setUpButtonClick(e, () => this._props.leftButtonOpts![1](template))
-                        }>
+                onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.menu.updateSelectedTemplate(template))}>
+                {this._props.leftButtonOpts ? (
+                    <button className="option-button left" onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.leftButtonOpts![1](template))}>
                         <FontAwesomeIcon icon={this._props.leftButtonOpts![0]} color="white" />
-                    </button> : null
-                    }
-                    { this._props.rightButtonOpts ? 
+                    </button>
+                ) : null}
+                {this._props.rightButtonOpts ? (
                     <button className="option-button right" onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.rightButtonOpts![1](template))}>
                         <FontAwesomeIcon icon={this._props.rightButtonOpts![0]} color="white" />
-                    </button> : null }
-                    <DocumentView
-                        Document={this.doc}
-                        isContentActive={emptyFunction} // !!! should be return false
-                        addDocument={returnFalse}
-                        moveDocument={returnFalse}
-                        removeDocument={returnFalse}
-                        PanelWidth={() => this.previewWindow?.clientWidth ?? this._props.menu._menuDimensions.height * .3}
-                        PanelHeight={() => this.previewWindow?.clientHeight ?? this._props.menu._menuDimensions.height * .3}
-                        ScreenToLocalTransform={() => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1)}
-                        renderDepth={1}
-                        whenChildContentsActiveChanged={emptyFunction}
-                        focus={emptyFunction}
-                        styleProvider={DefaultStyleProvider}
-                        addDocTab={this._props.menu._props.addDocTab}
-                        pinToPres={() => undefined}
-                        childFilters={returnEmptyFilter}
-                        childFiltersByRanges={returnEmptyFilter}
-                        searchFilterDocs={returnEmptyDoclist}
-                        fitContentsToBox={returnFalse}
-                        fitWidth={returnFalse}
-                        hideDecorations={true}
-                    />
+                    </button>
+                ) : null}
+                <DocumentView
+                    Document={this.doc}
+                    isContentActive={emptyFunction} // !!! should be return false
+                    addDocument={returnFalse}
+                    moveDocument={returnFalse}
+                    removeDocument={returnFalse}
+                    PanelWidth={this.docPanelWidth}
+                    PanelHeight={this.docPanelHeight}
+                    ScreenToLocalTransform={this.docScreenToLocalXf}
+                    renderDepth={1}
+                    whenChildContentsActiveChanged={emptyFunction}
+                    focus={emptyFunction}
+                    styleProvider={DefaultStyleProvider}
+                    addDocTab={this._props.menu._props.addDocTab}
+                    pinToPres={emptyFunction}
+                    childFilters={returnEmptyFilter}
+                    childFiltersByRanges={returnEmptyFilter}
+                    searchFilterDocs={returnEmptyDoclist}
+                    // fitContentsToBox={returnFalse}
+                    // fitWidth={returnFalse}
+                    hideDecorations={true}
+                />
             </div>
-        )
+        );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx
index f281f770e..9222d7349 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx
@@ -1,20 +1,20 @@
-import { action, computed, makeObservable, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { ObservableReactComponent } from "../../../../ObservableReactComponent";
-import { DocCreatorMenu, LayoutType } from "../DocCreatorMenu";
-import React from "react";
-import { IconProp } from "@fortawesome/fontawesome-svg-core";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { setupMoveUpEvents, returnFalse, returnEmptyFilter } from "../../../../../../ClientUtils";
-import { emptyFunction } from "../../../../../../Utils";
-import { undoable } from "../../../../../util/UndoManager";
-import ReactLoading from "react-loading";
-import { Doc, NumListCast, returnEmptyDoclist } from "../../../../../../fields/Doc";
-import { StrCast } from "../../../../../../fields/Types";
-import { DefaultStyleProvider } from "../../../../StyleProvider";
-import { DocumentView } from "../../../DocumentView";
-import { Transform } from "../../../../../util/Transform";
-import { Docs, DocumentOptions } from "../../../../../documents/Documents";
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { ObservableReactComponent } from '../../../../ObservableReactComponent';
+import { DocCreatorMenu, LayoutType } from '../DocCreatorMenu';
+import React from 'react';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { setupMoveUpEvents, returnFalse, returnEmptyFilter } from '../../../../../../ClientUtils';
+import { emptyFunction } from '../../../../../../Utils';
+import { undoable } from '../../../../../util/UndoManager';
+import ReactLoading from 'react-loading';
+import { Doc, NumListCast, returnEmptyDoclist } from '../../../../../../fields/Doc';
+import { NumCast, StrCast } from '../../../../../../fields/Types';
+import { DefaultStyleProvider } from '../../../../StyleProvider';
+import { DocumentView } from '../../../DocumentView';
+import { Transform } from '../../../../../util/Transform';
+import { Docs, DocumentOptions } from '../../../../../documents/Documents';
 
 interface TemplatesRenderPreviewWindowProps {
     menu: DocCreatorMenu;
@@ -22,8 +22,7 @@ interface TemplatesRenderPreviewWindowProps {
 
 @observer
 export class TemplatesRenderPreviewWindow extends ObservableReactComponent<TemplatesRenderPreviewWindowProps> {
-
-    @observable private _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.FREEFORM, yMargin: 10, xMargin: 10, columns: 0, repeat: 0 };
+    @observable private _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.FREEFORM, yMargin: 10, xMargin: 10, columns: 1, repeat: 0 };
 
     @observable private renderedDocs: Doc[] = [];
     @observable private renderedDocCollection: Doc | undefined = undefined;
@@ -62,7 +61,7 @@ export class TemplatesRenderPreviewWindow extends ObservableReactComponent<Templ
     @computed get columnsCount() {
         switch (this._layout.type) {
             case LayoutType.FREEFORM:
-                return this._layout.columns ?? 0;
+                return this._layout.columns ?? 1;
             case LayoutType.CAROUSEL3D:
                 return 3;
             default:
@@ -130,28 +129,27 @@ export class TemplatesRenderPreviewWindow extends ObservableReactComponent<Templ
         const { horizontalSpan, verticalSpan } = this.previewInfo;
         collection._height = verticalSpan;
         collection._width = horizontalSpan;
-
-        const columns: number = this._layout.columns ?? this.columnsCount;
-        const xGap: number = this._layout.xMargin;
-        const yGap: number = this._layout.yMargin;
-        const startX: number = -Number(collection._width) / 2;
-        const startY: number = -Number(collection._height) / 2;
-        const docHeight: number = Number(docs[0]._height);
-        const docWidth: number = Number(docs[0]._width);
-
-        if (columns === 0 || docs.length === 0) {
-            return;
-        }
-
-        let i: number = 0;
-        let docsChanged: number = 0;
-        let curX: number = startX;
-        let curY: number = startY;
+        collection.layout_fitWidth = true;
+        collection.freeform_fitContentsToBox = true;
+
+        const columns = (this._layout.columns ?? this.columnsCount) || 1;
+        const xGap = this._layout.xMargin;
+        const yGap = this._layout.yMargin;
+        const startX = -collection._width / 2;
+        const startY = -collection._height / 2;
+        const docHeight = NumCast(docs[0]?._height);
+        const docWidth = NumCast(docs[0]?._width);
+
+        let i = 0;
+        let docsChanged = 0;
+        let curX = startX;
+        let curY = startY;
 
         while (docsChanged < docs.length) {
             while (i < columns && docsChanged < docs.length) {
                 docs[docsChanged].x = curX;
                 docs[docsChanged].y = curY;
+                docs[docsChanged].layout_fitWidth = false;
                 curX += docWidth + xGap;
                 ++docsChanged;
                 ++i;
@@ -164,8 +162,8 @@ export class TemplatesRenderPreviewWindow extends ObservableReactComponent<Templ
 
     @computed
     get previewInfo() {
-        const docHeight: number = Number(this.renderedDocs[0]._height);
-        const docWidth: number = Number(this.renderedDocs[0]._width);
+        const docHeight = NumCast(this.renderedDocs[0]?._height);
+        const docWidth = NumCast(this.renderedDocs[0]?._width);
         const layout = this._layout;
         return {
             docHeight: docHeight,
@@ -201,6 +199,10 @@ export class TemplatesRenderPreviewWindow extends ObservableReactComponent<Templ
         }
     }
 
+    layoutPanelWidth = () => this._props.menu._menuDimensions.width - 80;
+    layoutPanelHeight = () => this._props.menu._menuDimensions.height - 105;
+    layoutScreenToLocalXf = () => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1);
+
     layoutPreviewContents = action(() => {
         return this.loading ? (
             <div className="docCreatorMenu-layout-preview-window-wrapper loading">
@@ -216,20 +218,18 @@ export class TemplatesRenderPreviewWindow extends ObservableReactComponent<Templ
                     addDocument={returnFalse}
                     moveDocument={returnFalse}
                     removeDocument={returnFalse}
-                    PanelWidth={() => this._props.menu._menuDimensions.width - 80}
-                    PanelHeight={() => this._props.menu._menuDimensions.height - 105}
-                    ScreenToLocalTransform={() => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1)}
+                    PanelWidth={this.layoutPanelWidth}
+                    PanelHeight={this.layoutPanelHeight}
+                    ScreenToLocalTransform={this.layoutScreenToLocalXf}
                     renderDepth={5}
                     whenChildContentsActiveChanged={emptyFunction}
                     focus={emptyFunction}
                     styleProvider={DefaultStyleProvider}
                     addDocTab={this._props.menu._props.addDocTab}
-                    pinToPres={() => undefined}
+                    pinToPres={emptyFunction}
                     childFilters={returnEmptyFilter}
                     childFiltersByRanges={returnEmptyFilter}
                     searchFilterDocs={returnEmptyDoclist}
-                    fitContentsToBox={returnFalse}
-                    fitWidth={returnFalse}
                     hideDecorations={true}
                 />
             </div>
@@ -273,7 +273,6 @@ export class TemplatesRenderPreviewWindow extends ObservableReactComponent<Templ
     };
 
     get optionsMenuContents() {
-
         const repeatOptions = [0, 1, 2, 3, 4, 5];
 
         return (
@@ -341,5 +340,7 @@ export class TemplatesRenderPreviewWindow extends ObservableReactComponent<Templ
         );
     }
 
-    render() { return this.optionsMenuContents }
-}
\ No newline at end of file
+    render() {
+        return this.optionsMenuContents;
+    }
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts
index 7591740e0..e2a2a3c1c 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts
@@ -9,7 +9,7 @@ import { TemplateDataField } from './TemplateFieldTypes/DataField';
 export class Template {
     _mainField: DynamicField;
 
-    private dataFields: TemplateDataField[] = [];
+    private _dataFields: TemplateDataField[] = [];
 
     /**
      * A Template can be created from a description of its fields (FieldSettings) or from a DynamicField
@@ -20,37 +20,13 @@ export class Template {
         this._mainField = definition instanceof DynamicField ? definition : this.setupMainField(definition);
     }
 
-    get childFields(): TemplateField[] {
-        return this._mainField?.getSubfields ?? [];
-    }
-    get allFields(): TemplateField[] {
-        return this._mainField?.getAllSubfields ?? [];
-    }
-    get contentFields(): TemplateField[] {
-        return this.allFields.filter(field => field.isContentField);
-    }
-    get doc() {
-        return this._mainField?.renderedDoc;
-    }
-    get title() {
-        return this._mainField?.getTitle();
-    }
-
-    get descriptionSummary(): string {
-        let summary: string = '';
-        this.contentFields.forEach(field => {
-            summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getDescription ?? ''} ---`;
-        });
-        return summary;
-    }
-
-    get compiledContent(): string {
-        let summary: string = '';
-        this.contentFields.forEach(field => {
-            summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getContent() ?? ''} ---`;
-        });
-        return summary;
-    }
+    get childFields()        { return this._mainField?.getSubfields ?? []; } // prettier-ignore
+    get allFields()          { return this._mainField?.getAllSubfields ?? []; } // prettier-ignore
+    get contentFields()      { return this.allFields.filter(field => field.isContentField); } // prettier-ignore
+    get doc()                { return this._mainField?.renderedDoc; } // prettier-ignore
+    get title()              { return this._mainField?.getTitle(); } // prettier-ignore
+    get descriptionSummary() { return this.contentFields.map(f => `--- Field #${f.getID} (title: ${f.getTitle()}): ${f.getDescription ?? ''} ---`).join(); } // prettier-ignore
+    get compiledContent()    { return this.contentFields.map(f => `--- Field #${f.getID} (title: ${f.getTitle()}): ${f.getContent() ?? ''} ---`).join(); } // prettier-ignore
 
     cleanup = () => {
         //dispose each subfields disposers, etc.
@@ -58,61 +34,56 @@ export class Template {
 
     clone = (withContent: boolean = false) => {
         const clone = new Template(this._mainField?.makeClone(undefined, withContent) ?? TemplateLayouts.BasicSettings);
-        this.dataFields.forEach(field => clone.addDataField(field.title));
+        this._dataFields.forEach(field => clone.addDataField(field.title));
         return clone;
     };
 
     getRenderedDoc = () => this.doc;
 
-    getFieldByID = (id: number): TemplateField => this.allFields.filter(field => field.getID === id)[0];
+    getFieldByID = (id: number): TemplateField | undefined => this.allFields.filter(field => field.getID === id)[0];
 
-    getFieldByTitle = (title: string) => [...this.allFields, ...this.dataFields].filter(field => field.getTitle() === title)[0];
+    getFieldByTitle = (title: string) => [...this.allFields, ...this._dataFields].filter(field => field.getTitle() === title)[0];
 
     setupMainField = (templateInfo: FieldSettings) => TemplateField.CreateField(templateInfo, 1, undefined) as DynamicField;
 
     assignColToField = (fieldID: number, col: Col) => {
         const field = this.getFieldByID(fieldID);
-        field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? ViewType.IMG : ViewType.TEXT);
-        field.setTitle(col.title);
-    }
+        field?.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? ViewType.IMG : ViewType.TEXT);
+        field?.setTitle(col.title);
+    };
 
-    addDataField = (title: string, content?: string) => {
-        this.dataFields.push(new TemplateDataField(title, content));
-    }
+    addDataField = (title: string, content?: string) => this._dataFields.push(new TemplateDataField(title, content));
 
-    removeDataField = (title: string) => {
-        this.dataFields = this.dataFields.filter(field => !(field.title === title));
-    }
+    removeDataField = (title: string) => (this._dataFields = this._dataFields.filter(field => field.title !== title));
 
-    isValidTemplate = (cols: Col[]) => {
-        const maxMatches = this.maxMatches(this.getMatches(cols));
-        return maxMatches === this.contentFields.length && this.title !== 'template_framework';
-    };
+    isValidTemplate = (cols: Col[]) => this.title !== 'template_framework' && this.maxMatches(this.getMatches(cols)) === this.contentFields.length;
 
     applyConditionalLogicToField = (field: TemplateField | TemplateDataField, logic: Record<string, Conditional[]>) => {
         if (field instanceof DynamicField) return;
-        const fieldStatements: Conditional[] = logic[field.getTitle()];
-        const content = field.getContent()
-        fieldStatements && fieldStatements.forEach(statement => {
+        const fieldStatements = logic[field.getTitle()];
+        const content = field.getContent();
+        fieldStatements?.forEach(statement => {
             if (content === statement.condition) {
                 if (statement.target === 'Template') {
-                    this._mainField.renderedDoc![statement.attribute] = statement.value;
-                    Object.assign(this._mainField.settings.opts, {[statement.attribute]: statement.value});
+                    if (this._mainField.renderedDoc) {
+                        this._mainField.renderedDoc[statement.attribute] = statement.value;
+                        Object.assign(this._mainField.settings.opts, { [statement.attribute]: statement.value });
+                    }
                 } else {
-                    const targetField: TemplateField = this.getFieldByTitle(statement.target) as TemplateField;
-                    if (targetField) {
-                        targetField.renderedDoc![statement.attribute] = statement.value;
-                        Object.assign(targetField.settings.opts, {[statement.attribute]: statement.value});
-                    } 
+                    const targetField = this.getFieldByTitle(statement.target);
+                    if (targetField instanceof TemplateField && targetField.renderedDoc) {
+                        targetField.renderedDoc[statement.attribute] = statement.value;
+                        Object.assign(targetField.settings.opts, { [statement.attribute]: statement.value });
+                    }
                 }
             }
-        })
-    }
+        });
+    };
 
     applyConditionalLogic = (logic: Record<string, Conditional[]>) => {
-        const fields: (TemplateField | TemplateDataField)[] = [...this.allFields, ...this.dataFields];
-        fields.forEach(field => this.applyConditionalLogicToField(field, logic));
-    }
+        [...this.allFields, ...this._dataFields].forEach(field => this.applyConditionalLogicToField(field, logic));
+        return this.getRenderedDoc();
+    };
 
     setImageAsBackground(url: string, makeTransparent: boolean = false) {
         const fieldSettings: FieldSettings = {
@@ -120,15 +91,15 @@ export class Template {
             br: [1, 1],
             opts: {},
             viewType: ViewType.IMG,
-        }
+        };
 
-        const field: TemplateField = TemplateField.CreateField(fieldSettings, Math.random() * 100 + 100, this._mainField);
+        const field = TemplateField.CreateField(fieldSettings, Math.random() * 100 + 100, this._mainField);
         field.setContent(url);
 
         if (makeTransparent) {
-            this.allFields.forEach(field => {
-                field.updateDocSetting('backgroundColor', 'transparent');
-                field.updateDocSetting('borderWidth', '0');
+            this.allFields.forEach(aField => {
+                aField.updateDocSetting('backgroundColor', 'transparent');
+                aField.updateDocSetting('borderWidth', '0');
             });
         }
 
@@ -138,7 +109,7 @@ export class Template {
     /**
      * This function is just a hack for now to get around weird document icon stuff (specifically it misses the background)
      */
-     setMatteBackground(makeTransparent: boolean = false) {
+    setMatteBackground(makeTransparent: boolean = false) {
         if (this._mainField.hasBackground) {
             return;
         }
@@ -146,18 +117,17 @@ export class Template {
         const fieldSettings: FieldSettings = {
             tl: [-1, -1],
             br: [1, 1],
-            opts: {backgroundColor: String(this._mainField.renderedDoc!.backgroundColor)},
+            opts: { backgroundColor: String(this._mainField.renderedDoc!.backgroundColor) },
             viewType: ViewType.TEXT,
-        }
+        };
 
-        const field: TemplateField = TemplateField.CreateField(fieldSettings, Math.random() * 100 + 100, this._mainField);
+        const field = TemplateField.CreateField(fieldSettings, Math.random() * 100 + 100, this._mainField);
 
-        if (makeTransparent) {
-            this.allFields.forEach(field => {
-                field.updateDocSetting('backgroundColor', 'transparent');
-                field.updateDocSetting('borderWidth', '0');
+        makeTransparent &&
+            this.allFields.forEach(aField => {
+                aField.updateDocSetting('backgroundColor', 'transparent');
+                aField.updateDocSetting('borderWidth', '0');
             });
-        }
 
         this._mainField.makeBackgroundField(field);
     }
@@ -167,9 +137,7 @@ export class Template {
 
         if (cols.length !== numFields) return [];
 
-        const matches: number[][] = Array(numFields)
-            .fill([])
-            .map(() => []);
+        const matches = Array<number[]>(numFields);
 
         this.contentFields.forEach((field, i) => (matches[i] = field.matches(cols)));
 
@@ -180,8 +148,8 @@ export class Template {
         if (matches.length === 0) return 0;
 
         const fieldsCt = this.contentFields.length;
-        const used: boolean[] = Array(fieldsCt).fill(false);
-        const mt: number[] = Array(fieldsCt).fill(-1);
+        const used = Array<boolean>(fieldsCt).fill(false);
+        const mt = Array<number>(fieldsCt).fill(-1);
 
         const augmentingPath = (v: number): boolean => {
             if (!used[v]) {
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts
index c100d1cce..091ef834a 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts
@@ -67,13 +67,13 @@ export abstract class TemplateField {
     setTitle = (title: string) => {
         this._title = title;
         this.settings.title = title;
-        if (this._renderDoc) this._renderDoc.title = title
+        if (this._renderDoc) this._renderDoc.title = title;
     };
     getTitle = () => this._title;
 
     updateDocSetting(setting: string, newVal: string) {
         if (this._renderDoc) this._renderDoc[setting] = newVal;
-        const settings: {[s: string]: string } = {[setting]: newVal}
+        const settings: { [s: string]: string } = { [setting]: newVal };
         Object.assign(this.settings.opts, settings);
     }
 
@@ -162,7 +162,7 @@ export enum ViewType {
     DEC = 'decoration',
     IMG = 'image',
     TEXT = 'text',
-    NONE = 'none'
+    NONE = 'none',
 }
 
 export type FieldDimensions = {
@@ -170,4 +170,3 @@ export type FieldDimensions = {
     height: number;
     coord: { x: number; y: number };
 };
-
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 30fc44f62..8ed59c6e1 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -175,6 +175,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }),
             ({ nativeSize, width, height }) => {
                 if (!this.layoutDoc._layout_nativeDimEditable || !height || this.layoutDoc.layout_resetNativeDim) {
+                    if (!this._props.TemplateDataDocument) this.layoutDoc._nativeWidth = this.layoutDoc._nativeHeight = undefined;
                     this.layoutDoc.layout_resetNativeDim = undefined; // reset dimensions of templates rendered with content or if image changes.  afterwards, remove this flag.
                     this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
                 }
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From 42b35b687f081e579cbec524426105df3ac695ef Mon Sep 17 00:00:00 2001
From: sharkiecodes <lanyi_stroud@brown.edu>
Date: Thu, 5 Jun 2025 22:48:33 -0400
Subject: attempting marqueeview gen of multiple scrapbooks

---
 src/client/apis/gpt/GPT.ts                         |  15 +-
 .../collectionFreeForm/MarqueeView.scss            |   2 +-
 .../collections/collectionFreeForm/MarqueeView.tsx | 191 +++++++++++++++++++--
 src/client/views/nodes/ImageBox.tsx                |   6 +
 src/client/views/nodes/VideoBox.tsx                |   3 +-
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx  | 165 +++++++++---------
 .../views/nodes/scrapbook/ScrapbookPicker.scss     |  40 +++++
 .../views/nodes/scrapbook/ScrapbookPicker.tsx      |  84 +++++++++
 .../views/nodes/scrapbook/ScrapbookPreset.tsx      |  44 ++---
 .../views/nodes/scrapbook/scrapbookleftover.ts     |  46 +++++
 10 files changed, 474 insertions(+), 122 deletions(-)
 create mode 100644 src/client/views/nodes/scrapbook/ScrapbookPicker.scss
 create mode 100644 src/client/views/nodes/scrapbook/ScrapbookPicker.tsx
 create mode 100644 src/client/views/nodes/scrapbook/scrapbookleftover.ts

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 03fce21f7..4642d79eb 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -14,7 +14,7 @@ export const DocSeperator = '------';
 export enum TextClassifications {
     Title = 'title', //a few words 
     Caption = 'caption', //few sentences
-    LengthyDescription = 'lengthy description' }
+    LengthyDescription = 'lengthy' }
 
 enum GPTCallType {
     SUMMARY = 'summary',
@@ -106,8 +106,10 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
         model: 'gpt-4o',
         maxTokens: 2048,
         temp: 0.25,
-        prompt: `Based on the content of the text, provide six descriptive tags (single words) separated by spaces. 
-            Finally, include a seventh more detailed summary phrase using underscores.`
+        prompt: `Based on the content of the the text, classify it into the  
+                most appropriate category: '${TextClassifications.Title}', '${TextClassifications.Caption}', or '${TextClassifications.LengthyDescription}'. 
+               Then provide five more descriptive tags (single words) separated by spaces. 
+            Finally, include a more detailed summary phrase tag using underscores, for a total of seven tags.`
     },
     describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
     flashcard: {
@@ -175,14 +177,15 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
          prompt: `Generate an aesthetically pleasing scrapbook layout preset based on these items.
     Return your response as JSON in the format:
     [{
-        "type": DocumentType.RTF or DocumentType.IMG or DocumentType.PDF
+        "type": rich text or image or pdf or video or collection
         "tag": a singular tag summarizing the document
+        "acceptTags": [a list of all relevant tags that this document accepts, like ['PERSON', 'LANDSCAPE']]
         "x": number,
         "y": number,
-        "width": number,
+        "width": number, **note: if it is in an image, please respect existing aspect ratio if it is provided
         "height": number
     }, ...]
-    If there are mutliple documents, you may include 
+    If there are mutliple documents and you wish to nest documents into a collection for aesthetic purposes, you may include 
     "children": [
                     { type: 
                       tag: 
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 7c9d0f6e1..b514b0911 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -28,4 +28,4 @@
     .marquee-legend::after {
         content: 'Press <space> for lasso';
     }
-}
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 0b91d628b..05d4cd81d 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -22,6 +22,7 @@ import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { MarqueeViewBounds } from '../../PinFuncs';
 import { PreviewCursor } from '../../PreviewCursor';
 import { DocumentView } from '../../nodes/DocumentView';
+import { OverlayDisposer } from '../../OverlayView';
 import { OpenWhere } from '../../nodes/OpenWhere';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -30,8 +31,15 @@ import { ImageLabelBoxData } from './ImageLabelBox';
 import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 import { StrListCast } from '../../../../fields/Doc';
 import { requestAiGeneratedPreset, DocumentDescriptor } from '../../nodes/scrapbook/AIPresetGenerator';
+import { ScrapbookItemConfig } from '../../nodes/scrapbook/ScrapbookPreset';
+import { OverlayView } from '../../OverlayView';
+import { runInAction } from 'mobx';
+import { ScrapbookPicker } from '../../nodes/scrapbook/ScrapbookPicker';
+import { buildPlaceholdersFromConfigs, slotRealDocIntoPlaceholders } from '../../nodes/scrapbook/ScrapbookBox';
 import './MarqueeView.scss';
 
+import { build } from 'xregexp';
+
 interface MarqueeViewProps {
     Doc: Doc;
     getContainerTransform: () => Transform;
@@ -78,6 +86,16 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     @observable _labelsVisibile: boolean = false;
     @observable _lassoPts: [number, number][] = [];
     @observable _lassoFreehand: boolean = false;
+      // ─── New Observables for “Pick 1 of N AI Scrapbook” ───
+    @observable aiChoices: Doc[] = [];      // temporary hidden Scrapbook docs
+    @observable pickerX = 0;                // popup x coordinate
+    @observable pickerY = 0;                // popup y coordinate
+    @observable pickerVisible = false;      // show/hide ScrapbookPicker
+    // This will hold the “disposer” function returned by addWindow()
+    private _overlayDisposer: OverlayDisposer | null = null;
+
+
+    
 
     @computed get Transform() {
         return this._props.getTransform();
@@ -277,7 +295,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             MarqueeOptionsMenu.Instance.createCollection = this.collection;
             MarqueeOptionsMenu.Instance.delete = this.delete;
             MarqueeOptionsMenu.Instance.summarize = this.summary;
-            MarqueeOptionsMenu.Instance.generateScrapbook = this.generateScrapbook;
+            MarqueeOptionsMenu.Instance.generateScrapbook = this.generateAiScrapbooks;
             MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee;
             MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee;
             MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY);
@@ -521,6 +539,17 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     });
 
 
+    getAiPresetsDescriptors(): DocumentDescriptor[] {
+    const selected = this.marqueeSelect(false);
+    return selected.map((doc) => ({
+      type: typeof doc.$type === 'string' ? doc.$type : 'UNKNOWN',
+      tags: (() => {
+        const s = new Set<string>();
+        StrListCast(doc.$tags_chat ?? new List<string>()).forEach((t) => s.add(t));
+        return Array.from(s);
+      })(),
+    }));
+  }
    
     
     generateScrapbook = action(async () => {
@@ -528,28 +557,23 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         const selectedDocs = this.marqueeSelect(false);
         if (!selectedDocs.length) return;
 
-       const descriptors: DocumentDescriptor[] = selectedDocs.map(doc => ({
-            type: typeof doc.$type === 'string' ? doc.$type : 'UNKNOWN',
-            tags: (() => {
-                const internalTagsSet = new Set<string>();
-                StrListCast(doc.$tags_chat ?? new List<string>()).forEach(tag => {
-                internalTagsSet.add(tag);
-                });
-                return Array.from(internalTagsSet);
-            })()
-            }));
-
+    const descriptors = this.getAiPresetsDescriptors();
+    if (descriptors.length === 0) {
+      alert('No documents selected to generate a scrapbook from!');
+      return;
+    }
 
         const aiPreset = await requestAiGeneratedPreset(descriptors);
         if (!aiPreset.length) {
             alert("Failed to generate preset");
             return;
         }
-
+        const scrapbookPlaceholders: Doc[] = buildPlaceholdersFromConfigs(aiPreset);
+        /*
         const scrapbookPlaceholders: Doc[] = aiPreset.map(cfg => {
             const placeholderDoc = Docs.Create.TextDocument(cfg.tag);
             placeholderDoc.accepts_docType = cfg.type as DocumentType;
-            placeholderDoc.accepts_tagType = cfg.acceptTag ?? cfg.tag;
+            placeholderDoc.accepts_tagType = new List<string>(cfg.acceptTags ?? [cfg.tag]);
 
             const placeholder = new Doc();
             placeholder.proto = placeholderDoc;
@@ -560,7 +584,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             if (cfg.height != null) placeholder._height = cfg.height;
 
             return placeholder;
-        });
+        });*/
 
         const scrapbook = Docs.Create.ScrapbookDocument(scrapbookPlaceholders, {
             backgroundColor: '#e2ad32',
@@ -570,6 +594,25 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             _height: 500,
             title: 'AI-generated Scrapbook'
         });
+
+
+
+        // 3) Now grab that new scrapbook’s flat placeholders
+        const flatPl = DocListCast(scrapbook[Doc.LayoutDataKey(scrapbook)]) as Doc[];
+        const unwrap = (items: Doc[]): Doc[] =>
+            items.flatMap(d =>
+            d.$type === DocumentType.COL
+                ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)]))
+                : [d]
+            );
+        const allPlaceholders = unwrap(flatPl);
+
+        // 4) Slot each selectedDocs[i] into the first matching placeholder
+        selectedDocs.forEach(realDoc => {
+            slotRealDocIntoPlaceholders(realDoc, allPlaceholders
+            );
+        });
+
         const selected = this.marqueeSelect(false).map(d => {
             this._props.removeDocument?.(d);
             d.x = NumCast(d.x) - this.Bounds.left;
@@ -589,6 +632,102 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     });
 
 
+  
+
+  /** Called when the user clicks one of the N thumbnails */
+  @action
+  onScrapbookChoice(idx: number) {
+    const chosenDoc = this.aiChoices[idx];
+    if (!chosenDoc) return;
+
+    // 1) Move chosenDoc from off‐screen to the marquee area
+    const bounds = this.Bounds;
+    chosenDoc.x = bounds.left;
+    chosenDoc.y = bounds.top;
+    chosenDoc._width = NumCast(bounds.width || chosenDoc._width);
+    chosenDoc._height = NumCast(bounds.height || chosenDoc._height);
+    chosenDoc.$title = 'AI‐chosen scrapbook';
+
+    // 2) Remove the other temp docs
+    this.aiChoices.forEach((doc, i) => {
+      if (i !== idx) {
+        this.props.removeDocument?.(doc);
+      }
+    });
+
+    // 3) Clear state and close the popup
+    this.aiChoices = [];
+    this.pickerVisible = false;
+  }
+
+  /** Called when user clicks outside or the “×” in ScrapbookPicker */
+  @action
+  cancelScrapbookChoice() {
+    // 1) Remove all temp scrapbooks
+    this.aiChoices.forEach((doc) => {
+      this.props.removeDocument?.(doc);
+    });
+
+    // 2) Clear array and hide popup
+    this.aiChoices = [];
+    this.pickerVisible = false;
+  }
+  @action
+  generateAiScrapbooks = async () => {
+    const n = 3; // Number of AI scrapbook presets
+    const descriptors = this.getAiPresetsDescriptors();
+    if (descriptors.length === 0) {
+      alert('No documents selected to generate a scrapbook from!');
+      return;
+    }
+
+    // 1) Start N parallel requests for JSON configs
+    const calls: Promise<ScrapbookItemConfig[]>[] = [];
+    for (let i = 0; i < n; i++) {
+      calls.push(requestAiGeneratedPreset(descriptors));
+    }
+
+    // 2) Optionally show a “spinner” overlay in‐line here if you like…
+    //    (But for brevity, let’s omit that.)
+
+    try {
+      const allConfigsArrays = await Promise.all(calls);
+
+      runInAction(() => {
+        // 3) For each returned config array, build placeholders and create _hidden_ Scrapbook doc
+        this.aiChoices = allConfigsArrays.map((cfgArr, i) => {
+          const placeholders = buildPlaceholdersFromConfigs(cfgArr);
+          // Off‐screen ScrapbookDocument:
+          const tempSb: Doc = Docs.Create.ScrapbookDocument(placeholders, {
+            title: `AI Preset (temp ${i + 1})`,
+            x: -10000, // far off screen
+            y: -10000,
+            _width: 600,
+            _height: 600,
+            backgroundColor: 'transparent',
+            _layout_showFlash: false, // keep it hidden until chosen
+          });
+          this.props.addDocument?.(tempSb);
+          tempSb.$tags_chat = new List<string>(['@ai_preset']);
+          return tempSb;
+        });
+
+        // 4) Compute where to show the popup in screen coords
+        const bounds = this.Bounds;
+        const screenTopLeft = this._props
+          .getContainerTransform()
+          .transformPoint(bounds.left, bounds.top);
+        this.pickerX = screenTopLeft[0] + 20;
+        this.pickerY = screenTopLeft[1] + 20;
+        this.pickerVisible = true;
+      });
+    } catch (err) {
+      console.error('Error generating AI scrapbooks:', err);
+      alert('Failed to generate presets. Please try again.');
+    }
+  };
+
+
     @action
     marqueeCommand = (e: KeyboardEvent) => {
         const ee = e as unknown as KeyboardEvent & { propagationIsStopped?: boolean };
@@ -755,6 +894,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     };
     render() {
         return (
+            <>
             <div
                 className="marqueeView"
                 ref={r => {
@@ -774,6 +914,27 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
                 {this._visible ? this.marqueeDiv : null}
                 {this.props.children}
             </div>
+            {this.pickerVisible && (
+  <div
+    className="marqueeView-scrapbook-overlay"
+    style={{
+      position: 'absolute',
+      top: this.pickerY,
+      left: this.pickerX,
+      zIndex: 2000,         // just ensure it floats above
+    }}
+  >
+    <ScrapbookPicker
+      choices={this.aiChoices}
+      x={this.pickerX}
+      y={this.pickerY}
+      onSelect={(i) => this.onScrapbookChoice(i)}
+      onCancel={() => this.cancelScrapbookChoice()}
+    />
+  </div>
+)}
+        </>
+    
         );
     }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9067f7e0c..2473f1c0a 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -141,6 +141,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         that describes the image.`
       );
 
+    const { nativeWidth, nativeHeight } = this.nativeSize;
+    const aspectRatio = nativeWidth && nativeHeight
+      ? (nativeWidth / nativeHeight).toFixed(2)
+      : '1.00';
+
       // 4) normalize and prefix
       const label = raw
         .trim()
@@ -152,6 +157,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
       this.Document.$tags_chat = new List<string>();
       tokens.forEach(tok => {
         (this.Document.$tags_chat as List<string>).push(tok)});
+        (this.Document.$tags_chat as List<string>).push(`ASPECT_${aspectRatio}`);  
       //!!! changed may 11 (this.Document.$tags_chat as List<string>).push(label);
 
       // 6) flip on “show tags” in the layout 
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 0e7afbab1..404be6a1b 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -151,7 +151,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const tokens = label.split(/\s+/);
         this.Document.$tags_chat = new List<string>();
         tokens.forEach(tok => (this.Document.$tags_chat as List<string>).push(tok));
-
+        const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1);
+        (this.Document.$tags_chat as List<string>).push(`ASPECT_${aspect}`);  
         // 5) Turn on tag display in layout
         this.Document._layout_showTags = true;
 
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index 5dd02295c..eb997024b 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -3,7 +3,6 @@ import * as React from 'react';
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { emptyFunction } from '../../../../Utils';
-import axios from 'axios';
 import { Docs } from '../../../documents/Documents';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { CollectionView } from '../../collections/CollectionView';
@@ -35,54 +34,51 @@ export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]): Do
 
   for (const cfg of configs) {
     if (cfg.children && cfg.children.length) {
-      // --- nested container ---
       const childDocs = cfg.children.map(child => {
         const doc = Docs.Create.TextDocument("[placeholder] " + child.tag);
         doc.accepts_docType = child.type;
+        doc.accepts_tagType = new List<string>(child.acceptTags ?? [child.tag]);
 
         const ph = new Doc();
-        ph.proto    = doc;
+        ph.proto = doc;
         ph.original = doc;
-        ph.x        = child.x;
-        ph.y        = child.y;
-        if (child.width  != null) ph._width  = child.width;
+        ph.x = child.x;
+        ph.y = child.y;
+        if (child.width != null) ph._width = child.width;
         if (child.height != null) ph._height = child.height;
         return ph;
       });
 
-      // wrap those children in a stacking container
       const protoW = cfg.containerWidth ?? cfg.width;
       const protoH = cfg.containerHeight ?? cfg.height;
-      const containerProto = Docs.Create.StackingDocument(
-        childDocs,
-        {
-          ...(protoW != null ? { _width:  protoW } : {}),
-          ...(protoH != null ? { _height: protoH } : {}),
-          title: cfg.tag
-        }
-      );
+      // Create a stacking document with the child placeholders
+      const containerProto = Docs.Create.StackingDocument(childDocs, {
+        ...(protoW != null ? { _width: protoW } : {}),
+        ...(protoH != null ? { _height: protoH } : {}),
+        title: cfg.tag,
+      });
 
       const ph = new Doc();
-      ph.proto    = containerProto;
+      ph.proto = containerProto;
       ph.original = containerProto;
-      ph.x        = cfg.x;
-      ph.y        = cfg.y;
-      if (cfg.width  != null) ph._width  = cfg.width;
+      ph.x = cfg.x;
+      ph.y = cfg.y;
+      if (cfg.width != null) ph._width = cfg.width;
       if (cfg.height != null) ph._height = cfg.height;
       placeholders.push(ph);
-
-    } else {
-      // --- flat placeholder ---
+    } 
+    
+    else {
       const doc = Docs.Create.TextDocument("[placeholder] " + cfg.tag);
       doc.accepts_docType = cfg.type;
-      doc.accepts_tagType = cfg.acceptTag ?? cfg.tag;
+      doc.accepts_tagType = new List<string>(cfg.acceptTags ?? [cfg.tag]);
 
       const ph = new Doc();
-      ph.proto    = doc;
+      ph.proto = doc;
       ph.original = doc;
-      ph.x        = cfg.x;
-      ph.y        = cfg.y;
-      if (cfg.width  != null) ph._width  = cfg.width;
+      ph.x = cfg.x;
+      ph.y = cfg.y;
+      if (cfg.width != null) ph._width = cfg.width;
       if (cfg.height != null) ph._height = cfg.height;
       placeholders.push(ph);
     }
@@ -90,7 +86,54 @@ export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]): Do
 
   return placeholders;
 }
-// Scrapbook view: a container that lays out its child items in a grid/template
+export function slotRealDocIntoPlaceholders(
+  realDoc: Doc,
+  placeholders: Doc[]
+): boolean {
+ const realTags = new Set<string>(
+  StrListCast(realDoc.$tags_chat ?? new List<string>())
+    .map(t => t.toLowerCase())
+);
+
+  // Find placeholder with most matching tags
+  let bestMatch: Doc | null = null;
+  let maxMatches = 0;
+/*
+                 (d.accepts_docType === docs[0].$type ||   // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type
+                  RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type)))*/
+
+  placeholders.forEach(ph => {
+    // 1) Enforce that placeholder.accepts_docType === realDoc.$type
+    if (ph.accepts_docType !== realDoc.$type) {
+      // Skip this placeholder entirely if types do not match.
+      return;
+    };
+    const phTagTypes = StrListCast(ph.accepts_tagType ?? new List<string>())
+  .map(t => t.toLowerCase());
+    console.log({ realTags, phTagTypes });
+    const matches = phTagTypes.filter(tag => realTags.has(tag));
+
+    if (matches.length > maxMatches) {
+      maxMatches = matches.length;
+      bestMatch = ph;
+    }
+
+  });
+
+  if (bestMatch && maxMatches > 0) {
+    setTimeout(
+      undoable(() => {
+        bestMatch!.proto = realDoc;
+      }, 'Scrapbook add'),
+      0
+    );
+    return true;
+  }
+
+  return false;
+}
+
+// Scrapbook view: a container that lays out its child items in a template
 @observer
 export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable selectedPreset = getPresetNames()[0];
@@ -406,56 +449,19 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
         // 3) produce a flat list of every doc, unwrapping any number of nested COLs
         const allDocs: Doc[] = unwrap(slots);
-     
+          if (docs?.length === 1) {
+        return slotRealDocIntoPlaceholders(
+            docs[0],
+            allDocs,
+        )
+            ? false
+            : false;
+    }
 
-        if (docs?.length === 1) {
-            const placeholder = allDocs.filter(d =>
+    return false;
+};
+     
 
-                 (d.accepts_docType === docs[0].$type ||   // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type
-                  RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type)))
-            ); // prettier-ignore
-
-            //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)])
-
-            if (placeholder) {
-                /**Look at the autotags and see what matches*RTFCast(d[Doc.LayoutDataKey(d)])?.Text*/
-                // ugh.  we have to tell the underlying view not to add the Doc so that we can add it where we want it.
-                // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo.
-                setTimeout(
-                    undoable(() => {
-
-                        const slotTagsList: Set<string>[] = placeholder.map(doc =>
-                            new Set<string>(StrListCast(doc.$tags_chat))
-                          );
-                          // turn docs[0].$tags_chat into a Set
-                        const targetTags = new Set(StrListCast(docs[0].$tags_chat));
-
-                        //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field])); // // shouldn't need to do this for layout fields since the placeholder already overrides its protos
-                        
-                            // find the first placeholder that shares *any* tag
-                        const match = placeholder.find(ph =>
-                            ph.accepts_tagType != null &&          // make sure it actually has one
-                            targetTags.has(StrCast(ph.accepts_tagType))     // test membership in the Set
-                            //StrListCast(ph.$tags_chat).some(tag => targetTags.has(tag))
-                        );
-                        if (match) {
-                            match.proto = docs[0];
-                          }
-
-                        /*const chosenPlaceholder = placeholder.find(d => 
-                            pl = new Set<string>(StrListCast(d.$tags_chat)
-                            
-                            d.$tags_chat && d.$tags_chat[0].equals(docs[0].$tags_chat)); //why [0]
-                        if (chosenPlaceholder){
-                            chosenPlaceholder.proto = docs[0];}*/
-                         //excess if statement??   
-                    }, 'Scrapbook add')
-                );
-                return false;
-            }
-        }
-        return false;
-    };
 
 
 
@@ -474,10 +480,15 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         
         allDocs.forEach(doc => {
             const tags = StrListCast(doc.$tags_chat ?? new List<string>());
-            tags.forEach(tag => internalTagsSet.add(tag));
+            tags.forEach(tag => 
+                 {if (!tag.startsWith("ASPECT_")) {
+            internalTagsSet.add(tag);
+            }
+    });
         });
     
         const internalTags = Array.from(internalTagsSet).join(', ');
+      
     
         return internalTags
             ? `Create a new scrapbook background featuring: ${internalTags}`
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPicker.scss b/src/client/views/nodes/scrapbook/ScrapbookPicker.scss
new file mode 100644
index 000000000..237274433
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookPicker.scss
@@ -0,0 +1,40 @@
+/* ScrapbookPicker.scss */
+
+.scrapbook-picker-popup {
+    background: rgba(255, 0, 0, 0.5);  /* semi-transparent red */
+  position: absolute;       /* ← make it float */
+  z-index: 10000;           /* so it sits above the overlay‐window’s background */
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+  padding: 8px;
+  min-width: 200px;         /* at least give it some size */
+}
+
+.scrapbook-picker-close {
+  position: absolute;
+  top: 4px;
+  right: 8px;
+  cursor: pointer;
+  font-size: 14px;
+}
+
+.scrapbook-picker-thumbnails {
+  margin-top: 24px;   /* room under the close button */
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.scrapbook-picker-thumb {
+  cursor: pointer;
+  border: 1px solid #ddd;
+  border-radius: 2px;
+  padding: 4px;
+  background: #f9f9f9;
+}
+
+.scrapbook-picker-thumb-inner {
+  font-size: 12px;
+  text-align: center;
+}
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPicker.tsx b/src/client/views/nodes/scrapbook/ScrapbookPicker.tsx
new file mode 100644
index 000000000..6054cb98d
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookPicker.tsx
@@ -0,0 +1,84 @@
+// src/client/views/nodes/scrapbook/ScrapbookPicker.tsx
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { Doc } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
+import { StrCast } from '../../../../fields/Types';
+import './ScrapbookPicker.scss';
+
+export interface ScrapbookPickerProps {
+  choices: Doc[];
+  x: number;
+  y: number;
+  onSelect: (index: number) => void;
+  onCancel: () => void;
+}
+
+/**
+ * A floating popup that shows N “temporary” Scrapbook documents.  
+ * When the user clicks one thumbnail, we call onSelect(i).  
+ * When the user clicks × or outside, we call onCancel(). 
+ *
+ * This component itself does not control its own visibility; MarqueeView / OverlayView will mount/unmount it.
+ */
+@observer
+export class ScrapbookPicker extends React.Component<ScrapbookPickerProps> {
+  containerRef = React.createRef<HTMLDivElement>();
+
+  // Close when user clicks outside the popup
+  handleClickOutside = (e: MouseEvent) => {
+    if (
+      this.containerRef.current &&
+      !this.containerRef.current.contains(e.target as Node)
+    ) {
+      this.props.onCancel();
+    }
+  };
+
+  componentDidMount() {
+    document.addEventListener('mousedown', this.handleClickOutside);
+  }
+  componentWillUnmount() {
+    document.removeEventListener('mousedown', this.handleClickOutside);
+  }
+
+  render() {
+    const { choices, x, y, onSelect, onCancel } = this.props;
+    return (
+      <div
+        className="scrapbook-picker-popup"
+        ref={this.containerRef}
+        style={{
+          top: `${y}px`,
+          left: `${x}px`,
+        }}
+      >
+        {/* close icon */}
+        <div className="scrapbook-picker-close" onClick={onCancel}>
+          ×
+        </div>
+        <div className="scrapbook-picker-thumbnails">
+          {choices.map((doc, i) => {
+            // We simply show a small thumbnail representation of each temp scrapbook
+            // You could replace this with DocumentThumbnail or a custom mini‐preview.
+            return (
+              <div
+                key={`${doc[Id]}_${i}`}
+                className="scrapbook-picker-thumb"
+                onClick={() => onSelect(i)}
+              >
+                {/* 
+                  For a minimal example, use the document’s title or ID as a placeholder.
+                  In a real version, you might render a proper thumbnail/view of doc.
+                */}
+                <div className="scrapbook-picker-thumb-inner">
+                  {StrCast(doc.title)}
+                </div>
+              </div>
+            );
+          })}
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
index 87821c7bf..96a8e9b5f 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
@@ -13,7 +13,7 @@ export interface ScrapbookItemConfig {
     /** text shown in the placeholder bubble */
     tag: string;
     /** what this slot actually accepts (defaults to `tag`) */
-    acceptTag?: string;
+    acceptTags?: string[];
 
     x: number;
     y: number;
@@ -48,22 +48,22 @@ export class ScrapbookPreset {
         return [
             { type: DocumentType.IMG,
               tag: '[placeholder] LANDSCAPE',
-              acceptTag: 'LANDSCAPE',
+              acceptTags: ['LANDSCAPE'],
               x: 0, y: -100, width: 250, height: 200
             },
             { type: DocumentType.RTF,
               tag: '[placeholder] caption',
-              acceptTag: 'caption',
+              acceptTags: ['caption'],
               x: 0, y: 200, width: 250, height: 50
             },
             { type: DocumentType.RTF,
               tag: '[placeholder] lengthy description',
-              acceptTag: 'lengthy description',
+              acceptTags: ['lengthy description'],
               x: 280, y: -50, width: 50, height: 200
             },
             { type: DocumentType.IMG,
               tag: '[placeholder] PERSON',
-              acceptTag: 'PERSON',
+              acceptTags: ['PERSON'],
               x: -200, y: -100, width: 100, height: 200
             },
         ];
@@ -71,12 +71,12 @@ export class ScrapbookPreset {
 
     private static createGalleryPreset(): ScrapbookItemConfig[] {
         return [
-          { type: DocumentType.IMG, tag: 'Gallery 1', acceptTag: 'LANDSCAPE', x: -150, y: -150, width: 150, height: 150 },
-          { type: DocumentType.IMG, tag: 'Gallery 2', acceptTag: 'LANDSCAPE', x:   0, y: -150, width: 150, height: 150 },
-          { type: DocumentType.IMG, tag: 'Gallery 3', acceptTag: 'LANDSCAPE', x: 150, y: -150, width: 150, height: 150 },
-          { type: DocumentType.IMG, tag: 'Gallery 4', acceptTag: 'LANDSCAPE', x: -150, y:   0, width: 150, height: 150 },
-          { type: DocumentType.IMG, tag: 'Gallery 5', acceptTag: 'LANDSCAPE', x:   0, y:   0, width: 150, height: 150 },
-          { type: DocumentType.IMG, tag: 'Gallery 6', acceptTag: 'LANDSCAPE', x: 150, y:   0, width: 150, height: 150 },
+          { type: DocumentType.IMG, tag: 'Gallery 1', acceptTags: ['LANDSCAPE'], x: -150, y: -150, width: 150, height: 150 },
+          { type: DocumentType.IMG, tag: 'Gallery 2', acceptTags: ['LANDSCAPE'], x:   0, y: -150, width: 150, height: 150 },
+          { type: DocumentType.IMG, tag: 'Gallery 3', acceptTags: ['LANDSCAPE'], x: 150, y: -150, width: 150, height: 150 },
+          { type: DocumentType.IMG, tag: 'Gallery 4', acceptTags: ['LANDSCAPE'], x: -150, y:   0, width: 150, height: 150 },
+          { type: DocumentType.IMG, tag: 'Gallery 5', acceptTags: ['LANDSCAPE'], x:   0, y:   0, width: 150, height: 150 },
+          { type: DocumentType.IMG, tag: 'Gallery 6', acceptTags: ['LANDSCAPE'], x: 150, y:   0, width: 150, height: 150 },
         ];
       }
       
@@ -85,17 +85,17 @@ export class ScrapbookPreset {
         return [
             { type: DocumentType.IMG,
               tag: 'image',
-              acceptTag: 'LANDSCAPE',
+              acceptTags: ['LANDSCAPE'],
               x: 0, y: -100, width: 250, height: 200
             },
             { type: DocumentType.RTF,
               tag: 'summary',
-              acceptTag: 'caption',
+              acceptTags: ['caption'],
               x: 0, y: 200, width: 250
             },
             { type: DocumentType.RTF,
               tag: 'sidebar',
-              acceptTag: 'lengthy description',
+              acceptTags: ['lengthy description'],
               x: 280, y: -50, width: 50, height: 200
             },
             {
@@ -106,7 +106,7 @@ export class ScrapbookPreset {
               children: [
                 { type: DocumentType.IMG,
                   tag: 'image internal',
-                  acceptTag: 'PERSON',
+                  acceptTags: ['PERSON'],
                   x: 0, y: 0, width: 50, height: 100
                 }
               ]
@@ -118,22 +118,22 @@ export class ScrapbookPreset {
         return [
             { type: DocumentType.IMG,
               tag: 'LANDSCAPE',
-              acceptTag: 'LANDSCAPE',
+              acceptTags: ['LANDSCAPE'],
               x: -150, y: -150, width: 150, height: 150
             },
             { type: DocumentType.IMG,
               tag: 'PERSON',
-              acceptTag: 'PERSON',
+              acceptTags: ['PERSON'],
               x: 0, y: -150, width: 150, height: 150
             },
             { type: DocumentType.RTF,
               tag: 'caption',
-              acceptTag: 'caption',
+              acceptTags: ['caption'],
               x: -150, y: 0, width: 300, height: 100
             },
             { type: DocumentType.RTF,
               tag: 'lengthy description',
-              acceptTag: 'lengthy description',
+              acceptTags: ['lengthy description'],
               x: 0, y: 100, width: 300, height: 100
             }
         ];
@@ -143,17 +143,17 @@ export class ScrapbookPreset {
         return [
             { type: DocumentType.RTF,
               tag: 'title',
-              acceptTag: 'title',
+              acceptTags: ['title'],
               x: 0, y: -180, width: 300, height: 40
             },
             { type: DocumentType.IMG,
               tag: 'LANDSCAPE',
-              acceptTag: 'LANDSCAPE',
+              acceptTags: ['LANDSCAPE'],
               x: 0, y: 0, width: 300, height: 200
             },
             { type: DocumentType.RTF,
               tag: 'caption',
-              acceptTag: 'caption',
+              acceptTags: ['caption'],
               x: 0, y: 230, width: 300, height: 50
             }
         ];
diff --git a/src/client/views/nodes/scrapbook/scrapbookleftover.ts b/src/client/views/nodes/scrapbook/scrapbookleftover.ts
new file mode 100644
index 000000000..2f381ab95
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/scrapbookleftover.ts
@@ -0,0 +1,46 @@
+
+   
+
+
+            //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)])
+
+            if (placeholder) {
+                /**Look at the autotags and see what matches*RTFCast(d[Doc.LayoutDataKey(d)])?.Text*/
+                // ugh.  we have to tell the underlying view not to add the Doc so that we can add it where we want it.
+                // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo.
+                setTimeout(
+                    undoable(() => {
+
+                        const slotTagsList: Set<string>[] = placeholder.map(doc =>
+                            new Set<string>(StrListCast(doc.$tags_chat))
+                          );
+                          // turn docs[0].$tags_chat into a Set
+                        const targetTags = new Set(StrListCast(docs[0].$tags_chat));
+
+                        //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field]));
+                        //  // // shouldn't need to do this for layout fields since the placeholder already overrides its protos
+                        
+                            // find the first placeholder that shares *any* tag
+                        const match = placeholder.find(ph =>
+                            ph.accepts_tagType != null &&          // make sure it actually has one
+                            targetTags.has(StrCast(ph.accepts_tagType))     // test membership in the Set
+                            //StrListCast(ph.$tags_chat).some(tag => targetTags.has(tag))
+                        );
+                        if (match) {
+                            match.proto = docs[0];
+                          }
+
+                        /*const chosenPlaceholder = placeholder.find(d => 
+                            pl = new Set<string>(StrListCast(d.$tags_chat)
+                            
+                            d.$tags_chat && d.$tags_chat[0].equals(docs[0].$tags_chat)); //why [0]
+                        if (chosenPlaceholder){
+                            chosenPlaceholder.proto = docs[0];}*/
+                         //excess if statement??   
+                    }, 'Scrapbook add')
+                );
+                return false;
+            }
+        }
+        return false;
+    };
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From bf33580a66c1f8ce87e85bea701415788a887401 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 11 Jun 2025 11:50:45 -0400
Subject: change how autoTag is triggered for images to not use a reaction

---
 src/client/views/nodes/ImageBox.tsx                | 83 +++++++++-------------
 src/client/views/search/FaceRecognitionHandler.tsx |  5 +-
 2 files changed, 34 insertions(+), 54 deletions(-)

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

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 1e16bbfc9..d7e21b0a6 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -142,56 +142,39 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     autoTag = async () => {
-
-    try {
-      // 1) grab the full-size URL
-      const layoutKey = Doc.LayoutDataKey(this.Document);
-      const url = ImageCastWithSuffix(this.Document[layoutKey], '_o') ?? '';
-      if (!url) throw new Error('No image URL found');
-
-      // 2) convert to base64
-      const base64 = await imageUrlToBase64(url);
-      if (!base64) throw new Error('Failed to load image data');
-
-      // 3) ask GPT for labels one label: PERSON or LANDSCAPE
-      const raw = await gptImageLabel(
-        base64,
-        `Classify this image as PERSON or LANDSCAPE.  You may only respond with one of these two options. Then
-        provide five additional descriptive tags to describe the image for a total of 6 words outputted, 
-        delimited by spaces. For example: "LANDSCAPE BUNNY NATURE FOREST PEACEFUL OUTDOORS". Then add one final lengthier summary tag (separated by underscores)
-        that describes the image.`
-      );
-
-    const { nativeWidth, nativeHeight } = this.nativeSize;
-    const aspectRatio = nativeWidth && nativeHeight
-      ? (nativeWidth / nativeHeight).toFixed(2)
-      : '1.00';
-
-      // 4) normalize and prefix
-      const label = raw
-        .trim()
-        .toUpperCase()
-
-      // 5) stash it on the Doc
-      //    overwrite any old tags so re-runs still work
-      const tokens = label.split(/\s+/);
-      this.Document.$tags_chat = new List<string>();
-      tokens.forEach(tok => {
-        (this.Document.$tags_chat as List<string>).push(tok)});
-        (this.Document.$tags_chat as List<string>).push(`ASPECT_${aspectRatio}`); 
-
-      // 6) flip on “show tags” in the layout 
-      //    (same flag that ImageLabelBox.toggleDisplayInformation uses)
-      this.Document._layout_showTags = true;
-
-    } catch (err) {
-      console.error('autoTag failed:', err);
-    } finally {
-    }
-  };
-      
-
-
+        if (this.Document.$tags_chat) return;
+        try {
+            // 1) grab the full-size URL
+            const layoutKey = Doc.LayoutDataKey(this.Document);
+            const url = ImageCastWithSuffix(this.Document[layoutKey], '_o');
+            if (!url) throw new Error('No image URL found');
+
+            // 2) convert to base64
+            const base64 = await imageUrlToBase64(url);
+            if (!base64) throw new Error('Failed to load image data');
+
+            // 3) ask GPT for labels one label: PERSON or LANDSCAPE
+            const label = await gptImageLabel(
+                base64,
+                `Classify this image as PERSON or LANDSCAPE.  You may only respond with one of these two options. 
+                Then provide five additional descriptive tags to describe the image for a total of 6 words outputted, delimited by spaces. 
+                For example: "LANDSCAPE BUNNY NATURE FOREST PEACEFUL OUTDOORS". 
+                Then add one final lengthier summary tag (separated by underscores) that describes the image.`
+            ).then(raw => raw.trim().toUpperCase());
+
+            const { nativeWidth, nativeHeight } = this.nativeSize;
+            const aspectRatio = ((nativeWidth || 1) / (nativeHeight || 1)).toFixed(2);
+
+            // 5) stash it on the Doc
+            //    overwrite any old tags so re-runs still work
+            this.Document.$tags_chat = new List<string>([...label.split(/\s+/), `ASPECT_${aspectRatio}`]);
+
+            // 6) flip on “show tags” in the layout
+            this.Document._layout_showTags = true;
+        } catch (err) {
+            console.error('autoTag failed:', err);
+        }
+    };
 
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const visibleAnchor = this._getAnchor?.(this._savedAnnotations, true); // use marquee anchor, otherwise, save zoom/pan as anchor
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index 256e68afd..dac91b89a 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -9,7 +9,6 @@ import { ImageField } from '../../../fields/URLField';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
-import { reaction } from 'mobx';
 import { DocumentView } from '../nodes/DocumentView';
 
 /**
@@ -210,9 +209,7 @@ export class FaceRecognitionHandler {
         } else if (imgDoc.type === DocumentType.LOADING && !imgDoc.loadingError) {
             setTimeout(() => this.classifyFacesInImage(imgDocView), 1000);
         } else {
-            reaction(() => ({sel:imgDocView.isSelected()}), ({sel}) => !sel &&
-             imgDocView.ComponentView?.autoTag?.(), {fireImmediately: true} 
-        )
+            imgDocView.ComponentView?.autoTag?.();
             const imgUrl = ImageCast(imgDoc[Doc.LayoutDataKey(imgDoc)]);
             if (imgUrl && !DocListCast(Doc.MyFaceCollection?.examinedFaceDocs).includes(imgDoc[DocData])) {
                 // only examine Docs that have an image and that haven't already been examined.
-- 
cgit v1.2.3-70-g09d2


From b4db1e2467337468139d0e92ef94799c4143a0fc Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 11 Jun 2025 12:43:16 -0400
Subject: added outpaintable method to streamline determining which docs can be
 outpainted.

---
 src/client/views/DocumentDecorations.tsx          |  8 ++++----
 src/client/views/ViewBoxInterface.ts              |  1 +
 src/client/views/nodes/ImageBox.tsx               | 10 ++++------
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx |  2 ++
 4 files changed, 11 insertions(+), 10 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index f36312056..383960444 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,7 +1,7 @@
+import { IconButton } from '@dash/components';
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { IconButton } from '@dash/components';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -27,6 +27,7 @@ import './DocumentDecorations.scss';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { InkingStroke } from './InkingStroke';
 import { ObservableReactComponent } from './ObservableReactComponent';
+import { TagsView } from './TagsView';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { Colors } from './global/globalEnums';
@@ -35,7 +36,6 @@ import { DocumentView } from './nodes/DocumentView';
 import { ImageBox } from './nodes/ImageBox';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { TagsView } from './TagsView';
 import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox';
 
 interface DocumentDecorationsProps {
@@ -431,7 +431,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     onPointerDown = (e: React.PointerEvent): void => {
         SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
         DocumentView.Selected()
-            .filter(dv => e.shiftKey && dv.ComponentView instanceof ImageBox || dv.ComponentView instanceof ScrapbookBox)
+            .filter(dv => e.shiftKey && dv.ComponentView?.isOutpaintable?.())
             .forEach(dv => {
                 dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalWidth'] = NumCast(dv.Document._width);
                 dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalHeight'] = NumCast(dv.Document._height);
@@ -487,7 +487,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 this._interactionLock = true;
                 this._snapPt = thisPt;
 
-                const outpainted = e.shiftKey ? DocumentView.Selected().filter(dv => dv.ComponentView instanceof ImageBox || dv.ComponentView instanceof ScrapbookBox) : [];
+                const outpainted = e.shiftKey ? DocumentView.Selected().filter(dv => dv.ComponentView?.isOutpaintable?.()) : [];
                 const notOutpainted = e.shiftKey ? DocumentView.Selected().filter(dv => !outpainted.includes(dv)) : DocumentView.Selected();
 
                 // Special handling for shift-drag resize (outpainting of Images by resizing without scaling content - fill in with firefly GAI)
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index b532dfe35..83e395867 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -25,6 +25,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
     hasChildDocs?: () => Doc[];
     docEditorView?: () => void;
     autoTag?: () => void; // auto tag the document
+    isOutpaintable?: () => boolean; // can document be resized and outpainted
     showSmartDraw?: (x: number, y: number, regenerate?: boolean) => void;
     updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
     getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d7e21b0a6..a99421f6d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -262,9 +262,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
-    handleSelection = async (selection: string) => {
-        this._searchInput = selection;
-    };
+    handleSelection = async (selection: string) => (this._searchInput = selection);
 
     drop = undoable(
         action((e: Event, de: DragManager.DropEvent) => {
@@ -429,9 +427,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     @action
-    handlePromptChange = (val: string | number) => {
-        this._outpaintPromptInput = '' + val;
-    };
+    handlePromptChange = (val: string | number) => (this._outpaintPromptInput = '' + val);
 
     @action
     submitOutpaintPrompt = () => {
@@ -532,6 +528,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return this._props.PanelWidth() / this._props.PanelHeight() < this.nativeSize.nativeWidth / this.nativeSize.nativeHeight;
     }
 
+    isOutpaintable = () => true;
+
     componentUI = (/* boundsLeft: number, boundsTop: number*/) =>
         !this._showOutpaintPrompt ? null : (
             <div
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index 94522c979..2db76c76f 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -174,6 +174,8 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     }
 
+    isOutpaintable = () => true;
+
     @action
     generateAiImage = (prompt: string) => {
         this._loading = true;
-- 
cgit v1.2.3-70-g09d2


From 2d6307f6eaa0803aefabae9e9232506611365b66 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 13 Jun 2025 09:00:49 -0400
Subject: updated css and project to HTML5 standards.  fixed border rounding
 for images.

---
 .eslintrc.json                                     |  11 +-
 .stylelintrc.json                                  |  15 +++
 deploy/index.html                                  |   1 +
 package-lock.json                                  |  66 +++++++++
 package.json                                       |   5 +-
 .../components/src/components/Button/Button.scss   |  10 +-
 .../src/components/Button/Button.stories.tsx       | 149 +++++++++++----------
 .../src/components/Dropdown/Dropdown.scss          |   4 +-
 .../src/components/EditableText/EditableText.scss  |  10 +-
 .../src/components/FormInput/FormInput.scss        |   2 +-
 .../components/src/components/Overlay/Overlay.scss |   6 +-
 .../src/components/Toggle/Toggle.stories.tsx       |  54 ++++----
 .../components/src/global/globalCssVariables.scss  |   2 +-
 src/client/util/BranchingTrailManager.tsx          |   3 -
 src/client/util/CaptureManager.scss                |   6 +-
 src/client/util/CurrentUserUtils.ts                |   4 +-
 src/client/util/GroupManager.scss                  |  19 ++-
 src/client/util/GroupMemberView.scss               |  20 ++-
 src/client/util/LinkFollower.ts                    |   2 +-
 src/client/util/PingManager.ts                     |   2 +-
 src/client/util/SettingsManager.scss               |  30 ++---
 src/client/util/SharingManager.scss                |  51 ++++---
 src/client/util/TrackMovements.ts                  |   2 -
 src/client/util/clamp.js                           |  24 ++--
 src/client/util/convertToCSSPTValue.js             |   9 +-
 src/client/util/reportManager/ReportManager.scss   |  30 ++---
 src/client/util/reportManager/ReportManager.tsx    |   2 -
 src/client/views/ContextMenu.scss                  |  12 +-
 src/client/views/DashboardView.scss                |   8 +-
 src/client/views/DocumentDecorations.scss          |  32 ++---
 src/client/views/DocumentDecorations.tsx           |   4 +-
 src/client/views/EditableView.scss                 |   9 +-
 src/client/views/FilterPanel.scss                  |  54 +++-----
 src/client/views/GestureOverlay.scss               |   2 +-
 src/client/views/InkStroke.scss                    |   4 +-
 src/client/views/InkTangentHandles.tsx             |   2 -
 src/client/views/LightboxView.scss                 |  48 +++----
 src/client/views/Main.scss                         |   6 +-
 src/client/views/MainView.scss                     |  40 +++---
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/MetadataEntryMenu.scss            |  20 +--
 src/client/views/OverlayView.scss                  |  12 +-
 src/client/views/PreviewCursor.scss                |   4 +-
 src/client/views/PropertiesSection.tsx             |   1 -
 src/client/views/PropertiesView.scss               |  50 +++----
 src/client/views/SidebarAnnos.scss                 |  12 +-
 src/client/views/StyleProvider.scss                |  22 +--
 src/client/views/StyleProviderQuiz.scss            |   8 +-
 src/client/views/animationtimeline/Timeline.scss   |   4 +-
 .../views/animationtimeline/TimelineMenu.tsx       |   4 +-
 .../views/animationtimeline/TimelineOverview.tsx   |   5 +-
 .../views/collections/CollectionCardDeckView.scss  |   4 +-
 .../collections/CollectionCarousel3DView.scss      |  12 +-
 .../views/collections/CollectionCarouselView.scss  |  16 +--
 .../views/collections/CollectionDockingView.scss   | 132 +++++++++---------
 .../collections/CollectionMasonryViewFieldRow.tsx  |   4 +-
 .../collections/CollectionNoteTakingView.scss      |  30 ++---
 .../collections/CollectionStackedTimeline.scss     |  16 +--
 .../views/collections/CollectionStackingView.scss  |  34 ++---
 .../views/collections/CollectionStackingView.tsx   |   8 +-
 .../CollectionStackingViewFieldColumn.tsx          |   2 +-
 .../views/collections/CollectionTimeView.scss      |  13 +-
 .../views/collections/CollectionTreeView.scss      |   6 +-
 src/client/views/collections/CollectionView.scss   |   8 +-
 .../views/collections/FlashcardPracticeUI.scss     |   8 +-
 src/client/views/collections/KeyRestrictionRow.tsx |  72 ----------
 src/client/views/collections/TabDocView.scss       |   4 +-
 src/client/views/collections/TreeView.scss         |   4 +-
 .../CollectionFreeFormRemoteCursors.scss           |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.scss |  32 ++---
 .../collectionFreeForm/FaceCollectionBox.scss      |  14 +-
 .../collectionFreeForm/MarqueeView.scss            |   6 +-
 .../collectionGrid/CollectionGridView.scss         |   6 +-
 .../collectionLinear/CollectionLinearView.scss     |   8 +-
 .../CollectionMulticolumnView.scss                 |   8 +-
 .../CollectionMultirowView.scss                    |   4 +-
 .../collectionMulticolumn/MulticolumnResizer.tsx   |   1 -
 .../collectionMulticolumn/MultirowHeightLabel.tsx  |   1 -
 .../collectionMulticolumn/MultirowResizer.tsx      |   3 +-
 .../collectionSchema/CollectionSchemaView.scss     |  10 +-
 src/client/views/linking/LinkMenuItem.scss         |   4 +-
 src/client/views/linking/LinkPopup.scss            |   6 +-
 src/client/views/linking/LinkPopup.tsx             |   1 -
 src/client/views/newlightbox/NewLightboxView.scss  |   4 +-
 .../components/Recommendation/Recommendation.scss  |   2 +-
 src/client/views/nodes/AudioBox.scss               |   4 +-
 .../nodes/CollectionFreeFormDocumentView.scss      |   4 +-
 src/client/views/nodes/ComparisonBox.scss          |  18 +--
 src/client/views/nodes/DataVizBox/DataVizBox.scss  |  10 +-
 .../DataVizBox/DocCreatorMenu/DocCreatorMenu.scss  |  26 ++--
 .../views/nodes/DataVizBox/SchemaCSVPopUp.scss     |   8 +-
 .../views/nodes/DataVizBox/components/Chart.scss   |   4 +-
 src/client/views/nodes/DocumentLinksButton.scss    |   4 +-
 src/client/views/nodes/DocumentView.scss           |  37 ++---
 .../views/nodes/FontIconBox/FontIconBadge.scss     |   2 +-
 .../views/nodes/FontIconBox/FontIconBox.scss       |  20 +--
 src/client/views/nodes/IconTagBox.scss             |   2 +-
 src/client/views/nodes/ImageBox.scss               |  26 ++--
 src/client/views/nodes/ImageBox.tsx                |   3 +-
 src/client/views/nodes/KeyValueBox.scss            |  10 +-
 src/client/views/nodes/KeyValuePair.scss           |   2 +-
 src/client/views/nodes/LabelBox.scss               |  12 +-
 src/client/views/nodes/LinkDocPreview.scss         |   2 +-
 src/client/views/nodes/LoadingBox.scss             |   4 +-
 src/client/views/nodes/MapBox/MapAnchorMenu.scss   |  37 ++---
 src/client/views/nodes/MapBox/MapBox.scss          |  10 +-
 src/client/views/nodes/PDFBox.scss                 |  34 ++---
 .../nodes/PhysicsBox/PhysicsSimulationBox.scss     |   4 +-
 .../views/nodes/RecordingBox/ProgressBar.scss      | 141 +++++++++----------
 .../views/nodes/RecordingBox/RecordingView.scss    |  12 +-
 src/client/views/nodes/ScreenshotBox.scss          |   8 +-
 src/client/views/nodes/ScriptingBox.scss           |   6 +-
 src/client/views/nodes/VideoBox.scss               |  10 +-
 src/client/views/nodes/WebBox.scss                 |  44 +++---
 src/client/views/nodes/audio/AudioWaveform.scss    |  10 +-
 .../nodes/chatbot/chatboxcomponents/ChatBox.scss   |  14 +-
 .../chatbot/chatboxcomponents/ProgressBar.scss     |  12 +-
 .../views/nodes/formattedText/DashFieldView.scss   |   2 +-
 .../views/nodes/formattedText/EquationEditor.scss  |  10 +-
 .../nodes/formattedText/FormattedTextBox.scss      |  54 ++++----
 .../formattedText/FormattedTextBoxComment.scss     |  16 +--
 .../views/nodes/formattedText/RichTextMenu.scss    |   4 +-
 .../views/nodes/formattedText/TooltipTextMenu.scss |   8 +-
 .../views/nodes/imageEditor/ImageEditor.scss       |  10 +-
 src/client/views/nodes/trails/PresBox.scss         | 104 +++++++-------
 src/client/views/nodes/trails/PresSlideBox.scss    |  10 +-
 src/client/views/pdf/AnchorMenu.scss               |  14 +-
 src/client/views/pdf/GPTPopup/GPTPopup.scss        |  10 +-
 src/client/views/pdf/PDFViewer.scss                |  10 +-
 src/client/views/topbar/TopBar.scss                |   2 +-
 src/debug/Viewer.tsx                               |   3 -
 src/fields/FieldLoader.scss                        |   2 +-
 src/server/ApiManagers/UploadManager.ts            |   2 +-
 src/server/PdfTypes.ts                             |   2 +-
 views/stylesheets/authentication.css               |  21 +--
 135 files changed, 1096 insertions(+), 1117 deletions(-)
 create mode 100644 .stylelintrc.json
 delete mode 100644 src/client/views/collections/KeyRestrictionRow.tsx

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

diff --git a/.eslintrc.json b/.eslintrc.json
index aba260657..187ddbdc7 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,6 +1,6 @@
 {
     "extends": ["airbnb", "prettier", "plugin:node/recommended", "plugin:import/react"],
-    "plugins": ["prettier", "import", "@typescript-eslint"],
+    "plugins": ["prettier", "import", "@typescript-eslint", "css-modules"],
     "ignorePatterns": ["solr-8.3.1/*", "deploy/*"], // <<< ignore all files in test folder
     "settings": {
         "import/parsers": {
@@ -14,7 +14,10 @@
                 "extensions": [".js", ".ts", ".tsx"]
             }
         },
-        "import/extensions": [".js", ".jsx", ".ts", ".tsx"]
+        "import/extensions": [".js", ".jsx", ".ts", ".tsx"],
+        "css-modules": {
+            "basePath": "src" // Adjust this to your project's base path
+        }
     },
     "rules": {
         "import/extensions": [
@@ -60,7 +63,9 @@
         "no-param-reassign": ["error", { "props": false }],
         // "import/no-cycle": 0,
         "no-alert": 0,
-        "radix": "off"
+        "radix": "off",
+        "css-modules/no-unused-class": "warn", // Warn for unused CSS classes
+        "css-modules/no-undef-class": "error" // Error for undefined CSS classes
     },
     "overrides": [
         {
diff --git a/.stylelintrc.json b/.stylelintrc.json
new file mode 100644
index 000000000..c9198c5dc
--- /dev/null
+++ b/.stylelintrc.json
@@ -0,0 +1,15 @@
+{
+    "extends": ["stylelint-config-standard-scss"],
+    "plugins": ["stylelint-scss"],
+    "rules": {
+        "unit-no-unknown": true,
+        "length-zero-no-unit": true,
+        "declaration-property-unit-required": {
+            "width": ["px", "%", "em", "rem", "vw", "vh"],
+            "height": ["px", "%", "em", "rem", "vw", "vh"]
+        },
+        "property-no-unknown": true,
+        "value-no-vendor-prefix": true,
+        "scss/at-rule-no-unknown": true
+    }
+}
diff --git a/deploy/index.html b/deploy/index.html
index 1c48f24bf..b469c7032 100644
--- a/deploy/index.html
+++ b/deploy/index.html
@@ -1,3 +1,4 @@
+<!doctype html>
 <html style="overflow: hidden">
     <head>
         <title>Dash</title>
diff --git a/package-lock.json b/package-lock.json
index 8e7219bdb..73b8b007b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
                 "@dash/components": "file:packages/components",
                 "@emotion/react": "^11.11.1",
                 "@emotion/styled": "^11.11.0",
+                "@eslint/css": "^0.8.1",
                 "@ffmpeg-installer/ffmpeg": "^1.1.0",
                 "@ffmpeg/core": "^0.12.5",
                 "@ffmpeg/ffmpeg": "^0.12.10",
@@ -109,6 +110,7 @@
                 "dompurify": "^3.1.7",
                 "dotenv": "^16.4.7",
                 "dropbox": "^10.34.0",
+                "eslint-plugin-css-modules": "^2.12.0",
                 "eslint-webpack-plugin": "^5.0.0",
                 "exif": "^0.6.0",
                 "exifr": "^7.1.3",
@@ -4083,6 +4085,33 @@
                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
             }
         },
+        "node_modules/@eslint/css": {
+            "version": "0.8.1",
+            "resolved": "https://registry.npmjs.org/@eslint/css/-/css-0.8.1.tgz",
+            "integrity": "sha512-674JJD1q8sDlJORLep+gGnm3VRCQo/qLmKQgCIf2LnUK/tHf96StWjLX2IF3yyp3yeU9npZ6ixySMr2G256eiQ==",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "@eslint/core": "^0.14.0",
+                "@eslint/css-tree": "^3.3.3",
+                "@eslint/plugin-kit": "^0.3.1"
+            },
+            "engines": {
+                "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+            }
+        },
+        "node_modules/@eslint/css-tree": {
+            "version": "3.6.1",
+            "resolved": "https://registry.npmjs.org/@eslint/css-tree/-/css-tree-3.6.1.tgz",
+            "integrity": "sha512-5DIsBME23tUQD5zHD+T38lC2DG4jB8x8JRa+yDncLne2TIZA0VuCpcSazOX1EC+sM/q8w24qeevXfmfsIxAeqA==",
+            "license": "MIT",
+            "dependencies": {
+                "mdn-data": "2.21.0",
+                "source-map-js": "^1.0.1"
+            },
+            "engines": {
+                "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+            }
+        },
         "node_modules/@eslint/eslintrc": {
             "version": "3.3.1",
             "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
@@ -21145,6 +21174,22 @@
                 }
             }
         },
+        "node_modules/eslint-plugin-css-modules": {
+            "version": "2.12.0",
+            "resolved": "https://registry.npmjs.org/eslint-plugin-css-modules/-/eslint-plugin-css-modules-2.12.0.tgz",
+            "integrity": "sha512-ruFBdad69ABrbCDCh5mXj7UzNmrvytfzPACjyvZWIAjFZAG8BXpYSbqmE8gU5wF+pIzV3jU2CWhLvfekXT/IgQ==",
+            "license": "MIT",
+            "dependencies": {
+                "gonzales-pe": "^4.3.0",
+                "lodash": "^4.17.2"
+            },
+            "engines": {
+                "node": ">=4.0.0"
+            },
+            "peerDependencies": {
+                "eslint": ">=2.0.0"
+            }
+        },
         "node_modules/eslint-plugin-react": {
             "version": "7.37.5",
             "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
@@ -23084,6 +23129,21 @@
             "integrity": "sha512-sIVQCiRWOymHbVD1Aw/T9/ijbPYAVGBlgGYd1N9MRKfcyBNSpjr87Vg9nSHm+RCT8ELrvK8IJYJV0QRJuVUkCQ==",
             "license": "MIT"
         },
+        "node_modules/gonzales-pe": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz",
+            "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==",
+            "license": "MIT",
+            "dependencies": {
+                "minimist": "^1.2.5"
+            },
+            "bin": {
+                "gonzales": "bin/gonzales.js"
+            },
+            "engines": {
+                "node": ">=0.6.0"
+            }
+        },
         "node_modules/google-auth-library": {
             "version": "9.15.1",
             "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
@@ -26886,6 +26946,12 @@
                 "url": "https://opencollective.com/unified"
             }
         },
+        "node_modules/mdn-data": {
+            "version": "2.21.0",
+            "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.21.0.tgz",
+            "integrity": "sha512-+ZKPQezM5vYJIkCxaC+4DTnRrVZR1CgsKLu5zsQERQx6Tea8Y+wMx5A24rq8A8NepCeatIQufVAekKNgiBMsGQ==",
+            "license": "CC0-1.0"
+        },
         "node_modules/mdurl": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
diff --git a/package.json b/package.json
index 5e17a419b..e9f9dc4df 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,8 @@
         "test": "mocha -r ts-node/register test/**/*.ts",
         "tsc": "tsc -t es5",
         "storybook": "npm -w @dash/components run storybook",
-        "build-storybook": "npm -w @dash/components run build-storybook"
+        "build-storybook": "npm -w @dash/components run build-storybook",
+        "lint:css": "stylelint '**/*.{css,scss}'"
     },
     "devDependencies": {
         "@eslint/js": "^9.10.0",
@@ -104,6 +105,7 @@
         "@dash/components": "file:packages/components",
         "@emotion/react": "^11.11.1",
         "@emotion/styled": "^11.11.0",
+        "@eslint/css": "^0.8.1",
         "@ffmpeg-installer/ffmpeg": "^1.1.0",
         "@ffmpeg/core": "^0.12.5",
         "@ffmpeg/ffmpeg": "^0.12.10",
@@ -193,6 +195,7 @@
         "dompurify": "^3.1.7",
         "dotenv": "^16.4.7",
         "dropbox": "^10.34.0",
+        "eslint-plugin-css-modules": "^2.12.0",
         "eslint-webpack-plugin": "^5.0.0",
         "exif": "^0.6.0",
         "exifr": "^7.1.3",
diff --git a/packages/components/src/components/Button/Button.scss b/packages/components/src/components/Button/Button.scss
index c86db9fad..c87198ddf 100644
--- a/packages/components/src/components/Button/Button.scss
+++ b/packages/components/src/components/Button/Button.scss
@@ -20,8 +20,8 @@
     pointer-events: all;
 
     &.icon {
-        padding: 0;
-        gap: 0;
+        padding: 0px;
+        gap: 0px;
     }
 
     .button-content {
@@ -44,8 +44,8 @@
         width: 100%;
         height: 100%;
         z-index: 0;
-        left: 0;
-        top: 0;
+        left: 0px;
+        top: 0px;
         position: absolute;
         transition: 0.4s;
     }
@@ -111,7 +111,7 @@
 
     .label {
         position: absolute;
-        bottom: 0;
+        bottom: 0px;
         width: 100%;
         display: flex;
         justify-content: center;
diff --git a/packages/components/src/components/Button/Button.stories.tsx b/packages/components/src/components/Button/Button.stories.tsx
index 3893d9ded..9e7e8cb05 100644
--- a/packages/components/src/components/Button/Button.stories.tsx
+++ b/packages/components/src/components/Button/Button.stories.tsx
@@ -1,94 +1,97 @@
-import { Meta, Story } from '@storybook/react'
-import React from 'react'
-import * as bi from 'react-icons/bi'
-import { Button, IButtonProps } from '..'
-import { Colors, Size } from '../../global/globalEnums'
-import { Type , getFormLabelSize } from '../../global'
+import { Meta, Story } from '@storybook/react';
+import React from 'react';
+import * as bi from 'react-icons/bi';
+import { Button, IButtonProps } from '..';
+import { Type } from '../../global';
+import { Size } from '../../global/globalEnums';
 
 export default {
-  title: 'Dash/Button',
-  component: Button,
-  argTypes: {},
-} as Meta<typeof Button>
+    title: 'Dash/Button',
+    component: Button,
+    argTypes: {},
+} as Meta<typeof Button>;
 
-const Template: Story<IButtonProps> = (args) => <Button {...args} />
+const Template: Story<IButtonProps> = args => <Button {...args} />;
 
-export const Primary = Template.bind({})
+export const Primary = Template.bind({});
 Primary.args = {
-  onClick: () => {},
-  text: 'Primary',
-  type: Type.PRIM,
-  style: {
-    fontWeight: 600
-  },
-  tooltip: 'Primary button'
-}
+    onClick: () => {},
+    text: 'Primary',
+    type: Type.PRIM,
+    style: {
+        fontWeight: 600,
+    },
+    tooltip: 'Primary button',
+};
 
-export const Secondary = Template.bind({})
+export const Secondary = Template.bind({});
 Secondary.args = {
-  onClick: () => {},
-  text: 'Secondary',
-  type: Type.SEC,
-  tooltip: 'Secondary button'
-}
+    onClick: () => {},
+    text: 'Secondary',
+    type: Type.SEC,
+    tooltip: 'Secondary button',
+    color: 'green',
+    background: 'yellow',
+};
 
-export const Tertiary = Template.bind({})
+export const Tertiary = Template.bind({});
 Tertiary.args = {
-  onClick: () => {},
-  text: 'Tertiary',
-  type: Type.TERT,
-  size: Size.SMALL,
-}
+    onClick: () => {},
+    text: 'Tertiary',
+    type: Type.TERT,
+    size: Size.SMALL,
+    background: 'yellow',
+};
 
-export const Small = Template.bind({})
+export const Small = Template.bind({});
 Small.args = {
-  onClick: () => {},
-  text: 'Small',
-  type: Type.PRIM,
-  size: Size.SMALL,
-}
+    onClick: () => {},
+    text: 'Small',
+    type: Type.PRIM,
+    size: Size.SMALL,
+};
 
-export const Medium = Template.bind({})
+export const Medium = Template.bind({});
 Medium.args = {
-  onClick: () => {},
-  text: 'Medium',
-  type: Type.PRIM,
-  size: Size.MEDIUM,
-}
+    onClick: () => {},
+    text: 'Medium',
+    type: Type.PRIM,
+    size: Size.MEDIUM,
+};
 
-export const Large = Template.bind({})
+export const Large = Template.bind({});
 Large.args = {
-  onClick: () => {},
-  text: 'Large',
-  type: Type.PRIM,
-  size: Size.LARGE,
-}
+    onClick: () => {},
+    text: 'Large',
+    type: Type.PRIM,
+    size: Size.LARGE,
+};
 
-export const ButtonWithLeftIcon = Template.bind({})
+export const ButtonWithLeftIcon = Template.bind({});
 ButtonWithLeftIcon.args = {
-  onClick: () => {},
-  text: 'New',
-  icon: <bi.BiPlus />,
-  iconPosition: 'left',
-  type: Type.PRIM,
-}
+    onClick: () => {},
+    text: 'New',
+    icon: <bi.BiPlus />,
+    iconPosition: 'left',
+    type: Type.PRIM,
+};
 
-export const ButtonWithRightIcon = Template.bind({})
+export const ButtonWithRightIcon = Template.bind({});
 ButtonWithRightIcon.args = {
-  onClick: () => {},
-  text: 'More',
-  iconPosition: 'right',
-  icon: <bi.BiMobile />,
-  type: Type.PRIM,
-}
+    onClick: () => {},
+    text: 'More',
+    iconPosition: 'right',
+    icon: <bi.BiMobile />,
+    type: Type.PRIM,
+};
 
-export const Label = Template.bind({})
+export const Label = Template.bind({});
 Label.args = {
-  onClick: () => {},
-  text: 'Label',
-  type: Type.PRIM,
-  style: {
-    fontWeight: 600
-  },
-  tooltip: 'Label button'
-}
\ No newline at end of file
+    onClick: () => {},
+    text: 'Label',
+    type: Type.PRIM,
+    style: {
+        fontWeight: 600,
+    },
+    tooltip: 'Label button',
+};
diff --git a/packages/components/src/components/Dropdown/Dropdown.scss b/packages/components/src/components/Dropdown/Dropdown.scss
index f9ea2711a..516c229a5 100644
--- a/packages/components/src/components/Dropdown/Dropdown.scss
+++ b/packages/components/src/components/Dropdown/Dropdown.scss
@@ -129,8 +129,8 @@
     .dropdown-toggle-mini {
         .toggle-caret {
             position: absolute;
-            top: 0;
-            left: 0;
+            top: 0px;
+            left: 0px;
         }
     }
 }
diff --git a/packages/components/src/components/EditableText/EditableText.scss b/packages/components/src/components/EditableText/EditableText.scss
index 15965e97e..691b05590 100644
--- a/packages/components/src/components/EditableText/EditableText.scss
+++ b/packages/components/src/components/EditableText/EditableText.scss
@@ -16,18 +16,18 @@
         justify-content: center;
         align-items: center;
         height: 100%;
-        right: 0;
-        top: 0;
+        right: 0px;
+        top: 0px;
     }
 
     .editableText-background {
         width: 100%;
         height: 100%;
-        z-index: -1;
+        z-index: -1px;
         position: absolute;
         transition: 0.4s;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
     }
 
     &.primary {
diff --git a/packages/components/src/components/FormInput/FormInput.scss b/packages/components/src/components/FormInput/FormInput.scss
index 2554cbd01..187827724 100644
--- a/packages/components/src/components/FormInput/FormInput.scss
+++ b/packages/components/src/components/FormInput/FormInput.scss
@@ -58,7 +58,7 @@
 
     .formInput-label {
         position: absolute;
-        top: 0;
+        top: 0px;
         transform: translate(0px, 8px);
         display: block;
         transition: 0.2s;
diff --git a/packages/components/src/components/Overlay/Overlay.scss b/packages/components/src/components/Overlay/Overlay.scss
index 5ba4f802c..2c3d9530d 100644
--- a/packages/components/src/components/Overlay/Overlay.scss
+++ b/packages/components/src/components/Overlay/Overlay.scss
@@ -1,9 +1,9 @@
 .overlay-container {
     width: 100vw;
     height: 100vh;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     background: pink;
     position: absolute;
     pointer-events: none;
-}
\ No newline at end of file
+}
diff --git a/packages/components/src/components/Toggle/Toggle.stories.tsx b/packages/components/src/components/Toggle/Toggle.stories.tsx
index 28ab2e712..6adf63424 100644
--- a/packages/components/src/components/Toggle/Toggle.stories.tsx
+++ b/packages/components/src/components/Toggle/Toggle.stories.tsx
@@ -1,35 +1,37 @@
-import { Meta, Story } from '@storybook/react'
-import React from 'react'
-import * as bi from 'react-icons/bi'
-import { IToggleProps, Toggle, ToggleType } from './Toggle'
-import { Type , getFormLabelSize } from '../../global'
+import { Meta, Story } from '@storybook/react';
+import React from 'react';
+import * as bi from 'react-icons/bi';
+import { Type } from '../../global';
+import { IToggleProps, Toggle, ToggleType } from './Toggle';
 
 export default {
-  title: 'Dash/Toggle',
-  component: Toggle,
-  argTypes: {},
-} as Meta<typeof Toggle>
+    title: 'Dash/Toggle',
+    component: Toggle,
+    argTypes: {},
+} as Meta<typeof Toggle>;
 
-const Template: Story<IToggleProps> = (args) => <Toggle {...args} />
+const Template: Story<IToggleProps> = args => <Toggle {...args} />;
 
-export const Button = Template.bind({})
+export const Button = Template.bind({});
 Button.args = {
-  // text: 'Button',
-  type: Type.TERT,
-  icon: <bi.BiAbacus/>,
-  toggleType: ToggleType.BUTTON,
-  tooltip: 'Test tooltip'
-}
+    // text: 'Button',
+    type: Type.PRIM,
+    icon: <bi.BiAbacus />,
+    toggleType: ToggleType.BUTTON,
+    tooltip: 'Test tooltip',
+    color: 'green',
+    background: 'yellow',
+};
 
-export const Checkbox = Template.bind({})
+export const Checkbox = Template.bind({});
 Checkbox.args = {
-  type: Type.SEC,
-  toggleType: ToggleType.CHECKBOX
-}
+    type: Type.SEC,
+    toggleType: ToggleType.CHECKBOX,
+};
 
-export const Switch = Template.bind({})
+export const Switch = Template.bind({});
 Switch.args = {
-  text: 'Button',
-  type: Type.SEC,
-  toggleType: ToggleType.SWITCH
-}
\ No newline at end of file
+    text: 'Button',
+    type: Type.SEC,
+    toggleType: ToggleType.SWITCH,
+};
diff --git a/packages/components/src/global/globalCssVariables.scss b/packages/components/src/global/globalCssVariables.scss
index ebc44106d..99fe7fb11 100644
--- a/packages/components/src/global/globalCssVariables.scss
+++ b/packages/components/src/global/globalCssVariables.scss
@@ -59,7 +59,7 @@ $small-text: 9px;
 // $sans-serif: "Roboto Slab", sans-serif;
 
 // misc values
-$search-thumnail-size: 130;
+$search-thumnail-size: 130px;
 $topbar-height: 50px;
 $antimodemenu-height: 36px;
 
diff --git a/src/client/util/BranchingTrailManager.tsx b/src/client/util/BranchingTrailManager.tsx
index 65336812d..a0871514c 100644
--- a/src/client/util/BranchingTrailManager.tsx
+++ b/src/client/util/BranchingTrailManager.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable react/no-unused-class-component-methods */
-/* eslint-disable react/no-array-index-key */
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -52,7 +50,6 @@ export class BranchingTrailManager extends React.Component {
         this.slideHistoryStack = newArr;
     });
 
-    // eslint-disable-next-line react/sort-comp
     observeDocumentChange = (targetDoc: Doc, pres: PresBox) => {
         const presId = pres.Document[Id];
         if (this.prevPresId === presId) {
diff --git a/src/client/util/CaptureManager.scss b/src/client/util/CaptureManager.scss
index e7cbb4287..9e81f2669 100644
--- a/src/client/util/CaptureManager.scss
+++ b/src/client/util/CaptureManager.scss
@@ -67,7 +67,7 @@
         font-weight: bold;
         text-align: left;
         color: black;
-        width: 80;
+        width: 80px;
         margin-right: 50px;
         margin-bottom: 5px;
     }
@@ -114,8 +114,8 @@
     .buttons {
         display: flex;
         position: absolute;
-        bottom: 0;
-        right: 15;
+        bottom: 0px;
+        right: 15px;
         justify-content: flex-end;
         align-items: center;
         height: 60px;
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index a25c491d9..45feb0b2f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -562,7 +562,7 @@ pie title Minerals in my tap water
         const reqdToolOps:DocumentOptions = {
             title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0",
             layout_explainer: "This is a palette of documents that can be created.", _layout_dontCenter: "y",
-            _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _dragOnlyWithinContainer: true, layout_hideContextMenu: true, _chromeHidden: true, 
+            _layout_showTitle: "title", _width: 500, _lockedPosition: true, _forceActive: true, _dragOnlyWithinContainer: true, layout_hideContextMenu: true, _chromeHidden: true, 
         };
         return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, userBtns]);
     }
@@ -994,7 +994,7 @@ pie title Minerals in my tap water
         const reqdOpts:DocumentOptions = {
             title: "My Imports", _forceActive: true, _layout_showTitle: "title", childLayoutString: ImportElementBox.LayoutString('data'),
             _dragOnlyWithinContainer: true, layout_hideContextMenu: true, childLimitHeight: 0, onClickScriptDisable:"never",
-            childDragAction: dropActionType.copy, _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true,
+            childDragAction: dropActionType.copy, _layout_autoHeight: true, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true,
             dontRegisterView: true, layout_explainer: "This is where documents that are Imported into Dash will go."
         };
         const myImports = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.MasonryDocument([], opts), reqdOpts, undefined, {onClick: "deselectAll()"});
diff --git a/src/client/util/GroupManager.scss b/src/client/util/GroupManager.scss
index 673af16ee..66f11c529 100644
--- a/src/client/util/GroupManager.scss
+++ b/src/client/util/GroupManager.scss
@@ -21,7 +21,6 @@
         }
 
         .group-input {
-
             input {
                 padding: 8px;
                 width: 100%;
@@ -33,13 +32,13 @@
                 border-color: inherit;
                 background: white;
                 color: black;
-                height: 30;
+                height: 30px;
             }
         }
 
         .select-users {
-            margin-right: 3;
-            max-height: 30;
+            margin-right: 3px;
+            max-height: 30px;
             width: 100%;
             display: inline-flex;
             flex-direction: row;
@@ -51,7 +50,6 @@
         }
     }
 
-
     button {
         align-self: center;
         outline: none;
@@ -108,7 +106,7 @@
 
         .sort-groups {
             text-align: left;
-            margin-left: 5;
+            margin-left: 5px;
             width: 50px;
             cursor: pointer;
             display: flex;
@@ -122,7 +120,7 @@
 
         .group-body {
             justify-content: space-between;
-            height: 225;
+            height: 225px;
 
             padding-right: 1em;
             justify-content: space-around;
@@ -149,8 +147,8 @@
                 button {
                     position: absolute;
                     width: 30%;
-                    right: 2;
-                    margin-top: 0;
+                    right: 2px;
+                    margin-top: 0px;
                 }
             }
 
@@ -161,7 +159,6 @@
                 min-width: 100%;
                 margin: 2px 0;
             }
-
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/util/GroupMemberView.scss b/src/client/util/GroupMemberView.scss
index d50569b26..cb1a4e098 100644
--- a/src/client/util/GroupMemberView.scss
+++ b/src/client/util/GroupMemberView.scss
@@ -6,7 +6,7 @@
     overflow: visible;
 
     hr {
-        margin-top: 20;
+        margin-top: 20px;
     }
 
     button {
@@ -31,7 +31,7 @@
     }
 
     .editing-header {
-        margin-bottom: 5;
+        margin-bottom: 5px;
 
         .group-title {
             font-weight: bold;
@@ -39,7 +39,7 @@
             text-align: center;
             border: none;
             outline: none;
-            height: 20;
+            height: 20px;
             text-overflow: ellipsis;
             background: none;
 
@@ -57,12 +57,12 @@
 
         .group-buttons {
             display: flex;
-            margin-top: 5;
-            margin-bottom: 25;
+            margin-top: 5px;
+            margin-bottom: 25px;
 
             .add-member-dropdown {
-                margin-right: 3;
-                max-height: 30;
+                margin-right: 3px;
+                max-height: 30px;
                 width: 250px;
                 display: inline-flex;
                 flex-direction: row;
@@ -108,11 +108,9 @@
 
             .remove-button {
                 position: absolute;
-                right: 10;
+                right: 10px;
                 cursor: pointer;
             }
         }
     }
-
-
-}
\ No newline at end of file
+}
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 6081c3fae..a4f54f1cf 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -134,6 +134,6 @@ export class LinkFollower {
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) {
-    DocumentView.DeselectAll();
+    // DocumentView.DeselectAll();
     return LinkFollower.FollowLink(undefined, doc, altKey) ? undefined : { select: true };
 });
diff --git a/src/client/util/PingManager.ts b/src/client/util/PingManager.ts
index 0e4f8cab0..593978b4a 100644
--- a/src/client/util/PingManager.ts
+++ b/src/client/util/PingManager.ts
@@ -27,7 +27,7 @@ export class PingManager {
             this.IsBeating = status;
             setTimeout(this.showAlert, 100);
         });
-        Networking.PostToServer('/ping', { date: new Date() })
+        Networking.PostToServer('/DashPing', { date: new Date() })
             .then(res => {
                 SnappingManager.SetServerVersion((res as { message: string }).message);
                 !this.IsBeating && setIsBeating(true);
diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
index f81f17589..51b719200 100644
--- a/src/client/util/SettingsManager.scss
+++ b/src/client/util/SettingsManager.scss
@@ -26,7 +26,7 @@
         font-size: 16;
         font-weight: bold;
         text-align: left;
-        width: 80;
+        width: 80px;
         margin-right: 50px;
     }
 
@@ -81,7 +81,7 @@
 
         .acl-text {
             color: black;
-            margin-top: 2;
+            margin-top: 2px;
             text-align: left;
         }
     }
@@ -119,7 +119,7 @@
 
         .preferences-color-text {
             margin-top: 3px;
-            margin-right: 4;
+            margin-right: 4px;
             flex: 1 1 auto;
             text-align: left;
         }
@@ -134,8 +134,8 @@
         margin-top: 2px;
 
         .preferences-font-text {
-            margin-top: 4;
-            margin-right: 4;
+            margin-top: 4px;
+            margin-right: 4px;
             margin-bottom: 2px;
             text-align: left;
         }
@@ -149,7 +149,7 @@
         .font-select {
             height: 35px;
             font-size: 9;
-            margin-right: 6;
+            margin-right: 6px;
             border-radius: 5px;
             width: 65%;
 
@@ -171,9 +171,9 @@
     }
 
     .preferences-check {
-        margin-right: 4;
+        margin-right: 4px;
         margin-bottom: -3;
-        margin-left: 5;
+        margin-left: 5px;
         margin-top: -1px;
         display: inline-block;
         padding-left: 5px;
@@ -205,14 +205,14 @@
     .error-text {
         color: #c40233;
         font-size: 10;
-        margin-bottom: 4;
+        margin-bottom: 4px;
     }
 
     .success-text {
-        width: 300;
+        width: 300px;
         margin-left: -20;
         font-size: 10;
-        margin-bottom: 4;
+        margin-bottom: 4px;
         margin-top: -3;
         color: #009f6b;
     }
@@ -225,7 +225,7 @@
         text-transform: uppercase;
         letter-spacing: 2px;
         font-size: 19;
-        margin-top: 0;
+        margin-top: 0px;
         font-weight: bold;
     }
 
@@ -246,7 +246,7 @@
         font-style: normal;
         font-size: 15;
         font-weight: normal;
-        padding: 0;
+        padding: 0px;
 
         .padding {
             padding: 0 0 0 20px;
@@ -273,8 +273,8 @@
         align-items: center;
         flex-direction: column;
         bottom: 10px;
-        left: 0;
-        right: 0;
+        left: 0px;
+        right: 0px;
 
         .settings-username {
             padding-right: 0px;
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index c9f86f0e1..553f3a184 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -4,7 +4,7 @@
 
     .select {
         text-align: justify;
-        text-align-last: end
+        text-align-last: end;
     }
 
     .sharing-contents {
@@ -36,15 +36,14 @@
         }
 
         .share-container {
-
             .share-setup {
                 display: flex;
                 margin-bottom: 20px;
                 align-items: center;
-                max-height: 28;
+                max-height: 28px;
 
                 .user-search {
-                    max-height: 30;
+                    max-height: 30px;
                     width: 90%;
                     display: inline-flex;
                     flex-direction: row;
@@ -68,7 +67,7 @@
                     text-align-last: end;
                     font-size: 13px;
                     min-width: 90px;
-                    height: 28;
+                    height: 28px;
                     margin-left: 2px;
                 }
 
@@ -99,8 +98,8 @@
                 display: flex;
                 float: right;
                 align-items: baseline;
-                margin-top: -12;
-                margin-bottom: 10;
+                margin-top: -12px;
+                margin-bottom: 10px;
 
                 .layoutDoc-acls,
                 .myDocs-acls {
@@ -109,7 +108,7 @@
                     label {
                         font-weight: normal;
                         font-style: italic;
-                        padding-right: 12;
+                        padding-right: 12px;
                     }
 
                     input {
@@ -127,13 +126,13 @@
             .group-container {
                 width: 50%;
                 display: flex;
-                top:0;
+                top: 0px;
                 flex-direction: column;
                 border-radius: 4px;
 
                 .user-sort {
                     text-align: left;
-                    margin-left: 10;
+                    margin-left: 10px;
                     width: 100%;
                     cursor: pointer;
                 }
@@ -180,7 +179,7 @@
             padding: 0 10;
             margin: 0 5;
             transition: transform 0.2s;
-            height: 25;
+            height: 25px;
         }
     }
 
@@ -205,7 +204,7 @@
 
         .people-with {
             font-size: 14px;
-            margin: 0;
+            margin: 0px;
             padding-top: 3px;
             font-style: normal;
         }
@@ -216,30 +215,30 @@
         }
     }
 
-    .title-individual{
+    .title-individual {
         height: 25px;
-        padding-left: 2;
+        padding-left: 2px;
         width: 97%;
         border-radius: 4px;
         margin-top: 10px;
         margin-left: -8px;
         font-size: 14;
-        margin-bottom: -4;
+        margin-bottom: -4px;
         border: 2px solid;
         border-bottom: none;
         align-items: center;
         display: flex;
     }
 
-    .title-group{
+    .title-group {
         height: 25px;
-        padding-left: 2;
+        padding-left: 2px;
         width: 97%;
         border-radius: 4px;
         margin-top: 10px;
         margin-left: -8px;
         font-size: 14;
-        margin-bottom: -4;
+        margin-bottom: -4px;
         border: 2px solid;
         border-bottom: none;
         align-items: center;
@@ -289,18 +288,17 @@
             text-align: right;
             margin-left: auto;
             margin-right: -12px;
-
         }
 
         .edit-actions {
             display: flex;
             position: absolute;
             align-items: flex-end;
-            right: -10;            
+            right: -10px;
         }
     }
 
-    .permissions-dropdown-None{
+    .permissions-dropdown-None {
         height: 100%;
         min-width: 85px;
         text-align: right;
@@ -325,7 +323,7 @@
         border-radius: 6px;
         border: 1px solid rgb(75, 75, 5);
     }
-    .permissions-dropdown-Augment{
+    .permissions-dropdown-Augment {
         height: 100%;
         min-width: 85px;
         text-align: right;
@@ -333,12 +331,11 @@
         padding: 0px;
         padding-left: 3px;
         background: rgb(208, 255, 208);
-        color:rgb(19, 80, 19);
+        color: rgb(19, 80, 19);
         border-radius: 6px;
         border: 1px solid rgb(19, 80, 19);
-
     }
-    .permissions-dropdown-View{
+    .permissions-dropdown-View {
         height: 100%;
         min-width: 85px;
         text-align: right;
@@ -350,7 +347,7 @@
         border-radius: 6px;
         border: 1px solid rgb(25, 25, 101);
     }
-    .permissions-dropdown-Not-Shared{
+    .permissions-dropdown-Not-Shared {
         height: 100%;
         min-width: 85px;
         text-align: right;
@@ -398,4 +395,4 @@
     //         padding-top: 12px;
     //     }
     // }
-}
\ No newline at end of file
+}
diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts
index 7da0281c0..74a3be83d 100644
--- a/src/client/util/TrackMovements.ts
+++ b/src/client/util/TrackMovements.ts
@@ -98,7 +98,6 @@ export class TrackMovements {
 
         // new tab was added - need to add it
         if (tabbedFFViews.size > this.recordingFFViews.size) {
-            // eslint-disable-next-line no-restricted-syntax
             for (const DashDoc of tabbedDocs) {
                 if (!this.recordingFFViews.has(DashDoc)) {
                     if (isFFView(DashDoc)) {
@@ -112,7 +111,6 @@ export class TrackMovements {
         }
         // tab was removed - need to remove it from recordingFFViews
         else if (tabbedFFViews.size < this.recordingFFViews.size) {
-            // eslint-disable-next-line no-restricted-syntax
             for (const [doc] of this.recordingFFViews) {
                 if (!tabbedFFViews.has(doc)) {
                     this.removeRecordingFFView(doc);
diff --git a/src/client/util/clamp.js b/src/client/util/clamp.js
index 9c7fd78a4..8d74b5636 100644
--- a/src/client/util/clamp.js
+++ b/src/client/util/clamp.js
@@ -1,15 +1,15 @@
-"use strict";
+'use strict';
 
-Object.defineProperty(exports, "__esModule", {
-  value: true
+Object.defineProperty(exports, '__esModule', {
+    value: true,
 });
-exports.default = clamp;
 function clamp(min, val, max) {
-  if (val < min) {
-    return min;
-  }
-  if (val > max) {
-    return max;
-  }
-  return val;
-}
\ No newline at end of file
+    if (val < min) {
+        return min;
+    }
+    if (val > max) {
+        return max;
+    }
+    return val;
+}
+exports.default = clamp;
diff --git a/src/client/util/convertToCSSPTValue.js b/src/client/util/convertToCSSPTValue.js
index 66f8db5a1..032385d81 100644
--- a/src/client/util/convertToCSSPTValue.js
+++ b/src/client/util/convertToCSSPTValue.js
@@ -3,11 +3,8 @@
 Object.defineProperty(exports, '__esModule', {
     value: true,
 });
-exports.PT_TO_PX_RATIO = exports.PX_TO_PT_RATIO = undefined;
-exports.default = convertToCSSPTValue;
-exports.toClosestFontPtSize = toClosestFontPtSize;
 
-var SIZE_PATTERN = /([\d\.]+)(px|pt)/i;
+var SIZE_PATTERN = /([\d.]+)(px|pt)/i;
 
 var PX_TO_PT_RATIO = (exports.PX_TO_PT_RATIO = 0.7518796992481203); // 1 / 1.33.
 var PT_TO_PX_RATIO = (exports.PT_TO_PX_RATIO = 1.33);
@@ -39,3 +36,7 @@ function toClosestFontPtSize(styleValue) {
         return Math.abs(curr - originalPTValue) < Math.abs(prev - originalPTValue) ? curr : prev;
     }, Number.NEGATIVE_INFINITY);
 }
+
+exports.PT_TO_PX_RATIO = exports.PX_TO_PT_RATIO = undefined;
+exports.default = convertToCSSPTValue;
+exports.toClosestFontPtSize = toClosestFontPtSize;
diff --git a/src/client/util/reportManager/ReportManager.scss b/src/client/util/reportManager/ReportManager.scss
index 806741c22..65894fbd6 100644
--- a/src/client/util/reportManager/ReportManager.scss
+++ b/src/client/util/reportManager/ReportManager.scss
@@ -14,8 +14,8 @@
     }
 
     h2 {
-        margin: 0;
-        padding: 0;
+        margin: 0px;
+        padding: 0px;
         font-size: 24px;
     }
 }
@@ -27,8 +27,8 @@
     gap: 4px;
 
     h2 {
-        margin: 0;
-        padding: 0;
+        margin: 0px;
+        padding: 0px;
         padding-bottom: 8px;
         font-size: 24px;
     }
@@ -92,7 +92,7 @@
     outline: none;
     border-bottom: 1px solid;
     padding: 8px;
-    padding-left: 0;
+    padding-left: 0px;
     transition: all 0.2s ease;
     background: transparent;
 
@@ -178,15 +178,15 @@
         cursor: pointer;
         font-size: 14px;
         font-weight: 400;
-        padding: 0;
-        margin: 0;
+        padding: 0px;
+        margin: 0px;
     }
 
     .issue-title {
         font-size: 16px;
         font-weight: 500;
-        padding: 0;
-        margin: 0;
+        padding: 0px;
+        margin: 0px;
     }
 }
 
@@ -211,8 +211,8 @@
 
 .file-list {
     box-sizing: border-box;
-    margin: 0;
-    padding: 0;
+    margin: 0px;
+    padding: 0px;
     font-size: 14px;
     width: 100%;
     overflow-x: auto;
@@ -251,8 +251,8 @@
 
     .issue-title {
         font-size: 24px;
-        margin: 0;
-        padding: 0;
+        margin: 0px;
+        padding: 0px;
     }
 
     .issue-date {
@@ -314,8 +314,8 @@
     .report-media-content::after {
         content: '';
         position: absolute;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
         width: 100%;
         height: 100%;
         background-color: rgba(0, 0, 0, 0.5); /* Adjust the opacity as desired */
diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx
index a6b5911f7..87e8a8bf1 100644
--- a/src/client/util/reportManager/ReportManager.tsx
+++ b/src/client/util/reportManager/ReportManager.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/no-unused-class-component-methods */
 import { Octokit } from '@octokit/core';
 import { Button, Dropdown, DropdownType, IconButton, Type } from '@dash/components';
 import { action, makeObservable, observable } from 'mobx';
@@ -31,7 +30,6 @@ export class ReportManager extends React.Component<object> {
     @observable private isOpen = false;
 
     @observable private query = '';
-    // eslint-disable-next-line react/sort-comp
     @action private setQuery = (q: string) => {
         this.query = q;
     };
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index d22c4d096..337c976cb 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -68,8 +68,8 @@
         width: 100%;
         height: 100%;
         position: absolute;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
         z-index: 0;
         filter: opacity(0);
     }
@@ -187,10 +187,10 @@
         display: flex;
 
         .close-menu {
-            margin-top: 0;
-            margin-bottom: 0;
-            margin-right: 0;
-            padding: 0;
+            margin-top: 0px;
+            margin-bottom: 0px;
+            margin-right: 0px;
+            padding: 0px;
             margin-left: auto;
             z-index: 999999999;
             width: 20px;
diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss
index daa711bc4..d74441b9c 100644
--- a/src/client/views/DashboardView.scss
+++ b/src/client/views/DashboardView.scss
@@ -79,8 +79,8 @@ $dashboard-container-width: 250px;
         position: absolute;
         width: 100%;
         height: 100%;
-        left: 0;
-        top: 0;
+        left: 0px;
+        top: 0px;
         z-index: -1;
     }
 }
@@ -137,8 +137,8 @@ $dashboard-container-width: 250px;
         position: absolute;
         width: 100%;
         height: 100%;
-        left: 0;
-        top: 0;
+        left: 0px;
+        top: 0px;
         z-index: -1;
     }
 }
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 55eb0b127..25a806abd 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -11,8 +11,8 @@ $resizeHandler: 8px;
     // Rotation handler
     .documentDecorations-rotation {
         border-radius: 100%;
-        height: 30;
-        width: 30;
+        height: 30px;
+        width: 30px;
         right: -40;
         bottom: -20;
         //top: calc(50% - 15px);
@@ -45,15 +45,15 @@ $resizeHandler: 8px;
         width: 100%;
         pointer-events: all;
         border-radius: 50%;
-        top: 30; // offset by height of documentButtonBar so that items can be clicked without overlap interference
+        top: 30px; // offset by height of documentButtonBar so that items can be clicked without overlap interference
         color: black;
     }
 }
 
 .documentDecorations-container {
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     transform-origin: 50% calc(50% + 10px);
     display: grid;
     grid-template-rows: $headerHeight $resizeHandler 1fr $resizeHandler;
@@ -121,7 +121,7 @@ $resizeHandler: 8px;
                 }
 
                 > svg {
-                    margin: 0;
+                    margin: 0px;
                 }
             }
 
@@ -150,7 +150,7 @@ $resizeHandler: 8px;
                 }
 
                 > svg {
-                    margin: 0;
+                    margin: 0px;
                 }
             }
             &:hover {
@@ -185,7 +185,7 @@ $resizeHandler: 8px;
             }
 
             > svg {
-                margin: 0;
+                margin: 0px;
             }
         }
 
@@ -246,7 +246,7 @@ $resizeHandler: 8px;
                 display: inline;
                 position: relative;
                 top: -2.5;
-                left: 35;
+                left: 35px;
                 zoom: 0.7;
             }
 
@@ -360,8 +360,8 @@ $resizeHandler: 8px;
         left: 7px;
         top: 7px;
         background: global.$medium-gray;
-        height: 10;
-        width: 10;
+        height: 10px;
+        width: 10px;
         opacity: 0.5;
         pointer-events: all;
         cursor: nwse-resize;
@@ -371,8 +371,8 @@ $resizeHandler: 8px;
         position: relative;
         background: black;
         color: rgb(145, 144, 144);
-        height: 20;
-        width: 20;
+        height: 20px;
+        width: 20px;
         pointer-events: all;
         margin: auto;
         display: flex;
@@ -380,8 +380,8 @@ $resizeHandler: 8px;
         border-radius: 100%;
         cursor: default;
         svg {
-            width: 10;
-            height: 10;
+            width: 10px;
+            height: 10px;
             margin: auto;
         }
     }
@@ -389,7 +389,7 @@ $resizeHandler: 8px;
     .documentDecorations-rotationPath {
         position: absolute;
         width: 100%;
-        height: 0;
+        height: 0px;
         transform: translate(0px, -25%);
         padding-bottom: 100%;
         border-radius: 100%;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 69c2467a3..2db334583 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -911,8 +911,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                                         transform: `rotate(${rotation}deg)`,
                                         width: this.Bounds.r - this.Bounds.x + 'px',
                                         height: this.Bounds.b - this.Bounds.y + 'px',
-                                        left: this.Bounds.x,
-                                        top: this.Bounds.y,
+                                        left: this.Bounds.r,
+                                        top: this.Bounds.b,
                                         pointerEvents: 'none',
                                     }}>
                                     {this._isRotating ? null : (
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
index fa4542ac4..333939d03 100644
--- a/src/client/views/EditableView.scss
+++ b/src/client/views/EditableView.scss
@@ -6,14 +6,14 @@
     overflow-y: auto;
     height: 100%;
     width: 100%;
-    min-width: 20;
+    min-width: 20px;
     text-overflow: ellipsis;
-    -ms-overflow-style: none; 
-    scrollbar-width: none; 
+    -ms-overflow-style: none;
+    scrollbar-width: none;
 }
 
 .editableView-container-editing::-webkit-scrollbar {
-    display: none; 
+    display: none;
 }
 
 .editableView-container-editing-oneLine {
@@ -44,4 +44,3 @@
     border: none;
     outline: none;
 }
-
diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss
index 508b1ee1f..e32db000f 100644
--- a/src/client/views/FilterPanel.scss
+++ b/src/client/views/FilterPanel.scss
@@ -1,4 +1,3 @@
-
 .filterBox-flyout {
     display: block;
     text-align: left;
@@ -29,7 +28,7 @@
 
 // .filterBox-bottom {
 //     // position: fixed;
-//     // bottom: 0;
+//     // bottom: 0px;
 //     // width: 100%;
 // }
 
@@ -88,7 +87,7 @@
 //     padding-bottom: 20px;
 //     border-bottom: 2px solid black;
 //     position: fixed;
-//     top: 0;
+//     top: 0px;
 //     width: 100%;
 // }
 
@@ -154,8 +153,8 @@
     flex-direction: column;
     width: 100%;
     position: relative;
-    right: 0;
-    top: 0;
+    right: 0px;
+    top: 0px;
     z-index: 1;
     //  background-color:  #9f9f9f;
 
@@ -240,60 +239,52 @@
     transition: all 0.3s ease-out;
     display: flex;
     flex-direction: row;
-    padding: 5px; 
-
+    padding: 5px;
 
-    &:hover{
+    &:hover {
         border-color: #e9e9e9;
-        background-color: #6d6c6c
+        background-color: #6d6c6c;
     }
 
-    .hotKey-icon, .hotKey-close{
+    .hotKey-icon,
+    .hotKey-close {
         background-color: transparent;
         border-radius: 10%;
         padding: 5px;
 
-
-        &:hover{
+        &:hover {
             background-color: #616060;
         }
     }
 
-    .hotKey-close{
+    .hotKey-close {
         right: 30px;
-        position: fixed;  
+        position: fixed;
         padding-top: 10px;
-  
-}
+    }
 
-    .hotkey-title{
+    .hotkey-title {
         top: 6px;
         position: relative;
         cursor: text;
-
     }
 
-    .hotkey-title-input{
+    .hotkey-title-input {
         background-color: transparent;
         border: none;
         border-color: transparent;
         outline: none;
         cursor: text;
-
     }
 }
 
 .hotKeyButtons {
     position: relative;
     width: 100%;
-
 }
 
 .hotKey-icon-button {
-
-        background-color: transparent;
-
-
+    background-color: transparent;
 }
 
 .icon-panel {
@@ -305,24 +296,19 @@
     border-radius: 10%;
     background-color: #323232;
 
-    .icon-panel-button{
+    .icon-panel-button {
         background-color: #323232;
         border-radius: 10%;
 
-
-        &:hover{
-            background-color:#7a7878
+        &:hover {
+            background-color: #7a7878;
         }
     }
-
-
-
 }
 
-
 // .sliderBox-outerDiv {
 //     width: 30%;// width: calc(100% - 14px);   // 14px accounts for handles that are at the max value of the slider that would extend outside the box
-//     height: 40; // height: 100%;
+//     height: 40px; // height: 100%;
 //     border-radius: inherit;
 //     display: flex;
 //     flex-direction: column;
diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss
index bfe2d5c64..0fa3fd973 100644
--- a/src/client/views/GestureOverlay.scss
+++ b/src/client/views/GestureOverlay.scss
@@ -3,7 +3,7 @@
     height: 100%;
     position: absolute;
     touch-action: none;
-    top: 0;
+    top: 0px;
 
     .pointerBubbles {
         width: 100%;
diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss
index c672824bf..0595283fc 100644
--- a/src/client/views/InkStroke.scss
+++ b/src/client/views/InkStroke.scss
@@ -8,8 +8,8 @@
     svg:not(:root) {
         overflow: visible !important;
         position: absolute;
-        left: 0;
-        top: 0;
+        left: 0px;
+        top: 0px;
     }
 }
 
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index 577acc4d1..0af46df5d 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -105,7 +105,6 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
         return (
             <>
                 {tangentHandles.map((pts, i) => (
-                    // eslint-disable-next-line react/no-array-index-key
                     <svg height="10" width="10" key={`hdl${i}`}>
                         <circle
                             cx={pts.X}
@@ -135,7 +134,6 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
                         />
                     );
                     return (
-                        // eslint-disable-next-line react/no-array-index-key
                         <svg height="100" width="100" key={`line${i}`}>
                             {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)}
                             {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)}
diff --git a/src/client/views/LightboxView.scss b/src/client/views/LightboxView.scss
index 3e65843df..7a481d887 100644
--- a/src/client/views/LightboxView.scss
+++ b/src/client/views/LightboxView.scss
@@ -1,12 +1,12 @@
 .lightboxView-navBtn {
     margin: auto;
     position: absolute;
-    right: 19;
-    top: 10;
+    right: 19px;
+    top: 10px;
     background: transparent;
-    border-radius: 8;
+    border-radius: 8px;
     opacity: 0.7;
-    width: 25;
+    width: 25px;
     flex-direction: column;
     display: flex;
     &:hover {
@@ -16,12 +16,12 @@
 .lightboxView-tabBtn {
     margin: auto;
     position: absolute;
-    right: 54;
-    top: 10;
+    right: 54px;
+    top: 10px;
     background: transparent;
-    border-radius: 8;
+    border-radius: 8px;
     opacity: 0.7;
-    width: 25;
+    width: 25px;
     flex-direction: column;
     display: flex;
     &:hover {
@@ -31,12 +31,12 @@
 .lightboxView-paletteBtn {
     margin: auto;
     position: absolute;
-    right: 89;
-    top: 10;
+    right: 89px;
+    top: 10px;
     background: transparent;
-    border-radius: 8;
+    border-radius: 8px;
     opacity: 0.7;
-    width: 25;
+    width: 25px;
     flex-direction: column;
     display: flex;
     &:hover {
@@ -47,12 +47,12 @@
 .lightboxView-penBtn {
     margin: auto;
     position: absolute;
-    right: 124;
-    top: 10;
+    right: 124px;
+    top: 10px;
     background: transparent;
-    border-radius: 8;
+    border-radius: 8px;
     opacity: 0.7;
-    width: 25;
+    width: 25px;
     flex-direction: column;
     display: flex;
     &:hover {
@@ -62,12 +62,12 @@
 .lightboxView-exploreBtn {
     margin: auto;
     position: absolute;
-    right: 159;
-    top: 10;
+    right: 159px;
+    top: 10px;
     background: transparent;
-    border-radius: 8;
+    border-radius: 8px;
     opacity: 0.7;
-    width: 25;
+    width: 25px;
     flex-direction: column;
     display: flex;
     &:hover {
@@ -76,8 +76,8 @@
 }
 .lightboxView-frame {
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     width: 100%;
     height: 100%;
     z-index: 1000;
@@ -91,9 +91,9 @@
             margin: auto;
             position: relative;
             background: transparent;
-            border-radius: 8;
+            border-radius: 8px;
             opacity: 0.7;
-            width: 35;
+            width: 35px;
             &:hover {
                 opacity: 1;
             }
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index bea1de435..df4160fc1 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -12,10 +12,10 @@ body {
     overflow: hidden;
     font-family: global.$sans-serif;
     font-size: global.$body-text;
-    margin: 0;
+    margin: 0px;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
 }
 
 // div {
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index db949285b..d5e6b8998 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -10,7 +10,7 @@ body {
 .dash-tooltip {
     font-size: 11px;
     padding: 2px;
-    max-width: 150;
+    max-width: 150px;
     line-height: 150%;
 }
 
@@ -49,8 +49,8 @@ body {
 
 .mainView-snapLines {
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     width: 100%;
     height: 100%;
     pointer-events: none;
@@ -61,8 +61,8 @@ body {
     height: 100%;
     position: absolute;
     pointer-events: all;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     z-index: 1;
     touch-action: none;
 }
@@ -86,8 +86,8 @@ body {
 .properties-container {
     height: 100%;
     position: absolute;
-    right: 0;
-    top: 0;
+    right: 0px;
+    top: 0px;
 }
 
 .mainView-propertiesDragger-minified,
@@ -98,7 +98,7 @@ body {
     width: 17px;
     position: absolute;
     top: 50%;
-    border-radius: 0;
+    border-radius: 0px;
     border-top-left-radius: 10px;
     border-bottom-left-radius: 10px;
     border-right: unset;
@@ -141,7 +141,7 @@ body {
     }
 
     .propertiesView {
-        left: 0;
+        left: 0px;
         position: absolute;
         z-index: 2;
         //   background-color: linen; //$light-gray;
@@ -165,7 +165,7 @@ body {
     }
 
     ::-webkit-scrollbar {
-        width: 0;
+        width: 0px;
     }
 }
 
@@ -173,19 +173,19 @@ body {
     width: 100%;
     height: 100%;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     overflow: hidden;
 }
 
 .buttonContainer {
     position: absolute;
-    bottom: 0;
+    bottom: 0px;
 
     .mainView-settings {
         // position: absolute;
-        // left: 0;
-        // bottom: 0;
+        // left: 0px;
+        // bottom: 0px;
         border-radius: 25%;
         margin-left: -5px;
         background: darkblue;
@@ -198,8 +198,8 @@ body {
 
 .mainView-logout {
     position: absolute;
-    right: 0;
-    bottom: 0;
+    right: 0px;
+    bottom: 0px;
     font-size: 8px;
 }
 
@@ -223,12 +223,12 @@ body {
     }
 
     .mainView-libraryFlyout-close {
-        right: 6;
-        top: 5;
+        right: 6px;
+        top: 5px;
         position: absolute;
         margin-right: 6px;
         z-index: 10;
-        margin-bottom: 10;
+        margin-bottom: 10px;
     }
 }
 
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 686f8ed88..13b14617c 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1039,7 +1039,7 @@ export class MainView extends ObservableReactComponent<object> {
 
     @computed get inkResources() {
         return (
-            <svg width={0} height={0}>
+            <svg width={0} height={0} style={{ display: 'block' }}>
                 <defs>
                     <filter id="inkSelectionHalo">
                         <feColorMatrix
diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
index 28de0b7a5..924476a30 100644
--- a/src/client/views/MetadataEntryMenu.scss
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -3,7 +3,7 @@
     width: 310px;
     flex-direction: column;
 
-    input[type=checkbox] {
+    input[type='checkbox'] {
         margin-left: 5px;
     }
 }
@@ -11,7 +11,7 @@
 .metadataEntry-autoSuggester {
     width: 80%;
     height: 100%;
-    margin: 0;
+    margin: 0px;
     display: inline-block;
 }
 
@@ -20,13 +20,13 @@
 }
 
 .metadataEntry-keys {
-    max-height: 80;
-    overflow-y: auto; 
+    max-height: 80px;
+    overflow-y: auto;
     display: flex;
     flex-direction: column;
 }
 .metadataEntry-inputArea {
-    display:inline-block;
+    display: inline-block;
     flex-direction: row;
 }
 
@@ -53,8 +53,8 @@
 }
 
 .react-autosuggest__input--open {
-    border-bottom-left-radius: 0;
-    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0px;
+    border-bottom-right-radius: 0px;
 }
 
 .react-autosuggest__suggestions-container {
@@ -78,8 +78,8 @@
 }
 
 .react-autosuggest__suggestions-list {
-    margin: 0;
-    padding: 0;
+    margin: 0px;
+    padding: 0px;
     list-style-type: none;
 }
 
@@ -90,4 +90,4 @@
 
 .react-autosuggest__suggestion--highlighted {
     background-color: #ddd;
-}
\ No newline at end of file
+}
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index 2e8621b5b..44203e38f 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -1,7 +1,7 @@
 .overlayView {
     position: absolute;
     pointer-events: none;
-    top: 0;
+    top: 0px;
     width: 100vw;
     height: 100vh;
     z-index: 2002; // shouold be greater than LightboxView's z-index so that link lines and the presentation mini player appear
@@ -14,8 +14,8 @@
     overflow: hidden;
     display: flex;
     flex-direction: column;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     pointer-events: all;
     box-shadow: black 5px 5px 5px;
 }
@@ -46,7 +46,7 @@
     float: right;
     height: 20px;
     width: 20px;
-    padding: 0;
+    padding: 0px;
     background-color: inherit;
 }
 
@@ -62,6 +62,6 @@
 .overlayView-doc {
     z-index: 9002; //so that it appears above chroma
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
 }
diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss
index 82488c750..eef120c58 100644
--- a/src/client/views/PreviewCursor.scss
+++ b/src/client/views/PreviewCursor.scss
@@ -2,8 +2,8 @@
     color: black;
     position: absolute;
     transform-origin: left top;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     pointer-events: none;
     opacity: 1;
     z-index: 1001;
diff --git a/src/client/views/PropertiesSection.tsx b/src/client/views/PropertiesSection.tsx
index 12a46c7a4..9ea9c3a3d 100644
--- a/src/client/views/PropertiesSection.tsx
+++ b/src/client/views/PropertiesSection.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed } from 'mobx';
 import { observer } from 'mobx-react';
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 280de4893..de3012948 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -5,7 +5,7 @@
 }
 .propertiesView-presentationTrails-title-icon {
     position: absolute;
-    right: 4;
+    right: 4px;
 }
 .propertiesView-palette {
     cursor: pointer;
@@ -24,7 +24,7 @@
 }
 .propertiesView {
     height: 100%;
-    width: 250;
+    width: 250px;
     font-family: 'Roboto';
     font-size: 12px;
     cursor: auto;
@@ -61,7 +61,7 @@
     }
 
     .propertiesView-info {
-        margin-top: -5;
+        margin-top: -5px;
         float: right;
         font-size: 20;
         path {
@@ -78,10 +78,10 @@
             display: flex;
 
             button {
-                width: 15;
-                height: 15;
-                padding: 0;
-                margin-top: -5;
+                width: 15px;
+                height: 15px;
+                padding: 0px;
+                margin-top: -5px;
             }
         }
 
@@ -89,8 +89,8 @@
             display: flex;
 
             button {
-                width: 5;
-                height: 5;
+                width: 5px;
+                height: 5px;
             }
 
             input {
@@ -171,16 +171,16 @@
                 display: flex;
 
                 button {
-                    width: 15;
-                    height: 15;
-                    padding: 0;
-                    margin-top: -5;
+                    width: 15px;
+                    height: 15px;
+                    padding: 0px;
+                    margin-top: -5px;
                 }
             }
 
             button {
-                width: 5;
-                height: 5;
+                width: 5px;
+                height: 5px;
             }
 
             input {
@@ -301,8 +301,8 @@
         padding: 7px;
         border-radius: 7px;
         margin-right: 32px;
-        width: 32;
-        height: 32;
+        width: 32px;
+        height: 32px;
         padding-top: 9px;
         margin-left: 18px;
 
@@ -318,8 +318,8 @@
         padding: 7px;
         border-radius: 7px;
         margin-right: 32px;
-        width: 32;
-        height: 32;
+        width: 32px;
+        height: 32px;
         padding-top: 9px;
         padding-left: 10px;
 
@@ -334,8 +334,8 @@
         background-color: #333333;
         padding: 7px;
         border-radius: 7px;
-        width: 32;
-        height: 32;
+        width: 32px;
+        height: 32px;
         padding-top: 9px;
         padding-left: 10px;
 
@@ -410,7 +410,7 @@
 
 .color-palette {
     width: 160px;
-    height: 360;
+    height: 360px;
 }
 
 .strokeAndFill {
@@ -465,7 +465,7 @@
     .propertiesView-selectedList {
         min-width: max-content;
         width: 100%;
-        max-height: 180;
+        max-height: 180px;
         overflow: hidden;
         overflow-y: scroll;
         border-left: solid 1px darkgrey;
@@ -475,7 +475,7 @@
         .selectedList-items {
             font-size: 12;
             font-weight: 300;
-            margin-top: 1;
+            margin-top: 1px;
         }
     }
 }
@@ -500,7 +500,7 @@
 
         .width-range {
             margin-right: 1px;
-            margin-bottom: 6;
+            margin-bottom: 6px;
         }
     }
 
diff --git a/src/client/views/SidebarAnnos.scss b/src/client/views/SidebarAnnos.scss
index abfd04f11..c2b9dcce5 100644
--- a/src/client/views/SidebarAnnos.scss
+++ b/src/client/views/SidebarAnnos.scss
@@ -2,7 +2,7 @@
     position: absolute;
     width: 100%;
     height: 100%;
-    right: 0;
+    right: 0px;
     .sidebarAnnos-stacking {
         width: 100%;
         position: relative;
@@ -20,12 +20,12 @@
     .sidebarAnnos-filterUser-active {
         font-weight: bold;
         font-size: 10px;
-        padding-left: 5;
-        padding-right: 5;
+        padding-left: 5px;
+        padding-right: 5px;
         box-shadow: black 1px 1px 3px;
-        border-radius: 5;
-        margin: 2;
-        height: 15;
+        border-radius: 5px;
+        margin: 2px;
+        height: 15px;
         background-color: lightgrey;
     }
     .sidebarAnnos-filterUser,
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index 99796f1fb..cbb1fd5d5 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -4,11 +4,11 @@
 .styleProvider-lock {
     z-index: 2; // has to be above title which is z-index 1
     font-size: 10;
-    width: 20;
-    height: 20;
+    width: 20px;
+    height: 20px;
     position: absolute;
-    right: -20;
-    top: 0;
+    right: -20px;
+    top: 0px;
     background: black;
     pointer-events: all;
     opacity: 0.3;
@@ -20,10 +20,10 @@
     cursor: default;
 }
 .styleProvider-filter {
-    right: 20;
+    right: 20px;
     .styleProvider-filterShift {
-        left: 0;
-        top: 0;
+        left: 0px;
+        top: 0px;
         position: absolute;
     }
     .dropdown-container {
@@ -33,10 +33,10 @@
 }
 .styleProvider-paint-selected,
 .styleProvider-paint {
-    top: 15;
+    top: 15px;
 }
 .styleProvider-paint-selected {
-    right: -40;
+    right: -40px;
 }
 .styleProvider-lock:hover,
 .styleProvider-filter:hover {
@@ -45,8 +45,8 @@
 
 .styleProvider-treeView-icon,
 .styleProvider-treeView-icon-active {
-    margin-left: 0;
-    margin-right: 0;
+    margin-left: 0px;
+    margin-right: 0px;
 }
 
 .styleProvider-treeView-icon {
diff --git a/src/client/views/StyleProviderQuiz.scss b/src/client/views/StyleProviderQuiz.scss
index 84b3f1fef..53ca34c1b 100644
--- a/src/client/views/StyleProviderQuiz.scss
+++ b/src/client/views/StyleProviderQuiz.scss
@@ -13,8 +13,8 @@
 
 .check-icon {
     position: absolute;
-    right: 40;
-    bottom: 10;
+    right: 40px;
+    bottom: 10px;
     color: green;
     display: inline-block;
     font-size: 20px;
@@ -23,8 +23,8 @@
 
 .redo-icon {
     position: absolute;
-    right: 10;
-    bottom: 10;
+    right: 10px;
+    bottom: 10px;
     color: black;
     display: inline-block;
     font-size: 20px;
diff --git a/src/client/views/animationtimeline/Timeline.scss b/src/client/views/animationtimeline/Timeline.scss
index e1d3b190c..3a50183d2 100644
--- a/src/client/views/animationtimeline/Timeline.scss
+++ b/src/client/views/animationtimeline/Timeline.scss
@@ -112,8 +112,8 @@ $timelineDark: #77a1aa;
         input {
             position: absolute;
             opacity: 0;
-            height: 0;
-            width: 0;
+            height: 0px;
+            width: 0px;
         }
 
         .round-toggle-slider {
diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx
index 0d7873931..79283479c 100644
--- a/src/client/views/animationtimeline/TimelineMenu.tsx
+++ b/src/client/views/animationtimeline/TimelineMenu.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
-/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { IconLookup } from '@fortawesome/fontawesome-svg-core';
 import { faChartLine, faClipboard } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -19,7 +17,7 @@ export class TimelineMenu extends React.Component {
     @observable private _y = 0;
     @observable private _currentMenu: JSX.Element[] = [];
 
-    constructor(props: any) {
+    constructor(props: object) {
         super(props);
         makeObservable(this);
         TimelineMenu.Instance = this;
diff --git a/src/client/views/animationtimeline/TimelineOverview.tsx b/src/client/views/animationtimeline/TimelineOverview.tsx
index 7bf685c9e..fff756980 100644
--- a/src/client/views/animationtimeline/TimelineOverview.tsx
+++ b/src/client/views/animationtimeline/TimelineOverview.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/no-unused-prop-types */
 import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -14,7 +13,7 @@ interface TimelineOverviewProps {
     isAuthoring: boolean;
     parent: Timeline;
     changeCurrentBarX: (pixel: number) => void;
-    movePanX: (pixel: number) => any;
+    movePanX: (pixel: number) => void;
     time: number;
     tickSpacing: number;
     tickIncrement: number;
@@ -139,7 +138,7 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps> {
         const percentVisible = this.visibleTime / this.props.time;
         const visibleBarWidth = percentVisible * this.activeOverviewWidth;
 
-        const percentScrubberStart = this.currentX / this.props.time;
+        // const percentScrubberStart = this.currentX / this.props.time;
         let scrubberStart = (this.props.currentBarX / this.props.totalLength) * this.activeOverviewWidth;
         if (scrubberStart > this.activeOverviewWidth) scrubberStart = this.activeOverviewWidth;
 
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index e6cc398af..981e528cc 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -18,7 +18,7 @@
 }
 
 .collectionCardView-flashcardUI {
-    top: 0;
+    top: 0px;
     position: absolute;
     width: 100%;
     height: 100%;
@@ -33,7 +33,7 @@
 }
 .collectionCardView-cardSizeDragger {
     position: absolute;
-    top: 0;
+    top: 0px;
     width: 28px;
     height: 28px;
     > svg {
diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss
index 13e6b54c2..361d88cb6 100644
--- a/src/client/views/collections/CollectionCarousel3DView.scss
+++ b/src/client/views/collections/CollectionCarousel3DView.scss
@@ -12,7 +12,7 @@
     position: absolute;
     top: global.$CAROUSEL3D_TOP * 1%;
     height: (global.$CAROUSEL3D_SIDE_SCALE * 100) * 1%;
-    align-items: center;
+    //align-items: center;
     transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
 
     .collectionCarousel3DView-item,
@@ -67,8 +67,8 @@
 .carousel3DView-fwd-scroll-hidden {
     position: absolute;
     display: flex;
-    width: 30;
-    height: 30;
+    width: 30px;
+    height: 30px;
     align-items: center;
     border-radius: 5px;
     justify-content: center;
@@ -78,7 +78,7 @@
 
 .carousel3DView-fwd,
 .carousel3DView-back {
-    top: 0;
+    top: 0px;
     background: transparent;
     width: calc((1 - #{global.$CAROUSEL3D_CENTER_SCALE} * 0.33) / 2 * 100%);
     height: 100%;
@@ -94,13 +94,13 @@
 .carousel3DView-fwd,
 .carousel3DView-fwd-scroll,
 .carousel3DView-fwd-scroll-hidden {
-    right: 0;
+    right: 0px;
 }
 
 .carousel3DView-back,
 .carousel3DView-back-scroll,
 .carousel3DView-back-scroll-hidden {
-    left: 0;
+    left: 0px;
 }
 
 .carousel3DView-fwd-scroll-hidden,
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index 962b590c8..4c999b6dd 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -6,10 +6,10 @@
     transform-origin: top left;
 
     .collectionCarouselView-caption {
-        height: 50;
+        height: 50px;
         display: inline-block;
         width: 100%;
-        bottom: 0;
+        bottom: 0px;
         position: absolute;
     }
     .collectionCarouselView-image {
@@ -18,8 +18,8 @@
         width: 100%;
         user-select: none;
         position: absolute;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
     }
 }
 .collectionCarouselView-recentlyMissed {
@@ -34,8 +34,8 @@
 .carouselView-fwd {
     position: absolute;
     display: flex;
-    width: 30;
-    height: 30;
+    width: 30px;
+    height: 30px;
     align-items: center;
     border-radius: 5px;
     justify-content: center;
@@ -47,12 +47,12 @@
 }
 .carouselView-fwd {
     top: calc(50% - 15px);
-    right: 0;
+    right: 0px;
     transform-origin: right top;
 }
 .carouselView-back {
     top: calc(50% - 15px);
-    left: 0;
+    left: 0px;
     transform-origin: top left;
 }
 .carouselView-back:hover,
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 7c19d39da..de214e2ef 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -17,8 +17,8 @@
 }
 .lm_maximised {
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     z-index: 40;
 }
 .lm_maximise_placeholder {
@@ -62,8 +62,8 @@
     text-align: center;
 }
 .lm_header ul {
-    margin: 0;
-    padding: 0;
+    margin: 0px;
+    padding: 0px;
     list-style-type: none;
 }
 .lm_header .lm_tab {
@@ -81,11 +81,11 @@
     position: absolute;
 }
 .lm_header .lm_tab i.lm_left {
-    top: 0;
+    top: 0px;
     left: -2px;
 }
 .lm_header .lm_tab i.lm_right {
-    top: 0;
+    top: 0px;
     right: -2px;
 }
 .lm_header .lm_tab .lm_title {
@@ -97,8 +97,8 @@
     width: 14px;
     height: 14px;
     position: absolute;
-    top: 0;
-    right: 0;
+    top: 0px;
+    right: 0px;
     text-align: center;
 }
 .lm_stack.lm_left .lm_header,
@@ -118,14 +118,14 @@
 .lm_stack.lm_left .lm_header .lm_tabs,
 .lm_stack.lm_right .lm_header .lm_tabs {
     transform-origin: left top;
-    top: 0;
+    top: 0px;
     width: 1000px;
 }
 .lm_dragProxy.lm_left .lm_header .lm_controls,
 .lm_dragProxy.lm_right .lm_header .lm_controls,
 .lm_stack.lm_left .lm_header .lm_controls,
 .lm_stack.lm_right .lm_header .lm_controls {
-    bottom: 0;
+    bottom: 0px;
 }
 .lm_dragProxy.lm_left .lm_items,
 .lm_dragProxy.lm_right .lm_items,
@@ -136,7 +136,7 @@
 .lm_dragProxy.lm_left .lm_header .lm_tabs,
 .lm_stack.lm_left .lm_header .lm_tabs {
     transform: rotate(-90deg) scaleX(-1);
-    left: 0;
+    left: 0px;
 }
 .lm_dragProxy.lm_left .lm_header .lm_tabs .lm_tab,
 .lm_stack.lm_left .lm_header .lm_tabs .lm_tab {
@@ -156,7 +156,7 @@
 .lm_stack.lm_right .lm_header .lm_tabs {
     transform: rotate(90deg) scaleX(1);
     left: 100%;
-    margin-left: 0;
+    margin-left: 0px;
 }
 .lm_dragProxy.lm_right .lm_header .lm_controls,
 .lm_stack.lm_right .lm_header .lm_controls {
@@ -169,7 +169,7 @@
 }
 .lm_dragProxy.lm_bottom .lm_header .lm_tab,
 .lm_stack.lm_bottom .lm_header .lm_tab {
-    margin-top: 0;
+    margin-top: 0px;
     border-top: none;
 }
 .lm_dragProxy.lm_bottom .lm_header .lm_controls,
@@ -189,8 +189,8 @@
 }
 .lm_header .lm_controls .lm_tabdropdown:before {
     content: '';
-    width: 0;
-    height: 0;
+    width: 0px;
+    height: 0px;
     vertical-align: middle;
     display: inline-block;
     border-top: 5px dashed;
@@ -201,14 +201,14 @@
 .lm_header .lm_tabdropdown_list {
     position: absolute;
     top: 20px;
-    right: 0;
+    right: 0px;
     z-index: 5;
     overflow: hidden;
 }
 .lm_header .lm_tabdropdown_list .lm_tab {
     clear: both;
     padding-right: 10px;
-    margin: 0;
+    margin: 0px;
 }
 .lm_header .lm_tabdropdown_list .lm_tab .lm_title {
     width: 100px;
@@ -218,8 +218,8 @@
 }
 .lm_dragProxy {
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     z-index: 30;
 }
 .lm_dragProxy .lm_header {
@@ -238,32 +238,32 @@
     width: 100%;
     height: 100%;
     position: relative;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
 }
 .lm_transition_indicator {
     display: none;
     width: 20px;
     height: 20px;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     z-index: 20;
 }
 .lm_popin {
     width: 20px;
     height: 20px;
     position: absolute;
-    bottom: 0;
-    right: 0;
+    bottom: 0px;
+    right: 0px;
     z-index: 9999;
 }
 .lm_popin > * {
     width: 100%;
     height: 100%;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
 }
 .lm_popin > .lm_bg {
     z-index: 10;
@@ -307,7 +307,7 @@
     width: max-content;
     height: 100%;
     display: flex;
-    max-width: 100;
+    max-width: 100px;
     text-overflow: ellipsis;
 }
 
@@ -328,7 +328,7 @@
 ul.lm_tabs::before {
     content: ' ';
     position: absolute;
-    bottom: 0;
+    bottom: 0px;
     width: 100%;
     z-index: 1;
     pointer-events: none;
@@ -349,9 +349,9 @@ ul.lm_tabs::before {
     }
 }
 .lm_header .lm_tab.lm_active {
-    padding: 0;
+    padding: 0px;
     opacity: 1;
-    margin: 0;
+    margin: 0px;
     box-shadow: none;
     height: 27px;
     margin-right: 2px;
@@ -405,7 +405,7 @@ ul.lm_tabs::before {
 }
 
 .lm_drag_tab {
-    padding: 0;
+    padding: 0px;
     width: 15px !important;
     height: 15px !important;
     position: relative !important;
@@ -418,7 +418,7 @@ ul.lm_tabs::before {
 
 .lm_close_tab {
     display: inline-flex !important;
-    padding: 0;
+    padding: 0px;
     opacity: 1 !important;
     align-self: center;
     margin-right: 5px;
@@ -455,7 +455,7 @@ ul.lm_tabs::before {
         content: 'x';
         margin: auto;
         position: relative;
-        top: -2;
+        top: -2px;
         font-size: medium;
         font-family: sans-serif;
     }
@@ -478,8 +478,8 @@ ul.lm_tabs::before {
     width: 100%;
     height: 100%;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
 
     .collectionDockingView-drag {
         touch-action: none;
@@ -514,7 +514,7 @@ ul.lm_tabs::before {
         border-bottom-left-radius: 10px;
         background: #93939347;
         z-index: 100;
-        //left: -3;
+        //left: -3px;
         &:hover {
             background: gray;
             color: white !important;
@@ -524,7 +524,7 @@ ul.lm_tabs::before {
         content: '+';
         margin: auto;
         font-size: x-large;
-        top: -4;
+        top: -4px;
         position: relative;
     }
     .lm_maximise {
@@ -548,10 +548,10 @@ ul.lm_tabs::before {
     }
 
     .flexlayout__layout {
-        left: 0;
-        top: 0;
-        right: 0;
-        bottom: 0;
+        left: 0px;
+        top: 0px;
+        right: 0px;
+        bottom: 0px;
         position: absolute;
         overflow: hidden;
     }
@@ -691,8 +691,8 @@ ul.lm_tabs::before {
 
     .flexlayout__tabset_header {
         position: absolute;
-        left: 0;
-        right: 0;
+        left: 0px;
+        right: 0px;
         color: #eee;
         background-color: #212121;
         padding: 3px 3px 3px 5px;
@@ -702,17 +702,17 @@ ul.lm_tabs::before {
 
     .flexlayout__tab_header_inner {
         position: absolute;
-        left: 0;
-        top: 0;
-        bottom: 0;
+        left: 0px;
+        top: 0px;
+        bottom: 0px;
         width: 10000px;
     }
 
     .flexlayout__tab_header_outer {
         background-color: global.$dark-gray;
         position: absolute;
-        left: 0;
-        right: 0;
+        left: 0px;
+        right: 0px;
         /*top: 0px;*/
         /*height: 100px;*/
         overflow: hidden;
@@ -731,23 +731,23 @@ ul.lm_tabs::before {
         display: flex;
         flex-direction: row-reverse;
         align-items: center;
-        top: 0;
-        bottom: 0;
-        right: 0;
+        top: 0px;
+        bottom: 0px;
+        right: 0px;
     }
 
     .flexlayout__tab_toolbar_button-min {
         width: 20px;
         height: 20px;
         border: none;
-        outline-width: 0;
+        outline-width: 0px;
     }
 
     .flexlayout__tab_toolbar_button-max {
         width: 20px;
         height: 20px;
         border: none;
-        outline-width: 0;
+        outline-width: 0px;
     }
 
     .flexlayout__popup_menu_item {
@@ -870,9 +870,9 @@ ul.lm_tabs::before {
         display: flex;
         flex-direction: column-reverse;
         align-items: center;
-        bottom: 0;
-        left: 0;
-        right: 0;
+        bottom: 0px;
+        left: 0px;
+        right: 0px;
     }
 
     .flexlayout__border_toolbar_right {
@@ -880,9 +880,9 @@ ul.lm_tabs::before {
         display: flex;
         flex-direction: column-reverse;
         align-items: center;
-        bottom: 0;
-        left: 0;
-        right: 0;
+        bottom: 0px;
+        left: 0px;
+        right: 0px;
     }
 
     .flexlayout__border_toolbar_top {
@@ -890,9 +890,9 @@ ul.lm_tabs::before {
         display: flex;
         flex-direction: row-reverse;
         align-items: center;
-        top: 0;
-        bottom: 0;
-        right: 0;
+        top: 0px;
+        bottom: 0px;
+        right: 0px;
     }
 
     .flexlayout__border_toolbar_bottom {
@@ -900,8 +900,8 @@ ul.lm_tabs::before {
         display: flex;
         flex-direction: row-reverse;
         align-items: center;
-        top: 0;
-        bottom: 0;
-        right: 0;
+        top: 0px;
+        bottom: 0px;
+        right: 0px;
     }
 }
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index d1f7971d4..164c6e831 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -7,6 +7,7 @@ import { emptyFunction, numberRange } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
 import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { ScriptField } from '../../../fields/ScriptField';
+import { StrCast } from '../../../fields/Types';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { CompileScript } from '../../util/Scripting';
@@ -16,6 +17,7 @@ import { undoBatch, undoable } from '../../util/UndoManager';
 import { EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DocumentView } from '../nodes/DocumentView';
+import { ImportElementBox } from '../nodes/importBox/ImportElementBox';
 import { CollectionStackingView } from './CollectionStackingView';
 import './CollectionStackingView.scss';
 
@@ -254,7 +256,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor(this._props.panelWidth() / this._props.columnWidth())));
         return this.collapsed ? null : (
             <div style={{ position: 'relative' }}>
-                {!this._props.chromeHidden ? (
+                {!this._props.chromeHidden && !StrCast(this._props.Doc.childLayoutString).includes(ImportElementBox.name) ? (
                     <div className="collectionStackingView-addDocumentButton">
                         <EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents="+ NEW" />
                     </div>
diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss
index 0d24a56b5..231085338 100644
--- a/src/client/views/collections/CollectionNoteTakingView.scss
+++ b/src/client/views/collections/CollectionNoteTakingView.scss
@@ -82,7 +82,7 @@
     height: 100%;
     width: 100%;
     position: absolute;
-    top: 0;
+    top: 0px;
     overflow-y: auto;
     overflow-x: hidden;
     transition: top 0.5s;
@@ -130,8 +130,8 @@
         display: flex;
         flex-direction: column;
         align-items: center;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
         width: 100%;
         position: absolute;
         margin: auto;
@@ -152,20 +152,20 @@
     }
 
     .collectionNoteTakingView-columnDragger {
-        width: 15;
-        height: 15;
+        width: 15px;
+        height: 15px;
         position: absolute;
-        margin-left: -5;
+        margin-left: -5px;
     }
 
     .collectionNoteTakingView-sectionDelete {
         display: none;
         position: absolute;
-        right: 0;
+        right: 0px;
         width: max-content;
         height: max-content;
-        top: 10;
-        padding: 2;
+        top: 10px;
+        padding: 2px;
     }
 
     // Documents in NoteTaking view
@@ -210,8 +210,8 @@
         height: 5px;
 
         &.active {
-            margin-left: 0;
-            margin-right: 0;
+            margin-left: 0px;
+            margin-right: 0px;
             background: red;
         }
     }
@@ -303,8 +303,8 @@
 
         .collectionNoteTakingView-sectionColor {
             position: absolute;
-            left: 0;
-            top: 0;
+            left: 0px;
+            top: 0px;
             height: 100%;
             display: none;
 
@@ -345,8 +345,8 @@
 
         .collectionNoteTakingView-sectionOptions {
             position: absolute;
-            right: 0;
-            top: 0;
+            right: 0px;
+            top: 0px;
             height: 100%;
             display: none;
 
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index d05c0ffde..e71df2164 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -38,7 +38,7 @@
         height: 100%;
         background-color: global.$dark-gray;
         opacity: 0.3;
-        top: 0;
+        top: 0px;
     }
 
     .collectionStackedTimeline-trim-controls {
@@ -49,8 +49,8 @@
         display: flex;
         justify-content: space-between;
         max-width: 100%;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
 
         .collectionStackedTimeline-trim-handle {
             background-color: global.$medium-blue;
@@ -106,18 +106,18 @@
         .collectionStackedTimeline-resizer {
             background: global.$dark-gray;
             position: absolute;
-            top: 0;
+            top: 0px;
             height: 100%;
             width: 10px;
             pointer-events: all;
             z-index: 100;
         }
         .collectionStackedTimeline-resizer {
-            right: 0;
+            right: 0px;
             cursor: e-resize;
         }
         .collectionStackedTimeline-left-resizer {
-            left: 0;
+            left: 0px;
             cursor: w-resize;
         }
     }
@@ -126,8 +126,8 @@
         position: absolute;
         width: 100%;
         height: 100%;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
         pointer-events: none;
     }
 }
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index d6e4943ff..2cf361847 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -16,10 +16,10 @@
     width: 100%;
 
     .collectionStackingView-columnDragger {
-        width: 28;
-        height: 28;
-        position: absolute;
-        margin-left: -5;
+        width: 28px;
+        height: 28px;
+        position: relative;
+        margin-left: -5px;
         z-index: 10;
         > svg {
             width: 100%;
@@ -55,7 +55,7 @@
         height: 100%;
         width: 100%;
         position: absolute;
-        top: 0;
+        top: 0px;
         overflow-y: auto;
         overflow-x: hidden;
         transition: top 0.5s;
@@ -93,8 +93,8 @@
         .collectionStackingView-masonryGrid {
             width: 100%;
             display: grid;
-            top: 0;
-            left: 0;
+            top: 0px;
+            left: 0px;
         }
 
         .collectionStackingView-masonrySingle {
@@ -115,8 +115,8 @@
             position: absolute;
             display: flex;
             flex-direction: column;
-            top: 0;
-            left: 0;
+            top: 0px;
+            left: 0px;
             width: 100%;
             position: absolute;
         }
@@ -159,12 +159,12 @@
         width: 100%;
         display: none;
         position: absolute;
-        top: 0;
+        top: 0px;
         cursor: default;
 
         &.active {
-            margin-left: 0;
-            margin-right: 0;
+            margin-left: 0px;
+            margin-right: 0px;
             background: red;
         }
     }
@@ -262,8 +262,8 @@
 
         .collectionStackingView-sectionColor {
             position: absolute;
-            left: 0;
-            top: 0;
+            left: 0px;
+            top: 0px;
             height: 100%;
             display: none;
 
@@ -304,8 +304,8 @@
 
         .collectionStackingView-sectionOptions {
             position: absolute;
-            right: 0;
-            top: 0;
+            right: 0px;
+            top: 0px;
             height: 100%;
             display: none;
 
@@ -339,7 +339,7 @@
         .collectionStackingView-sectionDelete {
             position: absolute;
             right: 0px;
-            top: 0;
+            top: 0px;
             height: 100%;
             display: none;
         }
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 4a0ddc631..bdeb7d944 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -64,7 +64,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     @observable _cursor: CSS.Property.Cursor = 'ew-resize';
     // gets reset whenever we scroll. Not sure what it is
     @observable _scroll = 0; // used to force the document decoration to update when scrolling
-    // does this mean whether the browser is hidden? Or is chrome something else entirely?
+    // whether ui/editing controls are shown
     @computed get chromeHidden() {
         return this._props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
     }
@@ -566,7 +566,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         return (
             <div key={(heading?.heading ?? '') + 'head'}>
                 {this._props.isContentActive() && !this.isStackingView && !this.chromeHidden ? this.columnDragger : null}
-                <div style={{ top: this.yMargin }}>
+                <div style={{ position: 'relative' }}>
                     <CollectionMasonryViewFieldRow
                         showHandle={first}
                         Doc={this.Document}
@@ -715,9 +715,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                             background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string,
                             pointerEvents: this._props.pointerEvents?.() ?? this.backgroundEvents,
                         }}
-                        onScroll={action(e => {
-                            this._scroll = e.currentTarget.scrollTop;
-                        })}
+                        onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
                         onDrop={this.onExternalDrop.bind(this)}
                         onContextMenu={this.onContextMenu}
                         onWheel={e => this.isContentActive() && e.stopPropagation()}>
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 8c7cb8276..8c535534a 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -4,6 +4,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { DivHeight, DivWidth, returnEmptyString, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
 import { RichTextField } from '../../../fields/RichTextField';
 import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { ScriptField } from '../../../fields/ScriptField';
@@ -26,7 +27,6 @@ import { EditableView } from '../EditableView';
 import { DocumentView } from '../nodes/DocumentView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import './CollectionStackingView.scss';
-import { DocData } from '../../../fields/DocSymbols';
 
 // So this is how we are storing a column
 interface CSVFieldColumnProps {
diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss
index d995cbcd2..d56999974 100644
--- a/src/client/views/collections/CollectionTimeView.scss
+++ b/src/client/views/collections/CollectionTimeView.scss
@@ -27,7 +27,7 @@
         transform: rotate(45deg);
         display: inline-block;
         background: gray;
-        bottom: 0;
+        bottom: 0px;
         margin-bottom: -17px;
         border-radius: 9px;
         opacity: 0.25;
@@ -67,9 +67,9 @@
         pointer-events: all;
         padding: 5px;
         border: 1px solid black;
-        display:none;
+        display: none;
         span {
-            margin-left : 10px;
+            margin-left: 10px;
         }
     }
 
@@ -86,8 +86,9 @@
     }
 }
 
-.collectionTimeView:hover, .collectionTimeView-pivot:hover {
+.collectionTimeView:hover,
+.collectionTimeView-pivot:hover {
     .pivotKeyEntry {
-        display:unset;
+        display: unset;
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 2a03ea708..95faaa3f0 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -11,7 +11,7 @@
     height: 100%;
     width: 100%;
     position: relative;
-    top: 0;
+    top: 0px;
     // background: global.$light-gray;
     font-size: 13px;
     overflow: auto;
@@ -33,12 +33,12 @@
     }
 
     .no-indent {
-        padding-left: 0;
+        padding-left: 0px;
         //width: max-content;
     }
 
     .no-indent-outline {
-        padding-left: 0;
+        padding-left: 0px;
         width: 100%;
     }
 
diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss
index 06c324bd0..837219e1d 100644
--- a/src/client/views/collections/CollectionView.scss
+++ b/src/client/views/collections/CollectionView.scss
@@ -1,7 +1,7 @@
 @use '../global/globalCssVariables.module.scss' as global;
 
 .collectionView {
-    border-width: 0;
+    border-width: 0px;
     border-color: global.$light-gray;
     border-style: solid;
     border-radius: 0 0 global.$border-radius global.$border-radius;
@@ -18,7 +18,7 @@
         position: absolute;
         top: 55%;
         border: 1px black solid;
-        border-radius: 0;
+        border-radius: 0px;
         border-top-left-radius: 20px;
         border-bottom-left-radius: 20px;
         border-right: unset;
@@ -31,8 +31,8 @@
         width: 200px;
         height: 100%;
         position: absolute;
-        right: 0;
-        top: 0;
+        right: 0px;
+        top: 0px;
         border-left: solid 1px;
         z-index: 1;
 
diff --git a/src/client/views/collections/FlashcardPracticeUI.scss b/src/client/views/collections/FlashcardPracticeUI.scss
index 210c6798f..0cc4711b3 100644
--- a/src/client/views/collections/FlashcardPracticeUI.scss
+++ b/src/client/views/collections/FlashcardPracticeUI.scss
@@ -8,8 +8,8 @@
 .FlashcardPracticeUI-check {
     position: absolute;
     display: flex;
-    width: 30;
-    height: 30;
+    width: 30px;
+    height: 30px;
     align-items: center;
     border-radius: 5px;
     justify-content: center;
@@ -39,7 +39,7 @@
     display: flex;
     top: 0px;
     left: 0px;
-    width: 30;
+    width: 30px;
     transform-origin: top left;
     border-radius: 5px;
     color: rgba(255, 255, 255, 0.5);
@@ -49,7 +49,7 @@
         width: 100%;
         display: flex;
         flex-direction: column;
-        top: 0;
+        top: 0px;
         position: relative;
         .FlashcardPracticeUI-quiz,
         .FlashcardPracticeUI-practice {
diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx
deleted file mode 100644
index 7dc08389b..000000000
--- a/src/client/views/collections/KeyRestrictionRow.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-/* eslint-disable react/button-has-type */
-import { observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-
-interface IKeyRestrictionProps {
-    contains: boolean;
-    script: (value: string) => void;
-    field: string;
-    value: string;
-}
-
-@observer
-export default class KeyRestrictionRow extends React.Component<IKeyRestrictionProps> {
-    @observable private _key = this.props.field;
-    @observable private _value = this.props.value;
-    @observable private _contains = this.props.contains;
-
-    render() {
-        if (this._key && this._value) {
-            let parsedValue: string | number = `"${this._value}"`;
-            const parsed = parseInt(this._value);
-            let type = 'string';
-            if (!isNaN(parsed)) {
-                parsedValue = parsed;
-                type = 'number';
-            }
-            const scriptText = `${this._contains ? '' : '!'}(((doc.${this._key} && (doc.${this._key} as ${type})${type === 'string' ? '.includes' : '<='}(${parsedValue}))) ||
-                ((doc.data_ext && doc.data_ext.${this._key}) && (doc.data_ext.${this._key} as ${type})${type === 'string' ? '.includes' : '<='}(${parsedValue}))))`;
-            // let doc = new Doc();
-            // ((doc.data_ext && doc.data_ext!.text) && (doc.data_ext!.text as string).includes("hello"));
-            this.props.script(scriptText);
-        } else {
-            this.props.script('');
-        }
-
-        return (
-            <div className="collectionViewBaseChrome-viewSpecsMenu-row">
-                <input
-                    className="collectionViewBaseChrome-viewSpecsMenu-rowLeft"
-                    value={this._key}
-                    onChange={e =>
-                        runInAction(() => {
-                            this._key = e.target.value;
-                        })
-                    }
-                    placeholder="KEY"
-                />
-                <button
-                    className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle"
-                    style={{ background: this._contains ? '#77dd77' : '#ff6961' }}
-                    onClick={() =>
-                        runInAction(() => {
-                            this._contains = !this._contains;
-                        })
-                    }>
-                    {this._contains ? 'CONTAINS' : 'DOES NOT CONTAIN'}
-                </button>
-                <input
-                    className="collectionViewBaseChrome-viewSpecsMenu-rowRight"
-                    value={this._value}
-                    onChange={e =>
-                        runInAction(() => {
-                            this._value = e.target.value;
-                        })
-                    }
-                    placeholder="VALUE"
-                />
-            </div>
-        );
-    }
-}
diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss
index 931cdac2b..b705d17f3 100644
--- a/src/client/views/collections/TabDocView.scss
+++ b/src/client/views/collections/TabDocView.scss
@@ -92,6 +92,6 @@ input.lm_title {
 .miniMap-hidden {
     cursor: pointer;
     position: absolute;
-    bottom: 5;
-    right: 5;
+    bottom: 5px;
+    right: 5px;
 }
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 78794d112..542b0cc87 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -136,8 +136,8 @@
         width: 100%;
         height: 100%;
         position: absolute;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
         z-index: 0;
         filter: opacity(0);
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
index 7951aff65..32cf3586f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
@@ -18,5 +18,5 @@
     color: black;
     // fontStyle: "italic",
     margin-left: -12;
-    margin-top: 4;
+    margin-top: 4px;
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 6c47a71b0..ac1ef7d65 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -2,8 +2,8 @@
 
 .collectionfreeformview-none {
     position: inherit;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     width: 100%;
     height: 100%;
     transform-origin: left top;
@@ -12,10 +12,10 @@
     border-radius: inherit;
 }
 .collectionFreeForm-groupDropper {
-    width: 10000;
-    height: 10000;
-    left: -5000;
-    top: -5000;
+    width: 10000px;
+    height: 10000px;
+    left: -5000px;
+    top: -5000px;
     position: absolute;
     background: transparent;
     pointer-events: all;
@@ -24,8 +24,8 @@
 .collectionfreeformview-grid {
     transform-origin: top left;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     pointer-events: none;
 }
 
@@ -219,8 +219,8 @@
     border-radius: inherit;
     box-sizing: border-box;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     width: 100%;
     height: 100%;
     align-items: center;
@@ -264,7 +264,7 @@
 .collectionFreeform-infoUI {
     position: absolute;
     display: block;
-    top: 0;
+    top: 0px;
 
     color: white;
     background-color: #5075ef;
@@ -275,19 +275,19 @@
     padding: 10px;
     .collectionFreeform-infoUI-close {
         position: absolute;
-        top: -10;
-        left: -10;
+        top: -10px;
+        left: -10px;
     }
 
     .collectionFreeform-infoUI-msg {
         position: relative;
-        max-width: 500;
-        margin: 10;
+        max-width: 500px;
+        margin: 10px;
     }
     .collectionFreeform-infoUI-button {
         border-radius: 50px;
         font-size: 12px;
-        padding: 6;
+        padding: 6px;
         position: relative;
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
index 0a001d84c..d0685e419 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
@@ -2,7 +2,7 @@
     display: flex;
     height: max-content;
     flex-direction: column;
-    top: 0;
+    top: 0px;
     position: absolute;
     width: 100%;
     height: 100%;
@@ -31,9 +31,9 @@
     }
     .face-document-top {
         position: relative;
-        top: 0;
+        top: 0px;
         width: 100%;
-        left: 0;
+        left: 0px;
     }
 
     .face-document-image-container {
@@ -69,8 +69,8 @@
 
             .remove-item {
                 position: absolute;
-                bottom: -5;
-                right: -5;
+                bottom: -5px;
+                right: -5px;
                 background-color: rgba(0, 0, 0, 0.5); // Optional: to add a background behind the icon for better visibility
                 border-radius: 30%;
                 width: 10px; // Adjust size as needed
@@ -98,7 +98,7 @@
 .faceCollectionBox {
     width: 100%;
     height: 100%;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     position: absolute;
 }
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 7c9d0f6e1..af787ed00 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -1,7 +1,7 @@
 .marqueeView {
     position: inherit;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     width: 100%;
     height: 100%;
     overflow: hidden;
@@ -20,7 +20,7 @@
     pointer-events: none;
     .marquee-legend {
         bottom: -18px;
-        left: 0;
+        left: 0px;
         position: absolute;
         font-size: 9;
         white-space: nowrap;
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss
index 4edaf9745..b95d3ea44 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.scss
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss
@@ -39,8 +39,8 @@
             background: #d3d3d3;
 
             position: absolute;
-            height: 3;
-            left: 5;
+            height: 3px;
+            left: 5px;
             transform-origin: left;
             transform: rotate(90deg);
             outline: none;
@@ -133,7 +133,7 @@
 input::-webkit-outer-spin-button,
 input::-webkit-inner-spin-button {
     -webkit-appearance: none;
-    margin: 0;
+    margin: 0px;
 }
 
 /* Firefox */
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
index 0dfaed38a..0053d3e60 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
@@ -47,7 +47,7 @@
             background: global.$medium-blue;
             display: flex;
             border-radius: 10px;
-            height: 35;
+            height: 35px;
             transform: translate3d(6px, 0px, 0px);
             align-content: center;
             justify-content: center;
@@ -95,11 +95,11 @@
             pointer-events: all;
             cursor: pointer;
             background-color: global.$medium-blue;
-            padding: 5;
+            padding: 5px;
             border-radius: 2px;
             height: 100%;
-            min-width: 25;
-            margin: 0;
+            min-width: 25px;
+            margin: 0px;
             color: global.$white;
             display: flex;
             font-weight: 100;
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
index 9ed247d50..1dc46102f 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
@@ -1,8 +1,8 @@
 .collectionMulticolumnView_drop {
     height: 100%;
     width: 100%;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     position: absolute;
 
     .collectionMulticolumnView_contents {
@@ -18,8 +18,8 @@
             align-items: center;
             position: relative;
             > .iconButton-container {
-                top: 0;
-                left: 0;
+                top: 0px;
+                left: 0px;
                 position: absolute;
             }
 
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
index 91779065d..eb157d155 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
@@ -1,8 +1,8 @@
 .collectionMultirowView_drop {
     height: 100%;
     width: 100%;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     position: absolute;
 
     .collectionMultirowView_contents {
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 10a6fa2e9..2ff99f134 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
 import { action } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx
index 66215f109..4f57e1656 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
 import { computed } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
index 918365700..1954b4743 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
 import { action } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -66,7 +65,7 @@ export default class ResizeBar extends React.Component<ResizerProps> {
                 style={{
                     pointerEvents: this.props.isContentActive?.() ? 'all' : 'none',
                     height: this.props.height,
-                    backgroundColor: !this.props.isContentActive?.() ? '' : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor) as string,
+                    backgroundColor: !this.props.isContentActive?.() ? '' : (this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor) as string),
                 }}>
                 <div className="multiRowResizer-hdl" onPointerDown={e => this.registerResizing(e)} />
             </div>
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 53c0823ea..e975ae6f6 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -18,7 +18,7 @@
 
         .schema-add {
             position: relative;
-            height: 35;
+            height: 35px;
             display: flex;
             align-items: center;
             top: -10px;
@@ -147,7 +147,7 @@
     flex-direction: row;
     justify-content: space-between;
     align-items: center;
-    padding: 0;
+    padding: 0px;
     z-index: 1;
     border: 1px solid global.$medium-gray;
 
@@ -231,7 +231,7 @@
     overflow-x: hidden;
     overflow-y: auto;
     display: inline-flex;
-    padding: 0;
+    padding: 0px;
     align-items: center;
     input[type='text'] {
         border: unset;
@@ -272,8 +272,8 @@
 
     .row-menu-infos {
         position: absolute;
-        top: 3;
-        left: 3;
+        top: 3px;
+        left: 3px;
         z-index: 1;
         display: flex;
         justify-content: flex-end;
diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss
index 3cd60c87f..cc8c168cf 100644
--- a/src/client/views/linking/LinkMenuItem.scss
+++ b/src/client/views/linking/LinkMenuItem.scss
@@ -114,7 +114,7 @@
     .linkMenu-deleteButton {
         width: 20px;
         height: 20px;
-        margin: 0;
+        margin: 0px;
         margin-right: 4px;
         padding-right: 6px;
         border-radius: 50%;
@@ -134,7 +134,7 @@
         }
 
         &:last-child {
-            margin-right: 0;
+            margin-right: 0px;
         }
 
         &:hover {
diff --git a/src/client/views/linking/LinkPopup.scss b/src/client/views/linking/LinkPopup.scss
index 4bfb4b0b9..f8d724767 100644
--- a/src/client/views/linking/LinkPopup.scss
+++ b/src/client/views/linking/LinkPopup.scss
@@ -1,7 +1,9 @@
 .linkPopup-container {
     background: white;
-    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
-    top: 0;
+    box-shadow:
+        0 10px 20px rgba(0, 0, 0, 0.19),
+        0 6px 6px rgba(0, 0, 0, 0.23);
+    top: 0px;
     height: 200px;
     width: 200px;
     // padding: 15px;
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index b654f9bd0..760850241 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
diff --git a/src/client/views/newlightbox/NewLightboxView.scss b/src/client/views/newlightbox/NewLightboxView.scss
index 76c34bcf9..c76a7d60d 100644
--- a/src/client/views/newlightbox/NewLightboxView.scss
+++ b/src/client/views/newlightbox/NewLightboxView.scss
@@ -2,8 +2,8 @@
 
 .newLightboxView-frame {
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     width: 100%;
     height: 100%;
     background: #474545bb;
diff --git a/src/client/views/newlightbox/components/Recommendation/Recommendation.scss b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss
index cf6b5ccb1..09d3ccc62 100644
--- a/src/client/views/newlightbox/components/Recommendation/Recommendation.scss
+++ b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss
@@ -135,7 +135,7 @@
         font-size: 10px;
         width: 100%;
         background: newstyles.$blue-l1;
-        border-radius: 0;
+        border-radius: 0px;
         padding: 10px;
 
         .concepts-container {
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 933a383ea..c25c09af9 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -138,7 +138,7 @@
 
     input[type='range']::-webkit-slider-thumb {
         box-shadow: 0;
-        border: 0;
+        border: 0px;
         height: 10px;
         width: 10px;
         border-radius: 10px;
@@ -168,7 +168,7 @@
             .audiobox-button {
                 width: 15px;
                 height: 15px;
-                margin: 0;
+                margin: 0px;
 
                 svg {
                     width: 10px;
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
index 7f0a39550..300533df8 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -3,7 +3,7 @@
     position: absolute;
     background-color: transparent;
     touch-action: manipulation;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     pointer-events: none;
 }
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index d2ba9796b..cbbd6bde3 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -18,7 +18,7 @@
     }
     .input-box {
         position: absolute;
-        top: 50;
+        top: 50px;
         padding: 10px;
         width: 100%;
         height: 70%;
@@ -33,7 +33,7 @@
         padding-right: 5px;
         border-radius: 2px;
         height: 17%;
-        bottom: 0;
+        bottom: 0px;
         overflow: hidden;
         display: flex;
         width: 100%;
@@ -101,7 +101,7 @@
         position: absolute;
         display: inline-block;
         margin-top: 150px;
-        bottom: 0;
+        bottom: 0px;
     }
 
     .dropup-content {
@@ -145,8 +145,8 @@
 
     .clip-div {
         position: absolute;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
         height: 100%;
         overflow: hidden;
 
@@ -180,8 +180,8 @@
 
     .afterBox-cont {
         position: absolute;
-        top: 0;
-        right: 0;
+        top: 0px;
+        right: 0px;
         height: 100%;
         width: 100%;
         overflow: hidden;
@@ -331,8 +331,8 @@
     justify-content: space-between;
     height: max-content;
     position: absolute;
-    bottom: 0;
-    right: 2;
+    bottom: 0px;
+    right: 2px;
     flex-direction: row-reverse;
     display: flex;
     cursor: pointer;
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss
index 9825d926f..32a01355e 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.scss
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
@@ -37,25 +37,25 @@
         margin-left: 10px;
         margin-bottom: -10px;
     }
-    
+
     .displaySchemaLive {
         margin-bottom: 20px;
     }
 
     .dataviz-sidebar {
         position: absolute;
-        right: 0;
-        top: 0;
+        right: 0px;
+        top: 0px;
         height: 100%;
     }
     .button-container {
         pointer-events: unset;
     }
 
-    .dataVizBox-annotationLayer{
+    .dataVizBox-annotationLayer {
         position: absolute;
         transform-origin: left top;
-        top: 0;
+        top: 0px;
         width: 100%;
         pointer-events: none;
         mix-blend-mode: multiply;
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
index 6eb7fa96a..e2261b9e2 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
@@ -110,12 +110,12 @@
     &::before {
         content: '';
         position: absolute;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
         border-bottom: 20px solid rgb(50, 50, 50);
         border-left: 12px solid transparent;
         border-right: 12px solid transparent;
-        height: 0;
+        height: 0px;
         width: 50px;
     }
 
@@ -127,7 +127,7 @@
         border-bottom: 22px solid rgb(180, 180, 180);
         border-left: 12px solid transparent;
         border-right: 12px solid transparent;
-        height: 0;
+        height: 0px;
         width: 52px;
         z-index: -1;
     }
@@ -418,8 +418,8 @@
 }
 
 .div {
-    width: 200;
-    height: 200;
+    width: 200px;
+    height: 200px;
     border: solid 1px white;
 }
 
@@ -588,7 +588,7 @@
 }
 
 .docCreatorMenu-configuration-bar {
-    width: 200;
+    width: 200px;
     gap: 5px;
     display: flex;
     flex-direction: row;
@@ -709,8 +709,8 @@
         width: 100%;
         aspect-ratio: 1;
         //height: auto;
-        // max-width: 240;
-        // max-height: 240;
+        // max-width: 240px;
+        // max-height: 240px;
         border: 1px solid rgb(180, 180, 180);
         border-radius: 5px;
         background-color: rgb(34, 34, 37);
@@ -718,8 +718,8 @@
         scrollbar-width: none;
 
         &.small {
-            max-width: 100;
-            max-height: 100;
+            max-width: 100px;
+            max-height: 100px;
         }
 
         .docCreatorMenu-layout-preview-item {
@@ -1009,8 +1009,8 @@
             }
 
             &:hover .operator-dropdown-current {
-                border-bottom-right-radius: 0;
-                border-bottom-left-radius: 0;
+                border-bottom-right-radius: 0px;
+                border-bottom-left-radius: 0px;
             }
 
             &:hover .operator-dropdown-option {
diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss
index 63a693918..0acc2c847 100644
--- a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss
+++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss
@@ -13,7 +13,7 @@ $highlightedText: #82e0ff;
     min-height: 200px;
     border-radius: 15px;
     padding: 15px;
-    padding-bottom: 0;
+    padding-bottom: 0px;
     z-index: 999;
     display: flex;
     flex-direction: column;
@@ -40,7 +40,7 @@ $highlightedText: #82e0ff;
         font-size: 12px;
         font-weight: 400;
         letter-spacing: 1px;
-        margin: 0;
+        margin: 0px;
         padding-right: 5px;
     }
 
@@ -124,8 +124,8 @@ $highlightedText: #82e0ff;
         .img-container::after {
             content: '';
             position: absolute;
-            top: 0;
-            left: 0;
+            top: 0px;
+            left: 0px;
             width: 100%;
             height: 100%;
             background-color: rgba(0, 0, 0, 0.5);
diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss
index ff1fa343d..a22e1153c 100644
--- a/src/client/views/nodes/DataVizBox/components/Chart.scss
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -91,7 +91,7 @@
         margin: 5px;
         margin-left: 25px;
         margin-right: 10px;
-        margin-bottom: 0;
+        margin-bottom: 0px;
         .tableBox-table {
             height: 100%;
             width: 100%;
@@ -101,7 +101,7 @@
                     text-overflow: ellipsis;
                     width: 100%;
                     white-space: pre;
-                    max-width: 150;
+                    max-width: 150px;
                     overflow: hidden;
                     margin-left: 2px;
                 }
diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss
index e1b83dc59..43b1e083f 100644
--- a/src/client/views/nodes/DocumentLinksButton.scss
+++ b/src/client/views/nodes/DocumentLinksButton.scss
@@ -17,8 +17,8 @@
 }
 
 .documentLinksButton-cont {
-    min-width: 20;
-    min-height: 20;
+    min-width: 20px;
+    min-height: 20px;
     position: absolute;
 }
 
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index c4351a200..98ca76339 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -1,6 +1,7 @@
 @use '../global/globalCssVariables.module.scss' as global;
 
 .documentView-effectsWrapper {
+    height: 100%;
     border-radius: inherit;
     transition: inherit;
 }
@@ -14,13 +15,13 @@
     width: 100%;
     height: 100%;
     position: absolute;
-    top: 0;
+    top: 0px;
 }
 
 .documentView-node {
     position: inherit;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     width: 100%;
     height: 100%;
     border-radius: inherit;
@@ -55,7 +56,7 @@
     .documentView-htmlOverlay {
         position: absolute;
         display: flex;
-        top: 0;
+        top: 0px;
         height: 100%;
         width: 100%;
         .documentView-htmlOverlayInner {
@@ -79,9 +80,9 @@
     .documentView-audioBackground {
         display: inline-block;
         width: 25px;
-        height: 25;
+        height: 25px;
         position: absolute;
-        top: 0;
+        top: 0px;
         left: 50%;
         border-radius: 25px;
         background: white;
@@ -130,7 +131,7 @@
             width: 30px;
             border-radius: 50%;
             position: absolute;
-            right: -15;
+            right: -15px;
             opacity: 0.9;
             pointer-events: auto;
             background-color: #9dca96;
@@ -147,8 +148,8 @@
 
     .documentView-anchorCont {
         position: absolute;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
         width: 100%;
         height: 100%;
         display: inline-block;
@@ -160,8 +161,8 @@
         position: absolute;
         width: 100%;
         height: 100%;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
     }
 
     .documentView-styleWrapper {
@@ -183,9 +184,9 @@
         .documentView-titleWrapper-hover {
             color: global.$black;
             transform-origin: top left;
-            top: 0;
+            top: 0px;
             width: 100%;
-            height: 14;
+            height: 14px;
             opacity: 0.5;
             text-align: center;
             text-overflow: ellipsis;
@@ -211,7 +212,7 @@
 
         .documentView-captionWrapper {
             position: absolute;
-            bottom: 0;
+            bottom: 0px;
             width: 100%;
             overflow-y: auto;
             transform-origin: bottom left;
@@ -275,20 +276,20 @@
 .documentView-noAIWidgets {
     transform-origin: top left;
     position: absolute;
-    bottom: 0;
+    bottom: 0px;
     pointer-events: none;
 }
 .documentView-widgetDecorations {
     transform-origin: top right;
     position: absolute;
-    top: 0;
-    right: 0;
+    top: 0px;
+    right: 0px;
 }
 
 .documentView-editorView-history {
     position: absolute;
     transform-origin: top right;
-    right: 0;
+    right: 0px;
     top: 0;
     overflow-y: scroll;
     scrollbar-width: thin;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBadge.scss b/src/client/views/nodes/FontIconBox/FontIconBadge.scss
index 2ff5c651f..e741936db 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBadge.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBadge.scss
@@ -6,7 +6,7 @@
     color: black;
     display: block;
     position: absolute;
-    right: 5;
+    right: 5px;
     border-radius: 50%;
     text-align: center;
 }
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index 8bc68c131..52eebba54 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -42,7 +42,7 @@
         letter-spacing: normal;
         background-color: inherit;
         border-radius: 8px;
-        padding: 0;
+        padding: 0px;
         width: 100%;
         font-family: 'system-ui';
         text-transform: uppercase;
@@ -96,22 +96,22 @@
                 display: inline-block;
                 width: 100%;
                 height: 25px;
-                margin: 0;
+                margin: 0px;
             }
 
             .switch input {
                 opacity: 0;
-                width: 0;
-                height: 0;
+                width: 0px;
+                height: 0px;
             }
 
             .slider {
                 position: absolute;
                 cursor: pointer;
-                top: 0;
-                left: 0;
-                right: 0;
-                bottom: 0;
+                top: 0px;
+                left: 0px;
+                right: 0px;
+                bottom: 0px;
                 background-color: lightgrey;
                 -webkit-transition: 0.4s;
                 transition: 0.4s;
@@ -223,7 +223,7 @@
             height: fit-content;
             color: black;
             top: 100%;
-            left: 0;
+            left: 0px;
             z-index: 21;
             background-color: #e3e3e3;
             box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
@@ -430,7 +430,7 @@
         border-radius: 0px 7px 7px 0px;
         width: 13px;
         height: 100%;
-        right: 0;
+        right: 0px;
     }
 
     .menuButton-dropdown-header {
diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss
index d6cf95958..c0977dfc5 100644
--- a/src/client/views/nodes/IconTagBox.scss
+++ b/src/client/views/nodes/IconTagBox.scss
@@ -15,7 +15,7 @@
         width: 20px;
         height: 20px;
         margin: auto;
-        padding: 0;
+        padding: 0px;
         border-radius: 50%;
         background-color: global.$dark-gray;
         background-color: transparent;
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 5a6292fab..90ede69dc 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -3,14 +3,14 @@
     width: 100%;
     height: 100%;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     transform-origin: top left;
 
     .imageBox-annotationLayer {
         position: absolute;
         transform-origin: left top;
-        top: 0;
+        top: 0px;
         width: 100%;
         pointer-events: none;
         mix-blend-mode: multiply; // bcz: makes text fuzzy!
@@ -24,8 +24,8 @@
 
 #upload-icon {
     position: absolute;
-    bottom: 0;
-    right: 0;
+    bottom: 0px;
+    right: 0px;
     width: 20px;
     height: 20px;
 }
@@ -51,8 +51,8 @@
 
 .imageBox-dot {
     position: absolute;
-    bottom: 10;
-    left: 0;
+    bottom: 10px;
+    left: 0px;
     border-radius: 10px;
     width: 20px;
     height: 20px;
@@ -131,8 +131,8 @@
     position: absolute;
     color: white;
     background: black;
-    right: 0;
-    bottom: 0;
+    right: 0px;
+    bottom: 0px;
     z-index: 2;
     transform-origin: bottom right;
     cursor: default;
@@ -142,13 +142,13 @@
     }
 }
 .imageBox-regenerateDropTarget {
-    right: 35;
+    right: 35px;
     transform-origin: 70px 35px;
 }
 
 .imageBox-fader img {
     position: absolute;
-    left: 0;
+    left: 0px;
 }
 
 .imageBox-fadeBlocker-hover {
@@ -223,7 +223,7 @@
         max-width: 90%;
         width: 100%;
         .imageBox-aiView-similarity {
-            max-width: 65;
+            max-width: 65px;
             overflow: hidden;
             text-overflow: ellipsis;
             width: 100%;
@@ -250,7 +250,7 @@
     z-index: 10000;
 
     h3 {
-        margin-top: 0;
+        margin-top: 0px;
     }
 
     input {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 8ed59c6e1..616046d5d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -962,8 +962,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView | undefined) => (this.dataDoc[this.fieldKey] === undefined ? true : (this._props.rejectDrop?.(de, subView) ?? false));
     render() {
         TraceMobx();
-        const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
-        const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad;
+        const borderRadius = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
         return (
             <>
                 <div
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 441fceba4..80ace6ae0 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -93,9 +93,9 @@ $header-height: 30px;
     height: 30px;
     width: 5px;
     z-index: 20;
-    right: 0;
-    top: 0;
-    border-radius: 0;
+    right: 0px;
+    top: 0px;
+    border-radius: 0px;
     background: black;
     pointer-events: all;
 }
@@ -105,8 +105,8 @@ $header-height: 30px;
     float: left;
     height: 37px;
     z-index: 20;
-    right: 0;
-    top: 0;
+    right: 0px;
+    top: 0px;
     background: transparent;
     pointer-events: none;
 }
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index 913ab641c..154fbdcfa 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -17,7 +17,7 @@
         }
         .keyValuePair-td-key-check {
             position: relative;
-            margin: 0;
+            margin: 0px;
         }
         .keyValuePair-keyField {
             width: 100%;
diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss
index 889cdc0ca..e1974d6a0 100644
--- a/src/client/views/nodes/LabelBox.scss
+++ b/src/client/views/nodes/LabelBox.scss
@@ -24,8 +24,8 @@
 
 .answer-icon {
     position: absolute;
-    right: 8;
-    bottom: 5;
+    right: 8px;
+    bottom: 5px;
     color: black;
     display: inline-block;
     font-size: 10px;
@@ -36,8 +36,8 @@
 
 .q-icon {
     position: absolute;
-    right: 6;
-    bottom: 5;
+    right: 6px;
+    bottom: 5px;
     color: white;
     display: inline-block;
     font-size: 10px;
@@ -48,8 +48,8 @@
 
 .edit-icon {
     position: absolute;
-    right: 20;
-    bottom: 5;
+    right: 20px;
+    bottom: 5px;
     display: inline-block;
     font-size: 10px;
     cursor: pointer;
diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss
index 28216394d..7d99247e7 100644
--- a/src/client/views/nodes/LinkDocPreview.scss
+++ b/src/client/views/nodes/LinkDocPreview.scss
@@ -42,7 +42,7 @@
 
             .linkDocPreview-button {
                 display: inline-flex;
-                margin: 0;
+                margin: 0px;
                 margin-right: 3px;
                 border-radius: 50%;
                 pointer-events: auto;
diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss
index cabd4de05..5a8f49000 100644
--- a/src/client/views/nodes/LoadingBox.scss
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -26,7 +26,7 @@
     }
     .loadingBox-spinner {
         position: absolute;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
     }
 }
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.scss b/src/client/views/nodes/MapBox/MapAnchorMenu.scss
index c36d98afe..217576203 100644
--- a/src/client/views/nodes/MapBox/MapAnchorMenu.scss
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss
@@ -6,19 +6,19 @@
 }
 .anchorMenu-highlighter {
     padding-right: 5px;
-    .antimodeMenu-button  {
-        padding: 0;
-        padding: 0;
+    .antimodeMenu-button {
+        padding: 0px;
+        padding: 0px;
         padding-right: 0px;
         padding-left: 0px;
         width: 5px;
     }
 }
-.anchor-color-preview-button {  
-    width: 25px !important;  
+.anchor-color-preview-button {
+    width: 25px !important;
     .anchor-color-preview {
         display: flex;
-        flex-direction:  column;
+        flex-direction: column;
         padding-right: 3px;
         width: unset !important;
         .color-preview {
@@ -72,12 +72,11 @@
         }
     }
 
-    .MuiInputBase-input{
+    .MuiInputBase-input {
         color: white !important;
     }
-    
-    
-    .css-1t8l2tu-MuiInputBase-input-MuiOutlinedInput-input.Mui-disabled{
+
+    .css-1t8l2tu-MuiInputBase-input-MuiOutlinedInput-input.Mui-disabled {
         -webkit-text-fill-color: #b3b2b2 !important;
     }
 
@@ -91,7 +90,7 @@
             gap: 5px;
         }
 
-        .selected-route-details-container{
+        .selected-route-details-container {
             display: flex;
             flex-direction: column;
             gap: 3px;
@@ -99,33 +98,25 @@
             align-items: flex-start;
             padding: 5px;
         }
-
-
     }
 
-    .customized-marker-container{
+    .customized-marker-container {
         display: flex;
         flex-direction: column;
         gap: 10px;
 
-        .current-marker-container{
+        .current-marker-container {
             display: flex;
             align-items: center;
             gap: 5px;
         }
 
-        .all-markers-container{
+        .all-markers-container {
             display: flex;
-            align-items:  center;
+            align-items: center;
             gap: 10px;
             flex-wrap: wrap;
             max-width: 400px;
         }
     }
-
-
-
-
 }
-
-
diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss
index bd4b51038..89d381070 100644
--- a/src/client/views/nodes/MapBox/MapBox.scss
+++ b/src/client/views/nodes/MapBox/MapBox.scss
@@ -21,7 +21,7 @@
     .mapBox-infoWindow {
         background-color: white;
         opacity: 0.75;
-        padding: 12;
+        padding: 12px;
         font-size: 17;
     }
     .mapBox-searchbar {
@@ -119,7 +119,7 @@
                 width: 100%;
 
                 label {
-                    margin-bottom: 0;
+                    margin-bottom: 0px;
                 }
 
                 .speed-label {
@@ -197,12 +197,12 @@
     }
     .mapBox-sidebar {
         position: absolute;
-        right: 0;
+        right: 0px;
         height: 100%;
     }
 
     .mapBox-sidebar-handle {
-        top: 0;
+        top: 0px;
         //top: calc(50% - 17.5px);  // use this to center vertically -- make sure it looks okay for slide views
         width: 10px;
         height: 100%;
@@ -215,7 +215,7 @@
         left: 50%;
         margin-left: 120px;
         right: unset !important;
-        margin-top: -10;
+        margin-top: -10px;
         height: max-content;
     }
     .searchbox {
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index f09a2630a..e34ca61d4 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -17,8 +17,8 @@
         height: 100%;
         z-index: 1;
         pointer-events: none;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
 
         // glr: This should really be the same component as text and PDFs
         .pdfBox-sidebarBtn {
@@ -72,16 +72,16 @@
             align-items: center;
             height: 20px;
             background: none;
-            padding: 0;
+            padding: 0px;
             position: absolute;
             pointer-events: all;
             color: white;
-            bottom: 0;
-            right: 0;
+            bottom: 0px;
+            right: 0px;
 
             .pdfBox-overlayButton-arrow {
-                width: 0;
-                height: 0;
+                width: 0px;
+                height: 0px;
                 border-top: 10px solid transparent;
                 border-bottom: 10px solid transparent;
                 border-right: 15px solid #121721;
@@ -122,8 +122,8 @@
 
         .pdfBox-settingsCont {
             position: absolute;
-            right: 0;
-            top: 3;
+            right: 0px;
+            top: 3px;
             pointer-events: all;
 
             .pdfBox-settingsButton {
@@ -133,11 +133,11 @@
                 align-items: center;
                 height: 20px;
                 background: none;
-                padding: 0;
+                padding: 0px;
 
                 .pdfBox-settingsButton-arrow {
-                    width: 0;
-                    height: 0;
+                    width: 0px;
+                    height: 0px;
                     border-top: 10px solid transparent;
                     border-bottom: 10px solid transparent;
                     border-right: 15px solid #121721;
@@ -189,7 +189,7 @@
             width: calc(100% - 40px);
             height: 20px;
             background: #121721;
-            bottom: 0;
+            bottom: 0px;
             display: flex;
             justify-content: center;
             align-items: center;
@@ -253,13 +253,13 @@
 .pdfBox-container {
     position: absolute;
     transform-origin: top left;
-    top: 0;
+    top: 0px;
 }
 .pdfBox-sidebarContainer {
     position: absolute;
     height: 100%;
-    right: 0;
-    top: 0;
+    right: 0px;
+    top: 0px;
 }
 
 .pdfBox-interactive {
@@ -290,7 +290,7 @@
         }
 
         .pdfBox-settingsButton-arrow {
-            height: 60;
+            height: 60px;
             border-top: 30px solid transparent;
             border-bottom: 30px solid transparent;
             border-right: 30px solid #121721;
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss
index ac2c611c7..78aa526bf 100644
--- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss
@@ -29,8 +29,8 @@
         .wedge {
             pointer-events: none;
             position: absolute;
-            left: 0;
-            top: 0;
+            left: 0px;
+            top: 0px;
         }
     }
 
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
index 28ad25ffa..ec01f0241 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.scss
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -1,36 +1,34 @@
-
 .progressbar {
-  touch-action: none;
-  vertical-align: middle;
-  text-align: center;
-
-  align-items: center;
-  cursor: default;
-
-
-      position: absolute;
-      display: flex;
-      justify-content: flex-start;
-      bottom: 2px;
-      width: 99%;
-      height: 30px;
-      background-color: gray;
-
-      &.done {
-            top: 0;
-            width: 0px;
-            height: 5px;
-            background-color: red;
-            z-index: 2;
-      }
-
-      &.mark {
-            top: 0;
-            background-color: transparent;
-            border-right: 2px solid white;
-            z-index: 3;
-            pointer-events: none; 
-      }
+    touch-action: none;
+    vertical-align: middle;
+    text-align: center;
+
+    align-items: center;
+    cursor: default;
+
+    position: absolute;
+    display: flex;
+    justify-content: flex-start;
+    bottom: 2px;
+    width: 99%;
+    height: 30px;
+    background-color: gray;
+
+    &.done {
+        top: 0px;
+        width: 0px;
+        height: 5px;
+        background-color: red;
+        z-index: 2;
+    }
+
+    &.mark {
+        top: 0px;
+        background-color: transparent;
+        border-right: 2px solid white;
+        z-index: 3;
+        pointer-events: none;
+    }
 }
 
 .progressbar-disabled {
@@ -43,37 +41,41 @@
 
 // citation: https://codepen.io/_Master_/pen/PRdjmQ
 @keyframes blinker {
-  from {opacity: 1.0;}
-  to {opacity: 0.0;}
+    from {
+        opacity: 1;
+    }
+    to {
+        opacity: 0;
+    }
 }
 .blink {
-	text-decoration: blink;
-	animation-name: blinker;
-	animation-duration: 0.6s;
-	animation-iteration-count:infinite;
-	animation-timing-function:ease-in-out;
-	animation-direction: alternate;
+    text-decoration: blink;
+    animation-name: blinker;
+    animation-duration: 0.6s;
+    animation-iteration-count: infinite;
+    animation-timing-function: ease-in-out;
+    animation-direction: alternate;
 }
 
 .segment {
-  border: 3px solid black;
-  background-color: red;
-  margin: 1px;
-  padding: 0;
-  cursor: pointer;
-  transition-duration: .5s;
-  user-select: none;
-
-  vertical-align: middle;
-  text-align: center;
+    border: 3px solid black;
+    background-color: red;
+    margin: 1px;
+    padding: 0px;
+    cursor: pointer;
+    transition-duration: 0.5s;
+    user-select: none;
+
+    vertical-align: middle;
+    text-align: center;
 }
 
 .segment-expanding {
-border-color: red;
-  background-color: white;
-  transition-duration: 0s;
-  opacity: .75;
-  pointer-events: none;
+    border-color: red;
+    background-color: white;
+    transition-duration: 0s;
+    opacity: 0.75;
+    pointer-events: none;
 }
 
 .segment-expanding:hover {
@@ -82,10 +84,10 @@ border-color: red;
 }
 
 .segment-disabled {
-  pointer-events: none;
-  opacity: 0.5;
-  transition-duration: 0s;
-  /* Hide the text. */
+    pointer-events: none;
+    opacity: 0.5;
+    transition-duration: 0s;
+    /* Hide the text. */
     text-indent: 100%;
     white-space: nowrap;
     overflow: hidden;
@@ -99,25 +101,26 @@ border-color: red;
 }
 
 .segment:first-child {
-  margin-left: 2px;
+    margin-left: 2px;
 }
 .segment:last-child {
-  margin-right: 2px;
+    margin-right: 2px;
 }
 
 .segment:hover {
     background-color: white;
 }
 
-.segment:hover, .segment-selected {
-  margin: 0px;
-  border: 4px solid red;
-  border-radius: 2px;
+.segment:hover,
+.segment-selected {
+    margin: 0px;
+    border: 4px solid red;
+    border-radius: 2px;
 }
 
 .segment-selected {
-  border: 4px solid #202020;
-  background-color: red;
-  opacity: .75;
-  cursor: grabbing;
+    border: 4px solid #202020;
+    background-color: red;
+    opacity: 0.75;
+    cursor: grabbing;
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index f2d5a980d..15b48c111 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -28,7 +28,7 @@ video {
     justify-content: center;
     //   overflow: hidden;
     border-radius: 10px;
-    margin: 0;
+    margin: 0px;
 }
 
 .video-wrapper:hover .controls {
@@ -108,7 +108,7 @@ video {
     .timer {
         font-size: 15px;
         color: white;
-        margin: 0;
+        margin: 0px;
     }
 
     .dot {
@@ -148,7 +148,7 @@ video {
         height: 80%;
         width: 80%;
         align-self: center;
-        margin: 0;
+        margin: 0px;
 
         &:hover {
             height: 85%;
@@ -163,7 +163,7 @@ video {
         height: 70%;
         width: 70%;
         align-self: center;
-        margin: 0;
+        margin: 0px;
 
         // &:hover {
         //   width: 40px;
@@ -178,8 +178,8 @@ video {
     flex-direction: row;
     align-content: center;
     position: relative;
-    top: 0;
-    bottom: 0;
+    top: 0px;
+    bottom: 0px;
 
     &.video-edit-wrapper {
         // right: 50% - 15;
diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss
index 1e9b64a0b..1714d87c2 100644
--- a/src/client/views/nodes/ScreenshotBox.scss
+++ b/src/client/views/nodes/ScreenshotBox.scss
@@ -32,10 +32,10 @@
 
 .screenshotBox-uiButtons {
     position: absolute;
-    right: 25;
-    top: 0;
-    width: 22;
-    height: 25;
+    right: 25px;
+    top: 0px;
+    width: 22px;
+    height: 25px;
 
     .screenshotBox-recorder {
         color: white;
diff --git a/src/client/views/nodes/ScriptingBox.scss b/src/client/views/nodes/ScriptingBox.scss
index 9789da55a..de70dbe74 100644
--- a/src/client/views/nodes/ScriptingBox.scss
+++ b/src/client/views/nodes/ScriptingBox.scss
@@ -82,14 +82,14 @@
                 }
 
                 .rta__autocomplete--top {
-                    margin-top: 0;
+                    margin-top: 0px;
                     margin-bottom: 1em;
                     max-height: 100px;
                 }
 
                 .rta__list {
-                    margin: 0;
-                    padding: 0;
+                    margin: 0px;
+                    padding: 0px;
                     background: #fff;
                     border: 1px solid #dfe2e5;
                     border-radius: 3px;
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index b5405f0fb..27f419198 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -3,8 +3,8 @@
 .mini-viewer {
     cursor: grab;
     position: absolute;
-    right: 10;
-    top: 10;
+    right: 10px;
+    top: 10px;
     opacity: 0.1;
     transition: all 0.4s;
     color: white;
@@ -38,7 +38,7 @@
     .videoBox-annotationLayer {
         position: relative;
         transform-origin: left top;
-        top: 0;
+        top: 0px;
         width: 100%;
         pointer-events: none;
         mix-blend-mode: multiply; // bcz: makes text fuzzy!
@@ -81,8 +81,8 @@
 // }
 
 .videoBox-ui-wrapper {
-    width: 0;
-    height: 0;
+    width: 0px;
+    height: 0px;
     position: relative;
     z-index: 2000;
 }
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 05d5babf9..e7c9cf095 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -3,8 +3,8 @@
 .webBox {
     height: 100%;
     width: 100%;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     position: relative;
     display: flex;
     overflow: hidden;
@@ -28,8 +28,8 @@
         height: 100%;
         z-index: 1;
         pointer-events: none;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
         overflow: hidden;
 
         .webBox-overlayButton {
@@ -39,16 +39,16 @@
             align-items: center;
             height: 20px;
             background: none;
-            padding: 0;
+            padding: 0px;
             position: absolute;
             pointer-events: all;
             color: white;
-            bottom: 0;
-            right: 0;
+            bottom: 0px;
+            right: 0px;
 
             .webBox-overlayButton-arrow {
-                width: 0;
-                height: 0;
+                width: 0px;
+                height: 0px;
                 border-top: 10px solid transparent;
                 border-bottom: 10px solid transparent;
                 border-right: 15px solid #121721;
@@ -92,7 +92,7 @@
             width: calc(100% - 40px);
             height: 20px;
             background: #121721;
-            bottom: 0;
+            bottom: 0px;
             display: flex;
             justify-content: center;
             align-items: center;
@@ -137,7 +137,7 @@
     .webBox-annotationLayer {
         position: absolute;
         transform-origin: left top;
-        top: 0;
+        top: 0px;
         width: 100%;
         pointer-events: none;
         mix-blend-mode: multiply; // bcz: makes text fuzzy!
@@ -156,8 +156,8 @@
 
         .webBox-htmlSpan {
             position: absolute;
-            top: 0;
-            left: 0;
+            top: 0px;
+            left: 0px;
             cursor: text;
             padding: 15px;
             height: 100%;
@@ -171,8 +171,8 @@
         .webBox-cont-interactive {
             padding: 0vw;
             position: absolute;
-            top: 0;
-            left: 0;
+            top: 0px;
+            left: 0px;
             width: 100%;
             height: 100%;
             transform-origin: top left;
@@ -181,8 +181,8 @@
                 width: 100%;
                 height: 100%;
                 position: absolute;
-                top: 0;
-                left: 0;
+                top: 0px;
+                left: 0px;
                 body {
                     ::selection {
                         color: white;
@@ -203,8 +203,8 @@
             height: 100%;
             position: absolute;
             transform-origin: top left;
-            top: 0;
-            left: 0;
+            top: 0px;
+            left: 0px;
             overflow: auto;
 
             .webBox-innerContent {
@@ -224,7 +224,7 @@
     }
 
     .webBox-buttons {
-        margin-left: 44;
+        margin-left: 44px;
         background: lightGray;
         width: 100%;
     }
@@ -232,8 +232,8 @@
     .webBox-annotationToggle {
         z-index: 901;
         position: absolute;
-        top: 2;
-        left: 2;
+        top: 2px;
+        left: 2px;
         cursor: pointer;
         box-shadow: black 0.3em 0.3em 1em;
         border-radius: 5px;
diff --git a/src/client/views/nodes/audio/AudioWaveform.scss b/src/client/views/nodes/audio/AudioWaveform.scss
index 6cbd1759a..c6b0da9c8 100644
--- a/src/client/views/nodes/audio/AudioWaveform.scss
+++ b/src/client/views/nodes/audio/AudioWaveform.scss
@@ -1,17 +1,17 @@
-.audioWaveform  {
+.audioWaveform {
     position: relative;
     width: 100%;
     height: 200%;
     overflow: hidden;
     z-index: -1000;
-    bottom: 0;
+    bottom: 0px;
     pointer-events: none;
     div {
         height: 100% !important;
-        width: 100%  !important;
+        width: 100% !important;
     }
-    canvas { 
+    canvas {
         height: 100% !important;
-        width: 100%  !important;
+        width: 100% !important;
     }
 }
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
index 3d27fa887..4db5cec3d 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
@@ -28,7 +28,7 @@ $transition: all 0.2s ease-in-out;
         box-shadow: 0 1px 4px $shadow-color;
 
         h2 {
-            margin: 0;
+            margin: 0px;
             font-size: 1.5em;
             font-weight: 500;
         }
@@ -126,7 +126,7 @@ $transition: all 0.2s ease-in-out;
         animation: fadeIn 0.3s ease-in-out;
 
         p {
-            margin: 0;
+            margin: 0px;
             font-size: 14px;
         }
 
@@ -263,10 +263,10 @@ $transition: all 0.2s ease-in-out;
 
 .uploading-overlay {
     position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
+    top: 0px;
+    left: 0px;
+    right: 0px;
+    bottom: 0px;
     background-color: rgba(255, 255, 255, 0.8);
     display: flex;
     justify-content: center;
@@ -285,7 +285,7 @@ $transition: all 0.2s ease-in-out;
 
 @media (max-width: 768px) {
     .chat-box {
-        border-radius: 0;
+        border-radius: 0px;
     }
 
     .message {
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss b/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss
index ff5be4a38..77d452830 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss
@@ -21,8 +21,8 @@
     background-color: #4a90e2;
     opacity: 0.6;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     animation: bounce 2s infinite ease-in-out;
 }
 
@@ -42,10 +42,10 @@
 
 .uploading-overlay {
     position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
+    top: 0px;
+    left: 0px;
+    right: 0px;
+    bottom: 0px;
     background-color: rgba(255, 255, 255, 0.8);
     display: flex;
     align-items: center;
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
index 3734ad9cc..1b2f76bbe 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.scss
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -17,7 +17,7 @@
         min-width: 12px;
         position: relative;
         display: inline-block;
-        margin: 0;
+        margin: 0px;
         transform: scale(0.7);
         background-color: rgba(155, 155, 155, 0.24);
     }
diff --git a/src/client/views/nodes/formattedText/EquationEditor.scss b/src/client/views/nodes/formattedText/EquationEditor.scss
index b0c17e56e..602135a30 100644
--- a/src/client/views/nodes/formattedText/EquationEditor.scss
+++ b/src/client/views/nodes/formattedText/EquationEditor.scss
@@ -32,7 +32,7 @@
     margin-left: -1px;
     position: relative;
     z-index: 1;
-    padding: 0;
+    padding: 0px;
     display: -moz-inline-box;
     display: inline-block;
 }
@@ -128,8 +128,8 @@
 .mq-math-mode * {
     font-size: inherit;
     line-height: inherit;
-    margin: 0;
-    padding: 0;
+    margin: 0px;
+    padding: 0px;
     border-color: black;
     -webkit-user-select: none;
     -moz-user-select: none;
@@ -178,7 +178,7 @@
     margin-left: 0.1em;
 }
 .mq-math-mode .mq-roman var.mq-f {
-    margin: 0;
+    margin: 0px;
 }
 .mq-math-mode big {
     font-size: 200%;
@@ -323,7 +323,7 @@
     padding: 0.1em;
 }
 .mq-math-mode .mq-sqrt-prefix {
-    padding-top: 0;
+    padding-top: 0px;
     position: relative;
     top: 0.1em;
     vertical-align: top;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 547a2efa8..d5e566226 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -46,7 +46,7 @@
 }
 
 audiotag {
-    left: 0;
+    left: 0px;
     position: absolute;
     cursor: pointer;
     border-radius: 10px;
@@ -62,7 +62,7 @@ audiotag:hover {
 .formattedTextBox {
     touch-action: none;
     background: inherit;
-    padding: 0;
+    padding: 0px;
     border-width: 0px;
     border-color: global.$medium-gray;
     box-sizing: border-box;
@@ -77,14 +77,14 @@ audiotag:hover {
     width: 100%;
     position: relative;
     transform-origin: left top;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
 }
 
 .formattedTextBox-cont {
     touch-action: none;
     background: inherit;
-    padding: 0;
+    padding: 0px;
     border-width: 0px;
     border-radius: inherit;
     border-color: global.$medium-gray;
@@ -111,7 +111,7 @@ audiotag:hover {
 .answer-tooltip {
     font-size: 15px;
     padding: 2px;
-    max-width: 150;
+    max-width: 150px;
     line-height: 150%;
     position: relative;
 }
@@ -122,10 +122,10 @@ audiotag:hover {
     position: absolute;
     color: white;
     background: black;
-    right: 0;
-    bottom: 0;
-    width: 15;
-    height: 22;
+    right: 0px;
+    bottom: 0px;
+    width: 15px;
+    height: 22px;
     cursor: default;
 }
 
@@ -139,8 +139,8 @@ audiotag:hover {
 
 .formattedTextBox-sidebar-handle {
     position: absolute;
-    top: 0;
-    right: 0;
+    top: 0px;
+    right: 0px;
     width: 20px;
     height: 20px;
     font-size: 11px;
@@ -168,7 +168,7 @@ audiotag:hover {
     height: 100%;
     display: inline-block;
     position: absolute;
-    right: 0;
+    right: 0px;
     overflow: hidden;
 
     .collectionfreeformview-container {
@@ -302,8 +302,8 @@ footnote::before {
     position: absolute;
     top: -0.5em;
     content: ' ';
-    height: 0;
-    width: 0;
+    height: 0px;
+    width: 0px;
 }
 
 .formattedTextBox-inlineComment {
@@ -346,7 +346,7 @@ footnote::before {
 .prosemirror-linkBtn {
     background: unset;
     color: unset;
-    padding: 0;
+    padding: 0px;
     text-transform: unset;
     letter-spacing: unset;
     font-size: unset;
@@ -357,7 +357,7 @@ footnote::before {
     background-color: dimgray;
     margin-top: 1.5em;
     z-index: 1;
-    padding: 5;
+    padding: 5px;
     border-radius: 2px;
 }
 .prosemirror-hrefoptions {
@@ -396,7 +396,7 @@ footnote::before {
     blockquote {
         padding: 10px 10px;
         font-size: smaller;
-        margin: 0;
+        margin: 0px;
         font-style: italic;
         background: lightgray;
         border-left: solid 2px dimgray;
@@ -415,7 +415,7 @@ footnote::before {
         p {
             font-family: inherit;
         }
-        margin-left: 0;
+        margin-left: 0px;
     }
     .bullet1 {
         p {
@@ -439,7 +439,7 @@ footnote::before {
             display: inline-block;
             font-family: inherit;
         }
-        margin-left: 0;
+        margin-left: 0px;
         background-color: inherit;
     }
     .decimal2-ol {
@@ -506,7 +506,7 @@ footnote::before {
             display: inline-block;
             font-family: inherit;
         }
-        margin-left: 0;
+        margin-left: 0px;
         padding-left: 1.2em;
         background-color: inherit;
     }
@@ -661,7 +661,7 @@ footnote::before {
     .formattedTextBox-cont {
         touch-action: none;
         background: inherit;
-        padding: 0;
+        padding: 0px;
         border-width: 0px;
         border-radius: inherit;
         border-color: global.$medium-gray;
@@ -706,7 +706,7 @@ footnote::before {
         height: 100%;
         display: inline-block;
         position: absolute;
-        right: 0;
+        right: 0px;
 
         .collectionfreeformview-container {
             position: relative;
@@ -832,8 +832,8 @@ footnote::before {
         position: absolute;
         top: -0.5em;
         content: ' ';
-        height: 0;
-        width: 0;
+        height: px;
+        width: 0px;
     }
 
     .formattedTextBox-inlineComment {
@@ -892,7 +892,7 @@ footnote::before {
                 display: inline;
                 font-family: inherit;
             }
-            margin-left: 0;
+            margin-left: 0px;
         }
         .decimal2-ol {
             counter-reset: deci2;
@@ -952,7 +952,7 @@ footnote::before {
                 display: inline;
                 font-family: inherit;
             }
-            margin-left: 0;
+            margin-left: 0px;
             padding-left: 1.2em;
         }
         .multi2-ol {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
index bc0810f22..92f3e3290 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
@@ -11,8 +11,8 @@
     -webkit-transform: translateX(-50%);
     transform: translateX(-50%);
     box-shadow: 3px 3px 1.5px grey;
-    max-width: 400;
-    max-height: 235;
+    max-width: 400px;
+    max-height: 235px;
     height: max-content;
     .formattedTextBox-tooltipText {
         height: max-content;
@@ -22,26 +22,26 @@
 
 .formattedTextBox-tooltip:before {
     content: '';
-    height: 0;
-    width: 0;
+    height: 0px;
+    width: 0px;
     position: absolute;
     left: 50%;
     margin-left: -5px;
     bottom: -6px;
     border: 5px solid transparent;
-    border-bottom-width: 0;
+    border-bottom-width: 0px;
     border-top-color: silver;
 }
 
 .formattedTextBox-tooltip:after {
     content: '';
-    height: 0;
-    width: 0;
+    height: 0px;
+    width: 0px;
     position: absolute;
     left: 50%;
     margin-left: -5px;
     bottom: -4.5px;
     border: 5px solid transparent;
-    border-bottom-width: 0;
+    border-bottom-width: 0px;
     border-top-color: white;
 }
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss
index fcc816447..7c747de1e 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.scss
+++ b/src/client/views/nodes/formattedText/RichTextMenu.scss
@@ -23,7 +23,7 @@
     .dropdown {
         position: absolute;
         top: 35px;
-        left: 0;
+        left: 0px;
         background-color: #323232;
         color: global.$light-gray;
         border: 1px solid #4d4d4d;
@@ -47,7 +47,7 @@
             }
 
             &:last-child {
-                margin-bottom: 0;
+                margin-bottom: 0px;
             }
         }
     }
diff --git a/src/client/views/nodes/formattedText/TooltipTextMenu.scss b/src/client/views/nodes/formattedText/TooltipTextMenu.scss
index 87320943d..8980a93a2 100644
--- a/src/client/views/nodes/formattedText/TooltipTextMenu.scss
+++ b/src/client/views/nodes/formattedText/TooltipTextMenu.scss
@@ -195,7 +195,7 @@
         left: 1px;
         width: 24px;
         height: 4px;
-        margin-top: 0;
+        margin-top: 0px;
     }
 }
 
@@ -221,7 +221,7 @@
     display: inline-block;
     width: 1em;
     height: 1em;
-    stroke-width: 0;
+    stroke-width: 0px;
     stroke: currentColor;
     fill: currentColor;
     margin-right: 15px;
@@ -231,7 +231,7 @@
     display: inline-block;
     width: 1em;
     height: 1em;
-    stroke-width: 3;
+    stroke-width: 3px;
     fill: greenyellow;
     margin-right: 15px;
 }
@@ -270,7 +270,7 @@
     &.ProseMirror-menu-dropdown {
         width: 10px;
         height: 25px;
-        margin: 0;
+        margin: 0px;
         padding: 0 2px;
         background-color: #323232;
         text-align: center;
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.scss b/src/client/views/nodes/imageEditor/ImageEditor.scss
index c691e6a18..942a7d4c6 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.scss
+++ b/src/client/views/nodes/imageEditor/ImageEditor.scss
@@ -4,8 +4,8 @@ $scale: 0.5;
 
 .imageEditorContainer {
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     z-index: 9999;
     height: 100vh;
     width: 100vw;
@@ -78,7 +78,7 @@ $scale: 0.5;
         .sideControlsContainer {
             width: 160px;
             position: absolute;
-            left: 0;
+            left: 0px;
             height: 100%;
 
             .sideControls {
@@ -129,8 +129,8 @@ $scale: 0.5;
 
             .originalImageLabel {
                 position: absolute;
-                bottom: 10;
-                left: 10;
+                bottom: 10px;
+                left: 10px;
                 color: #ffffff;
                 font-size: 0.8rem;
                 letter-spacing: 1px;
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index e24b47bd1..8875f7012 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -229,7 +229,7 @@
             .toolbar-transition {
                 display: flex;
                 font-size: 10;
-                width: 100;
+                width: 100px;
                 background-color: rgba(0, 0, 0, 0);
                 min-width: max-content;
 
@@ -358,7 +358,7 @@
         font-weight: 200;
         padding: 8px;
         border-radius: 4px;
-        // height: 20;
+        // height: 20px;
         // display: flex;
         // margin-left: 5px;
         // margin-top: 5px;
@@ -371,8 +371,8 @@
     }
 
     .ribbon-propertyUpDown {
-        height: 20;
-        width: 20;
+        height: 20px;
+        width: 20px;
         margin-top: 5px;
         display: grid;
         grid-template-rows: 10px 10px;
@@ -486,7 +486,7 @@
             -moz-user-select: none;
             -ms-user-select: none;
             user-select: none;
-            margin: 0;
+            margin: 0px;
             margin-right: 3px;
             border-radius: 100%;
             height: 15px;
@@ -658,9 +658,9 @@
             height: 25px;
             color: white;
             width: 100%;
-            max-width: 120;
-            padding-left: 10;
-            padding-right: 10;
+            max-width: 120px;
+            padding-left: 10px;
+            padding-right: 10px;
             border-radius: 10px;
             background-color: global.$medium-gray;
         }
@@ -683,9 +683,9 @@
             height: 25px;
             color: global.$light-gray;
             width: 100%;
-            max-width: 120;
-            padding-left: 10;
-            padding-right: 10;
+            max-width: 120px;
+            padding-left: 10px;
+            padding-right: 10px;
             border-radius: 10px;
             background-color: global.$black;
         }
@@ -745,9 +745,9 @@
 
     .selectedList {
         display: block;
-        min-width: 50;
-        max-width: 120;
-        height: 70;
+        min-width: 50px;
+        max-width: 120px;
+        height: 70px;
         overflow-y: scroll;
 
         .selectedList-items {
@@ -760,7 +760,7 @@
         cursor: pointer;
         font-size: 10.5;
         font-weight: 300;
-        height: 20;
+        height: 20px;
         background-color: global.$medium-gray;
         color: white;
         display: flex;
@@ -788,7 +788,7 @@
         cursor: pointer;
         font-size: 10.5;
         font-weight: 200;
-        height: 20;
+        height: 20px;
         background-color: global.$white;
         display: inline-flex;
         color: global.$black;
@@ -813,7 +813,7 @@
     }
 
     svg.svg-inline--fa.fa-thumbtack.fa-w-12.toolbar-thumbtack {
-        right: 40;
+        right: 40px;
         position: absolute;
         transform: rotate(45deg);
     }
@@ -850,7 +850,7 @@
             background-color: global.$light-gray;
             border-radius: 5px;
             font-size: 10;
-            height: 25;
+            height: 25px;
             color: global.$black;
             padding-left: 5px;
             align-items: center;
@@ -868,8 +868,8 @@
                 display: block;
                 padding-left: 10px;
                 padding-right: 5px;
-                padding-top: 3;
-                padding-bottom: 3;
+                padding-top: 3px;
+                padding-bottom: 3px;
                 opacity: 0.8;
             }
 
@@ -959,8 +959,8 @@
     border-radius: 4px;
     padding-left: 7px;
     padding-right: 7px;
-    border-bottom-right-radius: 0;
-    border-top-right-radius: 0;
+    border-bottom-right-radius: 0px;
+    border-top-right-radius: 0px;
 }
 
 .presBox-button-right {
@@ -976,8 +976,8 @@
     border-radius: 4px;
     padding-left: 7px;
     padding-right: 7px;
-    border-bottom-left-radius: 0;
-    border-top-left-radius: 0;
+    border-bottom-left-radius: 0px;
+    border-top-left-radius: 0px;
 }
 
 .presBox-button-right.active {
@@ -1043,8 +1043,8 @@
         cursor: pointer;
         align-self: center;
         justify-self: center;
-        margin-top: 5;
-        margin-bottom: 5;
+        margin-top: 5px;
+        margin-bottom: 5px;
         position: relative;
         height: 55px;
         min-width: 90px;
@@ -1063,7 +1063,7 @@
             padding-left: 3px;
             margin-left: 3px;
             margin-right: 3px;
-            height: 13;
+            height: 13px;
             font-size: 12;
             display: flex;
             background-color: global.$white;
@@ -1076,7 +1076,7 @@
             margin-left: 3px;
             margin-right: 3px;
             font-weight: 400;
-            height: 13;
+            height: 13px;
             font-size: 9;
             display: flex;
             background-color: global.$white;
@@ -1089,11 +1089,11 @@
             padding-left: 3px;
             margin-left: 3px;
             margin-right: 3px;
-            height: 13;
+            height: 13px;
             font-size: 10;
             display: flex;
             background-color: global.$white;
-            height: 33;
+            height: 33px;
             text-align: left;
             font-size: 8px;
         }
@@ -1112,7 +1112,7 @@
 
     .presBox-viewPicker {
         cursor: pointer;
-        height: 25;
+        height: 25px;
         position: relative;
         display: inline-block;
         grid-column: 1;
@@ -1184,9 +1184,9 @@
     }
 
     .collectionViewBaseChrome-viewPicker {
-        min-width: 50;
+        min-width: 50px;
         width: 5%;
-        height: 25;
+        height: 25px;
         position: relative;
         display: inline-block;
         left: 8px;
@@ -1210,11 +1210,11 @@
 }
 
 .presBox-backward {
-    left: 5;
+    left: 5px;
 }
 
 .presBox-forward {
-    right: 5;
+    right: 5px;
 }
 
 // CSS adjusted for mobile devices
@@ -1233,8 +1233,8 @@
 
         .presBox-button {
             margin-top: 5%;
-            height: 250;
-            width: 300;
+            height: 250px;
+            width: 300px;
             font-size: 100;
             display: flex;
             align-items: center;
@@ -1256,7 +1256,7 @@
     }
 
     .presBox-cont .presBox-listCont {
-        top: 50;
+        top: 50px;
         height: calc(100% - 80px);
     }
 
@@ -1269,8 +1269,8 @@
 .miniPres {
     cursor: grab;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     opacity: 0.5;
     transition: all 0.4s;
     color: global.$white;
@@ -1298,7 +1298,7 @@
     .presPanel-button-text {
         cursor: pointer;
         display: flex;
-        height: 20;
+        height: 20px;
         width: max-content;
         font-family: Roboto;
         font-weight: 400;
@@ -1327,8 +1327,8 @@
         grid-template-columns: auto auto auto;
         justify-content: space-around;
         font-size: 11;
-        margin-left: 7;
-        width: 30;
+        margin-left: 7px;
+        width: 30px;
         height: 85%;
         background-color: rgba(91, 157, 221, 0.4);
         border-radius: 5px;
@@ -1337,8 +1337,8 @@
     .presPanel-button {
         cursor: pointer;
         display: flex;
-        height: 20;
-        min-width: 20;
+        height: 20px;
+        min-width: 20px;
         margin-left: 3px;
         margin-right: 3px;
         border-radius: 100%;
@@ -1361,8 +1361,8 @@
 //     cursor: grab;
 //     position: absolute;
 //     overflow: hidden;
-//     right: 10;
-//     top: 10;
+//     right: 10px;
+//     top: 10px;
 //     opacity: 0.1;
 //     transition: all 0.4s;
 //     /* border: solid 1px; */
@@ -1380,7 +1380,7 @@
 //         .miniPres-button-text {
 //             cursor: pointer;
 //             display: flex;
-//             height: 20;
+//             height: 20px;
 //             font-weight: 400;
 //             min-width: 100%;
 //             border-radius: 5px;
@@ -1397,8 +1397,8 @@
 //             grid-template-columns: auto auto auto;
 //             justify-content: space-around;
 //             font-size: 11;
-//             margin-left: 7;
-//             width: 30;
+//             margin-left: 7px;
+//             width: 30px;
 //             height: 85%;
 //             background-color: rgba(91, 157, 221, 0.4);
 //             border-radius: 5px;
@@ -1413,8 +1413,8 @@
 //         .miniPres-button {
 //             cursor: pointer;
 //             display: flex;
-//             height: 20;
-//             min-width: 20;
+//             height: 20px;
+//             min-width: 20px;
 //             border-radius: 100%;
 //             align-items: center;
 //             justify-content: center;
diff --git a/src/client/views/nodes/trails/PresSlideBox.scss b/src/client/views/nodes/trails/PresSlideBox.scss
index 9ac2b5a94..740bcae5f 100644
--- a/src/client/views/nodes/trails/PresSlideBox.scss
+++ b/src/client/views/nodes/trails/PresSlideBox.scss
@@ -117,8 +117,8 @@ $slide-active: #5b9fdd;
         height: 100%;
         position: absolute;
         border-radius: 3px;
-        top: 0;
-        left: 0;
+        top: 0px;
+        left: 0px;
         z-index: 1;
         overflow: hidden;
     }
@@ -209,7 +209,7 @@ $slide-active: #5b9fdd;
     position: absolute;
     /* grid-row: 3; */
     /* grid-column: 1/8; */
-    top: 28;
+    top: 28px;
     display: block;
     background: #92adb9;
     width: 100%;
@@ -276,7 +276,7 @@ $slide-active: #5b9fdd;
     cursor: pointer;
     position: absolute;
     border-radius: 100px;
-    bottom: 0;
+    bottom: 0px;
     left: -18;
     z-index: 300;
     width: 15px;
@@ -302,7 +302,7 @@ $slide-active: #5b9fdd;
     color: #d5dce2;
     position: absolute;
     left: -15px;
-    top: 1;
+    top: 1px;
     font-weight: 600;
     font-size: 12;
 }
diff --git a/src/client/views/pdf/AnchorMenu.scss b/src/client/views/pdf/AnchorMenu.scss
index 6990bdcf1..98eeaf80e 100644
--- a/src/client/views/pdf/AnchorMenu.scss
+++ b/src/client/views/pdf/AnchorMenu.scss
@@ -6,19 +6,19 @@
 }
 .anchorMenu-highlighter {
     padding-right: 5px;
-    .antimodeMenu-button  {
-        padding: 0;
-        padding: 0;
+    .antimodeMenu-button {
+        padding: 0px;
+        padding: 0px;
         padding-right: 0px;
         padding-left: 0px;
         width: 5px;
     }
 }
-.anchor-color-preview-button {  
-    width: 25px !important;  
+.anchor-color-preview-button {
+    width: 25px !important;
     .anchor-color-preview {
         display: flex;
-        flex-direction:  column;
+        flex-direction: column;
         padding-right: 3px;
         width: unset !important;
         .color-preview {
@@ -51,4 +51,4 @@
             border: 2px solid white;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index f6fa45221..85c1f6759 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -20,8 +20,8 @@ $headingHeight: 32px;
     left: 75px;
     width: 100%;
     height: 100%;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     border-top: solid gray 20px;
     border-radius: 16px;
     z-index: 999;
@@ -58,7 +58,7 @@ $headingHeight: 32px;
         font-size: 12px;
         font-weight: 400;
         letter-spacing: 1px;
-        margin: 0;
+        margin: 0px;
         padding-right: 5px;
     }
 
@@ -214,8 +214,8 @@ $headingHeight: 32px;
         .img-container::after {
             content: '';
             position: absolute;
-            top: 0;
-            left: 0;
+            top: 0px;
+            left: 0px;
             width: 100%;
             height: 100%;
             background-color: rgba(0, 0, 0, 0.5);
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index a54505443..dafa908a2 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -3,8 +3,8 @@
     width: 100%;
     position: absolute;
     display: inline-block;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
 }
 
 :root {
@@ -16,8 +16,8 @@
     position: absolute;
     width: 100%;
     height: 100%;
-    top: 0;
-    left: 0;
+    top: 0px;
+    left: 0px;
     position: absolute;
     overflow-y: auto;
     overflow-x: hidden;
@@ -99,7 +99,7 @@
     .pdfViewerDash-annotationLayer {
         position: absolute;
         transform-origin: left top;
-        top: 0;
+        top: 0px;
         width: 100%;
         pointer-events: none;
         mix-blend-mode: multiply; // bcz: makes text fuzzy!
diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss
index 35a3da312..ca177c746 100644
--- a/src/client/views/topbar/TopBar.scss
+++ b/src/client/views/topbar/TopBar.scss
@@ -125,7 +125,7 @@
 
         .topbar-lozenge-user,
         .topbar-lozenge {
-            height: 23;
+            height: 23px;
             font-size: 12;
             color: white;
             font-family: 'Roboto';
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index f3295a6c6..354ed11d6 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -1,8 +1,5 @@
 /* eslint-disable react/no-unescaped-entities */
-/* eslint-disable react/button-has-type */
 /* eslint-disable no-use-before-define */
-/* eslint-disable react/no-array-index-key */
-/* eslint-disable no-redeclare */
 import { action, configure, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
diff --git a/src/fields/FieldLoader.scss b/src/fields/FieldLoader.scss
index 9a23c3e4f..62d85a763 100644
--- a/src/fields/FieldLoader.scss
+++ b/src/fields/FieldLoader.scss
@@ -1,7 +1,7 @@
 .fieldLoader {
     z-index: 10000;
     width: 200px;
-    height: 30;
+    height: 30px;
     background: lightblue;
     position: absolute;
     left: calc(50% - 99px);
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 7c55e4a42..1e68a4e30 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -18,7 +18,7 @@ export default class UploadManager extends ApiManager {
     protected initialize(register: Registration): void {
         register({
             method: Method.POST,
-            subscription: '/ping',
+            subscription: '/DashPing',
             secureHandler: async ({ /* req, */ res }) => {
                 _success(res, { message: DashVersion, date: new Date() });
             },
diff --git a/src/server/PdfTypes.ts b/src/server/PdfTypes.ts
index fb435a677..458df7b26 100644
--- a/src/server/PdfTypes.ts
+++ b/src/server/PdfTypes.ts
@@ -2,7 +2,7 @@ export interface PDFInfo {
     PDFFormatVersion: string;
     IsAcroFormPresent: boolean;
     IsXFAPresent: boolean;
-    [key: string]: any;
+    [key: string]: unknown;
 }
 export interface PDFMetadata {
     parse(): void;
diff --git a/views/stylesheets/authentication.css b/views/stylesheets/authentication.css
index ff1f4aace..dbd1043a4 100644
--- a/views/stylesheets/authentication.css
+++ b/views/stylesheets/authentication.css
@@ -110,10 +110,10 @@ body {
     text-align: center;
     position: absolute;
     margin: auto;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
+    top: 0px;
+    bottom: 0px;
+    left: 0px;
+    right: 0px;
     width: 400px;
     background-color: white;
     border-radius: 8px;
@@ -122,10 +122,10 @@ body {
 
 .inner {
     position: absolute;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
+    top: 0px;
+    bottom: 0px;
+    left: 0px;
+    right: 0px;
     width: 230px;
     margin: auto;
 }
@@ -141,7 +141,8 @@ body {
     font-size: 16px;
 }
 
-.outermost, .online-container {
+.outermost,
+.online-container {
     display: flex;
     flex-direction: row;
     height: 98vh;
@@ -220,4 +221,4 @@ span.user-type {
 
 #inactive-inner {
     border-bottom-right-radius: 8px;
-}
\ No newline at end of file
+}
-- 
cgit v1.2.3-70-g09d2


From 5d4ad5e02cb6262dd9d819eea0fa3eca4adc5124 Mon Sep 17 00:00:00 2001
From: geireann <geireann.lindfield@gmail.com>
Date: Mon, 16 Jun 2025 13:08:15 -0400
Subject: fixed typos

---
 src/client/views/nodes/ImageBox.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 616046d5d..232f4ae32 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -112,7 +112,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._props.setContentViewBox?.(this);
     }
 
-    @computed get oupaintOriginalSize(): { width: number; height: number } {
+    @computed get outpaintOriginalSize(): { width: number; height: number } {
         return {
             width: NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']),
             height: NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']),
@@ -385,7 +385,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     @action
     cancelOutpaintPrompt = () => {
-        [this.Document._width, this.Document._height] = [this.oupaintOriginalSize.width, this.oupaintOriginalSize.height];
+        [this.Document._width, this.Document._height] = [this.outpaintOriginalSize.width, this.outpaintOriginalSize.height];
         this._outpaintingInProgress = false;
         this.outpaintOriginalSize = undefined;
         this.closeOutpaintPrompt();
@@ -435,7 +435,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>';
             this._mainCont?.appendChild(loadingOverlay);
 
-            const { width: origWidth, height: origHeight } = this.oupaintOriginalSize;
+            const { width: origWidth, height: origHeight } = this.outpaintOriginalSize;
             const response = await Networking.PostToServer('/outpaintImage', {
                 imageUrl: currentPath,
                 prompt: customPrompt,
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From c0daf8b6aff92b71662d9791aeaa4bb63076dd02 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 19 Jun 2025 15:30:38 -0400
Subject: from last

---
 src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 4 ++--
 src/client/views/nodes/ImageBox.tsx                             | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 128606675..ff78b332a 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -589,8 +589,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         });
 
         this._props.addDocument?.(scrapbook);
-        const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
-        DocUtils.MakeLink(scrapbook, portal, { link_relationship: 'summary of:summarized by' });
+        const portal = Docs.Create.FreeformDocument(selected, { title: 'docs in scrapbook', x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
+        DocUtils.MakeLink(scrapbook, portal, { link_relationship: 'scrapbook of:in scrapbook' });
 
         portal.hidden = true;
         this._props.addDocument?.(portal);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 4d8d486b1..5b738ee19 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -262,7 +262,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
-    handleSelection = async (selection: string) => (this._searchInput = selection);
+    handleSelection = (selection: string) => (this._searchInput = selection);
 
     drop = undoable(
         action((e: Event, de: DragManager.DropEvent) => {
-- 
cgit v1.2.3-70-g09d2


From e7a96fa043cfc9c3c426e09bbef42c8df88a45f6 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 20 Jun 2025 09:30:54 -0400
Subject: cleanup

---
 src/client/views/InkTranscription.tsx | 21 +++++++++++----------
 src/client/views/nodes/ImageBox.tsx   |  6 ++----
 2 files changed, 13 insertions(+), 14 deletions(-)

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

diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 2e6b477e9..6220033d4 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -4,8 +4,8 @@ import * as React from 'react';
 import { imageUrlToBase64 } from '../../ClientUtils';
 import { aggregateBounds } from '../../Utils';
 import { Doc, DocListCast } from '../../fields/Doc';
-import { InkData, InkField, InkInkTool, InkTool } from '../../fields/InkField';
-import { Cast, DateCast, ImageCast, NumCast } from '../../fields/Types';
+import { InkData, InkInkTool, InkTool } from '../../fields/InkField';
+import { Cast, DateCast, ImageCast, InkCast, NumCast } from '../../fields/Types';
 import { ImageField, URLField } from '../../fields/URLField';
 import { gptHandwriting } from '../apis/gpt/GPT';
 import { DocumentType } from '../documents/DocumentTypes';
@@ -30,7 +30,7 @@ export class InkTranscription extends React.Component {
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     @observable _textRef: any = undefined;
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    @observable iinkEditor: any = undefined;
+    @observable _iinkEditor: any = undefined;
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     private lastJiix: any;
     private currGroup?: Doc;
@@ -94,7 +94,7 @@ export class InkTranscription extends React.Component {
                     },
                 },
             };
-            this.iinkEditor = await iink.Editor.load(r, 'INKV2', options);
+            this._iinkEditor = await iink.Editor.load(r, 'INKV2', options);
             this._textRegister = r;
             // eslint-disable-next-line @typescript-eslint/no-explicit-any
             r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
@@ -118,19 +118,20 @@ export class InkTranscription extends React.Component {
 
         const times: number[] = [];
         validInks
-            .filter(i => Cast(i[Doc.LayoutDataKey(i)], InkField))
+            .filter(i => InkCast(i[Doc.LayoutDataKey(i)]))
             .forEach(i => {
-                const d = Cast(i[Doc.LayoutDataKey(i)], InkField, null);
+                const d = InkCast(i[Doc.LayoutDataKey(i)])!;
+                const authorTime = DateCast(i.author_date)?.getDate().getTime() ?? 0;
                 const inkStroke = DocumentView.getDocumentView(i)?.ComponentView as InkingStroke;
                 strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y })));
-                times.push(DateCast(i.author_date).getDate().getTime());
+                times.push(authorTime);
             });
         this.currGroup = groupDoc;
         const pointerData = strokes.map((stroke, i) => this.inkJSON(stroke, times[i]));
         if (math) {
-            this.iinkEditor.importPointEvents(pointerData);
+            this._iinkEditor.importPointEvents(pointerData);
         } else {
-            this.iinkEditor.importPointEvents(pointerData);
+            this._iinkEditor.importPointEvents(pointerData);
         }
     };
     convertPointsToString(points: InkData[]): string {
@@ -234,7 +235,7 @@ export class InkTranscription extends React.Component {
                         const docList = DocListCast(this.currGroup.data);
                         docList.forEach((inkDoc: Doc) => {
                             // just having the times match up and be a unique value (actual timestamp doesn't matter)
-                            const ms = DateCast(inkDoc.author_date).getDate().getTime() + 14400000;
+                            const ms = (DateCast(inkDoc.author_date)?.getDate().getTime() ?? 0) + 14400000;
                             const word = timestampWord.get(ms);
                             if (!word) {
                                 return;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 5b738ee19..78bacdcac 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -72,7 +72,7 @@ export class ImageEditorData {
     public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore
 }
 
-const API_URL = 'https://api.unsplash.com/search/photos';
+const UNSPLASH_API = 'https://api.unsplash.com/search/photos';
 @observer
 export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
@@ -245,7 +245,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
      */
     fetchImages = async () => {
         try {
-            const { data } = await axios.get(`${API_URL}?query=${this._searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`);
+            const { data } = await axios.get(`${UNSPLASH_API}?query=${this._searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`);
             const imageSnapshot = Docs.Create.ImageDocument(data.results[0].urls.small, {
                 _nativeWidth: Doc.NativeWidth(this.layoutDoc),
                 _nativeHeight: Doc.NativeHeight(this.layoutDoc),
@@ -262,8 +262,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
-    handleSelection = (selection: string) => (this._searchInput = selection);
-
     drop = undoable(
         action((e: Event, de: DragManager.DropEvent) => {
             if (de.complete.docDragData && !this._props.rejectDrop?.(de, this.DocumentView?.())) {
-- 
cgit v1.2.3-70-g09d2

