diff --git a/README.md b/README.md index e7c1c4ff..2742597f 100644 --- a/README.md +++ b/README.md @@ -59,16 +59,26 @@ ## Context menu -Context menu allows you to quickly save selected text from any website to My Notes, or to transfer selected text to My Notes on other computers. +Context menu can be displayed on right-click on any website and based on the context used to quickly save: -To use Context menu, select the text on website, right-click and see "My Notes" Context menu. +
    +
  1. selected text
  2. +
  3. current page URL (right-click on an empty space)
  4. +
  5. image
  6. +
+ +Destination can be any local note, or My Notes on other computers. + +Options are based on the context but in general are: -Context menu has these options: +- `Save to [note name]` – Option for every note. My Notes doesn't have to be open. Google Drive Sync is not required. +- `Save to remotely open My Notes` – My Notes on other computers needs to be open. The same Google Account needs to be used. Google Drive Sync is not required. The destination note will be named `@received`. -- `Save to [note name]` – Option for every note. As you create new notes, they are automatically added to the list. My Notes doesn't have to be open. Google Drive Sync is not required. -- `Save to remotely open My Notes` – My Notes on other computers needs to be open. The same Google Account needs to be used. Google Drive Sync is not required. The destination note to save the text will be named **"@received"** (created automatically if it doesn't exist, otherwise updated). +There is a few general purpose notes that can be used as destination. Their name starts with `@` and they are created automatically when needed: -Context menu also allows you to save current page URL (no text selected) to **"@clipboard"** (created automatically if it doesn't exist, otherwise updated). +- `@received` +- `@clipboard` +- `@images`

@@ -87,17 +97,11 @@ Click on the **"Save"** button to save the custom theme. ## Command palette -Command palette is a window which you can open with `Cmd + P` (or `Ctrl + P`) and use your keyboard to quickly perform any of the following actions: - -
    -
  1. Find note(s) by their name, and open one. (default behavior)
  2. -
  3. Find note(s) by their content, and open one. (type ? first, then continue)
  4. -
  5. Find commands, and execute one. (type > first, then continue)
  6. -
+Command palette is a window which you can open with `Cmd + P` (or `Ctrl + P`) and use your keyboard to quickly find commands, and execute one. -To navigate between the results, use `Up` and `Down` arrow keys. +To navigate between the commands, use `Up` and `Down` arrow keys. -To open a selected note or execute a selected command, press `Enter`. +To execute a selected command, press `Enter`. The last executed command can be repeated with `Cmd + Shift + P` (or `Ctrl + Shift + P`). diff --git a/src/i18n/en.json b/src/i18n/en.json index 4eecf561..acf6022e 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -138,11 +138,6 @@ "Options": "Options", "Sync notes to and from Google Drive": "Sync notes to and from Google Drive", "Command palette": "Command palette", - "Command palette detail": { - "line1": "By default, Command palette looks for notes by their name.", - "line2": "Type {{symbol}} first, and continue to start search for commands.", - "line3": "Type {{symbol}} first, and continue to start search for notes by their content." - }, "Repeat last command": "Repeat last command", "Toggle Focus mode": "Toggle Focus mode", "Toggle Sidebar": "Toggle Sidebar", diff --git a/src/notes.tsx b/src/notes.tsx index 8b7a7983..5edb666e 100644 --- a/src/notes.tsx +++ b/src/notes.tsx @@ -2,9 +2,8 @@ /* eslint-disable react/jsx-pascal-case */ import { h, render, Fragment } from "preact"; import { - useState, useEffect, useRef, useCallback, useMemo, + useState, useEffect, useRef, useCallback, } from "preact/hooks"; - import { Os, Storage, @@ -44,17 +43,17 @@ import { } from "notes/content/save"; import sendMessage from "shared/messages/send"; -import { getActiveFromUrl, getFocusOverride } from "notes/location"; +import { getActiveFromUrl, getFocusOverride, isOverview } from "notes/location"; +import Overview from "notes/overview/Overview"; import getFirstAvailableNoteName from "notes/filters/get-first-available-note-name"; import notesHistory from "notes/history"; import keyboardShortcuts, { KeyboardShortcut } from "notes/keyboard-shortcuts"; import { useKeyboardShortcut } from "notes/hooks/use-keyboard-shortcut"; import { - Command, commands, toggleSidebar, toggleToolbar, + Command, toggleSidebar, toggleToolbar, toggleFocusMode, } from "notes/commands"; import { exportNote } from "notes/export"; import { notesToSidebarNotes } from "notes/adapters"; -import { t } from "i18n"; let autoSyncIntervalId: number | undefined; @@ -68,6 +67,8 @@ const Notes = (): h.JSX.Element => { const [tabId, setTabId] = useState(undefined); const [initialized, setInitialized] = useState(false); + const [view] = useState<"default" | "overview">(isOverview() ? "overview" : "default"); + // Notifications const [notification, setNotification] = useState(undefined); @@ -382,8 +383,8 @@ const Notes = (): h.JSX.Element => { // Sidebar useEffect(() => { - document.body.classList.toggle("with-sidebar", sidebar); - }, [sidebar]); + document.body.classList.toggle("with-sidebar", view === "default" && sidebar); + }, [sidebar, view]); // Sidebar width useEffect(() => { @@ -433,8 +434,8 @@ const Notes = (): h.JSX.Element => { raw: note.raw || false, }); - document.title = note ? notesProps.active : "My Notes"; - }, [notesProps.active]); + document.title = (view === "default" && note) ? notesProps.active : "My Notes"; + }, [notesProps.active, view]); // Toolbar useEffect(() => { @@ -459,18 +460,10 @@ const Notes = (): h.JSX.Element => { }); }); keyboardShortcuts.subscribe(KeyboardShortcut.OnOpenOptions, chrome.runtime.openOptionsPage); - keyboardShortcuts.subscribe(KeyboardShortcut.OnToggleFocusMode, () => { - if (getFocusOverride()) { - return; - } - chrome.storage.local.get(["focus"], (local) => { - chrome.storage.local.set({ focus: !local.focus }); - }); - }); keyboardShortcuts.subscribe(KeyboardShortcut.OnToggleSidebar, toggleSidebar); - keyboardShortcuts.subscribe(KeyboardShortcut.OnToggleToolbar, toggleToolbar); + keyboardShortcuts.subscribe(KeyboardShortcut.OnToggleFocusMode, toggleFocusMode); keyboardShortcuts.subscribe(KeyboardShortcut.OnSync, () => sendMessage(MessageType.SYNC)); }, [os]); @@ -504,45 +497,12 @@ const Notes = (): h.JSX.Element => { chrome.storage.local.set({ active: noteName }); }, [notesProps]); - // Command Palette - const commandPaletteCommands: { name: string, translation: h.JSX.Element, command: Command }[] = useMemo(() => [ - { - name: "Insert current Date", - translation: t("Insert current Date"), - command: commands.InsertCurrentDate, - }, - { - name: "Insert current Time", - translation: t("Insert current Time"), - command: commands.InsertCurrentTime, - }, - { - name: "Insert current Date and Time", - translation: t("Insert current Date and Time"), - command: commands.InsertCurrentDateAndTime, - }, - { - name: "Toggle Sidebar", - translation: t("Shortcuts descriptions.Toggle Sidebar"), - command: toggleSidebar, - }, - { - name: "Toggle Toolbar", - translation: t("Shortcuts descriptions.Toggle Toolbar"), - command: toggleToolbar, - }, - ], []); - // Repeat last executed command const [setOnRepeatLastExecutedCommandHandler] = useKeyboardShortcut(KeyboardShortcut.OnRepeatLastExecutedCommand); // Command Palette const [setOnToggleCommandPaletteHandler] = useKeyboardShortcut(KeyboardShortcut.OnToggleCommandPalette); useEffect(() => { - if (!notesOrder) { - return; - } - // Detach when there are no notes if (!Object.keys(notesProps.notes).length) { setOnToggleCommandPaletteHandler(undefined); @@ -550,27 +510,16 @@ const Notes = (): h.JSX.Element => { return; } - // Start preparing props for Command Palette - const currentNoteLocked: boolean = notesProps.active in notesProps.notes && notesProps.notes[notesProps.active].locked === true; - const newCommands = currentNoteLocked ? [] : commandPaletteCommands; - // Props for Command Palette + const currentNoteLocked: boolean = notesProps.active in notesProps.notes && notesProps.notes[notesProps.active].locked === true; const props: CommandPaletteProps = { - notes: notesToSidebarNotes(notesProps.notes, notesOrder, order), - commands: newCommands, - onActivateNote: (noteName: string) => { + includeContentCommands: !currentNoteLocked, + onCommandToExecute: (command: Command) => { setCommandPaletteProps(null); - range.restore(() => handleOnActivateNote(noteName)); - }, - onExecuteCommand: (commandName: string) => { - const foundCommand = commandPaletteCommands.find((command) => command.name === commandName); - if (foundCommand) { - setCommandPaletteProps(null); - range.restore(() => { - foundCommand.command(); - setOnRepeatLastExecutedCommandHandler(foundCommand.command); - }); - } + range.restore(() => { + command(); + setOnRepeatLastExecutedCommandHandler(command); + }); }, }; @@ -589,7 +538,7 @@ const Notes = (): h.JSX.Element => { // Update props for already visible Command Palette setCommandPaletteProps((prev) => (!prev ? prev : props)); - }, [os, notesProps, notesOrder, order, handleOnActivateNote, commandPaletteCommands]); + }, [notesProps]); // Automatically show modal to create a new note if there are 0 notes useEffect(() => { @@ -620,6 +569,12 @@ const Notes = (): h.JSX.Element => { }, 6000); // and then Auto Sync every 6 seconds }, [initialized, autoSync]); + if (view === "overview" && notesOrder && order) { + return ( + + ); + } + return ( {notification && ( diff --git a/src/notes/commands/index.ts b/src/notes/commands/index.ts index e6383a2f..747011ae 100644 --- a/src/notes/commands/index.ts +++ b/src/notes/commands/index.ts @@ -1,5 +1,5 @@ import dateUtils from "shared/date/date-utils"; -import { getFocusOverride } from "notes/location"; +import { getFocusOverride, isOverview } from "notes/location"; import table from "./table"; import highlight from "./highlight"; @@ -94,13 +94,15 @@ const commands: { [key in AvailableCommand]: Command } = { RemoveFormat, }; +const canToggle = () => !getFocusOverride() && !isOverview(); + const toggleSidebar: Command = () => { - if (getFocusOverride()) { + if (!canToggle()) { return; } chrome.storage.local.get(["focus"], (local) => { - if (!local.focus) { // toggle only if not in focus mode + if (!local.focus) { // toggle only if NOT in focus mode const hasSidebar = document.body.classList.toggle("with-sidebar"); chrome.storage.local.set({ sidebar: hasSidebar }); } @@ -108,18 +110,28 @@ const toggleSidebar: Command = () => { }; const toggleToolbar: Command = () => { - if (getFocusOverride()) { + if (!canToggle()) { return; } chrome.storage.local.get(["focus"], (local) => { - if (!local.focus) { // toggle only if not in focus mode + if (!local.focus) { // toggle only if NOT in focus mode const hasToolbar = document.body.classList.toggle("with-toolbar"); chrome.storage.local.set({ toolbar: hasToolbar }); } }); }; +const toggleFocusMode: Command = () => { + if (!canToggle()) { + return; + } + + chrome.storage.local.get(["focus"], (local) => { + chrome.storage.local.set({ focus: !local.focus }); + }); +}; + export { commands, @@ -133,4 +145,5 @@ export { toggleSidebar, toggleToolbar, + toggleFocusMode, }; diff --git a/src/notes/components/CommandPalette.tsx b/src/notes/components/CommandPalette.tsx index 2c65636f..7d785b69 100644 --- a/src/notes/components/CommandPalette.tsx +++ b/src/notes/components/CommandPalette.tsx @@ -1,136 +1,77 @@ import { h } from "preact"; import { - useRef, useState, useMemo, useCallback, useEffect, + useRef, useState, useEffect, useMemo, } from "preact/hooks"; import clsx from "clsx"; import useBodyClass from "notes/hooks/use-body-class"; -import { SidebarNote } from "notes/adapters"; import { t, tString } from "i18n"; +import { + Command, commands as contentCommands, toggleSidebar, toggleToolbar, +} from "notes/commands"; -export interface CommandPaletteProps { - notes: SidebarNote[] - commands: { name: string, translation: h.JSX.Element }[] - onActivateNote: (noteName: string) => void - onExecuteCommand: (commandName: string) => void -} - -export enum FilterType { - CommandsByName, - NotesByContent, - NotesByName, -} - -export interface Filter { - type: FilterType - input: string -} - -/** - * Returns filter to be used based on the input. - * - * We can filter: - * A) CommandsByName - * => filter commands by name, when input starts with ">" (whitespace before and after ">" is allowed, whitespace at the end is allowed) - * - * B) NotesByContent - * => filter notes by the content, when input starts with "?" (whitespace before and after "?" is allowed, whitespace at the end is allowed) - * - * C) NotesByName - * => filter notes by their name, when input does NOT start with [">", "?"] (whitespace before is allowed, whitespace at the end is allowed) - */ -export const prepareFilter = (rawInput: string): Filter => { - const input = rawInput.trim().toLowerCase().replace(/^([>?])\s*(.*)/, "$1$2"); // trim whitespace, remove whitespace after [">", "?"] - - // A) CommandsByName - if (input.startsWith(">")) { - return { - type: FilterType.CommandsByName, - input: input.slice(1), - }; - } - - // B) NotesByContent - if (input.startsWith("?")) { - return { - type: FilterType.NotesByContent, - input: input.slice(1), - }; - } - - // C) NotesByName - return { - type: FilterType.NotesByName, - input, - }; +type CommandPaletteCommand = { + title: string + command: Command + isContentCommand?: boolean }; -export const prepareItems = (notes: SidebarNote[], commands: string[], filter: Filter | undefined): string[] => { - const noteNames = notes.map((note) => note.name); +const allCommands: CommandPaletteCommand[] = [ + { + title: tString("Insert current Date"), + command: contentCommands.InsertCurrentDate, + isContentCommand: true, + }, + { + title: tString("Insert current Time"), + command: contentCommands.InsertCurrentTime, + isContentCommand: true, + }, + { + title: tString("Insert current Date and Time"), + command: contentCommands.InsertCurrentDateAndTime, + isContentCommand: true, + }, + { + title: tString("Shortcuts descriptions.Toggle Sidebar"), + command: toggleSidebar, + }, + { + title: tString("Shortcuts descriptions.Toggle Toolbar"), + command: toggleToolbar, + }, +]; - if (!filter) { - return noteNames; - } - - const input = filter.input.trim(); - const prepareFilterPredicate = (givenInput: string) => (item: string) => (givenInput ? item.toLowerCase().includes(givenInput) : item); - - // A) CommandsByName - if (filter.type === FilterType.CommandsByName) { - return commands.filter(prepareFilterPredicate(input)); - } - - // B) NotesByContent - if (filter.type === FilterType.NotesByContent) { - const filterPredicate = prepareFilterPredicate(input); - return input - ? noteNames.filter((noteName) => { - const foundNote = notes.find((note) => note.name === noteName); - return foundNote && filterPredicate(foundNote.content); - }) - : noteNames; - } - - // C) NotesByName - return noteNames.filter(prepareFilterPredicate(input)); -}; +export interface CommandPaletteProps { + includeContentCommands: boolean + onCommandToExecute: (command: Command) => void +} const CommandPalette = ({ - notes, commands, onActivateNote, onExecuteCommand, + includeContentCommands, + onCommandToExecute, }: CommandPaletteProps): h.JSX.Element => { useBodyClass("with-command-palette"); const inputRef = useRef(null); - const [filter, setFilter] = useState(undefined); - - // Items to show in Command Palette - const items: string[] = useMemo(() => prepareItems(notes, commands.map((command) => command.name), filter), [notes, commands, filter]); - - // Selected item; can be changed with Up / Down arrow keys - const [selectedItemIndex, setSelectedItemIndex] = useState(-1); - - // What to do on Enter - const handleItem = useCallback((name: string) => { - if (filter?.type === FilterType.CommandsByName) { - onExecuteCommand(name); - return; - } - - onActivateNote(name); - }, [filter, onActivateNote, onExecuteCommand]); + useEffect(() => inputRef.current?.focus(), []); - // auto-focus the input when Command Palette is open - useEffect(() => inputRef.current?.focus(), [inputRef]); + const [filter, setFilter] = useState(""); + const filteredCommands = useMemo(() => ( + allCommands + .filter((x) => (includeContentCommands ? true : (!x.isContentCommand))) + .filter((x) => (filter ? x.title.toLowerCase().includes(filter.trim().toLowerCase()) : true)) + ), [includeContentCommands, filter]); - // reset selected item when props change; auto-select it when there's just one - useEffect(() => setSelectedItemIndex(items.length === 1 ? 0 : -1), [filter, items]); + const [selectedIndex, setSelectedIndex] = useState(-1); + useEffect(() => setSelectedIndex(filteredCommands.length === 1 ? 0 : -1), [filteredCommands]); return (
{ - if (event.key === "Enter" && selectedItemIndex !== -1) { - const item = items[selectedItemIndex]; - handleItem(item); + if (event.key === "Enter" && selectedIndex !== -1) { + const { command } = filteredCommands[selectedIndex]; + onCommandToExecute(command); return; } @@ -140,10 +81,10 @@ const CommandPalette = ({ } event.preventDefault(); - const activeIndexCandidate = ((selectedItemIndex + direction) % items.length); - setSelectedItemIndex( + const activeIndexCandidate = ((selectedIndex + direction) % filteredCommands.length); + setSelectedIndex( activeIndexCandidate < 0 && direction === -1 - ? items.length - 1 + ? filteredCommands.length - 1 : activeIndexCandidate, ); }} @@ -155,34 +96,29 @@ const CommandPalette = ({ ref={inputRef} onBlur={() => inputRef.current?.focus()} onInput={(event) => { - const newFilter = prepareFilter((event.target as HTMLInputElement).value); - setFilter(newFilter); + setFilter((event.target as HTMLInputElement).value); }} autoComplete="off" /> - {items.length > 0 && ( + {filteredCommands.length > 0 && (
- {items.map((name, index) => ( + {filteredCommands.map((item, index) => (
handleItem(name)} - onMouseEnter={() => setSelectedItemIndex(index)} + className={clsx("command-palette-list-item", index === selectedIndex && "active")} + onClick={() => onCommandToExecute(item.command)} + onMouseEnter={() => setSelectedIndex(index)} > - {(filter?.type === FilterType.CommandsByName) - ? commands.find((command) => command.name === name)?.translation - : name} + {item.title}
))}
)} - {items.length === 0 && filter && ( + {filteredCommands.length === 0 && (
- {filter.type === FilterType.CommandsByName && t("(No matching commands)")} - {filter.type === FilterType.NotesByContent && t("(No matching notes)")} - {filter.type === FilterType.NotesByName && t("(No matching notes)")} + {t("(No matching commands)")}
)} diff --git a/src/notes/components/SidebarButtons.tsx b/src/notes/components/SidebarButtons.tsx index d50c023a..3af015d1 100644 --- a/src/notes/components/SidebarButtons.tsx +++ b/src/notes/components/SidebarButtons.tsx @@ -7,6 +7,7 @@ import SVG from "notes/components/SVG"; import FileSvgText from "svg/file.svg"; import GearSvgText from "svg/gear.svg"; import RefreshSvgText from "svg/refresh.svg"; +import GridSvgText from "svg/grid.svg"; import { importNoteFromTextFile } from "notes/import"; import sendMessage from "shared/messages/send"; import formatDate from "shared/date/format-date"; @@ -23,6 +24,7 @@ const SidebarButtons = ({ }: SidebarButtonsProps): h.JSX.Element => { const [dragOver, setDragOver] = useState(false); const openOptions = useCallback(() => chrome.runtime.openOptionsPage(), []); + const openOverview = useCallback(() => window.open("notes.html?overview", "_blank"), []); return (
+ + +
+ +
+
); }; diff --git a/src/notes/components/__tests__/CommandPalette.test.tsx b/src/notes/components/__tests__/CommandPalette.test.tsx deleted file mode 100644 index 88a77583..00000000 --- a/src/notes/components/__tests__/CommandPalette.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { SidebarNote } from "notes/adapters"; -import { - Filter, FilterType, prepareFilter, prepareItems, -} from "../CommandPalette"; - -describe("prepareFilter", () => { - it("detects CommandsByName filter", () => { - expect(prepareFilter(" > time ")).toEqual({ - type: FilterType.CommandsByName, - input: "time", - }); - - expect(prepareFilter(" > current time ")).toEqual({ - type: FilterType.CommandsByName, - input: "current time", - }); - }); - - it("detects NotesByContent filter", () => { - expect(prepareFilter(" ? muffins ")).toEqual({ - type: FilterType.NotesByContent, - input: "muffins", - }); - - expect(prepareFilter(" ? best ever muffins ")).toEqual({ - type: FilterType.NotesByContent, - input: "best ever muffins", - }); - }); - - it("detects NotesByName filter", () => { - expect(prepareFilter(" TODO ")).toEqual({ - type: FilterType.NotesByName, - input: "todo", - }); - }); -}); - -describe("prepareItems", () => { - const createdTime = "CT"; // not relevant for the test - const modifiedTime = "MT"; // not relevant for the test - - const notes: SidebarNote[] = [ - { - name: "Clipboard", - content: "", - createdTime, - modifiedTime, - }, - { - name: "Article", - content: "This is an interesting article", - createdTime, - modifiedTime, - }, - { - name: "TODO", - content: "buy milk, buy coffee", - createdTime, - modifiedTime, - }, - ]; - - const commands = [ - "Insert current Date", - "Insert current Time", - "Insert current Date and Time", - ]; - - it("returns notes by default", () => { - const items = prepareItems(notes, commands, undefined); - expect(items).toEqual([ - "Clipboard", - "Article", - "TODO", - ]); - }); - - describe("CommandsByName filter", () => { - it("returns commands that include input in their name", () => { - const filter: Filter = { type: FilterType.CommandsByName, input: "date" }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual([ - "Insert current Date", - "Insert current Date and Time", - ]); - }); - - it("returns all commands when no input is provided", () => { - const filter: Filter = { type: FilterType.CommandsByName, input: " " }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual(commands); - }); - }); - - describe("NotesByContent filter", () => { - it("returns notes that include input in their content", () => { - const filter: Filter = { type: FilterType.NotesByContent, input: "interesting" }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual([ - "Article", - ]); - }); - - it("returns all notes when no input is provided", () => { - const filter: Filter = { type: FilterType.NotesByContent, input: " " }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual([ - "Clipboard", - "Article", - "TODO", - ]); - }); - }); - - describe("NotesByName filter", () => { - it("returns notes that include input in their name", () => { - const filter: Filter = { type: FilterType.NotesByName, input: "ar" }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual([ - "Clipboard", - "Article", - ]); - }); - - it("returns all notes when no input is provided", () => { - const filter: Filter = { type: FilterType.NotesByName, input: " " }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual([ - "Clipboard", - "Article", - "TODO", - ]); - }); - }); -}); diff --git a/src/notes/history.ts b/src/notes/history.ts index 35e1a0cf..ff2a2afe 100644 --- a/src/notes/history.ts +++ b/src/notes/history.ts @@ -1,13 +1,17 @@ +import { isOverview } from "./location"; + +type NoteNameFunc = (noteName: string) => void; + // Use replace when a note is renamed or deleted -const replace = (noteName: string): void => +const replace: NoteNameFunc = (noteName: string): void => window.history.replaceState({ noteName }, noteName, `?note=${noteName}`); // Use push when a note is created -const push = (noteName: string): void => +const push: NoteNameFunc = (noteName: string): void => window.history.pushState({ noteName }, noteName, `?note=${noteName}`); let attached = false; -const attach = (onPop: (noteName: string) => void): void => { +const attach = (onPop: NoteNameFunc): void => { if (attached) { return; } @@ -22,9 +26,10 @@ const attach = (onPop: (noteName: string) => void): void => { attached = true; }; -export default { - replace, - push, +const canProxy = () => !isOverview(); - attach, +export default { + replace: (noteName: string) => canProxy() && replace(noteName), + push: (noteName: string) => canProxy() && push(noteName), + attach: (onPop: NoteNameFunc) => canProxy() && attach(onPop), }; diff --git a/src/notes/location.ts b/src/notes/location.ts index b5913cfa..c822cc1f 100644 --- a/src/notes/location.ts +++ b/src/notes/location.ts @@ -1,2 +1,5 @@ -export const getFocusOverride = (): boolean => new URL(window.location.href).searchParams.get("focus") === ""; -export const getActiveFromUrl = (): string => new URL(window.location.href).searchParams.get("note") || ""; // Bookmark +const getParam = (name: string) => new URL(window.location.href).searchParams.get(name); + +export const getFocusOverride = (): boolean => getParam("focus") === ""; +export const getActiveFromUrl = (): string => getParam("note") || ""; +export const isOverview = (): boolean => getParam("overview") === ""; diff --git a/src/notes/overview/Overview.tsx b/src/notes/overview/Overview.tsx new file mode 100644 index 00000000..dbf2fcc0 --- /dev/null +++ b/src/notes/overview/Overview.tsx @@ -0,0 +1,25 @@ +import { h } from "preact"; +import { SidebarNote } from "notes/adapters"; + +interface OverviewProps { + notes: SidebarNote[] +} + +const Overview = ({ notes }: OverviewProps): h.JSX.Element => ( +
+ {notes.map((note) => ( +
+ +
+
+ ))} +
+); + +export default Overview; diff --git a/src/options/KeyboardShortcuts.tsx b/src/options/KeyboardShortcuts.tsx index c24407d1..b07bcaf2 100644 --- a/src/options/KeyboardShortcuts.tsx +++ b/src/options/KeyboardShortcuts.tsx @@ -40,14 +40,6 @@ const KeyboardShortcuts = ({ os }: KeyboardShortcutsProps): h.JSX.Element => ( {t("Shortcuts other.if enabled")} )} - - {shortcut.description === "Command palette" && ( -
-
{t("Shortcuts descriptions.Command palette detail.line1")}
-
{t("Shortcuts descriptions.Command palette detail.line2", { symbol: ">" })}
-
{t("Shortcuts descriptions.Command palette detail.line3", { symbol: "?" })}
-
- )} ))} diff --git a/src/svg/grid.svg b/src/svg/grid.svg new file mode 100644 index 00000000..76b2eff2 --- /dev/null +++ b/src/svg/grid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/static/notes.css b/static/notes.css index b1d58262..c26fceaf 100644 --- a/static/notes.css +++ b/static/notes.css @@ -90,6 +90,44 @@ hr { border-bottom: 1px solid var(--notification-border-color); } +/* Overview */ + +#notes-overview { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)); + column-gap: 1em; + row-gap: 1em; + padding: 1em; + overflow-y: auto; +} + +.note-tile { + display: inline-block; + height: 400px; + border: 1px solid var(--overview-tile-border-color); + border-radius: 3px; + overflow: auto; +} + +.note-tile::-webkit-scrollbar-track, +.note-tile::-webkit-scrollbar-thumb { + border-radius: 3px; +} + +.note-tile-title { + position: sticky; + left: 0; + top: 0; + background: var(--background-color); + padding: 1em; + font-size: 1.2em; + font-weight: bold; +} + +.note-tile-content { + padding: 1em; +} + /* Bar (for #sidebar-buttons and #toolbar) */ #toolbar, #sidebar-buttons { @@ -164,8 +202,8 @@ body:not(.with-sidebar) { left: 0 !important; } align-items: center; cursor: pointer; font-weight: bold; - padding: .5em 1em; - margin-top: 3px; + padding: .5em; + margin-bottom: 3px; border-radius: 3px; } diff --git a/static/themes/dark.css b/static/themes/dark.css index 94e1d234..4f31b7f8 100644 --- a/static/themes/dark.css +++ b/static/themes/dark.css @@ -107,6 +107,10 @@ --command-palette-active-item-text-color: white; + /* Overview */ + --overview-tile-border-color: #454545; + + /* Only in Options */ --keyboard-shortcut-background-color: #222; diff --git a/static/themes/light.css b/static/themes/light.css index ef4c7387..cf11d864 100644 --- a/static/themes/light.css +++ b/static/themes/light.css @@ -107,6 +107,10 @@ --command-palette-active-item-text-color: black; + /* Overview */ + --overview-tile-border-color: #dfe1e5; + + /* Only in Options */ --keyboard-shortcut-background-color: #e1e1e1;