├── docs ├── .npmrc ├── src │ ├── routes │ │ ├── +layout.ts │ │ ├── from-docs │ │ │ ├── item.svelte │ │ │ ├── column.svelte │ │ │ ├── +page.svelte │ │ │ └── styles.css │ │ ├── header.svelte │ │ ├── +page.svelte │ │ └── +layout.svelte │ ├── lib │ │ ├── index.ts │ │ ├── components │ │ │ ├── examples │ │ │ │ ├── basic │ │ │ │ │ ├── draggable.svelte │ │ │ │ │ └── basic.svelte │ │ │ │ ├── sortable │ │ │ │ │ ├── sortable-item.svelte │ │ │ │ │ └── sortable-list.svelte │ │ │ │ └── nested │ │ │ │ │ ├── task-column.svelte │ │ │ │ │ ├── task-item.svelte │ │ │ │ │ └── draggable-containers.svelte │ │ │ ├── droppable.svelte │ │ │ ├── metadata.svelte │ │ │ └── section.svelte │ │ ├── config.ts │ │ └── assets │ │ │ └── favicon.svg │ ├── app.html │ ├── app.d.ts │ └── app.css ├── static │ ├── og.png │ └── robots.txt ├── vite.config.ts ├── .gitignore ├── tsconfig.json ├── svelte.config.js ├── README.md ├── package.json ├── uno.config.ts └── presets │ ├── shadcn-preset.ts │ └── custom-preset.ts ├── packages └── svelte │ ├── .npmrc │ ├── src │ ├── lib │ │ ├── utilities │ │ │ ├── index.ts │ │ │ ├── is-node-attached.ts │ │ │ ├── lazy-watch.ts │ │ │ └── make-ref.ts │ │ ├── sortable │ │ │ ├── index.ts │ │ │ └── use-sortable.svelte.ts │ │ ├── _runed │ │ │ ├── extract │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── unwrap.ts │ │ │ ├── vue-reactivity │ │ │ │ ├── index.ts │ │ │ │ ├── box.svelte.ts │ │ │ │ └── lens.svelte.ts │ │ │ ├── is.ts │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ └── watch.svelte.ts │ │ ├── hooks │ │ │ ├── use-computed.ts │ │ │ ├── index.ts │ │ │ ├── use-on-value-change.ts │ │ │ ├── use-on-element-change.svelte.ts │ │ │ ├── use-signal.ts │ │ │ └── use-deep-signal.ts │ │ ├── core │ │ │ ├── context │ │ │ │ ├── context.ts │ │ │ │ ├── renderer.svelte.ts │ │ │ │ └── drag-drop-provider.svelte │ │ │ ├── hooks │ │ │ │ ├── use-drag-drop-manager.ts │ │ │ │ ├── use-instance.svelte.ts │ │ │ │ └── use-drag-operation.ts │ │ │ ├── droppable │ │ │ │ └── use-droppable.ts │ │ │ └── draggable │ │ │ │ ├── drag-overlay.svelte │ │ │ │ └── use-draggable.ts │ │ └── index.ts │ ├── app.d.ts │ └── app.html │ ├── vite.config.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── README.md │ ├── svelte.config.js │ └── package.json ├── pnpm-workspace.yaml ├── .prettierignore ├── .npmrc ├── .gitignore ├── .prettierrc ├── .changeset ├── config.json └── README.md ├── NOTICE.txt ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── LICENSE ├── eslint.config.js ├── package.json └── README.md /docs/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /packages/svelte/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /docs/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - docs 4 | - playground/* 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /docs/static/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanielu/dnd-kit-svelte/HEAD/docs/static/og.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | @dnd-kit-svelte:registry=https://registry.npmjs.org/ 3 | access=public 4 | -------------------------------------------------------------------------------- /docs/static/robots.txt: -------------------------------------------------------------------------------- 1 | # allow crawling everything by default 2 | User-agent: * 3 | Disallow: /from-docs/ 4 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/utilities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lazy-watch.js'; 2 | export * from './is-node-attached.js'; 3 | export * from './make-ref.js'; 4 | -------------------------------------------------------------------------------- /docs/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import {KeyboardSensor, PointerSensor} from '@dnd-kit-svelte/svelte'; 2 | 3 | export const sensors = [KeyboardSensor, PointerSensor]; 4 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/utilities/is-node-attached.ts: -------------------------------------------------------------------------------- 1 | export function isNodeAttached(node: Element) { 2 | return 'isConnected' in node && node.isConnected; 3 | } 4 | -------------------------------------------------------------------------------- /packages/svelte/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/sortable/index.ts: -------------------------------------------------------------------------------- 1 | export {useSortable} from './use-sortable.svelte'; 2 | export type {UseSortableInput} from './use-sortable.svelte'; 3 | 4 | export {isSortable} from '@dnd-kit/dom/sortable'; 5 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/_runed/extract/index.ts: -------------------------------------------------------------------------------- 1 | export type {Getter, MaybeGetter, MaybeGetterObject, UnwrapMaybeGetter, UnwrapMaybeGetterObject} from './types.js'; 2 | export {resolve, resolveObj, toFnObject, asGetter} from './unwrap.js'; 3 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/hooks/use-computed.ts: -------------------------------------------------------------------------------- 1 | import {computed} from '@dnd-kit/state'; 2 | import {useSignal} from './use-signal.js'; 3 | 4 | export function useComputed(compute: () => T) { 5 | return useSignal(computed(compute)); 6 | } 7 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/utilities/lazy-watch.ts: -------------------------------------------------------------------------------- 1 | import {watch, type Getter} from 'runed'; 2 | 3 | export function lazyWatch(sources: Getter, effect: (value: T, previousValue: T | undefined) => void) { 4 | watch(sources, effect, {lazy: true}); 5 | } 6 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/_runed/vue-reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export {lens} from './lens.svelte.js'; 2 | export {isBox, box} from './box.svelte.js'; 3 | export type {Lens, WritableLensOptions, WritableLens} from './lens.svelte.js'; 4 | export type {Box} from './box.svelte.js'; 5 | -------------------------------------------------------------------------------- /docs/src/lib/components/examples/basic/draggable.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
Drag me
8 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from "@sveltejs/kit/vite"; 2 | import { defineConfig } from "vite"; 3 | import UnoCSS from "unocss/vite"; 4 | import Icons from "unplugin-icons/vite"; 5 | 6 | export default defineConfig({ 7 | plugins: [UnoCSS(), Icons({ compiler: "svelte" }), sveltekit()], 8 | }); 9 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/_runed/is.ts: -------------------------------------------------------------------------------- 1 | export function isFunction(value: unknown): value is (...args: unknown[]) => unknown { 2 | return typeof value === "function"; 3 | } 4 | 5 | export function isObject(value: unknown): value is Record { 6 | return value !== null && typeof value === "object"; 7 | } 8 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export {useComputed} from './use-computed.js'; 2 | export {useDeepSignal} from './use-deep-signal.js'; 3 | export {useOnElementChange} from './use-on-element-change.svelte'; 4 | export {useOnValueChange} from './use-on-value-change.js'; 5 | export {useSignal} from './use-signal.js'; 6 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | /dist 11 | 12 | # OS 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # Env 17 | .env 18 | .env.* 19 | !.env.example 20 | !.env.test 21 | 22 | # Vite 23 | vite.config.js.timestamp-* 24 | vite.config.ts.timestamp-* 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "tabWidth": 2, 4 | "useTabs": true, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "printWidth": 120, 8 | "plugins": ["prettier-plugin-svelte"], 9 | "overrides": [ 10 | { 11 | "files": "*.svelte", 12 | "options": { 13 | "parser": "svelte" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /docs/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %sveltekit.head% 7 | 8 | 9 |
%sveltekit.body%
10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/svelte/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | /dist 11 | 12 | # OS 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # Env 17 | .env 18 | .env.* 19 | !.env.example 20 | !.env.test 21 | 22 | # Vite 23 | vite.config.js.timestamp-* 24 | vite.config.ts.timestamp-* 25 | -------------------------------------------------------------------------------- /packages/svelte/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/core/context/context.ts: -------------------------------------------------------------------------------- 1 | import {DragDropManager} from '@dnd-kit/dom'; 2 | import {lens, Context, type Lens} from 'runed'; 3 | 4 | export const defaultManager = new DragDropManager(); 5 | 6 | export const DragDropContext = new Context>( 7 | 'DragDropContext', 8 | lens(() => defaultManager) 9 | ); 10 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", 3 | "changelog": ["@svitejs/changesets-changelog-github-compact", {"repo": "HanielU/svelte-dnd-kit"}], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["docs"] 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/app.d.ts: -------------------------------------------------------------------------------- 1 | import 'unplugin-icons/types/svelte'; 2 | 3 | // See https://svelte.dev/docs/kit/types#app.d.ts 4 | // for information about these interfaces 5 | declare global { 6 | namespace App { 7 | // interface Error {} 8 | // interface Locals {} 9 | // interface PageData {} 10 | // interface PageState {} 11 | // interface Platform {} 12 | } 13 | } 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /packages/svelte/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/svelte/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "module": "NodeNext", 13 | "moduleResolution": "NodeNext" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/utilities/make-ref.ts: -------------------------------------------------------------------------------- 1 | import type {Attachment} from 'svelte/attachments'; 2 | import {isNodeAttached} from './is-node-attached.js'; 3 | 4 | export function makeRef(obj: T, key: K): Attachment { 5 | return (node) => { 6 | (obj as any)[key] = node as any; 7 | return () => { 8 | if (isNodeAttached(node)) return; 9 | (obj as any)[key] = undefined; 10 | }; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /docs/src/routes/from-docs/item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | {id} 17 |
18 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/hooks/use-on-value-change.ts: -------------------------------------------------------------------------------- 1 | import {asGetter, watch, type MaybeGetter} from 'runed'; 2 | 3 | export function useOnValueChange( 4 | value: MaybeGetter | undefined, 5 | onChange: (value: T, oldValue: T) => void, 6 | effect: typeof watch | typeof watch.pre = watch, 7 | compare = Object.is 8 | ) { 9 | effect(asGetter(value), (value, oldValue) => { 10 | if (!compare(value, oldValue)) { 11 | onChange(value, oldValue!); 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/hooks/use-on-element-change.svelte.ts: -------------------------------------------------------------------------------- 1 | import {resolve, type MaybeGetter} from 'runed'; 2 | 3 | export function useOnElementChange( 4 | value: MaybeGetter | undefined, 5 | onChange: (value: Element | undefined) => void 6 | ) { 7 | let previous = resolve(value); 8 | 9 | $effect.pre(() => { 10 | const current = resolve(value); 11 | if (current !== previous) { 12 | previous = current; 13 | onChange(current); 14 | } 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /docs/src/routes/from-docs/column.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | {@render children()} 18 |
19 | -------------------------------------------------------------------------------- /docs/src/lib/components/droppable.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | {@render children()} 18 |
19 | -------------------------------------------------------------------------------- /docs/src/lib/config.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: 'dnd-kit-svelte - a modern drag and drop toolkit for svelte', 3 | url: 'https://svelte-dnd-kit.vercel.app', 4 | description: 'dnd-kit-svelte is a modern drag and drop toolkit for svelte based on dnd-kit for react.', 5 | ogImage: 'https://svelte-dnd-kit.vercel.app/og.png', 6 | links: { 7 | twitter: 'https://twitter.com/hvniel_', 8 | github: 'https://github.com/HanielU/dnd-kit-svelte', 9 | }, 10 | keywords: `svelte,dnd,dnd-kit,drag and drop,Drag and Drop,Toolkit`, 11 | }; 12 | 13 | export type SiteConfig = typeof siteConfig; 14 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/core/hooks/use-drag-drop-manager.ts: -------------------------------------------------------------------------------- 1 | import type {Data} from '@dnd-kit/abstract'; 2 | import type {DragDropManager, Draggable, Droppable} from '@dnd-kit/dom'; 3 | 4 | import {DragDropContext} from '../context/context.js'; 5 | import type {Lens} from 'runed'; 6 | 7 | export function useDragDropManager< 8 | T extends Data = Data, 9 | U extends Draggable = Draggable, 10 | V extends Droppable = Droppable, 11 | W extends DragDropManager = DragDropManager, 12 | >(): Lens { 13 | return DragDropContext.get() as Lens; 14 | } 15 | -------------------------------------------------------------------------------- /packages/svelte/README.md: -------------------------------------------------------------------------------- 1 | # @dnd-kit-svelte/svelte 2 | 3 | [![Stable release](https://img.shields.io/npm/v/@dnd-kit-svelte/svelte.svg)](https://npm.im/@dnd-kit-svelte/svelte) 4 | 5 | @dnd-kit – a lightweight React library for building performant and accessible drag and drop experiences. 6 | 7 | ## Installation 8 | 9 | To get started, install the `@dnd-kit-svelte/svelte` package via npm or yarn: 10 | 11 | ``` 12 | npm install @dnd-kit-svelte/svelte 13 | ``` 14 | 15 | ## Usage 16 | 17 | Visit [next.dndkit.com](https://next.dndkit.com/react/quickstart) to learn how to get started with @dnd-kit. 18 | -------------------------------------------------------------------------------- /docs/src/routes/header.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 16 |
17 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/_runed/vue-reactivity/box.svelte.ts: -------------------------------------------------------------------------------- 1 | import {isObject} from '../is.js'; 2 | 3 | export const BoxSymbol = Symbol('box'); 4 | 5 | export type Box = { 6 | [BoxSymbol]: true; 7 | get current(): T; 8 | set current(_: S); 9 | }; 10 | 11 | export function box(value: T): Box { 12 | let _state = $state(value); 13 | 14 | return { 15 | [BoxSymbol]: true, 16 | get current() { 17 | return _state; 18 | }, 19 | set current(v: T) { 20 | _state = v; 21 | }, 22 | }; 23 | } 24 | 25 | export function isBox(r: Box | unknown): r is Box; 26 | export function isBox(r: unknown): r is Box { 27 | return isObject(r) && BoxSymbol in r; 28 | } 29 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // To make changes to top-level options such as include and exclude, we recommend extending 18 | // the generated config; see https://svelte.dev/docs/kit/configuration#typescript 19 | } 20 | -------------------------------------------------------------------------------- /docs/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 |
11 | 12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/hooks/use-signal.ts: -------------------------------------------------------------------------------- 1 | import {effect, type Signal} from '@dnd-kit/state'; 2 | import {createSubscriber} from 'svelte/reactivity'; 3 | 4 | /** Trigger a re-run of Svelte effects/derivations when reading a @dnd-kit/state Signal. */ 5 | export function useSignal(signal: Signal, _sync = false) { 6 | let previous = signal.peek(); 7 | 8 | const subscribe = createSubscriber((update) => { 9 | return effect(() => { 10 | const current = signal.value; 11 | 12 | if (previous !== current) { 13 | previous = current; 14 | update(); 15 | } 16 | }); 17 | }); 18 | 19 | return { 20 | get value() { 21 | // Make this getter reactive if read in a Svelte effect/derived 22 | subscribe(); 23 | return signal.peek(); 24 | }, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /docs/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import {vitePreprocess} from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | compilerOptions: {runes: true}, 11 | 12 | kit: { 13 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 14 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 15 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 16 | adapter: adapter(), 17 | }, 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | dnd-kit-svelte 2 | ================= 3 | The following is a list of sources from which code was used/modified in this codebase. 4 | 5 | ------------------------------------------------------------------------------- 6 | 7 | This codebase contains a modified portion of code from Dnd Kit which can be obtained at: 8 | * SOURCE: 9 | * https://www.npmjs.com/package/@dnd-kit/core 10 | * https://www.npmjs.com/package/@dnd-kit/sortable 11 | * https://www.npmjs.com/package/@dnd-kit/utilities 12 | * https://www.npmjs.com/package/@dnd-kit/accessibility 13 | * https://www.npmjs.com/package/@dnd-kit/modifiers 14 | * LICENSE (MIT): 15 | * https://github.com/clauderic/dnd-kit/blob/master/LICENSE 16 | 17 | ------------------------------------------------------------------------------- -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [created] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Setup Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: '20.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v2 20 | with: 21 | version: 9 22 | 23 | - name: Install dependencies 24 | run: pnpm install --no-frozen-lockfile 25 | 26 | - name: Package 27 | run: pnpm package 28 | 29 | - name: Publish to npm 30 | run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/core/hooks/use-instance.svelte.ts: -------------------------------------------------------------------------------- 1 | import type {DragDropManager} from '@dnd-kit/abstract'; 2 | import type {CleanupFunction} from '@dnd-kit/state'; 3 | 4 | import {useDragDropManager} from './use-drag-drop-manager.js'; 5 | 6 | export interface Instance = DragDropManager> { 7 | manager: T | undefined; 8 | register(): CleanupFunction | void; 9 | } 10 | 11 | export function useInstance(initializer: (manager: DragDropManager | undefined) => T): T { 12 | const manager = useDragDropManager(); 13 | const instance = initializer(manager.current ?? undefined); 14 | 15 | $effect.pre(() => { 16 | if (instance.manager !== manager.current) { 17 | instance.manager = manager.current ?? undefined; 18 | } 19 | 20 | return instance.register(); 21 | }); 22 | 23 | return instance; 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/routes/from-docs/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | { 17 | const {source} = event.operation; 18 | if (source?.type === 'column') return; 19 | items = move(items, event); 20 | }} 21 | > 22 |
23 | {#each Object.entries(items) as [column, _items], colIdx (column)} 24 | 25 | {#each _items as id, itemIdx (id)} 26 | 27 | {/each} 28 | 29 | {/each} 30 |
31 |
32 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // // Reexport your entry components here 2 | 3 | export {default as DragDropProvider, type Events as DragDropEvents} from './core/context/drag-drop-provider.svelte'; 4 | 5 | export {useDraggable, type UseDraggableInput} from './core/draggable/use-draggable.js'; 6 | export {default as DragOverlay} from './core/draggable/drag-overlay.svelte'; 7 | 8 | export {useDroppable, type UseDroppableInput} from './core/droppable/use-droppable.js'; 9 | 10 | export {useDragDropManager} from './core/hooks/use-drag-drop-manager.js'; 11 | 12 | // export { 13 | // useDragDropMonitor, 14 | // type EventHandlers as DragDropEventHandlers, 15 | // } from './context/use-drag-drop-monitor.svelte.js'; 16 | 17 | export {useDragOperation} from './core/hooks/use-drag-operation.js'; 18 | 19 | export {KeyboardSensor, PointerSensor} from '@dnd-kit/dom'; 20 | export type {DragDropManager} from '@dnd-kit/dom'; 21 | -------------------------------------------------------------------------------- /packages/svelte/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import {vitePreprocess} from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | compilerOptions: {runes: true}, 11 | 12 | kit: { 13 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 14 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 15 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 16 | adapter: adapter(), 17 | 18 | alias: { 19 | runed: 'src/lib/_runed/index.js', 20 | $hooks: 'src/lib/hooks/index.js', 21 | }, 22 | }, 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/core/context/renderer.svelte.ts: -------------------------------------------------------------------------------- 1 | import type {Renderer as AbstractRenderer} from '@dnd-kit/abstract'; 2 | 3 | export type Renderer = { 4 | renderer: AbstractRenderer; 5 | trackRendering: (callback: () => void) => void; 6 | }; 7 | 8 | // Does this work? Idk, best to keep it though. 9 | export function useRenderer(): Renderer { 10 | let version = $state(0); 11 | let rendering: Promise | null = null; 12 | let resolve: (() => void) | null = null; 13 | 14 | $effect.pre(() => { 15 | void version; 16 | 17 | resolve?.(); 18 | rendering = null; 19 | }); 20 | 21 | return { 22 | renderer: { 23 | get rendering() { 24 | return rendering ?? Promise.resolve(); 25 | }, 26 | }, 27 | trackRendering(callback: () => void) { 28 | if (!rendering) { 29 | rendering = new Promise((res) => { 30 | resolve = res; 31 | }); 32 | } 33 | 34 | callback(); 35 | version++; 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/core/hooks/use-drag-operation.ts: -------------------------------------------------------------------------------- 1 | import type {Data} from '@dnd-kit/abstract'; 2 | import type {Draggable, Droppable, DragDropManager} from '@dnd-kit/dom'; 3 | import {useComputed} from '$hooks'; 4 | import {useDragDropManager} from './use-drag-drop-manager.js'; 5 | 6 | export function useDragOperation< 7 | T extends Data = Data, 8 | U extends Draggable = Draggable, 9 | V extends Droppable = Droppable, 10 | W extends DragDropManager = DragDropManager, 11 | >() { 12 | const manager = useDragDropManager(); 13 | // TODO: (haniel) there might be an issue with this, 14 | // idk if it accounts for manager.current being reactive yet 15 | const source = useComputed(() => manager.current?.dragOperation.source); 16 | const target = useComputed(() => manager.current?.dragOperation.target); 17 | 18 | return { 19 | get source() { 20 | return source.value; 21 | }, 22 | get target() { 23 | return target.value; 24 | }, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # sv 2 | 3 | Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```sh 10 | # create a new project in the current directory 11 | npx sv create 12 | 13 | # create a new project in my-app 14 | npx sv create my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```sh 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```sh 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/hooks/use-deep-signal.ts: -------------------------------------------------------------------------------- 1 | import {effect, untracked} from '@dnd-kit/state'; 2 | import {createSubscriber} from 'svelte/reactivity'; 3 | 4 | /** Trigger a re-render when reading signal properties of an object. */ 5 | export function useDeepSignal(target: T): T { 6 | if (!target) return target; 7 | 8 | const tracked = new Map(); 9 | 10 | const subscribe = createSubscriber((update) => 11 | effect(() => { 12 | for (const entry of tracked) { 13 | const [key] = entry; 14 | const prev = untracked(() => entry[1]); 15 | const latest = (target as any)[key]; 16 | if (prev !== latest) { 17 | update(); 18 | tracked.set(key, latest); 19 | } 20 | } 21 | }) 22 | ); 23 | 24 | return new Proxy(target, { 25 | get(target, key) { 26 | const value = (target as any)[key]; 27 | 28 | tracked.set(key, value); 29 | 30 | // Make this getter reactive if read in a Svelte effect/derived 31 | subscribe(); 32 | 33 | return value; 34 | }, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "prepare": "svelte-kit sync || echo ''", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 13 | }, 14 | "devDependencies": { 15 | "@iconify/json": "^2.2.380", 16 | "@sveltejs/adapter-static": "^3.0.9", 17 | "@sveltejs/kit": "^2.22.0", 18 | "@sveltejs/vite-plugin-svelte": "^6.0.0", 19 | "@unocss/preset-mini": "^66.5.0", 20 | "@unocss/reset": "^66.5.0", 21 | "svelte": "^5.38.6", 22 | "svelte-check": "^4.0.0", 23 | "typescript": "^5.0.0", 24 | "unocss": "^66.5.0", 25 | "unocss-preset-animations": "^1.2.1", 26 | "unplugin-icons": "^22.2.0", 27 | "vite": "^7.0.4" 28 | }, 29 | "dependencies": { 30 | "@dnd-kit-svelte/svelte": "^0.1.1", 31 | "@dnd-kit/abstract": "^0.1.21", 32 | "@dnd-kit/helpers": "^0.1.21", 33 | "motion-sve": "^0.2.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/src/routes/from-docs/styles.css: -------------------------------------------------------------------------------- 1 | .Root { 2 | display: flex; 3 | flex-direction: row; 4 | gap: 20px; 5 | flex-wrap: wrap; 6 | } 7 | 8 | .Column { 9 | display: flex; 10 | flex-direction: column; 11 | gap: 10px; 12 | padding: 20px; 13 | min-width: 175px; 14 | min-height: 200px; 15 | background-color: rgba(0, 0, 0, 0.1); 16 | border: 1px solid rgba(0, 0, 0, 0.1); 17 | border-radius: 10px; 18 | } 19 | 20 | .Item { 21 | appearance: none; 22 | background: #fff; 23 | color: #666; 24 | padding: 12px 20px; 25 | border: none; 26 | border-radius: 5px; 27 | cursor: grab; 28 | transition: 29 | transform 0.2s ease, 30 | box-shadow 0.2s ease; 31 | transform: scale(1); 32 | box-shadow: 33 | inset 0px 0px 1px rgba(0, 0, 0, 0.4), 34 | 0 0 0 calc(1px / var(--scale-x, 1)) rgba(63, 63, 68, 0.05), 35 | 0px 1px calc(2px / var(--scale-x, 1)) 0 rgba(34, 33, 81, 0.05); 36 | } 37 | 38 | .Item[data-dragging='true'] { 39 | transform: scale(1.02); 40 | box-shadow: 41 | inset 0px 0px 1px rgba(0, 0, 0, 0.5), 42 | -1px 0 15px 0 rgba(34, 33, 81, 0.01), 43 | 0px 15px 15px 0 rgba(34, 33, 81, 0.25); 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Haniel Ubogu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import {includeIgnoreFile} from '@eslint/compat'; 4 | import svelte from 'eslint-plugin-svelte'; 5 | import globals from 'globals'; 6 | import {fileURLToPath} from 'node:url'; 7 | import ts from 'typescript-eslint'; 8 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 9 | 10 | export default ts.config( 11 | includeIgnoreFile(gitignorePath), 12 | js.configs.recommended, 13 | ...ts.configs.recommended, 14 | ...svelte.configs['flat/recommended'], 15 | prettier, 16 | ...svelte.configs['flat/prettier'], 17 | { 18 | languageOptions: { 19 | globals: { 20 | ...globals.browser, 21 | ...globals.node, 22 | }, 23 | }, 24 | }, 25 | { 26 | files: ['**/*.svelte'], 27 | languageOptions: { 28 | parserOptions: { 29 | parser: ts.parser, 30 | }, 31 | }, 32 | }, 33 | { 34 | rules: { 35 | '@typescript-eslint/no-empty-object-type': 'off', 36 | '@typescript-eslint/no-explicit-any': 'off', 37 | '@typescript-eslint/no-unused-vars': 'off', 38 | '@typescript-eslint/no-unsafe-function-type': 'off', 39 | }, 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /docs/src/lib/components/examples/basic/basic.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | { 14 | if (event.canceled) return; 15 | target = event.operation.target?.id; 16 | }} 17 | > 18 |
19 | {#if !target} 20 | {@render draggable()} 21 | {:else} 22 |
Drop here
23 | {/if} 24 |
25 | 26 |
27 | {#each targets as id} 28 | 29 | {#if target === id} 30 | {@render draggable()} 31 | {:else} 32 |
Drop here
33 | {/if} 34 |
35 | {/each} 36 |
37 |
38 | 39 | 40 | {#snippet draggable()} 41 | 42 | {/snippet} 43 | -------------------------------------------------------------------------------- /docs/src/lib/components/examples/sortable/sortable-item.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 | 21 |
22 | {task.content} 23 |
24 | 25 | 26 | {#if !isOverlay && isDragging.current} 27 |
28 | 29 |
30 | Moving: {task.content} 31 |
32 |
33 | {/if} 34 |
35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "author": "Haniel Ubogu ", 7 | "scripts": { 8 | "build": "pnpm -r build", 9 | "build:packages": "pnpm -F \"./packages/**\" --parallel build", 10 | "check": "pnpm build:packages && pnpm -r check", 11 | "dev": "pnpm -F \"./packages/**\" svelte-kit sync && pnpm -r --parallel --reporter append-only --color dev", 12 | "format": "prettier --write .", 13 | "lint": "prettier --check . && eslint .", 14 | "release": "pnpm build:packages && changeset publish" 15 | }, 16 | "devDependencies": { 17 | "@changesets/cli": "^2.27.11", 18 | "@eslint/compat": "^1.2.3", 19 | "@sveltejs/adapter-vercel": "^5.5.2", 20 | "@sveltejs/kit": "^2.0.0", 21 | "@sveltejs/vite-plugin-svelte": "^4.0.0", 22 | "eslint": "^9.7.0", 23 | "eslint-config-prettier": "^9.1.0", 24 | "eslint-plugin-svelte": "^2.36.0", 25 | "globals": "^15.0.0", 26 | "prettier": "^3.3.2", 27 | "prettier-plugin-svelte": "^3.2.6", 28 | "svelte": "^5.0.0", 29 | "svelte-check": "^4.0.0", 30 | "typescript": "^5.0.0", 31 | "typescript-eslint": "^8.0.0", 32 | "vite": "^5.4.11" 33 | }, 34 | "pnpm": { 35 | "onlyBuiltDependencies": [ 36 | "esbuild" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetIcons, 4 | presetWebFonts, 5 | presetWind3, 6 | transformerDirectives, 7 | transformerVariantGroup, 8 | } from 'unocss'; 9 | import {fontFamily} from '@unocss/preset-mini/theme'; 10 | import shadcnPreset from './presets/shadcn-preset'; 11 | import customPreset from './presets/custom-preset'; 12 | 13 | export default defineConfig({ 14 | content: { 15 | filesystem: [ 16 | './node_modules/bits-ui/dist/**/*.{html,js,svelte,ts}', 17 | './node_modules/@tauri-controls/svelte/**/*.{js,svelte,ts}', 18 | ], 19 | pipeline: { 20 | include: [/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html|ts)($|\?)/], 21 | }, 22 | }, 23 | theme: { 24 | colors: { 25 | orange: '#FF3E00', 26 | }, 27 | fontFamily: { 28 | manrope: ['Manrope', fontFamily.sans], 29 | }, 30 | }, 31 | shortcuts: [ 32 | { 33 | 'container-base': 'max-w-3xl mx-a', 34 | 'droppable-container': 'bg-white p-3 rd-34px', 35 | }, 36 | ], 37 | configDeps: ['./presets/custom-preset.ts', './presets/shadcn-preset.ts'], 38 | presets: [ 39 | customPreset(), 40 | shadcnPreset(), 41 | presetWind3(), 42 | presetIcons({scale: 1.2}), 43 | presetWebFonts({ 44 | fonts: { 45 | manrope: 'Manrope:400;500;600;700;800', 46 | }, 47 | }), 48 | ], 49 | transformers: [transformerDirectives(), transformerVariantGroup()], 50 | }); 51 | -------------------------------------------------------------------------------- /docs/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | {@render children()} 18 |
19 | 20 | 49 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/_runed/extract/types.ts: -------------------------------------------------------------------------------- 1 | type AnyFn = (...args: any[]) => any; 2 | 3 | // util: detect any 4 | type IsAny = 0 extends 1 & T ? true : false; 5 | // strip/keep nil 6 | type StripNil = Exclude; 7 | type KeepNil = (undefined extends T ? U | undefined : U) extends infer V 8 | ? null extends T 9 | ? V | null 10 | : V 11 | : never; 12 | 13 | /** 14 | * Represents a value that can either be of type T or a function that returns type T 15 | * @template T The type of the value or return value 16 | * If T is a function type, require Getter. Else allow T | Getter. 17 | */ 18 | export type MaybeGetter = 19 | IsAny extends true 20 | ? T | Getter 21 | : [StripNil] extends [AnyFn] 22 | ? KeepNil>> 23 | : T | Getter; 24 | 25 | export type Getter = () => T; 26 | 27 | /** 28 | * Makes all properties of an object type resolvable (either the value or a function returning the value) 29 | * @template T The object type whose properties should be made resolvable 30 | */ 31 | export type MaybeGetterObject = { 32 | [K in keyof T]: MaybeGetter; 33 | }; 34 | 35 | export type UnwrapMaybeGetter = T extends MaybeGetter ? U : T; 36 | 37 | export type UnwrapMaybeGetterObject = { 38 | [K in keyof T]: UnwrapMaybeGetter; 39 | }; 40 | 41 | export type FnObject = { 42 | [K in keyof T]: Getter>; 43 | }; 44 | -------------------------------------------------------------------------------- /docs/src/lib/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | svelte-logo -------------------------------------------------------------------------------- /docs/src/lib/components/metadata.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | {title} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/_runed/vue-reactivity/lens.svelte.ts: -------------------------------------------------------------------------------- 1 | import {type Box, BoxSymbol} from './box.svelte.js'; 2 | 3 | const WritableLensSymbol = Symbol('is-writable-lens'); 4 | 5 | // Types 6 | export type LensGetter = (oldValue?: T) => T; 7 | export type LensSetter = (newValue: T) => void; 8 | 9 | export interface WritableLensOptions { 10 | get: LensGetter; 11 | set: LensSetter; 12 | } 13 | 14 | export type Lens = Box & { 15 | readonly current: T; 16 | }; 17 | 18 | export type WritableLens = Lens & { 19 | [WritableLensSymbol]: true; 20 | }; 21 | 22 | export function lens(getter: LensGetter): Lens; 23 | export function lens(options: WritableLensOptions): WritableLens; 24 | export function lens(arg: LensGetter | WritableLensOptions): Lens | WritableLens { 25 | let prev: T | undefined; 26 | 27 | const get: LensGetter = typeof arg === 'function' ? arg : arg.get; 28 | const set: LensSetter | undefined = typeof arg === 'function' ? undefined : arg.set; 29 | 30 | const derived = $derived.by(() => { 31 | const next = get(prev); 32 | prev = next; 33 | return next; 34 | }); 35 | 36 | if (set) { 37 | return { 38 | [BoxSymbol]: true, 39 | [WritableLensSymbol]: true, 40 | get current() { 41 | return derived; 42 | }, 43 | set current(newValue: T) { 44 | set(newValue as unknown as S); 45 | prev = newValue; 46 | }, 47 | }; 48 | } 49 | 50 | return { 51 | [BoxSymbol]: true, 52 | get current() { 53 | return derived; 54 | }, 55 | 56 | // TBD If this is something desirable 57 | // get current() { 58 | // const next = get(prev); 59 | // prev = next; 60 | // return next; 61 | // }, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Packages 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | release: 13 | name: Release 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20.x 25 | registry-url: 'https://registry.npmjs.org' 26 | scope: '@dnd-kit-svelte' 27 | always-auth: true 28 | 29 | - name: Setup PNPM 30 | uses: pnpm/action-setup@v2 31 | with: 32 | version: 9 33 | 34 | - name: Get pnpm store directory 35 | shell: bash 36 | run: | 37 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 38 | 39 | - name: Setup pnpm cache 40 | uses: actions/cache@v3 41 | with: 42 | path: ${{ env.STORE_PATH }} 43 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-pnpm-store- 46 | 47 | - name: Install dependencies 48 | run: pnpm install --no-frozen-lockfile 49 | 50 | - name: Create Release Pull Request or Publish 51 | id: changesets 52 | uses: changesets/action@v1 53 | with: 54 | publish: pnpm run release 55 | commit: 'chore: version packages' 56 | title: 'chore: version packages' 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 60 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 61 | -------------------------------------------------------------------------------- /docs/src/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: 0 0% 100%; 3 | --foreground: 224 71.4% 4.1%; 4 | 5 | --muted: 220 14.3% 95.9%; 6 | --muted-foreground: 220 8.9% 46.1%; 7 | 8 | --popover: 0 0% 100%; 9 | --popover-foreground: 224 71.4% 4.1%; 10 | 11 | --card: 0 0% 100%; 12 | --card-foreground: 224 71.4% 4.1%; 13 | 14 | --border: 220 13% 91%; 15 | --input: 220 13% 91%; 16 | 17 | --primary: 220.9 39.3% 11%; 18 | --primary-foreground: 210 20% 98%; 19 | 20 | --secondary: 220 14.3% 95.9%; 21 | --secondary-foreground: 220.9 39.3% 11%; 22 | 23 | --accent: 220 14.3% 95.9%; 24 | --accent-foreground: 220.9 39.3% 11%; 25 | 26 | --destructive: 0 72.2% 50.6%; 27 | --destructive-foreground: 210 20% 98%; 28 | 29 | --ring: 224 71.4% 4.1%; 30 | 31 | --radius: 0.5rem; 32 | 33 | font-weight: 400; 34 | --at-apply: font-manrope bg-#FAFAFA; 35 | font-synthesis: none; 36 | text-rendering: optimizeLegibility; 37 | -webkit-font-smoothing: antialiased; 38 | -moz-osx-font-smoothing: grayscale; 39 | 40 | scroll-behavior: smooth; 41 | } 42 | 43 | * { 44 | line-height: 1; 45 | } 46 | 47 | .dark { 48 | --background: 240 10% 3.9%; 49 | --foreground: 0 0% 98%; 50 | 51 | --muted: 240 3.7% 15.9%; 52 | --muted-foreground: 240 5% 64.9%; 53 | 54 | --popover: 240 10% 3.9%; 55 | --popover-foreground: 0 0% 98%; 56 | 57 | --card: 240 10% 3.9%; 58 | --card-foreground: 0 0% 98%; 59 | 60 | --border: 240 3.7% 15.9%; 61 | --input: 240 3.7% 15.9%; 62 | 63 | --primary: 0 0% 98%; 64 | --primary-foreground: 240 5.9% 10%; 65 | 66 | --secondary: 240 3.7% 15.9%; 67 | --secondary-foreground: 0 0% 98%; 68 | 69 | --accent: 240 3.7% 15.9%; 70 | --accent-foreground: 0 0% 98%; 71 | 72 | --destructive: 0 62.8% 30.6%; 73 | --destructive-foreground: 0 85.7% 97.3%; 74 | 75 | --ring: 240 3.7% 15.9%; 76 | } 77 | -------------------------------------------------------------------------------- /docs/src/lib/components/examples/nested/task-column.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | 28 |
29 | 30 |
31 |
32 |
33 |

34 | 35 | {data.title} 36 |

37 |

{data.description}

38 |
39 | 40 |
41 |
42 | 43 |
44 | {@render children(isDragging.current)} 45 |
46 |
47 | 48 | {#if !isOverlay && isDragging.current} 49 |
50 | {/if} 51 |
52 | -------------------------------------------------------------------------------- /docs/src/lib/components/examples/nested/task-item.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 31 | 32 |
33 | 34 |
40 |
41 |

{task.title}

42 |

{task.description}

43 |
44 | 45 |
46 |
47 | 48 | 49 | {#if !isOverlay && isDragging.current} 50 |
51 | 52 |
53 | Moving: {task.title} 54 |
55 |
56 | {/if} 57 |
58 | -------------------------------------------------------------------------------- /packages/svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dnd-kit-svelte/svelte", 3 | "version": "0.1.5", 4 | "license": "MIT", 5 | "author": "Haniel Ubogu ", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/HanielU/dnd-kit-svelte.git" 9 | }, 10 | "scripts": { 11 | "build": "pnpm package", 12 | "dev": "pnpm watch", 13 | "dev:svelte": "vite dev", 14 | "watch": "svelte-package --watch", 15 | "package": "svelte-kit sync && svelte-package && publint", 16 | "prepublishOnly": "npm run package", 17 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 18 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 19 | }, 20 | "files": [ 21 | "dist", 22 | "!dist/**/*.test.*", 23 | "!dist/**/*.spec.*" 24 | ], 25 | "sideEffects": [ 26 | "**/*.css" 27 | ], 28 | "svelte": "./dist/index.js", 29 | "types": "./dist/index.d.ts", 30 | "type": "module", 31 | "exports": { 32 | ".": { 33 | "types": "./dist/index.d.ts", 34 | "svelte": "./dist/index.js" 35 | }, 36 | "./sortable": { 37 | "types": "./dist/sortable/index.d.ts", 38 | "svelte": "./dist/sortable/index.js" 39 | } 40 | }, 41 | "peerDependencies": { 42 | "svelte": "^5.0.0" 43 | }, 44 | "devDependencies": { 45 | "@sveltejs/adapter-auto": "^6.0.0", 46 | "@sveltejs/kit": "^2.22.0", 47 | "@sveltejs/package": "^2.0.0", 48 | "@sveltejs/vite-plugin-svelte": "^6.0.0", 49 | "publint": "^0.3.2", 50 | "svelte": "^5.0.0", 51 | "svelte-check": "^4.0.0", 52 | "typescript": "^5.0.0", 53 | "vite": "^7.0.4" 54 | }, 55 | "keywords": [ 56 | "svelte" 57 | ], 58 | "dependencies": { 59 | "@dnd-kit/abstract": "^0.1.21", 60 | "@dnd-kit/collision": "^0.1.21", 61 | "@dnd-kit/dom": "^0.1.21", 62 | "@dnd-kit/state": "^0.1.21" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/core/droppable/use-droppable.ts: -------------------------------------------------------------------------------- 1 | import type {Data} from '@dnd-kit/abstract'; 2 | import type {DroppableInput} from '@dnd-kit/dom'; 3 | import {Droppable} from '@dnd-kit/dom'; 4 | import {deepEqual} from '@dnd-kit/state'; 5 | import {defaultCollisionDetection} from '@dnd-kit/collision'; 6 | import {resolveObj, lens, type MaybeGetterObject} from 'runed'; 7 | import {makeRef} from '$lib/utilities/index.js'; 8 | import {useDeepSignal, useOnElementChange, useOnValueChange} from '$hooks'; 9 | import {useInstance} from '../hooks/use-instance.svelte.js'; 10 | 11 | export type UseDroppableInput = MaybeGetterObject>; 12 | 13 | export function useDroppable(input: UseDroppableInput) { 14 | const {collisionDetector, data, disabled, element, id, accept, type} = input; 15 | const droppable = useInstance( 16 | (manager) => 17 | new Droppable( 18 | { 19 | ...resolveObj(input), 20 | register: false, 21 | }, 22 | manager 23 | ) 24 | ); 25 | const trackedDroppable = useDeepSignal(droppable); 26 | 27 | useOnValueChange(id, (id) => { 28 | droppable.id = id; 29 | }); 30 | useOnElementChange(element, (element) => { 31 | droppable.element = element; 32 | }); 33 | useOnValueChange(accept, (accept) => (droppable.accept = accept), undefined, deepEqual); 34 | useOnValueChange(collisionDetector, (collisionDetector) => { 35 | droppable.collisionDetector = collisionDetector ?? defaultCollisionDetection; 36 | }); 37 | useOnValueChange(data, (data) => { 38 | if (data) droppable.data = data; 39 | }); 40 | useOnValueChange(disabled, (disabled) => { 41 | droppable.disabled = disabled === true; 42 | }); 43 | useOnValueChange(type, (type) => { 44 | droppable.type = type; 45 | }); 46 | 47 | return { 48 | droppable: trackedDroppable, 49 | isDropTarget: lens(() => trackedDroppable.isDropTarget), 50 | ref: makeRef(droppable, 'element'), 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /docs/src/lib/components/examples/sortable/sortable-list.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | { 31 | todos = move(todos, event); 32 | }} 33 | > 34 |
35 | {@render taskList('in-progress', 'In Progress', todos['in-progress'])} 36 | {@render taskList('done', 'Done', todos['done'])} 37 |
38 | 39 | 40 | {#snippet children(source)} 41 | {@const task = todos[source.data.group].find((todo) => todo.id === source.id)!} 42 | 43 | {/snippet} 44 | 45 |
46 | 47 | {#snippet taskList(id: string, title: string, tasks: Todo[])} 48 | 55 |

{title}

56 | 57 |
58 | {#each tasks as task, index (task.id)} 59 | index} group={id} data={{group: id}} type="item" /> 60 | {/each} 61 |
62 |
63 | {/snippet} 64 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/_runed/context.ts: -------------------------------------------------------------------------------- 1 | import { getContext, hasContext, setContext } from "svelte"; 2 | 3 | export class Context { 4 | readonly #name: string; 5 | readonly #key: symbol; 6 | readonly #fallback?: TContext; 7 | 8 | /** 9 | * @param name The name of the context. 10 | * This is used for generating the context key and error messages. 11 | * @param fallback Optional fallback value to return when context doesn't exist. 12 | */ 13 | constructor(name: string, fallback?: TContext) { 14 | this.#name = name; 15 | this.#key = Symbol(name); 16 | this.#fallback = fallback; 17 | } 18 | 19 | /** 20 | * The key used to get and set the context. 21 | * 22 | * It is not recommended to use this value directly. 23 | * Instead, use the methods provided by this class. 24 | */ 25 | get key(): symbol { 26 | return this.#key; 27 | } 28 | 29 | /** 30 | * Checks whether this has been set in the context of a parent component. 31 | * 32 | * Must be called during component initialisation. 33 | */ 34 | exists(): boolean { 35 | return hasContext(this.#key); 36 | } 37 | 38 | /** 39 | * Retrieves the context that belongs to the closest parent component. 40 | * 41 | * Must be called during component initialisation. 42 | * 43 | * @throws An error if the context does not exist. 44 | */ 45 | get(): TContext { 46 | const context: TContext | undefined = getContext(this.#key); 47 | if (context === undefined) { 48 | throw new Error(`Context "${this.#name}" not found`); 49 | } 50 | return context; 51 | } 52 | 53 | /** 54 | * Retrieves the context that belongs to the closest parent component, 55 | * or the given fallback value if the context does not exist. 56 | * 57 | * Must be called during component initialisation. 58 | */ 59 | getOr(fallback?: TFallback) { 60 | const context = getContext(this.#key); 61 | 62 | if (context === undefined) { 63 | return (fallback ?? this.#fallback) as TFallback extends null ? TContext | null : TContext; 64 | } 65 | return context; 66 | } 67 | 68 | /** 69 | * Associates the given value with the current component and returns it. 70 | * 71 | * Must be called during component initialisation. 72 | */ 73 | set(context: TContext): TContext { 74 | return setContext(this.#key, context); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /docs/src/lib/components/examples/nested/draggable-containers.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | { 28 | const {source} = event.operation; 29 | if (source?.type === 'column') return; 30 | items = move(items, event); 31 | }} 32 | > 33 |
34 | {#each Object.entries(items) as [columnId, nesteds], colIdx (columnId)} 35 | 36 | {#each nesteds as nested, nestedIdx (nested.id)} 37 | 38 | {/each} 39 | 40 | {/each} 41 |
42 | 43 |

Drag and drop to reorder

44 | 45 | 46 | {#snippet children(source)} 47 | 48 | {#if source.data.group} 49 | {@const task = items[source.data.group as keyof typeof items]?.find((task) => task.id === source.id)!} 50 | 51 | {:else} 52 | 53 | 54 | {#each items[source.id as keyof typeof items] as item, itemIdx (item.id)} 55 | 56 | {/each} 57 | 58 | {/if} 59 | {/snippet} 60 | 61 |
62 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/core/draggable/drag-overlay.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 74 | 75 | {#if source} 76 | 77 | {@render children?.(useDeepSignal(source))} 78 | 79 | {/if} 80 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/_runed/extract/unwrap.ts: -------------------------------------------------------------------------------- 1 | import type {FnObject, Getter, MaybeGetter, MaybeGetterObject} from './types.js'; 2 | 3 | /** 4 | * Resolves a value that may be a getter function or a direct value. 5 | * 6 | * If the input is a function, it will be invoked to retrieve the actual value. 7 | * 8 | * @template T - The expected return type. 9 | * @param value - A value or a function that returns a value. 10 | * @returns The resolved value or the default. 11 | */ 12 | export function resolve(value: MaybeGetter): T { 13 | return typeof value === 'function' ? (value as Getter)() : (value as T); 14 | } 15 | 16 | /** 17 | * Resolves an object whose properties may be getter functions or direct values. 18 | * 19 | * For each property in the input object, if the value is a function, it will be 20 | * invoked to retrieve the actual value. 21 | * 22 | * @template T - The expected object type. 23 | * @param obj - An object whose property values may be values or functions that return values. 24 | * @returns A new object with all properties resolved to their actual values. 25 | */ 26 | export function resolveObj(obj: MaybeGetterObject): T { 27 | const out: Partial = {}; 28 | // const keys = Reflect.ownKeys(obj) as (keyof T)[]; 29 | const keys = Object.keys(obj) as (keyof T)[]; 30 | for (let i = 0; i < keys.length; i++) { 31 | const k = keys[i]; 32 | const v = (obj as any)[k]; 33 | // inline extract to avoid extra call and Function cast 34 | (out as any)[k] = typeof v === 'function' ? (v as () => unknown)() : v; 35 | } 36 | return out as T; 37 | } 38 | 39 | /** 40 | * Converts a value that may be a getter function or a direct value to a getter function. 41 | * 42 | * If the input is a function, return the function. 43 | * 44 | * @template T - The expected return type. 45 | * @param value - A value or a function that returns a value. 46 | * @returns A getter function 47 | */ 48 | export function asGetter(value: MaybeGetter | undefined): Getter { 49 | return typeof value === 'function' ? (value as Getter) : () => value as T; 50 | } 51 | 52 | /** 53 | * Ensures all properties are getter functions. 54 | * If a property is already a getter function, keep it. Otherwise wrap it in a no-arg function. 55 | */ 56 | export function toFnObject(obj: MaybeGetterObject): FnObject { 57 | const out: Partial> = {}; 58 | const keys = Object.keys(obj) as (keyof T)[]; 59 | for (let i = 0; i < keys.length; i++) { 60 | const k = keys[i]; 61 | const v = obj[k]; 62 | (out as any)[k] = typeof v === 'function' ? v : () => v; 63 | } 64 | return out as FnObject; 65 | } 66 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/core/draggable/use-draggable.ts: -------------------------------------------------------------------------------- 1 | import type {Data} from '@dnd-kit/abstract'; 2 | import type {DraggableInput} from '@dnd-kit/dom'; 3 | import {Draggable} from '@dnd-kit/dom'; 4 | import {deepEqual} from '@dnd-kit/state'; 5 | import {resolveObj, lens, type MaybeGetterObject} from 'runed'; 6 | import {makeRef} from '$lib/utilities/index.js'; 7 | import {useDeepSignal, useOnElementChange, useOnValueChange} from '$hooks'; 8 | import {useInstance} from '../hooks/use-instance.svelte.js'; 9 | 10 | export type UseDraggableInput = MaybeGetterObject>; 11 | 12 | export function useDraggable(input: UseDraggableInput) { 13 | const {disabled, data, element, handle, id, modifiers, sensors} = input; 14 | const draggable = useInstance( 15 | (manager) => 16 | new Draggable( 17 | { 18 | ...resolveObj(input), 19 | register: false, 20 | }, 21 | manager 22 | ) 23 | ); 24 | 25 | const trackedDraggable = useDeepSignal(draggable); 26 | 27 | useOnValueChange(id, (id) => { 28 | draggable.id = id; 29 | }); 30 | useOnElementChange(handle, (handle) => { 31 | draggable.handle = handle; 32 | }); 33 | useOnElementChange(element, (element) => { 34 | draggable.element = element; 35 | }); 36 | useOnValueChange(data, (data) => { 37 | if (data) draggable.data = data; 38 | }); 39 | useOnValueChange(disabled, (disabled) => { 40 | draggable.disabled = disabled === true; 41 | }); 42 | useOnValueChange(sensors, (sensors) => { 43 | draggable.sensors = sensors; 44 | }); 45 | useOnValueChange( 46 | modifiers, 47 | (modifiers) => { 48 | draggable.modifiers = modifiers; 49 | }, 50 | undefined, 51 | deepEqual 52 | ); 53 | useOnValueChange(input.feedback, (feedback) => { 54 | draggable.feedback = feedback ?? 'default'; 55 | }); 56 | useOnValueChange(input.alignment, (alignment) => { 57 | draggable.alignment = alignment; 58 | }); 59 | 60 | return { 61 | draggable: trackedDraggable, 62 | isDragging: lens(() => trackedDraggable.isDragging), 63 | isDropping: lens(() => trackedDraggable.isDropping), 64 | isDragSource: lens(() => trackedDraggable.isDragSource), 65 | handleRef: makeRef(draggable, 'handle'), 66 | ref: makeRef(draggable, 'element'), 67 | }; 68 | } 69 | 70 | // FROM CURSOR CHAT: 71 | // our useDeepSignal doesn’t support a true “flushSync”; 72 | // updates from the @dnd-kit/state effect already bump state synchronously enough, 73 | // and the microtask is only for first-time key registration. 74 | // Keeping the “synchronous” path in use-draggable.svelte.ts adds no value 75 | // function shouldUpdateSynchronously(key: string, oldValue: any, newValue: any) { 76 | // // Update synchronously after drop animation 77 | // if (key === 'isDragSource' && !newValue && oldValue) return true; 78 | 79 | // return false; 80 | // } 81 | -------------------------------------------------------------------------------- /docs/src/lib/components/section.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 |

{title}

17 | 35 |
36 | 37 |
38 | {@render children()} 39 |
40 |
41 | -------------------------------------------------------------------------------- /docs/presets/shadcn-preset.ts: -------------------------------------------------------------------------------- 1 | import {definePreset} from 'unocss'; 2 | 3 | export default definePreset(() => ({ 4 | name: 'shadcn', 5 | 6 | theme: { 7 | container: { 8 | center: true, 9 | padding: '2rem', 10 | screens: { 11 | '2xl': '1400px', 12 | }, 13 | }, 14 | colors: { 15 | border: 'hsl(var(--border) / )', 16 | input: 'hsl(var(--input) / )', 17 | ring: 'hsl(var(--ring) / )', 18 | background: 'hsl(var(--background) / )', 19 | foreground: 'hsl(var(--foreground) / )', 20 | primary: { 21 | DEFAULT: 'hsl(var(--primary) / )', 22 | foreground: 'hsl(var(--primary-foreground) / )', 23 | }, 24 | secondary: { 25 | DEFAULT: 'hsl(var(--secondary) / )', 26 | foreground: 'hsl(var(--secondary-foreground) / )', 27 | }, 28 | destructive: { 29 | DEFAULT: 'hsl(var(--destructive) / )', 30 | foreground: 'hsl(var(--destructive-foreground) / )', 31 | }, 32 | muted: { 33 | DEFAULT: 'hsl(var(--muted) / )', 34 | foreground: 'hsl(var(--muted-foreground) / )', 35 | }, 36 | accent: { 37 | DEFAULT: 'hsl(var(--accent) / )', 38 | foreground: 'hsl(var(--accent-foreground) / )', 39 | }, 40 | popover: { 41 | DEFAULT: 'hsl(var(--popover) / )', 42 | foreground: 'hsl(var(--popover-foreground) / )', 43 | }, 44 | card: { 45 | DEFAULT: 'hsl(var(--card) / )', 46 | foreground: 'hsl(var(--card-foreground) / )', 47 | }, 48 | }, 49 | borderRadius: { 50 | lg: 'var(--radius)', 51 | md: 'calc(var(--radius) - 2px)', 52 | sm: 'calc(var(--radius) - 4px)', 53 | }, 54 | fontFamily: { 55 | // sans: ["Inter", ...fontFamily.sans], 56 | }, 57 | }, 58 | 59 | preflights: [ 60 | // commented out because I put the CSS in app.css 61 | // also, I'm making use of this extension https://marketplace.visualstudio.com/items?itemName=dexxiez.shadcn-color-preview#:~:text=The%20shadcn%20HSL%20Preview%20extension,directly%20in%20your%20CSS%20files. 62 | // { 63 | // layer: 'default', 64 | // getCSS: () => `:root { 65 | // --background: 0 0% 100%; 66 | // --foreground: 224 71.4% 4.1%; 67 | // --muted: 220 14.3% 95.9%; 68 | // --muted-foreground: 220 8.9% 46.1%; 69 | // --popover: 0 0% 100%; 70 | // --popover-foreground: 224 71.4% 4.1%; 71 | // --card: 0 0% 100%; 72 | // --card-foreground: 224 71.4% 4.1%; 73 | // --border: 220 13% 91%; 74 | // --input: 220 13% 91%; 75 | // --primary: 220.9 39.3% 11%; 76 | // --primary-foreground: 210 20% 98%; 77 | // --secondary: 220 14.3% 95.9%; 78 | // --secondary-foreground: 220.9 39.3% 11%; 79 | // --accent: 220 14.3% 95.9%; 80 | // --accent-foreground: 220.9 39.3% 11%; 81 | // --destructive: 0 72.2% 50.6%; 82 | // --destructive-foreground: 210 20% 98%; 83 | // --ring: 224 71.4% 4.1%; 84 | // --radius: 0.5rem; 85 | // }`, 86 | // }, 87 | ], 88 | })); 89 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/_runed/index.ts: -------------------------------------------------------------------------------- 1 | export * from './watch.svelte.js'; 2 | export * from './is.js'; 3 | export * from './context.js'; 4 | export * from './extract/index.js'; 5 | export * from './vue-reactivity/index.js'; 6 | 7 | export const isDef = (val?: T): val is T => typeof val !== 'undefined'; 8 | 9 | /** 10 | * Converts a style object into a CSS string. 11 | * 12 | * - Filters out properties with `undefined` values. 13 | * - Converts camelCase keys into kebab-case. 14 | * - Appends `px` to numeric values unless the property is unitless. 15 | * 16 | * @param {Record} styleObj - 17 | * An object where keys are CSS property names in camelCase and values are 18 | * strings, numbers, or `undefined`. 19 | * 20 | * @returns {string} A CSS string suitable for inline styles or style attributes. 21 | * 22 | * @example 23 | * css({ backgroundColor: "red", width: 100, opacity: 0.5 }) 24 | * // "background-color:red;width:100px;opacity:0.5" 25 | */ 26 | export function css(styleObj: Record): string { 27 | return Object.entries(styleObj) 28 | .filter(([, value]) => value !== undefined) 29 | .map(([key, value]) => { 30 | const unitlessProps = ['opacity', 'zIndex', 'fontWeight', 'lineHeight', 'order', 'flexGrow', 'flexShrink']; 31 | const formattedValue = typeof value === 'number' && !unitlessProps.includes(key) ? `${value}px` : value; 32 | return `${key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${formattedValue}`; 33 | }) 34 | .join(';'); 35 | } 36 | 37 | /** 38 | * Returns a new object that copies all properties from the given object `props` 39 | * and adds (or overwrites) a property with the specified `key` and `value`. 40 | * 41 | * @template T - Type of the original object. 42 | * @template K - Type of the property key to add. 43 | * @template V - Type of the property value to add. 44 | * 45 | * @param {T} props - The source object whose properties should be copied. 46 | * @param {K} key - The property key to add or overwrite. 47 | * @param {V} value - The value to associate with the given key. 48 | * @returns {T & Record} A new object with all original properties from `props` 49 | * and the additional property `[key]: value`. 50 | */ 51 | export function withProp(props: T, key: K, value: V): T & Record { 52 | return Object.defineProperties({}, { 53 | ...Object.getOwnPropertyDescriptors(props), 54 | [key]: {value, writable: true, enumerable: true, configurable: true}, 55 | } as PropertyDescriptorMap) as any; 56 | } 57 | 58 | /** 59 | * Returns a new object that copies all properties from the given object `props` 60 | * and adds (or overwrites) properties from the `extras` object. 61 | * 62 | * @template T - Type of the original object. 63 | * @template E - Type of the extra properties to add. 64 | * 65 | * @param {T} props - The source object whose properties should be copied. 66 | * @param {E} extras - An object containing additional properties to merge into the result. 67 | * @returns {T & E} A new object with all original properties from `props` 68 | * and all properties from `extras`. 69 | */ 70 | export function withProps(props: T, extras: E): T & E { 71 | return Object.defineProperties( 72 | {}, 73 | { 74 | ...Object.getOwnPropertyDescriptors(props), 75 | ...Object.fromEntries( 76 | Object.entries(extras).map(([k, v]) => [k, {value: v, writable: true, enumerable: true, configurable: true}]) 77 | ), 78 | } 79 | ) as T & E; 80 | } 81 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/sortable/use-sortable.svelte.ts: -------------------------------------------------------------------------------- 1 | import type {Data} from '@dnd-kit/abstract'; 2 | import {batch, deepEqual} from '@dnd-kit/state'; 3 | import {defaultSortableTransition, Sortable, type SortableInput} from '@dnd-kit/dom/sortable'; 4 | import {resolveObj, resolve, lens, watch, asGetter, type MaybeGetterObject} from 'runed'; 5 | import {makeRef} from '$lib/utilities/index.js'; 6 | import {useDeepSignal, useOnElementChange, useOnValueChange} from '$hooks'; 7 | import {useInstance} from '../core/hooks/use-instance.svelte.js'; 8 | 9 | export type UseSortableInput = MaybeGetterObject>; 10 | 11 | export function useSortable(input: UseSortableInput) { 12 | const { 13 | accept, 14 | collisionDetector, 15 | collisionPriority, 16 | id, 17 | data, 18 | element, 19 | handle, 20 | index, 21 | group, 22 | disabled, 23 | feedback, 24 | modifiers, 25 | sensors, 26 | target, 27 | type, 28 | } = input; 29 | 30 | const transition = $derived({ 31 | ...defaultSortableTransition, 32 | ...resolve(input.transition), 33 | }); 34 | 35 | const sortable = useInstance((manager) => { 36 | return new Sortable( 37 | { 38 | ...resolveObj(input), 39 | transition, 40 | register: false, 41 | }, 42 | manager 43 | ); 44 | }); 45 | 46 | const trackedSortable = useDeepSignal(sortable); 47 | 48 | useOnValueChange(id, (id) => (sortable.id = id)); 49 | 50 | // group could be undefined when dragging 51 | watch.pre([asGetter(group), asGetter(index)], ([group, index]) => { 52 | batch(() => { 53 | sortable.group = group; 54 | sortable.index = index; 55 | }); 56 | }); 57 | 58 | useOnValueChange(type, (type) => (sortable.type = type)); 59 | useOnValueChange(accept, (accept) => (sortable.accept = accept), undefined, deepEqual); 60 | useOnValueChange(data, (data) => { 61 | if (data) sortable.data = data; 62 | }); 63 | useOnValueChange( 64 | index, 65 | () => { 66 | if (sortable.manager?.dragOperation.status.idle && transition?.idle) { 67 | sortable.refreshShape(); 68 | } 69 | }, 70 | watch.pre 71 | ); 72 | useOnElementChange(handle, (handle) => { 73 | sortable.handle = handle; 74 | }); 75 | useOnElementChange(element, (element) => { 76 | sortable.element = element; 77 | }); 78 | useOnElementChange(target, (target) => { 79 | sortable.target = target; 80 | }); 81 | useOnValueChange(disabled, (disabled) => { 82 | sortable.disabled = disabled === true; 83 | }); 84 | useOnValueChange(sensors, (sensors) => { 85 | sortable.sensors = sensors; 86 | }); 87 | useOnValueChange(collisionDetector, (collisionDetector) => { 88 | sortable.collisionDetector = collisionDetector; 89 | }); 90 | useOnValueChange(collisionPriority, (collisionPriority) => { 91 | sortable.collisionPriority = collisionPriority; 92 | }); 93 | useOnValueChange(feedback, (feedback) => { 94 | sortable.feedback = feedback ?? 'default'; 95 | }); 96 | useOnValueChange( 97 | () => transition, 98 | () => { 99 | sortable.transition = transition; 100 | }, 101 | undefined, 102 | deepEqual 103 | ); 104 | useOnValueChange( 105 | modifiers, 106 | (modifiers) => { 107 | sortable.modifiers = modifiers; 108 | }, 109 | undefined, 110 | deepEqual 111 | ); 112 | useOnValueChange(input.alignment, (alignment) => { 113 | sortable.alignment = alignment; 114 | }); 115 | 116 | return { 117 | sortable: trackedSortable, 118 | isDragging: lens(() => trackedSortable.isDragging), 119 | isDropping: lens(() => trackedSortable.isDropping), 120 | isDragSource: lens(() => trackedSortable.isDragSource), 121 | isDropTarget: lens(() => trackedSortable.isDropTarget), 122 | handleRef: makeRef(sortable, 'handle'), 123 | ref: makeRef(sortable, 'element'), 124 | sourceRef: makeRef(sortable, 'source'), 125 | targetRef: makeRef(sortable, 'target'), 126 | }; 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dnd-kit-svelte 2 | 3 | > 📚 **Original Documentation**: [next.dndkit.com](https://next.dndkit.com/react/quickstart) 4 | 5 | A Svelte port of the powerful [@dnd-kit][dnd-kit] library - the modern, lightweight, performant, accessible and extensible drag & drop toolkit. 6 | 7 | ## Quick start 8 | 9 | Install it: 10 | 11 | ```bash 12 | npm i @dnd-kit-svelte/svelte 13 | # or 14 | yarn add @dnd-kit-svelte/svelte 15 | # or 16 | pnpm add @dnd-kit-svelte/svelte 17 | ``` 18 | 19 | ## Overview 20 | 21 | This library provides a complete port of dnd-kit to Svelte, maintaining feature parity with the original React implementation while adapting to Svelte's reactivity system. All documentation and APIs from the [original dnd-kit][dnd-kit-docs] library apply here, with some Svelte-specific adaptations. 22 | 23 | ## Examples 24 | 25 | - [Sortable Tasks List](docs/src/lib/components/examples/sortable/sortable-list.svelte) 26 | - [Nested Sortable List](docs/src/lib/components/examples/nested/draggable-containers.svelte) 27 | - [Basic Drag & Drop](docs/src/lib/components/examples/basic/basic.svelte) 28 | 29 | ## Key Differences from React Implementation 30 | 31 | The main difference lies in how reactive values are handled. Since Svelte components don't rerender the same way React components do, we've adapted the API to work with Svelte's reactivity system. 32 | 33 | ### Using Functions for Reactive Inputs 34 | 35 | In hooks like `useSortable`, `useDraggable`, etc., you can pass a function to any field that needs to be reactive. The function will be called whenever the value needs to be accessed, ensuring you always get the latest value from Svelte's reactive scope. 36 | 37 | Example: 38 | 39 | React: 40 | 41 | ```ts 42 | import {useSortable} from '@dnd-kit/sortable'; 43 | 44 | useSortable({ 45 | id: item.id, 46 | data: item, 47 | }); 48 | ``` 49 | 50 | Svelte: 51 | 52 | ```ts 53 | import {useSortable} from '@dnd-kit-svelte/svelte/sortable'; 54 | 55 | useSortable({ 56 | // Static value 57 | id: item.id, 58 | // Reactive value using a function 59 | data: () => item, // Access reactive state value 60 | }); 61 | ``` 62 | 63 | ### Data returned from hooks 64 | 65 | In React, components re-render when their state changes, so hooks can return values directly. However, since Svelte components don't re-render, all non-function properties returned from hooks use a `.current` getter to ensure you always access the latest value. 66 | 67 | Example: 68 | 69 | React: 70 | 71 | ```ts 72 | // React dnd-kit 73 | const { ref, isDragging } = useSortable({ id }); 74 | 75 |
76 | {isDragging ? 'Dragging' : 'Not dragging'} 77 |
78 | ``` 79 | 80 | Svelte: 81 | 82 | ```svelte 83 | 88 | 89 |
90 | {isDragging.current ? 'Dragging' : 'Not dragging'} 91 |
92 | ``` 93 | 94 | This pattern is used consistently across all hooks: 95 | 96 | - [`useDraggable`](https://next.dndkit.com/react/hooks/use-draggable#output) 97 | - [`useDroppable`](https://next.dndkit.com/react/hooks/use-droppable#output) 98 | - [`useSortable`](https://next.dndkit.com/react/hooks/use-sortable#output) 99 | 100 | All state values (e.g `isDragging`, `isDropping`, `isDragSource`, `isDropTarget`) have a `.current` getter to ensure you always access the latest value. 101 | 102 | All refs (`ref`, `handleRef`, `sourceRef`, `targetRef`) are `Attachments` 103 | 104 | ## Core Concepts 105 | 106 | All core concepts from dnd-kit remain the same: 107 | 108 | - Draggable elements 109 | - Droppable areas 110 | - DragDropProvider provider 111 | - Sensors 112 | - Modifiers 113 | - Collision detection 114 | 115 | For detailed documentation on these concepts, please refer to the [original dnd-kit documentation][dnd-kit-docs]. 116 | 117 | ## License 118 | 119 | MIT © [Haniel Ubogu](https://github.com/HanielU) 120 | 121 | [dnd-kit]: https://github.com/clauderic/dnd-kit/tree/experimental 122 | [dnd-kit-docs]: https://next.dndkit.com/react/quickstart 123 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/core/context/drag-drop-provider.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 | 127 | 128 | {@render children?.()} 129 | -------------------------------------------------------------------------------- /packages/svelte/src/lib/_runed/watch.svelte.ts: -------------------------------------------------------------------------------- 1 | import {untrack} from 'svelte'; 2 | import type {Getter} from './extract/types.js'; 3 | 4 | function runEffect(flush: 'post' | 'pre', effect: () => void | VoidFunction): void { 5 | switch (flush) { 6 | case 'post': 7 | $effect(effect); 8 | break; 9 | case 'pre': 10 | $effect.pre(effect); 11 | break; 12 | } 13 | } 14 | 15 | export type WatchOptions = { 16 | /** 17 | * If `true`, the effect doesn't run until one of the `sources` changes. 18 | * 19 | * @default false 20 | */ 21 | lazy?: boolean; 22 | }; 23 | 24 | function runWatcher( 25 | sources: Getter | Array>, 26 | flush: 'post' | 'pre', 27 | effect: (values: T | Array, previousValues: T | undefined | Array) => void | VoidFunction, 28 | options: WatchOptions = {} 29 | ): void { 30 | const {lazy = false} = options; 31 | 32 | // Run the effect immediately if `lazy` is `false`. 33 | let active = !lazy; 34 | 35 | // On the first run, if the dependencies are an array, pass an empty array 36 | // to the previous value instead of `undefined` to allow destructuring. 37 | // 38 | // watch(() => [a, b], ([a, b], [prevA, prevB]) => { ... }); 39 | let previousValues: T | undefined | Array = Array.isArray(sources) ? [] : undefined; 40 | 41 | runEffect(flush, () => { 42 | const values = Array.isArray(sources) ? sources.map((source) => source()) : sources(); 43 | 44 | if (!active) { 45 | active = true; 46 | previousValues = values; 47 | return; 48 | } 49 | 50 | const cleanup = untrack(() => effect(values, previousValues)); 51 | previousValues = values; 52 | return cleanup; 53 | }); 54 | } 55 | 56 | function runWatcherOnce( 57 | sources: Getter | Array>, 58 | flush: 'post' | 'pre', 59 | effect: (values: T | Array, previousValues: T | Array) => void | VoidFunction 60 | ): void { 61 | const cleanupRoot = $effect.root(() => { 62 | let stop = false; 63 | runWatcher( 64 | sources, 65 | flush, 66 | (values, previousValues) => { 67 | if (stop) { 68 | cleanupRoot(); 69 | return; 70 | } 71 | 72 | // Since `lazy` is `true`, `previousValues` is always defined. 73 | const cleanup = effect(values, previousValues as T | Array); 74 | stop = true; 75 | return cleanup; 76 | }, 77 | // Running the effect immediately just once makes no sense at all. 78 | // That's just `onMount` with extra steps. 79 | {lazy: true} 80 | ); 81 | }); 82 | 83 | $effect(() => { 84 | return cleanupRoot; 85 | }); 86 | } 87 | 88 | export function watch>( 89 | sources: { 90 | [K in keyof T]: Getter; 91 | }, 92 | effect: ( 93 | values: T, 94 | previousValues: { 95 | [K in keyof T]: T[K] | undefined; 96 | } 97 | ) => void | VoidFunction, 98 | options?: WatchOptions 99 | ): void; 100 | 101 | export function watch( 102 | source: Getter, 103 | effect: (value: T, previousValue: T | undefined) => void | VoidFunction, 104 | options?: WatchOptions 105 | ): void; 106 | 107 | export function watch( 108 | sources: Getter | Array>, 109 | effect: (values: T | Array, previousValues: T | undefined | Array) => void | VoidFunction, 110 | options?: WatchOptions 111 | ): void { 112 | runWatcher(sources, 'post', effect, options); 113 | } 114 | 115 | function watchPre>( 116 | sources: { 117 | [K in keyof T]: Getter; 118 | }, 119 | effect: ( 120 | values: T, 121 | previousValues: { 122 | [K in keyof T]: T[K] | undefined; 123 | } 124 | ) => void | VoidFunction, 125 | options?: WatchOptions 126 | ): void; 127 | 128 | function watchPre( 129 | source: Getter, 130 | effect: (value: T, previousValue: T | undefined) => void | VoidFunction, 131 | options?: WatchOptions 132 | ): void; 133 | 134 | function watchPre( 135 | sources: Getter | Array>, 136 | effect: (values: T | Array, previousValues: T | undefined | Array) => void | VoidFunction, 137 | options?: WatchOptions 138 | ): void { 139 | runWatcher(sources, 'pre', effect, options); 140 | } 141 | 142 | watch.pre = watchPre; 143 | 144 | export function watchOnce>( 145 | sources: { 146 | [K in keyof T]: Getter; 147 | }, 148 | effect: (values: T, previousValues: T) => void | VoidFunction 149 | ): void; 150 | 151 | export function watchOnce(source: Getter, effect: (value: T, previousValue: T) => void | VoidFunction): void; 152 | 153 | export function watchOnce( 154 | source: Getter | Array>, 155 | effect: (value: T | Array, previousValue: T | Array) => void | VoidFunction 156 | ): void { 157 | runWatcherOnce(source, 'post', effect); 158 | } 159 | 160 | function watchOncePre>( 161 | sources: { 162 | [K in keyof T]: Getter; 163 | }, 164 | effect: (values: T, previousValues: T) => void | VoidFunction 165 | ): void; 166 | 167 | function watchOncePre(source: Getter, effect: (value: T, previousValue: T) => void | VoidFunction): void; 168 | 169 | function watchOncePre( 170 | source: Getter | Array>, 171 | effect: (value: T | Array, previousValue: T | Array) => void | VoidFunction 172 | ): void { 173 | runWatcherOnce(source, 'pre', effect); 174 | } 175 | 176 | watchOnce.pre = watchOncePre; 177 | -------------------------------------------------------------------------------- /docs/presets/custom-preset.ts: -------------------------------------------------------------------------------- 1 | // my-preset.ts 2 | import { definePreset } from 'unocss'; 3 | import { handler as h, variantGetParameter } from '@unocss/preset-mini/utils'; 4 | 5 | export default definePreset(() => ({ 6 | name: 'custom-preset', 7 | 8 | rules: [ 9 | ['abs', { position: 'absolute' }], 10 | ['flex|col', { display: 'flex', 'flex-direction': 'column' }] 11 | ], 12 | 13 | shortcuts: [ 14 | // [/^flex\|col$/, () => "flex flex-col", { layer: "default" }], 15 | [ 16 | // flex-s stands for flex-shortcut 17 | // to avoid mixups with default flex utilities like flex-wrap 18 | /^(inline-)?flex-s-(start|center|between|evenly|around|end)(-(start|center|baseline|end))?(\|(col))?$/, 19 | ([, i, justify, align, , col]) => 20 | `${i || ''}flex justify-${justify} items${align || '-center'} ${col ? 'flex-col' : ''}`, 21 | { layer: 'default' } 22 | ], 23 | // use when width and height values are the same 24 | [/^s-(.*)$/, ([, v]) => `h-${v} w-${v}`, { layer: 'utilities' }], 25 | // use when min width and height values are the same 26 | [/^min-s-(.*)$/, ([, v]) => `min-h-${v} min-w-${v}`, { layer: 'utilities' }], 27 | 28 | [ 29 | /^scrollbar-f-(thin)-(.*)$/, 30 | ([, size, colors]) => `[scrollbar-width:${size}] [scrollbar-color:${colors}]`, 31 | { layer: 'utilities' } 32 | ], 33 | [ 34 | /^teeny-scrollbar-(w|h)-(\d+)$/, 35 | ([, ax, dg]) => ` 36 | scrollbar:${ax}-${dg} 37 | scrollbar-track:(rd-xl bg-transparent) 38 | scrollbar-thumb:(rd-xl bg-grey-4) 39 | ` 40 | ] 41 | ], 42 | 43 | variants: [ 44 | { 45 | // adds support for "@min-[width]:class" and "@min-h-[width]:class" 46 | // or 47 | // "@min-width:class" and "@min-h-width:class" 48 | name: 'arbitrary-media-query', 49 | match(matcher, { theme }) { 50 | // prefix with @ to specify that it's a media query 51 | const minVariant = variantGetParameter('@min-', matcher, [':', '-']); 52 | const maxVariant = variantGetParameter('@max-', matcher, [':', '-']); 53 | const minHeightVariant = variantGetParameter('@min-h-', matcher, [':', '-']); 54 | const maxHeightVariant = variantGetParameter('@max-h-', matcher, [':', '-']); 55 | 56 | // the order that we check the variants is important 57 | // because we want to match the most specific one 58 | const matched = 59 | (minHeightVariant && { 60 | type: 'min-h', 61 | variant: minHeightVariant 62 | }) || 63 | (maxHeightVariant && { 64 | type: 'max-h', 65 | variant: maxHeightVariant 66 | }) || 67 | (minVariant && { 68 | type: 'min', 69 | variant: minVariant 70 | }) || 71 | (maxVariant && { 72 | type: 'max', 73 | variant: maxVariant 74 | }); 75 | 76 | if (matched?.variant) { 77 | const [match, rest] = matched.variant; 78 | // this is for extracting the value from the match and 79 | // makes sure it either has no brackets or has brackets 80 | const extractedValue = 81 | h.bracket(match) || (!match.startsWith('[') && !match.endsWith(']') && match) || ''; 82 | const endsWithUnit = /^\d+(em|px|rem)$/.test(extractedValue); 83 | const isOnlyNum = /^\d+$/.test(extractedValue); 84 | 85 | if (endsWithUnit || isOnlyNum || theme['breakpoints'][extractedValue]) { 86 | return { 87 | matcher: rest, 88 | layer: 'utilities', 89 | handle: (input, next) => 90 | next({ 91 | ...input, 92 | parent: `${input.parent ? `${input.parent} $$ ` : ''}@media (${ 93 | matched.type == 'min' 94 | ? 'min-width' 95 | : matched.type == 'max' 96 | ? 'max-width' 97 | : matched.type == 'min-h' 98 | ? 'min-height' 99 | : 'max-height' 100 | }:${ 101 | endsWithUnit 102 | ? extractedValue 103 | : isOnlyNum 104 | ? extractedValue + 'px' 105 | : theme['breakpoints'][extractedValue] 106 | })` 107 | }) 108 | }; 109 | } 110 | } 111 | } 112 | }, 113 | { 114 | name: 'firefox-only', 115 | match(matcher) { 116 | const ffVariant = variantGetParameter('@ff', matcher, [':']); 117 | if (ffVariant) { 118 | const [, rest] = ffVariant; 119 | return { 120 | matcher: rest, 121 | handle: (input, next) => 122 | next({ 123 | ...input, 124 | parent: `${input.parent ? `${input.parent} $$ ` : ''}@-moz-document url-prefix()` 125 | }) 126 | }; 127 | } 128 | } 129 | }, 130 | (matcher) => { 131 | const [m1, m2, m3] = ['scrollbar:', 'scrollbar-track:', 'scrollbar-thumb:']; 132 | let matchedStr = ''; 133 | 134 | if (matcher.startsWith(m1)) { 135 | matchedStr = m1; 136 | } else if (matcher.startsWith(m2)) { 137 | matchedStr = m2; 138 | } else if (matcher.startsWith(m3)) { 139 | matchedStr = m3; 140 | } else { 141 | return matcher; 142 | } 143 | 144 | return { 145 | matcher: matcher.slice(matchedStr.length), 146 | selector: (s) => 147 | `${s}::-webkit-scrollbar${matchedStr == m2 ? '-track' : matchedStr == m3 ? '-thumb' : ''}`, 148 | layer: 'default' 149 | }; 150 | } 151 | ] 152 | })); 153 | 154 | export function convertPalleteToHSL>>(obj: T) { 155 | const temp: Record> = {}; 156 | for (const colorKey in obj) { 157 | for (const colorShadeKey in obj[colorKey]) { 158 | if (!temp[colorKey]) { 159 | temp[colorKey] = { 160 | [colorShadeKey]: hexToHSL(obj[colorKey][colorShadeKey]) 161 | }; 162 | } else { 163 | temp[colorKey][colorShadeKey] = hexToHSL(obj[colorKey][colorShadeKey]); 164 | } 165 | } 166 | } 167 | return temp as T; 168 | } 169 | 170 | export function hexToHSL( 171 | hex: string, 172 | options?: { justNums: boolean; satAndLight?: { s?: number; l?: number } } 173 | ) { 174 | const { satAndLight, justNums } = options || { 175 | satAndLight: undefined, 176 | justNums: false 177 | }; 178 | 179 | // convert hex to rgb 180 | let r = 0, 181 | g = 0, 182 | b = 0; 183 | if (hex.length === 4) { 184 | r = +('0x' + hex[1] + hex[1]); 185 | g = +('0x' + hex[2] + hex[2]); 186 | b = +('0x' + hex[3] + hex[3]); 187 | } else if (hex.length === 7) { 188 | r = +('0x' + hex[1] + hex[2]); 189 | g = +('0x' + hex[3] + hex[4]); 190 | b = +('0x' + hex[5] + hex[6]); 191 | } 192 | 193 | // then to HSL 194 | r /= 255; 195 | g /= 255; 196 | b /= 255; 197 | const cmin = Math.min(r, g, b); 198 | const cmax = Math.max(r, g, b); 199 | const delta = cmax - cmin; 200 | let h = 0; 201 | let s = 0; 202 | let l = 0; 203 | 204 | if (delta === 0) h = 0; 205 | else if (cmax === r) h = ((g - b) / delta) % 6; 206 | else if (cmax === g) h = (b - r) / delta + 2; 207 | else h = (r - g) / delta + 4; 208 | h = Math.round(h * 60); 209 | if (h < 0) h += 360; 210 | l = (cmax + cmin) / 2; 211 | s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); 212 | s = +(s * 100).toFixed(1); 213 | l = +(l * 100).toFixed(1); 214 | 215 | if (justNums) return `${h}, ${satAndLight?.s || s}%, ${satAndLight?.l || l}%`; 216 | 217 | return `hsl(${h}, ${satAndLight?.s || s}%, ${satAndLight?.l || l}%)`; 218 | } 219 | 220 | export function hexToRgba(hex: string, alpha: number) { 221 | const [r, g, b] = hex.match(/\w\w/g)!.map((h) => parseInt(h, 16)); 222 | return `rgba(${r}, ${g}, ${b}, ${alpha})`; 223 | } 224 | --------------------------------------------------------------------------------