Compare commits

..

No commits in common. "2e15044c120c3c3924b64be7805144ee749d3c3b" and "6d00bffbec4c50641fb2fda8635fa4ba107f66ad" have entirely different histories.

26 changed files with 157 additions and 467 deletions
.gitignoreREADME.mdpackage.json
public
src
main.tsx
modules
editor
components
EmptyViewer
ReactMarkdownEditor
ReactMarkdownViewer
RemirrorEditor
containers/MarkdownEditorContainer
layout/components/GridLayout
settings/containers/ColorSettings
storage
utils
yarn.lock

2
.gitignore vendored
View file

@ -23,5 +23,3 @@ dist-ssr
*.sln
*.sw?
web-ext-artifacts
output
*.tgz

View file

@ -22,4 +22,4 @@ yarn dev
## TO-DO
- Use HyperMD editor
- Use Remirror as editor

View file

@ -1,12 +1,12 @@
{
"name": "markdown-home-tab",
"private": true,
"version": "0.0.5",
"version": "0.0.4",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"package": "yarn build && web-ext build -s ./dist -a ./output --overwrite-dest",
"package": "yarn build && web-ext build -s ./dist",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
@ -21,7 +21,6 @@
"i18next": "^22.4.15",
"i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.2.0",
"lodash.debounce": "^4.0.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.2.2",
@ -39,7 +38,6 @@
"@types/classnames": "^2.3.1",
"@types/color": "^3.0.3",
"@types/firefox-webext-browser": "^120.0.4",
"@types/lodash.debounce": "^4.0.9",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/uuid": "^9.0.1",

View file

@ -1,7 +1,7 @@
{
"name": "Markdown Home Tab",
"short_name": "Markdown New Tab",
"version": "0.0.6",
"version": "0.0.4",
"description": "Markdown right in your home tab! Paste links, pictures, lists and more. You can also customize colors to match your needs.",
"manifest_version": 2,
"permissions": ["storage"],

View file

@ -4,20 +4,16 @@ import { Editor } from "~/pages/editor";
import { ThemeProvider } from "./modules/theme/containers/ThemeProvider";
import { SettingsProvider } from "./modules/settings/providers/SettingsProvider";
import { StorageProvider } from "~/modules/storage/StorageProvider";
import "./i18n";
import "./styles/main.scss";
import "./utils/seed";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<StorageProvider>
<SettingsProvider>
<ThemeProvider>
<Editor />
</ThemeProvider>
</SettingsProvider>
</StorageProvider>
<SettingsProvider>
<ThemeProvider>
<Editor />
</ThemeProvider>
</SettingsProvider>
</React.StrictMode>
);

View file

@ -13,7 +13,7 @@ const EmptyViewer: FC<EmptyViewerProps> = ({ startEditing }) => {
const { t } = useTranslation();
return (
<div className={styles.empty} style={style} onDoubleClick={startEditing}>
<div className={styles.empty} style={style}>
<div className={styles.title}>{t(`Nothing's here yet`)}</div>
<div>
<Button

View file

@ -8,8 +8,6 @@
box-sizing: border-box;
opacity: 0;
transition: opacity 0.25s;
user-select: none;
cursor: pointer;
&:hover {
opacity: 0.5;

View file

@ -1,15 +1,13 @@
import { ChangeEvent, FC, useCallback, useMemo, KeyboardEvent } from "react";
import { ChangeEvent, FC, useCallback, useMemo } from "react";
import styles from "./styles.module.scss";
import { useTheme } from "~/modules/theme/context/ThemeContext";
interface ReactMarkdownEditorProps {
value: string;
onChange: (val: string) => void;
save: VoidFunction;
}
const SimpleTextareaEditor: FC<ReactMarkdownEditorProps> = ({
save,
const ReactMarkdownEditor: FC<ReactMarkdownEditorProps> = ({
value,
onChange,
}) => {
@ -29,22 +27,8 @@ const SimpleTextareaEditor: FC<ReactMarkdownEditorProps> = ({
[paddingHorizontal, paddingVertical]
);
const onKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === "Enter" && event.ctrlKey) {
save();
}
if (event.key === "Escape") {
save();
}
},
[save]
);
return (
<textarea
onKeyDown={onKeyDown}
onChange={changeHandler}
className={styles.textarea}
style={style}
@ -55,4 +39,4 @@ const SimpleTextareaEditor: FC<ReactMarkdownEditorProps> = ({
);
};
export { SimpleTextareaEditor };
export { ReactMarkdownEditor };

View file

@ -1,4 +1,4 @@
import { FC, useCallback, MouseEvent } from "react";
import { FC } from "react";
import ReactMarkdown from "react-markdown";
import { useContainerPaddings } from "~/modules/theme/hooks/useContainerPaddings";
import styles from "./styles.module.scss";
@ -7,26 +7,20 @@ import { useTranslation } from "react-i18next";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
interface Props {
interface ReactMarkdownViewerProps {
value: string;
startEditing: () => void;
}
const MarkdownViewer: FC<Props> = ({ value, startEditing }) => {
const ReactMarkdownViewer: FC<ReactMarkdownViewerProps> = ({
value,
startEditing,
}) => {
const { t } = useTranslation();
const style = useContainerPaddings();
const onDoubleClick = useCallback(
(event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
startEditing();
},
[startEditing]
);
return (
<div style={style} className={styles.editor} onDoubleClick={onDoubleClick}>
<div style={style} className={styles.editor}>
<div className={styles.edit}>
<Button
size="small"
@ -47,4 +41,4 @@ const MarkdownViewer: FC<Props> = ({ value, startEditing }) => {
);
};
export { MarkdownViewer };
export { ReactMarkdownViewer };

View file

@ -1,16 +1,37 @@
import { FC, useCallback } from "react";
import {
EditorComponent,
FloatingToolbar,
FormattingButtonGroup,
HeadingLevelButtonGroup,
Remirror,
useRemirror,
EditorComponent,
HeadingLevelButtonGroup,
FormattingButtonGroup,
} from "@remirror/react";
import { Extension, RemirrorEventListener } from "remirror";
import { useContainerPaddings } from "~/modules/theme/hooks/useContainerPaddings";
import jsx from "refractor/lang/jsx.js";
import typescript from "refractor/lang/typescript.js";
import { Extension, ExtensionPriority, RemirrorEventListener } from "remirror";
import {
BlockquoteExtension,
UnderlineExtension,
BoldExtension,
BulletListExtension,
CodeBlockExtension,
CodeExtension,
HardBreakExtension,
HeadingExtension,
ItalicExtension,
LinkExtension,
ListItemExtension,
MarkdownExtension,
OrderedListExtension,
StrikeExtension,
TableExtension,
TrailingNodeExtension,
GapCursorExtension,
} from "remirror/extensions";
import styles from "./styles.module.scss";
import { useContainerPaddings } from "~/modules/theme/hooks/useContainerPaddings";
interface RemirrorEditorProps {
locked: boolean;
@ -29,7 +50,7 @@ const RemirrorEditor: FC<RemirrorEditorProps> = ({
exitMarksOnArrowPress: false,
},
content: value,
// stringHandler: "markdown",
stringHandler: "markdown",
});
const onStateChange = useCallback<RemirrorEventListener<Extension>>(
@ -69,26 +90,26 @@ const RemirrorEditor: FC<RemirrorEditorProps> = ({
};
const extensions = (): Extension[] => [
// new LinkExtension({ autoLink: true }),
// new BoldExtension(),
// new UnderlineExtension(),
// new StrikeExtension(),
// new ItalicExtension(),
// new HeadingExtension(),
// new BlockquoteExtension(),
// new BulletListExtension({ enableSpine: false }),
// new OrderedListExtension(),
// new ListItemExtension({
// priority: ExtensionPriority.High,
// // enableCollapsible: true,
// }),
// new CodeExtension(),
// new CodeBlockExtension({ supportedLanguages: [jsx, typescript] }),
// new TrailingNodeExtension(),
// new TableExtension(),
// new MarkdownExtension({ copyAsMarkdown: true }),
// new GapCursorExtension(),
// new HardBreakExtension(),
new LinkExtension({ autoLink: true }),
new BoldExtension(),
new UnderlineExtension(),
new StrikeExtension(),
new ItalicExtension(),
new HeadingExtension(),
new BlockquoteExtension(),
new BulletListExtension({ enableSpine: false }),
new OrderedListExtension(),
new ListItemExtension({
priority: ExtensionPriority.High,
// enableCollapsible: true,
}),
new CodeExtension(),
new CodeBlockExtension({ supportedLanguages: [jsx, typescript] }),
new TrailingNodeExtension(),
new TableExtension(),
new MarkdownExtension({ copyAsMarkdown: true }),
new GapCursorExtension(),
new HardBreakExtension(),
];
export { RemirrorEditor };

View file

@ -0,0 +1,26 @@
import { useEffect, useMemo, useState } from "react";
import { BrowserSyncStorage } from '~/utils/index';
export const usePersistedValue = (
id: string,
prefix: string
) => {
const [hydrated, setHydrated] = useState(false);
const storage = useMemo(() => new BrowserSyncStorage(prefix), [prefix]);
const key = `${prefix}${id}`;
const [value, setValue] = useState('');
useEffect(() => {
storage.get<string>(key).then(val => setValue(val ?? '')).finally(() => setHydrated(true));
}, [key, storage]);
useEffect(() => {
if (!hydrated) {
return;
}
storage.set(key, value);
}, [key, value, storage, hydrated]);
return { value, setValue, hydrated };
};

View file

@ -1,52 +1,60 @@
import { FC, Suspense, useCallback } from "react";
import { EditorWrapper } from "../../components/EditorWrapper";
import { EmptyViewer } from "../../components/EmptyViewer";
import { MarkdownViewer } from "../../components/MarkdownViewer";
import { SimpleTextareaEditor } from "../../components/SimpleTextareaEditor";
import { FC, Suspense, lazy } from "react";
import { ReactMarkdownEditor } from "../../components/ReactMarkdownEditor";
import { ReactMarkdownViewer } from "../../components/ReactMarkdownViewer";
import { usePersistedValue } from "./hooks/usePersistedValue";
import styles from "./styles.module.scss";
import { useStorage } from "../../../../modules/storage/StorageContext";
import { useSettings } from "~/modules/settings/context/SettingsContext";
import { EmptyViewer } from "../../components/EmptyViewer";
import { EditorWrapper } from "../../components/EditorWrapper";
interface MarkdownEditorContainerProps {
id: string;
locked: boolean;
startEditing: VoidCallback;
remove: VoidCallback;
startEditing: () => void;
remove: () => void;
}
const RichEditor = lazy(() =>
import("../../components/RemirrorEditor").then((module) => ({
default: module.RemirrorEditor,
}))
);
export const MarkdownEditorContainer: FC<MarkdownEditorContainerProps> = ({
id,
locked,
startEditing,
remove,
}) => {
const { panels, setPanel, hydrated } = useStorage();
const {
settings: { richEditorEnabled },
} = useSettings();
const value = panels[id] ?? "";
const empty = !value.trim();
const onChange = useCallback(
(val: string) => setPanel(id, val),
[id, setPanel]
const { value, setValue, hydrated } = usePersistedValue(
id,
"MarkdownEditorContainer"
);
const empty = !value.trim();
const viewer = empty ? (
<EmptyViewer startEditing={startEditing} />
) : (
<MarkdownViewer value={value} startEditing={startEditing} />
<ReactMarkdownViewer value={value} startEditing={startEditing} />
);
const editor = (
<EditorWrapper save={startEditing} remove={remove}>
<SimpleTextareaEditor
value={value}
onChange={onChange}
save={startEditing}
/>
{richEditorEnabled ? (
<RichEditor value={value} onChange={setValue} locked={locked} />
) : (
<ReactMarkdownEditor value={value} onChange={setValue} />
)}
</EditorWrapper>
);
return (
<div className={styles.editor} id={id}>
<div className={styles.editor}>
{hydrated && <Suspense>{locked ? viewer : editor}</Suspense>}
</div>
);

View file

@ -1,27 +1,30 @@
import { DockviewApi, DockviewReadyEvent } from "dockview";
import { DockviewApi, DockviewReadyEvent, SerializedDockview } from "dockview";
import { useCallback, useEffect, useRef, useState } from "react";
import { useStorage } from "../../../../../modules/storage/StorageContext";
import { createDefaultLayout } from "../utils/createDefaultLayout";
import { BrowserSyncStorage } from "~/utils";
const storage = new BrowserSyncStorage();
const key = 'dockview_persistance_layout';
export const useGridLayoutPersistance = () => {
const api = useRef<DockviewApi>();
const [hydrated, setHydrated] = useState(false);
const { layout, setLayout } = useStorage();
const onReady = (event: DockviewReadyEvent) => {
if (hydrated) {
return;
}
api.current = event.api;
if (!layout) {
createDefaultLayout(event.api);
return;
}
storage.get<SerializedDockview>(key).then(layout => {
if (!layout) {
throw new Error("No layout saved, its okay");
}
event.api.fromJSON(layout);
setHydrated(true);
event.api.fromJSON(layout);
}).catch(() => {
createDefaultLayout(event.api);
}).finally(() => {
setHydrated(true);
});
};
const persistLayout = useCallback(() => {
@ -29,8 +32,8 @@ export const useGridLayoutPersistance = () => {
return;
}
setLayout(api.current.toJSON());
}, [setLayout]);
storage.set(key, api.current.toJSON());
}, []);
useEffect(() => {
const onLayoutChange = api.current?.onDidLayoutChange(() => {

View file

@ -44,15 +44,6 @@ const DefaultLayout = ({
...panelProps.params,
locked: !locked,
});
if (panelProps.params.locked) {
setTimeout(() => {
document
.getElementById(panelProps.api.id)
?.querySelector("textarea")
?.focus();
}, 0);
}
}, [locked, panelProps.api, panelProps.params]);
useEffect(() => {

View file

@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { RowGroup } from "~/components/containers/RowGroup";
import { SettingsRow } from "~/components/containers/SettingsRow";
import {
ColorSettings as ColorSettingsValue,
ColorSettings,
SettingsValue,
useSettings,
} from "~/modules/settings/context/SettingsContext";
@ -37,7 +37,7 @@ const ColorSettings: FC = () => {
);
const setThemeColors = useCallback(
(val: ColorSettingsValue) => {
(val: ColorSettings) => {
update(fillThemeHeadings(val));
},
[update]

View file

@ -1,13 +0,0 @@
import { createContext, useContext } from "react";
import { SerializedDockview } from "dockview";
import { noop } from "~/utils/noop";
export const StorageContext = createContext({
layout: null as SerializedDockview | null,
panels: {} as Record<string, string>,
hydrated: false,
setPanel: noop as (uuid: string, content: string) => void,
setLayout: noop as (layout: SerializedDockview) => void,
});
export const useStorage = () => useContext(StorageContext);

View file

@ -1,67 +0,0 @@
import { SerializedDockview } from "dockview";
import { ReactNode, useCallback, useEffect, useState } from "react";
import {
hydrateLayout,
storeLayoutLocally,
storePanelLocally,
} from "~/utils/hydrate";
import { useDelayedSync } from "./hooks/useDelayedSync";
import { StorageContext } from "./StorageContext";
const debounceDelay = 500;
export const StorageProvider = ({ children }: { children: ReactNode }) => {
const [hydrated, setHydrated] = useState(false);
const [layout, setLayoutValue] = useState<SerializedDockview | null>(null);
const [panels, setPanelsValue] = useState<Record<string, string>>({});
const { storeLayout, storePanel } = useDelayedSync(debounceDelay);
const setPanel = useCallback(
(uuid: string, value: string) => {
setPanelsValue((prev) => ({ ...prev, [uuid]: value }));
storePanelLocally(uuid, value);
storePanel(uuid, value);
},
[storePanel]
);
const setLayout = useCallback(
(value: SerializedDockview) => {
setLayoutValue(value);
storeLayoutLocally(value);
storeLayout(value);
},
[storeLayout]
);
useEffect(() => {
if (hydrated) {
return;
}
hydrateLayout()
.then((result) => {
if (!result) {
return;
}
setLayout(result.layout);
Object.entries(result.panels).forEach(([uuid, value]) => {
setPanel(uuid, value);
});
})
.finally(() => {
setHydrated(true);
});
}, [hydrated, setLayout, setPanel]);
return (
<StorageContext.Provider
value={{ hydrated, layout, panels, setLayout, setPanel }}
>
{hydrated ? children : null}
</StorageContext.Provider>
);
};

View file

@ -1,38 +0,0 @@
import { useCallback, useRef } from "react";
import { storeLayoutInSync, storePanelInSync } from "~/utils/hydrate";
import { DebouncedFunc } from "lodash";
import { SerializedDockview } from "dockview";
import debounce from "lodash.debounce";
export const useDelayedSync = (debounceDelay: number) => {
const layoutTimer = useRef<DebouncedFunc<typeof storeLayoutInSync>>();
const panelTimers = useRef<
Record<string, DebouncedFunc<typeof storePanelInSync>>
>({});
const storeLayout = useCallback(
(layout: SerializedDockview) => {
if (layoutTimer.current) {
layoutTimer.current.cancel();
}
layoutTimer.current = debounce(storeLayoutInSync, debounceDelay);
layoutTimer.current(layout);
},
[debounceDelay]
);
const storePanel = useCallback(
(uuid: string, value: string) => {
if (panelTimers.current[uuid]) {
panelTimers.current[uuid].cancel();
}
panelTimers.current[uuid] = debounce(storePanelInSync, debounceDelay);
panelTimers.current[uuid](uuid, value);
},
[debounceDelay]
);
return { storeLayout, storePanel };
};

View file

@ -1,127 +0,0 @@
import { SerializedDockview } from "dockview";
import { hasBrowserStorage, hasChromeStorage } from "~/utils/storage";
interface Result {
layout: SerializedDockview;
panels: Record<string, string>;
}
const layoutKey = "dockview_persistance_layout";
const panelPrefix = "MarkdownEditorContainerMarkdownEditorContainer";
const makePanelKey = (uuid: string) => `${panelPrefix}${uuid}`;
const getFromBrowserStorage = async (): Promise<Result | null> => {
const result = await browser.storage.sync.get();
const layout = result[layoutKey] as SerializedDockview | undefined;
if (!layout) {
return null;
}
const panels = Object.keys(layout.panels).reduce(
(acc, uuid) => ({
...acc,
[uuid]: (result[makePanelKey(uuid)] as string) ?? "",
}),
{} as Record<string, string>
);
return {
layout,
panels,
};
};
const getFromChromeStorage = async (): Promise<Result | null> => {
const result = await chrome.storage.sync.get();
const layout = result[layoutKey] as SerializedDockview | undefined;
if (!layout) {
return null;
}
const panels = Object.keys(layout.panels).reduce(
(acc, uuid) => ({
...acc,
[uuid]: (result[makePanelKey(uuid)] as string) ?? "",
}),
{} as Record<string, string>
);
return {
layout,
panels,
};
};
const getFromLocalStorage = () => {
const rawLayout = localStorage.getItem(layoutKey);
if (!rawLayout) {
return null;
}
const layout = JSON.parse(rawLayout) as SerializedDockview;
if (!layout.panels) {
return null;
}
const panels = Object.keys(layout.panels).reduce(
(acc, uuid) => ({
...acc,
[uuid]: localStorage.getItem(makePanelKey(uuid)) ?? "",
}),
{} as Record<string, string>
);
return {
layout,
panels,
};
};
export const hydrateLayout = async (): Promise<Result | null> => {
const local = getFromLocalStorage();
if (local) {
return local;
}
if (hasBrowserStorage()) {
return getFromBrowserStorage();
}
if (hasChromeStorage()) {
return getFromChromeStorage();
}
return null;
};
export const storeLayoutLocally = (layout: SerializedDockview) =>
localStorage.setItem(layoutKey, JSON.stringify(layout));
export const storeLayoutInSync = (layout: SerializedDockview) => {
if (hasBrowserStorage()) {
return browser.storage.sync.set({ [layoutKey]: layout });
}
if (hasChromeStorage()) {
return chrome.storage.sync.set({ [layoutKey]: layout });
}
};
export const storePanelLocally = (uuid: string, value: string) =>
localStorage.setItem(`${panelPrefix}${uuid}`, value);
export const storePanelInSync = (uuid: string, value: string) => {
if (hasBrowserStorage()) {
return browser.storage.sync.set({ [`${panelPrefix}${uuid}`]: value });
}
if (hasChromeStorage()) {
return chrome.storage.sync.set({ [`${panelPrefix}${uuid}`]: value });
}
};

View file

@ -1,13 +1,11 @@
import { hasBrowserStorage, hasChromeStorage } from "./storage";
export class BrowserSyncStorage {
constructor(private globalPrefix = "") {}
get engine() {
if (hasBrowserStorage()) {
return "browser";
} else if (hasChromeStorage()) {
return "chrome";
if (typeof browser !== 'undefined' && browser?.storage) {
return "browser"
} else if (typeof chrome !== 'undefined' && chrome?.storage) {
return "chrome"
}
return "local";
@ -17,20 +15,20 @@ export class BrowserSyncStorage {
set = async <T>(key: string, value: T) => {
switch (this.engine) {
case "browser":
case 'browser':
await browser.storage.sync.set({ [this.makeKey(key)]: value });
return;
case "chrome":
case 'chrome':
await chrome.storage.sync.set({ [this.makeKey(key)]: value });
return;
default:
localStorage.setItem(this.makeKey(key), JSON.stringify(value));
return;
return
}
};
get = async <T>(key: string): Promise<T | undefined> => {
if (this.engine === "browser") {
if (this.engine === 'browser') {
const value = await browser.storage.sync
.get([this.makeKey(key)])
.then((result) => result[this.makeKey(key)] as T | undefined);
@ -38,10 +36,10 @@ export class BrowserSyncStorage {
if (value) {
return value;
}
} else if (this.engine === "chrome") {
const value = await chrome.storage.sync
.get(this.makeKey(key))
.then((result) => result[this.makeKey(key)] as T | undefined);
} else if (this.engine === 'chrome') {
const value = await chrome.storage.sync.get(this.makeKey(key)).then(
(result) => result[this.makeKey(key)] as T | undefined
);
if (value) {
return value;

View file

@ -1 +0,0 @@
export const noop = () => {};

View file

@ -1,57 +0,0 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.seedBrowserData = async () => {
await browser.storage.sync.clear();
await browser.storage.sync.set({
"MarkdownEditorContainerMarkdownEditorContainerd87f6e7b-d21e-462e-9f15-a61da9178282":
"d123",
"MarkdownEditorContainerMarkdownEditorContainer8ea3f663-9c16-4d53-b0cc-25f47adae2b2":
"d456",
dockview_persistance_layout: {
grid: {
root: {
type: "branch",
data: [
{
type: "leaf",
data: {
views: ["d87f6e7b-d21e-462e-9f15-a61da9178282"],
activeView: "d87f6e7b-d21e-462e-9f15-a61da9178282",
id: "1",
hideHeader: true,
},
size: 960,
},
{
type: "leaf",
data: {
views: ["8ea3f663-9c16-4d53-b0cc-25f47adae2b2"],
activeView: "8ea3f663-9c16-4d53-b0cc-25f47adae2b2",
id: "2",
hideHeader: true,
},
size: 960,
},
],
size: 437,
},
width: 1920,
height: 437,
orientation: "HORIZONTAL",
},
panels: {
"d87f6e7b-d21e-462e-9f15-a61da9178282": {
id: "d87f6e7b-d21e-462e-9f15-a61da9178282",
contentComponent: "default",
params: { title: "", locked: true },
},
"8ea3f663-9c16-4d53-b0cc-25f47adae2b2": {
id: "8ea3f663-9c16-4d53-b0cc-25f47adae2b2",
contentComponent: "default",
params: { title: "", locked: true },
},
},
activeGroup: "2",
},
});
};

View file

@ -1,5 +0,0 @@
export const hasBrowserStorage = () =>
typeof browser !== "undefined" && browser?.storage;
export const hasChromeStorage = () =>
typeof chrome !== "undefined" && chrome?.storage;

View file

@ -1961,18 +1961,6 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
"@types/lodash.debounce@^4.0.9":
version "4.0.9"
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz#0f5f21c507bce7521b5e30e7a24440975ac860a5"
integrity sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.17.10"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.10.tgz#64f3edf656af2fe59e7278b73d3e62404144a6e6"
integrity sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==
"@types/marked@^4.0.2":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.8.tgz#b316887ab3499d0a8f4c70b7bd8508f92d477955"
@ -4977,11 +4965,6 @@ lodash-es@^4.17.15, lodash-es@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"