From 29a1fe16297c99ddbed7974b7c602294da9a311d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 25 Mar 2025 22:40:30 -0400
Subject: fixes to components so that things highlight reasonably in different
 skins.  fixed color picker alternate selection.

---
 src/client/views/nodes/FontIconBox/FontIconBox.tsx | 36 ++++++++++++----------
 1 file changed, 20 insertions(+), 16 deletions(-)

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

diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index f699568f1..966d8d7a0 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { ClientUtils, DashColor, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
+import { ClientUtils, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
 import { InkTool } from '../../../../fields/InkField';
 import { ScriptField } from '../../../../fields/ScriptField';
@@ -134,7 +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}
+                size={Size.XXSMALL}
                 setNumber={undoable(value => numScript(value), `${this.Document.title} button set from list`)}
                 fillWidth
             />
@@ -252,7 +252,9 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
      * Color button
      */
     @computed get colorButton() {
-        const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
+        const color = SnappingManager.userColor;
+        const pickedColor = 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 curColor = (this.colorScript?.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result as string) ?? 'transparent';
         const tooltip: string = StrCast(this.Document.toolTip);
 
@@ -270,10 +272,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                     }}
                     defaultPickerType="Classic"
                     selectedColor={curColor}
-                    type={Type.PRIM}
+                    type={Type.TERT}
                     color={color}
-                    background={SnappingManager.userBackgroundColor}
-                    icon={this.Icon(color) ?? undefined}
+                    background={background}
+                    icon={this.Icon(pickedColor) ?? undefined}
                     tooltip={tooltip}
                     label={this.label}
                 />
@@ -287,15 +289,16 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         const toggleStatus = script?.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean;
 
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
+        const background = this._props.styleProvider?.(this.Document, 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={`Click to Toggle ${tooltip} or select new option`}
-                type={Type.PRIM}
+                type={Type.TERT}
                 color={color}
-                background={undefined}
+                background={background}
                 multiSelect={true}
                 onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))}
                 isToggle={false}
@@ -339,11 +342,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
             <Toggle
                 tooltip={`Toggle ${tooltip}`}
                 toggleType={ToggleType.BUTTON}
-                type={inkShapeHack ? Type.TERT : Type.PRIM}
+                type={Type.TERT}
                 toggleStatus={toggleStatus}
                 text={buttonText}
                 color={color}
-                background={inkShapeHack ? DashColor(SnappingManager.userBackgroundColor).darken(0.05).toString() : undefined}
+                triState={inkShapeHack}
+                background={color}
                 icon={this.Icon(color)!}
                 label={this.label}
                 onPointerDown={e =>
@@ -404,12 +408,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
             case ButtonType.ColorButton:        return this.colorButton; 
             case ButtonType.MultiToggleButton:  return this.multiToggleButton;
             case ButtonType.ToggleButton:       return this.toggleButton;
-            case ButtonType.ClickButton:return <IconButton {...btnProps} size={Size.MEDIUM} color={color} />;
-            case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} />;
-            case ButtonType.TextButton: return <Button     {...btnProps} color={color} 
-                        background={SnappingManager.userBackgroundColor} text={StrCast(this.dataDoc.buttonText)}/>;
-            case ButtonType.MenuButton: return <IconButton {...btnProps} color={color} 
-                        background={SnappingManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; 
+            case ButtonType.ClickButton:        return <IconButton {...btnProps} size={Size.MEDIUM} color={color} background={color} />;
+            case ButtonType.ToolButton:         return <IconButton {...btnProps} size={Size.LARGE}  color={color} background={color} />;
+            case ButtonType.TextButton:         return <Button     {...btnProps}                    color={color} background={color} 
+                                                                   text={StrCast(this.dataDoc.buttonText)}/>;
+            case ButtonType.MenuButton:         return <IconButton size={Size.LARGE}  {...btnProps} color={color} background={color} 
+                                                                   tooltipPlacement='right' onPointerDown={scriptFunc} />; 
             default: 
         }
         return this.defaultButton;
-- 
cgit v1.2.3-70-g09d2


From a3d6fae1482c61ca725d0a103f13b621aa32b3e3 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 5 Apr 2025 14:29:03 -0400
Subject:  fixed multitoggle behavior to only toggle submenu closed.  switched
 to hard-light for masking ink.  fixed to make menu toggle take effect
 immediately.

---
 packages/components/src/components/Group/Group.tsx |  68 +++++------
 .../components/MultiToggle/MultiToggle.stories.tsx | 125 ++++++++++-----------
 .../src/components/MultiToggle/MultiToggle.tsx     |  36 +++---
 packages/components/src/components/Popup/Popup.tsx |  22 ++--
 src/client/documents/Documents.ts                  |  37 +++---
 src/client/views/collections/CollectionSubView.tsx |   7 +-
 .../views/collections/FlashcardPracticeUI.tsx      |   5 +-
 .../collectionFreeForm/CollectionFreeFormView.scss |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   5 +-
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |   1 -
 10 files changed, 139 insertions(+), 169 deletions(-)

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

diff --git a/packages/components/src/components/Group/Group.tsx b/packages/components/src/components/Group/Group.tsx
index 7abe4a1c7..6275a09dd 100644
--- a/packages/components/src/components/Group/Group.tsx
+++ b/packages/components/src/components/Group/Group.tsx
@@ -1,49 +1,33 @@
-import React from 'react'
-import './Group.scss'
-import { Colors, IGlobalProps, getFontSize, isDark , getFormLabelSize } from '../../global';
+import React from 'react';
+import './Group.scss';
+import { Colors, IGlobalProps, getFontSize, isDark, getFormLabelSize } from '../../global';
 
 export interface IGroupProps extends IGlobalProps {
-  children: any
-  rowGap?: number;
-  columnGap?: number;
-  padding?: number | string;
+    children: any;
+    rowGap?: number;
+    columnGap?: number;
+    padding?: number | string;
 }
 
 export const Group = (props: IGroupProps) => {
-  const { 
-    children, 
-    width = '100%',
-    rowGap = 5,
-    columnGap = 5,
-    padding = 0,
-    formLabel,
-    formLabelPlacement,
-    size,
-    style,
-    color,
-    fillWidth
-  } = props
+    const { children, width = '100%', rowGap = 5, columnGap = 5, padding = 0, formLabel, formLabelPlacement, size, style, fillWidth } = props;
 
-  const group: JSX.Element = 
-  (
-    <div
-      className="group-wrapper"
-      style={{ width, padding: padding, ...style }}
-    >
-      <div className={`group-container`}
-        style={{ rowGap, columnGap }}
-      >{children}</div>
-    </div>
-  )
+    const group: JSX.Element = (
+        <div className="group-wrapper" style={{ width, padding: padding, ...style }}>
+            <div className={`group-container`} style={{ rowGap, columnGap }}>
+                {children}
+            </div>
+        </div>
+    );
 
-  return (
-    formLabel ? 
-      <div className={`form-wrapper ${formLabelPlacement}`}
-          style={{ width: fillWidth ? '100%' : undefined}}>
-        <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div>
-        {group}
-      </div>
-    :
-      group
-  )
-}
+    return formLabel ? (
+        <div className={`form-wrapper ${formLabelPlacement}`} style={{ width: fillWidth ? '100%' : undefined }}>
+            <div className={'formLabel'} style={{ fontSize: getFormLabelSize(size) }}>
+                {formLabel}
+            </div>
+            {group}
+        </div>
+    ) : (
+        group
+    );
+};
diff --git a/packages/components/src/components/MultiToggle/MultiToggle.stories.tsx b/packages/components/src/components/MultiToggle/MultiToggle.stories.tsx
index e71423d7a..b9ba45f72 100644
--- a/packages/components/src/components/MultiToggle/MultiToggle.stories.tsx
+++ b/packages/components/src/components/MultiToggle/MultiToggle.stories.tsx
@@ -1,69 +1,68 @@
-import { Meta, Story } from '@storybook/react'
-import React from 'react'
-import { IMultiToggleProps, MultiToggle } from './MultiToggle'
-import { FaAlignLeft, FaAlignCenter, FaAlignJustify, FaAlignRight } from 'react-icons/fa'
+import { Meta, Story } from '@storybook/react';
+import React from 'react';
+import { IMultiToggleProps, MultiToggle } from './MultiToggle';
+import { FaAlignLeft, FaAlignCenter, FaAlignJustify, FaAlignRight } from 'react-icons/fa';
 
 export default {
-  title: 'Dash/MultiToggle',
-  component: MultiToggle,
-  argTypes: {},
-} as Meta<typeof MultiToggle>
+    title: 'Dash/MultiToggle',
+    component: MultiToggle,
+    argTypes: {},
+} as Meta<typeof MultiToggle>;
 
-const MultiToggleStory: Story<IMultiToggleProps> = (args) => <MultiToggle {...args} />
-export const MultiToggleOne = MultiToggleStory.bind({})
+const MultiToggleStory: Story<IMultiToggleProps> = args => <MultiToggle {...args} />;
+export const MultiToggleOne = MultiToggleStory.bind({});
 MultiToggleOne.args = {
-  tooltip: "Text alignment",
-  label: "Alignment",
-  defaultSelectedItems: "center",
-  toggleStatus: true,
-  isToggle: false,
-  items: [
-    {
-      icon: <FaAlignLeft/>,
-      tooltip: 'Align left',
-      val: "left"
-    },
-    {
-      icon: <FaAlignCenter/>,
-      tooltip: 'Align center',
-      val: "center"
-    },
-    {
-      icon: <FaAlignRight/>,
-      tooltip: 'Align right',
-      val: "right"
-    },
-    {
-      icon: <FaAlignJustify/>,
-      tooltip: 'Justify',
-      val: "justify"
-    },
-  ]
-}
+    tooltip: 'Text alignment',
+    label: 'Alignment',
+    defaultSelectedItems: 'center',
+    toggleStatus: true,
+    items: [
+        {
+            icon: <FaAlignLeft />,
+            tooltip: 'Align left',
+            val: 'left',
+        },
+        {
+            icon: <FaAlignCenter />,
+            tooltip: 'Align center',
+            val: 'center',
+        },
+        {
+            icon: <FaAlignRight />,
+            tooltip: 'Align right',
+            val: 'right',
+        },
+        {
+            icon: <FaAlignJustify />,
+            tooltip: 'Justify',
+            val: 'justify',
+        },
+    ],
+};
 
-export const MultiToggleTwo = MultiToggleStory.bind({})
+export const MultiToggleTwo = MultiToggleStory.bind({});
 MultiToggleTwo.args = {
-  tooltip: "Text Tags",
-  label: "Tags",
-  defaultSelectedItems : ["left"],
-  background: "green",
-  color: 'white',
-  multiSelect: true,
-  items: [
-    {
-      icon: <FaAlignLeft/>,
-      tooltip: 'Like',
-      val: "left"
-    },
-    {
-      icon: <FaAlignCenter/>,
-      tooltip: 'Todo',
-      val: "center"
-    },
-    {
-      icon: <FaAlignRight/>,
-      tooltip: 'Idea',
-      val: "right"
-    },
-  ]
-}
+    tooltip: 'Text Tags',
+    label: 'Tags',
+    defaultSelectedItems: ['left'],
+    background: 'green',
+    color: 'white',
+    multiSelect: true,
+    items: [
+        {
+            icon: <FaAlignLeft />,
+            tooltip: 'Like',
+            val: 'left',
+        },
+        {
+            icon: <FaAlignCenter />,
+            tooltip: 'Todo',
+            val: 'center',
+        },
+        {
+            icon: <FaAlignRight />,
+            tooltip: 'Idea',
+            val: 'right',
+        },
+    ],
+};
diff --git a/packages/components/src/components/MultiToggle/MultiToggle.tsx b/packages/components/src/components/MultiToggle/MultiToggle.tsx
index 0f659c5ca..0a6fb81c9 100644
--- a/packages/components/src/components/MultiToggle/MultiToggle.tsx
+++ b/packages/components/src/components/MultiToggle/MultiToggle.tsx
@@ -16,7 +16,6 @@ export interface IMultiToggleProps extends IGlobalProps {
     defaultSelectedItems?: (string | number) | (string | number)[];
     selectedItems?: (string | number) | (string | number)[];
     onSelectionChange?: (val: (string | number) | (string | number)[], added: boolean) => unknown;
-    isToggle?: boolean;
     toggleStatus?: boolean;
 }
 
@@ -37,33 +36,30 @@ export const MultiToggle = (props: IMultiToggleProps) => {
     const itemsMap = new Map();
     items.forEach(item => itemsMap.set(item.val, item));
     return (
-        <div className={`multiToggle-container`}>
+        <div className="multiToggle-container">
             <Popup
                 toggle={
-                    props.isToggle ? undefined : (
-                        <div style={{ position: 'relative' }}>
-                            <IconButton
-                                color={color}
-                                borderColor={background ? color : undefined}
-                                label={props.label}
-                                active={props.toggleStatus}
-                                background={color}
-                                {...(itemsMap.get(promoteToArray(selectedItems)[0]) ?? {})}
-                                tooltip={tooltip}
-                                tooltipPlacement={tooltipPlacement}
-                            />
-                            {promoteToArray(selectedItems).length < 2 ? null : <div style={{ position: 'absolute', top: '0', left: '0', color: color ?? Colors.MEDIUM_BLUE }}>+</div>}
-                        </div>
-                    )
+                    <div style={{ position: 'relative' }}>
+                        <IconButton
+                            color={color}
+                            borderColor={background ? color : undefined}
+                            label={props.label}
+                            active={props.toggleStatus}
+                            background={color}
+                            {...(itemsMap.get(promoteToArray(selectedItems)[0]) ?? {})}
+                            tooltip={tooltip}
+                            tooltipPlacement={tooltipPlacement}
+                        />
+                        {promoteToArray(selectedItems).length < 2 ? null : <div style={{ position: 'absolute', top: '0', left: '0', color: color ?? Colors.MEDIUM_BLUE }}>+</div>}
+                    </div>
                 }
-                isToggle={props.isToggle}
+                isToggle={true}
                 toggleFunc={() => {
                     const selItem = items.find(item => promoteToArray(selectedItems).includes(item.val));
                     selItem && setSelectedItemsLocal([selItem.val]);
                 }}
                 type={props.type}
-                label={props.isToggle ? props.label : undefined}
-                toggleStatus={props.isToggle ? props.toggleStatus : undefined}
+                label={undefined}
                 color={color}
                 background={background}
                 popup={
diff --git a/packages/components/src/components/Popup/Popup.tsx b/packages/components/src/components/Popup/Popup.tsx
index 82e60f343..9e72ece87 100644
--- a/packages/components/src/components/Popup/Popup.tsx
+++ b/packages/components/src/components/Popup/Popup.tsx
@@ -20,11 +20,10 @@ export interface IPopupProps extends IGlobalProps {
     toggle?: JSX.Element;
     popup: JSX.Element | string | (() => JSX.Element);
     trigger?: PopupTrigger;
-    toggleStatus?: boolean;
     isOpen?: boolean;
     setOpen?: (b: boolean) => void;
     background?: string;
-    isToggle?: boolean;
+    isToggle?: boolean; // whether popup stays open when background is clicked.  muyst click toggle button tp close it.
     toggleFunc?: () => void;
     popupContainsPt?: (x: number, y: number) => boolean;
 }
@@ -71,6 +70,7 @@ export const Popup = (props: IPopupProps) => {
         const rect = popperRef.current?.getBoundingClientRect();
         const rect2 = toggleRef.current?.getBoundingClientRect();
         if (
+            !props.isToggle &&
             (!rect2 || !(rect2.left < e.clientX && rect2.top < e.clientY && rect2.right > e.clientX && rect2.bottom > e.clientY)) &&
             rect &&
             !(rect.left < e.clientX && rect.top < e.clientY && rect.right > e.clientX && rect.bottom > e.clientY) &&
@@ -109,8 +109,8 @@ export const Popup = (props: IPopupProps) => {
                         timeout = setTimeout(() => setOpen(false), 1000);
                     }
                 }}>
-                {toggle ?? (
-                    <div ref={toggleRef}>
+                <div ref={toggleRef}>
+                    {toggle ?? (
                         <Toggle
                             tooltip={tooltip}
                             size={size}
@@ -122,19 +122,17 @@ export const Popup = (props: IPopupProps) => {
                             iconPlacement={iconPlacement}
                             text={text}
                             label={props.label}
-                            toggleStatus={isOpen || props.toggleStatus}
+                            toggleStatus={isOpen}
                             onClick={() => {
                                 if (trigger === PopupTrigger.CLICK) {
-                                    if (!props.isToggle || props.toggleStatus) {
-                                        setOpen(!isOpen);
-                                    }
+                                    setOpen(!isOpen);
                                     props.toggleFunc?.();
                                 }
                             }}
                             fillWidth={fillWidth}
                         />
-                    </div>
-                )}
+                    )}
+                </div>
             </div>
             <Popper open={isOpen} style={{ zIndex: 20000 }} anchorEl={triggerRef.current} placement={placement} modifiers={[]}>
                 <div
@@ -142,9 +140,7 @@ export const Popup = (props: IPopupProps) => {
                     ref={popperRef}
                     style={{ width, height, background }}
                     tabIndex={-1}
-                    onPointerDown={e => {
-                        e.stopPropagation();
-                    }}
+                    onPointerDown={e => e.stopPropagation()}
                     onPointerEnter={() => {
                         if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) {
                             clearTimeout(timeout);
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 8458699fa..f1655e3cf 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -4,7 +4,6 @@ import { basename } from 'path';
 import { ClientUtils, OmitKeys } from '../../ClientUtils';
 import { DateField } from '../../fields/DateField';
 import { CreateLinkToActiveAudio, Doc, FieldType, Opt, updateCachedAcls } from '../../fields/Doc';
-import { Initializing } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { HtmlField } from '../../fields/HtmlField';
 import { InkField } from '../../fields/InkField';
@@ -983,26 +982,22 @@ export namespace Docs {
 
         export function InkDocument(points: PointData[], options: DocumentOptions = {}, strokeWidth: number, color: string, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, isInkMask: boolean) {
             const ink = InstanceFromProto(Prototypes.get(DocumentType.INK), '', { title: 'ink', ...options });
-            const I = Doc.GetProto(ink);
-            // I.layout_hideOpenButton = true; // don't show open full screen button when selected
-            I.color = color;
-            I.fillColor = fillColor && fillColor !== 'transparent' ? fillColor : undefined;
-            I.stroke = new InkField(points);
-            I.stroke_width = strokeWidth;
-            I.stroke_bezier = strokeBezier;
-            I.stroke_startMarker = arrowStart;
-            I.stroke_endMarker = arrowEnd;
-            I.stroke_dash = dash;
-            I.stroke_isInkMask = isInkMask;
-            I.text_align = 'center';
-            I.rotation = 0;
-            I.width_min = 1;
-            I.height_min = 1;
-            I.defaultDoubleClick = 'ignore';
-            I.author_date = new DateField();
-            I.acl_Guest = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View;
-            // I.acl_Override = SharingPermissions.Unset;
-            I[Initializing] = false;
+            ink.$color = color;
+            ink.$fillColor = fillColor && fillColor !== 'transparent' ? fillColor : undefined;
+            ink.$stroke = new InkField(points);
+            ink.$stroke_width = strokeWidth;
+            ink.$stroke_bezier = strokeBezier;
+            ink.$stroke_startMarker = arrowStart;
+            ink.$stroke_endMarker = arrowEnd;
+            ink.$stroke_dash = dash;
+            ink.$stroke_isInkMask = isInkMask;
+            ink.$text_align = 'center';
+            ink.$rotation = 0;
+            ink.$width_min = 1;
+            ink.$height_min = 1;
+            ink.$defaultDoubleClick = 'ignore';
+            ink.$author_date = new DateField();
+            ink.$acl_Guest = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View;
 
             return ink;
         }
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index cfd52f9ee..a88707c6f 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -13,6 +13,7 @@ import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from '
 import { WebField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
+import { Upload } from '../../../server/SharedMediaTypes';
 import { DocServer } from '../../DocServer';
 import { Networking } from '../../Network';
 import { DocUtils } from '../../documents/DocUtils';
@@ -24,11 +25,11 @@ import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
+import { DocumentViewProps } from '../nodes/DocumentContentsView';
+import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
-import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
-import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
-import { Upload } from '../../../server/SharedMediaTypes';
+import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 
 export enum docSortings {
     Time = 'time',
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 3bcdd843e..8cd9c5452 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -12,10 +12,11 @@ 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 { DocumentView } from '../nodes/DocumentView';
 import './FlashcardPracticeUI.scss';
 import { StyleProp } from '../StyleProp';
 import { FieldViewProps } from '../nodes/FieldView';
+import { DocumentViewProps } from '../nodes/DocumentContentsView';
 
 export enum practiceMode {
     PRACTICE = 'practice',
@@ -142,7 +143,6 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
                     color={SnappingManager.userColor}
                     background={SnappingManager.userVariantColor}
                     multiSelect={false}
-                    isToggle={false}
                     toggleStatus={!!this.practiceMode}
                     label="Practice"
                     items={[
@@ -162,7 +162,6 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
                     color={SnappingManager.userColor}
                     background={SnappingManager.userVariantColor}
                     multiSelect={false}
-                    isToggle={false}
                     toggleStatus={!!this.practiceMode}
                     label={StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)}
                     items={[
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index cce0ff684..6c47a71b0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -41,7 +41,7 @@
     transition: background-color 1s ease 0s;
 }
 .collectionfreeformview-mask {
-    mix-blend-mode: multiply;
+    mix-blend-mode: hard-light;
     background-color: rgba(0, 0, 0, 0.8);
 }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f01fb8fc7..1f789432d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1601,7 +1601,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const childDoc = pair.layout;
         const layoutFrameNumber = Cast(this.Document._currentFrame, 'number'); // frame number that container is at which determines layout frame values
         const contentFrameNumber = Cast(childDoc._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
-        const { z, zIndex } = childDoc;
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
+        const { z, zIndex, stroke_isInkMask } = childDoc;
         const { backgroundColor, color } = contentFrameNumber === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, contentFrameNumber);
         const { x, y, autoDim, _width, _height, opacity, _rotation } =
             layoutFrameNumber === undefined // -1 for width/height means width/height should be PanelWidth/PanelHeight (prevents collectionfreeformdocumentview width/height from getting out of synch with panelWIdth/Height which causes detailView to re-render and lose focus because HTMLtag scaling gets set to a bad intermediate value)
@@ -1717,7 +1718,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 elements.push({
                     ele: this.getChildDocView(entry[1]),
                     bounds: entry[1].opacity === 0 ? { payload: undefined, type: '', ...entry[1], width: 0, height: 0 } : { payload: undefined, type: '', ...entry[1] },
-                    inkMask: BoolCast(entry[1].pair.layout.stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1,
+                    inkMask: BoolCast(entry[1].pair.layout?.$stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1,
                 })
             );
 
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 966d8d7a0..f83b5e351 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -301,7 +301,6 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                 background={background}
                 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={selectedItems.length === 1 ? selectedItems[0] : this.label}
                 items={items.map(item => ({
-- 
cgit v1.2.3-70-g09d2


From 40b8e5c30e7251db6258cac5b4ed5fb4a6a2418d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 7 Apr 2025 15:20:36 -0400
Subject: added multiToggle option to allow popup to stay up until explicitly
 untoggled. fixed multitoggle to honor toggleStatus.  fixed popup to follow
 target.   fixed setting text highlight background fro popup menu. fixed
 flashcardui buttons to have background sized properly.

---
 package-lock.json                                  | 24 ++++++++++++
 package.json                                       |  1 +
 .../src/components/MultiToggle/MultiToggle.tsx     | 16 +++++++-
 packages/components/src/components/Popup/Popup.tsx | 43 +++++++++++++++-------
 src/client/util/CurrentUserUtils.ts                |  5 ++-
 .../views/collections/CollectionCardDeckView.tsx   |  3 +-
 .../views/collections/FlashcardPracticeUI.tsx      |  7 ++--
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |  1 +
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 .../views/nodes/formattedText/RichTextMenu.tsx     |  3 +-
 10 files changed, 81 insertions(+), 24 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index 3b11eda7c..9acca90ee 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,6 +40,7 @@
                 "@pinecone-database/pinecone": "^2.2.2",
                 "@react-google-maps/api": "^2.19.2",
                 "@react-spring/web": "^9.7.5",
+                "@thednp/position-observer": "^1.0.7",
                 "@turf/turf": "^7.1.0",
                 "@types/bezier-js": "^4.1.3",
                 "@types/brotli": "^1.3.4",
@@ -12081,6 +12082,29 @@
                 "@testing-library/dom": ">=7.21.4"
             }
         },
+        "node_modules/@thednp/position-observer": {
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/@thednp/position-observer/-/position-observer-1.0.7.tgz",
+            "integrity": "sha512-MkUAMMgqZPxy71hpcrKr9ZtedMk+oIFbFs5B8uKD857iuYKRJxgJtC1Itus14EEM4qMyeN0x47AUZJmZJQyXbQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@thednp/shorty": "^2.0.10"
+            },
+            "engines": {
+                "node": ">=16",
+                "pnpm": ">=8.6.0"
+            }
+        },
+        "node_modules/@thednp/shorty": {
+            "version": "2.0.10",
+            "resolved": "https://registry.npmjs.org/@thednp/shorty/-/shorty-2.0.10.tgz",
+            "integrity": "sha512-H+hs1lw3Yc1NfwG0b7F7YmVjxQZ31NO2+6zx+I+9XabHxdwPKjvYJnkKKXr7bSItgm2AFrfOn5+3veB6W4iauw==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=16",
+                "pnpm": ">=8.6.0"
+            }
+        },
         "node_modules/@tokenizer/token": {
             "version": "0.3.0",
             "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
diff --git a/package.json b/package.json
index fe76189bf..1038c470e 100644
--- a/package.json
+++ b/package.json
@@ -123,6 +123,7 @@
         "@pinecone-database/pinecone": "^2.2.2",
         "@react-google-maps/api": "^2.19.2",
         "@react-spring/web": "^9.7.5",
+        "@thednp/position-observer": "^1.0.7",
         "@turf/turf": "^7.1.0",
         "@types/bezier-js": "^4.1.3",
         "@types/brotli": "^1.3.4",
diff --git a/packages/components/src/components/MultiToggle/MultiToggle.tsx b/packages/components/src/components/MultiToggle/MultiToggle.tsx
index 0a6fb81c9..c1d610c34 100644
--- a/packages/components/src/components/MultiToggle/MultiToggle.tsx
+++ b/packages/components/src/components/MultiToggle/MultiToggle.tsx
@@ -17,6 +17,7 @@ export interface IMultiToggleProps extends IGlobalProps {
     selectedItems?: (string | number) | (string | number)[];
     onSelectionChange?: (val: (string | number) | (string | number)[], added: boolean) => unknown;
     toggleStatus?: boolean;
+    showUntilToggle?: boolean; // whether popup stays open when background is clicked.  muyst click toggle button tp close it.
 }
 
 function promoteToArrayOrUndefined(d: (string | number)[] | (string | number) | undefined) {
@@ -32,12 +33,23 @@ export const MultiToggle = (props: IMultiToggleProps) => {
     init = false;
 
     const [selectedItemsLocal, setSelectedItemsLocal] = useState(initVal as (string | number) | (string | number)[]);
-    const { items, selectedItems = selectedItemsLocal, tooltip, tooltipPlacement = 'top', onSelectionChange, color, background } = props;
+    const {
+        items, //
+        selectedItems = selectedItemsLocal,
+        tooltip,
+        toggleStatus,
+        tooltipPlacement = 'top',
+        onSelectionChange,
+        color,
+        background,
+    } = props;
     const itemsMap = new Map();
     items.forEach(item => itemsMap.set(item.val, item));
     return (
         <div className="multiToggle-container">
             <Popup
+                isOpen={toggleStatus}
+                multitoggle={true} // this is used to indicate that this is a multi toggle, so it can be styled differently in the popup
                 toggle={
                     <div style={{ position: 'relative' }}>
                         <IconButton
@@ -53,7 +65,7 @@ export const MultiToggle = (props: IMultiToggleProps) => {
                         {promoteToArray(selectedItems).length < 2 ? null : <div style={{ position: 'absolute', top: '0', left: '0', color: color ?? Colors.MEDIUM_BLUE }}>+</div>}
                     </div>
                 }
-                isToggle={true}
+                showUntilToggle={props.showUntilToggle ?? true}
                 toggleFunc={() => {
                     const selItem = items.find(item => promoteToArray(selectedItems).includes(item.val));
                     selItem && setSelectedItemsLocal([selItem.val]);
diff --git a/packages/components/src/components/Popup/Popup.tsx b/packages/components/src/components/Popup/Popup.tsx
index 9e72ece87..9e91a75cf 100644
--- a/packages/components/src/components/Popup/Popup.tsx
+++ b/packages/components/src/components/Popup/Popup.tsx
@@ -3,6 +3,7 @@ import { IGlobalProps, Placement, Size } from '../../global';
 import { Toggle, ToggleType } from '../Toggle';
 import './Popup.scss';
 import { Popper } from '@mui/material';
+import PositionObserver from '@thednp/position-observer';
 
 export enum PopupTrigger {
     CLICK = 'click',
@@ -23,9 +24,10 @@ export interface IPopupProps extends IGlobalProps {
     isOpen?: boolean;
     setOpen?: (b: boolean) => void;
     background?: string;
-    isToggle?: boolean; // whether popup stays open when background is clicked.  muyst click toggle button tp close it.
+    showUntilToggle?: boolean; // whether popup stays open when background is clicked.  muyst click toggle button tp close it.
     toggleFunc?: () => void;
     popupContainsPt?: (x: number, y: number) => boolean;
+    multitoggle?: boolean;
 }
 
 /**
@@ -57,20 +59,21 @@ export const Popup = (props: IPopupProps) => {
         fillWidth,
         iconPlacement = 'left',
         background,
+        multitoggle,
         popupContainsPt,
     } = props;
 
     const triggerRef = useRef(null);
     const popperRef = useRef<HTMLDivElement | null>(null);
-    const toggleRef = useRef<HTMLDivElement | null>(null);
+    const [toggleRef, setToggleRef] = useState<HTMLDivElement | null>(null);
 
     let timeout = setTimeout(() => {});
 
     const handlePointerAwayDown = (e: PointerEvent) => {
         const rect = popperRef.current?.getBoundingClientRect();
-        const rect2 = toggleRef.current?.getBoundingClientRect();
+        const rect2 = toggleRef?.getBoundingClientRect();
         if (
-            !props.isToggle &&
+            !props.showUntilToggle &&
             (!rect2 || !(rect2.left < e.clientX && rect2.top < e.clientY && rect2.right > e.clientX && rect2.bottom > e.clientY)) &&
             rect &&
             !(rect.left < e.clientX && rect.top < e.clientY && rect.right > e.clientX && rect.bottom > e.clientY) &&
@@ -82,22 +85,36 @@ export const Popup = (props: IPopupProps) => {
         }
     };
 
+    let observer: PositionObserver | undefined = undefined;
+    const [previousPosition, setPreviousPosition] = useState<DOMRect | undefined>(toggleRef?.getBoundingClientRect());
+
     useEffect(() => {
         if (isOpen) {
             window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true });
             window.addEventListener('pointerdown', handlePointerAwayDown, { capture: true });
-            return () => window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true });
-        }
-    }, [isOpen, popupContainsPt]);
-
+            if (toggleRef && multitoggle) {
+                (observer = new PositionObserver(entries => {
+                    entries.forEach(entry => {
+                        const currentPosition = entry.boundingClientRect;
+                        if (Math.floor(currentPosition.top) !== Math.floor(previousPosition?.top ?? 0) || Math.floor(currentPosition.left) !== Math.floor(previousPosition?.left ?? 0)) {
+                            // Perform actions when position changes
+                            setPreviousPosition(currentPosition); // Update previous position
+                        }
+                    });
+                })).observe(toggleRef);
+            }
+            return () => {
+                window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true });
+                observer?.disconnect();
+            };
+        } else observer?.disconnect();
+    }, [isOpen, toggleRef, popupContainsPt]);
     return (
         <div className={`popup-wrapper ${fillWidth && 'fillWidth'}`}>
             <div
-                className={`trigger-container ${fillWidth && 'fillWidth'}`}
                 ref={triggerRef}
-                onClick={() => {
-                    if (trigger === PopupTrigger.CLICK) setOpen(!isOpen);
-                }}
+                className={`trigger-container ${fillWidth && 'fillWidth'}`}
+                onClick={() => trigger === PopupTrigger.CLICK && setOpen(!isOpen)}
                 onPointerEnter={() => {
                     if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) {
                         clearTimeout(timeout);
@@ -109,7 +126,7 @@ export const Popup = (props: IPopupProps) => {
                         timeout = setTimeout(() => setOpen(false), 1000);
                     }
                 }}>
-                <div ref={toggleRef}>
+                <div className="special" ref={R => setToggleRef(R)}>
                     {toggle ?? (
                         <Toggle
                             tooltip={tooltip}
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f1cc81c88..ca94791d0 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -57,6 +57,7 @@ export interface Button {
     expertMode?: boolean;// available only in expert mode
     btnList?: List<string>;
     ignoreClick?: boolean;
+    showUntilToggle?: boolean; // whether the popup should stay open when the background is clicked.
     buttonText?: string;
     backgroundColor?: string;
     waitForDoubleClickToClick?: boolean;
@@ -778,7 +779,7 @@ pie title Minerals in my tap water
             { 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_);}' }, 
+            { title: "Ink",    toolTip: "Ink",                              btnType: ButtonType.MultiToggleButton,                 toolType: InkTool.Ink,        showUntilToggle: true,  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_);}' }},
@@ -786,7 +787,7 @@ pie title Minerals in my tap water
                 ]},
             { title: "Width",      toolTip: "Stroke width",                 btnType: ButtonType.NumberSliderButton,                toolType: InkProperty.StrokeWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}, numBtnMin: 1, linearBtnWidth:40},
             { title: "Color",      toolTip: "Stroke color",                 btnType: ButtonType.ColorButton,  icon: "pen",         toolType: InkProperty.StrokeColor,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}},
-            { title: "Eraser",     toolTip: "Eraser (Ctrl+E)",              btnType: ButtonType.MultiToggleButton,                 toolType: InkTool.Eraser,                            scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' },
+            { title: "Eraser",     toolTip: "Eraser (Ctrl+E)",              btnType: ButtonType.MultiToggleButton,                 toolType: InkTool.Eraser,     showUntilToggle: true,  scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' },
               subMenu: [
                     { 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 between intersections",btnType:ButtonType.ToggleButton,icon:"xmark",      toolType:InkEraserTool.Segment,   ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 756b37f99..50de7c601 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -23,7 +23,8 @@ import { undoable, UndoManager } from '../../util/UndoManager';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
 import { TagItem } from '../TagsView';
-import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { DocumentViewProps } from '../nodes/DocumentContentsView';
+import { DocumentView } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import './CollectionCardDeckView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 8cd9c5452..f24a8acb7 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -63,8 +63,8 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
     @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
     @computed get practiceMode() {  return this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
 
-    btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale);
-    btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
+    btnHeight = () => NumCast(this.filterDoc?.height);
+    btnWidth = () => (!this.filterDoc ? 1 : NumCast(this.filterDoc._width));
 
     /**
      * Sets the practice mode answer style for flashcards
@@ -135,7 +135,6 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
                 className="FlashcardPracticeUI-practiceModes"
                 style={{
                     background: SnappingManager.userVariantColor,
-                    transform: this._props.ScreenToLocalBoxXf().Scale >= 1 ? undefined : `translateY(${this.btnHeight() / this._props.ScreenToLocalBoxXf().Scale - this.btnHeight()}px)`,
                 }}>
                 <MultiToggle
                     tooltip="Practice flashcards one at a time"
@@ -194,7 +193,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
                 {this.emptyMessage}
                 {this.practiceButtons}
                 {this._props.layoutDoc._chromeHidden ? null : (
-                    <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaling})` }}>
+                    <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnWidth(), transform: `scale(${this._props.uiBtnScaling})` }}>
                         {!this.filterDoc ? null : (
                             <div style={{ background: SnappingManager.userVariantColor }}>
                                 <DocumentView
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index f83b5e351..d14def9aa 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -302,6 +302,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                 multiSelect={true}
                 onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))}
                 toggleStatus={toggleStatus}
+                showUntilToggle={BoolCast(this.Document.showUntilToggle)} // allow the toggle to be clickable unless ignoreClick is set on the Document
                 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} />,
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d90be007f..341340363 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1761,7 +1761,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     tryKeepingFocus = (target: Element | null) => {
         for (let newFocusEle = target instanceof HTMLElement ? target : null; newFocusEle; newFocusEle = newFocusEle?.parentElement) {
             // test if parent of new focused element is a UI button (should be more specific than testing className)
-            if (newFocusEle?.className === 'fonticonbox' || newFocusEle?.className === 'popup-container') {
+            if (['fonticonbox', 'antimodeMenu-cont', 'popup-container'].includes(newFocusEle?.className ?? '')) {
                 return this.EditorView?.focus(); // keep focus on text box
             }
         }
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 758b4035e..10c95f1e1 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -367,7 +367,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
         if (this.TextView && this.view && fontField !== 'fitBox') {
-            if (this.view.hasFocus()) {
+            const anchorNode = window.getSelection()?.anchorNode;
+            if (this.view.hasFocus() || (anchorNode && this.TextView.ProseRef?.contains(anchorNode))) {
                 const attrs: { [key: string]: string } = {};
                 attrs[fontField] = value;
                 const fmark = this.view.state.schema.marks['pF' + fontField.substring(1)].create(attrs);
-- 
cgit v1.2.3-70-g09d2


From d60734c4cdd8fe64d50e3de32182cc6b04afc747 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 8 Apr 2025 14:26:56 -0400
Subject: fixed gptImageLabel calls and added ImageCastToNameType to deal with
 filenames with '.'s in them.  fixed vertical centered text.

---
 src/client/apis/gpt/GPT.ts                         |  1 -
 src/client/documents/DocUtils.ts                   |  4 +-
 src/client/documents/DocumentTypes.ts              | 19 +++--
 src/client/documents/Documents.ts                  |  8 +-
 src/client/util/CurrentUserUtils.ts                | 87 +++++++++++-----------
 src/client/views/GlobalKeyHandler.ts               |  9 +--
 src/client/views/MainView.tsx                      | 21 ++----
 src/client/views/PropertiesButtons.tsx             |  2 +-
 src/client/views/collections/CollectionView.tsx    |  1 -
 .../collectionFreeForm/FaceCollectionBox.tsx       |  4 +-
 .../collectionFreeForm/ImageLabelBox.tsx           | 20 +++--
 .../collections/collectionFreeForm/MarqueeView.tsx |  5 +-
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |  4 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 .../views/nodes/formattedText/RichTextMenu.tsx     |  6 +-
 src/client/views/search/FaceRecognitionHandler.tsx |  4 +-
 src/fields/Doc.ts                                  |  2 +-
 src/fields/Types.ts                                |  8 +-
 18 files changed, 97 insertions(+), 110 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 140aebfe0..9cb47995c 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -271,7 +271,6 @@ const gptImageLabel = async (src: string, prompt: string): Promise<string> => {
             ],
         });
         if (response.choices[0].message.content) {
-            console.log(response.choices[0].message.content);
             return response.choices[0].message.content;
         }
         return 'Missing labels';
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 69bf4815e..a657b673a 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -726,12 +726,12 @@ export namespace DocUtils {
                             : {
                                   _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200,
                                   _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35,
-                                  _layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
                                   _layout_fitWidth: true,
                                   _layout_autoHeight: true,
                                   backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor),
                                   borderColor: Doc.UserDoc().borderColor as string,
                                   borderWidth: Doc.UserDoc().borderWidth as number,
+                                  text_centered: BoolCast(Doc.UserDoc().textCentered),
                                   text_fitBox: BoolCast(Doc.UserDoc().fitBox),
                                   text_align: StrCast(Doc.UserDoc().textAlign),
                                   text_fontColor: StrCast(Doc.UserDoc().fontColor),
@@ -752,12 +752,12 @@ export namespace DocUtils {
                             : {
                                   _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200,
                                   _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35,
-                                  _layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
                                   _layout_fitWidth: true,
                                   _layout_autoHeight: true,
                                   backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor),
                                   borderColor: Doc.UserDoc().borderColor as string,
                                   borderWidth: Doc.UserDoc().borderWidth as number,
+                                  text_centered: BoolCast(Doc.UserDoc().textCentered),
                                   text_fitBox: BoolCast(Doc.UserDoc().fitBox),
                                   text_align: StrCast(Doc.UserDoc().textAlign),
                                   text_fontColor: StrCast(Doc.UserDoc().fontColor),
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 03626107f..dd0985182 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -47,25 +47,30 @@ export enum DocumentType {
     JOURNAL = 'journal', // AARAV ADD
 }
 export enum CollectionViewType {
-    Invalid = 'invalid',
+    // general collections
     Freeform = 'freeform',
-    Calendar = 'calendar',
     Card = 'card',
     Carousel = 'carousel',
     Carousel3D = '3D Carousel',
-    Docking = 'docking',
     Grid = 'grid',
-    Linear = 'linear',
-    Map = 'map',
     Masonry = 'masonry',
     Multicolumn = 'multicolumn',
     Multirow = 'multirow',
     NoteTaking = 'notetaking',
-    Pile = 'pileup',
     Pivot = 'pivot',
     Schema = 'schema',
     Stacking = 'stacking',
-    StackedTimeline = 'stacked timeline',
     Time = 'time',
     Tree = 'tree',
+    // under development
+    Calendar = 'calendar',
+    // special collections
+    Docking = 'docking',
+    Pile = 'pileup',
+    StackedTimeline = 'stacked timeline',
+    Linear = 'linear',
+    Invalid = 'invalid',
 }
+
+export const specialCollectionTypes = [CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.StackedTimeline, CollectionViewType.Linear, CollectionViewType.Invalid];
+export const standardViewTypes = Object.values(CollectionViewType).filter(key => !specialCollectionTypes.includes(key));
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f1655e3cf..bab9edf0e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -283,7 +283,6 @@ export class DocumentOptions {
     _layout_autoHeightMargins?: NUMt = new NumInfo('Margin heights to be added to the computed auto height of a Doc');
     _layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document', false);
     _layout_currentTimecode?: NUMt = new NumInfo('the current timecode of a time-based document (e.g., current time of a video)  value is in seconds', false);
-    _layout_centered?: BOOLt = new BoolInfo('whether text should be vertically centered in Doc');
     _layout_fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)');
     _layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition', false);
     _layout_enableAltContentUI?: BOOLt = new BoolInfo('whether to show alternate content button');
@@ -309,6 +308,7 @@ export class DocumentOptions {
     text_fontSize?: string;
     text_fontFamily?: string;
     text_fontWeight?: string;
+    text_centered?: BOOLt = new BoolInfo('whether text should be vertically centered in Doc');
     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']);
@@ -840,7 +840,7 @@ export namespace Docs {
             return TextDocument(RichTextField.textToRtf(text, img?.[Id]), {
                 title, //
                 _layout_autoHeight: true,
-                _layout_centered: true,
+                text_centered: true,
                 text_align: 'center',
                 _layout_fitWidth: true,
                 ...opts,
@@ -1072,10 +1072,6 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Linear }, id);
         }
 
-        export function MapCollectionDocument(documents: Array<Doc>, options: DocumentOptions = {}) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Map });
-        }
-
         export function CarouselDocument(documents: Array<Doc>, options: DocumentOptions) {
             return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel });
         }
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 3ce20fc7b..4c492cae0 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -15,7 +15,7 @@ import { SetCachedGroups, SharingPermissions } from "../../fields/util";
 import { Gestures } from "../../pen-gestures/GestureTypes";
 import { DocServer } from "../DocServer";
 import { DocUtils, FollowLinkScript } from '../documents/DocUtils';
-import { CollectionViewType, DocumentType } from "../documents/DocumentTypes";
+import { CollectionViewType, DocumentType, standardViewTypes } from "../documents/DocumentTypes";
 import { Docs, DocumentOptions, FInfo, FInfoFieldType } from "../documents/Documents";
 import { DashboardView } from "../views/DashboardView";
 import { OverlayView } from "../views/OverlayView";
@@ -481,22 +481,24 @@ pie title Minerals in my tap water
       const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(this.target?.data).filter(doc => !docList(this.target.viewed).includes(doc)).length.toString())"; 
       const getActiveDashTrails = "Doc.ActiveDashboard?.myTrails";
         return [
-            { title: "Dashboards",      toolTip: "Dashboards",      target: this.setupDashboards(doc, "myDashboards"),         ignoreClick: true, icon: "desktop",     funcs: {hidden: "IsNoviceMode()"} },
-            { title: "Search",          toolTip: "Search ⌘F",       target: this.setupSearcher(doc, "mySearcher"),             ignoreClick: true, icon: "search",      },
-            { title: "Files",           toolTip: "Files",           target: this.setupFilesystem(doc, "myFilesystem"),         ignoreClick: true, icon: "folder-open", },
-            { title: "Tools",           toolTip: "Tools",           target: this.setupToolsBtnPanel(doc, "myTools"),           ignoreClick: true, icon: "wrench",      },
-            { title: "Imports",         toolTip: "Imports ⌘I",      target: this.setupImportSidebar(doc, "myImports"),         ignoreClick:false, icon: "upload",      },
-            { title: "Closed",          toolTip: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), ignoreClick: true, icon: "archive",     hidden: true }, // this doc is hidden from the Sidebar, but it's still being used in MyFilesystem which ignores the hidden field
-            { title: "Shared",          toolTip: "Shared Docs",     target: Doc.MySharedDocs,                                  ignoreClick: true, icon: "users",       funcs: {badgeValue: badgeValue}},
-            { title: "Trails",          toolTip: "Trails ⌘R",       target: Doc.UserDoc(),                                     ignoreClick: true, icon: "pres-trail",  funcs: {target: getActiveDashTrails}},
-            { title: "Image Grouper",   toolTip: "Image Grouper",   target: this.setupImageGrouper(doc, "myImageGrouper"),     ignoreClick: true, icon: "folder-open", hidden: false },
-            { title: "Faces",           toolTip: "Unique Faces",    target: this.setupFaceCollection(doc, "myFaceCollection"), ignoreClick: true, icon: "face-smile",  hidden: false },
-            { title: "User Doc",        toolTip: "User Doc",        target: this.setupUserDocView(doc, "myUserDocView"),       ignoreClick: true, icon: "address-card",funcs: {hidden: "IsNoviceMode()"} },
+            { title: "Dashboards",      toolTip: "Dashboards",      target: this.setupDashboards(doc, "myDashboards"),         icon: "desktop",     funcs: {hidden: "IsNoviceMode()"} },
+            { title: "Search",          toolTip: "Search ⌘F",       target: this.setupSearcher(doc, "mySearcher"),             icon: "search",      },
+            { title: "Files",           toolTip: "Files",           target: this.setupFilesystem(doc, "myFilesystem"),         icon: "folder-open", },
+            { title: "Tools",           toolTip: "Tools",           target: this.setupToolsBtnPanel(doc, "myTools"),           icon: "wrench",      },
+            { title: "Imports",         toolTip: "Imports ⌘I",      target: this.setupImportSidebar(doc, "myImports"),         icon: "upload",      },
+            { title: "Closed",          toolTip: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), icon: "archive",     hidden: true }, // this doc is hidden from the Sidebar, but it's still being used in MyFilesystem which ignores the hidden field
+            { title: "Shared",          toolTip: "Shared Docs",     target: Doc.MySharedDocs,                                  icon: "users",       funcs: {badgeValue: badgeValue}},
+            { title: "Trails",          toolTip: "Trails ⌘R",       target: Doc.UserDoc(),                                     icon: "pres-trail",  funcs: {target: getActiveDashTrails}},
+            { title: "Image Grouper",   toolTip: "Image Grouper",   target: this.setupImageGrouper(doc, "myImageGrouper"),     icon: "folder-open", hidden: false },
+            { title: "Faces",           toolTip: "Unique Faces",    target: this.setupFaceCollection(doc, "myFaceCollection"), icon: "face-smile",  hidden: false },
+            { title: "User Doc",        toolTip: "User Doc",        target: this.setupUserDocView(doc, "myUserDocView"),       icon: "address-card",funcs: {hidden: "IsNoviceMode()"} },
             ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(this)'}}));
     }
 
     /// the empty panel that is filled with whichever left menu button's panel has been selected
     static setupLeftSidebarPanel(doc: Doc, field="myLeftSidebarPanel") {
+        const panel = DocCast(doc[field]);
+        if (panel) panel.proto = undefined;
         DocUtils.AssignDocField(doc, field, (opts) => Doc.assign(new Doc(), opts as {[key:string]: FieldType}), {title:"leftSidebarPanel", isSystem:true, undoIgnoreFields: new List<string>(['proto'])});
     }
 
@@ -737,12 +739,12 @@ pie title Minerals in my tap water
     } 
     static viewTools(): Button[] {
         return [
-            { 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)",
-                                                                                               btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll",    funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform
-            { title: "Clusters",  icon: "braille",             toolTip: "Show Doc Clusters",   btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters",   funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+            { 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)",
+                                                                                       btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll",    funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform
+            { title: "Clusters", icon: "braille",      toolTip: "Show Doc Clusters",   btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters",   funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
         ]
     }
     static textTools():Button[] {
@@ -760,14 +762,14 @@ pie title Minerals in my tap water
             { title: "Vcenter",  toolTip: "Vertical center",      btnType: ButtonType.ToggleButton, icon: "pallet",      toolType:"vcent",    ignoreClick: true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             { title: "Align",    toolTip: "Alignment",            btnType: ButtonType.MultiToggleButton,                 toolType:"alignment",ignoreClick: true,
               subMenu: [
-                { title: "Left",     toolTip: "Left align (Cmd-[)",   btnType: ButtonType.ToggleButton, icon: "align-left",  toolType:"left",  ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
-                { 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: "Left",    toolTip: "Left align (Cmd-[)",   btnType: ButtonType.ToggleButton, icon: "align-left",  toolType:"left",     ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
+             { 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()'}},
+            { 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()'}},
             // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}},
             // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}},
             // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}},
@@ -825,13 +827,9 @@ pie title Minerals in my tap water
             { title: "Rotate",toolTip: "Rotate 90",             btnType: ButtonType.ClickButton,  icon: "redo-alt",  scripts: { onClick: 'imageRotate90();' }},
         ];
     }
-    static contextMenuTools(doc:Doc):Button[] {
+    static contextMenuTools():Button[] {
         return [
-            { btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Card, CollectionViewType.Carousel,CollectionViewType.Carousel3D, 
-                    CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Linear, 
-                    CollectionViewType.Map, CollectionViewType.NoteTaking, CollectionViewType.Pivot, CollectionViewType.Schema, CollectionViewType.Stacking, 
-                    CollectionViewType.Calendar, CollectionViewType.Grid, CollectionViewType.Tree, CollectionViewType.Time, ]),
-              title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                                                             ignoreClick: true, width: 100, scripts: { script:  '{ return setView(value, shiftKey, _readOnly_); }'}},
+            { title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList, expertMode: false, btnList: new List<string>(standardViewTypes),                                                                                                                           ignoreClick: true, width: 100, scripts: { script:  '{ return setView(value, shiftKey, _readOnly_); }'}},
             { 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_); }'} },
@@ -843,18 +841,19 @@ pie title Minerals in my tap water
             { 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: "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
-            { title: "Ink",     icon: "Ink",    toolTip: "Ink functions",        subMenu: CurrentUserUtils.inkTools(),    expertMode: false, toolType:DocumentType.INK,            funcs: {hidden: `IsExploreMode()`,                                  linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
-            { title: "Doc",     icon: "Doc",    toolTip: "Freeform Doc tools",   subMenu: CurrentUserUtils.freeTools(),   expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
-            { title: "View",    icon: "View",   toolTip: "View tools",           subMenu: CurrentUserUtils.viewTools(),   expertMode: false, toolType:DocumentType.COL,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
-            { title: "Stack",   icon: "View",   toolTip: "Stacking tools",       subMenu: CurrentUserUtils.stackTools(),  expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
-            
-            { title: "Web",     icon: "Web",    toolTip: "Web functions",        subMenu: CurrentUserUtils.webTools(),    expertMode: false, toolType:DocumentType.WEB,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected
-            { title: "Video",   icon: "Video",  toolTip: "Video functions",      subMenu: CurrentUserUtils.videoTools(),  expertMode: false, toolType:DocumentType.VID,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
-            { title: "Image",   icon: "Image",  toolTip: "Image functions",      subMenu: CurrentUserUtils.imageTools(),  expertMode: false, toolType:DocumentType.IMG,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
-            { title: "Schema",  icon: "Schema", toolTip: "Schema functions",     subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema,   funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, linearBtnWidth:58 }, // Only when Schema is selected
+            { title: "Filter",  icon: "=",             toolTip: "Filter cards by tags", btnType: ButtonType.MultiToggleButton, 
+                                                                                        subMenu: this.filterTools(),      expertMode: false, toolType:DocumentType.COL,            funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},ignoreClick:true, width: 30},
+            { title: "Sort",    icon: "Sort",          toolTip: "Sort Documents",       subMenu: this.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: this.textTools(),        expertMode: false, toolType:DocumentType.RTF,            funcs: {                                                            linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+            { title: "Ink",     icon: "Ink",           toolTip: "Ink functions",        subMenu: this.inkTools(),         expertMode: false, toolType:DocumentType.INK,            funcs: {hidden: `IsExploreMode()`,                                  linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
+            { title: "Doc",     icon: "Doc",           toolTip: "Freeform Doc tools",   subMenu: this.freeTools(),        expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+            { title: "View",    icon: "View",          toolTip: "View tools",           subMenu: this.viewTools(),        expertMode: false, toolType:DocumentType.COL,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+            { title: "Stack",   icon: "View",          toolTip: "Stacking tools",       subMenu: this.stackTools(),       expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+                   
+            { title: "Web",     icon: "Web",           toolTip: "Web functions",        subMenu: this.webTools(),         expertMode: false, toolType:DocumentType.WEB,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected
+            { title: "Video",   icon: "Video",         toolTip: "Video functions",      subMenu: this.videoTools(),       expertMode: false, toolType:DocumentType.VID,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
+            { title: "Image",   icon: "Image",         toolTip: "Image functions",      subMenu: this.imageTools(),       expertMode: false, toolType:DocumentType.IMG,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
+            { title: "Schema",  icon: "Schema",        toolTip: "Schema functions",     subMenu: this.schemaTools(),      expertMode: false, toolType:CollectionViewType.Schema,   funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, linearBtnWidth:58 }, // Only when Schema is selected
          ];
     }
 
@@ -900,7 +899,7 @@ pie title Minerals in my tap water
     static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") {
         const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: dropActionType.embed, childDontRegisterViews: true,  linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 };
         const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
-        const ctxtMenuBtns = CurrentUserUtils.contextMenuTools(doc).map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) );
+        const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) );
         return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns);
     }
     /// Initializes all the default buttons for the top bar context menu
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 37060d20c..94c023330 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -11,14 +11,13 @@ import { GroupManager } from '../util/GroupManager';
 import { SettingsManager } from '../util/SettingsManager';
 import { SharingManager } from '../util/SharingManager';
 import { SnappingManager } from '../util/SnappingManager';
-import { UndoManager, undoable } from '../util/UndoManager';
+import { UndoManager } from '../util/UndoManager';
 import { ContextMenu } from './ContextMenu';
 import { DocumentDecorations } from './DocumentDecorations';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { MainView } from './MainView';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
-import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
 import { DocumentView } from './nodes/DocumentView';
@@ -242,7 +241,7 @@ export class KeyManager {
                 {
                     const importBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImports);
                     if (importBtn) {
-                        MainView.Instance.selectMenu(importBtn);
+                        MainView.Instance.selectLeftSidebarButton(importBtn);
                     }
                 }
                 break;
@@ -250,7 +249,7 @@ export class KeyManager {
                 {
                     const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyTrails);
                     if (trailsBtn) {
-                        MainView.Instance.selectMenu(trailsBtn);
+                        MainView.Instance.selectLeftSidebarButton(trailsBtn);
                     }
                 }
                 break;
@@ -260,7 +259,7 @@ export class KeyManager {
                 } else {
                     const searchBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MySearcher);
                     if (searchBtn) {
-                        MainView.Instance.selectMenu(searchBtn);
+                        MainView.Instance.selectLeftSidebarButton(searchBtn);
                     }
                 }
                 break;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2b2b77384..be6e2fecb 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -848,7 +848,7 @@ export class MainView extends ObservableReactComponent<object> {
     }
 
     @action
-    selectMenu = (button: Doc) => {
+    selectLeftSidebarButton = (button: Doc) => {
         const title = StrCast(button.$title);
         const willOpen = !this._leftMenuFlyoutWidth || this._panelContent !== title;
         this.closeFlyout();
@@ -860,7 +860,9 @@ export class MainView extends ObservableReactComponent<object> {
                 case 'Help':
                     break;
                 default:
-                    this.expandFlyout(button);
+                    this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250;
+                    this._sidebarContent.proto = DocCast(button.target);
+                    SnappingManager.SetLastPressedBtn(button[Id]);
             }
         }
         return true;
@@ -936,19 +938,6 @@ export class MainView extends ObservableReactComponent<object> {
             </div>
         );
     }
-
-    expandFlyout = action((button: Doc) => {
-        // bcz: What's going on here!? --- may be fixed now, so commenting out ...
-        // Chrome(not firefox) seems to have a bug when the flyout expands and there's a zoomed freeform tab.  All of the div below the CollectionFreeFormView's main div
-        // generate the wrong value from getClientRectangle() -- specifically they return an 'x' that is the flyout's width greater than it should be.
-        // interactively adjusting the flyout fixes the problem.  So does programmatically changing the value after a timeout to something *fractionally* different (ie, 1.5, not 1);)
-        this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250;
-        // setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5)));
-
-        this._sidebarContent.proto = DocCast(button.target);
-        SnappingManager.SetLastPressedBtn(button[Id]);
-    });
-
     closeFlyout = action(() => {
         SnappingManager.SetLastPressedBtn('');
         this._panelContent = 'none';
@@ -1162,7 +1151,7 @@ export class MainView extends ObservableReactComponent<object> {
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function selectMainMenu(doc: Doc) {
-    MainView.Instance.selectMenu(doc);
+    MainView.Instance.selectLeftSidebarButton(doc);
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function hideUI() {
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index a1e8fe7ba..8f18de33f 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -118,7 +118,7 @@ export class PropertiesButtons extends React.Component {
         // select text
         return this.propertyToggleBtn(
             on => (on ? 'ALIGN TOP' : 'ALIGN CENTER'),
-            '_layout_centered',
+            'text_centered',
             on => `${on ? 'Text is aligned with top of document' : 'Text is aligned with center of document'} `,
             () => <MdTouchApp /> // 'eye'
         );
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7ba8989ce..6a7336bca 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -130,7 +130,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
                 { description: 'Calendar',    event: () => func(CollectionViewType.Calendar), icon: 'columns' },
                 { description: 'Pivot',       event: () => func(CollectionViewType.Pivot), icon: 'columns' },
                 { description: 'Time',        event: () => func(CollectionViewType.Time), icon: 'columns' },
-                { description: 'Map',         event: () => func(CollectionViewType.Map), icon: 'globe-americas' },
                 { description: 'Grid',        event: () => func(CollectionViewType.Grid), icon: 'th-list' },
             ];
 
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index b40189d76..e4d38eb4a 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -11,7 +11,7 @@ import { emptyFunction } from '../../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
 import { DocData } from '../../../../fields/DocSymbols';
 import { List } from '../../../../fields/List';
-import { DocCast, ImageCast, NumCast, StrCast } from '../../../../fields/Types';
+import { DocCast, ImageCast, ImageCastToNameType, NumCast, StrCast } from '../../../../fields/Types';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
@@ -187,7 +187,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
                         })}>
                         {FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => {
-                            const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url.href.split('.') ?? ['-missing-', '.png'];
+                            const [name, type] = ImageCastToNameType(doc[Doc.LayoutFieldKey(doc)]) ?? ['-missing-', '.png'];
                             return (
                                 <div
                                     className="image-wrapper"
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
index c983d7c26..7c8ccb92d 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
@@ -10,7 +10,7 @@ import { imageUrlToBase64 } from '../../../../ClientUtils';
 import { Utils, numberRange } from '../../../../Utils';
 import { Doc, NumListCast, Opt } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
-import { ImageCast } from '../../../../fields/Types';
+import { ImageCastToNameType, ImageCastWithSuffix } from '../../../../fields/Types';
 import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
@@ -165,8 +165,8 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
         const imageInfos = this._selectedImages.map(async doc => {
             if (!doc.$tags_chat) {
-                const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.');
-                return imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 =>
+                const url = ImageCastWithSuffix(doc[Doc.LayoutFieldKey(doc)], '_o') ?? '';
+                return imageUrlToBase64(url).then(hrefBase64 =>
                 !hrefBase64 ? undefined : 
                     gptImageLabel(hrefBase64,'Give three labels to describe this image.').then(labels => 
                         ({ doc, labels }))) ; // prettier-ignore
@@ -199,13 +199,11 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
     groupImagesInBox = action(async () => {
         this.startLoading();
 
-        for (const doc of this._selectedImages) {
-            for (let index = 0; index < (doc.$tags_chat as List<string>).length; index++) {
-                const label = (doc.$tags_chat as List<string>)[index];
-                const embedding = await gptGetEmbedding(label);
-                doc[`$tags_embedding_${index + 1}`] = new List<number>(embedding);
-            }
-        }
+        await Promise.all(
+            this._selectedImages
+                .map(doc => ({ doc, labels: doc.$tags_chat as List<string> }))
+                .map(({ doc, labels }) => labels.map((label, index) => gptGetEmbedding(label).then(embedding => (doc[`$tags_embedding_${index + 1}`] = new List<number>(embedding)))))
+        );
 
         const labelToEmbedding = new Map<string, number[]>();
         // Create embeddings for the labels.
@@ -312,7 +310,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 {this._displayImageInformation ? (
                     <div className="image-information-list">
                         {this._selectedImages.map(doc => {
-                            const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.');
+                            const [name, type] = ImageCastToNameType(doc[Doc.LayoutFieldKey(doc)]);
                             return (
                                 <div className="image-information" style={{ borderColor: SettingsManager.userColor }} key={Utils.GenerateGuid()}>
                                     <img
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index ca7e7a311..5fbf72f39 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -8,7 +8,7 @@ import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSy
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkData, InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
-import { Cast, NumCast, StrCast } from '../../../../fields/Types';
+import { Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
 import { GetEffectiveAcl } from '../../../../fields/util';
 import { DocUtils } from '../../../documents/DocUtils';
@@ -18,7 +18,6 @@ import { SnappingManager, freeformScrollMode } from '../../../util/SnappingManag
 import { Transform } from '../../../util/Transform';
 import { UndoManager, undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
-import { MainView } from '../../MainView';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { MarqueeViewBounds } from '../../PinFuncs';
 import { PreviewCursor } from '../../PreviewCursor';
@@ -442,7 +441,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         if (groupButton) {
             this._selectedDocs = this.marqueeSelect(false, DocumentType.IMG);
             ImageLabelBoxData.Instance.setData(this._selectedDocs);
-            MainView.Instance.expandFlyout(groupButton);
+            ScriptCast(groupButton.onClick)?.script.run({ this: groupButton });
         }
     };
 
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index d14def9aa..3190757e2 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -302,7 +302,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                 multiSelect={true}
                 onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))}
                 toggleStatus={toggleStatus}
-                showUntilToggle={BoolCast(this.Document.showUntilToggle)} // allow the toggle to be clickable unless ignoreClick is set on the Document
+                showUntilToggle={BoolCast(this.Document.showUntilToggle)}
                 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} />,
@@ -413,7 +413,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
             case ButtonType.TextButton:         return <Button     {...btnProps}                    color={color} background={color} 
                                                                    text={StrCast(this.dataDoc.buttonText)}/>;
             case ButtonType.MenuButton:         return <IconButton size={Size.LARGE}  {...btnProps} color={color} background={color} 
-                                                                   tooltipPlacement='right' onPointerDown={scriptFunc} />; 
+                                                                   tooltipPlacement='right' onClick={scriptFunc} />; 
             default: 
         }
         return this.defaultButton;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 2a1ec0881..a7e342063 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -2165,7 +2165,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         onScroll={this.onScroll}
                         onDrop={this.ondrop}>
                         <div
-                            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}`}
+                            className={`formattedTextBox-inner${rounded} ${this.dataDoc.text_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),
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 10c95f1e1..4fa1fb13b 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -117,7 +117,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         return this._activeAlignment;
     }
     @computed get textVcenter() {
-        return BoolCast(this.dataDoc?._layout_centered, BoolCast(Doc.UserDoc().layout_centered));
+        return BoolCast(this.dataDoc?.text_centered, BoolCast(Doc.UserDoc().textCentered));
     }
 
     @action
@@ -426,8 +426,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     }
 
     vcenterToggle = () => {
-        if (this.dataDoc) this.dataDoc._layout_centered = !this.dataDoc._layout_centered;
-        else Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered;
+        if (this.dataDoc) this.dataDoc.text_centered = !this.dataDoc.text_centered;
+        else Doc.UserDoc().textCentered = !Doc.UserDoc().textCentered;
     };
     align = (view: EditorView | undefined, dispatch: undefined | ((tr: Transaction) => void), alignment: 'left' | 'right' | 'center') => {
         if (view && dispatch && this.RootSelected) {
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index 841546a04..86e28a768 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -4,7 +4,7 @@ import { Doc, DocListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { List } from '../../../fields/List';
 import { ComputedField } from '../../../fields/ScriptField';
-import { DocCast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { DocCast, ImageCast, ImageCastToNameType, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
@@ -43,7 +43,7 @@ export class FaceRecognitionHandler {
      * Loads an image
      */
     private static loadImage = (imgUrl: ImageField): Promise<HTMLImageElement> => {
-        const [name, type] = imgUrl.url.href.split('.');
+        const [name, type] = ImageCastToNameType(imgUrl.url.href);
         const imageURL = `${name}_o.${type}`;
 
         return new Promise((resolve, reject) => {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 734af4ccd..e48337651 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1461,7 +1461,7 @@ export namespace Doc {
         const docText = (async (tdoc:Doc) => {
             switch (tdoc.type) {
                 case DocumentType.PDF: return curDescription || StrCast(tdoc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text
-                case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(Doc.LayoutField(tdoc), '_o') ?? '')
+                case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(tdoc[Doc.LayoutFieldKey(tdoc)], '_o') ?? '')
                                                 .then(hrefBase64 => gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.'));
                 case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutFieldKey(tdoc)])?.Text ?? StrCast(tdoc[Doc.LayoutFieldKey(tdoc)]);
                 default:               return StrCast(tdoc.title).startsWith("Untitled") ? "" : StrCast(tdoc.title);
diff --git a/src/fields/Types.ts b/src/fields/Types.ts
index af9cb1180..dc7516f38 100644
--- a/src/fields/Types.ts
+++ b/src/fields/Types.ts
@@ -131,9 +131,13 @@ export function PDFCast(field: FieldResult, defaultVal: PdfField | null = null)
 export function ImageCast(field: FieldResult, defaultVal: ImageField | null = null) {
     return Cast(field, ImageField, defaultVal);
 }
-export function ImageCastWithSuffix(field: FieldResult, suffix: string, defaultVal: ImageField | null = null) {
+export function ImageCastToNameType(field: FieldResult, defaultVal: ImageField | null = null) {
     const href = ImageCast(field, defaultVal)?.url.href;
-    return href ? `${href.split('.')[0]}${suffix}.${href.split('.')[1]}` : null;
+    return href ? [href.replace(/.[^.]*$/, ''), href.split('.').lastElement()] : ["", ""];
+}
+export function ImageCastWithSuffix(field: FieldResult, suffix: string, defaultVal: ImageField | null = null) {
+    const [name, type] = ImageCastToNameType(field, defaultVal);
+    return name ? `${name}${suffix}.${type}` : null;
 }
 
 export function FieldValue<T extends FieldType, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>;
-- 
cgit v1.2.3-70-g09d2


From 7dc83b6737207ededb8d11260a89b9e3173b23f5 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 21 Apr 2025 14:56:03 -0400
Subject: warning fixes.

---
 src/client/views/DocumentDecorations.tsx           | 2 +-
 src/client/views/nodes/FontIconBox/FontIconBox.tsx | 9 +++++----
 src/fields/URLField.ts                             | 3 +--
 3 files changed, 7 insertions(+), 7 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 120467e8a..4b7fdc560 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -240,7 +240,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                         } else {
                             // if Doc is in the sticker palette, remove the flag indicating that it's saved
                             const dragFactory = DocCast(iconView.Document.dragFactory);
-                            if (dragFactory && DocCast(dragFactory.cloneOf).savedAsSticker) DocCast(dragFactory.cloneOf).savedAsSticker = undefined;
+                            if (dragFactory && DocCast(dragFactory.cloneOf)?.savedAsSticker) DocCast(dragFactory.cloneOf)!.savedAsSticker = undefined;
 
                             // if this is a face Annotation doc, then just hide it.
                             if (iconView.Document.annotationOn && iconView.Document.face) iconView.Document.hidden = true;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 3190757e2..a3167ee06 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -117,10 +117,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
             default:                               type = 'slider';
                 break;
         } // prettier-ignore
-        const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, value, _readOnly_: value === undefined });
+        const numScript = (value?: number) => ScriptCast(this.Document.script)?.script.run({ this: this.Document, value, _readOnly_: value === undefined });
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
         // Script for checking the outcome of the toggle
-        const checkResult = Number(Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)));
+        const checkResult = Number(Number(numScript()?.result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)));
 
         return (
             <NumberDropdown
@@ -207,6 +207,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
      */
     @computed get dropdownListButton() {
         const script = ScriptCast(this.Document.script);
+        if (!script) return null;
         const selectedFunc = () => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
         const { buttonList, selectedVal, getStyle, jsx, toolTip } = (() => {
             switch (this.Document.title) {
@@ -291,7 +292,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
         const background = this._props.styleProvider?.(this.Document, 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));
+        const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick)?.script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType));
 
         return (
             <MultiToggle
@@ -316,7 +317,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, value: toggleStatus, itemDoc, _readOnly_: false }));
+                    itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick)?.script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false }));
                 }}
             />
         );
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index 3a83e7ca0..8dedb0be7 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -17,9 +17,7 @@ export abstract class URLField extends ObjectField {
     readonly url: URL;
 
     constructor(urlVal: string);
-    // eslint-disable-next-line @typescript-eslint/no-shadow
     constructor(urlVal: URL);
-    // eslint-disable-next-line @typescript-eslint/no-shadow
     constructor(urlVal: URL | string) {
         super();
         this.url =
@@ -50,6 +48,7 @@ export abstract class URLField extends ObjectField {
     }
 
     [Copy](): this {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
         return new (this.constructor as any)(this.url);
     }
 }
-- 
cgit v1.2.3-70-g09d2

