From 5b5730a7df073659cbb6c326f748f7fcbe6625e8 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 18 Jan 2024 15:05:53 -0500
Subject: lots of changes to try to simplify API for viewPaths and related

---
 src/client/views/PropertiesButtons.tsx | 46 ++++++++++++++++------------------
 1 file changed, 21 insertions(+), 25 deletions(-)

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

diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 5905f8dbc..bba6285c2 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -345,9 +345,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     @undoBatch
     handlePerspectiveChange = (e: any) => {
         this.selectedDoc && (this.selectedDoc._type_collection = e.target.value);
-        SelectionManager.Views.filter(dv => dv.docView)
-            .map(dv => dv.docView!)
-            .forEach(docView => (docView.layoutDoc._type_collection = e.target.value));
+        SelectionManager.Views.forEach(docView => (docView.layoutDoc._type_collection = e.target.value));
     };
     @computed get onClickVal() {
         const linkButton = IsFollowLinkScript(this.selectedDoc.onClick);
@@ -408,28 +406,26 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     @undoBatch
     @action
     handleOptionChange = (onClick: string) => {
-        SelectionManager.Views.filter(dv => dv.docView)
-            .map(dv => dv.docView!)
-            .forEach(docView => {
-                const linkButton = IsFollowLinkScript(docView.Document.onClick);
-                docView.noOnClick();
-                switch (onClick) {
-                    case 'enterPortal':
-                        docView.makeIntoPortal();
-                        break;
-                    case 'toggleDetail':
-                        docView.setToggleDetail();
-                        break;
-                    case 'linkInPlace':
-                        docView.toggleFollowLink(false, false);
-                        docView.Document.followLinkLocation = linkButton ? OpenWhere.lightbox : undefined;
-                        break;
-                    case 'linkOnRight':
-                        docView.toggleFollowLink(false, false);
-                        docView.Document.followLinkLocation = linkButton ? OpenWhere.addRight : undefined;
-                        break;
-                }
-            });
+        SelectionManager.Views.forEach(docView => {
+            const linkButton = IsFollowLinkScript(docView.Document.onClick);
+            docView.noOnClick();
+            switch (onClick) {
+                case 'enterPortal':
+                    docView.makeIntoPortal();
+                    break;
+                case 'toggleDetail':
+                    docView.setToggleDetail();
+                    break;
+                case 'linkInPlace':
+                    docView.toggleFollowLink(false, false);
+                    docView.Document.followLinkLocation = linkButton ? OpenWhere.lightbox : undefined;
+                    break;
+                case 'linkOnRight':
+                    docView.toggleFollowLink(false, false);
+                    docView.Document.followLinkLocation = linkButton ? OpenWhere.addRight : undefined;
+                    break;
+            }
+        });
     };
 
     @undoBatch
-- 
cgit v1.2.3-70-g09d2


From e3fde25014d523c5f43a138093718899fe17d108 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 7 Feb 2024 16:18:16 -0500
Subject: made various render methods in DocumentView computed getters for
 efficiency and to avoid artifacts (LInkanchorBox dragging) when something
 else invalidates causing components to regenerate.  fixed linklines to
 animate when doing a zoom transition and to be able to target texts
 hyperlinks.  fixed link lines to share properties with ink and updated the
 properties panel / menus to allow editing of either.   addding toggling link
 lines on and off from linkitemmenu

---
 src/client/documents/Documents.ts                  |   2 +-
 src/client/views/DocComponent.tsx                  |   1 +
 src/client/views/PropertiesButtons.tsx             |   2 +-
 src/client/views/PropertiesView.tsx                | 104 ++---------
 src/client/views/StyleProvider.tsx                 |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   1 +
 src/client/views/global/globalScripts.ts           |  26 +--
 src/client/views/linking/LinkMenuItem.tsx          |  24 ++-
 src/client/views/nodes/DocumentView.tsx            |  35 ++--
 src/client/views/nodes/LinkBox.tsx                 | 197 +++++++++++++--------
 src/client/views/nodes/formattedText/marks_rts.ts  |   5 +-
 src/fields/Doc.ts                                  |   3 +-
 src/fields/DocSymbols.ts                           |   1 +
 13 files changed, 201 insertions(+), 202 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 1978c144b..355a4c937 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -599,7 +599,7 @@ export namespace Docs {
                         _width: 1,
                         link: '',
                         link_description: '',
-                        backgroundColor: 'lightblue', // lightblue is default color for linking dot and link documents text comment area
+                        color: 'lightBlue', // lightblue is default color for linking dot and link documents text comment area
                         _dropPropertiesToRemove: new List(['onClick']),
                     },
                 },
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index dacd359c5..99b9c3045 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -35,6 +35,7 @@ export interface ViewBoxInterface {
     addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections)
     select?: (ctrlKey: boolean, shiftKey: boolean) => void;
     focus?: (textAnchor: Doc, options: FocusViewOptions) => Opt<number>;
+    viewTransition?: () => Opt<string>; // duration of a view transition animation
     isAnyChildContentActive?: () => boolean; // is any child content of the document active
     onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
     getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index bba6285c2..cb38ab602 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -411,7 +411,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             docView.noOnClick();
             switch (onClick) {
                 case 'enterPortal':
-                    docView.makeIntoPortal();
+                    DocUtils.makeIntoPortal(docView.Document, docView.layoutDoc, docView.allLinks);
                     break;
                 case 'toggleDetail':
                     docView.setToggleDetail();
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 07f285eaf..3ae2362a1 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -741,7 +741,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         switch (field) {
             case 'Xps': selDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10); break;
             case 'Yps': selDoc.y = NumCast(this.selectedDoc?.y) + (dirs === 'up' ? 10 : -10); break;
-            case 'stk': selDoc.stroke_width = NumCast(this.selectedDoc?.stroke_width) + (dirs === 'up' ? 0.1 : -0.1); break;
+            case 'stk': selDoc.stroke_width = NumCast(this.selectedDoc?.[DocData].stroke_width) + (dirs === 'up' ? 0.1 : -0.1); break;
             case 'wid':
                 const oldWidth = NumCast(selDoc._width);
                 const oldHeight = NumCast(selDoc._height);
@@ -786,7 +786,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     };
 
     getField(key: string) {
-        return Field.toString(this.selectedDoc?.[key] as Field);
+        return Field.toString(this.selectedDoc?.[DocData][key] as Field);
     }
 
     @computed get shapeXps() {
@@ -801,6 +801,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @computed get shapeWid() {
         return NumCast(this.selectedDoc?._width);
     }
+    @computed get strokeThk() {
+        return NumCast(this.selectedDoc?.[DocData].stroke_width);
+    }
     set shapeXps(value) {
         this.selectedDoc && (this.selectedDoc.x = Math.round(value * 100) / 100);
     }
@@ -813,6 +816,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     set shapeHgt(value) {
         this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100);
     }
+    set strokeThk(value) {
+        this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100);
+    }
 
     @computed get hgtInput() {
         return this.inputBoxDuo(
@@ -845,16 +851,16 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     private _lastDash: any = '2';
 
     @computed get colorFil() {
-        return StrCast(this.selectedDoc?.fillColor);
+        return StrCast(this.selectedDoc?.[DocData].fillColor);
     }
     @computed get colorStk() {
-        return StrCast(this.selectedDoc?.color);
+        return StrCast(this.selectedDoc?.[DocData].color);
     }
     set colorFil(value) {
-        this.selectedDoc && (this.selectedDoc.fillColor = value ? value : undefined);
+        this.selectedDoc && (this.selectedDoc[DocData].fillColor = value ? value : undefined);
     }
     set colorStk(value) {
-        this.selectedDoc && (this.selectedDoc.color = value ? value : undefined);
+        this.selectedDoc && (this.selectedDoc[DocData].color = value ? value : undefined);
     }
 
     colorButton(value: string, type: string, setter: () => void) {
@@ -943,19 +949,19 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
     set dashdStk(value) {
         value && (this._lastDash = value);
-        this.selectedDoc && (this.selectedDoc.stroke_dash = value ? this._lastDash : undefined);
+        this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined);
     }
     set markScal(value) {
-        this.selectedDoc && (this.selectedDoc.stroke_markerScale = Number(value));
+        this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value));
     }
     set widthStk(value) {
-        this.selectedDoc && (this.selectedDoc.stroke_width = Number(value));
+        this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Number(value));
     }
     set markHead(value) {
-        this.selectedDoc && (this.selectedDoc.stroke_startMarker = value);
+        this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
     }
     set markTail(value) {
-        this.selectedDoc && (this.selectedDoc.stroke_endMarker = value);
+        this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
     }
 
     @computed get stkInput() {
@@ -996,52 +1002,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @computed get widthAndDash() {
         return (
             <div className="widthAndDash">
-                <div className="width">
-                    <div className="width-top">
-                        <div className="width-title">Width:</div>
-                        <div className="width-input">{this.stkInput}</div>
-                    </div>
-                    <input
-                        className="width-range"
-                        type="range"
-                        style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
-                        defaultValue={Number(this.widthStk)}
-                        min={1}
-                        max={100}
-                        onChange={action(e => (this.widthStk = e.target.value))}
-                        onMouseDown={e => {
-                            this._widthUndo = UndoManager.StartBatch('width undo');
-                        }}
-                        onMouseUp={e => {
-                            this._widthUndo?.end();
-                            this._widthUndo = undefined;
-                        }}
-                    />
-                </div>
+                <div className="width">{this.getNumber('Thickness', '', 0, Math.max(50, this.strokeThk), this.strokeThk, (val: number) => !isNaN(val) && (this.strokeThk = val), 50, 1)}</div>
+                <div className="width">{this.getNumber('Arrow Scale', '', 0, Math.max(10, this.markScal), this.markScal, (val: number) => !isNaN(val) && (this.markScal = val), 10, 1)}</div>
 
                 <div className="arrows">
-                    <div className="arrows-head">
-                        <div className="width-top">
-                            <div className="width-title">Arrow Scale:</div>
-                            {/* <div className="width-input">{this.markScalInput}</div> */}
-                        </div>
-                        <input
-                            className="width-range"
-                            type="range"
-                            defaultValue={this.markScal}
-                            style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
-                            min={0}
-                            max={10}
-                            onChange={action(e => (this.markScal = +e.target.value))}
-                            onMouseDown={e => {
-                                this._widthUndo = UndoManager.StartBatch('scale undo');
-                            }}
-                            onMouseUp={e => {
-                                this._widthUndo?.end();
-                                this._widthUndo = undefined;
-                            }}
-                        />
-                    </div>
                     <div className="arrows-head">
                         <div className="arrows-head-title">Arrow Head: </div>
                         <input key="markHead" className="arrows-head-input" type="checkbox" checked={this.markHead !== ''} onChange={undoBatch(action(() => (this.markHead = this.markHead ? '' : 'arrow')))} />
@@ -1442,36 +1406,6 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                         <p>Description</p>
                         {this.editDescription}
                     </div>
-                    <div className="propertiesView-input inline">
-                        <p>Show link</p>
-                        <button
-                            style={{ background: !LinkManager.Instance.currentLink?.link_displayLine ? '' : '#4476f7', borderRadius: 3 }}
-                            onPointerDown={e => this.toggleLinkProp(e, 'link_displayLine')}
-                            onClick={e => e.stopPropagation()}
-                            className="propertiesButton">
-                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                        </button>
-                    </div>
-                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
-                        <p>Auto-move anchors</p>
-                        <button
-                            style={{ background: !LinkManager.Instance.currentLink?.link_autoMoveAnchors ? '' : '#4476f7', borderRadius: 3 }}
-                            onPointerDown={e => this.toggleLinkProp(e, 'link_autoMoveAnchors')}
-                            onClick={e => e.stopPropagation()}
-                            className="propertiesButton">
-                            <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
-                        </button>
-                    </div>
-                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
-                        <p>Display arrow</p>
-                        <button
-                            style={{ background: !LinkManager.Instance.currentLink?.link_displayArrow ? '' : '#4476f7', borderRadius: 3 }}
-                            onPointerDown={e => this.toggleLinkProp(e, 'link_displayArrow')}
-                            onClick={e => e.stopPropagation()}
-                            className="propertiesButton">
-                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                        </button>
-                    </div>
                 </div>
                 {!hasSelectedAnchor ? null : (
                     <div className="propertiesView-section">
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index b7e64d9a8..0794efe4c 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -134,7 +134,7 @@ 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, 'transparent'));
+        case StyleProp.FillColor:  return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent')));
         case StyleProp.ShowCaption:return props?.hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption);
         case StyleProp.TitleHeight:return (props?.ScreenToLocalTransform().Scale ?? 1) * NumCast(Doc.UserDoc().headerHeight,30);
         case StyleProp.ShowTitle:
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b08221f99..61fd5fd05 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -238,6 +238,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => this._props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
     elementFunc = () => this._layoutElements;
+    viewTransition = () => (this._panZoomTransition ? '' + this._panZoomTransition : undefined);
     fitContentOnce = () => {
         const vals = this.fitToContentVals;
         this.layoutDoc._freeform_panX = vals.bounds.cx;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 813cb9338..0541a9ca7 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -43,14 +43,14 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
     } else if (selectedViews.length) {
         if (checkResult) {
             const selView = selectedViews.lastElement();
-            const fieldKey = selView.Document.type === DocumentType.INK ? 'fillColor' : 'backgroundColor';
+            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';
         }
         selectedViews.some(dv => dv.ComponentView instanceof InkingStroke) && SetActiveFillColor(color ?? 'transparent');
         selectedViews.forEach(dv => {
-            const fieldKey = dv.Document.type === DocumentType.INK ? 'fillColor' : 'backgroundColor';
+            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
             const contentFrameNumber = Cast(dv.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
             if (contentFrameNumber !== undefined) {
@@ -344,24 +344,24 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | '
     // prettier-ignore
     const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
         ['inkMask', {
-            checkResult: () => ((selected?.type === DocumentType.INK ? BoolCast(selected.stroke_isInkMask) : ActiveIsInkMask())),
-            setInk: (doc: Doc) => (doc.stroke_isInkMask = !doc.stroke_isInkMask),
+            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()),
         }],
         ['fillColor', {
-            checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ?? "transparent"),
-            setInk: (doc: Doc) => (doc.fillColor = StrCast(value)),
+            checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveFillColor() ?? "transparent"),
+            setInk: (doc: Doc) => (doc[DocData].fillColor = StrCast(value)),
             setMode: () => SetActiveFillColor(StrCast(value)),
         }],
         [ 'strokeWidth', {
-            checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.stroke_width) : ActiveInkWidth()),
-            setInk: (doc: Doc) => (doc.stroke_width = NumCast(value)),
-            setMode: () => { SetActiveInkWidth(value.toString()); setActiveTool( GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
+            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);},
         }],
         ['strokeColor', {
-            checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.color) : ActiveInkColor()),
-            setInk: (doc: Doc) => (doc.color = String(value)),
-            setMode: () => { SetActiveInkColor(StrCast(value));  setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
+            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);},
         }],
     ]);
 
@@ -369,7 +369,7 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | '
         return map.get(option)?.checkResult();
     }
     map.get(option)?.setMode();
-    SelectionManager.Docs.filter(doc => doc.type === DocumentType.INK).map(doc => map.get(option)?.setInk(doc));
+    SelectionManager.Docs.filter(doc => doc._layout_isSvg).map(doc => map.get(option)?.setInk(doc));
 });
 
 /** WEB
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 7427f4310..92c63cd56 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -74,6 +74,14 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
         return this._props.sourceDoc;
     }
 
+    onIconDown = (e: React.PointerEvent) => {
+        setupMoveUpEvents(this, e, returnFalse, returnFalse, () => {
+            if (!this._props.docView._props.removeDocument?.(this._props.linkDoc)) {
+                this._props.docView._props.addDocument?.(this._props.linkDoc);
+            }
+        });
+    };
+
     onEdit = (e: React.PointerEvent) => {
         setupMoveUpEvents(
             this,
@@ -196,12 +204,16 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                                 </p>
                             ) : null}
                             <div className="linkMenu-title-wrapper">
-                                <div className="destination-icon-wrapper">
-                                    <FontAwesomeIcon className="destination-icon" icon={destinationIcon} size="sm" />
-                                </div>
-                                <p className="linkMenu-destination-title">
-                                    {this._props.linkDoc.linksToAnnotation && Cast(this._props.destinationDoc.data, WebField)?.url.href === this._props.linkDoc.annotationUri ? 'Annotation in' : ''} {StrCast(title)}
-                                </p>
+                                <Tooltip title={<div className="dash-tooltip">Show/Hide Link</div>}>
+                                    <div title="click to show link" className="destination-icon-wrapper" onPointerDown={this.onIconDown}>
+                                        <FontAwesomeIcon className="destination-icon" icon={destinationIcon} size="sm" />
+                                    </div>
+                                </Tooltip>
+                                <Tooltip title={<div className="dash-tooltip">Follow Link</div>}>
+                                    <p className="linkMenu-destination-title">
+                                        {this._props.linkDoc.linksToAnnotation && Cast(this._props.destinationDoc.data, WebField)?.url.href === this._props.linkDoc.annotationUri ? 'Annotation in' : ''} {StrCast(title)}
+                                    </p>
+                                </Tooltip>
                             </div>
                             {!this._props.linkDoc.link_description ? null : <p className="linkMenu-description">{StrCast(this._props.linkDoc.link_description).split('\n')[0].substring(0, 50)}</p>}
                         </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5efa028d1..042ae6e55 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -48,6 +48,7 @@ import { KeyValueBox } from './KeyValueBox';
 import { LinkAnchorBox } from './LinkAnchorBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 interface Window {
     MediaRecorder: MediaRecorder;
 }
@@ -726,7 +727,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     };
 
     removeLinkByHiding = (link: Doc) => () => (link.link_displayLine = false);
-    allLinkEndpoints = () => {
+    @computed get allLinkEndpoints() {
         // the small blue dots that mark the endpoints of links
         if (this._componentView instanceof KeyValueBox || this._props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this._props.dontRegisterView || this.layoutDoc.layout_unrendered) return null;
         return this.filteredLinks.map(link => (
@@ -750,9 +751,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 />
             </div>
         ));
-    };
+    }
 
-    viewBoxContents = () => {
+    @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
         const noBackground = this.Document.isGroup && !this._props.LayoutTemplateString?.includes(KeyValueBox.name) && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
@@ -778,10 +779,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     setTitleFocus={this.setTitleFocus}
                     hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                 />
-                {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints()}
+                {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints}
             </div>
         );
-    };
+    }
 
     captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption');
     fieldsDropdown = (reqdFields: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => {
@@ -814,7 +815,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
      * setting layout_showTitle using the format:   field1[;field2[...][:hover]]
      * from the UI, this is done by clicking the title field and prefixin the format with '#'.  eg.,  #field1[;field2;...][:hover]
      **/
-    titleView = () => {
+    @computed get titleView() {
         const showTitle = this.layout_showTitle?.split(':')[0];
         const showTitleHover = this.layout_showTitle?.includes(':hover');
 
@@ -888,9 +889,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 </div>
             </div>
         );
-    };
+    }
 
-    captionView = () => {
+    @computed get captionView() {
         return !this.layout_showCaption ? null : (
             <div
                 className="documentView-captionWrapper"
@@ -913,7 +914,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 />
             </div>
         );
-    };
+    }
 
     renderDoc = (style: object) => {
         TraceMobx();
@@ -933,15 +934,15 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                           fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'),
                           fontSize: Cast(this.Document._text_fontSize, 'string', null),
                           transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
-                          transition: !this._animateScalingTo ? this._props.DataTransition?.() || StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
+                          transition: !this._animateScalingTo ? this._props.DataTransition?.() : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
                       }}>
                       {this._props.hideTitle || (!showTitle && !this.layout_showCaption) ? (
-                          this.viewBoxContents()
+                          this.viewBoxContents
                       ) : (
                           <div className="documentView-styleWrapper">
-                              {this.titleView()}
-                              {this.viewBoxContents()}
-                              {this.captionView()}
+                              {this.titleView}
+                              {this.viewBoxContents}
+                              {this.captionView}
                           </div>
                       )}
                       {this.widgetDecorations ?? null}
@@ -1191,8 +1192,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
             if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), transition: undefined };
         }
         // transition is returned so that the bounds will 'update' at the end of an animated transition.  This is needed by xAnchor in LinkBox
-        const transition = this.docViewPath().find((parent: DocumentView) => parent._props.DataTransition?.() || StrCast(parent.Document.dataTransition));
-        return { left, top, right, bottom, transition: transition?._props.DataTransition?.() || StrCast(transition?.Document.dataTransition) };
+        const transition = this.docViewPath().find((parent: DocumentView) => parent.DataTransition?.() || parent.ComponentView?.viewTransition?.());
+        return { left, top, right, bottom, transition: transition?.DataTransition?.() || transition?.ComponentView?.viewTransition?.() };
     }
 
     @computed get nativeWidth() {
@@ -1337,6 +1338,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
             }
         }
     };
+    DataTransition = () => this._props.DataTransition?.() || StrCast(this.Document.dataTransition);
     ShouldNotScale = () => this.shouldNotScale;
     NativeWidth = () => this.effectiveNativeWidth;
     NativeHeight = () => this.effectiveNativeHeight;
@@ -1402,6 +1404,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                         <DocumentViewInternal
                             {...this._props}
                             fieldKey={this.LayoutFieldKey}
+                            DataTransition={this.DataTransition}
                             DocumentView={this.selfView}
                             docViewPath={this.docViewPath}
                             PanelWidth={this.PanelWidth}
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 85d22abfd..0788e5adc 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,18 +1,21 @@
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import Xarrow from 'react-xarrows';
+import { DocData } from '../../../fields/DocSymbols';
+import { Id } from '../../../fields/FieldSymbols';
 import { DocCast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, lightOrDark, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../Utils';
 import { DocumentManager } from '../../util/DocumentManager';
 import { LinkManager } from '../../util/LinkManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
+import { EditableView } from '../EditableView';
+import { LightboxView } from '../LightboxView';
 import { StyleProp } from '../StyleProvider';
 import { ComparisonBox } from './ComparisonBox';
 import { FieldView, FieldViewProps } from './FieldView';
 import './LinkBox.scss';
-import { LinkDescriptionPopup } from './LinkDescriptionPopup';
 
 @observer
 export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -43,19 +46,20 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.disposer = reaction(
             () => ({ drag: SnappingManager.IsDragging, a: this.anchor1, b: this.anchor2 }),
             ({ drag, a, b }) => {
-                setTimeout(
-                    // need to wait for drag manager to set 'hidden' flag on dragged elements
-                    action(() => {
-                        let a1 = a && document.getElementById(a.Guid);
-                        let a2 = b && document.getElementById(b.Guid);
-                        if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true;
-                        else {
-                            for (; a1 && !a1.hidden; a1 = a1.parentElement);
-                            for (; a2 && !a2.hidden; a2 = a2.parentElement);
-                            this._hide = a1 || a2 ? true : false;
-                        }
-                    })
-                );
+                !LightboxView.Contains(this.DocumentView?.()) &&
+                    setTimeout(
+                        // need to wait for drag manager to set 'hidden' flag on dragged elements
+                        action(() => {
+                            let a1 = a && document.getElementById(a.Guid);
+                            let a2 = b && document.getElementById(b.Guid);
+                            if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true;
+                            else {
+                                for (; a1 && !a1.hidden; a1 = a1.parentElement);
+                                for (; a2 && !a2.hidden; a2 = a2.parentElement);
+                                this._hide = a1 || a2 ? true : false;
+                            }
+                        })
+                    );
             },
             { fireImmediately: true }
         );
@@ -63,89 +67,130 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     select = (ctrlKey: boolean, shiftKey: boolean) => (LinkManager.Instance.currentLink = this.Document);
 
-    descriptionDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(
-            this,
-            e,
-            returnFalse,
-            returnFalse,
-            action(() => {
-                LinkManager.Instance.currentLink = this.Document;
-                LinkDescriptionPopup.Instance.popupX = e.clientX;
-                LinkDescriptionPopup.Instance.popupY = e.clientY;
-                LinkDescriptionPopup.Instance.display = true;
-
-                const rect = document.body.getBoundingClientRect();
-                if (LinkDescriptionPopup.Instance.popupX + 200 > rect.width) {
-                    LinkDescriptionPopup.Instance.popupX -= 190;
-                }
-                if (LinkDescriptionPopup.Instance.popupY + 100 > rect.height) {
-                    LinkDescriptionPopup.Instance.popupY -= 40;
-                }
-            }),
-            false
-        );
-    };
     render() {
-        trace();
+        if (this._hide) return null;
         const a = this.anchor1;
         const b = this.anchor2;
         this._forceAnimate;
 
-        if (a && b && !this._hide) {
+        if (a && b && !LightboxView.Contains(this.DocumentView?.())) {
+            // text selection bounds are not directly observable, so we have to
+            // force an update when anything that could affect them changes (text edits causing reflow, scrolling)
+            a.Document[a.LayoutFieldKey];
+            b.Document[b.LayoutFieldKey];
+            a.Document.layout_scrollTop;
+            b.Document.layout_scrollTop;
+
             const axf = a.screenToViewTransform(); // these force re-render when a or b moves (so do NOT remove)
             const bxf = b.screenToViewTransform();
             const scale = a.CollectionFreeFormView === this.DocumentView?.().CollectionFreeFormView ? axf.Scale : bxf.Scale;
             const at = a.getBounds?.transition; // these force re-render when a or b change size and at the end of an animated transition
-            const bt = b.getBounds?.transition;
+            const bt = b.getBounds?.transition; //    inquring getBounds() also causes text anchors to update whether or not they reflow (any size change triggers an invalidation)
+
+            //   if there's an element in the DOM with a classname containing a link anchor's id (eg a hypertext <a>),
+            //   then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right
+            //   otherwise, we just use the computed nearest point on the document boundary to the target Document
+            const targetAhyperlink = Array.from(window.document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_1)[Id])).lastElement();
+            const targetBhyperlink = Array.from(window.document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_2)[Id])).lastElement();
+
+            const aid = targetAhyperlink?.id || a.Document[Id];
+            const bid = targetBhyperlink?.id || b.Document[Id];
+            if (!document.getElementById(aid) || !document.getElementById(bid)) {
+                setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01)));
+                return null;
+            }
+
             if (at || bt) setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); // this forces an update during a transition animation
             const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting);
             const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined;
+            const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
             const fontFamily = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily);
             const fontSize = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize);
-            const color = (c => (c !== 'transparent' ? c : undefined))(StrCast(this.layoutDoc.link_fontColor));
-            const { strokeWidth, stroke_startMarker, stroke_endMarker } = this.Document;
-            const dash = StrCast(this.Document.stroke_dash);
-            const stroke = highlightColor ?? 'lightblue';
+            const fontColor = (c => (c !== 'transparent' ? c : undefined))(StrCast(this.layoutDoc.link_fontColor));
+            const { stroke_markerScale, stroke_width, stroke_startMarker, stroke_endMarker, stroke_dash } = this.Document;
 
+            const strokeWidth = NumCast(stroke_width, 4);
             const linkDesc = StrCast(this.dataDoc.link_description) || ' ';
             const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '');
             return (
-                <Xarrow
-                    divContainerStyle={{ transform: `scale(${scale})` }}
-                    start={a.Guid}
-                    end={b.Guid} //
-                    strokeWidth={NumCast(strokeWidth, 4)}
-                    dashness={dash ? true : false}
-                    showHead={stroke_startMarker ? true : false}
-                    showTail={stroke_endMarker ? true : false}
-                    tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'}
-                    headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'}
-                    color={stroke}
-                    labels={
-                        <div
-                            onPointerDown={this.descriptionDown} //
-                            style={{
-                                borderRadius: '20%', //
-                                padding: '3',
-                                pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined,
-                                background: stroke,
-                                color: color || lightOrDark(stroke),
-                                fontSize,
-                                fontFamily /*, fontStyle: 'italic'*/,
-                            }}>
-                            {labelText}
-                        </div>
-                    }
-                    passProps={{ onPointerDown: this.descriptionDown }}
-                />
+                <>
+                    {!highlightColor ? null : (
+                        <Xarrow
+                            divContainerStyle={{ transform: `scale(${scale})` }}
+                            start={aid}
+                            end={bid} //
+                            strokeWidth={strokeWidth + Math.max(2, strokeWidth * 0.1)}
+                            showHead={stroke_startMarker ? true : false}
+                            showTail={stroke_endMarker ? true : false}
+                            headSize={NumCast(stroke_markerScale, 3)}
+                            tailSize={NumCast(stroke_markerScale, 3)}
+                            tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'}
+                            headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'}
+                            color={highlightColor}
+                        />
+                    )}
+                    <Xarrow
+                        divContainerStyle={{ transform: `scale(${scale})` }}
+                        start={aid}
+                        end={bid} //
+                        strokeWidth={strokeWidth}
+                        dashness={Number(stroke_dash) ? true : false}
+                        showHead={stroke_startMarker ? true : false}
+                        showTail={stroke_endMarker ? true : false}
+                        headSize={NumCast(stroke_markerScale, 3)}
+                        tailSize={NumCast(stroke_markerScale, 3)}
+                        tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'}
+                        headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'}
+                        color={color}
+                        labels={
+                            <div
+                                style={{
+                                    borderRadius: '8px',
+                                    pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined,
+                                    fontSize,
+                                    fontFamily /*, fontStyle: 'italic'*/,
+                                    color: fontColor || lightOrDark(DashColor(color).fade(0.5).toString()),
+                                    paddingLeft: 4,
+                                    paddingRight: 4,
+                                    paddingTop: 3,
+                                    paddingBottom: 3,
+                                    background: DashColor(highlightColor || color)
+                                        .fade(0.5)
+                                        .toString(),
+                                }}>
+                                <EditableView
+                                    key="editableView"
+                                    oneLine
+                                    contents={labelText}
+                                    height={fontSize + 4}
+                                    fontSize={fontSize}
+                                    GetValue={() => linkDesc}
+                                    SetValue={action(val => {
+                                        this.Document[DocData].link_description = val;
+                                        return true;
+                                    })}
+                                />
+
+                                {/* <EditableText
+                                    placeholder={labelText}
+                                    background={color}
+                                    color={fontColor || lightOrDark(DashColor(color).fade(0.5).toString())}
+                                    type={Type.PRIM}
+                                    val={StrCast(this.Document[DocData].link_description)}
+                                    setVal={action(val => (this.Document[DocData].link_description = val))}
+                                    fillWidth
+                                /> */}
+                            </div>
+                        }
+                        passProps={{}}
+                    />
+                </>
             );
         }
-        return null;
         return (
             <div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }}>
                 <ComparisonBox
-                    {...this._props} //
+                    {...this.props} //
                     fieldKey="link_anchor"
                     setHeight={emptyFunction}
                     dontRegisterView={true}
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index a342285b0..a141ef041 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -1,6 +1,7 @@
 import * as React from 'react';
 import { DOMOutputSpec, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from 'prosemirror-model';
 import { Doc } from '../../../../fields/Doc';
+import { Utils } from '../../../../Utils';
 
 const emDOM: DOMOutputSpec = ['em', 0];
 const strongDOM: DOMOutputSpec = ['strong', 0];
@@ -44,7 +45,7 @@ export const marks: { [index: string]: MarkSpec } = {
         toDOM(node: any) {
             const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), '');
             const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), '');
-            return ['a', { class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0];
+            return ['a', { id: Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0];
         },
     },
     noAutoLinkAnchor: {
@@ -104,7 +105,7 @@ export const marks: { [index: string]: MarkSpec } = {
                           node.attrs.title,
                       ],
                   ]
-                : ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, style: `text-decoration: underline; cursor: default` }, 0];
+                : ['a', { id: '' + Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, style: `text-decoration: underline; cursor: default` }, 0];
         },
     },
 
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 3cd4efcf7..56d50846a 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -16,7 +16,7 @@ import { DateField } from './DateField';
 import {
     AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, Animation, AudioPlay, Brushed, CachedUpdates, DirectLinks,
     DocAcl, DocCss, DocData, DocFields, DocLayout, DocViews, FieldKeys, FieldTuples, ForceServerWrite, Height, Highlight,
-    Initializing, Self, SelfProxy, UpdatingFromServer, Width
+    Initializing, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width
 } from './DocSymbols'; // prettier-ignore
 import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
 import { InkField, InkTool } from './InkField';
@@ -309,6 +309,7 @@ export class Doc extends RefField {
     public [DocFields] = () => this[Self][FieldTuples]; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any);
     public [Width] = () => NumCast(this[SelfProxy]._width);
     public [Height] = () => NumCast(this[SelfProxy]._height);
+    public [TransitionTimer]: any = undefined;
     public [ToJavascriptString] = () => `idToDoc("${this[Self][Id]}")`; // what should go here?
     public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`;
     public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? '-inaccessible-' : this[SelfProxy].title})`;
diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts
index 9c563abbf..f8a57acd5 100644
--- a/src/fields/DocSymbols.ts
+++ b/src/fields/DocSymbols.ts
@@ -15,6 +15,7 @@ export const DocLayout = Symbol('DocLayout');
 export const DocFields = Symbol('DocFields');
 export const DocCss = Symbol('DocCss');
 export const DocAcl = Symbol('DocAcl');
+export const TransitionTimer = Symbol('DocTransitionTimer');
 export const DirectLinks = Symbol('DocDirectLinks');
 export const AclPrivate = Symbol('DocAclOwnerOnly');
 export const AclReadonly = Symbol('DocAclReadOnly');
-- 
cgit v1.2.3-70-g09d2

