Skip to content

feat: add svelte portable #390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ packages/core/dist/
packages/react/dist/
packages/snap/dist/
packages/vue/dist/
packages/svelte/dist/

dist-new/
dist/
dist/

.svelte-kit
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Your support helps us smooth out the internet one library at a time—and lets u
- [lenis](https://github.com/darkroomengineering/lenis/blob/main/README.md)
- [lenis/react](https://github.com/darkroomengineering/lenis/blob/main/packages/react/README.md)
- [lenis/vue](https://github.com/darkroomengineering/lenis/tree/main/packages/vue/README.md)
- [lenis/svelte](https://github.com/darkroomengineering/lenis/blob/main/packages/svelte/README.md)
- [lenis/framer](https://lenis.framer.website/)
- [lenis/snap](https://github.com/darkroomengineering/lenis/tree/main/packages/snap/README.md)

Expand Down
18 changes: 15 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"type": "module",
"sideEffects": false,
"author": "darkroom.engineering",
"contributors": [
"Daniel Nahuel Acuña <[email protected]> (https://github.com/DaniAcu)"
],
"license": "MIT",
"repository": {
"type": "git",
Expand All @@ -28,7 +31,8 @@
"scripts": {
"build": "pnpm build:core && pnpm build:all",
"build:core": "tsup --config tsup.core.ts",
"build:all": "tsup",
"build:all": "tsup && pnpm run build:svelte",
"build:svelte": "pnpm --filter lenis-svelte run build",
"dev": "pnpm run -w --parallel /^dev:.*/",
"dev:build": "tsup --watch",
"dev:playground": "pnpm --filter playground dev",
Expand All @@ -47,11 +51,12 @@
],
"devDependencies": {
"terser": "^5.37.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
"tsup": "^8.4.0",
"typescript": "^5.4.5"
},
"peerDependencies": {
"react": ">=17.0.0",
"svelte": "^5.19.3",
"vue": ">=3.0.0",
"@nuxt/kit": ">=3.0.0"
},
Expand All @@ -62,6 +67,9 @@
"vue": {
"optional": true
},
"svelte": {
"optional": true
},
"@nuxt/kit": {
"optional": true
}
Expand All @@ -87,6 +95,10 @@
"types": "./dist/lenis-vue.d.ts",
"default": "./dist/lenis-vue.mjs"
},
"./svelte": {
"types": "./dist/svelte/index.d.ts",
"svelte": "./dist/svelte/index.js"
},
"./nuxt": {
"default": "./dist/lenis-vue-nuxt.mjs"
},
Expand Down
43 changes: 43 additions & 0 deletions packages/svelte/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Lenis Svelte

lenis/svelte provides a `<SvelteLenis>` component that creates a [Lenis](https://github.com/darkroomengineering/lenis) instance and provides it to its children via context. This allows you to use Lenis in your Svelte app without worrying about passing the instance down through props. It also provides a `useLenis` hook that allows you to access the Lenis instance from any component in your app.


## Installation

```bash
npm i lenis
```

## Usage

### Basic

```jsx
import { SvelteLenis, useLenis } from 'lenis/svelte'

function App() {
const lenis = useLenis(({ scroll }) => {
// called every scroll
})

return (
<SvelteLenis root>
{ /* content */ }
</SvelteLenis>
)
}
```

## Props
- `options`: [Lenis options](https://github.com/darkroomengineering/lenis#instance-settings).
- `root`: Lenis will be instanciate using `<html>` scroll. Default: `false`.

## Hooks
Once the Lenis context is set (components mounted inside `<SveltetLenis>`) you can use these handy hooks:

`useLenis` is a hook that returns the Lenis instance

The hook takes three argument:
- `callback`: The function to be called whenever a scroll event is emitted
- `priority`: Manage callback execution order
1 change: 1 addition & 0 deletions packages/svelte/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src';
24 changes: 24 additions & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "lenis-svelte",
"type": "module",
"scripts": {
"build": "svelte-package -i ./src -o ../../dist/svelte"
},
"exports": {
".": {
"svelte": "./dist/svelte/index.js",
"default": "./dist/svelte/index.js"
}
},
"dependencies": {
"lenis": "workspace:lenis*"
},
"peerDependencies": {
"svelte": "^5.19.3"
},
"devDependencies": {
"@sveltejs/package": "^2.3.10",
"svelte": "^5.19.3",
"typescript": "^5.4.5"
}
}
16 changes: 16 additions & 0 deletions packages/svelte/src/SvelteLenis.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import type { LenisProps } from './types'

import LenisByContext from './provider/LenisByContext.svelte'
import LenisByRoot from './provider/LenisByRoot.svelte'

type Props = LenisProps & { root: boolean; }

let props: Props = $props();
</script>

{#if props.root}
<LenisByRoot {...props} />
{:else}
<LenisByContext {...props} />
{/if}
28 changes: 28 additions & 0 deletions packages/svelte/src/callbacks/callbacks.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type Lenis from "lenis";

type Callback = {
action: (lenis: Lenis) => void;
priority: number
};

export class CallbackManager {

callbacks = $state<Callback[]>([])

constructor() {}

add(callback: Callback) {
this.callbacks.push(callback);
this.callbacks.sort((a, b) => a.priority - b.priority);
}

remove(callback: Callback) {
this.callbacks = this.callbacks.filter(cb => cb !== callback)
}

forEach(byItem: (callback: Callback) => void) {
this.callbacks.forEach(byItem);
}
}

export default CallbackManager;
6 changes: 6 additions & 0 deletions packages/svelte/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import SvelteLenis from './SvelteLenis.svelte';

export * from './types';
export * from './use-lenis.svelte';
export { SvelteLenis };

33 changes: 33 additions & 0 deletions packages/svelte/src/instances/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type Lenis from "lenis";
import { getContext, hasContext, setContext } from "svelte";
import CallbackManager from "../callbacks/callbacks.svelte";
import { root } from "./root.svelte";

const LENIS_CONTEXT = Symbol('__LENIS_CONTEXT__');
type LenisContext = {
instance: () => Lenis | undefined;
callbackManager: CallbackManager;
}

const Root = {
instance: () => root.value,
callbackManager: new CallbackManager()
}

export const LenisContext = {
get() {
if (hasContext(LENIS_CONTEXT)) {
return getContext<LenisContext>(LENIS_CONTEXT);
} else {
return Root
}
},
set(lenis: LenisContext['instance']) {
return setContext(LENIS_CONTEXT, {
instance: lenis,
callbackManager: new CallbackManager()
});
}
}

export default LenisContext;
3 changes: 3 additions & 0 deletions packages/svelte/src/instances/root.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type Lenis from "lenis";

export let root = $state<{ value: Lenis | undefined }>({ value: undefined });
32 changes: 32 additions & 0 deletions packages/svelte/src/provider/LenisByContext.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
import Lenis from 'lenis'
import { LenisContext } from '../instances/context'
import type { LenisProps } from '../types'

let props: LenisProps = $props();

let wrapper: HTMLDivElement;
let content: HTMLDivElement;

let lenis = $state<Lenis>();

$effect(() => {
const instance = new Lenis({
...props.options,
wrapper,
content
});

lenis = instance;

return () => instance.destroy();
})

LenisContext.set(() => lenis);
</script>

<div bind:this={wrapper} class={props.class} {...props}>
<div bind:this={content}>
{@render props.children()}
</div>
</div>
22 changes: 22 additions & 0 deletions packages/svelte/src/provider/LenisByRoot.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script lang="ts">
import Lenis from 'lenis'
import { LenisContext } from '../instances/context'
import { root } from '../instances/root.svelte'
import type { LenisProps } from '../types'

let props: LenisProps = $props();

$effect.root(() => {
const instance = new Lenis(props.options);

root.value = instance;

console.log(root.value)

return () => instance.destroy();
});

LenisContext.set(() => root.value);
</script>

{@render props.children()}
10 changes: 10 additions & 0 deletions packages/svelte/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { LenisOptions } from "lenis"
import type { Snippet } from "svelte"

export type LenisProps = {
root?: boolean
options?: LenisOptions
class?: string
children: Snippet
}

39 changes: 39 additions & 0 deletions packages/svelte/src/use-lenis.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type Lenis from "lenis";
import { untrack } from 'svelte';
import { LenisContext } from "./instances/context";

export const useLenis = (onScroll: (lenis: Lenis) => void, priority = 0) => {
const context = LenisContext.get();

$effect(() => {
const callback = { action: onScroll, priority };
untrack(() => {
context.callbackManager.add(callback);
})

return () => {
context.callbackManager.remove(callback);
}
})

$effect(() => {
const lenis = context.instance();

if (!lenis) return;

const onScroll = (lenis: Lenis) => {
context.callbackManager.forEach(
item => item.action(lenis)
);
}
lenis.on('scroll', onScroll)

return () => lenis.off('scroll', onScroll)
})

return {
get current() {
return context.instance()
}
};
}
3 changes: 3 additions & 0 deletions packages/svelte/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}
16 changes: 11 additions & 5 deletions playground/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { defineConfig } from 'astro/config'
import { defineConfig } from 'astro/config';

import react from '@astrojs/react';
import svelte from '@astrojs/svelte';
import vue from '@astrojs/vue';

import react from '@astrojs/react'
import vue from '@astrojs/vue'

export default defineConfig({
integrations: [react(), vue({ appEntrypoint: './vue/setup' })],
integrations: [
react(),
vue({ appEntrypoint: './vue/setup' }),
svelte()
],
devToolbar: {
enabled: false,
},
srcDir: './www',
})
})
2 changes: 2 additions & 0 deletions playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/react": "^4.1.6",
"@astrojs/svelte": "^7.0.5",
"@astrojs/vue": "^5.0.6",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
Expand All @@ -20,6 +21,7 @@
"lorem-ipsum": "^2.0.8",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"svelte": "^5.19.3",
"typescript": "^5.7.3",
"vue": "^3.5.13"
}
Expand Down
Loading