From c1684d28d17a0e5b7c301c0c0e9cd7b0e124743e Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Fri, 23 Aug 2024 15:18:49 +0800 Subject: [PATCH] suggestions modal (#260) --- frontend/app/block/block.less | 8 +- frontend/app/element/input.less | 85 ++++++++ frontend/app/element/input.tsx | 167 +++++++++++++++ frontend/app/element/inputdecoration.less | 21 ++ frontend/app/element/inputdecoration.tsx | 28 +++ frontend/app/hook/useDimensions.tsx | 91 ++++++++ frontend/app/modals/about.tsx | 2 +- frontend/app/modals/modal.less | 37 ++-- frontend/app/modals/modal.tsx | 25 +-- frontend/app/modals/typeaheadmodal.less | 83 +++++++ frontend/app/modals/typeaheadmodal.tsx | 202 ++++++++++++++++++ frontend/app/modals/userinputmodal.tsx | 7 +- frontend/app/store/global.ts | 2 + frontend/app/theme.less | 9 + .../app/view/preview/directorypreview.tsx | 1 + frontend/app/view/preview/preview.tsx | 55 ++++- frontend/types/custom.d.ts | 3 + 17 files changed, 766 insertions(+), 60 deletions(-) create mode 100644 frontend/app/element/input.less create mode 100644 frontend/app/element/input.tsx create mode 100644 frontend/app/element/inputdecoration.less create mode 100644 frontend/app/element/inputdecoration.tsx create mode 100644 frontend/app/hook/useDimensions.tsx create mode 100644 frontend/app/modals/typeaheadmodal.less create mode 100644 frontend/app/modals/typeaheadmodal.tsx diff --git a/frontend/app/block/block.less b/frontend/app/block/block.less index 50a3ace11..8d0d16dc3 100644 --- a/frontend/app/block/block.less +++ b/frontend/app/block/block.less @@ -17,6 +17,7 @@ } .block-content { + position: relative; display: flex; flex-grow: 1; width: 100%; @@ -184,7 +185,8 @@ } } - .block-frame-div-url { + .block-frame-div-url, + .block-frame-div-search { background: rgba(255, 255, 255, 0.1); input { @@ -255,14 +257,16 @@ } &.is-layoutmode .block-mask-inner { + margin-top: 35px; // TODO fix this magic background-color: rgba(0, 0, 0, 0.5); - height: 100%; + height: calc(100% - 35px); width: 100%; display: flex; align-items: center; justify-content: center; .bignum { + margin-top: -15%; font-size: 60px; font-weight: bold; opacity: 0.7; diff --git a/frontend/app/element/input.less b/frontend/app/element/input.less new file mode 100644 index 000000000..408675a2e --- /dev/null +++ b/frontend/app/element/input.less @@ -0,0 +1,85 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +.input { + display: flex; + align-items: center; + border-radius: 6px; + position: relative; + min-height: 32px; + min-width: 200px; + gap: 6px; + border: 2px solid var(--form-element-border-color); + background: var(--form-element-bg-color); + + &:hover { + cursor: text; + } + + &.focused { + border-color: var(--form-element-primary-color); + } + + &.disabled { + opacity: 0.75; + } + + &.error { + border-color: var(--form-element-error-color); + } + + &-inner { + display: flex; + flex-direction: column; + position: relative; + flex-grow: 1; + --inner-padding: 5px 0 5px 16px; + + &-label { + padding: var(--inner-padding); + margin-bottom: -10px; + font-size: 12.5px; + transition: all 0.3s; + color: var(--form-element-label-color); + line-height: 10px; + user-select: none; + + &.float { + font-size: 10px; + top: 5px; + } + + &.offset-left { + left: 0; + } + } + + &-input { + width: 100%; + height: 100%; + border: none; + padding: var(--inner-padding); + font-size: 12px; + outline: none; + background-color: transparent; + color: var(--form-element-text-color); + line-height: 20px; + + &.offset-left { + padding: 5px 16px 5px 0; + } + + &:placeholder-shown { + user-select: none; + } + } + } + + &.no-label { + height: 34px; + + input { + height: 32px; + } + } +} diff --git a/frontend/app/element/input.tsx b/frontend/app/element/input.tsx new file mode 100644 index 000000000..af7919506 --- /dev/null +++ b/frontend/app/element/input.tsx @@ -0,0 +1,167 @@ +import { clsx } from "clsx"; +import React, { forwardRef, useEffect, useRef, useState } from "react"; + +import "./input.less"; + +interface InputDecorationProps { + startDecoration?: React.ReactNode; + endDecoration?: React.ReactNode; +} + +interface InputProps { + label?: string; + value?: string; + className?: string; + onChange?: (value: string) => void; + onKeyDown?: (event: React.KeyboardEvent) => void; + onFocus?: () => void; + onBlur?: () => void; + placeholder?: string; + defaultValue?: string; + decoration?: InputDecorationProps; + required?: boolean; + maxLength?: number; + autoFocus?: boolean; + disabled?: boolean; + isNumber?: boolean; +} + +const Input = forwardRef( + ( + { + label, + value, + className, + onChange, + onKeyDown, + onFocus, + onBlur, + placeholder, + defaultValue = "", + decoration, + required, + maxLength, + autoFocus, + disabled, + isNumber, + }: InputProps, + ref + ) => { + const [focused, setFocused] = useState(false); + const [internalValue, setInternalValue] = useState(defaultValue); + const [error, setError] = useState(false); + const [hasContent, setHasContent] = useState(Boolean(value || defaultValue)); + const inputRef = useRef(null); + + useEffect(() => { + if (value !== undefined) { + setFocused(Boolean(value)); + } + }, [value]); + + const handleComponentFocus = () => { + if (inputRef.current && !inputRef.current.contains(document.activeElement)) { + inputRef.current.focus(); + } + }; + + const handleComponentBlur = () => { + if (inputRef.current?.contains(document.activeElement)) { + inputRef.current.blur(); + } + }; + + const handleFocus = () => { + setFocused(true); + onFocus && onFocus(); + }; + + const handleBlur = () => { + if (inputRef.current) { + const inputValue = inputRef.current.value; + if (required && !inputValue) { + setError(true); + setFocused(false); + } else { + setError(false); + setFocused(false); + } + } + onBlur && onBlur(); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const inputValue = e.target.value; + + if (isNumber && inputValue !== "" && !/^\d*$/.test(inputValue)) { + return; + } + + if (required && !inputValue) { + setError(true); + setHasContent(false); + } else { + setError(false); + setHasContent(Boolean(inputValue)); + } + + if (value === undefined) { + setInternalValue(inputValue); + } + + onChange && onChange(inputValue); + }; + + const inputValue = value ?? internalValue; + + return ( +
+ {decoration?.startDecoration && <>{decoration.startDecoration}} +
+ {label && ( + + )} + +
+ {decoration?.endDecoration && <>{decoration.endDecoration}} +
+ ); + } +); + +export { Input }; +export type { InputDecorationProps, InputProps }; diff --git a/frontend/app/element/inputdecoration.less b/frontend/app/element/inputdecoration.less new file mode 100644 index 000000000..bf4366f92 --- /dev/null +++ b/frontend/app/element/inputdecoration.less @@ -0,0 +1,21 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +.input-decoration { + display: flex; + align-items: center; + justify-content: center; + + i { + font-size: 13px; + color: var(--form-element-icon-color); + } +} + +.input-decoration.start-position { + margin: 0 4px 0 16px; +} + +.input-decoration.end-position { + margin: 0 16px 0 8px; +} diff --git a/frontend/app/element/inputdecoration.tsx b/frontend/app/element/inputdecoration.tsx new file mode 100644 index 000000000..1a92773d0 --- /dev/null +++ b/frontend/app/element/inputdecoration.tsx @@ -0,0 +1,28 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { clsx } from "clsx"; +import * as React from "react"; + +import "./inputdecoration.less"; + +interface InputDecorationProps { + position?: "start" | "end"; + children: React.ReactNode; +} + +const InputDecoration = (props: InputDecorationProps) => { + const { children, position = "end" } = props; + return ( +
+ {children} +
+ ); +}; + +export { InputDecoration }; diff --git a/frontend/app/hook/useDimensions.tsx b/frontend/app/hook/useDimensions.tsx new file mode 100644 index 000000000..7455b2622 --- /dev/null +++ b/frontend/app/hook/useDimensions.tsx @@ -0,0 +1,91 @@ +import debounce from "lodash.debounce"; +import { useCallback, useEffect, useRef, useState } from "react"; + +const useDimensions = (ref: React.RefObject, delay = 0) => { + const [dimensions, setDimensions] = useState<{ + height: number | null; + width: number | null; + widthDirection?: string; + heightDirection?: string; + }>({ + height: null, + width: null, + }); + + const previousDimensions = useRef<{ height: number | null; width: number | null }>({ + height: null, + width: null, + }); + + const updateDimensions = useCallback(() => { + if (ref.current) { + const element = ref.current; + const style = window.getComputedStyle(element); + const paddingTop = parseFloat(style.paddingTop); + const paddingBottom = parseFloat(style.paddingBottom); + const paddingLeft = parseFloat(style.paddingLeft); + const paddingRight = parseFloat(style.paddingRight); + const marginTop = parseFloat(style.marginTop); + const marginBottom = parseFloat(style.marginBottom); + const marginLeft = parseFloat(style.marginLeft); + const marginRight = parseFloat(style.marginRight); + + const parentHeight = element.clientHeight - paddingTop - paddingBottom - marginTop - marginBottom; + const parentWidth = element.clientWidth - paddingLeft - paddingRight - marginLeft - marginRight; + + let widthDirection = ""; + let heightDirection = ""; + + if (previousDimensions.current.width !== null && previousDimensions.current.height !== null) { + if (parentWidth > previousDimensions.current.width) { + widthDirection = "expanding"; + } else if (parentWidth < previousDimensions.current.width) { + widthDirection = "shrinking"; + } else { + widthDirection = "unchanged"; + } + + if (parentHeight > previousDimensions.current.height) { + heightDirection = "expanding"; + } else if (parentHeight < previousDimensions.current.height) { + heightDirection = "shrinking"; + } else { + heightDirection = "unchanged"; + } + } + + previousDimensions.current = { height: parentHeight, width: parentWidth }; + + setDimensions({ height: parentHeight, width: parentWidth, widthDirection, heightDirection }); + } + }, [ref]); + + const fUpdateDimensions = useCallback(delay > 0 ? debounce(updateDimensions, delay) : updateDimensions, [ + updateDimensions, + delay, + ]); + + useEffect(() => { + const resizeObserver = new ResizeObserver(() => { + fUpdateDimensions(); + }); + + if (ref.current) { + resizeObserver.observe(ref.current); + fUpdateDimensions(); + } + + return () => { + if (ref.current) { + resizeObserver.unobserve(ref.current); + } + if (delay > 0) { + fUpdateDimensions.cancel(); + } + }; + }, [fUpdateDimensions]); + + return dimensions; +}; + +export { useDimensions }; diff --git a/frontend/app/modals/about.tsx b/frontend/app/modals/about.tsx index 967867fe0..5368a4739 100644 --- a/frontend/app/modals/about.tsx +++ b/frontend/app/modals/about.tsx @@ -14,7 +14,7 @@ const AboutModal = ({}: AboutModalProps) => { const currentDate = new Date(); return ( - modalsModel.popModal()}> + modalsModel.popModal()}>
diff --git a/frontend/app/modals/modal.less b/frontend/app/modals/modal.less index fabac766f..46ebe01a6 100644 --- a/frontend/app/modals/modal.less +++ b/frontend/app/modals/modal.less @@ -29,42 +29,29 @@ display: flex; flex-direction: column; align-items: flex-start; - gap: 32px; padding: 24px 16px 16px; border-radius: 8px; border: 0.5px solid var(--modal-border-color); background: var(--modal-bg-color); box-shadow: 0px 8px 32px 0px rgba(0, 0, 0, 0.25); - .header-content-wrapper { + .modal-close-btn { + position: absolute; + right: 8px; + top: 8px; + padding: 8px 12px; + + i { + font-size: 18px; + } + } + + .content-wrapper { display: flex; flex-direction: column; gap: 8px; width: 100%; - .modal-header { - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - user-select: none; - - .modal-title { - color: var(--main-text-color); - font-size: var(--title-font-size); - } - - button { - position: absolute; - right: 8px; - top: 8px; - padding: 8px 16px; - - i { - font-size: 18px; - } - } - } .modal-content { width: 100%; padding: 0px 20px; diff --git a/frontend/app/modals/modal.tsx b/frontend/app/modals/modal.tsx index 1b4c591cf..3b0c1852f 100644 --- a/frontend/app/modals/modal.tsx +++ b/frontend/app/modals/modal.tsx @@ -7,22 +7,6 @@ import ReactDOM from "react-dom"; import "./modal.less"; -interface ModalHeaderProps { - description?: string; - onClose?: () => void; -} - -const ModalHeader = ({ onClose, description }: ModalHeaderProps) => ( -
- {description &&

{description}

} - {onClose && ( - - )} -
-); - interface ModalContentProps { children: React.ReactNode; } @@ -56,7 +40,6 @@ const ModalFooter = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }: }; interface ModalProps { - title: string; children?: React.ReactNode; description?: string; okLabel?: string; @@ -71,7 +54,6 @@ interface ModalProps { const Modal = ({ children, className, - title, description, cancelLabel, okLabel, @@ -90,8 +72,10 @@ const Modal = ({
{renderBackdrop(onClickBackdrop)}
-
- + +
{children}
{renderFooter() && ( @@ -123,7 +107,6 @@ const FlexiModal = ({ children, className, onClickBackdrop }: FlexiModalProps) = return ReactDOM.createPortal(renderModal(), document.getElementById("main")); }; -FlexiModal.Header = ModalHeader; FlexiModal.Content = ModalContent; FlexiModal.Footer = ModalFooter; diff --git a/frontend/app/modals/typeaheadmodal.less b/frontend/app/modals/typeaheadmodal.less new file mode 100644 index 000000000..d4a3b506d --- /dev/null +++ b/frontend/app/modals/typeaheadmodal.less @@ -0,0 +1,83 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +.type-ahead-modal-wrapper { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + z-index: var(--zindex-modal-wrapper); + + .type-ahead-modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(21, 23, 21, 0.7); + z-index: var(--zindex-modal-backdrop); + } +} + +.type-ahead-modal { + position: relative; + z-index: var(--zindex-modal); + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 16px; + border-radius: 8px; + border: 0.5px solid var(--modal-border-color); + background: var(--modal-bg-color); + box-shadow: 0px 8px 32px 0px rgba(0, 0, 0, 0.25); + + .modal-close-btn { + position: absolute; + right: 8px; + top: 8px; + padding: 8px 12px; + + i { + font-size: 18px; + } + } + + .content-wrapper { + display: flex; + flex-direction: column; + width: 100%; + min-width: 390px; + + .label { + opacity: 0.5; + font-size: 13px; + white-space: nowrap; + } + + input { + width: 100%; + flex-shrink: 0; + } + + .suggestions-wrapper { + width: 100%; + overflow-y: auto; + overflow-x: hidden; + + .suggestion { + width: 100%; + cursor: pointer; + padding: 7px 10px; + + &:hover { + background-color: var(--highlight-bg-color); + border-radius: 4px; + } + } + } + } +} diff --git a/frontend/app/modals/typeaheadmodal.tsx b/frontend/app/modals/typeaheadmodal.tsx new file mode 100644 index 000000000..8af77ca15 --- /dev/null +++ b/frontend/app/modals/typeaheadmodal.tsx @@ -0,0 +1,202 @@ +import { Input } from "@/app/element/input"; +import { InputDecoration } from "@/app/element/inputdecoration"; +import { useDimensions } from "@/app/hook/useDimensions"; +import clsx from "clsx"; +import React, { useEffect, useRef, useState } from "react"; +import ReactDOM from "react-dom"; + +import "./typeaheadmodal.less"; + +const dummy: SuggestionType[] = [ + { + label: "Apple", + value: "apple", + }, + { + label: "Banana", + value: "banana", + }, + { + label: "Cherry", + value: "cherry", + }, + { + label: "Date", + value: "date", + }, + { + label: "Elderberry", + value: "elderberry", + }, + { + label: "Apple", + value: "apple", + }, + { + label: "Banana", + value: "banana", + }, + { + label: "Cherry", + value: "cherry", + }, + { + label: "Date", + value: "date", + }, + { + label: "Elderberry", + value: "elderberry", + }, + { + label: "Apple", + value: "apple", + }, + { + label: "Banana", + value: "banana", + }, + { + label: "Cherry", + value: "cherry", + }, + { + label: "Date", + value: "date", + }, + { + label: "Elderberry", + value: "elderberry", + }, +]; + +type SuggestionType = { + label: string; + value: string; + icon?: string; +}; + +interface SuggestionsProps { + suggestions?: SuggestionType[]; + onSelect?: (_: string) => void; +} + +function Suggestions({ suggestions, onSelect }: SuggestionsProps) { + const suggestionsWrapperRef = useRef(null); + + return ( +
0 ? "8px" : "0" }} + > + {suggestions?.map((suggestion, index) => ( +
onSelect(suggestion.value)}> + {suggestion.label} +
+ ))} +
+ ); +} + +interface TypeAheadModalProps { + anchor: React.MutableRefObject; + suggestions?: SuggestionType[]; + label?: string; + className?: string; + onSelect?: (_: string) => void; + onChange?: (_: string) => void; + onClickBackdrop?: () => void; + onKeyDown?: (_) => void; +} + +const TypeAheadModal = ({ + className, + suggestions = dummy, + label, + anchor, + onChange, + onSelect, + onClickBackdrop, + onKeyDown, +}: TypeAheadModalProps) => { + const { width, height } = useDimensions(anchor); + const modalRef = useRef(null); + const inputRef = useRef(null); + const [suggestionsHeight, setSuggestionsHeight] = useState(undefined); + + useEffect(() => { + if (modalRef.current && inputRef.current) { + const modalHeight = modalRef.current.getBoundingClientRect().height; + const inputHeight = inputRef.current.getBoundingClientRect().height; + + // Get the padding value (assuming padding is uniform on all sides) + const padding = 16 * 2; // 16px top + 16px bottom + + // Subtract the input height and padding from the modal height + setSuggestionsHeight(modalHeight - inputHeight - padding); + } + }, [width, height]); + + const renderBackdrop = (onClick) =>
; + + const handleKeyDown = (e) => { + onKeyDown && onKeyDown(e); + }; + + const handleChange = (value) => { + onChange && onChange(value); + }; + + const handleSelect = (value) => { + onSelect && onSelect(value); + }; + + const renderModal = () => ( +
+ {renderBackdrop(onClickBackdrop)} +
+
+ +
{label}
+ + ), + }} + /> +
0 ? "8px" : "0", + height: suggestionsHeight, + overflowY: "auto", + }} + > + +
+
+
+
+ ); + + if (anchor.current == null) { + return null; + } + + return ReactDOM.createPortal(renderModal(), anchor.current); +}; + +export { TypeAheadModal }; +export type { SuggestionType }; diff --git a/frontend/app/modals/userinputmodal.tsx b/frontend/app/modals/userinputmodal.tsx index e30e46cce..8b8e8bcf5 100644 --- a/frontend/app/modals/userinputmodal.tsx +++ b/frontend/app/modals/userinputmodal.tsx @@ -108,12 +108,9 @@ const UserInputModal = (userInputRequest: UserInputRequest) => { }, [countdown]); return ( - handleSubmit()} - onCancel={() => handleSendCancel()} - > + handleSubmit()} onCancel={() => handleSendCancel()}>
+ {userInputRequest.title + ` (${countdown}s)`} {queryText} {inputBox}
diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 0a858e4e6..e9221e277 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -123,6 +123,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { // do nothing } const reducedMotionPreferenceAtom = jotai.atom((get) => get(settingsConfigAtom).window.reducedmotion); + const typeAheadModalAtom = jotai.atom({}); atoms = { // initialized in wave.ts (will not be null inside of application) windowId: windowIdAtom, @@ -138,6 +139,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { controlShiftDelayAtom, updaterStatusAtom, reducedMotionPreferenceAtom, + typeAheadModalAtom, }; } diff --git a/frontend/app/theme.less b/frontend/app/theme.less index 527432bed..b2682d015 100644 --- a/frontend/app/theme.less +++ b/frontend/app/theme.less @@ -79,4 +79,13 @@ --button-secondary-color: rgba(255, 255, 255, 0.1); --button-danger-color: #d43434; --button-focus-border-color: rgba(88, 193, 66, 0.8); + + /* form colors */ + --form-element-border-color: rgba(241, 246, 243, 0.15); + --form-element-bg-color: var(--main-bg-color); + --form-element-text-color: var(--main-text-color); + --form-element-primary-text-color: var(--main-text-color); + --form-element-primary-color: var(--accent-color); + --form-element-secondary-color: rgba(255, 255, 255, 0.2); + --form-element-error-color: var(--error-color); } diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index ef43c43a2..6801d9a87 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -26,6 +26,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { quote as shellQuote } from "shell-quote"; import { OverlayScrollbars } from "overlayscrollbars"; + import "./directorypreview.less"; interface DirectoryTableProps { diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 6510b5c72..3f134b72c 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -1,9 +1,10 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { TypeAheadModal } from "@/app/modals/typeaheadmodal"; import { ContextMenuModel } from "@/app/store/contextmenu"; import { Markdown } from "@/element/markdown"; -import { createBlock, globalStore, useBlockAtom } from "@/store/global"; +import { atoms, createBlock, globalStore, useBlockAtom } from "@/store/global"; import * as services from "@/store/services"; import * as WOS from "@/store/wos"; import { getWebServerEndpoint } from "@/util/endpoints"; @@ -13,7 +14,7 @@ import * as util from "@/util/util"; import clsx from "clsx"; import * as jotai from "jotai"; import { loadable } from "jotai/utils"; -import { useEffect, useRef } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { CenteredDiv } from "../../element/quickelems"; import { CodeEditor } from "../codeeditor/codeeditor"; import { CSVView } from "./csvview"; @@ -586,6 +587,7 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel const fileInfo = jotai.useAtomValue(statFileAtom); const ceReadOnly = jotai.useAtomValue(ceReadOnlyAtom); const conn = jotai.useAtomValue(model.connection); + const typeAhead = jotai.useAtomValue(atoms.typeAheadModalAtom); let blockIcon = iconForFile(mimeType, fileName); // ensure consistent hook calls @@ -642,6 +644,34 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel return view; })(); + const handleKeyDown = useCallback( + (waveEvent: WaveKeyboardEvent): boolean => { + if (keyutil.checkKeyPressed(waveEvent, "Cmd:o")) { + globalStore.set(atoms.typeAheadModalAtom, { + ...(typeAhead as TypeAheadModalType), + [blockId]: true, + }); + return true; + } + if (keyutil.checkKeyPressed(waveEvent, "Cmd:d")) { + globalStore.set(atoms.typeAheadModalAtom, { + ...(typeAhead as TypeAheadModalType), + [blockId]: false, + }); + model.giveFocus(); + return; + } + }, + [typeAhead, model, blockId] + ); + + const handleFileSuggestionSelect = (value) => { + globalStore.set(atoms.typeAheadModalAtom, { + ...(typeAhead as TypeAheadModalType), + [blockId]: false, + }); + }; + useEffect(() => { const blockIconOverrideAtom = useBlockAtom(blockId, "blockicon:override", () => { return jotai.atom(null); @@ -650,11 +680,24 @@ function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel }, [blockId, blockIcon]); return ( -
-
- {specializedView} + <> + {typeAhead[blockId] && ( + keyutil.keydownWrapper(handleKeyDown)(e)} + onSelect={handleFileSuggestionSelect} + /> + )} +
keyutil.keydownWrapper(handleKeyDown)(e)} + > +
+ {specializedView} +
-
+ ); } diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index bd6612c26..0917c0e81 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -19,6 +19,7 @@ declare global { controlShiftDelayAtom: jotai.PrimitiveAtom; reducedMotionPreferenceAtom: jotai.Atom; updaterStatusAtom: jotai.PrimitiveAtom; + typeAheadModalAtom: jotai.Primitive; }; type WritableWaveObjectAtom = jotai.WritableAtom; @@ -211,6 +212,8 @@ declare global { left: number; top: number; } + + type TypeAheadModalType = { [key: string]: boolean }; } export {};