@neodrag/solid
Advanced tools
| //#region src/_internal.d.ts | ||
| type Ref = (node: HTMLElement) => void; | ||
| //#endregion | ||
| export { Ref as t }; |
| import { createContext, useContext } from "solid-js"; | ||
| //#region src/_room-context.ts | ||
| /** | ||
| * The room a `<RoomProvider>` supplies, or `undefined` when none is mounted (collab is opt-in, so the | ||
| * capability primitives just skip their join). This module holds only a context handle + type-only | ||
| * `Room` references — importing it never pulls the heavy `Room` orchestrator into the bundle (that | ||
| * lives behind `@neodrag/solid/collab`), so `createSortable` without collab stays lean. | ||
| */ | ||
| const RoomContext = createContext(); | ||
| /** | ||
| * Raw (non-reactive) access to the ambient room — capability primitives use this to `add` their | ||
| * instance. For reactive presence UI (`peers`/`presences`) use `useRoom()` from `@neodrag/solid/collab`. | ||
| */ | ||
| function useRoomContext() { | ||
| return useContext(RoomContext); | ||
| } | ||
| /** | ||
| * Resolves the room from the `room` option or the ambient `<RoomProvider>`, and returns `join`/`leave` | ||
| * to call from the `ref` setter, co-located with the instance create + its `onCleanup`. `leave` is the | ||
| * per-target disposer (idempotent). | ||
| */ | ||
| function useRoomBinding(option_room) { | ||
| const room = option_room ?? useRoomContext(); | ||
| let off = null; | ||
| const join = (instance, id) => { | ||
| off = room?.add(instance, id) ?? null; | ||
| }; | ||
| const leave = () => { | ||
| off?.(); | ||
| off = null; | ||
| }; | ||
| return { | ||
| join, | ||
| leave | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { useRoomBinding as n, useRoomContext as r, RoomContext as t }; |
| import { Accessor, JSX } from "solid-js"; | ||
| import { CollabBackend, PresenceFrame, Room, RoomOptions } from "@neodrag/core/collab"; | ||
| export * from "@neodrag/core/collab"; | ||
| //#region src/_room-context.d.ts | ||
| /** | ||
| * Raw (non-reactive) access to the ambient room — capability primitives use this to `add` their | ||
| * instance. For reactive presence UI (`peers`/`presences`) use `useRoom()` from `@neodrag/solid/collab`. | ||
| */ | ||
| declare function useRoomContext(): Room | undefined; | ||
| //#endregion | ||
| //#region src/collab.d.ts | ||
| type RoomProviderProps = { | ||
| /** Use an existing room (you own its lifetime — the provider won't destroy it). */room?: Room; /** Create a room from this backend (destroyed when the provider unmounts). */ | ||
| backend?: CollabBackend; | ||
| children?: JSX.Element; | ||
| } & RoomOptions; | ||
| /** | ||
| * Provides a room to every capability primitive rendered below it — they auto-join (and leave on | ||
| * cleanup). Pass `room` to share an existing one, or `backend` to have the provider create and own it. | ||
| */ | ||
| declare function RoomProvider(props: RoomProviderProps): JSX.Element; | ||
| /** | ||
| * Reactive view of the ambient room — `peers` (connected ids) and `presences` (their in-flight | ||
| * gestures) are accessors that update as peers join, move and leave. Use for avatars, "N editing", | ||
| * remote cursors. Pass a room or rely on an ancestor `<RoomProvider>`. | ||
| */ | ||
| declare function useRoom(room?: Room): { | ||
| room: Room; | ||
| peers: Accessor<readonly string[]>; | ||
| presences: Accessor<ReadonlyMap<string, PresenceFrame>>; | ||
| }; | ||
| //#endregion | ||
| export { RoomProvider, RoomProviderProps, useRoom, useRoomContext }; |
| import { r as useRoomContext, t as RoomContext } from "./_room-context-Cg1LI93i.js"; | ||
| import { createComponent, createSignal, onCleanup } from "solid-js"; | ||
| import { Room } from "@neodrag/core/collab"; | ||
| export * from "@neodrag/core/collab"; | ||
| //#region src/collab.ts | ||
| /** | ||
| * Provides a room to every capability primitive rendered below it — they auto-join (and leave on | ||
| * cleanup). Pass `room` to share an existing one, or `backend` to have the provider create and own it. | ||
| */ | ||
| function RoomProvider(props) { | ||
| const room = props.room ?? new Room(props.backend, { | ||
| presenceThrottleMs: props.presenceThrottleMs, | ||
| presenceTtlMs: props.presenceTtlMs, | ||
| onRemotePresence: props.onRemotePresence, | ||
| onRemoteOp: props.onRemoteOp, | ||
| mirror: props.mirror | ||
| }); | ||
| if (!props.room) onCleanup(() => room.destroy()); | ||
| return createComponent(RoomContext.Provider, { | ||
| value: room, | ||
| get children() { | ||
| return props.children; | ||
| } | ||
| }); | ||
| } | ||
| /** | ||
| * Reactive view of the ambient room — `peers` (connected ids) and `presences` (their in-flight | ||
| * gestures) are accessors that update as peers join, move and leave. Use for avatars, "N editing", | ||
| * remote cursors. Pass a room or rely on an ancestor `<RoomProvider>`. | ||
| */ | ||
| function useRoom(room) { | ||
| const active = room ?? useRoomContext(); | ||
| if (!active) throw new Error("`useRoom()` needs a room — pass one or mount a <RoomProvider>."); | ||
| const [peers, set_peers] = createSignal(active.peers); | ||
| const [presences, set_presences] = createSignal(active.presences); | ||
| onCleanup(active.subscribe(() => { | ||
| set_peers(active.peers); | ||
| set_presences(active.presences); | ||
| })); | ||
| return { | ||
| room: active, | ||
| peers, | ||
| presences | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { RoomProvider, useRoom, useRoomContext }; |
| import { t as Ref } from "./_internal-B_qrKGUN.js"; | ||
| import { Accessor } from "solid-js"; | ||
| import { CollisionPolicy, DropAcceptCtx, DropEventData, DropOptions, DropOptions as DropOptions$1, Droppable, REMOTE_HOVER_ATTR, REMOTE_HOVER_MARKER_ATTR } from "@neodrag/core/drop"; | ||
| import { Room } from "@neodrag/core/collab"; | ||
| //#region src/drop.d.ts | ||
| declare function createDroppable(options?: DropOptions$1 & { | ||
| room?: Room; | ||
| }): { | ||
| ref: Ref; | ||
| isOver: Accessor<boolean>; | ||
| }; | ||
| //#endregion | ||
| export { type CollisionPolicy, type DropAcceptCtx, type DropEventData, type DropOptions, Droppable, REMOTE_HOVER_ATTR, REMOTE_HOVER_MARKER_ATTR, createDroppable }; |
+41
| import { n as useRoomBinding } from "./_room-context-Cg1LI93i.js"; | ||
| import { createEffect, createSignal, onCleanup } from "solid-js"; | ||
| import { Droppable, Droppable as Droppable$1, REMOTE_HOVER_ATTR, REMOTE_HOVER_MARKER_ATTR } from "@neodrag/core/drop"; | ||
| //#region src/drop.ts | ||
| function createDroppable(options = {}) { | ||
| const [isOver, set_over] = createSignal(false); | ||
| let inst = null; | ||
| const { join, leave } = useRoomBinding(options.room); | ||
| const build = () => ({ | ||
| ...options, | ||
| onEnter: (e) => { | ||
| set_over(true); | ||
| options.onEnter?.(e); | ||
| }, | ||
| onLeave: (e) => { | ||
| set_over(false); | ||
| options.onLeave?.(e); | ||
| } | ||
| }); | ||
| const ref = (node) => { | ||
| leave(); | ||
| inst?.destroy(); | ||
| inst = new Droppable$1(node, build()); | ||
| join(inst, options.id); | ||
| onCleanup(() => { | ||
| leave(); | ||
| inst?.destroy(); | ||
| inst = null; | ||
| }); | ||
| }; | ||
| createEffect(() => { | ||
| const next = build(); | ||
| inst?.update(next); | ||
| }); | ||
| return { | ||
| ref, | ||
| isOver | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { Droppable, REMOTE_HOVER_ATTR, REMOTE_HOVER_MARKER_ATTR, createDroppable }; |
| import { Accessor } from "solid-js"; | ||
| import { PanZoomBindOptions, PanZoomBindOptions as PanZoomBindOptions$1, PanZoomOptions, PanZoomTransform, PanZoomTransform as PanZoomTransform$1 } from "@neodrag/core/panzoom"; | ||
| //#region src/panzoom.d.ts | ||
| type RefSetter = (node: HTMLElement | null) => void; | ||
| /** | ||
| * Pan/zoom canvas primitive — a thin adapter over the core `PanZoom` binder (which owns the pan | ||
| * drag, wheel/pinch zoom, and the world transform). Put `viewport` on the clipping element's `ref` | ||
| * and `world` on the inner content layer's `ref`; read live `scale()` / `x()` / `y()` / `transform()`. | ||
| */ | ||
| declare function createPanZoom(options?: PanZoomBindOptions$1): { | ||
| viewport: RefSetter; | ||
| world: RefSetter; | ||
| scale: Accessor<number>; | ||
| x: Accessor<number>; | ||
| y: Accessor<number>; | ||
| transform: Accessor<PanZoomTransform$1>; | ||
| zoomBy: (factor: number, cx?: number, cy?: number) => void; | ||
| zoomTo: (scale: number, cx?: number, cy?: number) => void; | ||
| panBy: (dx: number, dy: number) => void; | ||
| setTransform: (t: Partial<PanZoomTransform$1>) => void; | ||
| reset: () => void; | ||
| }; | ||
| //#endregion | ||
| export { type PanZoomBindOptions, type PanZoomOptions, type PanZoomTransform, createPanZoom }; |
| import { createSignal, onCleanup } from "solid-js"; | ||
| import { PanZoom } from "@neodrag/core/panzoom"; | ||
| //#region src/panzoom.ts | ||
| /** | ||
| * Pan/zoom canvas primitive — a thin adapter over the core `PanZoom` binder (which owns the pan | ||
| * drag, wheel/pinch zoom, and the world transform). Put `viewport` on the clipping element's `ref` | ||
| * and `world` on the inner content layer's `ref`; read live `scale()` / `x()` / `y()` / `transform()`. | ||
| */ | ||
| function createPanZoom(options = {}) { | ||
| const [transform, set_transform] = createSignal({ | ||
| x: options.x ?? 0, | ||
| y: options.y ?? 0, | ||
| scale: options.scale ?? 1 | ||
| }); | ||
| const inst = new PanZoom({ | ||
| ...options, | ||
| onChange: (t) => set_transform(t) | ||
| }); | ||
| let v_dispose = null; | ||
| const viewport = (node) => { | ||
| v_dispose?.(); | ||
| v_dispose = node ? inst.viewport(node) : null; | ||
| }; | ||
| let w_dispose = null; | ||
| const world = (node) => { | ||
| w_dispose?.(); | ||
| w_dispose = node ? inst.world(node) : null; | ||
| }; | ||
| const zoomBy = (factor, cx, cy) => inst.zoomBy(factor, cx, cy); | ||
| const zoomTo = (scale, cx, cy) => inst.zoomTo(scale, cx, cy); | ||
| const panBy = (dx, dy) => inst.panBy(dx, dy); | ||
| const setTransform = (t) => inst.setTransform(t); | ||
| const reset = () => inst.reset(); | ||
| onCleanup(() => { | ||
| v_dispose?.(); | ||
| v_dispose = null; | ||
| w_dispose?.(); | ||
| w_dispose = null; | ||
| }); | ||
| return { | ||
| viewport, | ||
| world, | ||
| scale: () => transform().scale, | ||
| x: () => transform().x, | ||
| y: () => transform().y, | ||
| transform, | ||
| zoomBy, | ||
| zoomTo, | ||
| panBy, | ||
| setTransform, | ||
| reset | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { createPanZoom }; |
| import { t as Ref } from "./_internal-B_qrKGUN.js"; | ||
| import { Accessor } from "solid-js"; | ||
| import { RESIZE_EDGES, RESIZE_HANDLE_ATTR, Resizable, ResizeBoundsInput, ResizeEdge, ResizeEdge as ResizeEdge$1, ResizeEventData, ResizeHandleProps, ResizeHandleProps as ResizeHandleProps$1, ResizeOptions, ResizeOptions as ResizeOptions$1, preserveUnits } from "@neodrag/core/resize"; | ||
| import { Room } from "@neodrag/core/collab"; | ||
| //#region src/resize.d.ts | ||
| declare function createResizable(options?: ResizeOptions$1 & { | ||
| room?: Room; | ||
| }): { | ||
| ref: Ref; | ||
| handle: (edge: ResizeEdge$1) => ResizeHandleProps$1; | ||
| isResizing: Accessor<boolean>; | ||
| size: Accessor<{ | ||
| width: number; | ||
| height: number; | ||
| } | undefined>; | ||
| position: Accessor<{ | ||
| x: number; | ||
| y: number; | ||
| } | undefined>; | ||
| }; | ||
| //#endregion | ||
| export { RESIZE_EDGES, RESIZE_HANDLE_ATTR, Resizable, type ResizeBoundsInput, type ResizeEdge, type ResizeEventData, type ResizeHandleProps, type ResizeOptions, createResizable, preserveUnits }; |
| import { n as useRoomBinding } from "./_room-context-Cg1LI93i.js"; | ||
| import { createEffect, createSignal, onCleanup } from "solid-js"; | ||
| import { RESIZE_EDGES, RESIZE_HANDLE_ATTR, RESIZE_HANDLE_ATTR as RESIZE_HANDLE_ATTR$1, Resizable, Resizable as Resizable$1, preserveUnits } from "@neodrag/core/resize"; | ||
| //#region src/resize.ts | ||
| function createResizable(options = {}) { | ||
| const [isResizing, set_resizing] = createSignal(false); | ||
| const [size, set_size] = createSignal(void 0); | ||
| const [position, set_position] = createSignal(void 0); | ||
| let inst = null; | ||
| const { join, leave } = useRoomBinding(options.room); | ||
| const build = () => ({ | ||
| ...options, | ||
| onResizeStart: (e) => { | ||
| set_resizing(true); | ||
| set_size({ | ||
| width: e.width, | ||
| height: e.height | ||
| }); | ||
| set_position({ | ||
| x: e.x, | ||
| y: e.y | ||
| }); | ||
| options.onResizeStart?.(e); | ||
| }, | ||
| onResize: (e) => { | ||
| set_size({ | ||
| width: e.width, | ||
| height: e.height | ||
| }); | ||
| set_position({ | ||
| x: e.x, | ||
| y: e.y | ||
| }); | ||
| options.onResize?.(e); | ||
| }, | ||
| onResizeEnd: (e) => { | ||
| set_resizing(false); | ||
| set_size({ | ||
| width: e.width, | ||
| height: e.height | ||
| }); | ||
| set_position({ | ||
| x: e.x, | ||
| y: e.y | ||
| }); | ||
| options.onResizeEnd?.(e); | ||
| } | ||
| }); | ||
| const ref = (node) => { | ||
| leave(); | ||
| inst?.destroy(); | ||
| inst = new Resizable$1(node, build()); | ||
| join(inst, options.id); | ||
| onCleanup(() => { | ||
| leave(); | ||
| inst?.destroy(); | ||
| inst = null; | ||
| }); | ||
| }; | ||
| createEffect(() => { | ||
| inst?.update(build()); | ||
| }); | ||
| return { | ||
| ref, | ||
| handle: (edge) => ({ [RESIZE_HANDLE_ATTR$1]: edge }), | ||
| isResizing, | ||
| size, | ||
| position | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { RESIZE_EDGES, RESIZE_HANDLE_ATTR, Resizable, createResizable, preserveUnits }; |
| import { t as Ref } from "./_internal-B_qrKGUN.js"; | ||
| import { Accessor } from "solid-js"; | ||
| import { ROTATE_HANDLE_ATTR, Rotatable, RotateEventData, RotateHandlePos, RotateHandlePos as RotateHandlePos$1, RotateHandleProps, RotateHandleProps as RotateHandleProps$1, RotateOptions, RotateOptions as RotateOptions$1, RotateOrigin } from "@neodrag/core/rotate"; | ||
| import { Room } from "@neodrag/core/collab"; | ||
| //#region src/rotate.d.ts | ||
| /** | ||
| * Rotate primitive. Put `ref` on the element and spread `{...handle('top')}` on the rotate grip. | ||
| * `angle` (degrees) + `isRotating` come back as accessors. | ||
| * | ||
| * **Collab:** pass an `id` (and a `room`, or mount a `<RoomProvider>`) and rotation syncs live. | ||
| */ | ||
| declare function createRotatable(options?: RotateOptions$1 & { | ||
| room?: Room; | ||
| }): { | ||
| ref: Ref; | ||
| handle: (pos?: RotateHandlePos$1) => RotateHandleProps$1; | ||
| isRotating: Accessor<boolean>; | ||
| angle: Accessor<number>; | ||
| }; | ||
| //#endregion | ||
| export { ROTATE_HANDLE_ATTR, Rotatable, type RotateEventData, type RotateHandlePos, type RotateHandleProps, type RotateOptions, type RotateOrigin, createRotatable }; |
| import { n as useRoomBinding } from "./_room-context-Cg1LI93i.js"; | ||
| import { createEffect, createSignal, onCleanup } from "solid-js"; | ||
| import { ROTATE_HANDLE_ATTR, ROTATE_HANDLE_ATTR as ROTATE_HANDLE_ATTR$1, Rotatable, Rotatable as Rotatable$1 } from "@neodrag/core/rotate"; | ||
| //#region src/rotate.ts | ||
| /** | ||
| * Rotate primitive. Put `ref` on the element and spread `{...handle('top')}` on the rotate grip. | ||
| * `angle` (degrees) + `isRotating` come back as accessors. | ||
| * | ||
| * **Collab:** pass an `id` (and a `room`, or mount a `<RoomProvider>`) and rotation syncs live. | ||
| */ | ||
| function createRotatable(options = {}) { | ||
| const [isRotating, set_rotating] = createSignal(false); | ||
| const [angle, set_angle] = createSignal(0); | ||
| let inst = null; | ||
| const { join, leave } = useRoomBinding(options.room); | ||
| const build = () => ({ | ||
| ...options, | ||
| onRotateStart: (e) => { | ||
| set_rotating(true); | ||
| set_angle(e.angle); | ||
| options.onRotateStart?.(e); | ||
| }, | ||
| onRotate: (e) => { | ||
| set_angle(e.angle); | ||
| options.onRotate?.(e); | ||
| }, | ||
| onRotateEnd: (e) => { | ||
| set_rotating(false); | ||
| set_angle(e.angle); | ||
| options.onRotateEnd?.(e); | ||
| } | ||
| }); | ||
| const ref = (node) => { | ||
| leave(); | ||
| inst?.destroy(); | ||
| inst = new Rotatable$1(node, build()); | ||
| join(inst, options.id); | ||
| onCleanup(() => { | ||
| leave(); | ||
| inst?.destroy(); | ||
| inst = null; | ||
| }); | ||
| }; | ||
| createEffect(() => { | ||
| inst?.update(build()); | ||
| }); | ||
| return { | ||
| ref, | ||
| handle: (pos = "top") => ({ [ROTATE_HANDLE_ATTR$1]: pos }), | ||
| isRotating, | ||
| angle | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { ROTATE_HANDLE_ATTR, Rotatable, createRotatable }; |
| import { Accessor } from "solid-js"; | ||
| import { MarqueeOptions, SelectableOptions, SelectableOptions as SelectableOptions$1, rectsOverlap } from "@neodrag/core/select"; | ||
| //#region src/select.d.ts | ||
| type RefSetter = (node: HTMLElement | null) => void; | ||
| /** | ||
| * Rubber-band multi-select primitive — a thin adapter over the core `Selectable` binder (the region | ||
| * is a `Draggable` running the `marqueeSelect` plugin). Put `container` on the region's `ref` and | ||
| * `item(value)` on each child's `ref`. `selected()` is the live array of selected values; selected | ||
| * items also carry a `data-neodrag-selected` attribute to style. | ||
| */ | ||
| declare function createSelect<V = string>(options?: SelectableOptions$1<V>): { | ||
| container: RefSetter; | ||
| item: (value: V) => RefSetter; | ||
| selected: Accessor<V[]>; | ||
| clear: () => void; | ||
| }; | ||
| //#endregion | ||
| export { type MarqueeOptions, type SelectableOptions, createSelect, rectsOverlap }; |
| import { createSignal, onCleanup } from "solid-js"; | ||
| import { Selectable, rectsOverlap } from "@neodrag/core/select"; | ||
| //#region src/select.ts | ||
| /** | ||
| * Rubber-band multi-select primitive — a thin adapter over the core `Selectable` binder (the region | ||
| * is a `Draggable` running the `marqueeSelect` plugin). Put `container` on the region's `ref` and | ||
| * `item(value)` on each child's `ref`. `selected()` is the live array of selected values; selected | ||
| * items also carry a `data-neodrag-selected` attribute to style. | ||
| */ | ||
| function createSelect(options = {}) { | ||
| const [selected, set_selected] = createSignal([]); | ||
| const item_refs = /* @__PURE__ */ new Map(); | ||
| const inst = new Selectable({ | ||
| ...options, | ||
| onChange: (list) => set_selected(list) | ||
| }); | ||
| let c_dispose = null; | ||
| const container = (node) => { | ||
| c_dispose?.(); | ||
| c_dispose = node ? inst.container(node) : null; | ||
| }; | ||
| const item = (value) => { | ||
| let cb = item_refs.get(value); | ||
| if (!cb) { | ||
| let off = null; | ||
| cb = (node) => { | ||
| off?.(); | ||
| off = node ? inst.item(value, node) : null; | ||
| }; | ||
| item_refs.set(value, cb); | ||
| } | ||
| return cb; | ||
| }; | ||
| const clear = () => inst.clear(); | ||
| onCleanup(() => { | ||
| c_dispose?.(); | ||
| c_dispose = null; | ||
| }); | ||
| return { | ||
| container, | ||
| item, | ||
| selected, | ||
| clear | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { createSelect, rectsOverlap }; |
| import { t as Ref } from "./_internal-B_qrKGUN.js"; | ||
| import { MoveOp, SORTABLE_KEY_ATTR, SortAxis, SortStrategy, SortableList, SortableOp, SortableOptions, SortableOptions as SortableOptions$1, SortableRow, SortableRow as SortableRow$1, TransferContainer, TransferOp } from "@neodrag/core/sortable"; | ||
| import { Room } from "@neodrag/core/collab"; | ||
| //#region src/sortable.d.ts | ||
| /** | ||
| * Sortable primitive. Put `ref` on the container and spread `{...row(item.id)}` on each item instead | ||
| * of hand-writing `data-neodrag-sortable-key`. Pass `items` (and any option) as a getter to keep it live. | ||
| * | ||
| * **Collab:** pass an `id` (and a `room`, or mount a `<RoomProvider>`) and reorders sync live. | ||
| */ | ||
| declare function createSortable<T = unknown>(options: SortableOptions$1<T> & { | ||
| room?: Room; | ||
| }): { | ||
| ref: Ref; | ||
| row: (key: string) => SortableRow$1; | ||
| }; | ||
| //#endregion | ||
| export { type MoveOp, SORTABLE_KEY_ATTR, type SortAxis, type SortStrategy, SortableList, type SortableOp, type SortableOptions, type SortableRow, type TransferContainer, type TransferOp, createSortable }; |
| import { n as useRoomBinding } from "./_room-context-Cg1LI93i.js"; | ||
| import { createEffect, onCleanup } from "solid-js"; | ||
| import { SORTABLE_KEY_ATTR, SORTABLE_KEY_ATTR as SORTABLE_KEY_ATTR$1, SortableList, SortableList as SortableList$1 } from "@neodrag/core/sortable"; | ||
| //#region src/sortable.ts | ||
| /** | ||
| * Sortable primitive. Put `ref` on the container and spread `{...row(item.id)}` on each item instead | ||
| * of hand-writing `data-neodrag-sortable-key`. Pass `items` (and any option) as a getter to keep it live. | ||
| * | ||
| * **Collab:** pass an `id` (and a `room`, or mount a `<RoomProvider>`) and reorders sync live. | ||
| */ | ||
| function createSortable(options) { | ||
| let inst = null; | ||
| const { join, leave } = useRoomBinding(options.room); | ||
| const ref = (node) => { | ||
| leave(); | ||
| inst?.destroy(); | ||
| inst = new SortableList$1(node, { ...options }); | ||
| join(inst, options.id); | ||
| onCleanup(() => { | ||
| leave(); | ||
| inst?.destroy(); | ||
| inst = null; | ||
| }); | ||
| }; | ||
| createEffect(() => { | ||
| const next = { ...options }; | ||
| inst?.update(next); | ||
| }); | ||
| return { | ||
| ref, | ||
| row: (key) => ({ [SORTABLE_KEY_ATTR$1]: key }) | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { SORTABLE_KEY_ATTR, SortableList, createSortable }; |
| import { Accessor } from "solid-js"; | ||
| import { Room } from "@neodrag/core/collab"; | ||
| import { SplitAxis, SplitPaneOptions, SplitPaneOptions as SplitPaneOptions$1 } from "@neodrag/core/splitpane"; | ||
| //#region src/splitpane.d.ts | ||
| type RefSetter = (node: HTMLElement | null) => void; | ||
| /** | ||
| * Split-pane primitive — a thin adapter over the core `SplitPane` binder. Put `container` on the | ||
| * wrapper's `ref`, `pane(i)` on each pane's `ref`, and `gutter(i)` on each divider's `ref` (the | ||
| * gutter between pane `i` and `i + 1`). `sizes()` is the live array of weights. Nests: each call is | ||
| * independent. Pass a `room` to sync the layout. | ||
| */ | ||
| declare function createSplitPane(options?: SplitPaneOptions$1 & { | ||
| room?: Room; | ||
| }): { | ||
| container: RefSetter; | ||
| pane: (index: number) => RefSetter; | ||
| gutter: (index: number) => RefSetter; | ||
| sizes: Accessor<number[]>; | ||
| setSizes: (sizes: number[]) => void; | ||
| }; | ||
| //#endregion | ||
| export { type SplitAxis, type SplitPaneOptions, createSplitPane }; |
| import { createSignal, onCleanup } from "solid-js"; | ||
| import { SplitPane } from "@neodrag/core/splitpane"; | ||
| //#region src/splitpane.ts | ||
| /** | ||
| * Split-pane primitive — a thin adapter over the core `SplitPane` binder. Put `container` on the | ||
| * wrapper's `ref`, `pane(i)` on each pane's `ref`, and `gutter(i)` on each divider's `ref` (the | ||
| * gutter between pane `i` and `i + 1`). `sizes()` is the live array of weights. Nests: each call is | ||
| * independent. Pass a `room` to sync the layout. | ||
| */ | ||
| function createSplitPane(options = {}) { | ||
| const [sizes, set_sizes] = createSignal(options.sizes ?? []); | ||
| const pane_refs = /* @__PURE__ */ new Map(); | ||
| const gutter_refs = /* @__PURE__ */ new Map(); | ||
| const inst = new SplitPane({ | ||
| ...options, | ||
| onChange: (s) => set_sizes(s.slice()) | ||
| }); | ||
| let c_dispose = null; | ||
| const container = (node) => { | ||
| c_dispose?.(); | ||
| c_dispose = node ? inst.container(node) : null; | ||
| }; | ||
| const make = (cache, kind, index) => { | ||
| let cb = cache.get(index); | ||
| if (!cb) { | ||
| let off = null; | ||
| cb = (node) => { | ||
| off?.(); | ||
| off = node ? kind === "pane" ? inst.pane(node, index) : inst.gutter(node, index) : null; | ||
| }; | ||
| cache.set(index, cb); | ||
| } | ||
| return cb; | ||
| }; | ||
| const pane = (index) => make(pane_refs, "pane", index); | ||
| const gutter = (index) => make(gutter_refs, "gutter", index); | ||
| const setSizes = (s) => inst.setSizes(s); | ||
| onCleanup(() => { | ||
| c_dispose?.(); | ||
| c_dispose = null; | ||
| }); | ||
| return { | ||
| container, | ||
| pane, | ||
| gutter, | ||
| sizes, | ||
| setSizes | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { createSplitPane }; |
| import { Accessor } from "solid-js"; | ||
| import { SwipeAxis, SwipeOptions, SwipeOptions as SwipeOptions$1, SwipeResult, resolveSwipe } from "@neodrag/core/swipe"; | ||
| //#region src/swipe.d.ts | ||
| type RefSetter = (node: HTMLElement | null) => void; | ||
| /** | ||
| * Swipe-to-dismiss primitive — a thin adapter over the core `Swipeable` binder. Put `ref` on the | ||
| * element; read `offset()` / `isDismissed()`. Released past `threshold` the element flies out and | ||
| * fires `onDismiss`, otherwise it springs back. | ||
| */ | ||
| declare function createSwipe(options?: SwipeOptions$1): { | ||
| ref: RefSetter; | ||
| offset: Accessor<{ | ||
| x: number; | ||
| y: number; | ||
| }>; | ||
| isDismissed: Accessor<boolean>; | ||
| reset: () => void; | ||
| }; | ||
| //#endregion | ||
| export { type SwipeAxis, type SwipeOptions, type SwipeResult, createSwipe, resolveSwipe }; |
| import { createSignal, onCleanup } from "solid-js"; | ||
| import { Swipeable, resolveSwipe } from "@neodrag/core/swipe"; | ||
| //#region src/swipe.ts | ||
| /** | ||
| * Swipe-to-dismiss primitive — a thin adapter over the core `Swipeable` binder. Put `ref` on the | ||
| * element; read `offset()` / `isDismissed()`. Released past `threshold` the element flies out and | ||
| * fires `onDismiss`, otherwise it springs back. | ||
| */ | ||
| function createSwipe(options = {}) { | ||
| let inst = null; | ||
| let dispose = null; | ||
| const [offset, set_offset] = createSignal({ | ||
| x: 0, | ||
| y: 0 | ||
| }); | ||
| const [isDismissed, set_dismissed] = createSignal(false); | ||
| const ref = (node) => { | ||
| dispose?.(); | ||
| dispose = null; | ||
| inst = null; | ||
| if (node) { | ||
| inst = new Swipeable({ | ||
| ...options, | ||
| onChange: (s) => { | ||
| set_offset(s.offset); | ||
| set_dismissed(s.dismissed); | ||
| } | ||
| }); | ||
| dispose = inst.attach(node); | ||
| } | ||
| }; | ||
| onCleanup(() => { | ||
| dispose?.(); | ||
| dispose = null; | ||
| inst = null; | ||
| }); | ||
| const reset = () => inst?.reset(); | ||
| return { | ||
| ref, | ||
| offset, | ||
| isDismissed, | ||
| reset | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { createSwipe, resolveSwipe }; |
+25
-13
@@ -1,14 +0,26 @@ | ||
| import * as _neodrag_core from '@neodrag/core'; | ||
| import { Plugin, PluginResolver, DragEventData, Compartment } from '@neodrag/core/plugins'; | ||
| export * from '@neodrag/core/plugins'; | ||
| import { Accessor } from 'solid-js'; | ||
| import { AriaDragAnnounce, AriaDragOptions, AutoScrollOptions, Axis, BoundsInput, DragEventData, DragOptions, DragOptions as DragOptions$1, DragPlugin, GhostOptions, MagneticOptions, MagneticSpring, MarqueeOptions, ScrollLockOptions, SnapGuidesOptions, ariaDrag, autoScroll, ghost, haptics, magnetic, marqueeSelect, onMove, scrollLock, snapGuides } from "@neodrag/core"; | ||
| import { Accessor } from "solid-js"; | ||
| import { Room } from "@neodrag/core/collab"; | ||
| interface DragState extends DragEventData { | ||
| isDragging: boolean; | ||
| } | ||
| declare const useDraggable: (element: Accessor<HTMLElement | SVGElement | null | undefined>, plugins?: Accessor<Plugin[]> | ReturnType<PluginResolver>) => Accessor<DragState>; | ||
| declare function createCompartment(reactive: ConstructorParameters<typeof Compartment>[0]): Compartment; | ||
| declare const instances: Map<HTMLElement | SVGElement, _neodrag_core.DraggableInstance>; | ||
| export { createCompartment, instances, useDraggable }; | ||
| //#region src/index.d.ts | ||
| type Ref = (node: HTMLElement) => void; | ||
| /** | ||
| * Solid v3 — `create*` primitives returning a `ref` setter + reactive state accessors. | ||
| * | ||
| * **Collab:** pass an `id` (and a `room`, or mount a `<RoomProvider>`) and the draggable auto-joins | ||
| * that room on mount and leaves on unmount — its committed position syncs as a `drag` op. | ||
| */ | ||
| declare function createDraggable(options?: DragOptions$1 & { | ||
| room?: Room; | ||
| }): { | ||
| ref: Ref; | ||
| isDragging: Accessor<boolean>; | ||
| handle: (opts?: { | ||
| priority?: number; | ||
| }) => Ref; | ||
| cancel: (opts?: { | ||
| priority?: number; | ||
| }) => Ref; | ||
| }; | ||
| //#endregion | ||
| export { type AriaDragAnnounce, type AriaDragOptions, type AutoScrollOptions, type Axis, type BoundsInput, type DragEventData, type DragOptions, type DragPlugin, type GhostOptions, type MagneticOptions, type MagneticSpring, type MarqueeOptions, type ScrollLockOptions, type SnapGuidesOptions, ariaDrag, autoScroll, createDraggable, ghost, haptics, magnetic, marqueeSelect, onMove, scrollLock, snapGuides }; |
+74
-68
@@ -1,69 +0,75 @@ | ||
| import { DraggableFactory, DEFAULTS } from '@neodrag/core'; | ||
| import { unstable_definePlugin, Compartment } from '@neodrag/core/plugins'; | ||
| export * from '@neodrag/core/plugins'; | ||
| import { createSignal, createEffect, untrack, createRenderEffect } from 'solid-js'; | ||
| // src/index.ts | ||
| var draggable_factory = new DraggableFactory(DEFAULTS); | ||
| var default_drag_state = { | ||
| offset: { x: 0, y: 0 }, | ||
| rootNode: null, | ||
| currentNode: null, | ||
| isDragging: false, | ||
| event: null | ||
| }; | ||
| var state_sync = unstable_definePlugin((set_state) => { | ||
| const update_state = (ctx, event, overrides = {}) => ctx.effect.immediate( | ||
| () => set_state((prev) => ({ | ||
| ...prev, | ||
| offset: { ...ctx.offset }, | ||
| rootNode: ctx.rootNode, | ||
| currentNode: ctx.currentlyDraggedNode, | ||
| event, | ||
| ...overrides | ||
| })) | ||
| ); | ||
| return { | ||
| name: "sss", | ||
| // solid-state-sync | ||
| priority: -1e3, | ||
| cancelable: false, | ||
| start: (ctx, _state, event) => update_state(ctx, event, { isDragging: true }), | ||
| drag: (ctx, _state, event) => update_state(ctx, event), | ||
| end: (ctx, _state, event) => update_state(ctx, event, { isDragging: false }) | ||
| }; | ||
| }); | ||
| function resolve_plugins(plugins, state_sync_plugin) { | ||
| const p = typeof plugins === "function" ? plugins() : () => plugins; | ||
| if (typeof p === "function") { | ||
| return () => p().concat(state_sync_plugin); | ||
| } else { | ||
| return p.concat(state_sync_plugin); | ||
| } | ||
| import { n as useRoomBinding } from "./_room-context-Cg1LI93i.js"; | ||
| import { Draggable, ariaDrag, autoScroll, ghost, haptics, magnetic, marqueeSelect, onMove, scrollLock, snapGuides } from "@neodrag/core"; | ||
| import { createEffect, createSignal, onCleanup } from "solid-js"; | ||
| //#region src/index.ts | ||
| /** | ||
| * Solid v3 — `create*` primitives returning a `ref` setter + reactive state accessors. | ||
| * | ||
| * **Collab:** pass an `id` (and a `room`, or mount a `<RoomProvider>`) and the draggable auto-joins | ||
| * that room on mount and leaves on unmount — its committed position syncs as a `drag` op. | ||
| */ | ||
| function createDraggable(options = {}) { | ||
| const [isDragging, set_dragging] = createSignal(false); | ||
| let inst = null; | ||
| const { join, leave } = useRoomBinding(options.room); | ||
| const handles = /* @__PURE__ */ new Map(); | ||
| const cancels = /* @__PURE__ */ new Map(); | ||
| const position_two_way = Boolean(Object.getOwnPropertyDescriptor(options, "position")?.set); | ||
| const build = () => ({ | ||
| ...options, | ||
| onDragStart: (e) => { | ||
| set_dragging(true); | ||
| options.onDragStart?.(e); | ||
| }, | ||
| onDrag: (e) => { | ||
| if (position_two_way) options.position = e.offset; | ||
| options.onDrag?.(e); | ||
| }, | ||
| onDragEnd: (e) => { | ||
| set_dragging(false); | ||
| options.onDragEnd?.(e); | ||
| } | ||
| }); | ||
| const ref = (node) => { | ||
| leave(); | ||
| inst?.destroy(); | ||
| inst = new Draggable(node, build()); | ||
| join(inst, options.id); | ||
| for (const [n, e] of handles) e.off = inst.registerHandle(n, { priority: e.priority }); | ||
| for (const [n, e] of cancels) e.off = inst.registerCancel(n, { priority: e.priority }); | ||
| onCleanup(() => { | ||
| leave(); | ||
| for (const e of handles.values()) e.off = null; | ||
| for (const e of cancels.values()) e.off = null; | ||
| inst?.destroy(); | ||
| inst = null; | ||
| }); | ||
| }; | ||
| createEffect(() => { | ||
| const next = build(); | ||
| inst?.update(next); | ||
| }); | ||
| const marker = (map, is_handle, priority) => { | ||
| return (node) => { | ||
| const entry = { | ||
| priority, | ||
| off: inst ? is_handle ? inst.registerHandle(node, { priority }) : inst.registerCancel(node, { priority }) : null | ||
| }; | ||
| map.set(node, entry); | ||
| onCleanup(() => { | ||
| entry.off?.(); | ||
| map.delete(node); | ||
| }); | ||
| }; | ||
| }; | ||
| const handle = (o) => marker(handles, true, o?.priority ?? 0); | ||
| const cancel = (o) => marker(cancels, false, o?.priority ?? 0); | ||
| return { | ||
| ref, | ||
| isDragging, | ||
| handle, | ||
| cancel | ||
| }; | ||
| } | ||
| function wrapper(draggableFactory) { | ||
| return (element, plugins = () => []) => { | ||
| const [drag_state, set_drag_state] = createSignal(default_drag_state); | ||
| const state_sync_plugin = state_sync(set_drag_state); | ||
| createEffect(() => { | ||
| const node = element(); | ||
| if (!node) return; | ||
| return draggableFactory.draggable( | ||
| node, | ||
| untrack(() => resolve_plugins(plugins, state_sync_plugin)) | ||
| ); | ||
| }); | ||
| return drag_state; | ||
| }; | ||
| } | ||
| var useDraggable = wrapper(draggable_factory); | ||
| function createCompartment(reactive) { | ||
| const compartment = new Compartment(reactive); | ||
| createRenderEffect(() => { | ||
| compartment.current = reactive?.(); | ||
| }); | ||
| return compartment; | ||
| } | ||
| var instances = draggable_factory.instances; | ||
| export { createCompartment, instances, useDraggable }; | ||
| //#endregion | ||
| export { ariaDrag, autoScroll, createDraggable, ghost, haptics, magnetic, marqueeSelect, onMove, scrollLock, snapGuides }; |
+77
-24
| { | ||
| "name": "@neodrag/solid", | ||
| "version": "3.0.0-next.8", | ||
| "version": "3.0.0-next.10", | ||
| "description": "SolidJS library to add dragging to your apps 😉", | ||
| "type": "module", | ||
| "keywords": [ | ||
| "drag", | ||
| "draggable", | ||
| "neodrag", | ||
| "performant", | ||
| "react-draggable", | ||
| "small", | ||
| "solid", | ||
| "tiny" | ||
| ], | ||
| "homepage": "https://github.com/PuruVJ/neodrag/tree/main/packages/solid#readme", | ||
| "bugs": { | ||
| "url": "https://github.com/PuruVJ/neodrag/issues" | ||
| }, | ||
| "license": "MIT", | ||
| "author": "Puru Vijay", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/PuruVJ/neodrag.git" | ||
| }, | ||
| "files": [ | ||
| "dist/*" | ||
| ], | ||
| "type": "module", | ||
| "sideEffects": false, | ||
@@ -16,32 +36,65 @@ "exports": { | ||
| }, | ||
| "./collab": { | ||
| "types": "./dist/collab.d.ts", | ||
| "import": "./dist/collab.js", | ||
| "default": "./dist/collab.js" | ||
| }, | ||
| "./sortable": { | ||
| "types": "./dist/sortable.d.ts", | ||
| "import": "./dist/sortable.js", | ||
| "default": "./dist/sortable.js" | ||
| }, | ||
| "./resize": { | ||
| "types": "./dist/resize.d.ts", | ||
| "import": "./dist/resize.js", | ||
| "default": "./dist/resize.js" | ||
| }, | ||
| "./rotate": { | ||
| "types": "./dist/rotate.d.ts", | ||
| "import": "./dist/rotate.js", | ||
| "default": "./dist/rotate.js" | ||
| }, | ||
| "./drop": { | ||
| "types": "./dist/drop.d.ts", | ||
| "import": "./dist/drop.js", | ||
| "default": "./dist/drop.js" | ||
| }, | ||
| "./splitpane": { | ||
| "types": "./dist/splitpane.d.ts", | ||
| "import": "./dist/splitpane.js", | ||
| "default": "./dist/splitpane.js" | ||
| }, | ||
| "./panzoom": { | ||
| "types": "./dist/panzoom.d.ts", | ||
| "import": "./dist/panzoom.js", | ||
| "default": "./dist/panzoom.js" | ||
| }, | ||
| "./swipe": { | ||
| "types": "./dist/swipe.d.ts", | ||
| "import": "./dist/swipe.js", | ||
| "default": "./dist/swipe.js" | ||
| }, | ||
| "./select": { | ||
| "types": "./dist/select.d.ts", | ||
| "import": "./dist/select.js", | ||
| "default": "./dist/select.js" | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/PuruVJ/neodrag.git" | ||
| "devDependencies": { | ||
| "@vitest/browser": "^3.2.4", | ||
| "playwright": "^1.53.1", | ||
| "solid-js": "^1.9.7", | ||
| "vite-plugin-solid": "^2.11.6", | ||
| "vitest": "^3.2.4", | ||
| "@neodrag/core": "3.0.0-next.10" | ||
| }, | ||
| "keywords": [ | ||
| "draggable", | ||
| "solid", | ||
| "react-draggable", | ||
| "drag", | ||
| "neodrag", | ||
| "small", | ||
| "tiny", | ||
| "performant", | ||
| "neodrag" | ||
| ], | ||
| "author": "Puru Vijay", | ||
| "license": "MIT", | ||
| "bugs": { | ||
| "url": "https://github.com/PuruVJ/neodrag/issues" | ||
| }, | ||
| "homepage": "https://github.com/PuruVJ/neodrag/tree/main/packages/solid#readme", | ||
| "peerDependencies": { | ||
| "solid-js": "^1.0.0", | ||
| "@neodrag/core": "3.0.0-next.8" | ||
| "@neodrag/core": "3.0.0-next.10" | ||
| }, | ||
| "scripts": { | ||
| "compile": "tsup" | ||
| "compile": "vp pack", | ||
| "test": "vitest run" | ||
| } | ||
| } |
+52
-27
@@ -26,4 +26,4 @@ <p align="center"> | ||
| - ⚡ **Performance** - Event delegation, pointer capture, optimized for modern browsers | ||
| - 🎯 **SolidJS Native** - Built for SolidJS with `useDraggable` hook | ||
| - 🔄 **Reactive** - `createCompartment` for reactive plugin updates | ||
| - 🎯 **SolidJS Native** - `createDraggable`, `createDroppable`, `createSortable` follow Solid naming (`create*` / `with*`) | ||
| - 🔄 **Reactive** - pass `() => plugins`; the wrapper reconciles automatically | ||
@@ -41,10 +41,11 @@ # Installing | ||
| ```tsx | ||
| import { useDraggable } from '@neodrag/solid'; | ||
| import { createDraggable } from '@neodrag/solid'; | ||
| export const App: Component = () => { | ||
| const [draggableRef, setDraggableRef] = createSignal<HTMLElement | null>(null); | ||
| const [draggableRef, setDraggableRef] = | ||
| createSignal<HTMLElement | null>(null); | ||
| useDraggable(draggableRef); | ||
| createDraggable(draggableRef); | ||
| return <div ref={setDraggableRef}>You can drag me</div>; | ||
| return <div ref={setDraggableRef}>You can drag me</div>; | ||
| }; | ||
@@ -56,10 +57,11 @@ ``` | ||
| ```tsx | ||
| import { useDraggable, axis, grid } from '@neodrag/solid'; | ||
| import { createDraggable, axis, grid } from '@neodrag/solid'; | ||
| export const App: Component = () => { | ||
| const [draggableRef, setDraggableRef] = createSignal<HTMLElement | null>(null); | ||
| const [draggableRef, setDraggableRef] = | ||
| createSignal<HTMLElement | null>(null); | ||
| useDraggable(draggableRef, [axis('x'), grid([10, 10])]); | ||
| createDraggable(draggableRef, [axis('x'), grid([10, 10])]); | ||
| return <div ref={setDraggableRef}>Horizontal grid snapping</div>; | ||
| return <div ref={setDraggableRef}>Horizontal grid snapping</div>; | ||
| }; | ||
@@ -71,11 +73,18 @@ ``` | ||
| ```tsx | ||
| import { useDraggable, axis, bounds, BoundsFrom, type Plugin } from '@neodrag/solid'; | ||
| import { | ||
| createDraggable, | ||
| axis, | ||
| bounds, | ||
| BoundsFrom, | ||
| type Plugin, | ||
| } from '@neodrag/solid'; | ||
| export const App: Component = () => { | ||
| const [draggableRef, setDraggableRef] = createSignal<HTMLElement | null>(null); | ||
| const [draggableRef, setDraggableRef] = | ||
| createSignal<HTMLElement | null>(null); | ||
| const plugins: Plugin[] = [axis('y'), bounds(BoundsFrom.parent())]; | ||
| useDraggable(draggableRef, plugins); | ||
| const plugins: Plugin[] = [axis('y'), bounds(BoundsFrom.parent())]; | ||
| createDraggable(draggableRef, plugins); | ||
| return <div ref={setDraggableRef}>Type-safe dragging</div>; | ||
| return <div ref={setDraggableRef}>Type-safe dragging</div>; | ||
| }; | ||
@@ -87,22 +96,25 @@ ``` | ||
| ```tsx | ||
| import { useDraggable } from '@neodrag/solid'; | ||
| import { createDraggable } from '@neodrag/solid'; | ||
| export const App: Component = () => { | ||
| const [draggableRef, setDraggableRef] = createSignal<HTMLElement | null>(null); | ||
| const dragState = useDraggable(draggableRef); | ||
| const [draggableRef, setDraggableRef] = | ||
| createSignal<HTMLElement | null>(null); | ||
| const dragState = createDraggable(draggableRef); | ||
| createEffect(() => { | ||
| console.log('Position:', dragState().offset); | ||
| console.log('Is dragging:', dragState().isDragging); | ||
| }); | ||
| createEffect(() => { | ||
| console.log('Position:', dragState().offset); | ||
| console.log('Is dragging:', dragState().isDragging); | ||
| }); | ||
| return <div ref={setDraggableRef}>Check console while dragging</div>; | ||
| return ( | ||
| <div ref={setDraggableRef}>Check console while dragging</div> | ||
| ); | ||
| }; | ||
| ``` | ||
| Reactive plugins with createCompartment | ||
| Reactive plugins with reactive plugin factories | ||
| ```tsx | ||
| import { createSignal } from 'solid-js'; | ||
| import { useDraggable, axis, createCompartment } from '@neodrag/solid'; | ||
| import { createDraggable, axis } from '@neodrag/solid'; | ||
@@ -113,5 +125,4 @@ export const App: Component = () => { | ||
| const axisCompartment = createCompartment(() => axis(currentAxis())); | ||
| useDraggable(draggableRef, [axisCompartment]); | ||
| createDraggable(draggableRef, [axisreactive plugin factories]); | ||
@@ -127,2 +138,16 @@ return ( | ||
| ## Sortable lists | ||
| ```tsx | ||
| import { createSortable, createSortableItem } from '@neodrag/solid/sortable'; | ||
| const { list, dropRef } = createSortable({ | ||
| items, | ||
| keyBy: (i) => i.id, | ||
| onReorder: setItems, | ||
| }); | ||
| // <ul ref={dropRef}> … createSortableItem(list, item.id) per row | ||
| ``` | ||
| <a href="https://next.neodrag.dev/docs/solid" style="font-size: 2rem">Read the docs</a> | ||
@@ -129,0 +154,0 @@ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
34395
308.3%25
400%773
891.03%1
-50%155
19.23%6
Infinity%1
Infinity%