From 8f5860f3f2f991ef0f77b29ff989290fde82cfa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Thu, 25 Dec 2025 06:48:41 +0900 Subject: [PATCH 01/18] chore: auth --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e09d53..6c92c6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: permissions: contents: read pull-requests: write + issues: write concurrency: group: ci-${{ github.ref }} From 7e40481a25e225cf204391c4961a9a3e0f38de29 Mon Sep 17 00:00:00 2001 From: Dino0204 Date: Sun, 28 Dec 2025 23:21:55 +0900 Subject: [PATCH 02/18] chore: Get Started URL 404 Error --- docs/.vitepress/theme/Hero.vue | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/.vitepress/theme/Hero.vue b/docs/.vitepress/theme/Hero.vue index 618e1e4..93aba3e 100644 --- a/docs/.vitepress/theme/Hero.vue +++ b/docs/.vitepress/theme/Hero.vue @@ -80,11 +80,8 @@ onUnmounted(() => {

- + of items. - KB + + of + items. KB of code. Zero lag.

@@ -95,7 +92,7 @@ onUnmounted(() => {

- + Get Started Date: Sun, 28 Dec 2025 23:47:40 +0900 Subject: [PATCH 03/18] =?UTF-8?q?fix:=20=EC=83=81=EB=8C=80=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vitepress/theme/Hero.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/.vitepress/theme/Hero.vue b/docs/.vitepress/theme/Hero.vue index 93aba3e..8e68d93 100644 --- a/docs/.vitepress/theme/Hero.vue +++ b/docs/.vitepress/theme/Hero.vue @@ -1,5 +1,6 @@
- + Get Started Date: Fri, 26 Dec 2025 06:57:05 +0900 Subject: [PATCH 04/18] docs: desc --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a03e1f..c51af9d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "git+https://github.com/976520/scrolloop.git" }, - "description": "Just a virtual scrolling library for React", + "description": "The modern scrolling component for React and React Native", "type": "module", "workspaces": [ "packages/*" From 08c03a98a1e455e6c1d5b999e8f68cba82e2cdb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 07:09:19 +0900 Subject: [PATCH 05/18] chore: external bundle --- packages/react-native/tsup.config.ts | 5 ++--- packages/react/tsup.config.ts | 3 +-- packages/shared/tsup.config.ts | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/react-native/tsup.config.ts b/packages/react-native/tsup.config.ts index a6675ff..ec42cac 100644 --- a/packages/react-native/tsup.config.ts +++ b/packages/react-native/tsup.config.ts @@ -22,9 +22,8 @@ export default defineConfig({ comments: false, }, }, - target: "es2024", - external: ["react", "react-native"], - noExternal: ["@scrolloop/core", "@scrolloop/shared"], + target: "es2020", + external: ["react", "react-native", "@scrolloop/core", "@scrolloop/shared"], outExtension({ format }) { return { js: format === "esm" ? ".mjs" : ".cjs", diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts index 769696f..921cac1 100644 --- a/packages/react/tsup.config.ts +++ b/packages/react/tsup.config.ts @@ -23,8 +23,7 @@ export default defineConfig({ }, }, target: "es2020", - external: ["react", "react-dom"], - noExternal: ["@scrolloop/core", "@scrolloop/shared"], + external: ["react", "react-dom", "@scrolloop/core", "@scrolloop/shared"], outExtension({ format }) { return { js: format === "esm" ? ".mjs" : ".cjs", diff --git a/packages/shared/tsup.config.ts b/packages/shared/tsup.config.ts index 6877331..39b9882 100644 --- a/packages/shared/tsup.config.ts +++ b/packages/shared/tsup.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ comments: false, }, }, - target: "es2024", + target: "es2020", external: ["react"], outExtension({ format }) { return { From 4d2ed64fae8a15427a9ee6e0fd11db3fa30f354a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 07:16:17 +0900 Subject: [PATCH 06/18] =?UTF-8?q?fix:=20ecmascript=20=EC=83=81=20private?= =?UTF-8?q?=20field=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../strategies/layout/FixedLayoutStrategy.ts | 23 +++-- .../strategies/scroll/VirtualScrollSource.ts | 30 +++--- packages/core/src/virtualizer/Virtualizer.ts | 94 ++++++++++--------- packages/core/tsup.config.ts | 2 +- 4 files changed, 77 insertions(+), 72 deletions(-) diff --git a/packages/core/src/strategies/layout/FixedLayoutStrategy.ts b/packages/core/src/strategies/layout/FixedLayoutStrategy.ts index 2d4309b..ec2ef68 100644 --- a/packages/core/src/strategies/layout/FixedLayoutStrategy.ts +++ b/packages/core/src/strategies/layout/FixedLayoutStrategy.ts @@ -1,20 +1,24 @@ -import type { Range } from '../../types'; -import type { LayoutStrategy } from './LayoutStrategy'; -import { clamp } from '../../utils/clamp'; +import type { Range } from "../../types"; +import type { LayoutStrategy } from "./LayoutStrategy"; +import { clamp } from "../../utils/clamp"; export class FixedLayoutStrategy implements LayoutStrategy { - constructor(private itemSize: number) {} + #itemSize: number; + + constructor(itemSize: number) { + this.#itemSize = itemSize; + } getItemOffset(index: number): number { - return index * this.itemSize; + return index * this.#itemSize; } getItemSize(_index: number): number { - return this.itemSize; + return this.#itemSize; } getTotalSize(count: number): number { - return count * this.itemSize; + return count * this.#itemSize; } getVisibleRange( @@ -24,14 +28,13 @@ export class FixedLayoutStrategy implements LayoutStrategy { ): Range { const startIndex = clamp( 0, - Math.floor(scrollOffset / this.itemSize), + Math.floor(scrollOffset / this.#itemSize), count - 1 ); - const visibleCount = Math.ceil(viewportSize / this.itemSize); + const visibleCount = Math.ceil(viewportSize / this.#itemSize); const endIndex = Math.min(count - 1, startIndex + visibleCount); return { startIndex, endIndex }; } } - diff --git a/packages/core/src/strategies/scroll/VirtualScrollSource.ts b/packages/core/src/strategies/scroll/VirtualScrollSource.ts index 08a9ac6..88a7add 100644 --- a/packages/core/src/strategies/scroll/VirtualScrollSource.ts +++ b/packages/core/src/strategies/scroll/VirtualScrollSource.ts @@ -1,40 +1,40 @@ import type { ScrollSource } from "./ScrollSource"; export class VirtualScrollSource implements ScrollSource { - private scrollOffset = 0; - private viewportSize = 0; - private listeners = new Set<(offset: number) => void>(); + #scrollOffset = 0; + #viewportSize = 0; + #listeners = new Set<(offset: number) => void>(); getScrollOffset(): number { - return this.scrollOffset; + return this.#scrollOffset; } getViewportSize(): number { - return this.viewportSize; + return this.#viewportSize; } setScrollOffset(offset: number): void { - if (this.scrollOffset !== offset) { - this.scrollOffset = offset; - this.notifyListeners(); + if (this.#scrollOffset !== offset) { + this.#scrollOffset = offset; + this.#notifyListeners(); } } setViewportSize(size: number): void { - if (this.viewportSize !== size) { - this.viewportSize = size; - this.notifyListeners(); + if (this.#viewportSize !== size) { + this.#viewportSize = size; + this.#notifyListeners(); } } subscribe(callback: (offset: number) => void): () => void { - this.listeners.add(callback); + this.#listeners.add(callback); return () => { - this.listeners.delete(callback); + this.#listeners.delete(callback); }; } - private notifyListeners(): void { - this.listeners.forEach((listener) => listener(this.scrollOffset)); + #notifyListeners(): void { + this.#listeners.forEach((listener) => listener(this.#scrollOffset)); } } diff --git a/packages/core/src/virtualizer/Virtualizer.ts b/packages/core/src/virtualizer/Virtualizer.ts index d127bde..795942d 100644 --- a/packages/core/src/virtualizer/Virtualizer.ts +++ b/packages/core/src/virtualizer/Virtualizer.ts @@ -2,102 +2,105 @@ import type { VirtualItem, VirtualizerState, VirtualizerOptions, -} from '../types'; -import type { LayoutStrategy } from '../strategies/layout/LayoutStrategy'; -import type { ScrollSource } from '../strategies/scroll/ScrollSource'; -import type { Plugin } from '../plugins/Plugin'; +} from "../types"; +import type { LayoutStrategy } from "../strategies/layout/LayoutStrategy"; +import type { ScrollSource } from "../strategies/scroll/ScrollSource"; +import type { Plugin } from "../plugins/Plugin"; export class Virtualizer { - private count: number; - private overscan: number; - private plugins: Plugin[]; - private onChange?: (state: VirtualizerState) => void; + #count: number; + #overscan: number; + #plugins: Plugin[]; + #onChange?: (state: VirtualizerState) => void; - private layoutStrategy: LayoutStrategy; - private scrollSource: ScrollSource; + #layoutStrategy: LayoutStrategy; + #scrollSource: ScrollSource; - private state: VirtualizerState; - private unsubscribe?: () => void; + #state: VirtualizerState; + #unsubscribe?: () => void; constructor( layoutStrategy: LayoutStrategy, scrollSource: ScrollSource, options: VirtualizerOptions ) { - this.layoutStrategy = layoutStrategy; - this.scrollSource = scrollSource; - this.count = options.count; - this.overscan = options.overscan ?? 4; - this.onChange = options.onChange; - this.plugins = []; + this.#layoutStrategy = layoutStrategy; + this.#scrollSource = scrollSource; + this.#count = options.count; + this.#overscan = options.overscan ?? 4; + this.#onChange = options.onChange; + this.#plugins = []; - this.state = this.calculateState(); + this.#state = this.#calculateState(); - this.plugins.forEach((plugin) => plugin.onInit?.()); + this.#plugins.forEach((plugin) => plugin.onInit?.()); - this.unsubscribe = this.scrollSource.subscribe(() => { + this.#unsubscribe = this.#scrollSource.subscribe(() => { this.update(); }); } addPlugin(plugin: Plugin): void { - this.plugins.push(plugin); + this.#plugins.push(plugin); plugin.onInit?.(); } getState(): VirtualizerState { - return this.state; + return this.#state; } setCount(count: number): void { - if (this.count !== count) { - this.count = count; + if (this.#count !== count) { + this.#count = count; this.update(); } } update(): void { - const newState = this.calculateState(); + const newState = this.#calculateState(); let finalState = newState; - for (const plugin of this.plugins) { + for (const plugin of this.#plugins) { const result = plugin.beforeStateChange?.(finalState); if (result) finalState = result; } - this.state = finalState; + this.#state = finalState; - this.plugins.forEach((plugin) => plugin.afterStateChange?.(this.state)); + this.#plugins.forEach((plugin) => plugin.afterStateChange?.(this.#state)); - this.onChange?.(this.state); + this.#onChange?.(this.#state); } - private calculateState(): VirtualizerState { - const scrollOffset = this.scrollSource.getScrollOffset(); - const viewportSize = this.scrollSource.getViewportSize(); - const totalSize = this.layoutStrategy.getTotalSize(this.count); + #calculateState(): VirtualizerState { + const scrollOffset = this.#scrollSource.getScrollOffset(); + const viewportSize = this.#scrollSource.getViewportSize(); + const totalSize = this.#layoutStrategy.getTotalSize(this.#count); - let visibleRange = this.layoutStrategy.getVisibleRange( + let visibleRange = this.#layoutStrategy.getVisibleRange( scrollOffset, viewportSize, - this.count + this.#count ); let renderRange = { - startIndex: Math.max(0, visibleRange.startIndex - this.overscan), - endIndex: Math.min(this.count - 1, visibleRange.endIndex + this.overscan), + startIndex: Math.max(0, visibleRange.startIndex - this.#overscan), + endIndex: Math.min( + this.#count - 1, + visibleRange.endIndex + this.#overscan + ), }; - - for (const plugin of this.plugins) { + + for (const plugin of this.#plugins) { if (plugin.onRangeCalculated) { - renderRange = plugin.onRangeCalculated(visibleRange, this.count); + renderRange = plugin.onRangeCalculated(visibleRange, this.#count); } } const virtualItems: VirtualItem[] = []; for (let i = renderRange.startIndex; i <= renderRange.endIndex; i++) { - const start = this.layoutStrategy.getItemOffset(i); - const size = this.layoutStrategy.getItemSize(i); + const start = this.#layoutStrategy.getItemOffset(i); + const size = this.#layoutStrategy.getItemSize(i); virtualItems.push({ index: i, start, @@ -117,8 +120,7 @@ export class Virtualizer { } destroy(): void { - this.unsubscribe?.(); - this.plugins.forEach((plugin) => plugin.onDestroy?.()); + this.#unsubscribe?.(); + this.#plugins.forEach((plugin) => plugin.onDestroy?.()); } } - diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index 3a82893..eb3a43a 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ comments: false, }, }, - target: "es2020", + target: "es2022", outExtension({ format }) { return { js: format === "esm" ? ".mjs" : ".cjs", From bf1c692d401004a601b49a3922bbeebe97a21f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 07:47:19 +0900 Subject: [PATCH 07/18] Update packages/core/src/strategies/layout/FixedLayoutStrategy.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/core/src/strategies/layout/FixedLayoutStrategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/strategies/layout/FixedLayoutStrategy.ts b/packages/core/src/strategies/layout/FixedLayoutStrategy.ts index ec2ef68..94f05c4 100644 --- a/packages/core/src/strategies/layout/FixedLayoutStrategy.ts +++ b/packages/core/src/strategies/layout/FixedLayoutStrategy.ts @@ -3,7 +3,7 @@ import type { LayoutStrategy } from "./LayoutStrategy"; import { clamp } from "../../utils/clamp"; export class FixedLayoutStrategy implements LayoutStrategy { - #itemSize: number; + readonly #itemSize: number; constructor(itemSize: number) { this.#itemSize = itemSize; From 7a5ee32f6b0b7c92d4772997ee277c681a6b51a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Sun, 28 Dec 2025 07:52:03 +0900 Subject: [PATCH 08/18] fix: readonly --- packages/core/src/virtualizer/Virtualizer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/virtualizer/Virtualizer.ts b/packages/core/src/virtualizer/Virtualizer.ts index 795942d..756cbe0 100644 --- a/packages/core/src/virtualizer/Virtualizer.ts +++ b/packages/core/src/virtualizer/Virtualizer.ts @@ -9,15 +9,15 @@ import type { Plugin } from "../plugins/Plugin"; export class Virtualizer { #count: number; - #overscan: number; + readonly #overscan: number; #plugins: Plugin[]; - #onChange?: (state: VirtualizerState) => void; + readonly #onChange?: (state: VirtualizerState) => void; - #layoutStrategy: LayoutStrategy; - #scrollSource: ScrollSource; + readonly #layoutStrategy: LayoutStrategy; + readonly #scrollSource: ScrollSource; #state: VirtualizerState; - #unsubscribe?: () => void; + readonly #unsubscribe?: () => void; constructor( layoutStrategy: LayoutStrategy, From 797b120b61837eb7fca7e9d7cdd07e9bc22e462c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 07:56:43 +0900 Subject: [PATCH 09/18] =?UTF-8?q?perf:=20=EB=A9=94=EB=AA=A8=EC=9D=B4?= =?UTF-8?q?=EC=A0=9C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/virtualizer/Virtualizer.ts | 54 ++++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/core/src/virtualizer/Virtualizer.ts b/packages/core/src/virtualizer/Virtualizer.ts index 756cbe0..22ec056 100644 --- a/packages/core/src/virtualizer/Virtualizer.ts +++ b/packages/core/src/virtualizer/Virtualizer.ts @@ -19,6 +19,9 @@ export class Virtualizer { #state: VirtualizerState; readonly #unsubscribe?: () => void; + #prevRenderRange?: { startIndex: number; endIndex: number }; + #prevVirtualItems?: VirtualItem[]; + constructor( layoutStrategy: LayoutStrategy, scrollSource: ScrollSource, @@ -43,6 +46,8 @@ export class Virtualizer { addPlugin(plugin: Plugin): void { this.#plugins.push(plugin); plugin.onInit?.(); + this.#prevRenderRange = undefined; + this.#prevVirtualItems = undefined; } getState(): VirtualizerState { @@ -52,6 +57,8 @@ export class Virtualizer { setCount(count: number): void { if (this.#count !== count) { this.#count = count; + this.#prevRenderRange = undefined; + this.#prevVirtualItems = undefined; this.update(); } } @@ -84,29 +91,46 @@ export class Virtualizer { ); let renderRange = { - startIndex: Math.max(0, visibleRange.startIndex - this.#overscan), - endIndex: Math.min( - this.#count - 1, - visibleRange.endIndex + this.#overscan - ), + startIndex: (visibleRange.startIndex - this.#overscan) | 0, + endIndex: (visibleRange.endIndex + this.#overscan) | 0, }; + if (renderRange.startIndex < 0) renderRange.startIndex = 0; + if (renderRange.endIndex > this.#count - 1) + renderRange.endIndex = this.#count - 1; + for (const plugin of this.#plugins) { if (plugin.onRangeCalculated) { renderRange = plugin.onRangeCalculated(visibleRange, this.#count); } } - const virtualItems: VirtualItem[] = []; - for (let i = renderRange.startIndex; i <= renderRange.endIndex; i++) { - const start = this.#layoutStrategy.getItemOffset(i); - const size = this.#layoutStrategy.getItemSize(i); - virtualItems.push({ - index: i, - start, - size, - end: start + size, - }); + let virtualItems: VirtualItem[]; + if ( + this.#prevRenderRange && + this.#prevVirtualItems && + this.#prevRenderRange.startIndex === renderRange.startIndex && + this.#prevRenderRange.endIndex === renderRange.endIndex + ) { + virtualItems = this.#prevVirtualItems; + } else { + virtualItems = []; + const startIdx = renderRange.startIndex; + const endIdx = renderRange.endIndex; + + for (let i = startIdx; i <= endIdx; i++) { + const start = this.#layoutStrategy.getItemOffset(i); + const size = this.#layoutStrategy.getItemSize(i); + virtualItems.push({ + index: i, + start, + size, + end: start + size, + }); + } + + this.#prevRenderRange = renderRange; + this.#prevVirtualItems = virtualItems; } return { From e8fbea0617073e62732782058dd8544399561e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 07:57:15 +0900 Subject: [PATCH 10/18] fix: | 0 --- packages/core/src/strategies/layout/FixedLayoutStrategy.ts | 6 +----- packages/core/src/utils/calculateVirtualRange.ts | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/core/src/strategies/layout/FixedLayoutStrategy.ts b/packages/core/src/strategies/layout/FixedLayoutStrategy.ts index 94f05c4..46a0f66 100644 --- a/packages/core/src/strategies/layout/FixedLayoutStrategy.ts +++ b/packages/core/src/strategies/layout/FixedLayoutStrategy.ts @@ -26,11 +26,7 @@ export class FixedLayoutStrategy implements LayoutStrategy { viewportSize: number, count: number ): Range { - const startIndex = clamp( - 0, - Math.floor(scrollOffset / this.#itemSize), - count - 1 - ); + const startIndex = clamp(0, (scrollOffset / this.#itemSize) | 0, count - 1); const visibleCount = Math.ceil(viewportSize / this.#itemSize); const endIndex = Math.min(count - 1, startIndex + visibleCount); diff --git a/packages/core/src/utils/calculateVirtualRange.ts b/packages/core/src/utils/calculateVirtualRange.ts index 1537ef1..39b1479 100644 --- a/packages/core/src/utils/calculateVirtualRange.ts +++ b/packages/core/src/utils/calculateVirtualRange.ts @@ -8,7 +8,7 @@ export function calculateVirtualRange( overscan: number, prevScrollOffset?: number ): VirtualRange { - const startIndex = Math.max(0, Math.floor(scrollOffset / itemSize)); + const startIndex = Math.max(0, (scrollOffset / itemSize) | 0); const endIndex = Math.min( totalCount - 1, startIndex + Math.ceil(viewportSize / itemSize) @@ -22,7 +22,7 @@ export function calculateVirtualRange( const overscanStart = isScrollingUp ? overscan * 1.5 : overscan; const overscanEnd = isScrollingDown ? overscan * 1.5 : overscan; - const renderStart = Math.max(0, Math.floor(startIndex - overscanStart)); + const renderStart = Math.max(0, (startIndex - overscanStart) | 0); const renderEnd = Math.min(totalCount - 1, Math.ceil(endIndex + overscanEnd)); return { From 84c77df639cf8140e140cdab88244e2366aea8bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 08:02:41 +0900 Subject: [PATCH 11/18] =?UTF-8?q?perf:=20Terser=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/tsup.config.ts | 7 ++++++- packages/react-native/tsup.config.ts | 7 ++++++- packages/react/tsup.config.ts | 7 ++++++- packages/shared/tsup.config.ts | 7 ++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index eb3a43a..af1b21d 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -10,10 +10,15 @@ export default defineConfig({ minify: "terser", terserOptions: { compress: { - passes: 2, + passes: 3, drop_console: true, drop_debugger: true, pure_funcs: ["console.log", "console.debug"], + unsafe: true, + unsafe_arrows: true, + unsafe_methods: true, + booleans_as_integers: true, + ecma: 2020, }, mangle: { safari10: false, diff --git a/packages/react-native/tsup.config.ts b/packages/react-native/tsup.config.ts index ec42cac..065836d 100644 --- a/packages/react-native/tsup.config.ts +++ b/packages/react-native/tsup.config.ts @@ -10,10 +10,15 @@ export default defineConfig({ minify: "terser", terserOptions: { compress: { - passes: 2, + passes: 3, drop_console: true, drop_debugger: true, pure_funcs: ["console.log", "console.debug"], + unsafe: true, + unsafe_arrows: true, + unsafe_methods: true, + booleans_as_integers: true, + ecma: 2020, }, mangle: { safari10: false, diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts index 921cac1..78a86ef 100644 --- a/packages/react/tsup.config.ts +++ b/packages/react/tsup.config.ts @@ -10,10 +10,15 @@ export default defineConfig({ minify: "terser", terserOptions: { compress: { - passes: 2, + passes: 3, drop_console: true, drop_debugger: true, pure_funcs: ["console.log", "console.debug"], + unsafe: true, + unsafe_arrows: true, + unsafe_methods: true, + booleans_as_integers: true, + ecma: 2020, }, mangle: { safari10: false, diff --git a/packages/shared/tsup.config.ts b/packages/shared/tsup.config.ts index 39b9882..f2fe949 100644 --- a/packages/shared/tsup.config.ts +++ b/packages/shared/tsup.config.ts @@ -10,10 +10,15 @@ export default defineConfig({ minify: "terser", terserOptions: { compress: { - passes: 2, + passes: 3, drop_console: true, drop_debugger: true, pure_funcs: ["console.log", "console.debug"], + unsafe: true, + unsafe_arrows: true, + unsafe_methods: true, + booleans_as_integers: true, + ecma: 2020, }, mangle: { safari10: false, From cd54372d422ce9dfa23a6bd0153f11363570dc39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 08:11:14 +0900 Subject: [PATCH 12/18] =?UTF-8?q?refector:=20react=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B0=84=EA=B2=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../react/src/components/InfiniteList.tsx | 260 +++++------------- packages/react/src/hooks/useTransition.ts | 138 ++++------ packages/react/src/utils/domPruner.ts | 125 +++------ 3 files changed, 169 insertions(+), 354 deletions(-) diff --git a/packages/react/src/components/InfiniteList.tsx b/packages/react/src/components/InfiniteList.tsx index 59a1852..c90d494 100644 --- a/packages/react/src/components/InfiniteList.tsx +++ b/packages/react/src/components/InfiniteList.tsx @@ -37,99 +37,63 @@ function InfiniteListInner(props: InfiniteListProps) { ); const containerRef = useRef(null); + const scrollTopRef = useRef(0); - const initialPagesRef = useRef>(new Map()); - const initialTotalRef = useRef(0); - const initialHasMoreRef = useRef(true); + const { allItems, pages, loadingPages, hasMore, error, loadPage, retry } = + useInfinitePages({ fetchPage, pageSize, initialPage, onPageLoad, onError }); - if (isServerSide && initialData && initialData.length > 0) { + const ssrData = useMemo(() => { + if (!isServerSide || !initialData?.length) return null; const initialPages = new Map(); const totalPages = Math.ceil(initialData.length / pageSize); - for (let i = 0; i < totalPages; i++) { - const start = i * pageSize; - const end = start + pageSize; - initialPages.set(i, initialData.slice(start, end)); + initialPages.set(i, initialData.slice(i * pageSize, (i + 1) * pageSize)); } - - initialPagesRef.current = initialPages; - initialTotalRef.current = initialTotal ?? initialData.length; - initialHasMoreRef.current = initialTotal - ? initialData.length < initialTotal - : true; - } - - const { allItems, pages, loadingPages, hasMore, error, loadPage, retry } = - useInfinitePages({ - fetchPage, - pageSize, - initialPage, - onPageLoad, - onError, - }); + const total = initialTotal ?? initialData.length; + return { + pages: initialPages, + total, + hasMore: initialTotal ? initialData.length < initialTotal : true, + }; + }, [isServerSide, initialData, initialTotal, pageSize]); const mergedPages = useMemo(() => { - if (isServerSide && initialPagesRef.current.size > 0) { + if (ssrData) { const merged = new Map(pages); - initialPagesRef.current.forEach((items, pageNum) => { - if (!merged.has(pageNum)) { - merged.set(pageNum, items); - } - }); + ssrData.pages.forEach((v, k) => !merged.has(k) && merged.set(k, v)); return merged; } return pages; - }, [pages, isServerSide]); + }, [pages, ssrData]); - const mergedTotal = useMemo(() => { - if (isServerSide && initialTotalRef.current > 0) { - return Math.max(initialTotalRef.current, allItems.length); - } - return allItems.length; - }, [isServerSide, allItems.length]); - - const mergedHasMore = useMemo(() => { - if (isServerSide && initialPagesRef.current.size > 0) { - return initialHasMoreRef.current || hasMore; - } - return hasMore; - }, [isServerSide, hasMore]); + const mergedTotal = ssrData + ? Math.max(ssrData.total, allItems.length) + : allItems.length; + const mergedHasMore = ssrData ? ssrData.hasMore || hasMore : hasMore; const mergedAllItems = useMemo(() => { - if (isServerSide && initialData && initialData.length > 0) { - const items: (T | undefined)[] = new Array(mergedTotal); - - initialData.forEach((item, index) => { - items[index] = item; - }); - - mergedPages.forEach((pageItems, pageNum) => { - const startIndex = pageNum * pageSize; - pageItems.forEach((item, i) => { - items[startIndex + i] = item; - }); + if (ssrData && initialData) { + const items = new Array(mergedTotal); + initialData.forEach((v, i) => (items[i] = v)); + mergedPages.forEach((v, k) => { + const start = k * pageSize; + v.forEach((it, i) => (items[start + i] = it)); }); - return items; } return allItems; - }, [isServerSide, initialData, mergedTotal, mergedPages, pageSize, allItems]); + }, [ssrData, initialData, mergedTotal, mergedPages, pageSize, allItems]); useEffect(() => { - if (!isServerSide && mergedPages.size === 0 && !error) { - const totalNeededItems = Math.ceil(height / itemSize) + overscan * 2; - for ( - let page = 0; - page < Math.ceil(totalNeededItems / pageSize) + prefetchThreshold; - page++ - ) - loadPage(page); + if (!isServerSide && !mergedPages.size && !error) { + const needed = Math.ceil(height / itemSize) + overscan * 2; + for (let p = 0; p < Math.ceil(needed / pageSize) + prefetchThreshold; p++) + loadPage(p); } }, [ isServerSide, mergedPages.size, loadPage, - initialPage, error, height, itemSize, @@ -138,23 +102,21 @@ function InfiniteListInner(props: InfiniteListProps) { overscan, ]); - const scrollTopRef = useRef(0); const visibleRange = useMemo(() => { - const scrollTop = scrollTopRef.current; + const st = scrollTopRef.current; const { renderStart, renderEnd } = calculateVirtualRange( - scrollTop, + st, height, itemSize, mergedAllItems.length, overscan, - scrollTop + st ); return { start: renderStart, end: renderEnd }; }, [height, itemSize, mergedAllItems.length, overscan]); - const shouldUseTransition = isServerSide; const { isVirtualized } = useTransition({ - enabled: shouldUseTransition, + enabled: isServerSide, containerRef, itemSize, totalItems: mergedAllItems.length, @@ -168,22 +130,16 @@ function InfiniteListInner(props: InfiniteListProps) { scrollTopRef.current = containerRef.current?.scrollTop ?? 0; return; } - - const prefetchStart = Math.max( + const ps = Math.max( 0, - Math.floor(range.startIndex / pageSize) - - Math.floor(range.endIndex / pageSize) + ((range.startIndex / pageSize) | 0) - ((range.endIndex / pageSize) | 0) ); - const prefetchEnd = - Math.floor(range.endIndex / pageSize) + + const pe = + ((range.endIndex / pageSize) | 0) + prefetchThreshold + Math.ceil(overscan / pageSize); - - findMissingPages(prefetchStart, prefetchEnd, mergedPages, loadingPages); - - for (let page = prefetchStart; page <= prefetchEnd; page++) { - loadPage(page); - } + findMissingPages(ps, pe, mergedPages, loadingPages); + for (let p = ps; p <= pe; p++) loadPage(p); }, [ isServerSide, @@ -199,96 +155,33 @@ function InfiniteListInner(props: InfiniteListProps) { useEffect(() => { if (!isServerSide || !containerRef.current) return; - - const container = containerRef.current; - const handleScroll = () => { - scrollTopRef.current = container.scrollTop; + const scroll = () => { + scrollTopRef.current = containerRef.current?.scrollTop ?? 0; }; - - container.addEventListener("scroll", handleScroll, { passive: true }); - return () => container.removeEventListener("scroll", handleScroll); + containerRef.current.addEventListener("scroll", scroll, { passive: true }); + return () => containerRef.current?.removeEventListener("scroll", scroll); }, [isServerSide]); - const virtualListRenderItem = useCallback( - (index: number, itemStyle: CSSProperties) => { - const item = mergedAllItems[index]; - return renderItem(item, index, itemStyle); - }, - [mergedAllItems, renderItem] - ); - - const FullRenderItem = useCallback( - (item: T | undefined, index: number, style: CSSProperties) => { - return renderItem(item, index, style); - }, - [renderItem] - ); - - const errorContainerStyle = useMemo( - () => ({ - height, - display: "flex", - alignItems: "center", - justifyContent: "center", - }), - [height] - ); - - const errorContentStyle = useMemo( - () => ({ - textAlign: "center", - }), - [] - ); - - const errorMessageStyle = useMemo( - () => ({ - color: "#666", - fontSize: "0.9em", - }), - [] - ); - - const retryButtonStyle = useMemo( - () => ({ - marginTop: 8, - padding: "4px 12px", - cursor: "pointer", - }), - [] - ); - - const loadingContainerStyle = useMemo( - () => ({ - height, - display: "flex", - alignItems: "center", - justifyContent: "center", - }), - [height] - ); - - const emptyContainerStyle = useMemo( - () => ({ - height, - display: "flex", - alignItems: "center", - justifyContent: "center", - }), - [height] - ); - - const heightOnlyStyle = useMemo(() => ({ height }), [height]); + const commonContainerStyle: CSSProperties = { + height, + display: "flex", + alignItems: "center", + justifyContent: "center", + }; + const heightOnlyStyle: CSSProperties = { height }; - if (error && mergedAllItems.length === 0) { + if (error && !mergedAllItems.length) { if (renderError) return
{renderError(error, retry)}
; return ( -
-
+
+

Error.

-

{error.message}

-
@@ -296,37 +189,32 @@ function InfiniteListInner(props: InfiniteListProps) { ); } - if (mergedAllItems.length === 0 && loadingPages.size > 0) { - if (renderLoading) { - return
{renderLoading()}
; - } - return ( -
+ if (!mergedAllItems.length && loadingPages.size) { + return renderLoading ? ( +
{renderLoading()}
+ ) : ( +

Loading...

); } - if (mergedAllItems.length === 0 && !mergedHasMore) { - if (renderEmpty) { - return
{renderEmpty()}
; - } - return ( -
+ if (!mergedAllItems.length && !mergedHasMore) { + return renderEmpty ? ( +
{renderEmpty()}
+ ) : ( +

No data.

); } - const shouldRenderFullList = - isServerSideEnvironment() || (isServerSide && !isVirtualized); - - if (shouldRenderFullList) { + if (isServerSideEnvironment() || (isServerSide && !isVirtualized)) { return ( (props: InfiniteListProps) { className={className} style={style} onRangeChange={handleRangeChange} - renderItem={virtualListRenderItem} + renderItem={(index, itemStyle) => + renderItem(mergedAllItems[index], index, itemStyle) + } /> ); } diff --git a/packages/react/src/hooks/useTransition.ts b/packages/react/src/hooks/useTransition.ts index 6a4f334..010fed0 100644 --- a/packages/react/src/hooks/useTransition.ts +++ b/packages/react/src/hooks/useTransition.ts @@ -31,108 +31,78 @@ export function useTransition({ onTransitionError, }: useTransitionOptions) { const [state, setState] = useState({ type: "SSR_DOM" }); - const isHydratedRef = useRef(false); - const hasInteractedRef = useRef(false); - const pruneCancelRef = useRef<(() => void) | null>(null); - const transitionStrategy = { ...defaultTransitionStrategy, ...strategy }; + const isH = useRef(false); + const hasI = useRef(false); + const pC = useRef<(() => void) | null>(null); + const s = { ...defaultTransitionStrategy, ...strategy }; useEffect(() => { - if (!enabled || isHydratedRef.current) return; - - const checkHydration = () => { - if (containerRef.current && !isHydratedRef.current) { - isHydratedRef.current = true; + if (!enabled || isH.current) return; + const h = () => { + if (containerRef.current && !isH.current) { + isH.current = true; setState({ type: "HYDRATED" }); } }; - - checkHydration(); - - const timeoutId = setTimeout(checkHydration, 0); - - return () => clearTimeout(timeoutId); + h(); + const t = setTimeout(h, 0); + return () => clearTimeout(t); }, [enabled, containerRef]); useEffect(() => { if (!enabled || state.type !== "HYDRATED") return; + const c = containerRef.current; + if (!c) return; - const container = containerRef.current; - if (!container) return; - - const triggerTransition = () => { + const run = () => { try { onTransitionStart?.(); - - const snapshot = captureSnapshot(container, itemSize, totalItems); - - setState({ type: "SWITCHING", snapshot }); - - restoreSnapshot(container, snapshot); - - const pruneStrategy = transitionStrategy.pruneStrategy || "idle"; - const cancelPrune = - pruneStrategy === "chunk" + const sn = captureSnapshot(c, itemSize, totalItems); + setState({ type: "SWITCHING", snapshot: sn }); + restoreSnapshot(c, sn); + pC.current = + s.pruneStrategy === "chunk" ? pruneOffscreenDOMChunk( - container, + c, visibleRange, - transitionStrategy.chunkSize || 10, + s.chunkSize || 10, () => {} ) - : pruneOffscreenDOMIdle(container, visibleRange, () => {}); - - pruneCancelRef.current = cancelPrune; - + : pruneOffscreenDOMIdle(c, visibleRange, () => {}); setTimeout(() => { setState({ type: "VIRTUALIZED" }); onTransitionComplete?.(); }, 100); - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - onTransitionError?.(err); - - if (transitionStrategy.transitionStrategy === "replace-offscreen") { + } catch (e) { + onTransitionError?.(e instanceof Error ? e : new Error(String(e))); + if (s.transitionStrategy === "replace-offscreen") setState({ type: "VIRTUALIZED" }); - } } }; - if (transitionStrategy.switchTrigger === "immediate") { - triggerTransition(); - } else if (transitionStrategy.switchTrigger === "first-interaction") { - const handleInteraction = () => { - if (!hasInteractedRef.current) { - hasInteractedRef.current = true; - triggerTransition(); - container.removeEventListener("click", handleInteraction); - container.removeEventListener("keydown", handleInteraction); - container.removeEventListener("scroll", handleInteraction); - } - }; - - container.addEventListener("click", handleInteraction, { once: true }); - container.addEventListener("keydown", handleInteraction, { once: true }); - container.addEventListener("scroll", handleInteraction, { once: true }); - - return () => { - container.removeEventListener("click", handleInteraction); - container.removeEventListener("keydown", handleInteraction); - container.removeEventListener("scroll", handleInteraction); - }; - } else if (transitionStrategy.switchTrigger === "idle") { - const handleIdle = () => { - if ("requestIdleCallback" in window) { - window.requestIdleCallback(() => { - triggerTransition(); - }); - } else { - setTimeout(triggerTransition, 1000); + if (s.switchTrigger === "immediate") run(); + else if (s.switchTrigger === "first-interaction") { + const i = () => { + if (!hasI.current) { + hasI.current = true; + run(); + ["click", "keydown", "scroll"].forEach((ev) => + c.removeEventListener(ev, i) + ); } }; - - handleIdle(); - return; + ["click", "keydown", "scroll"].forEach((ev) => + c.addEventListener(ev, i, { once: true }) + ); + return () => + ["click", "keydown", "scroll"].forEach((ev) => + c.removeEventListener(ev, i) + ); + } else if (s.switchTrigger === "idle") { + "requestIdleCallback" in window + ? window.requestIdleCallback(run) + : setTimeout(run, 1000); } - return undefined; }, [ enabled, @@ -141,26 +111,18 @@ export function useTransition({ itemSize, totalItems, visibleRange, - transitionStrategy, + s, onTransitionStart, onTransitionComplete, onTransitionError, ]); - useEffect(() => { - return () => { - if (pruneCancelRef.current) pruneCancelRef.current(); - }; - }, []); - - const isVirtualized = state.type === "VIRTUALIZED"; - const isSwitching = state.type === "SWITCHING"; - const snapshot = state.type === "SWITCHING" ? state.snapshot : null; + useEffect(() => () => pC.current?.(), []); return { state, - isVirtualized, - isSwitching, - snapshot, + isVirtualized: state.type === "VIRTUALIZED", + isSwitching: state.type === "SWITCHING", + snapshot: state.type === "SWITCHING" ? state.snapshot : null, }; } diff --git a/packages/react/src/utils/domPruner.ts b/packages/react/src/utils/domPruner.ts index 6b80205..4db7968 100644 --- a/packages/react/src/utils/domPruner.ts +++ b/packages/react/src/utils/domPruner.ts @@ -7,102 +7,65 @@ export const defaultTransitionStrategy: TransitionStrategy = { chunkSize: 10, }; +const RIC = (cb: () => void) => + typeof window !== "undefined" && "requestIdleCallback" in window + ? window.requestIdleCallback(cb) + : setTimeout(cb, 1); +const CIC = (id: any) => + typeof window !== "undefined" && "cancelIdleCallback" in window + ? window.cancelIdleCallback(id) + : clearTimeout(id); + export function pruneOffscreenDOMIdle( container: HTMLElement, - visibleRange: { start: number; end: number }, - onPrune: (index: number) => void -): () => void { - let cancelled = false; - let requestId: ReturnType | null = null; - - const pruneChunk = () => { + range: { start: number; end: number }, + onPrune: (idx: number) => void +) { + let id: any, + cancelled = false; + const prune = () => { if (cancelled) return; - - const items = container.querySelectorAll("[data-item-index]"); - let pruned = 0; - const maxPrunePerFrame = 5; - - for (const item of items) { - if (pruned >= maxPrunePerFrame) break; - - const index = parseInt(item.getAttribute("data-item-index") || "-1", 10); - if (index < 0) continue; - - if (index < visibleRange.start || index > visibleRange.end) { - onPrune(index); - pruned++; - } - } - - if (pruned > 0 && !cancelled) { - requestId = requestIdleCallback(pruneChunk); - } + let count = 0; + container.querySelectorAll("[data-item-index]").forEach((el) => { + if (count++ > 5) return; + const i = parseInt(el.getAttribute("data-item-index") || "-1", 10); + if (i >= 0 && (i < range.start || i > range.end)) onPrune(i); + }); + if (count > 0 && !cancelled) id = RIC(prune); }; - - requestId = requestIdleCallback(pruneChunk); - + id = RIC(prune); return () => { cancelled = true; - if (requestId !== null) { - cancelIdleCallback(requestId); - } + CIC(id); }; } export function pruneOffscreenDOMChunk( container: HTMLElement, - visibleRange: { start: number; end: number }, - chunkSize: number, - onPrune: (index: number) => void -): () => void { - let cancelled = false; - let timeoutId: number | null = null; - - const pruneChunk = () => { + range: { start: number; end: number }, + chunk: number, + onPrune: (idx: number) => void +) { + let id: any, + cancelled = false; + const prune = () => { if (cancelled) return; - - const items = Array.from(container.querySelectorAll("[data-item-index]")); - const offscreenItems = items.filter((item) => { - const index = parseInt(item.getAttribute("data-item-index") || "-1", 10); - return ( - index >= 0 && (index < visibleRange.start || index > visibleRange.end) - ); - }); - - const chunk = offscreenItems.slice(0, chunkSize); - chunk.forEach((item) => { - const index = parseInt(item.getAttribute("data-item-index") || "-1", 10); - if (index >= 0) { - onPrune(index); + const items = [...container.querySelectorAll("[data-item-index]")].filter( + (el) => { + const i = parseInt(el.getAttribute("data-item-index") || "-1", 10); + return i >= 0 && (i < range.start || i > range.end); } - }); - - if (offscreenItems.length > chunkSize && !cancelled) { - timeoutId = window.setTimeout(pruneChunk, 16) as unknown as number; - } + ); + items + .slice(0, chunk) + .forEach((el) => + onPrune(parseInt(el.getAttribute("data-item-index")!, 10)) + ); + if (items.length > chunk && !cancelled) id = setTimeout(prune, 16); }; - - timeoutId = window.setTimeout(pruneChunk, 16) as unknown as number; - + id = setTimeout(prune, 16); return () => { cancelled = true; - if (timeoutId !== null) { - window.clearTimeout(timeoutId as any); - } + clearTimeout(id); }; } - -function requestIdleCallback(callback: () => void): number { - if (typeof window !== "undefined" && "requestIdleCallback" in window) { - return (window as any).requestIdleCallback(callback) as number; - } - return setTimeout(callback, 1) as unknown as number; -} - -function cancelIdleCallback(id: number): void { - if (typeof window !== "undefined" && "cancelIdleCallback" in window) { - (window as any).cancelIdleCallback(id); - } else { - clearTimeout(id as any); - } -} From 725156de31f016924d547ca69ea3256d16dd3da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 08:18:48 +0900 Subject: [PATCH 13/18] Update packages/core/tsup.config.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/core/tsup.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index af1b21d..57dd856 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ unsafe_arrows: true, unsafe_methods: true, booleans_as_integers: true, - ecma: 2020, + ecma: 2022, }, mangle: { safari10: false, From 1e13699da0b7385d67cba30449e3b599b79996bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 10:25:55 +0900 Subject: [PATCH 14/18] chore: ts 2022->2020 --- packages/core/tsup.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index 57dd856..af1b21d 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ unsafe_arrows: true, unsafe_methods: true, booleans_as_integers: true, - ecma: 2022, + ecma: 2020, }, mangle: { safari10: false, From 32aade5df7277a971650520a22535c100aaa4493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 10:43:50 +0900 Subject: [PATCH 15/18] chore: delete unsafe options --- packages/core/tsup.config.ts | 6 +++--- packages/react-native/tsup.config.ts | 6 +++--- packages/react/tsup.config.ts | 6 +++--- packages/shared/tsup.config.ts | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index af1b21d..5b3cb55 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -14,10 +14,10 @@ export default defineConfig({ drop_console: true, drop_debugger: true, pure_funcs: ["console.log", "console.debug"], - unsafe: true, - unsafe_arrows: true, + unsafe: false, + unsafe_arrows: false, unsafe_methods: true, - booleans_as_integers: true, + booleans_as_integers: false, ecma: 2020, }, mangle: { diff --git a/packages/react-native/tsup.config.ts b/packages/react-native/tsup.config.ts index 065836d..61f9654 100644 --- a/packages/react-native/tsup.config.ts +++ b/packages/react-native/tsup.config.ts @@ -14,10 +14,10 @@ export default defineConfig({ drop_console: true, drop_debugger: true, pure_funcs: ["console.log", "console.debug"], - unsafe: true, - unsafe_arrows: true, + unsafe: false, + unsafe_arrows: false, unsafe_methods: true, - booleans_as_integers: true, + booleans_as_integers: false, ecma: 2020, }, mangle: { diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts index 78a86ef..b53d170 100644 --- a/packages/react/tsup.config.ts +++ b/packages/react/tsup.config.ts @@ -14,10 +14,10 @@ export default defineConfig({ drop_console: true, drop_debugger: true, pure_funcs: ["console.log", "console.debug"], - unsafe: true, - unsafe_arrows: true, + unsafe: false, + unsafe_arrows: false, unsafe_methods: true, - booleans_as_integers: true, + booleans_as_integers: false, ecma: 2020, }, mangle: { diff --git a/packages/shared/tsup.config.ts b/packages/shared/tsup.config.ts index f2fe949..eb77cd7 100644 --- a/packages/shared/tsup.config.ts +++ b/packages/shared/tsup.config.ts @@ -14,10 +14,10 @@ export default defineConfig({ drop_console: true, drop_debugger: true, pure_funcs: ["console.log", "console.debug"], - unsafe: true, - unsafe_arrows: true, + unsafe: false, + unsafe_arrows: false, unsafe_methods: true, - booleans_as_integers: true, + booleans_as_integers: false, ecma: 2020, }, mangle: { From 827aaa8c123b4854ec00487ee17184d429a0bdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 11:37:58 +0900 Subject: [PATCH 16/18] docs: update readme.md --- README.md | 148 ++++++++++++++++-------------------------------------- 1 file changed, 43 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 495fc7d..8f233fd 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,53 @@ -4f11d7e413d6546a60fddc0e02219658e360d58512bc11c1acc57530fab307de +scrolloop logo -# scrolloop +# [scrolloop](https://976520.github.io/scrolloop/) -React 스크롤 컴포넌트 라이브러리 +The modern scrolling component for React and React Native -Just a scrolling library for React - -![NPM Downloads](https://img.shields.io/npm/d18m/scrolloop) +![NPM Downloads](https://img.shields.io/npm/dt/@scrolloop/react) ![Repo size](https://img.shields.io/github/repo-size/976520/scrolloop) ![Last commit](https://img.shields.io/github/last-commit/976520/scrolloop?color=red) -![Top language](https://img.shields.io/github/languages/top/976520/scrolloop) -[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) ## Install +### React + ```bash -npm install scrolloop -yarn add scrolloop -pnpm add scrolloop +npm install @scrolloop/react +# or +yarn add @scrolloop/react +# or +pnpm add @scrolloop/react ``` -## Examples +### React Native + +```bash +npm install @scrolloop/react-native +# or +yarn add @scrolloop/react-native +# or +pnpm add @scrolloop/react-native +``` -### VirtualList +## Quick Start + +### React ```tsx -const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`); +import { VirtualList } from "@scrolloop/react"; + +function App() { + const items = Array.from({ length: 1000 }, (_, i) => `Item #${i}`); -export default function App() { return ( ( -
+
{items[index]}
)} @@ -42,18 +56,21 @@ export default function App() { } ``` +### React Native + ```tsx import { View, Text } from "react-native"; +import { VirtualList } from "@scrolloop/react-native"; -const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`); +function App() { + const items = Array.from({ length: 1000 }, (_, i) => `Item #${i}`); -export default function App() { return ( ( - + {items[index]} )} @@ -62,91 +79,12 @@ export default function App() { } ``` -### InfiniteList - -```tsx -interface User { - id: string; - name: string; - email: string; -} - -export default function UserList() { - return ( - - fetchPage={fetchFunction()} - pageSize={20} - itemSize={60} - height={800} - renderItem={(user, index, style) => ( -
-

{user.name}

-

{user.email}

-
- )} - renderLoading={() =>
로딩 중...
} - renderError={(error, retry) => ( -
-

{error.message}

- -
- )} - renderEmpty={() =>
데이터가 없습니다.
} - /> - ); -} -``` - -```tsx -import { View, Text, TouchableOpacity } from "react-native"; - -interface User { - id: string; - name: string; - email: string; -} +## Packages -export default function UserList() { - return ( - - fetchPage={fetchFunction()} - pageSize={20} - itemSize={60} - height={800} - renderItem={(user, index, style) => ( - - {user.name} - {user.email} - - )} - renderLoading={() => 로딩 중...} - renderError={(error, retry) => ( - - {error.message} - - 재시도 - - - )} - renderEmpty={() => 데이터가 없습니다.} - /> - ); -} -``` +- **@scrolloop/core**: Platform-agnostic virtual scrolling logic +- **@scrolloop/react**: React implementation +- **@scrolloop/react-native**: React Native implementation -## license +## License MIT From 917e9d7fa1712ec05b5423b41d89a741b59600b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Mon, 29 Dec 2025 14:13:31 +0900 Subject: [PATCH 17/18] fix: animation --- docs/.vitepress/config.ts | 2 +- docs/.vitepress/theme/Hero.vue | 4 +- docs/.vitepress/theme/SlotMachine.vue | 204 ++++++++++++-------------- docs/public/favicon.svg | 5 + 4 files changed, 101 insertions(+), 114 deletions(-) create mode 100644 docs/public/favicon.svg diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index bc9ddfe..827f26e 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -8,7 +8,7 @@ export default defineConfig({ base: "/scrolloop/", head: [ - ["link", { rel: "icon", href: "/favicon.ico" }], + ["link", { rel: "icon", href: "/favicon.svg" }], ["meta", { name: "theme-color", content: "#7c3aed" }], ], diff --git a/docs/.vitepress/theme/Hero.vue b/docs/.vitepress/theme/Hero.vue index 8e68d93..d1b8a44 100644 --- a/docs/.vitepress/theme/Hero.vue +++ b/docs/.vitepress/theme/Hero.vue @@ -37,11 +37,11 @@ const animateCountUp = (targetValue, duration, callback) => { }; const startInitialAnimation = () => { - animateCountUp(42, 1500, (value) => { + animateCountUp(36, 2500, (value) => { kbCount.value = value; }); - animateCountUp(1048596, 2500, (value) => { + animateCountUp(1048596, 3500, (value) => { millionsCount.value = value; if (value >= 1048596) { if (!incrementStarted.value) { diff --git a/docs/.vitepress/theme/SlotMachine.vue b/docs/.vitepress/theme/SlotMachine.vue index f02afd0..127b1fc 100644 --- a/docs/.vitepress/theme/SlotMachine.vue +++ b/docs/.vitepress/theme/SlotMachine.vue @@ -1,18 +1,23 @@