diff --git a/app/assets/icons/ic-add.svg b/app/assets/icons/ic-add.svg new file mode 100644 index 00000000000..d92c119a2c2 --- /dev/null +++ b/app/assets/icons/ic-add.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-folder.svg b/app/assets/icons/ic-folder.svg new file mode 100644 index 00000000000..db3aea31e6d --- /dev/null +++ b/app/assets/icons/ic-folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-list-bulleted.svg b/app/assets/icons/ic-list-bulleted.svg index 0e928e6f7dd..98c5a43330a 100644 --- a/app/assets/icons/ic-list-bulleted.svg +++ b/app/assets/icons/ic-list-bulleted.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-menu-arrow-right.svg b/app/assets/icons/ic-menu-arrow-right.svg index 1dc58a46edf..ba5890ea7f5 100644 --- a/app/assets/icons/ic-menu-arrow-right.svg +++ b/app/assets/icons/ic-menu-arrow-right.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 8f6bfdf47f4..20e74fa707a 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -89,6 +89,7 @@ import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu/Quick import { ComponentViewDirective } from '@/components/ComponentView'; import { TagsListDirective } from '@/components/TagsList'; import { PinNoteButtonDirective } from '@/components/PinNoteButton'; +import { TagsSectionDirective } from './components/Tags/TagsSection'; function reloadHiddenFirefoxTab(): boolean { /** @@ -190,7 +191,8 @@ const startApplication: StartApplication = async function startApplication( .directive('notesListOptionsMenu', NotesListOptionsDirective) .directive('icon', IconDirective) .directive('noteTagsContainer', NoteTagsContainerDirective) - .directive('tags', TagsListDirective) + .directive('tagsList', TagsListDirective) + .directive('tagsSection', TagsSectionDirective) .directive('preferences', PreferencesDirective) .directive('purchaseFlow', PurchaseFlowDirective) .directive('pinNoteButton', PinNoteButtonDirective); diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index c20708ef236..c296e00a2d1 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -24,6 +24,7 @@ import MarkdownIcon from '../../icons/ic-markdown.svg'; import CodeIcon from '../../icons/ic-code.svg'; import AccessibilityIcon from '../../icons/ic-accessibility.svg'; +import AddIcon from '../../icons/ic-add.svg'; import HelpIcon from '../../icons/ic-help.svg'; import KeyboardIcon from '../../icons/ic-keyboard.svg'; import ListBulleted from '../../icons/ic-list-bulleted.svg'; @@ -100,6 +101,7 @@ const ICONS = { more: MoreIcon, tune: TuneIcon, accessibility: AccessibilityIcon, + add: AddIcon, help: HelpIcon, keyboard: KeyboardIcon, 'list-bulleted': ListBulleted, diff --git a/app/assets/javascripts/components/Premium/index.ts b/app/assets/javascripts/components/Premium/index.ts new file mode 100644 index 00000000000..d24a2c7daac --- /dev/null +++ b/app/assets/javascripts/components/Premium/index.ts @@ -0,0 +1 @@ +export { usePremiumModal, PremiumModalProvider } from './usePremiumModal'; diff --git a/app/assets/javascripts/components/Premium/usePremiumModal.tsx b/app/assets/javascripts/components/Premium/usePremiumModal.tsx new file mode 100644 index 00000000000..d52b1582384 --- /dev/null +++ b/app/assets/javascripts/components/Premium/usePremiumModal.tsx @@ -0,0 +1,49 @@ +import { FunctionalComponent } from 'preact'; +import { useCallback, useContext, useState } from 'preact/hooks'; +import { createContext } from 'react'; +import { PremiumFeaturesModal } from '../PremiumFeaturesModal'; + +type PremiumModalContextData = { + activate: (featureName: string) => void; +}; + +const PremiumModalContext = createContext(null); + +const PremiumModalProvider_ = PremiumModalContext.Provider; + +export const usePremiumModal = (): PremiumModalContextData => { + const value = useContext(PremiumModalContext); + + if (!value) { + throw new Error('invalid PremiumModal context'); + } + + return value; +}; + +export const PremiumModalProvider: FunctionalComponent = ({ children }) => { + const [featureName, setFeatureName] = useState(null); + + const activate = setFeatureName; + + const closeModal = useCallback(() => { + setFeatureName(null); + }, [setFeatureName]); + + const showModal = !!featureName; + + return ( + <> + {showModal && ( + + )} + + {children} + + + ); +}; diff --git a/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx b/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx index 66f4483bbc7..701be49dc35 100644 --- a/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx +++ b/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx @@ -306,16 +306,6 @@ const QuickSettingsMenu: FunctionComponent = observer( onClose={closeQuickSettingsMenu} isEnabled={focusModeEnabled} /> - {appState.features.hasUnfinishedFoldersFeature && ( - { - appState.features.hasFolders = checked; - }} - isEnabled={appState.features.hasFolders} - onClose={closeQuickSettingsMenu} - /> - )}
- setShowUpgradeModal(false)} - /> - - ); -}; diff --git a/app/assets/javascripts/components/RootTagDropZone.tsx b/app/assets/javascripts/components/RootTagDropZone.tsx new file mode 100644 index 00000000000..67e510f68f0 --- /dev/null +++ b/app/assets/javascripts/components/RootTagDropZone.tsx @@ -0,0 +1,59 @@ +import { + FeaturesState, + TAG_FOLDERS_FEATURE_NAME, +} from '@/ui_models/app_state/features_state'; +import { TagsState } from '@/ui_models/app_state/tags_state'; +import { observer } from 'mobx-react-lite'; +import { useDrop } from 'react-dnd'; +import { usePremiumModal } from './Premium'; +import { DropItem, DropProps, ItemTypes } from './TagsListItem'; + +type Props = { + tagsState: TagsState; + featuresState: FeaturesState; +}; + +export const RootTagDropZone: React.FC = observer( + ({ tagsState, featuresState }) => { + const premiumModal = usePremiumModal(); + const isNativeFoldersEnabled = featuresState.enableNativeFoldersFeature; + const hasFolders = tagsState.hasFolders; + + const [{ isOver, canDrop }, dropRef] = useDrop( + () => ({ + accept: ItemTypes.TAG, + canDrop: () => { + return true; + }, + drop: (item) => { + if (!hasFolders) { + premiumModal.activate(TAG_FOLDERS_FEATURE_NAME); + return; + } + + tagsState.assignParent(item.uuid, undefined); + }, + collect: (monitor) => ({ + isOver: !!monitor.isOver(), + canDrop: !!monitor.canDrop(), + }), + }), + [tagsState, hasFolders, premiumModal] + ); + + if (!isNativeFoldersEnabled || !hasFolders) { + return null; + } + + return ( +
+ Move the tag here to remove it from its folder. +
+ ); + } +); diff --git a/app/assets/javascripts/components/Tags/TagsSection.tsx b/app/assets/javascripts/components/Tags/TagsSection.tsx new file mode 100644 index 00000000000..a0064e1015c --- /dev/null +++ b/app/assets/javascripts/components/Tags/TagsSection.tsx @@ -0,0 +1,107 @@ +import { TagsList } from '@/components/TagsList'; +import { toDirective } from '@/components/utils'; +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { + FeaturesState, + TAG_FOLDERS_FEATURE_NAME, + TAG_FOLDERS_FEATURE_TOOLTIP, +} from '@/ui_models/app_state/features_state'; +import { Tooltip } from '@reach/tooltip'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { useCallback } from 'preact/hooks'; +import { IconButton } from '../IconButton'; +import { PremiumModalProvider, usePremiumModal } from '../Premium'; + +type Props = { + application: WebApplication; + appState: AppState; +}; + +const TagAddButton: FunctionComponent<{ + appState: AppState; + features: FeaturesState; +}> = observer(({ appState, features }) => { + const isNativeFoldersEnabled = features.enableNativeFoldersFeature; + + if (!isNativeFoldersEnabled) { + return null; + } + + return ( + appState.createNewTag()} + /> + ); +}); + +const TagTitle: FunctionComponent<{ + features: FeaturesState; +}> = observer(({ features }) => { + const isNativeFoldersEnabled = features.enableNativeFoldersFeature; + const hasFolders = features.hasFolders; + const modal = usePremiumModal(); + + const showPremiumAlert = useCallback(() => { + modal.activate(TAG_FOLDERS_FEATURE_NAME); + }, [modal]); + + if (!isNativeFoldersEnabled) { + return ( + <> +
+ Tags +
+ + ); + } + + if (hasFolders) { + return ( + <> +
+ Folders +
+ + ); + } + + return ( + <> +
+ Tags + + + +
+ + ); +}); + +export const TagsSection: FunctionComponent = observer( + ({ application, appState }) => { + return ( + +
+
+
+ + +
+
+ +
+
+ ); + } +); + +export const TagsSectionDirective = toDirective(TagsSection); diff --git a/app/assets/javascripts/components/TagsList.tsx b/app/assets/javascripts/components/TagsList.tsx index ef7ebf7b032..66925c2c10f 100644 --- a/app/assets/javascripts/components/TagsList.tsx +++ b/app/assets/javascripts/components/TagsList.tsx @@ -1,3 +1,4 @@ +import { PremiumModalProvider } from '@/components/Premium'; import { confirmDialog } from '@/services/alertService'; import { STRING_DELETE_TAG } from '@/strings'; import { WebApplication } from '@/ui_models/application'; @@ -11,7 +12,8 @@ import { useCallback } from 'preact/hooks'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { TouchBackend } from 'react-dnd-touch-backend'; -import { RootTagDropZone, TagsListItem } from './TagsListItem'; +import { RootTagDropZone } from './RootTagDropZone'; +import { TagsListItem } from './TagsListItem'; import { toDirective } from './utils'; type Props = { @@ -114,7 +116,7 @@ export const TagsList: FunctionComponent = observer( const backend = isMobile({ tablet: true }) ? TouchBackend : HTML5Backend; return ( - <> + {allTags.length === 0 ? (
@@ -136,11 +138,14 @@ export const TagsList: FunctionComponent = observer( /> ); })} - + )} - + ); } ); diff --git a/app/assets/javascripts/components/TagsListItem.tsx b/app/assets/javascripts/components/TagsListItem.tsx index f08e62f93f9..b41ea1653e0 100644 --- a/app/assets/javascripts/components/TagsListItem.tsx +++ b/app/assets/javascripts/components/TagsListItem.tsx @@ -1,5 +1,8 @@ +import { + FeaturesState, + TAG_FOLDERS_FEATURE_NAME, +} from '@/ui_models/app_state/features_state'; import { TagsState } from '@/ui_models/app_state/tags_state'; -import { Tooltip } from '@reach/tooltip'; import '@reach/tooltip/styles.css'; import { SNTag } from '@standardnotes/snjs'; import { computed, runInAction } from 'mobx'; @@ -8,16 +11,17 @@ import { FunctionComponent, JSX } from 'preact'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { useDrag, useDrop } from 'react-dnd'; import { Icon } from './Icon'; +import { usePremiumModal } from './Premium'; -enum ItemTypes { +export enum ItemTypes { TAG = 'TAG', } -type DropItemTag = { uuid: string }; +export type DropItemTag = { uuid: string }; -type DropItem = DropItemTag; +export type DropItem = DropItemTag; -type DropProps = { isOver: boolean; canDrop: boolean }; +export type DropProps = { isOver: boolean; canDrop: boolean }; type Props = { tag: SNTag; @@ -32,40 +36,9 @@ type Props = { export type TagsListState = { readonly selectedTag: SNTag | undefined; editingTag: SNTag | undefined; + features: FeaturesState; }; -export const RootTagDropZone: React.FC<{ tagsState: TagsState }> = observer( - ({ tagsState }) => { - const [{ isOver, canDrop }, dropRef] = useDrop( - () => ({ - accept: ItemTypes.TAG, - canDrop: () => { - return true; - }, - drop: (item) => { - tagsState.assignParent(item.uuid, undefined); - }, - collect: (monitor) => ({ - isOver: !!monitor.isOver(), - canDrop: !!monitor.canDrop(), - }), - }), - [tagsState] - ); - - return ( -
- Move the tag here to remove it from its folder. -
- ); - } -); - export const TagsListItem: FunctionComponent = observer( ({ tag, selectTag, saveTag, removeTag, appState, tagsState, level }) => { const [title, setTitle] = useState(tag.title || ''); @@ -80,6 +53,8 @@ export const TagsListItem: FunctionComponent = observer( const hasChildren = childrenTags.length > 0; const hasFolders = tagsState.hasFolders; + const isNativeFoldersEnabled = appState.features.enableNativeFoldersFeature; + const premiumModal = usePremiumModal(); useEffect(() => { setTitle(tag.title || ''); @@ -148,7 +123,7 @@ export const TagsListItem: FunctionComponent = observer( type: ItemTypes.TAG, item: { uuid: tag.uuid }, canDrag: () => { - return hasFolders; + return isNativeFoldersEnabled; }, collect: (monitor) => ({ isDragging: !!monitor.isDragging(), @@ -164,6 +139,10 @@ export const TagsListItem: FunctionComponent = observer( return tagsState.isValidTagParent(tag.uuid, item.uuid); }, drop: (item) => { + if (!hasFolders) { + premiumModal.activate(TAG_FOLDERS_FEATURE_NAME); + return; + } tagsState.assignParent(item.uuid, tag.uuid); }, collect: (monitor) => ({ @@ -171,7 +150,7 @@ export const TagsListItem: FunctionComponent = observer( canDrop: !!monitor.canDrop(), }), }), - [tag, tagsState] + [tag, tagsState, hasFolders, premiumModal] ); const readyToDrop = isOver && canDrop; @@ -183,18 +162,19 @@ export const TagsListItem: FunctionComponent = observer( readyToDrop ? 'is-drag-over' : '' }`} onClick={selectCurrentTag} - ref={hasFolders ? dragRef : undefined} + ref={dragRef} style={{ paddingLeft: `${level + 0.5}rem` }} > {!tag.errorDecrypting ? (
- {hasFolders && ( + {hasFolders && isNativeFoldersEnabled && (
{hasChildren && ( = observer( )}
)} - {hasFolders ? ( -
- -
- ) : ( - -
- -
-
- )} +
+ +
= const { currentPane } = appState.purchaseFlow; return ( -
+
diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts index b31de83182e..4be81e02866 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -431,6 +431,10 @@ export class AppState { } public async createNewTag() { + if (this.templateTag) { + return; + } + const newTag = (await this.application.createTemplateItem( ContentType.Tag )) as SNTag; diff --git a/app/assets/javascripts/ui_models/app_state/features_state.ts b/app/assets/javascripts/ui_models/app_state/features_state.ts index aebe2f88a40..ca022d8b4d0 100644 --- a/app/assets/javascripts/ui_models/app_state/features_state.ts +++ b/app/assets/javascripts/ui_models/app_state/features_state.ts @@ -6,50 +6,50 @@ import { import { computed, makeObservable, observable, runInAction } from 'mobx'; import { WebApplication } from '../application'; +export const TAG_FOLDERS_FEATURE_NAME = 'Tag folders'; +export const TAG_FOLDERS_FEATURE_TOOLTIP = + 'A Plus or Pro plan is required to enable Tag folders.'; + /** * Holds state for premium/non premium features for the current user features, * and eventually for in-development features (feature flags). */ export class FeaturesState { - readonly hasUnfinishedFeatures: boolean = window?._enable_unfinished_features; + readonly enableUnfinishedFeatures: boolean = + window?._enable_unfinished_features; _hasFolders = false; private unsub: () => void; constructor(private application: WebApplication) { - this._hasFolders = this.hasFoldersFeature(); + this._hasFolders = this.hasNativeFolders(); makeObservable(this, { _hasFolders: observable, hasFolders: computed, - hasUnfinishedFoldersFeature: computed, + enableNativeFoldersFeature: computed, }); - this.unsub = this.application.addEventObserver(async () => { - runInAction(() => { - this._hasFolders = this.hasFoldersFeature(); - }); - }, ApplicationEvent.FeaturesUpdated); + this.unsub = this.application.addEventObserver(async (eventName) => { + switch (eventName) { + case ApplicationEvent.FeaturesUpdated: + case ApplicationEvent.Launched: + runInAction(() => { + this._hasFolders = this.hasNativeFolders(); + }); + break; + default: + break; + } + }); } public deinit() { this.unsub(); } - public get hasUnfinishedFoldersFeature(): boolean { - return this.hasUnfinishedFeatures; - } - - public hasFoldersFeature(): boolean { - if (!this.hasUnfinishedFoldersFeature) { - return false; - } - - const status = this.application.getFeatureStatus( - FeatureIdentifier.TagNesting - ); - - return status === FeatureStatus.Entitled; + public get enableNativeFoldersFeature(): boolean { + return this.enableUnfinishedFeatures; } public get hasFolders(): boolean { @@ -62,9 +62,9 @@ export class FeaturesState { return; } - if (!this.hasFoldersFeature()) { + if (!this.hasNativeFolders()) { this.application.alertService?.alert( - 'Tag Folders requires at least a Plus Subscription.' + `${TAG_FOLDERS_FEATURE_NAME} requires at least a Plus Subscription.` ); this._hasFolders = false; return; @@ -72,4 +72,16 @@ export class FeaturesState { this._hasFolders = hasFolders; } + + private hasNativeFolders(): boolean { + if (!this.enableNativeFoldersFeature) { + return false; + } + + const status = this.application.getFeatureStatus( + FeatureIdentifier.TagNesting + ); + + return status === FeatureStatus.Entitled; + } } diff --git a/app/assets/javascripts/views/application/application-view.pug b/app/assets/javascripts/views/application/application-view.pug index 785ce2df5b5..4783b1a5b7e 100644 --- a/app/assets/javascripts/views/application/application-view.pug +++ b/app/assets/javascripts/views/application/application-view.pug @@ -3,14 +3,14 @@ ) #app.app( ng-class='self.state.appClass', - ng-if='!self.state.needsUnlock && self.state.ready' + ng-if='!self.state.needsUnlock && self.state.launched' ) tags-view(application='self.application') notes-view(application='self.application') editor-group-view.flex-grow(application='self.application') footer-view( - ng-if='!self.state.needsUnlock && self.state.ready' + ng-if='!self.state.needsUnlock && self.state.launched' application='self.application' ) diff --git a/app/assets/javascripts/views/application/application_view.ts b/app/assets/javascripts/views/application/application_view.ts index 91cd5e52125..fa8681ac1d7 100644 --- a/app/assets/javascripts/views/application/application_view.ts +++ b/app/assets/javascripts/views/application/application_view.ts @@ -3,25 +3,29 @@ import { WebDirective } from '@/types'; import { getPlatformString } from '@/utils'; import template from './application-view.pug'; import { AppStateEvent, PanelResizedData } from '@/ui_models/app_state'; -import { ApplicationEvent, Challenge, removeFromArray } from '@standardnotes/snjs'; import { - PANEL_NAME_NOTES, - PANEL_NAME_TAGS -} from '@/views/constants'; -import { - STRING_DEFAULT_FILE_ERROR -} from '@/strings'; + ApplicationEvent, + Challenge, + removeFromArray, +} from '@standardnotes/snjs'; +import { PANEL_NAME_NOTES, PANEL_NAME_TAGS } from '@/views/constants'; +import { STRING_DEFAULT_FILE_ERROR } from '@/strings'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { alertDialog } from '@/services/alertService'; -class ApplicationViewCtrl extends PureViewCtrl { - public platformString: string - private notesCollapsed = false - private tagsCollapsed = false +class ApplicationViewCtrl extends PureViewCtrl< + unknown, + { + started?: boolean; + launched?: boolean; + needsUnlock?: boolean; + appClass: string; + } +> { + public platformString: string; + private notesCollapsed = false; + private tagsCollapsed = false; + /** * To prevent stale state reads (setState is async), * challenges is a mutable array @@ -76,7 +80,7 @@ class ApplicationViewCtrl extends PureViewCtrl { this.challenges.push(challenge); }); - } + }, }); await this.application.launch(); } @@ -90,14 +94,17 @@ class ApplicationViewCtrl extends PureViewCtrl .tag-icon { - width: 10px; - font-size: var(--sn-stylekit-font-size-h2); - font-weight: bold; - margin-right: 6px; + svg { + display: block; + margin: auto; + } &.draggable { cursor: move; diff --git a/package.json b/package.json index 6f7358fdbea..73b501b4001 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "pug-loader": "^2.4.0", "sass-loader": "^12.2.0", "serve-static": "^1.14.1", - "sn-stylekit": "5.2.17", + "sn-stylekit": "5.2.20", "svg-jest": "^1.0.1", "ts-jest": "^27.0.7", "ts-loader": "^9.2.6", @@ -86,7 +86,7 @@ "@standardnotes/features": "1.10.2", "@reach/tooltip": "^0.16.2", "@standardnotes/sncrypto-web": "1.5.3", - "@standardnotes/snjs": "2.23.2", + "@standardnotes/snjs": "2.24.0", "mobx": "^6.3.5", "mobx-react-lite": "^3.2.2", "preact": "^10.5.15", diff --git a/yarn.lock b/yarn.lock index cdf97cbbafd..43cadd87692 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2623,7 +2623,7 @@ dependencies: "@standardnotes/auth" "^3.8.1" -"@standardnotes/features@1.10.2", "@standardnotes/features@^1.10.2": +"@standardnotes/features@1.10.2": version "1.10.2" resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.10.2.tgz#a0783f66c00e21cb7692edc0cea95ec25a0253a5" integrity sha512-Zh6EMjli4mL6jlXEhMyU3qYIKFJj5kuhbxtHXiErUGIDy+s1hHY+THFFO53Jdga2+8wgcATWlmSBY7dieVA8uA== @@ -2631,6 +2631,14 @@ "@standardnotes/auth" "3.8.3" "@standardnotes/common" "^1.2.1" +"@standardnotes/features@^1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.10.3.tgz#f5824342446e69f006ea8ac8916203d1d3992f21" + integrity sha512-PU4KthoDr6NL1bOfKnYV1WXYqRu1/IcdkZkJa2LHcYMPduUjDUKO6qRK73dF0+EEI1U+YXY/9rHyfadGwd0Ymg== + dependencies: + "@standardnotes/auth" "3.8.3" + "@standardnotes/common" "^1.2.1" + "@standardnotes/settings@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.2.1.tgz#4c7656ea86d784a2f77c70acc89face5d28da024" @@ -2650,15 +2658,15 @@ buffer "^6.0.3" libsodium-wrappers "^0.7.9" -"@standardnotes/snjs@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.23.2.tgz#16f76c7e4278be9f315e90b96dabfcee2d6b7961" - integrity sha512-CPLvizemAYRO0XaDOD8pnjrzYSkvcw/UI0V6hlfe9VkJ2jLHotbDSeHdzFotjosX5leGB4UXemjd09EzDNdqpA== +"@standardnotes/snjs@2.24.0": + version "2.24.0" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.24.0.tgz#28dce92d4f0a1108d5edf032a7bea1997d276dfd" + integrity sha512-R2LPLAx5q06BZ3qLF5AMXI62PFlEgj5i1wj5tEWglWxug+c478A6y+L1+ROTz1GIjv+vmn4bJ7M1MBbYnJgQaw== dependencies: "@standardnotes/auth" "^3.8.1" "@standardnotes/common" "^1.2.1" "@standardnotes/domain-events" "^2.5.1" - "@standardnotes/features" "^1.10.2" + "@standardnotes/features" "^1.10.3" "@standardnotes/settings" "^1.2.1" "@standardnotes/sncrypto-common" "^1.5.2" @@ -9170,10 +9178,10 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" -sn-stylekit@5.2.17: - version "5.2.17" - resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.17.tgz#5d8ceefacc044d24f71f99c343bc4e0a9f867e3b" - integrity sha512-YjIstWUjkRYc0zWKxTcLDk8ZKfb9nskQAbsNsihKbJsko1tQbDywrvoQff8TmsE8kXedTd7z/8yUYrYlqgKp6g== +sn-stylekit@5.2.20: + version "5.2.20" + resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.20.tgz#c18f40ff3aaf4c59af89152439a8efbdde35f2dd" + integrity sha512-JymHBiZOzQPfCqHYgnVPSA2PwJqiKR268qqQoEMqI85MMAWSG3WYzuKEbd0LgfIQAKLElCxJjeZkrhejyRg+2A== dependencies: "@reach/listbox" "^0.15.0" "@reach/menu-button" "^0.15.1"