diff --git a/app/components/AsciidocBlocks/Document.tsx b/app/components/AsciidocBlocks/Document.tsx index 7a73802..c1dfde7 100644 --- a/app/components/AsciidocBlocks/Document.tsx +++ b/app/components/AsciidocBlocks/Document.tsx @@ -9,16 +9,14 @@ import { useDelegatedReactRouterLinks } from '@oxide/design-system/components/di import { Content, type DocumentBlock } from '@oxide/react-asciidoc' import { useRef } from 'react' +// add styles for main +// max-w-full flex-shrink overflow-hidden 800:overflow-visible 800:pr-10 1200:w-[calc(100%-var(--toc-width))] 1200:pr-16 print:p-0 const CustomDocument = ({ document }: { document: DocumentBlock }) => { let ref = useRef(null) useDelegatedReactRouterLinks(ref, document.title) return ( -
+
) diff --git a/app/components/Dropdown.tsx b/app/components/Dropdown.tsx index 5ac6dac..d68a13d 100644 --- a/app/components/Dropdown.tsx +++ b/app/components/Dropdown.tsx @@ -19,18 +19,18 @@ export const dropdownInnerStyles = `focus:outline-0 focus:bg-hover px-3 py-2 pr- export const DropdownItem = ({ children, - classNames, + className, onSelect, }: { children: ReactNode | string - classNames?: string + className?: string onSelect?: () => void }) => ( ( - + {children} @@ -88,18 +88,18 @@ export const DropdownLink = ({ export const DropdownMenu = ({ children, - classNames, + className, align = 'end', }: { children: React.ReactNode - classNames?: string + className?: string align?: 'end' | 'start' | 'center' | undefined }) => ( *:last-child]:border-b-0', - classNames, + className, )} align={align} > @@ -110,16 +110,16 @@ export const DropdownMenu = ({ export const DropdownSubMenu = ({ children, - classNames, + className, }: { children: JSX.Element[] - classNames?: string + className?: string }) => ( *:last-child]:border-b-0', - classNames, + className, )} > {children} diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 6dd3989..c530773 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -10,10 +10,10 @@ import { buttonStyle } from '@oxide/design-system' import * as Dropdown from '@radix-ui/react-dropdown-menu' import { Link, useFetcher } from '@remix-run/react' import { useCallback, useState } from 'react' +import { useHotkeys } from 'react-hotkeys-hook' import Icon from '~/components/Icon' import NewRfdButton from '~/components/NewRfdButton' -import { useKey } from '~/hooks/use-key' import { useRootLoaderData } from '~/root' import type { RfdItem, RfdListItem } from '~/services/rfd.server' @@ -53,7 +53,7 @@ export default function Header({ currentRfd }: { currentRfd?: RfdItem }) { return false // Returning false prevents default behaviour in Firefox }, [open]) - useKey('mod+k', toggleSearchMenu) + useHotkeys('mod+k', toggleSearchMenu) return (
@@ -79,6 +79,12 @@ export default function Header({ currentRfd }: { currentRfd?: RfdItem }) { setOpen(false)} /> + + + {user ? ( diff --git a/app/components/SelectRfdCombobox.tsx b/app/components/SelectRfdCombobox.tsx index 36de404..ff98045 100644 --- a/app/components/SelectRfdCombobox.tsx +++ b/app/components/SelectRfdCombobox.tsx @@ -10,9 +10,9 @@ import { Link, useNavigate } from '@remix-run/react' import cn from 'classnames' import fuzzysort from 'fuzzysort' import { useCallback, useEffect, useRef, useState } from 'react' +import { useHotkeys } from 'react-hotkeys-hook' import Icon from '~/components/Icon' -import { useKey } from '~/hooks/use-key' import { useSteppedScroll } from '~/hooks/use-stepped-scroll' import type { RfdItem, RfdListItem } from '~/services/rfd.server' import { classed } from '~/utils/classed' @@ -33,7 +33,7 @@ const SelectRfdCombobox = ({ // memoized to avoid render churn in useKey const toggleCombobox = useCallback(() => setOpen(!open), [setOpen, open]) - useKey('mod+/', toggleCombobox) + useHotkeys('mod+/', toggleCombobox) const handleDismiss = () => setOpen(false) @@ -220,7 +220,7 @@ const ComboboxItem = ({ }) => { const [shouldPrefetch, setShouldPrefetch] = useState(false) - const timer = useRef(null) + const timer = useRef | null>(null) function clear() { if (timer.current) clearTimeout(timer.current) diff --git a/app/components/home/FilterDropdown.tsx b/app/components/home/FilterDropdown.tsx index bbbcc54..c7cd987 100644 --- a/app/components/home/FilterDropdown.tsx +++ b/app/components/home/FilterDropdown.tsx @@ -149,7 +149,7 @@ const DropdownFilterItem = ({ }) => ( {selected && }
diff --git a/app/components/note/Editor.tsx b/app/components/note/Editor.tsx new file mode 100644 index 0000000..1525c64 --- /dev/null +++ b/app/components/note/Editor.tsx @@ -0,0 +1,62 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { Editor, useMonaco } from '@monaco-editor/react' +import { shikiToMonaco } from '@shikijs/monaco' +import { useEffect } from 'react' +import { getHighlighter } from 'shiki' + +import theme from './oxide-dark.json' + +const EditorWrapper = ({ + body, + onChange, +}: { + body: string + onChange: (string: string | undefined) => void +}) => { + const monaco = useMonaco() + + useEffect(() => { + if (!monaco) { + return + } + + const highlight = async () => { + const highlighter = await getHighlighter({ + themes: [theme], + langs: ['asciidoc'], + }) + + monaco.languages.register({ id: 'asciidoc' }) + shikiToMonaco(highlighter, monaco) + } + + highlight() + }, [monaco]) + + return ( + + ) +} + +export default EditorWrapper diff --git a/app/components/note/NoteForm.tsx b/app/components/note/NoteForm.tsx new file mode 100644 index 0000000..455bb10 --- /dev/null +++ b/app/components/note/NoteForm.tsx @@ -0,0 +1,282 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { Spinner } from '@oxide/design-system' +import { Asciidoc, prepareDocument } from '@oxide/react-asciidoc' +import * as Dropdown from '@radix-ui/react-dropdown-menu' +import { useFetcher } from '@remix-run/react' +import dayjs from 'dayjs' +import { useCallback, useEffect, useMemo, useState } from 'react' + +import { opts } from '~/components/AsciidocBlocks' +import { DropdownItem, DropdownLink, DropdownMenu } from '~/components/Dropdown' +import Icon from '~/components/Icon' +import { useDebounce } from '~/hooks/use-debounce' +import { ad } from '~/utils/asciidoctor' + +import EditorWrapper from './Editor' +import { SidebarIcon } from './Sidebar' + +type EditorStatus = 'idle' | 'unsaved' | 'saving' | 'saved' | 'error' + +export const NoteForm = ({ + id, + initialTitle = '', + initialBody = '', + updated, + published, + onSave, + fetcher, + sidebarOpen, + setSidebarOpen, +}: { + id: string + initialTitle?: string + initialBody?: string + updated: string + published: 1 | 0 + onSave: (title: string, body: string) => void + fetcher: any + sidebarOpen: boolean + setSidebarOpen: (bool: boolean) => void +}) => { + const [status, setStatus] = useState('idle') + const [body, setBody] = useState(initialBody) + const [title, setTitle] = useState(initialTitle) + + const debouncedBody = useDebounce(body, 750) + const debouncedTitle = useDebounce(title, 750) + + useEffect(() => { + const hasChanges = body !== initialBody || title !== initialTitle + + const hasError = fetcher.data?.status === 'error' + + if (hasError && status !== 'error') { + setStatus('error') + } + + const isSaving = fetcher.state === 'submitting' + const isSaved = fetcher.state === 'idle' && status === 'saving' + + if (!hasChanges && (isSaving || isSaved)) { + if (isSaving) { + setStatus('saving') + } else if (isSaved) { + setStatus('saved') + } + } + + if (debouncedBody === body && debouncedTitle === title && status === 'unsaved') { + onSave(title, body) + setStatus('saving') + } + }, [ + body, + title, + initialBody, + initialTitle, + debouncedBody, + debouncedTitle, + fetcher, + status, + onSave, + ]) + + // Handle window resizing + const [leftPaneWidth, setLeftPaneWidth] = useState(50) // Initial width in percentage + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault() + const startX = e.clientX + const startWidth = leftPaneWidth + + const handleMouseMove = (moveEvent: MouseEvent) => { + const dx = moveEvent.clientX - startX + const newWidth = + (((startWidth / 100) * window.innerWidth + dx) * 100) / window.innerWidth + setLeftPaneWidth(Math.max(20, Math.min(80, newWidth))) + } + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } + + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + }, + [leftPaneWidth], + ) + + const doc = useMemo(() => { + return prepareDocument( + ad.load(body, { + standalone: true, + }), + ) + }, [body]) + + return ( + <> + +
+
+ +
+ + {title ? title : 'Title...'} + + { + setStatus('unsaved') + setTitle(el.target.value) + }} + name="title" + placeholder="Title..." + required + className="absolute left-1 w-full bg-transparent p-0 text-sans-xl text-raise placeholder:text-tertiary focus:outline-none" + /> +
+ + + + {fetcher.data?.status === 'error' && ( +
{fetcher.data.error}
+ )} +
+ + +
+
+
+ + + { + setStatus('unsaved') + setBody(val || '') + }} + /> +
+
+
+
+
+ +
+
+ + + ) +} + +const TypingIndicator = () => ( +
+ + + +
+) + +const SavingIndicator = ({ + status, + updated, +}: { + status: EditorStatus + updated: string +}) => { + return ( +
+ {dayjs(updated).format('MMM D YYYY, h:mm A')} + {status === 'unsaved' ? ( + + ) : status === 'error' ? ( + + ) : status === 'saved' ? ( + + ) : status === 'saving' ? ( + + ) : ( + + )} +
+ ) +} + +const MoreDropdown = ({ id, published }: { id: string; published: 1 | 0 }) => { + const fetcher = useFetcher() + + const handleDelete = () => { + if (window.confirm('Are you sure you want to delete this note?')) { + fetcher.submit( + { id: id }, + { + method: 'post', + action: `/notes/${id}/delete`, + encType: 'application/x-www-form-urlencoded', + }, + ) + } + } + + const handlePublish = async () => { + const isPublished = published === 1 + const confirmationMessage = isPublished + ? 'Are you sure you want to unpublish this note?' + : 'Are you sure you want to publish this note?' + + if (window.confirm(confirmationMessage)) { + fetcher.submit( + { publish: isPublished ? 0 : 1 }, + { + method: 'post', + action: `/notes/${id}/publish`, + encType: 'application/json', + }, + ) + } + } + + return ( + + + + + + + View + + {published ? 'Unpublish' : 'Publish'} + + + Delete + + + + ) +} diff --git a/app/components/note/Sidebar.tsx b/app/components/note/Sidebar.tsx new file mode 100644 index 0000000..74cd912 --- /dev/null +++ b/app/components/note/Sidebar.tsx @@ -0,0 +1,118 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { buttonStyle } from '@oxide/design-system' +import { Link, NavLink, useMatches } from '@remix-run/react' +import cn from 'classnames' +import { type ReactNode } from 'react' + +import Icon from '~/components/Icon' +import { type NoteItem } from '~/routes/notes' + +const navLinkStyles = ({ isActive }: { isActive: boolean }) => { + const activeStyle = isActive + ? 'bg-accent-secondary hover:!bg-accent-secondary-hover text-accent' + : null + return `block text-sans-md text-secondary hover:bg-hover px-2 py-1 rounded flex items-center group justify-between ${activeStyle}` +} + +const Divider = ({ className }: { className?: string }) => ( +
+) + +export const SidebarIcon = () => ( + + + +) + +interface HandleData { + notes: NoteItem[] +} + +export const Sidebar = () => { + const matches = useMatches() + + const data = matches[1]?.data as HandleData + const notes = data.notes || undefined + + return ( + + ) +} + +const LinkSection = ({ label, children }: { label: string; children: ReactNode }) => ( +
+
{label}
+
    {children}
+
+) diff --git a/app/components/note/oxide-dark.json b/app/components/note/oxide-dark.json new file mode 100644 index 0000000..2254877 --- /dev/null +++ b/app/components/note/oxide-dark.json @@ -0,0 +1,1386 @@ +{ + "name": "oxide-dark", + "colors": { + "editor.background": "#080F11", + "editor.foreground": "#E7E7E8" + }, + "tokenColors": [ + { + "scope": [ + "text", + "source", + "variable.other.readwrite", + "punctuation.definition.variable" + ], + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": "punctuation", + "settings": { + "foreground": "#A1A4A5", + "fontStyle": "" + } + }, + { + "scope": ["comment", "punctuation.definition.comment"], + "settings": { + "foreground": "#A1A4A5", + "fontStyle": "italic" + } + }, + { + "scope": ["string", "punctuation.definition.string"], + "settings": { + "foreground": "#68D9A7" + } + }, + { + "scope": "constant.character.escape", + "settings": { + "foreground": "#EFB7C2" + } + }, + { + "scope": [ + "constant.numeric", + "variable.other.constant", + "entity.name.constant", + "constant.language.boolean", + "constant.language.false", + "constant.language.true", + "keyword.other.unit.user-defined", + "keyword.other.unit.suffix.floating-point" + ], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": [ + "keyword", + "keyword.operator.word", + "keyword.operator.new", + "variable.language.super", + "support.type.primitive", + "storage.type", + "storage.modifier", + "punctuation.definition.keyword" + ], + "settings": { + "foreground": "#C6A5EA", + "fontStyle": "" + } + }, + { + "scope": "entity.name.tag.documentation", + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": [ + "keyword.operator", + "punctuation.accessor", + "punctuation.definition.generic", + "meta.function.closure punctuation.section.parameters", + "punctuation.definition.tag", + "punctuation.separator.key-value" + ], + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": [ + "entity.name.function", + "meta.function-call.method", + "support.function", + "support.function.misc", + "variable.function" + ], + "settings": { + "foreground": "#9DAFFA", + "fontStyle": "italic" + } + }, + { + "scope": [ + "entity.name.class", + "entity.other.inherited-class", + "support.class", + "meta.function-call.constructor", + "entity.name.struct" + ], + "settings": { + "foreground": "#EDD5A6", + "fontStyle": "italic" + } + }, + { + "scope": "entity.name.enum", + "settings": { + "foreground": "#EDD5A6", + "fontStyle": "italic" + } + }, + { + "scope": ["meta.enum variable.other.readwrite", "variable.other.enummember"], + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": "meta.property.object", + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": ["meta.type", "meta.type-alias", "support.type", "entity.name.type"], + "settings": { + "foreground": "#EDD5A6", + "fontStyle": "italic" + } + }, + { + "scope": [ + "meta.annotation variable.function", + "meta.annotation variable.annotation.function", + "meta.annotation punctuation.definition.annotation", + "meta.decorator", + "punctuation.decorator" + ], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": ["variable.parameter", "meta.function.parameters"], + "settings": { + "foreground": "#F39EAE", + "fontStyle": "italic" + } + }, + { + "scope": ["constant.language", "support.function.builtin"], + "settings": { + "foreground": "#F7869B" + } + }, + { + "scope": "entity.other.attribute-name.documentation", + "settings": { + "foreground": "#F7869B" + } + }, + { + "scope": ["keyword.control.directive", "punctuation.definition.directive"], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "punctuation.definition.typeparameters", + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": "entity.name.namespace", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "support.type.property-name.css", + "settings": { + "foreground": "#9DAFFA", + "fontStyle": "" + } + }, + { + "scope": [ + "variable.language.this", + "variable.language.this punctuation.definition.variable" + ], + "settings": { + "foreground": "#F7869B" + } + }, + { + "scope": "variable.object.property", + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": ["string.template variable", "string variable"], + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": "keyword.operator.new", + "settings": { + "fontStyle": "bold" + } + }, + { + "scope": "storage.modifier.specifier.extern.cpp", + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": [ + "entity.name.scope-resolution.template.call.cpp", + "entity.name.scope-resolution.parameter.cpp", + "entity.name.scope-resolution.cpp", + "entity.name.scope-resolution.function.definition.cpp" + ], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "storage.type.class.doxygen", + "settings": { + "fontStyle": "" + } + }, + { + "scope": ["storage.modifier.reference.cpp"], + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": "meta.interpolation.cs", + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": "comment.block.documentation.cs", + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": [ + "source.css entity.other.attribute-name.class.css", + "entity.other.attribute-name.parent-selector.css punctuation.definition.entity.css" + ], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "punctuation.separator.operator.css", + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": "source.css entity.other.attribute-name.pseudo-class", + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": "source.css constant.other.unicode-range", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "source.css variable.parameter.url", + "settings": { + "foreground": "#88DCB7", + "fontStyle": "" + } + }, + { + "scope": ["support.type.vendored.property-name"], + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": [ + "source.css meta.property-value variable", + "source.css meta.property-value variable.other.less", + "source.css meta.property-value variable.other.less punctuation.definition.variable.less", + "meta.definition.variable.scss" + ], + "settings": { + "foreground": "#F39EAE" + } + }, + { + "scope": [ + "source.css meta.property-list variable", + "meta.property-list variable.other.less", + "meta.property-list variable.other.less punctuation.definition.variable.less" + ], + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": "keyword.other.unit.percentage.css", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "source.css meta.attribute-selector", + "settings": { + "foreground": "#88DCB7" + } + }, + { + "scope": [ + "keyword.other.definition.ini", + "punctuation.support.type.property-name.json", + "support.type.property-name.json", + "punctuation.support.type.property-name.toml", + "support.type.property-name.toml", + "entity.name.tag.yaml", + "punctuation.support.type.property-name.yaml", + "support.type.property-name.yaml" + ], + "settings": { + "foreground": "#9DAFFA", + "fontStyle": "" + } + }, + { + "scope": ["constant.language.json", "constant.language.yaml"], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": ["entity.name.type.anchor.yaml", "variable.other.alias.yaml"], + "settings": { + "foreground": "#EDD5A6", + "fontStyle": "" + } + }, + { + "scope": ["support.type.property-name.table", "entity.name.section.group-title.ini"], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "constant.other.time.datetime.offset.toml", + "settings": { + "foreground": "#EFB7C2" + } + }, + { + "scope": ["punctuation.definition.anchor.yaml", "punctuation.definition.alias.yaml"], + "settings": { + "foreground": "#EFB7C2" + } + }, + { + "scope": "entity.other.document.begin.yaml", + "settings": { + "foreground": "#EFB7C2" + } + }, + { + "scope": "markup.changed.diff", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": [ + "meta.diff.header.from-file", + "meta.diff.header.to-file", + "punctuation.definition.from-file.diff", + "punctuation.definition.to-file.diff" + ], + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": "markup.inserted.diff", + "settings": { + "foreground": "#88DCB7" + } + }, + { + "scope": "markup.deleted.diff", + "settings": { + "foreground": "#F7869B" + } + }, + { + "scope": ["variable.other.env"], + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": ["string.quoted variable.other.env"], + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": "support.function.builtin.gdscript", + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": "constant.language.gdscript", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "comment meta.annotation.go", + "settings": { + "foreground": "#F39EAE" + } + }, + { + "scope": "comment meta.annotation.parameters.go", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "constant.language.go", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "variable.graphql", + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": "string.unquoted.alias.graphql", + "settings": { + "foreground": "#F2CDCD" + } + }, + { + "scope": "constant.character.enum.graphql", + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": "meta.objectvalues.graphql constant.object.key.graphql string.unquoted.graphql", + "settings": { + "foreground": "#F2CDCD" + } + }, + { + "scope": [ + "keyword.other.doctype", + "meta.tag.sgml.doctype punctuation.definition.tag", + "meta.tag.metadata.doctype entity.name.tag", + "meta.tag.metadata.doctype punctuation.definition.tag" + ], + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": ["entity.name.tag"], + "settings": { + "foreground": "#9DAFFA", + "fontStyle": "" + } + }, + { + "scope": [ + "text.html constant.character.entity", + "text.html constant.character.entity punctuation", + "constant.character.entity.xml", + "constant.character.entity.xml punctuation", + "constant.character.entity.js.jsx", + "constant.charactger.entity.js.jsx punctuation", + "constant.character.entity.tsx", + "constant.character.entity.tsx punctuation" + ], + "settings": { + "foreground": "#F7869B" + } + }, + { + "scope": ["entity.other.attribute-name"], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": [ + "support.class.component", + "support.class.component.jsx", + "support.class.component.tsx", + "support.class.component.vue" + ], + "settings": { + "foreground": "#EFB7C2", + "fontStyle": "" + } + }, + { + "scope": ["punctuation.definition.annotation", "storage.type.annotation"], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "constant.other.enum.java", + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": "storage.modifier.import.java", + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": "comment.block.javadoc.java keyword.other.documentation.javadoc.java", + "settings": { + "fontStyle": "" + } + }, + { + "scope": "meta.export variable.other.readwrite.js", + "settings": { + "foreground": "#F39EAE" + } + }, + { + "scope": [ + "variable.other.constant.js", + "variable.other.constant.ts", + "variable.other.property.js", + "variable.other.property.ts" + ], + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": ["variable.other.jsdoc", "comment.block.documentation variable.other"], + "settings": { + "foreground": "#F39EAE", + "fontStyle": "" + } + }, + { + "scope": "storage.type.class.jsdoc", + "settings": { + "fontStyle": "" + } + }, + { + "scope": "support.type.object.console.js", + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": ["support.constant.node", "support.type.object.module.js"], + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": "storage.modifier.implements", + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": [ + "constant.language.null.js", + "constant.language.null.ts", + "constant.language.undefined.js", + "constant.language.undefined.ts", + "support.type.builtin.ts" + ], + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": "variable.parameter.generic", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": ["keyword.declaration.function.arrow.js", "storage.type.function.arrow.ts"], + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": "punctuation.decorator.ts", + "settings": { + "foreground": "#9DAFFA", + "fontStyle": "italic" + } + }, + { + "scope": [ + "keyword.operator.expression.in.js", + "keyword.operator.expression.in.ts", + "keyword.operator.expression.infer.ts", + "keyword.operator.expression.instanceof.js", + "keyword.operator.expression.instanceof.ts", + "keyword.operator.expression.is", + "keyword.operator.expression.keyof.ts", + "keyword.operator.expression.of.js", + "keyword.operator.expression.of.ts", + "keyword.operator.expression.typeof.ts" + ], + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": "support.function.macro.julia", + "settings": { + "foreground": "#A7E0C8", + "fontStyle": "italic" + } + }, + { + "scope": "constant.language.julia", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "constant.other.symbol.julia", + "settings": { + "foreground": "#F39EAE" + } + }, + { + "scope": "text.tex keyword.control.preamble", + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": "text.tex support.function.be", + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": "constant.other.general.math.tex", + "settings": { + "foreground": "#F2CDCD" + } + }, + { + "scope": "comment.line.double-dash.documentation.lua storage.type.annotation.lua", + "settings": { + "foreground": "#C6A5EA", + "fontStyle": "" + } + }, + { + "scope": [ + "comment.line.double-dash.documentation.lua entity.name.variable.lua", + "comment.line.double-dash.documentation.lua variable.lua" + ], + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": [ + "heading.1.markdown punctuation.definition.heading.markdown", + "heading.1.markdown", + "heading.1.quarto punctuation.definition.heading.quarto", + "heading.1.quarto", + "markup.heading.atx.1.mdx", + "markup.heading.atx.1.mdx punctuation.definition.heading.mdx", + "markup.heading.setext.1.markdown", + "markup.heading.heading-0.asciidoc" + ], + "settings": { + "foreground": "#F7869B" + } + }, + { + "scope": [ + "heading.2.markdown punctuation.definition.heading.markdown", + "heading.2.markdown", + "heading.2.quarto punctuation.definition.heading.quarto", + "heading.2.quarto", + "markup.heading.atx.2.mdx", + "markup.heading.atx.2.mdx punctuation.definition.heading.mdx", + "markup.heading.setext.2.markdown", + "markup.heading.heading-1.asciidoc" + ], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": [ + "heading.3.markdown punctuation.definition.heading.markdown", + "heading.3.markdown", + "heading.3.quarto punctuation.definition.heading.quarto", + "heading.3.quarto", + "markup.heading.atx.3.mdx", + "markup.heading.atx.3.mdx punctuation.definition.heading.mdx", + "markup.heading.heading-2.asciidoc" + ], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": [ + "heading.4.markdown punctuation.definition.heading.markdown", + "heading.4.markdown", + "heading.4.quarto punctuation.definition.heading.quarto", + "heading.4.quarto", + "markup.heading.atx.4.mdx", + "markup.heading.atx.4.mdx punctuation.definition.heading.mdx", + "markup.heading.heading-3.asciidoc" + ], + "settings": { + "foreground": "#88DCB7" + } + }, + { + "scope": [ + "heading.5.markdown punctuation.definition.heading.markdown", + "heading.5.markdown", + "heading.5.quarto punctuation.definition.heading.quarto", + "heading.5.quarto", + "markup.heading.atx.5.mdx", + "markup.heading.atx.5.mdx punctuation.definition.heading.mdx", + "markup.heading.heading-4.asciidoc" + ], + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": [ + "heading.6.markdown punctuation.definition.heading.markdown", + "heading.6.markdown", + "heading.6.quarto punctuation.definition.heading.quarto", + "heading.6.quarto", + "markup.heading.atx.6.mdx", + "markup.heading.atx.6.mdx punctuation.definition.heading.mdx", + "markup.heading.heading-5.asciidoc" + ], + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": "markup.bold", + "settings": { + "foreground": "#F7869B", + "fontStyle": "bold" + } + }, + { + "scope": "markup.italic", + "settings": { + "foreground": "#F7869B", + "fontStyle": "italic" + } + }, + { + "scope": "markup.strikethrough", + "settings": { + "foreground": "#A6ADC8", + "fontStyle": "strikethrough" + } + }, + { + "scope": ["punctuation.definition.link", "markup.underline.link"], + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": [ + "text.html.markdown punctuation.definition.link.title", + "text.html.quarto punctuation.definition.link.title", + "string.other.link.title.markdown", + "string.other.link.title.quarto", + "markup.link", + "punctuation.definition.constant.markdown", + "punctuation.definition.constant.quarto", + "constant.other.reference.link.markdown", + "constant.other.reference.link.quarto", + "markup.substitution.attribute-reference" + ], + "settings": { + "foreground": "#B4BEFE" + } + }, + { + "scope": [ + "punctuation.definition.raw.markdown", + "punctuation.definition.raw.quarto", + "markup.inline.raw.string.markdown", + "markup.inline.raw.string.quarto", + "markup.raw.block.markdown", + "markup.raw.block.quarto" + ], + "settings": { + "foreground": "#88DCB7" + } + }, + { + "scope": "fenced_code.block.language", + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": [ + "markup.fenced_code.block punctuation.definition", + "markup.raw support.asciidoc" + ], + "settings": { + "foreground": "#A1A4A5" + } + }, + { + "scope": ["markup.quote", "punctuation.definition.quote.begin"], + "settings": { + "foreground": "#EFB7C2" + } + }, + { + "scope": "meta.separator.markdown", + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": [ + "punctuation.definition.list.begin.markdown", + "punctuation.definition.list.begin.quarto", + "markup.list.bullet" + ], + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": "markup.heading.quarto", + "settings": { + "fontStyle": "bold" + } + }, + { + "scope": [ + "entity.other.attribute-name.multipart.nix", + "entity.other.attribute-name.single.nix" + ], + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": "variable.parameter.name.nix", + "settings": { + "foreground": "#E7E7E8", + "fontStyle": "" + } + }, + { + "scope": "meta.embedded variable.parameter.name.nix", + "settings": { + "foreground": "#B4BEFE", + "fontStyle": "" + } + }, + { + "scope": "string.unquoted.path.nix", + "settings": { + "foreground": "#EFB7C2", + "fontStyle": "" + } + }, + { + "scope": ["support.attribute.builtin", "meta.attribute.php"], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "meta.function.parameters.php punctuation.definition.variable.php", + "settings": { + "foreground": "#F39EAE" + } + }, + { + "scope": "constant.language.php", + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": "text.html.php support.function", + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": "keyword.other.phpdoc.php", + "settings": { + "fontStyle": "" + } + }, + { + "scope": ["support.variable.magic.python", "meta.function-call.arguments.python"], + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": ["support.function.magic.python"], + "settings": { + "foreground": "#9DAFFA", + "fontStyle": "italic" + } + }, + { + "scope": [ + "variable.parameter.function.language.special.self.python", + "variable.language.special.self.python" + ], + "settings": { + "foreground": "#F7869B", + "fontStyle": "italic" + } + }, + { + "scope": ["keyword.control.flow.python", "keyword.operator.logical.python"], + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": "storage.type.function.python", + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": [ + "support.token.decorator.python", + "meta.function.decorator.identifier.python" + ], + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": ["meta.function-call.python"], + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": [ + "entity.name.function.decorator.python", + "punctuation.definition.decorator.python" + ], + "settings": { + "foreground": "#EDD5A6", + "fontStyle": "italic" + } + }, + { + "scope": "constant.character.format.placeholder.other.python", + "settings": { + "foreground": "#EFB7C2" + } + }, + { + "scope": ["support.type.exception.python", "support.function.builtin.python"], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": ["support.type.python"], + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "constant.language.python", + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": ["meta.indexed-name.python", "meta.item-access.python"], + "settings": { + "foreground": "#F39EAE", + "fontStyle": "italic" + } + }, + { + "scope": "storage.type.string.python", + "settings": { + "foreground": "#88DCB7", + "fontStyle": "italic" + } + }, + { + "scope": "meta.function.parameters.python", + "settings": { + "fontStyle": "" + } + }, + { + "scope": [ + "string.regexp punctuation.definition.string.begin", + "string.regexp punctuation.definition.string.end" + ], + "settings": { + "foreground": "#EFB7C2" + } + }, + { + "scope": "keyword.control.anchor.regexp", + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": "string.regexp.ts", + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": [ + "punctuation.definition.group.regexp", + "keyword.other.back-reference.regexp" + ], + "settings": { + "foreground": "#88DCB7" + } + }, + { + "scope": "punctuation.definition.character-class.regexp", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "constant.other.character-class.regexp", + "settings": { + "foreground": "#EFB7C2" + } + }, + { + "scope": "constant.other.character-class.range.regexp", + "settings": { + "foreground": "#F5E0DC" + } + }, + { + "scope": "keyword.operator.quantifier.regexp", + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": "constant.character.numeric.regexp", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": [ + "punctuation.definition.group.no-capture.regexp", + "meta.assertion.look-ahead.regexp", + "meta.assertion.negative-look-ahead.regexp" + ], + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": [ + "meta.annotation.rust", + "meta.annotation.rust punctuation", + "meta.attribute.rust", + "punctuation.definition.attribute.rust" + ], + "settings": { + "foreground": "#EDD5A6", + "fontStyle": "italic" + } + }, + { + "scope": [ + "meta.attribute.rust string.quoted.double.rust", + "meta.attribute.rust string.quoted.single.char.rust" + ], + "settings": { + "fontStyle": "" + } + }, + { + "scope": [ + "entity.name.function.macro.rules.rust", + "storage.type.module.rust", + "storage.modifier.rust", + "storage.type.struct.rust", + "storage.type.enum.rust", + "storage.type.trait.rust", + "storage.type.union.rust", + "storage.type.impl.rust", + "storage.type.rust", + "storage.type.function.rust", + "storage.type.type.rust" + ], + "settings": { + "foreground": "#C6A5EA", + "fontStyle": "" + } + }, + { + "scope": "entity.name.type.numeric.rust", + "settings": { + "foreground": "#C6A5EA", + "fontStyle": "" + } + }, + { + "scope": "meta.generic.rust", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "entity.name.impl.rust", + "settings": { + "foreground": "#EDD5A6", + "fontStyle": "italic" + } + }, + { + "scope": "entity.name.module.rust", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "entity.name.trait.rust", + "settings": { + "foreground": "#EDD5A6", + "fontStyle": "italic" + } + }, + { + "scope": "storage.type.source.rust", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "entity.name.union.rust", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": "meta.enum.rust storage.type.source.rust", + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": [ + "support.macro.rust", + "meta.macro.rust support.function.rust", + "entity.name.function.macro.rust" + ], + "settings": { + "foreground": "#9DAFFA", + "fontStyle": "italic" + } + }, + { + "scope": ["storage.modifier.lifetime.rust", "entity.name.type.lifetime"], + "settings": { + "foreground": "#9DAFFA", + "fontStyle": "italic" + } + }, + { + "scope": "string.quoted.double.rust constant.other.placeholder.rust", + "settings": { + "foreground": "#EFB7C2" + } + }, + { + "scope": "meta.function.return-type.rust meta.generic.rust storage.type.rust", + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": "meta.function.call.rust", + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": "punctuation.brackets.angle.rust", + "settings": { + "foreground": "#9DAFFA" + } + }, + { + "scope": "constant.other.caps.rust", + "settings": { + "foreground": "#EDD5A6" + } + }, + { + "scope": ["meta.function.definition.rust variable.other.rust"], + "settings": { + "foreground": "#F39EAE" + } + }, + { + "scope": "meta.function.call.rust variable.other.rust", + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": "variable.language.self.rust", + "settings": { + "foreground": "#F7869B" + } + }, + { + "scope": [ + "variable.other.metavariable.name.rust", + "meta.macro.metavariable.rust keyword.operator.macro.dollar.rust" + ], + "settings": { + "foreground": "#EFB7C2" + } + }, + { + "scope": [ + "comment.line.shebang", + "comment.line.shebang punctuation.definition.comment", + "comment.line.shebang", + "punctuation.definition.comment.shebang.shell", + "meta.shebang.shell" + ], + "settings": { + "foreground": "#EFB7C2", + "fontStyle": "italic" + } + }, + { + "scope": "comment.line.shebang constant.language", + "settings": { + "foreground": "#A7E0C8", + "fontStyle": "italic" + } + }, + { + "scope": [ + "meta.function-call.arguments.shell punctuation.definition.variable.shell", + "meta.function-call.arguments.shell punctuation.section.interpolation", + "meta.function-call.arguments.shell punctuation.definition.variable.shell", + "meta.function-call.arguments.shell punctuation.section.interpolation" + ], + "settings": { + "foreground": "#F7869B" + } + }, + { + "scope": "meta.string meta.interpolation.parameter.shell variable.other.readwrite", + "settings": { + "foreground": "#EDD5A6", + "fontStyle": "italic" + } + }, + { + "scope": [ + "source.shell punctuation.section.interpolation", + "punctuation.definition.evaluation.backticks.shell" + ], + "settings": { + "foreground": "#A7E0C8" + } + }, + { + "scope": "entity.name.tag.heredoc.shell", + "settings": { + "foreground": "#C6A5EA" + } + }, + { + "scope": "string.quoted.double.shell variable.other.normal.shell", + "settings": { + "foreground": "#E7E7E8" + } + }, + { + "scope": "token.info-token", + "settings": { + "foreground": "#8BA1FF" + } + }, + { + "scope": "token.warn-token", + "settings": { + "foreground": "#F5B944" + } + }, + { + "scope": "token.error-token", + "settings": { + "foreground": "#FB6E88" + } + }, + { + "scope": "token.debug-token", + "settings": { + "foreground": "#BE95EB" + } + } + ] +} diff --git a/app/components/rfd/index.css b/app/components/rfd/index.css index 907797b..8e2eece 100644 --- a/app/components/rfd/index.css +++ b/app/components/rfd/index.css @@ -23,52 +23,3 @@ .dialog[data-leave] { transition-duration: 50ms; } - -.spinner { - --radius: 4; - --PI: 3.14159265358979; - --circumference: calc(var(--PI) * var(--radius) * 2px); - animation: rotate 5s linear infinite; -} - -.spinner .path { - stroke-dasharray: var(--circumference); - transform-origin: center; - animation: dash 4s ease-in-out infinite; - stroke: var(--content-accent); -} - -@media (prefers-reduced-motion) { - .spinner { - animation: rotate 6s linear infinite; - } - - .spinner .path { - animation: none; - stroke-dasharray: 20; - stroke-dashoffset: 100; - } - - .spinner-lg .path { - stroke-dasharray: 50; - } -} - -.spinner .bg { - stroke: var(--content-default); -} - -@keyframes rotate { - 100% { - transform: rotate(360deg); - } -} - -@keyframes dash { - from { - stroke-dashoffset: var(--circumference); - } - to { - stroke-dashoffset: calc(var(--circumference) * -1); - } -} diff --git a/app/hooks/use-debounce.ts b/app/hooks/use-debounce.ts new file mode 100644 index 0000000..446299c --- /dev/null +++ b/app/hooks/use-debounce.ts @@ -0,0 +1,34 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { useEffect, useState } from 'react' + +/** + * Custom hook for debouncing a value. + * @template T - The type of the value to be debounced. + * @param {T} value - The value to be debounced. + * @param {number} [delay] - The delay in milliseconds for debouncing. Defaults to 500 milliseconds. + * @returns {T} The debounced value. + * @see [Documentation](https://usehooks-ts.com/react-hook/use-debounce) + * @example + * const debouncedSearchTerm = useDebounce(searchTerm, 300); + */ +export function useDebounce(value: T, delay?: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value) + }, delay ?? 500) + + return () => { + clearTimeout(timer) + } + }, [value, delay]) + + return debouncedValue +} diff --git a/app/hooks/use-key.ts b/app/hooks/use-key.ts deleted file mode 100644 index 58bef5d..0000000 --- a/app/hooks/use-key.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ - -import Mousetrap from 'mousetrap' -import { useEffect } from 'react' - -type Key = Parameters[0] -type Callback = Parameters[1] - -/** - * Bind a keyboard shortcut with [Mousetrap](https://craig.is/killing/mice). - * Callback `fn` should be memoized. `key` does not need to be memoized. - */ -export const useKey = (key: Key, fn: Callback) => { - useEffect(() => { - Mousetrap.bind(key, fn) - return () => { - Mousetrap.unbind(key) - } - // JSON.stringify lets us avoid having to memoize the keys at the call site. - // Doing something similar with the callback makes less sense. - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [JSON.stringify(key), fn]) -} diff --git a/app/root.tsx b/app/root.tsx index e6a4c30..fa0c785 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -23,6 +23,7 @@ import { useLoaderData, useRouteError, useRouteLoaderData, + type ShouldRevalidateFunction, } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' @@ -41,6 +42,13 @@ import styles from '~/styles/index.css?url' import LoadingBar from './components/LoadingBar' import { inlineCommentsCookie, themeCookie } from './services/cookies.server' +export const shouldRevalidate: ShouldRevalidateFunction = ({ currentUrl, nextUrl }) => { + if (currentUrl.pathname.startsWith('/notes/') && nextUrl.pathname.startsWith('/notes/')) { + return false + } + return true +} + export const meta: MetaFunction = () => { return [{ title: 'RFD / Oxide' }] } diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 5d84379..5b0fa96 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -25,6 +25,7 @@ import cn from 'classnames' import dayjs from 'dayjs' import fuzzysort from 'fuzzysort' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useHotkeys } from 'react-hotkeys-hook' import { ClientOnly } from '~/components/ClientOnly' import Container from '~/components/Container' @@ -34,7 +35,6 @@ import FilterDropdown from '~/components/home/FilterDropdown' import StatusBadge from '~/components/StatusBadge' import { ExactMatch, SuggestedAuthors, SuggestedLabels } from '~/components/Suggested' import { useIsOverflow } from '~/hooks/use-is-overflow' -import { useKey } from '~/hooks/use-key' import { useRootLoaderData } from '~/root' import { rfdSortCookie } from '~/services/cookies.server' import type { RfdListItem } from '~/services/rfd.server' @@ -190,7 +190,7 @@ export default function Index() { return false }, [inputEl]) - useKey('/', focusInput) + useHotkeys('/', focusInput) const fetcher = useFetcher() const submitSortOrder = (newSortAttr: SortAttr) => { diff --git a/app/routes/notes.$id.delete.tsx b/app/routes/notes.$id.delete.tsx new file mode 100644 index 0000000..379d08f --- /dev/null +++ b/app/routes/notes.$id.delete.tsx @@ -0,0 +1,33 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { json, redirect, type ActionFunction } from '@remix-run/node' + +// import { isAuthenticated } from '~/services/authn.server' + +export const action: ActionFunction = async ({ params }) => { + // const user = await isAuthenticated(request) + const user = { + id: process.env.NOTES_TEST_USER_ID || '', + } + + if (!user) throw new Response('User not found', { status: 401 }) + + const response = await fetch(`${process.env.NOTES_API}/notes/${params.id}`, { + method: 'DELETE', + headers: { + 'x-api-key': process.env.NOTES_API_KEY || '', + }, + }) + + if (response.ok) { + return redirect(`/notes`) + } else { + const result = await response.json() + return json({ error: result.error }, { status: response.status }) + } +} diff --git a/app/routes/notes.$id.publish.tsx b/app/routes/notes.$id.publish.tsx new file mode 100644 index 0000000..5e37cb2 --- /dev/null +++ b/app/routes/notes.$id.publish.tsx @@ -0,0 +1,37 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { json, type ActionFunction } from '@remix-run/node' + +// import { isAuthenticated } from '~/services/authn.server' + +export const action: ActionFunction = async ({ request, params }) => { + // const user = await isAuthenticated(request) + const user = { + id: process.env.NOTES_TEST_USER_ID || '', + } + + if (!user) throw new Response('User not found', { status: 401 }) + + const { publish } = await request.json() + + const response = await fetch(`${process.env.NOTES_API}/notes/${params.id}/publish`, { + method: 'POST', + headers: { + 'x-api-key': process.env.NOTES_API_KEY || '', + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ publish }), + }) + + if (response.ok) { + return json({ status: response.status }) + } else { + const result = await response.json() + return json({ error: result.error }, { status: response.status }) + } +} diff --git a/app/routes/notes.$id_.edit.tsx b/app/routes/notes.$id_.edit.tsx new file mode 100644 index 0000000..b908d9f --- /dev/null +++ b/app/routes/notes.$id_.edit.tsx @@ -0,0 +1,101 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { json, type ActionFunction, type LoaderFunction } from '@remix-run/node' +import { useFetcher, useLoaderData } from '@remix-run/react' +import { makePatches, stringifyPatches } from '@sanity/diff-match-patch' +import cn from 'classnames' +import { useState } from 'react' + +import { NoteForm } from '~/components/note/NoteForm' +import { Sidebar } from '~/components/note/Sidebar' + +// import { isAuthenticated } from '~/services/authn.server' + +export const loader: LoaderFunction = async ({ params: { id } }) => { + const response = await fetch(`${process.env.NOTES_API}/notes/${id}`, { + headers: { + 'x-api-key': process.env.NOTES_API_KEY || '', + }, + }) + if (!response.ok) { + throw new Response('Not Found', { status: 404 }) + } + const data = await response.json() + return data +} + +export const action: ActionFunction = async ({ request, params }) => { + try { + const formData = await request.formData() + const title = formData.get('title') + const body = formData.get('body') + + // const user = await isAuthenticated(request) + const user = { + id: process.env.NOTES_TEST_USER_ID || '', + } + if (!user) throw new Response('User not found', { status: 401 }) + + const response = await fetch(`${process.env.NOTES_API}/notes/${params.id}`, { + method: 'PUT', + headers: { + 'x-api-key': process.env.NOTES_API_KEY || '', + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ title, body }), + }) + + if (!response.ok) { + const result = await response.json() + throw new Response(result.error, { status: response.status }) + } + + return json({ status: 'success', message: 'Note updated successfully' }) + } catch (error) { + if (error instanceof Response) { + return json({ status: 'error', error: await error.text() }, { status: error.status }) + } + return json({ status: 'error', error: 'An unexpected error occurred' }, { status: 500 }) + } +} + +export default function NoteEdit() { + const data = useLoaderData() + const fetcher = useFetcher() + + const [sidebarOpen, setSidebarOpen] = useState(true) + + const handleSave = (title: string, body: string) => { + const patches = makePatches(data.body, body) + + fetcher.submit({ title, body: stringifyPatches(patches) }, { method: 'post' }) + } + + return ( +
+ {sidebarOpen && } + setSidebarOpen(bool)} + fetcher={fetcher} + /> +
+ ) +} diff --git a/app/routes/notes._index.tsx b/app/routes/notes._index.tsx new file mode 100644 index 0000000..7c59d92 --- /dev/null +++ b/app/routes/notes._index.tsx @@ -0,0 +1,40 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { redirect, type LoaderFunction } from '@remix-run/node' + +// import { isAuthenticated } from '~/services/authn.server' + +export const loader: LoaderFunction = async () => { + // const user = await isAuthenticated(request) + const user = { + id: process.env.NOTES_TEST_USER_ID || '', + } + + if (!user) throw new Response('Not authorized', { status: 401 }) + + console.log(`${process.env.NOTES_API}/user/${user.id}`) + + const response = await fetch(`${process.env.NOTES_API}/user/${user.id}`, { + headers: { + 'x-api-key': process.env.NOTES_API_KEY || '', + }, + }) + if (!response.ok) { + throw new Error(`Error fetching: ${response.statusText}`) + } + const data = await response.json() + + console.log('REDIRECT') + + if (data.length > 0) { + return redirect(`/notes/${data[0].id}/edit`) + } else { + return redirect('/notes/new') + } +} diff --git a/app/routes/notes.new.tsx b/app/routes/notes.new.tsx new file mode 100644 index 0000000..8e6543c --- /dev/null +++ b/app/routes/notes.new.tsx @@ -0,0 +1,40 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { json, redirect, type ActionFunction, type LoaderFunction } from '@remix-run/node' + +// import { isAuthenticated } from '~/services/authn.server' + +export const action: ActionFunction = async () => { + // const user = await isAuthenticated(request) + const user = { + id: process.env.NOTES_TEST_USER_ID || '', + } + + if (!user) throw new Response('User not found', { status: 401 }) + + const response = await fetch(`${process.env.NOTES_API}/notes`, { + method: 'POST', + headers: { + 'x-api-key': process.env.NOTES_API_KEY || '', + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ title: 'Untitled', user: user.id, body: '' }), + }) + + const result = await response.json() + + if (response.ok) { + return redirect(`/notes/${result.id}/edit`) + } else { + return json({ error: result.error }, { status: response.status }) + } +} + +export const loader: LoaderFunction = async (args) => { + return action(args) +} diff --git a/app/routes/notes.tsx b/app/routes/notes.tsx new file mode 100644 index 0000000..8e7fe1e --- /dev/null +++ b/app/routes/notes.tsx @@ -0,0 +1,50 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { type LoaderFunction } from '@remix-run/node' +import { Outlet } from '@remix-run/react' + +// import { isAuthenticated } from '~/services/authn.server' + +export const loader: LoaderFunction = async () => { + // const user = await isAuthenticated(request) + const user = { + id: process.env.NOTES_TEST_USER_ID || '', + } + + if (!user) throw new Response('Not authorized', { status: 401 }) + + const response = await fetch(`${process.env.NOTES_API}/user/${user.id}`, { + headers: { + 'x-api-key': process.env.NOTES_API_KEY || '', + }, + }) + + if (!response.ok) { + throw new Error(`Error fetching: ${response.statusText}`) + } + const data = await response.json() + return { + notes: data, + user, + } +} + +export type NoteItem = { + id: string + title: string + user: string + body: string + created: string + updated: string + published: 1 | 0 +} + +export default function Note() { + return +} diff --git a/app/routes/rfd.$slug.tsx b/app/routes/rfd.$slug.tsx index 23f9068..bdd6b02 100644 --- a/app/routes/rfd.$slug.tsx +++ b/app/routes/rfd.$slug.tsx @@ -332,7 +332,7 @@ export default function Rfd() { ) } -const PropertyRow = ({ +export const PropertyRow = ({ label, children, className, diff --git a/app/styles/index.css b/app/styles/index.css index 0397859..f359617 100644 --- a/app/styles/index.css +++ b/app/styles/index.css @@ -63,6 +63,19 @@ body { @apply bg-default; } +body.note { + @apply m-0; +} + +.cm-line { + @apply pl-4; +} + +#code_mirror_wrapper .cm-line, +#code_mirror_wrapper .cm-gutters { + @apply !text-[15px] !normal-case !tracking-normal text-mono-md; +} + @layer base { body { @apply text-sans-sm text-raise; @@ -118,12 +131,38 @@ input[type='checkbox']:focus:not(:focus-visible) { @apply outline-0 ring-0; } -.link-with-underline { - @apply text-default hover:text-raise; - text-decoration: underline; - text-decoration-color: var(--content-quinary); +.typing-indicator { + display: flex; + align-items: center; + justify-content: space-around; + width: 12px; + height: 12px; +} + +.typing-indicator span { + display: block; + width: 3px; + height: 3px; + background-color: var(--content-accent); + border-radius: 50%; + animation: bounce 1.4s infinite both; +} + +.typing-indicator span:nth-child(1) { + animation-delay: -0.32s; +} - &:hover { - text-decoration-color: var(--content-tertiary); +.typing-indicator span:nth-child(2) { + animation-delay: -0.16s; +} + +@keyframes bounce { + 0%, + 80%, + 100% { + transform: translateY(0); + } + 40% { + transform: translateY(-4px); } } diff --git a/notes/.gitignore b/notes/.gitignore new file mode 100644 index 0000000..52dd73e --- /dev/null +++ b/notes/.gitignore @@ -0,0 +1 @@ +db/notes.db \ No newline at end of file diff --git a/notes/api/auth.ts b/notes/api/auth.ts new file mode 100644 index 0000000..8019151 --- /dev/null +++ b/notes/api/auth.ts @@ -0,0 +1,12 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +if (!process.env.API_KEYS) { + throw new Error('API_KEYS environment variable is required') +} + +export const API_KEYS = new Set(process.env.API_KEYS.split(',')) diff --git a/notes/api/index.ts b/notes/api/index.ts new file mode 100644 index 0000000..15c7e12 --- /dev/null +++ b/notes/api/index.ts @@ -0,0 +1,121 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { Elysia } from 'elysia' + +import { API_KEYS } from './auth' +import { + addNote, + deleteNote, + getNote, + listAllNotes, + listNotes, + updateNote, + updateNotePublished, + type NoteBody, +} from './main' + +const ServerError = { error: 'Something went wrong' } + +const validateApiKey = (request: Request) => { + const apiKey = request.headers.get('x-api-key') + return API_KEYS.has(apiKey ?? '') +} + +export const run = () => { + new Elysia() + .onRequest(({ request, set }) => { + if (!validateApiKey(request)) { + set.status = 401 + return { error: 'Not Authorized' } + } + }) + .get('/user/:userId', async ({ params: { userId }, set }) => { + try { + const notes = await listNotes(userId) + set.status = 200 + return notes + } catch (error) { + console.error(error) + set.status = 500 + return ServerError + } + }) + .get('/notes', async ({ set }) => { + try { + const notes = await listAllNotes() + set.status = 200 + return notes + } catch (error) { + console.error(error) + set.status = 500 + return ServerError + } + }) + .get('/notes/:id', async ({ params: { id }, set }) => { + try { + const note = await getNote(id) + if (!note) { + set.status = 404 + return 'Not Found' + } + return note + } catch (error) { + console.error(error) + set.status = 500 + return ServerError + } + }) + .post('/notes', async ({ body, set }) => { + try { + const { title, user, body: noteBody } = body as NoteBody + const id = await addNote(title, user, noteBody) + set.status = 201 + return { id } + } catch (error) { + console.error(error) + set.status = 400 + return { error: (error as Error).message } + } + }) + .post('/notes/:id/publish', async ({ params, request, set }) => { + try { + const { id } = params + const { publish } = await request.json() + await updateNotePublished(id, publish) + set.status = 200 + return { message: `Note ${publish ? 'published' : 'unpublished'} successfully.` } + } catch (error) { + console.error(error) + set.status = 400 + return { error: (error as Error).message } + } + }) + .put('/notes/:id', async ({ params: { id }, body, set }) => { + try { + const { title, body: noteBody } = body as NoteBody + await updateNote(id, title, noteBody) + set.status = 200 + return { message: 'Note updated successfully' } + } catch (error) { + console.error(error) + set.status = 500 + return { error: (error as Error).message } + } + }) + .delete('/notes/:id', async ({ params: { id }, set }) => { + try { + await deleteNote(id) + set.status = 204 + } catch (error) { + console.error(error) + set.status = 500 + return ServerError + } + }) + .listen(8000) +} diff --git a/notes/api/main.ts b/notes/api/main.ts new file mode 100644 index 0000000..383bc6e --- /dev/null +++ b/notes/api/main.ts @@ -0,0 +1,102 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { applyPatches, parsePatch } from '@sanity/diff-match-patch' +import { Database } from 'bun:sqlite' +import { nanoid } from 'nanoid' +import z from 'zod' + +const db = new Database('./db/notes.db') + +export interface NoteBody { + title: string + user: string + body: string + published: boolean +} + +const noteCreateSchema = z.object({ + title: z.string().min(1, 'Title must not be empty'), + user: z.string().min(3, 'User must not be empty'), +}) + +export const addNote = async (title: string, user: string, body: string) => { + const validationResult = noteCreateSchema.safeParse({ title, user }) + if (!validationResult.success) { + throw new Error(`Validation failed: ${validationResult.error.message}`) + } + + const id = nanoid(6) + const created = new Date().toISOString() + const updated = created + + const statement = db.prepare( + 'INSERT INTO notes (id, title, created, updated, user, body, published) VALUES (?, ?, ?, ?, ?, ?, ?)', + ) + statement.run(id, title, created, updated, user, body, 0) + + return id +} + +export const getNote = async (id: string) => { + const query = db.query('SELECT * FROM notes WHERE id = $id') + return await query.get({ $id: id }) +} + +const noteUpdateSchema = z.object({ + title: z.string().min(1, 'Title must not be empty'), +}) + +export const updateNote = async (id: string, title: string, body: string) => { + const validationResult = noteUpdateSchema.safeParse({ title }) + if (!validationResult.success) { + throw new Error(`Validation failed: ${validationResult.error.message}`) + } + + const currentNote = await getNote(id) + if (!currentNote) { + throw new Error('Note not found') + } + const [newBody] = applyPatches(parsePatch(body), (currentNote as NoteBody).body) + + const updated = new Date().toISOString() + + const statement = db.prepare( + 'UPDATE notes SET title = ?, updated = ?, body = ? WHERE id = ?', + ) + statement.run(title, updated, newBody, id) +} + +export const updateNotePublished = async (id: string, publish: boolean) => { + const published = publish ? 1 : 0 // Convert boolean to integer for SQL + const statement = db.prepare('UPDATE notes SET published = ? WHERE id = ?') + statement.run(published, id) +} + +export const deleteNote = async (id: string) => { + const statement = db.prepare('DELETE FROM notes WHERE id = ?') + statement.run(id) +} + +export const listNotes = async (userId: string) => { + const query = db.query('SELECT * FROM notes WHERE user = $userId') + const notes = query.all({ $userId: userId }) + + // We only want the first 20 lines so we're not sending a huge response + const trimmedNotes = notes.map((note) => ({ + ...(note as NoteBody), + body: (note as NoteBody).body.split('\n').slice(0, 20).join('\n'), + })) + + return trimmedNotes +} + +export const listAllNotes = async () => { + const query = db.query('SELECT * FROM notes') + + return query.all() +} diff --git a/notes/bun.lockb b/notes/bun.lockb new file mode 100755 index 0000000..a34189f Binary files /dev/null and b/notes/bun.lockb differ diff --git a/notes/db/drop.sh b/notes/db/drop.sh new file mode 100755 index 0000000..331c911 --- /dev/null +++ b/notes/db/drop.sh @@ -0,0 +1,15 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# Copyright Oxide Computer Company +#!/bin/bash + +DATABASE="notes.db" + +if [ -f "$DATABASE" ]; then + rm "$DATABASE" + echo "Database $DATABASE destroyed." +else + echo "Database $DATABASE does not exist." +fi diff --git a/notes/db/init.sh b/notes/db/init.sh new file mode 100755 index 0000000..d194753 --- /dev/null +++ b/notes/db/init.sh @@ -0,0 +1,14 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# Copyright Oxide Computer Company +#!/bin/bash + +DATABASE="notes.db" + +if [ ! -f "$DATABASE" ]; then + sqlite3 "$DATABASE" < "seed.sql" && echo "Database $DATABASE initialized." +else + echo "Database $DATABASE already exists." +fi diff --git a/notes/db/seed.sql b/notes/db/seed.sql new file mode 100644 index 0000000..a2066c9 --- /dev/null +++ b/notes/db/seed.sql @@ -0,0 +1,17 @@ +BEGIN TRANSACTION; + +CREATE TABLE IF NOT EXISTS notes ( + id TEXT PRIMARY KEY, + title TEXT, + created TEXT, + updated TEXT, + user TEXT, + body TEXT, + published BOOLEAN +); + +INSERT INTO notes (id, title, created, updated, user, body, published) VALUES +('AoGfVy', 'First Note', '2024-04-12 12:00:00', '2024-04-12 12:00:00', '96861ac2-f56e-4b7d-b128-c04467e6dd5f', 'In a time of enchantment when the moon played hide and seek with the stars, the mystical lands have awoken.', TRUE), +('Z5dQt1', 'Second Note', '2024-04-13 14:15:00', '2024-04-13 14:15:00', '96861ac2-f56e-4b7d-b128-c04467e6dd5f', 'The ancient runes whispered tales of forgotten magic, painting images of sparkling fountains and palaces of precious stones.', FALSE); + +COMMIT; \ No newline at end of file diff --git a/notes/index.ts b/notes/index.ts new file mode 100644 index 0000000..b31247c --- /dev/null +++ b/notes/index.ts @@ -0,0 +1,10 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { run } from './api' + +run() diff --git a/notes/package.json b/notes/package.json new file mode 100644 index 0000000..b2861c6 --- /dev/null +++ b/notes/package.json @@ -0,0 +1,24 @@ +{ + "name": "notes", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "^1.0.12" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@sanity/diff-match-patch": "^3.1.1", + "bun-types": "^1.1.3", + "elysia": "^1.0.13", + "nanoid": "^5.0.7", + "tsc": "", + "zod": "^3.22.4" + }, + "scripts": { + "start": "bun run index.ts", + "drop": "cd db && ./drop.sh", + "seed": "cd db && ./init.sh" + } +} diff --git a/notes/tsconfig.json b/notes/tsconfig.json new file mode 100644 index 0000000..666c94f --- /dev/null +++ b/notes/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + + "types": ["bun-types"] + } +} diff --git a/package-lock.json b/package-lock.json index c049dd9..0b83c4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "@asciidoctor/core": "^3.0.4", "@floating-ui/react": "^0.17.0", "@meilisearch/instant-meilisearch": "^0.8.2", + "@monaco-editor/loader": "^1.4.0", + "@monaco-editor/react": "^4.6.0", "@oxide/design-system": "^1.8.2", "@oxide/react-asciidoc": "^1.0.2", "@radix-ui/react-accordion": "^1.1.2", @@ -16,6 +18,8 @@ "@remix-run/node": "2.13.1", "@remix-run/react": "2.13.1", "@remix-run/serve": "2.13.1", + "@sanity/diff-match-patch": "^3.1.1", + "@shikijs/monaco": "^1.24.2", "@tanstack/react-query": "^4.3.9", "@vercel/remix": "^2.13.1", "classnames": "^2.3.1", @@ -28,15 +32,15 @@ "marked": "^4.2.5", "mermaid": "^11.4.1", "mime-types": "^2.1.35", - "mousetrap": "^1.6.5", "octokit": "^3.1.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hotkeys-hook": "^4.6.1", "react-instantsearch": "^7.13.4", "remeda": "^2.17.4", "remix-auth": "^3.6.0", "remix-auth-oauth2": "^1.11.1", - "shiki": "^1.23.1", + "shiki": "^1.5.1", "simple-text-diff": "^1.7.0", "tunnel-rat": "^0.1.2", "zod": "^3.22.3" @@ -1778,6 +1782,32 @@ "langium": "3.0.0" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -2364,6 +2394,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@oxide/react-asciidoc/-/react-asciidoc-1.0.2.tgz", "integrity": "sha512-CRA7LGX8aYhdpYZ3is6MSS1Lb0g+mIdi8rLuLA1mb/LXuXh9SOOPItucPaz+8PNIB+Owub6PpVUnqYCUJzNWew==", + "license": "MPL 2.0", "dependencies": { "html-react-parser": "^5.1.15" }, @@ -3792,6 +3823,15 @@ "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", "dev": true }, + "node_modules/@sanity/diff-match-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sanity/diff-match-patch/-/diff-match-patch-3.1.1.tgz", + "integrity": "sha512-dSZqGeYjHKGIkqAzGqLcG92LZyJGX+nYbs/FWawhBbTBDWi21kvQ0hsL3DJThuFVWtZMWTQijN3z6Cnd44Pf2g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.18" + } + }, "node_modules/@shikijs/core": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.24.2.tgz", @@ -3836,6 +3876,17 @@ "@shikijs/vscode-textmate": "^9.3.0" } }, + "node_modules/@shikijs/monaco": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/@shikijs/monaco/-/monaco-1.24.2.tgz", + "integrity": "sha512-cR+dHHBZQ1S4Xm9Kp0fM+auqn87bLHdudJjag3fDjcQFQAVUMikGYZq1zWgIu3uhipQH16id6ZkiMRuGavVljA==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.24.2", + "@shikijs/types": "1.24.2", + "@shikijs/vscode-textmate": "^9.3.0" + } + }, "node_modules/@shikijs/types": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.24.2.tgz", @@ -12515,6 +12566,13 @@ "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.0.1.tgz", "integrity": "sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA==" }, + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "license": "MIT", + "peer": true + }, "node_modules/morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -12558,11 +12616,6 @@ "node": ">= 0.8" } }, - "node_modules/mousetrap": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", - "integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==" - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -14476,6 +14529,16 @@ "react": "^18.3.1" } }, + "node_modules/react-hotkeys-hook": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.6.1.tgz", + "integrity": "sha512-XlZpbKUj9tkfgPgT9gA+1p7Ey6vFIZHttUjPqpTdyT5nqQ8mHL7elxvSbaC+dpSiHUSmr21Ya1mDxBZG3aje4Q==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.1", + "react-dom": ">=16.8.1" + } + }, "node_modules/react-instantsearch": { "version": "7.13.4", "resolved": "https://registry.npmjs.org/react-instantsearch/-/react-instantsearch-7.13.4.tgz", @@ -15483,6 +15546,12 @@ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==" }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index 0f3eac1..da777f2 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@asciidoctor/core": "^3.0.4", "@floating-ui/react": "^0.17.0", "@meilisearch/instant-meilisearch": "^0.8.2", + "@monaco-editor/loader": "^1.4.0", + "@monaco-editor/react": "^4.6.0", "@oxide/design-system": "^1.8.2", "@oxide/react-asciidoc": "^1.0.2", "@radix-ui/react-accordion": "^1.1.2", @@ -24,6 +26,8 @@ "@remix-run/node": "2.13.1", "@remix-run/react": "2.13.1", "@remix-run/serve": "2.13.1", + "@sanity/diff-match-patch": "^3.1.1", + "@shikijs/monaco": "^1.24.2", "@tanstack/react-query": "^4.3.9", "@vercel/remix": "^2.13.1", "classnames": "^2.3.1", @@ -36,15 +40,15 @@ "marked": "^4.2.5", "mermaid": "^11.4.1", "mime-types": "^2.1.35", - "mousetrap": "^1.6.5", "octokit": "^3.1.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hotkeys-hook": "^4.6.1", "react-instantsearch": "^7.13.4", "remeda": "^2.17.4", "remix-auth": "^3.6.0", "remix-auth-oauth2": "^1.11.1", - "shiki": "^1.23.1", + "shiki": "^1.5.1", "simple-text-diff": "^1.7.0", "tunnel-rat": "^0.1.2", "zod": "^3.22.3" diff --git a/tsconfig.json b/tsconfig.json index 50339c7..e4d068f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "include": ["vite.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["notes/**"], "compilerOptions": { "incremental": true, "lib": ["DOM", "DOM.Iterable", "ES2021"],