From 5e20f148308b330e111fad5079c64fcfca2f4dcb Mon Sep 17 00:00:00 2001 From: Viktor Pasynok Date: Wed, 5 Nov 2025 16:59:36 +0700 Subject: [PATCH 1/5] Update size limit to 900 B and enhance documentation for v3.0.0, introducing a breaking change in API structure to a target-first approach, along with a new `clear` method for slot management. --- .size-limit.json | 2 +- CHANGELOG.md | 27 ++++++++++++++++++ README.md | 74 +++++++++++++++++++++++++++++++++++------------- src/index.tsx | 36 ++++++++++++++++++++--- 4 files changed, 114 insertions(+), 25 deletions(-) diff --git a/.size-limit.json b/.size-limit.json index 9fa916f..3f9c63e 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -1 +1 @@ -[{ "path": "dist/index.js", "limit": "850 B" }] +[{ "path": "dist/index.js", "limit": "900 B" }] diff --git a/CHANGELOG.md b/CHANGELOG.md index e8480e7..80237ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](http://semver.org). +## 3.0.0 + +### Changed + +- **BREAKING:** API structure changed to target-first approach for better DX + +**Before (v2):** + +```tsx +slotsApi.insert.into.Description({ component: MyComponent }); +``` + +**After (v3):** + +```tsx +slotsApi.Description.insert({ component: MyComponent }); +``` + +**Benefits:** + +- **Discoverability:** Type `slotsApi.` to see all available slots, then `slotsApi.[SlotName].` to see all actions for that slot +- **Logical grouping:** All methods for a specific slot are in one place + +### Added + +- `clear` method to clear all components from a slot + ## 2.0.0 ### Changed diff --git a/README.md b/README.md index 31fd4d9..8bdfc2c 100644 --- a/README.md +++ b/README.md @@ -76,24 +76,21 @@ const Sidebar = () => ( ); // plugin-analytics/index.ts - inject from anywhere! -slotsApi.insert.into.Widgets({ +slotsApi.Widgets.insert({ component: () => , }); // plugin-user-stats/index.ts - another plugin -slotsApi.insert.into.Widgets({ +slotsApi.Widgets.insert({ component: () => , }); -``` - -### Result -```tsx - +// Result: +// ``` No props drilling, no boilerplate - just define slots and inject content from anywhere in your codebase. @@ -110,7 +107,7 @@ bun add @grlt-hub/react-slots yarn add @grlt-hub/react-slots ``` -TypeScript types are included out of the box. +**Note:** TypeScript types are included out of the box. ### Peer dependencies @@ -140,7 +137,7 @@ const App = () => ( ); // 3. Insert content into the slot -slotsApi.insert.into.Footer({ +slotsApi.Footer.insert({ component: () =>

© 1955–1985–2015 Outatime Corp.

, }); @@ -165,7 +162,7 @@ const { slotsApi, Slots } = createSlots({ ; // Insert component - receives props automatically -slotsApi.insert.into.UserPanel({ +slotsApi.UserPanel.insert({ component: (props) => , }); ``` @@ -179,7 +176,7 @@ const { slotsApi, Slots } = createSlots({ ; -slotsApi.insert.into.UserPanel({ +slotsApi.UserPanel.insert({ // Transform userId into userName and isAdmin before passing to component mapProps: (slotProps) => ({ userName: getUserName(slotProps.userId), @@ -195,13 +192,13 @@ Components are inserted in any order, but rendered according to `order` value (l ```tsx // This is inserted first, but will render second -slotsApi.insert.into.Sidebar({ +slotsApi.Sidebar.insert({ component: () => , order: 2, }); // This is inserted second, but will render first -slotsApi.insert.into.Sidebar({ +slotsApi.Sidebar.insert({ component: () => , order: 1, }); @@ -215,6 +212,35 @@ slotsApi.insert.into.Sidebar({ **Note:** Components with the same `order` value keep their insertion order and all of them are rendered. +### Clear slot content + +Remove all components from a slot: + +```tsx +// Insert components +slotsApi.Sidebar.insert({ + component: () => , +}); + +slotsApi.Sidebar.insert({ + component: () => , +}); + +// Result after inserts: +// + +// Later, clear the slot +slotsApi.Sidebar.clear(); + +// Result after clear: +// +``` + ### Defer insertion until event fires Wait for data to load before inserting component. The component won't render until the event fires: @@ -225,7 +251,7 @@ import { createEvent } from 'effector'; const userLoaded = createEvent<{ id: number; name: string }>(); // Component will be inserted only after userLoaded fires -slotsApi.insert.into.Header({ +slotsApi.Header.insert({ when: userLoaded, mapProps: (slotProps, whenPayload) => ({ userId: whenPayload.id, @@ -234,10 +260,18 @@ slotsApi.insert.into.Header({ component: (props) => , }); -// Component is not rendered yet... +// Result before userLoaded fires: +//
+// {/* Header slot is empty, waiting... */} +//
// Later, when data arrives: -userLoaded({ id: 123, name: 'John' }); // NOW the component is inserted and rendered +userLoaded({ id: 123, name: 'John' }); + +// Result after userLoaded fires: +//
+// +//
``` **Note:** You can pass an array of events `when: [event1, event2]` - component inserts when **any** of them fires. Use [once](https://patronum.effector.dev/operators/once/) from `patronum` if you need one-time insertion. diff --git a/src/index.tsx b/src/index.tsx index 7aec810..f699c19 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,4 @@ -import { createEvent, createStore } from 'effector'; +import { createEvent, createStore, type EventCallable } from 'effector'; import { useStoreMap } from 'effector-react'; import { nanoid } from 'nanoid'; import React, { memo, type FunctionComponent } from 'react'; @@ -19,6 +19,17 @@ const createSlots = >>(config: T) => [key in keyof T]: T[key] extends (_: any) => unknown ? Payload[0]> : never; }; + type ClearApi = { + [key in keyof T]: EventCallable; + }; + + type SlotApi = { + [key in keyof T]: { + insert: SetApi[key]; + clear: ClearApi[key]; + }; + }; + type State = { [key in keyof T]: (Parameters[0] & { id: string })[]; }; @@ -80,6 +91,19 @@ const createSlots = >>(config: T) => return acc; }, {} as SetApi); + const clearApi = keys.reduce((acc, key) => { + const clear = createEvent(); + + $slots.on(clear, (state) => { + if (state[key].length === 0) return state; + return { ...state, [key]: [] }; + }); + + acc[key] = clear; + + return acc; + }, {} as ClearApi); + const slots = keys.reduce((acc, key) => { const component = memo>((props) => { const slotChildren = useStoreMap($slots, (x) => x[key]); @@ -108,9 +132,13 @@ const createSlots = >>(config: T) => return acc; }, {} as Slots); - const slotsApi = { - insert: { into: insertApi }, - }; + const slotsApi = keys.reduce((acc, key) => { + acc[key] = { + insert: insertApi[key], + clear: clearApi[key], + }; + return acc; + }, {} as SlotApi); return { slotsApi, Slots: slots }; }; From 1ae4ee0b009610a12a524688cb20b27dfaaadbe1 Mon Sep 17 00:00:00 2001 From: Viktor Pasynok Date: Wed, 5 Nov 2025 17:00:19 +0700 Subject: [PATCH 2/5] Refactor slot insertion syntax in sandbox.tsx to streamline API usage, removing deprecated `insert.into` method in favor of direct `ConfirmScreenBottom.insert` calls. --- sandbox.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sandbox.tsx b/sandbox.tsx index c7c99a8..7a4e13d 100644 --- a/sandbox.tsx +++ b/sandbox.tsx @@ -1,4 +1,3 @@ -import { createEvent } from 'effector'; import { createGate } from 'effector-react'; import React from 'react'; import { createSlotIdentifier, createSlots } from './src'; @@ -9,13 +8,13 @@ const { slotsApi } = createSlots({ const appGate = createGate(); -slotsApi.insert.into.ConfirmScreenBottom({ +slotsApi.ConfirmScreenBottom.insert({ when: [appGate.open], mapProps: (__, y) => ({ id: Number(y) }), component: (props) =>

Hello world! {props.id}

, }); -slotsApi.insert.into.ConfirmScreenBottom({ +slotsApi.ConfirmScreenBottom.insert({ mapProps: (x) => x, component: (props) =>

Hello world! {props.id}

, }); From a646de58b494227ff8491c3181362d606d539325 Mon Sep 17 00:00:00 2001 From: Viktor Pasynok Date: Wed, 5 Nov 2025 17:22:28 +0700 Subject: [PATCH 3/5] Refactor API to use `Component` instead of `component` for slot insertion, enhancing compatibility with React hooks. Update documentation and examples to reflect this breaking change and provide migration guidance from v2 to v3. --- CHANGELOG.md | 40 ++++++++++++++++++++++---------- README.md | 20 ++++++++-------- sandbox.tsx | 4 ++-- src/__tests__/payload.spec-d.tsx | 36 ++++++++++++++-------------- src/helpers.tsx | 2 +- src/index.tsx | 2 +- src/payload.ts | 6 ++--- 7 files changed, 63 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80237ce..d532b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,29 +7,45 @@ and this project adheres to [Semantic Versioning](http://semver.org). ### Changed -- **BREAKING:** API structure changed to target-first approach for better DX +- **BREAKING:** `component` field renamed to `Component` (capital C) to enable React hooks usage in inline components without ESLint warnings -**Before (v2):** +- **BREAKING:** API structure changed to target-first approach -```tsx -slotsApi.insert.into.Description({ component: MyComponent }); -``` + **Before (v2):** -**After (v3):** + ```tsx + slotsApi.insert.into.Description({ component: MyComponent }); + ``` -```tsx -slotsApi.Description.insert({ component: MyComponent }); -``` + **After (v3):** -**Benefits:** + ```tsx + slotsApi.Description.insert({ Component: MyComponent }); + ``` -- **Discoverability:** Type `slotsApi.` to see all available slots, then `slotsApi.[SlotName].` to see all actions for that slot -- **Logical grouping:** All methods for a specific slot are in one place + **Benefits:** + - **Discoverability:** Type `slotsApi.` to see all available slots, then `slotsApi.[SlotName].` to see all actions for that slot + - **Logical grouping:** All methods for a specific slot are in one place ### Added - `clear` method to clear all components from a slot +### Migration from v2 to v3 + +1. Replace `slotsApi.insert.into.[SlotName]` with `slotsApi.[SlotName].insert` +2. Replace `component:` with `Component:` in all insert calls + +**Example:** + +```tsx +// v2 +slotsApi.insert.into.Header({ component: MyComponent }); + +// v3 +slotsApi.Header.insert({ Component: MyComponent }); +``` + ## 2.0.0 ### Changed diff --git a/README.md b/README.md index 8bdfc2c..97941c8 100644 --- a/README.md +++ b/README.md @@ -77,12 +77,12 @@ const Sidebar = () => ( // plugin-analytics/index.ts - inject from anywhere! slotsApi.Widgets.insert({ - component: () => , + Component: () => , }); // plugin-user-stats/index.ts - another plugin slotsApi.Widgets.insert({ - component: () => , + Component: () => , }); // Result: @@ -138,7 +138,7 @@ const App = () => ( // 3. Insert content into the slot slotsApi.Footer.insert({ - component: () =>

© 1955–1985–2015 Outatime Corp.

, + Component: () =>

© 1955–1985–2015 Outatime Corp.

, }); // Result: @@ -163,7 +163,7 @@ const { slotsApi, Slots } = createSlots({ // Insert component - receives props automatically slotsApi.UserPanel.insert({ - component: (props) => , + Component: (props) => , }); ``` @@ -182,7 +182,7 @@ slotsApi.UserPanel.insert({ userName: getUserName(slotProps.userId), isAdmin: checkAdmin(slotProps.userId), }), - component: (props) => , + Component: (props) => , }); ``` @@ -193,13 +193,13 @@ Components are inserted in any order, but rendered according to `order` value (l ```tsx // This is inserted first, but will render second slotsApi.Sidebar.insert({ - component: () => , + Component: () => , order: 2, }); // This is inserted second, but will render first slotsApi.Sidebar.insert({ - component: () => , + Component: () => , order: 1, }); @@ -219,11 +219,11 @@ Remove all components from a slot: ```tsx // Insert components slotsApi.Sidebar.insert({ - component: () => , + Component: () => , }); slotsApi.Sidebar.insert({ - component: () => , + Component: () => , }); // Result after inserts: @@ -257,7 +257,7 @@ slotsApi.Header.insert({ userId: whenPayload.id, userName: whenPayload.name, }), - component: (props) => , + Component: (props) => , }); // Result before userLoaded fires: diff --git a/sandbox.tsx b/sandbox.tsx index 7a4e13d..4f5a52f 100644 --- a/sandbox.tsx +++ b/sandbox.tsx @@ -11,10 +11,10 @@ const appGate = createGate(); slotsApi.ConfirmScreenBottom.insert({ when: [appGate.open], mapProps: (__, y) => ({ id: Number(y) }), - component: (props) =>

Hello world! {props.id}

, + Component: (props) =>

Hello world! {props.id}

, }); slotsApi.ConfirmScreenBottom.insert({ mapProps: (x) => x, - component: (props) =>

Hello world! {props.id}

, + Component: (props) =>

Hello world! {props.id}

, }); diff --git a/src/__tests__/payload.spec-d.tsx b/src/__tests__/payload.spec-d.tsx index 083f052..002fd6a 100644 --- a/src/__tests__/payload.spec-d.tsx +++ b/src/__tests__/payload.spec-d.tsx @@ -13,68 +13,68 @@ const { slotsApi } = createSlots({ Bottom: noPropsSlot, }); -slotsApi.insert.into.Top({ +slotsApi.Top.insert({ mapProps: (data) => ({ text: data.text }), - component: (props) => { + Component: (props) => { expectTypeOf<{ text: string }>(props); return
; }, }); -slotsApi.insert.into.Top({ +slotsApi.Top.insert({ when: signal, mapProps: (__, signalPayload) => ({ text: String(signalPayload) }), - component: (props) => { + Component: (props) => { expectTypeOf<{ text: string }>(props); return
; }, }); -slotsApi.insert.into.Top({ +slotsApi.Top.insert({ when: [signal], mapProps: (__, signalPayload) => ({ text: String(signalPayload) }), - component: (props) => { + Component: (props) => { expectTypeOf<{ text: string }>(props); return
; }, }); -slotsApi.insert.into.Top({ +slotsApi.Top.insert({ when: [signal, createEvent()], mapProps: (__, signalPayload) => { const text = Array.isArray(signalPayload) ? signalPayload[0] : String(signalPayload); return { text }; }, - component: (props) => { + Component: (props) => { expectTypeOf<{ text: string }>(props); return
; }, }); -slotsApi.insert.into.Top({ - component: (props) => { +slotsApi.Top.insert({ + Component: (props) => { expectTypeOf<{ text: string }>(props); return
; }, }); -slotsApi.insert.into.Bottom({ - component: (props) => { +slotsApi.Bottom.insert({ + Component: (props) => { expectTypeOf(props); return
; }, }); -slotsApi.insert.into.Top({ +slotsApi.Top.insert({ mapProps: () => {}, - component: () =>
, + Component: () =>
, }); -slotsApi.insert.into.Top({ +slotsApi.Top.insert({ when: signal, mapProps: (slotPayload, signalPayload) => ({ data: { signalPayload, slotPayload } }), - component: (props) => { + Component: (props) => { expectTypeOf<{ data: { slotPayload: { text: string }; signalPayload: number }; }>(props); @@ -82,8 +82,8 @@ slotsApi.insert.into.Top({ }, }); -slotsApi.insert.into.Top({ +slotsApi.Top.insert({ mapProps: (data) => ({ text: data.text }), // @ts-expect-error - component: (_: { wrong: number }) =>
, + Component: (_: { wrong: number }) =>
, }); diff --git a/src/helpers.tsx b/src/helpers.tsx index 41af7c3..7158509 100644 --- a/src/helpers.tsx +++ b/src/helpers.tsx @@ -20,7 +20,7 @@ const makeChildWithProps = (child) => memo((props) => { const childProps = useMemo(() => child.mapProps(props), [props]); - return ; + return ; }); export { insertSorted, isNil, makeChildWithProps, type EmptyObject, type Entries }; diff --git a/src/index.tsx b/src/index.tsx index f699c19..9c1305c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -112,7 +112,7 @@ const createSlots = >>(config: T) => if (isNil(child.mapProps)) { return ( - + ); } diff --git a/src/payload.ts b/src/payload.ts index 70c74cc..f03c7d6 100644 --- a/src/payload.ts +++ b/src/payload.ts @@ -6,7 +6,7 @@ type ExtractWhenPayload = T extends Event ? P : T extends Event type Payload = { // When mapProps is provided with when | Event[]>(params: { - component: (props: unknown extends R ? EmptyObject : R extends void ? EmptyObject : R) => React.JSX.Element; + Component: (props: unknown extends R ? EmptyObject : R extends void ? EmptyObject : R) => React.JSX.Element; mapProps: (arg: T, whenPayload: ExtractWhenPayload) => R; order?: number; when: W; @@ -14,7 +14,7 @@ type Payload = { // When mapProps is provided without when (params: { - component: (props: unknown extends R ? EmptyObject : R extends void ? EmptyObject : R) => React.JSX.Element; + Component: (props: unknown extends R ? EmptyObject : R extends void ? EmptyObject : R) => React.JSX.Element; mapProps: (arg: T) => R; order?: number; when?: undefined; @@ -22,7 +22,7 @@ type Payload = { // When mapProps is not provided (params: { - component: (props: unknown extends T ? EmptyObject : T extends void ? EmptyObject : T) => React.JSX.Element; + Component: (props: unknown extends T ? EmptyObject : T extends void ? EmptyObject : T) => React.JSX.Element; mapProps?: undefined; order?: number; when?: undefined; From 15057cd4f28ab4b47985726905da06627d2b80c3 Mon Sep 17 00:00:00 2001 From: Viktor Pasynok Date: Wed, 5 Nov 2025 17:25:43 +0700 Subject: [PATCH 4/5] Upgrade package version to 3.0.0 in package.json and package-lock.json to reflect major changes and improvements in the API. This update aligns with recent refactoring efforts and prepares for upcoming features. --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1c6272c..7ea10ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@grlt-hub/react-slots", - "version": "2.0.0", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@grlt-hub/react-slots", - "version": "2.0.0", + "version": "3.0.0", "license": "MIT", "devDependencies": { "@rslib/core": "0.17.0", diff --git a/package.json b/package.json index 5eb9172..6b2da71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@grlt-hub/react-slots", - "version": "2.0.0", + "version": "3.0.0", "type": "module", "private": false, "main": "dist/index.js", From 164ffc63c50e8901805dd7058c25c24ff7baa562 Mon Sep 17 00:00:00 2001 From: Viktor Pasynok Date: Wed, 5 Nov 2025 17:28:48 +0700 Subject: [PATCH 5/5] fixed: clean-publish --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d532b06..fb510fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](http://semver.org). +## 3.0.1 + +### Fixed + +- use `clean-publish` + ## 3.0.0 ### Changed diff --git a/package-lock.json b/package-lock.json index 7ea10ad..0c946cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@grlt-hub/react-slots", - "version": "3.0.0", + "version": "3.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@grlt-hub/react-slots", - "version": "3.0.0", + "version": "3.0.1", "license": "MIT", "devDependencies": { "@rslib/core": "0.17.0", diff --git a/package.json b/package.json index 6b2da71..24bceb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@grlt-hub/react-slots", - "version": "3.0.0", + "version": "3.0.1", "type": "module", "private": false, "main": "dist/index.js",