feat: Replaced margin resizers with "Editor width" options. You can set it globally from Preferences > Appearance or per-note from the note options menu (#2324)

This commit is contained in:
Aman Harwara
2023-05-04 17:42:16 +05:30
committed by GitHub
parent 488142683c
commit 9fbb845b1d
18 changed files with 403 additions and 84 deletions

View File

@@ -5,6 +5,7 @@ import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
import { NoteContent, NoteContentSpecialized } from './NoteContent'
import { EditorLineWidth } from '../UserPrefs'
export const isNote = (x: ItemInterface): x is SNNote => x.content_type === ContentType.Note
@@ -15,6 +16,7 @@ export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpe
public readonly preview_plain: string
public readonly preview_html: string
public readonly spellcheck?: boolean
public readonly editorWidth?: EditorLineWidth
public readonly noteType?: NoteType
public readonly authorizedForListed: boolean
@@ -30,6 +32,7 @@ export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpe
this.preview_plain = String(this.payload.content.preview_plain || '')
this.preview_html = String(this.payload.content.preview_html || '')
this.spellcheck = this.payload.content.spellcheck
this.editorWidth = this.payload.content.editorWidth
this.noteType = this.payload.content.noteType
this.editorIdentifier = this.payload.content.editorIdentifier
this.authorizedForListed = this.payload.content.authorizedForListed || false

View File

@@ -1,5 +1,6 @@
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { EditorLineWidth } from '../UserPrefs'
export interface NoteContentSpecialized {
title: string
@@ -8,6 +9,7 @@ export interface NoteContentSpecialized {
preview_plain?: string
preview_html?: string
spellcheck?: boolean
editorWidth?: EditorLineWidth
noteType?: NoteType
editorIdentifier?: FeatureIdentifier | string
authorizedForListed?: boolean

View File

@@ -5,6 +5,7 @@ import { NoteToNoteReference } from '../../Abstract/Reference/NoteToNoteReferenc
import { ContentType } from '@standardnotes/common'
import { ContentReferenceType } from '../../Abstract/Item'
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
import { EditorLineWidth } from '../UserPrefs'
export class NoteMutator extends DecryptedItemMutator<NoteContent> {
set title(title: string) {
@@ -31,6 +32,10 @@ export class NoteMutator extends DecryptedItemMutator<NoteContent> {
this.mutableContent.spellcheck = spellcheck
}
set editorWidth(editorWidth: EditorLineWidth) {
this.mutableContent.editorWidth = editorWidth
}
set noteType(noteType: NoteType) {
this.mutableContent.noteType = noteType
}

View File

@@ -12,6 +12,7 @@ export enum PrefKey {
EditorSpellcheck = 'spellcheck',
EditorResizersEnabled = 'marginResizersEnabled',
EditorLineHeight = 'editorLineHeight',
EditorLineWidth = 'editorLineWidth',
EditorFontSize = 'editorFontSize',
SortNotesBy = 'sortBy',
SortNotesReverse = 'sortReverse',
@@ -65,6 +66,13 @@ export enum EditorLineHeight {
Loose = 'Loose',
}
export enum EditorLineWidth {
Narrow = 'Narrow',
Wide = 'Wide',
Dynamic = 'Dynamic',
FullWidth = 'FullWidth',
}
export enum EditorFontSize {
ExtraSmall = 'ExtraSmall',
Small = 'Small',
@@ -107,6 +115,7 @@ export type PrefValue = {
[PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat
[PrefKey.CustomNoteTitleFormat]: string
[PrefKey.EditorLineHeight]: EditorLineHeight
[PrefKey.EditorLineWidth]: EditorLineWidth
[PrefKey.EditorFontSize]: EditorFontSize
[PrefKey.UpdateSavingStatusIndicator]: boolean
[PrefKey.DarkMode]: boolean

View File

@@ -37,3 +37,5 @@ export const SUPER_EXPORT_JSON = createKeyboardCommand('SUPER_EXPORT_JSON')
export const SUPER_EXPORT_MARKDOWN = createKeyboardCommand('SUPER_EXPORT_MARKDOWN')
export const SUPER_EXPORT_HTML = createKeyboardCommand('SUPER_EXPORT_HTML')
export const OPEN_PREFERENCES_COMMAND = createKeyboardCommand('OPEN_PREFERENCES_COMMAND')
export const CHANGE_EDITOR_WIDTH_COMMAND = createKeyboardCommand('CHANGE_EDITOR_WIDTH_COMMAND')

View File

@@ -29,6 +29,7 @@ import {
SUPER_SEARCH_NEXT_RESULT,
SUPER_SEARCH_PREVIOUS_RESULT,
SUPER_SEARCH_TOGGLE_REPLACE_MODE,
CHANGE_EDITOR_WIDTH_COMMAND,
} from './KeyboardCommands'
import { KeyboardKey } from './KeyboardKey'
import { KeyboardModifier } from './KeyboardModifier'
@@ -182,5 +183,11 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme
modifiers: [primaryModifier],
preventDefault: true,
},
{
command: CHANGE_EDITOR_WIDTH_COMMAND,
key: 'j',
modifiers: [primaryModifier, KeyboardModifier.Shift],
preventDefault: true,
},
]
}

View File

@@ -30,6 +30,7 @@ import DotOrgNotice from './DotOrgNotice'
import LinkingControllerProvider from '@/Controllers/LinkingControllerProvider'
import ImportModal from '../ImportModal/ImportModal'
import IosKeyboardClose from '../IosKeyboardClose/IosKeyboardClose'
import EditorWidthSelectionModalWrapper from '../EditorWidthSelectionModal/EditorWidthSelectionModal'
type Props = {
application: WebApplication
@@ -268,6 +269,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
<ToastContainer />
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
<PermissionsModalWrapper application={application} />
<EditorWidthSelectionModalWrapper />
<ConfirmDeleteAccountContainer
application={application}
viewControllerManager={viewControllerManager}

View File

@@ -4,7 +4,6 @@ import { DropdownItem } from './DropdownItem'
import { classNames } from '@standardnotes/snjs'
import {
Select,
SelectArrow,
SelectItem,
SelectLabel,
SelectPopover,
@@ -102,7 +101,7 @@ const Dropdown = ({
) : null}
<div className="text-base lg:text-sm">{currentItem?.label}</div>
</div>
<SelectArrow className={classNames('text-passive-1', isExpanded && 'rotate-180')} />
<Icon type="chevron-down" size="normal" className={isExpanded ? 'rotate-180' : ''} />
</Select>
<SelectPopover
store={select}

View File

@@ -0,0 +1,216 @@
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { classNames, EditorLineWidth, PrefKey, SNNote } from '@standardnotes/snjs'
import { useCallback, useEffect, useMemo, useState } from 'react'
import Button from '../Button/Button'
import Modal, { ModalAction } from '../Modal/Modal'
import ModalDialogButtons from '../Modal/ModalDialogButtons'
import RadioButtonGroup from '../RadioButtonGroup/RadioButtonGroup'
import { EditorMargins, EditorMaxWidths } from './EditorWidths'
import { useApplication } from '../ApplicationProvider'
import ModalOverlay from '../Modal/ModalOverlay'
import { CHANGE_EDITOR_WIDTH_COMMAND, ESCAPE_COMMAND } from '@standardnotes/ui-services'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { observer } from 'mobx-react-lite'
import Switch from '../Switch/Switch'
const DoubleSidedArrow = ({ className }: { className?: string }) => {
return (
<div
className={classNames(
'relative h-[2px] w-full bg-current',
'before:absolute before:-left-px before:top-1/2 before:h-0 before:w-0 before:-translate-y-1/2 before:border-r-[6px] before:border-t-[6px] before:border-b-[6px] before:border-current before:border-b-transparent before:border-t-transparent',
'after:absolute after:-right-px after:top-1/2 after:h-0 after:w-0 after:-translate-y-1/2 after:border-l-[6px] after:border-t-[6px] after:border-b-[6px] after:border-current after:border-b-transparent after:border-t-transparent',
className,
)}
/>
)
}
const EditorWidthSelectionModal = ({
initialValue,
handleChange,
close,
note,
}: {
initialValue: EditorLineWidth
handleChange: (value: EditorLineWidth, setGlobally: boolean) => void
close: () => void
note: SNNote | undefined
}) => {
const application = useApplication()
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const [value, setValue] = useState<EditorLineWidth>(() => initialValue)
const [setGlobally, setSetGlobally] = useState(false)
const options = useMemo(
() => [
{
label: 'Narrow',
value: EditorLineWidth.Narrow,
},
{
label: 'Wide',
value: EditorLineWidth.Wide,
},
{
label: 'Dynamic',
value: EditorLineWidth.Dynamic,
},
{
label: 'Full width',
value: EditorLineWidth.FullWidth,
},
],
[],
)
const accept = useCallback(() => {
handleChange(value, setGlobally)
close()
}, [close, handleChange, setGlobally, value])
const actions = useMemo(
(): ModalAction[] => [
{
label: 'Cancel',
type: 'cancel',
onClick: close,
mobileSlot: 'left',
},
{
label: 'Done',
type: 'primary',
onClick: accept,
mobileSlot: 'right',
},
],
[accept, close],
)
useEffect(() => {
return application.keyboardService.addCommandHandler({
command: ESCAPE_COMMAND,
onKeyDown() {
close()
return
},
})
}, [application.keyboardService, close])
const DynamicMargin = (
<div className="text-center text-sm text-passive-2">
<div className={value !== EditorLineWidth.Dynamic ? 'hidden' : ''}>
<div className="mb-2">{EditorMargins[value]}</div>
<DoubleSidedArrow />
</div>
</div>
)
return (
<Modal
title="Set editor width"
close={close}
customHeader={<></>}
customFooter={<></>}
disableCustomHeader={isMobileScreen}
actions={actions}
className={{
content: 'select-none md:min-w-[40vw]',
description: 'flex min-h-[50vh] flex-col',
}}
>
<div className="flex min-h-0 flex-grow flex-col overflow-hidden rounded bg-passive-5 p-4 pb-0">
<div
className={classNames(
'grid flex-grow grid-cols-[0fr_1fr_0fr] gap-3 rounded rounded-b-none bg-default px-2 pt-4 shadow-[0_1px_4px_rgba(0,0,0,0.12),0_2px_8px_rgba(0,0,0,0.04)] transition-all duration-200 md:px-4',
value === EditorLineWidth.Narrow && 'md:grid-cols-[1fr_60%_1fr]',
value === EditorLineWidth.Wide && 'md:grid-cols-[1fr_70%_1fr]',
value === EditorLineWidth.Dynamic && 'md:grid-cols-[1fr_80%_1fr]',
value === EditorLineWidth.FullWidth && 'md:grid-cols-[1fr_95%_1fr]',
)}
>
{DynamicMargin}
<div className="flex flex-col text-info">
<div className="mb-2 text-center text-sm">
{value === EditorLineWidth.Narrow || value === EditorLineWidth.Wide
? `Max. ${EditorMaxWidths[value]}`
: EditorMaxWidths[value]}
</div>
<DoubleSidedArrow />
<div className="w-full flex-grow bg-[linear-gradient(transparent_50%,var(--sn-stylekit-info-color)_50%)] bg-[length:100%_2.5rem] bg-repeat-y opacity-10" />
</div>
{DynamicMargin}
</div>
</div>
{!!note && (
<div className="border-t border-border bg-default px-4 py-2">
<label className="flex items-center gap-2">
<Switch checked={setGlobally} onChange={setSetGlobally} />
Set globally {note.editorWidth != undefined && '(will not apply to current note)'}
</label>
</div>
)}
<ModalDialogButtons className="justify-center md:justify-between">
<RadioButtonGroup items={options} value={value} onChange={(value) => setValue(value as EditorLineWidth)} />
<div className="hidden items-center gap-2 md:flex">
<Button onClick={close}>Cancel</Button>
<Button onClick={accept} primary>
Apply
</Button>
</div>
</ModalDialogButtons>
</Modal>
)
}
const EditorWidthSelectionModalWrapper = () => {
const application = useApplication()
const { notesController } = application.getViewControllerManager()
const [isOpen, setIsOpen] = useState(false)
const [isGlobal, setIsGlobal] = useState(false)
const note = notesController.selectedNotesCount === 1 && !isGlobal ? notesController.selectedNotes[0] : undefined
const lineWidth = note
? notesController.getEditorWidthForNote(note)
: application.getPreference(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth])
const setLineWidth = useCallback(
(lineWidth: EditorLineWidth, setGlobally: boolean) => {
if (note && !setGlobally) {
notesController.setNoteEditorWidth(note, lineWidth).catch(console.error)
} else {
application.setPreference(PrefKey.EditorLineWidth, lineWidth).catch(console.error)
}
},
[application, note, notesController],
)
const toggle = useCallback(() => {
setIsOpen((open) => !open)
}, [])
useEffect(() => {
return application.keyboardService.addCommandHandler({
command: CHANGE_EDITOR_WIDTH_COMMAND,
onKeyDown: (_, data) => {
if (typeof data === 'boolean' && data) {
setIsGlobal(data)
} else {
setIsGlobal(false)
}
toggle()
},
})
}, [application, toggle])
return (
<ModalOverlay isOpen={isOpen}>
<EditorWidthSelectionModal initialValue={lineWidth} handleChange={setLineWidth} close={toggle} note={note} />
</ModalOverlay>
)
}
export default observer(EditorWidthSelectionModalWrapper)

View File

@@ -0,0 +1,15 @@
import { EditorLineWidth } from '@standardnotes/snjs'
export const EditorMaxWidths: { [k in EditorLineWidth]: string } = {
[EditorLineWidth.Narrow]: '512px',
[EditorLineWidth.Wide]: '720px',
[EditorLineWidth.Dynamic]: '80%',
[EditorLineWidth.FullWidth]: '100%',
}
export const EditorMargins: { [k in EditorLineWidth]: string } = {
[EditorLineWidth.Narrow]: 'auto',
[EditorLineWidth.Wide]: 'auto',
[EditorLineWidth.Dynamic]: '10%',
[EditorLineWidth.FullWidth]: '0',
}

View File

@@ -42,6 +42,7 @@ export const IconNameToSvgMapping = {
'hashtag-off': icons.HashtagOffIcon,
'keyboard-close': icons.KeyboardCloseIcon,
'link-off': icons.LinkOffIcon,
'line-width': icons.LineWidthIcon,
'list-bulleted': icons.ListBulleted,
'list-numbered': icons.ListNumbered,
'lock-filled': icons.LockFilledIcon,

View File

@@ -35,6 +35,7 @@ describe('NoteView', () => {
notesController = {} as jest.Mocked<NotesController>
notesController.setShowProtectedWarning = jest.fn()
notesController.getSpellcheckStateForNote = jest.fn()
notesController.getEditorWidthForNote = jest.fn()
viewControllerManager = {
notesController: notesController,

View File

@@ -2,20 +2,20 @@ import { AbstractComponent } from '@/Components/Abstract/PureComponent'
import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton'
import ComponentView from '@/Components/ComponentView/ComponentView'
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
import PanelResizer, { PanelResizeType, PanelSide } from '@/Components/PanelResizer/PanelResizer'
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
import { ElementIds } from '@/Constants/ElementIDs'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
import { log, LoggingDomain } from '@/Logging'
import { debounce, isDesktopApplication, isMobileScreen, isTabletOrMobileScreen } from '@/Utils'
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
import { classNames } from '@standardnotes/utils'
import {
ApplicationEvent,
ComponentArea,
ComponentViewerInterface,
ContentType,
EditorLineWidth,
isPayloadSourceInternalChange,
isPayloadSourceRetrieved,
NoteType,
@@ -26,7 +26,7 @@ import {
SNNote,
} from '@standardnotes/snjs'
import { confirmDialog, DELETE_NOTE_KEYBOARD_COMMAND, KeyboardKey } from '@standardnotes/ui-services'
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
import { ChangeEventHandler, createRef, CSSProperties, KeyboardEventHandler, RefObject } from 'react'
import { SuperEditor } from '../SuperEditor/SuperEditor'
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
@@ -44,6 +44,7 @@ import {
import { SuperEditorContentId } from '../SuperEditor/Constants'
import { NoteViewController } from './Controller/NoteViewController'
import { PlainEditor, PlainEditorInterface } from './PlainEditor/PlainEditor'
import { EditorMargins, EditorMaxWidths } from '../EditorWidthSelectionModal/EditorWidths'
const MinimumStatusDuration = 400
@@ -58,7 +59,7 @@ type State = {
editorStateDidLoad: boolean
editorTitle: string
isDesktop?: boolean
marginResizersEnabled?: boolean
editorLineWidth: EditorLineWidth
noteLocked: boolean
noteStatus?: NoteStatus
saveError?: boolean
@@ -68,10 +69,6 @@ type State = {
syncTakingTooLong: boolean
monospaceFont?: boolean
plainEditorFocused?: boolean
leftResizerWidth: number
leftResizerOffset: number
rightResizerWidth: number
rightResizerOffset: number
updateSavingIndicator?: boolean
editorFeatureIdentifier?: string
@@ -112,6 +109,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
availableStackComponents: [],
editorStateDidLoad: false,
editorTitle: '',
editorLineWidth: PrefDefaults[PrefKey.EditorLineWidth],
isDesktop: isDesktopApplication(),
noteStatus: undefined,
noteLocked: this.controller.item.locked,
@@ -119,10 +117,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
spellcheck: true,
stackComponentViewers: [],
syncTakingTooLong: false,
leftResizerWidth: 0,
leftResizerOffset: 0,
rightResizerWidth: 0,
rightResizerOffset: 0,
editorFeatureIdentifier: this.controller.item.editorIdentifier,
noteType: this.controller.item.noteType,
}
@@ -280,6 +274,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
this.reloadSpellcheck().catch(console.error)
this.reloadLineWidth()
const isTemplateNoteInsertedToBeInteractableWithEditor = source === PayloadEmitSource.LocalInserted && note.dirty
if (isTemplateNoteInsertedToBeInteractableWithEditor) {
return
@@ -660,6 +656,14 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
}
}
reloadLineWidth() {
const editorLineWidth = this.viewControllerManager.notesController.getEditorWidthForNote(this.note)
this.setState({
editorLineWidth,
})
}
async reloadPreferences() {
log(LoggingDomain.NoteView, 'Reload preferences')
const monospaceFont = this.application.getPreference(
@@ -667,10 +671,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
PrefDefaults[PrefKey.EditorMonospaceEnabled],
)
const marginResizersEnabled =
!isTabletOrMobileScreen() &&
this.application.getPreference(PrefKey.EditorResizersEnabled, PrefDefaults[PrefKey.EditorResizersEnabled])
const updateSavingIndicator = this.application.getPreference(
PrefKey.UpdateSavingStatusIndicator,
PrefDefaults[PrefKey.UpdateSavingStatusIndicator],
@@ -678,30 +678,14 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
await this.reloadSpellcheck()
this.reloadLineWidth()
this.setState({
monospaceFont,
marginResizersEnabled,
updateSavingIndicator,
})
reloadFont(monospaceFont)
if (marginResizersEnabled) {
const width = this.application.getPreference(PrefKey.EditorWidth, PrefDefaults[PrefKey.EditorWidth])
if (width != null) {
this.setState({
leftResizerWidth: width,
rightResizerWidth: width,
})
}
const left = this.application.getPreference(PrefKey.EditorLeft, PrefDefaults[PrefKey.EditorLeft])
if (left != null) {
this.setState({
leftResizerOffset: left,
rightResizerOffset: left,
})
}
}
}
async reloadStackComponents() {
@@ -896,24 +880,18 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
<div
id={ElementIds.EditorContent}
className={`${ElementIds.EditorContent} z-editor-content overflow-auto`}
className={classNames(
ElementIds.EditorContent,
'z-editor-content overflow-auto [&>*]:mx-[var(--editor-margin)] [&>*]:max-w-[var(--editor-max-width)]',
)}
style={
{
'--editor-margin': EditorMargins[this.state.editorLineWidth],
'--editor-max-width': EditorMaxWidths[this.state.editorLineWidth],
} as CSSProperties
}
ref={this.editorContentRef}
>
{this.state.marginResizersEnabled && this.editorContentRef.current ? (
<PanelResizer
minWidth={300}
hoverable={true}
collapsable={false}
panel={this.editorContentRef.current}
side={PanelSide.Left}
type={PanelResizeType.OffsetAndWidth}
left={this.state.leftResizerOffset}
width={this.state.leftResizerWidth}
resizeFinishCallback={this.onPanelResizeFinish}
modifyElementWidth={true}
/>
) : null}
{editorMode === 'component' && this.state.editorComponentViewer && (
<div className="component-view flex-grow">
<ComponentView
@@ -950,21 +928,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
/>
</div>
)}
{this.state.marginResizersEnabled && this.editorContentRef.current ? (
<PanelResizer
minWidth={300}
hoverable={true}
collapsable={false}
panel={this.editorContentRef.current}
side={PanelSide.Right}
type={PanelResizeType.OffsetAndWidth}
left={this.state.rightResizerOffset}
width={this.state.rightResizerWidth}
resizeFinishCallback={this.onPanelResizeFinish}
modifyElementWidth={true}
/>
) : null}
</div>
<div id="editor-pane-component-stack">

View File

@@ -3,6 +3,7 @@ import { observer } from 'mobx-react-lite'
import { useState, useEffect, useMemo, useCallback } from 'react'
import { NoteType, Platform, SNNote } from '@standardnotes/snjs'
import {
CHANGE_EDITOR_WIDTH_COMMAND,
OPEN_NOTE_HISTORY_COMMAND,
PIN_NOTE_COMMAND,
SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND,
@@ -165,6 +166,14 @@ const NotesOptions = ({
commandService.triggerCommand(SUPER_SHOW_MARKDOWN_PREVIEW)
}, [commandService])
const toggleLineWidthModal = useCallback(() => {
application.keyboardService.triggerCommand(CHANGE_EDITOR_WIDTH_COMMAND)
}, [application.keyboardService])
const editorWidthShortcut = useMemo(
() => application.keyboardService.keyboardShortcutForCommand(CHANGE_EDITOR_WIDTH_COMMAND),
[application],
)
const unauthorized = notes.some((note) => !application.isAuthorizedToRenderItem(note))
if (unauthorized) {
return <ProtectedUnauthorizedLabel />
@@ -186,6 +195,11 @@ const NotesOptions = ({
{historyShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={historyShortcut} />}
</MenuItem>
<HorizontalSeparator classes="my-2" />
<MenuItem onClick={toggleLineWidthModal}>
<Icon type="line-width" className={iconClass} />
Editor width
{editorWidthShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={editorWidthShortcut} />}
</MenuItem>
</>
)}
<MenuSwitchButtonItem

View File

@@ -1,13 +1,15 @@
import { WebApplication } from '@/Application/WebApplication'
import Dropdown from '@/Components/Dropdown/Dropdown'
import Icon from '@/Components/Icon/Icon'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Switch from '@/Components/Switch/Switch'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { EditorFontSize, EditorLineHeight, PrefKey } from '@standardnotes/snjs'
import { useMemo, useState } from 'react'
import { ApplicationEvent, EditorFontSize, EditorLineHeight, EditorLineWidth, PrefKey } from '@standardnotes/snjs'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Subtitle, Title, Text } from '../../PreferencesComponents/Content'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import { CHANGE_EDITOR_WIDTH_COMMAND } from '@standardnotes/ui-services'
type Props = {
application: WebApplication
@@ -59,28 +61,25 @@ const EditorDefaults = ({ application }: Props) => {
[],
)
const [marginResizers, setMarginResizers] = useState(() =>
application.getPreference(PrefKey.EditorResizersEnabled, PrefDefaults[PrefKey.EditorResizersEnabled]),
const [editorWidth, setEditorWidth] = useState(() =>
application.getPreference(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth]),
)
const toggleMarginResizers = () => {
setMarginResizers(!marginResizers)
application.setPreference(PrefKey.EditorResizersEnabled, !marginResizers).catch(console.error)
}
const toggleEditorWidthModal = useCallback(() => {
application.keyboardService.triggerCommand(CHANGE_EDITOR_WIDTH_COMMAND, true)
}, [application.keyboardService])
useEffect(() => {
return application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => {
setEditorWidth(application.getPreference(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth]))
})
}, [application])
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Editor appearance</Title>
<div className="mt-2">
<div className="flex items-center justify-between">
<div className="flex flex-col">
<Subtitle>Margin Resizers</Subtitle>
<Text>Allows left and right editor margins to be resized.</Text>
</div>
<Switch onChange={toggleMarginResizers} checked={marginResizers} />
</div>
<HorizontalSeparator classes="my-4" />
<div className="flex items-center justify-between">
<div className="flex flex-col">
<Subtitle>Monospace Font</Subtitle>
@@ -114,6 +113,20 @@ const EditorDefaults = ({ application }: Props) => {
/>
</div>
</div>
<HorizontalSeparator classes="my-4" />
<div>
<Subtitle>Editor width</Subtitle>
<Text>Sets the max editor width for all notes</Text>
<div className="mt-2">
<button
className="flex w-full min-w-55 items-center justify-between rounded border border-border bg-default py-1.5 px-3.5 text-left text-base text-foreground md:w-fit lg:text-sm"
onClick={toggleEditorWidthModal}
>
{editorWidth === EditorLineWidth.FullWidth ? 'Full width' : editorWidth}
<Icon type="chevron-down" size="normal" />
</button>
</div>
</div>
</div>
</PreferencesSegment>
</PreferencesGroup>

View File

@@ -0,0 +1,41 @@
import { VisuallyHidden, Radio, RadioGroup, useRadioStore } from '@ariakit/react'
import { classNames } from '@standardnotes/utils'
type Props = {
items: { label: string; value: string }[]
value: string
onChange: (value: string) => void
}
const RadioButtonGroup = ({ value, items, onChange }: Props) => {
const radio = useRadioStore({
value,
orientation: 'horizontal',
setValue(value) {
onChange(value as string)
},
})
return (
<RadioGroup store={radio} className="flex divide-x divide-border rounded border border-border">
{items.map(({ label, value: itemValue }) => (
<label
className={classNames(
'flex-grow select-none py-1.5 px-3.5 text-center',
'first:rounded-tl first:rounded-bl last:rounded-tr last:rounded-br',
itemValue === value &&
'bg-info-backdrop font-medium text-info ring-1 ring-inset ring-info focus-within:ring-2',
)}
key={itemValue}
>
<VisuallyHidden>
<Radio value={itemValue} />
</VisuallyHidden>
{label}
</label>
))}
</RadioGroup>
)
}
export default RadioButtonGroup

View File

@@ -1,4 +1,11 @@
import { PrefKey, CollectionSort, NewNoteTitleFormat, EditorLineHeight, EditorFontSize } from '@standardnotes/models'
import {
PrefKey,
CollectionSort,
NewNoteTitleFormat,
EditorLineHeight,
EditorFontSize,
EditorLineWidth,
} from '@standardnotes/models'
import { FeatureIdentifier } from '@standardnotes/snjs'
export const PrefDefaults = {
@@ -10,6 +17,7 @@ export const PrefDefaults = {
[PrefKey.EditorSpellcheck]: true,
[PrefKey.EditorResizersEnabled]: false,
[PrefKey.EditorLineHeight]: EditorLineHeight.Normal,
[PrefKey.EditorLineWidth]: EditorLineWidth.FullWidth,
[PrefKey.EditorFontSize]: EditorFontSize.Normal,
[PrefKey.SortNotesBy]: CollectionSort.CreatedAt,
[PrefKey.SortNotesReverse]: false,

View File

@@ -10,6 +10,7 @@ import {
InternalEventBus,
PrefKey,
ApplicationEvent,
EditorLineWidth,
} from '@standardnotes/snjs'
import { makeObservable, observable, action, computed, runInAction } from 'mobx'
import { WebApplication } from '../../Application/WebApplication'
@@ -358,6 +359,23 @@ export class NotesController extends AbstractViewController implements NotesCont
this.application.sync.sync().catch(console.error)
}
getEditorWidthForNote(note: SNNote) {
return (
note.editorWidth ?? this.application.getPreference(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth])
)
}
async setNoteEditorWidth(note: SNNote, editorWidth: EditorLineWidth) {
await this.application.mutator.changeItem<NoteMutator>(
note,
(mutator) => {
mutator.editorWidth = editorWidth
},
false,
)
this.application.sync.sync().catch(console.error)
}
async addTagToSelectedNotes(tag: SNTag): Promise<void> {
const selectedNotes = this.getSelectedNotesList()
await Promise.all(