From fafc9837f85d084785e6a5ce19cb2cd2fffc4b40 Mon Sep 17 00:00:00 2001 From: Denperidge Date: Mon, 17 Aug 2026 13:23:04 +0200 Subject: [PATCH 1/5] client: KeybindingsMap is now generated from a simpler, less boilerplate-heavy format All keybindings have been tested manually since conversion --- client/js/shortcuts.ts | 261 +++++++++++++++++++++++------------------ 1 file changed, 147 insertions(+), 114 deletions(-) diff --git a/client/js/shortcuts.ts b/client/js/shortcuts.ts index 92c33145b..994ead533 100644 --- a/client/js/shortcuts.ts +++ b/client/js/shortcuts.ts @@ -1,4 +1,4 @@ -import { tinykeys } from 'tinykeys'; +import { KeybindingsMap, tinykeys } from 'tinykeys'; import selfoss from './selfoss-base'; import { Direction } from './helpers/navigation'; @@ -20,147 +20,180 @@ function ignoreWhenInteracting( }; } +interface IKeybinding { + readableName?: string; + description: string; + action: KeyboardEventHandler; +} + /** - * Set up shortcuts on document. + * A selfoss-side definition of all keybindings + * This is used to: + * - Limit boilerplate + * - Generate a tinykeys compatible KeybindingsMap (@see makeKeybindingsMap) */ -export default function makeShortcuts(): () => void { - return tinykeys(window, { - // 'space': next article - Space: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); +export const KEYBINDINGS: { [keycombo: string]: IKeybinding } = { + Space: { + description: 'next article', + action: () => { selfoss.entriesPage?.jumpToNext(); - }), - - // 'n': next article - n: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + n: { + description: 'next article', + action: () => { selfoss.entriesPage?.nextPrev(Direction.NEXT, false); - }), - - // 'right cursor': next article - ArrowRight: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + Arrowright: { + readableName: 'right cursor', + description: 'next article', + action: () => { selfoss.entriesPage?.entryNav(Direction.NEXT); - }), - - // 'j': next article - j: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + j: { + description: 'next article', + action: () => { selfoss.entriesPage?.nextPrev(Direction.NEXT, true); - }), - - // 'shift+space': previous article - 'Shift+Space': ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + 'Shift+Space': { + description: 'previous article', + action: () => { selfoss.entriesPage?.nextPrev(Direction.PREV, true); - }), - - // 'p': previous article - p: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + p: { + description: 'previous article', + action: () => { selfoss.entriesPage?.nextPrev(Direction.PREV, false); - }), - - // 'left': previous article - ArrowLeft: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + ArrowLeft: { + readableName: 'left', + description: + 'select previous entry (and open it when the current is open)', + action: () => { selfoss.entriesPage?.entryNav(Direction.PREV); - }), - - // 'k': previous article - k: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + k: { + description: 'select and open previous entry', + action: () => { selfoss.entriesPage?.nextPrev(Direction.PREV, true); - }), - - // 's': star/unstar - s: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + s: { + description: 'star/unstar', + action: () => { selfoss.entriesPage?.toggleSelectedStarred(); - }), - - // 'm': mark/unmark - m: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + m: { + description: 'mark/unmark', + action: () => { selfoss.entriesPage?.toggleSelectedRead(); - }), - - // 'o': open/close entry - o: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + o: { + description: 'open/close entry', + action: () => { selfoss.entriesPage?.toggleSelectedExpanded(); - }), - - // 'Shift + o': close open entries - 'Shift+o': ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + 'Shift+o': { + description: 'close open entries', + action: () => { selfoss.entriesPage?.collapseAllEntries(); - }), - - // 'v': open target - v: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + v: { + description: 'open target', + action: () => { selfoss.entriesPage?.openSelectedTarget(); - }), - - // 'Shift + v': open target and mark read - 'Shift+v': ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + 'Shift+v': { + description: 'open target and mark read', + action: () => { selfoss.entriesPage?.openSelectedTargetAndMarkRead(); - }), - - // 'r': Reload the current view - r: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + r: { + description: 'Reload the current view', + action: () => { selfoss.entriesPage?.reload(); - }), - - // 'Shift + r': Refresh sources - 'Shift+r': ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + 'Shift+r': { + description: 'Refresh sources', + action: () => { document.querySelector('#nav-refresh').click(); - }), - - // 'Control+m': mark all as read - 'Control+m': ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + 'Control+m': { + description: 'mark all as read', + action: () => { document.querySelector('#nav-mark').click(); - }), - - // 't': throw (mark as read & open next) - t: ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + t: { + description: 'throw (mark as read & open next)', + action: () => { selfoss.entriesPage?.throw(Direction.NEXT); - }), - - // throw (mark as read & open previous) - 'Shift+t': ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + 'Shift+t': { + description: 'throw (mark as read & open previous)', + action: () => { selfoss.entriesPage?.throw(Direction.PREV); - }), - - // 'Shift+n': switch to newest items overview / menu item - 'Shift+n': ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + 'Shift+n': { + description: 'switch to newest items overview / menu item', + action: () => { document .querySelector('#nav-filter-newest') .click(); - }), - - // 'Shift+u': switch to unread items overview / menu item - 'Shift+u': ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + 'Shift+u': { + description: 'switch to unread items overview / menu item', + action: () => { document .querySelector('#nav-filter-unread') .click(); - }), - - // 'Shift+s': switch to starred items overview / menu item - 'Shift+s': ignoreWhenInteracting((event: KeyboardEvent): void => { - event.preventDefault(); + }, + }, + 'Shift+s': { + description: 'switch to starred items overview / menu item', + action: () => { document .querySelector('#nav-filter-starred') .click(); - }), - }); + }, + }, +}; + +function makeKeybindingsMap(): KeybindingsMap { + const shortcuts = Object.entries(KEYBINDINGS); + const keybindingsMap: KeybindingsMap = {}; + + for (const [keycombo, keybind] of shortcuts) { + keybindingsMap[keycombo] = ignoreWhenInteracting( + (event: KeyboardEvent) => { + event.preventDefault(); + keybind.action(event); + }, + ); + } + return keybindingsMap; +} + +/** + * Set up shortcuts on document. + */ +export default function makeShortcuts(): () => void { + return tinykeys(window, makeKeybindingsMap()); } From 3ce3f2a7f3349c25fd9fb9108e991f8082ae9f79 Mon Sep 17 00:00:00 2001 From: Denperidge Date: Mon, 17 Aug 2026 13:27:47 +0200 Subject: [PATCH 2/5] client: moved shortcut ctrl+m next to m --- client/js/shortcuts.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/js/shortcuts.ts b/client/js/shortcuts.ts index 994ead533..9df03b4a0 100644 --- a/client/js/shortcuts.ts +++ b/client/js/shortcuts.ts @@ -96,6 +96,12 @@ export const KEYBINDINGS: { [keycombo: string]: IKeybinding } = { selfoss.entriesPage?.toggleSelectedRead(); }, }, + 'Control+m': { + description: 'mark all as read', + action: () => { + document.querySelector('#nav-mark').click(); + }, + }, o: { description: 'open/close entry', action: () => { @@ -132,12 +138,6 @@ export const KEYBINDINGS: { [keycombo: string]: IKeybinding } = { document.querySelector('#nav-refresh').click(); }, }, - 'Control+m': { - description: 'mark all as read', - action: () => { - document.querySelector('#nav-mark').click(); - }, - }, t: { description: 'throw (mark as read & open next)', action: () => { From bc70908e161c2648e82fe87cbb1dde471ffe85a9 Mon Sep 17 00:00:00 2001 From: Denperidge Date: Mon, 17 Aug 2026 13:31:04 +0200 Subject: [PATCH 3/5] client: adapted shortcuts.ts names/descriptions to their equivalents from the docs - Adapted arrow left & arrow right to the symbol used in docs - Exception: "item" is replaced by "entry" for consistency - Exception: Renamed throws to throw to previous / throw to next Link to docs as of development of this commit: https://github.com/fossar/selfoss/blob/81187a39a1b1db0e2d48fb0fccad949ca771a635/docs/content/docs/usage/shortcuts.md --- client/js/shortcuts.ts | 45 ++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/client/js/shortcuts.ts b/client/js/shortcuts.ts index 9df03b4a0..08194d892 100644 --- a/client/js/shortcuts.ts +++ b/client/js/shortcuts.ts @@ -34,44 +34,44 @@ interface IKeybinding { */ export const KEYBINDINGS: { [keycombo: string]: IKeybinding } = { Space: { - description: 'next article', + description: 'select and open next entry', action: () => { selfoss.entriesPage?.jumpToNext(); }, }, n: { - description: 'next article', + description: 'select next entry', action: () => { selfoss.entriesPage?.nextPrev(Direction.NEXT, false); }, }, Arrowright: { - readableName: 'right cursor', - description: 'next article', + readableName: '→', + description: 'select next entry (and open it when the current is open)', action: () => { selfoss.entriesPage?.entryNav(Direction.NEXT); }, }, j: { - description: 'next article', + description: 'select and open next entry', action: () => { selfoss.entriesPage?.nextPrev(Direction.NEXT, true); }, }, 'Shift+Space': { - description: 'previous article', + description: 'select and open previous entry', action: () => { selfoss.entriesPage?.nextPrev(Direction.PREV, true); }, }, p: { - description: 'previous article', + description: 'select previous entry', action: () => { selfoss.entriesPage?.nextPrev(Direction.PREV, false); }, }, ArrowLeft: { - readableName: 'left', + readableName: '←', description: 'select previous entry (and open it when the current is open)', action: () => { @@ -85,13 +85,14 @@ export const KEYBINDINGS: { [keycombo: string]: IKeybinding } = { }, }, s: { - description: 'star/unstar', + description: + 'mark and unmark current selected entry as starred/unstarred', action: () => { selfoss.entriesPage?.toggleSelectedStarred(); }, }, m: { - description: 'mark/unmark', + description: 'mark and unmark current selected entry as read/unread', action: () => { selfoss.entriesPage?.toggleSelectedRead(); }, @@ -103,55 +104,57 @@ export const KEYBINDINGS: { [keycombo: string]: IKeybinding } = { }, }, o: { - description: 'open/close entry', + description: 'open / close current entry', action: () => { selfoss.entriesPage?.toggleSelectedExpanded(); }, }, 'Shift+o': { - description: 'close open entries', + description: 'close all open entries', action: () => { selfoss.entriesPage?.collapseAllEntries(); }, }, v: { - description: 'open target', + description: 'open url of current entry in new tab/window', action: () => { selfoss.entriesPage?.openSelectedTarget(); }, }, 'Shift+v': { - description: 'open target and mark read', + description: + 'open url of current entry in new tab/window and mark read', action: () => { selfoss.entriesPage?.openSelectedTargetAndMarkRead(); }, }, r: { - description: 'Reload the current view', + description: 'reload the list', action: () => { selfoss.entriesPage?.reload(); }, }, 'Shift+r': { - description: 'Refresh sources', + description: 'refresh sources', action: () => { document.querySelector('#nav-refresh').click(); }, }, t: { - description: 'throw (mark as read & open next)', + description: 'throw current entry to next (mark as read & open next)', action: () => { selfoss.entriesPage?.throw(Direction.NEXT); }, }, 'Shift+t': { - description: 'throw (mark as read & open previous)', + description: + 'throw current entry to previous (mark as read & open previous)', action: () => { selfoss.entriesPage?.throw(Direction.PREV); }, }, 'Shift+n': { - description: 'switch to newest items overview / menu item', + description: 'open newest entries page', action: () => { document .querySelector('#nav-filter-newest') @@ -159,7 +162,7 @@ export const KEYBINDINGS: { [keycombo: string]: IKeybinding } = { }, }, 'Shift+u': { - description: 'switch to unread items overview / menu item', + description: 'open unread entries page', action: () => { document .querySelector('#nav-filter-unread') @@ -167,7 +170,7 @@ export const KEYBINDINGS: { [keycombo: string]: IKeybinding } = { }, }, 'Shift+s': { - description: 'switch to starred items overview / menu item', + description: 'open starred entries page', action: () => { document .querySelector('#nav-filter-starred') From 3ba293369eab7388d395ae3723db3428d2d9bed8 Mon Sep 17 00:00:00 2001 From: Denperidge Date: Sun, 23 Aug 2026 18:31:34 +0200 Subject: [PATCH 4/5] client: Dialog component Thanks to argyleink https://web.dev/articles/building/a-dialog-component#adding_light_dismiss --- client/js/templates/Dialog.tsx | 37 ++++++++++++++++++++++++++++++++++ client/locale/cs.json | 1 + client/locale/en-GB.json | 1 + client/locale/en.json | 1 + client/styles/main.scss | 14 +++++++++++++ 5 files changed, 54 insertions(+) create mode 100644 client/js/templates/Dialog.tsx diff --git a/client/js/templates/Dialog.tsx b/client/js/templates/Dialog.tsx new file mode 100644 index 000000000..3bb74facb --- /dev/null +++ b/client/js/templates/Dialog.tsx @@ -0,0 +1,37 @@ +import React, { ReactNode, RefObject, use } from 'react'; +import { LocalizationContext } from '../helpers/i18n'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import * as icons from '../icons'; + +type DialogProps = { + ref: RefObject; + children: ReactNode; +}; + +export default function Dialog(props: DialogProps): React.JSX.Element { + const { ref, children } = props; + const _ = use(LocalizationContext); + + return ( + { + if (target === currentTarget) { + ref.current.close(); + } + }} + > + + + {children} + + ); +} diff --git a/client/locale/cs.json b/client/locale/cs.json index 29935d37b..143c5d099 100644 --- a/client/locale/cs.json +++ b/client/locale/cs.json @@ -112,6 +112,7 @@ "info_url_copied": "URL zkopírována do schránky.", "error_share_native_abort": "Nelze sdílet položku: buď váš prohlížeč nepodporuje sdílení odkazů, nebo jste sdílení zrušili.", "error_share_native": "Nelze sdílet položku.", + "close_dialog": "Zavřít", "close_entry": "Zavřít", "article_reading_time": "{0} min čtení", "error_offline_storage_not_available": "Offline úložiště není dostupné. Ujistěte se, že váš prohlížeč {0}podporuje rozhraní IndexedDB{1} a, pokud používáte prohlížeč založený na Google Chrome, že nespouštíte selfoss v režimu anonymního prohlížení.", diff --git a/client/locale/en-GB.json b/client/locale/en-GB.json index 354805a9f..9658dbcd2 100644 --- a/client/locale/en-GB.json +++ b/client/locale/en-GB.json @@ -13,6 +13,7 @@ "share_diaspora_label": "Share on Diaspora", "share_native_label": "Share", "info_url_copied": "URL copied to clipboard.", + "close_dialog": "Close", "error_configuration": "Unable to load configuration.", "article_actions": "Actions", "search_label": "Search term", diff --git a/client/locale/en.json b/client/locale/en.json index 68ba2600b..d26bb54eb 100644 --- a/client/locale/en.json +++ b/client/locale/en.json @@ -115,6 +115,7 @@ "error_share_native_abort": "Unable to share item—either there are no share targets, or you cancelled it.", "error_share_native": "Unable to share item.", "info_url_copied": "URL copied to clipboard.", + "close_dialog": "Close", "close_entry": "Close", "article_actions": "Actions", "article_reading_time": "{0} min read" diff --git a/client/styles/main.scss b/client/styles/main.scss index 25f2e1a2a..f4450dd64 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -75,6 +75,20 @@ button::-moz-focus-inner { border: 0; } +html:has(dialog[open]) { + overflow: hidden; +} + +dialog { + position: relative; + + .close { + position: absolute; + right: 10; + top: 10; + } +} + @import 'popup-menu'; .visually-hidden { From 7459407e6a0ba504986d9c8fe197b26a8a51a9fb Mon Sep 17 00:00:00 2001 From: Denperidge Date: Tue, 18 Aug 2026 22:05:14 +0200 Subject: [PATCH 5/5] client: created shortcuts help dialog Thanks to Jan for getting it to display using ! And dealing with a bunch of my questions. Co-authored-by: Jan Tojnar --- client/js/shortcuts.ts | 46 +++++++++++++++++++++------ client/js/templates/App.tsx | 9 +++++- client/js/templates/HelpShortcuts.tsx | 32 +++++++++++++++++++ client/styles/_help.scss | 26 +++++++++++++++ client/styles/main.scss | 1 + 5 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 client/js/templates/HelpShortcuts.tsx create mode 100644 client/styles/_help.scss diff --git a/client/js/shortcuts.ts b/client/js/shortcuts.ts index 08194d892..8823ea934 100644 --- a/client/js/shortcuts.ts +++ b/client/js/shortcuts.ts @@ -1,6 +1,7 @@ import { KeybindingsMap, tinykeys } from 'tinykeys'; import selfoss from './selfoss-base'; import { Direction } from './helpers/navigation'; +import { RefObject } from 'react'; type KeyboardEventHandler = (event: KeyboardEvent) => void; @@ -10,10 +11,13 @@ type KeyboardEventHandler = (event: KeyboardEvent) => void; */ function ignoreWhenInteracting( handler: KeyboardEventHandler, + ignoreDialog: RefObject = undefined, ): KeyboardEventHandler { return (event: KeyboardEvent): void => { if (selfoss.lightboxActive.value) { return; + } else if (ignoreDialog && ignoreDialog.current.open) { + return; } handler(event); @@ -31,6 +35,7 @@ interface IKeybinding { * This is used to: * - Limit boilerplate * - Generate a tinykeys compatible KeybindingsMap (@see makeKeybindingsMap) + * - Generate a keybindings overview for the help page (see HelpShortcuts component) */ export const KEYBINDINGS: { [keycombo: string]: IKeybinding } = { Space: { @@ -177,19 +182,40 @@ export const KEYBINDINGS: { [keycombo: string]: IKeybinding } = { .click(); }, }, + '[Shift]+?': { + readableName: '?', + description: 'show help', + action: () => {}, + }, }; -function makeKeybindingsMap(): KeybindingsMap { +function makeKeybindingsMap( + shortcutsDialog: RefObject, +): KeybindingsMap { const shortcuts = Object.entries(KEYBINDINGS); const keybindingsMap: KeybindingsMap = {}; for (const [keycombo, keybind] of shortcuts) { - keybindingsMap[keycombo] = ignoreWhenInteracting( - (event: KeyboardEvent) => { - event.preventDefault(); - keybind.action(event); - }, - ); + if (keycombo === '[Shift]+?') { + keybindingsMap[keycombo] = ignoreWhenInteracting( + (event: KeyboardEvent) => { + event.preventDefault(); + if (shortcutsDialog.current.open) { + shortcutsDialog.current.close(); + } else { + shortcutsDialog.current.showModal(); + } + }, + ); + } else { + keybindingsMap[keycombo] = ignoreWhenInteracting( + (event: KeyboardEvent) => { + event.preventDefault(); + keybind.action(event); + }, + shortcutsDialog, + ); + } } return keybindingsMap; } @@ -197,6 +223,8 @@ function makeKeybindingsMap(): KeybindingsMap { /** * Set up shortcuts on document. */ -export default function makeShortcuts(): () => void { - return tinykeys(window, makeKeybindingsMap()); +export default function makeShortcuts( + shortcutsDialog: RefObject, +): () => void { + return tinykeys(window, makeKeybindingsMap(shortcutsDialog)); } diff --git a/client/js/templates/App.tsx b/client/js/templates/App.tsx index b07821ffa..adb3f32a0 100644 --- a/client/js/templates/App.tsx +++ b/client/js/templates/App.tsx @@ -8,6 +8,7 @@ import React, { useMemo, useState, MouseEvent, + useRef, } from 'react'; import { BrowserRouter as Router, @@ -44,6 +45,8 @@ import locales, { } from '../locales'; import { useEntriesParams, useLocation } from '../helpers/uri'; import { NavSource, NavTag } from '../requests/items'; +import Dialog from './Dialog'; +import HelpShortcuts from './HelpShortcuts'; type MessageAction = { label: string; @@ -246,6 +249,7 @@ function PureApp(props: PureAppProps): React.JSX.Element { } = props; const [navExpanded, setNavExpanded] = useState(false); + const shortcutsDialog = useRef(null); const smartphone = useIsSmartphone(); const offlineEnabled = useListenableValue(selfoss.db.enableOffline); const [entriesPage, setEntriesPage] = useState(null); @@ -253,7 +257,7 @@ function PureApp(props: PureAppProps): React.JSX.Element { useEffect(() => { // init shortcut handler - const destroyShortcuts = makeShortcuts(); + const destroyShortcuts = makeShortcuts(shortcutsDialog); return () => { destroyShortcuts(); @@ -480,6 +484,9 @@ function PureApp(props: PureAppProps): React.JSX.Element { /> } /> + + + } diff --git a/client/js/templates/HelpShortcuts.tsx b/client/js/templates/HelpShortcuts.tsx new file mode 100644 index 000000000..9901edb0d --- /dev/null +++ b/client/js/templates/HelpShortcuts.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { KEYBINDINGS } from '../shortcuts'; +import { intersperse } from 'ramda'; + +export default function HelpShortcuts(): React.JSX.Element { + return ( +
+

Keybindings

+
+ {Object.entries(KEYBINDINGS).map(([keycombo, keybinding]) => { + let readableKeycombo; + if (keybinding.readableName) { + readableKeycombo = {keybinding.readableName}; + } else if (keycombo.includes('+')) { + const keys = keycombo + .split('+') // key={key} required for TS + .map((key) => {key}); + readableKeycombo = intersperse(+, keys); + } else { + readableKeycombo = {keycombo}; + } + return ( + <> +
{readableKeycombo}
+
{keybinding.description}
+ + ); + })} +
+
+ ); +} diff --git a/client/styles/_help.scss b/client/styles/_help.scss new file mode 100644 index 000000000..b95b574dd --- /dev/null +++ b/client/styles/_help.scss @@ -0,0 +1,26 @@ +.shortcuts { + dl { + display: grid; + grid-template-columns: max-content auto; + gap: 0.5rem; + } + + dt { + font-weight: bolder; + } + + @supports (display: grid) { + dd { + margin-left: 0; + } + } + + kbd { + border: 1px solid #353535; + border-radius: 0.25rem; + padding: 0.1875rem 0.3125rem; + margin: 0.125rem; + color: #353535; + text-decoration: none; + } +} diff --git a/client/styles/main.scss b/client/styles/main.scss index f4450dd64..68a18be1e 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -3,6 +3,7 @@ @import 'npm:yet-another-react-lightbox/styles.css'; @import 'npm:reset-css/sass/reset'; @import 'mixins/visually-hidden'; +@import 'help'; /* base */