From 585f03bf45df4ac7ed61d22c9dbe10d8e453199d Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Wed, 5 Jun 2024 15:06:15 -0400
Subject: Flashcards changes

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

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7a1f94948..8bf7b094f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -575,7 +575,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'eye' });
             if (this.Document._layout_isFlashcard) {
-                appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(), icon: 'id-card' });
+                appearanceItems.push({ description: 'Create an answer on the back', event: () => this.askGPT(), icon: 'id-card' });
             }
 
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
-- 
cgit v1.2.3-70-g09d2


From c76505f56a83625e3993427838aaee0c54c5fb81 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Thu, 6 Jun 2024 09:53:12 -0400
Subject: Fixes to chatcard and quizcard

---
 src/client/apis/gpt/GPT.ts                         |  2 +-
 .../views/collections/CollectionCarouselView.tsx   |  7 ++--
 src/client/views/nodes/ComparisonBox.tsx           | 43 ++++++++++++++++++----
 src/client/views/nodes/DocumentView.tsx            | 19 +---------
 4 files changed, 40 insertions(+), 31 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 05007960d..b036349dc 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -51,7 +51,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
-        prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If there are no differences, say correct',
+        prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If the Rubric is incorrect, explain why. If there are no differences, say correct. If it is empty, say there is nothing for me to evaluate.',
     },
 };
 
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 2893de762..53d14e6e0 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -35,6 +35,7 @@ export class CollectionCarouselView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
     @observable private _message = 'Drag a document';
     get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
+    get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
     get starField()     { return this.fieldKey + "_star"; } // prettier-ignore
 
     constructor(props: any) {
@@ -131,9 +132,6 @@ export class CollectionCarouselView extends CollectionSubView() {
         curDoc.layout[this.practiceField] = val;
         this.advance(e);
     };
-    clearContent = () => {
-        this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
-    };
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string): any => {
         // first look for properties on the document in the carousel, then fallback to properties on the container
         const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
@@ -146,7 +144,8 @@ export class CollectionCarouselView extends CollectionSubView() {
     setFilterMode = (mode: cardMode) => {
         this.layoutDoc.filterOp = mode;
         if (mode == cardMode.STAR) this.move(1);
-        this.clearContent();
+        if (mode == cardMode.QUIZ) this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
+        this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
     };
     specificMenu = (): void => {
         const cm = ContextMenu.Instance;
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index c0c173d9a..6d4af2f3e 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -25,6 +25,8 @@ import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import ReactLoading from 'react-loading';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -282,7 +284,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         // Call the GPT model and get the output
         this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate';
         this._outputValue = '';
-        if (this._inputValue) this.askGPT();
+        if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
     };
 
     @action handleRenderClick = () => {
@@ -290,12 +292,22 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         this.layoutDoc[`_${this._props.fieldKey}_usePath`] = undefined;
     };
 
-    animateRes = (resIndex: number, newText: string) => {
+    animateRes = (resIndex: number, newText: string, callType: GPTCallType) => {
         if (resIndex < newText.length) {
             // const marks = this._editorView?.state.storedMarks ?? [];
-            this._outputValue += newText[resIndex];
+            switch (callType) {
+                case GPTCallType.CHATCARD:
+                    DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text += newText[resIndex];
+                    break;
+                case GPTCallType.QUIZ:
+                    this._outputValue += newText[resIndex];
+                    break;
+                default:
+                    return;
+            }
+
             // this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(this._outputValue));
-            setTimeout(() => this.animateRes(resIndex + 1, newText), 20);
+            setTimeout(() => this.animateRes(resIndex + 1, newText, callType), 20);
         }
     };
 
@@ -303,18 +315,22 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * Calls the GPT model to create QuizCards. Evaluates how similar the user's response is to the alternate
      * side of the flashcard.
      */
-    askGPT = async (): Promise<string | undefined> => {
+    askGPT = async (callType: GPTCallType): Promise<string | undefined> => {
         const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
         const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text);
         const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText;
         this._loading = true;
+        if (callType == GPTCallType.CHATCARD) {
+            DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = '';
+            this.flipFlashcard();
+        }
         try {
-            const res = await gptAPICall(queryText, GPTCallType.QUIZ);
+            const res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType);
             if (!res) {
                 console.error('GPT call failed');
                 return;
             }
-            this.animateRes(0, '\n\n' + res);
+            this.animateRes(0, res, callType);
             // this._outputValue = res;
             console.log(res);
         } catch (err) {
@@ -325,6 +341,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     layoutWidth = () => NumCast(this.layoutDoc.width, 200);
     layoutHeight = () => NumCast(this.layoutDoc.height, 200);
 
+    specificMenu = (): void => {
+        const cm = ContextMenu.Instance;
+        cm.addItem({ description: 'Create an Answer on the Back', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'pencil' });
+    };
+
     render() {
         const clearButton = (which: string) => (
             <Tooltip title={<div className="dash-tooltip">remove</div>}>
@@ -425,7 +446,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         </div>
                         <div className="submit-button" style={{ overflow: 'hidden', marginBottom: '2px', display: this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 'flex' : 'none' }}>
                             <button type="button" onClick={this.handleRenderClick} style={{ borderRadius: '2px' }}>
-                                Try Again
+                                Redo the Question
                             </button>
                         </div>
                     </div>
@@ -436,6 +457,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             return (
                 <div
                     className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
+                    onContextMenu={this.specificMenu}
                     style={{ display: 'flex', flexDirection: 'column' }}
                     onMouseEnter={() => {
                         this.hoverFlip('alternate');
@@ -446,6 +468,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     // onPointerUp={() => (this._isAnyChildContentActive = true)}
                 >
                     {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
+                    {this._loading ? (
+                        <div className="loading-spinner" style={{ position: 'absolute' }}>
+                            <ReactLoading type="spin" height={30} width={30} color={'blue'} />
+                        </div>
+                    ) : null}
                     {this.overlayAlternateIcon}
                 </div>
             );
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 8bf7b094f..a25249eac 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -111,6 +111,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     private _downTime: number = 0;
     private _lastTap: number = 0;
     private _doubleTap = false;
+    private _loading = false;
     private _mainCont = React.createRef<HTMLDivElement>();
     private _titleRef = React.createRef<EditableView>();
     private _dropDisposer?: DragManager.DragDropDisposer;
@@ -501,21 +502,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         input.click();
     };
 
-    askGPT = async (): Promise<string | undefined> => {
-        const queryText = RTFCast(DocCast(this.dataDoc[this.props.fieldKey + '_1']).text)?.Text;
-        try {
-            const res = await gptAPICall('Question: ' + StrCast(queryText), GPTCallType.CHATCARD);
-            if (!res) {
-                console.error('GPT call failed');
-                return;
-            }
-            DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
-            console.log(res);
-        } catch (err) {
-            console.error('GPT call failed');
-        }
-    };
-
     onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
         if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) {
             e.preventDefault();
@@ -574,9 +560,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'eye' });
-            if (this.Document._layout_isFlashcard) {
-                appearanceItems.push({ description: 'Create an answer on the back', event: () => this.askGPT(), icon: 'id-card' });
-            }
 
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
-- 
cgit v1.2.3-70-g09d2


From 7594f649890d27d558be35c9d40a0aa8211aec67 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Thu, 6 Jun 2024 22:20:24 -0400
Subject: Flashcard changes - menu added

---
 src/client/views/nodes/ComparisonBox.scss          | 20 +----
 src/client/views/nodes/ComparisonBox.tsx           | 99 +++++++++++++++++-----
 src/client/views/nodes/DocumentView.tsx            |  3 +-
 src/client/views/nodes/FieldView.tsx               |  1 +
 .../nodes/formattedText/FormattedTextBox.scss      |  6 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  1 +
 6 files changed, 87 insertions(+), 43 deletions(-)

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

diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index f3dc0f68c..dc107b576 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -47,6 +47,7 @@
         width: '91%';
         height: '80%';
         z-index: '-1';
+        overscroll-behavior: contain;
     }
 
     .clip-div {
@@ -241,24 +242,5 @@
                 }
             }
         }
-
-        // .loading-circle {
-        //     position: relative;
-        //     width: 50px;
-        //     height: 50px;
-        //     border-radius: 50%;
-        //     border: 3px solid #ccc;
-        //     border-top-color: #333;
-        //     animation: spin 1s infinite linear;
-        // }
-
-        // @keyframes spin {
-        //     0% {
-        //         transform: rotate(0deg);
-        //     }
-        //     100% {
-        //         transform: rotate(360deg);
-        //     }
-        // }
     }
 }
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 6d4af2f3e..f844892c5 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,9 +1,9 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { action, computed, makeObservable, observable } from 'mobx';
+import { action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { returnFalse, returnNone, setupMoveUpEvents } from '../../../ClientUtils';
+import { returnFalse, returnNone, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
@@ -27,6 +27,7 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import ReactLoading from 'react-loading';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
+import { tickStep } from 'd3';
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -37,7 +38,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
-        // this._isAnyChildContentActive = true;
     }
 
     @observable private _inputValue = '';
@@ -63,12 +63,19 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @computed get clipHeight() {
         return NumCast(this.layoutDoc[this.clipHeightKey], 200);
     }
+    get revealOp() {
+        return this.layoutDoc[`_${this._props.fieldKey}_revealOp`];
+    }
     get clipHeightKey() {
         return '_' + this._props.fieldKey + '_clipHeight';
     }
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
+        reaction(
+            () => this._props.isSelected(),
+            selected => !selected && (this.childActive = false)
+        );
     }
     protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => {
         this._disposers[disposerId]?.();
@@ -98,7 +105,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 emptyFunction,
                 action((moveEv, doubleTap) => {
                     if (doubleTap) {
-                        this._isAnyChildContentActive = true;
+                        this.childActive = true;
                         if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
                         if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
                     }
@@ -106,7 +113,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 false,
                 undefined,
                 action(() => {
-                    if (this._isAnyChildContentActive) return;
+                    if (this.childActive) return;
                     this._animating = 'all 200ms';
                     // on click, animate slider movement to the targetWidth
                     this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth();
@@ -247,7 +254,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     hoverFlip = (side: string | undefined) => {
         if (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'hover') this.layoutDoc[`_${this._props.fieldKey}_usePath`] = side;
     };
-
     /**
      * Creates the button used to flip the flashcards.
      */
@@ -259,7 +265,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     className="formattedTextBox-alternateButton"
                     onPointerDown={e =>
                         setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
-                            console.log(this.layoutDoc[`_${this._props.fieldKey}_revealOp`]);
                             if (!this.layoutDoc[`_${this._props.fieldKey}_revealOp`] || this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'flip') {
                                 this.flipFlashcard();
 
@@ -271,6 +276,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     style={{
                         background: usepath === 'alternate' ? 'white' : 'black',
                         color: usepath === 'alternate' ? 'black' : 'white',
+                        display: 'inline-block',
                     }}>
                     <div key="alternate" className="formattedTextBox-flip">
                         <FontAwesomeIcon icon="turn-up" size="1x" />
@@ -280,6 +286,37 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     }
 
+    @computed get flashcardMenu() {
+        return (
+            <div>
+                <Tooltip
+                    title={
+                        this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? (
+                            <div className="dash-tooltip">Flip to front side to use GPT</div>
+                        ) : (
+                            <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard</div>
+                        )
+                    }>
+                    <div style={{ position: 'absolute', bottom: '3px', right: '55px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}>
+                        <FontAwesomeIcon icon="lightbulb" size="xl" />
+                    </div>
+                </Tooltip>
+                <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
+                    <div
+                        style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }}
+                        onClick={e => (this.revealOp === 'hover' ? (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip') : (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'))}>
+                        <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="layer-group" size="xl" />
+                    </div>
+                </Tooltip>
+                {this.overlayAlternateIcon}
+            </div>
+        );
+    }
+
+    @action activateContent = () => {
+        this.childActive = true;
+    };
+
     @action handleRenderGPTClick = () => {
         // Call the GPT model and get the output
         this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate';
@@ -320,8 +357,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text);
         const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText;
         this._loading = true;
+        const doc = DocCast(this.dataDoc[this.props.fieldKey + '_0']);
         if (callType == GPTCallType.CHATCARD) {
-            DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = '';
+            if (StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '') {
+                this._loading = false;
+                return;
+            }
             this.flipFlashcard();
         }
         try {
@@ -330,7 +371,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 console.error('GPT call failed');
                 return;
             }
-            this.animateRes(0, res, callType);
+            // this.animateRes(0, res, callType);
+            if (callType == GPTCallType.CHATCARD) {
+                DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
+                // this.flipFlashcard();
+            }
+            if (callType == GPTCallType.QUIZ) this._outputValue = res;
+            // DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
             // this._outputValue = res;
             console.log(res);
         } catch (err) {
@@ -341,10 +388,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     layoutWidth = () => NumCast(this.layoutDoc.width, 200);
     layoutHeight = () => NumCast(this.layoutDoc.height, 200);
 
-    specificMenu = (): void => {
-        const cm = ContextMenu.Instance;
-        cm.addItem({ description: 'Create an Answer on the Back', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'pencil' });
-    };
+    // specificMenu = (): void => {
+    //     const cm = ContextMenu.Instance;
+    //     cm.addItem({ description: 'Create an Answer on the Back', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'pencil' });
+    // };
+    @observable childActive = false;
 
     render() {
         const clearButton = (which: string) => (
@@ -378,12 +426,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
                         NativeWidth={this.layoutWidth}
                         NativeHeight={this.layoutHeight}
-                        isContentActive={emptyFunction}
+                        isContentActive={() => this.childActive}
                         isDocumentActive={returnFalse}
+                        dontSelect={returnTrue}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-                        styleProvider={this._isAnyChildContentActive ? this._props.styleProvider : this.docStyleProvider}
+                        styleProvider={this.childActive ? this._props.styleProvider : this.docStyleProvider}
                         hideLinkButton
-                        pointerEvents={this._isAnyChildContentActive ? undefined : returnNone}
+                        pointerEvents={this.childActive ? undefined : returnNone}
                     />
                     <div style={{ position: 'absolute', top: '-5px', left: '2px' }}>{layoutString ? null : clearButton(whichSlot)}</div>
                 </> // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px`
@@ -394,7 +443,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             );
         };
         const displayBox = (which: string, index: number, cover: number) => (
-            <div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}>
+            <div
+                className={`${index === 0 ? 'before' : 'after'}Box-cont`}
+                key={which}
+                style={{ width: this._props.PanelWidth() }}
+                onPointerDown={e => {
+                    this.registerSliding(e, cover);
+                    this.activateContent();
+                }}
+                ref={ele => this.createDropTarget(ele, which, index)}>
                 {displayDoc(which)}
             </div>
         );
@@ -431,6 +488,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             <textarea
                                 value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._outputValue : this._inputValue}
                                 onChange={this.handleInputChange}
+                                onScroll={e => e.stopPropagation()}
                                 readOnly={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate'}></textarea>
 
                             {this._loading ? (
@@ -457,7 +515,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             return (
                 <div
                     className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
-                    onContextMenu={this.specificMenu}
+                    // onContextMenu={this.specificMenu}
                     style={{ display: 'flex', flexDirection: 'column' }}
                     onMouseEnter={() => {
                         this.hoverFlip('alternate');
@@ -473,7 +531,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             <ReactLoading type="spin" height={30} width={30} color={'blue'} />
                         </div>
                     ) : null}
-                    {this.overlayAlternateIcon}
+                    {this.flashcardMenu}
+                    {/* {this.overlayAlternateIcon} */}
                 </div>
             );
         }
@@ -491,7 +550,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         left: `calc(${this.clipWidth + '%'} - 0.5px)`,
                         cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined,
                     }}
-                    onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */
+                    onPointerDown={e => !this.childActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */
                 >
                     <div className="slide-handle" />
                 </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a25249eac..2f3357791 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -503,6 +503,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     };
 
     onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
+        if (this._props.dontSelect?.()) return;
         if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) {
             e.preventDefault();
             e.stopPropagation();
@@ -1378,7 +1379,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale;
     isSelected = () => this.IsSelected;
     select = (extendSelection: boolean, focusSelection?: boolean) => {
-        DocumentView.SelectView(this, extendSelection);
+        if (!this._props.dontSelect?.()) DocumentView.SelectView(this, extendSelection);
         if (focusSelection) {
             DocumentView.showDocument(this.Document, {
                 willZoomCentered: true,
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 818d26956..138f00492 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -50,6 +50,7 @@ export interface FieldViewSharedProps {
     PanelHeight: () => number;
     isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
     isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
+    dontSelect: () => boolean | undefined;
     childFilters: () => string[];
     childFiltersByRanges: () => string[];
     styleProvider: Opt<StyleProviderFuncType>;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 8ac8c2c5f..54643b4a5 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -89,14 +89,14 @@ audiotag:hover {
     right: 0;
     bottom: 0;
     width: 15;
-    height: 15;
+    height: 22;
     cursor: default;
 }
 .formattedTextBox-flip {
     align-items: center;
     position: absolute;
-    right: 3px;
-    bottom: 1px;
+    right: 2px;
+    bottom: 4px;
 }
 
 .formattedTextBox-outer {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 82133a06e..3e2befb5f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -816,6 +816,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     isTargetToggler = (anchor: Doc) => BoolCast(anchor.followLinkToggle);
 
     specificContextMenu = (e: React.MouseEvent): void => {
+        if (this._props.dontSelect?.()) return;
         const cm = ContextMenu.Instance;
 
         let target = e.target as any; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
-- 
cgit v1.2.3-70-g09d2


From d1b7e29761fa0395263bff3521f148170659ff62 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Fri, 7 Jun 2024 21:48:34 -0400
Subject: Flashcard menus

---
 src/client/views/MainView.tsx                      |  1 +
 .../views/collections/CollectionCarouselView.scss  | 42 ++++++++++++-
 .../views/collections/CollectionCarouselView.tsx   | 53 ++++++++++++----
 src/client/views/nodes/ComparisonBox.tsx           | 73 +++++++++++++++++-----
 src/client/views/nodes/DocumentView.tsx            | 14 ++---
 5 files changed, 147 insertions(+), 36 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 31d88fb87..541b15006 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -548,6 +548,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 fa.faRobot,
                 fa.faSatellite,
                 fa.faStar,
+                fa.faFilePen,
             ]
         );
     }
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index 975b352cf..c4679e888 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -50,7 +50,7 @@
 }
 .carouselView-star {
     top: 0;
-    right: 20;
+    left: 0;
 }
 .carouselView-remove {
     top: 80%;
@@ -60,6 +60,46 @@
     top: 80%;
     right: 52%;
 }
+.carouselView-quiz {
+    position: absolute;
+    display: flex;
+    top: 5px;
+    right: 8px;
+    &:hover {
+        color: white;
+    }
+}
+
+.carouselView-practice {
+    position: absolute;
+    display: flex;
+    top: 22px;
+    right: 8px;
+    &:hover {
+        color: white;
+    }
+}
+.carouselView-starFilter {
+    position: absolute;
+    display: flex;
+    top: 40px;
+    right: 7px;
+    &:hover {
+        color: white;
+    }
+}
+
+.carouselView-menu {
+    position: absolute;
+    display: flex;
+    top: 2px;
+    right: 2px;
+    width: 30;
+    height: 60;
+    border-radius: 5px;
+    color: rgba(255, 255, 255, 0.5);
+    background: rgba(0, 0, 0, 0.1);
+}
 
 .carouselView-back:hover,
 .carouselView-fwd:hover {
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 53d14e6e0..8da808065 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -19,6 +19,7 @@ import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView } from './CollectionSubView';
+import { Tooltip } from '@mui/material';
 
 enum cardMode {
     PRACTICE = 'practice',
@@ -143,22 +144,26 @@ export class CollectionCarouselView extends CollectionSubView() {
     captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
     setFilterMode = (mode: cardMode) => {
         this.layoutDoc.filterOp = mode;
+        console.log('MODE' + mode);
+        console.log('FILT' + this.layoutDoc.filterOp + ';');
         if (mode == cardMode.STAR) this.move(1);
-        if (mode == cardMode.QUIZ) this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
+        if (mode == cardMode.QUIZ) {
+            this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
+        }
         this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
     };
-    specificMenu = (): void => {
-        const cm = ContextMenu.Instance;
+    // specificMenu = (): void => {
+    //     const cm = ContextMenu.Instance;
 
-        const revealOptions = cm.findByDescription('Filter Flashcards');
-        const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
-        revealItems.push({description: 'All',           event: () => {this.setFilterMode(cardMode.ALL);},         icon: 'layer-group',}); // prettier-ignore
-        revealItems.push({description: 'Star',          event: () => {this.setFilterMode(cardMode.STAR);},     icon: 'star',}); // prettier-ignore
-        revealItems.push({description: 'Practice Mode', event: () => {this.setFilterMode(cardMode.PRACTICE);}, icon: 'check',}); // prettier-ignore
-        cm.addItem({description: 'Quiz Cards',    event: () => {this.setFilterMode(cardMode.QUIZ);},     icon: 'pencil',}); // prettier-ignore
-        !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
-        //cm.addItem({description: 'Quiz Cards',    event: () => {this.layoutDoc.filterOp = cardMode.QUIZ; this.clearContent()});
-    };
+    //     const revealOptions = cm.findByDescription('Filter Flashcards');
+    //     const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
+    //     revealItems.push({description: 'All',           event: () => {this.setFilterMode(cardMode.ALL);},         icon: 'layer-group',}); // prettier-ignore
+    //     revealItems.push({description: 'Star',          event: () => {this.setFilterMode(cardMode.STAR);},     icon: 'star',}); // prettier-ignore
+    //     revealItems.push({description: 'Practice Mode', event: () => {this.setFilterMode(cardMode.PRACTICE);}, icon: 'check',}); // prettier-ignore
+    //     cm.addItem({description: 'Quiz Cards',    event: () => {this.setFilterMode(cardMode.QUIZ);},     icon: 'pencil',}); // prettier-ignore
+    //     !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+    //     //cm.addItem({description: 'Quiz Cards',    event: () => {this.layoutDoc.filterOp = cardMode.QUIZ; this.clearContent()});
+    // };
     @computed get content() {
         const index = NumCast(this.layoutDoc._carousel_index);
         const curDoc = this.carouselItems?.[index];
@@ -226,6 +231,28 @@ export class CollectionCarouselView extends CollectionSubView() {
         );
     }
 
+    @computed get menu() {
+        return (
+            <div className="carouselView-menu">
+                <Tooltip title="Practice flashcards using GPT">
+                    <div key="back" className="carouselView-quiz" onClick={e => (this.layoutDoc.filterOp === cardMode.QUIZ ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.QUIZ))}>
+                        <FontAwesomeIcon icon="file-pen" color={this.layoutDoc.filterOp === cardMode.QUIZ ? 'white' : 'light gray'} size="1x" />
+                    </div>
+                </Tooltip>
+                <Tooltip title={this.layoutDoc.filterOp === cardMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
+                    <div key="back" className="carouselView-practice" onClick={e => (this.layoutDoc.filterOp === cardMode.PRACTICE ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.PRACTICE))}>
+                        <FontAwesomeIcon icon="check" color={this.layoutDoc.filterOp === cardMode.PRACTICE ? 'white' : 'light gray'} size="1x" />
+                    </div>
+                </Tooltip>
+                <Tooltip title={this.layoutDoc.filterOp === cardMode.STAR ? 'Show all cards' : 'Show only starred cards'}>
+                    <div key="back" className="carouselView-starFilter" onClick={e => (!this.layoutDoc.filterOp ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.STAR))}>
+                        <FontAwesomeIcon icon="filter" color={this.layoutDoc.filterOp === cardMode.STAR || !this.layoutDoc.filterOp ? 'white' : 'light gray'} size="1x" />
+                    </div>
+                </Tooltip>
+            </div>
+        );
+    }
+
     render() {
         return (
             <div
@@ -250,6 +277,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                 <p
                     style={{
                         color: 'red',
+                        fontWeight: 'bold',
                         zIndex: '999',
                         position: 'relative',
                         left: '10px',
@@ -263,6 +291,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                     }}>
                     Recently missed!
                 </p>
+                {this.menu}
                 {this.Document._chromeHidden || !this.layoutDoc.filterOp ? null : this.buttons}
             </div>
         );
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index f844892c5..084723d56 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -45,6 +45,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @observable private _loading = false;
     @observable private _errorMessage = '';
     @observable private _outputMessage = '';
+    @observable private _isEmpty = false;
+
+    public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
 
     @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
         this._inputValue = e.target.value;
@@ -162,23 +165,29 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     };
 
     clearDoc = undoable((fieldKey: string) => {
-        delete this.dataDoc[fieldKey];
-        this.dataDoc[fieldKey] = 'empty';
+        // delete this.dataDoc[fieldKey];
+        this.dataDoc[fieldKey] = undefined;
+        this._isEmpty = true;
+        // this.dataDoc[fieldKey] = 'empty';
+        console.log('HERE' + fieldKey + ';');
     }, 'clear doc');
     // clearDoc = (fieldKey: string) => delete this.dataDoc[fieldKey];
 
     moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc);
     addDoc = (doc: Doc, which: string) => {
-        if (this.dataDoc[which] && this.dataDoc[which] !== 'empty') return false;
+        if (this.dataDoc[which] && !this._isEmpty) return false;
         this.dataDoc[which] = doc;
         return true;
     };
     remDoc = (doc: Doc, which: string) => {
         if (this.dataDoc[which] === doc) {
-            this.dataDoc[which] = 'empty';
+            this._isEmpty = true;
+            // this.dataDoc[which] = 'empty';
+            console.log('HEREEEE');
             this.dataDoc[which] = undefined;
             return true;
         }
+        console.log('FALSE');
         return false;
     };
 
@@ -268,8 +277,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             if (!this.layoutDoc[`_${this._props.fieldKey}_revealOp`] || this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'flip') {
                                 this.flipFlashcard();
 
-                                console.log('Print Front of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text ?? ''));
-                                console.log('Print Back of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text ?? ''));
+                                // console.log('Print Front of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text ?? ''));
+                                // console.log('Print Back of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text ?? ''));
                             }
                         })
                     }
@@ -297,10 +306,31 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard</div>
                         )
                     }>
-                    <div style={{ position: 'absolute', bottom: '3px', right: '55px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}>
+                    <div style={{ position: 'absolute', bottom: '3px', right: '56px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}>
                         <FontAwesomeIcon icon="lightbulb" size="xl" />
                     </div>
                 </Tooltip>
+                <Tooltip title={<div>Create a flashcard pile</div>}>
+                    <div
+                        style={{ position: 'absolute', bottom: '3px', right: '80px', cursor: 'pointer' }}
+                        onPointerDown={e => {
+                            const collectionArr: Doc[] = [];
+                            collectionArr.push(this.Document);
+                            const newCol = Docs.Create.CarouselDocument(collectionArr, {
+                                _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250),
+                                _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200),
+                                _layout_fitWidth: false,
+                                _layout_autoHeight: true,
+                            });
+                            newCol['x'] = e.clientX - 820;
+                            newCol['y'] = e.clientY - 640;
+                            this._props.addDocument?.(newCol);
+                            this._props.removeDocument?.(this.Document);
+                            this.Document.embedContainer = newCol;
+                        }}>
+                        <FontAwesomeIcon icon="folder-plus" size="xl" />
+                    </div>
+                </Tooltip>
                 <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
                     <div
                         style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }}
@@ -308,11 +338,18 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="layer-group" size="xl" />
                     </div>
                 </Tooltip>
-                {this.overlayAlternateIcon}
+                {/* <Tooltip title={<div className="dash-tooltip">Remove this side of the flashcard</div>}>
+                    <div
+                        style={{ position: 'absolute', bottom: '3px', right: '80px', cursor: 'pointer' }}
+                        onPointerDown={e => this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._props.fieldKey + '_1' : this._props.fieldKey + '_0')}>
+                        <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="trash-can" size="xl" />
+                    </div>
+                </Tooltip> */}
+                {/* {this.overlayAlternateIcon} */}
             </div>
         );
     }
-
+    // this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this.fieldKey + '_0' : this.fieldKey + '_1')}
     @action activateContent = () => {
         this.childActive = true;
     };
@@ -434,7 +471,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         hideLinkButton
                         pointerEvents={this.childActive ? undefined : returnNone}
                     />
-                    <div style={{ position: 'absolute', top: '-5px', left: '2px' }}>{layoutString ? null : clearButton(whichSlot)}</div>
+                    {/* <div style={{ position: 'absolute', top: '-5px', left: '2px' }}>{layoutString ? null : clearButton(whichSlot)}</div> */}
                 </> // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px`
             ) : (
                 <div className="placeholder">
@@ -452,7 +489,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     this.activateContent();
                 }}
                 ref={ele => this.createDropTarget(ele, which, index)}>
-                {displayDoc(which)}
+                {!this._isEmpty ? displayDoc(which) : null}
+                {/* {this.dataDoc[this.fieldKey + '_0'] !== 'empty' ? displayDoc(which) : null} */}
             </div>
         );
 
@@ -460,7 +498,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             const side = this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 1 : 0;
 
             // add text box to each side when comparison box is first created
-            if (!(this.dataDoc[this.fieldKey + '_0'] || this.dataDoc[this.fieldKey + '_0'] === 'empty')) {
+            // (!this.dataDoc[this.fieldKey + '_0'] && this.dataDoc[this._props.fieldKey + '_0'] !== 'empty')
+            if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
                 const dataSplit = StrCast(this.dataDoc.data).split('Answer');
                 const newDoc = Docs.Create.TextDocument(dataSplit[1]);
                 // if there is text from the pdf ai cards, put the question on the front side.
@@ -468,7 +507,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 newDoc[DocData].text = dataSplit[1];
                 this.addDoc(newDoc, this.fieldKey + '_0');
             }
-            if (!(this.dataDoc[this.fieldKey + '_1'] || this.dataDoc[this.fieldKey + '_1'] === 'empty')) {
+            if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) {
                 const dataSplit = StrCast(this.dataDoc.data).split('Answer');
                 const newDoc = Docs.Create.TextDocument(dataSplit[0]);
                 // if there is text from the pdf ai cards, put the answer on the alternate side.
@@ -478,6 +517,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             }
 
             // render the QuizCards
+            console.log('GERE' + DocCast(this.Document.embedContainer).filterOp);
             if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).filterOp === 'quiz') {
                 const text = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
                 return (
@@ -489,6 +529,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                                 value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._outputValue : this._inputValue}
                                 onChange={this.handleInputChange}
                                 onScroll={e => e.stopPropagation()}
+                                placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
                                 readOnly={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate'}></textarea>
 
                             {this._loading ? (
@@ -516,7 +557,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 <div
                     className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
                     // onContextMenu={this.specificMenu}
-                    style={{ display: 'flex', flexDirection: 'column' }}
+                    style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
                     onMouseEnter={() => {
                         this.hoverFlip('alternate');
                     }}
@@ -531,8 +572,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             <ReactLoading type="spin" height={30} width={30} color={'blue'} />
                         </div>
                     ) : null}
-                    {this.flashcardMenu}
-                    {/* {this.overlayAlternateIcon} */}
+                    {this._props.isContentActive() ? this.flashcardMenu : null}
+                    {this.overlayAlternateIcon}
                 </div>
             );
         }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2f3357791..d4c31a5b3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -566,13 +566,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
 
             // creates menu for the user to select how to reveal the flashcards
-            if (this.Document._layout_isFlashcard) {
-                const revealOptions = cm.findByDescription('Reveal Options');
-                const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
-                revealItems.push({ description: 'Hover', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'; }, icon: 'hand-point-up' }); // prettier-ignore
-                revealItems.push({ description: 'Flip', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; }, icon: 'rotate' }); // prettier-ignore
-                !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
-            }
+            // if (this.Document._layout_isFlashcard) {
+            //     const revealOptions = cm.findByDescription('Reveal Options');
+            //     const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
+            //     revealItems.push({ description: 'Hover', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'; }, icon: 'hand-point-up' }); // prettier-ignore
+            //     revealItems.push({ description: 'Flip', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; }, icon: 'rotate' }); // prettier-ignore
+            //     !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+            // }
 
             if (this._props.bringToFront) {
                 const zorders = cm.findByDescription('ZOrder...');
-- 
cgit v1.2.3-70-g09d2


From 14296a5e2b9726d449a9ddf59a3af0a6945e3cb7 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 25 Sep 2024 13:37:36 -0400
Subject: updated carousel to show same filter as in context menu bar.

---
 src/client/util/CurrentUserUtils.ts                |  2 -
 src/client/views/StyleProvider.tsx                 |  2 +-
 .../views/collections/CollectionCarouselView.tsx   | 44 ++++++++++++++++++----
 src/client/views/nodes/DocumentView.tsx            |  1 +
 4 files changed, 39 insertions(+), 10 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 3e7921a08..03975a5e7 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -981,12 +981,10 @@ pie title Minerals in my tap water
         doc.fontColor ?? (doc.fontColor = "black");
         doc.fontHighlight ?? (doc.fontHighlight = "");
         doc.defaultAclPrivate ?? (doc.defaultAclPrivate = false);
-        doc.savedFilters ?? (doc.savedFilters = new List<Doc>());
         doc.userBackgroundColor ?? (doc.userBackgroundColor = Colors.DARK_GRAY);
         doc.userVariantColor ?? (doc.userVariantColor = Colors.MEDIUM_BLUE);
         doc.userColor ?? (doc.userColor = Colors.LIGHT_GRAY);
         doc.userTheme ?? (doc.userTheme = ColorScheme.Dark);
-        doc.filterDocCount = 0;
         doc.treeView_FreezeChildren = "remove|add";
         doc.activePage = doc.activeDashboard === undefined ? 'home': doc.activePage;
 
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3545afcee..16f6aa40b 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -318,7 +318,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         : childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || childFiltersByRanges?.().length
                         ? 'orange' // 'inheritsFilter'
                         : undefined;
-                return !showFilterIcon ? null : (
+                return !showFilterIcon || props?.hideFilterStatus ? null : (
                     <div className="styleProvider-filter">
                         <Dropdown
                             type={Type.TERT}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 43ab4462b..b6c3744e6 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -4,7 +4,7 @@ import { IReactionDisposer, action, computed, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
@@ -16,6 +16,7 @@ import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { TagItem } from '../TagsView';
+import { tickStep } from 'd3';
 
 enum cardMode {
     STAR = 'star',
@@ -180,9 +181,9 @@ export class CollectionCarouselView extends CollectionSubView() {
                 NativeHeight={returnZero}
                 fitWidth={undefined}
                 showTags={true}
+                hideFilterStatus={true}
                 containerViewPath={this.childContainerViewPath}
                 setContentViewBox={undefined}
-                ScreenToLocalTransform={this.childScreenToLocalXf}
                 onDoubleClickScript={this.onContentDoubleClick}
                 onClickScript={this.onContentClick}
                 isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
@@ -311,16 +312,45 @@ export class CollectionCarouselView extends CollectionSubView() {
     toggleFilterMode = () => Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true);
     setColor = (mode: practiceMode | cardMode, which: string) => (which === mode ? 'white' : 'light gray');
 
+    @computed get filterDoc() {
+        return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter');
+    }
+    filterHeight = () => NumCast(this.filterDoc?.height);
+    filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
     @computed get menu() {
         const curDoc = this.carouselItems?.[this.carouselIndex];
         return (
             <div className="carouselView-menu">
-                {/* NOTE: this should really show the same filter as in the workspace View menu, not just something hardwire for 'star' */}
-                <Tooltip title={Doc.hasDocFilter(this.Document, 'tags', this.starField) ? 'Show all cards' : 'Show only starred cards'}>
-                    <div key="back" className="carouselView-starFilter" onClick={e => this.toggleFilterMode()}>
-                        <FontAwesomeIcon icon="filter" color={Doc.hasDocFilter(this.Document, 'tags', this.starField) ? 'white' : 'lightgray'} size="1x" />
+                {!this.filterDoc ? (null) : (
+                    <div style={{ height: this.filterHeight(), width: this.filterHeight() }}>
+                        <DocumentView
+                            {...this._props}
+                            Document={this.filterDoc}
+                            TemplateDataDocument={undefined}
+                            LayoutTemplate={this._props.childLayoutTemplate}
+                            LayoutTemplateString={this._props.childLayoutString}
+                            renderDepth={this._props.renderDepth + 1}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
+                            fitWidth={undefined}
+                            showTags={false}
+                            hideFilterStatus={true}
+                            containerViewPath={this.childContainerViewPath}
+                            setContentViewBox={undefined}
+                            onDoubleClickScript={this.onContentDoubleClick}
+                            onClickScript={this.onContentClick}
+                            isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
+                            isContentActive={this.isChildContentActive}
+                            hideCaptions={true}
+                            childFilters={this.childDocFilters}
+                            hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
+                            addDocument={this._props.addDocument}
+                            ScreenToLocalTransform={this.contentScreentToLocalXf}
+                            PanelWidth={this.filterWidth}
+                            PanelHeight={this.filterHeight}
+                        />
                     </div>
-                </Tooltip>
+                )}
                 <div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}>
                     <Tooltip title="Practice flashcards using GPT">
                         <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 884220722..b85cb22bb 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -68,6 +68,7 @@ export interface DocumentViewProps extends FieldViewSharedProps {
     contentPointerEvents?: Property.PointerEvents | undefined; // pointer events allowed for content of a document view.  eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
     dontCenter?: 'x' | 'y' | 'xy';
     showTags?: boolean;
+    hideFilterStatus?: boolean;
     childHideDecorationTitle?: boolean;
     childHideResizeHandles?: boolean;
     childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar.
-- 
cgit v1.2.3-70-g09d2


From 11244a698bce594982ee5dca55b9695bb774451c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 7 Oct 2024 18:13:38 -0400
Subject: moved all quiz code out of LabelBox and ImageBox and into
 StyleProviderQuiz.  changed quizBoxes and quizMode to be stored as Doc
 metadata. Extended styles to cover contextMenuItems.   remove
 this.setListening() from comparisonBox until contextMenu selectedVal is
 fixed.

---
 src/client/views/StyleProp.ts            |   2 +
 src/client/views/StyleProvider.tsx       |  10 +-
 src/client/views/StyleProviderQuiz.scss  |  40 ++++
 src/client/views/StyleProviderQuiz.tsx   | 391 +++++++++++++++++++++++++++++++
 src/client/views/nodes/ComparisonBox.tsx |   1 -
 src/client/views/nodes/DocumentView.tsx  |   3 +
 src/client/views/nodes/FieldView.tsx     |   2 +
 src/client/views/nodes/ImageBox.scss     |  41 ----
 src/client/views/nodes/ImageBox.tsx      | 365 ++---------------------------
 src/client/views/nodes/LabelBox.tsx      |  52 +---
 10 files changed, 466 insertions(+), 441 deletions(-)
 create mode 100644 src/client/views/StyleProviderQuiz.scss
 create mode 100644 src/client/views/StyleProviderQuiz.tsx

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

diff --git a/src/client/views/StyleProp.ts b/src/client/views/StyleProp.ts
index dd5b98cfe..44d3bf757 100644
--- a/src/client/views/StyleProp.ts
+++ b/src/client/views/StyleProp.ts
@@ -21,4 +21,6 @@ export enum StyleProp {
     FontFamily = 'fontFamily', // font family of text
     FontWeight = 'fontWeight', // font weight of text
     Highlighting = 'highlighting', // border highlighting
+    ContextMenuItems = 'contextMenuItems', // menu items to add to context menu
+    AnchorMenuItems = 'anchorMenuItems',
 }
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 16f6aa40b..44bea57eb 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -21,6 +21,7 @@ import { TreeSort } from './collections/TreeSort';
 import { Colors } from './global/globalEnums';
 import { DocumentView, DocumentViewProps } from './nodes/DocumentView';
 import { FieldViewProps } from './nodes/FieldView';
+import { styleProviderQuiz } from './StyleProviderQuiz';
 import { StyleProp } from './StyleProp';
 import './StyleProvider.scss';
 import { TagsView } from './TagsView';
@@ -89,6 +90,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
         containerViewPath,
         childFilters,
         hideCaptions,
+        hideFilterStatus,
         showTitle,
         childFiltersByRanges,
         renderDepth,
@@ -110,6 +112,12 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
     const color = () => styleProvider?.(doc, props, StyleProp.Color) as string;
     const opacity = () => styleProvider?.(doc, props, StyleProp.Opacity);
     const layoutShowTitle = () => styleProvider?.(doc, props, StyleProp.ShowTitle) as string;
+
+    // bcz: For now, this is how to add custom-stylings (like a Quiz styling) for app-specific purposes.  The quiz styling will short-circuit
+    //      the regular stylings for items that it controls (eg., things with a quiz field, or images)
+    const quizProp = styleProviderQuiz.quizStyleProvider(doc, props, property);
+    if (quizProp !== undefined) return quizProp;
+
     // prettier-ignore
     switch (property.split(':')[0]) {
         case StyleProp.TreeViewIcon: {
@@ -318,7 +326,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         : childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || childFiltersByRanges?.().length
                         ? 'orange' // 'inheritsFilter'
                         : undefined;
-                return !showFilterIcon || props?.hideFilterStatus ? null : (
+                return !showFilterIcon || hideFilterStatus ? null : (
                     <div className="styleProvider-filter">
                         <Dropdown
                             type={Type.TERT}
diff --git a/src/client/views/StyleProviderQuiz.scss b/src/client/views/StyleProviderQuiz.scss
new file mode 100644
index 000000000..2f52c8dec
--- /dev/null
+++ b/src/client/views/StyleProviderQuiz.scss
@@ -0,0 +1,40 @@
+.loading-spinner {
+    position: absolute;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    width: 100%;
+    // left: 50%;
+    // top: 50%;
+    z-index: 200;
+    font-size: 20px;
+    font-weight: bold;
+    color: #17175e;
+}
+
+.check-icon {
+    position: absolute;
+    right: 40;
+    bottom: 10;
+    color: green;
+    display: inline-block;
+    font-size: 20px;
+    overflow: hidden;
+}
+
+.redo-icon {
+    position: absolute;
+    right: 10;
+    bottom: 10;
+    color: black;
+    display: inline-block;
+    font-size: 20px;
+    overflow: hidden;
+}
+
+@keyframes spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx
new file mode 100644
index 000000000..8973ada95
--- /dev/null
+++ b/src/client/views/StyleProviderQuiz.tsx
@@ -0,0 +1,391 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
+import axios from 'axios';
+import * as React from 'react';
+import { returnFalse, setupMoveUpEvents } from '../../ClientUtils';
+import { emptyFunction } from '../../Utils';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
+import { List } from '../../fields/List';
+import { NumCast, StrCast } from '../../fields/Types';
+import { GPTCallType, gptAPICall, gptImageLabel } from '../apis/gpt/GPT';
+import { Docs } from '../documents/Documents';
+import { ContextMenu } from './ContextMenu';
+import { ContextMenuProps } from './ContextMenuItem';
+import { StyleProp } from './StyleProp';
+import { AnchorMenu } from './pdf/AnchorMenu';
+import { DocumentViewProps } from './nodes/DocumentView';
+import { FieldViewProps } from './nodes/FieldView';
+import { ImageBox } from './nodes/ImageBox';
+import { ImageUtility } from './nodes/generativeFill/generativeFillUtils/ImageHandler';
+import './StyleProviderQuiz.scss';
+
+export namespace styleProviderQuiz {
+    enum quizMode {
+        SMART = 'smart',
+        NORMAL = 'normal',
+        NONE = 'none',
+    }
+
+    async function selectUrlToBase64(blob: Blob): Promise<string> {
+        try {
+            return new Promise((resolve, reject) => {
+                const reader = new FileReader();
+                reader.readAsDataURL(blob);
+                reader.onloadend = () => resolve(reader.result as string);
+                reader.onerror = error => reject(error);
+            });
+        } catch (error) {
+            console.error('Error:', error);
+            throw error;
+        }
+    }
+    /**
+     * Creates label boxes over text on the image to be filled in.
+     * @param boxes
+     * @param texts
+     */
+    async function createBoxes(img: ImageBox, boxes: [[[number, number]]], texts: [string]) {
+        img.Document._quizBoxes = new List<Doc>([]);
+        for (let i = 0; i < boxes.length; i++) {
+            const coords = boxes[i] ? boxes[i] : [];
+            const width = coords[1][0] - coords[0][0];
+            const height = coords[2][1] - coords[0][1];
+            const text = texts[i];
+
+            const newCol = Docs.Create.LabelDocument({
+                _width: width,
+                _height: height,
+                _layout_fitWidth: true,
+                title: '',
+            });
+            const scaling = 1 / (img._props.NativeDimScaling?.() || 1);
+            newCol.x = coords[0][0] + NumCast(img.marqueeref.current?.left) * scaling;
+            newCol.y = coords[0][1] + NumCast(img.marqueeref.current?.top) * scaling;
+
+            newCol.zIndex = 1000;
+            newCol.forceActive = true;
+            newCol.quiz = text;
+            newCol[DocData].textTransform = 'none';
+            Doc.AddDocToList(img.Document, '_quizBoxes', newCol);
+            img.addDocument(newCol);
+            //   img._loading = false;
+        }
+    }
+
+    /**
+     * Calls backend to find any text on an image. Gets the text and the
+     * coordinates of the text and creates label boxes at those locations.
+     * @param quiz
+     * @param i
+     */
+    async function pushInfo(imgBox: ImageBox, quiz: quizMode, i?: string) {
+        imgBox.Document._quizMode = quiz;
+        const quizBoxes = DocListCast(imgBox.Document.quizBoxes);
+        if (!quizBoxes.length) {
+            //    this._loading = true;
+
+            const img = {
+                file: i ? i : imgBox.paths[0],
+                drag: i ? 'drag' : 'full',
+                smart: quiz,
+            };
+            const response = await axios.post('http://localhost:105/labels/', img, {
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+            });
+            if (response.data['boxes'].length != 0) {
+                createBoxes(imgBox, response.data['boxes'], response.data['text']);
+            } else {
+                // this._loading = false;
+            }
+        } else quizBoxes.forEach(box => (box.hidden = false));
+    }
+
+    async function createCanvas(img: ImageBox) {
+        const canvas = document.createElement('canvas');
+        const scaling = 1 / (img._props.NativeDimScaling?.() || 1);
+        const w = AnchorMenu.Instance.marqueeWidth * scaling;
+        const h = AnchorMenu.Instance.marqueeHeight * scaling;
+        canvas.width = w;
+        canvas.height = h;
+        const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions
+        if (ctx) {
+            img.imageRef && ctx.drawImage(img.imageRef, NumCast(img.marqueeref.current?.left) * scaling, NumCast(img.marqueeref.current?.top) * scaling, w, h, 0, 0, w, h);
+        }
+        const blob = await ImageUtility.canvasToBlob(canvas);
+        return selectUrlToBase64(blob);
+    }
+    /**
+     * Create flashcards from an image.
+     */
+    async function getImageDesc(img: ImageBox) {
+        // this._loading = true;
+        try {
+            const hrefBase64 = await createCanvas(img);
+            const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this image with each question and answer labeled as "question" and "answer". Do not label each flashcard and do not include asterisks: ');
+            AnchorMenu.Instance.transferToFlashcard(response, NumCast(img.layoutDoc['x']), NumCast(img.layoutDoc['y']));
+        } catch (error) {
+            console.log('Error', error);
+        }
+        // this._loading = false;
+    }
+
+    /**
+     * Calls the createCanvas and pushInfo methods to convert the
+     * image to a form that can be passed to GPT and find the locations
+     * of the text.
+     */
+    async function makeLabels(img: ImageBox) {
+        try {
+            const hrefBase64 = await createCanvas(img);
+            pushInfo(img, quizMode.NORMAL, hrefBase64);
+        } catch (error) {
+            console.log('Error', error);
+        }
+    }
+
+    /**
+     * Determines whether two words should be considered
+     * the same, allowing minor typos.
+     * @param str1
+     * @param str2
+     * @returns
+     */
+    function levenshteinDistance(str1: string, str2: string) {
+        const len1 = str1.length;
+        const len2 = str2.length;
+        const dp = Array.from(Array(len1 + 1), () => Array(len2 + 1).fill(0));
+
+        if (len1 === 0) return len2;
+        if (len2 === 0) return len1;
+
+        for (let i = 0; i <= len1; i++) dp[i][0] = i;
+        for (let j = 0; j <= len2; j++) dp[0][j] = j;
+
+        for (let i = 1; i <= len1; i++) {
+            for (let j = 1; j <= len2; j++) {
+                const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
+                dp[i][j] = Math.min(
+                    dp[i - 1][j] + 1, // deletion
+                    dp[i][j - 1] + 1, // insertion
+                    dp[i - 1][j - 1] + cost // substitution
+                );
+            }
+        }
+
+        return dp[len1][len2];
+    }
+
+    /**
+     * Different algorithm for determining string similarity.
+     * @param str1
+     * @param str2
+     * @returns
+     */
+    function jaccardSimilarity(str1: string, str2: string) {
+        const set1 = new Set(str1.split(' '));
+        const set2 = new Set(str2.split(' '));
+
+        const intersection = new Set([...set1].filter(x => set2.has(x)));
+        const union = new Set([...set1, ...set2]);
+
+        return intersection.size / union.size;
+    }
+
+    /**
+     * Averages the jaccardSimilarity and levenshteinDistance scores
+     * to determine string similarity for the labelboxes answers and
+     * the users response.
+     * @param str1
+     * @param str2
+     * @returns
+     */
+    function stringSimilarity(str1: string, str2: string) {
+        const levenshteinDist = levenshteinDistance(str1, str2);
+        const levenshteinScore = 1 - levenshteinDist / Math.max(str1.length, str2.length);
+
+        const jaccardScore = jaccardSimilarity(str1, str2);
+
+        // Combine the scores with a higher weight on Jaccard similarity
+        return 0.5 * levenshteinScore + 0.5 * jaccardScore;
+    }
+    /**
+     * Returns whether two strings are similar
+     * @param input
+     * @param target
+     * @returns
+     */
+    function compareWords(input: string, target: string) {
+        const distance = stringSimilarity(input.toLowerCase(), target.toLowerCase());
+        return distance >= 0.7;
+    }
+
+    /**
+     * GPT returns a hex color for what color the label box should be based on
+     * the correctness of the users answer.
+     * @param inputString
+     * @returns
+     */
+    function extractHexAndSentences(inputString: string) {
+        // Regular expression to match a hexadecimal number at the beginning followed by a period and sentences
+        const regex = /^#([0-9A-Fa-f]+)\.\s*(.+)$/s;
+        const match = inputString.match(regex);
+
+        if (match) {
+            const hexNumber = match[1];
+            const sentences = match[2].trim();
+            return { hexNumber, sentences };
+        } else {
+            return { error: 'The input string does not match the expected format.' };
+        }
+    }
+    /**
+     * Check whether the contents of the label boxes on an image are correct.
+     */
+    function check(img: ImageBox) {
+        //this._loading = true;
+        img.quizBoxes.forEach(async doc => {
+            const input = StrCast(doc[DocData].title);
+            if (img.quizMode == quizMode.SMART && input) {
+                const questionText = 'Question: What was labeled in this image?';
+                const rubricText = ' Rubric: ' + StrCast(doc.quiz);
+                const queryText =
+                    questionText +
+                    ' UserAnswer: ' +
+                    input +
+                    '. ' +
+                    rubricText +
+                    '. One sentence and evaluate based on meaning, not wording. Provide a hex color at the beginning with a period after it on a scale of green (minor details missed) to red (big error) for how correct the answer is. Example: "#FFFFFF. Pasta is delicious."';
+                const response = await gptAPICall(queryText, GPTCallType.QUIZ);
+                const hexSent = extractHexAndSentences(response);
+                doc.quiz = hexSent.sentences?.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
+                doc.backgroundColor = '#' + hexSent.hexNumber;
+            } else {
+                const match = compareWords(input, StrCast(doc.quiz).trim());
+                doc.backgroundColor = match ? '#11c249' : '#eb2d2d';
+            }
+        });
+        //this._loading = false;
+    }
+
+    function redo(img: ImageBox) {
+        img.quizBoxes.forEach(doc => {
+            doc[DocData].title = '';
+            doc.backgroundColor = '#e4e4e4';
+        });
+    }
+
+    /**
+     * Get rid of all the label boxes on the images.
+     */
+    function exitQuizMode(img: ImageBox) {
+        img.Document._quizMode = quizMode.NONE;
+        DocListCast(img.Document._quizBoxes).forEach(box => {
+            box.hidden = true;
+        });
+    }
+
+    export function quizStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & DocumentViewProps>, property: string) {
+        const editLabelAnswer = (qdoc: Doc) => {
+            // when click the pencil, set the text to the quiz content. when click off, set the quiz text to that and set textbox to nothing.
+            if (!qdoc._editLabel) {
+                qdoc.title = StrCast(qdoc.quiz);
+            } else {
+                qdoc.quiz = StrCast(qdoc.title);
+                qdoc.title = '';
+            }
+            qdoc._editLabel = !qdoc._editLabel;
+        };
+        const editAnswer = (qdoc: Opt<Doc>) => {
+            return (
+                <Tooltip
+                    title={
+                        <div className="answer-tooltip" style={{ minWidth: '150px' }}>
+                            {qdoc?._editLabel ? 'save' : 'edit correct answer'}
+                        </div>
+                    }>
+                    <div className="answer-tool-tip" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => qdoc && editLabelAnswer(qdoc))}>
+                        <FontAwesomeIcon className="edit-icon" color={qdoc?._editLabel ? 'white' : 'black'} icon="pencil" size="sm" />
+                    </div>
+                </Tooltip>
+            );
+        };
+        const answerIcon = (qdoc: Opt<Doc>) => {
+            return (
+                <Tooltip
+                    title={
+                        <div className="answer-tooltip" style={{ minWidth: '150px' }}>
+                            {StrCast(qdoc?.quiz ?? '')}
+                        </div>
+                    }>
+                    <div className="answer-tool-tip">
+                        <FontAwesomeIcon className="q-icon" icon="circle" color="white" />
+                        <FontAwesomeIcon className="answer-icon" icon="question" />
+                    </div>
+                </Tooltip>
+            );
+        };
+        const checkIcon = (img: ImageBox) => (
+            <Tooltip title={<div className="dash-tooltip">Check</div>}>
+                <div className="check-icon" onPointerDown={() => check(img)}>
+                    <FontAwesomeIcon icon="circle-check" size="lg" />
+                </div>
+            </Tooltip>
+        );
+        const redoIcon = (img: ImageBox) => (
+            <Tooltip title={<div className="dash-tooltip">Redo</div>}>
+                <div className="redo-icon" onPointerDown={() => redo(img)}>
+                    <FontAwesomeIcon icon="redo-alt" size="lg" />
+                </div>
+            </Tooltip>
+        );
+
+        const imgBox = props?.DocumentView?.().ComponentView as ImageBox;
+        switch (property) {
+            case StyleProp.Decorations:
+                {
+                    if (doc?.quiz) {
+                        // this should only be set on Labels that are part of an image quiz
+                        return (
+                            <>
+                                {editAnswer(doc?.[DocData])}
+                                {answerIcon(doc)}
+                            </>
+                        );
+                    } else if (imgBox?.Document._quizMode && imgBox.Document._quizMode !== quizMode.NONE) {
+                        return (
+                            <>
+                                {checkIcon(imgBox)}
+                                {redoIcon(imgBox)}
+                            </>
+                        );
+                    }
+                }
+                break;
+            case StyleProp.ContextMenuItems:
+                if (imgBox) {
+                    const quizes: ContextMenuProps[] = [];
+                    quizes.push({
+                        description: 'Smart Check',
+                        event: doc?.quizMode == quizMode.NONE ? () => pushInfo(imgBox, quizMode.SMART) : () => exitQuizMode(imgBox),
+                        icon: 'pen-to-square',
+                    });
+                    quizes.push({
+                        description: 'Normal',
+                        event: doc?.quizMode == quizMode.NONE ? () => pushInfo(imgBox, quizMode.NORMAL) : () => exitQuizMode(imgBox),
+                        icon: 'pencil',
+                    });
+                    ContextMenu.Instance?.addItem({ description: 'Quiz Mode', subitems: quizes, icon: 'file-pen' });
+                }
+                break;
+            case StyleProp.AnchorMenuItems:
+                if (imgBox) {
+                    AnchorMenu.Instance.gptFlashcards = () => getImageDesc(imgBox);
+                    AnchorMenu.Instance.makeLabels = () => makeLabels(props?.DocumentView?.().ComponentView as ImageBox);
+                }
+        }
+        return undefined;
+    }
+}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 9fb8bc4d6..c32bbc803 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -44,7 +44,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
-        this.setListening();
     }
 
     @observable private _inputValue = '';
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5054432a9..04a31fd83 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -545,6 +545,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             return;
         }
 
+        const items = this._props.styleProvider?.(this.Document, this._props, StyleProp.ContextMenuItems) as ContextMenuProps[];
+        items?.forEach(item => ContextMenu.Instance.addItem(item));
+
         const customScripts = Cast(this.Document.contextMenuScripts, listSpec(ScriptField), []);
         StrListCast(this.Document.contextMenuLabels).forEach((label, i) =>
             cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' })
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index ca783d034..170966471 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -14,6 +14,7 @@ import { DocumentView } from './DocumentView';
 import { FocusViewOptions } from './FocusViewOptions';
 import { OpenWhere } from './OpenWhere';
 import { WebField } from '../../../fields/URLField';
+import { ContextMenuProps } from '../ContextMenuItem';
 
 export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>;
 export type StyleProviderFuncType = (
@@ -23,6 +24,7 @@ export type StyleProviderFuncType = (
     property: string
 ) =>
     | Opt<FieldType>
+    | ContextMenuProps[]
     | { clipPath: string; jsx: JSX.Element }
     | JSX.Element
     | JSX.IntrinsicElements
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 4d199b360..3ffda5a35 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -139,44 +139,3 @@
 .imageBox-fadeBlocker-hover {
     opacity: 0;
 }
-
-.loading-spinner {
-    position: absolute;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    height: 100%;
-    width: 100%;
-    // left: 50%;
-    // top: 50%;
-    z-index: 200;
-    font-size: 20px;
-    font-weight: bold;
-    color: #17175e;
-}
-
-.check-icon {
-    position: absolute;
-    right: 40;
-    bottom: 10;
-    color: green;
-    display: inline-block;
-    font-size: 20px;
-    overflow: hidden;
-}
-
-.redo-icon {
-    position: absolute;
-    right: 10;
-    bottom: 10;
-    color: black;
-    display: inline-block;
-    font-size: 20px;
-    overflow: hidden;
-}
-
-@keyframes spin {
-    to {
-        transform: rotate(360deg);
-    }
-}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 31f6df2ea..0b474076b 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -17,13 +17,11 @@ import { Cast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Type
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
-import { gptAPICall, GPTCallType, gptImageLabel } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
 import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
-import { dropActionType } from '../../util/DropActionTypes';
 import { SnappingManager } from '../../util/SnappingManager';
 import { undoable, undoBatch } from '../../util/UndoManager';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
@@ -38,16 +36,8 @@ import { StyleProp } from '../StyleProp';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
-import { ImageUtility } from './generativeFill/generativeFillUtils/ImageHandler';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
-// import stringSimilarity from 'string-similarity';
-
-enum quizMode {
-    SMART = 'smart',
-    NORMAL = 'normal',
-    NONE = 'none',
-}
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -79,25 +69,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
+    _ffref = React.createRef<CollectionFreeFormView>();
     private _ignoreScroll = false;
     private _forcedScroll = false;
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined;
     private _overlayIconRef = React.createRef<HTMLDivElement>();
-    private _marqueeref = React.createRef<MarqueeAnnotator>();
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
-    private _imageRef: HTMLImageElement | null = null; // <video> ref
-    @observable private _quizBoxes: Doc[] = [];
+    imageRef: HTMLImageElement | null = null; // <video> ref
+    marqueeref = React.createRef<MarqueeAnnotator>();
+
     @observable private _searchInput = '';
-    @observable private _quizMode = quizMode.NONE;
     @observable private _savedAnnotations = new ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>();
     @observable private _curSuffix = '';
     @observable private _error = '';
     @observable private _loading = false;
     @observable private _isHovering = false; // flag to switch between primary and alternate images on hover
-    _ffref = React.createRef<CollectionFreeFormView>();
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -110,6 +99,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document));
     };
 
+    @computed get quizBoxes() {
+        return DocListCast(this.Document.quizBoxes);
+    }
+    @computed get quizMode() {
+        return StrCast(this.Document._quizMode);
+    }
+
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const visibleAnchor = this._getAnchor?.(this._savedAnnotations, true); // use marquee anchor, otherwise, save zoom/pan as anchor
         const anchor =
@@ -315,331 +311,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return cropping;
     };
 
-    createCanvas = async () => {
-        const canvas = document.createElement('canvas');
-        const scaling = 1 / (this._props.NativeDimScaling?.() || 1);
-        const w = AnchorMenu.Instance.marqueeWidth * scaling;
-        const h = AnchorMenu.Instance.marqueeHeight * scaling;
-        canvas.width = w;
-        canvas.height = h;
-        const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions
-        if (ctx) {
-            this._imageRef && ctx.drawImage(this._imageRef, NumCast(this._marqueeref.current?.left) * scaling, NumCast(this._marqueeref.current?.top) * scaling, w, h, 0, 0, w, h);
-        }
-        const blob = await ImageUtility.canvasToBlob(canvas);
-        return ImageBox.selectUrlToBase64(blob);
-    };
-
-    createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
-        const url = !imagePath.startsWith('/') ? ClientUtils.CorsProxy(imagePath) : imagePath;
-        const width = NumCast(this.layoutDoc._width) || 1;
-        const height = NumCast(this.layoutDoc._height);
-        const imageSnapshot = Docs.Create.ImageDocument(url, {
-            _nativeWidth: Doc.NativeWidth(this.layoutDoc),
-            _nativeHeight: Doc.NativeHeight(this.layoutDoc),
-            x: NumCast(this.layoutDoc.x) + width,
-            y: NumCast(this.layoutDoc.y),
-            onClick: FollowLinkScript(),
-            _width: 150,
-            _height: (height / width) * 150,
-            title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
-        });
-        Doc.SetNativeWidth(imageSnapshot[DocData], Doc.NativeWidth(this.layoutDoc));
-        Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc));
-        this._props.addDocument?.(imageSnapshot);
-        DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' });
-        setTimeout(() => downX !== undefined && downY !== undefined && DocumentView.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true));
-    };
-
-    static selectUrlToBase64 = async (blob: Blob): Promise<string> => {
-        try {
-            return new Promise((resolve, reject) => {
-                const reader = new FileReader();
-                reader.readAsDataURL(blob);
-                reader.onloadend = () => resolve(reader.result as string);
-                reader.onerror = error => reject(error);
-            });
-        } catch (error) {
-            console.error('Error:', error);
-            throw error;
-        }
-    };
-
-    /**
-     * Calls backend to find any text on an image. Gets the text and the
-     * coordinates of the text and creates label boxes at those locations.
-     * @param quiz
-     * @param i
-     */
-    pushInfo = async (quiz: quizMode, i?: string) => {
-        this._quizMode = quiz;
-        this._loading = true;
-
-        const img = {
-            file: i ? i : this.paths[0],
-            drag: i ? 'drag' : 'full',
-            smart: quiz,
-        };
-        const response = await axios.post('http://localhost:105/labels/', img, {
-            headers: {
-                'Content-Type': 'application/json',
-            },
-        });
-        if (response.data['boxes'].length != 0) {
-            this.createBoxes(response.data['boxes'], response.data['text']);
-        } else {
-            this._loading = false;
-        }
-    };
-
-    /**
-     * Creates label boxes over text on the image to be filled in.
-     * @param boxes
-     * @param texts
-     */
-    createBoxes = (boxes: [[[number, number]]], texts: [string]) => {
-        for (let i = 0; i < boxes.length; i++) {
-            const coords = boxes[i] ? boxes[i] : [];
-            const width = coords[1][0] - coords[0][0];
-            const height = coords[2][1] - coords[0][1];
-            const text = texts[i];
-
-            const newCol = Docs.Create.LabelDocument({
-                _width: width,
-                _height: height,
-                _layout_fitWidth: true,
-                title: '',
-            });
-            const scaling = 1 / (this._props.NativeDimScaling?.() || 1);
-            newCol.x = coords[0][0] + NumCast(this._marqueeref.current?.left) * scaling;
-            newCol.y = coords[0][1] + NumCast(this._marqueeref.current?.top) * scaling;
-
-            newCol.zIndex = 1000;
-            newCol.forceActive = true;
-            newCol.quiz = text;
-            newCol.showQuiz = false;
-            newCol[DocData].textTransform = 'none';
-            this._quizBoxes.push(newCol);
-            this.addDocument(newCol);
-            this._loading = false;
-        }
-    };
-
-    /**
-     * Create flashcards from an image.
-     */
-    getImageDesc = async () => {
-        this._loading = true;
-        try {
-            const hrefBase64 = await this.createCanvas();
-            const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this image with each question and answer labeled as "question" and "answer". Do not label each flashcard and do not include asterisks: ');
-            AnchorMenu.Instance.transferToFlashcard(response, NumCast(this.layoutDoc['x']), NumCast(this.layoutDoc['y']));
-        } catch (error) {
-            console.log('Error', error);
-        }
-        this._loading = false;
-    };
-
-    /**
-     * Calls the createCanvas and pushInfo methods to convert the
-     * image to a form that can be passed to GPT and find the locations
-     * of the text.
-     */
-    makeLabels = async () => {
-        try {
-            const hrefBase64 = await this.createCanvas();
-            this.pushInfo(quizMode.NORMAL, hrefBase64);
-        } catch (error) {
-            console.log('Error', error);
-        }
-    };
-
-    /**
-     * Determines whether two words should be considered
-     * the same, allowing minor typos.
-     * @param str1
-     * @param str2
-     * @returns
-     */
-    levenshteinDistance = (str1: string, str2: string) => {
-        const len1 = str1.length;
-        const len2 = str2.length;
-        const dp = Array.from(Array(len1 + 1), () => Array(len2 + 1).fill(0));
-
-        if (len1 === 0) return len2;
-        if (len2 === 0) return len1;
-
-        for (let i = 0; i <= len1; i++) dp[i][0] = i;
-        for (let j = 0; j <= len2; j++) dp[0][j] = j;
-
-        for (let i = 1; i <= len1; i++) {
-            for (let j = 1; j <= len2; j++) {
-                const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
-                dp[i][j] = Math.min(
-                    dp[i - 1][j] + 1, // deletion
-                    dp[i][j - 1] + 1, // insertion
-                    dp[i - 1][j - 1] + cost // substitution
-                );
-            }
-        }
-
-        return dp[len1][len2];
-    };
-
-    /**
-     * Different algorithm for determining string similarity.
-     * @param str1
-     * @param str2
-     * @returns
-     */
-    jaccardSimilarity = (str1: string, str2: string) => {
-        const set1 = new Set(str1.split(' '));
-        const set2 = new Set(str2.split(' '));
-
-        const intersection = new Set([...set1].filter(x => set2.has(x)));
-        const union = new Set([...set1, ...set2]);
-
-        return intersection.size / union.size;
-    };
-
-    /**
-     * Averages the jaccardSimilarity and levenshteinDistance scores
-     * to determine string similarity for the labelboxes answers and
-     * the users response.
-     * @param str1
-     * @param str2
-     * @returns
-     */
-    stringSimilarity(str1: string, str2: string) {
-        const levenshteinDist = this.levenshteinDistance(str1, str2);
-        const levenshteinScore = 1 - levenshteinDist / Math.max(str1.length, str2.length);
-
-        const jaccardScore = this.jaccardSimilarity(str1, str2);
-
-        // Combine the scores with a higher weight on Jaccard similarity
-        return 0.5 * levenshteinScore + 0.5 * jaccardScore;
-    }
-
-    @computed get checkIcon() {
-        return (
-            <Tooltip title={<div className="dash-tooltip">Check</div>}>
-                <div className="check-icon" onPointerDown={this.check}>
-                    <FontAwesomeIcon icon="circle-check" size="lg" />
-                </div>
-            </Tooltip>
-        );
-    }
-
-    @computed get redoIcon() {
-        return (
-            <Tooltip title={<div className="dash-tooltip">Redo</div>}>
-                <div className="redo-icon" onPointerDown={this.redo}>
-                    <FontAwesomeIcon icon="redo-alt" size="lg" />
-                </div>
-            </Tooltip>
-        );
-    }
-
-    /**
-     * Returns whether two strings are similar
-     * @param input
-     * @param target
-     * @returns
-     */
-    compareWords = (input: string, target: string) => {
-        const distance = this.stringSimilarity(input.toLowerCase(), target.toLowerCase());
-        return distance >= 0.7;
-    };
-
-    /**
-     * GPT returns a hex color for what color the label box should be based on
-     * the correctness of the users answer.
-     * @param inputString
-     * @returns
-     */
-    extractHexAndSentences = (inputString: string) => {
-        // Regular expression to match a hexadecimal number at the beginning followed by a period and sentences
-        const regex = /^#([0-9A-Fa-f]+)\.\s*(.+)$/s;
-        const match = inputString.match(regex);
-
-        if (match) {
-            const hexNumber = match[1];
-            const sentences = match[2].trim();
-            return { hexNumber, sentences };
-        } else {
-            return { error: 'The input string does not match the expected format.' };
-        }
-    };
-
-    /**
-     * Check whether the contents of the label boxes on an image are correct.
-     */
-    check = () => {
-        this._loading = true;
-        this._quizBoxes.forEach(async doc => {
-            const input = StrCast(doc[DocData].title);
-            if (this._quizMode == quizMode.SMART && input) {
-                const questionText = 'Question: What was labeled in this image?';
-                const rubricText = ' Rubric: ' + StrCast(doc.quiz);
-                const queryText =
-                    questionText +
-                    ' UserAnswer: ' +
-                    input +
-                    '. ' +
-                    rubricText +
-                    '. One sentence and evaluate based on meaning, not wording. Provide a hex color at the beginning with a period after it on a scale of green (minor details missed) to red (big error) for how correct the answer is. Example: "#FFFFFF. Pasta is delicious."';
-                const response = await gptAPICall(queryText, GPTCallType.QUIZ);
-                const hexSent = this.extractHexAndSentences(response);
-                doc.quiz = hexSent.sentences?.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
-                doc.backgroundColor = '#' + hexSent.hexNumber;
-            } else {
-                const match = this.compareWords(input, StrCast(doc.quiz));
-                doc.backgroundColor = match ? '#11c249' : '#eb2d2d';
-            }
-            doc.showQuiz = true;
-        });
-        this._loading = false;
-    };
-
-    redo = () => {
-        this._quizBoxes.forEach(doc => {
-            doc[DocData].title = '';
-            doc.backgroundColor = '#e4e4e4';
-            doc.showQuiz = false;
-        });
-    };
-
-    /**
-     * Get rid of all the label boxes on the images.
-     */
-    exitQuizMode = () => {
-        this._quizMode = quizMode.NONE;
-        this._quizBoxes.forEach(doc => {
-            this.removeDocument?.(doc);
-        });
-        this._quizBoxes = [];
-    };
-
-    @action
-    setRef = (iref: HTMLImageElement | null) => {
-        this._imageRef = iref;
-    };
-
     specificContextMenu = (): void => {
         const field = Cast(this.dataDoc[this.fieldKey], ImageField);
         if (field) {
             const funcs: ContextMenuProps[] = [];
-            const quizes: ContextMenuProps[] = [];
-            quizes.push({
-                description: 'Smart Check',
-                event: this._quizMode == quizMode.NONE ? () => this.pushInfo(quizMode.SMART) : this.exitQuizMode,
-                icon: 'pen-to-square',
-            });
-            quizes.push({
-                description: 'Normal',
-                event: this._quizMode == quizMode.NONE ? () => this.pushInfo(quizMode.NORMAL) : this.exitQuizMode,
-                icon: 'pencil',
-            });
             funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
             funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
             funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
@@ -654,7 +329,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 }),
                 icon: 'pencil-alt',
             });
-            ContextMenu.Instance?.addItem({ description: 'Quiz Mode', subitems: quizes, icon: 'file-pen' });
             ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
         }
     };
@@ -770,7 +444,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 <div className="imageBox-fader" style={{ opacity: backAlpha }}>
                     <img
                         alt=""
-                        ref={this.setRef}
+                        ref={action((r: HTMLImageElement | null) => (this.imageRef = r))}
                         key="paths"
                         src={srcpath}
                         style={{ transform, transformOrigin }}
@@ -811,7 +485,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 e,
                 action(moveEv => {
                     MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
-                    this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]);
+                    this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]);
                     return true;
                 }),
                 returnFalse,
@@ -823,12 +497,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @action
     finishMarquee = () => {
         this._getAnchor = AnchorMenu.Instance?.GetAnchor;
-        AnchorMenu.Instance.gptFlashcards = this.getImageDesc;
+        this._props.styleProvider?.(this.Document, this._props, StyleProp.AnchorMenuItems);
         AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
-        AnchorMenu.Instance.makeLabels = this.makeLabels;
-        AnchorMenu.Instance.marqueeWidth = this._marqueeref.current?.Width ?? 0;
-        AnchorMenu.Instance.marqueeHeight = this._marqueeref.current?.Height ?? 0;
-        this._marqueeref.current?.onTerminateSelection();
+        AnchorMenu.Instance.marqueeWidth = this.marqueeref.current?.Width ?? 0;
+        AnchorMenu.Instance.marqueeHeight = this.marqueeref.current?.Height ?? 0;
+        this.marqueeref.current?.onTerminateSelection();
         this._props.select(false);
     };
     focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options));
@@ -898,7 +571,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : (
                     <MarqueeAnnotator
                         Document={this.Document}
-                        ref={this._marqueeref}
+                        ref={this.marqueeref}
                         scrollTop={0}
                         annotationLayerScrollTop={0}
                         scaling={returnOne}
@@ -915,8 +588,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         // anchorMenuFlashcard={() => this.getImageDesc()}
                     />
                 )}
-                {this._quizMode != quizMode.NONE ? this.checkIcon : null}
-                {this._quizMode != quizMode.NONE ? this.redoIcon : null}
             </div>
         );
     }
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 058932457..07c0a114a 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -1,12 +1,8 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@mui/material';
 import { Property } from 'csstype';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as textfit from 'textfit';
-import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
-import { emptyFunction } from '../../../Utils';
 import { Field, FieldType } from '../../../fields/Doc';
 import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
@@ -49,48 +45,6 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string;
     }
 
-    @computed get answerIcon() {
-        return (
-            <Tooltip
-                title={
-                    <div className="answer-tooltip" style={{ minWidth: '150px' }}>
-                        {StrCast(this.Document.quiz)}
-                    </div>
-                }>
-                <div className="answer-tool-tip">
-                    <FontAwesomeIcon className="q-icon" icon="circle" color="white" />
-                    <FontAwesomeIcon className="answer-icon" icon="question" />
-                </div>
-            </Tooltip>
-        );
-    }
-
-    @computed get editAnswer() {
-        return (
-            <Tooltip
-                title={
-                    <div className="answer-tooltip" style={{ minWidth: '150px' }}>
-                        {this._editLabel ? 'save' : 'edit correct answer'}
-                    </div>
-                }>
-                <div className="answer-tool-tip" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => this.editLabelAnswer())}>
-                    <FontAwesomeIcon className="edit-icon" color={this._editLabel ? 'white' : 'black'} icon="pencil" size="sm" />
-                </div>
-            </Tooltip>
-        );
-    }
-
-    editLabelAnswer = () => {
-        // when click the pencil, set the text to the quiz content. when click off, set the quiz text to that and set textbox to nothing.
-        if (!this._editLabel) {
-            this.dataDoc.title = StrCast(this.Document.quiz);
-        } else {
-            this.Document.quiz = this.Title;
-            this.dataDoc.title = '';
-        }
-        this._editLabel = !this._editLabel;
-    };
-
     componentDidMount() {
         this._props.setContentViewBox?.(this);
     }
@@ -98,8 +52,6 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this._timeout && clearTimeout(this._timeout);
     }
 
-    specificContextMenu = (): void => {};
-
     drop = (/* e: Event, de: DragManager.DropEvent */) => {
         return false;
     };
@@ -152,7 +104,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
         const label = this.Title.startsWith('#') ? null : this.Title;
         return (
-            <div key={label?.length} className="labelBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu} style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string }}>
+            <div key={label?.length} className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string }}>
                 <div
                     className="labelBox-mainButton"
                     style={{
@@ -203,8 +155,6 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         {label}
                     </div>
                 </div>
-                {this.Document.showQuiz ? this.answerIcon : null}
-                {this.Document.showQuiz ? this.editAnswer : null}
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From 66f2b03283a1e42c48b1c16b4344b730c0a2e9f3 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 11 Oct 2024 14:19:35 -0400
Subject: cleaned up comparisonBox a bit - fixed text doc placeholder text and
 fixed parsing keyword/question/answer from flashcard creator.  also fixed
 text boxes within in comparisonBox

---
 .../views/collections/FlashcardPracticeUI.tsx      |  17 +-
 src/client/views/nodes/ComparisonBox.tsx           | 308 ++++++++++-----------
 src/client/views/nodes/DocumentView.tsx            |   9 -
 .../views/nodes/formattedText/FormattedTextBox.tsx |   9 +
 4 files changed, 170 insertions(+), 173 deletions(-)

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

diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 7697d308b..45e040653 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -1,19 +1,19 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
+import { IconButton, MultiToggle, Type } from 'browndash-components';
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnFalse, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import './FlashcardPracticeUI.scss';
-import { IconButton, MultiToggle, Type } from 'browndash-components';
-import { SnappingManager } from '../../util/SnappingManager';
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { emptyFunction } from '../../../Utils';
 
 export enum practiceMode {
     PRACTICE = 'practice',
@@ -24,6 +24,11 @@ enum practiceVal {
     CORRECT = 'correct',
 }
 
+export enum flashcardRevealOp {
+    HOVER = 'hover',
+    FLIP = 'flip',
+}
+
 interface PracticeUIProps {
     fieldKey: string;
     layoutDoc: Doc;
@@ -154,11 +159,11 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
                     text={StrCast(this._props.layoutDoc.revealOp)}
                     color={SnappingManager.userColor}
                     background={SnappingManager.userVariantColor}
-                    icon={<FontAwesomeIcon color={SnappingManager.userColor} icon={this._props.layoutDoc.revealOp === 'hover' ? 'hand-point-up' : 'question'} size="sm" />}
+                    icon={<FontAwesomeIcon color={SnappingManager.userColor} icon={this._props.layoutDoc.revealOp === flashcardRevealOp.HOVER ? 'hand-point-up' : 'question'} size="sm" />}
                     label={StrCast(this._props.layoutDoc.revealOp)}
                     onPointerDown={e =>
                         setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => {
-                            this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === 'hover' ? 'flip' : 'hover';
+                            this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === flashcardRevealOp.HOVER ? flashcardRevealOp.FLIP : flashcardRevealOp.HOVER;
                             this._props.layoutDoc.childDocumentsActive = this._props.layoutDoc.revealOp === 'hover' ? true : undefined;
                         })
                     }
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index b2a717c3c..a57090e99 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -11,7 +11,7 @@ import { Doc, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { RichTextField } from '../../../fields/RichTextField';
-import { DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
+import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
 import { nullAudio } from '../../../fields/URLField';
 import { GPTCallType, gptAPICall, gptImageLabel } from '../../apis/gpt/GPT';
 import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
@@ -25,12 +25,12 @@ import { ContextMenu } from '../ContextMenu';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
+import { flashcardRevealOp, practiceMode } from '../collections/FlashcardPracticeUI';
 import '../pdf/GPTPopup/GPTPopup.scss';
 import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { practiceMode } from '../collections/FlashcardPracticeUI';
 
 const API_URL = 'https://api.unsplash.com/search/photos';
 
@@ -41,7 +41,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     }
     private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
     private _closeRef = React.createRef<HTMLDivElement>();
-    private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined];
+    private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {};
     private _reactDisposer: IReactionDisposer | undefined;
     constructor(props: FieldViewProps) {
         super(props);
@@ -55,9 +55,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @observable private _childActive = false;
     @observable private _animating = '';
     @observable private _listening = false;
-    @observable private _frontSide = false;
-    @observable recognition = new this.SpeechRecognition();
+    @observable private _renderSide = this.fieldKey;
+    @observable private _recognition = new this.SpeechRecognition();
 
+    @computed get isFlashcard()   { return BoolCast(this.Document.layout_isFlashcard); } // prettier-ignore
+    @computed get frontKey()      { return this._props.fieldKey; } // prettier-ignore
+    @computed get backKey()       { return this._props.fieldKey + '_back'; } // prettier-ignore
     @computed get revealOpKey()   { return `_${this._props.fieldKey}_revealOp`; } // prettier-ignore
     @computed get clipHeightKey() { return `_${this._props.fieldKey}_clipHeight`; } // prettier-ignore
     @computed get clipWidthKey()  { return `_${this._props.fieldKey}_clipWidth`;  } // prettier-ignore
@@ -65,6 +68,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @computed get clipHeight()    { return NumCast(this.layoutDoc[this.clipHeightKey], 200);  } // prettier-ignore
     @computed get revealOp()      { return StrCast(this.layoutDoc[this.revealOpKey], StrCast(this._props.docViewPath().slice(-2)[0]?.Document.revealOp)); } // prettier-ignore
     set revealOp(value:string)    { this.layoutDoc[this.revealOpKey] = value; } // prettier-ignore
+    @computed get loading()       { return this._loading; } // prettier-ignore
+    set loading(value)            { runInAction(() => { this._loading = value; })} // prettier-ignore
 
     @computed get overlayAlternateIcon() {
         return (
@@ -73,14 +78,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     className="comparisonBox-alternateButton ccomparisonBox-button"
                     onPointerDown={e =>
                         setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
-                            if (!this.revealOp || this.revealOp === 'flip') {
+                            if (!this.revealOp || this.revealOp === flashcardRevealOp.FLIP) {
                                 this.flipFlashcard();
                             }
                         })
                     }
                     style={{
-                        background: this.revealOp === 'hover' ? 'gray' : this._frontSide ? 'white' : 'black',
-                        color: this.revealOp === 'hover' ? 'black' : this._frontSide ? 'black' : 'white',
+                        background: this.revealOp === flashcardRevealOp.HOVER ? 'gray' : this._renderSide === this.backKey ? 'white' : 'black',
+                        color: this.revealOp === flashcardRevealOp.HOVER ? 'black' : this._renderSide === this.backKey ? 'black' : 'white',
                         display: 'inline-block',
                     }}>
                     <FontAwesomeIcon icon="turn-up" size="xl" />
@@ -106,18 +111,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @computed get flashcardMenu() {
         return SnappingManager.HideDecorations ? null : (
             <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaling})` }}>
-                {this.revealOp === 'hover' || !this._props.isSelected() ? null : this.overlayAlternateIcon}
+                {this.revealOp === flashcardRevealOp.HOVER || !this._props.isSelected() ? null : this.overlayAlternateIcon}
                 {!this._props.isSelected() ? null : (
                     <>
-                        {!this._frontSide ? null : (
+                        {this._renderSide === this.frontKey ? null : (
                             <Tooltip
                                 title={
-                                    <div className="dash-tooltip">{
-                                        !this._frontSide ? "Flip to front side to use GPT":
-                                                "Ask GPT to create an answer on the back side of the flashcard based on your question on the front"}
-                                   </div> // prettier-ignore
+                                    <div className="dash-tooltip">Ask GPT to create an answer for the question on the front</div> // prettier-ignore
                                 }>
-                                <div className="comparisonBox-button" onPointerDown={() => (this._frontSide ? this.askGPT(GPTCallType.CHATCARD) : null)}>
+                                <div className="comparisonBox-button" onPointerDown={() => this.askGPT(GPTCallType.CHATCARD)}>
                                     <FontAwesomeIcon icon="lightbulb" size="xl" />
                                 </div>
                             </Tooltip>
@@ -143,17 +145,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         this._childActive = true;
     };
 
-    @action handleRenderClick = () => {
-        this._frontSide = !this._frontSide;
-    };
-
     @action handleRenderGPTClick = () => {
         const phonTrans = DocCast(this.Document.audio) ? DocCast(this.Document.audio).phoneticTranscription : undefined;
         if (phonTrans) {
             this._inputValue = StrCast(phonTrans);
             this.askGPTPhonemes(this._inputValue);
         } else if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
-        this._frontSide = false;
+        this._renderSide = this.backKey;
         this._outputValue = '';
     };
 
@@ -169,7 +167,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         this._props.setContentViewBox?.(this);
         this._reactDisposer = reaction(
             () => this._props.isSelected(), // when this reaction should update
-            selected => !selected && (this._childActive = false) // what it should update to
+            selected => {
+                if (selected && this.isFlashcard) this.activateContent();
+                !selected && (this._childActive = false);
+            }, // what it should update to
+            { fireImmediately: true }
         );
     }
 
@@ -177,10 +179,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         this._reactDisposer?.();
     }
 
-    protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => {
-        this._disposers[disposerId]?.();
+    protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => {
+        this._disposers[fieldKey]?.();
         if (ele) {
-            this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc);
+            this._disposers[fieldKey] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc);
         }
     };
 
@@ -253,10 +255,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             default:                      return this._props.styleProvider?.(doc, props, property);
         } // prettier-ignore
     };
-    moveDoc1 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true);
-    moveDoc2 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true);
-    remDoc1 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true);
-    remDoc2 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
+    moveDocFront = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.frontKey), true);
+    moveDocBack = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.backKey), true);
+    remDocFront = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.frontKey), true);
+    remDocBack = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.backKey), true);
 
     registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => {
         if (e.button !== 2) {
@@ -268,8 +270,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 action((moveEv, doubleTap) => {
                     if (doubleTap) {
                         this._childActive = true;
-                        if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
-                        if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+                        if (!this.dataDoc[this.frontKey] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.frontKey] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+                        if (!this.dataDoc[this.backKey] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.backKey] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
                     }
                 }),
                 false,
@@ -296,26 +298,26 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      */
     setListening = () => {
         if (this.SpeechRecognition) {
-            this.recognition.continuous = true;
-            this.recognition.interimResults = true;
-            this.recognition.lang = 'en-US';
-            this.recognition.onresult = this.handleResult.bind(this);
+            this._recognition.continuous = true;
+            this._recognition.interimResults = true;
+            this._recognition.lang = 'en-US';
+            this._recognition.onresult = this.handleResult.bind(this);
         }
         ContextMenu.Instance.setLangIndex(0);
     };
 
     startListening = () => {
-        this.recognition.start();
+        this._recognition.start();
         this._listening = true;
     };
 
     stopListening = () => {
-        this.recognition.stop();
+        this._recognition.stop();
         this._listening = false;
     };
 
     setLanguage = (language: string, ind: number) => {
-        this.recognition.lang = language;
+        this._recognition.lang = language;
         ContextMenu.Instance.setLangIndex(ind);
     };
 
@@ -324,7 +326,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * @returns
      */
     convertAbr = () => {
-        switch (this.recognition.lang) {
+        switch (this._recognition.lang) {
             case 'en-US': return 'English'; //prettier-ignore
             case 'es-ES': return 'Spanish'; //prettier-ignore
             case 'fr-FR': return 'French'; //prettier-ignore
@@ -389,7 +391,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      */
     youtubeUpload = async () => {
         const audio = {
-            file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)),
+            file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text)),
         };
         const response = await axios.post('http://localhost:105/youtube/', audio, {
             headers: {
@@ -407,13 +409,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             _layout_autoHeight: true,
             _xMargin: 5,
             _yMargin: 5,
+            x: NumCast(this.layoutDoc.x),
+            y: NumCast(this.layoutDoc.y) + 50,
         });
-        newCol.x = this.layoutDoc.x;
-        newCol.y = NumCast(this.layoutDoc.y) + 50;
-        newCol.type_collection = CollectionViewType.Carousel as string;
-        for (let i = 0; i < collectionArr.length; i++) {
-            DocCast(collectionArr[i])[DocData].embedContainer = newCol;
-        }
 
         if (gpt) {
             this._props.DocumentView?.()._props.addDocument?.(newCol);
@@ -421,7 +419,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         } else {
             this._props.addDocument?.(newCol);
             this._props.removeDocument?.(this.Document);
-            this.Document.embedContainer = newCol;
+            Doc.SetContainer(this.Document, newCol);
         }
     }
 
@@ -450,65 +448,69 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * Transfers the content of flashcards into a flashcard pile.
      */
     gptFlashcardPile = async () => {
-        const text = await this.askGPT(GPTCallType.STACK);
-        const senArr = text?.split('Question: ') ?? [];
-        const collectionArr: Doc[] = [];
-        for (let i = 1; i < senArr.length; i++) {
-            const newDoc = Docs.Create.ComparisonDocument(senArr[i], { _layout_isFlashcard: true, _width: 300, _height: 300 });
-
-            if (senArr[i].includes('Keyword: ')) {
-                const question = StrCast(senArr![i]).split('Keyword: ');
-                const questionTxt = question[0].includes('Answer: ') ? question[0].split('Answer: ')[0] : question[0];
-                const answerTxt = question[0].includes('Answer: ') ? question[0].split('Answer: ')[1] : question[1];
-                this.fetchImages(question[1]).then(img => {
-                    newDoc[DocData][this.fieldKey + '_1'] = Docs.Create.TextDocument(this.textToRtf(questionTxt));
-                    newDoc[DocData][this.fieldKey + '_0'] = Docs.Create.TextDocument(this.textToRtf(answerTxt, img));
+        this.askGPT(GPTCallType.STACK).then(text => {
+            const [qtoken, ktoken, atoken] = ['Question: ', 'Keyword: ', 'Answer: '];
+            const collectionArr: Doc[] = [];
+            const promises = text
+                .split(qtoken)
+                .filter(t => t)
+                .map(tuple => {
+                    const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
+                    const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0];
+                    const rest = tuple.replace(question, '');
+                    // prettier-ignore
+                    const answer = rest.startsWith(ktoken) ?     // if keyword comes first, 
+                                        tuple.includes(atoken) ? tuple.split(atoken)[1] : "" : //if tuple includes answer, split at answer and take what's left, otherwise there's no answer
+                                    rest.includes(ktoken) ?      // otherwise if keyword is present it must come after answer, 
+                                        rest.split(ktoken)[0].split(atoken)[1] :  // split at keyword and take what comes first and split that at answer and take what's left
+                                        rest.replace(atoken,""); // finally if there's no keyword, just get rid of answer token and take what's left
+                    const keyword = rest.replace(atoken, '').replace(answer, '').replace(ktoken, '').trim();
+                    const fillInFlashcard = (img?: Doc) => {
+                        newDoc[DocData][this.frontKey] = this.textCreator('question', question, img);
+                        newDoc[DocData][this.backKey] = this.textCreator('answer', answer);
+                        collectionArr.push(newDoc);
+                    };
+                    return keyword && keyword !== 'none' ? this.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard();
                 });
-            }
-
-            collectionArr.push(newDoc);
-        }
-        this.createFlashcardPile(collectionArr, true);
+            Promise.all(promises).then(() => this.createFlashcardPile(collectionArr, true));
+        });
     };
 
     /**
      * Calls GPT for each flashcard type.
      */
-    askGPT = async (callType: GPTCallType): Promise<string | undefined> => {
-        const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
-        const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text);
+    askGPT = async (callType: GPTCallType) => {
+        const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text);
+        const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text);
         const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText;
-        this._loading = true;
+        this.loading = true;
+        let res = '';
 
-        if (callType == GPTCallType.CHATCARD) {
-            if (StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '') {
-                this._loading = false;
-                return;
-            }
-        }
-        try {
-            const res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType);
-            runInAction(() => {
+        if (callType !== GPTCallType.CHATCARD || StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text) !== '') {
+            try {
+                res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType);
                 if (!res) {
                     console.error('GPT call failed');
-                    return;
-                }
-                if (callType == GPTCallType.CHATCARD) {
-                    DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
-                } else if (callType == GPTCallType.QUIZ) {
-                    this._frontSide = true;
-                    this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
-                } else if (callType === GPTCallType.FLASHCARD) {
-                    this._loading = false;
-                    return res;
-                }
-                this._loading = false;
-            });
-            return res;
-        } catch (err) {
-            console.error('GPT call failed', err);
+                } else
+                    switch (callType) {
+                        case GPTCallType.CHATCARD:
+                            DocCast(this.dataDoc[this.backKey])[DocData].text = res;
+                            break;
+                        case GPTCallType.QUIZ:
+                            runInAction(() => {
+                                this._renderSide = this.frontKey;
+                                this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
+                            });
+                            break;
+                        case GPTCallType.FLASHCARD:
+                        default:
+                    }
+            } catch (err) {
+                console.error('GPT call failed', err);
+            }
         }
-        this._loading = false;
+        this.loading = false;
+        return res;
     };
     layoutWidth = () => NumCast(this.layoutDoc.width, 200);
     layoutHeight = () => NumCast(this.layoutDoc.height, 200);
@@ -517,11 +519,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         const c = this.DocumentView?.().ContentDiv?.getElementsByTagName('img');
         if (c?.length === 0) this.askGPT(GPTCallType.CHATCARD);
         if (c) {
-            this._loading = true;
+            this.loading = true;
             for (const i of c) {
                 if (i.className !== 'ProseMirror-separator') this.getImageDesc(i.src);
             }
-            this._loading = false;
+            this.loading = false;
         }
     };
 
@@ -531,7 +533,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * @param phonemes
      */
     askGPTPhonemes = async (phonemes: string) => {
-        const sentence = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
+        const sentence = StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text);
         const phon6 = 'huː ɑɹ juː tədeɪ';
         const phon4 = 'kamo estas hɔi';
         const promptEng =
@@ -557,17 +559,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             this.convertAbr() +
             ' speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". Do not make "θ" and "f" interchangable. Do not make "n" and "ɲ" interchangable. Do not make "e" and "i" interchangable. If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Provide a response like this: "Lets work on improving the pronunciation of "coffee." You said "cawffee," which is close, but we need to adjust the vowel sound. In American English, "coffee" is pronounced /ˈkɔːfi/, with a long "aw" sound. Try saying "kah-fee." Your intonation is good, but try putting a bit more stress on "like" in the sentence "I would like a coffee with milk." This will make your speech sound more natural. Keep practicing, and lets try saying the whole sentence again!"';
 
-        switch (this.recognition.lang) {
-            case 'en-US':
-                this._outputValue = await gptAPICall(promptEng, GPTCallType.PRONUNCIATION);
-                break;
-            case 'es-ES':
-                this._outputValue = await gptAPICall(promptSpa, GPTCallType.PRONUNCIATION);
-                break;
-            default:
-                this._outputValue = await gptAPICall(promptAll, GPTCallType.PRONUNCIATION);
-                break;
-        }
+        switch (this._recognition.lang) {
+            case 'en-US': this._outputValue = await gptAPICall(promptEng, GPTCallType.PRONUNCIATION); break;
+            case 'es-ES': this._outputValue = await gptAPICall(promptSpa, GPTCallType.PRONUNCIATION); break;
+            default:      this._outputValue = await gptAPICall(promptAll, GPTCallType.PRONUNCIATION); break;
+        } // prettier-ignore
     };
 
     /**
@@ -603,8 +599,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 _height: 150,
                 title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
             });
-            imageSnapshot.x = this.layoutDoc.x;
-            imageSnapshot.y = this.layoutDoc.y;
             return imageSnapshot;
         } catch (error) {
             console.log(error);
@@ -616,7 +610,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             const hrefBase64 = await imageUrlToBase64(u);
             const response = await gptImageLabel(hrefBase64, 'Answer the following question as a short flashcard response. Do not include a label.' + (this.dataDoc.text as RichTextField)?.Text);
 
-            DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = response;
+            DocCast(this.dataDoc[this.backKey])[DocData].text = response;
         } catch (error) {
             console.log('Error', error);
         }
@@ -624,11 +618,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
     @action
     flipFlashcard = () => {
-        this._frontSide = !this._frontSide;
+        this._renderSide = this._renderSide === this.frontKey ? this.backKey : this.frontKey;
     };
 
-    hoverFlip = (side: boolean) => {
-        if (this.revealOp === 'hover') this._frontSide = side;
+    @action
+    hoverFlip = (side: string) => {
+        if (this.revealOp === flashcardRevealOp.HOVER) this._renderSide = side;
     };
 
     testForTextFields = (whichSlot: string) => {
@@ -638,8 +633,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim();
         const layoutTemplateString =
             slotHasText ? FormattedTextBox.LayoutString(whichSlot):
-            whichSlot.endsWith('1') ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) :
-                                       altText !== undefined     ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore
+            whichSlot === this.frontKey ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) :
+                                          altText !== undefined     ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore
 
         // A bit hacky to try out the concept of using GPT to fill in flashcards
         //   If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string)
@@ -647,8 +642,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         // eg., this.text_alternate is
         //     "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))"
         //     where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field
-        // The GPT call will put the "answer"  in the second slot of the comparison (eg., text_2)
-        if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) {
+        // The GPT call will put the "answer"  in the second slot of the comparison (eg., text_0)
+        if (whichSlot === this.backKey && !layoutTemplateString?.includes(whichSlot)) {
             const queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in Doc.setField but it doesn't know about the fieldKey ...
             if (queryText?.match(/\(\(.*\)\)/)) {
                 Doc.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
@@ -656,7 +651,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         }
         return layoutTemplateString;
     };
-
+    textCreator = (title: string, text: string, img?: Doc) => {
+        const newDoc = Docs.Create.TextDocument(this.textToRtf(text, img), {
+            title, //
+            _layout_autoHeight: true,
+            _layout_centered: true,
+            text_align: 'center',
+            _layout_fitWidth: true,
+        });
+        return newDoc;
+    };
     childActiveFunc = () => this._childActive;
 
     contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
@@ -688,8 +692,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         LayoutTemplateString={layoutString}
                         Document={layoutString ? this.Document : targetDoc}
                         containerViewPath={this._props.docViewPath}
-                        moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
-                        removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
+                        moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack}
+                        removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
                         ScreenToLocalTransform={this.contentScreenToLocalXf}
@@ -701,7 +705,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         hideLinkButton
                         pointerEvents={this._childActive ? undefined : returnNone}
                     />
-                    {!this.Document._layout_isFlashcard ? clearButton(whichSlot) : null}
+                    {!this.isFlashcard ? clearButton(whichSlot) : null}
                 </>
             ) : (
                 <div className="placeholder">
@@ -709,67 +713,56 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 </div>
             );
         };
-        const displayBox = (which: string, index: number, cover: number) => (
+        const displayBox = (which: string, cover: number) => (
             <div
-                className={`${index === 0 ? 'before' : 'after'}Box-cont`}
+                className={`${which === this.frontKey ? 'before' : 'after'}Box-cont`}
                 key={which}
                 style={{ width: this._props.PanelWidth() }}
                 onPointerDown={e => {
                     this.registerSliding(e, cover);
-                    this.Document._layout_isFlashcard && this.activateContent();
+                    this.isFlashcard && this.activateContent();
                 }}
-                ref={ele => this.createDropTarget(ele, which, index)}>
+                ref={ele => this.createDropTarget(ele, which)}>
                 {!this._isEmpty ? displayDoc(which) : null}
             </div>
         );
 
-        if (this.Document._layout_isFlashcard) {
-            const side = this._frontSide ? 1 : 0;
+        if (this.isFlashcard) {
             const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
-            const textCreator = (which: number, title: string, text: string) => {
-                const newDoc = Docs.Create.TextDocument(this.textToRtf(text), {
-                    title, //
-                    _layout_autoHeight: true,
-                    _layout_centered: true,
-                    text_align: 'center',
-                    _layout_fitWidth: true,
-                });
-                this.addDoc(newDoc, this.fieldKey + '_' + which);
-                return newDoc;
-            };
 
             // add text box to each side when comparison box is first created
-            if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
-                textCreator(0, 'answer', dataSplit[1]);
+            if (!this.dataDoc[this.backKey] && !this._isEmpty) {
+                this.dataDoc[this.backKey] = this.textCreator('answer', dataSplit[1]);
             }
 
-            if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) {
-                const question = textCreator(1, 'question', dataSplit[0] || 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards');
-                Doc.SelectOnLoad = dataSplit[0] ? undefined : question;
+            if (!this.dataDoc[this.frontKey] && !this._isEmpty) {
+                const question = this.textCreator('question', dataSplit[0] || 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards');
+                this.dataDoc[this.frontKey] = question;
+                !dataSplit[0] && (question[DocData].text_placeholder = true);
             }
 
             if (DocCast(this.Document.embedContainer).practiceMode === practiceMode.QUIZ) {
-                const text = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
+                const text = StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text);
                 return (
                     <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`}>
                         <p style={{ color: 'white', padding: 10 }}>{text}</p>
                         <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p>
                         <div className="input-box">
                             <textarea
-                                value={this._frontSide ? this._outputValue : this._inputValue}
+                                value={this._renderSide ? this._outputValue : this._inputValue}
                                 onChange={this.handleInputChange}
                                 onScroll={e => {
                                     e.stopPropagation();
                                     e.preventDefault();
                                 }}
                                 placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
-                                readOnly={this._frontSide}></textarea>
+                                readOnly={this._renderSide === this.frontKey}></textarea>
 
-                            {this._loading ? (
+                            {!this.loading ? null : (
                                 <div className="loading-spinner">
                                     <ReactLoading type="spin" height={30} width={30} color={'blue'} />
                                 </div>
-                            ) : null}
+                            )}
                         </div>
                         <div>
                             <div className="submit-button">
@@ -785,8 +778,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                                 <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}>
                                     Evaluate Pronunciation
                                 </button>
-                                <button className="submit-buttonsubmit" type="button" onClick={this._frontSide ? this.handleRenderClick : this.handleRenderGPTClick}>
-                                    {this._frontSide ? 'Redo the Question' : 'Submit'}
+                                <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.frontKey ? this.flipFlashcard : this.handleRenderGPTClick}>
+                                    {this._renderSide === this.frontKey ? 'Redo the Question' : 'Submit'}
                                 </button>
                             </div>
                         </div>
@@ -798,11 +791,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             return (
                 <div
                     className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
-                    style={{ display: 'flex', flexDirection: 'column' }}
-                    onMouseEnter={() => this.hoverFlip(true)}
-                    onMouseLeave={() => this.hoverFlip(false)}>
-                    {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
-                    {this._loading ? (
+                    onMouseEnter={() => this.hoverFlip(this.backKey)}
+                    onMouseLeave={() => this.hoverFlip(this.frontKey)}>
+                    {displayBox(this._renderSide, this._props.PanelWidth() - 3)}
+                    {this.loading ? (
                         <div className="loading-spinner">
                             <ReactLoading type="spin" height={30} width={30} color={'blue'} />
                         </div>
@@ -814,9 +806,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         // render a comparison box that compares items side by side
         return (
             <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
-                {displayBox(`${this.fieldKey}_2`, 1, this._props.PanelWidth() - 3)}
+                {displayBox(this.backKey, this._props.PanelWidth() - 3)}
                 <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
-                    {displayBox(`${this.fieldKey}_1`, 0, 0)}
+                    {displayBox(this.frontKey, 0)}
                 </div>
 
                 <div
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index f7aba7542..c052a2823 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -576,15 +576,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
 
-            // creates menu for the user to select how to reveal the flashcards
-            // if (this.Document._layout_isFlashcard) {
-            //     const revealOptions = cm.findByDescription('Reveal Options');
-            //     const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
-            //     revealItems.push({ description: 'Hover', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'; }, icon: 'hand-point-up' }); // prettier-ignore
-            //     revealItems.push({ description: 'Flip', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; }, icon: 'rotate' }); // prettier-ignore
-            //     !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
-            // }
-
             if (this._props.bringToFront) {
                 const zorders = cm.findByDescription('ZOrder...');
                 const zorderItems = zorders?.subitems ?? [];
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index c57307974..bf19a2f82 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -360,6 +360,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
                         if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) {
                             textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
+                            textChange && (dataDoc[this.fieldKey + '_placeholder'] = undefined);
                             const numstring = NumCast(dataDoc[this.fieldKey], null);
                             dataDoc[this.fieldKey] =
                                 numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined;
@@ -1330,6 +1331,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         this._disposers.selected = reaction(
             () => this._props.rootSelected?.() || this._props.isContentActive(),
             action(selected => {
+                if (selected && this.dataDoc[this.fieldKey + '_placeholder']) {
+                    setTimeout(() => {
+                        selectAll(this._editorView!.state, (tx: Transaction) => {
+                            this._editorView?.dispatch(tx);
+                            this._editorView!.focus();
+                        });
+                    });
+                }
                 this.prepareForTyping();
                 if (FormattedTextBox._globalHighlights.has('Bold Text')) {
                     this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed
-- 
cgit v1.2.3-70-g09d2


From bb8fe2933154c6db70cfe5da1e890535bc9012d4 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 11 Oct 2024 20:21:33 -0400
Subject: fixed verticalalign of text boxes on load when existing text was
 there.  fixe scrolling of vertical align textboxes when fitwidth is set. 
 added flashcard contextmenu to comparisonbox and

---
 src/client/views/StyleProvider.tsx                 |   3 +-
 src/client/views/nodes/ComparisonBox.tsx           | 217 ++++++++++-----------
 src/client/views/nodes/DocumentView.tsx            |  21 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  18 +-
 4 files changed, 115 insertions(+), 144 deletions(-)

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

diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 02e0a34d8..8859f6464 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -54,7 +54,6 @@ export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: nu
 }
 
 export function border(doc: Doc, pw: number, ph: number, rad: number = 0, inset: number = 0) {
-    if (!rad) rad = 0;
     const width = pw * inset;
     const height = ph * inset;
 
@@ -218,7 +217,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
             const radiusRatio = borderRadius / docWidth;
             const radius = radiusRatio * ((2 * borderWidth) + docWidth);
 
-            const borderPath = doc && border(doc, NumCast(doc._width), NumCast(doc._height), radius, -ratio/2 ?? 0);
+            const borderPath = doc && border(doc, NumCast(doc._width), NumCast(doc._height), radius, -ratio/2);
                 return !borderPath
                     ? null
                     : {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 672008968..0582bc996 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -15,7 +15,7 @@ import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../f
 import { nullAudio } from '../../../fields/URLField';
 import { GPTCallType, gptAPICall, gptImageLabel } from '../../apis/gpt/GPT';
 import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
-import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
@@ -31,6 +31,7 @@ import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 
 const API_URL = 'https://api.unsplash.com/search/photos';
 
@@ -55,17 +56,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(ComparisonBox, fieldKey);
     }
+    private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+
     static qtoken = 'Question: ';
     static ktoken = 'Keyword: ';
     static atoken = 'Answer: ';
-    private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+    private _slideTiming = 200;
+    private _sideBtnWidth = 35;
     private _closeRef = React.createRef<HTMLDivElement>();
     private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {};
     private _reactDisposer: IReactionDisposer | undefined;
-    constructor(props: FieldViewProps) {
-        super(props);
-        makeObservable(this);
-    }
 
     @observable private _inputValue = '';
     @observable private _outputValue = '';
@@ -77,6 +77,47 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @observable private _renderSide = this.frontKey;
     @observable private _recognition = new this.SpeechRecognition();
 
+    constructor(props: FieldViewProps) {
+        super(props);
+        makeObservable(this);
+    }
+
+    componentDidMount() {
+        this._props.setContentViewBox?.(this);
+        this._reactDisposer = reaction(
+            () => this._props.isSelected(), // when this reaction should update
+            selected => {
+                if (selected && this.isFlashcard) this.activateContent();
+                !selected && (this._childActive = false);
+            }, // what it should update to
+            { fireImmediately: true }
+        );
+    }
+
+    componentWillUnmount() {
+        this._reactDisposer?.();
+    }
+
+    protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => {
+        this._disposers[fieldKey]?.();
+        if (ele) {
+            this._disposers[fieldKey] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc);
+        }
+    };
+
+    private internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
+        if (dropEvent.complete.docDragData) {
+            const { droppedDocuments } = dropEvent.complete.docDragData;
+            const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(toList(doc).lastElement(), fieldKey));
+            Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc);
+            !added && e.preventDefault();
+            e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
+            // this.childActive = false;
+            return added;
+        }
+        return undefined;
+    }, 'internal drop');
+
     @computed get isFlashcard()   { return BoolCast(this.Document.layout_isFlashcard); } // prettier-ignore
     @computed get frontKey()      { return this._props.fieldKey + '_front'; } // prettier-ignore
     @computed get backKey()       { return this._props.fieldKey + '_back'; } // prettier-ignore
@@ -112,8 +153,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             </Tooltip>
         );
     }
-
-    _sideBtnWidth = 35;
     /**
      * How much the content of the view is being scaled based on its nesting and its fit-to-width settings
      */
@@ -131,35 +170,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         return SnappingManager.HideDecorations ? null : (
             <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaling})` }}>
                 {this.revealOp === flashcardRevealOp.HOVER || !this._props.isSelected() ? null : this.overlayAlternateIcon}
-                {!this._props.isSelected() ? null : (
-                    <>
-                        {this._renderSide === this.frontKey ? null : (
-                            <Tooltip
-                                title={
-                                    <div className="dash-tooltip">Ask GPT to create an answer for the question on the front</div> // prettier-ignore
-                                }>
-                                <div className="comparisonBox-button" onPointerDown={() => this.askGPT(GPTCallType.CHATCARD)}>
-                                    <FontAwesomeIcon icon="lightbulb" size="xl" />
-                                </div>
-                            </Tooltip>
-                        )}
-                        {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform || this._renderSide === this.backKey ? null : (
-                            <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
-                                <div className="comparisonBox-button" onClick={() => this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck)}>
-                                    <FontAwesomeIcon icon="layer-group" size="xl" />
-                                </div>
-                            </Tooltip>
-                        )}
-                    </>
+                {!this._props.isSelected() || this._renderSide === this.frontKey ? null : (
+                    <Tooltip title={<div className="dash-tooltip">Ask GPT to create an answer for the question on the front</div>}>
+                        <div className="comparisonBox-button" onPointerDown={() => this.askGPT(GPTCallType.CHATCARD)}>
+                            <FontAwesomeIcon icon="lightbulb" size="xl" />
+                        </div>
+                    </Tooltip>
+                )}
+                {!this._props.isSelected() || this._renderSide === this.backKey || CollectionFreeFormView.from(this.DocumentView?.()) ? null : (
+                    <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
+                        <div className="comparisonBox-button" onClick={() => this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck)}>
+                            <FontAwesomeIcon icon="layer-group" size="xl" />
+                        </div>
+                    </Tooltip>
                 )}
             </div>
         );
     }
 
-    @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
-        this._inputValue = e.target.value;
-    };
-
     @action activateContent = () => {
         this._childActive = true;
     };
@@ -182,42 +210,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         return false;
     };
 
-    componentDidMount() {
-        this._props.setContentViewBox?.(this);
-        this._reactDisposer = reaction(
-            () => this._props.isSelected(), // when this reaction should update
-            selected => {
-                if (selected && this.isFlashcard) this.activateContent();
-                !selected && (this._childActive = false);
-            }, // what it should update to
-            { fireImmediately: true }
-        );
-    }
-
-    componentWillUnmount() {
-        this._reactDisposer?.();
-    }
-
-    protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => {
-        this._disposers[fieldKey]?.();
-        if (ele) {
-            this._disposers[fieldKey] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc);
-        }
-    };
-
-    private internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
-        if (dropEvent.complete.docDragData) {
-            const { droppedDocuments } = dropEvent.complete.docDragData;
-            const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(toList(doc).lastElement(), fieldKey));
-            Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc);
-            !added && e.preventDefault();
-            e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
-            // this.childActive = false;
-            return added;
-        }
-        return undefined;
-    }, 'internal drop');
-
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const anchor = Docs.Create.ConfigDocument({
             title: 'CompareAnchor:' + this.Document.title,
@@ -296,17 +288,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 false,
                 undefined,
                 action(() => {
-                    if (this._childActive) return;
-                    this._animating = 'all 200ms';
-                    // on click, animate slider movement to the targetWidth
-                    this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth();
-
-                    setTimeout(
-                        action(() => {
-                            this._animating = '';
-                        }),
-                        200
-                    );
+                    if (!this._childActive) {
+                        this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth
+                        this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth();
+                        setTimeout(  action(() => {this._animating = '';  }), this._slideTiming); // prettier-ignore
+                    }
                 })
             );
         }
@@ -381,17 +367,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * Gets the transcription of an audio recording by sending the
      * recording to backend.
      */
-    pushInfo = async () => {
-        const audio = {
-            file: DocCast(this.Document.audio)[DocData].url,
-        };
-        const response = await axios.post('http://localhost:105/recognize/', audio, {
-            headers: {
-                'Content-Type': 'application/json',
-            },
-        });
-        this.Document.phoneticTranscription = response.data.transcription;
-    };
+    pushInfo = () =>
+        axios
+            .post(
+                'http://localhost:105/recognize/', //
+                { file: DocCast(this.Document.audio)[DocData].url },
+                { headers: { 'Content-Type': 'application/json' } }
+            )
+            .then(response => {
+                this.Document.phoneticTranscription = response.data.transcription;
+            });
 
     /**
      * Extracts the id of the youtube video url.
@@ -408,17 +393,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * Gets the transcript of a youtube video by sending the video url to the backend.
      * @returns transcription of youtube recording
      */
-    youtubeUpload = async () => {
-        const audio = {
-            file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text)),
-        };
-        const response = await axios.post('http://localhost:105/youtube/', audio, {
-            headers: {
-                'Content-Type': 'application/json',
-            },
-        });
-        return response.data.transcription;
-    };
+    youtubeUpload = async () =>
+        axios
+            .post(
+                'http://localhost:105/youtube/', //
+                { file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text)) },
+                { headers: { 'Content-Type': 'application/json' } }
+            )
+            .then(response => response.data.transcription);
 
     /**
      * Creates a flashcard (or fills in flashcard data to a specified Doc) from a control string containing a question and answer
@@ -478,15 +460,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * Calls GPT for each flashcard type.
      */
     askGPT = async (callType: GPTCallType) => {
-        const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text);
-        const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text);
-        const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText;
+        const frontText = RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text;
+        const backText = RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text;
+        const questionText = 'Question: ' + frontText;
+        const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + backText : '');
         this.loading = true;
         let res = '';
 
-        if (callType !== GPTCallType.CHATCARD || StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text) !== '') {
+        if (callType !== GPTCallType.CHATCARD || frontText) {
             try {
-                res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType);
+                res = await gptAPICall(queryText, callType);
                 if (!res) {
                     console.error('GPT call failed');
                 } else
@@ -624,6 +607,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         if (this.revealOp === flashcardRevealOp.HOVER) this._renderSide = side;
     };
 
+    flashcardContextMenu = () => {
+        const appearance = ContextMenu.Instance.findByDescription('Appearance...');
+        const appearanceItems = appearance?.subitems ?? [];
+        if (this.Document._layout_isFlashcard) {
+            appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'id-card' });
+        }
+        !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
+    };
+
     testForTextFields = (whichSlot: string) => {
         const slotData = Doc.Get(this.dataDoc, whichSlot, true);
         const slotHasText = slotData instanceof RichTextField || typeof slotData === 'string';
@@ -752,17 +744,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         <div className="input-box">
                             <textarea
                                 value={this._renderSide === this.backKey ? this._outputValue : this._inputValue}
-                                onChange={this.handleInputChange}
-                                onScroll={e => {
-                                    e.stopPropagation();
-                                    e.preventDefault();
-                                }}
+                                onChange={action(e => {
+                                    this._inputValue = e.target.value;
+                                })}
                                 placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
                                 readOnly={this._renderSide === this.backKey}
                             />
                             {!this.loading ? null : (
                                 <div className="loading-spinner">
-                                    <ReactLoading type="spin" height={30} width={30} color={'blue'} />
+                                    <ReactLoading type="spin" height={30} width={30} color='blue' />
                                 </div>
                             )}
                         </div>
@@ -793,12 +783,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             return (
                 <div
                     className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
+                    onContextMenu={this.flashcardContextMenu}
                     onMouseEnter={() => this.hoverFlip(this.backKey)}
                     onMouseLeave={() => this.hoverFlip(this.frontKey)}>
                     {displayBox(this._renderSide, this._props.PanelWidth() - 3)}
                     {this.loading ? (
                         <div className="loading-spinner">
-                            <ReactLoading type="spin" height={30} width={30} color={'blue'} />
+                            <ReactLoading type="spin" height={30} width={30} color="blue" />
                         </div>
                     ) : null}
                     {this.flashcardMenu}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c052a2823..a343b9a39 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -16,12 +16,11 @@ import { List } from '../../../fields/List';
 import { PrefetchProxy } from '../../../fields/Proxy';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, ImageCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { AudioAnnoState } from '../../../server/SharedMediaTypes';
 import { DocServer } from '../../DocServer';
-import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
 import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
@@ -492,21 +491,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         input.click();
     };
 
-    askGPT = async (): Promise<string | undefined> => {
-        const queryText = RTFCast(DocCast(this.dataDoc[this.props.fieldKey + '_1']).text)?.Text;
-        try {
-            const res = await gptAPICall('Question: ' + StrCast(queryText), GPTCallType.CHATCARD);
-            if (!res) {
-                console.error('GPT call failed');
-                return;
-            }
-            DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
-            console.log(res);
-        } catch (err) {
-            console.error('GPT call failed', err);
-        }
-    };
-
     onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
         if (this._props.dontSelect?.()) return;
         if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) {
@@ -569,9 +553,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'map-pin' });
-            if (this.Document._layout_isFlashcard) {
-                appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(), icon: 'id-card' });
-            }
 
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 36f06aaf2..29be8d285 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1515,20 +1515,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 clipboardTextSerializer: this.clipboardTextSerializer,
                 handlePaste: this.handlePaste,
             });
-            const { state, dispatch } = this._editorView;
+            const { state } = this._editorView;
             if (!rtfField) {
                 const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
                 const startupText = Field.toString(dataDoc[fieldKey] as FieldType);
-                if (startupText) {
-                    dispatch(state.tr.insertText(startupText));
-                }
-                const textAlign = StrCast(this.dataDoc.text_align, StrCast(Doc.UserDoc().textAlign, 'left'));
-                if (textAlign && textAlign !== 'left') {
+                const textAlign = StrCast(this.dataDoc.text_align, StrCast(Doc.UserDoc().textAlign)) || 'left';
+                if (textAlign !== 'left') {
                     selectAll(this._editorView.state, tr => {
-                        this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign })));
+                        this._editorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign })));
                     });
-                    this.tryUpdateDoc(true);
                 }
+                if (startupText) {
+                    this._editorView?.dispatch(this._editorView.state.tr.insertText(startupText));
+                }
+                this.tryUpdateDoc(true);
             }
             this._editorView.TextView = this;
         }
@@ -2147,7 +2147,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         onScroll={this.onScroll}
                         onDrop={this.ondrop}>
                         <div
-                            className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered && this.scrollHeight < NumCast(this.layoutDoc._height) ? 'centered' : ''} ${this.layoutDoc.hCentering}`}
+                            className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered && this.scrollHeight <= (this._props.fitWidth?.(this.Document) ? this._props.PanelHeight() : NumCast(this.layoutDoc._height)) ? 'centered' : ''} ${this.layoutDoc.hCentering}`}
                             ref={this.createDropTarget}
                             style={{
                                 padding: StrCast(this.layoutDoc._textBoxPadding),
-- 
cgit v1.2.3-70-g09d2


From 63f9f9573dab9745d2f570e1917b325a474fdd93 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 28 Oct 2024 20:07:51 -0400
Subject: reworked LabelBox's to be fully editable.  gor rid of singleLine for
 label boxes. made a text_fitBox field to render text with a labelBox. fixed
 styleprovider in freeformview clusters to be untracked() to avoid
 invalidations.  added a default text color to Settings panel.   fixed
 clicking text buttons to not lose focus in labelBox.

---
 package-lock.json                                  |   8 +-
 package.json                                       |   2 +-
 src/client/documents/DocUtils.ts                   |   5 +-
 src/client/documents/Documents.ts                  |  14 +-
 src/client/util/CurrentUserUtils.ts                |   9 +-
 src/client/util/SettingsManager.tsx                |  24 +-
 src/client/views/MarqueeAnnotator.tsx              |   3 +-
 src/client/views/PinFuncs.ts                       |   4 +-
 src/client/views/SidebarAnnos.tsx                  |   4 +-
 src/client/views/StyleProvider.tsx                 |   4 +-
 src/client/views/StyleProviderQuiz.tsx             |   2 +-
 .../views/collections/CollectionCardDeckView.tsx   |   1 -
 .../CollectionFreeFormClusters.ts                  |   6 +-
 .../collections/collectionSchema/SchemaRowBox.tsx  |   2 +-
 src/client/views/global/globalScripts.ts           |  24 +-
 .../views/nodes/DataVizBox/DocCreatorMenu.tsx      |  35 +--
 src/client/views/nodes/DocumentView.tsx            |  12 +-
 src/client/views/nodes/EquationBox.tsx             |   2 +-
 .../views/nodes/FontIconBox/FontIconBox.scss       |   4 +
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |   4 +-
 src/client/views/nodes/LabelBox.scss               |   1 -
 src/client/views/nodes/LabelBox.tsx                | 168 +++++++++----
 .../views/nodes/formattedText/FormattedTextBox.tsx | 259 +++++++++++----------
 .../formattedText/ProsemirrorExampleTransfer.ts    |   4 +-
 .../views/nodes/formattedText/RichTextMenu.tsx     |  71 +++---
 .../views/nodes/formattedText/RichTextRules.ts     |   2 +-
 src/client/views/nodes/trails/PresBox.tsx          |  14 +-
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         |  23 +-
 src/fields/documentSchemas.ts                      |   7 +-
 29 files changed, 426 insertions(+), 292 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index 4e95fcee0..43dcb1c3f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -72,7 +72,7 @@
                 "body-parser": "^1.20.2",
                 "bootstrap": "^5.3.2",
                 "brotli": "^1.3.3",
-                "browndash-components": "^0.1.50",
+                "browndash-components": "0.1.53",
                 "browser-assert": "^1.2.1",
                 "bson": "^6.2.0",
                 "canvas": "^2.11.2",
@@ -13575,9 +13575,9 @@
             }
         },
         "node_modules/browndash-components": {
-            "version": "0.1.50",
-            "resolved": "https://registry.npmjs.org/browndash-components/-/browndash-components-0.1.50.tgz",
-            "integrity": "sha512-8EgU82os8/Tg3gayh+nYXCQZ8P7GGQWnibz0U2RM0sXTbc3BWTSYiC1Sj1VpL7HPkZ1KMGHzWynCo9QKIOWZBg==",
+            "version": "0.1.53",
+            "resolved": "https://registry.npmjs.org/browndash-components/-/browndash-components-0.1.53.tgz",
+            "integrity": "sha512-lBZlXe2u/6d9NlrR+fH+9ncCFGRUxGqJRejpfhbhVmQ4Q4uvP9LlmTuaVQElqys5wGUyK0SuqrLYPGhJthdQLQ==",
             "dependencies": {
                 "@emotion/react": "^11.11.1",
                 "@emotion/styled": "^11.11.0",
diff --git a/package.json b/package.json
index 06179fd7d..63d5e387d 100644
--- a/package.json
+++ b/package.json
@@ -155,7 +155,7 @@
         "body-parser": "^1.20.2",
         "bootstrap": "^5.3.2",
         "brotli": "^1.3.3",
-        "browndash-components": "^0.1.50",
+        "browndash-components": "0.1.53",
         "browser-assert": "^1.2.1",
         "bson": "^6.2.0",
         "canvas": "^2.11.2",
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 19f3c89ef..e3cb5e72c 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -679,10 +679,13 @@ export namespace DocUtils {
                 ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance
                 : {
                       _width: width || 200,
-                      _height: 35,
+                      _height: BoolCast(Doc.UserDoc().fitBox) ? 70 : 35,
                       _layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
                       _layout_fitWidth: true,
                       _layout_autoHeight: true,
+                      text_fitBox: BoolCast(Doc.UserDoc().fitBox),
+                      text_align: StrCast(Doc.UserDoc().textAlign),
+                      text_fontColor: StrCast(Doc.UserDoc().fontColor),
                   }),
         });
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 98e8bc932..b0e1a7545 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -294,7 +294,6 @@ export class DocumentOptions {
     _yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts', false);
     _xPadding?: NUMt = new NumInfo('x padding', false);
     _yPadding?: NUMt = new NumInfo('y padding', false);
-    _singleLine?: boolean; // whether label box is restricted to one line of text
     _createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents
     _columnWidth?: NUMt = new NumInfo('width of table column', false);
     _columnsHideIfEmpty?: BOOLt = new BoolInfo('whether stacking view column headings should be hidden');
@@ -302,10 +301,14 @@ export class DocumentOptions {
     _caption_yMargin?: NUMt = new NumInfo('y margin of caption inside of a carousel collection', false, true);
     icon_nativeWidth?: NUMt = new NumInfo('native width of icon view', false, true);
     icon_nativeHeight?: NUMt = new NumInfo('native height of icon view', false, true);
-    _text_fontSize?: string;
-    _text_fontFamily?: string;
-    _text_fontWeight?: string;
-    text_align?: STRt = new StrInfo('horizontal text alignment default');
+    text_fontSize?: string;
+    text_fontFamily?: string;
+    text_fontWeight?: string;
+    text_fitBox?: BOOLt = new BoolInfo("whether text box should be scaled to fit it's containing render box");
+    text_align?: STRt = new StrInfo('horizontal text alignment default', undefined, undefined, ['left', 'center', 'right']);
+    title_align?: STRt = new StrInfo('horizontal title alignment in label box', undefined, undefined, ['left', 'center', 'right']);
+    title_transform?: STRt = new StrInfo('transformation to apply to title in label box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']);
+    text_transform?: STRt = new StrInfo('transformation to apply to text in text box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']);
     text_placeholder?: BOOLt = new BoolInfo('makes the text act like a placeholder and automatically select when the text box is selected');
     fontSize?: string;
     _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views
@@ -496,7 +499,6 @@ export class DocumentOptions {
     sidebar_type_collection?: string; // collection type of text sidebar
 
     data_dashboards?: List<FieldType>; // list of dashboards used in shareddocs;
-    textTransform?: string;
     letterSpacing?: string;
     iconTemplate?: string; // name of icon template style
     icon_fieldKey?: string; // specifies the icon template to use (e.g., icon_fieldKey='george', then the icon template's name is icon_george; otherwise, the template's name would be icon_<type> where type is the Doc's type(pdf,rich text, etc))
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 30c75c659..b3aa5e145 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -186,7 +186,7 @@ export class CurrentUserUtils {
         const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts));
 
         const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({
-            layout: LabelBox.LayoutString(fieldKey), textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px",  _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
+            layout: LabelBox.LayoutString(fieldKey), letterSpacing: "unset", _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px",  _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
         });
         const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG,  _width: 360 / 4, _height: 270 / 4,  _layout_showTitle: "title", ...opts });
         const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts  });
@@ -268,7 +268,7 @@ export class CurrentUserUtils {
             MakeTemplate(Docs.Create.MultirowDocument(
                     [
                         Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200,  isSystem: true }),
-                        Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true,  _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) })
+                        Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true,  _height: 100, isSystem: true, text_fontFamily: StrCast(Doc.UserDoc().fontFamily), text_fontSize: StrCast(Doc.UserDoc().fontSize) })
                     ], {...opts, title: "Slide View Template"}));
         const plotlyApi = () => {
             let plotly = Doc.MyPublishedDocs.find(fdoc => fdoc.title === "@plotly");
@@ -392,7 +392,7 @@ pie title Minerals in my tap water
             {key: "Trail",      creator: Docs.Create.PresDocument,                          opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, _layout_dontCenter:'xy', dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }},
             {key: "Tab",        creator: opts => Docs.Create.FreeformDocument([], opts),    opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }},
             {key: "Slide",      creator: opts => Docs.Create.TreeDocument([], opts),        opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree, 
-                                                                                                treeView_HasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true, 
+                                                                                                treeView_HasOverlay: true, text_fontSize: "20px", _layout_autoHeight: true, 
                                                                                                 dropAction:dropActionType.move, treeView_Type: TreeViewType.outline, 
                                                                                                 backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true 
                                                                                             }, funcs: {title: 'this.text?.Text'}},
@@ -731,7 +731,7 @@ pie title Minerals in my tap water
         return [
             { title: "Font",     toolTip: "Font", width: 100,     btnType: ButtonType.DropdownList,                      toolType:"font",     ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'},
               btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
-            { title: "Font Size",toolTip: "Font size (%size)",    btnType: ButtonType.NumberDropdownButton,              toolType:"fontSize", ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 },
+            { title: "Font Size",toolTip: "Font size (%size)",    btnType: ButtonType.NumberDropdownButton,              toolType:"fontSize", ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 9 },
             { title: "Color",    toolTip: "Font color (%color)",  btnType: ButtonType.ColorButton,  icon: "font",        toolType:"fontColor",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
             { title: "Highlight",toolTip: "Font highlight",       btnType: ButtonType.ColorButton,  icon: "highlighter", toolType:"highlight",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
             { title: "Bold",     toolTip: "Bold (Ctrl+B)",        btnType: ButtonType.ToggleButton, icon: "bold",        toolType:"bold",     ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
@@ -746,6 +746,7 @@ pie title Minerals in my tap water
                 { title: "Center",   toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
                 { title: "Right",    toolTip: "Right align (Cmd-])",  btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             ]},
+            { title: "Fit Box",  toolTip: "Fit text to box",      btnType: ButtonType.ToggleButton, icon: "object-group",toolType:"fitBox",    ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             { title: "Elide",    toolTip: "Elide selection",      btnType: ButtonType.ToggleButton, icon: "eye",         toolType:"elide",     ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             { title: "Dictate",  toolTip: "Dictate",              btnType: ButtonType.ToggleButton, icon: "microphone",  toolType:"dictation", ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             { title: "NoLink",   toolTip: "Auto Link",            btnType: ButtonType.ToggleButton, icon: "link",        toolType:"noAutoLink", expertMode:true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 9200d68db..33d7d90a8 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components';
+import { Button, ColorPicker, Colors, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -45,7 +45,6 @@ export class SettingsManager extends React.Component<object> {
     public closeMgr = action(() => {
         this._isOpen = false;
     });
-    // eslint-disable-next-line react/no-unused-class-component-methods
     public openMgr = action(() => {
         this._isOpen = true;
     });
@@ -354,6 +353,27 @@ export class SettingsManager extends React.Component<object> {
                                     Doc.UserDoc().fontSize = val + 'px';
                                 }}
                             />
+                            <ColorPicker
+                                color={SettingsManager.userColor}
+                                type={Type.PRIM}
+                                defaultPickerType="Classic"
+                                selectedColor={StrCast(Doc.UserDoc().textBackgroundColor, Colors.LIGHT_GRAY)}
+                                background={SnappingManager.userBackgroundColor}
+                                icon={<FontAwesomeIcon icon="palette" size="lg" />}
+                                tooltip="default text background color"
+                                label="background"
+                                setSelectedColor={value => {
+                                    Doc.UserDoc().textBackgroundColor = value;
+                                    // if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
+                                    // this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+                                }}
+                                setFinalColor={value => {
+                                    Doc.UserDoc().textBackgroundColor = value;
+                                    // this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+                                    // this.colorBatch?.end();
+                                    // this.colorBatch = undefined;
+                                }}
+                            />
                             <Dropdown
                                 items={fontFamilies.map(val => ({
                                     text: val,
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 7266875c5..fa1123a2d 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -34,7 +34,7 @@ export interface MarqueeAnnotatorProps {
     getPageFromScroll?: (top: number) => number;
     finishMarquee: (x?: number, y?: number) => void;
     anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
-    anchorMenuFlashcard?: () => Promise<String>;
+    anchorMenuFlashcard?: () => Promise<string>;
     anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined;
     highlightDragSrcColor?: string;
 }
@@ -220,7 +220,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
                   e.preventDefault();
                   e.stopPropagation();
                   let cropRegion: Doc | undefined;
-                  // eslint-disable-next-line no-return-assign
                   const sourceAnchorCreator = () => (cropRegion = this.highlight('', true, undefined, true)); // hyperlink color
                   const targetCreator = (/* annotationOn: Doc | undefined */) => this.props.anchorMenuCrop!(cropRegion, false)!;
                   DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
diff --git a/src/client/views/PinFuncs.ts b/src/client/views/PinFuncs.ts
index bed473eec..ab02c2d07 100644
--- a/src/client/views/PinFuncs.ts
+++ b/src/client/views/PinFuncs.ts
@@ -1,4 +1,4 @@
-import { Doc, DocListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { Copy, Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
@@ -74,7 +74,7 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
         const fkey = Doc.LayoutFieldKey(targetDoc);
         if (pinProps.pinData.dataview) {
             pinDoc.config_usePath = targetDoc[fkey + '_usePath'];
-            pinDoc.config_data = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data;
+            pinDoc.config_data = Field.Copy(targetDoc[fkey]);
         }
         if (pinProps.pinData.dataannos) {
             const fieldKey = Doc.LayoutFieldKey(targetDoc);
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 1f3ad8444..87076bf65 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -78,8 +78,8 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
             _height: 50,
             _layout_fitWidth: true,
             _layout_autoHeight: true,
-            _text_fontSize: StrCast(Doc.UserDoc().fontSize),
-            _text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
+            text_fontSize: StrCast(Doc.UserDoc().fontSize),
+            text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
         });
         Doc.SetSelectOnLoad(target);
         FormattedTextBox.DontSelectInitialText = true;
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 8859f6464..12c032964 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -251,7 +251,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 case DocumentType.PRESELEMENT: docColor = docColor || ""; break;
                 case DocumentType.PRES:        docColor = docColor || 'transparent'; break;
                 case DocumentType.FONTICON:    docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY; break;
-                case DocumentType.RTF:         docColor = docColor || Colors.LIGHT_GRAY; break;
+                case DocumentType.RTF:         docColor = docColor || StrCast(Doc.UserDoc().textBackgroundColor, Colors.LIGHT_GRAY); break;
                 case DocumentType.LINK:        docColor = (isAnchor ? docColor : undefined); break;
                 case DocumentType.INK:         docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; break;
                 case DocumentType.EQUATION:    docColor = docColor || 'transparent'; break;
@@ -313,7 +313,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         ? undefined // if it's a background & has a cluster color, make the shadow spread really big
                         : fieldKey.includes('_inline')  // if doc is an inline document in a text box
                         ? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0vw 0vw 0.1vw')}`
-                        : DocCast(doc.embedContainer)?.type === DocumentType.RTF && !isInk() // if doc is embedded in a text document (but not an inline)
+                        :doc.rootDocument !== doc.embedContainer && DocCast(doc.embedContainer)?.type === DocumentType.RTF && !isInk() // if doc is embedded in a text document (but not an inline) and this isn't a simple text template (where the layoutDoc's rootDocument is its embed container)
                         ? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}`
                         : StrCast(doc.layout_boxShadow, '');
             }
diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx
index 1f2ad1485..f07a2c803 100644
--- a/src/client/views/StyleProviderQuiz.tsx
+++ b/src/client/views/StyleProviderQuiz.tsx
@@ -66,7 +66,7 @@ export namespace styleProviderQuiz {
             newCol.zIndex = 1000;
             newCol.forceActive = true;
             newCol.quiz = text;
-            newCol[DocData].textTransform = 'none';
+            newCol[DocData][Doc.LayoutFieldKey(newCol) + '_transform'] = 'none';
             Doc.AddDocToList(img.Document, '_quizBoxes', newCol);
             img.addDocument(newCol);
             //   img._loading = false;
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index beb45f326..0ba6b679c 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -620,7 +620,6 @@ export class CollectionCardView extends CollectionSubView() {
      * Actually renders all the cards
      */
     @computed get renderCards() {
-        console.log('CHILDPw = ' + this.childPanelWidth());
         // Map sorted documents to their rendered components
         return this.sortedDocs.map((doc, index) => {
             const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
index 6ad67a864..8530f7a23 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
@@ -1,4 +1,4 @@
-import { action, observable } from 'mobx';
+import { action, observable, untracked } from 'mobx';
 import { CollectionFreeFormView } from '.';
 import { intersectRect } from '../../../../Utils';
 import { Doc, Opt } from '../../../../fields/Doc';
@@ -179,7 +179,9 @@ export class CollectionFreeFormClusters {
     };
 
     styleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => {
-        if (doc && this.childDocs?.includes(doc))
+        // without untracked, every inquired style property for any Doc will be invalidated if a change is made to the collection's childDocs.
+        // this prevents that by assuming that a Doc is generally always (or never) a member of childDocs - if it's removed or added, then all of its properties get updated anyway.
+        if (doc && untracked(() => this.childDocs)?.includes(doc))
             switch (property.split(':')[0]) {
                 case StyleProp.BackgroundColor:
                     {
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 6ffb0865a..7f519b065 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -188,7 +188,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
                             selectedCells={this.selectedCells}
                             selectedCol={this.selectedCol}
                             setColumnValues={this.setColumnValues}
-                            oneLine={BoolCast(this.schemaDoc?._singleLine)}
+                            oneLine={BoolCast(this.schemaDoc?._schema_singleLine)}
                             menuTarget={this.schemaView.MenuTarget}
                             transform={() => {
                                 const ind = index === this.schemaView.columnKeys.length - 1 ? this.schemaView.columnKeys.length - 3 : index;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 954c79f7d..013b18a97 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -295,10 +295,10 @@ ScriptingGlobals.add(function setTagFilter(tag: string, added: boolean, checkRes
 }, '');
 
 // eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize' | 'alignment', value: string | number, checkResult?: boolean) {
+ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: string | number, checkResult?: boolean) {
     const editorView = RichTextMenu.Instance?.TextView?.EditorView;
     // prettier-ignore
-    const map: Map<'font'|'fontColor'|'highlight'|'fontSize'|'alignment', { checkResult: () => string | undefined; setDoc: () => void;}> = new Map([
+    const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => string | undefined; setDoc: () => void;}> = new Map([
         ['font', {
             checkResult: () => RichTextMenu.Instance?.fontFamily,
             setDoc: () => value && RichTextMenu.Instance?.setFontField(value.toString(), 'fontFamily'),
@@ -311,10 +311,6 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
             checkResult: () => RichTextMenu.Instance?.fontColor,
             setDoc: () => value && RichTextMenu.Instance?.setFontField(value.toString(), 'fontColor'),
         }],
-        ['alignment', {
-            checkResult: () => RichTextMenu.Instance?.textAlign,
-            setDoc: () => { value && editorView?.state ? RichTextMenu.Instance?.align(editorView, editorView.dispatch, value.toString() as "center"|"left"|"right"):(Doc.UserDoc().textAlign = value); },
-        }],
         ['fontSize', {
             checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''),
             setDoc: () => {
@@ -334,7 +330,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
     return undefined;
 });
 
-type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
+type attrname = 'noAutoLink' | 'dictation' | 'fitBox' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
 type attrfuncs = [attrname, { checkResult: () => boolean; toggle?: () => unknown }];
 
 // eslint-disable-next-line prefer-arrow-callback
@@ -343,14 +339,10 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
     const editorView = textView?.EditorView;
     // prettier-ignore
     const alignments:attrfuncs[] = (['left','right','center','vcent'] as ("left"|"center"|"right"|"vcent")[]).map((where) =>
-        [ where, {      checkResult: () => editorView ? (where === 'vcent' ?  RichTextMenu.Instance?.textVcenter ?? false:
-                                                                             (RichTextMenu.Instance?.textAlign === where)): 
-                                                         where === 'vcent' ?  BoolCast(Doc.UserDoc()._layout_centered):
-                                                                            (Doc.UserDoc().textAlign === where),
-                        toggle: () => { editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle():
-                                                                                 RichTextMenu.Instance?.align(editorView, editorView.dispatch, where)):
-                                                            where === 'vcent' ?  Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered:
-                                                                                (Doc.UserDoc().textAlign = where); }
+        [ where, {      checkResult: () => (where === 'vcent' ?  RichTextMenu.Instance?.textVcenter ?? false:
+                                                             (RichTextMenu.Instance?.textAlign === where)),
+                        toggle: () => { (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle():
+                                                            RichTextMenu.Instance?.align(editorView, editorView?.dispatch, where)); }
         }]); // prettier-ignore
     // prettier-ignore
     const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list =>
@@ -360,6 +352,8 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
     const attrs:attrfuncs[] = [
         ['dictation', { checkResult: () => !!textView?._recordingDictation,
                         toggle: () => textView && runInAction(() => { textView._recordingDictation = !textView._recordingDictation;} ) }],
+        ['fitBox',    { checkResult: () => RichTextMenu.Instance?.fitBox ?? false,
+                        toggle: () => RichTextMenu.Instance?.toggleFitBox()}],
         ['elide',      { checkResult: () => false,
                         toggle: () => editorView ? RichTextMenu.Instance?.elideSelection(): 0}],
         ['noAutoLink',{ checkResult: () => ((editorView && RichTextMenu.Instance?.noAutoLink) ?? false),
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
index 16c016d6c..7c601185e 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
@@ -245,7 +245,7 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
     }
 
     updateIcons = (docs: Doc[]) => {
-        console.log('called')
+        console.log('called');
         docs.map(this.getIcon);
     };
 
@@ -416,7 +416,7 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
                 const height = bottom - top;
                 const width = right - left;
                 const doc = !field.title.includes('$$')
-                    ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, _text_fontSize: `${height / 2}` })
+                    ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, text_fontSize: `${height / 2}` })
                     : Docs.Create.ImageDocument('', { _height: height, _width: width, title: field.title.replace(/\$\$/g, ''), x: left, y: top });
                 return doc;
             });
@@ -919,17 +919,17 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
     @action setExpandedView = (info: { icon: ImageField; doc: Doc } | undefined) => {
         if (info) {
             const doc = info.doc;
-            const wrapper: Doc = Docs.Create.FreeformDocument([info.doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''});
-            const newInfo = {icon: new ImageField(''), doc: wrapper}
+            const wrapper: Doc = Docs.Create.FreeformDocument([info.doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: '' });
+            const newInfo = { icon: new ImageField(''), doc: wrapper };
             this._expandedPreview = newInfo;
         } else {
             this._expandedPreview = info;
         }
     };
 
-    get editingWindow(){
+    get editingWindow() {
         const doc = this._expandedPreview?.doc ?? new Doc();
-        const rendered = 
+        const rendered = (
             <div className="docCreatorMenu-expanded-template-preview">
                 <CollectionFreeFormView
                     Document={this._expandedPreview!.doc}
@@ -945,7 +945,7 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
                     removeDocument={returnFalse}
                     PanelWidth={() => this._menuDimensions.width - 10}
                     PanelHeight={() => this._menuDimensions.height - 60}
-                    ScreenToLocalTransform={() => new Transform(-this._pageX,-this._pageY, 1)}
+                    ScreenToLocalTransform={() => new Transform(-this._pageX, -this._pageY, 1)}
                     renderDepth={5}
                     whenChildContentsActiveChanged={emptyFunction}
                     focus={emptyFunction}
@@ -961,14 +961,21 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
                     yPadding={0}
                 />
             </div>
-
+        );
 
         return (
             <div className="docCreatorMenu-expanded-template-preview">
-                <div className="top-panel"/>
+                <div className="top-panel" />
                 {rendered}
                 <div className="right-buttons-panel">
-                    <button className="docCreatorMenu-menu-button section-reveal-options top-right" onPointerDown={e => this.setUpButtonClick(e, () => {this._expandedPreview && this.updateIcons(this._suggestedTemplates.slice()); this.setExpandedView(undefined)})}>
+                    <button
+                        className="docCreatorMenu-menu-button section-reveal-options top-right"
+                        onPointerDown={e =>
+                            this.setUpButtonClick(e, () => {
+                                this._expandedPreview && this.updateIcons(this._suggestedTemplates.slice());
+                                this.setExpandedView(undefined);
+                            })
+                        }>
                         <FontAwesomeIcon icon="minimize" />
                     </button>
                     <button className="docCreatorMenu-menu-button section-reveal-options top-right-lower" onPointerDown={e => this.setUpButtonClick(e, () => this._expandedPreview && this._templateDocs.push(this._expandedPreview.doc))}>
@@ -976,7 +983,6 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
                     </button>
                 </div>
             </div>
-
         );
     }
 
@@ -2260,11 +2266,11 @@ export class FieldUtils {
             title: title,
             x: coord.x,
             y: coord.y,
-            _text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`,
+            text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`,
             backgroundColor: opts.backgroundColor ?? '',
             text_fontColor: opts.color,
             contentBold: opts.fontBold,
-            textTransform: opts.fontTransform,
+            text_transform: opts.fontTransform,
             color: opts.color,
             _layout_borderRounding: `${opts.cornerRounding ?? 0}px`,
             borderColor: opts.borderColor,
@@ -2306,7 +2312,7 @@ export class FieldUtils {
     public static CarouselField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, fields: Doc[]) => {
         const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight);
 
-        const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, _text_fontSize: `${height / 2}` });
+        const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, text_fontSize: `${height / 2}` });
 
         return doc;
     };
@@ -2359,4 +2365,3 @@ export class FieldUtils {
 //         }]
 //     };
 //  }
-
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a343b9a39..eb7f333b9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -880,7 +880,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                           background: this.backgroundBoxColor,
                           opacity: this.opacity,
                           cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
-                          color: StrCast(this.layoutDoc.color, 'inherit'),
+                          color: StrCast(this.Document._color, 'inherit'),
                           fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'),
                           fontSize: Cast(this.Document._text_fontSize, 'string', null),
                           transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
@@ -981,12 +981,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             type: SpringType.GENTLE,
             ...springMappings.gentle,
         };
-        switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
-            case PresEffect.Expand:     return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Expand} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
-            case PresEffect.Flip:       return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Flip}   springSettings={timingConfig}>{renderDoc}</SpringAnimation>
-            case PresEffect.Rotate:     return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Rotate} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
-            case PresEffect.Bounce:     return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Bounce} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
-            case PresEffect.Roll:       return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Roll}   springSettings={timingConfig}>{renderDoc}</SpringAnimation>
+        const presEffect = StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect));
+        switch (presEffect) {
+            case PresEffect.Expand: case PresEffect.Flip: case PresEffect.Rotate: case PresEffect.Bounce:    
+            case PresEffect.Roll:       return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={presEffect}   springSettings={timingConfig}>{renderDoc}</SpringAnimation>
             // case PresEffect.Fade:    return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Fade} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect>
             case PresEffect.Fade:       return <Fade {...effectProps}>{renderDoc}</Fade>
             // keep as preset, doesn't really make sense with spring config
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index fefe25764..290c90d6e 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -122,7 +122,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     width: 'fit-content', // `${100 / scale}%`,
                     height: `${100 / scale}%`,
                     pointerEvents: !this._props.isSelected() ? 'none' : undefined,
-                    fontSize: StrCast(this.layoutDoc._text_fontSize),
+                    fontSize: StrCast(this.Document._text_fontSize),
                 }}
                 onKeyDown={e => e.stopPropagation()}>
                 <EquationEditor ref={this._ref} value={StrCast(this.dataDoc.text, 'x')} spaceBehavesLikeTab onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" />
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index 2db285910..ab03a2318 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -10,6 +10,10 @@
         height: 3px !important;
     }
 }
+.fonticonbox {
+    margin: auto;
+    width: 100%;
+}
 .menuButton {
     height: 100%;
     display: flex;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index d4898eb3c..b45774a75 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -262,11 +262,13 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
 
         const script = ScriptCast(this.Document.onClick)?.script;
         const toggleStatus = script?.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean;
+
         // Colors
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
         const background = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string;
         const items = DocListCast(this.dataDoc.data);
         const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType));
+
         return (
             <MultiToggle
                 tooltip={`Toggle ${tooltip}`}
@@ -392,7 +394,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
 
     render() {
         return (
-            <div style={{ margin: 'auto', width: '100%' }} onContextMenu={this.specificContextMenu}>
+            <div className="fonticonbox" onContextMenu={this.specificContextMenu}>
                 {this.renderButton()}
             </div>
         );
diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss
index ca4b3d467..889cdc0ca 100644
--- a/src/client/views/nodes/LabelBox.scss
+++ b/src/client/views/nodes/LabelBox.scss
@@ -13,7 +13,6 @@
     height: 100%;
     border-radius: inherit;
     //letter-spacing: 2px; // bcz: doesn't work with LabelBigText
-    text-transform: uppercase;
     overflow: hidden;
     display: inline-block;
     margin: auto;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 94a9541f2..dcf9e1fed 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -1,19 +1,22 @@
 import { Property } from 'csstype';
-import { action, computed, makeObservable } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as textfit from 'textfit';
-import { Field, FieldType } from '../../../fields/Doc';
-import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { Doc, Field } from '../../../fields/Doc';
+import { NumCast, StrCast } from '../../../fields/Types';
 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 { ViewBoxBaseComponent } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
 import { FieldView, FieldViewProps } from './FieldView';
 import './LabelBox.scss';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { RichTextMenu } from './formattedText/RichTextMenu';
 
 @observer
 export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -22,7 +25,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
     private dropDisposer?: DragManager.DragDropDisposer;
     private _timeout: NodeJS.Timeout | undefined;
-    _divRef: HTMLDivElement | null = null;
+    private _divRef: HTMLDivElement | null = null;
+    private _reaction: IReactionDisposer | undefined;
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -36,21 +40,29 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     };
 
-    @computed get Title() {
-        return Field.toString(this.dataDoc[this.fieldKey] as FieldType) || StrCast(this.Document.title);
-    }
-
-    @computed get backgroundColor() {
-        return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string;
-    }
-
     componentDidMount() {
         this._props.setContentViewBox?.(this);
+        this._reaction = reaction(
+            () => this.Title,
+            () => document.activeElement !== this._divRef && this._forceRerender++
+        );
     }
     componentWillUnMount() {
         this._timeout && clearTimeout(this._timeout);
+        this.setText(this._divRef?.innerText ?? '');
+        this._reaction?.();
     }
 
+    @observable _forceRerender = 0;
+
+    @computed get Title()           { return Field.toString(this.dataDoc[this.fieldKey]); } // prettier-ignore
+    @computed get backgroundColor() { return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; } // prettier-ignore
+    @computed get boxShadow()       { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string; } // prettier-ignore
+
+    setText = undoable((text: string) => {
+        this.dataDoc[this.fieldKey] = text;
+    }, 'set label text');
+
     drop = (/* e: Event, de: DragManager.DropEvent */) => {
         return false;
     };
@@ -82,10 +94,11 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const textfitParams = {
             minFontSize: NumCast(this.layoutDoc._label_minFontSize, 1),
             maxFontSize: NumCast(this.layoutDoc._label_maxFontSize, 100),
-            multiLine: BoolCast(this.layoutDoc._singleLine, true) ? false : true,
-            alignHoriz: true,
+            multiLine: r?.textContent?.includes('\n') ? true : false,
+            // hack because tetFit doesn't support align 'right', but we need mobx to invalidate, so treat null as false and set to right inline
+            alignHoriz: StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'center' ? true : StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'right' ? (null as unknown as boolean) : false,
             alignVert: true,
-            detectMultiLine: true,
+            detectMultiLine: false,
         };
         if (r) {
             if (!r.offsetHeight || !r.offsetWidth) {
@@ -94,65 +107,140 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 this._timeout = setTimeout(() => this.fitTextToBox(r));
                 return textfitParams;
             }
+            r.style.whiteSpace = ''; // textfit sets to nowrap if not multiline, but doesn't reeset if it becomes multiline
+            r.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']); // textfit doesn't reset textAlign if it has been set to center, so we just set it to what we want
+            r.firstChild instanceof HTMLElement && (r.firstChild.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']));
             textfit(r, textfitParams);
         }
         return textfitParams;
     };
+    resetCursor = (cranchor?: number) => {
+        if (this._divRef && (cranchor || this._divRef === document.activeElement)) {
+            const range = document.createRange();
+            const anchor = cranchor ?? this._divRef.childNodes.length;
+            const container = cranchor === undefined ? this._divRef : (this._divRef.firstChild?.firstChild ?? this._divRef);
+            range.setStart(container, anchor);
+            range.setEnd(container, anchor);
+            const sel = window.getSelection();
+            sel?.removeAllRanges();
+            sel?.addRange(range);
+        }
+    };
+
+    beforeInput = action((event: InputEvent) => {
+        const spanChild = this._divRef?.firstChild?.firstChild;
+        if (spanChild?.nodeName === '#text' && ['insertLineBreak', 'insertParagraph'].includes(event.inputType)) {
+            event.preventDefault();
+            event.stopPropagation();
+
+            const selection = document.getSelection();
+            if (selection && document.activeElement === event.target) {
+                const text = spanChild.textContent ?? '';
+                const cranchor = selection.anchorNode === this._divRef ? (selection.anchorOffset ? text.length : 0) : selection.anchorOffset;
+                const addReturnHack = text.length <= cranchor && text[text.length - 1] !== '\n' ? '\n\n' : '\n'; // not sure why, but need to add a second carriage return if typing enter at the end of the text
+                const splitText = text.substring(0, cranchor) + addReturnHack + text.substring(cranchor);
+                spanChild.textContent = splitText;
+                this.resetCursor(cranchor + addReturnHack.length);
+            }
+            // const span = document.createElement('span');
+            // span.innerHTML = '&#8203;';
+            // this._divRef!.append(span);
+        }
+    });
+    // .labelBox-mainButton > div > span:nth-child(2) {
+
+    /**
+     * When an IconButton is clicked, it will receive focus.  However, we don't want that since we want or need that since we really want
+     * to maintain focus in the label's editing div (and cursor position).  so this relies on IconButton's having a tabindex set to -1 so that
+     * we can march up the tree from the 'relatedTarget' to determine if the loss of focus was caused by a fonticonbox.   If it is, we then
+     * restore focus
+     * @param e focusout event on the editing div
+     */
+    keepFocus = (e: FocusEvent) => {
+        if (e.relatedTarget instanceof HTMLElement && e.relatedTarget.tabIndex === -1) {
+            for (let ele: HTMLElement | null = e.relatedTarget; ele; ele = (ele as HTMLElement)?.parentElement) {
+                if ((ele as HTMLElement)?.className === 'fonticonbox') {
+                    setTimeout(() => this._divRef?.focus());
+                    break;
+                }
+            }
+        }
+    };
+
     render() {
         TraceMobx();
         const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
-        const label = this.Title.startsWith('#') ? null : this.Title;
         return (
-            <div key={label?.length} className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string }}>
+            <div className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this.boxShadow }}>
                 <div
                     className="labelBox-mainButton"
                     style={{
                         backgroundColor: this.backgroundColor,
-                        // fontSize: StrCast(this.layoutDoc._text_fontSize),
-                        color: StrCast(this.layoutDoc._color),
-                        fontFamily: StrCast(this.layoutDoc._text_fontFamily) || 'inherit',
+                        color: StrCast(this.layoutDoc._text_fontColor, StrCast(this.layoutDoc._color)),
+                        fontFamily: StrCast(this.layoutDoc._text_fontFamily, StrCast(Doc.UserDoc().fontFamily)) || 'inherit',
                         letterSpacing: StrCast(this.layoutDoc.letterSpacing),
-                        textTransform: StrCast(this.layoutDoc.textTransform) as Property.TextTransform,
+                        textTransform: StrCast(this.layoutDoc[this.fieldKey + '_transform']) as Property.TextTransform,
                         paddingLeft: NumCast(this.layoutDoc._xPadding),
                         paddingRight: NumCast(this.layoutDoc._xPadding),
                         paddingTop: NumCast(this.layoutDoc._yPadding),
                         paddingBottom: NumCast(this.layoutDoc._yPadding),
                         width: this._props.PanelWidth(),
                         height: this._props.PanelHeight(),
-                        whiteSpace: 'multiLine' in boxParams && boxParams.multiLine ? 'pre-wrap' : 'pre',
+                        whiteSpace: boxParams.multiLine ? 'pre-wrap' : 'pre',
                     }}>
                     <div
+                        key={this._forceRerender}
                         style={{
                             width: this._props.PanelWidth() - 2 * NumCast(this.layoutDoc._xPadding),
                             height: this._props.PanelHeight() - 2 * NumCast(this.layoutDoc._yPadding),
                             outline: 'unset !important',
                         }}
-                        onKeyDown={action(e => {
+                        onKeyDown={e => {
                             e.stopPropagation();
-                        })}
+                        }}
                         onKeyUp={action(e => {
                             e.stopPropagation();
-                            this.dataDoc[this.fieldKey] = this._divRef?.innerText ?? '';
-                            setTimeout(() => this._props.select(false));
+                            const text = this._divRef?.firstChild;
+                            if (text && (text as HTMLElement)?.nodeType === 3) {
+                                this._divRef?.removeChild(text);
+                                this._divRef?.firstChild?.appendChild(text);
+                                this.resetCursor();
+                            }
+                            this.fitTextToBox(this._divRef);
                         })}
+                        onFocus={() => {
+                            RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, this.dataDoc);
+                            this._divRef?.removeEventListener('focusout', this.keepFocus);
+                            this._divRef?.addEventListener('focusout', this.keepFocus);
+                        }}
                         onBlur={() => {
-                            this.dataDoc[this.fieldKey] = this._divRef?.innerText ?? '';
+                            this._divRef?.removeEventListener('focusout', this.keepFocus);
+                            this.setText(this._divRef?.innerText ?? '');
+                            RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
+                            FormattedTextBox.LiveTextUndo?.end();
+                            FormattedTextBox.LiveTextUndo = undefined;
+                        }}
+                        dangerouslySetInnerHTML={{
+                            __html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title.startsWith('#') ? null : (this.Title ?? '')}</span>`,
                         }}
                         contentEditable={this._props.onClickScript?.() ? undefined : true}
                         ref={r => {
+                            this._divRef?.removeEventListener('beforeinput', this.beforeInput);
                             this._divRef = r;
-                            this.fitTextToBox(r);
-                            if (this._props.isSelected() && this._divRef) {
-                                const range = document.createRange();
-                                range.setStart(this._divRef, this._divRef.childNodes.length);
-                                range.setEnd(this._divRef, this._divRef.childNodes.length);
-                                const sel = window.getSelection();
-                                sel?.removeAllRanges();
-                                sel?.addRange(range);
+                            if (this._divRef) {
+                                this._divRef.addEventListener('beforeinput', this.beforeInput);
+
+                                if (Doc.SelectOnLoad === this.Document) {
+                                    Doc.SelectOnLoad = undefined;
+                                    this._divRef.focus();
+                                }
+                                this.fitTextToBox(this._divRef);
+                                if (this.Title) {
+                                    this.resetCursor();
+                                }
                             }
-                        }}>
-                        {label}
-                    </div>
+                        }}
+                    />
                 </div>
             </div>
         );
@@ -161,9 +249,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
 Docs.Prototypes.TemplateMap.set(DocumentType.LABEL, {
     layout: { view: LabelBox, dataField: 'title' },
-    options: { acl: '', _singleLine: true, _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true },
+    options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' },
 });
 Docs.Prototypes.TemplateMap.set(DocumentType.BUTTON, {
     layout: { view: LabelBox, dataField: 'title' },
-    options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true },
+    options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' },
 });
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 2aee7b6b3..659cda9dd 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -65,6 +65,7 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
 import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
 import { Property } from 'csstype';
+import { LabelBox } from '../LabelBox';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 
 export interface FormattedTextBoxProps extends FieldViewProps {
@@ -158,6 +159,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     @computed get titleHeight()                { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number || 0; } // prettier-ignore
     @computed get layout_autoHeightMargins()   { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } // prettier-ignore
     @computed get sidebarKey()                 { return this.fieldKey + '_sidebar'; } // prettier-ignore
+    @computed get isLabel()                    { return this.dataDoc[this.fieldKey+"_fitBox"]; } // prettier-ignore
 
     constructor(props: FormattedTextBoxProps) {
         super(props);
@@ -165,7 +167,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         this._recordingStart = Date.now();
     }
 
-    public get EditorView() { return this._editorView; } // prettier-ignore
+    public get EditorView() { return this.isLabel ? undefined : this._editorView; } // prettier-ignore
 
     // public makeAIFlashcards: () => void = unimplementedFunction;
     public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
@@ -175,10 +177,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     // but since removing one anchor from the list of attr anchors isn't implemented, this will end up removing nothing.
     public RemoveLinkFromDoc(linkDoc?: Doc) {
         this.unhighlightSearchTerms();
-        const state = this._editorView?.state;
+        const state = this.EditorView?.state;
         const a1 = DocCast(linkDoc?.link_anchor_1);
         const a2 = DocCast(linkDoc?.link_anchor_2);
-        if (state && a1 && a2 && this._editorView) {
+        if (state && a1 && a2 && this.EditorView) {
             this.removeDocument(a1);
             this.removeDocument(a2);
             let allFoundLinkAnchors: { href: string; title: string; anchorId: string }[] = [];
@@ -188,7 +190,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 return true;
             });
             if (allFoundLinkAnchors.length) {
-                this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allAnchors: allFoundLinkAnchors }));
+                this.EditorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allAnchors: allFoundLinkAnchors }));
 
                 this.setupEditor(this.config, this.fieldKey);
             }
@@ -197,16 +199,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     // removes all the specified link references from the selection.
     // NOTE: as above, this won't work correctly if there are marks with overlapping but not exact sets of link references.
     public RemoveAnchorFromSelection(allAnchors: { href: string; title: string; linkId: string; targetId: string }[]) {
-        const state = this._editorView?.state;
-        if (state && this._editorView) {
-            this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors }));
+        const state = this.EditorView?.state;
+        if (state && this.EditorView) {
+            this.EditorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors }));
             this.setupEditor(this.config, this.fieldKey);
         }
     }
 
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const rootDoc: Doc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document);
-        if (!pinProps && this._editorView?.state.selection.empty) return rootDoc;
+        if (!pinProps && this.EditorView?.state.selection.empty) return rootDoc;
         const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc });
         this.addDocument(anchor);
         this._finishingLink = true;
@@ -263,7 +265,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 }
             });
         };
-        AnchorMenu.Instance.Highlight = undoable((color: string) => this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text');
+        AnchorMenu.Instance.Highlight = undoable((color: string) => this.EditorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text');
         AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true);
         AnchorMenu.Instance.StartCropDrag = unimplementedFunction;
         /**
@@ -292,7 +294,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         };
 
         AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? '');
-        const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
+        const coordsB = this.EditorView!.coordsAtPos(this.EditorView!.state.selection.to);
         this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
         let ele: Opt<HTMLDivElement>;
         try {
@@ -309,7 +311,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     };
 
     leafText = (node: Node) => {
-        if (node.type === this._editorView?.state.schema.nodes.dashField) {
+        if (node.type === this.EditorView?.state.schema.nodes.dashField) {
             const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
             const fieldKey = StrCast(node.attrs.fieldKey);
             return (
@@ -320,16 +322,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         return '';
     };
     dispatchTransaction = (tx: Transaction) => {
-        if (this._editorView && !this._editorView.isDestroyed) {
-            const state = this._editorView.state.apply(tx);
-            this._editorView.updateState(state);
+        if (this.EditorView && !this.EditorView.isDestroyed) {
+            const state = this.EditorView.state.apply(tx);
+            this.EditorView.updateState(state);
             this.tryUpdateDoc(false);
         }
     };
 
     tryUpdateDoc = (force: boolean) => {
-        if (this._editorView) {
-            const { state } = this._editorView;
+        if (this.EditorView) {
+            const { state } = this.EditorView;
             const { dataDoc } = this;
             const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText);
             const newJson = JSON.stringify(state.toJSON());
@@ -372,7 +374,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
                         // if we've deleted all the text in a note driven by a template, then restore the template data
                         dataDoc[this.fieldKey] = undefined;
-                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(rtField.Data)));
+                        this.EditorView.updateState(EditorState.fromJSON(this.config, JSON.parse(rtField.Data)));
                         ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText });
                         unchanged = false;
                     }
@@ -387,7 +389,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 if (jsonstring) {
                     const json = JSON.parse(jsonstring);
                     json.selection = state.toJSON().selection;
-                    this._editorView.updateState(EditorState.fromJSON(this.config, json));
+                    this.EditorView.updateState(EditorState.fromJSON(this.config, json));
                 }
             }
             if (window.getSelection()?.isCollapsed && this._props.rootSelected?.()) {
@@ -407,8 +409,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 linkAnchor = anchor;
             }
         });
-        if (this._editorView && linkTime) {
-            const { state } = this._editorView;
+        if (this.EditorView && linkTime) {
+            const { state } = this.EditorView;
             const node = state.selection.$from.node();
             if (linkAnchor && node.type !== state.schema.nodes.code_block) {
                 const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000;
@@ -416,7 +418,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 const { from } = state.selection;
                 const value = state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] });
                 const replaced = state.tr.insert(from - 1, value);
-                this._editorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1))));
+                this.EditorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1))));
             }
         }
     };
@@ -431,16 +433,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                  (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
                 link.link_relationship === LinkManager.AutoKeywords
         ); // prettier-ignore
-        if (this._editorView?.state.doc.textContent) {
-            let { tr } = this._editorView.state;
-            const { from, to } = this._editorView.state.selection;
-            const { autoLinkAnchor } = this._editorView.state.schema.marks;
+        if (this.EditorView?.state.doc.textContent) {
+            let { tr } = this.EditorView.state;
+            const { from, to } = this.EditorView.state.selection;
+            const { autoLinkAnchor } = this.EditorView.state.schema.marks;
             tr = tr.removeMark(0, tr.doc.content.size, autoLinkAnchor);
             Doc.MyPublishedDocs.filter(term => term.title).forEach(term => {
                 tr = this.hyperlinkTerm(tr, term, newAutoLinks);
             });
             tr = tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)));
-            this._editorView?.dispatch(tr);
+            this.EditorView?.dispatch(tr);
         }
         oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(doc => Doc.DeleteLink?.(doc));
     };
@@ -450,11 +452,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         if (
             !this._props.dontRegisterView && // (this.Document.isTemplateForField === "text" || !this.Document.isTemplateForField) && // only update the title if the data document's data field is changing
             title.startsWith('-') &&
-            this._editorView &&
+            this.EditorView &&
             !this.dataDoc.title_custom &&
             (Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text')
         ) {
-            let node = this._editorView.state.doc;
+            let node = this.EditorView.state.doc;
             while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild;
             const str = node.textContent;
             const prefix = '-';
@@ -477,7 +479,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
      */
     hyperlinkTerm = (trIn: Transaction, target: Doc, newAutoLinks: Set<Doc>) => {
         let tr = trIn;
-        const editorView = this._editorView;
+        const editorView = this.EditorView;
         if (editorView && !Doc.AreProtosEqual(target, this.Document)) {
             const autoLinkTerm = Field.toString(target.title as FieldType).replace(/^@/, '');
             let alink: Doc | undefined;
@@ -553,18 +555,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     };
 
     unhighlightSearchTerms = () => {
-        if (this._editorView) {
-            const { state } = this._editorView;
+        if (this.EditorView) {
+            const { state } = this.EditorView;
             if (state) {
                 const mark = state.schema.mark(state.schema.marks.search_highlight);
                 const activeMark = state.schema.mark(state.schema.marks.search_highlight, { selected: true });
                 const end = state.doc.nodeSize - 2;
-                this._editorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
+                this.EditorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
             }
         }
     };
     adoptAnnotation = (start: number, end: number, mark: Mark) => {
-        const view = this._editorView!;
+        const view = this.EditorView!;
         const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail() });
         view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
     };
@@ -616,7 +618,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     if (added) {
                         draggedDoc._freeform_fitContentsToBox = true;
                         Doc.SetContainer(draggedDoc, this.Document);
-                        const view = this._editorView!;
+                        const view = this.EditorView!;
                         try {
                             this._inDrop = true;
                             const pos = view.posAtCoords({ left: de.x, top: de.y })?.pos;
@@ -810,7 +812,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
         let target: Element | HTMLElement | null = e.target as HTMLElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
         while (target && (!(target instanceof HTMLElement) || !target.dataset?.targethrefs)) target = target.parentElement;
-        const editor = this._editorView;
+        const editor = this.EditorView;
         if (editor && target && !(e.nativeEvent instanceof simMouseEvent ? e.nativeEvent.dash : false)) {
             const hrefs = (target.dataset?.targethrefs as string)
                 ?.trim()
@@ -996,8 +998,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const c = this.ProseRef?.getElementsByTagName('img');
         if (c) {
             for (const i of c) {
-                console.log(i);
-
                 // console.log(canvas.toDataURL());
                 // canvas.style.zIndex = '2000000';
                 // document.body.appendChild(canvas);
@@ -1022,8 +1022,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     animateRes = (resIndex: number, newText: string) => {
         if (resIndex < newText.length) {
-            const marks = this._editorView?.state.storedMarks ?? [];
-            this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks));
+            const marks = this.EditorView?.state.storedMarks ?? [];
+            this.EditorView?.dispatch(this.EditorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks));
             setTimeout(() => this.animateRes(resIndex + 1, newText), 20);
         }
     };
@@ -1035,8 +1035,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
             if (!res) {
                 this.animateRes(0, 'Something went wrong.');
-            } else if (this._editorView) {
-                const { dispatch, state } = this._editorView;
+            } else if (this.EditorView) {
+                const { dispatch, state } = this.EditorView;
                 // for no animation, use: dispatch(state.tr.insertText(res));
                 // for animted response starting at end of text, use:
                 dispatch(state.tr.setSelection(Selection.atEnd(state.doc)));
@@ -1057,13 +1057,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     };
 
     breakupDictation = () => {
-        if (this._editorView && this._recordingDictation) {
+        if (this.EditorView && this._recordingDictation) {
             this.stopDictation(/* true */);
             this._break = true;
-            const { state } = this._editorView;
+            const { state } = this.EditorView;
             const { to } = state.selection;
             const updated = TextSelection.create(state.doc, to, to);
-            this._editorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({})));
+            this.EditorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({})));
             if (this._recordingDictation) {
                 this.recordDictation();
             }
@@ -1082,7 +1082,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     stopDictation = (/* abort: boolean */) => DictationManager.Controls.stop(/* !abort */);
 
     setDictationContent = (value: string) => {
-        if (this._editorView && this._recordingStart) {
+        if (this.EditorView && this._recordingStart) {
             if (this._break) {
                 const textanchorFunc = () => {
                     const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' });
@@ -1095,22 +1095,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     const textanchor = Cast(link.link_anchor_1, Doc, null);
                     if (audioanchor) {
                         audioanchor.backgroundColor = 'tan';
-                        const audiotag = this._editorView.state.schema.nodes.audiotag.create({
+                        const audiotag = this.EditorView.state.schema.nodes.audiotag.create({
                             timeCode: NumCast(audioanchor._timecodeToShow),
                             audioId: audioanchor[Id],
                             textId: textanchor[Id],
                         });
                         textanchor[DocData].title = 'dictation:' + audiotag.attrs.timeCode;
-                        const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag);
+                        const tr = this.EditorView.state.tr.insert(this.EditorView.state.doc.content.size, audiotag);
                         const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size));
-                        this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size)));
+                        this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size)));
                     }
                 }
             }
-            const { from } = this._editorView.state.selection;
+            const { from } = this.EditorView.state.selection;
             this._break = false;
-            const tr = this._editorView.state.tr.insertText(value);
-            this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView());
+            const tr = this.EditorView.state.tr.insertText(value);
+            this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView());
         }
     };
 
@@ -1143,7 +1143,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     }
                 });
                 this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents
-                this._editorView!.dispatch(tr.removeMark(selection.from, selection.to, splitter));
+                this.EditorView!.dispatch(tr.removeMark(selection.from, selection.to, splitter));
                 this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false;
                 anchor.text = selectedText;
                 anchor.text_html = this._selectionHTML ?? selectedText;
@@ -1177,7 +1177,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             let hadStart = start !== 0;
             frag.forEach((node, index) => {
                 const examinedNode = findAnchorNode(node, editor);
-                if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) {
+                if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this.EditorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this.EditorView?.state.schema.nodes.audiotag)) {
                     nodes.push(examinedNode.node);
                     !hadStart && (start = index + examinedNode.start);
                     hadStart = true;
@@ -1186,13 +1186,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             return { frag: Fragment.fromArray(nodes), start };
         };
         const findAnchorNode = (node: Node, editor: EditorView) => {
-            if (node.type === this._editorView?.state.schema.nodes.audiotag) {
+            if (node.type === this.EditorView?.state.schema.nodes.audiotag) {
                 if (node.attrs.textId === textAnchorId) {
                     return { node, start: 0 };
                 }
                 return undefined;
             }
-            if (node.type === this._editorView?.state.schema.nodes.dashDoc) {
+            if (node.type === this.EditorView?.state.schema.nodes.dashDoc) {
                 if (node.attrs.docId === textAnchorId) {
                     return { node, start: 0 };
                 }
@@ -1208,9 +1208,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         };
 
         this._didScroll = false; // assume we don't need to scroll.  if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below
-        if (this._editorView && textAnchorId) {
-            const { state } = this._editorView;
-            const ret = findAnchorFrag(state.doc.content, this._editorView);
+        if (this.EditorView && textAnchorId) {
+            const { state } = this.EditorView;
+            const ret = findAnchorFrag(state.doc.content, this.EditorView);
 
             const firstChild = ret.frag.childCount ? ret.frag.child(0) : undefined;
             if (ret.start >= 0 && (ret.frag.size || (firstChild && [state.schema.nodes.dashDoc, state.schema.nodes.audioTag].includes(firstChild.type)))) {
@@ -1219,7 +1219,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 if (ret.frag.firstChild) {
                     selection = TextSelection.between(state.doc.resolve(ret.start), state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
                 }
-                this._editorView.dispatch(state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
+                this.EditorView.dispatch(state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
                 const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId;
                 addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' });
                 setTimeout(() => {
@@ -1307,15 +1307,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
             },
             incomingValue => {
-                if (this._editorView && this._applyingChange !== this.fieldKey) {
+                if (this.EditorView && this._applyingChange !== this.fieldKey) {
                     if (incomingValue?.data) {
                         const updatedState = JSON.parse(incomingValue.data.Data);
-                        if (JSON.stringify(this._editorView.state.toJSON()) !== JSON.stringify(updatedState)) {
-                            this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
+                        if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) {
+                            this.EditorView.updateState(EditorState.fromJSON(this.config, updatedState));
                             this.tryUpdateScrollHeight();
                         }
-                    } else if (this._editorView.state.doc.textContent !== incomingValue?.str) {
-                        selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
+                    } else if (this.EditorView.state.doc.textContent !== (incomingValue?.str ?? '')) {
+                        selectAll(this.EditorView.state, tx => this.EditorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
                     }
                 }
             },
@@ -1333,9 +1333,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             action(selected => {
                 if (selected && this.dataDoc[this.fieldKey + '_placeholder']) {
                     setTimeout(() => {
-                        selectAll(this._editorView!.state, (tx: Transaction) => {
-                            this._editorView?.dispatch(tx);
-                            this._editorView!.focus();
+                        selectAll(this.EditorView!.state, (tx: Transaction) => {
+                            this.EditorView?.dispatch(tx);
+                            this.EditorView!.focus();
                         });
                     });
                 }
@@ -1343,12 +1343,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 if (FormattedTextBox._globalHighlights.has('Bold Text')) {
                     this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed
                 }
-                if (RichTextMenu.Instance?.view === this._editorView && !selected) {
+                if (((RichTextMenu.Instance?.view === this.EditorView && this.EditorView) || this.isLabel) && !selected) {
                     RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
                 }
-                if (this._editorView && selected) {
-                    RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc);
-                    setTimeout(this.autoLink, 20);
+                if (selected) {
+                    RichTextMenu.Instance?.updateMenu(this.EditorView, undefined, this._props, this.dataDoc);
+                    this.EditorView && setTimeout(this.autoLink, 20);
                 }
             }),
             { fireImmediately: true }
@@ -1389,7 +1389,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
             // DocCast(this.Document.image)._freeform_fitContentsToBox = true;
             // Doc.SetContainer(DocCast(this.Document.image), this.Document);
-            // const view = this._editorView!;
+            // const view = this.EditorView!;
             // try {
             //     this._inDrop = true;
             //     const pos = view.posAtCoords({ left: 0, top: 0 })?.pos;
@@ -1436,7 +1436,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     };
 
     addPdfReference = (pdfAnchorId: string) => {
-        const view = this._editorView!;
+        const view = this.EditorView!;
         if (pdfAnchorId) {
             DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => {
                 if (pdfAnchor instanceof Doc) {
@@ -1487,7 +1487,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const curText = Cast(this.dataDoc[this.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.fieldKey]);
         const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField);
         if (this.ProseRef) {
-            this._editorView?.destroy();
+            this.EditorView?.destroy();
             this._editorView = new EditorView(this.ProseRef, {
                 state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config),
                 handleScrollToSelection: editorView => {
@@ -1519,14 +1519,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             if (!rtfField) {
                 const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
                 const startupText = Field.toString(dataDoc[fieldKey] as FieldType);
-                const textAlign = StrCast(this.dataDoc.text_align, StrCast(Doc.UserDoc().textAlign)) || 'left';
+                const textAlign = StrCast(this.dataDoc[this.fieldKey + '_align'], StrCast(Doc.UserDoc().textAlign)) || 'left';
                 if (textAlign !== 'left') {
                     selectAll(this._editorView.state, tr => {
-                        this._editorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign })));
+                        this.EditorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign })));
                     });
                 }
                 if (startupText) {
-                    this._editorView?.dispatch(this._editorView.state.tr.insertText(startupText));
+                    this.EditorView?.dispatch(this.EditorView.state.tr.insertText(startupText));
                 }
                 this.tryUpdateDoc(true);
             }
@@ -1539,56 +1539,57 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             Doc.SetSelectOnLoad(undefined);
             FormattedTextBox.SelectOnLoadChar = '';
         }
-        if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
+        if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
             this._props.select(false);
             if (selLoadChar) {
-                const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
+                const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
                 const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
-                const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
+                const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
                 const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
-                const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks);
-                const tr2 = selLoadChar === 'Enter' ? tr1.insert(this._editorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this._editorView.state.doc.content.size - 1);
+                const tr1 = this.EditorView.state.tr.setStoredMarks(storedMarks);
+                const tr2 = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1);
                 const tr = tr2.setStoredMarks(storedMarks);
 
-                this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
+                this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
                 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
             } else if (!FormattedTextBox.DontSelectInitialText) {
                 const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
-                selectAll(this._editorView.state, (tx: Transaction) => {
-                    this._editorView?.dispatch(tx.addStoredMark(mark));
+                selectAll(this.EditorView.state, (tx: Transaction) => {
+                    this.EditorView?.dispatch(tx.addStoredMark(mark));
                 });
+                this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(new TextSelection(this.EditorView.state.doc.resolve(1))));
                 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
             } else {
-                const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
+                const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
                 const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
-                const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
+                const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
                 const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
-                const { tr } = this._editorView.state;
-                this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks));
+                const { tr } = this.EditorView.state;
+                this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks));
                 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) {
             FormattedTextBox.DontSelectInitialText = false;
-            this._editorView!.focus();
+            this.EditorView!.focus();
         }
         if (this._props.isContentActive()) this.prepareForTyping();
-        if (this._editorView && FormattedTextBox.PasteOnLoad) {
+        if (this.EditorView && FormattedTextBox.PasteOnLoad) {
             const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor');
             FormattedTextBox.PasteOnLoad = undefined;
             pdfAnchorId && this.addPdfReference(pdfAnchorId);
         }
-        if (this._props.autoFocus) setTimeout(() => this._editorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it.
+        if (this._props.autoFocus) setTimeout(() => this.EditorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it.
     }
 
     // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
     prepareForTyping = () => {
-        if (this._editorView) {
+        if (this.EditorView) {
             const { text, paragraph } = schema.nodes;
-            const selNode = this._editorView.state.selection.$anchor.node();
-            if (this._editorView.state.selection.from === 1 && this._editorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
+            const selNode = this.EditorView.state.selection.$anchor.node();
+            if (this.EditorView.state.selection.from === 1 && this.EditorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
                 const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })];
-                this._editorView.state.selection.empty && this._editorView.state.selection.from === 1 && this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor));
+                this.EditorView.state.selection.empty && this.EditorView.state.selection.from === 1 && this.EditorView?.dispatch(this.EditorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor));
             }
         }
     };
@@ -1602,7 +1603,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         FormattedTextBox.LiveTextUndo?.end();
         FormattedTextBox.LiveTextUndo = undefined;
         this.unhighlightSearchTerms();
-        this._editorView?.destroy();
+        this.EditorView?.destroy();
         RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined);
         FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = 'none');
     }
@@ -1681,26 +1682,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
     };
     setFocus = (ipos?: number) => {
-        const pos = ipos ?? (this._editorView?.state.selection.$from.pos || 1);
-        setTimeout(() => this._editorView?.dispatch(this._editorView.state.tr.setSelection(TextSelection.near(this._editorView.state.doc.resolve(pos)))), 100);
+        const pos = ipos ?? (this.EditorView?.state.selection.$from.pos || 1);
+        setTimeout(() => this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(TextSelection.near(this.EditorView.state.doc.resolve(pos)))), 100);
         setTimeout(() => (this.ProseRef?.children?.[0] as HTMLElement).focus(), 200);
     };
     @action
     onFocused = (e: React.FocusEvent): void => {
-        // applyDevTools.applyDevTools(this._editorView);
+        // applyDevTools.applyDevTools(this.EditorView);
         e.stopPropagation();
     };
 
     onClick = (e: React.MouseEvent): void => {
         if (!this._props.isContentActive()) return;
-        const editorView = this._editorView;
+        const editorView = this.EditorView;
         const editorRoot = editorView?.root instanceof Document ? editorView.root : undefined;
         if (editorView && (!this._forceUncollapse || editorRoot?.getSelection()?.isCollapsed)) {
             // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment.  Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
             const pcords = editorView.posAtCoords({ left: e.clientX, top: e.clientY });
             const node = pcords && editorView.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
             if (pcords && node?.type === editorView.state.schema.nodes.dashComment) {
-                this._editorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2)));
+                this.EditorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2)));
                 e.preventDefault();
             }
             if (!node && this.ProseRef) {
@@ -1726,33 +1727,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, selectOrderedList: boolean = false) {
         this._forceUncollapse = false;
         clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
-        const clickPos = this._editorView!.posAtCoords({ left: x, top: y });
+        const clickPos = this.EditorView!.posAtCoords({ left: x, top: y });
         const clickPosVal = clickPos?.pos || 1;
         let olistPos = clickPosVal;
         if (clickPos && olistPos && this._props.rootSelected?.()) {
-            const clickNode = this._editorView?.state.doc.resolve(olistPos).node();
-            const nodeBef = this._editorView?.state.doc.resolve(Math.max(0, olistPos - 1)).node();
-            olistPos = nodeBef?.type === this._editorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos;
-            let $olistPos = this._editorView?.state.doc.resolve(olistPos);
-            let olistNode = (nodeBef !== null || clickNode?.type === this._editorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef;
-            if (olistNode?.type === this._editorView?.state.schema.nodes.list_item) {
+            const clickNode = this.EditorView?.state.doc.resolve(olistPos).node();
+            const nodeBef = this.EditorView?.state.doc.resolve(Math.max(0, olistPos - 1)).node();
+            olistPos = nodeBef?.type === this.EditorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos;
+            let $olistPos = this.EditorView?.state.doc.resolve(olistPos);
+            let olistNode = (nodeBef !== null || clickNode?.type === this.EditorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef;
+            if (olistNode?.type === this.EditorView?.state.schema.nodes.list_item) {
                 if ($olistPos && $olistPos.depth) {
                     olistNode = $olistPos.parent;
-                    $olistPos = this._editorView?.state.doc.resolve($olistPos.start($olistPos.depth - 1));
+                    $olistPos = this.EditorView?.state.doc.resolve($olistPos.start($olistPos.depth - 1));
                 }
             }
-            const maxSize = this._editorView?.state.doc.content.size ?? 0;
-            const listPos = this._editorView?.state.doc.resolve(Math.min(maxSize, clickPosVal === olistPos ? clickPosVal + 1 : clickPosVal));
+            const maxSize = this.EditorView?.state.doc.content.size ?? 0;
+            const listPos = this.EditorView?.state.doc.resolve(Math.min(maxSize, clickPosVal === olistPos ? clickPosVal + 1 : clickPosVal));
             const listNode = listPos?.node();
-            if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list && listNode) {
+            if (olistNode && olistNode.type === this.EditorView?.state.schema.nodes.ordered_list && listNode) {
                 if (!highlightOnly) {
                     if (selectOrderedList) {
-                        this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!)));
+                        this.EditorView.dispatch(this.EditorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!)));
                     } else {
                         const nodePos = clickPosVal - (olistPos === clickPosVal ? 0 : 1);
-                        if (this._editorView.state.doc.nodeAt(nodePos)) {
-                            const tr = this._editorView.state.tr.setNodeMarkup(nodePos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility });
-                            this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, nodePos)));
+                        if (this.EditorView.state.doc.nodeAt(nodePos)) {
+                            const tr = this.EditorView.state.tr.setNodeMarkup(nodePos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility });
+                            this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, nodePos)));
                         }
                     }
                 }
@@ -1772,19 +1773,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     onBlur = (e: React.FocusEvent) => {
         if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
         if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) {
-            const stordMarks = this._editorView?.state.storedMarks?.slice();
+            const stordMarks = this.EditorView?.state.storedMarks?.slice();
             if (!(this.EditorView?.state.selection instanceof NodeSelection)) {
                 this.autoLink();
-                if (this._editorView?.state.tr) {
+                if (this.EditorView?.state.tr) {
                     const tr = stordMarks?.reduce((tr2, m) => {
                         tr2.addStoredMark(m);
                         return tr2;
-                    }, this._editorView.state.tr);
-                    tr && this._editorView.dispatch(tr);
+                    }, this.EditorView.state.tr);
+                    tr && this.EditorView.dispatch(tr);
                 }
             }
         }
-        if (RichTextMenu.Instance?.view === this._editorView && !(this._props.isContentActive() || this._props.rootSelected?.())) {
+        if (RichTextMenu.Instance?.view === this.EditorView && !(this._props.isContentActive() || this._props.rootSelected?.())) {
             RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
         }
 
@@ -1831,7 +1832,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
         switch (e.key) {
             case 'Escape':
-                this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
+                this.EditorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
                 (document.activeElement as HTMLElement).blur?.();
                 DocumentView.DeselectAll();
                 RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
@@ -1857,7 +1858,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         this.startUndoTypingBatch();
     };
     ondrop = (e: React.DragEvent) => {
-        this._editorView!.dispatch(updateBullets(this._editorView!.state.tr, this._editorView!.state.schema));
+        this.EditorView?.dispatch(updateBullets(this.EditorView.state.tr, this.EditorView.state.schema));
         e.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash.
     };
     onScroll = (e: React.UIEvent) => {
@@ -1872,7 +1873,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     tryUpdateScrollHeight = () => {
         const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
         const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
-        if (children && !SnappingManager.IsDragging) {
+        if (this.EditorView && children && !SnappingManager.IsDragging) {
             const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
             const toNum = (val: string) => Number(val.replace('px', ''));
             const toHgt = (node: Element): number => {
@@ -2093,7 +2094,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), this._props.xPadding ?? 0, 0, ((this._props.screenXPadding?.() ?? 0) - scrMargin[0]) * this.ScreenToLocalBoxXf().Scale);
         const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, ((this._props.yPadding ?? 0) - scrMargin[1]) * this.ScreenToLocalBoxXf().Scale);
         const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._header_height}px' >
-        return styleFromLayout?.height === '0px' ? null : (
+        return this.isLabel ? (
+            <LabelBox {...this._props} />
+        ) : styleFromLayout?.height === '0px' ? null : (
             <div
                 className="formattedTextBox"
                 ref={r => {
@@ -2156,8 +2159,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                                 paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
                                 paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
                                 color: StrCast(this.layoutDoc.text_fontColor),
-                                fontWeight: `${this.layoutDoc.contentBold ? 'bold' : ''}`,
-                                textTransform: `${this.layoutDoc.textTransform}` as Property.TextTransform,
+                                fontWeight: this.layoutDoc.contentBold ? 'bold' : '',
+                                textTransform: StrCast(this.dataDoc[this.fieldKey + '_transform']) as Property.TextTransform,
                             }}
                         />
                     </div>
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 7a8b72be0..3c84e5a10 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -349,7 +349,9 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa
                                 dispatch(tx4);
                             }
 
-                            if (view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) {
+                            if (view.state.selection.$anchor.depth > 0 &&
+                                view.state.selection.$anchor.node(view.state.selection.$anchor.depth-1).type === schema.nodes.list_item &&
+                                view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) {
                                 // if text is selected across list items, then we need to forcibly insert a new line since the splitBlock code joins the two list items.
                                 enter(view.state, dispatch, view, false);
                             }
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 55e6a3a5b..65a415a23 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { lift, toggleMark, wrapIn } from 'prosemirror-commands';
 import { Mark, MarkType } from 'prosemirror-model';
@@ -32,7 +32,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
     private _linkToRef = React.createRef<HTMLInputElement>();
 
-    layoutDoc: Doc | undefined;
+    dataDoc: Doc | undefined;
     @observable public view?: EditorView & { TextView?: FormattedTextBox } = undefined;
     public editorProps: FieldViewProps | AntimodeMenuProps | undefined;
 
@@ -49,6 +49,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     @observable private _activeFontSize: string = '13px';
     @observable private _activeFontFamily: string = '';
+    @observable private _activeFitBox: boolean = false;
     @observable private _activeListType: string = '';
     @observable private _activeAlignment: string = 'left';
 
@@ -64,13 +65,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     @observable private currentLink: string | undefined = '';
     @observable private showLinkDropdown: boolean = false;
 
-    _reaction: IReactionDisposer | undefined;
     constructor(props: AntimodeMenuProps) {
         super(props);
         makeObservable(this);
         runInAction(() => {
             RichTextMenu._instance.menu = this;
-            this.updateMenu(undefined, undefined, props, this.layoutDoc);
+            this.updateMenu(undefined, undefined, props, this.dataDoc);
             this._canFade = false;
             this.Pinned = true;
         });
@@ -101,6 +101,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     @computed get fontHighlight() {
         return this._activeHighlightColor;
     }
+    @computed get fitBox() {
+        return this._activeFitBox;
+    }
     @computed get fontFamily() {
         return this._activeFontFamily;
     }
@@ -114,26 +117,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         return this._activeAlignment;
     }
     @computed get textVcenter() {
-        return BoolCast(this.layoutDoc?._layout_centered);
-    }
-    _disposer: IReactionDisposer | undefined;
-    componentDidMount() {
-        // this._disposer = reaction(
-        //     () => DocumentView.Selected().slice(),
-        //     () => this.updateMenu(undefined, undefined, undefined, undefined)
-        // );
-    }
-    componentWillUnmount() {
-        this._disposer?.();
+        return BoolCast(this.dataDoc?._layout_centered, BoolCast(Doc.UserDoc().layout_centered));
     }
 
     @action
-    public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: FormattedTextBoxProps | AntimodeMenuProps | undefined, layoutDoc: Doc | undefined) {
+    public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: FormattedTextBoxProps | AntimodeMenuProps | undefined, dataDoc: Doc | undefined) {
         if (this._linkToRef.current?.getBoundingClientRect().width) {
             return;
         }
         this.view = view;
-        this.layoutDoc = layoutDoc;
+        this.dataDoc = dataDoc;
         props && (this.editorProps = props);
 
         // Don't do anything if the document/selection didn't change
@@ -147,12 +140,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         const { activeSizes } = active;
         const { activeColors } = active;
         const { activeHighlights } = active;
-        const refDoc = DocumentView.Selected().lastElement()?.layoutDoc ?? Doc.UserDoc();
+        const refDoc = DocumentView.Selected().lastElement()?.dataDoc ?? Doc.UserDoc();
         const refField = (pfx => (pfx ? pfx + '_' : ''))(DocumentView.Selected().lastElement()?.LayoutFieldKey);
         const refVal = (field: string, dflt: string) => StrCast(refDoc[refField + field], StrCast(Doc.UserDoc()[field], dflt));
 
         this._activeListType = this.getActiveListStyle();
         this._activeAlignment = this.getActiveAlignment();
+        this._activeFitBox = BoolCast(refDoc[refField + 'fitBox'], BoolCast(Doc.UserDoc().fitBox));
         this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, refVal('fontFamily', 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
         this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, refVal('fontSize', '10px')) : activeSizes[0];
         this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, refVal('fontColor', 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...';
@@ -181,7 +175,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
                     toggleMark(mark.type, mark.attrs)(state, dispatch);
                 }
             }
-            //    this.updateMenu(this.view, undefined, undefined, this.layoutDoc);
         }
     };
 
@@ -195,8 +188,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
                     return node.attrs.align || 'left';
                 }
             }
+        } else if (this.dataDoc) {
+            return StrCast(this.dataDoc.text_align) || 'left';
         }
-        return 'left';
+        return StrCast(Doc.UserDoc().textAlign) || 'left';
     };
 
     // finds font sizes and families in selection
@@ -330,6 +325,17 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
             this.view.focus();
         }
     };
+    toggleFitBox = () => {
+        if (this.dataDoc) {
+            const doc = this.dataDoc;
+            (document.activeElement as HTMLElement)?.blur();
+            doc.text_fitBox = !doc.text_fitBox;
+        } else {
+            Doc.UserDoc().fitBox = !Doc.UserDoc().fitBox;
+            Doc.UserDoc().textAlign = Doc.UserDoc().fitBox ? 'center' : undefined;
+        }
+        this.updateMenu(undefined, undefined, undefined, this.dataDoc);
+    };
     toggleBold = () => {
         if (this.view) {
             const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong);
@@ -354,11 +360,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         }
     };
 
-    setFontField = (value: string, fontField: 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
-        if (this.TextView && this.view) {
+    setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
+        if (this.dataDoc) {
+            this.dataDoc[`text_${fontField}`] = value;
+            this.updateMenu(undefined, undefined, undefined, this.dataDoc);
+        } else if (this.TextView && this.view) {
             const { text, paragraph } = this.view.state.schema.nodes;
             const selNode = this.view.state.selection.$anchor.node();
-            if (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
+            if ((fontField === 'fontSize' && value === '0px') || (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type))) {
                 this.TextView.dataDoc[this.TextView.fieldKey + `_${fontField}`] = value;
                 this.view.focus();
             }
@@ -369,7 +378,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
             this.view.focus();
         } else {
             Doc.UserDoc()[fontField] = value;
-            // this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
         }
     };
 
@@ -395,7 +403,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
                 this.view!.dispatch(tx3);
             });
         this.view.focus();
-        // this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
     };
 
     insertSummarizer(state: EditorState, dispatch: (tr: Transaction) => void) {
@@ -410,10 +417,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     }
 
     vcenterToggle = () => {
-        this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered);
+        if (this.dataDoc) this.dataDoc._layout_centered = !this.dataDoc._layout_centered;
+        else Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered;
     };
-    align = (view: EditorView, dispatch: (tr: Transaction) => void, alignment: 'left' | 'right' | 'center') => {
-        if (this.RootSelected) {
+    align = (view: EditorView | undefined, dispatch: undefined | ((tr: Transaction) => void), alignment: 'left' | 'right' | 'center') => {
+        if (view && dispatch && this.RootSelected) {
             let { tr } = view.state;
             view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos) => {
                 if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) {
@@ -425,6 +433,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
             });
             view.focus();
             dispatch?.(tr);
+        } else {
+            if (this.dataDoc) {
+                this.dataDoc.text_align = alignment;
+            } else Doc.UserDoc().textAlign = alignment;
+            this.updateMenu(undefined, undefined, undefined, this.dataDoc);
         }
     };
 
@@ -702,7 +715,7 @@ interface RichTextMenuPluginProps {
 }
 export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> {
     update(view: EditorView & { TextView?: FormattedTextBox }, lastState: EditorState | undefined) {
-        RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.layoutDoc);
+        RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.dataDoc);
     }
     render() {
         return null;
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index f58434906..3d14ce4c0 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -121,7 +121,7 @@ export class RichTextRules {
                     annotationOn: textDoc,
                     _layout_fitWidth: true,
                     _layout_autoHeight: true,
-                    _text_fontSize: '9px',
+                    text_fontSize: '9px',
                     title: 'inline comment',
                 });
                 textDocInline.title = inlineFieldKey; // give the annotation its own title
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 5d3672aee..06869717a 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -2616,13 +2616,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     createTemplate = (layout: string, input?: string) => {
         const x = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.x) : 0;
         const y = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.y) + NumCast(this.targetDoc._height) + 20 : 0;
-        const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _text_fontSize: '24pt' });
-        const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _text_fontSize: '16pt' });
-        const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _text_fontSize: '20pt' });
-        const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _text_fontSize: '24pt' });
-        const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _text_fontSize: '14pt' });
-        const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _text_fontSize: '14pt' });
-        const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _text_fontSize: '14pt' });
+        const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, text_fontSize: '24pt' });
+        const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, text_fontSize: '16pt' });
+        const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, text_fontSize: '20pt' });
+        const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, text_fontSize: '24pt' });
+        const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, text_fontSize: '14pt' });
+        const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, text_fontSize: '14pt' });
+        const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, text_fontSize: '14pt' });
         // prettier-ignore
         switch (layout) {
             case 'blank': return Docs.Create.FreeformDocument([], { title: input || 'Blank slide', _width: 400, _height: 225, x, y });
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index d5f5f620c..a7e78bcea 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -572,18 +572,19 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
         <div style={{ height: '80%' }}>
             {this.heading(this.mode === GPTPopupMode.SORT ? 'SORTING' : 'QUIZ')}
             <>
-                {!this.cardsDoneLoading ? (
-                    <div className="content-wrapper">
-                        <div className="loading-spinner">
-                            <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
-                            {this.loading ? <span>Loading...</span> : <span>Reading Cards...</span>}
+                {
+                    !this.cardsDoneLoading ? (
+                        <div className="content-wrapper">
+                            <div className="loading-spinner">
+                                <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
+                                {this.loading ? <span>Loading...</span> : <span>Reading Cards...</span>}
+                            </div>
                         </div>
-                    </div>
-                ) : this.mode === GPTPopupMode.CARD ? (
-                    this.cardMenu()
-                ) : (
-                    this.cardActual(this.mode)
-                ) // Call the functions to render JSX
+                    ) : this.mode === GPTPopupMode.CARD ? (
+                        this.cardMenu()
+                    ) : (
+                        this.cardActual(this.mode)
+                    ) // Call the functions to render JSX
                 }
             </>
         </div>
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 335683270..b27816f55 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -48,8 +48,8 @@ export const documentSchema = createSchema({
     _columnsHideIfEmpty: 'boolean', // whether empty stacking view column headings should be hidden
     // _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry
     // _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views
-    _text_fontSize: 'string',
-    _text_fontFamily: 'string',
+    text_fontSize: 'string',
+    text_fontFamily: 'string',
     _layout_sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar
 
     // appearance properties on the data document
@@ -70,7 +70,7 @@ export const documentSchema = createSchema({
     stroke_startMarker: 'string',
     stroke_endMarker: 'string',
     stroke_dash: 'string',
-    textTransform: 'string',
+    text_transform: 'string',
     treeView_Open: 'boolean', //  flag denoting whether the documents sub-tree (contents) is visible or hidden
     treeView_ExpandedView: 'string', // name of field whose contents are being displayed as the document's subtree
     treeView_ExpandedViewLock: 'boolean', // whether the expanded view can be changed
@@ -112,5 +112,4 @@ export const collectionSchema = createSchema({
 });
 
 export type Document = makeInterface<[typeof documentSchema]>;
-// eslint-disable-next-line no-redeclare
 export const Document = makeInterface(documentSchema);
-- 
cgit v1.2.3-70-g09d2


From 4c768162e0436115a05b9c8b0e4d837d626d45ba Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 30 Oct 2024 18:54:52 -0400
Subject: reworked how context menu buttons for ink and text work. added
 disableMixBlend for making transparent docs not use 'multiply'.

---
 src/ClientUtils.ts                                 |  11 +-
 src/client/documents/DocUtils.ts                   |  16 +-
 src/client/documents/Documents.ts                  |   1 -
 src/client/util/CurrentUserUtils.ts                |  78 +++++----
 src/client/util/InteractionUtils.tsx               |   6 +-
 src/client/util/SettingsManager.tsx                |   4 +-
 src/client/util/SnappingManager.ts                 |   7 +
 src/client/views/FilterPanel.tsx                   |   2 +-
 src/client/views/GestureOverlay.tsx                | 164 ++++++------------
 src/client/views/GlobalKeyHandler.ts               |   9 +-
 src/client/views/InkingStroke.tsx                  |   3 +-
 src/client/views/LightboxView.tsx                  |   4 +-
 src/client/views/StyleProp.ts                      |   4 +-
 src/client/views/StyleProvider.tsx                 |   5 +-
 .../views/collections/CollectionDockingView.tsx    |   2 +-
 .../CollectionFreeFormInfoUI.tsx                   |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  71 ++++----
 .../collections/collectionFreeForm/MarqueeView.tsx |   2 +-
 src/client/views/global/globalScripts.ts           | 112 +++++++------
 .../views/newlightbox/ButtonMenu/ButtonMenu.tsx    |   6 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   5 +-
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |   4 +-
 src/client/views/nodes/DocumentView.tsx            |  72 ++++----
 .../views/nodes/FontIconBox/FontIconBox.scss       |   3 +
 src/client/views/nodes/FontIconBox/FontIconBox.tsx | 186 ++++++++++++---------
 src/client/views/nodes/ImageBox.tsx                |  10 +-
 src/client/views/nodes/VideoBox.tsx                |   2 +-
 src/client/views/nodes/WebBox.tsx                  |  13 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  12 +-
 .../views/nodes/formattedText/RichTextMenu.tsx     |  22 +--
 src/client/views/pdf/PDFViewer.tsx                 |   2 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  12 +-
 src/fields/Doc.ts                                  |   6 +-
 src/fields/InkField.ts                             |  27 +--
 src/pen-gestures/GestureTypes.ts                   |   1 -
 35 files changed, 444 insertions(+), 446 deletions(-)

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

diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index e7aee1c2a..baad9a06c 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -107,12 +107,12 @@ export namespace ClientUtils {
             default:                 return type.charAt(0).toUpperCase() + type.substring(1,3);
         } // prettier-ignore
     }
-    export function cleanDocumentType(type: DocumentType, colType: CollectionViewType) {
+    export function cleanDocumentType(type: DocumentType, colType?: CollectionViewType) {
         switch (type) {
             case DocumentType.PDF:   return 'PDF';
             case DocumentType.IMG:   return 'Image';
             case DocumentType.AUDIO: return 'Audio';
-            case DocumentType.COL:   return 'Collection:'+colType;
+            case DocumentType.COL:   return 'Collection:'+ (colType ?? "");
             case DocumentType.RTF:   return 'Text';
             default:                 return type.charAt(0).toUpperCase() + type.slice(1);
         } // prettier-ignore
@@ -212,7 +212,7 @@ export namespace ClientUtils {
         return { r: r, g: g, b: b, a: a };
     }
 
-    const isTransparentFunctionHack = 'isTransparent(__value__)';
+    export const isTransparentFunctionHack = 'isTransparent(__value__)';
     export const noRecursionHack = '__noRecursion';
 
     // special case filters
@@ -223,11 +223,6 @@ export namespace ClientUtils {
     export function IsRecursiveFilter(val: string) {
         return !val.includes(noRecursionHack);
     }
-    export function HasFunctionFilter(val: string) {
-        if (val.includes(isTransparentFunctionHack)) return (color: string) => color !== '' && DashColor(color).alpha() !== 1;
-        // add other function filters here...
-        return undefined;
-    }
 
     export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) {
         return 'rgba(' + col.r + ',' + col.g + ',' + col.b + (col.a !== undefined ? ',' + col.a : '') + ')';
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index dae3e48ad..067b9c5e0 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -3,7 +3,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { saveAs } from 'file-saver';
 import * as JSZip from 'jszip';
 import { action, runInAction } from 'mobx';
-import { ClientUtils } from '../../ClientUtils';
+import { ClientUtils, DashColor } from '../../ClientUtils';
 import * as JSZipUtils from '../../JSZipUtils';
 import { decycle } from '../../decycler/decycler';
 import { DateField } from '../../fields/DateField';
@@ -36,11 +36,16 @@ import { DocumentView } from '../views/nodes/DocumentView';
 import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
 
 export namespace DocUtils {
+    function HasFunctionFilter(val: string) {
+        if (val.includes(ClientUtils.isTransparentFunctionHack)) return (d: Doc, color: string) => !d.disableMixBlend && color !== '' && DashColor(color).alpha() !== 1;
+        // add other function filters here...
+        return undefined;
+    }
     function matchFieldValue(doc: Doc, key: string, valueIn: unknown): boolean {
         let value = valueIn;
-        const hasFunctionFilter = ClientUtils.HasFunctionFilter(value as string);
+        const hasFunctionFilter = HasFunctionFilter(value as string);
         if (hasFunctionFilter) {
-            return hasFunctionFilter(StrCast(doc[key]));
+            return hasFunctionFilter(doc, StrCast(doc[key]));
         }
         if (key === LinkedTo) {
             // links are not a field value, so handled here.  value is an expression of form ([field=]idToDoc("..."))
@@ -683,9 +688,14 @@ export namespace DocUtils {
                       _layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
                       _layout_fitWidth: true,
                       _layout_autoHeight: true,
+                      backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor),
                       text_fitBox: BoolCast(Doc.UserDoc().fitBox),
                       text_align: StrCast(Doc.UserDoc().textAlign),
                       text_fontColor: StrCast(Doc.UserDoc().fontColor),
+                      text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
+                      text_fontWeight: StrCast(Doc.UserDoc().fontWeight),
+                      text_fontStyle: StrCast(Doc.UserDoc().fontStyle),
+                      text_fontDecoration: StrCast(Doc.UserDoc().fontDecoration),
                   }),
         });
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b0e1a7545..efd6ce3c9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -927,7 +927,6 @@ export namespace Docs {
             I.text_align = 'center';
             I.rotation = 0;
             I.defaultDoubleClick = 'ignore';
-            I.keepZWhenDragged = true;
             I.author_date = new DateField();
             I.acl_Guest = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View;
             // I.acl_Override = SharingPermissions.Unset;
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index b3aa5e145..98a3e8914 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -4,7 +4,7 @@ import * as rp from 'request-promise';
 import { ClientUtils, OmitKeys } from "../../ClientUtils";
 import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from "../../fields/Doc";
 import { DocData } from "../../fields/DocSymbols";
-import { InkTool } from "../../fields/InkField";
+import { InkEraserTool, InkInkTool, InkTool } from "../../fields/InkField";
 import { List } from "../../fields/List";
 import { PrefetchProxy } from "../../fields/Proxy";
 import { RichTextField } from "../../fields/RichTextField";
@@ -39,6 +39,7 @@ import { SelectionManager } from "./SelectionManager";
 import { ColorScheme } from "./SettingsManager";
 import { SnappingManager } from "./SnappingManager";
 import { UndoManager } from "./UndoManager";
+import { DocumentView } from "../views/nodes/DocumentView";
 
 export interface Button {
     // DocumentOptions fields a button can set
@@ -731,11 +732,11 @@ pie title Minerals in my tap water
         return [
             { title: "Font",     toolTip: "Font", width: 100,     btnType: ButtonType.DropdownList,                      toolType:"font",     ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'},
               btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
-            { title: "Font Size",toolTip: "Font size (%size)",    btnType: ButtonType.NumberDropdownButton,              toolType:"fontSize", ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 9 },
+            { title: " Size",    toolTip: "Font size (%size)",    btnType: ButtonType.NumberDropdownButton,              toolType:"fontSize", ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 9 },
             { title: "Color",    toolTip: "Font color (%color)",  btnType: ButtonType.ColorButton,  icon: "font",        toolType:"fontColor",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
             { title: "Highlight",toolTip: "Font highlight",       btnType: ButtonType.ColorButton,  icon: "highlighter", toolType:"highlight",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
             { title: "Bold",     toolTip: "Bold (Ctrl+B)",        btnType: ButtonType.ToggleButton, icon: "bold",        toolType:"bold",     ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
-            { title: "Italic",   toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",      toolType:"italics",  ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
+            { title: "Italic",   toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",      toolType:"italic",   ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             { title: "Under",    toolTip: "Underline (Ctrl+U)",   btnType: ButtonType.ToggleButton, icon: "underline",   toolType:"underline",ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             { title: "Bullets",  toolTip: "Bullet List",          btnType: ButtonType.ToggleButton, icon: "list",        toolType:"bullet",   ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             { title: "#",        toolTip: "Number List",          btnType: ButtonType.ToggleButton, icon: "list-ol",     toolType:"decimal",  ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
@@ -758,24 +759,27 @@ pie title Minerals in my tap water
 
     static inkTools():Button[] {
         return [
-            { title: "Pen",      toolTip: "Pen (Ctrl+P)",                   btnType: ButtonType.ToggleButton, icon: "pen-nib",    toolType: "pen",                                   scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
-            { title: "Highlight",toolTip: "Highlight (Ctrl+H)",             btnType: ButtonType.ToggleButton, icon: "highlighter",toolType: "highlighter",                           scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
-            { title: "Write",    toolTip: "Write (Ctrl+Shift+P)",           btnType: ButtonType.ToggleButton, icon: "pen",        toolType: "write",                                 scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
-            { title: "Eraser",   toolTip: "Eraser (Ctrl+E)",                btnType: ButtonType.MultiToggleButton,                toolType: InkTool.Eraser,                          scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' },
+            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",      toolType: Gestures.Circle,                          scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`,   onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",      toolType: Gestures.Rectangle,                       scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`,   onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",       toolType: Gestures.Line,                            scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`,   onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+            { title: "Ink",    toolTip: "Ink",                              btnType: ButtonType.MultiToggleButton,                 toolType: InkTool.Ink,                              scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }, 
+                subMenu: [
+                    { title: "Pen",      toolTip: "Pen (Ctrl+P)",           btnType: ButtonType.ToggleButton, icon: "pen-nib",     toolType: InkInkTool.Pen,        ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
+                    { title: "Highlight",toolTip: "Highlight (Ctrl+H)",     btnType: ButtonType.ToggleButton, icon: "highlighter", toolType: InkInkTool.Highlight,  ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
+                    { title: "Write",    toolTip: "Write (Ctrl+Shift+P)",   btnType: ButtonType.ToggleButton, icon: "pen",         toolType: InkInkTool.Write,      ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' },  funcs: {hidden:"IsNoviceMode()" }},      
+                ]},
+            { title: "Width",      toolTip: "Stroke width",                 btnType: ButtonType.NumberSliderButton,                toolType: "strokeWidth",         ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}, numBtnMin: 1, linearBtnWidth:40},
+            { title: "Color",      toolTip: "Stroke color",                 btnType: ButtonType.ColorButton,  icon: "pen",         toolType: "strokeColor",         ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}},
+            { title: "Eraser",     toolTip: "Eraser (Ctrl+E)",              btnType: ButtonType.MultiToggleButton,                 toolType: InkTool.Eraser,                           scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' },
               subMenu: [
-                    { title: "Stroke",   toolTip: "Stroke Erase",           btnType: ButtonType.ToggleButton, icon: "eraser",      toolType:InkTool.StrokeEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
-                    { title: "Segment",  toolTip: "Segment Erase",          btnType: ButtonType.ToggleButton, icon: "xmark",       toolType:InkTool.SegmentEraser,ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
-                    { title: "Radius",   toolTip: "Radius Erase",           btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkTool.RadiusEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
+                    { title: "Stroke",   toolTip: "Eraser complete strokes",btnType: ButtonType.ToggleButton, icon: "eraser",      toolType:InkEraserTool.Stroke,   ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
+                    { title: "Segment",  toolTip: "Erase segments up to intersections",btnType: ButtonType.ToggleButton,icon: "xmark",toolType:InkEraserTool.Segment,ignoreClick:true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
+                    { title: "Area",     toolTip: "Erase like a pencil",    btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkEraserTool.Radius,   ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
             ]},
-            { title: "Eraser Width",  toolTip: "Eraser Width",              btnType: ButtonType.NumberSliderButton,               toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1, funcs: {hidden:"NotRadiusEraser()"}},
-            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",     toolType: Gestures.Circle,      scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
-            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",     toolType: Gestures.Rectangle,   scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
-            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",      toolType: Gestures.Line,        scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
-            { title: "Mask",   toolTip: "Mask",                             btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask",                        scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
-            { title: "Labels", toolTip: "Labels",                           btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels",                         scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
-            { title: "Width",  toolTip: "Stroke width",                     btnType: ButtonType.NumberSliderButton,               toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
-            { title: "Ink",    toolTip: "Stroke color",                     btnType: ButtonType.ColorButton,  icon: "pen",        toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} },
-            { title: "Smart Draw", toolTip: "Draw with GPT",                btnType: ButtonType.ToggleButton, icon: "user-pen",   toolType: "smartdraw",                      scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}},
+            { title: " Size",      toolTip: "Size of area pencil eraser",   btnType: ButtonType.NumberSliderButton,               toolType: "eraserWidth",          ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"NotRadiusEraser()"}, numBtnMin: 1, linearBtnWidth:40},
+            { title: "Mask",       toolTip: "Make Stroke a Stencil Mask",   btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask",                                 scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
+            { title: "Labels",     toolTip: "Show Labels Inside Shapes",    btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels",                                  scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}},
+            { title: "Smart Draw", toolTip: "Draw with GPT",                btnType: ButtonType.ToggleButton, icon: "user-pen",   toolType: "smartdraw",                               scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'},  funcs: {hidden: "IsNoviceMode()"}},
         ];
     }
 
@@ -814,11 +818,11 @@ pie title Minerals in my tap water
             { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         expertMode: false,                                                                                                                             width: 30,  scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}},
             { title: "Header",  icon: "heading",       toolTip: "Doc Titlebar Color",   btnType: ButtonType.ColorButton,         expertMode: false,                                                                                                          ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'} },
             { title: "Template",icon: "scroll",        toolTip: "Default Note Template",btnType: ButtonType.ToggleButton,        expertMode: false, toolType:DocumentType.RTF,                                                                                                              scripts: { onClick: '{ return setDefaultTemplate(_readOnly_); }'} },
-            { title: "Fill",    icon: "fill-drip",     toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton,         expertMode: false,                                                                                                          ignoreClick: true, width: 30,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected
-            { title: "Overlay", icon: "layer-group",   toolTip: "Overlay",              btnType: ButtonType.ToggleButton,        expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'},       scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
-            { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
+            { title: "Fill",    icon: "fill-drip",     toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton,         expertMode: false,                                                                                                          ignoreClick: true, width: 30,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'} }, // Only when a document is selected
+            { title: "Overlay", icon: "layer-group",   toolTip: "Overlay",              btnType: ButtonType.ToggleButton,        expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'},                        scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
+            { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
             { title: "Num",     icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton,          expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
-            { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
+            { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             
             { title: "Filter",  icon: "=",      toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL,            funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
             { title: "Text",    icon: "Text",   toolTip: "Text functions",       subMenu: CurrentUserUtils.textTools(),   expertMode: false, toolType:DocumentType.RTF,            funcs: {                                                                             linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
@@ -863,7 +867,7 @@ pie title Minerals in my tap water
         }  
         // linear view
         const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]),
-            childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: !params.scripts?.onClick,
+            childDontRegisterViews: true, flexGap: 0, _height: 30, _width: 30, ignoreClick: !params.scripts?.onClick,
             linearView_SubMenu: true, linearView_Expandable: true, embedContainer: menuDoc};
         
         const items = (menutBtn?:Doc) => !menutBtn ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menutBtn) );
@@ -987,15 +991,23 @@ pie title Minerals in my tap water
         Doc.noviceMode ?? (Doc.noviceMode = true);
         doc._showLabel ?? (doc._showLabel = true);
         doc.textAlign ?? (doc.textAlign = "left");
+        doc.textBackgroundColor ?? (doc.textBackgroundColor = Colors.LIGHT_GRAY);
         doc.activeTool = InkTool.None;
         doc.openInkInLightbox ?? (doc.openInkInLightbox = false);
-        doc.activeInkHideTextLabels ?? (doc.activeInkHideTextLabels = false);
-        doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");
-        doc.activeInkWidth ?? (doc.activeInkWidth = 1);
-        doc.activeInkBezier ?? (doc.activeInkBezier = "0");
-        doc.activeFillColor ?? (doc.activeFillColor = "");
-        doc.activeArrowStart ?? (doc.activeArrowStart = "");
-        doc.activeArrowEnd ?? (doc.activeArrowEnd = "");
+        doc.activeHideTextLabels ?? (doc.activeHideTextLabels = false);
+        doc[`active${InkInkTool.Pen}Color`]        ?? (doc[`active${InkInkTool.Pen}Color`] = "rgb(0, 0, 0)");
+        doc[`active${InkInkTool.Pen}Width`]        ?? (doc[`active${InkInkTool.Pen}Width`] = 2);
+        doc[`active${InkInkTool.Pen}Bezier`]       ?? (doc[`active${InkInkTool.Pen}Bezier`] = "0");
+        doc[`active${InkInkTool.Write}Color`]      ?? (doc[`active${InkInkTool.Write}Color`] = "rgb(255, 0, 0)");
+        doc[`active${InkInkTool.Write}Width`]      ?? (doc[`active${InkInkTool.Write}Width`] = 1);
+        doc[`active${InkInkTool.Write}Bezier`]     ?? (doc[`active${InkInkTool.Write}Bezier`] = "0");
+        doc[`active${InkInkTool.Highlight}Color`]  ?? (doc[`active${InkInkTool.Highlight}Color`] = 'transparent');
+        doc[`active${InkInkTool.Highlight}Width`]  ?? (doc[`active${InkInkTool.Highlight}Width`] = 20);
+        doc[`active${InkInkTool.Highlight}Bezier`] ?? (doc[`active${InkInkTool.Highlight}Bezier`] = "0");
+        doc[`active${InkInkTool.Highlight}Fill`]   ?? (doc[`active${InkInkTool.Highlight}Fill`] = "rgba(0, 255, 255, 0.4)");
+        doc.activeInkTool ?? (doc.activeInkTool = InkInkTool.Pen);
+        doc.activeEraserTool ?? (doc.activeEraserTool = InkEraserTool.Stroke);
+        doc.activeEraserWidth ?? (doc.activeEraserWidth = 20);
         doc.activeDash ?? (doc.activeDash === "0");
         doc.fontSize ?? (doc.fontSize = "12px");
         doc.fontFamily ?? (doc.fontFamily = "Arial");
@@ -1130,7 +1142,9 @@ pie title Minerals in my tap water
 }
 
 // eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.RadiusEraser; }, "is the active tool anything but the radius eraser");
+ScriptingGlobals.add(function activeInkTool() { return Doc.ActiveTool=== InkTool.Ink || DocumentView.Selected().some(dv => dv.layoutDoc.layout_isSvg); }, "is a pen tool or an ink stroke active");
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.Eraser || Doc.ActiveEraser !== InkEraserTool.Radius; }, "is the active tool anything but the radius eraser");
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function MySharedDocs()    { return Doc.MySharedDocs; }, "document containing all shared Docs");
 // eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 4231c2ca8..ca1cb8014 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -109,7 +109,7 @@ export namespace InteractionUtils {
         dash: string | undefined,
         scalexIn: number,
         scaleyIn: number,
-        shape: Gestures,
+        shape: Gestures | undefined,
         pevents: Property.PointerEvents,
         opacity: number,
         nodefs: boolean,
@@ -136,7 +136,7 @@ export namespace InteractionUtils {
         const arrowLengthFactor = 5 * (markerScale || 0.5);
         const arrowNotchFactor = 2 * (markerScale || 0.5);
         return (
-            <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
+            <svg fill={color || 'transparent'} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
                 {' '}
                 {/* setting the svg fill sets the arrowStart fill */}
                 {nodefs ? null : (
@@ -186,7 +186,7 @@ export namespace InteractionUtils {
                         opacity: 1.0,
                         // opacity: strokeWidth !== width ? 0.5 : undefined,
                         pointerEvents: pevents === 'all' ? 'visiblePainted' : pevents,
-                        stroke: color ?? 'rgb(0, 0, 0)',
+                        stroke: (color ?? 'rgb(0, 0, 0)') || 'transparent',
                         strokeWidth,
                         strokeLinecap: strokeLineCap,
                         strokeDasharray: dashArray,
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 33d7d90a8..628097ca8 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -268,9 +268,9 @@ export class SettingsManager extends React.Component<object> {
                     formLabelPlacement="right"
                     toggleType={ToggleType.SWITCH}
                     onClick={() => {
-                        Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels;
+                        Doc.UserDoc().activeHideTextLabels = !Doc.UserDoc().activeHideTextLabels;
                     }}
-                    toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)}
+                    toggleStatus={BoolCast(Doc.UserDoc().activeHideTextLabels)}
                     size={Size.XSMALL}
                     color={SettingsManager.userColor}
                 />
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 5f6c7d9ac..2a150dc5a 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -1,4 +1,5 @@
 import { observable, action, runInAction, makeObservable } from 'mobx';
+import { Gestures } from '../../pen-gestures/GestureTypes';
 
 export enum freeformScrollMode {
     Pan = 'pan',
@@ -29,6 +30,8 @@ export class SnappingManager {
     @observable _propertyWid: number = 0;
     @observable _printToConsole: boolean = false;
     @observable _hideDecorations: boolean = false;
+    @observable _keepGestureMode: boolean = false; // for whether primitive selection enters a one-shot or persistent mode
+    @observable _inkShape: Gestures | undefined = undefined;
 
     private constructor() {
         SnappingManager._manager = this;
@@ -61,6 +64,8 @@ export class SnappingManager {
     public static get PropertiesWidth(){ return this.Instance._propertyWid; } // prettier-ignore
     public static get PrintToConsole() { return this.Instance._printToConsole; } // prettier-ignore
     public static get HideDecorations(){ return this.Instance._hideDecorations; } // prettier-ignore
+    public static get KeepGestureMode(){ return this.Instance._keepGestureMode; } // prettier-ignore
+    public static get InkShape()       { return this.Instance._inkShape; } // prettier-ignore
 
     public static SetLongPress      = (press: boolean) => runInAction(() => {this.Instance._longPress = press}); // prettier-ignore
     public static SetShiftKey       = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
@@ -78,6 +83,8 @@ export class SnappingManager {
     public static SetPropertiesWidth= (wid:number)     =>runInAction(() => {this.Instance._propertyWid = wid}); // prettier-ignore
     public static SetPrintToConsole = (state:boolean)  =>runInAction(() => {this.Instance._printToConsole = state}); // prettier-ignore
     public static SetHideDecorations= (state:boolean)  =>runInAction(() => {this.Instance._hideDecorations = state}); // prettier-ignore
+    public static SetKeepGestureMode= (state:boolean)  =>runInAction(() => {this.Instance._keepGestureMode = state}); // prettier-ignore
+    public static SetInkShape       = (shape?:Gestures)=>runInAction(() => {this.Instance._inkShape = shape}); // prettier-ignore
 
     public static userColor: string | undefined;
     public static userVariantColor: string | undefined;
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 11425e477..99738052d 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -102,7 +102,7 @@ const HotKeyIconButton: React.FC<HotKeyButtonProps> = observer(({ hotKey /*, sel
 
     return (
         <div
-            className={`filterHotKey-button`}
+            className="filterHotKey-button"
             onClick={e => {
                 e.stopPropagation();
                 state.startEditing();
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 59d37df91..2d6cd03e0 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -2,9 +2,9 @@ import * as fitCurve from 'fit-curve';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
+import { setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction, intersectRect } from '../../Utils';
-import { Doc, Opt, returnEmptyDoclist } from '../../fields/Doc';
+import { Doc } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { NumCast } from '../../fields/Types';
 import { Gestures } from '../../pen-gestures/GestureTypes';
@@ -14,27 +14,25 @@ import { DocumentType } from '../documents/DocumentTypes';
 import { Docs } from '../documents/Documents';
 import { InteractionUtils } from '../util/InteractionUtils';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { Transform } from '../util/Transform';
+import { SnappingManager } from '../util/SnappingManager';
 import { undoable } from '../util/UndoManager';
 import './GestureOverlay.scss';
 import { InkingStroke } from './InkingStroke';
 import { ObservableReactComponent } from './ObservableReactComponent';
-import { returnEmptyDocViewList } from './StyleProvider';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import {
-    ActiveArrowEnd,
-    ActiveArrowScale,
-    ActiveArrowStart,
-    ActiveDash,
-    ActiveFillColor,
-    ActiveInkBezierApprox,
+    ActiveInkArrowEnd,
+    ActiveInkArrowScale,
+    ActiveInkArrowStart,
     ActiveInkColor,
+    ActiveInkDash,
+    ActiveInkFillColor,
     ActiveInkWidth,
     DocumentView,
-    SetActiveArrowStart,
-    SetActiveDash,
-    SetActiveFillColor,
+    SetActiveInkArrowStart,
     SetActiveInkColor,
+    SetActiveInkDash,
+    SetActiveInkFillColor,
     SetActiveInkWidth,
 } from './nodes/DocumentView';
 export enum ToolglassTools {
@@ -57,18 +55,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     // eslint-disable-next-line no-use-before-define
     static Instances: GestureOverlay[] = [];
 
-    @observable public InkShape: Opt<Gestures> = undefined;
     @observable public SavedColor?: string = undefined;
     @observable public SavedWidth?: number = undefined;
     @observable public Tool: ToolglassTools = ToolglassTools.None;
-    @observable public KeepPrimitiveMode = false; // for whether primitive selection enters a one-shot or persistent mode
 
     @observable private _thumbX?: number = undefined;
     @observable private _thumbY?: number = undefined;
     @observable private _pointerY?: number = undefined;
     @observable private _points: { X: number; Y: number }[] = [];
-    @observable private _strokes: InkData[] = [];
-    @observable private _palette?: JSX.Element = undefined;
     @observable private _clipboardDoc?: JSX.Element = undefined;
 
     @computed private get height(): number {
@@ -97,7 +91,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     onPointerDown = (e: React.PointerEvent) => {
         (document.activeElement as HTMLElement)?.blur();
         if (!(e.target as HTMLElement)?.className?.toString().startsWith('lm_')) {
-            if ([InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+            if (Doc.ActiveTool === InkTool.Ink) {
                 this._points.push({ X: e.clientX, Y: e.clientY });
                 setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
             }
@@ -123,13 +117,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     };
 
     @action primCreated() {
-        if (!this.KeepPrimitiveMode) {
-            this.InkShape = undefined;
-            // get out of ink mode after each stroke=
-            // if (Doc.ActiveTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor);
+        if (!SnappingManager.KeepGestureMode) {
+            SnappingManager.SetInkShape(undefined);
             Doc.ActiveTool = InkTool.None;
-            // SetActiveArrowStart('none');
-            // SetActiveArrowEnd('none');
         }
     }
     /**
@@ -253,8 +243,8 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             const B = this.svgBounds;
             const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
             const { Name, Score } =
-                (this.InkShape
-                    ? new Result(this.InkShape, 1, Date.now) 
+                (SnappingManager.InkShape
+                    ? new Result(SnappingManager.InkShape, 1, Date.now) 
                     : Doc.UserDoc().recognizeGestures && points.length > 2
                       ? GestureUtils.GestureRecognizer.Recognize([points])
                       : undefined) ?? 
@@ -292,6 +282,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             (downView?.ComponentView as CollectionFreeFormView)?._marqueeViewRef?.current?.setPreviewCursor?.(this._points[0].X, this._points[0].Y, false, false, undefined);
             e.preventDefault();
         }
+        this.primCreated();
         this._points.length = 0;
     };
     /**
@@ -557,97 +548,38 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         B.bottom += width / 2;
         B.width += width;
         B.height += width;
-        const fillColor = ActiveFillColor();
+        const fillColor = ActiveInkFillColor();
         const strokeColor = fillColor && fillColor !== 'transparent' ? fillColor : ActiveInkColor();
         return [
             this.props.children,
-            this._palette,
-            [
-                this._strokes.map((l, i) => {
-                    const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(l, true);
-                    return (
-                        <svg key={i} width={b.width} height={b.height} style={{ top: 0, left: 0, transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
-                            {InteractionUtils.CreatePolyline(
-                                l,
-                                b.left,
-                                b.top,
-                                strokeColor,
-                                width,
-                                width,
-                                'miter',
-                                'round',
-                                ActiveInkBezierApprox(),
-                                'none' /* ActiveFillColor() */,
-                                ActiveArrowStart(),
-                                ActiveArrowEnd(),
-                                ActiveArrowScale(),
-                                ActiveDash(),
-                                1,
-                                1,
-                                this.InkShape as Gestures,
-                                'none',
-                                1.0,
-                                false
-                            )}
-                        </svg>
-                    );
-                }),
-                this._points.length <= 1 ? null : (
-                    <svg key="svg" width={B.width} height={B.height} style={{ top: 0, left: 0, transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
-                        {InteractionUtils.CreatePolyline(
-                            this._points.map(p => ({ X: p.X - (rect?.x || 0), Y: p.Y - (rect?.y || 0) })),
-                            B.left,
-                            B.top,
-                            ActiveInkColor(),
-                            width,
-                            width,
-                            'miter',
-                            'round',
-                            '',
-                            'none' /* ActiveFillColor() */,
-                            ActiveArrowStart(),
-                            ActiveArrowEnd(),
-                            ActiveArrowScale(),
-                            ActiveDash(),
-                            1,
-                            1,
-                            this.InkShape as Gestures,
-                            'none',
-                            1.0,
-                            false
-                        )}
-                    </svg>
-                ),
-            ],
+            this._points.length <= 1 ? null : (
+                <svg key="svg" width={B.width} height={B.height} style={{ top: 0, left: 0, transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
+                    {InteractionUtils.CreatePolyline(
+                        this._points.map(p => ({ X: p.X - (rect?.x || 0), Y: p.Y - (rect?.y || 0) })),
+                        B.left,
+                        B.top,
+                        strokeColor,
+                        width,
+                        width,
+                        'miter',
+                        'round',
+                        '',
+                        'none' /* ActiveFillColor() */,
+                        ActiveInkArrowStart(),
+                        ActiveInkArrowEnd(),
+                        ActiveInkArrowScale(),
+                        ActiveInkDash(),
+                        1,
+                        1,
+                        SnappingManager.InkShape,
+                        'none',
+                        1.0,
+                        false
+                    )}
+                </svg>
+            ),
         ];
     }
-    screenToLocalTransform = () => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + this.height, 1);
-    return300 = () => 300;
-    @action
-    public openFloatingDoc = (doc: Doc) => {
-        this._clipboardDoc = (
-            <DocumentView
-                Document={doc}
-                addDocument={undefined}
-                addDocTab={returnFalse}
-                pinToPres={emptyFunction}
-                removeDocument={undefined}
-                ScreenToLocalTransform={this.screenToLocalTransform}
-                PanelWidth={this.return300}
-                PanelHeight={this.return300}
-                isDocumentActive={returnFalse}
-                isContentActive={returnFalse}
-                renderDepth={0}
-                styleProvider={returnEmptyString}
-                containerViewPath={returnEmptyDocViewList}
-                focus={emptyFunction}
-                whenChildContentsActiveChanged={emptyFunction}
-                childFiltersByRanges={returnEmptyFilter}
-                childFilters={returnEmptyFilter}
-                searchFilterDocs={returnEmptyDoclist}
-            />
-        );
-    };
 
     @action
     public closeFloatingDoc = () => {
@@ -694,10 +626,10 @@ ScriptingGlobals.add(function setPen(width: string, color: string, fill: string,
         SetActiveInkColor(color);
         GestureOverlay.Instance.SavedWidth = ActiveInkWidth();
         SetActiveInkWidth(width);
-        SetActiveFillColor(fill);
-        SetActiveArrowStart(arrowStart);
-        SetActiveArrowStart(arrowEnd);
-        SetActiveDash(dash);
+        SetActiveInkFillColor(fill);
+        SetActiveInkArrowStart(arrowStart);
+        SetActiveInkArrowStart(arrowEnd);
+        SetActiveInkDash(dash);
     });
 });
 // eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d7d8e9506..f16338361 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -2,7 +2,7 @@ import { random } from 'lodash';
 import { action } from 'mobx';
 import { Doc, DocListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
-import { InkTool } from '../../fields/InkField';
+import { InkInkTool, InkTool } from '../../fields/InkField';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, PromiseValue } from '../../fields/Types';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
@@ -280,10 +280,10 @@ export class KeyManager {
                 }
                 break;
             case 'e':
-                Doc.ActiveTool = [InkTool.StrokeEraser, InkTool.SegmentEraser, InkTool.RadiusEraser].includes(Doc.ActiveTool) ? InkTool.None : InkTool.StrokeEraser;
+                Doc.ActiveTool = Doc.ActiveTool === InkTool.Eraser ? InkTool.None : InkTool.Eraser;
                 break;
             case 'p':
-                Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+                Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
                 break;
             case 'r':
                 preventDefault = false;
@@ -378,7 +378,8 @@ export class KeyManager {
                 UndoManager.Redo();
                 break;
             case 'p':
-                Doc.ActiveTool = InkTool.Write;
+                Doc.ActiveInk = InkInkTool.Write;
+                Doc.ActiveTool = InkTool.Ink;
                 break;
             default:
         }
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 270266a94..5199eb02b 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -470,14 +470,13 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     className="inkStroke"
                     style={{
                         transform: isInkMask ? `rotate(-${NumCast(this._props.LocalRotation?.() ?? 0)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
-                        // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
                         cursor: this._props.isSelected() ? 'default' : undefined,
                     }}
                     {...interactions}>
                     {clickableLine(this.onPointerDown, isInkMask)}
                     {isInkMask ? null : inkLine}
                 </svg>
-                {!closed || this.dataDoc[this.fieldKey + '_showLabel'] === false || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : (
+                {!closed || this.dataDoc[this.fieldKey + '_showLabel'] === false || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeHideTextLabels)) ? null : (
                     <div
                         className="inkStroke-text"
                         style={{
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index a543b4875..3a8a38073 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -214,7 +214,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         // if (this._showPalette === false) AnnotationPalette.Instance.resetPalette(true);
     };
     togglePen = () => {
-        Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+        Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
     };
     toggleExplore = () => SnappingManager.SetExploreMode(!SnappingManager.ExploreMode);
 
@@ -332,7 +332,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                 {toggleBtn('lightboxView-navBtn', 'toggle reading view', BoolCast(this._doc?._layout_fitWidth), 'book-open', 'book', this.toggleFitWidth)}
                 {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-export', '', this.downloadDoc)}
                 {toggleBtn('lightboxView-paletteBtn', 'toggle annotation palette', this._showPalette === true, 'palette', '', this.togglePalette)}
-                {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)}
+                {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Ink, 'pen', '', this.togglePen)}
                 {toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)}
             </div>
         );
diff --git a/src/client/views/StyleProp.ts b/src/client/views/StyleProp.ts
index 44d3bf757..56367e70b 100644
--- a/src/client/views/StyleProp.ts
+++ b/src/client/views/StyleProp.ts
@@ -19,7 +19,9 @@ export enum StyleProp {
     FontColor = 'fontColor', // color o tet
     FontSize = 'fontSize', // size of text font
     FontFamily = 'fontFamily', // font family of text
-    FontWeight = 'fontWeight', // font weight of text
+    FontWeight = 'fontWeight', // font weight of text (eg bold)
+    FontStyle = 'fontStyle', // font style of text (eg italic)
+    FontDecoration = 'fontDecoration', // text decoration of text (eg underline)
     Highlighting = 'highlighting', // border highlighting
     ContextMenuItems = 'contextMenuItems', // menu items to add to context menu
     AnchorMenuItems = 'anchorMenuItems',
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 12c032964..3a5f57908 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -25,6 +25,7 @@ import { styleProviderQuiz } from './StyleProviderQuiz';
 import { StyleProp } from './StyleProp';
 import './StyleProvider.scss';
 import { TagsView } from './TagsView';
+import { InkInkTool } from '../../fields/InkField';
 
 function toggleLockedPosition(doc: Doc) {
     UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -171,7 +172,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
         case StyleProp.FontSize:     return StrCast(doc?.[fieldKey + 'fontSize'],   StrCast(Doc.UserDoc().fontSize));
         case StyleProp.FontFamily:   return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily));
         case StyleProp.FontWeight:   return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(Doc.UserDoc().fontWeight));
-        case StyleProp.FillColor:    return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent')));
+        case StyleProp.FontStyle:    return StrCast(doc?.[fieldKey + 'fontStyle'], StrCast(Doc.UserDoc().fontStyle));
+        case StyleProp.FontDecoration:return StrCast(doc?.[fieldKey + 'fontDecoration'], StrCast(Doc.UserDoc().fontDecoration));
+        case StyleProp.FillColor:    return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, StrCast(Doc.UserDoc()[Doc.ActiveInk === InkInkTool.Highlight ? "inkHighlighterColor": "inkFillColor"], 'transparent'))));
         case StyleProp.ShowCaption:  return hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption);
         case StyleProp.TitleHeight:  return Math.min(4,(docView?.().screenToViewTransform().Scale ?? 1)) * NumCast(Doc.UserDoc().headerHeight,30);
         case StyleProp.ShowTitle:    return (
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e1786d2c9..3e8138390 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -453,7 +453,7 @@ export class CollectionDockingView extends CollectionSubView() {
                 }
             }
         }
-        if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+        if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && Doc.ActiveTool !== InkTool.Ink) {
             e.stopPropagation();
         }
     };
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
index 5d8373fc7..8b9a3e0ec 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
@@ -29,7 +29,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
     }
     _firstDocPos = { x: 0, y: 0 };
 
-    constructor(props: any) {
+    constructor(props: CollectionFreeFormInfoUIProps) {
         super(props);
         makeObservable(this);
         this._currState = this.setupStates();
@@ -163,7 +163,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
                     return presentDocs;
                 }],
                 // eslint-disable-next-line no-use-before-define
-                activePen: [() => activeTool() === InkTool.Pen, () => penMode],
+                activePen: [() => activeTool() === InkTool.Ink, () => penMode],
             },
             'documentation.png',
             () => TopBar.Instance.FlipDocumentationIcon()
@@ -187,7 +187,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
 
         const penMode = InfoState('You\'re in pen mode. Click and drag to draw your first masterpiece.', {
             // activePen: [() => activeTool() === InkTool.Eraser, () => eraserMode],
-            activePen: [() => activeTool() !== InkTool.Pen, () => viewedLink],
+            activePen: [() => activeTool() !== InkTool.Ink, () => viewedLink],
         }); // prettier-ignore
 
         // const eraserMode = InfoState('You\'re in eraser mode. Say goodbye to your first masterpiece.', {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ff711314b..f2a5780c5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -10,7 +10,7 @@ import { DateField } from '../../../../fields/DateField';
 import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
 import { DocData, Height, Width } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
-import { InkData, InkField, InkTool, Segment } from '../../../../fields/InkField';
+import { InkData, InkEraserTool, InkField, InkInkTool, InkTool, Segment } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { listSpec } from '../../../../fields/Schema';
@@ -36,7 +36,20 @@ import { ContextMenu } from '../../ContextMenu';
 import { InkingStroke } from '../../InkingStroke';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
 import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveEraserWidth, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, SetActiveInkColor, SetActiveInkWidth } from '../../nodes/DocumentView';
+import {
+    ActiveInkArrowEnd,
+    ActiveInkArrowStart,
+    ActiveInkDash,
+    ActiveEraserWidth,
+    ActiveInkFillColor,
+    ActiveInkBezierApprox,
+    ActiveInkColor,
+    ActiveInkWidth,
+    ActiveIsInkMask,
+    DocumentView,
+    SetActiveInkColor,
+    SetActiveInkWidth,
+} from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { FocusViewOptions } from '../../nodes/FocusViewOptions';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -497,13 +510,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // prettier-ignore
                 const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
                 switch (Doc.ActiveTool) {
-                    case InkTool.Highlighter:
-                    case InkTool.Write:
-                    case InkTool.Pen:
+                    case InkTool.Ink:
                         break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
-                    case InkTool.StrokeEraser:
-                    case InkTool.SegmentEraser:
-                    case InkTool.RadiusEraser:
+                    case InkTool.Eraser:
                         this._batch = UndoManager.StartBatch('collectionErase');
                         this._eraserPts.length = 0;
                         setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, this.onEraserClick, hit !== -1);
@@ -543,7 +552,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const { points } = ge;
                 const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
                 const inkDoc = this.createInkDoc(points, B);
-                if (Doc.ActiveTool === InkTool.Write) {
+                if (Doc.ActiveInk === InkInkTool.Highlight) inkDoc[DocData].backgroundColor = 'transparent';
+                if (Doc.ActiveInk === InkInkTool.Write) {
                     this.unprocessedDocs.push(inkDoc);
                     CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
                 }
@@ -606,7 +616,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const currPoint = { X: e.clientX, Y: e.clientY };
         this._eraserPts.push([currPoint.X, currPoint.Y]);
         this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
-        if (Doc.ActiveTool === InkTool.RadiusEraser) {
+        if (Doc.ActiveEraser === InkEraserTool.Radius) {
             const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
             strokeMap.forEach((intersects, stroke) => {
                 if (!this._deleteList.includes(stroke)) {
@@ -614,13 +624,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
                     SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
                     const segments = this.radiusErase(stroke, intersects.sort());
-                    segments?.forEach(segment =>
-                        this.forceStrokeGesture(
-                            e,
-                            Gestures.Stroke,
-                            segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
-                        )
-                    );
+                    segments?.forEach(segment => {
+                        const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
+                        const bounds = InkField.getBounds(points);
+                        const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+                        const inkDoc = this.createInkDoc(points, B);
+                        ['color', 'fillColor', 'stroke_width', 'stroke_dash', 'stroke_bezier'].forEach(field => {
+                            inkDoc[DocData][field] = stroke.dataDoc[field];
+                        });
+                        this.addDocument(inkDoc);
+                    });
                 }
                 stroke.layoutDoc.opacity = 0;
                 stroke.layoutDoc.dontIntersect = true;
@@ -632,7 +645,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1');
                     SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black');
                     // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
-                    if (Doc.ActiveTool !== InkTool.StrokeEraser) {
+                    if (Doc.ActiveEraser !== InkEraserTool.Stroke) {
                         // this._eraserLock++;
                         const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
                         const newStrokes = segments?.map(segment => {
@@ -1195,14 +1208,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 y: B.y - inkWidth / 2,
                 _width: B.width + inkWidth,
                 _height: B.height + inkWidth,
-                stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+                stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
             inkWidth,
             ActiveInkColor(),
             ActiveInkBezierApprox(),
-            ActiveFillColor(),
-            ActiveArrowStart(),
-            ActiveArrowEnd(),
-            ActiveDash(),
+            ActiveInkFillColor(),
+            ActiveInkArrowStart(),
+            ActiveInkArrowEnd(),
+            ActiveInkDash(),
             ActiveIsInkMask()
         );
     };
@@ -1235,14 +1248,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     y: B.y - inkWidth / 2,
                     _width: B.width + inkWidth,
                     _height: B.height + inkWidth,
-                    stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+                    stroke_showLabel: BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
                 inkWidth,
                 opts.autoColor ? stroke[1] : ActiveInkColor(),
                 ActiveInkBezierApprox(),
-                stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
-                ActiveArrowStart(),
-                ActiveArrowEnd(),
-                ActiveDash(),
+                stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2],
+                ActiveInkArrowStart(),
+                ActiveInkArrowEnd(),
+                ActiveInkDash(),
                 ActiveIsInkMask()
             );
             this._drawing.push(inkDoc);
@@ -2195,7 +2208,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     width: `${100 / this.nativeDimScaling}%`,
                     height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
                 }}>
-                {Doc.ActiveTool === InkTool.RadiusEraser && this._showEraserCircle && (
+                {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
                     <div
                         onPointerMove={this.onCursorMove}
                         style={{
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index c865c681d..ddc50871d 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -690,7 +690,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
                 }}
                 style={{
                     overflow: StrCast(this._props.Document._overflow),
-                    cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer',
+                    cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
                 }}
                 onDragOver={e => e.preventDefault()}
                 onScroll={e => {
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 013b18a97..40144c4ce 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -3,7 +3,7 @@ import { Colors } from 'browndash-components';
 import { runInAction } from 'mobx';
 import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
-import { InkTool } from '../../../fields/InkField';
+import { InkEraserTool, InkInkTool, InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
@@ -16,21 +16,20 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager, undoable } from '../../util/UndoManager';
 import { GestureOverlay } from '../GestureOverlay';
 import { InkTranscription } from '../InkTranscription';
-import { InkingStroke } from '../InkingStroke';
 import { PropertiesView } from '../PropertiesView';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import {
     ActiveEraserWidth,
-    ActiveFillColor,
+    ActiveInkFillColor,
     ActiveInkColor,
-    ActiveInkHideTextLabels,
+    ActiveHideTextLabels,
     ActiveInkWidth,
     ActiveIsInkMask,
     DocumentView,
-    SetActiveFillColor,
+    SetActiveInkFillColor,
     SetActiveInkColor,
-    SetActiveInkHideTextLabels,
+    SetactiveHideTextLabels,
     SetActiveInkWidth,
     SetActiveIsInkMask,
     SetEraserWidth,
@@ -60,20 +59,21 @@ ScriptingGlobals.add(function setView(view: string, getSelected: boolean) {
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
     const selectedViews = DocumentView.Selected();
+    const selectedDoc = selectedViews.lastElement()?.Document;
+    const defaultFill = selectedDoc?._layout_isSvg ? () => StrCast(selectedDoc[DocData].fillColor) : !Doc.ActiveTool || Doc.ActiveTool === InkTool.None ? () => StrCast(Doc.UserDoc().textBackgroundColor, 'transparent') : () => ActiveInkFillColor();
+    const setDefaultFill = !Doc.ActiveTool || Doc.ActiveTool === InkTool.None  ? (c: string) => { Doc.UserDoc().textBackgroundColor = c; }: SetActiveInkFillColor; // prettier-ignore
     if (Doc.ActiveTool !== InkTool.None && !selectedViews.lastElement()?.Document._layout_isSvg) {
-        if (checkResult) {
-            return ActiveFillColor();
-        }
-        SetActiveFillColor(color ?? 'transparent');
+        if (checkResult) return defaultFill();
+        setDefaultFill(color ?? 'transparent');
     } else if (selectedViews.length) {
         if (checkResult) {
             const selView = selectedViews.lastElement();
             const fieldKey = selView.Document._layout_isSvg ? 'fillColor' : 'backgroundColor';
             const layoutFrameNumber = Cast(selView.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
             const contentFrameNumber = Cast(selView.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
-            return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] ?? 'transparent';
+            return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] || defaultFill();
         }
-        selectedViews.some(dv => dv.ComponentView instanceof InkingStroke) && SetActiveFillColor(color ?? 'transparent');
+        setDefaultFill(color ?? 'transparent');
         selectedViews.forEach(dv => {
             const fieldKey = dv.Document._layout_isSvg ? 'fillColor' : 'backgroundColor';
             const layoutFrameNumber = Cast(dv.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
@@ -92,10 +92,13 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
     } else {
         const selected = DocumentView.SelectedDocs().length ? DocumentView.SelectedDocs() : LinkManager.Instance.currentLink ? [LinkManager.Instance.currentLink] : [];
         if (checkResult) {
-            return selected.lastElement()?._backgroundColor ?? 'transparent';
+            return selected.lastElement()?._backgroundColor ?? defaultFill();
         }
-        SetActiveFillColor(color ?? 'transparent');
-        selected.forEach(doc => { doc[DocData].backgroundColor = color; }); // prettier-ignore
+        if (!selected.length) setDefaultFill(color ?? 'transparent');
+        else
+            selected.forEach(doc => {
+                doc[DocData][doc._layout_isSvg ? 'fillColor' : 'backgroundColor'] = color;
+            });
     }
     return '';
 });
@@ -330,7 +333,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
     return undefined;
 });
 
-type attrname = 'noAutoLink' | 'dictation' | 'fitBox' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
+type attrname = 'noAutoLink' | 'dictation' | 'fitBox' | 'bold' | 'italic' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
 type attrfuncs = [attrname, { checkResult: () => boolean; toggle?: () => unknown }];
 
 // eslint-disable-next-line prefer-arrow-callback
@@ -360,10 +363,10 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
                         toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
         ['bold',      { checkResult: () => (editorView ? RichTextMenu.Instance?.bold??false : (Doc.UserDoc().fontWeight === 'bold')),
                         toggle: editorView ? RichTextMenu.Instance?.toggleBold : () => { Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold'; }}],
-        ['italics',   { checkResult: () => (editorView ? RichTextMenu.Instance?.italics ?? false : (Doc.UserDoc().fontStyle === 'italics')),
-                        toggle: editorView ? RichTextMenu.Instance?.toggleItalics : () => { Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics'; }}],
-        ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().textDecoration === 'underline')),
-                        toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => { Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline'; } }]]
+        ['italic',    { checkResult: () => (editorView ? RichTextMenu.Instance?.italic ?? false : (Doc.UserDoc().fontStyle === 'italic')),
+                        toggle: editorView ? RichTextMenu.Instance?.toggleItalic : () => { Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italic' ? undefined : 'italic'; }}],
+        ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().fontDecoration === 'underline')),
+                        toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => { Doc.UserDoc().fontDecoration = Doc.UserDoc().fontDecoration === 'underline' ? undefined : 'underline'; } }]]
 
     const map = new Map(attrs.concat(alignments).concat(listings));
     if (checkResult) {
@@ -373,43 +376,46 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
     return undefined;
 });
 
-function setActiveTool(toolIn: InkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
+function setActiveTool(tool: InkTool | InkEraserTool | InkInkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
     InkTranscription.Instance?.createInkGroup();
-    const tool = toolIn === InkTool.Eraser ? Doc.UserDoc().activeEraserTool : toolIn;
     if (checkResult) {
-        return ((Doc.ActiveTool === tool || (Doc.UserDoc().activeEraserTool === tool && (tool === toolIn || Doc.ActiveTool === tool))) && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
-            ? GestureOverlay.Instance?.KeepPrimitiveMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures)
+        return Doc.ActiveTool === tool || Doc.ActiveEraser === tool || Doc.ActiveInk === tool || SnappingManager.InkShape === tool
+            ? true //SnappingManager.KeepGestureMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures)
             : false;
     }
     runInAction(() => {
+        const eraserTool = tool === InkTool.Eraser ? Doc.ActiveEraser : [InkEraserTool.Stroke, InkEraserTool.Radius, InkEraserTool.Segment].includes(tool as InkEraserTool) ? (tool as InkEraserTool) : undefined;
+        const inkTool = tool === InkTool.Ink ? Doc.ActiveInk : [InkInkTool.Pen, InkInkTool.Write, InkInkTool.Highlight].includes(tool as InkInkTool) ? (tool as InkInkTool) : undefined;
         if (GestureOverlay.Instance) {
-            GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
+            SnappingManager.SetKeepGestureMode(keepPrim);
         }
         if (Object.values(Gestures).includes(tool as Gestures)) {
-            if (GestureOverlay.Instance.InkShape === tool && !keepPrim) {
+            if (SnappingManager.InkShape === tool && !keepPrim) {
                 Doc.ActiveTool = InkTool.None;
-                GestureOverlay.Instance.InkShape = undefined;
+                SnappingManager.SetInkShape(undefined);
             } else {
-                Doc.ActiveTool = InkTool.Pen;
-                GestureOverlay.Instance.InkShape = tool as Gestures;
+                Doc.ActiveTool = InkTool.Ink;
+                SnappingManager.SetInkShape(tool as Gestures);
             }
-        } else if (tool) {
-            if (Doc.UserDoc().ActiveTool === tool) {
+        } else if (eraserTool) {
+            if (Doc.ActiveTool === InkTool.Eraser && Doc.ActiveTool === tool) {
                 Doc.ActiveTool = InkTool.None;
             } else {
-                if ([InkTool.StrokeEraser, InkTool.RadiusEraser, InkTool.SegmentEraser].includes(tool as InkTool)) {
-                    Doc.UserDoc().activeEraserTool = tool;
-                }
-                // pen or eraser
-                if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
-                    Doc.ActiveTool = InkTool.None;
-                } else {
-                    Doc.ActiveTool = tool as InkTool;
-                    GestureOverlay.Instance.InkShape = undefined;
-                }
+                Doc.ActiveEraser = eraserTool;
+                Doc.ActiveTool = InkTool.Eraser;
+                SnappingManager.SetInkShape(undefined);
+            }
+        } else if (inkTool) {
+            if (Doc.ActiveTool === InkTool.Ink && Doc.ActiveTool === tool) {
+                Doc.ActiveTool = InkTool.None;
+            } else {
+                Doc.ActiveInk = inkTool;
+                Doc.ActiveTool = InkTool.Ink;
+                SnappingManager.SetInkShape(undefined);
             }
         } else {
-            Doc.ActiveTool = InkTool.None;
+            if ((Doc.ActiveTool === tool || !tool) && !keepPrim) Doc.ActiveTool = InkTool.None;
+            else Doc.ActiveTool = tool as InkTool;
         }
     });
     return undefined;
@@ -419,44 +425,44 @@ ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function activeEraserTool() {
-    return StrCast(Doc.UserDoc().activeEraserTool, InkTool.StrokeEraser);
+    return StrCast(Doc.UserDoc().activeEraserTool, InkEraserTool.Stroke);
 }, 'returns the current eraser tool');
 
 // toggle: Set overlay status of selected document
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', value: string | number, checkResult?: boolean) {
-    const selected = DocumentView.SelectedDocs().lastElement() ?? Doc.UserDoc();
+    const selected = DocumentView.SelectedDocs().lastElement();
     // prettier-ignore
     const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', { checkResult: () => number|boolean|string|undefined; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
         ['inkMask', {
             checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_isInkMask) : ActiveIsInkMask())),
             setInk: (doc: Doc) => { doc[DocData].stroke_isInkMask = !doc.stroke_isInkMask; },
-            setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()),
+            setMode: () => SetActiveIsInkMask(value ? true : false)
         }],
         ['labels', {
-            checkResult: () => ((selected?._stroke_showLabel ? BoolCast(selected[DocData].stroke_showLabel) : ActiveInkHideTextLabels())),
-            setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = !doc.stroke_showLabel; },
-            setMode: () => selected?.type !== DocumentType.INK && SetActiveInkHideTextLabels(!ActiveInkHideTextLabels()),
+            checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_showLabel) : !ActiveHideTextLabels())),
+            setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = value; },
+            setMode: () => SetactiveHideTextLabels(value? false : true),
         }],
         ['fillColor', {
-            checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveFillColor() ?? "transparent"),
+            checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveInkFillColor() ?? "transparent"),
             setInk: (doc: Doc) => { doc[DocData].fillColor = StrCast(value); },
-            setMode: () => SetActiveFillColor(StrCast(value)),
+            setMode: () => SetActiveInkFillColor(StrCast(value)),
         }],
         [ 'strokeWidth', {
             checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width) : ActiveInkWidth()),
             setInk: (doc: Doc) => { doc[DocData].stroke_width = NumCast(value); },
-            setMode: () => { SetActiveInkWidth(value.toString()); selected?.type === DocumentType.INK && setActiveTool( GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
+            setMode: () => SetActiveInkWidth(value.toString()),
         }],
         ['strokeColor', {
             checkResult: () => (selected?._layout_isSvg? StrCast(selected[DocData].color) : ActiveInkColor()),
             setInk: (doc: Doc) => { doc[DocData].color = String(value); },
-            setMode: () => { SetActiveInkColor(StrCast(value)); selected?.type === DocumentType.INK &&  setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
+            setMode: () => SetActiveInkColor(StrCast(value))
         }],
         [ 'eraserWidth', {
             checkResult: () => ActiveEraserWidth() === 0 ? 1 : ActiveEraserWidth(),
             setInk: (doc: Doc) => { },
-            setMode: () => { SetEraserWidth(+value);},
+            setMode: () => SetEraserWidth(+value),
         }]
     ]);
 
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
index 3eb99f47a..8115bafbf 100644
--- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable jsx-a11y/no-static-element-interactions */
-/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { action } from 'mobx';
 import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
@@ -35,10 +33,10 @@ export function ButtonMenu() {
             <div
                 className="newLightboxView-penBtn"
                 title="toggle pen annotation"
-                style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }}
+                style={{ background: Doc.ActiveTool === InkTool.Ink ? 'white' : undefined }}
                 onClick={e => {
                     e.stopPropagation();
-                    Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+                    Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
                 }}
             />
             <div
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index d51b1cd3a..a9edc90e7 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,7 +1,8 @@
+import { Colors } from 'browndash-components';
 import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { OmitKeys } from '../../../ClientUtils';
+import { DashColor, OmitKeys } from '../../../ClientUtils';
 import { numberRange } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { TransitionTimer } from '../../../fields/DocSymbols';
@@ -296,12 +297,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     transition: this.DataTransition(),
                     zIndex: this.ZIndex,
                     display: this.Width ? undefined : 'none',
+                    mixBlendMode: DashColor(StrCast(this.layoutDoc[this.layoutDoc._layout_isSvg ? 'fillColor' : 'backgroundColor'], Colors.WHITE)).alpha() !== 1 ? 'multiply' : undefined,
                 }}>
                 {this.RenderCutoffProvider(this.Document) ? (
                     <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} />
                 ) : (
                     <DocumentView
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
                         Document={this._props.Document}
                         renderDepth={this._props.renderDepth}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index d79a37181..7925410e2 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -73,7 +73,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return <div className="dataVizBox-annotationLayer" style={{ height: this._props.PanelHeight(), width: this._props.PanelWidth() }} ref={this._annotationLayer} />;
     }
     marqueeDown = (e: React.PointerEvent) => {
-        if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             setupMoveUpEvents(
                 this,
                 e,
@@ -453,7 +453,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @action
     onPointerDown = (e: React.PointerEvent): void => {
         if ((this.Document._freeform_scale || 1) !== 1) return;
-        if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             this._props.select(false);
             MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
             this._marqueeing = [e.clientX, e.clientY];
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index eb7f333b9..30f9e6363 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -380,7 +380,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     };
 
     onPointerMove = (e: PointerEvent): void => {
-        if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
+        if (e.buttons !== 1 || Doc.ActiveTool === InkTool.Ink) return;
 
         if (!ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
             this.cleanupPointerEvents();
@@ -1560,55 +1560,45 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         } else func();
     }
 }
-
-export function ActiveFillColor(): string { 
-    const dv = DocumentView.Selected().lastElement() ?.Document._layout_isSvg ? DocumentView.Selected().lastElement() : undefined;
-    return StrCast(dv?.Document.fillColor, StrCast(ActiveInkPen()?.activeFillColor, "")); 
-} // prettier-ignore
-export function ActiveInkPen(): Doc { return Doc.UserDoc(); } // prettier-ignore
-export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, 'black'); } // prettier-ignore
-export function ActiveIsInkMask(): boolean { return BoolCast(ActiveInkPen()?.activeIsInkMask, false); } // prettier-ignore
-export function ActiveInkHideTextLabels(): boolean { return BoolCast(ActiveInkPen().activeInkHideTextLabels, false); } // prettier-ignore
-export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ''); } // prettier-ignore
-export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ''); } // prettier-ignore
-export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } // prettier-ignore
-export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore
-export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore
-export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore
-export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth ?? 25); } // prettier-ignore
-
+export function ActiveHideTextLabels(): boolean { return BoolCast(Doc.UserDoc().activeHideTextLabels, false); } // prettier-ignore
+export function ActiveIsInkMask(): boolean { return BoolCast(Doc.UserDoc()?.activeIsInkMask, false); } // prettier-ignore
+export function ActiveEraserWidth(): number { return Number(Doc.UserDoc()?.activeEraserWidth ?? 25); } // prettier-ignore
+
+export function ActiveInkFillColor(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Fill`]); } // prettier-ignore
+export function ActiveInkColor(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Color`], 'black'); } // prettier-ignore
+export function ActiveInkArrowStart(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowStart`], ''); } // prettier-ignore
+export function ActiveInkArrowEnd(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowEnd`], ''); } // prettier-ignore
+export function ActiveInkArrowScale(): number { return NumCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowScale`], 1); } // prettier-ignore
+export function ActiveInkDash(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Dash`], '0'); } // prettier-ignore
+export function ActiveInkWidth(): number { return Number(Doc.UserDoc()?.[`active${Doc.ActiveInk}Width`]); } // prettier-ignore
+export function ActiveInkBezierApprox(): string { return StrCast(Doc.UserDoc()[`active${Doc.ActiveInk}Bezier`]); } // prettier-ignore
+
+export function SetActiveIsInkMask(value: boolean) { Doc.UserDoc() && (Doc.UserDoc().activeIsInkMask = value); } // prettier-ignore
+export function SetactiveHideTextLabels(value: boolean) { Doc.UserDoc() && (Doc.UserDoc().activeHideTextLabels = value); } // prettier-ignore
+export function SetEraserWidth(width: number): void { Doc.UserDoc() && (Doc.UserDoc().activeEraserWidth = width); } // prettier-ignore
 export function SetActiveInkWidth(width: string): void {
-    !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width);
+    !isNaN(parseInt(width)) && Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Width`] = width);
 }
-export function SetActiveBezierApprox(bezier: string): void {
-    ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier);
+export function SetActiveInkBezierApprox(bezier: string): void {
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Bezier`] = isNaN(parseInt(bezier)) ? '' : bezier);
 }
 export function SetActiveInkColor(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeInkColor = value);
-}
-export function SetActiveIsInkMask(value: boolean) {
-    ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value);
-}
-export function SetActiveInkHideTextLabels(value: boolean) {
-    ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value);
-}
-export function SetActiveFillColor(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeFillColor = value);
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Color`] = value);
 }
-export function SetActiveArrowStart(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowStart = value);
+export function SetActiveInkFillColor(value: string) {
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Fill`] = value);
 }
-export function SetActiveArrowEnd(value: string) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value);
+export function SetActiveInkArrowStart(value: string) {
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowStart`] = value);
 }
-export function SetActiveArrowScale(value: number) {
-    ActiveInkPen() && (ActiveInkPen().activeArrowScale = value);
+export function SetActiveInkArrowEnd(value: string) {
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowEnd`] = value);
 }
-export function SetActiveDash(dash: string): void {
-    !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash);
+export function SetActiveInkArrowScale(value: number) {
+    Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowScale`] = value);
 }
-export function SetEraserWidth(width: number): void {
-    ActiveInkPen() && (ActiveInkPen().eraserWidth = width);
+export function SetActiveInkDash(dash: string): void {
+    !isNaN(parseInt(dash)) && Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}`] = dash);
 }
 
 // eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index ab03a2318..2405889cf 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -13,6 +13,9 @@
 .fonticonbox {
     margin: auto;
     width: 100%;
+    .formLabel {
+        height: 5px;
+    }
 }
 .menuButton {
     height: 100%;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index b45774a75..8c138c2ee 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -4,7 +4,7 @@ import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { ClientUtils, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
+import { ClientUtils, DashColor, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction } from '../../../../Utils';
@@ -21,6 +21,8 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { OpenWhere } from '../OpenWhere';
 import './FontIconBox.scss';
 import TrailsIcon from './TrailsIcon';
+import { InkTool } from '../../../../fields/InkField';
+import { ScriptField } from '../../../../fields/ScriptField';
 
 export enum ButtonType {
     TextButton = 'textBtn',
@@ -126,7 +128,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                 background={SnappingManager.userBackgroundColor}
                 numberDropdownType={type}
                 showPlusMinus={false}
-                tooltip={this.label}
+                formLabel={(StrCast(this.Document.title).startsWith(' ') ? '\u00A0' : '') + StrCast(this.Document.title)}
+                tooltip={StrCast(this.Document.toolTip, this.label)}
                 type={Type.PRIM}
                 min={NumCast(this.dataDoc.numBtnMin, 0)}
                 max={NumCast(this.dataDoc.numBtnMax, 100)}
@@ -148,65 +151,80 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         return false;
     };
 
+    /**
+     * Displays custom dropdown menu for fonts -- this is a HACK -- fix for generality, don't copy
+     */
+    handleFontDropdown = (script: () => string, buttonList: string[]) => {
+        // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
+        return {
+            buttonList,
+            jsx: undefined,
+            selectedVal: script(),
+            getStyle: (val: string) => ({ fontFamily: val }),
+        };
+    };
+    /**
+     * Displays custom dropdown menu for view selection -- this is a HACK -- fix for generality, don't copy
+     */
+    handleViewDropdown = (script: ScriptField, buttonList: string[]) => {
+        const selected = Array.from(script?.script.run({ _readOnly_: true }).result as Doc[]);
+        const noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Card, CollectionViewType.Carousel3D, CollectionViewType.Carousel, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
+        return selected.length === 1 && selected[0].type === DocumentType.COL
+            ? {
+                  buttonList: buttonList.filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value as CollectionViewType)),
+                  getStyle: undefined,
+                  selectedVal: StrCast(selected[0]._type_collection),
+              }
+            : {
+                  jsx: selected.length ? (
+                      <Popup
+                          icon={<FontAwesomeIcon size="1x" icon={selected.length > 1 ? 'caret-down' : (Doc.toIcon(selected.lastElement()) as IconProp)} />}
+                          text={selected.length === 1 ? ClientUtils.cleanDocumentType(StrCast(selected[0].type) as DocumentType) : selected.length + ' selected'}
+                          type={Type.TERT}
+                          color={SnappingManager.userColor}
+                          background={SnappingManager.userVariantColor}
+                          popup={<SelectedDocView selectedDocs={selected} />}
+                          fillWidth
+                      />
+                  ) : (
+                      <Button
+                          text={`${Doc.ActiveTool === InkTool.None ? 'Text box' : Doc.ActiveInk} defaults`} //
+                          type={Type.TERT}
+                          color={SnappingManager.userColor}
+                          background={SnappingManager.userVariantColor}
+                          fillWidth
+                          inactive
+                      />
+                  ),
+              };
+    };
+
     /**
      * Dropdown list
      */
     @computed get dropdownListButton() {
         const script = ScriptCast(this.Document.script);
-
-        let noviceList: string[] = [];
-        let text: string | undefined;
-        let getStyle: (val: string) => { [key: string]: string } = () => ({});
-        let icon: IconProp = 'caret-down';
-        const isViewDropdown = script?.script.originalScript.startsWith('{ return setView');
-        if (isViewDropdown) {
-            const selected = Array.from(script?.script.run({ _readOnly_: true }).result as Doc[]);
-            // const selected = DocumentView.SelectedDocs();
-            if (selected.lastElement()) {
-                if (StrCast(selected.lastElement().type) === DocumentType.COL) {
-                    text = StrCast(selected.lastElement()._type_collection);
-                } else {
-                    if (selected.length > 1) {
-                        text = selected.length + ' selected';
-                    } else {
-                        text = ClientUtils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType, '' as CollectionViewType);
-                        icon = Doc.toIcon(selected.lastElement());
-                    }
-                    return (
-                        <Popup
-                            icon={<FontAwesomeIcon size="1x" icon={icon} />}
-                            text={text}
-                            type={Type.TERT}
-                            color={SnappingManager.userColor}
-                            background={SnappingManager.userVariantColor}
-                            popup={<SelectedDocView selectedDocs={selected} />}
-                            fillWidth
-                        />
-                    );
-                }
-            } else {
-                return <Button text="None Selected" type={Type.TERT} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} fillWidth inactive />;
-            }
-            noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Card, CollectionViewType.Carousel3D, CollectionViewType.Carousel, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
-        } else {
-            text = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
-            // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
-            if (this.Document.title === 'Font') getStyle = (val: string) => ({ fontFamily: val }); // bcz: major hack to style the font dropdown items --- needs to become part of the dropdown's metadata
-        }
+        const selectedFunc = () => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
+        const { buttonList, selectedVal, getStyle, jsx } = (() => {
+            switch (this.Document.title) {
+                case 'Font':         return this.handleFontDropdown(selectedFunc, this.buttonList);
+                case 'Perspective':  return this.handleViewDropdown(script, this.buttonList);
+                default:             return { buttonList: this.buttonList, selectedVal: selectedFunc(), jsx: undefined, getStyle: undefined };
+            } // prettier-ignore
+        })();
+        if (jsx) return jsx;
 
         // Get items to place into the list
-        const list: IListItemProps[] = this.buttonList
-            .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value))
-            .map(value => ({
-                text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title),
-                val: value,
-                style: getStyle(value),
-                // shortcut: '#',
-            }));
+        const list: IListItemProps[] = buttonList.map(value => ({
+            text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title),
+            val: value,
+            style: getStyle?.(value),
+            // shortcut: '#',
+        }));
 
         return (
             <Dropdown
-                selectedVal={text}
+                selectedVal={selectedVal}
                 setSelectedVal={undoable(value => script.script.run({ this: this.Document, value }), `dropdown select ${this.label}`)}
                 color={SnappingManager.userColor}
                 background={SnappingManager.userVariantColor}
@@ -215,7 +233,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                 dropdownType={DropdownType.SELECT}
                 onItemDown={this.dropdownItemDown}
                 items={list}
-                tooltip={this.label}
+                tooltip={StrCast(this.Document.toolTip, this.label)}
                 fillWidth
             />
         );
@@ -235,51 +253,53 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         const tooltip: string = StrCast(this.Document.toolTip);
 
         return (
-            <ColorPicker
-                setSelectedColor={value => {
-                    if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
-                    this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
-                }}
-                setFinalColor={value => {
-                    this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
-                    this.colorBatch?.end();
-                    this.colorBatch = undefined;
-                }}
-                defaultPickerType="Classic"
-                selectedColor={curColor}
-                type={Type.PRIM}
-                color={color}
-                background={SnappingManager.userBackgroundColor}
-                icon={this.Icon(color) ?? undefined}
-                tooltip={tooltip}
-                label={this.label}
-            />
+            <div
+                onPointerDown={e => {
+                    e.stopPropagation();
+                }}>
+                <ColorPicker
+                    setSelectedColor={value => {
+                        if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
+                        this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+                    }}
+                    setFinalColor={value => {
+                        this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+                        this.colorBatch?.end();
+                        this.colorBatch = undefined;
+                    }}
+                    defaultPickerType="Classic"
+                    selectedColor={curColor}
+                    type={Type.PRIM}
+                    color={color}
+                    background={SnappingManager.userBackgroundColor}
+                    icon={this.Icon(color) ?? undefined}
+                    tooltip={tooltip}
+                    label={this.label}
+                />
+            </div>
         );
     }
     @computed get multiToggleButton() {
-        // Determine the type of toggle button
-        const tooltip: string = StrCast(this.Document.toolTip);
+        const tooltip = StrCast(this.Document.toolTip);
 
         const script = ScriptCast(this.Document.onClick)?.script;
         const toggleStatus = script?.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean;
 
-        // Colors
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
-        const background = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string;
         const items = DocListCast(this.dataDoc.data);
         const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType));
 
         return (
             <MultiToggle
-                tooltip={`Toggle ${tooltip}`}
+                tooltip={`Click to Toggle ${tooltip} or select new option`}
                 type={Type.PRIM}
                 color={color}
-                background={background === SnappingManager.userBackgroundColor ? undefined : background}
+                background={undefined}
                 multiSelect={true}
                 onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))}
                 isToggle={false}
                 toggleStatus={toggleStatus}
-                label={this.label}
+                label={selectedItems.length === 1 ? selectedItems[0] : this.label}
                 items={items.map(item => ({
                     icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />,
                     tooltip: StrCast(item.toolTip),
@@ -292,7 +312,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                     //   it would be better to pas the 'added' flag to the callback script, but our script generator from currentUserUtils makes it hard to define
                     //   arbitrary parameter variables (but it could be done as a special case or with additional effort when creating the sript)
                     const itemsChanged = items.filter(item => (val instanceof Array ? val.includes(item.toolType as string | number) : item.toolType === val));
-                    itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, itemDoc, _readOnly_: false }));
+                    itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false }));
                 }}
             />
         );
@@ -310,17 +330,19 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         const toggleStatus = (script?.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean) ?? false;
         // Colors
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
-        // const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
 
+        // bcz: ink shapes are tri-state - off, one-shot, and on.  Need to update Toggle buttons to allow this and update currentUserUtils to set the tri-state on the Doc
+        // in the meantime, if the button matches a tool type that is not locked, we want to set the background color to something distinct.
+        const inkShapeHack = ((this.Document.toolType && this.Document.toolType === SnappingManager.InkShape) || this.Document.toolType === Doc.ActiveTool) && !SnappingManager.KeepGestureMode;
         return (
             <Toggle
                 tooltip={`Toggle ${tooltip}`}
                 toggleType={ToggleType.BUTTON}
-                type={Type.PRIM}
+                type={inkShapeHack ? Type.TERT : Type.PRIM}
                 toggleStatus={toggleStatus}
                 text={buttonText}
                 color={color}
-                // background={SnappingManager.userBackgroundColor}
+                background={inkShapeHack ? DashColor(SnappingManager.userBackgroundColor).darken(0.05).toString() : undefined}
                 icon={this.Icon(color)!}
                 label={this.label}
                 onPointerDown={e =>
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0dfc0ec28..ff879a2ab 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -338,7 +338,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     @computed get nativeSize() {
         TraceMobx();
-        if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0}
+        if (this.paths.length && this.paths[0].includes('icon-hi')) 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 nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1);
@@ -465,13 +465,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     marqueeDown = (e: React.PointerEvent) => {
         if (!this.dataDoc[this.fieldKey]) {
             this.chooseImage();
-        } else if (
-            !e.altKey &&
-            e.button === 0 &&
-            NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) &&
-            this._props.isContentActive() &&
-            ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)
-        ) {
+        } else if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             setupMoveUpEvents(
                 this,
                 e,
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index de51f6447..3bf7de2fe 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -776,7 +776,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     // starts marquee selection
     marqueeDown = (e: React.PointerEvent) => {
-        if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             setupMoveUpEvents(
                 this,
                 e,
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index a5788d02a..9ba4f8ead 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -44,6 +44,7 @@ import { LinkInfo } from './LinkDocPreview';
 import { OpenWhere } from './OpenWhere';
 import './WebBox.scss';
 
+// eslint-disable-next-line @typescript-eslint/no-require-imports
 const { CreateImage } = require('./WebBoxRenderer');
 
 @observer
@@ -335,7 +336,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 ele = document.createElement('div');
                 ele.append(contents);
             }
-        } catch (e) {
+        } catch {
             /* empty */
         }
         const visibleAnchor = this._getAnchor(this._savedAnnotations, true);
@@ -506,7 +507,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         let href: Opt<string>;
         try {
             href = iframe?.contentWindow?.location.href;
-        } catch (e) {
+        } catch {
             runInAction(() => this._warning++);
             href = undefined;
         }
@@ -713,7 +714,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     this._webUrl = this._url;
                 }
             }
-        } catch (e) {
+        } catch {
             console.log('WebBox URL error:' + this._url);
         }
         return true;
@@ -805,7 +806,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             sel.empty(); // Chrome
         else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
         this.marqueeing = [e.clientX, e.clientY];
-        if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             setupMoveUpEvents(
                 this,
                 e,
@@ -855,7 +856,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     })}
                     contentEditable
                     onPointerDown={this.webClipDown}
-                    // eslint-disable-next-line react/no-danger
                     dangerouslySetInnerHTML={{ __html: field.html }}
                 />
             );
@@ -1031,7 +1031,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 {this.inlineTextAnnotations
                     .sort((a, b) => NumCast(a.y) - NumCast(b.y))
                     .map(anno => (
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         <Annotation {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} containerDataDoc={this.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} />
                     ))}
             </div>
@@ -1042,7 +1041,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     }
     renderAnnotations = (childFilters: () => string[]) => (
         <CollectionFreeFormView
-            // eslint-disable-next-line react/jsx-props-no-spreading
             {...this._props}
             setContentViewBox={this.setInnerContent}
             NativeWidth={returnZero}
@@ -1217,7 +1215,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>
                     <SidebarAnnos
                         ref={this._sidebarRef}
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                         fieldKey={this.fieldKey + '_' + this._urlHash}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 659cda9dd..905c69bb8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -135,10 +135,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     @observable _showSidebar = false;
 
-    @computed get fontColor() {  return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore
-    @computed get fontSize()  {  return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore
-    @computed get fontFamily() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore
-    @computed get fontWeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string;  } // prettier-ignore
+    @computed get fontColor()      { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore
+    @computed get fontSize()       { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore
+    @computed get fontFamily()     { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore
+    @computed get fontWeight()     { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string;  } // prettier-ignore
+    @computed get fontStyle()      { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontStyle) as string;  } // prettier-ignore
+    @computed get fontDecoration() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontDecoration) as string;  } // prettier-ignore
 
     set _recordingDictation(value) {
         !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined);
@@ -2119,6 +2121,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     fontSize: this.fontSize,
                     fontFamily: this.fontFamily,
                     fontWeight: this.fontWeight,
+                    fontStyle: this.fontStyle,
+                    textDecoration: this.fontDecoration,
                     ...styleFromLayout,
                 }}>
                 <div
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 33cfbb510..62fbd23ea 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -41,7 +41,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     @observable private collapsed: boolean = false;
     @observable private _noLinkActive: boolean = false;
     @observable private _boldActive: boolean = false;
-    @observable private _italicsActive: boolean = false;
+    @observable private _italicActive: boolean = false;
     @observable private _underlineActive: boolean = false;
     @observable private _strikethroughActive: boolean = false;
     @observable private _subscriptActive: boolean = false;
@@ -89,8 +89,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     @computed get underline() {
         return this._underlineActive;
     }
-    @computed get italics() {
-        return this._italicsActive;
+    @computed get italic() {
+        return this._italicActive;
     }
     @computed get strikeThrough() {
         return this._strikethroughActive;
@@ -280,7 +280,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
 
         this._noLinkActive = false;
         this._boldActive = false;
-        this._italicsActive = false;
+        this._italicActive = false;
         this._underlineActive = false;
         this._strikethroughActive = false;
         this._subscriptActive = false;
@@ -290,7 +290,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
             switch (mark.name) {
                 case 'noAutoLinkAnchor': this._noLinkActive = true; break;
                 case 'strong':           this._boldActive = true; break;
-                case 'em':               this._italicsActive = true; break;
+                case 'em':               this._italicActive = true; break;
                 case 'underline':        this._underlineActive = true; break;
                 case 'strikethrough':    this._strikethroughActive = true; break;
                 case 'subscript':        this._subscriptActive = true; break;
@@ -352,7 +352,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         }
     };
 
-    toggleItalics = () => {
+    toggleItalic = () => {
         if (this.view) {
             const mark = this.view.state.schema.mark(this.view.state.schema.marks.em);
             this.setMark(mark, this.view.state, this.view.dispatch, false);
@@ -361,13 +361,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     };
 
     setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
-        if (this.dataDoc) {
-            this.dataDoc[`text_${fontField}`] = value;
-            this.updateMenu(undefined, undefined, undefined, this.dataDoc);
-        } else if (this.TextView && this.view) {
+        if (this.TextView && this.view && fontField !== 'fitBox') {
             const { text, paragraph } = this.view.state.schema.nodes;
             const selNode = this.view.state.selection.$anchor.node();
-            if ((fontField === 'fontSize' && value === '0px') || (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type))) {
+            if (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
                 this.TextView.dataDoc[this.TextView.fieldKey + `_${fontField}`] = value;
                 this.view.focus();
             }
@@ -376,6 +373,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
             const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs);
             this.setMark(fmark, this.view.state, (tx: Transaction) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
             this.view.focus();
+        } else if (this.dataDoc) {
+            this.dataDoc[`text_${fontField}`] = value;
+            this.updateMenu(undefined, undefined, undefined, this.dataDoc);
         } else {
             Doc.UserDoc()[fontField] = value;
             this.updateMenu(undefined, undefined, undefined, this.dataDoc);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 358557ad7..920c9ea8b 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -377,7 +377,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         if ((e.button !== 0 || e.altKey) && this._props.isContentActive()) {
             this._setPreviewCursor?.(e.clientX, e.clientY, true, false, this._props.Document);
         }
-        if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             this._props.select(false);
             MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
             this.isAnnotating = true;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index b4635673c..d0f6566a5 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -21,7 +21,7 @@ import { SVGToBezier, SVGType } from '../../util/bezierFit';
 import { InkingStroke } from '../InkingStroke';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { MarqueeView } from '../collections/collectionFreeForm';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
+import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
 import './SmartDrawHandler.scss';
 
 export interface DrawingOptions {
@@ -105,14 +105,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                         y: bounds.top - inkWidth / 2,
                         _width: bounds.width + inkWidth,
                         _height: bounds.height + inkWidth,
-                        stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+                        stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
                 inkWidth,
                 opts.autoColor ? stroke[1] : ActiveInkColor(),
                 ActiveInkBezierApprox(),
-                stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
-                ActiveArrowStart(),
-                ActiveArrowEnd(),
-                ActiveDash(),
+                stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2],
+                ActiveInkArrowStart(),
+                ActiveInkArrowEnd(),
+                ActiveInkDash(),
                 ActiveIsInkMask()
             );
             drawing.push(inkDoc);
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 6ec195910..aef7bf330 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -14,7 +14,7 @@ import {
     Initializing, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width
 } from './DocSymbols'; // prettier-ignore
 import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
-import { InkTool } from './InkField';
+import { InkEraserTool, InkInkTool, InkTool } from './InkField';
 import { List } from './List';
 import { ObjectField, serverOpType } from './ObjectField';
 import { PrefetchProxy, ProxyField } from './Proxy';
@@ -253,6 +253,10 @@ export class Doc extends RefField {
     public static set ActivePage(val)         { Doc.UserDoc().activePage = val; } // prettier-ignore
     public static get ActiveTool(): InkTool   { return StrCast(Doc.UserDoc().activeTool, InkTool.None) as InkTool;  } // prettier-ignore
     public static set ActiveTool(tool:InkTool){ Doc.UserDoc().activeTool = tool; } // prettier-ignore
+    public static get ActiveInk(): InkInkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkInkTool;  } // prettier-ignore
+    public static set ActiveInk(tool:InkInkTool){ Doc.UserDoc().activeInkTool = tool; } // prettier-ignore
+    public static get ActiveEraser(): InkEraserTool { return StrCast(Doc.UserDoc().activeEraserTool, InkTool.None) as InkEraserTool;  } // prettier-ignore
+    public static set ActiveEraser(tool:InkEraserTool){ Doc.UserDoc().activeEraserTool = tool; } // prettier-ignore
     public static get ActivePresentation()    { return DocCast(Doc.ActiveDashboard?.activePresentation) as Opt<Doc>;  } // prettier-ignore
     public static set ActivePresentation(val) { Doc.ActiveDashboard && (Doc.ActiveDashboard.activePresentation = val) } // prettier-ignore
     public static get ActiveDashboard()       { return DocCast(Doc.UserDoc().activeDashboard); } // prettier-ignore
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 17b99b033..f3ff5f11f 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -8,17 +8,22 @@ import { ObjectField } from './ObjectField';
 
 // Helps keep track of the current ink tool in use.
 export enum InkTool {
-    None = 'none',
-    Pen = 'pen',
-    Highlighter = 'highlighter',
-    StrokeEraser = 'strokeeraser',
-    SegmentEraser = 'segmenteraser',
-    RadiusEraser = 'radiuseraser',
-    Eraser = 'eraser', // not a real tool, but a class of tools
-    Stamp = 'stamp',
-    Write = 'write',
-    PresentationPin = 'presentationpin',
-    SmartDraw = 'smartdraw',
+    None = 'None',
+    Ink = 'Ink',
+    Eraser = 'Eraser', // not a real tool, but a class of tools
+    SmartDraw = 'Smartdraw',
+}
+
+export enum InkInkTool {
+    Pen = 'Pen',
+    Highlight = 'Highlight',
+    Write = 'Write',
+}
+
+export enum InkEraserTool {
+    Stroke = 'Stroke',
+    Segment = 'Segment',
+    Radius = 'Radius',
 }
 
 export type Segment = Array<Bezier>;
diff --git a/src/pen-gestures/GestureTypes.ts b/src/pen-gestures/GestureTypes.ts
index 5a8e9bd97..10f9ba6d0 100644
--- a/src/pen-gestures/GestureTypes.ts
+++ b/src/pen-gestures/GestureTypes.ts
@@ -8,7 +8,6 @@ export enum Gestures {
     Arrow = 'arrow',
     RightAngle = 'rightangle',
 }
-
 // Defines a point in an ink as a pair of x- and y-coordinates.
 export interface PointData {
     X: number;
-- 
cgit v1.2.3-70-g09d2


From 8680d1c31d4f835663c070f5b8cef57254e75e28 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 12 Nov 2024 16:42:35 -0500
Subject: made equation background same as text. fixed dflt stroke/link width
 to both be 1.  made function plot axis ranges get saved to Doc. marked
 equation->function links as being svgs.  fixed initial size of equation
 boxes..

---
 src/client/documents/DocUtils.ts                   |  3 +-
 src/client/util/CurrentUserUtils.ts                |  2 +-
 src/client/views/PropertiesView.tsx                |  2 +-
 src/client/views/StyleProvider.tsx                 |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  7 +---
 src/client/views/global/globalScripts.ts           |  2 +-
 src/client/views/linking/LinkMenuItem.tsx          |  2 +-
 src/client/views/nodes/DocumentView.tsx            |  3 +-
 src/client/views/nodes/EquationBox.scss            |  2 -
 src/client/views/nodes/EquationBox.tsx             |  2 +-
 src/client/views/nodes/FunctionPlotBox.tsx         | 43 +++++++++++++++++++---
 src/client/views/nodes/LinkBox.tsx                 |  2 +-
 12 files changed, 50 insertions(+), 22 deletions(-)

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

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 067b9c5e0..741d3a10e 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -168,7 +168,7 @@ export namespace DocUtils {
         return rangeFilteredDocs;
     }
 
-    export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string }, id?: string, showPopup?: number[]) {
+    export function MakeLink(source: Doc, target: Doc, linkSettings: { layout_isSvg?: boolean; link_relationship?: string; link_description?: string }, id?: string, showPopup?: number[]) {
         if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
         if (target.doc === Doc.UserDoc()) return undefined;
 
@@ -220,6 +220,7 @@ export namespace DocUtils {
                     link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined,
                     link_relationship: linkSettings.link_relationship,
                     link_description: linkSettings.link_description,
+                    layout_isSvg: linkSettings.layout_isSvg,
                     x: ComputedField.MakeFunction(`((this.${a}?.x||0)+(this.${b}?.x||0))/2`) as unknown as number, // x can accept functions even though type says it can't
                     y: ComputedField.MakeFunction(`((this.${a}?.y||0)+(this.${b}?.y||0))/2`) as unknown as number, // y can accept functions even though type says it can't
                     link_autoMoveAnchors: true,
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index fe23fe062..7b2a7ae0f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1142,7 +1142,7 @@ pie title Minerals in my tap water
 }
 
 // eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function activeInkTool() { return Doc.ActiveTool=== InkTool.Ink || DocumentView.Selected().some(dv => dv.layoutDoc.layout_isSvg); }, "is a pen tool or an ink stroke active");
+ScriptingGlobals.add(function activeInkTool()   { return Doc.ActiveTool=== InkTool.Ink || DocumentView.Selected().some(dv => dv.layoutDoc.layout_isSvg); }, "is a pen tool or an ink stroke active");
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.Eraser || Doc.ActiveEraser !== InkEraserTool.Radius; }, "is the active tool anything but the radius eraser");
 // eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 10c2a9898..2bd8bc98a 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -830,7 +830,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     set shapeWid(value)  { this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100); } // prettier-ignore
     @computed get shapeHgt() { return NumCast(this.selectedDoc?._height); } // prettier-ignore
     set shapeHgt(value)  { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } // prettier-ignore
-    @computed get strokeThk(){ return NumCast(this.selectedStrokes.lastElement()?.[DocData].stroke_width); } // prettier-ignore
+    @computed get strokeThk(){ return NumCast(this.selectedStrokes.lastElement()?.[DocData].stroke_width, 1); } // prettier-ignore
     set strokeThk(value) { this.selectedStrokes.forEach(doc => { doc[DocData].stroke_width = Math.round(value * 100) / 100; }); } // prettier-ignore
 
     @computed get hgtInput() {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3a5f57908..0a9da1237 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -257,7 +257,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 case DocumentType.RTF:         docColor = docColor || StrCast(Doc.UserDoc().textBackgroundColor, Colors.LIGHT_GRAY); break;
                 case DocumentType.LINK:        docColor = (isAnchor ? docColor : undefined); break;
                 case DocumentType.INK:         docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; break;
-                case DocumentType.EQUATION:    docColor = docColor || 'transparent'; break;
+                case DocumentType.EQUATION:    docColor = docColor || StrCast(Doc.UserDoc().textBackgroundColor, 'transparent'); break;
                 case DocumentType.LABEL:       docColor = docColor || Colors.LIGHT_GRAY; break;
                 case DocumentType.BUTTON:      docColor = docColor || Colors.LIGHT_GRAY; break;
                 case DocumentType.IMG:
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f2a5780c5..3de97f836 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -477,11 +477,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             //  do nothing if link is dropped into any freeform view parent of dragged document
             const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' });
             const added = !!this._props.addDocument?.(source);
-            de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' });
-            if (de.complete.linkDocument) {
-                de.complete.linkDocument.layout_isSvg = true;
-                this.addDocument(de.complete.linkDocument);
-            }
+            de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { layout_isSvg: true, link_relationship: 'annotated by:annotation of' });
+            de.complete.linkDocument && this.addDocument(de.complete.linkDocument);
             e.stopPropagation();
             !added && e.preventDefault();
             return added;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 5a17bc8f6..c632d6001 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -445,7 +445,7 @@ ScriptingGlobals.add(function setInkProperty(option: InkProperty, value: string
             setMode: () => SetactiveHideTextLabels(value? false : true),
         }],
         [ InkProperty.StrokeWidth, {
-            checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width) : ActiveInkWidth()),
+            checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width, 1) : ActiveInkWidth()),
             setInk: (doc: Doc) => { doc[DocData].stroke_width = NumCast(value); },
             setMode: () => SetActiveInkWidth(value.toString()),
         }],
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index b24fca8e2..1c92c3b0d 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -90,7 +90,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
             moveEv => {
                 const dragData = new DragManager.DocumentDragData([this._props.linkDoc], dropActionType.embed);
                 dragData.dropPropertiesToRemove = ['hidden'];
-                DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y, undefined, e => (this._props.linkDoc._layout_isSvg = true));
+                DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y, undefined, () => (this._props.linkDoc._layout_isSvg = true));
                 return true;
             },
             emptyFunction,
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 30f9e6363..92e98db87 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -459,10 +459,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 }
                 if (annoData || this.Document !== linkdrag.linkSourceDoc.embedContainer) {
                     const dropDoc = annoData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.Document;
-                    const linkDoc = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]);
+                    const linkDoc = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {layout_isSvg: true }, undefined, [de.x, de.y - 50]);
                     if (linkDoc) {
                         de.complete.linkDocument = linkDoc;
-                        linkDoc.layout_isSvg = true;
                         DocumentView.linkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc);
                     }
                 }
diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss
index cbd13d924..55e0f5184 100644
--- a/src/client/views/nodes/EquationBox.scss
+++ b/src/client/views/nodes/EquationBox.scss
@@ -2,9 +2,7 @@
 
 .equationBox-cont {
     transform-origin: center;
-    background-color: #e7e7e7;
     width: fit-content;
-    min-width: 100%;
     > span {
         width: fit-content;
     }
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 472fa56a0..34b46381d 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -81,7 +81,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 _height: 300,
                 backgroundColor: 'white',
             });
-            const link = DocUtils.MakeLink(this.Document, graph, { link_relationship: 'function', link_description: 'input' });
+            const link = DocUtils.MakeLink(this.Document, graph, { layout_isSvg: true, link_relationship: 'function', link_description: 'input' });
             this._props.addDocument?.(graph);
             link && this._props.addDocument?.(link);
             e.stopPropagation();
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 6b439cd64..91c351895 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -1,5 +1,5 @@
 import functionPlot, { Chart } from 'function-plot';
-import { computed, makeObservable, reaction } from 'mobx';
+import { action, computed, makeObservable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../../fields/Doc';
@@ -15,6 +15,8 @@ import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { FieldView, FieldViewProps } from './FieldView';
+import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
 
 @observer
 export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -65,18 +67,24 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         );
         return funcs;
     }
+    computeYScale = (width: number, height: number, xScale: number[]) => {
+        const xDiff = xScale[1] - xScale[0];
+        const yDiff = (height * xDiff) / width;
+        return [-yDiff / 2, yDiff / 2];
+    };
     createGraph = (ele?: HTMLDivElement) => {
         this._plotEle = ele || this._plotEle;
         const width = this._props.PanelWidth();
         const height = this._props.PanelHeight();
+        const xrange = Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]);
         try {
             this._plotEle?.children.length && this._plotEle.removeChild(this._plotEle.children[0]);
             this._plot = functionPlot({
                 target: '#' + this._plotEle?.id,
                 width,
                 height,
-                xAxis: { domain: Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]) },
-                yAxis: { domain: Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) },
+                xAxis: { domain: xrange },
+                yAxis: { domain: this.computeYScale(width, height, xrange) }, //  Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) },
                 grid: true,
                 data: this.graphFuncs.map(fn => ({
                     fn,
@@ -94,7 +102,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
             const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => {
                 // const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc);
                 if (res) {
-                    const link = DocUtils.MakeLink(doc, this.Document, { link_relationship: 'function', link_description: 'input' });
+                    const link = DocUtils.MakeLink(doc, this.Document, { layout_isSvg: true, link_relationship: 'function', link_description: 'input' });
                     link && this._props.addDocument?.(link);
                 }
                 return res;
@@ -115,7 +123,32 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         // if (this.layout_autoHeight) this.tryUpdateScrollHeight();
     };
     @computed get theGraph() {
-        return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />;
+        return (
+            <div
+                id={`${this._plotId}`}
+                ref={r => r && this.createGraph(r)}
+                style={{ position: 'absolute', width: '100%', height: '100%' }}
+                onPointerDown={e => {
+                    e.stopPropagation();
+                    setupMoveUpEvents(
+                        this,
+                        e,
+                        returnFalse,
+                        action(() => {
+                            if (this._plot?.options.xAxis?.domain) {
+                                this.Document.xRange = new List<number>(this._plot.options.xAxis.domain);
+                            }
+                            if (this._plot?.options.yAxis?.domain) {
+                                this.Document.yRange = new List<number>(this._plot.options.yAxis.domain);
+                            }
+                        }),
+                        emptyFunction,
+                        false,
+                        false
+                    );
+                }}
+            />
+        );
     }
     render() {
         TraceMobx();
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 4d9d2460e..c62a8afb1 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -160,7 +160,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
             // eslint-disable-next-line camelcase
             const { stroke_markerScale: strokeMarkerScale, stroke_width: strokeRawWidth, stroke_startMarker: strokeStartMarker, stroke_endMarker: strokeEndMarker, stroke_dash: strokeDash } = this.Document;
 
-            const strokeWidth = NumCast(strokeRawWidth, 4);
+            const strokeWidth = NumCast(strokeRawWidth, 1);
             const linkDesc = StrCast(this.dataDoc.link_description) || ' ';
             const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '');
             return (
-- 
cgit v1.2.3-70-g09d2


From 196b92cb84095780d2b36244831cac03e9b66d8e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 14 Nov 2024 10:48:16 -0500
Subject: changed isScribble related code to be more robust at determine cusps
 and deciding if cusps amount to a scribble. enabled undo of scribble erase
 without undoing scribble. added cusp visualizer for debugging, and cleaned up
 downDocView to  be downFfview on CollectionFreeformView.

---
 src/client/views/GestureOverlay.tsx                | 34 ++++++++++++----------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  7 +++++
 src/client/views/nodes/DocumentView.tsx            |  7 +----
 3 files changed, 27 insertions(+), 21 deletions(-)

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

diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index b0a750a9a..777a34ebc 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -64,6 +64,8 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     @observable private _pointerY?: number = undefined;
     @observable private _points: { X: number; Y: number }[] = [];
     @observable private _clipboardDoc?: JSX.Element = undefined;
+    @observable private _debugCusps: { X: number; Y: number }[] = [];
+    @observable private _debugGestures = false;
 
     @computed private get height(): number {
         return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 100, 100);
@@ -170,9 +172,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
      * Determines if what the array of cusp/intersection data corresponds to a scribble.
      * true if there are at least 4 cusps and either:
      *    1) the initial and final quarters of the array contain objects
-     *    2) or half of the cusps contain objects
+     *    2) or a declining percentage (ranges from 0.5 to 0.2 - based on the number of cusps) of cusp lines intersect strokes
      * @param intersectArray array of booleans coresponding to which scribble sections (regions separated by a cusp) contain Docs
-     * @returns
+     * @returns truthy if it's a scribble
      */
     determineIfScribble = (intersectArray: boolean[]) => {
         const quarterArrayLength = Math.ceil(intersectArray.length / 3.9); // use 3.9 instead of 4 to work better with strokes with only 4 cusps
@@ -182,7 +184,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             }), { start: false, end: false }); // prettier-ignore
 
         const percentCuspsWithContent = intersectArray.filter(value => value).length / intersectArray.length;
-        return intersectArray.length > 3 && (percentCuspsWithContent >= 0.5 || (start && end));
+        return intersectArray.length > 3 && (percentCuspsWithContent >= Math.max(0.2, 1 / (intersectArray.length - 1)) || (start && end));
     };
     /**
      * determines if inks intersect
@@ -236,9 +238,8 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     };
     @action
     onPointerUp = (e: PointerEvent) => {
-        const ffView = DocumentView.DownDocView?.ComponentView instanceof CollectionFreeFormView && DocumentView.DownDocView.ComponentView;
-        const downView = DocumentView.DownDocView;
-        DocumentView.DownDocView = undefined;
+        const ffView = CollectionFreeFormView.DownFfview;
+        CollectionFreeFormView.DownFfview = undefined;
         if (this._points.length > 1) {
             const B = this.svgBounds;
             const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
@@ -251,6 +252,8 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                 new Result(Gestures.Stroke, 1, Date.now); // prettier-ignore
 
             const cuspArray = this.getCusps(points);
+            const rect = this._overlayRef.current?.getBoundingClientRect();
+            this._debugCusps = rect ? cuspArray.map(p => ({ X: p.X + B.left - rect?.left, Y: p.Y + B.top - rect.top })) : [];
             // if any of the shape is activated in the CollectionFreeFormViewChrome
             // need to decide when to turn gestures back on
             const actionPerformed = ((name: Gestures) => {
@@ -272,14 +275,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
 
             if (!actionPerformed) {
                 const scribbledOver = ffView && this.isScribble(ffView, cuspArray, this._points);
+                this.dryInk();
                 if (scribbledOver) {
-                    undoable(() => ffView.removeDocument(scribbledOver), 'scribble erase')();
-                } else {
-                    this.dryInk();
+                    // can undo the erase without undoing the scribble, or undo a second time to undo the scribble
+                    setTimeout(undoable(() => ffView.removeDocument(scribbledOver.concat([ffView.childDocs.lastElement()])), 'scribble erase'));
                 }
             }
         } else {
-            (downView?.ComponentView as CollectionFreeFormView)?._marqueeViewRef?.current?.setPreviewCursor?.(this._points[0].X, this._points[0].Y, false, false, undefined);
+            ffView?._marqueeViewRef?.current?.setPreviewCursor?.(this._points[0].X, this._points[0].Y, false, false, undefined);
             e.preventDefault();
         }
         this.primCreated();
@@ -336,13 +339,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     getCusps(points: InkData) {
         const arrayOfPoints: { X: number; Y: number }[] = [];
         arrayOfPoints.push(points[0]);
-        for (let i = 0; i < points.length - 2; i++) {
+        for (let i = 0; i < points.length - 4; i++) {
             const point1 = points[i];
-            const point2 = points[i + 1];
-            const point3 = points[i + 2];
+            const point2 = points[i + 2];
+            const point3 = points[i + 4];
             if (this.find_angle(point1, point2, point3) < 90) {
                 // NOTE: this is not an accurate way to find cusps -- it is highly dependent on sampling rate and doesn't work well with slowly drawn scribbles
                 arrayOfPoints.push(point2);
+                i += 2;
             }
         }
         arrayOfPoints.push(points[points.length - 1]);
@@ -538,7 +542,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     }
 
     get elements() {
-        const selView = DocumentView.DownDocView;
+        const selView = CollectionFreeFormView.DownFfview;
         const width = Number(ActiveInkWidth()) * NumCast(selView?.Document._freeform_scale, 1); // * (selView?.screenToViewTransform().Scale || 1);
         const rect = this._overlayRef.current?.getBoundingClientRect();
         const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(this._points, true);
@@ -590,7 +594,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         return (
             <div className="gestureOverlay-cont" style={{ pointerEvents: this._props.isActive ? 'all' : 'none' }} ref={this._overlayRef} onPointerDown={this.onPointerDown}>
                 {this.elements}
-
+                {this._debugGestures && this._debugCusps.map(c => <div key={c.toString()} style={{ top: 0, left: 0, position: 'absolute', transform: `translate(${c.X}px, ${c.Y}px)`, width: 4, height: 4, background: 'red' }} />)}
                 <div
                     className="clipboardDoc-cont"
                     style={{
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3de97f836..49b432d7c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -98,6 +98,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const parent = CollectionFreeFormDocumentView.from(dv)?._props.reactParent;
         return parent instanceof CollectionFreeFormView ? parent : undefined;
     }
+    /**
+     * The Freeformview below the cursor at the start of a gesture (that receives the pointerDown event).  Used by GestureOverlay to determine the doc a gesture should apply to.
+     */
+    // eslint-disable-next-line no-use-before-define
+    public static DownFfview: CollectionFreeFormView | undefined; // the first DocView that receives a pointerdown event.  used by GestureOverlay to determine the doc a gesture should apply to.
 
     private _clusters = new CollectionFreeFormClusters(this);
     private _oldWheel: HTMLDivElement | null = null;
@@ -497,6 +502,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
+        if (!CollectionFreeFormView.DownFfview) CollectionFreeFormView.DownFfview = this;
+
         this._downX = this._lastX = e.pageX;
         this._downY = this._lastY = e.pageY;
         this._downTime = Date.now();
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 92e98db87..4bfa7fc92 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -355,7 +355,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     onPointerDown = (e: React.PointerEvent): void => {
         if (this._props.isGroupActive?.() === GroupActive.child && !this._props.isDocumentActive?.()) return;
         this._longPressSelector = setTimeout(() => SnappingManager.LongPress && this._props.select(false), 1000);
-        if (!DocumentView.DownDocView) DocumentView.DownDocView = this._docView;
 
         this._downX = e.clientX;
         this._downY = e.clientY;
@@ -459,7 +458,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 }
                 if (annoData || this.Document !== linkdrag.linkSourceDoc.embedContainer) {
                     const dropDoc = annoData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.Document;
-                    const linkDoc = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {layout_isSvg: true }, undefined, [de.x, de.y - 50]);
+                    const linkDoc = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, { layout_isSvg: true }, undefined, [de.x, de.y - 50]);
                     if (linkDoc) {
                         de.complete.linkDocument = linkDoc;
                         DocumentView.linkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc);
@@ -1075,10 +1074,6 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
             ?.ComponentView?.updateIcon?.()
             .then(() => ImageCast(DocCast(doc).icon));
     }
-    /**
-     * The DocumentView below the cursor at the start of a gesture (that receives the pointerDown event).  Used by GestureOverlay to determine the doc a gesture should apply to.
-     */
-    public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event.  used by GestureOverlay to determine the doc a gesture should apply to.
 
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
     private _htmlOverlayEffect: Opt<Doc>;
-- 
cgit v1.2.3-70-g09d2


From b7105063030ee29ba644d8fbb5878c8ee41fd389 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 16 Dec 2024 11:52:42 -0500
Subject: fixed api for creating an image using another image as a structure
 reference.

---
 src/client/views/ContextMenuItem.tsx               |  10 +-
 src/client/views/PropertiesView.tsx                |   4 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   3 -
 src/client/views/nodes/DocumentView.tsx            |   4 +-
 src/client/views/nodes/ImageBox.tsx                |   3 +-
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  70 ++++-------
 src/server/ApiManagers/FireflyManager.ts           | 132 ++++++++-------------
 7 files changed, 82 insertions(+), 144 deletions(-)

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

diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 6f8f41bdd..218718b18 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,6 +1,6 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { SnappingManager } from '../util/SnappingManager';
@@ -26,7 +26,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
     _hoverTimeout?: NodeJS.Timeout;
     _overPosY = 0;
     _overPosX = 0;
-    @observable _items: ContextMenuProps[] = [];
+    @observable.shallow _items: ContextMenuProps[] = [];
     @observable _overItem = false;
 
     constructor(props: ContextMenuProps & { selected?: boolean }) {
@@ -34,8 +34,8 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
         makeObservable(this);
     }
 
-    componentDidMount() {
-        runInAction(() => this._items.push(...(this._props.subitems ?? [])));
+    @computed get items() {
+        return this._items.concat(this._props.subitems ?? []);
     }
 
     handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => {
@@ -91,7 +91,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
     };
 
     render() {
-        const submenu = this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />);
+        const submenu = this.items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />);
         return this.props.event || this._props.noexpand ? this.renderItem(submenu) : <div className="contextMenu-inlineMenu">{submenu}</div>;
     }
 }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 4e12e0b2d..aefdeee17 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -995,9 +995,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                                 align="flex-start"
                                 fillWidth
                                 toggleType={ToggleType.BUTTON}
-                                onClick={undoable(() => {
-                                    DrawingFillHandler.drawingToImage(targetDoc, 'fill in the details of this image');
-                                }, 'createImage')}
+                                onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, 'fill in the details of this image'), 'createImage')}
                             />
                         </div>
                         <div className="color">
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 79aad0ef2..272c13546 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -102,7 +102,6 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
             replica: '',
         });
     });
-    // eslint-disable-next-line no-use-before-define
     return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []);
 }
 
@@ -272,7 +271,6 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
         payload: pivotColumnGroups.get(key)?.filters,
     }));
     groupNames.push(...dividers);
-    // eslint-disable-next-line no-use-before-define
     return normalizeResults(panelDim, maxText, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
 }
 
@@ -347,7 +345,6 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc:
         if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) {
             groupNames.push({ type: 'text', text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined });
         }
-        // eslint-disable-next-line no-use-before-define
         layoutDocsAtTime(keyDocs, key);
     });
     if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4bfa7fc92..8519cda3c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -52,6 +52,7 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails/PresEnums';
 import SpringAnimation from './trails/SlideEffect';
 import { SpringType, springMappings } from './trails/SpringUtils';
+import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
 
 export interface DocumentViewProps extends FieldViewSharedProps {
     hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
@@ -551,6 +552,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'map-pin' });
+            appearanceItems.push({ description: 'Make Image', event: () => DrawingFillHandler.drawingToImage(this.Document, StrCast(this.Document.title)), icon: 'map-pin' });
 
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
@@ -1072,7 +1074,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static GetDocImage(doc: Doc) {
         return DocumentView.getDocumentView(doc)
             ?.ComponentView?.updateIcon?.()
-            .then(() => ImageCast(DocCast(doc).icon));
+            .then(() => ImageCast(doc?.icon, ImageCast(doc[Doc.LayoutFieldKey(doc)])));
     }
 
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d847b7940..03d417e4e 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -40,7 +40,6 @@ import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
 import { Upload } from '../../../server/SharedMediaTypes';
-import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -361,6 +360,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
+    updateIcon = () => new Promise<void>(res => res());
+
     choosePath = (url: URL) => {
         if (!url?.href) return '';
         const lower = url.href.toLowerCase();
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 6f111e95b..32416cec5 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,50 +1,22 @@
-// import { action, makeObservable } from 'mobx';
-// import { observer } from 'mobx-react';
-// import React from 'react';
-// import { Doc } from '../../../fields/Doc';
-// import { ImageCast } from '../../../fields/Types';
-// import { ImageField } from '../../../fields/URLField';
-// import { Docs } from '../../documents/Documents';
-// import { Networking } from '../../Network';
-// import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
-// import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
-// import { ImageUtility } from '../nodes/imageEditor/imageEditorUtils/ImageHandler';
-// import { OpenWhere } from '../nodes/OpenWhere';
-// import { ObservableReactComponent } from '../ObservableReactComponent';
+import { Doc } from '../../../fields/Doc';
+import { ImageCast } from '../../../fields/Types';
+import { Upload } from '../../../server/SharedMediaTypes';
+import { Docs } from '../../documents/Documents';
+import { Networking } from '../../Network';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { OpenWhere } from '../nodes/OpenWhere';
 
-// export class DrawingFillHandler {
-//     static drawingToImage = async (drawing: Doc, prompt: string) => {
-//         const imageField = await DocumentView.GetDocImage(drawing);
-//         if (!imageField) return;
-//         const { href } = ImageCast(imageField).url;
-//         const hrefParts = href.split('.');
-//         const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
-//         try {
-//             const response = await fetch(hrefComplete);
-//             const blob: Blob = await response.blob();
-//             const strength: number = 100;
-//             const img = await Networking.PostToServer('/oldQueryFireflyImage', { prompt, blob, strength });
-//             DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight);
-//             // Networking.PostToServer('/oldQueryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight));
-//         } catch (error) {
-//             console.error('Error fetching image:', error);
-//             return;
-//         }
-
-//         // const image = new Image();
-//         // image.src = imageField.url?.href;
-//         // // image.onload = async () => {
-//         //     const canvas = document.createElement('canvas');
-//         //     canvas.width = image.width;
-//         //     canvas.height = image.height;
-//         //     const ctx = canvas.getContext('2d');
-//         //     if (!ctx) return;
-//         //     ctx.globalCompositeOperation = 'source-over';
-//         //     ctx.clearRect(0, 0, image.width, image.height);
-//         //     ctx.drawImage(image, 0, 0);
-//         // const blob: Blob = await ImageUtility.canvasToBlob(canvas);
-//         // const strength: number = 100;
-//         // Networking.PostToServer('/oldQueryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight));
-//         // };
-//     };
-// }
+export class DrawingFillHandler {
+    static drawingToImage = (drawing: Doc, prompt: string) =>
+        DocumentView.GetDocImage(drawing)?.then(imageField => {
+            if (imageField) {
+                const { href } = ImageCast(imageField).url;
+                const hrefParts = href.split('.');
+                const structureUrl = `${hrefParts[0]}_o.${hrefParts[1]}`;
+                const strength: number = 100;
+                Networking.PostToServer('/queryFireflyImageFromStructure', { prompt, structureUrl, strength }).then((info: Upload.ImageInformation) => 
+                                DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {}), OpenWhere.addRight)) // prettier-ignore
+            }
+            return false;
+        });
+}
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 73cf94206..c84e9e8fa 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -20,21 +20,10 @@ export default class FireflyManager extends ApiManager {
             return undefined;
         });
 
-    askFireflyOld = (prompt: string = 'a realistic illustration of a cat coding', uploadId: string, strength: number) => {
-        const fetched = this.getBearerToken().then(response =>
-            response?.json().then((data: { access_token: string }) => {
-                const body: any = {
-                    prompt: prompt,
-                    structure: {
-                        strength: strength,
-                        imageReference: {
-                            source: {
-                                uploadId: uploadId,
-                            },
-                        },
-                    },
-                };
-                return fetch('https://firefly-api.adobe.io/v3/images/generate', {
+    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', structureUrl: string, strength: number) =>
+        this.getBearerToken().then(response =>
+            response?.json().then((data: { access_token: string }) =>
+                fetch('https://firefly-api.adobe.io/v3/images/generate', {
                     method: 'POST',
                     headers: [
                         ['Content-Type', 'application/json'],
@@ -42,40 +31,40 @@ export default class FireflyManager extends ApiManager {
                         ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
                         ['Authorization', `Bearer ${data.access_token}`],
                     ],
-                    body: JSON.stringify(body),
+                    body: JSON.stringify({
+                        prompt,
+                        structure: !structureUrl
+                            ? undefined
+                            : {
+                                  strength,
+                                  imageReference: {
+                                      source: { url: structureUrl },
+                                  },
+                              },
+                    }),
                 })
-                    .then(response => response.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
+                    .then(response2 => response2.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
                     .catch(error => {
                         console.error('Error:', error);
                         return '';
+                    })
+            )
+        );
+
+    uploadImageToDropbox = (fileUrl: string, dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN })) =>
+        new Promise<string>((res, rej) =>
+            fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(fileUrl)}`), undefined, (err, contents) => {
+                if (err) {
+                    console.log('Error: ', err);
+                    rej();
+                } else {
+                    dbx.filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents }).then(response => {
+                        dbx.filesGetTemporaryLink({ path: response.result.path_display ?? '' }).then(link => res(link.result.link));
                     });
+                }
             })
         );
-        return fetched;
-    };
 
-    uploadImageToFirefly = (image: Blob) => {
-        const fetched = this.getBearerToken().then(response =>
-            response?.json().then((data: { access_token: string }) =>
-                fetch('https://firefly-api.adobe.io/v3/uploads', {
-                    method: 'POST',
-                    headers: [
-                        ['Content-Type', image.type],
-                        ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
-                        ['Authorization', `Bearer ${data.access_token}`],
-                    ],
-                    body: image,
-                })
-                    .then(response => response.json())
-                    .then(data => data.uploadId) // Extract the uploadId from the response
-                    .catch(error => {
-                        console.error('Error:', error);
-                        return '';
-                    })
-            )
-        );
-        return fetched;
-    };
     generateImage = (prompt: string = 'a realistic illustration of a cat coding') => {
         const fetched = this.getBearerToken().then(response =>
             response?.json().then((data: { access_token: string }) =>
@@ -222,17 +211,16 @@ export default class FireflyManager extends ApiManager {
     protected initialize(register: Registration): void {
         register({
             method: Method.POST,
-            subscription: '/oldQueryFireflyImage',
-            secureHandler: async ({ req, res }) => {
-                const { prompt, imageBlob, strength = 0.5 } = req.body;
-                const uploadId = imageBlob ? await this.uploadImageToFirefly(imageBlob) : null;
-                this.askFireflyOld(prompt, uploadId, strength).then(fire =>
-                    DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
-                        if (info instanceof Error) _invalid(res, info.message);
-                        else _success(res, info);
-                    })
-                );
-            },
+            subscription: '/queryFireflyImageFromStructure',
+            secureHandler: async ({ req, res }) =>
+                this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl =>
+                    this.generateImageFromStructure(req.body.prompt, uploadUrl, req.body.strength).then(fire =>
+                        DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
+                            if (info instanceof Error) _invalid(res, info.message);
+                            else _success(res, info);
+                        })
+                    )
+                ),
         });
         register({
             method: Method.POST,
@@ -263,36 +251,16 @@ export default class FireflyManager extends ApiManager {
             method: Method.POST,
             subscription: '/expandImage',
             secureHandler: ({ req, res }) =>
-                new Promise<void>((resolve, reject) => {
-                    const dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN });
-                    fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(req.body.file)}`), undefined, (err, contents) => {
-                        if (err) {
-                            console.log('Error: ', err);
-                            reject();
-                        } else {
-                            dbx.filesUpload({ path: `/Apps/browndash/${path.basename(req.body.file)}`, contents })
-                                .then(response => {
-                                    dbx.filesGetTemporaryLink({ path: response.result.path_display ?? '' }).then(link => {
-                                        console.log(link.result);
-                                        this.expandImage(link.result.link, req.body.prompt).then(text => {
-                                            if (text.error_code) _error(res, text.message);
-                                            else
-                                                DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => {
-                                                    if (info instanceof Error) _invalid(res, info.message);
-                                                    else _success(res, info);
-                                                    resolve();
-                                                });
-                                        });
-                                    });
-                                })
-                                .catch(uploadErr => {
-                                    console.log(uploadErr);
-                                    _error(res, 'upload to dropbox failed');
-                                    reject();
-                                });
-                        }
-                    });
-                }),
+                this.uploadImageToDropbox(req.body.file).then(uploadUrl =>
+                    this.expandImage(uploadUrl, req.body.prompt).then(text => {
+                        if (text.error_code) _error(res, text.message);
+                        else
+                            DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => {
+                                if (info instanceof Error) _invalid(res, info.message);
+                                else _success(res, info);
+                            });
+                    })
+                ),
         });
     }
 }
-- 
cgit v1.2.3-70-g09d2


From 6e7cb570f9bad527cd4772bb5c715dd588fb77df Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 2 Jan 2025 22:39:25 -0500
Subject: tags can now be used as style presets

---
 src/client/apis/gpt/GPT.ts                        |  2 +-
 src/client/views/DocumentButtonBar.tsx            | 18 ++++++++++++++++++
 src/client/views/nodes/DocumentView.tsx           |  2 ++
 src/client/views/smartdraw/DrawingFillHandler.tsx | 10 +++++++---
 src/client/views/smartdraw/FireflyConstants.ts    | 22 ++++++++++++++++++++++
 src/server/ApiManagers/FireflyManager.ts          |  7 +++++--
 6 files changed, 55 insertions(+), 6 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 9241eb120..4b0d58cc1 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -260,7 +260,7 @@ const gptDescribeImage = async (image: string): Promise<string> => {
                     content: [
                         {
                             type: 'text',
-                            text: `Identify what this drawing is, naming as many elements and their location in the drawing as possible`,
+                            text: `Briefly identify what this drawing is, naming all the drawing elements and their location within the image. Do not include anything about the drawing style.`,
                         },
                         {
                             type: 'image_url',
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 32bf67df1..b7033af3f 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -314,6 +314,23 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         );
     }
 
+    @computed
+    get aiEditorButton() {
+        const targetDoc = this.view0?.Document;
+        return !targetDoc ? null : (
+            <Tooltip title={<div className="dash-ai-editor-button">Edit with AI</div>}>
+                <div
+                    className="documentButtonBar-icon"
+                    style={{ color: 'white' }}
+                    onClick={() => {
+                        CalendarManager.Instance.open(this.view0, targetDoc);
+                    }}>
+                    <FontAwesomeIcon className="documentdecorations-icon" icon="robot" />
+                </div>
+            </Tooltip>
+        );
+    }
+
     @observable _isRecording = false;
     _stopFunc: () => void = emptyFunction;
     @computed
@@ -484,6 +501,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                 <div className="documentButtonBar-button">{this.pinButton}</div>
                 <div className="documentButtonBar-button">{this.recordButton}</div>
                 <div className="documentButtonBar-button">{this.calendarButton}</div>
+                <div className="documentButtonBar-button">{this.aiEditorButton}</div>
                 <div className="documentButtonBar-button">{this.keywordButton}</div>
                 {!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
                 <div className="documentButtonBar-button">{this.menuButton}</div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 8519cda3c..048b92c71 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1400,6 +1400,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     NativeHeight = () => this.effectiveNativeHeight;
     PanelWidth = () => this.panelWidth;
     PanelHeight = () => this.panelHeight;
+    ReducedPanelWidth = () => this.panelWidth / 2;
+    ReducedPanelHeight = () => this.panelWidth / 2;
     NativeDimScaling = () => this.nativeScaling;
     hideLinkCount = () => !!this.hideLinkButton;
     isHovering = () => this._isHovering;
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 7a95e27c2..52652d377 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -8,10 +8,13 @@ import { Docs } from '../../documents/Documents';
 import { Networking } from '../../Network';
 import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { OpenWhere } from '../nodes/OpenWhere';
-import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions } from './FireflyConstants';
+import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, FireflyStylePresets } from './FireflyConstants';
 
 export class DrawingFillHandler {
-    static drawingToImage = (drawing: Doc, strength: number, prompt: string) =>
+    static drawingToImage = (drawing: Doc, strength: number, prompt: string) => {
+        const docData = drawing[DocData];
+        const tags: string[] = ((docData?.tags as unknown as string[]) ?? []).map(tag => tag.slice(1)) ?? [];
+        const styles = tags.filter(tag => FireflyStylePresets.has(tag));
         DocumentView.GetDocImage(drawing)?.then(imageField => {
             if (imageField) {
                 const aspectRatio = (drawing.width as number) / (drawing.height as number);
@@ -31,11 +34,12 @@ export class DrawingFillHandler {
                 imageUrlToBase64(structureUrl)
                     .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
                     .then((prompt: string) => {
-                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: prompt, width: dims.width, height: dims.height, structureUrl, strength }).then((info: Upload.ImageInformation) =>
+                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: prompt, width: dims.width, height: dims.height, structureUrl: structureUrl, strength: strength, styles: styles }).then((info: Upload.ImageInformation) =>
                             DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { ai_generated: true, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
                         ); // prettier-ignore
                     });
             }
             return false;
         });
+    };
 }
diff --git a/src/client/views/smartdraw/FireflyConstants.ts b/src/client/views/smartdraw/FireflyConstants.ts
index f51305fba..3574039e4 100644
--- a/src/client/views/smartdraw/FireflyConstants.ts
+++ b/src/client/views/smartdraw/FireflyConstants.ts
@@ -18,3 +18,25 @@ export const AspectRatioLimits = {
     portrait: 0.875,
     widescreen: 1.472,
 };
+
+// prettier-ignore
+export const FireflyStylePresets = 
+    new Set<string>(['graphic', 'wireframe',
+        'vector_look','bw','cool_colors','golden','monochromatic','muted_color','toned_image','vibrant_colors','warm_tone','closeup',
+        'knolling','landscape_photography','macrophotography','photographed_through_window','shallow_depth_of_field','shot_from_above',
+        'shot_from_below','surface_detail','wide_angle','beautiful','bohemian','chaotic','dais','divine','eclectic','futuristic','kitschy',
+        'nostalgic','simple','antique_photo','bioluminescent','bokeh','color_explosion','dark','faded_image','fisheye','gomori_photography',
+        'grainy_film','iridescent','isometric','misty','neon','otherworldly_depiction','ultraviolet','underwater', 'backlighting', 
+        'dramatic_light', 'golden_hour', 'harsh_light','long','low_lighting','multiexposure','studio_light','surreal_lighting',
+        '3d_patterns','charcoal','claymation','fabric','fur','guilloche_patterns','layered_paper','marble_sculpture','made_of_metal',
+        'origami','paper_mache','polka','strange_patterns','wood_carving','yarn','art_deco','art_nouveau','baroque','bauhaus',
+        'constructivism','cubism','cyberpunk','fantasy','fauvism', 'film_noir','glitch_art','impressionism','industrialism','maximalism',
+        'minimalism','modern_art','modernism','neo','pointillism','psychedelic','science_fiction','steampunk','surrealism','synthetism',
+        'synthwave','vaporwave','acrylic_paint','bold_lines','chiaroscuro','color_shift_art','daguerreotype','digital_fractal',
+        'doodle_drawing','double_exposure_portrait','fresco','geometric_pen','halftone','ink','light_painting','line_drawing','linocut',
+        'oil_paint','paint_spattering','painting','palette_knife','photo_manipulation','scribble_texture','sketch','splattering',
+        'stippling_drawing','watercolor','3d','anime','cartoon','cinematic','comic_book','concept_art','cyber_matrix','digital_art',
+        'flat_design','geometric','glassmorphism','glitch_graphic','graffiti','hyper_realistic','interior_design','line_gradient',
+        'low_poly','newspaper_collage','optical_illusion','pattern_pixel','pixel_art','pop_art','product_photo','psychedelic_background',
+        'psychedelic_wonderland','scandinavian','splash_images','stamp','trompe_loeil'
+    ]);
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 4c4aac5e0..cb5d7c1da 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -20,9 +20,10 @@ export default class FireflyManager extends ApiManager {
             return undefined;
         });
 
-    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50) =>
+    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[]) =>
         this.getBearerToken().then(response =>
             response?.json().then((data: { access_token: string }) =>
+                //prettier-ignore
                 fetch('https://firefly-api.adobe.io/v3/images/generate', {
                     method: 'POST',
                     headers: [
@@ -42,6 +43,8 @@ export default class FireflyManager extends ApiManager {
                                       source: { url: structureUrl },
                                   },
                               },
+                        //prettier-ignore
+                        style: { presets: styles }
                     }),
                 })
                     .then(response2 => response2.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
@@ -227,7 +230,7 @@ export default class FireflyManager extends ApiManager {
             subscription: '/queryFireflyImageFromStructure',
             secureHandler: async ({ req, res }) =>
                 this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl =>
-                    this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength).then(fire =>
+                    this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength, req.body.styles).then(fire =>
                         DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
                             if (info instanceof Error) _invalid(res, info.message);
                             else _success(res, info);
-- 
cgit v1.2.3-70-g09d2


From 1d62d867621b293c41ff8488ca5a3bd6010723d5 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sun, 5 Jan 2025 23:47:18 -0500
Subject: added AI image editor

---
 src/client/documents/Documents.ts                  |   6 +-
 src/client/util/CurrentUserUtils.ts                |   2 +-
 src/client/views/DocumentButtonBar.tsx             |   7 +-
 src/client/views/ViewBoxInterface.ts               |   1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  12 +--
 src/client/views/nodes/DocumentView.tsx            |  22 +++-
 src/client/views/nodes/ImageBox.scss               |  28 +++++
 src/client/views/nodes/ImageBox.tsx                | 113 ++++++++++++++++++++-
 src/client/views/pdf/AnchorMenu.tsx                |  12 +--
 src/client/views/smartdraw/DrawingFillHandler.tsx  |   2 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  92 +++++++++--------
 src/client/views/smartdraw/StickerPalette.tsx      |  17 ++--
 src/server/ApiManagers/FireflyManager.ts           |   1 -
 13 files changed, 235 insertions(+), 80 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 785af3409..7f1387ff8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -517,9 +517,9 @@ export class DocumentOptions {
     card_sort?: STRt = new StrInfo('way cards are sorted in deck view');
     card_sort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
 
-    ai_generated?: boolean; // to mark items as ai generated
-    firefly_seed?: number;
-    firefly_prompt?: string;
+    ai?: string; // to mark items as ai generated
+    ai_firefly_seed?: number;
+    ai_firefly_prompt?: string;
 }
 
 export const DocOptions = new DocumentOptions();
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 9af79a02e..b41fd09dc 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -779,7 +779,7 @@ pie title Minerals in my tap water
             { title: " Size",      toolTip: "Size of area pencil eraser",   btnType: ButtonType.NumberSliderButton,                toolType: InkProperty.EraserWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"NotRadiusEraser()"}, numBtnMin: 1, linearBtnWidth:40},
             { title: "Mask",       toolTip: "Make Stroke a Stencil Mask",   btnType: ButtonType.ToggleButton, icon: "user-circle", toolType: InkProperty.Mask,                          scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
             { title: "Labels",     toolTip: "Show Labels Inside Shapes",    btnType: ButtonType.ToggleButton, icon: "text-width",  toolType: InkProperty.Labels,                        scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}},
-            { title: "Smart Draw", toolTip: "Draw with GPT",                btnType: ButtonType.ToggleButton, icon: "user-pen",    toolType: InkTool.SmartDraw,                         scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'},  funcs: {hidden: "IsNoviceMode()"}},
+            { title: "Smart Draw", toolTip: "Draw with AI",                btnType: ButtonType.ToggleButton, icon: "user-pen",    toolType: InkTool.SmartDraw,                         scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'},  funcs: {hidden: "IsNoviceMode()"}},
         ];
     }
 
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index b7033af3f..d722b28b5 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -319,12 +319,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         const targetDoc = this.view0?.Document;
         return !targetDoc ? null : (
             <Tooltip title={<div className="dash-ai-editor-button">Edit with AI</div>}>
-                <div
-                    className="documentButtonBar-icon"
-                    style={{ color: 'white' }}
-                    onClick={() => {
-                        CalendarManager.Instance.open(this.view0, targetDoc);
-                    }}>
+                <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={undoable(() => this.view0?.toggleAIEditor(), 'toggle AI editor')}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="robot" />
                 </div>
             </Tooltip>
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index b7980d74e..df08f2564 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -60,4 +60,5 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
     search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
     dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views
     isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations
+    componentAIView?: (top: number) => JSX.Element;
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ef0b80720..9af698ec7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1290,12 +1290,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const docData = doc[DocData];
         docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
         docData._width = opts.size;
-        docData.drawingInput = opts.text;
-        docData.drawingComplexity = opts.complexity;
-        docData.drawingColored = opts.autoColor;
-        docData.drawingSize = opts.size;
-        docData.drawingData = gptRes;
-        docData.ai_generated = true;
+        docData.ai_drawing_input = opts.text;
+        docData.ai_drawing_complexity = opts.complexity;
+        docData.ai_drawing_colored = opts.autoColor;
+        docData.ai_drawing_size = opts.size;
+        docData.ai_drawing_data = gptRes;
+        docData.ai = 'gpt';
         this._drawingContainer = doc;
         this.addDocument(doc);
         this._batch?.end();
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 048b92c71..ab58023e3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -123,6 +123,12 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @observable _componentView: Opt<ViewBoxInterface<FieldViewProps>> = undefined; // needs to be accessed from DocumentView wrapper class
     @observable _animateScaleTime: Opt<number> = undefined; // milliseconds for animating between views.  defaults to 300 if not uset
     @observable _animateScalingTo = 0;
+    @observable public _showAIEditor: boolean = false;
+
+    @action
+    showAIEditor() {
+        this._showAIEditor = !this._showAIEditor;
+    }
 
     get _contentDiv()  { return this._mainCont.current; } // prettier-ignore
     get _docView()     { return this._props.DocumentView?.(); } // prettier-ignore
@@ -552,7 +558,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'map-pin' });
-            appearanceItems.push({ description: 'Make Image', event: () => DrawingFillHandler.drawingToImage(this.Document, StrCast(this.Document.title)), icon: 'map-pin' });
 
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
@@ -711,6 +716,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
+    rpw = () => this._props.PanelWidth() * 0.6;
+    rph = () => this.panelHeight() * 0.6;
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -720,7 +727,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 className="documentView-contentsView"
                 style={{
                     pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
-                    height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                    width: this._showAIEditor ? this.rpw() : undefined,
+                    height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                    justifySelf: 'center',
                 }}>
                 <DocumentContentsView
                     {...this._props}
@@ -728,7 +737,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     pointerEvents={this.contentPointerEvents}
                     setContentViewBox={this.setContentView}
                     childFilters={this.childFilters}
-                    PanelHeight={this.panelHeight}
+                    PanelHeight={this._showAIEditor ? this.rpw : this.panelHeight}
+                    PanelWidth={this._showAIEditor ? this.rph : this._props.PanelWidth}
                     setHeight={this.setHeight}
                     isContentActive={this.isContentActive}
                     ScreenToLocalTransform={this.screenToLocalContent}
@@ -737,6 +747,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     setTitleFocus={this.setTitleFocus}
                     hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                 />
+                {this._showAIEditor && (this._componentView?.componentAIView?.(this.rph()) ?? null)}
             </div>
         );
     }
@@ -1286,6 +1297,11 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         }
     };
 
+    @action
+    public toggleAIEditor = () => {
+        this._docViewInternal && this._docViewInternal.showAIEditor();
+    };
+
     public setTextHtmlOverlay = action((text: string | undefined, effect?: Doc) => {
         this._htmlOverlayText = text;
         this._htmlOverlayEffect = effect;
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 3ffda5a35..03314e90f 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -139,3 +139,31 @@
 .imageBox-fadeBlocker-hover {
     opacity: 0;
 }
+
+.imageBox-aiView {
+    padding: 5px;
+    position: absolute;
+    overflow: scroll;
+    text-align: center;
+    font-weight: bold;
+    margin-top: 5px;
+
+    .imageBox-aiView-subtitle {
+        align-self: start;
+    }
+
+    .imageBox-aiView-regenerate-container,
+    .imageBox-aiView-options-container {
+        font-weight: normal;
+        text-align: start;
+    }
+
+    .imageBox-aiView-regenerate,
+    .imageBox-aiView-options {
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+        flex-direction: row;
+        gap: 5px;
+    }
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 7ce429f0f..f00580d77 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,7 +1,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import axios from 'axios';
-import { Colors } from 'browndash-components';
+import { Colors, Type } from 'browndash-components';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { extname } from 'path';
@@ -41,6 +41,9 @@ import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
 import { Upload } from '../../../server/SharedMediaTypes';
 import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
+import { Button } from 'browndash-components';
+import { SettingsManager } from '../../util/SettingsManager';
+import { AiOutlineSend } from 'react-icons/ai';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -352,7 +355,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 }),
                 icon: 'pencil-alt',
             });
-            this.layoutDoc.ai_generated &&
+            this.layoutDoc.ai &&
                 funcs.push({
                     description: 'Regenerate AI Image',
                     event: action(e => {
@@ -526,6 +529,112 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
 
+    @observable private _regenInput = '';
+    @observable private _canInteract = true;
+    @observable private _regenerateLoading = false;
+
+    componentAIView = (top: number) => {
+        const field = Cast(this.dataDoc[this.fieldKey], ImageField);
+        const showRegenerate = this.Document[DocData].ai;
+        return (
+            <div className="imageBox-aiView" style={{ top: top, width: NumCast(this.Document.width), height: NumCast(this.Document.width) - top }}>
+                Edit Image with AI
+                {showRegenerate && (
+                    <div className="imageBox-aiView-regenerate-container">
+                        <text className="imageBox-aiView-subtitle">Regenerate AI Image</text>
+                        <div className="imageBox-aiView-regenerate">
+                            <input
+                                aria-label="Edit instructions input"
+                                // className="smartdraw-input"
+                                type="text"
+                                value={this._regenInput}
+                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                                // onKeyDown={this.handleKeyPress}
+                                placeholder="Prompt (Optional)"
+                            />
+                            <Button
+                                text="Regenerate"
+                                type={Type.SEC}
+                                // style={{ alignSelf: 'flex-end' }}
+                                icon={this._regenerateLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                onClick={action(async () => {
+                                    this._regenerateLoading = true;
+                                    await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput);
+                                    this._regenerateLoading = false;
+                                    this._regenInput = '';
+                                })}
+                            />
+                            <Button
+                                // style={{ alignSelf: 'flex-end' }}
+                                text="Get Variations"
+                                type={Type.SEC}
+                                // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                // onClick={this.handleSendClick}
+                            />
+                        </div>
+                    </div>
+                )}
+                <div className="imageBox-aiView-options-container">
+                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Image Options </text>}
+                    <div className="imageBox-aiView-options">
+                        <Button
+                            type={Type.TERT}
+                            text="Get Text"
+                            icon={<FontAwesomeIcon icon="font" />}
+                            color={SettingsManager.userBackgroundColor}
+                            iconPlacement="right"
+                            onClick={() => {
+                                Networking.PostToServer('/queryFireflyImageText', {
+                                    file: (file => {
+                                        const ext = extname(file);
+                                        return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+                                    })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+                                }).then(text => alert(text));
+                            }}
+                        />
+                        <Button
+                            type={Type.TERT}
+                            text="Generative Fill"
+                            icon={<FontAwesomeIcon icon="fill" />}
+                            color={SettingsManager.userBackgroundColor}
+                            // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={action(() => {
+                                ImageEditorData.Open = true;
+                                ImageEditorData.Source = (field && this.choosePath(field.url)) || '';
+                                ImageEditorData.AddDoc = this._props.addDocument;
+                                ImageEditorData.RootDoc = this.Document;
+                            })}
+                        />
+                        <Button
+                            type={Type.TERT}
+                            text="Expand"
+                            icon={<FontAwesomeIcon icon="expand" />}
+                            color={SettingsManager.userBackgroundColor}
+                            // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={() => {
+                                Networking.PostToServer('/expandImage', {
+                                    prompt: 'sunny skies',
+                                    file: (file => {
+                                        const ext = extname(file);
+                                        return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+                                    })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+                                }).then((info: Upload.ImageInformation) => {
+                                    const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
+                                    DocUtils.assignImageInfo(info, img);
+                                    this._props.addDocTab(img, OpenWhere.addRight);
+                                });
+                            }}
+                        />
+                    </div>
+                </div>
+            </div>
+        );
+    };
+
     @computed get annotationLayer() {
         TraceMobx();
         return <div className="imageBox-annotationLayer" style={{ height: this._props.PanelHeight() }} ref={this._annotationLayer} />;
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index bb8082061..2e704aa8d 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -153,12 +153,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         this.AddDrawingAnnotation(drawing);
         const docData = drawing[DocData];
         docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
-        docData.drawingInput = opts.text;
-        docData.drawingComplexity = opts.complexity;
-        docData.drawingColored = opts.autoColor;
-        docData.drawingSize = opts.size;
-        docData.drawingData = gptRes;
-        docData.ai_generated = true;
+        docData.ai_drawing_input = opts.text;
+        docData.ai_drawing_complexity = opts.complexity;
+        docData.ai_drawing_colored = opts.autoColor;
+        docData.ai_drawing_size = opts.size;
+        docData.ai_drawing_data = gptRes;
+        docData.ai = 'gpt';
     });
 
     pointerDown = (e: React.PointerEvent) => {
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 52652d377..8e41ee105 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -35,7 +35,7 @@ export class DrawingFillHandler {
                     .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
                     .then((prompt: string) => {
                         Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: prompt, width: dims.width, height: dims.height, structureUrl: structureUrl, strength: strength, styles: styles }).then((info: Upload.ImageInformation) =>
-                            DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { ai_generated: true, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
+                            DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { ai: 'firefly', ai_firefly_prompt: prompt, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
                         ); // prettier-ignore
                     });
             }
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 6c9470480..0c67c7a13 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -58,7 +58,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
 
     private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
     private _lastResponse: string = '';
-    private _selectedDoc: Doc | undefined = undefined;
+    private _selectedDocs: Doc[] = [];
     private _errorOccurredOnce = false;
 
     @observable private _display: boolean = false;
@@ -144,14 +144,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      */
     @action
     displayRegenerate = (x: number, y: number) => {
-        this._selectedDoc = DocumentView.SelectedDocs()?.lastElement();
+        this._selectedDocs = [DocumentView.SelectedDocs()?.lastElement()];
         [this._pageX, this._pageY] = [x, y];
         this._display = false;
         this.ShowRegenerate = true;
         this._showEditBox = false;
-        const docData = this._selectedDoc[DocData];
+        const docData = this._selectedDocs[0][DocData];
         this._lastResponse = StrCast(docData.drawingData);
-        this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY };
+        this._lastInput = { text: StrCast(docData.ai_drawing_input), complexity: NumCast(docData.ai_drawing_complexity), size: NumCast(docData.ai_drawing_size), autoColor: BoolCast(docData.ai_drawing_colored), x: this._pageX, y: this._pageY };
     };
 
     /**
@@ -205,8 +205,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         this._isLoading = true;
         this._canInteract = false;
         if (this.ShowRegenerate) {
-            await this.regenerate();
+            await this.regenerate(this._selectedDocs);
             runInAction(() => {
+                this._selectedDocs = [];
                 this._regenInput = '';
                 this._showEditBox = false;
             });
@@ -253,7 +254,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 const strokeData = await this.parseSvg(res, startPt, false, autoColor);
                 const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
                 drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
-                this._selectedDoc = drawingDoc;
+                drawingDoc && this._selectedDocs.push(drawingDoc);
                 this._errorOccurredOnce = false;
                 return strokeData;
             } else {
@@ -274,57 +275,62 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
                 nativeWidth: dims.width,
                 nativeHeight: dims.height,
-                ai_generated: true,
-                firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1],
-                firefly_prompt: input,
+                ai: 'firefly',
+                ai_firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1],
+                ai_firefly_prompt: input,
             });
             DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
-            this._selectedDoc = imgDoc;
+            this._selectedDocs.push(imgDoc);
         });
     };
 
     /**
      * Regenerates drawings with the option to add a specific regenerate prompt/request.
+     * @param doc the drawing Docs to regenerate
      */
     @action
-    regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
+    regenerate = async (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
         if (lastInput) this._lastInput = lastInput;
         if (lastResponse) this._lastResponse = lastResponse;
         if (regenInput) this._regenInput = regenInput;
-        if (this._generateImage) {
-            if (this._regenInput !== '') {
-                if (this._selectedDoc) {
-                    const docData = this._selectedDoc[DocData];
-                    const newPrompt = `${docData.firefly_prompt}, ${this._regenInput}`;
-                    const seed: number = docData?.firefly_seed as number;
-                    await this.createImageWithFirefly(newPrompt, seed);
+        await Promise.all(
+            drawingDocs.map(async doc => {
+                const docData = doc[DocData];
+                if (docData.type == 'image') {
+                    const seed: number = docData?.ai_firefly_seed as number;
+                    if (this._regenInput !== '') {
+                        // if (this._selectedDoc) {
+                        const newPrompt = `${docData.ai_firefly_prompt}, ${this._regenInput}`;
+                        await this.createImageWithFirefly(newPrompt, seed);
+                        // }
+                    } else {
+                        await this.createImageWithFirefly(this._lastInput.text || StrCast(docData.ai_firefly_prompt));
+                    }
                 }
-            } else {
-                await this.createImageWithFirefly(this._lastInput.text);
-            }
-        }
-        if (this._generateDrawing) {
-            try {
-                let res;
-                if (this._regenInput !== '') {
-                    const prompt: string = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
-                    res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
-                    this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`;
-                } else {
-                    res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
-                }
-                if (!res) {
-                    console.error('GPT call failed');
-                    return;
+                if (docData.type == 'collection') {
+                    try {
+                        let res;
+                        if (this._regenInput !== '') {
+                            const prompt: string = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
+                            res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
+                            this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`;
+                        } else {
+                            res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
+                        }
+                        if (!res) {
+                            console.error('GPT call failed');
+                            return;
+                        }
+                        const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+                        this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, doc);
+                        const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
+                        drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+                    } catch (err) {
+                        console.error('Error regenerating drawing', err);
+                    }
                 }
-                const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
-                this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, this._selectedDoc);
-                const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
-                drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
-            } catch (err) {
-                console.error('Error regenerating drawing', err);
-            }
-        }
+            })
+        );
     };
 
     /**
diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx
index 352a02e32..d23763eb9 100644
--- a/src/client/views/smartdraw/StickerPalette.tsx
+++ b/src/client/views/smartdraw/StickerPalette.tsx
@@ -142,19 +142,20 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
     }
 
     /**
-     * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from
+     * Calls the draw with AI functions in SmartDrawHandler to allow users to generate drawings straight from
      * the sticker palette.
      */
     @undoBatch
     generateDrawings = action(async () => {
         this._isLoading = true;
+        const prevDrawings = DocListCast(this._props.Document[DocData].data);
         this._props.Document[DocData].data = undefined;
         SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
         this._canInteract = false;
         await Promise.all(
             Array.from({ length: 3 }).map((_, i) => {
                 return this._showRegenerate
-                    ? SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput)
+                    ? SmartDrawHandler.Instance.regenerate(prevDrawings, this._opts, this._gptRes[i], this._userInput)
                     : SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
             })
         );
@@ -181,12 +182,12 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
         const focusedDrawing = DocListCast(this._props.Document.data)[cIndex];
         const docData = focusedDrawing[DocData];
         docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
-        docData.drawingInput = this._opts.text;
-        docData.drawingComplexity = this._opts.complexity;
-        docData.drawingColored = this._opts.autoColor;
-        docData.drawingSize = this._opts.size;
-        docData.drawingData = this._gptRes[cIndex];
-        docData.ai_generated = true;
+        docData.ai_drawing_input = this._opts.text;
+        docData.ai_drawing_complexity = this._opts.complexity;
+        docData.ai_drawing_colored = this._opts.autoColor;
+        docData.ai_drawing_size = this._opts.size;
+        docData.ai_drawing_data = this._gptRes[cIndex];
+        docData.ai = 'gpt';
         focusedDrawing.width = this._opts.size;
         docData.x = this._opts.x;
         docData.y = this._opts.y;
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index cb5d7c1da..6daa5840e 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -70,7 +70,6 @@ export default class FireflyManager extends ApiManager {
         );
 
     generateImage = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, seed?: number) => {
-        console.log('DIMENSIONS', width, height);
         let body = `{  "prompt": "${prompt}", "size": { "width": ${width}, "height": ${height}} }`;
         if (seed) {
             console.log('RECEIVED SEED', seed);
-- 
cgit v1.2.3-70-g09d2


From f273c97121300eb9b242f7272cc1cc56c396ca8b Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Mon, 6 Jan 2025 18:21:02 -0500
Subject: started a previous images display

---
 src/client/views/ViewBoxInterface.ts            |  3 +-
 src/client/views/nodes/DocumentView.scss        | 17 ++++++-
 src/client/views/nodes/DocumentView.tsx         | 62 +++++++++++++++----------
 src/client/views/nodes/ImageBox.scss            | 28 +++++++++--
 src/client/views/nodes/ImageBox.tsx             | 43 ++++++++++++-----
 src/client/views/smartdraw/SmartDrawHandler.tsx | 35 +++++++-------
 6 files changed, 129 insertions(+), 59 deletions(-)

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

diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index df08f2564..a66a20cf6 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -60,5 +60,6 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
     search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
     dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views
     isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations
-    componentAIView?: (top: number) => JSX.Element;
+    componentAIView?: () => JSX.Element;
+    componentAIViewHistory?: () => JSX.Element;
 }
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 7568e3b57..9490be98c 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -242,7 +242,7 @@
 
 .contentFittingDocumentView * {
     ::-webkit-scrollbar-track {
-        background: none; 
+        background: none;
     }
 }
 
@@ -270,3 +270,18 @@
         position: relative;
     }
 }
+
+.documentView-editorView-history {
+    position: absolute;
+    left: 0;
+    top: 0;
+}
+
+.documentView-editorView {
+    width: 100%;
+    overflow-y: scroll;
+
+    .documentView-editorView-resizer {
+        height: 5px;
+    }
+}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ab58023e3..f79eadde8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -723,31 +723,43 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
         const noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
         return (
-            <div
-                className="documentView-contentsView"
-                style={{
-                    pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
-                    width: this._showAIEditor ? this.rpw() : undefined,
-                    height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
-                    justifySelf: 'center',
-                }}>
-                <DocumentContentsView
-                    {...this._props}
-                    layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')}
-                    pointerEvents={this.contentPointerEvents}
-                    setContentViewBox={this.setContentView}
-                    childFilters={this.childFilters}
-                    PanelHeight={this._showAIEditor ? this.rpw : this.panelHeight}
-                    PanelWidth={this._showAIEditor ? this.rph : this._props.PanelWidth}
-                    setHeight={this.setHeight}
-                    isContentActive={this.isContentActive}
-                    ScreenToLocalTransform={this.screenToLocalContent}
-                    rootSelected={this.rootSelected}
-                    onClickScript={this.onClickFunc}
-                    setTitleFocus={this.setTitleFocus}
-                    hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
-                />
-                {this._showAIEditor && (this._componentView?.componentAIView?.(this.rph()) ?? null)}
+            <div>
+                <div
+                    className="documentView-contentsView"
+                    style={{
+                        pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
+                        width: this._showAIEditor ? this.rpw() : undefined,
+                        height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                        justifySelf: this._showAIEditor ? 'center' : undefined,
+                    }}>
+                    <DocumentContentsView
+                        {...this._props}
+                        layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')}
+                        pointerEvents={this.contentPointerEvents}
+                        setContentViewBox={this.setContentView}
+                        childFilters={this.childFilters}
+                        PanelHeight={this._showAIEditor ? this.rpw : this.panelHeight}
+                        PanelWidth={this._showAIEditor ? this.rph : this._props.PanelWidth}
+                        setHeight={this.setHeight}
+                        isContentActive={this.isContentActive}
+                        ScreenToLocalTransform={this.screenToLocalContent}
+                        rootSelected={this.rootSelected}
+                        onClickScript={this.onClickFunc}
+                        setTitleFocus={this.setTitleFocus}
+                        hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
+                    />
+                </div>
+                {this._showAIEditor && (
+                    <div className="documentView-editorView-history" style={{ height: this.rph(), width: this.rpw() / 2 }}>
+                        {this._componentView?.componentAIViewHistory?.() ?? null}
+                    </div>
+                )}
+                {this._showAIEditor && (
+                    <div className="documentView-editorView" style={{ height: this.panelHeight() - this.rph() }}>
+                        <div className="documentView-editorView-resizer" />
+                        {this._componentView?.componentAIView?.() ?? null}
+                    </div>
+                )}
             </div>
         );
     }
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 03314e90f..9f1ac11ef 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -140,15 +140,29 @@
     opacity: 0;
 }
 
-.imageBox-aiView {
+.imageBox-aiView-history {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
     padding: 5px;
-    position: absolute;
-    overflow: scroll;
+    gap: 5px;
+    overflow-y: scroll;
+
+    .imageBox-aiView-img {
+        width: 100%;
+    }
+}
+
+.imageBox-aiView {
+    overflow-y: scroll;
     text-align: center;
     font-weight: bold;
-    margin-top: 5px;
+    align-content: center;
+    height: 100%;
 
     .imageBox-aiView-subtitle {
+        position: relative;
+        left: 5px;
         align-self: start;
     }
 
@@ -156,6 +170,8 @@
     .imageBox-aiView-options-container {
         font-weight: normal;
         text-align: start;
+        margin: 5px;
+        padding-left: 5px;
     }
 
     .imageBox-aiView-regenerate,
@@ -166,4 +182,8 @@
         flex-direction: row;
         gap: 5px;
     }
+
+    .imageBox-aiView-input {
+        width: 50%;
+    }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index f00580d77..fc8b7bc27 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -7,8 +7,8 @@ import { observer } from 'mobx-react';
 import { extname } from 'path';
 import * as React from 'react';
 import ReactLoading from 'react-loading';
-import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { ClientUtils, DashColor, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
+import { Doc, DocListCast, Opt, returnEmptyDoclist } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
@@ -34,7 +34,7 @@ import { AnchorMenu } from '../pdf/AnchorMenu';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StickerPalette } from '../smartdraw/StickerPalette';
 import { StyleProp } from '../StyleProp';
-import { DocumentView } from './DocumentView';
+import { DocumentView, DocumentViewInternal } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
@@ -44,6 +44,7 @@ import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 import { Button } from 'browndash-components';
 import { SettingsManager } from '../../util/SettingsManager';
 import { AiOutlineSend } from 'react-icons/ai';
+import { returnEmptyDocViewList } from '../StyleProvider';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -532,18 +533,30 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _regenInput = '';
     @observable private _canInteract = true;
     @observable private _regenerateLoading = false;
+    @observable private _prevImgUrls: string[] = [];
 
-    componentAIView = (top: number) => {
+    componentAIViewHistory = () => {
+        return (
+            <div className="imageBox-aiView-history">
+                {this._prevImgUrls.map((url: string) => (
+                    <img className="imageBox-aiView-img" src={url} onClick={() => (this.dataDoc[this.fieldKey] = new ImageField(url))} />
+                ))}
+            </div>
+        );
+    };
+
+    componentAIView = () => {
         const field = Cast(this.dataDoc[this.fieldKey], ImageField);
         const showRegenerate = this.Document[DocData].ai;
         return (
-            <div className="imageBox-aiView" style={{ top: top, width: NumCast(this.Document.width), height: NumCast(this.Document.width) - top }}>
+            <div className="imageBox-aiView">
                 Edit Image with AI
                 {showRegenerate && (
                     <div className="imageBox-aiView-regenerate-container">
                         <text className="imageBox-aiView-subtitle">Regenerate AI Image</text>
                         <div className="imageBox-aiView-regenerate">
                             <input
+                                className="imageBox-aiView-input"
                                 aria-label="Edit instructions input"
                                 // className="smartdraw-input"
                                 type="text"
@@ -556,14 +569,20 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                 text="Regenerate"
                                 type={Type.SEC}
                                 // style={{ alignSelf: 'flex-end' }}
-                                icon={this._regenerateLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                                 iconPlacement="right"
-                                onClick={action(async () => {
-                                    this._regenerateLoading = true;
-                                    await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput);
-                                    this._regenerateLoading = false;
-                                    this._regenInput = '';
-                                })}
+                                onClick={undoable(
+                                    action(async () => {
+                                        this._regenerateLoading = true;
+                                        SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newDocs => {
+                                            this._prevImgUrls.push(this.paths.lastElement());
+                                            this.dataDoc[this.fieldKey] = new ImageField(newDocs[0]);
+                                            this._regenerateLoading = false;
+                                            this._regenInput = '';
+                                        });
+                                    }),
+                                    'regenerate image'
+                                )}
                             />
                             <Button
                                 // style={{ alignSelf: 'flex-end' }}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 0c67c7a13..13a93367c 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -267,20 +267,23 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     /**
      * Calls Firefly API to create an image based on user input
      */
-    createImageWithFirefly = (input: string, seed?: number) => {
+    createImageWithFirefly = (input: string, seed?: number, changeInPlace?: boolean) => {
         this._lastInput.text = input;
         const dims = FireflyDimensionsMap[this._imgDims];
         return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed }).then(img => {
-            const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
-                title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
-                nativeWidth: dims.width,
-                nativeHeight: dims.height,
-                ai: 'firefly',
-                ai_firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1],
-                ai_firefly_prompt: input,
-            });
-            DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
-            this._selectedDocs.push(imgDoc);
+            if (!changeInPlace) {
+                const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
+                    title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
+                    nativeWidth: dims.width,
+                    nativeHeight: dims.height,
+                    ai: 'firefly',
+                    ai_firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1],
+                    ai_firefly_prompt: input,
+                });
+                DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
+                this._selectedDocs.push(imgDoc);
+            }
+            return img.accessPaths.agnostic.client;
         });
     };
 
@@ -289,22 +292,22 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      * @param doc the drawing Docs to regenerate
      */
     @action
-    regenerate = async (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
+    regenerate = async (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string, changeInPlace?: boolean) => {
         if (lastInput) this._lastInput = lastInput;
         if (lastResponse) this._lastResponse = lastResponse;
         if (regenInput) this._regenInput = regenInput;
-        await Promise.all(
+        return await Promise.all(
             drawingDocs.map(async doc => {
                 const docData = doc[DocData];
                 if (docData.type == 'image') {
                     const seed: number = docData?.ai_firefly_seed as number;
                     if (this._regenInput !== '') {
                         // if (this._selectedDoc) {
-                        const newPrompt = `${docData.ai_firefly_prompt}, ${this._regenInput}`;
-                        await this.createImageWithFirefly(newPrompt, seed);
+                        const newPrompt = `${docData.ai_firefly_prompt} ~~~ ${this._regenInput}`;
+                        return this.createImageWithFirefly(newPrompt, seed, changeInPlace);
                         // }
                     } else {
-                        await this.createImageWithFirefly(this._lastInput.text || StrCast(docData.ai_firefly_prompt));
+                        return this.createImageWithFirefly(this._lastInput.text || StrCast(docData.ai_firefly_prompt), undefined, changeInPlace);
                     }
                 }
                 if (docData.type == 'collection') {
-- 
cgit v1.2.3-70-g09d2


From f5ee4974ab41af5b473aa94332ccdd6449db074f Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 7 Jan 2025 20:31:56 -0500
Subject: imagebox now has basic ai functionalities

---
 src/client/views/nodes/DocumentView.scss |  3 ++-
 src/client/views/nodes/DocumentView.tsx  |  2 +-
 src/client/views/nodes/ImageBox.scss     |  8 +++++---
 src/client/views/nodes/ImageBox.tsx      | 21 +++++++++++++++------
 4 files changed, 23 insertions(+), 11 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 9490be98c..82195b9c1 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -273,8 +273,9 @@
 
 .documentView-editorView-history {
     position: absolute;
-    left: 0;
+    right: 0;
     top: 0;
+    overflow-y: scroll;
 }
 
 .documentView-editorView {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index f79eadde8..622eccc4f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -750,7 +750,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     />
                 </div>
                 {this._showAIEditor && (
-                    <div className="documentView-editorView-history" style={{ height: this.rph(), width: this.rpw() / 2 }}>
+                    <div className="documentView-editorView-history" style={{ height: this.rph(), width: (this._props.PanelWidth() - this.rpw()) / 2 }}>
                         {this._componentView?.componentAIViewHistory?.() ?? null}
                     </div>
                 )}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 9f1ac11ef..e083f52b4 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -144,17 +144,19 @@
     display: flex;
     flex-direction: column;
     align-items: center;
-    padding: 5px;
+    padding: 5 0 0 5;
     gap: 5px;
-    overflow-y: scroll;
 
     .imageBox-aiView-img {
         width: 100%;
+
+        &:hover {
+            filter: brightness(0.8);
+        }
     }
 }
 
 .imageBox-aiView {
-    overflow-y: scroll;
     text-align: center;
     font-weight: bold;
     align-content: center;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index fc8b7bc27..09daea6bb 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -533,20 +533,26 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _regenInput = '';
     @observable private _canInteract = true;
     @observable private _regenerateLoading = false;
-    @observable private _prevImgUrls: string[] = [];
+    @observable private _prevImgs: { href: string; pathname: string }[] = [];
 
     componentAIViewHistory = () => {
         return (
             <div className="imageBox-aiView-history">
-                {this._prevImgUrls.map((url: string) => (
-                    <img className="imageBox-aiView-img" src={url} onClick={() => (this.dataDoc[this.fieldKey] = new ImageField(url))} />
+                {this._prevImgs.map(img => (
+                    <img
+                        className="imageBox-aiView-img"
+                        src={img.href}
+                        onClick={() => {
+                            this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
+                        }}
+                    />
                 ))}
             </div>
         );
     };
 
     componentAIView = () => {
-        const field = Cast(this.dataDoc[this.fieldKey], ImageField);
+        const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
         const showRegenerate = this.Document[DocData].ai;
         return (
             <div className="imageBox-aiView">
@@ -575,8 +581,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                     action(async () => {
                                         this._regenerateLoading = true;
                                         SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newDocs => {
-                                            this._prevImgUrls.push(this.paths.lastElement());
-                                            this.dataDoc[this.fieldKey] = new ImageField(newDocs[0]);
+                                            const url = newDocs[0];
+                                            const imgField = new ImageField(url);
+                                            this._prevImgs.length === 0 && this._prevImgs.push({ href: this.paths.lastElement(), pathname: field.url.pathname });
+                                            this.dataDoc[this.fieldKey] = imgField;
+                                            this._prevImgs.unshift({ href: this.paths.lastElement(), pathname: url });
                                             this._regenerateLoading = false;
                                             this._regenInput = '';
                                         });
-- 
cgit v1.2.3-70-g09d2


From 51a9f85b4ddc38ac825efcefc0c6db23f3b9914e Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 9 Jan 2025 11:19:31 -0500
Subject: added ai editor to collections

---
 src/client/apis/gpt/GPT.ts                         |  2 +-
 src/client/documents/Documents.ts                  |  1 +
 .../collectionFreeForm/CollectionFreeFormView.scss | 29 +++++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 98 +++++++++++++++-------
 src/client/views/nodes/DocumentView.scss           |  2 +
 src/client/views/nodes/DocumentView.tsx            |  4 +-
 src/client/views/nodes/ImageBox.scss               |  2 -
 src/client/views/nodes/ImageBox.tsx                | 73 +++++++++-------
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  2 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  2 +-
 10 files changed, 151 insertions(+), 64 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 4b0d58cc1..cf3a28a8e 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -260,7 +260,7 @@ const gptDescribeImage = async (image: string): Promise<string> => {
                     content: [
                         {
                             type: 'text',
-                            text: `Briefly identify what this drawing is, naming all the drawing elements and their location within the image. Do not include anything about the drawing style.`,
+                            text: `Very briefly identify what this drawing is and list all the drawing elements and their location within the image. Do not include anything about the drawing style.`,
                         },
                         {
                             type: 'image_url',
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7f1387ff8..0bff74ac1 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -19,6 +19,7 @@ import { DocServer } from '../DocServer';
 import { dropActionType } from '../util/DropActionTypes';
 import { CollectionViewType, DocumentType } from './DocumentTypes';
 import { Id } from '../../fields/FieldSymbols';
+import { FireflyImageData } from '../views/smartdraw/FireflyConstants';
 
 class EmptyBox {
     public static LayoutString() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 2c94446fb..dff2cb282 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -304,3 +304,32 @@
         display: none;
     }
 }
+
+.collectionFreeformView-aiView {
+    text-align: center;
+    font-weight: bold;
+    width: 100%;
+
+    .collectionfreeformview-aiView-prompt {
+        height: 25px;
+    }
+
+    .collectionFreeFormView-aiView-strength {
+        text-align: center;
+    }
+
+    .collectionFreeformView-aiView-options-container,
+    .collectionFreeFormView-aiView-regenerate-container {
+        text-align: start;
+        font-weight: normal;
+        padding: 5px;
+    }
+    .collectionFreeformView-aiView-options,
+    .collectionFreeFormView-aiView-regenerate {
+        display: flex;
+        flex-direction: row;
+        gap: 10px;
+        justify-content: center;
+        align-items: center;
+    }
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7834eb352..112bfd178 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,9 +1,5 @@
 import { Bezier } from 'bezier-js';
-<<<<<<< HEAD
-import { Button, Colors, Type } from 'browndash-components';
-=======
-import { Colors } from '@dash/components';
->>>>>>> 2f7d1f0073943e1eb9e0f34c4459bc0176377697
+import { Button, Colors, Type } from '@dash/components';
 import { Property } from 'csstype';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -73,6 +69,9 @@ import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 import ReactLoading from 'react-loading';
 import { SettingsManager } from '../../../util/SettingsManager';
+import { Slider } from '@mui/material';
+import { AiOutlineSend } from 'react-icons/ai';
+import { DrawingFillHandler } from '../../smartdraw/DrawingFillHandler';
 
 @observer
 class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
@@ -2194,8 +2193,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     @observable private _regenInput = '';
+    @observable private _drawingFillInput = '';
     @observable private _canInteract = true;
-    @observable private _regenerateLoading = false;
+    @observable private _regenLoading = false;
+    @observable private _drawingFillLoading = false;
+    @observable private _fireflyRefStrength = 50;
+    // _oldDrag: HTMLElement | null = null;
+
+    // onPassiveDrag = (e: PointerEvent) => e.stopPropagation();
+    // protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+    //     // this._dropDisposer?.();
+    //     this._oldDrag?.removeEventListener('pointerdown', this.onPassiveDrag);
+    //     this._oldDrag = ele;
+    //     // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+    //     ele?.addEventListener('pointerdown', this.onPassiveDrag, { passive: false });
+    // };
 
     componentAIViewHistory = () => {
         return (
@@ -2218,49 +2230,79 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     componentAIView = () => {
         const showRegenerate = this.Document[DocData].ai;
         return (
-            <div className="collectionfreeformview-aiView">
-                Edit Image with AI
+            <div className="collectionfreeformview-aiView" onPointerDown={e => e.stopPropagation()}>
+                Edit Collection with AI
                 {showRegenerate && (
                     <div className="collectionfreeformview-aiView-regenerate-container">
-                        {/* <text className="collectionfreeformview-aiView-subtitle">Regenerate AI Image</text>
+                        <text className="collectionfreeformview-aiView-subtitle">Regenerate AI Image</text>
                         <div className="collectionfreeformview-aiView-regenerate">
                             <input
                                 className="collectionfreeformview-aiView-input"
                                 aria-label="Edit instructions input"
-                                // className="smartdraw-input"
                                 type="text"
                                 value={this._regenInput}
                                 onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                                // onKeyDown={this.handleKeyPress}
                                 placeholder="Prompt (Optional)"
                             />
                             <Button
                                 text="Regenerate"
                                 type={Type.SEC}
-                                icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                                 iconPlacement="right"
                                 onClick={action(async () => {
-                                    this._regenerateLoading = true;
-                                    SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newDrawings => {
-                                        if (newDrawings[0]) {
-                                        }
-                                    });
+                                    this._regenLoading = true;
+                                    SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+                                    SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+                                    SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+                                    await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
+                                    this._regenLoading = false;
                                 })}
                             />
-                            <Button
-                                // style={{ alignSelf: 'flex-end' }}
-                                text="Get Variations"
-                                type={Type.SEC}
-                                // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
-                                // onClick={this.handleSendClick}
-                            />
-                        </div> */}
+                        </div>
                     </div>
                 )}
                 <div className="collectionfreeformview-aiView-options-container">
-                    {showRegenerate && <text className="collectionfreeformview-aiView-subtitle"> Turn Drawing to Image </text>}
-                    <div className="collectionfreeformview-aiView-options"></div>
+                    <text className="collectionfreeformview-aiView-subtitle"> Create Image with Firefly </text>
+                    <div className="collectionfreeformview-aiView-options">
+                        <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))} />
+                        <div className="collectionfreeformview-aiView-strength">
+                            Reference Strength
+                            <Slider
+                                className="collectionfreeformview-aiView-slider"
+                                sx={{
+                                    '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                                    '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
+                                    '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                                }}
+                                min={1}
+                                max={100}
+                                step={1}
+                                size="small"
+                                value={this._fireflyRefStrength}
+                                onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
+                                valueLabelDisplay="auto"
+                            />
+                        </div>
+                        <Button
+                            text="Send"
+                            type={Type.SEC}
+                            icon={this._drawingFillLoading && this._drawingFillInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={undoable(
+                                action(() => {
+                                    this._drawingFillLoading = true;
+                                    DrawingFillHandler.drawingToImage(
+                                        this.props.Document,
+                                        this._fireflyRefStrength,
+                                        this._drawingFillInput !== '' ? this._drawingFillInput : StrCast(this.props.Document.title) !== 'grouping' ? StrCast(this.props.Document.title) : ''
+                                    );
+                                    this._drawingFillInput = '';
+                                    this._drawingFillLoading = false;
+                                }),
+                                'create image'
+                            )}
+                        />
+                    </div>
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 82195b9c1..a3d47290a 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -281,6 +281,8 @@
 .documentView-editorView {
     width: 100%;
     overflow-y: scroll;
+    justify-items: center;
+    background-color: rgb(223, 223, 223);
 
     .documentView-editorView-resizer {
         height: 5px;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 622eccc4f..d656bcf50 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -716,8 +716,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    rpw = () => this._props.PanelWidth() * 0.6;
-    rph = () => this.panelHeight() * 0.6;
+    rpw = () => this._props.PanelWidth() / 2;
+    rph = () => this.panelHeight() / 2;
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index e083f52b4..9532f4ad3 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -159,8 +159,6 @@
 .imageBox-aiView {
     text-align: center;
     font-weight: bold;
-    align-content: center;
-    height: 100%;
 
     .imageBox-aiView-subtitle {
         position: relative;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index fe069eacc..db8bb2c6e 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,7 +1,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import axios from 'axios';
-import { Colors, Button, Type } from '@dash/components';
+import { Colors, Button, Type, Size } from '@dash/components';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { extname } from 'path';
@@ -76,6 +76,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
     _ffref = React.createRef<CollectionFreeFormView>();
+    _oldWheel: HTMLElement | null = null;
     private _ignoreScroll = false;
     private _forcedScroll = false;
     private _dropDisposer?: DragManager.DragDropDisposer;
@@ -94,6 +95,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _error = '';
     @observable private _isHovering = false; // flag to switch between primary and alternate images on hover
 
+    // variables for AI Image Editor
+    @observable private _regenInput = '';
+    @observable private _canInteract = true;
+    @observable private _regenerateLoading = false;
+    @observable private _prevImgs: FireflyImageData[] = [];
+
     constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
@@ -529,25 +536,34 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
 
-    @observable private _regenInput = '';
-    @observable private _canInteract = true;
-    @observable private _regenerateLoading = false;
-    @observable private _prevImgs: FireflyImageData[] = [];
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
+
+    protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+        // this._dropDisposer?.();
+        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+        this._oldWheel = ele;
+        // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+        ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+    };
 
     componentAIViewHistory = () => {
         return (
-            <div className="imageBox-aiView-history">
+            <div className="imageBox-aiView-history" ref={this.createDashEventsTarget}>
+                <Button text="Clear History" type={Type.SEC} size={Size.XSMALL} />
                 {this._prevImgs.map(img => (
-                    <img
-                        key={img.pathname}
-                        className="imageBox-aiView-img"
-                        src={img.href}
-                        onClick={() => {
-                            this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
-                            this.dataDoc.ai_firefly_prompt = img.prompt;
-                            this.dataDoc.ai_firefly_seed = img.seed;
-                        }}
-                    />
+                    <div>
+                        <img
+                            key={img.pathname}
+                            className="imageBox-aiView-img"
+                            src={img.href}
+                            onClick={() => {
+                                this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
+                                this.dataDoc.ai_firefly_prompt = img.prompt;
+                                this.dataDoc.ai_firefly_seed = img.seed;
+                            }}
+                        />
+                        <text>{img.prompt}</text>
+                    </div>
                 ))}
             </div>
         );
@@ -557,7 +573,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
         const showRegenerate = this.Document[DocData].ai;
         return (
-            <div className="imageBox-aiView">
+            <div
+                className="imageBox-aiView"
+                ref={(ele: HTMLDivElement | null) => {
+                    this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                    this._oldWheel = ele;
+                    // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+                    ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                }}>
                 Edit Image with AI
                 {showRegenerate && (
                     <div className="imageBox-aiView-regenerate-container">
@@ -566,11 +589,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             <input
                                 className="imageBox-aiView-input"
                                 aria-label="Edit instructions input"
-                                // className="smartdraw-input"
                                 type="text"
                                 value={this._regenInput}
                                 onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                                // onKeyDown={this.handleKeyPress}
                                 placeholder="Prompt (Optional)"
                             />
                             <Button
@@ -587,22 +608,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                             const imgField = new ImageField(url);
                                             this._prevImgs.length === 0 &&
                                                 this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
-                                            this.dataDoc[this.fieldKey] = imgField;
                                             this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, href: this.paths.lastElement(), pathname: url });
+                                            this.dataDoc.ai_firefly_history = `${this._prevImgs}`;
+                                            this.dataDoc[this.fieldKey] = imgField;
                                             this._regenerateLoading = false;
                                             this._regenInput = '';
                                         }
                                     });
                                 })}
                             />
-                            <Button
-                                // style={{ alignSelf: 'flex-end' }}
-                                text="Get Variations"
-                                type={Type.SEC}
-                                // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
-                                // onClick={this.handleSendClick}
-                            />
+                            <Button text="Get Variations" type={Type.SEC} iconPlacement="right" />
                         </div>
                     </div>
                 )}
@@ -704,8 +719,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const { nativeWidth: width, nativeHeight: height } = await Networking.PostToServer('/inspectImage', { source: this.paths[0] });
         return { width, height };
     };
-
     savedAnnotations = () => this._savedAnnotations;
+
     render() {
         TraceMobx();
         const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index fea4acb67..43d16df89 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -35,7 +35,7 @@ export class DrawingFillHandler {
                     .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
                     .then(prompt => {
                         Networking.PostToServer('/queryFireflyImageFromStructure', 
-                            { prompt: user_prompt || prompt, width: dims.width, height: dims.height, structureUrl, strength, styles })
+                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
                             .then((info: Upload.ImageInformation) =>
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
                                     { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 9248cbee3..4052ea852 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -279,7 +279,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     nativeWidth: dims.width,
                     nativeHeight: dims.height,
                     ai: 'firefly',
-                    ai_firefly_seed: aiseed,
+                    ai_firefly_seed: seed,
                     ai_firefly_prompt: input,
                 });
                 DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
-- 
cgit v1.2.3-70-g09d2


From c2312afe4ee969043ba3ce4cb9c275a48b0f1ee0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 10 Jan 2025 12:38:00 -0500
Subject: fixed scrolling when ai editor is open.  rearranged items in ImageBox
 ai editor to be more compact. added error catch handler to queryFireflyImage

---
 src/client/views/nodes/DocumentView.tsx         | 50 +++++++++++++++++--------
 src/client/views/nodes/ImageBox.scss            |  6 +--
 src/client/views/nodes/ImageBox.tsx             | 31 +++------------
 src/client/views/smartdraw/SmartDrawHandler.tsx | 34 +++++++++--------
 4 files changed, 59 insertions(+), 62 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d656bcf50..640d27aa1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -52,7 +52,6 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails/PresEnums';
 import SpringAnimation from './trails/SlideEffect';
 import { SpringType, springMappings } from './trails/SpringUtils';
-import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
 
 export interface DocumentViewProps extends FieldViewSharedProps {
     hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
@@ -716,21 +715,21 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    rpw = () => this._props.PanelWidth() / 2;
-    rph = () => this.panelHeight() / 2;
+    @observable _aiWinHeight = 100;
+    rpw = () => this._props.PanelWidth() - this._aiWinHeight;
+    rph = () => this.panelHeight() - this._aiWinHeight;
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
         const noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
         return (
-            <div>
+            <>
                 <div
                     className="documentView-contentsView"
                     style={{
                         pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
                         width: this._showAIEditor ? this.rpw() : undefined,
                         height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
-                        justifySelf: this._showAIEditor ? 'center' : undefined,
                     }}>
                     <DocumentContentsView
                         {...this._props}
@@ -749,20 +748,39 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                     />
                 </div>
-                {this._showAIEditor && (
-                    <div className="documentView-editorView-history" style={{ height: this.rph(), width: (this._props.PanelWidth() - this.rpw()) / 2 }}>
-                        {this._componentView?.componentAIViewHistory?.() ?? null}
-                    </div>
-                )}
-                {this._showAIEditor && (
-                    <div className="documentView-editorView" style={{ height: this.panelHeight() - this.rph() }}>
-                        <div className="documentView-editorView-resizer" />
-                        {this._componentView?.componentAIView?.() ?? null}
-                    </div>
+                {!this._showAIEditor ? null : (
+                    <>
+                        <div
+                            className="documentView-editorView-history"
+                            ref={r => {
+                                this.historyRef(this._oldAiWheel, (this._oldAiWheel = r));
+                            }}
+                            style={{ height: this.rph(), width: (this._props.PanelWidth() - this.rpw()) * 0.95 }}>
+                            {this._componentView?.componentAIViewHistory?.() ?? null}
+                        </div>
+                        <div
+                            className="documentView-editorView"
+                            ref={r => {
+                                this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r));
+                            }}>
+                            <div className="documentView-editorView-resizer" />
+                            {this._componentView?.componentAIView?.() ?? null}
+                        </div>
+                    </>
                 )}
-            </div>
+            </>
         );
     }
+    _oldHistoryWheel: HTMLDivElement | null = null;
+    _oldAiWheel: HTMLDivElement | null = null;
+    onPassiveWheel = (e: WheelEvent) => {
+        e.stopPropagation();
+    };
+
+    protected historyRef = (lastEle: HTMLDivElement | null, ele: HTMLDivElement | null) => {
+        lastEle?.removeEventListener('wheel', this.onPassiveWheel);
+        ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+    };
 
     captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption');
     fieldsDropdown = (placeholder: string) => (
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 9532f4ad3..1b6b3c85a 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -162,16 +162,14 @@
 
     .imageBox-aiView-subtitle {
         position: relative;
-        left: 5px;
-        align-self: start;
+        align-content: center;
     }
 
     .imageBox-aiView-regenerate-container,
     .imageBox-aiView-options-container {
         font-weight: normal;
-        text-align: start;
         margin: 5px;
-        padding-left: 5px;
+        display: flex;
     }
 
     .imageBox-aiView-regenerate,
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index db8bb2c6e..9838af4d0 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -76,7 +76,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
     _ffref = React.createRef<CollectionFreeFormView>();
-    _oldWheel: HTMLElement | null = null;
     private _ignoreScroll = false;
     private _forcedScroll = false;
     private _dropDisposer?: DragManager.DragDropDisposer;
@@ -536,24 +535,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
 
-    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
-
-    protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
-        // this._dropDisposer?.();
-        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
-        this._oldWheel = ele;
-        // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
-        ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
-    };
-
     componentAIViewHistory = () => {
         return (
-            <div className="imageBox-aiView-history" ref={this.createDashEventsTarget}>
+            <div className="imageBox-aiView-history">
                 <Button text="Clear History" type={Type.SEC} size={Size.XSMALL} />
                 {this._prevImgs.map(img => (
-                    <div>
+                    <div key={img.pathname}>
                         <img
-                            key={img.pathname}
                             className="imageBox-aiView-img"
                             src={img.href}
                             onClick={() => {
@@ -573,18 +561,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
         const showRegenerate = this.Document[DocData].ai;
         return (
-            <div
-                className="imageBox-aiView"
-                ref={(ele: HTMLDivElement | null) => {
-                    this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
-                    this._oldWheel = ele;
-                    // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
-                    ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
-                }}>
-                Edit Image with AI
+            <div className="imageBox-aiView">
                 {showRegenerate && (
                     <div className="imageBox-aiView-regenerate-container">
-                        <text className="imageBox-aiView-subtitle">Regenerate AI Image</text>
                         <div className="imageBox-aiView-regenerate">
                             <input
                                 className="imageBox-aiView-input"
@@ -595,7 +574,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                 placeholder="Prompt (Optional)"
                             />
                             <Button
-                                text="Regenerate"
+                                text="Regenerate Image"
                                 type={Type.SEC}
                                 // style={{ alignSelf: 'flex-end' }}
                                 icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
@@ -622,7 +601,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     </div>
                 )}
                 <div className="imageBox-aiView-options-container">
-                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Image Options </text>}
+                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Options: </text>}
                     <div className="imageBox-aiView-options">
                         <Button
                             type={Type.TERT}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 4052ea852..f635b5642 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -271,22 +271,24 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     createImageWithFirefly = (input: string, seed?: number, changeInPlace?: boolean): Promise<FireflyImageData> => {
         this._lastInput.text = input;
         const dims = FireflyDimensionsMap[this._imgDims];
-        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed }).then(img => {
-            const seed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
-            if (!changeInPlace) {
-                const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
-                    title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
-                    nativeWidth: dims.width,
-                    nativeHeight: dims.height,
-                    ai: 'firefly',
-                    ai_firefly_seed: seed,
-                    ai_firefly_prompt: input,
-                });
-                DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
-                this._selectedDocs.push(imgDoc);
-            }
-            return { prompt: input, seed, pathname: img.accessPaths.agnostic.client };
-        });
+        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed })
+            .then(img => {
+                const seed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
+                if (!changeInPlace) {
+                    const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
+                        title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
+                        nativeWidth: dims.width,
+                        nativeHeight: dims.height,
+                        ai: 'firefly',
+                        ai_firefly_seed: seed,
+                        ai_firefly_prompt: input,
+                    });
+                    DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
+                    this._selectedDocs.push(imgDoc);
+                }
+                return { prompt: input, seed, pathname: img.accessPaths.agnostic.client };
+            })
+            .catch(e => alert('create image failed: ' + e.toString()));
     };
 
     /**
-- 
cgit v1.2.3-70-g09d2


From 12f5a435ee6476e2e07ded0c9cdd597c70ca8af0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 13 Jan 2025 22:26:35 -0500
Subject: changing ai for images and collections to work mostly the same way. 
 fixes for document view/collection/images to keep things working when the ai
 editor view reduces the rendered document size.  fixed so that freeform views
 overlaid on images/vieos/etc have the ui menu items of collections.

---
 src/ClientUtils.ts                                 |  23 +-
 src/client/util/SelectionManager.ts                |   3 +-
 src/client/views/GlobalKeyHandler.ts               |   8 +-
 src/client/views/PropertiesView.tsx                |  48 +++--
 src/client/views/TagsView.tsx                      |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.scss |  15 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 236 ++++++++++-----------
 src/client/views/nodes/DocumentView.scss           |   1 -
 src/client/views/nodes/DocumentView.tsx            |  48 +++--
 src/client/views/nodes/ImageBox.scss               |  19 +-
 src/client/views/nodes/ImageBox.tsx                | 113 ++++++----
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  17 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |   8 +-
 src/server/ApiManagers/UploadManager.ts            |  22 +-
 14 files changed, 316 insertions(+), 247 deletions(-)

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

diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index 8f62b9060..e5dbb81db 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -144,15 +144,20 @@ export namespace ClientUtils {
     export async function convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename: string | undefined = undefined) {
         try {
             const posting = ClientUtils.prepend('/uploadURI');
-            const returnedUri = await rp.post(posting, {
-                body: {
-                    uri: imageUri,
-                    name: returnedFilename,
-                    nosuffix,
-                    replaceRootFilename,
-                },
-                json: true,
-            });
+            const returnedUri = await rp
+                .post(posting, {
+                    body: {
+                        uri: imageUri,
+                        name: returnedFilename,
+                        nosuffix,
+                        replaceRootFilename,
+                    },
+                    json: true,
+                })
+                .catch(e => {
+                    alert('Data URI Error: ' + e.toString());
+                    return undefined;
+                });
             return returnedUri;
         } catch (e) {
             console.log('ConvertDataURI :' + e);
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1ab84421c..a1f2849cd 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -88,7 +88,8 @@ ScriptingGlobals.add(function SelectedDocType(type: string, expertMode: boolean,
         return DocumentView.Selected().lastElement()?._props.renderDepth === 0;
     }
     const selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(DocumentView.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
-    return selected?.type === type || selected?.type_collection === type || !type;
+    const matchOverlayFreeform = type === CollectionViewType.Freeform && DocumentView.Selected().lastElement()?.ComponentView?.annotationKey;
+    return matchOverlayFreeform || selected?.type === type || selected?.type_collection === type || !type;
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function deselectAll() {
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index f16338361..b200aff65 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -169,10 +169,10 @@ export class KeyManager {
                     return { stopPropagation: true, preventDefault: true };
                 }
                 break;
-            case 'arrowleft':  return this.nudge(-1,0, 'nudge left')
-            case 'arrowright': return this.nudge(1,0, 'nudge right');
-            case 'arrowup':    return this.nudge(0, -1, 'nudge up');
-            case 'arrowdown':  return this.nudge(0, 1, 'nudge down');
+            case 'arrowleft':  return (e.target as any).type !== 'text' && this.nudge(-1, 0, 'nudge left') // if target is an input box, then we don't want to nudge any Docs since we're justing moving within the text itself.
+            case 'arrowright': return (e.target as any).type !== 'text' && this.nudge( 1, 0, 'nudge right');
+            case 'arrowup':    return (e.target as any).type !== 'text' && this.nudge(0, -1, 'nudge up');
+            case 'arrowdown':  return (e.target as any).type !== 'text' && this.nudge(0,  1, 'nudge down');
             default:
         } // prettier-ignore
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index e52189f56..42aa6782f 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -111,6 +111,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @observable openContexts: boolean = true;
     @observable openLinks: boolean = true;
     @observable openAppearance: boolean = true;
+    @observable openFirefly: boolean = true;
     @observable openTransform: boolean = true;
     @observable openFilters: boolean = false;
     @observable openStyling: boolean = true;
@@ -982,26 +983,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             10,
             1
         );
-        const strength = this.getNumber('Reference Strength', '', 1, 100, this.refStrength, (val: number) => {
-            !isNaN(val) && (this.refStrength = val);
-        });
         return (
             <div>
-                {!targetDoc.layout_isSvg && this.containsInkDoc && (
-                    <div>
-                        <div className="drawing-to-image">
-                            <Toggle
-                                text={'Create Image'}
-                                color={SettingsManager.userColor}
-                                icon={<FontAwesomeIcon icon="fill-drip" />}
-                                iconPlacement="left"
-                                align="flex-start"
-                                fillWidth
-                                toggleType={ToggleType.BUTTON}
-                                onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, StrCast(targetDoc.title) !== 'grouping' ? StrCast(targetDoc.title) : ''), 'createImage')}
-                            />
-                        </div>
-                        <div className="strength-slider">{strength}</div>
+                <div>
+                    {!targetDoc.layout_isSvg && this.containsInkDoc && (
                         <div className="color">
                             <Toggle
                                 text={'Color with GPT'}
@@ -1016,8 +1001,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                                 }, 'colorWithGPT')}
                             />
                         </div>
-                    </div>
-                )}
+                    )}
+                </div>
                 <div className="smooth">
                     <Toggle
                         text={'Smooth Ink Strokes'}
@@ -1113,6 +1098,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         this.openTransform = false;
         this.openFields = false;
         this.openSharing = false;
+        this.openAppearance = false;
+        this.openFirefly = false;
         this.openLayout = false;
         this.openFilters = false;
     };
@@ -1323,11 +1310,32 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get inkSubMenu() {
+        const strength = this.getNumber('Reference Strength', '', 1, 100, this.refStrength, (val: number) => {
+            !isNaN(val) && (this.refStrength = val);
+        });
+        const targetDoc = this.selectedLayoutDoc;
         return (
             <>
                 <PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}>
                     {this.selectedStrokes.length ? this.inkEditor : null}
                 </PropertiesSection>
+                <PropertiesSection  title="Firefly" isOpen={this.openFirefly} setIsOpen={bool => { this.openFirefly = bool; }} onDoubleClick={this.CloseAll}>
+                    <>
+                <div className="drawing-to-image">
+                        <Toggle
+                            text="Create Image"
+                            color={SettingsManager.userColor}
+                            icon={<FontAwesomeIcon icon="fill-drip" />}
+                            iconPlacement="left"
+                            align="flex-start"
+                            fillWidth
+                            toggleType={ToggleType.BUTTON}
+                            onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, StrCast(targetDoc.title) !== 'grouping' ? StrCast(targetDoc.title) : ''), 'createImage')}
+                        />
+                    </div>
+                    <div className="strength-slider">{strength}</div>
+                    </>
+                </PropertiesSection>
                 <PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => { this.openTransform = bool; }} onDoubleClick={this.CloseAll}>
                     {this.transformEditor}
                 </PropertiesSection>
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index 21b6a76c7..0edd89204 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -366,7 +366,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
                     backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
                     borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
                     position: 'relative',
-                    top: this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
+                    top: this._props.Views.lastElement()._showAIEditor ? undefined : this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
                 }}>
                 <div className="tagsView-content" style={{ width: '100%' }}>
                     <div className="tagsView-list">
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index dff2cb282..82887a7a5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -312,10 +312,14 @@
 
     .collectionfreeformview-aiView-prompt {
         height: 25px;
+        width: 100%;
     }
 
     .collectionFreeFormView-aiView-strength {
         text-align: center;
+        align-items: center;
+        display: flex;
+        width: 150px;
     }
 
     .collectionFreeformView-aiView-options-container,
@@ -323,13 +327,20 @@
         text-align: start;
         font-weight: normal;
         padding: 5px;
+        width: 100%;
+        display: flex;
+        .collectionFreeformView-aiView-subtitle {
+            margin: auto;
+            width: 40px;
+        }
     }
     .collectionFreeformView-aiView-options,
     .collectionFreeFormView-aiView-regenerate {
         display: flex;
         flex-direction: row;
-        gap: 10px;
-        justify-content: center;
         align-items: center;
+        align-items: center;
+        width: 100%;
+        gap: 10px;
     }
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 112bfd178..5b4f81379 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1877,8 +1877,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             : UpdateIcon(
                   this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
                   contentDiv,
-                  usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
-                  usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
+                  usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+                  usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
                   this._props.PanelWidth(),
                   this._props.PanelHeight(),
                   0,
@@ -2228,45 +2228,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     componentAIView = () => {
-        const showRegenerate = this.Document[DocData].ai;
         return (
             <div className="collectionfreeformview-aiView" onPointerDown={e => e.stopPropagation()}>
-                Edit Collection with AI
-                {showRegenerate && (
-                    <div className="collectionfreeformview-aiView-regenerate-container">
-                        <text className="collectionfreeformview-aiView-subtitle">Regenerate AI Image</text>
-                        <div className="collectionfreeformview-aiView-regenerate">
-                            <input
-                                className="collectionfreeformview-aiView-input"
-                                aria-label="Edit instructions input"
-                                type="text"
-                                value={this._regenInput}
-                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                                placeholder="Prompt (Optional)"
-                            />
-                            <Button
-                                text="Regenerate"
-                                type={Type.SEC}
-                                icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
-                                onClick={action(async () => {
-                                    this._regenLoading = true;
-                                    SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
-                                    SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
-                                    SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
-                                    await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
-                                    this._regenLoading = false;
-                                })}
-                            />
-                        </div>
-                    </div>
-                )}
                 <div className="collectionfreeformview-aiView-options-container">
-                    <text className="collectionfreeformview-aiView-subtitle"> Create Image with Firefly </text>
+                    <span className="collectionfreeformview-aiView-subtitle">Firefly:</span>
                     <div className="collectionfreeformview-aiView-options">
-                        <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))} />
+                        <input
+                            className="collectionfreeformview-aiView-prompt"
+                            placeholder={this._drawingFillInput || StrCast(this.Document.title) || 'Describe image'}
+                            type="text"
+                            value={this._drawingFillInput}
+                            onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))}
+                        />
                         <div className="collectionfreeformview-aiView-strength">
-                            Reference Strength
+                            Similarity
                             <Slider
                                 className="collectionfreeformview-aiView-slider"
                                 sx={{
@@ -2286,24 +2261,49 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         <Button
                             text="Send"
                             type={Type.SEC}
-                            icon={this._drawingFillLoading && this._drawingFillInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            icon={this._drawingFillLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                             iconPlacement="right"
                             onClick={undoable(
                                 action(() => {
                                     this._drawingFillLoading = true;
-                                    DrawingFillHandler.drawingToImage(
-                                        this.props.Document,
-                                        this._fireflyRefStrength,
-                                        this._drawingFillInput !== '' ? this._drawingFillInput : StrCast(this.props.Document.title) !== 'grouping' ? StrCast(this.props.Document.title) : ''
+                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then(
+                                        action(() => {
+                                            this._drawingFillLoading = false;
+                                        })
                                     );
-                                    this._drawingFillInput = '';
-                                    this._drawingFillLoading = false;
                                 }),
                                 'create image'
                             )}
                         />
                     </div>
                 </div>
+                <div className="collectionfreeformview-aiView-regenerate-container">
+                    <span className="collectionfreeformview-aiView-subtitle">Regenerate</span>
+                    <div className="collectionfreeformview-aiView-regenerate">
+                        <input
+                            className="collectionfreeformview-aiView-input"
+                            aria-label="Edit instructions input"
+                            type="text"
+                            value={this._regenInput}
+                            onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                            placeholder="..under development.."
+                        />
+                        <Button
+                            text="Regenerate"
+                            type={Type.SEC}
+                            icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            // onClick={action(async () => {
+                            //     this._regenLoading = true;
+                            //     SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+                            //     SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+                            //     SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+                            //     await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
+                            //     this._regenLoading = false;
+                            // })}
+                        />
+                    </div>
+                </div>
             </div>
         );
     };
@@ -2311,83 +2311,81 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     render() {
         TraceMobx();
         return (
-            <div>
-                <div
-                    className="collectionfreeformview-container"
-                    id={this._paintedId}
-                    ref={r => {
-                        this.createDashEventsTarget(r);
-                        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
-                        this._oldWheel = r;
-                        // prevent wheel events from passivly propagating up through containers
-                        r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
-                        r?.addEventListener('mouseleave', this.onMouseLeave);
-                        r?.addEventListener('mouseenter', this.onMouseEnter);
-                    }}
-                    onWheel={this.onPointerWheel}
-                    onClick={this.onClick}
-                    onPointerDown={this.onPointerDown}
-                    onPointerMove={this.onCursorMove}
-                    onDrop={this.onExternalDrop}
-                    onDragOver={e => e.preventDefault()}
-                    onContextMenu={this.onContextMenu}
-                    style={{
-                        pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : this._props.pointerEvents?.(),
-                        textAlign: this.isAnnotationOverlay ? 'initial' : undefined,
-                        transform: `scale(${this.nativeDimScaling})`,
-                        width: `${100 / this.nativeDimScaling}%`,
-                        height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
-                    }}>
-                    {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
-                        <div
-                            onPointerMove={this.onCursorMove}
-                            style={{
-                                position: 'fixed',
-                                left: this._eraserX,
-                                top: this._eraserY,
-                                width: (ActiveEraserWidth() + 5) * 2,
-                                height: (ActiveEraserWidth() + 5) * 2,
-                                borderRadius: '50%',
-                                border: '1px solid gray',
-                                transform: 'translate(-50%, -50%)',
-                            }}
+            <div
+                className="collectionfreeformview-container"
+                id={this._paintedId}
+                ref={r => {
+                    this.createDashEventsTarget(r);
+                    this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                    this._oldWheel = r;
+                    // prevent wheel events from passivly propagating up through containers
+                    r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                    r?.addEventListener('mouseleave', this.onMouseLeave);
+                    r?.addEventListener('mouseenter', this.onMouseEnter);
+                }}
+                onWheel={this.onPointerWheel}
+                onClick={this.onClick}
+                onPointerDown={this.onPointerDown}
+                onPointerMove={this.onCursorMove}
+                onDrop={this.onExternalDrop}
+                onDragOver={e => e.preventDefault()}
+                onContextMenu={this.onContextMenu}
+                style={{
+                    pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : this._props.pointerEvents?.(),
+                    textAlign: this.isAnnotationOverlay ? 'initial' : undefined,
+                    transform: `scale(${this.nativeDimScaling})`,
+                    width: `${100 / this.nativeDimScaling}%`,
+                    height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
+                }}>
+                {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
+                    <div
+                        onPointerMove={this.onCursorMove}
+                        style={{
+                            position: 'fixed',
+                            left: this._eraserX,
+                            top: this._eraserY,
+                            width: (ActiveEraserWidth() + 5) * 2,
+                            height: (ActiveEraserWidth() + 5) * 2,
+                            borderRadius: '50%',
+                            border: '1px solid gray',
+                            transform: 'translate(-50%, -50%)',
+                        }}
+                    />
+                )}
+                {this.paintFunc ? (
+                    <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
+                ) : this._lightboxDoc ? (
+                    <div style={{ padding: 15, width: '100%', height: '100%' }}>
+                        <DocumentView
+                            {...this._props}
+                            Document={this._lightboxDoc}
+                            containerViewPath={this.DocumentView?.().docViewPath}
+                            TemplateDataDocument={undefined}
+                            PanelWidth={this.lightboxPanelWidth}
+                            PanelHeight={this.lightboxPanelHeight}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
+                            onClickScript={this.onChildClickHandler}
+                            onKey={this.onKeyDown}
+                            onDoubleClickScript={this.onChildDoubleClickHandler}
+                            childFilters={this.childDocFilters}
+                            childFiltersByRanges={this.childDocRangeFilters}
+                            searchFilterDocs={this.searchFilterDocs}
+                            isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive}
+                            isContentActive={this._props.childContentsActive ?? emptyFunction}
+                            addDocTab={this.addDocTab}
+                            ScreenToLocalTransform={this.lightboxScreenToLocal}
+                            fitContentsToBox={undefined}
+                            focus={this.focus}
                         />
-                    )}
-                    {this.paintFunc ? (
-                        <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
-                    ) : this._lightboxDoc ? (
-                        <div style={{ padding: 15, width: '100%', height: '100%' }}>
-                            <DocumentView
-                                {...this._props}
-                                Document={this._lightboxDoc}
-                                containerViewPath={this.DocumentView?.().docViewPath}
-                                TemplateDataDocument={undefined}
-                                PanelWidth={this.lightboxPanelWidth}
-                                PanelHeight={this.lightboxPanelHeight}
-                                NativeWidth={returnZero}
-                                NativeHeight={returnZero}
-                                onClickScript={this.onChildClickHandler}
-                                onKey={this.onKeyDown}
-                                onDoubleClickScript={this.onChildDoubleClickHandler}
-                                childFilters={this.childDocFilters}
-                                childFiltersByRanges={this.childDocRangeFilters}
-                                searchFilterDocs={this.searchFilterDocs}
-                                isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive}
-                                isContentActive={this._props.childContentsActive ?? emptyFunction}
-                                addDocTab={this.addDocTab}
-                                ScreenToLocalTransform={this.lightboxScreenToLocal}
-                                fitContentsToBox={undefined}
-                                focus={this.focus}
-                            />
-                        </div>
-                    ) : (
-                        <>
-                            {this._firstRender ? this.placeholder : this.marqueeView}
-                            {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
-                            {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />}
-                        </>
-                    )}
-                </div>
+                    </div>
+                ) : (
+                    <>
+                        {this._firstRender ? this.placeholder : this.marqueeView}
+                        {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
+                        {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />}
+                    </>
+                )}
             </div>
         );
     }
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index a3d47290a..8e7307d23 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -280,7 +280,6 @@
 
 .documentView-editorView {
     width: 100%;
-    overflow-y: scroll;
     justify-items: center;
     background-color: rgb(223, 223, 223);
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 640d27aa1..e37658ca5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -85,7 +85,7 @@ export interface DocumentViewProps extends FieldViewSharedProps {
     reactParent?: React.Component; // parent React component view (see CollectionFreeFormDocumentView)
 }
 @observer
-export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps>() {
+export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps & { showAIEditor: boolean }>() {
     // this makes mobx trace() statements more descriptive
     public get displayName()  { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore
     public static SelectAfterContextMenu = true; //  whether a document should be selected after it's contextmenu is triggered.
@@ -109,7 +109,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     private _mainCont = React.createRef<HTMLDivElement>();
     private _titleRef = React.createRef<EditableView>();
     private _dropDisposer?: DragManager.DragDropDisposer;
-    constructor(props: FieldViewProps & DocumentViewProps) {
+    constructor(props: FieldViewProps & DocumentViewProps & { showAIEditor: boolean }) {
         super(props);
         makeObservable(this);
     }
@@ -122,12 +122,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @observable _componentView: Opt<ViewBoxInterface<FieldViewProps>> = undefined; // needs to be accessed from DocumentView wrapper class
     @observable _animateScaleTime: Opt<number> = undefined; // milliseconds for animating between views.  defaults to 300 if not uset
     @observable _animateScalingTo = 0;
-    @observable public _showAIEditor: boolean = false;
-
-    @action
-    showAIEditor() {
-        this._showAIEditor = !this._showAIEditor;
-    }
 
     get _contentDiv()  { return this._mainCont.current; } // prettier-ignore
     get _docView()     { return this._props.DocumentView?.(); } // prettier-ignore
@@ -689,7 +683,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     rootSelected = () => this._rootSelected;
     panelHeight = () => this._props.PanelHeight() - this.headerMargin;
-    screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin);
+    screenToLocalContent = () =>
+        this._props
+            .ScreenToLocalTransform()
+            .translate(0, -this.headerMargin)
+            .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.rph() : 1);
     onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr;
     setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore
     setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore
@@ -715,9 +713,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    @observable _aiWinHeight = 100;
-    rpw = () => this._props.PanelWidth() - this._aiWinHeight;
-    rph = () => this.panelHeight() - this._aiWinHeight;
+    @observable _aiWinHeight = 95;
+    rpw = () => (this.rph() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
+    rph = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight);
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -728,8 +726,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     className="documentView-contentsView"
                     style={{
                         pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
-                        width: this._showAIEditor ? this.rpw() : undefined,
-                        height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                        width: this._props.showAIEditor ? this.rpw() : undefined,
+                        height: this._props.showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
                     }}>
                     <DocumentContentsView
                         {...this._props}
@@ -737,8 +735,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         pointerEvents={this.contentPointerEvents}
                         setContentViewBox={this.setContentView}
                         childFilters={this.childFilters}
-                        PanelHeight={this._showAIEditor ? this.rpw : this.panelHeight}
-                        PanelWidth={this._showAIEditor ? this.rph : this._props.PanelWidth}
+                        PanelWidth={this._props.showAIEditor ? this.rpw : this._props.PanelWidth}
+                        PanelHeight={this._props.showAIEditor ? this.rph : this.panelHeight}
                         setHeight={this.setHeight}
                         isContentActive={this.isContentActive}
                         ScreenToLocalTransform={this.screenToLocalContent}
@@ -748,7 +746,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                     />
                 </div>
-                {!this._showAIEditor ? null : (
+                {!this._props.showAIEditor ? null : (
                     <>
                         <div
                             className="documentView-editorView-history"
@@ -1168,10 +1166,13 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     @computed private get nativeScaling() {
         if (this.shouldNotScale) return 1;
         const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0;
-        if (this.layout_fitWidth || this._props.PanelHeight() / (this.effectiveNativeHeight || 1) > this._props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
-            return Math.max(minTextScale, this._props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth
+        const ai = this._showAIEditor && this.nativeWidth === this.layoutDoc.width ? 95 : 0;
+        const effNW = Math.max(this.effectiveNativeWidth - ai, 1);
+        const effNH = Math.max(this.effectiveNativeHeight - ai, 1);
+        if (this.layout_fitWidth || (this._props.PanelHeight() - ai) / effNH > (this._props.PanelWidth() - ai) / effNW) {
+            return Math.max(minTextScale, (this._props.PanelWidth() - ai) / effNW); // width-limited or layout_fitWidth
         }
-        return Math.max(minTextScale, this._props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
+        return Math.max(minTextScale, (this._props.PanelHeight() - ai) / effNH); // height-limited or unscaled
     }
     @computed private get panelWidth() {
         return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this._props.PanelWidth();
@@ -1231,7 +1232,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     }
 
     @computed get layout_fitWidth() {
-        return this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth;
+        return this._showAIEditor ? false : (this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth);
     }
     @computed get anchorViewDoc() {
         return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document.link_anchor_2) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document.link_anchor_1) : undefined;
@@ -1327,9 +1328,11 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         }
     };
 
+    @observable public _showAIEditor: boolean = false;
+
     @action
     public toggleAIEditor = () => {
-        this._docViewInternal && this._docViewInternal.showAIEditor();
+        this._showAIEditor = !this._showAIEditor;
     };
 
     public setTextHtmlOverlay = action((text: string | undefined, effect?: Doc) => {
@@ -1516,6 +1519,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                         }}>
                         <DocumentViewInternal
                             {...this._props}
+                            showAIEditor={this._showAIEditor}
                             reactParent={undefined}
                             isHovering={this.isHovering}
                             fieldKey={this.LayoutFieldKey}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 1b6b3c85a..a50320f5e 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -40,6 +40,7 @@
     max-height: 100%;
     pointer-events: inherit;
     background: transparent;
+    z-index: -10000;
 
     img {
         height: auto;
@@ -159,6 +160,7 @@
 .imageBox-aiView {
     text-align: center;
     font-weight: bold;
+    width: 100%;
 
     .imageBox-aiView-subtitle {
         position: relative;
@@ -168,20 +170,29 @@
     .imageBox-aiView-regenerate-container,
     .imageBox-aiView-options-container {
         font-weight: normal;
-        margin: 5px;
         display: flex;
     }
 
     .imageBox-aiView-regenerate,
     .imageBox-aiView-options {
         display: flex;
-        flex-direction: row;
-        justify-content: center;
+        align-items: center;
         flex-direction: row;
         gap: 5px;
+        width: 100%;
     }
 
+    .imageBox-aiView-strength {
+        text-align: center;
+        align-items: center;
+        display: flex;
+        width: 125px;
+    }
+    .imageBox-aiView-slider {
+        width: 50px;
+        margin-left: 5px;
+    }
     .imageBox-aiView-input {
-        width: 50%;
+        width: 100%;
     }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9838af4d0..9223fc180 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@mui/material';
+import { Slider, Tooltip } from '@mui/material';
 import axios from 'axios';
 import { Colors, Button, Type, Size } from '@dash/components';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
@@ -44,6 +44,7 @@ import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 import { SettingsManager } from '../../util/SettingsManager';
 import { AiOutlineSend } from 'react-icons/ai';
 import { FireflyImageData } from '../smartdraw/FireflyConstants';
+import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -98,7 +99,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _regenInput = '';
     @observable private _canInteract = true;
     @observable private _regenerateLoading = false;
-    @observable private _prevImgs: FireflyImageData[] = [];
+    @observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : [];
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -386,8 +387,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             : UpdateIcon(
                   this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
                   contentDiv,
-                  usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
-                  usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
+                  usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+                  usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
                   this._props.PanelWidth(),
                   this._props.PanelHeight(),
                   0,
@@ -543,65 +544,91 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     <div key={img.pathname}>
                         <img
                             className="imageBox-aiView-img"
-                            src={img.href}
+                            src={ClientUtils.prepend(img.pathname.replace(extname(img.pathname), '_s' + extname(img.pathname)))}
                             onClick={() => {
                                 this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
                                 this.dataDoc.ai_firefly_prompt = img.prompt;
                                 this.dataDoc.ai_firefly_seed = img.seed;
                             }}
                         />
-                        <text>{img.prompt}</text>
+                        <span>{img.prompt}</span>
                     </div>
                 ))}
             </div>
         );
     };
+    @observable private _fireflyRefStrength = 0;
 
     componentAIView = () => {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
-        const showRegenerate = this.Document[DocData].ai;
         return (
             <div className="imageBox-aiView">
-                {showRegenerate && (
-                    <div className="imageBox-aiView-regenerate-container">
-                        <div className="imageBox-aiView-regenerate">
-                            <input
-                                className="imageBox-aiView-input"
-                                aria-label="Edit instructions input"
-                                type="text"
-                                value={this._regenInput}
-                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                                placeholder="Prompt (Optional)"
-                            />
-                            <Button
-                                text="Regenerate Image"
-                                type={Type.SEC}
-                                // style={{ alignSelf: 'flex-end' }}
-                                icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
-                                onClick={action(async () => {
-                                    this._regenerateLoading = true;
-                                    SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newImgs => {
-                                        if (newImgs[0]) {
-                                            const url = newImgs[0].pathname;
-                                            const imgField = new ImageField(url);
-                                            this._prevImgs.length === 0 &&
-                                                this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
-                                            this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, href: this.paths.lastElement(), pathname: url });
-                                            this.dataDoc.ai_firefly_history = `${this._prevImgs}`;
-                                            this.dataDoc[this.fieldKey] = imgField;
-                                            this._regenerateLoading = false;
-                                            this._regenInput = '';
-                                        }
-                                    });
-                                })}
+                <div className="imageBox-aiView-regenerate-container">
+                    <div className="imageBox-aiView-regenerate">
+                        Firefly:
+                        <input
+                            className="imageBox-aiView-input"
+                            aria-label="Edit instructions input"
+                            type="text"
+                            value={this._regenInput}
+                            onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                            placeholder={this._regenInput || StrCast(this.Document.title)}
+                        />
+                        <div className="imageBox-aiView-strength">
+                            <span style={{ width: 60 }}>Similarity</span>
+                            <Slider
+                                className="imageBox-aiView-slider"
+                                sx={{
+                                    '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                                    '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
+                                    '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                                }}
+                                min={0}
+                                max={100}
+                                step={1}
+                                size="small"
+                                value={this._fireflyRefStrength}
+                                onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
+                                valueLabelDisplay="auto"
                             />
-                            <Button text="Get Variations" type={Type.SEC} iconPlacement="right" />
                         </div>
+                        <Button
+                            text="Create"
+                            type={Type.SEC}
+                            // style={{ alignSelf: 'flex-end' }}
+                            icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={action(async () => {
+                                this._regenerateLoading = true;
+                                if (this._fireflyRefStrength) {
+                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title))?.then(
+                                        action(() => {
+                                            this._regenerateLoading = false;
+                                        })
+                                    );
+                                } else
+                                    SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then(
+                                        action(newImgs => {
+                                            if (newImgs[0]) {
+                                                const url = newImgs[0].pathname;
+                                                const imgField = new ImageField(url);
+                                                this._prevImgs.length === 0 &&
+                                                    this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
+                                                this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, pathname: url });
+                                                this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs);
+                                                this.dataDoc.ai_firefly_prompt = newImgs[0].prompt;
+                                                this.dataDoc[this.fieldKey] = imgField;
+                                                this._regenerateLoading = false;
+                                                this._regenInput = '';
+                                            }
+                                        })
+                                    );
+                            })}
+                        />
                     </div>
-                )}
+                </div>
                 <div className="imageBox-aiView-options-container">
-                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Options: </text>}
+                    <span className="imageBox-aiView-subtitle"> More Options: </span>
                     <div className="imageBox-aiView-options">
                         <Button
                             type={Type.TERT}
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 423e3b2ac..d9a01c436 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -15,7 +15,7 @@ export class DrawingFillHandler {
         const docData = drawing[DocData];
         const tags: string[] = ((docData?.tags as unknown as string[]) ?? []).map(tag => tag.slice(1)) ?? [];
         const styles = tags.filter(tag => FireflyStylePresets.has(tag));
-        DocumentView.GetDocImage(drawing)?.then(imageField => {
+        return DocumentView.GetDocImage(drawing)?.then(imageField => {
             if (imageField) {
                 const aspectRatio = (drawing.width as number) / (drawing.height as number);
                 let dims: { width: number; height: number };
@@ -31,18 +31,17 @@ export class DrawingFillHandler {
                 const { href } = ImageCast(imageField).url;
                 const hrefParts = href.split('.');
                 const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
-                imageUrlToBase64(structureUrl)
-                    .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
-                    .then(prompt => {
+                return imageUrlToBase64(structureUrl)
+                    .then(gptDescribeImage)
+                    .then(prompt => 
                         Networking.PostToServer('/queryFireflyImageFromStructure', 
-                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
+                            { prompt: `${user_prompt || prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
                             .then((info: Upload.ImageInformation) =>
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
-                                    { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
-                        ).catch(e => alert("create image failed: " + e.toString())); // prettier-ignore
-                    });
+                                    { ai: 'firefly', title: user_prompt || prompt, ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
+                        ).catch(e => alert("create image failed: " + e.toString())) 
+                    ); // prettier-ignore
             }
-            return false;
         });
     };
 }
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index f635b5642..7db9ef133 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -271,16 +271,16 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     createImageWithFirefly = (input: string, seed?: number, changeInPlace?: boolean): Promise<FireflyImageData> => {
         this._lastInput.text = input;
         const dims = FireflyDimensionsMap[this._imgDims];
-        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed })
+        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed })
             .then(img => {
-                const seed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
+                const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
                 if (!changeInPlace) {
                     const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
                         title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
                         nativeWidth: dims.width,
                         nativeHeight: dims.height,
                         ai: 'firefly',
-                        ai_firefly_seed: seed,
+                        ai_firefly_seed: newseed,
                         ai_firefly_prompt: input,
                     });
                     DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
@@ -306,7 +306,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     case DocumentType.IMG:
                         if (this._regenInput) {
                             // if (this._selectedDoc) {
-                            const newPrompt = `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}`;
+                            const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput;
                             return this.createImageWithFirefly(newPrompt, NumCast(doc?.ai_firefly_seed), changeInPlace);
                             // }
                         }
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 5a880901b..c9d5df547 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -274,14 +274,20 @@ export default class UploadManager extends ApiManager {
                         .filter(f => regex.test(f))
                         .map(f => fs.unlinkSync(serverPath + f));
                 }
-                imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
-                    const ext = path.extname(savedName).toLowerCase();
-                    const outputPath = serverPathToFile(Directory.images, filename + ext);
-                    if (AcceptableMedia.imageFormats.includes(ext)) {
-                        workerResample(savedName, outputPath, origSuffix, false);
-                    }
-                    res.send(clientPathToFile(Directory.images, filename + ext));
-                });
+                imageDataUri
+                    .outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix)))
+                    .then((savedName: string) => {
+                        const ext = path.extname(savedName).toLowerCase();
+                        const outputPath = serverPathToFile(Directory.images, filename + ext);
+                        if (AcceptableMedia.imageFormats.includes(ext)) {
+                            workerResample(savedName, outputPath, origSuffix, false);
+                        }
+                        res.send(clientPathToFile(Directory.images, filename + ext));
+                    })
+                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+                    .catch((e: any) => {
+                        res.status(404).json({ error: e.toString() });
+                    });
             },
         });
     }
-- 
cgit v1.2.3-70-g09d2


From 156a2040cc51397c3656615f9133279dac900c82 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 17 Jan 2025 11:17:15 -0500
Subject: changed to generating 4 images for each firefly request.

---
 src/client/views/nodes/DocumentView.tsx           |  4 ++--
 src/client/views/smartdraw/DrawingFillHandler.tsx | 28 ++++++++++++-----------
 src/server/ApiManagers/FireflyManager.ts          | 22 ++++++++++--------
 3 files changed, 29 insertions(+), 25 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e37658ca5..bc3f27124 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1110,10 +1110,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
      * @param doc Doc to snapshot
      * @returns promise of icon ImageField
      */
-    public static GetDocImage(doc: Doc) {
+    public static GetDocImage(doc?: Doc) {
         return DocumentView.getDocumentView(doc)
             ?.ComponentView?.updateIcon?.()
-            .then(() => ImageCast(doc?.icon, ImageCast(doc[Doc.LayoutFieldKey(doc)])));
+            .then(() => ImageCast(doc!.icon, ImageCast(doc![Doc.LayoutFieldKey(doc!)])));
     }
 
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 16ac2b79e..d0a840883 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,7 +1,7 @@
 import { imageUrlToBase64 } from '../../../ClientUtils';
 import { Doc, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
-import { DocCast, ImageCast, NumCast } from '../../../fields/Types';
+import { DocCast, ImageCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { Upload } from '../../../server/SharedMediaTypes';
 import { gptDescribeImage } from '../../apis/gpt/GPT';
@@ -48,20 +48,22 @@ export class DrawingFillHandler {
                     .then(gptDescribeImage)
                     .then((prompt, newPrompt = user_prompt || prompt) =>
                         Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength, presets: styles, styleUrl })
-                            .then((info: Upload.ImageInformation) => {
+                            .then((infos: Upload.ImageInformation[]) => {
                                 const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { _width: 400, _height: 400 });
                                 drawing[DocData].ai_firefly_generatedDocs = genratedDocs;
-                                Doc.AddDocToList(
-                                    genratedDocs,
-                                    undefined,
-                                    Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
-                                        ai: 'firefly',
-                                        title: newPrompt,
-                                        ai_firefly_prompt: newPrompt,
-                                        _width: 500,
-                                        data_nativeWidth: info.nativeWidth,
-                                        data_nativeHeight: info.nativeHeight,
-                                    })
+                                infos.map(info =>
+                                    Doc.AddDocToList(
+                                        genratedDocs,
+                                        undefined,
+                                        Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
+                                            ai: 'firefly',
+                                            title: newPrompt,
+                                            ai_firefly_prompt: newPrompt,
+                                            _width: 500,
+                                            data_nativeWidth: info.nativeWidth,
+                                            data_nativeHeight: info.nativeHeight,
+                                        })
+                                    )
                                 );
                                 if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight);
                             })
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index f49cf4132..160a94d40 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -36,8 +36,9 @@ export default class FireflyManager extends ApiManager {
                     ],
                     body: JSON.stringify({
                         prompt,
-                        // detailLevel: 'preview', 
-                        // modelVersion: 'image3_fast',
+                        numVariations: 4,
+                        detailLevel: 'preview', 
+                        modelVersion: 'image3_fast',
                         size: { width, height },
                         structure: !structureUrl
                             ? undefined
@@ -61,7 +62,7 @@ export default class FireflyManager extends ApiManager {
                 .then(response2 => response2.json().then(json => 
                 {
                     if (json.outputs?.length)
-                        return JSON.stringify((json.outputs?.[0] as { image: { url: string } } ?? {})?.image);
+                        return (json.outputs as {image: {url:string }}[]).map(output => output.image);
                     throw new Error(JSON.stringify(json));
                 })
             )
@@ -306,13 +307,14 @@ export default class FireflyManager extends ApiManager {
                                 })
                                 .then(uploads =>
                                     this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, uploads.styleUrl)
-                                        .then(fire =>
-                                            DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
-                                                if (info instanceof Error) _invalid(res, info.message);
-                                                else _success(res, info);
-                                                resolver();
-                                            })
-                                        )
+                                        .then(images => {
+                                            Promise.all((images ?? [new Error('no images were generated')]).map(fire => (fire instanceof Error ? fire : DashUploadUtils.UploadImage(fire.url))))
+                                                .then(dashImages => {
+                                                    if (dashImages.every(img => img instanceof Error)) _invalid(res, dashImages[0]!.message);
+                                                    else _success(res, JSON.stringify(dashImages.filter(img => !(img instanceof Error))));
+                                                })
+                                                .then(resolver);
+                                        })
                                         .catch(e => {
                                             _invalid(res, e.message);
                                             resolver();
-- 
cgit v1.2.3-70-g09d2


From e4fca20fd4f7d7401fb62d5f700a15c604070484 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 19 Jan 2025 14:35:23 -0500
Subject: css fixes for ai editor views.

---
 src/client/views/TagsView.scss                     |  20 ++-
 src/client/views/TagsView.tsx                      |  18 +--
 .../collectionFreeForm/CollectionFreeFormView.scss |  27 +++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  78 +++++----
 src/client/views/nodes/DocumentView.scss           |   7 +
 src/client/views/nodes/DocumentView.tsx            |  76 ++++++---
 src/client/views/nodes/ImageBox.scss               |  42 ++++-
 src/client/views/nodes/ImageBox.tsx                | 180 ++++++++++-----------
 8 files changed, 264 insertions(+), 184 deletions(-)

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

diff --git a/src/client/views/TagsView.scss b/src/client/views/TagsView.scss
index b741e7bfd..303a08e1e 100644
--- a/src/client/views/TagsView.scss
+++ b/src/client/views/TagsView.scss
@@ -4,13 +4,19 @@
     flex-direction: column;
     border: 1px solid;
     border-radius: 4px;
-}
-
-.tagsView-list {
-    display: flex;
-    flex-wrap: wrap;
-    .iconButton-container {
-        min-height: unset !important;
+    width: 100%;
+    position: relative;
+    .tagsView-content {
+        width: 100%;
+        height: inherit;
+        .tagsView-list {
+            display: flex;
+            flex-wrap: wrap;
+            height: inherit;
+            .iconButton-container {
+                min-height: unset !important;
+            }
+        }
     }
 }
 
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index 0edd89204..73f404596 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -295,15 +295,6 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
         return this._props.Views.lastElement();
     }
 
-    // x: 1 => 1/vs   0 => 1      1/(vs - (1-x)*(vs-1))
-    @computed get currentScale() {
-        if (this._props.Views.length > 1) return 1;
-        const x = NumCast(this.View.Document.height) / this.View.screenToContentsTransform().Scale / 80;
-        const xscale = x >= 1 ? 0 : 1 / (1 + x * (this.View.screenToLocalScale() - 1)); //docheight / this.View.screenToContentsTransform().Scale / 35  /  this.View.screenToLocalScale() - ;
-        const y = NumCast(this.View.Document.width) / this.View.screenToContentsTransform().Scale / 200;
-        const yscale = y >= 1 ? 0 : 1 / (1 + y * (this.View.screenToLocalScale() - 1)); //docheight / this.View.screenToContentsTransform().Scale / 35  /  this.View.screenToLocalScale() - ;
-        return Math.max(xscale, yscale, 1 / this.View.screenToLocalScale());
-    }
     @computed get isEditing() {
         return this._isEditing && (this._props.Views.length > 1 || (DocumentView.Selected().length === 1 && DocumentView.Selected().includes(this.View)));
     }
@@ -359,16 +350,11 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
                 ref={r => r && new ResizeObserver(action(() => this._props.Views.length === 1 && (this.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)}
                 style={{
                     display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined,
-                    transformOrigin: 'top left',
-                    maxWidth: `${100 * this.currentScale}%`,
-                    width: `${100 * this.currentScale}%`,
-                    transform: `scale(${1 / this.currentScale})`,
                     backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
                     borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
-                    position: 'relative',
-                    top: this._props.Views.lastElement()._showAIEditor ? undefined : this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
+                    height: !this._props.Views.lastElement()?.isSelected() ? 0 : undefined,
                 }}>
-                <div className="tagsView-content" style={{ width: '100%' }}>
+                <div className="tagsView-content">
                     <div className="tagsView-list">
                         {this._props.Views.length === 1 && !this.View.showTags ? null : ( //
                             <IconButton
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 6dabcb8b2..46bd37f6d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -312,14 +312,28 @@
 
     .collectionfreeformview-aiView-prompt {
         height: 25px;
-        width: 100%;
+        width: 65%;
     }
 
     .collectionFreeFormView-aiView-strength {
         text-align: center;
         align-items: center;
         display: flex;
-        width: 150px;
+        width: 25%;
+        .collectionFreeFormView-aiView-similarity {
+            max-width: 65px;
+            width: 100%;
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+    }
+
+    .collectionFreeForView-aiView-send {
+        width: 10%;
+        .button-container {
+            width: 100% !important;
+            justify-content: left !important;
+        }
     }
 
     .collectionFreeformView-aiView-options-container,
@@ -341,5 +355,14 @@
         align-items: center;
         width: 100%;
         gap: 10px;
+        .collectionFreeformView-aiView-input {
+            width: 100%;
+        }
+        .collectionFreeFormView-aiView-regenBtn {
+            width: 10%;
+            .button-container {
+                width: 100% !important;
+            }
+        }
     }
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index eeb97bf15..796949378 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -2209,10 +2209,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             placeholder={this._drawingFillInput || StrCast(this.Document.title) || 'Describe image'}
                             type="text"
                             value={this._drawingFillInput}
-                            onChange={action(e => {this._drawingFillInput = e.target.value})}
+                            onChange={action(e => {
+                                this._drawingFillInput = e.target.value;
+                            })}
                         />
-                        <div className="collectionfreeformview-aiView-strength">
-                            Similarity
+                        <div className="collectionFreeFormView-aiView-strength">
+                            <span className="collectionFreeFormView-aiView-similarity">Similarity</span>
                             <Slider
                                 className="collectionfreeformview-aiView-slider"
                                 sx={{
@@ -2229,23 +2231,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                 valueLabelDisplay="auto"
                             />
                         </div>
-                        <Button
-                            text="Send"
-                            type={Type.SEC}
-                            icon={this._drawingFillLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                            iconPlacement="right"
-                            onClick={undoable(
-                                action(() => {
-                                    this._drawingFillLoading = true;
-                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then(
-                                        action(() => {
-                                            this._drawingFillLoading = false;
-                                        })
-                                    );
-                                }),
-                                'create image'
-                            )}
-                        />
+                        <div className="collectionFreeFormView-aiView-send">
+                            <Button
+                                text="Send"
+                                type={Type.SEC}
+                                icon={this._drawingFillLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                onClick={undoable(
+                                    action(() => {
+                                        this._drawingFillLoading = true;
+                                        DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then(
+                                            action(() => {
+                                                this._drawingFillLoading = false;
+                                            })
+                                        );
+                                    }),
+                                    'create image'
+                                )}
+                            />
+                        </div>
                     </div>
                 </div>
                 <div className="collectionfreeformview-aiView-regenerate-container">
@@ -2256,23 +2260,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             aria-label="Edit instructions input"
                             type="text"
                             value={this._regenInput}
-                            onChange={action(e => {this._regenInput = e.target.value})}
+                            onChange={action(e => {
+                                this._regenInput = e.target.value;
+                            })}
                             placeholder="..under development.."
                         />
-                        <Button
-                            text="Regenerate"
-                            type={Type.SEC}
-                            icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                            iconPlacement="right"
-                            // onClick={action(async () => {
-                            //     this._regenLoading = true;
-                            //     SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
-                            //     SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
-                            //     SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
-                            //     await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
-                            //     this._regenLoading = false;
-                            // })}
-                        />
+                        <div className="collectionFreeFormView-aiView-regenBtn">
+                            <Button
+                                text="Regenerate"
+                                type={Type.SEC}
+                                icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                // onClick={action(async () => {
+                                //     this._regenLoading = true;
+                                //     SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+                                //     SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+                                //     SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+                                //     await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
+                                //     this._regenLoading = false;
+                                // })}
+                            />
+                        </div>
                     </div>
                 </div>
             </div>
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index cd813cb67..7e5507586 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -271,8 +271,14 @@
     }
 }
 
+.documentView-noAIWidgets {
+    transform-origin: top left;
+    position: relative;
+}
+
 .documentView-editorView-history {
     position: absolute;
+    transform-origin: top right;
     right: 0;
     top: 0;
     overflow-y: scroll;
@@ -284,6 +290,7 @@
     scrollbar-width: thin;
     justify-items: center;
     background-color: rgb(223, 223, 223);
+    transform-origin: top left;
 
     .documentView-editorView-resizer {
         height: 5px;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index bc3f27124..83c948f0b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -551,6 +551,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'map-pin' });
+            appearanceItems.push({ description: 'AI view', event: () => this._docView?.toggleAIEditor(), icon: 'map-pin' });
 
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
@@ -687,7 +688,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         this._props
             .ScreenToLocalTransform()
             .translate(0, -this.headerMargin)
-            .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.rph() : 1);
+            .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.aiContentsHeight() : 1);
     onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr;
     setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore
     setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore
@@ -713,9 +714,32 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    @observable _aiWinHeight = 95;
-    rpw = () => (this.rph() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
-    rph = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight);
+    @observable _aiWinHeight = 88;
+
+    private _sideBtnWidth = 22;
+    @computed get currentScale() {
+        const viewXfScale = this._props.DocumentView!().screenToLocalScale();
+        const x = NumCast(this.Document.height) / viewXfScale / 80;
+        const xscale = x >= 1 ? 0 : 1 / (1 + x * (viewXfScale - 1));
+        const y = NumCast(this.Document.width) / viewXfScale / 200;
+        const yscale = y >= 1 ? 0 : 1 / (1 + y * viewXfScale - 1);
+        return Math.max(xscale, yscale, 1 / viewXfScale);
+    }
+    /**
+     * How much the content of the view is being scaled based on its nesting and its fit-to-width settings
+     */
+    @computed get viewScaling() { return 1 / this.currentScale; } // prettier-ignore
+    /**
+     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+     */
+    @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.viewScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height))); } // prettier-ignore
+    /**
+     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+     */
+    @computed get uiBtnScaling() {  return Math.max(this.maxWidgetSize / this._sideBtnWidth, 1) * Math.min(1, this.viewScaling); } // prettier-ignore
+
+    aiContentsWidth = () => (this.aiContentsHeight() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
+    aiContentsHeight = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight * this.uiBtnScaling);
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -726,8 +750,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     className="documentView-contentsView"
                     style={{
                         pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
-                        width: this._props.showAIEditor ? this.rpw() : undefined,
-                        height: this._props.showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                        width: this._props.showAIEditor ? this.aiContentsWidth() : undefined,
+                        height: this._props.showAIEditor ? this.aiContentsHeight() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
                     }}>
                     <DocumentContentsView
                         {...this._props}
@@ -735,8 +759,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         pointerEvents={this.contentPointerEvents}
                         setContentViewBox={this.setContentView}
                         childFilters={this.childFilters}
-                        PanelWidth={this._props.showAIEditor ? this.rpw : this._props.PanelWidth}
-                        PanelHeight={this._props.showAIEditor ? this.rph : this.panelHeight}
+                        PanelWidth={this._props.showAIEditor ? this.aiContentsWidth : this._props.PanelWidth}
+                        PanelHeight={this._props.showAIEditor ? this.aiContentsHeight : this.panelHeight}
                         setHeight={this.setHeight}
                         isContentActive={this.isContentActive}
                         ScreenToLocalTransform={this.screenToLocalContent}
@@ -746,23 +770,38 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                     />
                 </div>
-                {!this._props.showAIEditor ? null : (
+                {!this._props.showAIEditor ? (
+                    <div
+                        className="documentView-noAiWidgets"
+                        style={{
+                            width: `${100 / this.uiBtnScaling}%`, //
+                            transform: `scale(${this.uiBtnScaling})`,
+                            bottom: this.maxWidgetSize,
+                        }}>
+                        {this.widgetDecorations ?? null}
+                    </div>
+                ) : (
                     <>
                         <div
                             className="documentView-editorView-history"
-                            ref={r => {
-                                this.historyRef(this._oldAiWheel, (this._oldAiWheel = r));
-                            }}
-                            style={{ height: this.rph(), width: (this._props.PanelWidth() - this.rpw()) * 0.95 }}>
+                            ref={r => this.historyRef(this._oldAiWheel, (this._oldAiWheel = r))}
+                            style={{
+                                transform: `scale(${this.uiBtnScaling})`,
+                                height: this.aiContentsHeight() / this.uiBtnScaling,
+                                width: ((this._props.PanelWidth() - this.aiContentsWidth()) * 0.95) / this.uiBtnScaling,
+                            }}>
                             {this._componentView?.componentAIViewHistory?.() ?? null}
                         </div>
                         <div
                             className="documentView-editorView"
-                            ref={r => {
-                                this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r));
-                            }}>
+                            style={{
+                                width: `${100 / this.uiBtnScaling}%`, //
+                                transform: `scale(${this.uiBtnScaling})`,
+                            }}
+                            ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}>
                             <div className="documentView-editorView-resizer" />
                             {this._componentView?.componentAIView?.() ?? null}
+                            {this.widgetDecorations ?? null}
                         </div>
                     </>
                 )}
@@ -934,7 +973,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                               {this.captionView}
                           </div>
                       )}
-                      {this.widgetDecorations ?? null}
                   </div>
               ));
     };
@@ -1232,7 +1270,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     }
 
     @computed get layout_fitWidth() {
-        return this._showAIEditor ? false : (this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth);
+        return this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth;
     }
     @computed get anchorViewDoc() {
         return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document.link_anchor_2) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document.link_anchor_1) : undefined;
@@ -1430,7 +1468,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     public docViewPath = () => (this.containerViewPath ? [...this.containerViewPath(), this] : [this]);
 
     layout_fitWidthFunc = (/* doc: Doc */) => BoolCast(this.layout_fitWidth);
-    screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale;
+    screenToLocalScale = () => this.screenToViewTransform().Scale;
     isSelected = () => this.IsSelected;
     select = (extendSelection: boolean, focusSelection?: boolean) => {
         if (!this._props.dontSelect?.()) DocumentView.SelectView(this, extendSelection);
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 29b24b774..fe4f0b1a2 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -164,16 +164,15 @@
 .imageBox-aiView {
     text-align: center;
     font-weight: bold;
+    transform-origin: top left;
+    width: 100%;
 
     .imageBox-aiView-subtitle {
         position: relative;
         align-content: center;
-    }
-
-    .imageBox-aiView-regenerate-container,
-    .imageBox-aiView-options-container {
-        font-weight: normal;
-        display: flex;
+        max-width: 10%;
+        overflow: hidden;
+        text-overflow: ellipsis;
     }
 
     .imageBox-aiView-regenerate,
@@ -183,19 +182,46 @@
         flex-direction: row;
         gap: 5px;
         width: 100%;
+        .imageBox-aiView-regenerate-createBtn {
+            max-width: 10%;
+            .button-container {
+                width: 100% !important;
+                justify-content: left !important;
+            }
+        }
+    }
+
+    .imageBox-aiView-firefly {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        max-width: 10%;
+        width: 100%;
+    }
+    .imageBox-aiView-regenerate-send {
+        max-width: 10%;
     }
 
     .imageBox-aiView-strength {
         text-align: center;
         align-items: center;
         display: flex;
-        width: 125px;
+        max-width: 25%;
+        width: 100%;
+        .imageBox-aiView-similarity {
+            max-width: 65;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            width: 100%;
+        }
     }
     .imageBox-aiView-slider {
-        width: 50px;
+        width: 90%;
         margin-left: 5px;
     }
     .imageBox-aiView-input {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        max-width: 65%;
         width: 100%;
     }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 82d8ea5f1..caefbf542 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -541,10 +541,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     protected _sideBtnMaxPanelPct = 0.12;
     @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
     @observable private _fireflyRefStrength = 0;
-    @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore
-    @computed get maxWidgetSize() { return Math.min(this._btnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width) ? 1 : this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
-    @computed get uiBtnScaling() { return this.maxWidgetSize / this._btnWidth; } // prettier-ignore
-    @computed get uiInputScaling() { return this.maxWidgetSize / this._inputWidth; } // prettier-ignore
 
     componentAIViewHistory = () => (
         <div className="imageBox-aiView-history">
@@ -570,35 +566,35 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
         return (
             <div className="imageBox-aiView">
-                <div className="imageBox-aiView-regenerate-container">
-                    <div className="imageBox-aiView-regenerate">
-                        Firefly:
-                        <input
-                            className="imageBox-aiView-input"
-                            aria-label="Edit instructions input"
-                            type="text"
-                            value={this._regenInput}
-                            onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                            placeholder={this._regenInput || StrCast(this.Document.title)}
+                <div className="imageBox-aiView-regenerate">
+                    <span className="imageBox-aiView-firefly">Firefly:</span>
+                    <input
+                        className="imageBox-aiView-input"
+                        aria-label="Edit instructions input"
+                        type="text"
+                        value={this._regenInput}
+                        onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                        placeholder={this._regenInput || StrCast(this.Document.title)}
+                    />
+                    <div className="imageBox-aiView-strength">
+                        <span className="imageBox-aiView-similarity">Similarity</span>
+                        <Slider
+                            className="imageBox-aiView-slider"
+                            sx={{
+                                '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                                '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
+                                '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                            }}
+                            min={0}
+                            max={100}
+                            step={1}
+                            size="small"
+                            value={this._fireflyRefStrength}
+                            onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
+                            valueLabelDisplay="auto"
                         />
-                        <div className="imageBox-aiView-strength">
-                            <span style={{ width: 60 }}>Similarity</span>
-                            <Slider
-                                className="imageBox-aiView-slider"
-                                sx={{
-                                    '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
-                                    '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
-                                    '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
-                                }}
-                                min={0}
-                                max={100}
-                                step={1}
-                                size="small"
-                                value={this._fireflyRefStrength}
-                                onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
-                                valueLabelDisplay="auto"
-                            />
-                        </div>
+                    </div>
+                    <div className="imageBox-aiView-regenerate-createBtn">
                         <Button
                             text="Create"
                             type={Type.SEC}
@@ -634,71 +630,61 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         />
                     </div>
                 </div>
-                <div className="imageBox-aiView-options-container">
-                    <span className="imageBox-aiView-subtitle"> More Options: </span>
-                    <div className="imageBox-aiView-options">
-                        {SmartDrawHandler.Instance.ShowRegenerate && (
-                            <text className="imageBox-aiView-subtitle" style={{ transform: `scale(${this.uiBtnScaling})` }}>
-                                More Options:
-                            </text>
-                        )}
-                        <Button
-                            style={{ transform: `scale(${this.uiBtnScaling})` }}
-                            type={Type.TERT}
-                            text="Get Text"
-                            icon={<FontAwesomeIcon icon="font" />}
-                            color={SettingsManager.userBackgroundColor}
-                            iconPlacement="right"
-                            onClick={() => {
-                                Networking.PostToServer('/queryFireflyImageText', {
-                                    file: (file => {
-                                        const ext = extname(file);
-                                        return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
-                                    })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
-                                }).then(text => alert(text));
-                            }}
-                        />
-                        <Button
-                            style={{ transform: `scale(${this.uiBtnScaling})` }}
-                            type={Type.TERT}
-                            text="Generative Fill"
-                            icon={<FontAwesomeIcon icon="fill" />}
-                            color={SettingsManager.userBackgroundColor}
-                            iconPlacement="right"
-                            onClick={action(() => {
-                                ImageEditorData.Open = true;
-                                ImageEditorData.Source = (field && this.choosePath(field.url)) || '';
-                                ImageEditorData.AddDoc = this._props.addDocument;
-                                ImageEditorData.RootDoc = this.Document;
-                            })}
-                        />
-                        <Button
-                            style={{ transform: `scale(${this.uiBtnScaling})` }}
-                            type={Type.TERT}
-                            text="Expand"
-                            icon={<FontAwesomeIcon icon="expand" />}
-                            color={SettingsManager.userBackgroundColor}
-                            iconPlacement="right"
-                            onClick={() => {
-                                Networking.PostToServer('/expandImage', {
-                                    prompt: 'sunny skies',
-                                    file: (file => {
-                                        const ext = extname(file);
-                                        return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
-                                    })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
-                                }).then((info: Upload.ImageInformation) => {
-                                    const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
-                                    DocUtils.assignImageInfo(info, img);
-                                    const genratedDocs = this.Document.generatedDocs
-                                        ? DocCast(this.Document.generatedDocs)
-                                        : Docs.Create.MasonryDocument([], { _width: 400, _height: 400, x: NumCast(this.Document.x) + NumCast(this.Document.width), y: NumCast(this.Document.y) });
-                                    Doc.AddDocToList(genratedDocs, undefined, img);
-                                    this.Document[DocData].generatedDocs = genratedDocs;
-                                    if (!DocumentView.getFirstDocumentView(genratedDocs)) this._props.addDocTab(genratedDocs, OpenWhere.addRight);
-                                });
-                            }}
-                        />
-                    </div>
+                <div className="imageBox-aiView-options">
+                    <span className="imageBox-aiView-subtitle"> More: </span>
+                    <Button
+                        type={Type.TERT}
+                        text="Get Text"
+                        icon={<FontAwesomeIcon icon="font" />}
+                        color={SettingsManager.userBackgroundColor}
+                        iconPlacement="right"
+                        onClick={() => {
+                            Networking.PostToServer('/queryFireflyImageText', {
+                                file: (file => {
+                                    const ext = extname(file);
+                                    return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+                                })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+                            }).then(text => alert(text));
+                        }}
+                    />
+                    <Button
+                        type={Type.TERT}
+                        text="Generative Fill"
+                        icon={<FontAwesomeIcon icon="fill" />}
+                        color={SettingsManager.userBackgroundColor}
+                        iconPlacement="right"
+                        onClick={action(() => {
+                            ImageEditorData.Open = true;
+                            ImageEditorData.Source = (field && this.choosePath(field.url)) || '';
+                            ImageEditorData.AddDoc = this._props.addDocument;
+                            ImageEditorData.RootDoc = this.Document;
+                        })}
+                    />
+                    <Button
+                        type={Type.TERT}
+                        text="Expand"
+                        icon={<FontAwesomeIcon icon="expand" />}
+                        color={SettingsManager.userBackgroundColor}
+                        iconPlacement="right"
+                        onClick={() => {
+                            Networking.PostToServer('/expandImage', {
+                                prompt: 'sunny skies',
+                                file: (file => {
+                                    const ext = extname(file);
+                                    return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+                                })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+                            }).then((info: Upload.ImageInformation) => {
+                                const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
+                                DocUtils.assignImageInfo(info, img);
+                                const genratedDocs = this.Document.generatedDocs
+                                    ? DocCast(this.Document.generatedDocs)
+                                    : Docs.Create.MasonryDocument([], { _width: 400, _height: 400, x: NumCast(this.Document.x) + NumCast(this.Document.width), y: NumCast(this.Document.y) });
+                                Doc.AddDocToList(genratedDocs, undefined, img);
+                                this.Document[DocData].generatedDocs = genratedDocs;
+                                if (!DocumentView.getFirstDocumentView(genratedDocs)) this._props.addDocTab(genratedDocs, OpenWhere.addRight);
+                            });
+                        }}
+                    />
                 </div>
             </div>
         );
-- 
cgit v1.2.3-70-g09d2


From 000773de9807f3932d9f0a223779f38767a982fc Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 19 Jan 2025 14:36:03 -0500
Subject: from last

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

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 83c948f0b..f1b48be4a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -716,7 +716,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     @observable _aiWinHeight = 88;
 
-    private _sideBtnWidth = 22;
+    private _tagsBtnHeight = 22;
     @computed get currentScale() {
         const viewXfScale = this._props.DocumentView!().screenToLocalScale();
         const x = NumCast(this.Document.height) / viewXfScale / 80;
@@ -732,11 +732,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     /**
      * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
      */
-    @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.viewScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height))); } // prettier-ignore
+    @computed get maxWidgetSize() { return Math.min(this._tagsBtnHeight * this.viewScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height))); } // prettier-ignore
     /**
      * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
      */
-    @computed get uiBtnScaling() {  return Math.max(this.maxWidgetSize / this._sideBtnWidth, 1) * Math.min(1, this.viewScaling); } // prettier-ignore
+    @computed get uiBtnScaling() {  return Math.max(this.maxWidgetSize / this._tagsBtnHeight, 1) * Math.min(1, this.viewScaling); } // prettier-ignore
 
     aiContentsWidth = () => (this.aiContentsHeight() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
     aiContentsHeight = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight * this.uiBtnScaling);
-- 
cgit v1.2.3-70-g09d2


From 406706ab42bc1fdac81b3bffdcadfcfe05cfc8a0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 27 Jan 2025 12:06:50 -0500
Subject: fixed placement of widget decorations

---
 src/client/views/StyleProvider.tsx      | 9 +++------
 src/client/views/nodes/DocumentView.tsx | 6 ++++--
 2 files changed, 7 insertions(+), 8 deletions(-)

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

diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index e825a27d3..bebc9a341 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -1,7 +1,7 @@
+import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from '@dash/components';
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from '@dash/components';
 import { action, untracked } from 'mobx';
 import { extname } from 'path';
 import * as React from 'react';
@@ -10,6 +10,7 @@ import { FaFilter } from 'react-icons/fa';
 import { ClientUtils, DashColor, lightOrDark } from '../../ClientUtils';
 import { Doc, Opt, StrListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
+import { InkInkTool } from '../../fields/InkField';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
 import { AudioAnnoState } from '../../server/SharedMediaTypes';
@@ -21,11 +22,9 @@ import { TreeSort } from './collections/TreeSort';
 import { Colors } from './global/globalEnums';
 import { DocumentView, DocumentViewProps } from './nodes/DocumentView';
 import { FieldViewProps } from './nodes/FieldView';
-import { styleProviderQuiz } from './StyleProviderQuiz';
 import { StyleProp } from './StyleProp';
 import './StyleProvider.scss';
-import { TagsView } from './TagsView';
-import { InkInkTool } from '../../fields/InkField';
+import { styleProviderQuiz } from './StyleProviderQuiz';
 
 function toggleLockedPosition(doc: Doc) {
     UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -394,7 +393,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                     </Tooltip>
                 );
             };
-            const tags = () => docView?.() ? <TagsView Views={[docView?.()]}/> :  null;
         
             return (
                 <>
@@ -402,7 +400,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                     {lock()}
                     {filter()}
                     {audio()}
-                    {tags()}
                 </>
             );
         }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index f1b48be4a..5fd9223e3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -52,6 +52,7 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails/PresEnums';
 import SpringAnimation from './trails/SlideEffect';
 import { SpringType, springMappings } from './trails/SpringUtils';
+import { TagsView } from '../TagsView';
 
 export interface DocumentViewProps extends FieldViewSharedProps {
     hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
@@ -778,7 +779,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                             transform: `scale(${this.uiBtnScaling})`,
                             bottom: this.maxWidgetSize,
                         }}>
-                        {this.widgetDecorations ?? null}
+                        {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null}
                     </div>
                 ) : (
                     <>
@@ -801,10 +802,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                             ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}>
                             <div className="documentView-editorView-resizer" />
                             {this._componentView?.componentAIView?.() ?? null}
-                            {this.widgetDecorations ?? null}
+                            {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null}
                         </div>
                     </>
                 )}
+                {this.widgetDecorations ?? null}
             </>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From f66eaad00dbfe27e8dbe28019d1148a70f6ba0d9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 3 Feb 2025 12:44:38 -0500
Subject: fixed defaults for roll animations effect.  fixed hideBefore pres
 effect to not show Doc before effect starts

---
 src/client/util/DocumentManager.ts         | 29 +++++++++-----------
 src/client/views/nodes/DocumentView.tsx    | 12 ++++-----
 src/client/views/nodes/FocusViewOptions.ts | 11 ++++++++
 src/client/views/nodes/trails/PresBox.tsx  | 43 ++++++++++++++----------------
 4 files changed, 50 insertions(+), 45 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 286112bf9..acb35f7eb 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -8,7 +8,7 @@ import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { AudioField } from '../../fields/URLField';
 import { CollectionViewType } from '../documents/DocumentTypes';
 import { DocumentView, DocumentViewInternal } from '../views/nodes/DocumentView';
-import { FocusViewOptions } from '../views/nodes/FocusViewOptions';
+import { FocusEffectDelay, FocusViewOptions } from '../views/nodes/FocusViewOptions';
 import { OpenWhere } from '../views/nodes/OpenWhere';
 import { PresBox } from '../views/nodes/trails';
 
@@ -352,21 +352,18 @@ export class DocumentManager {
             // if there's an options.effect, it will be handled from linkFollowHighlight.  We delay the start of
             // the highlight so that the target document can be somewhat centered so that the effect/highlight will be seen
             // bcz: should this delay be an options parameter?
-            setTimeout(
-                () => {
-                    Doc.linkFollowHighlight(viewSpec ? [docView.Document, viewSpec] : docView.Document, undefined, options.effect);
-                    if (options.zoomTextSelections && Doc.IsUnhighlightTimerSet() && contextView && targetDoc.text_html) {
-                        // if the docView is a text anchor, the contextView is the PDF/Web/Text doc
-                        contextView.setTextHtmlOverlay(StrCast(targetDoc.text_html), options.effect);
-                        DocumentManager._overlayViews.add(contextView);
-                    }
-                    Doc.AddUnHighlightWatcher(() => {
-                        docView.Document[Animation] = undefined;
-                        DocumentManager.removeOverlayViews();
-                    });
-                },
-                (options.zoomTime ?? 0) * 0.5
-            );
+            setTimeout(() => {
+                Doc.linkFollowHighlight(viewSpec ? [docView.Document, viewSpec] : docView.Document, undefined, options.effect);
+                if (options.zoomTextSelections && Doc.IsUnhighlightTimerSet() && contextView && targetDoc.text_html) {
+                    // if the docView is a text anchor, the contextView is the PDF/Web/Text doc
+                    contextView.setTextHtmlOverlay(StrCast(targetDoc.text_html), options.effect);
+                    DocumentManager._overlayViews.add(contextView);
+                }
+                Doc.AddUnHighlightWatcher(() => {
+                    docView.Document[Animation] = undefined;
+                    DocumentManager.removeOverlayViews();
+                });
+            }, FocusEffectDelay(options));
             if (options.playMedia) docView.ComponentView?.playFrom?.(NumCast(docView.Document._layout_currentTimecode));
             if (options.playAudio) DocumentManager.playAudioAnno(docView.Document);
             if (options.toggleTarget && (!options.didMove || docView.Document.hidden)) docView.Document.hidden = !docView.Document.hidden;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5fd9223e3..a60415e43 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1043,13 +1043,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         >,
         root: Doc
     ) {
-        const dir = ((presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) || PresEffectDirection.Center) as PresEffectDirection;
+        const effectDirection = (presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) as PresEffectDirection;
         const duration = Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null));
         const effectProps = {
-            left: dir === PresEffectDirection.Left,
-            right: dir === PresEffectDirection.Right,
-            top: dir === PresEffectDirection.Top,
-            bottom: dir === PresEffectDirection.Bottom,
+            left: effectDirection === PresEffectDirection.Left,
+            right: effectDirection === PresEffectDirection.Right,
+            top: effectDirection === PresEffectDirection.Top,
+            bottom: effectDirection === PresEffectDirection.Bottom,
             opposite: true,
             delay: 0,
             duration,
@@ -1063,7 +1063,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const presEffect = StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect));
         switch (presEffect) {
             case PresEffect.Expand: case PresEffect.Flip: case PresEffect.Rotate: case PresEffect.Bounce:    
-            case PresEffect.Roll:       return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={presEffect}   springSettings={timingConfig}>{renderDoc}</SpringAnimation>
+            case PresEffect.Roll:       return <SpringAnimation doc={root} startOpacity={0} dir={effectDirection || PresEffectDirection.Left} presEffect={presEffect}   springSettings={timingConfig}>{renderDoc}</SpringAnimation>
             // case PresEffect.Fade:    return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Fade} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect>
             case PresEffect.Fade:       return <Fade {...effectProps}>{renderDoc}</Fade>
             // keep as preset, doesn't really make sense with spring config
diff --git a/src/client/views/nodes/FocusViewOptions.ts b/src/client/views/nodes/FocusViewOptions.ts
index bb0d2b03c..1c462e98f 100644
--- a/src/client/views/nodes/FocusViewOptions.ts
+++ b/src/client/views/nodes/FocusViewOptions.ts
@@ -22,3 +22,14 @@ export interface FocusViewOptions {
     pointFocus?: { X: number; Y: number }; // clientX and clientY coordinates to focus on instead of a document target (used by explore mode)
     contextPath?: Doc[]; // path of inner documents that will also be focused
 }
+
+/**
+ * if there's an options.effect, it will be handled from linkFollowHighlight.  We delay the start of
+ * the highlight so that the target document can be somewhat centered so that the effect/highlight will be seen
+ *  bcz: should this delay be an options parameter?
+ * @param options
+ * @returns
+ */
+export function FocusEffectDelay(options: FocusViewOptions) {
+    return (options.zoomTime ?? 0) * 0.5;
+}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index f23b32a48..7ca0bbc8f 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1,7 +1,8 @@
+import { Button, Dropdown, DropdownType, IconButton, Toggle, ToggleType, Type } from '@dash/components';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import Slider from '@mui/material/Slider';
-import { Button, Dropdown, DropdownType, IconButton, Toggle, ToggleType, Type } from '@dash/components';
+import _ from 'lodash';
 import { IReactionDisposer, ObservableSet, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -40,7 +41,7 @@ import { CollectionFreeFormPannableContents } from '../../collections/collection
 import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
-import { FocusViewOptions } from '../FocusViewOptions';
+import { FocusEffectDelay, FocusViewOptions } from '../FocusViewOptions';
 import { OpenWhere, OpenWhereMod } from '../OpenWhere';
 import { ScriptingBox } from '../ScriptingBox';
 import CubicBezierEditor, { EaseFuncToPoints, TIMING_DEFAULT_MAPPINGS } from './CubicBezierEditor';
@@ -48,7 +49,6 @@ import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
 import SlideEffect from './SlideEffect';
 import { AnimationSettings, SpringSettings, SpringType, easeItems, effectItems, effectTimings, movementItems, presEffectDefaultTimings, springMappings, springPreviewColors } from './SpringUtils';
-import _ from 'lodash';
 
 @observer
 export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -547,8 +547,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (!group) this.clearSelectedArray();
             this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); // Update selected array
             this.turnOffEdit();
-            this.navigateToActiveItem(finished); // Handles movement to element only when presentationTrail is list
-            this.doHideBeforeAfter(); // Handles hide after/before
+            this.navigateToActiveItem((options: FocusViewOptions) => {
+                setTimeout(this.doHideBeforeAfter, FocusEffectDelay(options)); // Handles hide after/before
+                finished?.();
+            }); // Handles movement to element only when presentationTrail is list
         }
     });
     static pinDataTypes(target?: Doc): dataTypes {
@@ -784,11 +786,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 });
             setTimeout(
                 () =>
-                    Array.from(transitioned).forEach(
-                        action(doc => {
-                            doc._dataTransition = undefined;
-                        })
-                    ),
+                    Array.from(transitioned).forEach(doc => {
+                        doc._dataTransition = undefined;
+                    }),
                 transTime + 10
             );
         }
@@ -826,16 +826,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * a new tab. If presCollection is undefined it will open the document
      * on the right.
      */
-    navigateToActiveItem = (afterNav?: () => void) => {
+    navigateToActiveItem = (afterNav?: (options: FocusViewOptions) => void) => {
         const { activeItem, targetDoc } = this;
-        const finished = () => {
-            afterNav?.();
+        const finished = (options: FocusViewOptions) => {
+            afterNav?.(options);
             targetDoc[Animation] = undefined;
         };
         const selViewCache = Array.from(this.selectedArray);
         const dragViewCache = Array.from(this._dragArray);
         const eleViewCache = Array.from(this._eleArray);
-        const resetSelection = action(() => {
+        const resetSelection = action((options: FocusViewOptions) => {
             if (!this._props.isSelected()) {
                 const presDocView = DocumentView.getDocumentView(this.Document);
                 if (presDocView) DocumentView.SelectView(presDocView, false);
@@ -844,14 +844,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 this._dragArray.splice(0, this._dragArray.length, ...dragViewCache);
                 this._eleArray.splice(0, this._eleArray.length, ...eleViewCache);
             }
-            finished();
+            finished(options);
         });
         PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection);
     };
 
     public static PanelName = 'PRESBOX';
 
-    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: () => void) {
+    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: (options: FocusViewOptions) => void) {
         if (activeItem.presentation_movement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
             (DocumentView.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
             return;
@@ -886,9 +886,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 if (!DocumentView.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) {
                     DocumentView.SetLightboxDoc(undefined);
                 }
-                DocumentView.showDocument(targetDoc, options, finished);
+                DocumentView.showDocument(targetDoc, options, () => finished?.(options));
             });
-        } else finished?.();
+        } else finished?.(options);
     }
 
     /**
@@ -900,8 +900,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.childDocs.forEach((doc, index) => {
             const curDoc = Cast(doc, Doc, null);
             const tagDoc = PresBox.targetRenderedDoc(curDoc);
-            const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, curDoc);
-            let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined;
+            const itemIndexes = this.getAllIndexes(this.tagDocs, curDoc);
+            let opacity = index === this.itemIndex ? 1 : undefined;
             if (curDoc.presentation_hide) {
                 if (index !== this.itemIndex) {
                     opacity = 1;
@@ -913,9 +913,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     opacity = 0;
                 } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) {
                     opacity = 1;
-                    setTimeout(() => {
-                        tagDoc._dataTransition = undefined;
-                    }, 1000);
                 }
             }
             const hidingIndAft =
-- 
cgit v1.2.3-70-g09d2


From deb038e6b659a1c7563ccc9df2ef51221c0492ea Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 3 Feb 2025 14:42:42 -0500
Subject: fixed pres animation of colection that's progresivized by removing
 dataTransition assignments that made no sense.

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  2 +-
 src/client/views/nodes/DocumentView.tsx            | 26 ++++++++------------
 src/client/views/nodes/trails/PresBox.tsx          | 28 ++++++++++------------
 4 files changed, 25 insertions(+), 33 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 69fdf52ff..3c31b584e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -232,7 +232,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) {
-        return DocumentView.SetViewTransition(docs, 'all', duration, timer, undefined, true);
+        return DocumentView.SetViewTransition(docs, 'all', duration, timer, true);
     }
     changeKeyFrame = (back = false) => {
         const currentFrame = Cast(this.Document._currentFrame, 'number', null);
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 68105f2f1..beea6ab3c 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -198,7 +198,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 
     public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number) {
-        const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, undefined, true);
+        const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, true);
         const timecode = Math.round(time);
         docs.forEach(doc => {
             this.animFields.forEach(val => {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a60415e43..0193fd328 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1390,8 +1390,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         this.Document[Animation] = presEffect;
         this._animEffectTimer = setTimeout(() => { this.Document[Animation] = undefined; }, timeInMs); // prettier-ignore
     };
-    public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => {
-        this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, afterTrans, dataTrans);
+    public setViewTransition = (transProp: string, timeInMs: number, dataTrans = false) => {
+        this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, dataTrans);
     };
 
     public setCustomView = undoable((custom: boolean, layout: string): void => {
@@ -1590,21 +1590,15 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         );
     }
 
-    public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, timer?: NodeJS.Timeout | undefined, afterTrans?: () => void, dataTrans = false) {
-        docs.forEach(doc => {
-            doc._viewTransition = `${transProp} ${timeInMs}ms`;
-            dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`);
-        });
+    public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, timer?: NodeJS.Timeout | undefined, dataTrans = false) {
+        const setTrans = (transition?: string) =>
+            docs.forEach(doc => {
+                doc._viewTransition = transition;
+                dataTrans && (doc.dataTransition = transition);
+            });
+        setTrans(`${transProp} ${timeInMs}ms`);
         timer && clearTimeout(timer);
-        return setTimeout(
-            () =>
-                docs.forEach(doc => {
-                    doc._viewTransition = undefined;
-                    dataTrans && (doc.dataTransition = 'inherit');
-                    afterTrans?.();
-                }),
-            timeInMs + 10
-        );
+        return setTimeout(setTrans, timeInMs + 10);
     }
 
     // shows a stacking view collection (by default, but the user can change) of all documents linked to the source
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 7ca0bbc8f..68634076d 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -452,27 +452,25 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const progressiveReveal = (first: boolean) => {
             const presIndexed = Cast(this.activeItem?.presentation_indexed, 'number', null);
             if (presIndexed !== undefined) {
-                const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem);
-                targetRenderedDoc._dataTransition = 'all 1s';
-                targetRenderedDoc.opacity = 1;
-                setTimeout(() => {
-                    targetRenderedDoc._dataTransition = 'inherit';
-                }, 1000);
                 const listItems = this.progressivizedItems(this.activeItem);
-                if (listItems && presIndexed < listItems.length) {
+                const listItemDoc = listItems?.[presIndexed];
+                if (listItems && listItemDoc) {
                     if (!first) {
-                        const listItemDoc = listItems[presIndexed];
-                        const targetView = listItems && DocumentView.getFirstDocumentView(listItemDoc);
+                        const presBulletTiming = 500; // bcz: hardwired for now
                         Doc.linkFollowUnhighlight();
                         Doc.HighlightDoc(listItemDoc);
                         listItemDoc.presentation_effect = this.activeItem.presBulletEffect;
-                        listItemDoc.presentation_transition = 500;
-                        targetView?.setAnimEffect(listItemDoc, 500);
-                        if (targetView && this.activeItem.presBulletExpand) {
-                            targetView.setAnimateScaling(1.2, 400);
-                            Doc.AddUnHighlightWatcher(() => targetView?.setAnimateScaling(0, undefined));
-                        }
+                        listItemDoc.presentation_transition = presBulletTiming;
                         listItemDoc.opacity = undefined;
+
+                        const targetView = DocumentView.getFirstDocumentView(listItemDoc);
+                        if (targetView) {
+                            targetView.setAnimEffect(listItemDoc, presBulletTiming);
+                            if (this.activeItem.presBulletExpand) {
+                                targetView.setAnimateScaling(1.2, presBulletTiming * 0.8);
+                                Doc.AddUnHighlightWatcher(() => targetView.setAnimateScaling(0, undefined));
+                            }
+                        }
                         this.activeItem.presentation_indexed = presIndexed + 1;
                     }
                     return true;
-- 
cgit v1.2.3-70-g09d2


From 1c7c5bcadc7081e1a7e582413eee37b8201e1c4c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 24 Feb 2025 18:07:36 -0500
Subject: added borders (width, color, and style) for DocumentViews.  fixed
 dropdown slider buttons to show text labels.

---
 .../components/NumberDropdown/NumberDropdown.scss  |  18 +-
 .../components/NumberDropdown/NumberDropdown.tsx   | 182 +++++++++------------
 packages/components/src/components/Popup/Popup.tsx |   8 +-
 packages/components/src/global/globalTypes.ts      | 137 +++++++---------
 src/client/documents/DocUtils.ts                   |   4 +
 src/client/documents/Documents.ts                  |   4 +-
 src/client/util/CurrentUserUtils.ts                |   6 +-
 src/client/views/StyleProp.ts                      |   1 +
 src/client/views/StyleProvider.tsx                 |  51 +++---
 src/client/views/global/globalScripts.ts           |  55 +++++++
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  21 +--
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |   1 +
 .../views/nodes/formattedText/FormattedTextBox.tsx |   2 +-
 14 files changed, 263 insertions(+), 229 deletions(-)

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

diff --git a/packages/components/src/components/NumberDropdown/NumberDropdown.scss b/packages/components/src/components/NumberDropdown/NumberDropdown.scss
index 0999afb98..4ed5855d9 100644
--- a/packages/components/src/components/NumberDropdown/NumberDropdown.scss
+++ b/packages/components/src/components/NumberDropdown/NumberDropdown.scss
@@ -1,5 +1,19 @@
 @import '../../global/globalCssVariables.scss';
 
 .numberDropdown-container {
-    
-}
\ No newline at end of file
+}
+
+.form-wrapper {
+    .iconButton-label {
+        position: relative;
+        z-index: 2;
+        width: 100%;
+        overflow: hidden;
+        white-space: normal;
+        display: flex;
+        text-align: center;
+        justify-content: center;
+        align-items: center;
+        font-size: $xsmall-fontSize !important;
+    }
+}
diff --git a/packages/components/src/components/NumberDropdown/NumberDropdown.tsx b/packages/components/src/components/NumberDropdown/NumberDropdown.tsx
index a26cd71ab..7f12198d5 100644
--- a/packages/components/src/components/NumberDropdown/NumberDropdown.tsx
+++ b/packages/components/src/components/NumberDropdown/NumberDropdown.tsx
@@ -1,137 +1,101 @@
-import * as React from 'react'
-import { Colors, IGlobalProps, INumberProps, Size, Type, getFontSize , getFormLabelSize } from '../../global'
-import { Popup } from '../Popup'
-import { Toggle, ToggleType } from '../Toggle'
-import { useState } from 'react'
-import { Slider } from '../Slider'
-import { ListBox } from '../ListBox'
-import { IListItemProps } from '../ListItem'
-import { Group } from '../Group'
-import { IconButton } from '../IconButton'
-import * as fa from 'react-icons/fa'
+import * as React from 'react';
+import { Colors, INumberProps, Size, getFormLabelSize } from '../../global';
+import { Popup } from '../Popup';
+import { Toggle, ToggleType } from '../Toggle';
+import { useState } from 'react';
+import { Slider } from '../Slider';
+import { ListBox } from '../ListBox';
+import { IListItemProps } from '../ListItem';
+import { Group } from '../Group';
+import { IconButton } from '../IconButton';
+import * as fa from 'react-icons/fa';
+import './NumberDropdown.scss';
 
-
-export type NumberDropdownType = 'slider' | 'dropdown' | 'input'
+export type NumberDropdownType = 'slider' | 'dropdown' | 'input';
 
 export interface INumberDropdownProps extends INumberProps {
-    numberDropdownType: NumberDropdownType,
-    showPlusMinus?: boolean
+    numberDropdownType: NumberDropdownType;
+    showPlusMinus?: boolean;
 }
 
 export const NumberDropdown = (props: INumberDropdownProps) => {
-    const [numberLoc, setNumberLoc] = useState<number>(0)
-    const { 
-        fillWidth, 
-        numberDropdownType = false, 
-        color = Colors.MEDIUM_BLUE, 
-        type,
-        formLabelPlacement, 
-        showPlusMinus, 
-        min, 
-        max, 
-        unit, 
-        step = 1, 
-        number = numberLoc, 
-        setNumber = setNumberLoc, 
-        size, 
-        formLabel, 
-        tooltip } = 
-    props;
+    const [numberLoc, setNumberLoc] = useState<number>(0);
+    const { fillWidth, numberDropdownType = false, color = Colors.MEDIUM_BLUE, type, formLabelPlacement, showPlusMinus, min, max, unit, step = 1, number = numberLoc, setNumber = setNumberLoc, size, formLabel, tooltip } = props;
     const [isOpen, setOpen] = useState<boolean>(false);
     let toggleText = number.toString();
-    if (unit) toggleText = toggleText + unit
-    let toggle = <Toggle 
-        tooltip={tooltip} 
-        color={color} 
-        fillWidth={fillWidth} 
-        type={type} 
-        size={size} 
-        align={'center'}
-        text={toggleText} 
-        toggleType={ToggleType.BUTTON} 
-        toggleStatus={isOpen}
-        onPointerDown={() => setOpen(!isOpen)}
-    />;
-    
+    if (unit) toggleText = toggleText + unit;
+    let toggle = <Toggle tooltip={tooltip} color={color} fillWidth={fillWidth} type={type} size={size} align={'center'} text={toggleText} toggleType={ToggleType.BUTTON} toggleStatus={isOpen} onPointerDown={() => setOpen(!isOpen)} />;
+
     if (showPlusMinus) {
-        toggle = <Group columnGap={0} style={{overflow: 'hidden'}}>
-            <IconButton 
-                size={size} 
-                icon={<fa.FaMinus/>} 
-                color={color}
-                onClick={(e) => {
-                    e.stopPropagation();
-                    setNumber(number - step);
-                }}
-                fillWidth={fillWidth}
-                tooltip={`Subtract ${step}${unit}`}
-            />
-            {toggle}
-            <IconButton 
-                size={size} 
-                icon={<fa.FaPlus/>} 
-                color={color}
-                onClick={(e) => {
-                    e.stopPropagation();
-                    setNumber(number + step);
-                }}
-                fillWidth={fillWidth}
-                tooltip={`Add ${step}${unit}`}
-            />
-        </Group>
+        toggle = (
+            <Group columnGap={0} style={{ overflow: 'hidden' }}>
+                <IconButton
+                    size={size}
+                    icon={<fa.FaMinus />}
+                    color={color}
+                    onClick={e => {
+                        e.stopPropagation();
+                        setNumber(number - step);
+                    }}
+                    fillWidth={fillWidth}
+                    tooltip={`Subtract ${step}${unit}`}
+                />
+                {toggle}
+                <IconButton
+                    size={size}
+                    icon={<fa.FaPlus />}
+                    color={color}
+                    onClick={e => {
+                        e.stopPropagation();
+                        setNumber(number + step);
+                    }}
+                    fillWidth={fillWidth}
+                    tooltip={`Add ${step}${unit}`}
+                />
+            </Group>
+        );
     }
-    
+
     let popup;
     switch (numberDropdownType) {
         case 'dropdown':
-            let items: IListItemProps[] = [];
-            for (let i = min; i <= max; i += step) {
-                let text = i.toString()
-                if (unit) text = i.toString() + unit
-                items.push(
-                    {
+            {
+                const items: IListItemProps[] = [];
+                for (let i = min; i <= max; i += step) {
+                    let text = i.toString();
+                    if (unit) text = i.toString() + unit;
+                    items.push({
                         text: text,
                         val: i,
-                        style: { textAlign: 'center' }
-                    }
-                )
+                        style: { textAlign: 'center' },
+                    });
+                }
+                popup = <ListBox color={color} selectedVal={number} setSelectedVal={num => setNumber(num as number)} items={items} />;
             }
-            popup = <ListBox 
-                color={color}
-                selectedVal={number}
-                setSelectedVal={(num) => setNumber(num as number)} 
-                items={items}
-            />
             break;
         case 'slider':
         default:
-            popup = <Slider size={Size.SMALL} unit={unit} multithumb={false} min={min} max={max} step={step} number={number} setNumber={setNumber}/>
+            popup = <Slider size={Size.SMALL} unit={unit} multithumb={false} min={min} max={max} step={step} number={number} setNumber={setNumber} />;
             break;
         case 'input':
-            popup = <Slider multithumb={false} min={min} max={max} step={step} number={number}/>
+            popup = <Slider multithumb={false} min={min} max={max} step={step} number={number} />;
             break;
     }
 
-    const numberDropdown: JSX.Element = <div className={`numberDropdown-container`} style={{width: fillWidth ? '100%' : 'fit-content'}}>
-        <Popup 
-            setOpen={setOpen} 
-            placement={'bottom'} 
-            isOpen={isOpen} 
-            popup={popup} 
-            toggle={toggle} 
-            fillWidth={fillWidth}
-            color={color}
-        />
-    </div>
+    const numberDropdown: JSX.Element = (
+        <div className={`numberDropdown-container`} style={{ height: '75%', width: fillWidth ? '100%' : 'fit-content' }}>
+            <Popup setOpen={setOpen} placement={'bottom'} size={size} isOpen={isOpen} popup={popup} toggle={toggle} fillWidth={fillWidth} color={color} />
+        </div>
+    );
 
-    return (
-        formLabel ? 
-        <div className={`form-wrapper ${formLabelPlacement}`}
-style={{ width: fillWidth ? '100%' : undefined}}>
+    return formLabel ? (
+        <div className={`form-wrapper ${formLabelPlacement}`} style={{ height: '100%', width: fillWidth ? '100%' : undefined }}>
             {numberDropdown}
-            <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div>
+            <div className="iconButton-label" style={{ fontSize: getFormLabelSize(size) }}>
+                {formLabel}
+            </div>
         </div>
-        :
+    ) : (
         numberDropdown
-    )
-}
\ No newline at end of file
+    );
+};
diff --git a/packages/components/src/components/Popup/Popup.tsx b/packages/components/src/components/Popup/Popup.tsx
index 92e7227bd..aa732d568 100644
--- a/packages/components/src/components/Popup/Popup.tsx
+++ b/packages/components/src/components/Popup/Popup.tsx
@@ -1,5 +1,5 @@
 import React, { useEffect, useRef, useState } from 'react';
-import { Colors, IGlobalProps, Placement, Size, getFormLabelSize, isDark } from '../../global';
+import { Colors, IGlobalProps, Placement, Size, isDark } from '../../global';
 import { Toggle, ToggleType } from '../Toggle';
 import './Popup.scss';
 import { Popper } from '@mui/material';
@@ -16,7 +16,7 @@ export interface IPopupProps extends IGlobalProps {
     iconPlacement?: Placement;
     placement?: Placement;
     size?: Size;
-    height?: number;
+    height?: number | string;
     toggle?: JSX.Element;
     popup: JSX.Element | string | (() => JSX.Element);
     trigger?: PopupTrigger;
@@ -62,12 +62,12 @@ export const Popup = (props: IPopupProps) => {
     } = props;
 
     const triggerRef = useRef(null);
-    const popperRef = useRef(null);
+    const popperRef = useRef<HTMLDivElement | null>(null);
 
     let timeout = setTimeout(() => {});
 
     const handlePointerAwayDown = (e: PointerEvent) => {
-        const rect = (popperRef.current as any)?.getBoundingClientRect();
+        const rect = popperRef.current?.getBoundingClientRect();
         if (rect && !(rect.left < e.clientX && rect.top < e.clientY && rect.right > e.clientX && rect.bottom > e.clientY) && !popupContainsPt?.(e.clientX, e.clientY)) {
             e.preventDefault();
             setOpen(false);
diff --git a/packages/components/src/global/globalTypes.ts b/packages/components/src/global/globalTypes.ts
index aa8451a9c..764fe7422 100644
--- a/packages/components/src/global/globalTypes.ts
+++ b/packages/components/src/global/globalTypes.ts
@@ -1,87 +1,76 @@
-import { PointerEventHandler } from "react"
-import { Size } from "./globalEnums"
+import { PointerEventHandler } from 'react';
+import { Size } from './globalEnums';
 
-export interface IGlobalProps {
-  // Size
-  size?: Size
-  height?: number
-  width?: number
-  fillWidth?: boolean
-  color?: string
-  background?: string
+export enum Type {
+    PRIM = 'primary',
+    SEC = 'secondary',
+    TERT = 'tertiary',
+}
 
-  // Type
-  type?: Type
+export type Placement = 'bottom-end' | 'bottom-start' | 'bottom' | 'left-end' | 'left-start' | 'left' | 'right-end' | 'right-start' | 'right' | 'top-end' | 'top-start' | 'top';
 
-  // Status
-  inactive?: boolean
+export type Alignment = 'flex-start' | 'flex-end' | 'center';
 
-  // Content
-  tooltip?: string
-  tooltipPlacement?: Placement
+export type TextAlignment = 'center' | 'left' | 'right';
 
-  // Label
-  label?: string
-  hideLabel?: boolean
+export interface IGlobalProps {
+    // Size
+    size?: Size;
+    height?: number | string;
+    width?: number;
+    fillWidth?: boolean;
+    color?: string;
+    background?: string;
 
-  // Label when used in forms
-  formLabel?: string
-  formLabelPlacement?: Placement
+    // Type
+    type?: Type;
 
-  // Custom style
-  style?: React.CSSProperties
+    // Status
+    inactive?: boolean;
 
-  // Global pointer events
-  onPointerDown?: PointerEventHandler | undefined;
-  onPointerDownCapture?: PointerEventHandler | undefined;
-  onPointerMove?: PointerEventHandler | undefined;
-  onPointerMoveCapture?: PointerEventHandler | undefined;
-  onPointerUp?: PointerEventHandler | undefined;
-  onPointerUpCapture?: PointerEventHandler | undefined;
-  onPointerCancel?: PointerEventHandler | undefined;
-  onPointerCancelCapture?: PointerEventHandler | undefined;
-  onPointerEnter?: PointerEventHandler | undefined;
-  onPointerEnterCapture?: PointerEventHandler | undefined;
-  onPointerLeave?: PointerEventHandler | undefined;
-  onPointerLeaveCapture?: PointerEventHandler | undefined;
-  onPointerOver?: PointerEventHandler | undefined;
-  onPointerOverCapture?: PointerEventHandler | undefined;
-  onPointerOut?: PointerEventHandler | undefined;
-  onPointerOutCapture?: PointerEventHandler | undefined;
-  onGotPointerCapture?: PointerEventHandler | undefined;
-  onGotPointerCaptureCapture?: PointerEventHandler | undefined;
-  onLostPointerCapture?: PointerEventHandler | undefined;
-  onLostPointerCaptureCapture?: PointerEventHandler | undefined;
-}
+    // Content
+    tooltip?: string;
+    tooltipPlacement?: Placement;
 
-export interface INumberProps extends IGlobalProps {
-  min: number,
-  max: number,
-  step?: number,
-  number: number
-  setNumber?: (num: number) => unknown,
-  unit?: string
-}
+    // Label
+    label?: string;
+    hideLabel?: boolean;
 
-export enum Type {
-  PRIM = "primary",
-  SEC = "secondary",
-  TERT = "tertiary",
-}
+    // Label when used in forms
+    formLabel?: string;
+    formLabelPlacement?: Placement;
 
-export type Placement = 'bottom-end'
-                      | 'bottom-start'
-                      | 'bottom'
-                      | 'left-end'
-                      | 'left-start'
-                      | 'left'
-                      | 'right-end'
-                      | 'right-start'
-                      | 'right'
-                      | 'top-end'
-                      | 'top-start'
-                      | 'top'
+    // Custom style
+    style?: React.CSSProperties;
 
-export type Alignment = 'flex-start' | 'flex-end' | 'center'
+    // Global pointer events
+    onPointerDown?: PointerEventHandler | undefined;
+    onPointerDownCapture?: PointerEventHandler | undefined;
+    onPointerMove?: PointerEventHandler | undefined;
+    onPointerMoveCapture?: PointerEventHandler | undefined;
+    onPointerUp?: PointerEventHandler | undefined;
+    onPointerUpCapture?: PointerEventHandler | undefined;
+    onPointerCancel?: PointerEventHandler | undefined;
+    onPointerCancelCapture?: PointerEventHandler | undefined;
+    onPointerEnter?: PointerEventHandler | undefined;
+    onPointerEnterCapture?: PointerEventHandler | undefined;
+    onPointerLeave?: PointerEventHandler | undefined;
+    onPointerLeaveCapture?: PointerEventHandler | undefined;
+    onPointerOver?: PointerEventHandler | undefined;
+    onPointerOverCapture?: PointerEventHandler | undefined;
+    onPointerOut?: PointerEventHandler | undefined;
+    onPointerOutCapture?: PointerEventHandler | undefined;
+    onGotPointerCapture?: PointerEventHandler | undefined;
+    onGotPointerCaptureCapture?: PointerEventHandler | undefined;
+    onLostPointerCapture?: PointerEventHandler | undefined;
+    onLostPointerCaptureCapture?: PointerEventHandler | undefined;
+}
 
-export type TextAlignment = 'center' | 'left' | 'right'
\ No newline at end of file
+export interface INumberProps extends IGlobalProps {
+    min: number;
+    max: number;
+    step?: number;
+    number: number;
+    setNumber?: (num: number) => unknown;
+    unit?: string;
+}
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 23032b62e..f1de6fe36 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -692,6 +692,8 @@ export namespace DocUtils {
                       //
                       annotationOn,
                       backgroundColor: backgroundColor ?? StrCast(Doc.UserDoc().textBackgroundColor),
+                      borderColor: Doc.UserDoc().borderColor as string,
+                      borderWidth: Doc.UserDoc().borderWidth as number,
                       x,
                       y,
                       title,
@@ -718,6 +720,8 @@ export namespace DocUtils {
                                 _layout_fitWidth: true,
                                 _layout_autoHeight: true,
                                 backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor),
+                                borderColor: Doc.UserDoc().borderColor as string,
+                                borderWidth: Doc.UserDoc().borderWidth as number,
                                 text_fitBox: BoolCast(Doc.UserDoc().fitBox),
                                 text_align: StrCast(Doc.UserDoc().textAlign),
                                 text_fontColor: StrCast(Doc.UserDoc().fontColor),
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 891223952..1ce25165c 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -240,8 +240,8 @@ export class DocumentOptions {
     dataViz?: string;
     dataViz_savedTemplates?: LISTt;
 
-    borderWidth?: STRt = new StrInfo('Width of user-added border', false);
-    borderColor?: STRt = new StrInfo('Color of user-added border', false);
+    borderWidth?: NUMt = new NumInfo('Width of docuent border', false);
+    borderColor?: STRt = new StrInfo('Color of document border', false);
     text_fontColor?: STRt = new StrInfo('Color of text', false);
     hCentering?: 'h-left' | 'h-center' | 'h-right';
     isDefaultTemplateDoc?: BOOLt = new BoolInfo('');
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index fb349abd9..d2d7cabd2 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -734,7 +734,7 @@ pie title Minerals in my tap water
     } 
     static viewTools(): Button[] {
         return [
-            { title: "Show Tags", icon: "id-card",             toolTip: "Toggle tags",         btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"toggle-tags",funcs: { },                                                                              scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
+            { title: "Tags",      icon: "id-card",             toolTip: "Toggle Tags display", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"toggle-tags",funcs: { },                                                                              scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
             { title: "Snap",      icon: "th",                  toolTip: "Show Snap Lines",     btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines",  funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
             { title: "Grid",      icon: "border-all",          toolTip: "Show Grid",           btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid",       funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
             { title: "Fit All",   icon: "object-group",        toolTip:"Fit Docs to View (double tap to persist)",
@@ -833,11 +833,13 @@ pie title Minerals in my tap water
             { title: "Header",  icon: "heading",       toolTip: "Doc Titlebar Color",   btnType: ButtonType.ColorButton,  expertMode: false,                                                                                                          ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'} },
             { title: "Template",icon: "scroll",        toolTip: "Default Note Template",btnType: ButtonType.ToggleButton, expertMode: false, toolType:DocumentType.RTF,                                                                                                              scripts: { onClick: '{ return setDefaultTemplate(_readOnly_); }'} },
             { title: "Fill",    icon: "fill-drip",     toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton,  expertMode: false,                                                                                                          ignoreClick: true, width: 30,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'} }, // Only when a document is selected
+            { title: "Border",  icon: "pen",           toolTip: "Border Color",         btnType: ButtonType.ColorButton,  expertMode: false,                                                                                                          ignoreClick: true, width: 30,  scripts: { script:  'return setBorderColor(value, _readOnly_)'} }, // Only when a document is selected
+            { title: "B.Width",                        toolTip: "Border width",         btnType: ButtonType.NumberSliderButton,                                                                                                                       ignoreClick: true,             scripts: {script: '{ return setBorderWidth(value, _readOnly_);}'}, numBtnMin: 0, linearBtnWidth:40},
             { title: "Overlay", icon: "layer-group",   toolTip: "Overlay",              btnType: ButtonType.ToggleButton, expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'},                        scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
             { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,  expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
             { title: "Num",     icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton,   expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,  expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
-            { title: "Chat",    icon:"lightbulb",      toolTip: "Toggle Chat Assistant",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat",               funcs: {},                                                                            width: 30,  scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
+            { title: "Chat",    icon: "lightbulb",     toolTip: "Toggle Chat Assistant",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat",               funcs: {},                                                                            width: 30,  scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
             { title: "Filter",  icon: "=",      toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.filterTools(), ignoreClick:true,  toolType:DocumentType.COL,            funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
             { title: "Sort",    icon: "Sort",   toolTip: "Sort Documents",       subMenu: CurrentUserUtils.sortTools(),   expertMode: false, toolType:DocumentType.COL,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Text",    icon: "Text",   toolTip: "Text functions",       subMenu: CurrentUserUtils.textTools(),   expertMode: false, toolType:DocumentType.RTF,            funcs: {                                                            linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
diff --git a/src/client/views/StyleProp.ts b/src/client/views/StyleProp.ts
index 56367e70b..1ef7a9e1f 100644
--- a/src/client/views/StyleProp.ts
+++ b/src/client/views/StyleProp.ts
@@ -5,6 +5,7 @@ export enum StyleProp {
     Opacity = 'opacity', // opacity of the document view
     BoxShadow = 'boxShadow', // box shadow - used for making collections standout and for showing clusters in free form views
     BorderRounding = 'borderRounding', // border radius of the document view
+    Border = 'border', // border of document view
     Color = 'color', // foreground color of Document view items
     BackgroundColor = 'backgroundColor', // background color of a document view
     FillColor = 'fillColor', // fill color of an ink stroke or shape
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index bebc9a341..b04b1ae65 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -206,33 +206,40 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
             const rounding = StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.layout_borderRounding, doc?._type_collection === CollectionViewType.Pile ? '50%' : ''));
             return (doc?.[StrCast(doc?.layout_fieldKey)] instanceof Doc || doc?.isTemplateDoc) ? StrCast(doc._layout_borderRounding,rounding) : rounding;
         }
+        case StyleProp.Border: {
+            const bcolor = StrCast(doc?.borderColor, StrCast(doc?.[fieldKey + 'borderColor'], StrCast(doc?.layout_borderColor)));
+            return bcolor + " " +
+                   StrCast(doc?.borderStyle, StrCast(doc?.[fieldKey + 'borderStyle'], StrCast(doc?.layout_borderStyle, "solid"))) + " " +
+                   (StrCast(doc?.borderWidth || doc?.[fieldKey + 'borderWidth'] || doc?.layout_borderWidth) ||
+                    (NumCast(doc?.borderWidth, NumCast(doc?.[fieldKey + 'borderWidth'], NumCast(doc?.layout_borderWidth, bcolor ?1:0)))+"px"))
+        }
         // Doc.IsComicStyle(doc) &&
         //         renderDepth &&
         //         !doc?.layout_isSvg && 
         //case StyleProp.
-        case StyleProp.BorderPath: {
-            const docWidth = Number(doc?._width);
-            const borderWidth = Number(StrCast(doc?.borderWidth));
-            //console.log(borderWidth);
-            const ratio = borderWidth / docWidth;
-            const borderRadius = Number(StrCast(layoutDoc?._layout_borderRounding).replace('px', ''));
-            const radiusRatio = borderRadius / docWidth;
-            const radius = radiusRatio * ((2 * borderWidth) + docWidth);
+        // case StyleProp.BorderPath: {
+        //     const docWidth = Number(doc?._width);
+        //     const borderWidth = Number(StrCast(doc?.borderWidth));
+        //     //console.log(borderWidth);
+        //     const ratio = borderWidth / docWidth;
+        //     const borderRadius = Number(StrCast(layoutDoc?._layout_borderRounding).replace('px', ''));
+        //     const radiusRatio = borderRadius / docWidth;
+        //     const radius = radiusRatio * ((2 * borderWidth) + docWidth);
 
-            const borderPath = doc && border(doc, NumCast(doc._width), NumCast(doc._height), radius, -ratio/2);
-                return !borderPath
-                    ? null
-                    : {
-                        clipPath: `path('${borderPath}')`,
-                        jsx: (
-                            <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
-                                <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${PanelWidth?.()} ${PanelHeight?.()}`}>
-                                    <path d={borderPath} style={{ stroke: StrCast(doc?.borderColor), fill: 'transparent', strokeWidth: `${StrCast(doc?.borderWidth)}px` }} />
-                                </svg>
-                            </div>
-                        ),
-                    };
-        }
+        //     const borderPath = doc && border(doc, NumCast(doc._width), NumCast(doc._height), radius, -ratio/2);
+        //         return !borderPath
+        //             ? null
+        //             : {
+        //                 clipPath: `path('${borderPath}')`,
+        //                 jsx: (
+        //                     <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
+        //                         <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${PanelWidth?.()} ${PanelHeight?.()}`}>
+        //                             <path d={borderPath} style={{ stroke: StrCast(doc?.borderColor), fill: 'transparent', strokeWidth: `${StrCast(doc?.borderWidth)}px` }} />
+        //                         </svg>
+        //                     </div>
+        //                 ),
+        //             };
+        // }
         case StyleProp.HeaderMargin:
             return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._type_collection as CollectionViewType) ||
                 (doc?.type === DocumentType.RTF && !layoutShowTitle()?.includes('noMargin')) ||
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 79873ed8f..b455f76e4 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -66,6 +66,49 @@ ScriptingGlobals.add(function setView(view: string, shiftKey: boolean, checkResu
     return undefined;
 });
 
+// toggle: Set overlay status of selected document
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function setBorderColor(color?: string, checkResult?: boolean) {
+    const selectedViews = DocumentView.Selected();
+    const defaultBorder = () => StrCast(Doc.UserDoc().borderColor, 'transparent');
+    const setDefaultBorder = (c: string) => { Doc.UserDoc().borderColor = c; }; // prettier-ignore
+    const fieldKey = 'borderColor';
+    if (selectedViews.length) {
+        if (checkResult) {
+            const selView = selectedViews.lastElement();
+            const layoutFrameNumber = Cast(selView.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
+            const contentFrameNumber = Cast(selView.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
+            return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] || defaultBorder();
+        }
+        setDefaultBorder(color ?? 'transparent');
+        selectedViews.forEach(dv => {
+            const layoutFrameNumber = Cast(dv.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
+            const contentFrameNumber = Cast(dv.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
+            if (contentFrameNumber !== undefined) {
+                const obj: { [key: string]: Opt<string> } = {};
+                obj[fieldKey] = color;
+                CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.Document, obj);
+            } else {
+                const dataKey = Doc.LayoutFieldKey(dv.Document);
+                const alternate = (dv.layoutDoc[dataKey + '_usePath'] ? '_' + dv.layoutDoc[dataKey + '_usePath'] : '').replace(':hover', '');
+                dv.layoutDoc[fieldKey + alternate] = undefined;
+                dv.dataDoc[fieldKey + alternate] = color;
+            }
+        });
+    } else {
+        const selected = DocumentView.SelectedDocs().length ? DocumentView.SelectedDocs() : LinkManager.Instance.currentLink ? [LinkManager.Instance.currentLink] : [];
+        if (checkResult) {
+            return (selected.lastElement() ?? Doc.UserDoc()).borderColor ?? defaultBorder();
+        }
+        if (!selected.length) setDefaultBorder(color ?? 'transparent');
+        else
+            selected.forEach(doc => {
+                doc[DocData].borderColor = color;
+            });
+    }
+    return '';
+});
+
 // toggle: Set overlay status of selected document
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
@@ -408,6 +451,18 @@ ScriptingGlobals.add(function activeEraserTool() {
     return StrCast(Doc.UserDoc().activeEraserTool, InkEraserTool.Stroke);
 }, 'returns the current eraser tool');
 
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function setBorderWidth(value: number, checkResult?: boolean) {
+    const selected = DocumentView.SelectedDocs().lastElement();
+    if (checkResult) return NumCast((selected ?? Doc.UserDoc()).borderWidth);
+    if (!selected) Doc.UserDoc().borderWidth = value;
+    else
+        DocumentView.SelectedDocs().map(doc => {
+            doc.borderWidth = value;
+        });
+    return undefined;
+}, 'sets the border width of the selected document');
+
 // toggle: Set overlay status of selected document
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function setInkProperty(option: InkProperty, value: string | number, checkResult?: boolean) {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index beea6ab3c..76d6ed80b 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -69,7 +69,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         { key: 'freeform_panX' },
         { key: 'freeform_panY' },
     ]; // fields that are configured to be animatable using animation frames
-    public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames
+    public static animStringFields = ['backgroundColor', 'borderColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames
     public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames
     public static from(dv?: DocumentView): CollectionFreeFormDocumentView | undefined {
         return dv?._props.reactParent instanceof CollectionFreeFormDocumentView ? dv._props.reactParent : undefined;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 0193fd328..441d6053f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -131,6 +131,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     style = (doc: Doc, sprop: StyleProp | string) => this._props.styleProvider?.(doc, this._props, sprop);
     @computed get opacity()           { return this.style(this.layoutDoc, StyleProp.Opacity) as number; } // prettier-ignore
     @computed get boxShadow()         { return this.style(this.layoutDoc, StyleProp.BoxShadow) as string; } // prettier-ignore
+    @computed get border()            { return this.style(this.layoutDoc, StyleProp.Border) as string || ""; } // prettier-ignore
     @computed get borderRounding()    { return this.style(this.layoutDoc, StyleProp.BorderRounding) as string; } // prettier-ignore
     @computed get widgetDecorations() { return this.style(this.layoutDoc, StyleProp.Decorations) as JSX.Element; } // prettier-ignore
     @computed get backgroundBoxColor(){ return this.style(this.layoutDoc, StyleProp.BackgroundColor + ':docView') as string; } // prettier-ignore
@@ -684,14 +685,14 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     };
 
     rootSelected = () => this._rootSelected;
-    panelHeight = () => this._props.PanelHeight() - this.headerMargin;
+    panelHeight = () => this._props.PanelHeight() - this.headerMargin - 2 * NumCast(this.Document.borderWidth);
     screenToLocalContent = () =>
         this._props
             .ScreenToLocalTransform()
-            .translate(0, -this.headerMargin)
+            .translate(-NumCast(this.Document.borderWidth), -this.headerMargin - NumCast(this.Document.borderWidth))
             .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.aiContentsHeight() : 1);
     onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr;
-    setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore
+    setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height + 2 * NumCast(this.Document.borderWidth))); } // prettier-ignore
     setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore
     isContentActive = (): boolean | undefined => this._isContentActive;
     childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)];
@@ -989,15 +990,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             highlightStroke: undefined,
         };
         const { clipPath, jsx } = (borderPath as { clipPath: string; jsx: JSX.Element }) ?? { clipPath: undefined, jsx: undefined };
-        const boxShadow = !highlighting
-            ? this.boxShadow
-            : highlighting && this.borderRounding && highlightStyle !== 'dashed'
-              ? `0 0 0 ${highlightIndex}px ${highlightColor}`
-              : this.boxShadow || (this.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined);
+        const boxShadow = this.boxShadow;
         const renderDoc = this.renderDoc({
             borderRadius: this.borderRounding,
-            outline: highlighting && !this.borderRounding && !highlightStroke ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : 'solid 0px',
-            border: highlighting && this.borderRounding && highlightStyle === 'dashed' ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined,
+            outline: highlighting && !highlightStroke ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : 'solid 0px',
+            border: this._componentView?.isUnstyledView?.() ? undefined : this.border,
             boxShadow,
             clipPath,
         });
@@ -1013,7 +1010,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 onPointerOver={() => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)}
                 onPointerLeave={e => !isParentOf(this._contentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.Document)}
                 style={{
-                    borderRadius: this.borderRounding,
+                    borderRadius: this._componentView?.isUnstyledView?.() ? undefined : this.borderRounding,
                     pointerEvents: this._pointerEvents === 'visiblePainted' ? 'none' : this._pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here)
                 }}>
                 {this._componentView?.isUnstyledView?.() || this.Document.type === DocumentType.CONFIG || !renderDoc ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation], this.Document)}
@@ -1487,7 +1484,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     ShouldNotScale = () => this.shouldNotScale;
     NativeWidth = () => this.effectiveNativeWidth;
     NativeHeight = () => this.effectiveNativeHeight;
-    PanelWidth = () => this.panelWidth;
+    PanelWidth = () => this.panelWidth - 2 * NumCast(this.Document.borderWidth);
     PanelHeight = () => this.panelHeight;
     ReducedPanelWidth = () => this.panelWidth / 2;
     ReducedPanelHeight = () => this.panelWidth / 2;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index f58862028..f60c83343 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -134,6 +134,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                 min={NumCast(this.dataDoc.numBtnMin, 0)}
                 max={NumCast(this.dataDoc.numBtnMax, 100)}
                 number={checkResult}
+                size={Size.XSMALL}
                 setNumber={undoable(value => numScript(value), `${this.Document.title} button set from list`)}
                 fillWidth
             />
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 3abb39ff2..da2131940 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1282,7 +1282,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         );
         this._disposers.componentHeights = reaction(
             // set the document height when one of the component heights changes and layout_autoHeight is on
-            () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
+            () => ({ border: this._props.PanelHeight(), sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
             ({ sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
                 const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
                 if (
-- 
cgit v1.2.3-70-g09d2


From 515707c4561eb526426b8fa07dd50bd499fb91cc Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 25 Feb 2025 01:03:25 -0500
Subject: added a hideUI option to hide buttons.  fixed a mess of runtime
 warnings mostly related to how scss files can be included in each other

---
 .../components/src/components/Button/Button.scss   | 184 +++++------
 .../src/components/ColorPicker/ColorPicker.scss    |  36 +--
 .../src/components/Dropdown/Dropdown.scss          | 223 +++++++-------
 .../components/DropdownSearch/DropdownSearch.scss  | 207 +++++++------
 .../src/components/EditableText/EditableText.scss  | 224 +++++++-------
 .../src/components/FormInput/FormInput.scss        | 112 +++----
 .../components/src/components/Group/Group.scss     |  23 +-
 .../src/components/IconButton/IconButton.scss      | 190 ++++++------
 .../components/src/components/ListBox/ListBox.scss |   4 +-
 .../src/components/ListItem/ListItem.scss          | 132 ++++----
 .../components/src/components/Modal/Modal.scss     |  74 ++---
 .../src/components/MultiToggle/MultiToggle.scss    |   6 +-
 .../components/NumberDropdown/NumberDropdown.scss  |   7 +-
 .../src/components/NumberInput/NumberInput.scss    |   4 +-
 .../components/src/components/Popup/Popup.scss     |  40 +--
 .../components/src/components/Slider/Slider.scss   | 305 +++++++++----------
 .../components/src/components/Slider/Slider.tsx    | 338 +++++++++++----------
 .../src/components/Template/Template.scss          |   6 +-
 .../components/src/components/Toggle/Toggle.scss   | 122 ++++----
 .../components/src/global/globalCssVariables.scss  | 110 +++----
 src/client/util/CaptureManager.scss                |   2 -
 src/client/util/CurrentUserUtils.ts                |  14 +-
 src/client/util/SettingsManager.scss               |   2 -
 src/client/util/SnappingManager.ts                 |   3 +
 src/client/util/reportManager/ReportManager.scss   |   4 +-
 src/client/views/AntimodeMenu.scss                 |   6 +-
 src/client/views/ContextMenu.scss                  |  16 +-
 src/client/views/DashboardView.scss                |  16 +-
 src/client/views/DocumentButtonBar.scss            |  23 +-
 src/client/views/DocumentDecorations.scss          |  38 +--
 src/client/views/LightboxView.tsx                  |   4 +-
 src/client/views/Main.scss                         |  12 +-
 src/client/views/Main.tsx                          |   3 +-
 src/client/views/MainView.scss                     |  18 +-
 src/client/views/MainView.tsx                      |   8 +-
 src/client/views/PropertiesButtons.scss            |  20 +-
 src/client/views/PropertiesSection.scss            |   2 -
 src/client/views/PropertiesView.scss               |   6 +-
 src/client/views/TemplateMenu.scss                 |   8 +-
 src/client/views/animationtimeline/Region.scss     |  10 +-
 src/client/views/animationtimeline/Timeline.scss   |   6 +-
 .../views/animationtimeline/TimelineMenu.scss      |  16 +-
 .../views/animationtimeline/TimelineOverview.scss  |   2 +-
 src/client/views/animationtimeline/Track.scss      |   6 +-
 .../views/collections/CollectionCardDeckView.scss  |   2 -
 .../collections/CollectionCarousel3DView.scss      |  12 +-
 .../views/collections/CollectionDockingView.scss   |  22 +-
 src/client/views/collections/CollectionMenu.scss   |   6 +-
 .../collections/CollectionNoteTakingView.scss      |  22 +-
 .../collections/CollectionStackedTimeline.scss     |  26 +-
 .../views/collections/CollectionStackingView.scss  |  22 +-
 .../views/collections/CollectionTreeView.scss      |  10 +-
 src/client/views/collections/CollectionView.scss   |   6 +-
 src/client/views/collections/TabDocView.scss       |   2 +-
 src/client/views/collections/TreeView.scss         |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.scss |  16 +-
 .../collectionFreeForm/ImageLabelHandler.tsx       |  14 +-
 .../collectionLinear/CollectionLinearView.scss     |  22 +-
 .../collectionSchema/CollectionSchemaView.scss     |  40 ++-
 src/client/views/linking/LinkMenu.scss             |   2 +-
 src/client/views/linking/LinkMenuItem.scss         |   8 +-
 .../views/newlightbox/ButtonMenu/ButtonMenu.scss   |  12 +-
 .../views/newlightbox/ExploreView/ExploreView.scss |  48 +--
 .../views/newlightbox/Header/LightboxHeader.scss   |  28 +-
 .../RecommendationList/RecommendationList.scss     | 179 ++++++-----
 .../components/EditableText/EditableText.scss      |  46 +--
 .../components/Recommendation/Recommendation.scss  | 122 ++++----
 .../components/SkeletonDoc/SkeletonDoc.scss        | 133 ++++----
 .../newlightbox/components/Template/Template.scss  |  12 +-
 src/client/views/nodes/AudioBox.scss               |  42 +--
 .../views/nodes/DataVizBox/components/Chart.scss   |  10 +-
 src/client/views/nodes/DocumentLinksButton.scss    |  18 +-
 src/client/views/nodes/DocumentView.scss           |   6 +-
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/client/views/nodes/EquationBox.scss            |   2 -
 .../views/nodes/FontIconBox/FontIconBox.scss       |  36 +--
 src/client/views/nodes/IconTagBox.scss             |   4 +-
 src/client/views/nodes/KeyValueBox.scss            |  22 +-
 src/client/views/nodes/KeyValuePair.scss           |   2 +-
 src/client/views/nodes/LinkDescriptionPopup.scss   |  10 +-
 src/client/views/nodes/MapBox/AnimationUtility.ts  |   4 +-
 src/client/views/nodes/MapBox/MapBox.scss          |  16 +-
 src/client/views/nodes/PDFBox.scss                 |   8 +-
 src/client/views/nodes/VideoBox.scss               |  16 +-
 src/client/views/nodes/WebBox.scss                 |   4 +-
 .../nodes/chatbot/chatboxcomponents/ChatBox.scss   |  11 +-
 .../views/nodes/formattedText/DashFieldView.scss   |   4 +-
 .../nodes/formattedText/FormattedTextBox.scss      |  20 +-
 .../views/nodes/formattedText/RichTextMenu.scss    |   4 +-
 src/client/views/nodes/imageEditor/ImageEditor.tsx |   8 +-
 src/client/views/nodes/trails/PresBox.scss         | 142 ++++-----
 src/client/views/search/SearchBox.scss             |  10 +-
 src/client/views/topbar/TopBar.scss                |  29 +-
 93 files changed, 1909 insertions(+), 1931 deletions(-)

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

diff --git a/packages/components/src/components/Button/Button.scss b/packages/components/src/components/Button/Button.scss
index a31923e6d..bbe2e2470 100644
--- a/packages/components/src/components/Button/Button.scss
+++ b/packages/components/src/components/Button/Button.scss
@@ -1,118 +1,118 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .button-container {
-  position: relative;
-  width: fit-content;
-  padding: $padding;
-  cursor: pointer;
-  overflow: hidden;
-  user-select: none;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  gap: 5px;
-  font-family: $default-font;
-  border-radius: $standard-border-radius;
-  white-space: nowrap;
-  transition: 0.4s;
-  border: solid 1px;
-  border-color: transparent;
-  pointer-events: all;
-
-  &.icon {
-    padding: 0;
-    gap: 0;
-  }
-
-  .button-content {
+    position: relative;
+    width: fit-content;
+    padding: global.$padding;
+    cursor: pointer;
+    overflow: hidden;
+    user-select: none;
     display: flex;
     justify-content: center;
     align-items: center;
-    width: fit-content;
-    height: 100%;
-    z-index: 1;
     gap: 5px;
+    font-family: global.$default-font;
+    border-radius: global.$standard-border-radius;
+    white-space: nowrap;
+    transition: 0.4s;
+    border: solid 1px;
+    border-color: transparent;
+    pointer-events: all;
 
-    .icon {
-      display: flex;
-      justify-content: center;
-      align-items: center;
+    &.icon {
+        padding: 0;
+        gap: 0;
     }
-  }
-
-  .background {
-    width: 100%;
-    height: 100%;
-    z-index: 0;
-    left: 0;
-    top: 0;
-    position: absolute;
-    transition: 0.4s;
-  }
 
-  &.inactive {
-    &:hover {
-      .background {
-        filter: opacity(0) !important;
-      }
+    .button-content {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: fit-content;
+        height: 100%;
+        z-index: 1;
+        gap: 5px;
+
+        .icon {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
     }
-  }
 
-  &.primary {
     .background {
-      filter: opacity(0);
-
-      &.active {
-        filter: opacity(0.2) !important;
-      }
+        width: 100%;
+        height: 100%;
+        z-index: 0;
+        left: 0;
+        top: 0;
+        position: absolute;
+        transition: 0.4s;
     }
 
-    &:hover{
-      .background {
-        filter: opacity(0.2)
-      }
+    &.inactive {
+        &:hover {
+            .background {
+                filter: opacity(0) !important;
+            }
+        }
     }
-  }
 
-  &.secondary {
-    .background {
-      filter: opacity(0);
+    &.primary {
+        .background {
+            filter: opacity(0);
 
-      &.active {
-        filter: opacity(0.2) !important;
-      }
-    }
+            &.active {
+                filter: opacity(0.2) !important;
+            }
+        }
 
-    &:hover{
-      .background {
-        filter: opacity(0.2)
-      }
+        &:hover {
+            .background {
+                filter: opacity(0.2);
+            }
+        }
     }
-  }
 
-  &.tertiary {
-    &:hover{
-      box-shadow: $standard-shadow;
-    }
+    &.secondary {
+        .background {
+            filter: opacity(0);
 
-    .background {
-      filter: opacity(1) !important;
+            &.active {
+                filter: opacity(0.2) !important;
+            }
+        }
+
+        &:hover {
+            .background {
+                filter: opacity(0.2);
+            }
+        }
     }
 
-    &:hover{
-      .background {
-        filter: brightness(0.8);
-      }
+    &.tertiary {
+        &:hover {
+            box-shadow: global.$standard-shadow;
+        }
+
+        .background {
+            filter: opacity(1) !important;
+        }
+
+        &:hover {
+            .background {
+                filter: brightness(0.8);
+            }
+        }
     }
-  }
 
-  .label {
-    position: absolute;
-    bottom: 0;
-    width: 100%;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    font-size: $xsmall-fontSize;
-  }
+    .label {
+        position: absolute;
+        bottom: 0;
+        width: 100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        font-size: global.$xsmall-fontSize;
+    }
 }
diff --git a/packages/components/src/components/ColorPicker/ColorPicker.scss b/packages/components/src/components/ColorPicker/ColorPicker.scss
index e3ed32a45..32b912fe5 100644
--- a/packages/components/src/components/ColorPicker/ColorPicker.scss
+++ b/packages/components/src/components/ColorPicker/ColorPicker.scss
@@ -1,23 +1,23 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .colorPicker-container {
-  display: flex;
-  border-radius: $standard-border-radius;
-  width: fit-content;
-  height: fit-content;
-  position: relative;
-
-  .colorPicker-toggle {
-    width: 100%;
-    height: 100%;
-    position: relative;
-    cursor: pointer;
-  }
-
-  .colorPicker-popup {
-    position: absolute;
-    top: calc(100% + 5px);
+    display: flex;
+    border-radius: global.$standard-border-radius;
     width: fit-content;
     height: fit-content;
-  }
+    position: relative;
+
+    .colorPicker-toggle {
+        width: 100%;
+        height: 100%;
+        position: relative;
+        cursor: pointer;
+    }
+
+    .colorPicker-popup {
+        position: absolute;
+        top: calc(100% + 5px);
+        width: fit-content;
+        height: fit-content;
+    }
 }
diff --git a/packages/components/src/components/Dropdown/Dropdown.scss b/packages/components/src/components/Dropdown/Dropdown.scss
index 34ed84004..f9ea2711a 100644
--- a/packages/components/src/components/Dropdown/Dropdown.scss
+++ b/packages/components/src/components/Dropdown/Dropdown.scss
@@ -1,135 +1,136 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .dropdown {
-  margin-top: 10px;
+    margin-top: 10px;
 }
 
 .divider {
-  height: 1px;
-  width: 100%;
-  background: $medium-gray;
+    height: 1px;
+    width: 100%;
+    background: global.$medium-gray;
 }
 
 .dropdown-container {
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  min-width: fit-content;
-  width: 100%;
-  border-radius: $standard-border-radius;
-  height: 100%;
-  position: relative;
-  transition: 0.4s;
-
-  .dropdown-list {
-    position: absolute;
-    top: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    min-width: fit-content;
     width: 100%;
-  }
-  .dropdown-toggle-mini,
-  .dropdown-toggle {
-    width: calc(100% - 2px);
-    display: grid;
-    grid-template-columns: calc(100% - 30px) 30px;
-    grid-template-areas: 'button end';
-    grid-template-rows: 1fr;
+    border-radius: global.$standard-border-radius;
+    height: 100%;
     position: relative;
-    align-items: center;
-    border: solid 1px;
-    border-color: transparent;
-    border-radius: $standard-border-radius;
-    overflow: hidden;
-
-    &.inactive {
-      filter: opacity(0.5);
-      pointer-events: none;
-      cursor: not-allowed;
-    }
+    transition: 0.4s;
 
-    .background {
-      width: 100%;
-      height: 100%;
-      z-index: 0;
-      position: absolute;
-      transition: 0.4s;
+    .dropdown-list {
+        position: absolute;
+        top: 100%;
+        width: 100%;
     }
+    .dropdown-toggle-mini,
+    .dropdown-toggle {
+        width: calc(100% - 2px);
+        display: grid;
+        grid-template-columns: calc(100% - 30px) 30px;
+        grid-template-areas: 'button end';
+        grid-template-rows: 1fr;
+        position: relative;
+        align-items: center;
+        border: solid 1px;
+        border-color: transparent;
+        border-radius: global.$standard-border-radius;
+        overflow: hidden;
 
-    &.inactive {
-      &:hover {
-        .background {
-          filter: opacity(0) !important;
+        &.inactive {
+            filter: opacity(0.5);
+            pointer-events: none;
+            cursor: not-allowed;
         }
-      }
-    }
-  
-    &.primary {
-      .background {
-        filter: opacity(0);
-  
-        &.active {
-          filter: opacity(0.2) !important;
-        }
-      }
-  
-      &:hover{
+
         .background {
-          filter: opacity(0.2)
+            width: 100%;
+            height: 100%;
+            z-index: 0;
+            position: absolute;
+            transition: 0.4s;
         }
-      }
-    }
-  
-    &.secondary {
-      .background {
-        filter: opacity(0);
-  
-        &.active {
-          filter: opacity(0.2) !important;
+
+        &.inactive {
+            &:hover {
+                .background {
+                    filter: opacity(0) !important;
+                }
+            }
         }
-      }
-  
-      &:hover{
-        .background {
-          filter: opacity(0.2)
+
+        &.primary {
+            .background {
+                filter: opacity(0);
+
+                &.active {
+                    filter: opacity(0.2) !important;
+                }
+            }
+
+            &:hover {
+                .background {
+                    filter: opacity(0.2);
+                }
+            }
         }
-      }
-    }
-  
-    &.tertiary {  
-      &:hover{
-        box-shadow: $standard-button-shadow;
-      }
-  
-      &:hover{
-        .background {
-          filter: brightness(0.8);
+
+        &.secondary {
+            .background {
+                filter: opacity(0);
+
+                &.active {
+                    filter: opacity(0.2) !important;
+                }
+            }
+
+            &:hover {
+                .background {
+                    filter: opacity(0.2);
+                }
+            }
         }
-      }
-    }
 
-    .toggle-button {
-      grid-area: button;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      width: 100%;
-      height: 100%;
-      min-width: 70px;
-      justify-self: center;
-    }
+        &.tertiary {
+            &:hover {
+                box-shadow: global.$standard-button-shadow;
+            }
 
-    .toggle-caret {
-      cursor: pointer;
-      grid-area: end;
-      display: flex;
-      justify-content: flex-end;
-      align-items: center;
-      justify-self: center;
+            &:hover {
+                .background {
+                    filter: brightness(0.8);
+                }
+            }
+        }
+
+        .toggle-button {
+            grid-area: button;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            width: 100%;
+            height: 100%;
+            min-width: 70px;
+            justify-self: center;
+        }
+
+        .toggle-caret {
+            cursor: pointer;
+            grid-area: end;
+            display: flex;
+            justify-content: flex-end;
+            align-items: center;
+            justify-self: center;
+        }
     }
-  }
-  .dropdown-toggle-mini {
-    .toggle-caret {
-      position: absolute;
-      top:0; left:0;
+    .dropdown-toggle-mini {
+        .toggle-caret {
+            position: absolute;
+            top: 0;
+            left: 0;
+        }
     }
-  }
 }
diff --git a/packages/components/src/components/DropdownSearch/DropdownSearch.scss b/packages/components/src/components/DropdownSearch/DropdownSearch.scss
index e111c822b..d937df540 100644
--- a/packages/components/src/components/DropdownSearch/DropdownSearch.scss
+++ b/packages/components/src/components/DropdownSearch/DropdownSearch.scss
@@ -1,123 +1,122 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .dropdownsearch {
-  margin-top: 10px;
+    margin-top: 10px;
 }
 
 .divider {
-  height: 1px;
-  width: 100%;
-  background: $medium-gray;
+    height: 1px;
+    width: 100%;
+    background: global.$medium-gray;
 }
 
 .dropdownsearch-container {
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  min-width: fit-content;
-  border-radius: $standard-border-radius;
-  height: 100%;
-  position: relative;
-  transition: 0.4s;
-
-  .dropdownsearch-list {
-    position: absolute;
-    top: 100%;
-    width: 100%;
-  }
-
-  .dropdownsearch-toggle {
-    width: 100%;
-    display: grid;
-    grid-template-columns: calc(100% - 30px) 30px;
-    grid-template-areas: 'button end';
-    grid-template-rows: 1fr;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    min-width: fit-content;
+    border-radius: global.$standard-border-radius;
+    height: 100%;
     position: relative;
-    align-items: center;
-    border: solid 1px;
-    border-color: transparent;
-    border-radius: $standard-border-radius;
-    overflow: hidden;
-
-    .toggle-background {
-      width: 100%;
-      height: 100%;
-      z-index: 0;
-      position: absolute;
-      transition: 0.4s;
-
-      &.active {
-        filter: opacity(0.2) !important;
-      }
+    transition: 0.4s;
+
+    .dropdownsearch-list {
+        position: absolute;
+        top: 100%;
+        width: 100%;
     }
 
-    &.primary {
-      color: $medium-blue;
-      .toggle-background {
-        background: $medium-blue;
-        filter: opacity(0);
-      }
-  
-      &:hover{
+    .dropdownsearch-toggle {
+        width: 100%;
+        display: grid;
+        grid-template-columns: calc(100% - 30px) 30px;
+        grid-template-areas: 'button end';
+        grid-template-rows: 1fr;
+        position: relative;
+        align-items: center;
+        border: solid 1px;
+        border-color: transparent;
+        border-radius: global.$standard-border-radius;
+        overflow: hidden;
+
         .toggle-background {
-          filter: opacity(0.2)
+            width: 100%;
+            height: 100%;
+            z-index: 0;
+            position: absolute;
+            transition: 0.4s;
+
+            &.active {
+                filter: opacity(0.2) !important;
+            }
         }
-      }
-  
-    }
-  
-    &.secondary {
-      .toggle-background {
-        background: $medium-blue;
-        filter: opacity(0);
-      }
-  
-      border: solid 1px $medium-blue;
-      color: $medium-blue;
-  
-      &:hover{
-        .toggle-background {
-          filter: opacity(0.2)
+
+        &.primary {
+            color: global.$medium-blue;
+            .toggle-background {
+                background: global.$medium-blue;
+                filter: opacity(0);
+            }
+
+            &:hover {
+                .toggle-background {
+                    filter: opacity(0.2);
+                }
+            }
         }
-      }
-    }
-  
-    &.tertiary {
-      color: white;
-  
-      .toggle-background {
-        background: $medium-blue;
-      }
-  
-      &:hover{
-        box-shadow: $standard-button-shadow;
-      }
-  
-      &:hover{
-        .toggle-background {
-          filter: brightness(0.8);
+
+        &.secondary {
+            .toggle-background {
+                background: global.$medium-blue;
+                filter: opacity(0);
+            }
+
+            border: solid 1px global.$medium-blue;
+            color: global.$medium-blue;
+
+            &:hover {
+                .toggle-background {
+                    filter: opacity(0.2);
+                }
+            }
         }
-      }
-    }
 
-    .toggle-button {
-      grid-area: button;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      width: 100%;
-      height: 100%;
-      min-width: 70px;
-      justify-self: center;
-    }
+        &.tertiary {
+            color: white;
+
+            .toggle-background {
+                background: global.$medium-blue;
+            }
 
-    .toggle-caret {
-      cursor: pointer;
-      grid-area: end;
-      display: flex;
-      justify-content: flex-end;
-      align-items: center;
-      justify-self: center;
+            &:hover {
+                box-shadow: global.$standard-button-shadow;
+            }
+
+            &:hover {
+                .toggle-background {
+                    filter: brightness(0.8);
+                }
+            }
+        }
+
+        .toggle-button {
+            grid-area: button;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            width: 100%;
+            height: 100%;
+            min-width: 70px;
+            justify-self: center;
+        }
+
+        .toggle-caret {
+            cursor: pointer;
+            grid-area: end;
+            display: flex;
+            justify-content: flex-end;
+            align-items: center;
+            justify-self: center;
+        }
     }
-  }
 }
diff --git a/packages/components/src/components/EditableText/EditableText.scss b/packages/components/src/components/EditableText/EditableText.scss
index 19e5af2cd..15965e97e 100644
--- a/packages/components/src/components/EditableText/EditableText.scss
+++ b/packages/components/src/components/EditableText/EditableText.scss
@@ -1,131 +1,129 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .editableText-container {
-  position: relative;
-  width: fit-content;
-  border: solid 1px;
-  border-color: transparent;
-  border-radius: $standard-border-radius;
-  font-family: $default-font;
-  overflow: hidden;
-  padding: $padding;
-
-  .password {
-    position: absolute;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    height: 100%;
-    right: 0;
-    top: 0;
-  }
-
-  .editableText-background {
-    width: 100%;
-    height: 100%;
-    z-index: -1;
-    position: absolute;
-    transition: 0.4s;
-    top: 0;
-    left: 0;
-  }
-
-  &.primary {
-
-    &:focus-within {
-      .editableText-background {
-        filter: opacity(0.2) !important;
-      }
+    position: relative;
+    width: fit-content;
+    border: solid 1px;
+    border-color: transparent;
+    border-radius: global.$standard-border-radius;
+    font-family: global.$default-font;
+    overflow: hidden;
+    padding: global.$padding;
+
+    .password {
+        position: absolute;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        height: 100%;
+        right: 0;
+        top: 0;
     }
 
     .editableText-background {
-      filter: opacity(0);
-
-      &.active {
-        filter: opacity(0.2) !important;
-      }
+        width: 100%;
+        height: 100%;
+        z-index: -1;
+        position: absolute;
+        transition: 0.4s;
+        top: 0;
+        left: 0;
     }
 
-    &:hover{
-      .editableText-background {
-        filter: opacity(0.2)
-      }
+    &.primary {
+        &:focus-within {
+            .editableText-background {
+                filter: opacity(0.2) !important;
+            }
+        }
+
+        .editableText-background {
+            filter: opacity(0);
+
+            &.active {
+                filter: opacity(0.2) !important;
+            }
+        }
+
+        &:hover {
+            .editableText-background {
+                filter: opacity(0.2);
+            }
+        }
     }
-  }
-
-  &.secondary {
-    &:focus-within {
-      .editableText-background {
-        filter: opacity(0.2) !important;
-      }
-    }
-
-    .editableText-background {
-      filter: opacity(0);
 
-      &.active {
-        filter: opacity(0.2) !important;
-      }
+    &.secondary {
+        &:focus-within {
+            .editableText-background {
+                filter: opacity(0.2) !important;
+            }
+        }
+
+        .editableText-background {
+            filter: opacity(0);
+
+            &.active {
+                filter: opacity(0.2) !important;
+            }
+        }
+
+        &:hover {
+            .editableText-background {
+                filter: opacity(0.2);
+            }
+        }
     }
 
-    &:hover{
-      .editableText-background {
-        filter: opacity(0.2)
-      }
-    }
-  }
+    &.tertiary {
+        &:hover {
+            box-shadow: global.$standard-shadow;
+        }
 
-  &.tertiary {
-    &:hover{
-      box-shadow: $standard-shadow;
+        &:hover {
+            .editableText-background {
+                filter: brightness(0.8);
+            }
+        }
     }
 
-    &:hover{
-      .editableText-background {
-        filter: brightness(0.8);
-      }
+    .editableText {
+        -webkit-appearance: none;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        font-size: inherit;
+        border: none;
+        outline: none;
+        margin: 0px !important;
+        padding: 0px !important;
+        box-shadow: none !important;
+        background: transparent;
+        color: inherit;
+        z-index: 1;
+
+        &.center {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
     }
-  }
 
-  .editableText {
-    -webkit-appearance: none;
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    font-size: inherit;
-    border: none;
-    outline: none;
-    margin: 0px !important;
-    padding: 0px !important;
-    box-shadow: none !important;
-    background: transparent;
-    color: inherit;
-    z-index: 1;
-
-    &.center {
-      display: flex;
-      justify-content: center;
-      align-items: center;
+    .displayText {
+        cursor: text !important;
+        width: 100%;
+        height: 100%;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        display: flex;
+        align-items: center;
+        font-size: inherit;
+        color: inherit;
+        z-index: 1;
+
+        &.center {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
     }
-  }
-  
-  .displayText {
-    cursor: text !important;
-    width: 100%;
-    height: 100%;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    display: flex;
-    align-items: center;
-    font-size: inherit;
-    color: inherit;
-    z-index: 1;
-
-    &.center {
-      display: flex;
-      justify-content: center;
-      align-items: center;
-    }
-  }
 }
-
diff --git a/packages/components/src/components/FormInput/FormInput.scss b/packages/components/src/components/FormInput/FormInput.scss
index db04ff8cf..2554cbd01 100644
--- a/packages/components/src/components/FormInput/FormInput.scss
+++ b/packages/components/src/components/FormInput/FormInput.scss
@@ -1,69 +1,69 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .formInput-container {
-  display: flex;
-  flex-direction: column;
-  width: 100%;
-  height: fit-content;
-  position: relative;
-  margin-top: 20px;
-
-  .formInput {
-    font-family: inherit;
+    display: flex;
+    flex-direction: column;
     width: 100%;
-    border: 0;
-    border-bottom: 2px solid black;
-    outline: 0;
-    font-size: 1rem;
-    color: black;
-    padding: 7px 0;
-    background: transparent;
-    transition: border-color 0.2s;
-
-    &::placeholder {
-      color: transparent;
-    }
+    height: fit-content;
+    position: relative;
+    margin-top: 20px;
 
-    &:focus {
-      ~ .formInput-label {
-        position: absolute;
-        transform: translate(0px, -13px);
-        display: block;
-        transition: 0.2s;
+    .formInput {
+        font-family: inherit;
+        width: 100%;
+        border: 0;
+        border-bottom: 2px solid black;
+        outline: 0;
         font-size: 1rem;
-        font-weight: 700;
-      }
-      padding-bottom: 6px;
-      font-weight: 700;
-      border-width: 3px;
-      border-image: linear-gradient(to right, black, white);
-      border-image-slice: 1;
+        color: black;
+        padding: 7px 0;
+        background: transparent;
+        transition: border-color 0.2s;
+
+        &::placeholder {
+            color: transparent;
+        }
+
+        &:focus {
+            ~ .formInput-label {
+                position: absolute;
+                transform: translate(0px, -13px);
+                display: block;
+                transition: 0.2s;
+                font-size: 1rem;
+                font-weight: 700;
+            }
+            padding-bottom: 6px;
+            font-weight: 700;
+            border-width: 3px;
+            border-image: linear-gradient(to right, black, white);
+            border-image-slice: 1;
+        }
+
+        &:valid {
+            ~ .formInput-label {
+                position: absolute;
+                transform: translate(0px, -13px);
+                display: block;
+                transition: 0.2s;
+                font-size: 1rem;
+            }
+        }
+
+        &:required,
+        &:invalid {
+            box-shadow: none;
+        }
     }
 
-    &:valid {
-      ~ .formInput-label {
+    .formInput-label {
         position: absolute;
-        transform: translate(0px, -13px);
+        top: 0;
+        transform: translate(0px, 8px);
         display: block;
         transition: 0.2s;
         font-size: 1rem;
-      }
+        color: gray;
+        pointer-events: none;
     }
-
-    &:required,
-    &:invalid {
-      box-shadow: none;
-    }
-  }
-
-  .formInput-label {
-    position: absolute;
-    top: 0;
-    transform: translate(0px, 8px);
-    display: block;
-    transition: 0.2s;
-    font-size: 1rem;
-    color: gray;
-    pointer-events: none;
-  }
 }
diff --git a/packages/components/src/components/Group/Group.scss b/packages/components/src/components/Group/Group.scss
index 885472a5d..7cd3dfd9e 100644
--- a/packages/components/src/components/Group/Group.scss
+++ b/packages/components/src/components/Group/Group.scss
@@ -1,16 +1,15 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .group-wrapper {
-  overflow: hidden;
+    overflow: hidden;
 
-  .group-container {
-    width: fit-content;
-    display: flex;
-    flex-flow: row wrap;
-    height: fit-content;
-    flex-flow: row;
-    justify-content: flex-start;
-    align-items: center;
-  }
+    .group-container {
+        width: fit-content;
+        display: flex;
+        flex-flow: row wrap;
+        height: fit-content;
+        flex-flow: row;
+        justify-content: flex-start;
+        align-items: center;
+    }
 }
-
diff --git a/packages/components/src/components/IconButton/IconButton.scss b/packages/components/src/components/IconButton/IconButton.scss
index 9a0b53c0f..f899dc50f 100644
--- a/packages/components/src/components/IconButton/IconButton.scss
+++ b/packages/components/src/components/IconButton/IconButton.scss
@@ -1,121 +1,119 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .iconButton-container {
-  position: relative;
-  cursor: pointer;
-  overflow: hidden;
-  user-select: none;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-family: $default-font;
-  border-radius: $standard-border-radius;
-  white-space: nowrap;
-  transition: 0.4s;
-  border: solid 1px;
-  border-color: transparent;
-  pointer-events: all;
-
-
-  .iconButton-content {
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-    align-items: center;
-    height: 100%;
-    width: 100%;
-    z-index: 1;
-    font-family: Verdana, sans-serif;
-    font-weight: 500;
-  }
-
-  .icon {
-    z-index: 1;
+    position: relative;
+    cursor: pointer;
+    overflow: hidden;
+    user-select: none;
     display: flex;
     justify-content: center;
     align-items: center;
-  }
-
-  .background {
-    width: 100%;
-    height: 100%;
-    z-index: 0;
-    position: absolute;
+    font-family: global.$default-font;
+    border-radius: global.$standard-border-radius;
+    white-space: nowrap;
     transition: 0.4s;
-  }
+    border: solid 1px;
+    border-color: transparent;
+    pointer-events: all;
+
+    .iconButton-content {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        height: 100%;
+        width: 100%;
+        z-index: 1;
+        font-family: Verdana, sans-serif;
+        font-weight: 500;
+    }
 
-  &.inactive {
-    .background {
-      filter: opacity(0) !important;
+    .icon {
+        z-index: 1;
+        display: flex;
+        justify-content: center;
+        align-items: center;
     }
-  }
 
-  &.primary {
     .background {
-      filter: opacity(0);
-
-      &.active {
-        filter: opacity(0.2) !important;
-      }
+        width: 100%;
+        height: 100%;
+        z-index: 0;
+        position: absolute;
+        transition: 0.4s;
     }
 
-    &:hover{
-      .background {
-        filter: opacity(0.2)
-      }
+    &.inactive {
+        .background {
+            filter: opacity(0) !important;
+        }
     }
 
-  }
+    &.primary {
+        .background {
+            filter: opacity(0);
 
-  &.secondary {
-    .background {
-      filter: opacity(0);
+            &.active {
+                filter: opacity(0.2) !important;
+            }
+        }
 
-      &.active {
-        filter: opacity(0.2) !important;
-      }
+        &:hover {
+            .background {
+                filter: opacity(0.2);
+            }
+        }
     }
 
-    &:hover{
-      .background {
-        filter: opacity(0.2)
-      }
-    }
-  }
+    &.secondary {
+        .background {
+            filter: opacity(0);
+
+            &.active {
+                filter: opacity(0.2) !important;
+            }
+        }
 
-  &.tertiary {
-    &:hover{
-      box-shadow: $standard-button-shadow;
+        &:hover {
+            .background {
+                filter: opacity(0.2);
+            }
+        }
     }
 
-    &:hover{
-      .background {
-        filter: brightness(0.8);
-      }
+    &.tertiary {
+        &:hover {
+            box-shadow: global.$standard-button-shadow;
+        }
+
+        &:hover {
+            .background {
+                filter: brightness(0.8);
+            }
+        }
     }
-  }
 
-  .color {
-    position: relative;
-    width: 70%;
-    height: 15%;
-    z-index: 3;
-    margin-top: 2px;
-    border-radius: 10px;
-    outline: solid 0.3px;
-    outline-offset: -0.3px;
-  }
+    .color {
+        position: relative;
+        width: 70%;
+        height: 15%;
+        z-index: 3;
+        margin-top: 2px;
+        border-radius: 10px;
+        outline: solid 0.3px;
+        outline-offset: -0.3px;
+    }
 
-  .iconButton-label {
-    position: relative;
-    z-index: 2;
-    max-width: 100%;
-    overflow: hidden;
-    white-space: normal;
-    display: flex;
-    text-align: center;
-    justify-content: center;
-    align-items: center;
-    font-size: $xsmall-fontSize;
-  }
+    .iconButton-label {
+        position: relative;
+        z-index: 2;
+        max-width: 100%;
+        overflow: hidden;
+        white-space: normal;
+        display: flex;
+        text-align: center;
+        justify-content: center;
+        align-items: center;
+        font-size: global.$xsmall-fontSize;
+    }
 }
diff --git a/packages/components/src/components/ListBox/ListBox.scss b/packages/components/src/components/ListBox/ListBox.scss
index dc449c943..dc2a44513 100644
--- a/packages/components/src/components/ListBox/ListBox.scss
+++ b/packages/components/src/components/ListBox/ListBox.scss
@@ -1,4 +1,4 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .listBox-container {
     position: relative;
@@ -13,4 +13,4 @@
     max-width: 300px;
     padding: 5px;
     gap: 2px;
-}
\ No newline at end of file
+}
diff --git a/packages/components/src/components/ListItem/ListItem.scss b/packages/components/src/components/ListItem/ListItem.scss
index 736078360..8d46605d0 100644
--- a/packages/components/src/components/ListItem/ListItem.scss
+++ b/packages/components/src/components/ListItem/ListItem.scss
@@ -1,78 +1,78 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .listItem-container {
-  position: relative;
-  width: 100%;
-  border-radius: $standard-border-radius;
-  display: flex;
-  justify-content: center;
-  align-items: flex-start;
-  flex-direction: column;
-  cursor: pointer;
-  font-family: Verdana, sans-serif;
-  overflow: hidden;
-  text-align: left;
-
-  .listItem-background {
-    position: absolute;
+    position: relative;
     width: 100%;
-    height: 100%;
-    background: $medium-blue;
-    filter: opacity(0);
-    transition: 0.4s;
-  }
-
-  .listItem-top {
+    border-radius: global.$standard-border-radius;
     display: flex;
-    height: 30px;
-    width: 100%;
-    justify-content: space-between;
-    align-items: center;
-    gap: 20px;
+    justify-content: center;
+    align-items: flex-start;
+    flex-direction: column;
+    cursor: pointer;
+    font-family: Verdana, sans-serif;
+    overflow: hidden;
+    text-align: left;
 
-    .content {
-      display: flex;
-      justify-content: flex-start;
-      align-items: center;
-      padding: $padding;
-      width: 100%;
-      height: 100%;
-      z-index: 1;
-      gap: 5px;
-      font-weight: 500;
-
-      .text {
-        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis;
+    .listItem-background {
+        position: absolute;
         width: 100%;
-      }
+        height: 100%;
+        background: global.$medium-blue;
+        filter: opacity(0);
+        transition: 0.4s;
     }
 
-    .shortcut {
-      grid-area: end;
-      padding: $padding;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      font-size: $xsmall-fontSize;
-      font-family: $default-font;
-    }
+    .listItem-top {
+        display: flex;
+        height: 30px;
+        width: 100%;
+        justify-content: space-between;
+        align-items: center;
+        gap: 20px;
 
-    .caret {
-      grid-area: end;
-      display: flex;
-      justify-content: flex-end;
-      align-items: center;
-      justify-self: center;
+        .content {
+            display: flex;
+            justify-content: flex-start;
+            align-items: center;
+            padding: global.$padding;
+            width: 100%;
+            height: 100%;
+            z-index: 1;
+            gap: 5px;
+            font-weight: 500;
+
+            .text {
+                white-space: nowrap;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                width: 100%;
+            }
+        }
+
+        .shortcut {
+            grid-area: end;
+            padding: global.$padding;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            font-size: global.$xsmall-fontSize;
+            font-family: global.$default-font;
+        }
+
+        .caret {
+            grid-area: end;
+            display: flex;
+            justify-content: flex-end;
+            align-items: center;
+            justify-self: center;
+        }
     }
-  }
 
-  .listItem-description {
-    font-size: $small-fontSize;
-    display: flex;
-    padding: 0px 5px 10px 5px;
-    justify-content: flex-start;
-    width: calc(100% - 10px);
-  }
+    .listItem-description {
+        font-size: global.$small-fontSize;
+        display: flex;
+        padding: 0px 5px 10px 5px;
+        justify-content: flex-start;
+        width: calc(100% - 10px);
+    }
 }
diff --git a/packages/components/src/components/Modal/Modal.scss b/packages/components/src/components/Modal/Modal.scss
index c0667ed26..a5698432d 100644
--- a/packages/components/src/components/Modal/Modal.scss
+++ b/packages/components/src/components/Modal/Modal.scss
@@ -1,46 +1,46 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .modal-container {
-  top: 0px;
-  left: 0px;
-  width: 100vw;
-  height: 100vh;
-  position: fixed;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  z-index: 100;
-
-  .modal-popup {
-    position: relative;
+    top: 0px;
+    left: 0px;
+    width: 100vw;
+    height: 100vh;
+    position: fixed;
     display: flex;
-    flex-direction: column;
-    align-items: left;
-    z-index: 10;
-    width: 400px;
-    height: fit-content;
-    padding: 20px;
-    border-radius: $standard-border-radius;
-    font-weight: bold;
-    font-size: 1.5rem;
+    justify-content: center;
+    align-items: center;
+    z-index: 100;
+
+    .modal-popup {
+        position: relative;
+        display: flex;
+        flex-direction: column;
+        align-items: left;
+        z-index: 10;
+        width: 400px;
+        height: fit-content;
+        padding: 20px;
+        border-radius: global.$standard-border-radius;
+        font-weight: bold;
+        font-size: 1.5rem;
 
-    .modal-closeButton {
-      top: -15px;
-      right: -15px;
-      position: absolute;
-      width: fit-content;
-      height: fit-content;
+        .modal-closeButton {
+            top: -15px;
+            right: -15px;
+            position: absolute;
+            width: fit-content;
+            height: fit-content;
+        }
     }
-  }
 }
 
 .modal-background {
-  z-index: 9;
-  position: absolute;
-  top: -10vh;
-  left: -10vw;
-  backdrop-filter: blur(15px);
-  width: 200vw;
-  height: 200vh;
-  background: $modal-background;
+    z-index: 9;
+    position: absolute;
+    top: -10vh;
+    left: -10vw;
+    backdrop-filter: blur(15px);
+    width: 200vw;
+    height: 200vh;
+    background: global.$modal-background;
 }
diff --git a/packages/components/src/components/MultiToggle/MultiToggle.scss b/packages/components/src/components/MultiToggle/MultiToggle.scss
index 2522549e9..854abdca0 100644
--- a/packages/components/src/components/MultiToggle/MultiToggle.scss
+++ b/packages/components/src/components/MultiToggle/MultiToggle.scss
@@ -1,5 +1 @@
-@import '../../global/globalCssVariables.scss';
-
-.multiToggle-container {
-    
-}
\ No newline at end of file
+@use '../../global/globalCssVariables.scss' as global;
diff --git a/packages/components/src/components/NumberDropdown/NumberDropdown.scss b/packages/components/src/components/NumberDropdown/NumberDropdown.scss
index 4ed5855d9..b3111623b 100644
--- a/packages/components/src/components/NumberDropdown/NumberDropdown.scss
+++ b/packages/components/src/components/NumberDropdown/NumberDropdown.scss
@@ -1,7 +1,4 @@
-@import '../../global/globalCssVariables.scss';
-
-.numberDropdown-container {
-}
+@use '../../global/globalCssVariables.scss' as global;
 
 .form-wrapper {
     .iconButton-label {
@@ -14,6 +11,6 @@
         text-align: center;
         justify-content: center;
         align-items: center;
-        font-size: $xsmall-fontSize !important;
+        font-size: global.$xsmall-fontSize !important;
     }
 }
diff --git a/packages/components/src/components/NumberInput/NumberInput.scss b/packages/components/src/components/NumberInput/NumberInput.scss
index 2a562d395..887d64857 100644
--- a/packages/components/src/components/NumberInput/NumberInput.scss
+++ b/packages/components/src/components/NumberInput/NumberInput.scss
@@ -1,5 +1,5 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .numberInput-container {
     width: 100%;
-}
\ No newline at end of file
+}
diff --git a/packages/components/src/components/Popup/Popup.scss b/packages/components/src/components/Popup/Popup.scss
index 39dd2c947..3087293f1 100644
--- a/packages/components/src/components/Popup/Popup.scss
+++ b/packages/components/src/components/Popup/Popup.scss
@@ -1,30 +1,30 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .popup-wrapper {
-  width: fit-content;
-
-  &.fillWidth {
-    width: 100%;
-  }
-  
-  .trigger-container {
     width: fit-content;
-    height: fit-content;
 
     &.fillWidth {
-      width: 100%;
+        width: 100%;
+    }
+
+    .trigger-container {
+        width: fit-content;
+        height: fit-content;
+
+        &.fillWidth {
+            width: 100%;
+        }
     }
-  }
 }
 
 .popup-container {
-  display: flex;
-  height: fit-content;
-  min-width: fit-content;
-  width: fit-content;
-  position: relative;
-  border: solid 1px $black;
-  border-radius: $standard-border-radius;
-  overflow: hidden;
-  background: $white;
+    display: flex;
+    height: fit-content;
+    min-width: fit-content;
+    width: fit-content;
+    position: relative;
+    border: solid 1px global.$black;
+    border-radius: global.$standard-border-radius;
+    overflow: hidden;
+    background: global.$white;
 }
diff --git a/packages/components/src/components/Slider/Slider.scss b/packages/components/src/components/Slider/Slider.scss
index 9a9fc6172..a653f024f 100644
--- a/packages/components/src/components/Slider/Slider.scss
+++ b/packages/components/src/components/Slider/Slider.scss
@@ -1,168 +1,163 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .slider-container {
-  display: flex;
-  position: relative;
-  justify-content: center;
-  align-items: center;
-  min-width: 200px;
-  width: 100%;
-  height: 100%;
-  font-family: $default-font;
-
-  .selected-range {
-    width: 100%;
-    background: $medium-blue;
-  }
-
-  .range {
-    position: absolute;
-    background: $light-gray;
-  }
-
-  .box-minmax{
-    width: 100%;
     display: flex;
-    justify-content: space-between;
-    font-size: 20px;
-    color: $medium-blue;
-    position: absolute;
-    top: 110%;
-  }
-  .range-slider {
-    margin: 0px;
-    position: absolute;
+    position: relative;
+    justify-content: center;
+    align-items: center;
+    min-width: 200px;
     width: 100%;
     height: 100%;
-    top: 0px;
-    left: 0px;
-
-    .rs-label-container {
-      display: flex;
-      position: absolute;
-      justify-content: center;
-      align-items: center;
-      overflow: visible;
-      border-radius: $standard-border-radius;
-      z-index: 45;
-      pointer-events: none;
-
-      .rs-label {
-        display: flex;
-        font-size: smaller;
-        white-space: nowrap;
-        border-radius: 100%;
-        text-align: center;
-        text-wrap: wrap;
-        word-break: break-all;
-        justify-content: center;
-        align-items: center;
-        font-family: $default-font;
-        user-select: none;
-        pointer-events: none;
-        top: 0px;
-        width: fit-content;
-        border-radius: $standard-border-radius;
-        z-index: 40;
-      }
-
-    }
-  
-    .rs-range {
-      width: 100%;
-      position: relative;
-      background: transparent;
-      pointer-events: none;
-      -webkit-appearance: none;
-      margin: 0px;
-      z-index: 20;
-  
-      &:focus {
-        outline: none;
-      }
-  
-      &::-webkit-slider-runnable-track {
-          width: 100%;
-          background: none;
-          cursor: pointer;
-          box-shadow: none;
-          -webkit-appearance: none;
-          pointer-events: none;
-      }
-      &::-moz-range-track {
-          width: 100%;
-          cursor: pointer;
-          box-shadow: none;
-          -webkit-appearance: none;
-          pointer-events: none;
-      }
-    
-    &::-webkit-slider-thumb {
-          cursor: ew-resize;
-          -webkit-appearance: none;
-          pointer-events: auto;
-    }
-    &::-moz-range-thumb {
-          cursor: pointer;
-          -webkit-appearance: none;
-          pointer-events: auto;
-    }
-
-    &::-moz-focus-outer {
-      border: 0;
-    }
-
-    &.xsmall {
-    &::-webkit-slider-runnable-track {
-        height: $xsmall;
-      }
+    font-family: global.$default-font;
 
-      &::-webkit-slider-thumb {
-        height: $xsmall;
-        width: $xsmall;
-        border-radius: $xsmall;
-      }
+    .selected-range {
+        width: 100%;
+        background: global.$medium-blue;
     }
 
-    &.small {
-      &::-webkit-slider-runnable-track {
-        height: $small;
-      }
-
-      &::-webkit-slider-thumb {
-        height: $small;
-        width: $small;
-        border-radius: $small;
-      }
+    .range {
+        position: absolute;
+        background: global.$light-gray;
     }
 
-    &.medium {
-      &::-webkit-slider-runnable-track {
-        height: $medium;
-      }
-
-      &::-webkit-slider-thumb {
-        height: $medium;
-        width: $medium;
-        border-radius: $medium;
-      }
+    .box-minmax {
+        width: 100%;
+        display: flex;
+        justify-content: space-between;
+        font-size: 20px;
+        color: global.$medium-blue;
+        position: absolute;
+        top: 110%;
     }
-
-    &.large {
-      &::-webkit-slider-runnable-track {
-        height: $large;
-      }
-
-      &::-webkit-slider-thumb {
-        height: $large;
-        width: $large;
-        border-radius: $large;
-      }
+    .range-slider {
+        margin: 0px;
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        top: 0px;
+        left: 0px;
+
+        .rs-label-container {
+            display: flex;
+            position: absolute;
+            justify-content: center;
+            align-items: center;
+            overflow: visible;
+            border-radius: global.$standard-border-radius;
+            z-index: 45;
+            pointer-events: none;
+
+            .rs-label {
+                display: flex;
+                font-size: smaller;
+                white-space: nowrap;
+                border-radius: 100%;
+                text-align: center;
+                text-wrap: wrap;
+                word-break: break-all;
+                justify-content: center;
+                align-items: center;
+                font-family: global.$default-font;
+                user-select: none;
+                pointer-events: none;
+                top: 0px;
+                width: fit-content;
+                border-radius: global.$standard-border-radius;
+                z-index: 40;
+            }
+        }
+
+        .rs-range {
+            width: 100%;
+            position: relative;
+            background: transparent;
+            pointer-events: none;
+            -webkit-appearance: none;
+            margin: 0px;
+            z-index: 20;
+
+            &:focus {
+                outline: none;
+            }
+
+            &::-webkit-slider-runnable-track {
+                width: 100%;
+                background: none;
+                cursor: pointer;
+                box-shadow: none;
+                -webkit-appearance: none;
+                pointer-events: none;
+            }
+            &::-moz-range-track {
+                width: 100%;
+                cursor: pointer;
+                box-shadow: none;
+                -webkit-appearance: none;
+                pointer-events: none;
+            }
+
+            &::-webkit-slider-thumb {
+                cursor: ew-resize;
+                -webkit-appearance: none;
+                pointer-events: auto;
+            }
+            &::-moz-range-thumb {
+                cursor: pointer;
+                -webkit-appearance: none;
+                pointer-events: auto;
+            }
+
+            &::-moz-focus-outer {
+                border: 0;
+            }
+
+            &.xsmall {
+                &::-webkit-slider-runnable-track {
+                    height: global.$xsmall;
+                }
+
+                &::-webkit-slider-thumb {
+                    height: global.$xsmall;
+                    width: global.$xsmall;
+                    border-radius: global.$xsmall;
+                }
+            }
+
+            &.small {
+                &::-webkit-slider-runnable-track {
+                    height: global.$small;
+                }
+
+                &::-webkit-slider-thumb {
+                    height: global.$small;
+                    width: global.$small;
+                    border-radius: global.$small;
+                }
+            }
+
+            &.medium {
+                &::-webkit-slider-runnable-track {
+                    height: global.$medium;
+                }
+
+                &::-webkit-slider-thumb {
+                    height: global.$medium;
+                    width: global.$medium;
+                    border-radius: global.$medium;
+                }
+            }
+
+            &.large {
+                &::-webkit-slider-runnable-track {
+                    height: global.$large;
+                }
+
+                &::-webkit-slider-thumb {
+                    height: global.$large;
+                    width: global.$large;
+                    border-radius: global.$large;
+                }
+            }
+        }
     }
-  }
-  }
-  
 }
-
-
-
diff --git a/packages/components/src/components/Slider/Slider.tsx b/packages/components/src/components/Slider/Slider.tsx
index 3ca51efed..f6f53799c 100644
--- a/packages/components/src/components/Slider/Slider.tsx
+++ b/packages/components/src/components/Slider/Slider.tsx
@@ -1,178 +1,188 @@
-import React, { useEffect, useRef, useState } from 'react'
-import { Colors, getFontSize, getHeight, IGlobalProps, Size , getFormLabelSize, isDark, INumberProps } from '../../global'
-import './Slider.scss'
+import React, { useState } from 'react';
+import { Colors, getFontSize, getHeight, Size, getFormLabelSize, isDark, INumberProps } from '../../global';
+import './Slider.scss';
 
 export interface ISliderProps extends INumberProps {
-  multithumb: boolean
-  autorangeMinVal?: number // minimimum value that min can have when autoranging
-  autorangeMinSize?: number // minimum difference between min and max when autoranging
-  autorange?: number        // automatically adjust min/max to be +/- autorange/2 around the current value when the thumb is 15% from the min/max, or when the multithumbs are within 20% of the range and the range is bigger than autorange
-  endNumber?: number
-  setEndNumber?: (newVal: number) => void
-  setFinalNumber?: (newVal: number) => void
-  setFinalEndNumber?: (newVal: number) => void
-  decimals?: number; 
-  step?: number
-  minDiff?: number
+    multithumb: boolean;
+    autorangeMinVal?: number; // minimimum value that min can have when autoranging
+    autorangeMinSize?: number; // minimum difference between min and max when autoranging
+    autorange?: number; // automatically adjust min/max to be +/- autorange/2 around the current value when the thumb is 15% from the min/max, or when the multithumbs are within 20% of the range and the range is bigger than autorange
+    endNumber?: number;
+    setEndNumber?: (newVal: number) => void;
+    setFinalNumber?: (newVal: number) => void;
+    setFinalEndNumber?: (newVal: number) => void;
+    decimals?: number;
+    step?: number;
+    minDiff?: number;
 }
 
-let lastVal = 0;  // bcz: WHY do I have to do this??  the pointerdown event locks in the value of 'valLoc' when it's created so need some other way to get the current value to that old handler...
+let lastVal = 0; // bcz: WHY do I have to do this??  the pointerdown event locks in the value of 'valLoc' when it's created so need some other way to get the current value to that old handler...
 let lastEndVal = 0;
 
 export const Slider = (props: ISliderProps) => {
-  const [width, setWidth] = useState<number>(100);
-  const [valLoc, setNumberLoc] = useState<number>(props.number??(props.min + (props.max-props.min)/2));
-  const [endNumberLoc, setEndNumberLoc] = useState<number>(props.endNumber??(props.min + 2*(props.max-props.min)/3));
-  const [min, setMin] = useState<number>(props.min);
-  const [max, setMax] = useState<number>(props.max);
-  const { 
-    formLabel, 
-    formLabelPlacement, 
-    multithumb, 
-    autorange,
-    autorangeMinVal,
-    autorangeMinSize,
-    decimals,
-    step = 1, 
-    number = valLoc, 
-    endNumber = endNumberLoc, 
-    minDiff = (max-min)/20, 
-    size = Size.SMALL, 
-    height, 
-    unit, 
-    onPointerDown, 
-    setNumber, 
-    setEndNumber,
-    setFinalNumber, 
-    setFinalEndNumber,
-    color = Colors.MEDIUM_BLUE,
-    fillWidth
-  } = props
+    const [width, setWidth] = useState<number>(100);
+    const [valLoc, setNumberLoc] = useState<number>(props.number ?? props.min + (props.max - props.min) / 2);
+    const [endNumberLoc, setEndNumberLoc] = useState<number>(props.endNumber ?? props.min + (2 * (props.max - props.min)) / 3);
+    const [min, setMin] = useState<number>(props.min);
+    const [max, setMax] = useState<number>(props.max);
+    const {
+        formLabel,
+        formLabelPlacement,
+        multithumb,
+        autorange,
+        autorangeMinVal,
+        autorangeMinSize,
+        decimals,
+        step = 1,
+        number = valLoc,
+        endNumber = endNumberLoc,
+        minDiff = (max - min) / 20,
+        size = Size.SMALL,
+        height,
+        unit,
+        onPointerDown,
+        setNumber,
+        setEndNumber,
+        setFinalNumber,
+        setFinalEndNumber,
+        color = Colors.MEDIUM_BLUE,
+        fillWidth,
+    } = props;
 
-  const toDecimal = (num:number) => decimals !== undefined ? Math.round(num*Math.pow(10,decimals))/Math.pow(10,decimals): num;
+    const toDecimal = (num: number) => (decimals !== undefined ? Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals) : num);
 
-  const getLeftPos = (locVal: number) => {
-    const dragger = getHeight(height,size)
-    return (((locVal-min)/ (max-min)) * (width-dragger))
-  }
+    const getLeftPos = (locVal: number) => {
+        const dragger = getHeight(+(height || 0), size);
+        return ((locVal - min) / (max - min)) * (width - dragger);
+    };
 
-  const getValueLabel = (locVal: number): JSX.Element => {
-    return (<div className="rs-label-container" 
-            style={{
-              left: `${getLeftPos(locVal)}px`,
-              background: color,
-              color: isDark(color) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY,
-              fontSize: getFontSize(size),
-              height: getHeight(height, size),
-              width: getHeight(height, size),
-              top: 0
-            }}
-            >
-            <span className="rs-label">
-              {toDecimal(locVal)}
-            </span>
-          </div>)
-  }
-  const checkAutorange = () => {
-    if (autorange) {
-      const minval = multithumb ? Math.min(lastVal, lastEndVal) : lastVal;
-      const maxval = multithumb ? Math.max(lastVal, lastEndVal) : lastVal;
-      const autosize = Math.max(autorangeMinSize??0,(autorange ?? (maxval-minval)))/2;
-      if ((Math.abs((minval - min)/(max-min)) < .15) || (Math.abs((max - maxval)/(max-min)) < .15) ||
-          (multithumb && maxval - minval < (max-min)/5 && autosize < max-min)
-       ) {
-        const newminval = autorangeMinVal !== undefined && minval-autosize < autorangeMinVal? autorangeMinVal : minval-autosize;
-        setMin(newminval)
-        setMax(newminval !== minval ? Math.max(maxval + autosize, newminval +autosize): maxval+autosize )
-      }
-    }
-  }
+    const getValueLabel = (locVal: number): JSX.Element => {
+        return (
+            <div
+                className="rs-label-container"
+                style={{
+                    left: `${getLeftPos(locVal)}px`,
+                    background: color,
+                    color: isDark(color) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY,
+                    fontSize: getFontSize(size),
+                    height: getHeight(+(height || 0), size),
+                    width: getHeight(+(height || 0), size),
+                    top: 0,
+                }}>
+                <span className="rs-label">{toDecimal(locVal)}</span>
+            </div>
+        );
+    };
+    const checkAutorange = () => {
+        if (autorange) {
+            const minval = multithumb ? Math.min(lastVal, lastEndVal) : lastVal;
+            const maxval = multithumb ? Math.max(lastVal, lastEndVal) : lastVal;
+            const autosize = Math.max(autorangeMinSize ?? 0, autorange ?? maxval - minval) / 2;
+            if (Math.abs((minval - min) / (max - min)) < 0.15 || Math.abs((max - maxval) / (max - min)) < 0.15 || (multithumb && maxval - minval < (max - min) / 5 && autosize < max - min)) {
+                const newminval = autorangeMinVal !== undefined && minval - autosize < autorangeMinVal ? autorangeMinVal : minval - autosize;
+                setMin(newminval);
+                setMax(newminval !== minval ? Math.max(maxval + autosize, newminval + autosize) : maxval + autosize);
+            }
+        }
+    };
 
-  const valSlider = (which: string, val:number, onchange: (val:number) => void, setFinal: () => void) => {
-    const valPointerup = (e:PointerEvent) => {
-        document.removeEventListener('pointerup', valPointerup, true)
-        setFinal();
-        checkAutorange();
-    }
-    return (<div key={which} className={`range-slider ${size}`}>
-          {getValueLabel(val)}
-          <input
-            className={`rs-range ${size}`}
-            type="range"
-            color={color}
-            min={min}
-            max={max}
-            step={step}
-            value={val}
-            onPointerDown={e => document.addEventListener('pointerup', valPointerup, true)}
-            onChange={e => {
-              onchange(+e.target.value);
-              e.stopPropagation();
-            }}
-          />
-      </div>);
-  }
-  const onchange = (val:number) => {
-      if (autorangeMinVal && val < autorangeMinVal) val = autorangeMinVal;
-      setNumber?.(lastVal = Math.min(multithumb ? endNumber - (minDiff??0):Number.MAX_VALUE, val))
-      setNumberLoc(lastVal = Math.min(multithumb ? endNumber - (minDiff??0):Number.MAX_VALUE, val))
-  }
-  const onendchange = (val:number) => {
-    setEndNumber?.(lastEndVal = Math.max(number + (minDiff??0), val))
-    setEndNumberLoc(lastEndVal = Math.max(number + (minDiff??0), val))
-  }
-  const Slider:(JSX.Element|null)[] = [
-      !multithumb ? (null) : valSlider("end", endNumberLoc,onendchange, () => setFinalEndNumber?.(lastEndVal)),
-      valSlider("start", valLoc, onchange, () => setFinalNumber?.(lastVal))
-  ];
+    const valSlider = (which: string, val: number, onchange: (val: number) => void, setFinal: () => void) => {
+        const valPointerup = () => {
+            document.removeEventListener('pointerup', valPointerup, true);
+            setFinal();
+            checkAutorange();
+        };
+        return (
+            <div key={which} className={`range-slider ${size}`}>
+                {getValueLabel(val)}
+                <input
+                    className={`rs-range ${size}`}
+                    type="range"
+                    color={color}
+                    min={min}
+                    max={max}
+                    step={step}
+                    value={val}
+                    onPointerDown={() => document.addEventListener('pointerup', valPointerup, true)}
+                    onChange={e => {
+                        onchange(+e.target.value);
+                        e.stopPropagation();
+                    }}
+                />
+            </div>
+        );
+    };
+    const onchange = (val: number) => {
+        // eslint-disable-next-line no-param-reassign
+        if (autorangeMinVal && val < autorangeMinVal) val = autorangeMinVal;
+        setNumber?.((lastVal = Math.min(multithumb ? endNumber - (minDiff ?? 0) : Number.MAX_VALUE, val)));
+        setNumberLoc((lastVal = Math.min(multithumb ? endNumber - (minDiff ?? 0) : Number.MAX_VALUE, val)));
+    };
+    const onendchange = (val: number) => {
+        setEndNumber?.((lastEndVal = Math.max(number + (minDiff ?? 0), val)));
+        setEndNumberLoc((lastEndVal = Math.max(number + (minDiff ?? 0), val)));
+    };
+    const Slider: (JSX.Element | null)[] = [!multithumb ? null : valSlider('end', endNumberLoc, onendchange, () => setFinalEndNumber?.(lastEndVal)), valSlider('start', valLoc, onchange, () => setFinalNumber?.(lastVal))];
 
-  const slider: JSX.Element = (
-    <div className={`slider-wrapper`} 
-      onPointerEnter={e => {
-        lastVal = valLoc;
-        lastEndVal = endNumberLoc;
-      }}
-      style={{
-        padding: `5px 0px ${getHeight(height, size)}px 0px`,
-        width: fillWidth ? '100%' : 'fit-content'
-      }}>
-      <div className="slider-container" 
-            ref={r => {
-              r && new ResizeObserver(() => setWidth(+(r?.clientWidth??100))).observe(r);
-              setWidth(+(r?.clientWidth??100));
-            }} 
-            style={{height: getHeight(height, size)}}
-            onPointerDown={onPointerDown}
-      >
-          {Slider}
-          <div className="selected-range" style={{
-            height: getHeight(height, size) / 10,
-            background: multithumb ? Colors.LIGHT_GRAY : color
-
-          }}/>
-          <div className="range" style={{
-            height: getHeight(height, size) / 10,
-            width: getLeftPos(endNumber) - getLeftPos(number),
-            left: getLeftPos(number) + getHeight(height, size),
-            display: multithumb ? undefined: 'none',
-            background: color,
-          }}/>
-          <div className="box-minmax" style={{ fontSize: getFontSize(size), color }}>
-          <span>{toDecimal(min)}{unit}</span>
-          <span>{toDecimal(max)}{unit}</span>
+    const slider: JSX.Element = (
+        <div
+            className={`slider-wrapper`}
+            onPointerEnter={() => {
+                lastVal = valLoc;
+                lastEndVal = endNumberLoc;
+            }}
+            style={{
+                padding: `5px 0px ${getHeight(+(height || 0), size)}px 0px`,
+                width: fillWidth ? '100%' : 'fit-content',
+            }}>
+            <div
+                className="slider-container"
+                ref={r => {
+                    r && new ResizeObserver(() => setWidth(+(r?.clientWidth ?? 100))).observe(r);
+                    setWidth(+(r?.clientWidth ?? 100));
+                }}
+                style={{ height: getHeight(+(height || 0), size) }}
+                onPointerDown={onPointerDown}>
+                {Slider}
+                <div
+                    className="selected-range"
+                    style={{
+                        height: getHeight(+(height || 0), size) / 10,
+                        background: multithumb ? Colors.LIGHT_GRAY : color,
+                    }}
+                />
+                <div
+                    className="range"
+                    style={{
+                        height: getHeight(+(height || 0), size) / 10,
+                        width: getLeftPos(endNumber) - getLeftPos(number),
+                        left: getLeftPos(number) + getHeight(+(height || 0), size),
+                        display: multithumb ? undefined : 'none',
+                        background: color,
+                    }}
+                />
+                <div className="box-minmax" style={{ fontSize: getFontSize(size), color }}>
+                    <span>
+                        {toDecimal(min)}
+                        {unit}
+                    </span>
+                    <span>
+                        {toDecimal(max)}
+                        {unit}
+                    </span>
+                </div>
+            </div>
         </div>
-      </div>
-    </div>
-  )
-
-  return (
-    formLabel ? 
-    <div  className={`form-wrapper ${formLabelPlacement}`}>
-        <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div>
-        {slider}
-    </div>
-    :
-    slider
-)
-}
+    );
 
+    return formLabel ? (
+        <div className={`form-wrapper ${formLabelPlacement}`}>
+            <div className={'formLabel'} style={{ fontSize: getFormLabelSize(size) }}>
+                {formLabel}
+            </div>
+            {slider}
+        </div>
+    ) : (
+        slider
+    );
+};
diff --git a/packages/components/src/components/Template/Template.scss b/packages/components/src/components/Template/Template.scss
index c91147200..854abdca0 100644
--- a/packages/components/src/components/Template/Template.scss
+++ b/packages/components/src/components/Template/Template.scss
@@ -1,5 +1 @@
-@import '../../global/globalCssVariables.scss';
-
-.template-container {
-    
-}
\ No newline at end of file
+@use '../../global/globalCssVariables.scss' as global;
diff --git a/packages/components/src/components/Toggle/Toggle.scss b/packages/components/src/components/Toggle/Toggle.scss
index b2faa8d99..d65cb8e23 100644
--- a/packages/components/src/components/Toggle/Toggle.scss
+++ b/packages/components/src/components/Toggle/Toggle.scss
@@ -1,77 +1,77 @@
-@import '../../global/globalCssVariables.scss';
+@use '../../global/globalCssVariables.scss' as global;
 
 .toggle-label {
-  position: relative;
-  bottom: 0;
-  width: 100%;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-size: $xsmall-fontSize;
+    position: relative;
+    bottom: 0;
+    width: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: global.$xsmall-fontSize;
 }
 
 .toggle-container {
-  position: relative;
-  width: fit-content;
-  cursor: pointer;
-  overflow: hidden;
-  user-select: none;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  gap: 5px;
-  font-family: $default-font;
-  font-size: $medium-fontSize;
-  border-radius: 100px;
-  white-space: nowrap;
-  transition: 0.4s ease;
-  border: solid 1px;
-  border-color: $medium-blue;
+    position: relative;
+    width: fit-content;
+    cursor: pointer;
+    overflow: hidden;
+    user-select: none;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    gap: 5px;
+    font-family: global.$default-font;
+    font-size: global.$medium-fontSize;
+    border-radius: 100px;
+    white-space: nowrap;
+    transition: 0.4s ease;
+    border: solid 1px;
+    border-color: global.$medium-blue;
 
-  &:hover {
-    .toggle-background {
-      filter: opacity(0.2);
+    &:hover {
+        .toggle-background {
+            filter: opacity(0.2);
+        }
     }
-  }
 
-  &.switch {
-    &:hover {
-      .toggle-background {
-        filter: opacity(0);
-      }
+    &.switch {
+        &:hover {
+            .toggle-background {
+                filter: opacity(0);
+            }
+        }
     }
-  }
 
-  .toggle-content {
-    position: absolute;
-    display: flex;
-    align-items: center;
-    width: 100%;
-    height: 100%;
-    z-index: 1;
-    text-transform: uppercase;
-    font-family: Verdana, sans-serif;
-    font-weight: 500;
-    transition: 0.4s;
+    .toggle-content {
+        position: absolute;
+        display: flex;
+        align-items: center;
+        width: 100%;
+        height: 100%;
+        z-index: 1;
+        text-transform: uppercase;
+        font-family: Verdana, sans-serif;
+        font-weight: 500;
+        transition: 0.4s;
 
-    .toggle-switch {
-      background: $medium-blue;
-      transition: 0.4s;
-      border-radius: 100px;
+        .toggle-switch {
+            background: global.$medium-blue;
+            transition: 0.4s;
+            border-radius: 100px;
+        }
     }
-  }
 
-  .toggle-background {
-    width: 100%;
-    height: 100%;
-    z-index: 0;
-    position: absolute;
-    background: $medium-blue;
-    transition: 0.4s ease;
-    filter: opacity(0);
+    .toggle-background {
+        width: 100%;
+        height: 100%;
+        z-index: 0;
+        position: absolute;
+        background: global.$medium-blue;
+        transition: 0.4s ease;
+        filter: opacity(0);
 
-    &.active {
-      filter: opacity(0.4) !important;
+        &.active {
+            filter: opacity(0.4) !important;
+        }
     }
-  }
 }
diff --git a/packages/components/src/global/globalCssVariables.scss b/packages/components/src/global/globalCssVariables.scss
index 1ac2ef45c..ebc44106d 100644
--- a/packages/components/src/global/globalCssVariables.scss
+++ b/packages/components/src/global/globalCssVariables.scss
@@ -75,8 +75,10 @@ $standard-border-radius: 5px;
 
 // shadow
 $standard-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
-$standard-button-shadow: rgb(0 0 0 / 20%) 0px 2px 4px -1px,
-  rgb(0 0 0 / 14%) 0px 4px 5px 0px, rgb(0 0 0 / 12%) 0px 1px 10px 0px;
+$standard-button-shadow:
+    rgb(0 0 0 / 20%) 0px 2px 4px -1px,
+    rgb(0 0 0 / 14%) 0px 4px 5px 0px,
+    rgb(0 0 0 / 12%) 0px 1px 10px 0px;
 
 $dashboardselector-height: 32px;
 $mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc
@@ -91,70 +93,68 @@ $LEFT_MENU_WIDTH: 60px;
 $TREE_BULLET_WIDTH: 20px;
 
 :export {
-  contextMenuZindex: $contextMenu-zindex;
-  SCHEMA_DIVIDER_WIDTH: $SCHEMA_DIVIDER_WIDTH;
-  COLLECTION_BORDER_WIDTH: $COLLECTION_BORDER_WIDTH;
-  MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE;
-  MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT;
-  SEARCH_THUMBNAIL_SIZE: $search-thumnail-size;
-  ANTIMODEMENU_HEIGHT: $antimodemenu-height;
-  DASHBOARD_SELECTOR_HEIGHT: $dashboardselector-height;
-  DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM;
-  LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH;
-  TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH;
+    contextMenuZindex: $contextMenu-zindex;
+    SCHEMA_DIVIDER_WIDTH: $SCHEMA_DIVIDER_WIDTH;
+    COLLECTION_BORDER_WIDTH: $COLLECTION_BORDER_WIDTH;
+    MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE;
+    MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT;
+    SEARCH_THUMBNAIL_SIZE: $search-thumnail-size;
+    ANTIMODEMENU_HEIGHT: $antimodemenu-height;
+    DASHBOARD_SELECTOR_HEIGHT: $dashboardselector-height;
+    DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM;
+    LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH;
+    TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH;
 }
 
 .form-wrapper {
-  display: flex;
-  flex-direction: column;
-  justify-content: flex-start;
-  align-items: flex-start;
-  gap: 0px;
-  padding-bottom: 5px;
-
-
-  .formLabel {
     display: flex;
-    font-family: $default-font;
-    text-transform: uppercase;
-    opacity: 0.8;
-    min-width: 'fit-content'
-  }
-
-  &.left {
-    flex-direction: row;
-    align-items: center;
-    gap: 3px;
+    flex-direction: column;
+    justify-content: flex-start;
+    align-items: flex-start;
+    gap: 0px;
+    padding-bottom: 5px;
 
     .formLabel {
-      text-align: left;
+        display: flex;
+        font-family: $default-font;
+        text-transform: uppercase;
+        opacity: 0.8;
+        min-width: 'fit-content';
     }
-  }
 
-  &.right {
-    flex-direction: row-reverse;
-    justify-content: flex-end;
-    align-items: center;
-    gap: 3px;
+    &.left {
+        flex-direction: row;
+        align-items: center;
+        gap: 3px;
 
-    .formLabel {
-      text-align: right;
+        .formLabel {
+            text-align: left;
+        }
     }
-  }
 
-  &.top {
-    flex-direction: column;
-    gap: 1px;
-  }
+    &.right {
+        flex-direction: row-reverse;
+        justify-content: flex-end;
+        align-items: center;
+        gap: 3px;
 
-  &.top-start {
-    flex-direction: column;
-    align-items: flex-start;
-  }
+        .formLabel {
+            text-align: right;
+        }
+    }
 
-  &.top-end {
-    flex-direction: column;
-    align-items: flex-end;
-  }
-}
+    &.top {
+        flex-direction: column;
+        gap: 1px;
+    }
 
+    &.top-start {
+        flex-direction: column;
+        align-items: flex-start;
+    }
+
+    &.top-end {
+        flex-direction: column;
+        align-items: flex-end;
+    }
+}
diff --git a/src/client/util/CaptureManager.scss b/src/client/util/CaptureManager.scss
index 8679a0101..e7cbb4287 100644
--- a/src/client/util/CaptureManager.scss
+++ b/src/client/util/CaptureManager.scss
@@ -1,5 +1,3 @@
-@import '../views/global/globalCssVariables.module';
-
 .capture-interface {
     //background-color: whitesmoke !important;
     width: 450px;
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index d2d7cabd2..60acd5b21 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -445,7 +445,6 @@ pie title Minerals in my tap water
             { toolTip: "Tap or drag to create a view slide",      title: "View Slide", icon: "address-card",       dragFactory: doc.emptyViewSlide as Doc,   clickFactory: DocCast(doc.emptyViewSlide), openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data note",       title: "DataNote", icon: "window-maximize",      dragFactory: doc.emptyHeader as Doc,      clickFactory: DocCast(doc.emptyHeader),    openFactoryAsDelegate: true,           funcs: { hidden: "IsNoviceMode()"} },
             { toolTip: "Toggle a Calculator REPL",                title: "replviewer",  icon: "calculator",                                      clickFactory: '<ScriptingRepl />' as unknown as Doc, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
-           // { toolTip: "Toggle an UndoStack",                     title: "undostacker", icon: "calculator",                                      clickFactory: "<UndoStack />" as any,        openFactoryLocation: OpenWhere.overlay},
           ].map(tuple => (
               { openFactoryLocation: OpenWhere.addRight, 
                 scripts: { onClick:     'openDoc(copyDragFactory(this.clickFactory,this.openFactoryAsDelegate), this.openFactoryLocation)', 
@@ -674,12 +673,13 @@ pie title Minerals in my tap water
                             CurrentUserUtils.createToolButton(opts), scripts, funcs);
         
         const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
-            { scripts: { onClick: "undo()"}, opts: { title: "Undo",              icon: "undo-alt",               toolTip: "Undo ⌘Z" }},
-            { scripts: { onClick: "redo()"}, opts: { title: "Redo",              icon: "redo-alt",               toolTip: "Redo ⌘⇧Z" }},
-            { scripts: { },                  opts: { title: "undoStack",         layout: "<UndoStack>",          toolTip: "Undo/Redo Stack"}}, // note: layout fields are hacks -- they don't actually run through the JSX parser (yet)
-            { scripts: { },                  opts: { title: "linker",            layout: "<LinkingUI>",          toolTip: "link started"}},
-            { scripts: { },                  opts: { title: "currently playing", layout: "<CurrentlyPlayingUI>", toolTip: "currently playing media"}},
-            { scripts: { },                  opts: { title: "Branching",         layout: "<Branching>",          toolTip: "Branch, baby!"}}
+            { scripts: { onClick: "undo()"},  opts: { title: "Undo",              icon: "undo-alt",               toolTip: "Undo ⌘Z" }},
+            { scripts: { onClick: "redo()"},  opts: { title: "Redo",              icon: "redo-alt",               toolTip: "Redo ⌘⇧Z" }},
+            { scripts: { },                   opts: { title: "undoStack",         layout: "<UndoStack>",          toolTip: "Undo/Redo Stack"}}, // note: layout fields are hacks -- they don't actually run through the JSX parser (yet)
+            { scripts: { },                   opts: { title: "linker",            layout: "<LinkingUI>",          toolTip: "link started"}},
+            { scripts: { },                   opts: { title: "currently playing", layout: "<CurrentlyPlayingUI>", toolTip: "currently playing media"}},
+            { scripts: { onClick: "hideUI()"},opts: { title: "Toggle UI",         icon: "portrait",               toolTip: "Toggle visibility of UI buttons"}},
+            { scripts: { },                   opts: { title: "Branching",         layout: "<Branching>",          toolTip: "Branch, baby!"}}
         ];
         const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts));
         const dockBtnsReqdOpts:DocumentOptions = {
diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
index dbfc48c63..f81f17589 100644
--- a/src/client/util/SettingsManager.scss
+++ b/src/client/util/SettingsManager.scss
@@ -1,5 +1,3 @@
-@import '../views/global/globalCssVariables.module';
-
 .settings-interface {
     //background-color: whitesmoke !important;
     width: 450px;
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 9d8a41844..3bbc297b8 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -16,6 +16,7 @@ export class SnappingManager {
     @observable _shiftKey = false;
     @observable _ctrlKey = false;
     @observable _metaKey = false;
+    @observable _hideUI = false;
     @observable _showPresPaths = false;
     @observable _isLinkFollowing = false;
     @observable _isDragging: boolean = false;
@@ -53,6 +54,7 @@ export class SnappingManager {
     public static get ShiftKey()       { return this.Instance._shiftKey; } // prettier-ignore
     public static get CtrlKey()        { return this.Instance._ctrlKey; } // prettier-ignore
     public static get MetaKey()        { return this.Instance._metaKey; } // prettier-ignore
+    public static get HideUI()         { return this.Instance._hideUI; } // prettier-ignore
     public static get ShowPresPaths()  { return this.Instance._showPresPaths; } // prettier-ignore
     public static get IsLinkFollowing(){ return this.Instance._isLinkFollowing; } // prettier-ignore
     public static get IsDragging()     { return this.Instance._isDragging; } // prettier-ignore
@@ -73,6 +75,7 @@ export class SnappingManager {
     public static SetShiftKey       = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
     public static SetCtrlKey        = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore
     public static SetMetaKey        = (down: boolean) => runInAction(() => {this.Instance._metaKey = down}); // prettier-ignore
+    public static SetHideUI         = (vis: boolean) => runInAction(() => {this.Instance._hideUI = vis}); // prettier-ignore
     public static SetShowPresPaths  = (paths:boolean) => runInAction(() => {this.Instance._showPresPaths = paths}); // prettier-ignore
     public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => {this.Instance._isLinkFollowing = follow}); // prettier-ignore
     public static SetIsDragging     = (drag: boolean) => runInAction(() => {this.Instance._isDragging = drag}); // prettier-ignore
diff --git a/src/client/util/reportManager/ReportManager.scss b/src/client/util/reportManager/ReportManager.scss
index fd343ac8e..806741c22 100644
--- a/src/client/util/reportManager/ReportManager.scss
+++ b/src/client/util/reportManager/ReportManager.scss
@@ -1,4 +1,4 @@
-@import '../../views/global/globalCssVariables.module';
+@use '../../views/global/globalCssVariables.module' as global;
 
 // header
 
@@ -97,7 +97,7 @@
     background: transparent;
 
     // &:hover {
-    //     // border-bottom-color: $text-gray;
+    //     // border-bottom-color: global.$text-gray;
     // }
     // &:focus {
     //     // border-bottom-color: #4476f7;
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss
index 2ebf673fe..48fa86276 100644
--- a/src/client/views/AntimodeMenu.scss
+++ b/src/client/views/AntimodeMenu.scss
@@ -1,11 +1,11 @@
-@import './global/globalCssVariables.module';
+@use './global/globalCssVariables.module' as global;
 
 .antimodeMenu-cont {
     position: absolute;
     z-index: 10001;
-    height: $antimodemenu-height;
+    height: global.$antimodemenu-height;
     width: fit-content;
-    border-radius: $standard-border-radius;
+    border-radius: global.$standard-border-radius;
     overflow: hidden;
     // box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
     // border-radius: 0px 6px 6px 6px;
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 88e147a03..d22c4d096 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -1,9 +1,9 @@
-@import 'global/globalCssVariables.module.scss';
+@use 'global/globalCssVariables.module.scss' as global;
 
 .contextMenu-cont {
     position: absolute;
     display: flex;
-    z-index: $contextMenu-zindex;
+    z-index: global.$contextMenu-zindex;
     box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
     flex-direction: column;
     background: whitesmoke;
@@ -109,7 +109,7 @@
 .contextMenu-item:hover {
     border-width: 0.11px;
     border-style: none;
-    border-color: $medium-gray; //  rgb(187, 186, 186);
+    border-color: global.$medium-gray; //  rgb(187, 186, 186);
     border-bottom-style: solid;
     border-top-style: solid;
 
@@ -139,7 +139,7 @@
     transition: all 0.1s;
     border-width: 0.11px;
     border-style: none;
-    border-color: $medium-gray; //  rgb(187, 186, 186);
+    border-color: global.$medium-gray; //  rgb(187, 186, 186);
     // padding: 10px 0px 10px 0px;
     white-space: nowrap;
     font-size: 13px;
@@ -184,7 +184,7 @@
     .top-bar {
         height: 20px;
         width: 100%;
-        display: flex; 
+        display: flex;
 
         .close-menu {
             margin-top: 0;
@@ -200,7 +200,7 @@
         }
     }
 
-    .bottom-box{
+    .bottom-box {
         display: flex;
         flex-direction: row;
         justify-content: center;
@@ -209,11 +209,11 @@
         height: 100%;
         width: 100%;
 
-        .width-selector{
+        .width-selector {
             width: 100px;
         }
 
-        .max-min-selector{
+        .max-min-selector {
             height: 15px;
             width: 30px;
         }
diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss
index 25feca7bf..daa711bc4 100644
--- a/src/client/views/DashboardView.scss
+++ b/src/client/views/DashboardView.scss
@@ -1,4 +1,4 @@
-@import './global/globalCssVariables.module';
+@use './global/globalCssVariables.module' as global;
 
 $dashboard-left-menu-width: 250px;
 $dashboard-view-padding: 20px;
@@ -61,9 +61,9 @@ $dashboard-container-width: 250px;
     font-size: 120px;
     font-weight: 100;
     text-align: center;
-    border: solid 2px $light-gray;
+    border: solid 2px global.$light-gray;
     cursor: pointer;
-    color: $light-gray;
+    color: global.$light-gray;
     display: flex;
     display: flex;
     justify-content: center;
@@ -71,8 +71,8 @@ $dashboard-container-width: 250px;
     position: relative;
 
     &:hover {
-        color: $light-blue;
-        border: solid 2px $light-blue;
+        color: global.$light-blue;
+        border: solid 2px global.$light-blue;
     }
 
     .background {
@@ -91,14 +91,14 @@ $dashboard-container-width: 250px;
     cursor: pointer;
     width: $dashboard-container-width;
     height: $dashboard-container-height;
-    outline: solid 2px $light-gray;
+    outline: solid 2px global.$light-gray;
     outline-offset: -2px;
     display: flex;
     flex-direction: column;
     overflow: hidden;
 
     &:hover {
-        outline: solid 2px $light-blue;
+        outline: solid 2px global.$light-blue;
     }
 
     .title {
@@ -144,7 +144,7 @@ $dashboard-container-width: 250px;
 }
 
 .new-dashboard {
-    color: $dark-gray;
+    color: global.$dark-gray;
     padding: 10px;
     display: flex;
     width: 100%;
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index ede277aae..f19fecfa6 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -1,4 +1,4 @@
-@import 'global/globalCssVariables.module';
+@use 'global/globalCssVariables.module' as global;
 
 $linkGap: 3px;
 
@@ -7,13 +7,13 @@ $linkGap: 3px;
 }
 
 .documentButtonBar-linkButton-empty:hover {
-    background: $medium-gray;
+    background: global.$medium-gray;
     transform: scale(1.05);
     cursor: pointer;
 }
 
 .documentButtonBar-linkButton-nonempty:hover {
-    background: $medium-gray;
+    background: global.$medium-gray;
     transform: scale(1.05);
     cursor: pointer;
 }
@@ -32,7 +32,6 @@ $linkGap: 3px;
 
     .tags {
         width: 40px;
-
     }
 }
 .documentButtonBar-followTypes {
@@ -92,8 +91,8 @@ $linkGap: 3px;
     border-radius: 50%;
     opacity: 0.9;
     pointer-events: auto;
-    background-color: $dark-gray;
-    color: $white;
+    background-color: global.$dark-gray;
+    color: global.$white;
     text-transform: uppercase;
     letter-spacing: 2px;
     font-size: 75%;
@@ -104,7 +103,7 @@ $linkGap: 3px;
     align-items: center;
 
     &:hover {
-        background: $medium-gray;
+        background: global.$medium-gray;
         transform: scale(1.05);
         cursor: pointer;
     }
@@ -132,12 +131,12 @@ $linkGap: 3px;
     text-align: center;
     border-radius: 50%;
     pointer-events: auto;
-    background-color: $dark-gray;
+    background-color: global.$dark-gray;
     border: none;
     transition: 0.2s ease all;
 
     &:hover {
-        background-color: $medium-gray;
+        background-color: global.$medium-gray;
     }
 }
 
@@ -148,7 +147,7 @@ $linkGap: 3px;
     text-align: center;
     border-radius: 50%;
     pointer-events: auto;
-    background-color: $dark-gray;
+    background-color: global.$dark-gray;
     border: none;
     transition: 0.2s ease all;
     display: flex;
@@ -157,8 +156,8 @@ $linkGap: 3px;
     align-items: center;
 
     &:hover {
-        background-color: $black;
-        
+        background-color: global.$black;
+
         .documentButtonBar-pinTypes {
             display: flex;
         }
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 346df10d5..a5afb1305 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -1,4 +1,4 @@
-@import 'global/globalCssVariables.module';
+@use 'global/globalCssVariables.module' as global;
 
 $linkGap: 3px;
 $headerHeight: 20px;
@@ -195,14 +195,14 @@ $resizeHandler: 8px;
             .documentDecorations-titleSpan {
                 width: 100%;
                 border-radius: 8px;
-                background: $light-gray;
+                background: global.$contextMenu-zindex;
                 display: inline-block;
                 cursor: move;
             }
         }
 
         .documentDecorations-titleBackground {
-            background: $light-gray;
+            background: global.$light-gray;
             border-radius: 8px;
             width: 100%;
             height: 100%;
@@ -314,7 +314,7 @@ $resizeHandler: 8px;
     .documentDecorations-bottomResizer,
     .documentDecorations-rightResizer {
         pointer-events: auto;
-        background: $medium-gray-dim;
+        background: global.$medium-gray-dim;
         //opacity: 0.2;
         &:hover {
             opacity: 1;
@@ -344,7 +344,7 @@ $resizeHandler: 8px;
         border-radius: 100%;
         left: 7px;
         top: 7px;
-        background: $medium-gray;
+        background: global.$medium-gray;
         height: 10;
         width: 10;
         opacity: 0.5;
@@ -378,7 +378,7 @@ $resizeHandler: 8px;
         transform: translate(0px, -25%);
         padding-bottom: 100%;
         border-radius: 100%;
-        border: solid $medium-gray 10px;
+        border: solid global.$medium-gray 10px;
     }
 
     .documentDecorations-topLeftResizer,
@@ -497,13 +497,13 @@ $resizeHandler: 8px;
 }
 
 .linkButton-empty:hover {
-    background: $medium-gray;
+    background: global.$medium-gray;
     transform: scale(1.05);
     cursor: pointer;
 }
 
 .linkButton-nonempty:hover {
-    background: $medium-gray;
+    background: global.$medium-gray;
     transform: scale(1.05);
     cursor: pointer;
 }
@@ -520,7 +520,7 @@ $resizeHandler: 8px;
     align-items: center;
     gap: 5px;
     //top: 4px;
-    background: $light-gray;
+    background: global.$light-gray;
     opacity: 0.2;
     pointer-events: all;
     transition: opacity 1s;
@@ -542,8 +542,8 @@ $resizeHandler: 8px;
     text-align: center;
     border-radius: 50%;
     pointer-events: auto;
-    color: $dark-gray;
-    border: $dark-gray 1px solid;
+    color: global.$dark-gray;
+    border: global.$dark-gray 1px solid;
 }
 
 .linkButton-linker:hover {
@@ -558,8 +558,8 @@ $resizeHandler: 8px;
     border-radius: 50%;
     opacity: 0.9;
     pointer-events: auto;
-    background-color: $dark-gray;
-    color: $white;
+    background-color: global.$dark-gray;
+    color: global.$white;
     text-transform: uppercase;
     letter-spacing: 2px;
     font-size: 75%;
@@ -570,7 +570,7 @@ $resizeHandler: 8px;
     align-items: center;
 
     &:hover {
-        background: $medium-gray;
+        background: global.$medium-gray;
         transform: scale(1.05);
         cursor: pointer;
     }
@@ -600,13 +600,13 @@ $resizeHandler: 8px;
     border-radius: 50%;
     opacity: 0.9;
     font-size: 14;
-    background-color: $dark-gray;
-    color: $white;
+    background-color: global.$dark-gray;
+    color: global.$white;
     text-align: center;
     cursor: pointer;
 
     &:hover {
-        background: $medium-gray;
+        background: global.$medium-gray;
         transform: scale(1.05);
     }
 }
@@ -616,9 +616,9 @@ $resizeHandler: 8px;
     top: 25px;
     left: 0px;
     width: max-content;
-    font-family: $sans-serif;
+    font-family: global.$sans-serif;
     font-size: 12px;
-    background-color: $light-gray;
+    background-color: global.$light-gray;
     padding: 2px 12px;
     list-style: none;
 
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index e3df01bbb..0d3feb073 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -18,7 +18,7 @@ import { GestureOverlay } from './GestureOverlay';
 import './LightboxView.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { OverlayView } from './OverlayView';
-import { DefaultStyleProvider, returnEmptyDocViewList, wavyBorderPath } from './StyleProvider';
+import { DefaultStyleProvider, returnEmptyDocViewList /* wavyBorderPath */ } from './StyleProvider';
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { StickerPalette } from './smartdraw/StickerPalette';
@@ -283,7 +283,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                         top: this.topBorder,
                         width: this.lightboxWidth(),
                         height: this.lightboxHeight(),
-                        clipPath: `path('${Doc.UserDoc().renderStyle === 'comic' ? wavyBorderPath(this.lightboxWidth(), this.lightboxHeight()) : undefined}')`,
+                        // clipPath: `path('${Doc.UserDoc().renderStyle === 'comic' ? wavyBorderPath(this.lightboxWidth(), this.lightboxHeight()) : undefined}')`,
                         background: SnappingManager.userBackgroundColor,
                     }}>
                     <GestureOverlay isActive>
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 02916e48e..bea1de435 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -1,5 +1,5 @@
-@import 'global/globalCssVariables.module';
-@import 'nodeModuleOverrides';
+@use 'global/globalCssVariables.module' as global;
+//  bcz: fix @import 'nodeModuleOverrides';
 
 :root {
     --flyoutHandleWidth: 28px;
@@ -10,8 +10,8 @@ body {
     width: 100%;
     height: 100%;
     overflow: hidden;
-    font-family: $sans-serif;
-    font-size: $body-text;
+    font-family: global.$sans-serif;
+    font-size: global.$body-text;
     margin: 0;
     position: absolute;
     top: 0;
@@ -54,7 +54,7 @@ button {
     background: black;
     outline: none;
     border: 0px;
-    color: $white;
+    color: global.$white;
     text-transform: uppercase;
     letter-spacing: 2px;
     font-size: 75%;
@@ -63,7 +63,7 @@ button {
 }
 
 button:hover {
-    background: $medium-gray;
+    background: global.$medium-gray;
     transform: scale(1.05);
     cursor: pointer;
 }
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index dda543470..3c1da88cc 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -87,10 +87,9 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
     await CurrentUserUtils.loadUserDocument(info);
     setTimeout(() => {
         // prevent zooming browser
-        document.getElementById('root')!.addEventListener('wheel', event => event.ctrlKey && event.preventDefault(), true);
+        document.getElementById('root')!.addEventListener('wheel', event => event.ctrlKey && event.preventDefault(), { capture: true, passive: true });
         const startload = (document as unknown as { startLoad: number }).startLoad; // see index.html in deploy/
         const loading = Date.now() - (startload ? Number(startload) : Date.now() - 3000);
-        console.log('Loading Time = ' + loading);
         const d = new Date();
         d.setTime(d.getTime() + 100 * 24 * 60 * 60 * 1000);
         const expires = 'expires=' + d.toUTCString();
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index e204759ab..2170e0c34 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -1,5 +1,5 @@
-@import 'global/globalCssVariables.module.scss';
-@import 'nodeModuleOverrides';
+@use 'global/globalCssVariables.module.scss' as global;
+//  bcz: fix @import 'nodeModuleOverrides';
 html {
     overscroll-behavior-x: none;
 }
@@ -68,10 +68,10 @@ body {
 }
 
 .mainView-container {
-    color: $dark-gray;
+    color: global.$dark-gray;
 
     .lm_goldenlayout {
-        background: $medium-gray;
+        background: global.$medium-gray;
     }
 }
 
@@ -93,7 +93,7 @@ body {
 .mainView-propertiesDragger-minified,
 .mainView-propertiesDragger {
     //background-color: rgb(140, 139, 139);
-    background-color: $light-gray;
+    background-color: global.$light-gray;
     height: 55px;
     width: 17px;
     position: absolute;
@@ -133,10 +133,10 @@ body {
         flex-direction: column;
         position: relative;
         height: 100%;
-        background: $medium-gray;
+        background: global.$medium-gray;
 
         .documentView-node-topmost {
-            background: $light-gray;
+            background: global.$light-gray;
         }
     }
 
@@ -153,12 +153,12 @@ body {
 }
 
 .mainView-libraryHandle {
-    background-color: $light-gray;
+    background-color: global.$light-gray;
 }
 
 .mainView-leftMenuPanel {
     min-width: var(--menuPanelWidth);
-    border-right: $standard-border;
+    border-right: global.$standard-border;
 
     .collectionStackingView {
         scrollbar-width: none;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 195b1c572..8b0354471 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -99,7 +99,7 @@ export class MainView extends ObservableReactComponent<object> {
     @observable private _sidebarContent: Doc = Doc.MyLeftSidebarPanel;
     @observable private _leftMenuFlyoutWidth: number = 0;
     @computed get _hideUI() {
-        return this.mainDoc && this.mainDoc._type_collection !== CollectionViewType.Docking;
+        return SnappingManager.HideUI || (this.mainDoc && this.mainDoc._type_collection !== CollectionViewType.Docking);
     }
 
     @computed private get dashboardTabHeight() {
@@ -712,7 +712,7 @@ export class MainView extends ObservableReactComponent<object> {
                     childFiltersByRanges={returnEmptyFilter}
                     searchFilterDocs={returnEmptyDoclist}
                     suppressSetHeight
-                    renderDepth={this._hideUI ? 0 : -1}
+                    renderDepth={-1}
                 />
             </>
         );
@@ -1168,6 +1168,10 @@ ScriptingGlobals.add(function selectMainMenu(doc: Doc) {
     MainView.Instance.selectMenu(doc);
 });
 // eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function hideUI() {
+    SnappingManager.SetHideUI(!SnappingManager.HideUI);
+});
+// eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function createNewPresentation() {
     return MainView.Instance.createNewPresentation();
 }, 'creates a new presentation when called');
diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss
index b8c73b6d3..6c2cda346 100644
--- a/src/client/views/PropertiesButtons.scss
+++ b/src/client/views/PropertiesButtons.scss
@@ -1,4 +1,4 @@
-@import 'global/globalCssVariables.module.scss';
+@use 'global/globalCssVariables.module.scss' as global;
 
 $linkGap: 3px;
 
@@ -7,13 +7,13 @@ $linkGap: 3px;
 }
 
 .propertiesButtons-linkButton-empty:hover {
-    background: $medium-gray;
+    background: global.$medium-gray;
     transform: scale(1.05);
     cursor: pointer;
 }
 
 .propertiesButtons-linkButton-nonempty:hover {
-    background: $medium-gray;
+    background: global.$medium-gray;
     transform: scale(1.05);
     cursor: pointer;
 }
@@ -46,19 +46,19 @@ $linkGap: 3px;
     // margin-left: 4px;
 
     &:hover {
-        background: $medium-gray;
+        background: global.$medium-gray;
         transform: scale(1.05);
         cursor: pointer;
     }
 }
 .propertiesButtons-linkButton-empty.toggle-on {
-    background-color: $medium-blue;
-    color: $white;
+    background-color: global.$medium-blue;
+    color: global.$white;
     width: 100%;
 }
 .propertiesButtons-linkButton-empty.toggle-hover {
-    background-color: $light-blue;
-    color: $black;
+    background-color: global.$light-blue;
+    color: global.$black;
     width: 100%;
 }
 .propertiesButtons-linkButton-empty.toggle-off {
@@ -88,7 +88,7 @@ $linkGap: 3px;
     cursor: pointer;
     text-align: center;
     margin-top: 5px;
-    border: 0.5px solid $medium-gray;
+    border: 0.5px solid global.$medium-gray;
     background-color: rgb(230, 230, 230);
     border-radius: 5px;
     padding: 4px;
@@ -111,7 +111,7 @@ $linkGap: 3px;
 
     .list-item {
         cursor: pointer;
-        color: $black;
+        color: global.$black;
         width: 100%;
         height: 25px;
         font-weight: 400;
diff --git a/src/client/views/PropertiesSection.scss b/src/client/views/PropertiesSection.scss
index d32da1bf1..f7138dd50 100644
--- a/src/client/views/PropertiesSection.scss
+++ b/src/client/views/PropertiesSection.scss
@@ -1,5 +1,3 @@
-@import './global/globalCssVariables.module.scss';
-
 .propertiesView-section {
     .propertiesView-content {
         padding: 10px;
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 7866e67e7..280de4893 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -1,4 +1,4 @@
-@import './global/globalCssVariables.module.scss';
+@use './global/globalCssVariables.module.scss' as global;
 
 .propertiesView-presentationTrails-title {
     display: flex;
@@ -28,7 +28,7 @@
     font-family: 'Roboto';
     font-size: 12px;
     cursor: auto;
-    border-left: $standard-border;
+    border-left: global.$standard-border;
 
     .slider-text {
         font-size: 8px;
@@ -567,7 +567,7 @@
     height: fit-content;
 
     &:hover {
-        border: 0.75px solid $medium-blue;
+        border: 0.75px solid global.$medium-blue;
     }
 }
 
diff --git a/src/client/views/TemplateMenu.scss b/src/client/views/TemplateMenu.scss
index 36a9ce6d0..8879fc20d 100644
--- a/src/client/views/TemplateMenu.scss
+++ b/src/client/views/TemplateMenu.scss
@@ -1,4 +1,4 @@
-@import 'global/globalCssVariables.module.scss';
+@use 'global/globalCssVariables.module.scss' as global;
 .templating-menu {
     position: absolute;
     pointer-events: auto;
@@ -24,15 +24,15 @@
     cursor: pointer;
 
     &:hover {
-        background: $medium-gray;
+        background: global.$medium-gray;
         transform: scale(1.05);
     }
 }
 
 .template-list {
-    font-family: $sans-serif;
+    font-family: global.$sans-serif;
     font-size: 12px;
-    background-color: $light-gray;
+    background-color: global.$light-gray;
     padding: 2px 12px;
     list-style: none;
     position: relative;
diff --git a/src/client/views/animationtimeline/Region.scss b/src/client/views/animationtimeline/Region.scss
index b390ae34e..df82febea 100644
--- a/src/client/views/animationtimeline/Region.scss
+++ b/src/client/views/animationtimeline/Region.scss
@@ -1,4 +1,4 @@
-@import './../global/globalCssVariables.module.scss';
+@use './../global/globalCssVariables.module.scss' as global;
 
 $timelineColor: #9acedf;
 $timelineDark: #77a1aa;
@@ -14,11 +14,11 @@ $timelineDark: #77a1aa;
         height: 200px;
         top: 50%;
         position: relative;
-        background-color: $white;
+        background-color: global.$white;
 
         .menutable {
             tr:nth-child(odd) {
-                background-color: $light-gray;
+                background-color: global.$light-gray;
             }
         }
     }
@@ -70,7 +70,7 @@ $timelineDark: #77a1aa;
         height: 100%;
         position: absolute;
         pointer-events: none;
-        background: linear-gradient(to left, $timelineColor 10%, $white);
+        background: linear-gradient(to left, $timelineColor 10%, global.$white);
     }
 
     .fadeRight {
@@ -78,7 +78,7 @@ $timelineDark: #77a1aa;
         height: 100%;
         position: absolute;
         pointer-events: none;
-        background: linear-gradient(to right, $timelineColor 10%, $white);
+        background: linear-gradient(to right, $timelineColor 10%, global.$white);
     }
 
     .divider {
diff --git a/src/client/views/animationtimeline/Timeline.scss b/src/client/views/animationtimeline/Timeline.scss
index 35ba0fa7f..e1d3b190c 100644
--- a/src/client/views/animationtimeline/Timeline.scss
+++ b/src/client/views/animationtimeline/Timeline.scss
@@ -1,4 +1,4 @@
-@import './../global/globalCssVariables.module.scss';
+@use './../global/globalCssVariables.module.scss' as global;
 
 $timelineColor: #9acedf;
 $timelineDark: #77a1aa;
@@ -159,7 +159,7 @@ $timelineDark: #77a1aa;
     width: 100%;
     height: 300px;
     position: absolute;
-    background-color: $light-gray;
+    background-color: global.$light-gray;
     border-bottom: 2px solid $timelineDark;
     transition: transform 500ms ease;
 
@@ -247,7 +247,7 @@ $timelineDark: #77a1aa;
             top: 0px;
             width: 100px;
             height: 30%;
-            border: 1px solid $dark-gray;
+            border: 1px solid global.$dark-gray;
             font-size: 12px;
             line-height: 11px;
             background-color: $timelineDark;
diff --git a/src/client/views/animationtimeline/TimelineMenu.scss b/src/client/views/animationtimeline/TimelineMenu.scss
index de2042f17..5398a4a97 100644
--- a/src/client/views/animationtimeline/TimelineMenu.scss
+++ b/src/client/views/animationtimeline/TimelineMenu.scss
@@ -1,9 +1,9 @@
-@import './../global/globalCssVariables.module.scss';
+@use './../global/globalCssVariables.module.scss' as global;
 
 .timeline-menu-container {
     position: absolute;
     display: flex;
-    box-shadow: $medium-gray 0.2vw 0.2vw 0.4vw;
+    box-shadow: global.$medium-gray 0.2vw 0.2vw 0.4vw;
     flex-direction: column;
     background: whitesmoke;
     z-index: 10000;
@@ -14,7 +14,7 @@
     border: solid #bbbbbbbb 1px;
 
     .timeline-menu-input {
-        font: $sans-serif;
+        font: global.$sans-serif;
         font-size: 13px;
         width: 100%;
         text-transform: uppercase;
@@ -33,11 +33,11 @@
         border-top-left-radius: 15px;
         border-top-right-radius: 15px;
         text-transform: uppercase;
-        background: $dark-gray;
+        background: global.$dark-gray;
         letter-spacing: 2px;
 
         .timeline-menu-header-desc {
-            font: $sans-serif;
+            font: global.$sans-serif;
             font-size: 13px;
             text-align: center;
             color: whitesmoke;
@@ -72,15 +72,15 @@
     .timeline-menu-item:hover {
         border-width: 0.11px;
         border-style: none;
-        border-color: $medium-gray;
+        border-color: global.$medium-gray;
         border-bottom-style: solid;
         border-top-style: solid;
-        background: $medium-blue;
+        background: global.$medium-blue;
     }
 
     .timeline-menu-desc {
         padding-left: 10px;
-        font: $sans-serif;
+        font: global.$sans-serif;
         font-size: 13px;
     }
 }
diff --git a/src/client/views/animationtimeline/TimelineOverview.scss b/src/client/views/animationtimeline/TimelineOverview.scss
index 2878232e6..8336f2b2f 100644
--- a/src/client/views/animationtimeline/TimelineOverview.scss
+++ b/src/client/views/animationtimeline/TimelineOverview.scss
@@ -1,4 +1,4 @@
-@import './../global/globalCssVariables.module.scss';
+@use './../global/globalCssVariables.module.scss' as global;
 
 $timelineColor: #9acedf;
 $timelineDark: #77a1aa;
diff --git a/src/client/views/animationtimeline/Track.scss b/src/client/views/animationtimeline/Track.scss
index f56b2fe5f..7f5e8b8f3 100644
--- a/src/client/views/animationtimeline/Track.scss
+++ b/src/client/views/animationtimeline/Track.scss
@@ -1,12 +1,12 @@
-@import './../global/globalCssVariables.module.scss';
+@use './../global/globalCssVariables.module.scss' as global;
 
 .track-container {
     .track {
         .inner {
             top: 0px;
             width: calc(100%);
-            background-color: $white;
-            border: 1px solid $dark-gray;
+            background-color: global.$white;
+            border: 1px solid global.$dark-gray;
             position: relative;
             z-index: 100;
         }
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index 79c53db08..06dd4627f 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -1,5 +1,3 @@
-@import '../global/globalCssVariables.module.scss';
-
 .collectionCardView-outer {
     height: 100%;
     width: 100%;
diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss
index 42e112906..13e6b54c2 100644
--- a/src/client/views/collections/CollectionCarousel3DView.scss
+++ b/src/client/views/collections/CollectionCarousel3DView.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 .collectionCarousel3DView-outer {
     height: 100%;
     position: relative;
@@ -10,8 +10,8 @@
 .carousel-wrapper {
     display: flex;
     position: absolute;
-    top: $CAROUSEL3D_TOP * 1%;
-    height: ($CAROUSEL3D_SIDE_SCALE * 100) * 1%;
+    top: global.$CAROUSEL3D_TOP * 1%;
+    height: (global.$CAROUSEL3D_SIDE_SCALE * 100) * 1%;
     align-items: center;
     transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
 
@@ -24,7 +24,7 @@
         pointer-events: none;
         opacity: 0.5;
         z-index: 1;
-        transform: scale($CAROUSEL3D_SIDE_SCALE);
+        transform: scale(global.$CAROUSEL3D_SIDE_SCALE);
         user-select: none;
     }
 
@@ -32,7 +32,7 @@
         pointer-events: unset;
         opacity: 1;
         z-index: 2;
-        transform: scale($CAROUSEL3D_CENTER_SCALE);
+        transform: scale(global.$CAROUSEL3D_CENTER_SCALE);
     }
 }
 
@@ -80,7 +80,7 @@
 .carousel3DView-back {
     top: 0;
     background: transparent;
-    width: calc((1 - #{$CAROUSEL3D_CENTER_SCALE} * 0.33) / 2 * 100%);
+    width: calc((1 - #{global.$CAROUSEL3D_CENTER_SCALE} * 0.33) / 2 * 100%);
     height: 100%;
 }
 
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index a747ef45f..7c19d39da 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .lm_root {
     position: relative;
@@ -285,7 +285,7 @@
     background: transparent;
     border: solid 0px transparent;
     cursor: grab;
-    color: $black;
+    color: global.$black;
 }
 .collectiondockingview-container .lm_splitter {
     opacity: 0.2;
@@ -378,7 +378,7 @@ ul.lm_tabs::before {
     z-index: 1;
     text-align: center;
     font-size: 18;
-    color: $dark-gray;
+    color: global.$dark-gray;
 
     img {
         position: relative;
@@ -491,7 +491,7 @@ ul.lm_tabs::before {
     }
 
     .lm_content {
-        background: $white;
+        background: global.$white;
     }
 
     .lm_controls {
@@ -557,7 +557,7 @@ ul.lm_tabs::before {
     }
 
     .flexlayout__splitter {
-        background-color: $dark-gray;
+        background-color: global.$dark-gray;
     }
 
     .flexlayout__splitter:hover {
@@ -626,7 +626,7 @@ ul.lm_tabs::before {
         position: absolute;
         box-sizing: border-box;
         background-color: #222;
-        color: $dark-gray;
+        color: global.$dark-gray;
     }
 
     .flexlayout__tab_button {
@@ -709,7 +709,7 @@ ul.lm_tabs::before {
     }
 
     .flexlayout__tab_header_outer {
-        background-color: $dark-gray;
+        background-color: global.$dark-gray;
         position: absolute;
         left: 0;
         right: 0;
@@ -769,28 +769,28 @@ ul.lm_tabs::before {
     }
 
     .flexlayout__border_top {
-        background-color: $dark-gray;
+        background-color: global.$dark-gray;
         border-bottom: 1px solid #ddd;
         box-sizing: border-box;
         overflow: hidden;
     }
 
     .flexlayout__border_bottom {
-        background-color: $dark-gray;
+        background-color: global.$dark-gray;
         border-top: 1px solid #333;
         box-sizing: border-box;
         overflow: hidden;
     }
 
     .flexlayout__border_left {
-        background-color: $dark-gray;
+        background-color: global.$dark-gray;
         border-right: 1px solid #333;
         box-sizing: border-box;
         overflow: hidden;
     }
 
     .flexlayout__border_right {
-        background-color: $dark-gray;
+        background-color: global.$dark-gray;
         border-left: 1px solid #333;
         box-sizing: border-box;
         overflow: hidden;
diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss
index 45d9394ed..11fce720c 100644
--- a/src/client/views/collections/CollectionMenu.scss
+++ b/src/client/views/collections/CollectionMenu.scss
@@ -1,13 +1,13 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .collectionMenu-container {
     display: flex;
     position: relative;
     align-content: center;
     justify-content: space-between;
-    background-color: $dark-gray;
+    background-color: global.$dark-gray;
     height: 40px;
-    border-bottom: $standard-border;
+    border-bottom: global.$standard-border;
     padding: 0 10px;
     align-items: center;
     overflow-x: auto;
diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss
index 95fda7b0a..0d24a56b5 100644
--- a/src/client/views/collections/CollectionNoteTakingView.scss
+++ b/src/client/views/collections/CollectionNoteTakingView.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .collectionNoteTakingView-DocumentButtons {
     opacity: 0;
@@ -58,7 +58,7 @@
 .documentButtonMenu {
     position: relative;
     height: fit-content;
-    border-bottom: $standard-border;
+    border-bottom: global.$standard-border;
     display: flex;
     justify-content: center;
     flex-direction: column;
@@ -70,11 +70,11 @@
         width: 90%;
         margin: 5px;
         font-size: 11px;
-        background-color: $light-blue;
-        color: $medium-blue;
+        background-color: global.$light-blue;
+        color: global.$medium-blue;
         padding: 10px;
         border-radius: 10px;
-        border: solid 2px $medium-blue;
+        border: solid 2px global.$medium-blue;
     }
 }
 
@@ -146,9 +146,9 @@
         padding: 10px;
         height: 2vw;
         width: 100%;
-        font-family: $sans-serif;
-        background: $dark-gray;
-        color: $white;
+        font-family: global.$sans-serif;
+        background: global.$dark-gray;
+        color: global.$white;
     }
 
     .collectionNoteTakingView-columnDragger {
@@ -206,7 +206,7 @@
         margin-left: 2px;
         margin-right: 2px;
         margin-top: 2px;
-        background: $medium-gray;
+        background: global.$medium-gray;
         height: 5px;
 
         &.active {
@@ -258,7 +258,7 @@
         text-align: center;
         margin: auto;
         margin-bottom: 10px;
-        background: $medium-gray;
+        background: global.$medium-gray;
         // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
 
         .editableView-input:hover,
@@ -279,7 +279,7 @@
             display: flex;
             align-items: center;
             justify-content: center;
-            color: $dark-gray;
+            color: global.$dark-gray;
 
             .editableView-container-editing-oneLine,
             .editableView-container-editing {
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 0ced3f9e3..d05c0ffde 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .collectionStackedTimeline-timelineContainer {
     height: 100%;
@@ -6,7 +6,7 @@
     overflow-x: auto;
     overflow-y: hidden;
     border: none;
-    background-color: $white;
+    background-color: global.$white;
     border-width: 0 2px 0 2px;
 
     &:hover {
@@ -28,7 +28,7 @@
 
 .collectionStackedTimeline {
     position: absolute;
-    background: $off-white;
+    background: global.$off-white;
     z-index: 1000;
     height: 100%;
     overflow: hidden;
@@ -36,7 +36,7 @@
     .collectionStackedTimeline-trim-shade {
         position: absolute;
         height: 100%;
-        background-color: $dark-gray;
+        background-color: global.$dark-gray;
         opacity: 0.3;
         top: 0;
     }
@@ -45,7 +45,7 @@
         height: 100%;
         position: absolute;
         box-sizing: border-box;
-        border: 2px solid $medium-blue;
+        border: 2px solid global.$medium-blue;
         display: flex;
         justify-content: space-between;
         max-width: 100%;
@@ -53,7 +53,7 @@
         left: 0;
 
         .collectionStackedTimeline-trim-handle {
-            background-color: $medium-blue;
+            background-color: global.$medium-blue;
             height: 100%;
             width: 5px;
             cursor: ew-resize;
@@ -65,12 +65,12 @@
         width: 10px;
         top: 2.5%;
         height: 95%;
-        background: $light-blue;
+        background: global.$light-blue;
         border-radius: 3px;
         opacity: 0.3;
         z-index: 500;
         border-style: solid;
-        border-color: $medium-blue;
+        border-color: global.$medium-blue;
         border-width: 1px;
     }
 
@@ -84,12 +84,12 @@
     }
 
     .collectionStackedTimeline-current {
-        background-color: $pink;
+        background-color: global.$pink;
     }
 
     .collectionStackedTimeline-hover {
         display: none;
-        background-color: $medium-blue;
+        background-color: global.$medium-blue;
     }
 
     .collectionStackedTimeline-marker-timeline {
@@ -97,14 +97,14 @@
         top: 2.5%;
         height: 95%;
         border-radius: 4px;
-        //background: $light-gray;
+        //background: global.$light-gray;
         &:hover {
             opacity: 1;
         }
 
         .collectionStackedTimeline-left-resizer,
         .collectionStackedTimeline-resizer {
-            background: $dark-gray;
+            background: global.$dark-gray;
             position: absolute;
             top: 0;
             height: 100%;
@@ -141,7 +141,7 @@
 
     .hoverTime {
         position: absolute;
-        color: $dark-gray;
+        color: global.$dark-gray;
         transform: translate(0, -100%);
 
         font-weight: bold;
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 6400a0a8e..5237bdffb 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .collectionMasonryView {
     display: inline;
@@ -18,7 +18,7 @@
 .documentButtonMenu {
     position: relative;
     height: fit-content;
-    border-bottom: $standard-border;
+    border-bottom: global.$standard-border;
     display: flex;
     justify-content: center;
     flex-direction: column;
@@ -30,10 +30,10 @@
         width: 90%;
         margin: 5px;
         font-size: 11px;
-        color: $medium-blue;
+        color: global.$medium-blue;
         padding: 10px;
         border-radius: 5px;
-        border: solid 0.5px $medium-blue;
+        border: solid 0.5px global.$medium-blue;
     }
 }
 
@@ -115,9 +115,9 @@
         padding: 10px;
         height: 2vw;
         width: 100%;
-        font-family: $sans-serif;
-        background: $dark-gray;
-        color: $white;
+        font-family: global.$sans-serif;
+        background: global.$dark-gray;
+        color: global.$white;
     }
 
     .collectionStackingView-columnDragger {
@@ -149,7 +149,7 @@
 
     .collectionStackingView-collapseBar {
         margin-top: 2px;
-        background: $medium-gray;
+        background: global.$medium-gray;
         height: 5px;
         width: 100%;
         display: none;
@@ -207,11 +207,11 @@
         text-align: center;
         margin: auto;
         margin-bottom: 10px;
-        background: $medium-gray;
+        background: global.$medium-gray;
         // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
 
         .editableView-input {
-            color: $dark-gray;
+            color: global.$dark-gray;
         }
 
         .editableView-input:hover,
@@ -232,7 +232,7 @@
             display: flex;
             align-items: center;
             justify-content: center;
-            color: $dark-gray;
+            color: global.$dark-gray;
 
             .editableView-container-editing-oneLine,
             .editableView-container-editing {
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index bbbef78b4..2a03ea708 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .collectionTreeView-container {
     transform-origin: top left;
@@ -12,7 +12,7 @@
     width: 100%;
     position: relative;
     top: 0;
-    // background: $light-gray;
+    // background: global.$light-gray;
     font-size: 13px;
     overflow: auto;
     user-select: none;
@@ -21,7 +21,7 @@
 
     ul {
         list-style: none;
-        padding-left: $TREE_BULLET_WIDTH;
+        padding-left: global.$TREE_BULLET_WIDTH;
         margin-bottom: 1px; // otherwise vertical scrollbars may pop up for no apparent reason....
         > .contentFittingDocumentView {
             width: unset;
@@ -47,7 +47,7 @@
     }
 
     .delete-button {
-        color: $medium-gray;
+        color: global.$medium-gray;
         // float: right;
         margin-left: 15px;
         // margin-top: 3px;
@@ -90,7 +90,7 @@
 .collectionTreeView-subtitle {
     font-style: italic;
     font-size: 8pt;
-    color: $medium-gray;
+    color: global.$medium-gray;
 }
 
 .docContainer {
diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss
index de53a2c62..06c324bd0 100644
--- a/src/client/views/collections/CollectionView.scss
+++ b/src/client/views/collections/CollectionView.scss
@@ -1,10 +1,10 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .collectionView {
     border-width: 0;
-    border-color: $light-gray;
+    border-color: global.$light-gray;
     border-style: solid;
-    border-radius: 0 0 $border-radius $border-radius;
+    border-radius: 0 0 global.$border-radius global.$border-radius;
     box-sizing: border-box;
     border-radius: inherit;
     width: 100%;
diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss
index dd4c0b881..397e35ca9 100644
--- a/src/client/views/collections/TabDocView.scss
+++ b/src/client/views/collections/TabDocView.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .tabDocView-content {
     height: 100%;
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 2ab1a5ac1..78794d112 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .treeView-label {
     max-height: 1.5em;
@@ -14,7 +14,7 @@
     .bullet-outline {
         position: relative;
         width: fit-content;
-        color: $medium-gray;
+        color: global.$medium-gray;
         transform: scale(0.5);
         display: inline-flex;
         align-items: center;
@@ -66,7 +66,7 @@
         min-height: 20px;
         min-width: 15px;
         margin-right: 3px;
-        color: $medium-gray;
+        color: global.$medium-gray;
         border: #80808030 1px solid;
         border-radius: 5px;
         z-index: 1;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 46bd37f6d..cce0ff684 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -1,4 +1,4 @@
-@import '../../global/globalCssVariables.module.scss';
+@use '../../global/globalCssVariables.module.scss' as global;
 
 .collectionfreeformview-none {
     position: inherit;
@@ -32,9 +32,9 @@
 .collectionfreeformview-mask-empty,
 .collectionfreeformview-mask {
     z-index: 5000;
-    width: $INK_MASK_SIZE;
-    height: $INK_MASK_SIZE;
-    transform: translate($INK_MASK_SIZE_HALF, $INK_MASK_SIZE_HALF);
+    width: global.$INK_MASK_SIZE;
+    height: global.$INK_MASK_SIZE;
+    transform: translate(global.$INK_MASK_SIZE_HALF, global.$INK_MASK_SIZE_HALF);
     pointer-events: none;
     position: absolute;
     background-color: transparent;
@@ -211,11 +211,11 @@
 
     //nested freeform views
     // .collectionfreeformview-container {
-    // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px),
-    //   linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px);
+    // background-image: linear-gradient(to right, global.$light-color-secondary 1px, transparent 1px),
+    //   linear-gradient(to bottom, global.$light-color-secondary 1px, transparent 1px);
     // background-size: 30px 30px;
     // }
-    border: 0px solid $light-gray;
+    border: 0px solid global.$light-gray;
     border-radius: inherit;
     box-sizing: border-box;
     position: absolute;
@@ -233,7 +233,7 @@
     box-sizing: border-box;
     width: 98%;
     height: 98%;
-    border-radius: $border-radius;
+    border-radius: global.$border-radius;
 }
 
 //this is an animation for the blinking cursor!
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx
index f050b9846..1a44e094d 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx
@@ -9,7 +9,8 @@ import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 import './ImageLabelHandler.scss';
 
 @observer
-export class ImageLabelHandler extends ObservableReactComponent<{}> {
+export class ImageLabelHandler extends ObservableReactComponent<object> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: ImageLabelHandler;
 
     @observable _display: boolean = false;
@@ -19,11 +20,10 @@ export class ImageLabelHandler extends ObservableReactComponent<{}> {
     @observable _currentLabel: string = '';
     @observable _labelGroups: string[] = [];
 
-    constructor(props: any) {
+    constructor(props: object) {
         super(props);
         makeObservable(this);
         ImageLabelHandler.Instance = this;
-        console.log('Instantiated label handler!');
     }
 
     @action
@@ -41,8 +41,8 @@ export class ImageLabelHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    addLabel = (label: string) => {
-        label = label.toUpperCase().trim();
+    addLabel = (labelIn: string) => {
+        const label = labelIn.toUpperCase().trim();
         if (label.length > 0) {
             if (!this._labelGroups.includes(label)) {
                 this._labelGroups = [...this._labelGroups, label];
@@ -96,10 +96,10 @@ export class ImageLabelHandler extends ObservableReactComponent<{}> {
                     <div>
                         {this._labelGroups.map(group => {
                             return (
-                                <div>
+                                <div key={group}>
                                     <p>{group}</p>
                                     <IconButton
-                                        tooltip={'Remove Label'}
+                                        tooltip="Remove Label"
                                         onPointerDown={() => {
                                             this.removeLabel(group);
                                         }}
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
index b8ceec139..0dfaed38a 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
@@ -1,12 +1,12 @@
-@import '../../global/globalCssVariables.module.scss';
-@import '../../_nodeModuleOverrides';
+@use '../../global/globalCssVariables.module.scss' as global;
+// bcz fix   @import '../../_nodeModuleOverrides';
 
 .collectionLinearView {
     width: 100%;
 }
 .collectionLinearView-label {
     color: black;
-    background-color: $light-gray;
+    background-color: global.$light-gray;
     width: 100%;
 }
 .collectionLinearView-outer {
@@ -32,8 +32,8 @@
         }
 
         > span {
-            background: $dark-gray;
-            color: $white;
+            background: global.$dark-gray;
+            color: global.$white;
             border-radius: 18px;
             margin-right: 6px;
             cursor: pointer;
@@ -44,7 +44,7 @@
         }
 
         .bottomPopup-background {
-            background: $medium-blue;
+            background: global.$medium-blue;
             display: flex;
             border-radius: 10px;
             height: 35;
@@ -55,7 +55,7 @@
         }
 
         .bottomPopup-text {
-            color: $white;
+            color: global.$white;
             display: inline;
             white-space: nowrap;
             padding-left: 8px;
@@ -72,7 +72,7 @@
             padding-left: 8px;
             padding-right: 8px;
             vertical-align: middle;
-            background-color: $light-gray;
+            background-color: global.$light-gray;
             border-radius: 3px;
             color: black;
             margin-right: 5px;
@@ -86,7 +86,7 @@
             padding-left: 8px;
             padding-right: 8px;
             vertical-align: middle;
-            background-color: $close-red;
+            background-color: global.$close-red;
             border-radius: 3px;
             color: black;
         }
@@ -94,13 +94,13 @@
         > label {
             pointer-events: all;
             cursor: pointer;
-            background-color: $medium-blue;
+            background-color: global.$medium-blue;
             padding: 5;
             border-radius: 2px;
             height: 100%;
             min-width: 25;
             margin: 0;
-            color: $white;
+            color: global.$white;
             display: flex;
             font-weight: 100;
             transition: transform 0.2s;
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 817ceac97..0bf78f57c 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -1,4 +1,4 @@
-@import '../../global/globalCssVariables.module.scss';
+@use '../../global/globalCssVariables.module.scss' as global;
 
 .collectionSchemaView {
     cursor: default;
@@ -7,7 +7,7 @@
     flex-direction: row;
 
     .schema-table {
-        background-color: $white;
+        background-color: global.$white;
         cursor: grab;
         width: 100%;
 
@@ -49,10 +49,10 @@
 
 .schema-column-menu,
 .schema-filter-menu {
-    background: $light-gray;
+    background: global.$light-gray;
     position: absolute;
-    border: 1px solid $medium-gray;
-    border-bottom: 2px solid $medium-gray;
+    border: 1px solid global.$medium-gray;
+    border-bottom: 2px solid global.$medium-gray;
     max-height: 201px;
     display: flex;
     overflow: hidden;
@@ -66,7 +66,7 @@
         width: 100%;
 
         &:hover {
-            background-color: $medium-gray;
+            background-color: global.$medium-gray;
         }
         .schema-search-result-type {
             font-family: 'Courier New', Courier, monospace;
@@ -74,8 +74,8 @@
 
         .schema-search-result-type,
         .schema-search-result-desc {
-            color: $dark-gray;
-            font-size: $body-text;
+            color: global.$dark-gray;
+            font-size: global.$body-text;
         }
         .schema-search-result-desc {
             font-style: italic;
@@ -120,9 +120,9 @@
     .schema-column-menu-button {
         cursor: pointer;
         padding: 2px 5px;
-        background: $medium-blue;
+        background: global.$medium-blue;
         border-radius: 9999px;
-        color: $white;
+        color: global.$white;
         width: fit-content;
         margin: 5px;
         align-self: center;
@@ -141,7 +141,7 @@
 }
 
 .schema-column-header {
-    background-color: $light-gray;
+    background-color: global.$light-gray;
     font-weight: bold;
     display: flex;
     flex-direction: row;
@@ -149,7 +149,7 @@
     align-items: center;
     padding: 0;
     z-index: 1;
-    border: 1px solid $medium-gray;
+    border: 1px solid global.$medium-gray;
 
     .schema-column-title {
         flex-grow: 2;
@@ -175,7 +175,7 @@
         cursor: ew-resize;
 
         &:hover {
-            background-color: $light-blue;
+            background-color: global.$light-blue;
         }
     }
 
@@ -188,7 +188,7 @@
         min-width: 5px;
         transform: translate(-3px, 0px);
         align-self: flex-start;
-        background-color: $medium-gray;
+        background-color: global.$medium-gray;
     }*/ // creates awkward thick gray borders between colheaders
 }
 
@@ -202,7 +202,7 @@
 }
 
 .schema-header-row {
-    background-color: $light-gray;
+    background-color: global.$light-gray;
     overflow: hidden;
 }
 
@@ -226,7 +226,7 @@
 
 .schema-table-cell,
 .row-menu {
-    border: 1px solid $medium-gray;
+    border: 1px solid global.$medium-gray;
     overflow-x: hidden;
     overflow-y: auto;
     display: inline-flex;
@@ -264,7 +264,7 @@
     .row-menu-infos {
         position: absolute;
         top: 3;
-        left: 3; 
+        left: 3;
         z-index: 1;
         display: flex;
         justify-content: flex-end;
@@ -278,7 +278,7 @@
 
 .schema-row-button,
 .schema-header-button {
-    color: $dark-gray;
+    color: global.$dark-gray;
     margin: 3px;
     cursor: pointer;
     display: flex;
@@ -294,7 +294,7 @@
     width: 17px;
     height: 17px;
     border-radius: 30%;
-    background-color: $dark-gray;
+    background-color: global.$dark-gray;
     color: white;
     margin: 3px;
     cursor: pointer;
@@ -311,5 +311,3 @@
     outline: none;
     height: 100%;
 }
-
-
diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss
index 636b6415c..ebf60b287 100644
--- a/src/client/views/linking/LinkMenu.scss
+++ b/src/client/views/linking/LinkMenu.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .linkMenu {
     width: auto;
diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss
index 66ddd6eca..3cd60c87f 100644
--- a/src/client/views/linking/LinkMenuItem.scss
+++ b/src/client/views/linking/LinkMenuItem.scss
@@ -1,7 +1,7 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .linkMenu-item {
-    // border-top: 0.5px solid $medium-gray;
+    // border-top: 0.5px solid global.$medium-gray;
     position: relative;
     display: flex;
     border-top: 0.5px solid #cdcdcd;
@@ -120,7 +120,7 @@
         border-radius: 50%;
         pointer-events: auto;
         background-color: red;
-        color: $white;
+        color: global.$white;
         font-size: 65%;
         transition: transform 0.2s;
         text-align: center;
@@ -138,7 +138,7 @@
         }
 
         &:hover {
-            background: $medium-gray;
+            background: global.$medium-gray;
             cursor: pointer;
         }
     }
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss
index 74fbfbb2c..cb1e11780 100644
--- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss
@@ -1,15 +1,15 @@
-@import '../NewLightboxStyles.scss';
+@use '../NewLightboxStyles.scss' as newstyles;
 
 .newLightboxButtonMeny-container {
     width: 100vw;
     height: 100vh;
-  
+
     &.dark {
-      background: $black;
+        background: newstyles.$black;
     }
-  
+
     &.light,
     &.default {
-      background: $white;
+        background: newstyles.$white;
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/newlightbox/ExploreView/ExploreView.scss b/src/client/views/newlightbox/ExploreView/ExploreView.scss
index 5a8ab2f87..2c264c514 100644
--- a/src/client/views/newlightbox/ExploreView/ExploreView.scss
+++ b/src/client/views/newlightbox/ExploreView/ExploreView.scss
@@ -1,4 +1,4 @@
-@import '../NewLightboxStyles.scss';
+@use '../NewLightboxStyles.scss' as newstyles;
 
 .exploreView-container {
     width: 100%;
@@ -6,39 +6,39 @@
     border-radius: 20px;
     position: relative;
     // transform: scale(1);
-    background: $gray-l1;
-    border-top: $standard-border;
-    border-color: $gray-l2;
+    background: newstyles.$gray-l1;
+    border-top: newstyles.$standard-border;
+    border-color: newstyles.$gray-l2;
     border-radius: 0px 0px 20px 20px;
     transform-origin: 50% 50%;
     overflow: hidden;
 
     &.dark {
-      background: $black;
+        background: newstyles.$black;
     }
-  
+
     &.light,
     &.default {
-      background: $gray-l1;
+        background: newstyles.$gray-l1;
     }
 
     .exploreView-doc {
-      width: 60px;
-      height: 80px;
-      position: absolute;
-      background: $blue-l2;
-      // opacity: 0.8;
-      transform-origin: 50% 50%;
-      transform: translate(-50%, -50%) scale(1);
-      cursor: pointer;
-      transition: 0.2s ease;
-      overflow: hidden;
-      font-size: 9px;
-      padding: 10px;
-      border-radius: 5px;
+        width: 60px;
+        height: 80px;
+        position: absolute;
+        background: newstyles.$blue-l2;
+        // opacity: 0.8;
+        transform-origin: 50% 50%;
+        transform: translate(-50%, -50%) scale(1);
+        cursor: pointer;
+        transition: 0.2s ease;
+        overflow: hidden;
+        font-size: 9px;
+        padding: 10px;
+        border-radius: 5px;
 
-      &:hover {
-        transform: translate(calc(-50% * 1.125), calc(-50% * 1.125)) scale(1.5);
-      }
+        &:hover {
+            transform: translate(calc(-50% * 1.125), calc(-50% * 1.125)) scale(1.5);
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/newlightbox/Header/LightboxHeader.scss b/src/client/views/newlightbox/Header/LightboxHeader.scss
index a9e60ea98..5b316890d 100644
--- a/src/client/views/newlightbox/Header/LightboxHeader.scss
+++ b/src/client/views/newlightbox/Header/LightboxHeader.scss
@@ -1,9 +1,9 @@
-@import '../NewLightboxStyles.scss';
+@use '../NewLightboxStyles.scss' as newstyles;
 
 .newLightboxHeader-container {
     width: 100%;
     height: 100%;
-    background: $gray-l1;
+    background: newstyles.$gray-l1;
     border-radius: 20px 20px 0px 0px;
     padding: 20px;
     display: grid;
@@ -29,13 +29,13 @@
         grid-row: 2;
         .type {
             padding: 2px 7px !important;
-            background: $gray-l2;
+            background: newstyles.$gray-l2;
         }
     }
 
     .lb-label {
-        color: $gray-l3;
-        font-weight: $h1-weight;
+        color: newstyles.$gray-l3;
+        font-weight: newstyles.$h1-weight;
     }
 
     .lb-button {
@@ -47,25 +47,25 @@
         justify-content: space-evenly;
         align-items: center;
         transition: 0.2s ease;
-        gap: 5px; 
-        font-size: $body-size;
+        gap: 5px;
+        font-size: newstyles.$body-size;
         height: fit-content;
 
         &:hover {
-            background: $gray-l2;
+            background: newstyles.$gray-l2;
         }
 
         &.true {
-            background: $blue-l1;
+            background: newstyles.$blue-l1;
         }
     }
-    
+
     &.dark {
-      background: $black;
+        background: newstyles.$black;
     }
-  
+
     &.light,
     &.default {
-      background: $white;
+        background: newstyles.$white;
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/newlightbox/RecommendationList/RecommendationList.scss b/src/client/views/newlightbox/RecommendationList/RecommendationList.scss
index 40dd47e47..99c935e0c 100644
--- a/src/client/views/newlightbox/RecommendationList/RecommendationList.scss
+++ b/src/client/views/newlightbox/RecommendationList/RecommendationList.scss
@@ -1,4 +1,4 @@
-@import '../NewLightboxStyles.scss';
+@use '../NewLightboxStyles.scss' as newstyles;
 
 .recommendationlist-container {
     height: calc(100% - 40px);
@@ -7,111 +7,110 @@
     overflow-y: scroll;
 
     .recommendations {
-      height: fit-content;
-      padding: 20px;
-      display: flex;
-      flex-direction: column;
-      gap: 20px;
-      background: $gray-l1;
-      border-radius: 0px 0px 20px 20px;
+        height: fit-content;
+        padding: 20px;
+        display: flex;
+        flex-direction: column;
+        gap: 20px;
+        background: newstyles.$gray-l1;
+        border-radius: 0px 0px 20px 20px;
     }
 
     .header {
-      top: 0px;
-      position: sticky;
-      background: $gray-l1;
-      border-bottom: $standard-border;
-      border-color: $gray-l2;
-      display: flex;
-      flex-direction: column;
-      justify-content: flex-start;
-      align-items: flex-start;
-      border-radius: 20px 20px 0px 0px;
-      padding: 20px;
-      z-index: 2;
-      gap: 10px;
-      color: $text-color-lm;
-
-      .lb-label {
-        color: $gray-l3;
-        font-weight: $h1-weight;
-        font-size: $body-size;
-      }
-
-      .lb-caret {
+        top: 0px;
+        position: sticky;
+        background: newstyles.$gray-l1;
+        border-bottom: newstyles.$standard-border;
+        border-color: newstyles.$gray-l2;
         display: flex;
-        flex-direction: row;
-        justify-content: flex-end;
-        align-items: center;
-        gap: 5px;
-        cursor: pointer;
-        width: 100%;
-        user-select: none;
-        font-size: $body-size;
-      }
+        flex-direction: column;
+        justify-content: flex-start;
+        align-items: flex-start;
+        border-radius: 20px 20px 0px 0px;
+        padding: 20px;
+        z-index: 2;
+        gap: 10px;
+        color: newstyles.$text-color-lm;
 
-      .more {
-        width: 100%;
-      }
+        .lb-label {
+            color: newstyles.$gray-l3;
+            font-weight: newstyles.$h1-weight;
+            font-size: newstyles.$body-size;
+        }
 
-      &.dark {
-        color: $text-color-dm;
-      }
+        .lb-caret {
+            display: flex;
+            flex-direction: row;
+            justify-content: flex-end;
+            align-items: center;
+            gap: 5px;
+            cursor: pointer;
+            width: 100%;
+            user-select: none;
+            font-size: newstyles.$body-size;
+        }
 
-      .title {
-        height: 30px;
-        min-height: 30px;
-        font-size: $h1-size;
-        font-weight: $h1-weight;
-        text-align: left;
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        width: 100%;
-      }
+        .more {
+            width: 100%;
+        }
 
-      .keywords {
-        display: flex;
-        flex-flow: row wrap;
-        gap: 5px;
+        &.dark {
+            color: newstyles.$text-color-dm;
+        }
 
-        .keyword-input {
-          padding: 3px 7px;
-          background: $gray-l2;
-          outline: none;
-          border: none;
-          height: 21.5px;
-          color: $text-color-lm;
+        .title {
+            height: 30px;
+            min-height: 30px;
+            font-size: newstyles.$h1-size;
+            font-weight: newstyles.$h1-weight;
+            text-align: left;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            width: 100%;
         }
 
-        .keyword {
-          padding: 3px 7px;
-          width: fit-content;
-          background: $gray-l2;
-          display: flex;
-          justify-content: center;
-          align-items: center;
-          flex-direction: row;
-          gap: 10px;
-          font-size: $body-size;
-          font-weight: $body-weight;
+        .keywords {
+            display: flex;
+            flex-flow: row wrap;
+            gap: 5px;
 
-          &.loading {
-            animation: skeleton-loading-l2 1s linear infinite alternate;
-            min-width: 70px;
-            height: 21.5px;
-          }
-        }
+            .keyword-input {
+                padding: 3px 7px;
+                background: newstyles.$gray-l2;
+                outline: none;
+                border: none;
+                height: 21.5px;
+                color: newstyles.$text-color-lm;
+            }
+
+            .keyword {
+                padding: 3px 7px;
+                width: fit-content;
+                background: newstyles.$gray-l2;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                flex-direction: row;
+                gap: 10px;
+                font-size: newstyles.$body-size;
+                font-weight: newstyles.$body-weight;
 
-      }
+                &.loading {
+                    animation: skeleton-loading-l2 1s linear infinite alternate;
+                    min-width: 70px;
+                    height: 21.5px;
+                }
+            }
+        }
     }
-  
+
     &.dark {
-      background: $black;
+        background: newstyles.$black;
     }
-  
+
     &.light,
     &.default {
-      background: $gray-l1;
+        background: newstyles.$gray-l1;
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/newlightbox/components/EditableText/EditableText.scss b/src/client/views/newlightbox/components/EditableText/EditableText.scss
index 7828538ab..8007e8d43 100644
--- a/src/client/views/newlightbox/components/EditableText/EditableText.scss
+++ b/src/client/views/newlightbox/components/EditableText/EditableText.scss
@@ -1,34 +1,34 @@
-@import '../../NewLightboxStyles.scss';
+@use '../../NewLightboxStyles.scss' as newstyles;
 
 .lb-editableText,
 .lb-displayText {
     padding: 4px 7px !important;
-    border: $standard-border !important;
-    border-color: $gray-l2 !important;
+    border: newstyles.$standard-border !important;
+    border-color: newstyles.$gray-l2 !important;
 }
 
 .lb-editableText {
-  -webkit-appearance: none;
-  overflow: hidden;
-  font-size: inherit;
-  border: none;
-  outline: none;
-  width: 100%;
-  margin: 0px;
-  padding: 0px;
-  box-shadow: none !important;
-  background: none;
-
-  &:focus {
+    -webkit-appearance: none;
+    overflow: hidden;
+    font-size: inherit;
+    border: none;
     outline: none;
-    background-color: $blue-l1;
-  }
+    width: 100%;
+    margin: 0px;
+    padding: 0px;
+    box-shadow: none !important;
+    background: none;
+
+    &:focus {
+        outline: none;
+        background-color: newstyles.$blue-l1;
+    }
 }
 
 .lb-displayText {
-  cursor: text !important;
-  width: 100%;
-  display: flex;
-  align-items: center;
-  font-size: inherit;
-}
\ No newline at end of file
+    cursor: text !important;
+    width: 100%;
+    display: flex;
+    align-items: center;
+    font-size: inherit;
+}
diff --git a/src/client/views/newlightbox/components/Recommendation/Recommendation.scss b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss
index c86c63ba0..cf6b5ccb1 100644
--- a/src/client/views/newlightbox/components/Recommendation/Recommendation.scss
+++ b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss
@@ -1,4 +1,4 @@
-@import '../../NewLightboxStyles.scss';
+@use '../../NewLightboxStyles.scss' as newstyles;
 
 .recommendation-container {
     width: 100%;
@@ -8,22 +8,22 @@
     display: grid;
     grid-template-columns: 0% 100%;
     grid-template-rows: auto auto auto auto auto;
-    gap: 5px 0px;   
+    gap: 5px 0px;
     padding: 10px;
     cursor: pointer;
     transition: 0.2s ease;
-    border: $standard-border;
-    border-color: $gray-l2;
+    border: newstyles.$standard-border;
+    border-color: newstyles.$gray-l2;
     background: white;
 
     &:hover {
-    //   background: white !important;
-      transform: scale(1.02);
-      z-index: 0;
+        //   background: white !important;
+        transform: scale(1.02);
+        z-index: 0;
 
-      .title {
-        text-decoration: underline;
-      }
+        .title {
+            text-decoration: underline;
+        }
     }
 
     &.previewUrl {
@@ -39,18 +39,18 @@
         grid-template-rows: auto auto auto auto auto;
         gap: 5px 10px;
 
-      .image-container,
-      .title,
-      .info,
-      .source,
-      .explainer,
-      .hide-rec {
-        animation: skeleton-loading-l3 1s linear infinite alternate;
-      }
+        .image-container,
+        .title,
+        .info,
+        .source,
+        .explainer,
+        .hide-rec {
+            animation: skeleton-loading-l3 1s linear infinite alternate;
+        }
 
-      .title {
-        border-radius: 20px;
-      }
+        .title {
+            border-radius: 20px;
+        }
     }
 
     .distance-container,
@@ -64,62 +64,62 @@
     }
 
     .image-container {
-      grid-row: 2/5;
-      grid-column: 1;
-      border-radius: 20px;
-      overflow: hidden;
+        grid-row: 2/5;
+        grid-column: 1;
+        border-radius: 20px;
+        overflow: hidden;
 
-      .image {
-        width: 100%;
-        height: 100%;
-        object-fit: cover;
-      }
+        .image {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+        }
     }
 
     .title {
-      grid-row: 1;
-      grid-column: 1/3;
-      border-radius: 20px;
-      font-size: $h2-size;
-      font-weight: $h2-weight;
-      overflow: hidden;
-      border-radius: 0px;
-      min-height: 30px;
+        grid-row: 1;
+        grid-column: 1/3;
+        border-radius: 20px;
+        font-size: newstyles.$h2-size;
+        font-weight: newstyles.$h2-weight;
+        overflow: hidden;
+        border-radius: 0px;
+        min-height: 30px;
     }
 
     .info {
-      grid-row: 2;
-      grid-column: 2;
-      border-radius: 20px;
-      display: flex;
-      flex-direction: row;
-      gap: 5px;
-      font-size: $body-size;
+        grid-row: 2;
+        grid-column: 2;
+        border-radius: 20px;
+        display: flex;
+        flex-direction: row;
+        gap: 5px;
+        font-size: newstyles.$body-size;
 
         .lb-type {
             padding: 2px 7px !important;
-            background: $gray-l2;
+            background: newstyles.$gray-l2;
         }
     }
 
     .lb-label {
-        color: $gray-l3;
-        font-weight: $h1-weight;
-        font-size: $body-size;
+        color: newstyles.$gray-l3;
+        font-weight: newstyles.$h1-weight;
+        font-size: newstyles.$body-size;
     }
 
     .source {
         grid-row: 3;
         grid-column: 2;
         border-radius: 20px;
-        font-size: $body-size;
+        font-size: newstyles.$body-size;
         display: flex;
         justify-content: flex-start;
         align-items: center;
 
         .lb-source {
             padding: 2px 7px !important;
-            background: $gray-l2;
+            background: newstyles.$gray-l2;
             border-radius: 10px;
             white-space: nowrap;
             max-width: 130px;
@@ -134,7 +134,7 @@
         border-radius: 20px;
         font-size: 10px;
         width: 100%;
-        background: $blue-l1;
+        background: newstyles.$blue-l1;
         border-radius: 0;
         padding: 10px;
 
@@ -145,7 +145,7 @@
             gap: 3px;
             .concept {
                 padding: 2px 7px !important;
-                background: $gray-l2;
+                background: newstyles.$gray-l2;
             }
         }
     }
@@ -154,7 +154,7 @@
         grid-row: 5;
         grid-column: 2;
         border-radius: 20px;
-        font-size: $body-size;
+        font-size: newstyles.$body-size;
         display: flex;
         align-items: center;
         margin-top: 5px;
@@ -162,15 +162,15 @@
         justify-content: flex-end;
         text-transform: underline;
     }
-  
+
     &.dark {
-      background: $black;
-      border-color: $white;
+        background: newstyles.$black;
+        border-color: newstyles.$white;
     }
-  
+
     &.light,
     &.default {
-      background: $white;
-      border-color: $white;
+        background: newstyles.$white;
+        border-color: newstyles.$white;
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss
index e541e3f3c..bbc730144 100644
--- a/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss
+++ b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss
@@ -1,82 +1,81 @@
-@import '../../NewLightboxStyles.scss';
+@use '../../NewLightboxStyles.scss' as newstyles;
 
 .skeletonDoc-container {
-  display: flex;
-  flex-direction: column;
-  height: calc(100% - 40px);
-  margin: 20px;
-  gap: 20px;
+    display: flex;
+    flex-direction: column;
+    height: calc(100% - 40px);
+    margin: 20px;
+    gap: 20px;
 
-  .header {
-    width: calc(100% - 20px);
-    height: 80px;
-    background: $gray-l2;
-    animation: skeleton-loading-l2 1s linear infinite alternate; 
-    display: grid;
-    grid-template-rows: 60% 40%;
-    padding: 10px;
-    grid-template-columns: auto auto auto auto;
-    border-radius: 20px;
+    .header {
+        width: calc(100% - 20px);
+        height: 80px;
+        background: newstyles.$gray-l2;
+        animation: skeleton-loading-l2 1s linear infinite alternate;
+        display: grid;
+        grid-template-rows: 60% 40%;
+        padding: 10px;
+        grid-template-columns: auto auto auto auto;
+        border-radius: 20px;
 
-    .title {
-      grid-row: 1;
-      grid-column: 1 / 5;
-      display: flex;
-      width: fit-content;
-      height: 100%;
-      min-width: 500px;
-      font-size: $title-size;
-      animation: skeleton-loading-l3 1s linear infinite alternate;
-      border-radius: 20px;
-    }
+        .title {
+            grid-row: 1;
+            grid-column: 1 / 5;
+            display: flex;
+            width: fit-content;
+            height: 100%;
+            min-width: 500px;
+            font-size: newstyles.$title-size;
+            animation: skeleton-loading-l3 1s linear infinite alternate;
+            border-radius: 20px;
+        }
 
-    .type {
-      display: flex;
-      padding: 3px 7px;
-      width: fit-content;
-      height: fit-content;
-      margin-top: 8px;
-      min-height: 15px;
-      min-width: 60px;
-      grid-row: 2;
-      grid-column: 1;
-      animation: skeleton-loading-l3 1s linear infinite alternate;
-      border-radius: 20px;
-    }
+        .type {
+            display: flex;
+            padding: 3px 7px;
+            width: fit-content;
+            height: fit-content;
+            margin-top: 8px;
+            min-height: 15px;
+            min-width: 60px;
+            grid-row: 2;
+            grid-column: 1;
+            animation: skeleton-loading-l3 1s linear infinite alternate;
+            border-radius: 20px;
+        }
 
-    .buttons-container {
-      grid-row: 1 / 3;
-      grid-column: 5;
-      display: flex;
-      justify-content: flex-end;
-      align-items: center;
-      gap: 10px;
+        .buttons-container {
+            grid-row: 1 / 3;
+            grid-column: 5;
+            display: flex;
+            justify-content: flex-end;
+            align-items: center;
+            gap: 10px;
 
-      .button {
-        width: 50px;
-        height: 50px;
-        border-radius: 100%;
-        animation: skeleton-loading-l3 1s linear infinite alternate;
-      }
+            .button {
+                width: 50px;
+                height: 50px;
+                border-radius: 100%;
+                animation: skeleton-loading-l3 1s linear infinite alternate;
+            }
+        }
     }
 
-  }
-
-  .content {
-    width: 100%;
-    flex: 1;
-    -webkit-flex: 1; /* Chrome */
-    background: $gray-l2;
-    animation: skeleton-loading-l2 1s linear infinite alternate;
-    border-radius: 20px;
-  }
+    .content {
+        width: 100%;
+        flex: 1;
+        -webkit-flex: 1; /* Chrome */
+        background: newstyles.$gray-l2;
+        animation: skeleton-loading-l2 1s linear infinite alternate;
+        border-radius: 20px;
+    }
 
     // &.dark {
-    //   background: $black;
+    //   background: newstyles.$black;
     // }
-  
+
     // &.light,
     // &.default {
-    //   background: $white;
+    //   background: newstyles.$white;
     // }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/newlightbox/components/Template/Template.scss b/src/client/views/newlightbox/components/Template/Template.scss
index 5b72ddaf9..c2fb9fba4 100644
--- a/src/client/views/newlightbox/components/Template/Template.scss
+++ b/src/client/views/newlightbox/components/Template/Template.scss
@@ -1,15 +1,15 @@
-@import '../../NewLightboxStyles.scss';
+@use '../../NewLightboxStyles.scss' as newstyles;
 
 .template-container {
     width: 100vw;
     height: 100vh;
-  
+
     &.dark {
-      background: $black;
+        background: newstyles.$black;
     }
-  
+
     &.light,
     &.default {
-      background: $white;
+        background: newstyles.$white;
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 4337401e3..933a383ea 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .audiobox-container {
     width: 100%;
@@ -19,30 +19,30 @@
 
     .audiobox-dictation {
         width: 40px;
-        background: $medium-gray;
-        color: $dark-gray;
+        background: global.$medium-gray;
+        color: global.$dark-gray;
         display: flex;
         justify-content: center;
         align-items: center;
 
         &:hover {
-            color: $black;
+            color: global.$black;
         }
     }
 
     .audiobox-start-record {
-        color: $white;
-        background: $dark-gray;
+        color: global.$white;
+        background: global.$dark-gray;
         display: flex;
         align-items: center;
         justify-content: center;
-        font-size: $body-text;
+        font-size: global.$body-text;
         width: 100%;
         height: 100%;
         gap: 5px;
 
         &:hover {
-            background: $black;
+            background: global.$black;
         }
     }
 
@@ -54,11 +54,11 @@
         gap: 5px;
         width: 100%;
         height: 100%;
-        background: $dark-gray;
+        background: global.$dark-gray;
         color: white;
 
         .record-timecode {
-            font-size: $large-header;
+            font-size: global.$large-header;
         }
 
         .record-button {
@@ -66,7 +66,7 @@
             width: 30px;
             height: 30px;
             border-radius: 50%;
-            background: $dark-gray;
+            background: global.$dark-gray;
             display: flex;
             align-items: center;
             justify-content: center;
@@ -76,7 +76,7 @@
             }
 
             &:hover {
-                background: $black;
+                background: global.$black;
             }
         }
     }
@@ -87,10 +87,10 @@
     display: flex;
     flex-direction: column;
     align-items: center;
-    background: $dark-gray;
+    background: global.$dark-gray;
     width: 100%;
     height: 100%;
-    color: $white;
+    color: global.$white;
 
     .audiobox-button {
         margin: 2.5px;
@@ -98,7 +98,7 @@
         width: 25px;
         height: 25px;
         border-radius: 50%;
-        background: $dark-gray;
+        background: global.$dark-gray;
         display: flex;
         align-items: center;
         justify-content: center;
@@ -108,7 +108,7 @@
         }
 
         &:hover {
-            background: $black;
+            background: global.$black;
         }
     }
 
@@ -132,7 +132,7 @@
         height: 6px;
         cursor: pointer;
         box-shadow: 0;
-        background: $light-gray;
+        background: global.$light-gray;
         border-radius: 3px;
     }
 
@@ -142,7 +142,7 @@
         height: 10px;
         width: 10px;
         border-radius: 10px;
-        background: $medium-blue;
+        background: global.$medium-blue;
         cursor: pointer;
         -webkit-appearance: none;
         margin-top: -2px;
@@ -180,12 +180,12 @@
     .audiobox-playback {
         width: 100%;
         height: 100%;
-        background: $white;
+        background: global.$white;
 
         .audiobox-timeline {
             height: calc(100% - 50px);
             width: 100%;
-            background: $white;
+            background: global.$white;
             position: absolute;
         }
 
@@ -203,7 +203,7 @@
         width: 100%;
         height: 20px;
         padding: 3px;
-        font-size: $small-text;
+        font-size: global.$small-text;
 
         .bottom-controls-middle {
             display: flex;
diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss
index 0eb27b65b..ff1fa343d 100644
--- a/src/client/views/nodes/DataVizBox/components/Chart.scss
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -1,4 +1,4 @@
-@import '../../../global/globalCssVariables.module.scss';
+@use '../../../global/globalCssVariables.module.scss' as global;
 .chart-container {
     display: flex;
     flex-direction: column;
@@ -108,7 +108,7 @@
             }
         }
         tr td {
-            height: $DATA_VIZ_TABLE_ROW_HEIGHT !important; // bcz: hack.  you can't set a <tr> height directly, but you can set the height of all of it's <td>s.  So this is the height of a tableBox row.
+            height: global.$DATA_VIZ_TABLE_ROW_HEIGHT !important; // bcz: hack.  you can't set a <tr> height directly, but you can set the height of all of it's <td>s.  So this is the height of a tableBox row.
             padding: 0 !important;
             vertical-align: middle !important;
         }
@@ -135,7 +135,7 @@
 }
 
 .tableBox-filterPopup {
-    background: $light-gray;
+    background: global.$light-gray;
     position: absolute;
     min-width: 235px;
     top: 60px;
@@ -152,7 +152,7 @@
         .tableBox-filterPopup-selectColumn-each {
             margin-left: 25px;
             border-radius: 3px;
-            background: $light-gray;
+            background: global.$light-gray;
         }
     }
     .tableBox-filterPopup-setValue {
@@ -162,7 +162,7 @@
         .tableBox-filterPopup-setValue-each {
             margin-right: 5px;
             border-radius: 3px;
-            background: $light-gray;
+            background: global.$light-gray;
         }
         .tableBox-filterPopup-setValue-input {
             margin: 5px;
diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss
index b32b27e65..e1b83dc59 100644
--- a/src/client/views/nodes/DocumentLinksButton.scss
+++ b/src/client/views/nodes/DocumentLinksButton.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .documentLinksButton-wrapper {
     transform-origin: top left;
@@ -29,7 +29,7 @@
     pointer-events: auto;
     display: flex;
     align-items: center;
-    background-color: $light-blue;
+    background-color: global.$light-blue;
     color: black;
 }
 .documentLinksButton,
@@ -59,30 +59,30 @@
     }
 }
 .documentLinksButton {
-    background-color: $dark-gray;
-    color: $white;
+    background-color: global.$dark-gray;
+    color: global.$white;
     font-weight: bold;
     font-size: 100%;
     font-family: 'Roboto';
     transition: 0.2s ease all;
 
     &:hover {
-        background-color: $black;
+        background-color: global.$black;
     }
 }
 .documentLinksButton.startLink {
-    background-color: $medium-blue;
+    background-color: global.$medium-blue;
     width: 75%;
     height: 75%;
-    color: $white;
+    color: global.$white;
     font-weight: bold;
     font-size: 100%;
     transition: 0.2s ease all;
 }
 
 .documentLinksButton-endLink {
-    border: $medium-blue 2px dashed;
-    color: $medium-blue;
+    border: global.$medium-blue 2px dashed;
+    color: global.$medium-blue;
     background-color: none !important;
     font-size: 100%;
     transition: 0.2s ease all;
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 7e5507586..294af4d96 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .documentView-effectsWrapper {
     border-radius: inherit;
@@ -28,7 +28,7 @@
     // overflow: hidden; // need this so that title will be clipped when borderRadius is set
     //  transition: outline 0.3s linear;
 
-    // background: $white; //overflow: hidden;
+    // background: global.$white; //overflow: hidden;
     transform-origin: center;
 
     &.minimized {
@@ -180,7 +180,7 @@
 
         .documentView-titleWrapper,
         .documentView-titleWrapper-hover {
-            color: $black;
+            color: global.$black;
             transform-origin: top left;
             top: 0;
             width: 100%;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 441d6053f..b40ead03a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -778,7 +778,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         style={{
                             width: `${100 / this.uiBtnScaling}%`, //
                             transform: `scale(${this.uiBtnScaling})`,
-                            bottom: this.maxWidgetSize,
+                            bottom: Number.isNaN(this.maxWidgetSize) ? undefined : this.maxWidgetSize,
                         }}>
                         {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null}
                     </div>
diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss
index 55e0f5184..bcbb44e68 100644
--- a/src/client/views/nodes/EquationBox.scss
+++ b/src/client/views/nodes/EquationBox.scss
@@ -1,5 +1,3 @@
-@import '../global/globalCssVariables.module.scss';
-
 .equationBox-cont {
     transform-origin: center;
     width: fit-content;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index 2405889cf..186d24e92 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -1,4 +1,4 @@
-@import '../../global/globalCssVariables.module.scss';
+@use '../../global/globalCssVariables.module.scss' as global;
 
 // bcz: something's messed up with the IconButton css.  this mostly fixes the fit-all button, the color buttons, the undo +/- expander and the dropdown doc type list (eg 'text')
 .iconButton-container {
@@ -23,7 +23,7 @@
     justify-content: center;
     align-items: center;
     font-size: 80%;
-    border-radius: $standard-border-radius;
+    border-radius: global.$standard-border-radius;
     transition: 0.15s;
 
     .menuButton-wrap {
@@ -34,7 +34,7 @@
     }
 
     .fontIconBox-label {
-        color: $white;
+        color: global.$white;
         bottom: -1;
         position: absolute;
         text-align: center;
@@ -124,17 +124,17 @@
                 width: 21px;
                 left: 2px;
                 bottom: 2px;
-                background-color: $white;
+                background-color: global.$white;
                 -webkit-transition: 0.4s;
                 transition: 0.4s;
             }
 
             input:checked + .slider {
-                background-color: $medium-blue;
+                background-color: global.$medium-blue;
             }
 
             input:focus + .slider {
-                box-shadow: 0 0 1px $medium-blue;
+                box-shadow: 0 0 1px global.$medium-blue;
             }
 
             input:checked + .slider:before {
@@ -145,11 +145,11 @@
 
             /* Rounded sliders */
             .slider.round {
-                border-radius: $standard-border-radius;
+                border-radius: global.$standard-border-radius;
             }
 
             .slider.round:before {
-                border-radius: $standard-border-radius;
+                border-radius: global.$standard-border-radius;
             }
         }
 
@@ -259,12 +259,12 @@
             height: fit-content;
             top: 100%;
             z-index: 21;
-            background-color: $white;
+            background-color: global.$white;
             box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
             padding: 1px;
 
             .list-item {
-                color: $black;
+                color: global.$black;
                 width: 100%;
                 height: 25px;
                 font-weight: 400;
@@ -285,7 +285,7 @@
         background: transparent;
 
         &.slider {
-            color: $white;
+            color: global.$white;
             cursor: pointer;
             flex-direction: column;
             background: transparent;
@@ -302,7 +302,7 @@
                 z-index: 21;
                 background-color: #e3e3e3;
                 box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
-                border-radius: $standard-border-radius;
+                border-radius: global.$standard-border-radius;
 
                 .menu-slider {
                     height: 10px;
@@ -340,7 +340,7 @@
                     border: none;
                     text-align: right;
                     width: 100%;
-                    color: $white;
+                    color: global.$white;
                     height: 100%;
                     text-align: center;
                 }
@@ -354,7 +354,7 @@
         &.list {
             width: 100%;
             justify-content: space-around;
-            border: $standard-border;
+            border: global.$standard-border;
 
             .menuButton-dropdownList {
                 position: absolute;
@@ -365,12 +365,12 @@
                 overflow-y: scroll;
                 top: 100%;
                 z-index: 21;
-                background-color: $white;
+                background-color: global.$white;
                 box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
                 padding: 1px;
 
                 .list-item {
-                    color: $black;
+                    color: global.$black;
                     width: 100%;
                     height: 25px;
                     font-weight: 400;
@@ -394,7 +394,7 @@
         padding-left: 10px;
         justify-content: flex-start;
         color: black;
-        background-color: $light-gray;
+        background-color: global.$light-gray;
         padding: 5px;
         padding-left: 10px;
         width: 100%;
@@ -417,7 +417,7 @@
             top: 100%;
             background-color: #e3e3e3;
             box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
-            border-radius: $standard-border-radius;
+            border-radius: global.$standard-border-radius;
         }
     }
 
diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss
index c79d662f4..202b0c701 100644
--- a/src/client/views/nodes/IconTagBox.scss
+++ b/src/client/views/nodes/IconTagBox.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .card-button-container {
     display: flex;
@@ -18,7 +18,7 @@
         margin: auto;
         padding: 0;
         border-radius: 50%;
-        background-color: $dark-gray;
+        background-color: global.$dark-gray;
         background-color: transparent;
     }
 }
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index a44f614b2..441fceba4 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -1,11 +1,11 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 .keyValueBox-cont {
     overflow-y: scroll;
     width: 100%;
     height: 100%;
-    background-color: $white;
-    border: 1px solid $medium-gray;
-    border-radius: $border-radius;
+    background-color: global.$white;
+    border: 1px solid global.$medium-gray;
+    border-radius: global.$border-radius;
     box-sizing: border-box;
     display: inline-block;
     cursor: default;
@@ -56,8 +56,8 @@ $header-height: 30px;
     width: 100%;
     position: relative;
     display: inline-block;
-    background: $medium-gray;
-    color: $white;
+    background: global.$medium-gray;
+    color: global.$white;
     text-transform: uppercase;
     letter-spacing: 2px;
     font-size: 12px;
@@ -66,7 +66,7 @@ $header-height: 30px;
     th {
         font-weight: normal;
         &:first-child {
-            border-right: 1px solid $white;
+            border-right: 1px solid global.$white;
         }
     }
 }
@@ -76,9 +76,9 @@ $header-height: 30px;
     display: flex;
     width: 100%;
     height: $header-height;
-    background: $white;
+    background: global.$white;
     .formattedTextBox-cont {
-        background: $white;
+        background: global.$white;
     }
 }
 .keyValueBox-cont {
@@ -116,8 +116,8 @@ $header-height: 30px;
     display: flex;
     width: 100%;
     height: 30px;
-    background: $light-gray;
+    background: global.$light-gray;
     .formattedTextBox-cont {
-        background: $light-gray;
+        background: global.$light-gray;
     }
 }
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index 46ea9c18e..913ab641c 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .keyValuePair-td-key {
     display: inline-block;
diff --git a/src/client/views/nodes/LinkDescriptionPopup.scss b/src/client/views/nodes/LinkDescriptionPopup.scss
index 104301656..b44b69af5 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.scss
+++ b/src/client/views/nodes/LinkDescriptionPopup.scss
@@ -1,12 +1,12 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .linkDescriptionPopup {
     display: flex;
     flex-direction: row;
     justify-content: center;
     align-items: center;
-    border: 2px solid $medium-blue;
-    background-color: $white;
+    border: 2px solid global.$medium-blue;
+    background-color: global.$white;
     width: auto;
     position: absolute;
 
@@ -35,7 +35,7 @@
             white-space: nowrap;
             padding: 5px;
             vertical-align: middle;
-            background-color: $close-red;
+            background-color: global.$close-red;
             border-radius: 3px;
             color: black;
         }
@@ -46,7 +46,7 @@
             white-space: nowrap;
             padding: 5px;
             vertical-align: middle;
-            background-color: $light-blue;
+            background-color: global.$light-blue;
             border-radius: 3px;
             color: black;
         }
diff --git a/src/client/views/nodes/MapBox/AnimationUtility.ts b/src/client/views/nodes/MapBox/AnimationUtility.ts
index f4bae66bb..3eac50f1f 100644
--- a/src/client/views/nodes/MapBox/AnimationUtility.ts
+++ b/src/client/views/nodes/MapBox/AnimationUtility.ts
@@ -65,7 +65,7 @@ export class AnimationUtility {
         const coords: mapboxgl.LngLatLike = [this.previousLngLat.lng, this.previousLngLat.lat];
         // console.log('MAP REF: ', this.MAP_REF)
         // console.log("current elevation: ", this.MAP_REF?.queryTerrainElevation(coords));
-        let altitude = this.MAP_REF ? this.MAP_REF.queryTerrainElevation(coords) ?? 0 : 0;
+        let altitude = this.MAP_REF ? (this.MAP_REF.queryTerrainElevation(coords) ?? 0) : 0;
         if (altitude === 0) {
             altitude += 50;
         }
@@ -178,7 +178,7 @@ export class AnimationUtility {
 
         this.ROUTE_COORDINATES = routeCoordinates;
         this.PATH = turf.lineString(routeCoordinates);
-        this.PATH_DISTANCE = turf.lineDistance(this.PATH);
+        this.PATH_DISTANCE = turf.length(this.PATH);
         this.terrainDisplayed = terrainDisplayed;
 
         const bearing = this.calculateBearing(
diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss
index 25b4587a5..fdd8a29d7 100644
--- a/src/client/views/nodes/MapBox/MapBox.scss
+++ b/src/client/views/nodes/MapBox/MapBox.scss
@@ -1,4 +1,6 @@
-@import '../../global/globalCssVariables.module.scss';
+@use 'sass:color';
+@use '../../global/globalCssVariables.module.scss' as global;
+
 .mapBox {
     width: 100%;
     height: 100%;
@@ -25,14 +27,6 @@
         gap: 5px;
         align-items: center;
         width: calc(100% - 40px);
-
-        // .editableText-container {
-        //     width: 100%;
-        //     font-size: 16px !important;
-        // }
-        // input {
-        //     width: 100%;
-        // }
     }
 
     .mapbox-settings-panel {
@@ -83,7 +77,7 @@
             width: 100%;
             padding: 10px;
             &:hover {
-                background-color: lighten(rgb(187, 187, 187), 10%);
+                background-color: color.adjust(rgb(187, 187, 187), $lightness: 10%);
             }
         }
     }
@@ -167,7 +161,7 @@
         pointer-events: all;
         z-index: 1; // so it appears on top of the document's title, if shown
 
-        box-shadow: $standard-box-shadow;
+        box-shadow: global.$standard-box-shadow;
         transition: 0.2s;
 
         &:hover {
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index f6908d5fd..f2160feb7 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .pdfBox,
 .pdfBox-interactive {
@@ -22,11 +22,11 @@
 
         // glr: This should really be the same component as text and PDFs
         .pdfBox-sidebarBtn {
-            background: $black;
+            background: global.$black;
             height: 25px;
             width: 25px;
             right: 5px;
-            color: $white;
+            color: global.$white;
             display: flex;
             position: absolute;
             align-items: center;
@@ -35,7 +35,7 @@
             pointer-events: all;
             z-index: 1; // so it appears on top of the document's title, if shown
 
-            box-shadow: $standard-box-shadow;
+            box-shadow: global.$standard-box-shadow;
             transition: 0.2s;
 
             &:hover {
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 460155446..b5405f0fb 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .mini-viewer {
     cursor: grab;
@@ -22,7 +22,7 @@
         height: 100%;
         border-radius: inherit;
         opacity: 0.99; // hack!  overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger
-        background: $dark-gray;
+        background: global.$dark-gray;
     }
 
     .inkingCanvas-paths-markers {
@@ -93,7 +93,7 @@
     align-items: center;
     justify-content: center;
     display: flex;
-    background-color: $dark-gray;
+    background-color: global.$dark-gray;
     color: white;
     border-radius: 100px;
     height: 40px;
@@ -128,13 +128,13 @@
         width: 25px;
         height: 25px;
         border-radius: 50%;
-        background: $dark-gray;
+        background: global.$dark-gray;
         display: flex;
         align-items: center;
         justify-content: center;
 
         &:hover {
-            background: $black;
+            background: global.$black;
         }
 
         svg {
@@ -157,7 +157,7 @@
     cursor: pointer;
 
     &:hover {
-        background-color: $medium-gray;
+        background-color: global.$medium-gray;
     }
 }
 
@@ -198,7 +198,7 @@ input[type='range']::-webkit-slider-runnable-track {
     height: 10px;
     cursor: pointer;
     box-shadow: 0;
-    background: $light-gray;
+    background: global.$light-gray;
     border-radius: 10px;
 }
 
@@ -208,7 +208,7 @@ input[type='range']::-webkit-slider-thumb {
     height: 12px;
     width: 12px;
     border-radius: 10px;
-    background: $medium-blue;
+    background: global.$medium-blue;
     cursor: pointer;
     -webkit-appearance: none;
     margin-top: -1px;
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index a1686adaf..05d5babf9 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .webBox {
     height: 100%;
@@ -120,7 +120,7 @@
         pointer-events: all;
         z-index: 1; // so it appears on top of the document's title, if shown
 
-        box-shadow: $standard-box-shadow;
+        box-shadow: global.$standard-box-shadow;
         transition: 0.2s;
 
         &:hover {
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
index 9cf760a12..3d27fa887 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
@@ -1,3 +1,4 @@
+@use 'sass:color';
 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
 
 $primary-color: #3f51b5;
@@ -68,7 +69,7 @@ $transition: all 0.2s ease-in-out;
             &:focus {
                 outline: none;
                 border-color: $primary-color;
-                box-shadow: 0 0 0 2px rgba($primary-color, 0.2);
+                box-shadow: 0 0 0 2px color.adjust($primary-color, $alpha: -0.8);
             }
 
             &:disabled {
@@ -92,11 +93,11 @@ $transition: all 0.2s ease-in-out;
             transition: $transition;
 
             &:hover {
-                background-color: darken($primary-color, 10%);
+                background-color: color.adjust($primary-color, $lightness: -10%);
             }
 
             &:disabled {
-                background-color: lighten($primary-color, 20%);
+                background-color: color.adjust($primary-color, $lightness: 20%);
                 cursor: not-allowed;
             }
 
@@ -178,7 +179,7 @@ $transition: all 0.2s ease-in-out;
         margin-bottom: 16px;
 
         &:hover {
-            background-color: rgba($primary-color, 0.1);
+            background-color: color.adjust($primary-color, $alpha: -0.9);
         }
     }
 
@@ -220,7 +221,7 @@ $transition: all 0.2s ease-in-out;
             transition: $transition;
 
             &:hover {
-                background-color: rgba($primary-color, 0.2);
+                background-color: color.adjust($primary-color, $alpha: -0.8);
                 color: #fff;
             }
         }
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
index d79df4272..78bbb520e 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.scss
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -1,4 +1,4 @@
-@import '../../global/globalCssVariables.module.scss';
+@use '../../global/globalCssVariables.module.scss' as global;
 
 .dashFieldView-active,
 .dashFieldView {
@@ -64,5 +64,5 @@
 }
 
 .ProseMirror-selectedNode {
-    outline: solid 1px $light-blue !important;
+    outline: solid 1px global.$light-blue !important;
 }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 84859b94d..f9de4ab5a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -1,4 +1,4 @@
-@import '../../global/globalCssVariables.module.scss';
+@use '../../global/globalCssVariables.module.scss' as global;
 
 .ProseMirror {
     width: 100%;
@@ -22,7 +22,7 @@
 
     &.h-left * {
         display: flex;
-        justify-content: flex-start; 
+        justify-content: flex-start;
     }
 
     &.h-right * {
@@ -32,7 +32,7 @@
 
     &.template * {
         ::-webkit-scrollbar-track {
-            background: none; 
+            background: none;
         }
     }
 
@@ -64,7 +64,7 @@ audiotag:hover {
     background: inherit;
     padding: 0;
     border-width: 0px;
-    border-color: $medium-gray;
+    border-color: global.$medium-gray;
     box-sizing: border-box;
     background-color: inherit;
     border-style: solid;
@@ -79,7 +79,6 @@ audiotag:hover {
     transform-origin: left top;
     top: 0;
     left: 0;
-    
 }
 
 .formattedTextBox-cont {
@@ -88,7 +87,7 @@ audiotag:hover {
     padding: 0;
     border-width: 0px;
     border-radius: inherit;
-    border-color: $medium-gray;
+    border-color: global.$medium-gray;
     box-sizing: border-box;
     background-color: inherit;
     border-style: solid;
@@ -147,13 +146,13 @@ audiotag:hover {
     font-size: 11px;
     border-radius: 3px;
     color: white;
-    background: $medium-gray;
+    background: global.$medium-gray;
     border-radius: 5px;
     display: flex;
     justify-content: center;
     align-items: center;
     cursor: grabbing;
-    box-shadow: $standard-box-shadow;
+    box-shadow: global.$standard-box-shadow;
     // transition: 0.2s;
     opacity: 0.3;
     &:hover {
@@ -646,7 +645,7 @@ footnote::before {
 }
 
 @media only screen and (max-width: 1000px) {
-    @import '../../global/globalCssVariables.module.scss';
+    // @import '../../global/globalCssVariables.module.scss';
 
     .ProseMirror {
         width: 100%;
@@ -664,7 +663,7 @@ footnote::before {
         padding: 0;
         border-width: 0px;
         border-radius: inherit;
-        border-color: $medium-gray;
+        border-color: global.$medium-gray;
         box-sizing: border-box;
         background-color: inherit;
         border-style: solid;
@@ -1074,4 +1073,3 @@ footnote::before {
         }
     }
 }
-
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss
index d6ed5ebee..fcc816447 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.scss
+++ b/src/client/views/nodes/formattedText/RichTextMenu.scss
@@ -1,4 +1,4 @@
-@import '../../global/globalCssVariables.module.scss';
+@use '../../global/globalCssVariables.module.scss' as global;
 
 .button-dropdown-wrapper {
     position: relative;
@@ -25,7 +25,7 @@
         top: 35px;
         left: 0;
         background-color: #323232;
-        color: $light-gray;
+        color: global.$light-gray;
         border: 1px solid #4d4d4d;
         border-radius: 0 6px 6px 6px;
         box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 3c0ab3da5..657e689bb 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -610,7 +610,9 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                             <Slider
                                 sx={{
                                     '& input[type="range"]': {
-                                        WebkitAppearance: 'slider-vertical',
+                                        writingMode: 'vertical-lr',
+                                        direction: 'rtl',
+                                        // WebkitAppearance: 'slider-vertical',
                                     },
                                 }}
                                 orientation="vertical"
@@ -626,7 +628,9 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                             <Slider
                                 sx={{
                                     '& input[type="range"]': {
-                                        WebkitAppearance: 'slider-vertical',
+                                        writingMode: 'vertical-lr',
+                                        direction: 'rtl',
+                                        // WebkitAppearance: 'slider-vertical',
                                     },
                                 }}
                                 orientation="vertical"
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index e34e1b380..e24b47bd1 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -1,4 +1,4 @@
-@import '../../global/globalCssVariables.module.scss';
+@use '../../global/globalCssVariables.module.scss' as global;
 
 .presBox-gpt-chat {
     padding: 16px;
@@ -203,8 +203,8 @@
         align-items: center;
         height: 30px;
         width: 100%;
-        color: $white;
-        background-color: $dark-gray;
+        color: global.$white;
+        background-color: global.$dark-gray;
 
         .toolbar-button {
             cursor: pointer;
@@ -218,7 +218,7 @@
         }
 
         .toolbar-button.active {
-            color: $light-blue;
+            color: global.$light-blue;
             background-color: white;
             border-radius: 100%;
         }
@@ -266,7 +266,7 @@
     }
 
     .toolbar-divider {
-        border-left: solid $medium-gray 0.5px;
+        border-left: solid global.$medium-gray 0.5px;
         height: 20px;
     }
 }
@@ -274,13 +274,13 @@
 .dropdown {
     font-size: 10;
     margin-left: 5px;
-    color: $medium-gray;
+    color: global.$medium-gray;
     transition: 0.5s ease;
 }
 
 .dropdown.active {
     transform: rotate(180deg);
-    color: $light-blue;
+    color: global.$light-blue;
     opacity: 0.7;
 }
 
@@ -340,7 +340,7 @@
 
     .ribbon-colorBox {
         cursor: pointer;
-        border: solid 1px $black;
+        border: solid 1px global.$black;
         display: flex;
         margin-left: 5px;
         margin-top: 5px;
@@ -387,7 +387,7 @@
         }
 
         .ribbon-propertyUpDownItem:hover {
-            background: $medium-gray;
+            background: global.$medium-gray;
             transform: scale(1.05);
         }
     }
@@ -413,7 +413,7 @@
     @media screen and (-webkit-min-device-pixel-ratio: 0) {
         .multiThumb-slider {
             display: grid;
-            background-color: $white;
+            background-color: global.$white;
             height: 10px;
             border-radius: 10px;
             overflow: hidden;
@@ -431,8 +431,8 @@
                 -webkit-appearance: none;
                 height: 10px;
                 cursor: ew-resize;
-                background: $medium-blue;
-                box-shadow: -100vw 0 0 100vw $white;
+                background: global.$medium-blue;
+                box-shadow: -100vw 0 0 100vw global.$white;
             }
 
             .toolbar-slider.end::-webkit-slider-thumb {
@@ -441,8 +441,8 @@
                 -webkit-appearance: none;
                 height: 10px;
                 cursor: ew-resize;
-                background: $medium-blue;
-                box-shadow: -100vw 0 0 100vw $light-blue;
+                background: global.$medium-blue;
+                box-shadow: -100vw 0 0 100vw global.$light-blue;
             }
         }
 
@@ -456,7 +456,7 @@
             height: 10px;
             border-radius: 10px;
             -webkit-appearance: none;
-            background-color: $white;
+            background-color: global.$white;
         }
 
         .toolbar-slider:focus {
@@ -476,8 +476,8 @@
             -webkit-appearance: none;
             height: 10px;
             cursor: ew-resize;
-            background: $medium-blue;
-            box-shadow: -100vw 0 0 100vw $light-blue;
+            background: global.$medium-blue;
+            box-shadow: -100vw 0 0 100vw global.$light-blue;
         }
 
         .presBox-checkbox {
@@ -493,7 +493,7 @@
             width: 15px;
             min-width: 15px;
             cursor: pointer;
-            background: $white;
+            background: global.$white;
         }
 
         .presBox-checkbox:focus {
@@ -501,11 +501,11 @@
         }
 
         .presBox-checkbox:hover {
-            background: $light-gray;
+            background: global.$light-gray;
         }
 
         .presBox-checkbox:checked {
-            background: $light-blue;
+            background: global.$light-blue;
         }
     }
 
@@ -554,9 +554,9 @@
         text-align: center;
         font-size: 16;
         width: 90%;
-        color: $black;
+        color: global.$black;
         transform: translate(5%, 0px);
-        border-bottom: solid 2px $medium-gray;
+        border-bottom: solid 2px global.$medium-gray;
     }
 
     .ribbon-textInput {
@@ -568,8 +568,8 @@
         justify-self: left;
         margin-top: 5px;
         padding-left: 10px;
-        background-color: $white;
-        border: solid 1px $black;
+        background-color: global.$white;
+        border: solid 1px global.$black;
         min-width: 80px;
         max-width: 200px;
         width: 100%;
@@ -590,7 +590,7 @@
     }
 
     .ribbon-frameSelector {
-        border: $black solid 1px;
+        border: global.$black solid 1px;
         width: 60px;
         height: 20px;
         margin-top: 5px;
@@ -607,12 +607,12 @@
             cursor: pointer;
             position: relative;
             height: 100%;
-            background: $white;
+            background: global.$white;
             display: flex;
             align-items: center;
             justify-content: center;
             text-align: center;
-            color: $black;
+            color: global.$black;
         }
 
         .numKeyframe {
@@ -620,7 +620,7 @@
             font-size: 10;
             font-weight: 600;
             position: relative;
-            color: $black;
+            color: global.$black;
             display: flex;
             width: 100%;
             height: 100%;
@@ -662,7 +662,7 @@
             padding-left: 10;
             padding-right: 10;
             border-radius: 10px;
-            background-color: $medium-gray;
+            background-color: global.$medium-gray;
         }
 
         .ribbon-final-button:hover {
@@ -681,13 +681,13 @@
             align-items: center;
             margin-bottom: 5px;
             height: 25px;
-            color: $light-gray;
+            color: global.$light-gray;
             width: 100%;
             max-width: 120;
             padding-left: 10;
             padding-right: 10;
             border-radius: 10px;
-            background-color: $black;
+            background-color: global.$black;
         }
 
         .ribbon-final-button-hidden:hover {
@@ -698,15 +698,15 @@
         .ribbon-frameList {
             width: calc(100% - 5px);
             height: 50px;
-            background-color: $white;
-            border: 1px solid $medium-gray;
+            background-color: global.$white;
+            border: 1px solid global.$medium-gray;
             grid-template-rows: max-content;
 
             .frameList-header {
                 display: grid;
                 width: 100%;
                 height: 20px;
-                background-color: $medium-gray;
+                background-color: global.$medium-gray;
 
                 .frameList-headerButtons {
                     display: flex;
@@ -761,7 +761,7 @@
         font-size: 10.5;
         font-weight: 300;
         height: 20;
-        background-color: $medium-gray;
+        background-color: global.$medium-gray;
         color: white;
         display: flex;
         margin-top: 5px;
@@ -780,8 +780,8 @@
         transition: all 0.4s;
         font-weight: 400;
         opacity: 1;
-        color: $white;
-        background-color: $black;
+        color: global.$white;
+        background-color: global.$black;
     }
 
     .ribbon-toggle {
@@ -789,9 +789,9 @@
         font-size: 10.5;
         font-weight: 200;
         height: 20;
-        background-color: $white;
+        background-color: global.$white;
         display: inline-flex;
-        color: $black;
+        color: global.$black;
         border-radius: 5px;
         width: max-content;
         justify-content: center;
@@ -831,13 +831,13 @@
         position: relative;
         font-size: 13;
         padding-bottom: 10px;
-        border-bottom: solid 1px $dark-gray;
+        border-bottom: solid 1px global.$dark-gray;
 
         .presBox-dropdown:hover {
-            border: solid 1px $medium-blue;
+            border: solid 1px global.$medium-blue;
 
             .presBox-dropdownIcon {
-                color: $medium-blue;
+                color: global.$medium-blue;
             }
         }
 
@@ -846,12 +846,12 @@
             display: grid;
             grid-template-columns: auto 20%;
             position: relative;
-            border: solid 1px $black;
-            background-color: $light-gray;
+            border: solid 1px global.$black;
+            background-color: global.$light-gray;
             border-radius: 5px;
             font-size: 10;
             height: 25;
-            color: $black;
+            color: global.$black;
             padding-left: 5px;
             align-items: center;
             margin-top: 5px;
@@ -917,7 +917,7 @@
     height: 100px;
     padding-top: 5px;
     padding-bottom: 5px;
-    border: solid 1px $black;
+    border: solid 1px global.$black;
     // overflow: auto;
 
     ::-webkit-scrollbar {
@@ -967,7 +967,7 @@
     cursor: pointer;
     position: relative;
     text-align: center;
-    border-left: solid 1px $medium-gray;
+    border-left: solid 1px global.$medium-gray;
     width: 20%;
     height: 100%;
     display: flex;
@@ -998,7 +998,7 @@
     box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.8);
     z-index: 200;
     background-color: white;
-    color: $black;
+    color: global.$black;
     position: absolute;
     overflow: hidden;
 }
@@ -1014,12 +1014,12 @@
     align-items: center;
     justify-content: center;
     transform: translate(0px, -1px);
-    background-color: $white;
+    background-color: global.$white;
     width: 40px;
     height: 15px;
     align-self: center;
     justify-self: center;
-    border: solid 1px $black;
+    border: solid 1px global.$black;
     border-top: 0px;
     border-bottom-right-radius: 7px;
     border-bottom-left-radius: 7px;
@@ -1028,15 +1028,15 @@
 .layout-container {
     padding: 5px;
     display: grid;
-    background-color: $white;
+    background-color: global.$white;
     grid-template-columns: repeat(auto-fit, minmax(90px, 100px));
     width: 100%;
-    border: solid 1px $black;
+    border: solid 1px global.$black;
     min-width: 100px;
     overflow: hidden;
 
     .layout:hover {
-        border: solid 2px $medium-blue;
+        border: solid 2px global.$medium-blue;
     }
 
     .layout {
@@ -1051,7 +1051,7 @@
         width: 90px;
         overflow: hidden;
         background-color: white;
-        border: solid $medium-gray 1px;
+        border: solid global.$medium-gray 1px;
         display: grid;
         grid-template-rows: auto;
         align-items: center;
@@ -1066,7 +1066,7 @@
             height: 13;
             font-size: 12;
             display: flex;
-            background-color: $white;
+            background-color: global.$white;
         }
 
         .subtitle {
@@ -1079,7 +1079,7 @@
             height: 13;
             font-size: 9;
             display: flex;
-            background-color: $white;
+            background-color: global.$white;
         }
 
         .content {
@@ -1092,7 +1092,7 @@
             height: 13;
             font-size: 10;
             display: flex;
-            background-color: $white;
+            background-color: global.$white;
             height: 33;
             text-align: left;
             font-size: 8px;
@@ -1103,7 +1103,7 @@
 .presBox-buttons {
     position: relative;
     width: 100%;
-    background: $medium-gray;
+    background: global.$medium-gray;
     min-height: 35px;
     padding-top: 5px;
     padding-bottom: 5px;
@@ -1137,8 +1137,8 @@
     }
 
     select {
-        background: $dark-gray;
-        color: $white;
+        background: global.$dark-gray;
+        color: global.$white;
     }
 
     .presBox-button {
@@ -1152,8 +1152,8 @@
         text-align: center;
         letter-spacing: normal;
         width: inherit;
-        background: $dark-gray;
-        color: $white;
+        background: global.$dark-gray;
+        color: global.$white;
     }
 
     .presBox-button.active {
@@ -1161,7 +1161,7 @@
     }
 
     .presBox-button.active:hover {
-        background-color: $medium-blue;
+        background-color: global.$medium-blue;
     }
 
     .presBox-button.edit {
@@ -1238,8 +1238,8 @@
             font-size: 100;
             display: flex;
             align-items: center;
-            background: $dark-gray;
-            color: $white;
+            background: global.$dark-gray;
+            color: global.$white;
         }
 
         .presBox-viewPicker {
@@ -1273,7 +1273,7 @@
     left: 0;
     opacity: 0.5;
     transition: all 0.4s;
-    color: $white;
+    color: global.$white;
     width: 100%;
     height: 100%;
 }
@@ -1283,8 +1283,8 @@
 }
 
 .presPanelOverlay {
-    background-color: $dark-gray;
-    color: $white;
+    background-color: global.$dark-gray;
+    color: global.$white;
     border-radius: 5px;
     grid-template-rows: 100%;
     height: 100%;
@@ -1316,7 +1316,7 @@
     .presPanel-divider {
         width: 0.5px;
         height: 80%;
-        border-right: solid 1px $medium-gray;
+        border-right: solid 1px global.$medium-gray;
     }
 
     .presPanel-button-frame {
@@ -1348,12 +1348,12 @@
     }
 
     .presPanel-button:hover {
-        background-color: $medium-gray;
+        background-color: global.$medium-gray;
         transform: scale(1.2);
     }
 
     .presPanel-button-text:hover {
-        background-color: $medium-gray;
+        background-color: global.$medium-gray;
     }
 }
 
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index 94e64b952..030d56f0c 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -1,4 +1,4 @@
-@import '../global/globalCssVariables.module.scss';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .searchBox-container {
     width: 100%;
@@ -22,7 +22,7 @@
         top: 0px;
         position: sticky;
         overflow-y: scroll;
-        border-bottom: $standard-border;
+        border-bottom: global.$standard-border;
 
         .searchBox-type {
             display: block;
@@ -48,20 +48,20 @@
 
     .section-header {
         .section-title {
-            font-size: $body-text;
+            font-size: global.$body-text;
             font-weight: 600;
         }
 
         .section-subtitle {
             display: flex;
-            color: $light-gray;
+            color: global.$light-gray;
         }
 
         padding: 5px 10px;
         display: flex;
         flex-direction: column;
         gap: 3px;
-        background: $medium-blue;
+        background: global.$medium-blue;
         color: white;
     }
 
diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss
index 20245104e..35a3da312 100644
--- a/src/client/views/topbar/TopBar.scss
+++ b/src/client/views/topbar/TopBar.scss
@@ -1,4 +1,5 @@
-@import '../global/globalCssVariables.module.scss';
+@use 'sass:color';
+@use '../global/globalCssVariables.module.scss' as global;
 
 .topbar-container {
     flex-direction: column;
@@ -6,13 +7,13 @@
     line-height: 1;
     overflow-y: auto;
     overflow-x: visible;
-    background: $dark-gray;
+    background: global.$dark-gray;
     overflow: visible;
     z-index: 1000;
     align-items: center;
-    height: $topbar-height;
-    background-color: $dark-gray;
-    border-bottom: $standard-border;
+    height: global.$topbar-height;
+    background-color: global.$dark-gray;
+    border-bottom: global.$standard-border;
     padding: 0px 10px;
     cursor: default;
     display: flex;
@@ -34,7 +35,7 @@
     }
 
     .topbar-button-text {
-        color: $white;
+        color: global.$white;
         padding: 10px;
         size: 15;
 
@@ -54,16 +55,16 @@
         align-self: center;
         padding: 5px;
         transition: linear 0.2s;
-        color: $white;
+        color: global.$white;
 
         &:hover {
-            background-color: darken($color: $light-gray, $amount: 20);
+            background-color: color.adjust(global.$light-gray, $lightness: -20%);
             font-weight: 500;
         }
     }
 
     .topbar-title {
-        color: $white;
+        color: global.$white;
         font-size: 17;
         font-weight: 500;
     }
@@ -119,7 +120,7 @@
         }
 
         .topBar-icon:hover {
-            background-color: $close-red;
+            background-color: global.$close-red;
         }
 
         .topbar-lozenge-user,
@@ -180,7 +181,7 @@
         &.topbar-input {
             margin: 5px;
             border-radius: 20px;
-            border: $dark-gray;
+            border: global.$dark-gray;
             display: block;
             width: 130px;
             -webkit-transition: width 0.4s;
@@ -212,8 +213,8 @@
         }
 
         &.topbar-close {
-            color: $white;
-            max-height: $topbar-height;
+            color: global.$white;
+            max-height: global.$topbar-height;
         }
     }
 }
@@ -229,7 +230,7 @@
 
     .no-result {
         width: 500px;
-        background: $light-gray;
+        background: global.$light-gray;
         padding: 10px;
         height: 50px;
         text-transform: uppercase;
-- 
cgit v1.2.3-70-g09d2


From bcf3b005d55ec851374049f4c188d96ffc7a7c8b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 27 Feb 2025 14:46:20 -0500
Subject: prevent tagsview from appeqring in spreadsheet.  set default fill
 color only if nothing is selected.  place column divider at top of
 stacking/card views.  move card view flashcard view to top left independent
 of margin settings.

---
 .../views/collections/CollectionCardDeckView.tsx   |   5 +-
 .../collections/CollectionMasonryViewFieldRow.tsx  |   1 -
 .../views/collections/CollectionStackingView.scss  | 194 ++++++++++-----------
 .../views/collections/CollectionStackingView.tsx   |  45 ++---
 src/client/views/global/globalScripts.ts           |   2 +-
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 6 files changed, 125 insertions(+), 124 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 94f85d429..756b37f99 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -540,11 +540,10 @@ export class CollectionCardView extends CollectionSubView() {
                             height: `${100 / this.nativeScaling / fitContentScale}%`,
                             width: `${100 / this.nativeScaling / fitContentScale}%`,
                             transform: `scale(${this.nativeScaling * fitContentScale})`,
-                        }}>
-                        {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
-                    </div>
+                        }}></div>
                 </div>
 
+                {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
                 <div
                     className="collectionCardView-cardSizeDragger"
                     onPointerDown={this.cardSizerDown}
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 710c00841..a0d5f9f70 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -258,7 +258,6 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         const stackPad = showChrome ? `0px ${this._props.parent.xMargin}px` : `${this._props.parent.yMargin}px ${this._props.parent.xMargin}px 0px ${this._props.parent.xMargin}px `;
         return this.collapsed ? null : (
             <div style={{ position: 'relative' }}>
-                {this._props.showHandle && this._props.parent._props.isContentActive() ? this._props.parent.columnDragger : null}
                 {showChrome ? (
                     <div
                         className="collectionStackingView-addDocumentButton"
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 565536623..05ac52ff9 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -12,123 +12,123 @@
     position: relative;
     height: 100%;
     width: 100%;
-}
-
-// TODO:glr Turn this into a seperate class
-.documentButtonMenu {
-    position: relative;
-    height: fit-content;
-    border-bottom: global.$standard-border;
-    display: flex;
-    justify-content: center;
-    flex-direction: column;
-    align-items: center;
-    align-content: center;
-    padding: 5px 0 5px 0;
 
-    .documentExplanation {
-        width: 90%;
-        margin: 5px;
-        font-size: 11px;
-        color: global.$medium-blue;
-        padding: 10px;
-        border-radius: 5px;
-        border: solid 0.5px global.$medium-blue;
+    .collectionStackingView-columnDragger {
+        width: 28;
+        height: 28;
+        position: absolute;
+        margin-left: -5;
+        z-index: 10;
+        > svg {
+            width: 100%;
+            height: 100%;
+        }
     }
-}
 
-.collectionStackingView,
-.collectionMasonryView {
-    height: 100%;
-    width: 100%;
-    position: absolute;
-    top: 0;
-    overflow-y: auto;
-    overflow-x: hidden;
-    flex-wrap: wrap;
-    transition: top 0.5s;
-
-    > div {
+    // TODO:glr Turn this into a seperate class
+    .documentButtonMenu {
         position: relative;
-        display: block;
-    }
-
-    .collectionStackingViewFieldColumn {
+        height: fit-content;
+        border-bottom: global.$standard-border;
         display: flex;
+        justify-content: center;
         flex-direction: column;
+        align-items: center;
+        align-content: center;
+        padding: 5px 0 5px 0;
+
+        .documentExplanation {
+            width: 90%;
+            margin: 5px;
+            font-size: 11px;
+            color: global.$medium-blue;
+            padding: 10px;
+            border-radius: 5px;
+            border: solid 0.5px global.$medium-blue;
+        }
     }
 
-    .collectionSchemaView-previewDoc {
+    .collectionStackingView,
+    .collectionMasonryView {
         height: 100%;
+        width: 100%;
         position: absolute;
-    }
+        top: 0;
+        overflow-y: auto;
+        overflow-x: hidden;
+        flex-wrap: wrap;
+        transition: top 0.5s;
 
-    .collectionStackingView-docView-container {
-        width: 45%;
-        margin: 5% 2.5%;
-        padding-left: 2.5%;
-        height: auto;
-    }
+        > div {
+            position: relative;
+            display: block;
+        }
 
-    .collectionStackingView-flexCont {
-        display: flex;
-        flex-direction: row;
-        flex-wrap: wrap;
-        align-items: center;
-    }
+        .collectionStackingViewFieldColumn {
+            display: flex;
+            flex-direction: column;
+        }
 
-    .collectionStackingView-masonrySingle,
-    .collectionStackingView-masonryGrid {
-        width: 100%;
-        display: grid;
-        top: 0;
-        left: 0;
-    }
+        .collectionSchemaView-previewDoc {
+            height: 100%;
+            position: absolute;
+        }
 
-    .collectionStackingView-masonrySingle {
-        height: 100%;
-        position: absolute;
-    }
+        .collectionStackingView-docView-container {
+            width: 45%;
+            margin: 5% 2.5%;
+            padding-left: 2.5%;
+            height: auto;
+        }
 
-    .collectionStackingView-masonryGrid {
-        margin: auto;
-        height: max-content;
-        position: relative;
-        grid-auto-rows: 0px;
-    }
+        .collectionStackingView-flexCont {
+            display: flex;
+            flex-direction: row;
+            flex-wrap: wrap;
+            align-items: center;
+        }
 
-    .collectionStackingView-masonrySingle {
-        width: 100%;
-        height: 100%;
-        position: absolute;
-        display: flex;
-        flex-direction: column;
-        top: 0;
-        left: 0;
-        width: 100%;
-        position: absolute;
-    }
+        .collectionStackingView-masonrySingle,
+        .collectionStackingView-masonryGrid {
+            width: 100%;
+            display: grid;
+            top: 0;
+            left: 0;
+        }
 
-    .collectionStackingView-description {
-        font-size: 100%;
-        margin-bottom: 1vw;
-        padding: 10px;
-        height: 2vw;
-        width: 100%;
-        font-family: global.$sans-serif;
-        background: global.$dark-gray;
-        color: global.$white;
-    }
+        .collectionStackingView-masonrySingle {
+            height: 100%;
+            position: absolute;
+        }
 
-    .collectionStackingView-columnDragger {
-        width: 28;
-        height: 28;
-        position: absolute;
-        margin-left: -5;
-        z-index: 10;
-        > svg {
+        .collectionStackingView-masonryGrid {
+            margin: auto;
+            height: max-content;
+            position: relative;
+            grid-auto-rows: 0px;
+        }
+
+        .collectionStackingView-masonrySingle {
             width: 100%;
             height: 100%;
+            position: absolute;
+            display: flex;
+            flex-direction: column;
+            top: 0;
+            left: 0;
+            width: 100%;
+            position: absolute;
+        }
+
+        .collectionStackingView-description {
+            font-size: 100%;
+            margin-bottom: 1vw;
+            padding: 10px;
+            height: 2vw;
+            width: 100%;
+            font-family: global.$sans-serif;
+            background: global.$dark-gray;
+            color: global.$white;
         }
     }
 
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 0a25882ff..e8fa8b01e 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -452,7 +452,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     };
     @action
     onDividerMove = (e: PointerEvent) => {
-        this.Document._layout_columnWidth = Math.max(10, this.ScreenToLocalBoxXf().transformPoint(e.clientX, 0)[0] - this.xMargin);
+        this.Document._layout_columnWidth = Math.max(10, (this._props.DocumentView?.().screenToViewTransform().transformPoint(e.clientX, 0)[0] ?? 0) - this.xMargin);
         return false;
     };
 
@@ -606,26 +606,29 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         }
         const rows = () => (!this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this._props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))));
         return (
-            <div style={{ top: this.yMargin }}>
-                <CollectionMasonryViewFieldRow
-                    showHandle={first}
-                    Document={this.Document}
-                    chromeHidden={this.chromeHidden}
-                    pivotField={this.pivotField}
-                    refList={this._refList}
-                    key={heading ? heading.heading : ''}
-                    rows={rows}
-                    headings={this.headings}
-                    heading={heading ? heading.heading : ''}
-                    headingObject={heading}
-                    docList={docList}
-                    parent={this}
-                    type={type}
-                    createDropTarget={this.createDashEventsTarget}
-                    screenToLocalTransform={this.ScreenToLocalBoxXf}
-                    setDocHeight={this.setDocHeight}
-                />
-            </div>
+            <>
+                {this._props.isContentActive() && !this.isStackingView ? this.columnDragger : null}
+                <div style={{ top: this.yMargin }}>
+                    <CollectionMasonryViewFieldRow
+                        showHandle={first}
+                        Document={this.Document}
+                        chromeHidden={this.chromeHidden}
+                        pivotField={this.pivotField}
+                        refList={this._refList}
+                        key={heading ? heading.heading : ''}
+                        rows={rows}
+                        headings={this.headings}
+                        heading={heading ? heading.heading : ''}
+                        headingObject={heading}
+                        docList={docList}
+                        parent={this}
+                        type={type}
+                        createDropTarget={this.createDashEventsTarget}
+                        screenToLocalTransform={this.ScreenToLocalBoxXf}
+                        setDocHeight={this.setDocHeight}
+                    />
+                </div>
+            </>
         );
     };
 
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index b455f76e4..835c28daa 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -127,7 +127,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
             const contentFrameNumber = Cast(selView.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
             return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] || defaultFill();
         }
-        setDefaultFill(color ?? 'transparent');
+        !selectedViews.length && setDefaultFill(color ?? 'transparent');
         selectedViews.forEach(dv => {
             const fieldKey = dv.Document._layout_isSvg ? 'fillColor' : 'backgroundColor';
             const layoutFrameNumber = Cast(dv.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b40ead03a..5f5dd1210 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -780,7 +780,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                             transform: `scale(${this.uiBtnScaling})`,
                             bottom: Number.isNaN(this.maxWidgetSize) ? undefined : this.maxWidgetSize,
                         }}>
-                        {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null}
+                        {this._props.DocumentView?.() && !this._props.docViewPath().slice(-2)[0].ComponentView?.isUnstyledView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null}
                     </div>
                 ) : (
                     <>
-- 
cgit v1.2.3-70-g09d2


From db3bdb19ff7bc1c69c544797c05a6db3b72b1464 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 5 Mar 2025 14:54:18 -0500
Subject: added:hover mode for images to make primary image fade in instead of
 alternate.

---
 src/client/documents/Documents.ts                  |  3 ++
 src/client/views/MainView.tsx                      |  2 +-
 src/client/views/TagsView.tsx                      |  3 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/DocumentView.scss           |  1 +
 src/client/views/nodes/DocumentView.tsx            | 16 +++++----
 src/client/views/nodes/ImageBox.scss               |  6 ++++
 src/client/views/nodes/ImageBox.tsx                | 40 +++++++++++++++-------
 .../views/nodes/chatbot/tools/ImageCreationTool.ts |  3 +-
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         |  3 ++
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  4 +++
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  8 +++--
 12 files changed, 65 insertions(+), 26 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 1ce25165c..21d3c978b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -384,6 +384,9 @@ export class DocumentOptions {
     presentation_duration?: NUMt = new NumInfo('the duration of the slide in presentation view', false);
     presentation_zoomText?: BOOLt = new BoolInfo('whether text anchors should shown in a larger box when following links to make them stand out', false);
 
+    data_annotations?: List<Doc>;
+    _data_usePath?: STRt = new StrInfo("description of field key to display in image box ('alternate','alternate:hover', 'data:hover'). defaults to primary", false);
+    data_alternates?: List<Doc>;
     data?: FieldType;
     data_useCors?: BOOLt = new BoolInfo('whether CORS protocol should be used for web page');
     _face_showImages?: BOOLt = new BoolInfo('whether to show images in uniqe face Doc');
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index afefe3f03..cc7c1a42b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -686,7 +686,7 @@ export class MainView extends ObservableReactComponent<object> {
         );
     }
     @computed get mainDocView() {
-        const headerBar = null; // this._hideUI || !this.headerBarDocHeight?.() ? null : this.headerBarDocView;
+        const headerBar = this._hideUI || !this.headerBarDocHeight?.() ? null : this.headerBarDocView;
         return (
             <>
                 {headerBar}
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index b70e21918..93d6fb684 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -398,8 +398,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
                                         e.stopPropagation();
                                     }}
                                     type="text"
-                                    placeholder="Input tags for document..."
-                                    aria-label="tagsView-input"
+                                    placeholder="Enter #tags or @metadata"
                                     className="tagsView-input"
                                     style={{ width: '100%', borderRadius: '5px' }}
                                 />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b3d908da4..43addfc29 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -344,7 +344,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     focusOnPoint = (options: FocusViewOptions) => {
         const { pointFocus, zoomTime, didMove } = options;
         if (!this.Document.isGroup && pointFocus && !didMove) {
-            const dfltScale = this.isAnnotationOverlay ? 1 : 0.5;
+            const dfltScale = this.isAnnotationOverlay ? 1 : 0.25;
             if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) {
                 this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(pointFocus.X, pointFocus.Y), dfltScale, zoomTime);
                 options.didMove = true;
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 294af4d96..dd5fd0d0c 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -291,6 +291,7 @@
     justify-items: center;
     background-color: rgb(223, 223, 223);
     transform-origin: top left;
+    background: transparent;
 
     .documentView-editorView-resizer {
         height: 5px;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5f5dd1210..595abc7f8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -279,16 +279,17 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined
     };
     onBrowseClick = (e: React.MouseEvent) => {
-        const browseTransitionTime = 500;
+        //const browseTransitionTime = 500;
         DocumentView.DeselectAll();
         DocumentView.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
-            const options: FocusViewOptions = { pointFocus: { X: e.clientX, Y: e.clientY }, zoomTime: browseTransitionTime };
+            // const options: FocusViewOptions = { pointFocus: { X: e.clientX, Y: e.clientY }, zoomTime: browseTransitionTime };
             if (!focused && this._docView) {
-                this._docView
-                    .docViewPath()
-                    .reverse()
-                    .forEach(cont => cont.ComponentView?.focus?.(cont.Document, options));
-                Doc.linkFollowHighlight(this.Document, false);
+                DocumentView.showDocument(this.Document, { zoomScale: 0.3, willZoomCentered: true });
+                // this._docView
+                //     .docViewPath()
+                //     .reverse()
+                //     .forEach(cont => cont.ComponentView?.focus?.(cont.Document, options));
+                // Doc.linkFollowHighlight(this.Document, false);
             }
         });
         e.stopPropagation();
@@ -797,6 +798,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         <div
                             className="documentView-editorView"
                             style={{
+                                background: SnappingManager.userVariantColor,
                                 width: `${100 / this.uiBtnScaling}%`, //
                                 transform: `scale(${this.uiBtnScaling})`,
                             }}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 15581a9e8..3d6942e6f 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -40,6 +40,7 @@
     max-height: 100%;
     pointer-events: inherit;
     background: transparent;
+    z-index: 0;
     // z-index: -10000; // bcz: not sure why this was here.  it broke dropping images on the image box alternate bullseye icon.
 
     img {
@@ -103,6 +104,10 @@
     margin: 0 auto;
     display: flex;
     height: 100%;
+    img {
+        object-fit: contain;
+        height: 100%;
+    }
 
     .imageBox-fadeBlocker,
     .imageBox-fadeBlocker-hover {
@@ -237,5 +242,6 @@
         text-overflow: ellipsis;
         max-width: 65%;
         width: 100%;
+        color: black;
     }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 4979a4d4d..017ef7191 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -436,6 +436,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
     getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
 
+    @computed get usingAlternate() {
+        const usePath = StrCast(this.Document[this.fieldKey + '_usePath']);
+        return 'alternate' === usePath || ('alternate:hover' === usePath && this._isHovering) || (':hover' === usePath && !this._isHovering);
+    }
+
     @computed get nativeSize() {
         TraceMobx();
         if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 };
@@ -471,10 +476,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}>
                             <em>alternate, </em>
                         </span>
-                        and show
                         <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}>
                             <em> alternate on hover</em>
                         </span>
+                        and show
+                        <span style={{ color: usePath === ':hover' ? 'black' : undefined }}>
+                            <em> primary on hover</em>
+                        </span>
                     </div>
                 }>
                 <div
@@ -482,7 +490,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     ref={this._overlayIconRef}
                     onPointerDown={e =>
                         setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
-                            this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined;
+                            this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : usePath === 'alternate:hover' ? ':hover' : undefined;
                         })
                     }
                     style={{
@@ -527,7 +535,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 .filter(url => url)
                 .map(url => this.choosePath(url)) ?? []; // acc  ess the primary layout data of the alternate documents
         const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
-        return paths.length ? paths : [defaultUrl.href];
+        return paths.length ? paths.reverse() : [defaultUrl.href];
     }
 
     @computed get content() {
@@ -552,7 +560,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             transformOrigin = 'right top';
             transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`;
         }
-        const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`];
 
         return (
             <div
@@ -571,7 +578,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         ref={action((r: HTMLImageElement | null) => (this.imageRef = r))}
                         key="paths"
                         src={srcpath}
-                        style={{ transform, transformOrigin, objectFit: 'fill', height: '100%' }}
+                        style={{ transform, transformOrigin }}
                         onError={action(e => {
                             this._error = e.toString();
                         })}
@@ -579,7 +586,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         width={nativeWidth}
                     />
                     {fadepath === srcpath ? null : (
-                        <div className={`imageBox-fadeBlocker${(this._isHovering && usePath === 'alternate:hover') || usePath === 'alternate' ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}>
+                        <div className={`imageBox-fadeBlocker${this.usingAlternate ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}>
                             <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
                         </div>
                     )}
@@ -619,8 +626,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return (
             <div className="imageBox-aiView">
                 <div className="imageBox-aiView-regenerate">
-                    <span className="imageBox-aiView-firefly">Firefly:</span>
+                    <span className="imageBox-aiView-firefly" style={{ color: SnappingManager.userColor }}>
+                        Firefly:
+                    </span>
                     <input
+                        style={{ color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
                         className="imageBox-aiView-input"
                         aria-label="Edit instructions input"
                         type="text"
@@ -631,7 +641,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     <div className="imageBox-aiView-regenerate-createBtn">
                         <Button
                             text="Create"
-                            type={Type.SEC}
+                            type={Type.TERT}
+                            color={SnappingManager.userColor}
+                            background={SnappingManager.userBackgroundColor}
                             // style={{ alignSelf: 'flex-end' }}
                             icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                             iconPlacement="right"
@@ -663,13 +675,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     </div>
                 </div>
                 <div className="imageBox-aiView-strength">
-                    <span className="imageBox-aiView-similarity">Similarity</span>
+                    <span className="imageBox-aiView-similarity" style={{ color: SnappingManager.userColor }}>
+                        Similarity
+                    </span>
                     <Slider
                         className="imageBox-aiView-slider"
                         sx={{
-                            '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                            '& .MuiSlider-track': { color: SettingsManager.userColor },
                             '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
-                            '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                            '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
                         }}
                         min={0}
                         max={100}
@@ -725,11 +739,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return { width, height };
     };
     savedAnnotations = () => this._savedAnnotations;
-
     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 alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']);
+        const doc = this.usingAlternate ? (alts.lastElement() ?? this.Document) : this.Document;
         return (
             <div
                 className="imageBox"
@@ -754,6 +769,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 <CollectionFreeFormView
                     ref={this._ffref}
                     {...this._props}
+                    Document={doc}
                     setContentViewBox={emptyFunction}
                     NativeWidth={returnZero}
                     NativeHeight={returnZero}
diff --git a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
index e92d87dfd..37907fd4f 100644
--- a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
+++ b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
@@ -5,6 +5,7 @@ import { ParametersType, ToolInfo } from '../types/tool_types';
 import { Observation } from '../types/types';
 import { BaseTool } from './BaseTool';
 import { Upload } from '../../../../../server/SharedMediaTypes';
+import { List } from '../../../../../fields/List';
 
 const imageCreationToolParams = [
     {
@@ -41,7 +42,7 @@ export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> {
                 image_prompt,
             })) as { result: Upload.FileInformation & Upload.InspectionResults; url: string };
             console.log('Image generation result:', result);
-            this._createImage(result, { text: RTFCast(image_prompt) });
+            this._createImage(result, { text: RTFCast(image_prompt), ai: 'dall-e-3', tags: new List<string>(['@ai']) });
             return url
                 ? [
                       {
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 79f5121ed..4dc45e6a0 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -30,6 +30,7 @@ import { Upload } from '../../../../server/SharedMediaTypes';
 import { OpenWhere } from '../../nodes/OpenWhere';
 import { DrawingFillHandler } from '../../smartdraw/DrawingFillHandler';
 import { ImageField } from '../../../../fields/URLField';
+import { List } from '../../../../fields/List';
 
 export enum GPTPopupMode {
     SUMMARY, // summary of seleted document text
@@ -352,6 +353,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
                 y: NumCast(textAnchor.y),
                 _height: 200,
                 _width: 200,
+                ai: 'dall-e',
+                tags: new List<string>(['@ai']),
                 data_nativeWidth: 1024,
                 data_nativeHeight: 1024,
             });
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 0a30b14b8..c672bc718 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,6 +1,7 @@
 import { imageUrlToBase64 } from '../../../ClientUtils';
 import { Doc, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
+import { List } from '../../../fields/List';
 import { DocCast, ImageCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { Upload } from '../../../server/SharedMediaTypes';
@@ -53,7 +54,10 @@ export class DrawingFillHandler {
                                         undefined,
                                         Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
                                             ai: 'firefly',
+                                            tags: new List<string>(['@ai']),
                                             title: newPrompt,
+                                            _data_usePath: 'alternate:hover',
+                                            data_alternates: new List<Doc>([drawing]),
                                             ai_firefly_prompt: newPrompt,
                                             _width: 500,
                                             data_nativeWidth: info.nativeWidth,
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index ca308015d..1cceabed3 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -28,6 +28,7 @@ import { FireflyDimensionsMap, FireflyImageData, FireflyImageDimensions } from '
 import './SmartDrawHandler.scss';
 import { Upload } from '../../../server/SharedMediaTypes';
 import { PointData } from '../../../pen-gestures/GestureTypes';
+import { List } from '../../../fields/List';
 
 export interface DrawingOptions {
     text?: string;
@@ -293,15 +294,17 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     return undefined;
                 }
                 const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)?.[1];
-                const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
+                return Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
                     title: input,
                     nativeWidth: dims.width,
                     nativeHeight: dims.height,
+                    tags: new List<string>(['@ai']),
+                    _width: Math.min(400, dims.width),
+                    _height: (Math.min(400, dims.width) * dims.height) / dims.width,
                     ai: 'firefly',
                     ai_firefly_seed: +(newseed ?? 0),
                     ai_firefly_prompt: input,
                 });
-                return imgDoc;
             })
             .catch(e => {
                 alert('create image failed: ' + e.toString());
@@ -568,6 +571,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                         color={SettingsManager.userColor}
                     />
                     <input
+                        style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}
                         aria-label="Smart Draw Input"
                         className="smartdraw-input"
                         type="text"
-- 
cgit v1.2.3-70-g09d2


From 1ab6f6c87b746e0c2898694216db50d5faadf7f5 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 6 Mar 2025 19:46:43 -0500
Subject: a bunch of changes to improve how docs are selected automatically
 when created.

---
 src/client/documents/DocUtils.ts                   |  5 +-
 src/client/views/GlobalKeyHandler.ts               | 19 +++--
 src/client/views/MarqueeAnnotator.tsx              |  2 +-
 src/client/views/SidebarAnnos.tsx                  |  9 +--
 .../collections/CollectionMasonryViewFieldRow.tsx  |  7 +-
 .../views/collections/CollectionNoteTakingView.tsx |  2 +-
 .../collections/CollectionNoteTakingViewColumn.tsx | 21 ++----
 .../views/collections/CollectionStackingView.tsx   |  2 +-
 .../CollectionStackingViewFieldColumn.tsx          | 26 ++-----
 .../views/collections/CollectionTreeView.tsx       |  2 +-
 src/client/views/collections/TreeView.tsx          |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  5 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  2 +-
 .../collectionGrid/CollectionGridView.tsx          |  2 +-
 src/client/views/nodes/DocumentView.tsx            |  6 ++
 src/client/views/nodes/EquationBox.tsx             | 10 +--
 src/client/views/nodes/LabelBox.tsx                |  5 +-
 src/client/views/nodes/MapBox/MapBox.tsx           |  2 +-
 src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx |  2 +-
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx   |  2 +-
 .../views/nodes/formattedText/DailyJournal.tsx     | 25 +++----
 .../views/nodes/formattedText/FormattedTextBox.tsx | 41 +++-------
 src/fields/Doc.ts                                  |  4 -
 src/fields/RichTextField.ts                        | 87 +++++++++++++++++-----
 24 files changed, 149 insertions(+), 141 deletions(-)

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

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 7e0416447..9797a407b 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -385,7 +385,7 @@ export namespace DocUtils {
                         newDoc.x = x;
                         newDoc.y = y;
                         newDoc[DocData].backgroundColor = Doc.UserDoc().textBackgroundColor;
-                        Doc.SetSelectOnLoad(newDoc);
+                        DocumentView.SetSelectOnLoad(newDoc);
                         if (pivotField) {
                             newDoc[pivotField] = pivotValue;
                         }
@@ -408,6 +408,7 @@ export namespace DocUtils {
             description: ':Daily Journal',
             event: undoable(() => {
                 const newDoc = Docs.Create.DailyJournalDocument('', { x, y });
+                DocumentView.SetSelectOnLoad(newDoc);
                 docAdder?.(newDoc);
             }, 'Create Daily Journal'),
             icon: 'book',
@@ -456,7 +457,7 @@ export namespace DocUtils {
                         newDoc.author = ClientUtils.CurrentUserEmail();
                         newDoc.x = x;
                         newDoc.y = y;
-                        Doc.SetSelectOnLoad(newDoc);
+                        DocumentView.SetSelectOnLoad(newDoc);
                         if (pivotField) {
                             newDoc[pivotField] = pivotValue;
                         }
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index b200aff65..2d342d1b1 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -23,7 +23,6 @@ import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocume
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhereMod } from './nodes/OpenWhere';
-import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { AnchorMenu } from './pdf/AnchorMenu';
 
 const modifiers = ['control', 'meta', 'shift', 'alt'];
@@ -75,7 +74,6 @@ export class KeyManager {
 
     public handle = action((e: KeyboardEvent) => {
         // accumulate buffer of characters to insert in a new text note.  once the note is created, it will stop keyboard events from reaching this function.
-        if (FormattedTextBox.SelectOnLoadChar) FormattedTextBox.SelectOnLoadChar += e.key === 'Enter' ? '\n' : e.key;
         const keyname = e.key && e.key.toLowerCase();
         this.handleGreedy(/* keyname */);
 
@@ -106,6 +104,10 @@ export class KeyManager {
     };
 
     private unmodified = action((keyname: string, e: KeyboardEvent) => {
+        const nothing = {
+            stopPropagation: false,
+            preventDefault: false,
+        };
         switch (keyname) {
             case 'u':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
@@ -169,17 +171,14 @@ export class KeyManager {
                     return { stopPropagation: true, preventDefault: true };
                 }
                 break;
-            case 'arrowleft':  return (e.target as any).type !== 'text' && this.nudge(-1, 0, 'nudge left') // if target is an input box, then we don't want to nudge any Docs since we're justing moving within the text itself.
-            case 'arrowright': return (e.target as any).type !== 'text' && this.nudge( 1, 0, 'nudge right');
-            case 'arrowup':    return (e.target as any).type !== 'text' && this.nudge(0, -1, 'nudge up');
-            case 'arrowdown':  return (e.target as any).type !== 'text' && this.nudge(0,  1, 'nudge down');
+            case 'arrowleft':  return (e.target as HTMLInputElement)?.type !== 'text' ? this.nudge(-1, 0, 'nudge left') : nothing; // if target is an input box, then we don't want to nudge any Docs since we're justing moving within the text itself.
+            case 'arrowright': return (e.target as HTMLInputElement)?.type !== 'text' ? this.nudge( 1, 0, 'nudge right') : nothing;
+            case 'arrowup':    return (e.target as HTMLInputElement)?.type !== 'text' ? this.nudge(0, -1, 'nudge up') : nothing;
+            case 'arrowdown':  return (e.target as HTMLInputElement | null)?.type !== 'text'?  this.nudge(0,  1, 'nudge down'): nothing;
             default:
         } // prettier-ignore
 
-        return {
-            stopPropagation: false,
-            preventDefault: false,
-        };
+        return nothing;
     });
 
     private shift = action((keyname: string) => {
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 02516264c..3f4200dce 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -199,7 +199,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
 
             const targetCreator = (annotationOn: Doc | undefined) => {
                 const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
-                Doc.SetSelectOnLoad(target);
+                DocumentView.SetSelectOnLoad(target);
                 return target;
             };
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 87076bf65..816fc8ed3 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -3,23 +3,23 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, returnAll, returnFalse, returnOne, returnZero } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
-import { Doc, DocListCast, Field, FieldType, FieldResult, StrListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, FieldType, StrListCast } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
 import { RichTextField } from '../../fields/RichTextField';
 import { DocCast, NumCast, StrCast } from '../../fields/Types';
+import { DocUtils } from '../documents/DocUtils';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { Docs } from '../documents/Documents';
-import { DocUtils } from '../documents/DocUtils';
 import { SearchUtil } from '../util/SearchUtil';
 import { Transform } from '../util/Transform';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import './SidebarAnnos.scss';
 import { StyleProp } from './StyleProp';
 import { CollectionStackingView } from './collections/CollectionStackingView';
+import { DocumentView } from './nodes/DocumentView';
 import { FieldViewProps } from './nodes/FieldView';
-import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 
 interface ExtraProps {
     fieldKey: string;
@@ -81,8 +81,7 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
             text_fontSize: StrCast(Doc.UserDoc().fontSize),
             text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
         });
-        Doc.SetSelectOnLoad(target);
-        FormattedTextBox.DontSelectInitialText = true;
+        DocumentView.SetSelectOnLoad(target);
         DocUtils.MakeLink(anchor, target, { link_relationship: 'inline comment:comment on' });
 
         const taggedContent = this.childFilters()
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index a0d5f9f70..996626575 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -16,7 +16,7 @@ import { Transform } from '../../util/Transform';
 import { undoBatch, undoable } from '../../util/UndoManager';
 import { EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { DocumentView } from '../nodes/DocumentView';
 import { CollectionStackingView } from './CollectionStackingView';
 import './CollectionStackingView.scss';
 
@@ -162,9 +162,8 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         if (!value && !forceEmptyNote) return false;
         this._createEmbeddingSelected = false;
         const { pivotField } = this._props;
-        const newDoc = Docs.Create.TextDocument('', { _layout_autoHeight: true, _width: 200, _layout_fitWidth: true, title: value });
-        Doc.SetSelectOnLoad(newDoc);
-        FormattedTextBox.SelectOnLoadChar = value;
+        const newDoc = Docs.Create.TextDocument(value, { _layout_autoHeight: true, _width: 200, _layout_fitWidth: true, title: value });
+        DocumentView.SetSelectOnLoad(newDoc);
         pivotField && (newDoc[DocData][pivotField] = this.getValue(this._props.heading));
         const docs = this._props.parent.childDocList;
         return docs ? !!docs.splice(0, 0, newDoc) : this._props.parent._props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list)
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 4c23d4c48..c499bd288 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -442,7 +442,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
             e.stopPropagation?.();
             const newDoc = Doc.MakeCopy(fieldProps.Document, true);
             newDoc[DocData].text = undefined;
-            Doc.SetSelectOnLoad(newDoc);
+            DocumentView.SetSelectOnLoad(newDoc);
             return this.addDocument?.(newDoc);
         }
         return undefined;
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index 226d06f37..40b3f9ef2 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -2,7 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { lightOrDark, returnEmptyString } from '../../../ClientUtils';
+import { lightOrDark, returnEmptyString, returnTrue } from '../../../ClientUtils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
@@ -17,8 +17,8 @@ import { undoBatch, undoable } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { EditableProps, EditableView } from '../EditableView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
-import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionNoteTakingView.scss';
+import { DocumentView } from '../nodes/DocumentView';
 
 interface CSVFieldColumnProps {
     Document: Doc;
@@ -140,20 +140,15 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
         this._hover = false;
     };
 
-    addTextNote = undoable(() => this.addNewTextDoc('-typed text-', false, true), 'add text note');
-
     // addNewTextDoc is called when a user starts typing in a column to create a new node
-    @action
-    addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
-        if (!value && !forceEmptyNote) return false;
+    addTextNote = undoable(() => {
         const key = this._props.pivotField;
-        const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true });
+        const newDoc = Docs.Create.TextDocument('', { _height: 18, _width: 200, _layout_fitWidth: true, _layout_autoHeight: true });
         const colValue = this.getValue(this._props.heading);
         newDoc[key] = colValue;
-        Doc.SetSelectOnLoad(newDoc);
-        FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' ';
+        DocumentView.SetSelectOnLoad(newDoc);
         return this._props.addDocument?.(newDoc) || false;
-    };
+    }, 'add text note');
 
     // deleteColumn is called when a user deletes a column using the 'trash' icon in the button area.
     // If the user deletes the first column, the documents get moved to the second column. Otherwise,
@@ -177,7 +172,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
             doc => {
                 const key = this._props.pivotField;
                 doc[key] = this.getValue(this._props.heading);
-                Doc.SetSelectOnLoad(doc);
+                DocumentView.SetSelectOnLoad(doc);
                 return this._props.addDocument?.(doc);
             },
             this._props.addDocument,
@@ -249,7 +244,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
                         {!this._props.chromeHidden ? (
                             <div className="collectionNoteTakingView-DocumentButtons" style={{ display: this._props.isContentActive() ? 'flex' : 'none', marginBottom: 10 }}>
                                 <div className="collectionNoteTakingView-addDocumentButton" style={{ color: lightOrDark(this._props.backgroundColor?.()) }}>
-                                    <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents="+ Node" menuCallback={this.menuCallback} />
+                                    <EditableView GetValue={returnEmptyString} SetValue={returnTrue} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents="+ Node" menuCallback={this.menuCallback} />
                                 </div>
                                 <div className="collectionNoteTakingView-addDocumentButton" style={{ color: lightOrDark(this._props.backgroundColor?.()) }}>
                                     <EditableView {...this._props.editableViewProps()} />
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 96125f2c2..72e6bb8f2 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -321,7 +321,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 newDoc[layoutFieldKey] = fieldProps.Document[layoutFieldKey];
             }
             newDoc[DocData].text = undefined;
-            Doc.SetSelectOnLoad(newDoc);
+            DocumentView.SetSelectOnLoad(newDoc);
             return this.addDocument?.(newDoc);
         }
         return false;
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index ed0cabd0a..6f32dd2e0 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -2,7 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { DivHeight, DivWidth, returnEmptyString, setupMoveUpEvents } from '../../../ClientUtils';
+import { DivHeight, DivWidth, returnEmptyString, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { RichTextField } from '../../../fields/RichTextField';
 import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField';
@@ -23,7 +23,7 @@ import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from '../EditableView';
-import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { DocumentView } from '../nodes/DocumentView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import './CollectionStackingView.scss';
 
@@ -149,19 +149,14 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
 
     @action pointerEntered = () => { SnappingManager.IsDragging && (this._background = '#b4b4b4'); } // prettier-ignore
     @action pointerLeave = () => { this._background = 'inherit'}; // prettier-ignore
-    @undoBatch typedNote = () => this.addNewTextDoc('-typed text-', false, true);
-
-    @action
-    addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
-        if (!value && !forceEmptyNote) return false;
+    @undoBatch typedNote = () => {
         const key = this._props.pivotField;
-        const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true });
+        const newDoc = Docs.Create.TextDocument('', { _height: 18, _width: 200, _layout_fitWidth: true, _layout_autoHeight: true });
         key && (newDoc[key] = this.getValue(this._props.heading));
         const maxHeading = this._props.docList.reduce((prevHeading, doc) => (NumCast(doc.heading) > prevHeading ? NumCast(doc.heading) : prevHeading), 0);
         const heading = maxHeading === 0 || this._props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
         newDoc.heading = heading;
-        Doc.SetSelectOnLoad(newDoc);
-        FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' ';
+        DocumentView.SetSelectOnLoad(newDoc);
         return this._props.addDocument?.(newDoc) || false;
     };
 
@@ -240,7 +235,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         const height = this._ele ? DivHeight(this._ele) : 0;
         DocUtils.addDocumentCreatorMenuItems(
             doc => {
-                Doc.SetSelectOnLoad(doc);
+                DocumentView.SetSelectOnLoad(doc);
                 return this._props.addDocument?.(doc);
             },
             this._props.addDocument,
@@ -394,14 +389,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                                 onKeyDown={e => e.stopPropagation()}
                                 className="collectionStackingView-addDocumentButton"
                                 style={{ width: 'calc(100% - 25px)', maxWidth: this._props.columnWidth / this._props.numGroupColumns - 25, marginBottom: 10 }}>
-                                <EditableView
-                                    GetValue={returnEmptyString}
-                                    SetValue={this.addNewTextDoc}
-                                    textCallback={this.typedNote}
-                                    placeholder={"Type ':' for commands"}
-                                    contents={<FontAwesomeIcon icon="plus" />}
-                                    menuCallback={this.menuCallback}
-                                />
+                                <EditableView GetValue={returnEmptyString} SetValue={returnTrue} textCallback={this.typedNote} placeholder={"Type ':' for commands"} contents={<FontAwesomeIcon icon="plus" />} menuCallback={this.menuCallback} />
                             </div>
                         ) : null}
                     </div>
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index a60cd98ac..e93724dd4 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -174,7 +174,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                 const prev = ind && DocListCast(targetDataDoc[this._props.fieldKey])[ind - 1];
                 this._props.removeDocument?.(docIn);
                 if (ind > 0 && prev) {
-                    Doc.SetSelectOnLoad(prev);
+                    DocumentView.SetSelectOnLoad(prev);
                     DocumentView.getDocumentView(prev, this.DocumentView?.())?.select(false);
                 }
                 return true;
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index ab4d8b060..6208905fe 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1300,7 +1300,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                     const fieldKey = Doc.LayoutFieldKey(newParent);
                     if (remove && fieldKey && Cast(newParent[fieldKey], listSpec(Doc)) !== undefined) {
                         remove(child);
-                        Doc.SetSelectOnLoad(child);
+                        DocumentView.SetSelectOnLoad(child);
                         TreeView._editTitleOnLoad = editTitle ? { id: child[Id], parent } : undefined;
                         Doc.AddDocToList(newParent, fieldKey, child, addAfter, false);
                         newParent.treeView_Open = true;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 43addfc29..89aa53c35 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -293,7 +293,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
     isAnyChildContentActive = () => this._props.isAnyChildContentActive();
     addLiveTextBox = (newDoc: Doc) => {
-        Doc.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+        DocumentView.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
         this.addDocument(newDoc);
     };
     selectDocuments = (docs: Doc[]) => {
@@ -1496,8 +1496,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         newDoc[DocData][Doc.LayoutFieldKey(newDoc, fieldProps.LayoutTemplateString)] = undefined; // the copy should not copy the text contents of it source, just the render style
         newDoc.x = NumCast(textDoc.x) + (below ? 0 : NumCast(textDoc._width) + 10);
         newDoc.y = NumCast(textDoc.y) + (below ? NumCast(textDoc._height) + 10 : 0);
-        Doc.SetSelectOnLoad(newDoc);
-        FormattedTextBox.DontSelectInitialText = true;
+        DocumentView.SetSelectOnLoad(newDoc);
         return this.addDocument?.(newDoc);
     }, 'copied text note');
 
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 9cfb0416c..00d7ea451 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -173,7 +173,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             const slide = DocUtils.copyDragFactory(DocCast(Doc.UserDoc().emptySlide))!;
             slide.x = x;
             slide.y = y;
-            Doc.SetSelectOnLoad(slide);
+            DocumentView.SetSelectOnLoad(slide);
             TreeView._editTitleOnLoad = { id: slide[Id], parent: undefined };
             this._props.addDocument?.(slide);
             e.stopPropagation();
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 80bf4bd12..e7f1de7f1 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -368,7 +368,7 @@ export class CollectionGridView extends CollectionSubView() {
                         undoable(
                             action(() => {
                                 const text = Docs.Create.TextDocument('', { _width: 150, _height: 50 });
-                                Doc.SetSelectOnLoad(text); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+                                DocumentView.SetSelectOnLoad(text); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
                                 Doc.AddDocToList(this.Document, this._props.fieldKey, text);
                                 this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(clickEv.clientX, clickEv.clientY))));
                             }),
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 595abc7f8..a514ee04e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1110,6 +1110,12 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static DeselectAll: (except?: Doc) => void | undefined;
     public static DeselectView: (dv: DocumentView | undefined) => void | undefined;
     public static SelectView: (dv: DocumentView | undefined, extendSelection: boolean) => void | undefined;
+
+    public static SelectOnLoad: Doc | undefined;
+    public static SetSelectOnLoad(doc?: Doc) {
+        DocumentView.SelectOnLoad = doc;
+        doc && DocumentView.addViewRenderedCb(doc, dv => dv.select(false));
+    }
     /**
      * returns a list of all currently selected DocumentViews
      */
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 576b5bbe0..dcc6e27ed 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -30,12 +30,12 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        if (Doc.SelectOnLoad === this.Document && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) {
+        if (DocumentView.SelectOnLoad === this.Document && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) {
             this._props.select(false);
 
-            this._ref.current!.mathField.focus();
-            this.dataDoc.text === 'x' && this._ref.current!.mathField.select();
-            Doc.SetSelectOnLoad(undefined);
+            this._ref.current?.mathField.focus();
+            this.dataDoc.text === 'x' && this._ref.current?.mathField.select();
+            DocumentView.SetSelectOnLoad(undefined);
         }
         reaction(
             () => this._props.isSelected(),
@@ -66,7 +66,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 color: StrCast(this.Document.color),
                 fontSize: this.fontSize,
             });
-            Doc.SetSelectOnLoad(nextEq);
+            DocumentView.SetSelectOnLoad(nextEq);
             this._props.addDocument?.(nextEq);
             e.stopPropagation();
         }
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index dcf9e1fed..7fb83571f 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -17,6 +17,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import './LabelBox.scss';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { RichTextMenu } from './formattedText/RichTextMenu';
+import { DocumentView } from './DocumentView';
 
 @observer
 export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -230,8 +231,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             if (this._divRef) {
                                 this._divRef.addEventListener('beforeinput', this.beforeInput);
 
-                                if (Doc.SelectOnLoad === this.Document) {
-                                    Doc.SelectOnLoad = undefined;
+                                if (DocumentView.SelectOnLoad === this.Document) {
+                                    DocumentView.SetSelectOnLoad(undefined);
                                     this._divRef.focus();
                                 }
                                 this.fitTextToBox(this._divRef);
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 541b41bf7..792cb6b46 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -353,7 +353,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
         const targetCreator = (annotationOn: Doc | undefined) => {
             const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
-            Doc.SetSelectOnLoad(target);
+            DocumentView.SetSelectOnLoad(target);
             return target;
         };
         const docView = this.DocumentView?.();
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index eb0431b85..a27a8bda1 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -32,7 +32,7 @@
 //     addNoteClick = (e: React.PointerEvent) => {
 //         setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => {
 //             const newDoc = Docs.Create.TextDocument('Note', { _layout_autoHeight: true });
-//             Doc.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+//             DocumentView.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
 //             Doc.AddDocToList(this.props.place, 'data', newDoc);
 //             this._stack?.scrollToBottom();
 //             e.stopPropagation();
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index 95f89a573..0627d382e 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -237,7 +237,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
 
         const targetCreator = (annotationOn: Doc | undefined) => {
             const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
-            Doc.SetSelectOnLoad(target);
+            DocumentView.SetSelectOnLoad(target);
             return target;
         };
         const docView = this.DocumentView?.();
diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx
index 9decbfaf0..7999357b0 100644
--- a/src/client/views/nodes/formattedText/DailyJournal.tsx
+++ b/src/client/views/nodes/formattedText/DailyJournal.tsx
@@ -51,22 +51,13 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @action
     setDailyText() {
         console.log('setDailyText() called...');
-        const initialText = `Journal Entry - ${this.journalDate}\n\nStart writing here...`;
+        const placeholderText = 'Start writing here...';
+        const initialText = `Journal Entry - ${this.journalDate}\n${placeholderText}`;
 
         console.log('Checking if dataDoc has text field...');
 
         console.log('Setting new text field with:', initialText);
-        this.dataDoc[this.fieldKey] = new RichTextField(
-            JSON.stringify({
-                doc: {
-                    type: 'doc',
-                    content: [{ type: 'paragraph', content: [{ type: 'text', text: initialText }] }],
-                },
-                selection: { type: 'text', anchor: 1, head: 1 },
-                storedMarks: [],
-            }),
-            initialText
-        );
+        this.dataDoc[this.fieldKey] = RichTextField.textToRtf(initialText, undefined, placeholderText.length);
 
         console.log('Current text field:', this.dataDoc[this.fieldKey]);
     }
@@ -85,10 +76,11 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
     }
 
     render() {
-        return <div style={{ background: 'beige', width: '100%', height: '100%' }}>     
-         <FormattedTextBox {...this._props} fieldKey={'text'} Document={this.Document} TemplateDataDocument={undefined} />
-                          
-        </div>;
+        return (
+            <div style={{ background: 'beige', width: '100%', height: '100%' }}>
+                <FormattedTextBox {...this._props} fieldKey={'text'} Document={this.Document} TemplateDataDocument={undefined} />
+            </div>
+        );
     }
 }
 
@@ -99,6 +91,7 @@ Docs.Prototypes.TemplateMap.set(DocumentType.JOURNAL, {
         _height: 35,
         _xMargin: 10,
         _yMargin: 10,
+        _layout_autoHeight: true,
         _layout_nativeDimEditable: true,
         _layout_reflowVertical: true,
         _layout_reflowHorizontal: true,
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index c2a2caecf..83ee619d0 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -99,7 +99,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     public static Init(nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }) { FormattedTextBox._nodeViews = nodeViews; } // prettier-ignore
 
     public static PasteOnLoad: ClipboardEvent | undefined;
-    public static DontSelectInitialText = false; // whether initial text should be selected or not
     public static SelectOnLoadChar = '';
     public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
 
@@ -286,7 +285,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             e.stopPropagation();
             const targetCreator = (annotationOn?: Doc) => {
                 const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn);
-                Doc.SetSelectOnLoad(target);
+                DocumentView.SetSelectOnLoad(target);
                 return target;
             };
 
@@ -1542,44 +1541,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             this._editorView.TextView = this;
         }
 
-        const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, Doc.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()));
+        const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, DocumentView.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()));
         const selLoadChar = FormattedTextBox.SelectOnLoadChar;
         if (selectOnLoad) {
-            Doc.SetSelectOnLoad(undefined);
+            DocumentView.SetSelectOnLoad(undefined);
             FormattedTextBox.SelectOnLoadChar = '';
         }
         if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
-            this._props.select(false);
+            const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
+            const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
+            const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
+            const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
+            let { tr } = this.EditorView.state;
             if (selLoadChar) {
-                const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
-                const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
-                const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
-                const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
                 const tr1 = this.EditorView.state.tr.setStoredMarks(storedMarks);
-                const tr2 = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1);
-                const tr = tr2.setStoredMarks(storedMarks);
-
-                this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
-                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
-            } else if (!FormattedTextBox.DontSelectInitialText) {
-                const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
-                selectAll(this.EditorView.state, (tx: Transaction) => {
-                    this.EditorView?.dispatch(tx.addStoredMark(mark));
-                });
-                this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(new TextSelection(this.EditorView.state.doc.resolve(1))));
-                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
-            } else {
-                const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
-                const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
-                const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
-                const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
-                const { tr } = this.EditorView.state;
-                this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks));
-                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
+                tr = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1);
             }
+            this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks));
+            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) {
-            FormattedTextBox.DontSelectInitialText = false;
             this.EditorView!.focus();
         }
         if (this._props.isContentActive()) this.prepareForTyping();
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index bdd41b0bb..805bb4526 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -468,10 +468,6 @@ export class Doc extends RefField {
     }
 }
 export namespace Doc {
-    export let SelectOnLoad: Doc | undefined;
-    export function SetSelectOnLoad(doc: Doc | undefined) {
-        SelectOnLoad = doc;
-    }
     export let DocDragDataName: string = '';
     export function SetDocDragDataName(name: string) {
         DocDragDataName = name;
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
index dc636031a..4b1403b88 100644
--- a/src/fields/RichTextField.ts
+++ b/src/fields/RichTextField.ts
@@ -48,26 +48,77 @@ export class RichTextField extends ObjectField {
             ''
         );
     }
+    static Initialize = (initial?: string) => {
+        const content: object[] = [];
+        const state = {
+            doc: {
+                type: 'doc',
+                content,
+            },
+            selection: {
+                type: 'text',
+                anchor: 0,
+                head: 0,
+            },
+        };
+        if (initial && initial.length) {
+            content.push({
+                type: 'paragraph',
+                content: {
+                    type: 'text',
+                    text: initial,
+                },
+            });
+            state.selection.anchor = state.selection.head = initial.length + 1;
+        }
+        return JSON.stringify(state);
+    };
+
+    private static ToProsemirrorState = (plainText: string, selectBack?: number, delimeter = '\n') => {
+        // Remap the text, creating blocks split on newlines
+        const elements = plainText.split(delimeter);
+
+        // Google Docs adds in an extra carriage return automatically, so this counteracts it
+        !elements[elements.length - 1].length && elements.pop();
+
+        // Preserve the current state, but re-write the content to be the blocks
+        const parsed: Record<string, unknown> = JSON.parse(RichTextField.Initialize());
+        (parsed.doc as Record<string, unknown>).content = elements.map(text => {
+            const paragraph: object = {
+                type: 'paragraph',
+                content: text.length ? [{ type: 'text', marks: [], text }] : undefined, // An empty paragraph gets treated as a line break
+            };
+            return paragraph;
+        });
 
-    public static textToRtf(text: string, imgDocId?: string) {
+        // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it
+        parsed.selection = { type: 'text', anchor: 2 + plainText.length - (selectBack ?? 0), head: 2 + plainText.length };
+
+        // Export the ProseMirror-compatible state object we've just built
+        return JSON.stringify(parsed);
+    };
+
+    public static textToRtf(text: string, imgDocId?: string, selectBack?: number) {
         return new RichTextField(
-            JSON.stringify({
-                // this is a RichText json that has the question text placed above a related image
-                doc: {
-                    type: 'doc',
-                    content: [
-                        {
-                            type: 'paragraph',
-                            attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
-                            content: [
-                                ...(text ? [{ type: 'text', text }] : []), //
-                                ...(imgDocId ? [{ type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: imgDocId } }] : []),
-                            ],
-                        },
-                    ],
-                },
-                selection: { type: 'text', anchor: 2, head: 2 },
-            }),
+            !imgDocId
+                ? this.ToProsemirrorState(text, selectBack)
+                : JSON.stringify({
+                      // this is a RichText json that has the question text placed above a related image
+                      doc: {
+                          type: 'doc',
+                          content: [
+                              {
+                                  type: 'paragraph',
+                                  attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+                                  content: [
+                                      ...(text ? [{ type: 'text', text }] : []), //
+                                      ...(imgDocId ? [{ type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: imgDocId } }] : []),
+                                  ],
+                              },
+                          ],
+                      },
+                      selection: { type: 'text', anchor: 2 + text.length, head: 2 + text.length },
+                  }),
             text
         );
     }
-- 
cgit v1.2.3-70-g09d2


From df173b02ffb7e2937d20a02beec1815b3af7c34e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 9 Mar 2025 17:40:08 -0400
Subject: removed simulation doc.  fixed empty imageBox uploading.  fixed
 script typechecking.  fixed metadata doc.  fixed setting background colors of
 docs that have a template.

---
 src/client/documents/DocUtils.ts                       |  7 +++++--
 src/client/documents/DocumentTypes.ts                  |  1 -
 src/client/documents/Documents.ts                      |  4 ----
 src/client/util/CurrentUserUtils.ts                    | 16 ++++++++--------
 src/client/util/Scripting.ts                           |  7 +++----
 src/client/views/Main.tsx                              |  2 --
 src/client/views/StyleProvider.tsx                     |  2 +-
 .../collections/collectionSchema/SchemaCellField.tsx   |  2 --
 src/client/views/nodes/DiagramBox.tsx                  |  5 ++++-
 src/client/views/nodes/DocumentContentsView.tsx        | 11 ++++-------
 src/client/views/nodes/DocumentView.tsx                |  2 +-
 src/client/views/nodes/ImageBox.tsx                    | 18 +++++++-----------
 src/fields/Doc.ts                                      |  1 -
 13 files changed, 33 insertions(+), 45 deletions(-)

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

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index b216551d5..1c7ccadd1 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -691,6 +691,7 @@ export namespace DocUtils {
             }
             generatedDocuments.push(doc);
         }
+        return doc;
     }
 
     export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, annotationOn?: Doc, backgroundColor?: string) {
@@ -841,7 +842,7 @@ export namespace DocUtils {
     export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) {
         const generatedDocuments: Doc[] = [];
         // Since this file has an overwriteDoc, we can set the client tracking guid to the overwriteDoc's guid.
-        Networking.UploadFilesToServer([{ file, guid: overwriteDoc[Id] }]).then(upfiles => {
+        return Networking.UploadFilesToServer([{ file, guid: overwriteDoc[Id] }]).then(upfiles => {
             const {
                 source: { newFilename, mimetype },
                 result,
@@ -851,7 +852,9 @@ export namespace DocUtils {
                     overwriteDoc.loadingError = result.message;
                     Doc.removeCurrentlyLoading(overwriteDoc);
                 }
-            } else newFilename && mimetype && processFileupload(generatedDocuments, newFilename, mimetype, result, options, overwriteDoc);
+                return undefined;
+            }
+            return newFilename && mimetype ? processFileupload(generatedDocuments, newFilename, mimetype, result, options, overwriteDoc) : undefined;
         });
     }
 
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 00f5dedd6..03626107f 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -31,7 +31,6 @@ export enum DocumentType {
     DATAVIZ = 'dataviz',
     ANNOPALETTE = 'annopalette',
     LOADING = 'loading',
-    SIMULATION = 'simulation', // physics simulation
     MESSAGE = 'message', // chat message
 
     // special purpose wrappers that either take no data or are compositions of lower level types
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4b5979965..317bb7feb 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1150,9 +1150,5 @@ export namespace Docs {
         export function DelegateDocument(proto: Doc, options: DocumentOptions = {}) {
             return InstanceFromProto(proto, undefined, options);
         }
-
-        export function SimulationDocument(options?: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.SIMULATION), undefined, { ...(options || {}) });
-        }
     }
 }
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 778b522c4..21e1d3e12 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -109,7 +109,7 @@ export class CurrentUserUtils {
         const reqdClickList = reqdTempOpts.map(opts => {
             const allOpts = {...reqdClickOpts, ...opts.opts};
             const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(fdoc => fdoc.title === opts.opts.title): undefined;
-            return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, allOpts),allOpts);
+            return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script),allOpts);
         });
 
         const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, isSystem: true};
@@ -270,21 +270,23 @@ export class CurrentUserUtils {
 
             // "<div style={'height:100%'}>" +
             // "    <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._header_pointerEvents||`none`}' fontSize='{this._header_fontSize}px' height='{this._header_height}px' background='{this._header_color}' />" +
-            // "    <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{(this._header_height)*scale}px' height='calc({100/scale}% - {this._header_height}px)'/>" +
+            // "    <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{this._header_height}px' height='calc(100% - {this._header_height}px)'/>" +
             // "</div>";
         const headerBtnHgt = 10;
         const headerTemplate = (opts:DocumentOptions) => 
             MakeTemplate(Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "Header Template", 
-                layout:`<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>
+                layout:`<HTMLdiv transformOrigin='top left' width='100%' height='100%'>
                             <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._header_height||0}px)'  />
                             <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._header_fontSize||9}px' height='{(this._header_height||0)}px' backgroundColor='{this._header_color || "lightGray"}' />
-                            <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._header_height=Math.min(Math.max(0,this._height-30),this._header_height===0?50:0)) + (this._layout_autoHeightMargins=this._header_height ? this._header_height+${headerBtnHgt}:0)’} > Metadata</HTMLdiv>
+                            <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow'
+                                   onClick={‘(this._header_height=(this._header_height===0?50:0)) + (this._layout_autoHeightMargins=this._header_height ? this._header_height+${headerBtnHgt}:0)’} > Metadata
+                                   </HTMLdiv>
                         </HTMLdiv>`
                     }, "header"));
         const slideView = (opts:DocumentOptions) => 
             MakeTemplate(Docs.Create.MultirowDocument(
                     [
-                        Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200,  isSystem: true }),
+                        Docs.Create.MulticolumnDocument([], { title: "hero", _xMargin: 10, _height: 200,  isSystem: true }),
                         Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true,  _height: 100, isSystem: true, text_fontFamily: StrCast(Doc.UserDoc().fontFamily), text_fontSize: StrCast(Doc.UserDoc().fontSize) })
                     ], {...opts, title: "Slide View Template"}));
         const plotlyApi = () => {
@@ -390,7 +392,6 @@ pie title Minerals in my tap water
             {key: "Image",      creator: opts => Docs.Create.ImageDocument("", opts),       opts: { _width: 400, _height:400 }},
             {key: "Equation",   creator: opts => Docs.Create.EquationDocument("",opts),     opts: { _width: 50,  _height: 50, nativeWidth: 40, nativeHeight: 40, _xMargin: 10, _yMargin: 10}},
             {key: "Noteboard",  creator: opts => Docs.Create.NoteTakingDocument([], opts),  opts: { _width: 250, _height: 200, _layout_fitWidth: true}},
-            {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts),      opts: { _width: 300, _height: 300, }},
             {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts),    opts: { _width: 150, _height: 100, _layout_fitWidth: true }},
             {key: "Webpage",    creator: opts => Docs.Create.WebDocument("",opts),          opts: { _width: 400, _height: 512, _nativeWidth: 850, data_useCors: true, }},
             {key: "Comparison", creator: opts => Docs.Create.ComparisonDocument("", opts),  opts: { _width: 300, _height: 300 }},
@@ -428,7 +429,6 @@ pie title Minerals in my tap water
             { toolTip: "Tap or drag to create an equation",       title: "Math", icon: "calculator",    dragFactory: doc.emptyEquation as Doc,   clickFactory: DocCast(doc.emptyEquation)},
             { toolTip: "Tap or drag to create a mermaid node",    title: "Mermaids", icon: "rocket",    dragFactory: doc.emptyMermaids as Doc,   clickFactory: DocCast(doc.emptyMermaids)}, 
             { toolTip: "Tap or drag to create a plotly node",     title: "Plotly", icon: "rocket",      dragFactory: doc.emptyPlotly as Doc,     clickFactory: DocCast(doc.emptyMermaids)}, 
-            { toolTip: "Tap or drag to create a physics simulation",title: "Simulation", icon: "rocket",dragFactory: doc.emptySimulation as Doc, clickFactory: DocCast(doc.emptySimulation), funcs: { hidden: "IsNoviceMode()"}}, 
             { toolTip: "Tap or drag to create a note board",      title: "Notes", icon: "book",         dragFactory: doc.emptyNoteboard as Doc,  clickFactory: DocCast(doc.emptyNoteboard)},
             { toolTip: "Tap or drag to create an image",          title: "Image", icon: "image",        dragFactory: doc.emptyImage as Doc,      clickFactory: DocCast(doc.emptyImage)},
             { toolTip: "Tap or drag to create a collection",      title: "Col", icon: "folder",         dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)},
@@ -443,7 +443,7 @@ pie title Minerals in my tap water
             { toolTip: "Tap or drag to create a button",          title: "Button", icon: "circle",      dragFactory: doc.emptyButton as Doc,     clickFactory: DocCast(doc.emptyButton)},
             { toolTip: "Tap or drag to create a scripting box",   title: "Script", icon: "terminal",    dragFactory: doc.emptyScript as Doc,     clickFactory: DocCast(doc.emptyScript),                                             funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data viz node",   title: "DataViz", icon: "chart-bar",  dragFactory: doc.emptyDataViz as Doc,    clickFactory: DocCast(doc.emptyDataViz)},
-            { toolTip: "Tap or drag to create a journal entry",   title: "Journal", icon: "note",       dragFactory: doc.emptyDailyJournal as Doc,clickFactory:DocCast(doc.emptyDataJournal), },
+            { toolTip: "Tap or drag to create a journal entry",   title: "Journal", icon: "book",       dragFactory: doc.emptyDailyJournal as Doc,clickFactory:DocCast(doc.emptyDataJournal), },
             { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "person-chalkboard",   dragFactory: doc.emptySlide as Doc,       clickFactory: DocCast(doc.emptySlide),     openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a view slide",      title: "View Slide", icon: "address-card",       dragFactory: doc.emptyViewSlide as Doc,   clickFactory: DocCast(doc.emptyViewSlide), openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data note",       title: "DataNote", icon: "window-maximize",      dragFactory: doc.emptyHeader as Doc,      clickFactory: DocCast(doc.emptyHeader),    openFactoryAsDelegate: true,           funcs: { hidden: "IsNoviceMode()"} },
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index c6d98496a..b0886a67c 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -54,6 +54,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: ts
         (diag.code !== 2552 ||!Object.keys(scriptingGlobals).includes(diagnostics[0].messageText.toString().match(/Cannot find name '([A-Za-z0-9$-_]+)'/)?.[1]??"-------"))
     ); // prettier-ignore
     if ((options.typecheck !== false && errors.length) || !script) {
+        console.log('Script Compile Failed: ' + script + ' ', errors);
         return { compiled: false, errors };
     }
 
@@ -228,10 +229,8 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
     }
     paramNames.push(...Object.keys(params).filter(p => p !== 'this' && !Object.keys(capturedVariables).includes(p)));
 
-    const paramList = paramNames.map(key => {
-        const val = typeof params[key] === 'string' && params[key].length && !'"\'`'.includes(params[key][0]) ? `"${params[key]}"` : params[key];
-        return `${key}: ${val}`;
-    });
+    const paramList = paramNames.map(key => `${key}: ${params[key] === Doc.name ? 'any' : params[key]}`);
+
     for (const key in capturedVariables) {
         if (key !== 'this') {
             const val = capturedVariables[key];
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index e5347e692..22725a2b9 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -45,7 +45,6 @@ import { LoadingBox } from './nodes/LoadingBox';
 import { MapBox } from './nodes/MapBox/MapBox';
 import { MapPushpinBox } from './nodes/MapBox/MapPushpinBox';
 import { PDFBox } from './nodes/PDFBox';
-import { PhysicsSimulationBox } from './nodes/PhysicsBox/PhysicsSimulationBox';
 import { RecordingBox } from './nodes/RecordingBox';
 import { ScreenshotBox } from './nodes/ScreenshotBox';
 import { ScriptingBox } from './nodes/ScriptingBox';
@@ -154,7 +153,6 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
             CalendarBox,
             ComparisonBox,
             LoadingBox,
-            PhysicsSimulationBox,
             SchemaRowBox,
             ImportElementBox,
             MapPushpinBox,
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index b04b1ae65..331ee1707 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -254,7 +254,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
             const usePath = StrCast(doc?.[dataKey + "_usePath"]);
             const alternate = usePath.includes(":hover") ? ( isHovering?.() ? '_' + usePath.replace(":hover","") : "") : usePath ? "_" +usePath:usePath;
             let docColor: Opt<string> = StrCast(doc?.[fieldKey+alternate], StrCast(doc?.['backgroundColor' +alternate], isCaption ? 'rgba(0,0,0,0.4)' : ''));
-            if (doc?.[StrCast(doc?.layout_fieldKey)] instanceof Doc) docColor = StrCast(doc._backgroundColor,docColor)
+            if (!docColor && doc?.[StrCast(doc?.layout_fieldKey)] instanceof Doc) docColor = StrCast(doc._backgroundColor,docColor)
             // prettier-ignore
             switch (layoutDoc?.type) {
                 case DocumentType.PRESELEMENT: docColor = docColor || ""; break;
diff --git a/src/client/views/collections/collectionSchema/SchemaCellField.tsx b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
index 20ec2f151..e6acff061 100644
--- a/src/client/views/collections/collectionSchema/SchemaCellField.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
@@ -107,7 +107,6 @@ export class SchemaCellField extends ObservableReactComponent<SchemaCellFieldPro
         this._disposers.fieldUpdate = reaction(
             () => this._props.GetValue(),
             fieldVal => {
-                console.log('Update: ' + this._props.Document.title, this._props.fieldKey, fieldVal);
                 this._unrenderedContent = fieldVal ?? '';
                 this.finalizeEdit(false, false, false);
             }
@@ -127,7 +126,6 @@ export class SchemaCellField extends ObservableReactComponent<SchemaCellFieldPro
     _unmounted = false;
     componentWillUnmount(): void {
         this._unmounted = true;
-        console.log('Unmount: ' + this._props.Document.title, this._props.fieldKey);
         this._overlayDisposer?.();
         Object.values(this._disposers).forEach(disposer => disposer?.());
         this.finalizeEdit(false, true, false);
diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx
index e77cde431..a49c69be3 100644
--- a/src/client/views/nodes/DiagramBox.tsx
+++ b/src/client/views/nodes/DiagramBox.tsx
@@ -18,6 +18,7 @@ import { InkingStroke } from '../InkingStroke';
 import './DiagramBox.scss';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { Tooltip } from '@mui/material';
 /**
  * this is a class for the diagram box doc type that can be found in the tools section of the side bar
  */
@@ -208,7 +209,9 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     <button type="button" onClick={this.generateMermaidCode}>
                         Gen
                     </button>
-                    <input type="checkbox" onClick={action(() => (this._showCode = !this._showCode))} />
+                    <Tooltip title="show diagram code">
+                        <input type="checkbox" onClick={action(() => (this._showCode = !this._showCode))} />
+                    </Tooltip>
                 </div>
                 <div className="DIYNodeBox-content">
                     {this._showCode ? (
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index aab8a183a..47c5734f7 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -23,7 +23,6 @@ interface HTMLtagProps {
     htmltag: string;
     onClick?: ScriptField;
     onInput?: ScriptField;
-    scaling: number;
     children?: JSX.Element[];
 }
 
@@ -43,7 +42,7 @@ interface HTMLtagProps {
 export class HTMLtag extends React.Component<HTMLtagProps> {
     click = () => {
         const clickScript = this.props.onClick as Opt<ScriptField>;
-        clickScript?.script.run({ this: this.props.Document, scale: this.props.scaling });
+        clickScript?.script.run({ this: this.props.Document });
     };
     onInput = (e: React.FormEvent<unknown>) => {
         const onInputScript = this.props.onInput as Opt<ScriptField>;
@@ -56,7 +55,6 @@ export class HTMLtag extends React.Component<HTMLtagProps> {
             'dragStarting',
             'dragEnding',
             'htmltag',
-            'scaling',
             'Document',
             'key',
             'onInput',
@@ -65,7 +63,7 @@ export class HTMLtag extends React.Component<HTMLtagProps> {
         ]).omit;
         const replacer = (match: string, expr: string) =>
             // bcz: this executes a script to convert a property expression string:  { script }  into a value
-            (ScriptField.MakeFunction(expr, { this: Doc.name, scale: 'number' })?.script.run({ this: this.props.Document, scale: this.props.scaling }).result as string) || '';
+            (ScriptField.MakeFunction(expr, { this: Doc.name })?.script.run({ this: this.props.Document }).result as string) || '';
         Object.keys(divKeys).forEach((prop: string) => {
             const p = (this.props as unknown as { [key: string]: string })[prop] as string;
             style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer);
@@ -166,12 +164,11 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
         layoutFrame = layoutFrame.replace(/(>[^{]*)[^=]\{([^.'][^<}]+)\}([^}]*<)/g, replacer);
 
         // replace HTML<tag> with corresponding HTML tag as in:  <HTMLdiv> becomes  <HTMLtag Document={props.Document} htmltag='div'>
-        const replacer2 = (match: string, p1: string) => `<HTMLtag Document={props.Document} scaling='${this._props.NativeDimScaling?.() || 1}' htmltag='${p1}'`;
+        const replacer2 = (match: string, p1: string) => `<HTMLtag Document={props.Document} htmltag='${p1}'`;
         layoutFrame = layoutFrame.replace(/<HTML([a-zA-Z0-9_-]+)/g, replacer2);
 
         // replace /HTML<tag> with </HTMLdiv>  as in:  </HTMLdiv> becomes  </HTMLtag>
         const replacer3 = (/* match: any, p1: string, offset: any, string: any */) => `</HTMLtag`;
-
         layoutFrame = layoutFrame.replace(/<\/HTML([a-zA-Z0-9_-]+)/g, replacer3);
 
         // add onClick function to props
@@ -181,7 +178,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
                 const code = XRegExp.matchRecursive(splits[1], '{', '}', '', { valueNames: ['between', 'left', 'match', 'right', 'between'] });
                 layoutFrame = splits[0] + ` ${func}={props.${func}} ` + splits[1].substring(code[1].end + 1);
                 const script = code[1].value.replace(/^‘/, '').replace(/’$/, ''); // ‘’ are not valid quotes in javascript so get rid of them -- they may be present to make it easier to write complex scripts - see headerTemplate in currentUserUtils.ts
-                return ScriptField.MakeScript(script, { this: Doc.name, scale: 'number', value: 'string' });
+                return ScriptField.MakeScript(script, { this: Doc.name, value: 'string' });
             }
             return undefined;
             // add input function to props
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a514ee04e..cac276535 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -134,7 +134,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @computed get border()            { return this.style(this.layoutDoc, StyleProp.Border) as string || ""; } // prettier-ignore
     @computed get borderRounding()    { return this.style(this.layoutDoc, StyleProp.BorderRounding) as string; } // prettier-ignore
     @computed get widgetDecorations() { return this.style(this.layoutDoc, StyleProp.Decorations) as JSX.Element; } // prettier-ignore
-    @computed get backgroundBoxColor(){ return this.style(this.layoutDoc, StyleProp.BackgroundColor + ':docView') as string; } // prettier-ignore
+    @computed get backgroundBoxColor(){ return this.style(this.Document, StyleProp.BackgroundColor + ':docView') as string; } // prettier-ignore
     @computed get showTitle()         { return this.style(this.layoutDoc, StyleProp.ShowTitle) as Opt<string>; } // prettier-ignore
     @computed get showCaption()       { return this.style(this.layoutDoc, StyleProp.ShowCaption) as string ?? "";  } // prettier-ignore
     @computed get headerMargin()      { return this.style(this.layoutDoc, StyleProp.HeaderMargin)  as number ?? 0; } // prettier-ignore
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 017ef7191..5b06e9fc5 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -148,9 +148,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
         const { layoutDoc } = this;
         this._disposers.path = reaction(
-            () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }),
-            ({ nativeSize, width }) => {
-                if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !this.layoutDoc._height) {
+            () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }),
+            ({ nativeSize, width, height }) => {
+                if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !height) {
                     this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
                 }
             },
@@ -834,14 +834,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             const file = input.files?.[0];
             if (file) {
                 const disposer = OverlayView.ShowSpinner();
-                const [{ result }] = await Networking.UploadFilesToServer({ file });
-                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.assignImageInfo(result, this.dataDoc);
-                }
-                disposer();
+                DocUtils.uploadFileToDoc(file, {}, this.Document).then(doc => {
+                    disposer();
+                    doc && (doc.height = undefined);
+                });
             } else {
                 console.log('No file selected');
             }
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index dc4a5a011..fc89dcbe7 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1517,7 +1517,6 @@ export namespace Doc {
             case DocumentType.MAP: return 'map-marker-alt';
             case DocumentType.DATAVIZ: return 'chart-bar';
             case DocumentType.EQUATION: return 'calculator';
-            case DocumentType.SIMULATION: return 'rocket';
             case DocumentType.CONFIG: return 'folder-closed';
             default:
         }
-- 
cgit v1.2.3-70-g09d2

