UNPKG

@runstack/ui

Version:

React UI components library for runstack

775 lines (761 loc) 26.7 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { Button: () => Button, DragHandle: () => DragHandle, DraggableListEditableText: () => DraggableListEditableText, DraggableListItem: () => DraggableListItem, DraggableListItemBottom: () => DraggableListItemBottom, DraggableListItemContent: () => DraggableListItemContent, DraggableListItemEnd: () => DraggableListItemEnd, DraggableListItemStart: () => DraggableListItemStart, DraggableListMain: () => DraggableListMain, DraggableListProvider: () => DraggableListProvider, ENTRY_GAP: () => ENTRY_GAP, calculateIndentDecrease: () => calculateIndentDecrease, calculateIndentIncrease: () => calculateIndentIncrease, cn: () => cn, flattenTreePreorder: () => flattenTreePreorder, getAllDescendants: () => getAllDescendants, getChildren: () => getChildren, getPreviousSibling: () => getPreviousSibling, getRootItems: () => getRootItems, getSiblings: () => getSiblings, useDraggableListContext: () => useDraggableListContext }); module.exports = __toCommonJS(index_exports); // src/draggable-list/index.tsx var React = __toESM(require("react")); var import_react = require("motion/react"); var import_react2 = require("react"); // src/lib/utils.ts var import_clsx = require("clsx"); var import_tailwind_merge = require("tailwind-merge"); function cn(...inputs) { return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs)); } // src/draggable-list/index.tsx var import_core = require("@dnd-kit/core"); // src/draggable-list/utils/shared.ts function sortByRank(a, b) { return Number(a.rank) - Number(b.rank); } // src/draggable-list/utils/tree-traversal.ts function getChildren(id, items) { return items.filter((item) => item.parent_id === id).sort(sortByRank); } function getSiblings(id, items) { if (id === "root") return items.filter((item2) => !item2.parent_id).sort(sortByRank); const item = items.find((i) => i.sync_id === id); if (!item) return []; const parent = items.find((i) => i.sync_id === item.parent_id); if (!parent) return items.filter((item2) => !item2.parent_id).sort(sortByRank); return items.filter((item2) => item2.parent_id === parent?.sync_id).sort(sortByRank); } function isFirstChild(id, items) { const siblings = getSiblings(id, items); return siblings.findIndex((s) => s.sync_id === id) === 0; } function isLastChild(id, items) { const siblings = getSiblings(id, items); return siblings.findIndex((s) => s.sync_id === id) === siblings.length - 1; } function getPreviousSibling(id, items) { const siblings = getSiblings(id, items); const index = siblings.findIndex((s) => s.sync_id === id); return siblings[index - 1]; } function getNextSibling(id, items) { const siblings = getSiblings(id, items); const index = siblings.findIndex((s) => s.sync_id === id); return siblings[index + 1]; } function getAllDescendants(id, items) { const result = []; const children = getChildren(id, items); for (const child of children) { result.push(child); result.push(...getAllDescendants(child.sync_id, items)); } return result; } function getRootItems(items) { return items.filter((item) => !item.parent_id).sort(sortByRank); } // src/draggable-list/utils/constants.ts var ENTRY_GAP = 1e5; // src/draggable-list/utils/indentation.ts function calculateIndentIncrease(item, items = []) { const previousSibling = getPreviousSibling(item.sync_id, items); if (!previousSibling) return []; const previousSiblingChildren = getChildren(previousSibling.sync_id, items); const updates = [ ...previousSiblingChildren, { ...item, parent_id: previousSibling.sync_id } ].map((child, index) => { return { ...child, sync_id: child.sync_id, parent_id: child.parent_id, rank: BigInt(index * ENTRY_GAP) // Rank system: 0, 100000, 200000, etc. }; }); return updates; } function calculateIndentDecrease(item, items = []) { const parent = items.find((i) => i.sync_id === item.parent_id); if (!parent) return []; const parentSiblings = getSiblings(parent.sync_id, items); const parentIndex = parentSiblings.findIndex( (s) => s.sync_id === parent.sync_id ); const targetIndex = parentIndex + 1; const updates = [ ...parentSiblings.slice(0, targetIndex), { ...item, parent_id: parent.parent_id ?? null }, // Promote to parent's level ...parentSiblings.slice(targetIndex) ].map((sibling, index) => { return { ...sibling, sync_id: sibling.sync_id, parent_id: sibling.parent_id, rank: BigInt(index * ENTRY_GAP) // Recalculate ranks with gaps }; }); return updates; } // src/draggable-list/utils/flatten.ts function flattenTreePreorder(tree) { const result = {}; const index = { current: 0 }; const sortedTree = tree.sort(sortByRank); const walk = (item) => { result[item.sync_id] = index.current++; const children = getChildren(item.sync_id, sortedTree); for (const child of children) { walk(child); } }; const rootItems = sortedTree.filter((item) => !item.parent_id); for (const root of rootItems) { walk(root); } return result; } // src/draggable-list/handlers/drag-handlers.ts var createDragEndHandler = (data, onDrop) => { return (event) => { console.log("createDragEndHandler"); if (!event.over) return; const itemId = event.active.id; const position = getPositionFromDroppable(event.over.id.toString()); console.log("createDragEndHandler: position", position); if (!position) return; let updates; if (position === "last_root") { console.log("createDragEndHandler: dropEntryAsLastRoot"); updates = dropEntryAsLastRoot(itemId, data); } else { console.log("createDragEndHandler: dropEntry"); const dropId = getEntryIdFromDroppable(event.over.id.toString()); console.log("createDragEndHandler: dropId", dropId); updates = dropEntry(itemId, dropId, position, data); } console.log("createDragEndHandler: updates", updates); if (!updates.length) return; console.log("actually drop"); onDrop?.(updates); }; }; function getEntryIdFromDroppable(droppableId) { return droppableId.split("--")[2] ?? ""; } function getPositionFromDroppable(droppableId) { const position = droppableId.split("--")[1]; const isValid = position && ["before", "after", "last_root"].includes(position); return isValid ? position : null; } function dropEntryAsLastRoot(entryId, items) { const entry = items.find((i) => i.sync_id === entryId); if (!entry) return []; const rootItems = getRootItems(items); return [...rootItems, { ...entry, parent_id: null }].map( (child, index) => { return { ...entry, sync_id: child.sync_id, parent_id: child.parent_id, rank: BigInt(Number(index) * ENTRY_GAP), title: child.title }; } ); } function dropEntry(entryId, targetId, position, items) { const entry = items.find((i) => i.sync_id === entryId); const target = items.find((i) => i.sync_id === targetId); if (!entry || !target) return []; const targetHasChildren = position === "after" && getChildren(targetId, items).length > 0; if (targetHasChildren) { const targetChildren = getChildren(targetId, items).filter( (child) => child.sync_id !== entryId ); const updates = [ { ...entry, parent_id: target.sync_id }, ...targetChildren ].map((child, index) => { return { ...child, sync_id: child.sync_id, parent_id: child.parent_id, rank: BigInt(Number(index) * ENTRY_GAP), title: child.title }; }); return updates; } else { const newParentId = target.parent_id; const targetSiblings = getSiblings(target.sync_id, items).filter( (s) => s.sync_id !== entryId ); const targetIndex = targetSiblings.findIndex((s) => s.sync_id === targetId); const insertIndex = position === "before" ? targetIndex : targetIndex + 1; const updates = [ ...targetSiblings.slice(0, insertIndex), { ...entry, parent_id: newParentId }, ...targetSiblings.slice(insertIndex) ].map((sibling, index) => { return { ...sibling, sync_id: sibling.sync_id, parent_id: sibling.parent_id, rank: BigInt(Number(index) * ENTRY_GAP), // Recalculate ranks with gaps title: sibling.title }; }); return updates; } } // src/draggable-list/index.tsx var import_jsx_runtime = require("react/jsx-runtime"); var DraggableListContext = (0, import_react2.createContext)({ items: [], index: -1, level: 0, hasAnimated: false, updateHasAnimated: () => { }, updateFocusItemId: () => { }, onAddEntry: () => { }, focusItemId: "", updateTitle: () => { }, updateTree: () => { }, renderProps: () => null }); var useDraggableListContext = () => { const context = (0, import_react2.useContext)(DraggableListContext); if (!context) { throw new Error( "useDraggableListContext must be used within a DraggableListProvider" ); } return context; }; var DraggableListProvider = (props) => { const sensors = (0, import_core.useSensors)((0, import_core.useSensor)(import_core.PointerSensor)); const rootItems = getRootItems(props.data); const [focusItemId, setFocusItemId] = React.useState(""); const [hasAnimated, setHasAnimated] = React.useState(false); React.useEffect(() => { setHasAnimated(false); }, [props._animationKey]); const handleDragEnd = React.useMemo( () => createDragEndHandler(props.data, props.onDrop), [props.data, props.onDrop] ); const initialContext = { items: props.data, index: -1, level: -1, hasAnimated, updateHasAnimated: setHasAnimated, focusItemId, updateFocusItemId: setFocusItemId, updateTitle: props.onTitleChange ?? (() => { }), updateTree: props.onTreeChange ?? (() => { }), renderProps: props.children, onAddEntry: props.onAddEntry ?? (() => { }) }; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_core.DndContext, { sensors, onDragEnd: handleDragEnd, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DraggableListContext.Provider, { value: initialContext, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DraggableOverlayMonitor, { children: [ rootItems.map((item, index) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( DraggableListContext.Provider, { value: { ...initialContext, items: props.data, index, level: 0, currentItem: item }, children: props.children( item, { index, level: 0, focusItemId, updateFocusItemId: setFocusItemId }, initialContext ) }, item.sync_id ); }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DroppableSensorLast, {}) ] }) }) }); }; var DraggableOverlayMonitor = (props) => { const [active, updateActive] = React.useState(null); (0, import_core.useDndMonitor)({ onDragStart(event) { updateActive(event.active); }, onDragEnd() { updateActive(null); } }); const context = useDraggableListContext(); const dragged = context.items.find((item) => item.sync_id === active?.id); const width = active?.data.current?.width; return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ props.children, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_core.DragOverlay, { children: dragged ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DraggableListContext, { value: { ...context, currentItem: dragged }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "opacity-75", style: width ? { width } : {}, children: context.renderProps(dragged, { index: context.index, level: context.level, focusItemId: context.focusItemId, updateFocusItemId: context.updateFocusItemId }) }) }) : null }) ] }); }; var DraggableListMain = (props) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DroppableIndicatorBefore, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("flex mb-1 gap-2 relative", props.className), children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DroppableSensorBefore, {}), props.children, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DroppableSensorAfter, {}) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DroppableIndicatorAfter, {}) ] }); }; var DroppableSensorBefore = () => { const context = useDraggableListContext(); const droppable = (0, import_core.useDroppable)({ id: _makeDroppableId("before", context.currentItem?.sync_id) }); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { ref: droppable.setNodeRef, className: cn( "bg-red-500 opacity-50 w-full h-1/2 absolute top-0 left-0 z-10 pointer-events-none opacity-0", { "bg-blue-300": droppable.isOver } ) } ); }; var DroppableSensorAfter = () => { const context = useDraggableListContext(); const droppable = (0, import_core.useDroppable)({ id: _makeDroppableId("after", context.currentItem?.sync_id) }); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { ref: droppable.setNodeRef, className: cn( "bg-yellow-500 opacity-50 w-full h-1/2 absolute bottom-0 left-0 z-10 opacity-0 pointer-events-none", { "bg-blue-300": droppable.isOver } ) } ); }; var DroppableSensorLast = () => { const droppable = (0, import_core.useDroppable)({ id: "droppable--last_root" }); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: droppable.setNodeRef, className: cn("w-full h-10", {}), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { className: cn({ "bg-blue-500 h-[5px] rounded-full": droppable.isOver }) } ) }); }; var DroppableIndicatorBefore = () => { const context = useDraggableListContext(); const [over, setOver] = React.useState(null); (0, import_core.useDndMonitor)({ onDragOver(event) { setOver(event.over); }, onDragEnd() { setOver(null); } }); if (!context.currentItem) return null; const sibling = { previous: { ...getPreviousSibling(context.currentItem.sync_id, context.items), get hasNoChildren() { return this.sync_id ? getChildren(this.sync_id, context.items).length === 0 : false; } } }; const hover = { overParent: over?.id === _makeDroppableId("after", context.currentItem.parent_id), overSelf: over?.id === _makeDroppableId("before", context.currentItem.sync_id), overPreviousSiblingBottom: over?.id === _makeDroppableId("after", sibling.previous?.sync_id) }; const currentItem = { isFirstChild: isFirstChild(context.currentItem.sync_id, context.items) }; const isActive = hover.overParent && currentItem.isFirstChild || hover.overSelf || hover.overPreviousSiblingBottom && sibling.previous.hasNoChildren; const height = isActive ? 5 : 0; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react.motion.div, { className: cn("w-full rounded-lg bg-gray-500 invisible", { "visible my-2 bg-blue-500": isActive }), style: { height } } ); }; var DroppableIndicatorAfter = () => { const context = useDraggableListContext(); const [over, setOver] = React.useState(null); (0, import_core.useDndMonitor)({ onDragOver(event) { setOver(event.over); }, onDragEnd() { setOver(null); } }); if (!context.currentItem) return null; const hover = { overSelf: over?.id === _makeDroppableId("after", context.currentItem.sync_id) }; const currentItem = { isLastChild: isLastChild(context.currentItem.sync_id, context.items), hasNoChildren: getChildren(context.currentItem.sync_id, context.items).length === 0 }; const isActive = hover.overSelf && currentItem.isLastChild && currentItem.hasNoChildren; const height = isActive ? 5 : 0; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react.motion.div, { className: cn("w-full bg-yellow-500 rounded-lg invisible ", { "visible my-2 bg-blue-500": isActive }), style: { height } } ); }; var DraggableListItem = (props) => { const context = useDraggableListContext(); const children = context.currentItem ? getChildren(context.currentItem.sync_id, context.items) : []; const flatIndexMap = flattenTreePreorder(context.items); const flatIndex = flatIndexMap[context.currentItem?.sync_id ?? ""] ?? 0; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( DraggableItemContainer, { id: context.currentItem?.sync_id ?? "NOOP", className: props.className, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("relative w-full"), children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_react.motion.div, { initial: context.hasAnimated ? "visible" : "hidden", animate: "visible", variants: { hidden: { opacity: 0, translateX: -10 }, visible: { opacity: 1, translateX: 0 } }, onAnimationComplete: () => { context.updateHasAnimated(true); }, transition: { delay: flatIndex * 0.1 }, children: props.children }, context.currentItem?.sync_id ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pl-5", children: children.map((item, index) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( DraggableListContext.Provider, { value: { ...context, currentItem: item, index, level: context.level + 1 }, children: context.renderProps(item, { index, level: context.level + 1, focusItemId: context.focusItemId, updateFocusItemId: context.updateFocusItemId }) }, item.sync_id ); }) }) ] }) } ); }; var DraggableListItemStart = (props) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn(props.className), children: props.children }); }; var DraggableListItemContent = (props) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex-1", children: props.children }); }; var DraggableListItemBottom = (props) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "", children: props.children }); }; var DraggableListItemEnd = (props) => { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: props.className, children: props.children }); }; var DragHandle = (props) => { const context = useDraggableListContext(); const draggable = (0, import_core.useDraggable)({ id: `${context.currentItem?.sync_id}` }); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { className: "flex items-center justify-center w-8 h-8 cursor-grab", ...draggable.listeners, children: props.children } ); }; var DraggableItemContainer = (props) => { const elRef = React.useRef(null); const draggable = (0, import_core.useDraggable)({ id: `${props.id}`, data: { width: elRef.current?.getBoundingClientRect().width } }); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { ref: (node) => { draggable.setNodeRef(node); elRef.current = node; }, ...draggable.attributes, className: cn(props.className, { "opacity-0 invisible": draggable.isDragging }), children: props.children } ); }; function _makeDroppableId(position, itemId = "") { return `droppable--${position}--${itemId}`; } // src/draggable-list/draggable-list-editable-text.tsx var import_react3 = require("react"); var import_react_textarea_autosize = __toESM(require("react-textarea-autosize")); var import_lodash_es = require("lodash-es"); var import_jsx_runtime2 = require("react/jsx-runtime"); var DraggableListEditableText = (props) => { const context = useDraggableListContext(); const [value, updateValue] = (0, import_react3.useState)(context.currentItem?.title ?? ""); (0, import_react3.useEffect)(() => { updateValue(context.currentItem?.title ?? ""); }, [context.currentItem?.title]); const textareaRef = (0, import_react3.useRef)(null); (0, import_react3.useEffect)(() => { if (context.focusItemId === context.currentItem?.sync_id) { textareaRef.current?.focus(); } }, [context.focusItemId, context.currentItem?.sync_id]); (0, import_react3.useEffect)(() => { if (!textareaRef.current) return; moveCursorToEnd(textareaRef.current); }, []); return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cn("w-full", props.className), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_react_textarea_autosize.default, { value, ref: textareaRef, onFocus: () => { if (!context.currentItem) return; context.updateFocusItemId(context.currentItem.sync_id); }, onBlur: () => { context.updateFocusItemId(""); }, style: { resize: "none" }, onChange: (e) => { updateValue(e.target.value); if (!context.currentItem) return; context.updateTitle(context.currentItem.sync_id, e.target.value); }, className: cn( "w-full bg-transparent m-0 p-0 outline-none", props.className ), onKeyDown: createKeydownHandlers(context, value), placeholder: props.placeholder ?? "Enter a title" } ) }); }; function createKeydownHandlers(context, value) { return (e) => { if (!context.currentItem) return; const item = { ...context.currentItem, title: value }; const flatIndexMap = flattenTreePreorder(context.items); const flatIndexMapInvert = (0, import_lodash_es.invert)(flatIndexMap); if (e.key === "Tab") { e.preventDefault(); if (e.shiftKey) { context.updateTree(calculateIndentDecrease(item, context.items)); return; } context.updateTree(calculateIndentIncrease(item, context.items)); } if (e.key === "ArrowUp") { e.preventDefault(); const currentFlatIndex = flatIndexMap[item.sync_id] ?? 0; const prevIndex = currentFlatIndex - 1; const upperItemId = flatIndexMapInvert[prevIndex]; if (!upperItemId) return; context.updateFocusItemId(upperItemId ?? ""); } if (e.key === "ArrowDown") { e.preventDefault(); const currentFlatIndex = flatIndexMap[item.sync_id] ?? 0; const nextIndex = currentFlatIndex + 1; const upperItemId = flatIndexMapInvert[nextIndex]; if (!upperItemId) return; context.updateFocusItemId(upperItemId ?? ""); } if (e.key === "Enter") { if (e.shiftKey) { return; } e.preventDefault(); const hasChildren = getChildren(item.sync_id, context.items).length > 0; const children = getChildren(item.sync_id, context.items); const firstChild = children[0]; let parentId; let rank; if (hasChildren) { parentId = item.sync_id; rank = firstChild ? BigInt(firstChild.rank) - BigInt(ENTRY_GAP / 2) : BigInt(ENTRY_GAP); } else { parentId = item.parent_id ?? null; const nextSibling = getNextSibling(item.sync_id, context.items); if (nextSibling) { rank = BigInt(Number(item.rank + nextSibling.rank) / 2); } else { rank = BigInt(Number(item.rank) + ENTRY_GAP); } } const payload = { title: "", parent_id: parentId, rank }; context.onAddEntry(payload, item, context); } }; } function moveCursorToEnd(textarea) { setTimeout(() => { textarea.setSelectionRange(textarea.value.length, textarea.value.length); }, 0); } // src/button.tsx var import_jsx_runtime3 = require("react/jsx-runtime"); var Button = ({ children, className, appName }) => { return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( "button", { className: cn("bg-red-500", className), onClick: () => alert(`Hello from your ${appName} app!`), children } ); }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Button, DragHandle, DraggableListEditableText, DraggableListItem, DraggableListItemBottom, DraggableListItemContent, DraggableListItemEnd, DraggableListItemStart, DraggableListMain, DraggableListProvider, ENTRY_GAP, calculateIndentDecrease, calculateIndentIncrease, cn, flattenTreePreorder, getAllDescendants, getChildren, getPreviousSibling, getRootItems, getSiblings, useDraggableListContext });