@rectify-dev/core 2.1.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,445 @@
1
+ # @rectify-dev/core
2
+
3
+ [![npm](https://img.shields.io/npm/v/@rectify-dev/core)](https://www.npmjs.com/package/@rectify-dev/core) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE)
4
+
5
+ A lightweight React-like UI library built from scratch — fiber reconciler, concurrent rendering, class components, a complete hooks API, lazy/Suspense, and context. All with zero React dependencies and **~10 KB gzipped**.
6
+
7
+ | Feature | Status |
8
+ |---------|--------|
9
+ | Function components + auto-bailout | ✅ |
10
+ | Class components (full lifecycle) | ✅ |
11
+ | `useState`, `useReducer` | ✅ |
12
+ | `useEffect`, `useLayoutEffect` | ✅ |
13
+ | `useRef` (object + callback refs) | ✅ |
14
+ | `useMemo`, `useCallback` | ✅ |
15
+ | `useContext` + `createContext` | ✅ |
16
+ | `useId` | ✅ |
17
+ | `memo()` with custom comparator | ✅ |
18
+ | `lazy()` + `<Suspense>` | ✅ |
19
+ | SVG elements | ✅ |
20
+ | Client-side router | [`@rectify-dev/router`](../../libs/rectify-router) |
21
+
22
+ ---
23
+
24
+ ## Quick Start
25
+
26
+ Scaffold a new project with one command — no config needed:
27
+
28
+ ```bash
29
+ # pnpm
30
+ pnpm create @rectify-dev/rectify-app my-app
31
+
32
+ # npm
33
+ npm create @rectify-dev/rectify-app@latest my-app
34
+
35
+ # yarn
36
+ yarn create @rectify-dev/rectify-app my-app
37
+ ```
38
+
39
+ Then:
40
+
41
+ ```bash
42
+ cd my-app
43
+ pnpm install
44
+ pnpm dev
45
+ ```
46
+
47
+ This scaffolds a Vite + TypeScript project pre-configured with the Rectify JSX runtime, `@rectify-dev/vite-plugin`, and a ready-to-edit `src/App.tsx`.
48
+
49
+ ---
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pnpm add @rectify-dev/core
55
+ # or
56
+ npm install @rectify-dev/core
57
+ ```
58
+
59
+ Configure your bundler to use the Rectify JSX runtime:
60
+
61
+ ```json
62
+ // tsconfig.json
63
+ {
64
+ "compilerOptions": {
65
+ "jsx": "react-jsx",
66
+ "jsxImportSource": "@rectify-dev/core"
67
+ }
68
+ }
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Table of Contents
74
+
75
+ - [Quick Start](#quick-start)
76
+ - [Rendering](#rendering)
77
+ - [Hooks](#hooks)
78
+ - [useState](#usestate)
79
+ - [useReducer](#usereducer)
80
+ - [useEffect](#useeffect)
81
+ - [useLayoutEffect](#uselayouteffect)
82
+ - [useRef](#useref)
83
+ - [useMemo](#usememo)
84
+ - [useCallback](#usecallback)
85
+ - [useId](#useid)
86
+ - [useContext / createContext](#usecontext--createcontext)
87
+ - [Class Components](#class-components)
88
+ - [memo](#memo)
89
+ - [lazy + Suspense](#lazy--suspense)
90
+ - [Fragment](#fragment)
91
+ - [TypeScript](#typescript)
92
+
93
+ ---
94
+
95
+ ## Rendering
96
+
97
+ ```tsx
98
+ import { createRoot } from "@rectify-dev/core";
99
+
100
+ const root = createRoot(document.getElementById("app")!);
101
+ root.render(<App />);
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Hooks
107
+
108
+ ### useState
109
+
110
+ ```tsx
111
+ import { useState } from "@rectify-dev/core";
112
+
113
+ const Counter = () => {
114
+ const [count, setCount] = useState(0);
115
+ return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
116
+ };
117
+ ```
118
+
119
+ Supports lazy initializer:
120
+
121
+ ```tsx
122
+ const [state, setState] = useState(() => expensiveComputation());
123
+ ```
124
+
125
+ ---
126
+
127
+ ### useReducer
128
+
129
+ ```tsx
130
+ import { useReducer } from "@rectify-dev/core";
131
+
132
+ type Action = { type: "inc" } | { type: "dec" };
133
+
134
+ const reducer = (state: number, action: Action) => {
135
+ if (action.type === "inc") return state + 1;
136
+ if (action.type === "dec") return state - 1;
137
+ return state;
138
+ };
139
+
140
+ const Counter = () => {
141
+ const [count, dispatch] = useReducer(reducer, 0);
142
+ return (
143
+ <>
144
+ <button onClick={() => dispatch({ type: "dec" })}>-</button>
145
+ {count}
146
+ <button onClick={() => dispatch({ type: "inc" })}>+</button>
147
+ </>
148
+ );
149
+ };
150
+ ```
151
+
152
+ ---
153
+
154
+ ### useEffect
155
+
156
+ Runs after the browser has painted. Clean up by returning a function.
157
+
158
+ ```tsx
159
+ import { useEffect } from "@rectify-dev/core";
160
+
161
+ const Timer = () => {
162
+ useEffect(() => {
163
+ const id = setInterval(() => console.log("tick"), 1000);
164
+ return () => clearInterval(id);
165
+ }, []); // empty deps → run once on mount
166
+ };
167
+ ```
168
+
169
+ ---
170
+
171
+ ### useLayoutEffect
172
+
173
+ Same signature as `useEffect` but fires synchronously after DOM mutations, before the browser paints. Use for measuring layout or imperatively updating the DOM.
174
+
175
+ ```tsx
176
+ import { useLayoutEffect, useRef } from "@rectify-dev/core";
177
+
178
+ const Tooltip = () => {
179
+ const ref = useRef<HTMLDivElement>(null);
180
+ useLayoutEffect(() => {
181
+ console.log(ref.current?.getBoundingClientRect());
182
+ });
183
+ return <div ref={ref}>hover me</div>;
184
+ };
185
+ ```
186
+
187
+ ---
188
+
189
+ ### useRef
190
+
191
+ Returns a stable mutable container whose `.current` value persists across renders.
192
+
193
+ ```tsx
194
+ import { useRef } from "@rectify-dev/core";
195
+
196
+ const Input = () => {
197
+ const ref = useRef<HTMLInputElement>(null);
198
+ return <input ref={ref} onFocus={() => ref.current?.select()} />;
199
+ };
200
+ ```
201
+
202
+ Callback refs are also supported:
203
+
204
+ ```tsx
205
+ <div ref={(node) => { /* attach */ return () => { /* cleanup */ }; }} />
206
+ ```
207
+
208
+ ---
209
+
210
+ ### useMemo
211
+
212
+ Memoises an expensive computation; recomputes only when `deps` change.
213
+
214
+ ```tsx
215
+ import { useMemo } from "@rectify-dev/core";
216
+
217
+ const List = ({ items }: { items: number[] }) => {
218
+ const sorted = useMemo(() => [...items].sort((a, b) => a - b), [items]);
219
+ return <ul>{sorted.map(n => <li key={n}>{n}</li>)}</ul>;
220
+ };
221
+ ```
222
+
223
+ ---
224
+
225
+ ### useCallback
226
+
227
+ Memoises a function reference; useful for stable event handler props passed to `memo`-wrapped children.
228
+
229
+ ```tsx
230
+ import { useCallback } from "@rectify-dev/core";
231
+
232
+ const Parent = () => {
233
+ const handleClick = useCallback(() => console.log("click"), []);
234
+ return <Child onClick={handleClick} />;
235
+ };
236
+ ```
237
+
238
+ ---
239
+
240
+ ### useId
241
+
242
+ Returns a stable, globally unique string ID that never changes for the lifetime of the component. Ideal for linking form labels to inputs.
243
+
244
+ ```tsx
245
+ import { useId } from "@rectify-dev/core";
246
+
247
+ const Field = ({ label }: { label: string }) => {
248
+ const id = useId();
249
+ return (
250
+ <>
251
+ <label htmlFor={id}>{label}</label>
252
+ <input id={id} />
253
+ </>
254
+ );
255
+ };
256
+ ```
257
+
258
+ Multiple calls in the same component each return a distinct ID:
259
+
260
+ ```tsx
261
+ const Form = () => {
262
+ const nameId = useId(); // ":r0:"
263
+ const emailId = useId(); // ":r1:"
264
+ // ...
265
+ };
266
+ ```
267
+
268
+ ---
269
+
270
+ ### useContext / createContext
271
+
272
+ Share values through the tree without prop drilling.
273
+
274
+ ```tsx
275
+ import { createContext, useContext } from "@rectify-dev/core";
276
+
277
+ const ThemeContext = createContext<"light" | "dark">("light");
278
+
279
+ const App = () => (
280
+ <ThemeContext.Provider value="dark">
281
+ <Page />
282
+ </ThemeContext.Provider>
283
+ );
284
+
285
+ const Page = () => {
286
+ const theme = useContext(ThemeContext);
287
+ return <div className={theme}>...</div>;
288
+ };
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Class Components
294
+
295
+ Extend `Component<Props, State>` to write class-based components.
296
+
297
+ ```tsx
298
+ import { Component } from "@rectify-dev/core";
299
+
300
+ interface Props { title: string }
301
+ interface State { open: boolean }
302
+
303
+ class Accordion extends Component<Props, State> {
304
+ state = { open: false };
305
+
306
+ componentDidMount() {
307
+ console.log("mounted");
308
+ }
309
+
310
+ componentDidUpdate(prevProps: Props, prevState: State) {
311
+ // prevProps and prevState are correct snapshots from before the render
312
+ if (prevState.open !== this.state.open) {
313
+ console.log("toggled", this.state.open);
314
+ }
315
+ }
316
+
317
+ componentWillUnmount() {
318
+ console.log("unmounted");
319
+ }
320
+
321
+ shouldComponentUpdate(nextProps: Props, nextState: State) {
322
+ // return false to skip re-render
323
+ return nextState.open !== this.state.open || nextProps.title !== this.props.title;
324
+ }
325
+
326
+ render() {
327
+ return (
328
+ <div>
329
+ <button onClick={() => this.setState({ open: !this.state.open })}>
330
+ {this.props.title}
331
+ </button>
332
+ {this.state.open && <div>content</div>}
333
+ </div>
334
+ );
335
+ }
336
+ }
337
+ ```
338
+
339
+ `setState` accepts a partial state object or an updater function:
340
+
341
+ ```tsx
342
+ this.setState({ count: 42 });
343
+ this.setState(prev => ({ count: prev.count + 1 }));
344
+ ```
345
+
346
+ ---
347
+
348
+ ## memo
349
+
350
+ Prevents re-renders when props are shallowly equal.
351
+
352
+ ```tsx
353
+ import { memo } from "@rectify-dev/core";
354
+
355
+ const Item = memo(({ name }: { name: string }) => <li>{name}</li>);
356
+
357
+ // Custom comparator
358
+ const Item = memo(
359
+ ({ value }: { value: number }) => <span>{value}</span>,
360
+ (prev, next) => prev.value === next.value,
361
+ );
362
+ ```
363
+
364
+ ---
365
+
366
+ ## lazy + Suspense
367
+
368
+ Code-split a component and show a fallback while it loads.
369
+
370
+ ```tsx
371
+ import { lazy, Suspense } from "@rectify-dev/core";
372
+
373
+ const HeavyChart = lazy(() => import("./HeavyChart"));
374
+
375
+ const Dashboard = () => (
376
+ <Suspense fallback={<div>Loading chart...</div>}>
377
+ <HeavyChart data={data} />
378
+ </Suspense>
379
+ );
380
+ ```
381
+
382
+ Multiple lazy components under one `Suspense` boundary are all handled — the fallback shows until every child resolves.
383
+
384
+ ---
385
+
386
+ ## Fragment
387
+
388
+ Group children without adding a DOM node.
389
+
390
+ ```tsx
391
+ import { Fragment } from "@rectify-dev/core";
392
+
393
+ const Rows = () => (
394
+ <Fragment>
395
+ <tr><td>A</td></tr>
396
+ <tr><td>B</td></tr>
397
+ </Fragment>
398
+ );
399
+
400
+ // Shorthand (requires Babel / TSX support)
401
+ const Rows = () => (
402
+ <>
403
+ <tr><td>A</td></tr>
404
+ <tr><td>B</td></tr>
405
+ </>
406
+ );
407
+ ```
408
+
409
+ ---
410
+
411
+ ## TypeScript
412
+
413
+ All JSX element attributes, event handlers, CSS properties, and ARIA attributes are fully typed. Import convenience types directly from the package:
414
+
415
+ ```tsx
416
+ import type {
417
+ FC,
418
+ RectifyNode,
419
+ CSSProperties,
420
+ HTMLAttributes,
421
+ InputHTMLAttributes,
422
+ SuspenseProps,
423
+ RefObject,
424
+ RectifyContext,
425
+ Reducer,
426
+ Dispatch,
427
+ SyntheticMouseEvent,
428
+ SyntheticKeyboardEvent,
429
+ // ... and all other synthetic event types
430
+ } from "@rectify-dev/core";
431
+ ```
432
+
433
+ ### `key` prop
434
+
435
+ The `key` prop is accepted on every element (host and component) without appearing in the component's own props type:
436
+
437
+ ```tsx
438
+ items.map(item => <Row key={item.id} data={item} />);
439
+ ```
440
+
441
+ ---
442
+
443
+ ## License
444
+
445
+ MIT