diff --git a/web/app/custom.css b/web/app/custom.css new file mode 100644 index 00000000..3d0b3660 --- /dev/null +++ b/web/app/custom.css @@ -0,0 +1,11 @@ +:root { + --link-background-muted: hsl(0, 0%, 97.3%); + --link-border-after: hsl(0, 0%, 91%); + --link-shadow: hsl(240, 5.6%, 82.5%); +} + +.dark { + --link-background-muted: hsl(220, 6.7%, 8.8%); + --link-border-after: hsl(230, 10%, 11.8%); + --link-shadow: hsl(234.9, 27.1%, 25.3%); +} diff --git a/web/app/globals.css b/web/app/globals.css index 8bdc5be5..0b181a0d 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -73,3 +73,4 @@ } @import "./command-palette.css"; +@import "./custom.css"; diff --git a/web/components/routes/link/LinkRoute.tsx b/web/components/routes/link/LinkRoute.tsx index bfbc424a..e6cb4cef 100644 --- a/web/components/routes/link/LinkRoute.tsx +++ b/web/components/routes/link/LinkRoute.tsx @@ -1,13 +1,12 @@ "use client" -import React, { useEffect, useState, useCallback, useRef } from "react" +import React, { useState } from "react" import { LinkHeader } from "@/components/routes/link/header" import { LinkList } from "@/components/routes/link/list" import { LinkManage } from "@/components/routes/link/manage" -import { parseAsBoolean, useQueryState } from "nuqs" -import { atom, useAtom } from "jotai" +import { useQueryState } from "nuqs" +import { atom } from "jotai" import { LinkBottomBar } from "./bottom-bar" -import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette" import { useKey } from "react-use" export const isDeleteConfirmShownAtom = atom(false) @@ -15,44 +14,9 @@ export const isDeleteConfirmShownAtom = atom(false) export function LinkRoute(): React.ReactElement { const [nuqsEditId, setNuqsEditId] = useQueryState("editId") const [activeItemIndex, setActiveItemIndex] = useState(null) - const [isInCreateMode] = useQueryState("create", parseAsBoolean) - const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom) - const [isDeleteConfirmShown] = useAtom(isDeleteConfirmShownAtom) - const [disableEnterKey, setDisableEnterKey] = useState(false) - const timeoutRef = useRef(null) - - const handleCommandPaletteClose = useCallback(() => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - } - - setDisableEnterKey(true) - timeoutRef.current = setTimeout(() => { - setDisableEnterKey(false) - timeoutRef.current = null - }, 100) - }, []) - - useEffect(() => { - if (isDeleteConfirmShown || isCommandPaletteOpen || isInCreateMode) { - setDisableEnterKey(true) - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - timeoutRef.current = null - } - } else if (!isCommandPaletteOpen) { - handleCommandPaletteClose() - } - - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - } - } - }, [isDeleteConfirmShown, isCommandPaletteOpen, isInCreateMode, handleCommandPaletteClose]) + const [keyboardActiveIndex, setKeyboardActiveIndex] = useState(null) useKey("Escape", () => { - setDisableEnterKey(false) setNuqsEditId(null) }) @@ -64,7 +28,8 @@ export function LinkRoute(): React.ReactElement { key={nuqsEditId} activeItemIndex={activeItemIndex} setActiveItemIndex={setActiveItemIndex} - disableEnterKey={disableEnterKey} + keyboardActiveIndex={keyboardActiveIndex} + setKeyboardActiveIndex={setKeyboardActiveIndex} /> diff --git a/web/components/routes/link/bottom-bar.tsx b/web/components/routes/link/bottom-bar.tsx index df2165ab..2dbf44c4 100644 --- a/web/components/routes/link/bottom-bar.tsx +++ b/web/components/routes/link/bottom-bar.tsx @@ -1,11 +1,11 @@ "use client" -import React, { useCallback, useEffect, useRef } from "react" +import React, { useCallback, useEffect, useMemo, useRef } from "react" import { motion, AnimatePresence } from "framer-motion" import type { icons } from "lucide-react" import { Button } from "@/components/ui/button" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" -import { cn, getShortcutKeys } from "@/lib/utils" +import { cn, getShortcutKeys, isEditableElement } from "@/lib/utils" import { LaIcon } from "@/components/custom/la-icon" import { useAtom } from "jotai" import { parseAsBoolean, useQueryState } from "nuqs" @@ -70,13 +70,13 @@ export const LinkBottomBar: React.FC = () => { const handleCreateMode = useCallback(() => { setEditId(null) - setTimeout(() => { + requestAnimationFrame(() => { setCreateMode(prev => !prev) - }, 100) + }) }, [setEditId, setCreateMode]) - useEffect(() => { - setGlobalLinkFormExceptionRefsAtom([ + const exceptionRefs = useMemo( + () => [ overlayRef, contentRef, deleteBtnRef, @@ -85,8 +85,13 @@ export const LinkBottomBar: React.FC = () => { confirmBtnRef, plusBtnRef, plusMoreBtnRef - ]) - }, [setGlobalLinkFormExceptionRefsAtom]) + ], + [] + ) + + useEffect(() => { + setGlobalLinkFormExceptionRefsAtom(exceptionRefs) + }, [setGlobalLinkFormExceptionRefsAtom, exceptionRefs]) const handleDelete = async (e: React.MouseEvent) => { if (!personalLink || !me) return @@ -122,8 +127,9 @@ export const LinkBottomBar: React.FC = () => { const handleKeydown = useCallback( (event: KeyboardEvent) => { const isCreateShortcut = event.key === "c" + const target = event.target as HTMLElement - if (isCreateShortcut) { + if (isCreateShortcut && !isEditableElement(target)) { event.preventDefault() handleCreateMode() } @@ -136,29 +142,26 @@ export const LinkBottomBar: React.FC = () => { const shortcutText = getShortcutKeys(["c"]) return ( - +
{editId && ( - setEditId(null)} /> + setEditId(null)} aria-label="Go back" /> - + )} @@ -171,19 +174,20 @@ export const LinkBottomBar: React.FC = () => { exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.1 }} > - {createMode && } + {createMode && } {!createMode && ( s.symbol).join("")})`} ref={plusBtnRef} + aria-label="New link" /> )} )} - +
) } diff --git a/web/components/routes/link/header.tsx b/web/components/routes/link/header.tsx index 93d14e7c..9215fc27 100644 --- a/web/components/routes/link/header.tsx +++ b/web/components/routes/link/header.tsx @@ -42,7 +42,7 @@ export const LinkHeader = React.memo(() => { {isTablet && ( -
+
)} @@ -115,7 +115,7 @@ const FilterAndSort = React.memo(() => {
- diff --git a/web/components/routes/link/hooks/use-link-actions.ts b/web/components/routes/link/hooks/use-link-actions.ts index 98fb4428..02d576db 100644 --- a/web/components/routes/link/hooks/use-link-actions.ts +++ b/web/components/routes/link/hooks/use-link-actions.ts @@ -9,18 +9,20 @@ export const useLinkActions = () => { try { const index = me.root.personalLinks.findIndex(item => item?.id === link.id) if (index === -1) { - console.error("Delete operation fail", { index, link }) - return + throw new Error(`Link with id ${link.id} not found`) } + me.root.personalLinks.splice(index, 1) + toast.success("Link deleted.", { position: "bottom-right", description: `${link.title} has been deleted.` }) - - me.root.personalLinks.splice(index, 1) } catch (error) { - toast.error("Failed to delete link") + console.error("Failed to delete link:", error) + toast.error("Failed to delete link", { + description: error instanceof Error ? error.message : "An unknown error occurred" + }) } }, []) diff --git a/web/components/routes/link/list.tsx b/web/components/routes/link/list.tsx index 4a4f1f49..46342985 100644 --- a/web/components/routes/link/list.tsx +++ b/web/components/routes/link/list.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo } from "react" +import React, { useCallback, useMemo } from "react" import { DndContext, closestCenter, @@ -8,17 +8,20 @@ import { useSensors, DragEndEvent, DragStartEvent, - UniqueIdentifier + UniqueIdentifier, + MeasuringStrategy, + TouchSensor } from "@dnd-kit/core" -import { Primitive } from "@radix-ui/react-primitive" import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable" +import type { MeasuringConfiguration } from "@dnd-kit/core" +import { restrictToVerticalAxis } from "@dnd-kit/modifiers" import { useAccount } from "@/lib/providers/jazz-provider" import { PersonalLinkLists } from "@/lib/schema/personal-link" import { useAtom } from "jotai" import { linkSortAtom } from "@/store/link" import { useKey } from "react-use" import { LinkItem } from "./partials/link-item" -import { useQueryState } from "nuqs" +import { parseAsBoolean, useQueryState } from "nuqs" import { learningStateAtom } from "./header" import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette" import { useConfirm } from "@omit/react-confirm-dialog" @@ -27,30 +30,43 @@ import { isDeleteConfirmShownAtom } from "./LinkRoute" import { useActiveItemScroll } from "@/hooks/use-active-item-scroll" import { useKeyboardManager } from "@/hooks/use-keyboard-manager" import { useKeydownListener } from "@/hooks/use-keydown-listener" +import { useTouchSensor } from "@/hooks/use-touch-sensor" interface LinkListProps { activeItemIndex: number | null setActiveItemIndex: React.Dispatch> - disableEnterKey: boolean + keyboardActiveIndex: number | null + setKeyboardActiveIndex: React.Dispatch> } -const LinkList: React.FC = ({ activeItemIndex, setActiveItemIndex, disableEnterKey }) => { - const [isCommandPalettePpen] = useAtom(commandPaletteOpenAtom) +const measuring: MeasuringConfiguration = { + droppable: { + strategy: MeasuringStrategy.Always + } +} + +const LinkList: React.FC = ({ + activeItemIndex, + setActiveItemIndex, + keyboardActiveIndex, + setKeyboardActiveIndex +}) => { + const isTouchDevice = useTouchSensor() + const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom) const [, setIsDeleteConfirmShown] = useAtom(isDeleteConfirmShownAtom) const [editId, setEditId] = useQueryState("editId") + const [createMode] = useQueryState("create", parseAsBoolean) const [activeLearningState] = useAtom(learningStateAtom) const [draggingId, setDraggingId] = React.useState(null) + const [sort] = useAtom(linkSortAtom) const { deleteLink } = useLinkActions() const confirm = useConfirm() + const { me } = useAccount({ root: { personalLinks: [] } }) + const { isKeyboardDisabled } = useKeyboardManager("XComponent") - const { me } = useAccount({ - root: { personalLinks: [] } - }) const personalLinks = useMemo(() => me?.root?.personalLinks || [], [me?.root?.personalLinks]) - const [sort] = useAtom(linkSortAtom) - const filteredLinks = useMemo( () => personalLinks.filter(link => { @@ -70,9 +86,9 @@ const LinkList: React.FC = ({ activeItemIndex, setActiveItemIndex ) const sensors = useSensors( - useSensor(PointerSensor, { + useSensor(isTouchDevice ? TouchSensor : PointerSensor, { activationConstraint: { - distance: 8 + distance: 5 } }), useSensor(KeyboardSensor, { @@ -80,51 +96,6 @@ const LinkList: React.FC = ({ activeItemIndex, setActiveItemIndex }) ) - useKey( - event => (event.metaKey || event.ctrlKey) && event.key === "Backspace", - async () => { - if (activeItemIndex !== null) { - setIsDeleteConfirmShown(true) - const activeLink = sortedLinks[activeItemIndex] - if (activeLink) { - const result = await confirm({ - title: `Delete "${activeLink.title}"?`, - description: "This action cannot be undone.", - alertDialogTitle: { - className: "text-base" - }, - cancelButton: { - variant: "outline" - }, - confirmButton: { - variant: "destructive" - } - }) - - if (result) { - if (!me) return - deleteLink(me, activeLink) - - setIsDeleteConfirmShown(false) - } else { - setIsDeleteConfirmShown(false) - } - } - } - }, - { event: "keydown" } - ) - - // on mounted, if editId is set, set activeItemIndex to the index of the item with the editId - useEffect(() => { - if (editId) { - const index = sortedLinks.findIndex(link => link?.id === editId) - if (index !== -1) { - setActiveItemIndex(index) - } - } - }, [editId, sortedLinks, setActiveItemIndex]) - const updateSequences = useCallback((links: PersonalLinkLists) => { links.forEach((link, index) => { if (link) { @@ -133,62 +104,105 @@ const LinkList: React.FC = ({ activeItemIndex, setActiveItemIndex }) }, []) - const { isKeyboardDisabled } = useKeyboardManager("XComponent") + const handleDeleteLink = useCallback(async () => { + if (activeItemIndex === null) return + setIsDeleteConfirmShown(true) + const activeLink = sortedLinks[activeItemIndex] + if (!activeLink || !me) return + + const result = await confirm({ + title: `Delete "${activeLink.title}"?`, + description: "This action cannot be undone.", + alertDialogTitle: { className: "text-base" }, + cancelButton: { variant: "outline" }, + confirmButton: { variant: "destructive" } + }) + + if (result) { + deleteLink(me, activeLink) + } + setIsDeleteConfirmShown(false) + }, [activeItemIndex, sortedLinks, me, confirm, deleteLink, setIsDeleteConfirmShown]) + + useKey(event => (event.metaKey || event.ctrlKey) && event.key === "Backspace", handleDeleteLink, { event: "keydown" }) useKeydownListener((e: KeyboardEvent) => { if ( isKeyboardDisabled || - isCommandPalettePpen || + isCommandPaletteOpen || !me?.root?.personalLinks || sortedLinks.length === 0 || - editId !== null + editId !== null || + e.defaultPrevented ) return - if (e.key === "ArrowUp" || e.key === "ArrowDown") { - e.preventDefault() - setActiveItemIndex(prevIndex => { - if (prevIndex === null) return 0 - const newIndex = - e.key === "ArrowUp" ? Math.max(0, prevIndex - 1) : Math.min(sortedLinks.length - 1, prevIndex + 1) + switch (e.key) { + case "ArrowUp": + case "ArrowDown": + e.preventDefault() + setActiveItemIndex(prevIndex => { + if (prevIndex === null) return 0 - if (e.metaKey && sort === "manual") { - const linksArray = [...me.root.personalLinks] - const newLinks = arrayMove(linksArray, prevIndex, newIndex) + const newIndex = + e.key === "ArrowUp" ? Math.max(0, prevIndex - 1) : Math.min(sortedLinks.length - 1, prevIndex + 1) - while (me.root.personalLinks.length > 0) { - me.root.personalLinks.pop() + if (e.metaKey && sort === "manual") { + const linksArray = [...me.root.personalLinks] + const newLinks = arrayMove(linksArray, prevIndex, newIndex) + + while (me.root.personalLinks.length > 0) { + me.root.personalLinks.pop() + } + + newLinks.forEach(link => { + if (link) { + me.root.personalLinks.push(link) + } + }) + + updateSequences(me.root.personalLinks) } - newLinks.forEach(link => { - if (link) { - me.root.personalLinks.push(link) - } - }) + setKeyboardActiveIndex(newIndex) - updateSequences(me.root.personalLinks) - } - - return newIndex - }) - } else if (e.key === "Enter" && !disableEnterKey && activeItemIndex !== null) { - e.preventDefault() - const activeLink = sortedLinks[activeItemIndex] - if (activeLink) { - setEditId(activeLink.id) - } + return newIndex + }) + break + case "Home": + e.preventDefault() + setActiveItemIndex(0) + break + case "End": + e.preventDefault() + setActiveItemIndex(sortedLinks.length - 1) + break } }) const handleDragStart = useCallback( (event: DragStartEvent) => { if (sort !== "manual") return + if (!me) return + const { active } = event + const activeIndex = me?.root.personalLinks.findIndex(item => item?.id === active.id) + + if (activeIndex === -1) { + console.error("Drag operation fail", { activeIndex, activeId: active.id }) + return + } + + setActiveItemIndex(activeIndex) setDraggingId(active.id) }, - [sort] + [sort, me, setActiveItemIndex] ) + const handleDragCancel = useCallback(() => { + setDraggingId(null) + }, []) + const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event @@ -226,51 +240,64 @@ const LinkList: React.FC = ({ activeItemIndex, setActiveItemIndex }) updateSequences(me.root.personalLinks) - setActiveItemIndex(newIndex) } catch (error) { console.error("Error during link reordering:", error) } } + setActiveItemIndex(null) setDraggingId(null) } - const setElementRef = useActiveItemScroll({ activeIndex: activeItemIndex }) + const { setElementRef } = useActiveItemScroll({ activeIndex: keyboardActiveIndex }) return ( - - +
item?.id || "") || []} strategy={verticalListSortingStrategy}> -
    - {sortedLinks.map( - (linkItem, index) => - linkItem && ( - setElementRef(el, index)} - /> - ) - )} -
+
+
+
+ {sortedLinks.map( + (linkItem, index) => + linkItem && ( + { + if (editId !== null || draggingId !== null || createMode) { + return undefined + } + + setKeyboardActiveIndex(null) + setActiveItemIndex(index) + }} + index={index} + onItemSelected={link => setEditId(link.id)} + data-keyboard-active={keyboardActiveIndex === index} + ref={el => setElementRef(el, index)} + /> + ) + )} +
+
+
- - +
+
) } diff --git a/web/components/routes/link/partials/link-item.tsx b/web/components/routes/link/partials/link-item.tsx index 9a430d8b..1b5c4816 100644 --- a/web/components/routes/link/partials/link-item.tsx +++ b/web/components/routes/link/partials/link-item.tsx @@ -15,34 +15,35 @@ import { cn, ensureUrlProtocol } from "@/lib/utils" import { LEARNING_STATES, LearningStateValue } from "@/lib/constants" import { linkOpenPopoverForIdAtom } from "@/store/link" -interface LinkItemProps extends React.HTMLAttributes { +interface LinkItemProps extends React.HTMLAttributes { personalLink: PersonalLink disabled?: boolean - isEditing: boolean + editId: string | null setEditId: (id: string | null) => void - isDragging: boolean isActive: boolean setActiveItemIndex: (index: number | null) => void index: number + onItemSelected?: (personalLink: PersonalLink) => void } -export const LinkItem = React.forwardRef( - ({ personalLink, disabled, isEditing, setEditId, isDragging, isActive, setActiveItemIndex, index }, ref) => { +export const LinkItem = React.forwardRef( + ( + { personalLink, disabled, editId, setEditId, isActive, setActiveItemIndex, index, onItemSelected, ...props }, + ref + ) => { const [openPopoverForId, setOpenPopoverForId] = useAtom(linkOpenPopoverForIdAtom) const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: personalLink.id, disabled }) const style = useMemo( () => ({ transform: CSS.Transform.toString(transform), - transition, - pointerEvents: isDragging ? "none" : "auto" + transition }), - [transform, transition, isDragging] + [transform, transition] ) const handleSuccess = useCallback(() => setEditId(null), [setEditId]) const handleOnClose = useCallback(() => setEditId(null), [setEditId]) - const handleRowDoubleClick = useCallback(() => setEditId(personalLink.id), [setEditId, personalLink.id]) const selectedLearningState = useMemo( () => LEARNING_STATES.find(ls => ls.value === personalLink.learningState), @@ -58,14 +59,14 @@ export const LinkItem = React.forwardRef( [personalLink, setOpenPopoverForId] ) - if (isEditing) { + if (editId === personalLink.id) { return ( {}} /> ) } return ( -
  • { setNodeRef(node) if (typeof ref === "function") { @@ -75,61 +76,73 @@ export const LinkItem = React.forwardRef( } }} style={style as React.CSSProperties} + {...props} {...attributes} {...listeners} tabIndex={0} - onFocus={() => setActiveItemIndex(index)} - onBlur={() => setActiveItemIndex(null)} - className={cn( - "relative cursor-default outline-none", - "grid grid-cols-[auto_1fr_auto] items-center gap-x-2 py-2 max-lg:px-4 sm:px-5 sm:py-2", - { - "bg-muted-foreground/5": isActive, - "hover:bg-muted/50": !isActive + onDoubleClick={() => onItemSelected?.(personalLink)} + aria-disabled={disabled} + aria-selected={isActive} + data-disabled={disabled} + data-active={isActive} + className="w-full overflow-visible border-b-[0.5px] border-transparent outline-none data-[active='true']:bg-[var(--link-background-muted)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]" + onKeyDown={e => { + if (e.key === "Enter") { + e.preventDefault() + onItemSelected?.(personalLink) } - )} - onDoubleClick={handleRowDoubleClick} + }} > - setOpenPopoverForId(open ? personalLink.id : null)} - > - - - - e.preventDefault()} - > - - - - -
    - {personalLink.icon && ( - {personalLink.title} +
    -

    {personalLink.title}

    + > + setOpenPopoverForId(open ? personalLink.id : null)} + > + + + + + + + + +
    +
    + {personalLink.icon && ( + {personalLink.title} + )} +

    {personalLink.title}

    +
    {personalLink.url && (
    )}
    + +
    + +
    + {personalLink.topic && ( + + {personalLink.topic.prettyName} + + )} +
    -
    - {personalLink.topic && ( - - {personalLink.topic.prettyName} - - )} -
    -
  • +
    +
    ) } ) diff --git a/web/components/routes/page/list.tsx b/web/components/routes/page/list.tsx index 62be86d2..4afc59f5 100644 --- a/web/components/routes/page/list.tsx +++ b/web/components/routes/page/list.tsx @@ -89,7 +89,7 @@ interface PageListItemsProps { } const PageListItems: React.FC = ({ personalPages, activeItemIndex }) => { - const setElementRef = useActiveItemScroll({ activeIndex: activeItemIndex }) + const { setElementRef } = useActiveItemScroll({ activeIndex: activeItemIndex }) return ( = ({ personalTopics, activeItemIndex }) => { - const setElementRef = useActiveItemScroll({ activeIndex: activeItemIndex }) + const { setElementRef } = useActiveItemScroll({ activeIndex: activeItemIndex }) return ( (options: ActiveItemSc const scrollActiveElementIntoView = useCallback((index: number) => { const activeElement = elementRefs.current[index] - activeElement?.scrollIntoView({ block: "nearest" }) + activeElement?.focus() + // activeElement?.scrollIntoView({ block: "nearest" }) }, []) useEffect(() => { @@ -26,5 +27,5 @@ export function useActiveItemScroll(options: ActiveItemSc elementRefs.current[index] = element }, []) - return setElementRef + return { setElementRef, scrollActiveElementIntoView } } diff --git a/web/hooks/use-touch-sensor.ts b/web/hooks/use-touch-sensor.ts new file mode 100644 index 00000000..437aff41 --- /dev/null +++ b/web/hooks/use-touch-sensor.ts @@ -0,0 +1,20 @@ +import { useState, useEffect } from "react" + +export function useTouchSensor() { + const [isTouchDevice, setIsTouchDevice] = useState(false) + + useEffect(() => { + const detectTouch = () => { + setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0) + } + + detectTouch() + window.addEventListener("touchstart", detectTouch, false) + + return () => { + window.removeEventListener("touchstart", detectTouch) + } + }, []) + + return isTouchDevice +} diff --git a/web/lib/utils/index.ts b/web/lib/utils/index.ts index 7e4dfbe3..9be23460 100644 --- a/web/lib/utils/index.ts +++ b/web/lib/utils/index.ts @@ -34,6 +34,24 @@ export function shuffleArray(array: T[]): T[] { return shuffled } +export const isEditableElement = (element: HTMLElement): boolean => { + if (element.isContentEditable) { + return true + } + + const tagName = element.tagName.toLowerCase() + const editableTags = ["input", "textarea", "select", "option"] + + if (editableTags.includes(tagName)) { + return true + } + + const role = element.getAttribute("role") + const editableRoles = ["textbox", "combobox", "listbox"] + + return role ? editableRoles.includes(role) : false +} + export * from "./urls" export * from "./slug" export * from "./keyboard" diff --git a/web/package.json b/web/package.json index 67037938..619a7b73 100644 --- a/web/package.json +++ b/web/package.json @@ -1,127 +1,128 @@ { - "name": "web", - "version": "0.1.0", - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "test": "jest" - }, - "dependencies": { - "@clerk/nextjs": "^5.6.0", - "@dnd-kit/core": "^6.1.0", - "@dnd-kit/sortable": "^8.0.0", - "@hookform/resolvers": "^3.9.0", - "@nothing-but/force-graph": "^0.9.5", - "@nothing-but/utils": "^0.16.0", - "@omit/react-confirm-dialog": "^1.1.5", - "@omit/react-fancy-switch": "^0.1.3", - "@radix-ui/react-alert-dialog": "^1.1.1", - "@radix-ui/react-avatar": "^1.1.0", - "@radix-ui/react-checkbox": "^1.1.1", - "@radix-ui/react-context-menu": "^2.2.1", - "@radix-ui/react-dialog": "^1.1.1", - "@radix-ui/react-dismissable-layer": "^1.1.0", - "@radix-ui/react-dropdown-menu": "^2.1.1", - "@radix-ui/react-focus-scope": "^1.1.0", - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-popover": "^1.1.1", - "@radix-ui/react-scroll-area": "^1.1.0", - "@radix-ui/react-select": "^2.1.1", - "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-switch": "^1.1.0", - "@radix-ui/react-toggle": "^1.1.0", - "@radix-ui/react-toggle-group": "^1.1.0", - "@radix-ui/react-tooltip": "^1.1.2", - "@sentry/nextjs": "^8.30.0", - "@tanstack/react-virtual": "^3.10.8", - "@tiptap/core": "^2.7.2", - "@tiptap/extension-blockquote": "^2.7.2", - "@tiptap/extension-bold": "^2.7.2", - "@tiptap/extension-bullet-list": "^2.7.2", - "@tiptap/extension-code": "^2.7.2", - "@tiptap/extension-code-block-lowlight": "^2.7.2", - "@tiptap/extension-color": "^2.7.2", - "@tiptap/extension-document": "^2.7.2", - "@tiptap/extension-dropcursor": "^2.7.2", - "@tiptap/extension-focus": "^2.7.2", - "@tiptap/extension-gapcursor": "^2.7.2", - "@tiptap/extension-hard-break": "^2.7.2", - "@tiptap/extension-heading": "^2.7.2", - "@tiptap/extension-history": "^2.7.2", - "@tiptap/extension-horizontal-rule": "^2.7.2", - "@tiptap/extension-image": "^2.7.2", - "@tiptap/extension-italic": "^2.7.2", - "@tiptap/extension-link": "^2.7.2", - "@tiptap/extension-list-item": "^2.7.2", - "@tiptap/extension-ordered-list": "^2.7.2", - "@tiptap/extension-paragraph": "^2.7.2", - "@tiptap/extension-placeholder": "^2.7.2", - "@tiptap/extension-strike": "^2.7.2", - "@tiptap/extension-task-item": "^2.7.2", - "@tiptap/extension-task-list": "^2.7.2", - "@tiptap/extension-text": "^2.7.2", - "@tiptap/extension-typography": "^2.7.2", - "@tiptap/pm": "^2.7.2", - "@tiptap/react": "^2.7.2", - "@tiptap/starter-kit": "^2.7.2", - "@tiptap/suggestion": "^2.7.2", - "axios": "^1.7.7", - "cheerio": "1.0.0", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", - "cmdk": "^1.0.0", - "date-fns": "^3.6.0", - "framer-motion": "^11.5.5", - "geist": "^1.3.1", - "jazz-browser-auth-clerk": "0.7.35-guest-auth.5", - "jazz-react": "0.7.35-guest-auth.5", - "jazz-react-auth-clerk": "0.7.35-guest-auth.5", - "jazz-tools": "0.7.35-guest-auth.5", - "jotai": "^2.9.3", - "lowlight": "^3.1.0", - "lucide-react": "^0.429.0", - "next": "14.2.10", - "next-themes": "^0.3.0", - "nuqs": "^1.19.1", - "react": "^18.3.1", - "react-day-picker": "^8.10.1", - "react-dom": "^18.3.1", - "react-hook-form": "^7.53.0", - "react-textarea-autosize": "^8.5.3", - "react-use": "^17.5.1", - "ronin": "^4.3.1", - "slugify": "^1.6.6", - "sonner": "^1.5.0", - "streaming-markdown": "^0.0.14", - "tailwind-merge": "^2.5.2", - "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.4", - "zod": "^3.23.8", - "zsa": "^0.6.0", - "zsa-react": "^0.2.2" - }, - "devDependencies": { - "@ronin/learn-anything": "^0.0.0-3452357373461", - "@testing-library/jest-dom": "^6.5.0", - "@testing-library/react": "^16.0.1", - "@types/jest": "^29.5.13", - "@types/node": "^22.5.5", - "@types/react": "^18.3.7", - "@types/react-dom": "^18.3.0", - "dotenv": "^16.4.5", - "eslint": "^8.57.1", - "eslint-config-next": "14.2.5", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "postcss": "^8.4.47", - "prettier-plugin-tailwindcss": "^0.6.6", - "tailwindcss": "^3.4.12", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "typescript": "^5.6.2" - } + "name": "web", + "version": "0.1.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "test": "jest" + }, + "dependencies": { + "@clerk/nextjs": "^5.6.0", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/modifiers": "^7.0.0", + "@dnd-kit/sortable": "^8.0.0", + "@hookform/resolvers": "^3.9.0", + "@nothing-but/force-graph": "^0.9.5", + "@nothing-but/utils": "^0.16.0", + "@omit/react-confirm-dialog": "^1.1.5", + "@omit/react-fancy-switch": "^0.1.3", + "@radix-ui/react-alert-dialog": "^1.1.1", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-context-menu": "^2.2.1", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dismissable-layer": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-focus-scope": "^1.1.0", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.2", + "@sentry/nextjs": "^8.30.0", + "@tanstack/react-virtual": "^3.10.8", + "@tiptap/core": "^2.7.2", + "@tiptap/extension-blockquote": "^2.7.2", + "@tiptap/extension-bold": "^2.7.2", + "@tiptap/extension-bullet-list": "^2.7.2", + "@tiptap/extension-code": "^2.7.2", + "@tiptap/extension-code-block-lowlight": "^2.7.2", + "@tiptap/extension-color": "^2.7.2", + "@tiptap/extension-document": "^2.7.2", + "@tiptap/extension-dropcursor": "^2.7.2", + "@tiptap/extension-focus": "^2.7.2", + "@tiptap/extension-gapcursor": "^2.7.2", + "@tiptap/extension-hard-break": "^2.7.2", + "@tiptap/extension-heading": "^2.7.2", + "@tiptap/extension-history": "^2.7.2", + "@tiptap/extension-horizontal-rule": "^2.7.2", + "@tiptap/extension-image": "^2.7.2", + "@tiptap/extension-italic": "^2.7.2", + "@tiptap/extension-link": "^2.7.2", + "@tiptap/extension-list-item": "^2.7.2", + "@tiptap/extension-ordered-list": "^2.7.2", + "@tiptap/extension-paragraph": "^2.7.2", + "@tiptap/extension-placeholder": "^2.7.2", + "@tiptap/extension-strike": "^2.7.2", + "@tiptap/extension-task-item": "^2.7.2", + "@tiptap/extension-task-list": "^2.7.2", + "@tiptap/extension-text": "^2.7.2", + "@tiptap/extension-typography": "^2.7.2", + "@tiptap/pm": "^2.7.2", + "@tiptap/react": "^2.7.2", + "@tiptap/starter-kit": "^2.7.2", + "@tiptap/suggestion": "^2.7.2", + "axios": "^1.7.7", + "cheerio": "1.0.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "date-fns": "^3.6.0", + "framer-motion": "^11.5.6", + "geist": "^1.3.1", + "jazz-browser-auth-clerk": "0.7.35-guest-auth.5", + "jazz-react": "0.7.35-guest-auth.5", + "jazz-react-auth-clerk": "0.7.35-guest-auth.5", + "jazz-tools": "0.7.35-guest-auth.5", + "jotai": "^2.10.0", + "lowlight": "^3.1.0", + "lucide-react": "^0.429.0", + "next": "14.2.10", + "next-themes": "^0.3.0", + "nuqs": "^1.19.1", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", + "react-textarea-autosize": "^8.5.3", + "react-use": "^17.5.1", + "ronin": "^4.3.1", + "slugify": "^1.6.6", + "sonner": "^1.5.0", + "streaming-markdown": "^0.0.14", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.4", + "zod": "^3.23.8", + "zsa": "^0.6.0", + "zsa-react": "^0.2.3" + }, + "devDependencies": { + "@ronin/learn-anything": "0.0.0-3452357373461", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@types/jest": "^29.5.13", + "@types/node": "^22.5.5", + "@types/react": "^18.3.8", + "@types/react-dom": "^18.3.0", + "dotenv": "^16.4.5", + "eslint": "^8.57.1", + "eslint-config-next": "14.2.5", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "postcss": "^8.4.47", + "prettier-plugin-tailwindcss": "^0.6.6", + "tailwindcss": "^3.4.12", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "^5.6.2" + } }