Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions apps/web/components/memories-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -542,13 +542,13 @@ export function MemoriesGrid({
{!isEmpty && !isSelectionMode && (
<div
id="filter-pills"
className="flex items-center justify-between gap-4 mb-3 pr-2"
className="mb-3 flex flex-col gap-2 pr-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4"
>
<div className="flex flex-wrap items-center gap-1.5">
<div className="scrollbar-none -mx-1 flex items-center gap-1.5 overflow-x-auto px-1 pb-1 sm:mx-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0">
<Button
className={cn(
dmSansClassName(),
"rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
"h-auto shrink-0 rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs hover:bg-[#00173C] hover:border-[#2261CA33]",
selectedCategories.length === 0 &&
"bg-[#00173C] border-[#2261CA33]",
)}
Expand All @@ -566,7 +566,7 @@ export function MemoriesGrid({
key={facet.category}
className={cn(
dmSansClassName(),
"rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
"h-auto shrink-0 rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs hover:bg-[#00173C] hover:border-[#2261CA33]",
selectedCategoriesSet.has(facet.category) &&
"bg-[#00173C] border-[#2261CA33]",
)}
Expand All @@ -577,7 +577,7 @@ export function MemoriesGrid({
</Button>
))}
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="flex shrink-0 items-center justify-end gap-2">
{/* View mode toggle — segmented control */}
<div
role="tablist"
Expand Down
245 changes: 152 additions & 93 deletions apps/web/components/quick-note-card.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use client"

Check failure on line 1 in apps/web/components/quick-note-card.tsx

View workflow job for this annotation

GitHub Actions / Quality Checks

format

File content differs from formatting output

import { useRef, useCallback } from "react"
import { useRef, useCallback, useEffect, useState } from "react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { Maximize2, Plus, Loader2 } from "lucide-react"
import { useProject } from "@/stores"
import { useQuickNoteDraft } from "@/stores/quick-note-draft"
import { TextEditor } from "./text-editor"

interface QuickNoteCardProps {
onSave: (content: string) => void
Expand All @@ -18,28 +19,23 @@
onMaximize,
isSaving = false,
}: QuickNoteCardProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null)
const [isExpanded, setIsExpanded] = useState(false)
const { selectedProject } = useProject()
const { draft, setDraft } = useQuickNoteDraft(selectedProject)
const draftRef = useRef(draft)
draftRef.current = draft

const handleChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setDraft(e.target.value)
},
const handleContentChange = useCallback(
(content: string) => setDraft(content),
[setDraft],
)

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
e.preventDefault()
if (draft.trim() && !isSaving) {
onSave(draft)
}
}
},
[draft, isSaving, onSave],
)
const handleEditorSubmit = useCallback(() => {
const currentDraft = draftRef.current
if (currentDraft.trim() && !isSaving) {
onSave(currentDraft)
}
}, [isSaving, onSave])

const handleSaveClick = useCallback(() => {
if (draft.trim() && !isSaving) {
Expand All @@ -51,109 +47,172 @@
onMaximize(draft)
}, [draft, onMaximize])

const handleBlurCapture = useCallback(
(e: React.FocusEvent<HTMLDivElement>) => {
if (e.currentTarget.contains(e.relatedTarget as Node | null)) return
setIsExpanded(false)
},
[],
)

const handleBackdropPointerDown = useCallback(() => {
const activeElement = document.activeElement
if (activeElement instanceof HTMLElement) {
activeElement.blur()
}
setIsExpanded(false)
}, [])

useEffect(() => {
if (!isExpanded) return

const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Escape") return
const activeElement = document.activeElement
if (activeElement instanceof HTMLElement) {
activeElement.blur()
}
setIsExpanded(false)
}

window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [isExpanded])

const canSave = draft.trim().length > 0 && !isSaving
const hasDraft = draft.trim().length > 0
const editorHeight =

Check warning on line 84 in apps/web/components/quick-note-card.tsx

View workflow job for this annotation

GitHub Actions / Quality Checks

lint/correctness/noUnusedVariables

This variable editorHeight is unused.
isExpanded
? "min-h-[min(58dvh,520px)] sm:min-h-[min(54vh,560px)]"
: hasDraft
? "min-h-[188px]"
: "min-h-[120px]"

return (
<div
className="bg-[#1B1F24] rounded-[22px] p-1"
style={{
boxShadow:
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}}
>
<>
{isExpanded && (
<div
className="fixed inset-0 z-[60] bg-[#05080D]/45 backdrop-blur-[10px]"
onPointerDown={handleBackdropPointerDown}
aria-hidden
/>
)}
<div
id="quick-note-inner"
className="bg-[#0B1017] rounded-[18px] p-3 relative"
className={cn(
"relative w-full rounded-[22px] bg-[#1B1F24] p-1 transition-[box-shadow,transform,width] duration-200",
isExpanded &&
"z-[70] w-[min(calc(100vw-1.5rem),640px)] scale-[1.01]",
)}
style={{
boxShadow: "inset 1.421px 1.421px 4.263px 0 rgba(11, 15, 21, 0.4)",
boxShadow: isExpanded
? "0 24px 80px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255,255,255,0.08), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset"
: "0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}}
>
<button
type="button"
onClick={handleMaximizeClick}
className="absolute top-3 right-3 text-[#737373] hover:text-white transition-colors cursor-pointer"
aria-label="Expand to full screen"
>
<Maximize2 className="size-[14px]" />
</button>

<textarea
ref={textareaRef}
value={draft}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="Start writing..."
disabled={isSaving}
<div
id="quick-note-inner"
className={cn(
dmSansClassName(),
"w-full h-[120px] bg-transparent resize-none outline-none text-[12px] leading-normal text-white placeholder:text-[#737373] pr-5 disabled:opacity-50",
"relative flex flex-col rounded-[18px] bg-[#0B1017] p-3",
isExpanded &&
"fixed inset-x-3 top-[calc(env(safe-area-inset-top)+72px)] z-[71] max-h-[calc(100dvh-96px)] sm:static sm:inset-auto",
)}
/>

<div
id="quick-note-action-bar"
className="bg-[#1B1F24] rounded-[8px] px-2 py-1.5 flex items-center justify-center gap-8 w-full"
onFocusCapture={() => setIsExpanded(true)}
onBlurCapture={handleBlurCapture}
style={{
boxShadow:
"0 4px 20px 0 rgba(0, 0, 0, 0.25), inset 1px 1px 1px 0 rgba(255, 255, 255, 0.1)",
boxShadow: "inset 1.421px 1.421px 4.263px 0 rgba(11, 15, 21, 0.4)",
}}
>
<button
type="button"
onClick={handleSaveClick}
disabled={!canSave}
onClick={handleMaximizeClick}
className="absolute top-3 right-3 text-[#737373] hover:text-white transition-colors cursor-pointer"
aria-label="Expand to full screen"
>
<Maximize2 className="size-[14px]" />
</button>

<div
className={cn(
"flex items-center gap-1.5 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
dmSansClassName(),
"min-h-0 w-full flex-1 overflow-y-auto pr-5 text-white disabled:opacity-50",
"[&_.ProseMirror]:text-[12px] [&_.ProseMirror]:leading-normal [&_.ProseMirror_p.is-editor-empty:first-child::before]:text-[#737373]",
)}
aria-disabled={isSaving}
>
<span className="flex items-center gap-1.5">
{isSaving ? (
<Loader2 className="size-2 animate-spin text-[#fafafa]" />
) : (
<Plus className="size-2 text-[#fafafa]" />
)}
<span
className={cn(
dmSansClassName(),
"text-[10px] font-medium text-[#fafafa]",
)}
>
{isSaving ? "Saving..." : "Save note"}
</span>
</span>
<TextEditor
content={draft}
onContentChange={handleContentChange}
onSubmit={handleEditorSubmit}
debounceMs={0}
editable={!isSaving}
/>
</div>

<span
<div
id="quick-note-action-bar"
className="mt-3 flex w-full items-center justify-center gap-8 rounded-[8px] bg-[#1B1F24] px-2 py-1.5"
style={{
boxShadow:
"0 4px 20px 0 rgba(0, 0, 0, 0.25), inset 1px 1px 1px 0 rgba(255, 255, 255, 0.1)",
}}
>
<button
type="button"
onClick={handleSaveClick}
disabled={!canSave}
className={cn(
"bg-[rgba(33,33,33,0.5)] border border-[rgba(115,115,115,0.2)] rounded px-1 py-0.5 flex items-center gap-1 h-4",
"flex items-center gap-1.5 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
)}
>
<svg
className="size-[10px]"
viewBox="0 0 9 9"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Command Key</title>
<path
d="M6.67 0.42C6.34 0.42 6.02 0.55 5.78 0.78C5.55 1.02 5.42 1.34 5.42 1.67V6.67C5.42 7 5.55 7.32 5.78 7.55C6.02 7.78 6.34 7.92 6.67 7.92C7 7.92 7.32 7.78 7.55 7.55C7.78 7.32 7.92 7 7.92 6.67C7.92 6.34 7.78 6.02 7.55 5.78C7.32 5.55 7 5.42 6.67 5.42H1.67C1.34 5.42 1.02 5.55 0.78 5.78C0.55 6.02 0.42 6.34 0.42 6.67C0.42 7 0.55 7.32 0.78 7.55C1.02 7.78 1.34 7.92 1.67 7.92C2 7.92 2.32 7.78 2.55 7.55C2.78 7.32 2.92 7 2.92 6.67V1.67C2.92 1.34 2.78 1.02 2.55 0.78C2.32 0.55 2 0.42 1.67 0.42C1.34 0.42 1.02 0.55 0.78 0.78C0.55 1.02 0.42 1.34 0.42 1.67C0.42 2 0.55 2.32 0.78 2.55C1.02 2.78 1.34 2.92 1.67 2.92H6.67C7 2.92 7.32 2.78 7.55 2.55C7.78 2.32 7.92 2 7.92 1.67C7.92 1.34 7.78 1.02 7.55 0.78C7.32 0.55 7 0.42 6.67 0.42Z"
stroke="#737373"
strokeWidth="0.833333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span className="flex items-center gap-1.5">
{isSaving ? (
<Loader2 className="size-2 animate-spin text-[#fafafa]" />
) : (
<Plus className="size-2 text-[#fafafa]" />
)}
<span
className={cn(
dmSansClassName(),
"text-[10px] font-medium text-[#fafafa]",
)}
>
{isSaving ? "Saving..." : "Save note"}
</span>
</span>

<span
className={cn(
dmSansClassName(),
"text-[10px] font-medium text-[#737373]",
"bg-[rgba(33,33,33,0.5)] border border-[rgba(115,115,115,0.2)] rounded px-1 py-0.5 flex items-center gap-1 h-4",
)}
>
Enter
<svg
className="size-[10px]"
viewBox="0 0 9 9"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Command Key</title>
<path
d="M6.67 0.42C6.34 0.42 6.02 0.55 5.78 0.78C5.55 1.02 5.42 1.34 5.42 1.67V6.67C5.42 7 5.55 7.32 5.78 7.55C6.02 7.78 6.34 7.92 6.67 7.92C7 7.92 7.32 7.78 7.55 7.55C7.78 7.32 7.92 7 7.92 6.67C7.92 6.34 7.78 6.02 7.55 5.78C7.32 5.55 7 5.42 6.67 5.42H1.67C1.34 5.42 1.02 5.55 0.78 5.78C0.55 6.02 0.42 6.34 0.42 6.67C0.42 7 0.55 7.32 0.78 7.55C1.02 7.78 1.34 7.92 1.67 7.92C2 7.92 2.32 7.78 2.55 7.55C2.78 7.32 2.92 7 2.92 6.67V1.67C2.92 1.34 2.78 1.02 2.55 0.78C2.32 0.55 2 0.42 1.67 0.42C1.34 0.42 1.02 0.55 0.78 0.78C0.55 1.02 0.42 1.34 0.42 1.67C0.42 2 0.55 2.32 0.78 2.55C1.02 2.78 1.34 2.92 1.67 2.92H6.67C7 2.92 7.32 2.78 7.55 2.55C7.78 2.32 7.92 2 7.92 1.67C7.92 1.34 7.78 1.02 7.55 0.78C7.32 0.55 7 0.42 6.67 0.42Z"
stroke="#737373"
strokeWidth="0.833333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span
className={cn(
dmSansClassName(),
"text-[10px] font-medium text-[#737373]",
)}
>
Enter
</span>
</span>
</span>
</button>
</button>
</div>
</div>
</div>
</div>
</>
)
}
14 changes: 13 additions & 1 deletion apps/web/components/text-editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ export function TextEditor({
onContentChange,
onSubmit,
debounceMs = 500,
editable = true,
}: {
content: string | undefined
onContentChange: (content: string) => void
onSubmit: () => void
debounceMs?: number
editable?: boolean
}) {
const containerRef = useRef<HTMLDivElement>(null)
const editorRef = useRef<Editor | null>(null)
Expand All @@ -44,6 +46,7 @@ export function TextEditor({
extensions,
content: initialContent,
contentType: "markdown",
editable,
immediatelyRender: true,
onCreate: ({ editor }) => {
editorRef.current = editor
Expand Down Expand Up @@ -86,12 +89,21 @@ export function TextEditor({
})

useEffect(() => {
if (editor && initialContent) {
if (editor && initialContent !== undefined) {
const json = editor.getJSON()
const currentContent =
editor.storage.markdown?.manager?.serialize(json) ?? ""
if (currentContent === initialContent) return

hasUserEditedRef.current = false
editor.commands.setContent(initialContent, { contentType: "markdown" })
}
}, [editor, initialContent])

useEffect(() => {
editor?.setEditable(editable)
}, [editor, editable])

const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement
if (target.closest(".ProseMirror")) {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"portless": { "name": "app.dev.supermemory", "script": "dev:app" },
"scripts": {
"dev": "portless",
"dev:app": "next dev --port ${PORT:-3000}",
"dev:app": "next dev --port $PORT",
"build": "next build",
"start": "next start",
"lint": "biome check --write",
Expand Down
Loading
Loading