@blinkorb/rcx 0.0.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/.eslintignore +4 -0
- package/.eslintrc.json +286 -0
- package/.gitattributes +2 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/workflows/ci.yml +19 -0
- package/.nvmrc +1 -0
- package/.prettierignore +28 -0
- package/.prettierrc.json +4 -0
- package/demo/index.html +29 -0
- package/demo/index.tsx +316 -0
- package/demo/tsconfig.json +12 -0
- package/jest.config.ts +21 -0
- package/package.json +80 -0
- package/scripts/prep-package.js +29 -0
- package/src/components/canvas/context.ts +6 -0
- package/src/components/canvas/index.ts +98 -0
- package/src/components/index.ts +5 -0
- package/src/components/paths/arc-to.ts +66 -0
- package/src/components/paths/clip.ts +32 -0
- package/src/components/paths/index.ts +5 -0
- package/src/components/paths/line.ts +53 -0
- package/src/components/paths/path.ts +59 -0
- package/src/components/paths/point.ts +24 -0
- package/src/components/shapes/circle.tsx +32 -0
- package/src/components/shapes/ellipse.ts +75 -0
- package/src/components/shapes/index.ts +3 -0
- package/src/components/shapes/rectangle.ts +45 -0
- package/src/components/text/index.ts +1 -0
- package/src/components/text/text.ts +137 -0
- package/src/components/transform/index.ts +3 -0
- package/src/components/transform/rotate.ts +26 -0
- package/src/components/transform/scale.ts +34 -0
- package/src/components/transform/translate.ts +27 -0
- package/src/context/create-context.ts +49 -0
- package/src/context/index.ts +1 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/use-canvas-context.ts +11 -0
- package/src/hooks/use-linear-gradient.ts +39 -0
- package/src/hooks/use-loop.ts +11 -0
- package/src/hooks/use-on.ts +18 -0
- package/src/hooks/use-radial-gradient.ts +45 -0
- package/src/hooks/use-render.ts +14 -0
- package/src/hooks/use-state.ts +9 -0
- package/src/hooks/use-window-size.ts +24 -0
- package/src/index.ts +6 -0
- package/src/internal/emitter.ts +39 -0
- package/src/internal/global.ts +5 -0
- package/src/internal/hooks.ts +32 -0
- package/src/internal/reactive.test.ts +20 -0
- package/src/internal/reactive.ts +20 -0
- package/src/jsx-runtime.ts +21 -0
- package/src/render.ts +299 -0
- package/src/types.ts +151 -0
- package/src/utils/apply-fill-and-stroke-style.ts +33 -0
- package/src/utils/get-recommended-pixel-ratio.ts +2 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/is-own-property-of.ts +6 -0
- package/src/utils/is-valid-fill-or-stroke-style.ts +5 -0
- package/src/utils/is-valid-stroke-cap.ts +10 -0
- package/src/utils/is-valid-stroke-join.ts +10 -0
- package/src/utils/resolve-styles.ts +21 -0
- package/src/utils/type-guards.ts +4 -0
- package/src/utils/with-px.ts +4 -0
- package/tsb.config.ts +11 -0
- package/tsconfig.dist.json +13 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useRenderBeforeChildren } from '../../hooks/use-render.ts';
|
|
2
|
+
import type {
|
|
3
|
+
RCXChildren,
|
|
4
|
+
RCXComponent,
|
|
5
|
+
RCXFontStyle,
|
|
6
|
+
RCXPropsWithChildren,
|
|
7
|
+
RCXShapeStyle,
|
|
8
|
+
RCXStyleProp,
|
|
9
|
+
} from '../../types.ts';
|
|
10
|
+
import { isValidFillOrStrokeStyle } from '../../utils/is-valid-fill-or-stroke-style.ts';
|
|
11
|
+
import { isValidStrokeCap } from '../../utils/is-valid-stroke-cap.ts';
|
|
12
|
+
import { isValidStrokeJoin } from '../../utils/is-valid-stroke-join.ts';
|
|
13
|
+
import { resolveStyles } from '../../utils/resolve-styles.ts';
|
|
14
|
+
import { isArray } from '../../utils/type-guards.ts';
|
|
15
|
+
import { withPx } from '../../utils/with-px.ts';
|
|
16
|
+
|
|
17
|
+
export interface TextStyle extends RCXShapeStyle, RCXFontStyle {
|
|
18
|
+
align?: CanvasTextAlign;
|
|
19
|
+
baseline?: CanvasTextBaseline;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TextProps = RCXPropsWithChildren<{
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
maxWidth?: number;
|
|
26
|
+
children?: RCXChildren;
|
|
27
|
+
style?: RCXStyleProp<TextStyle>;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
const DEFAULT_FONT_STYLE = {
|
|
31
|
+
// font string
|
|
32
|
+
fontStyle: 'normal',
|
|
33
|
+
fontWeight: 'normal',
|
|
34
|
+
fontSize: 10,
|
|
35
|
+
fontFamily: 'sans-serif',
|
|
36
|
+
// other ctx2d properties
|
|
37
|
+
fontStretch: 'normal',
|
|
38
|
+
fontVariant: 'normal',
|
|
39
|
+
fontKerning: 'normal',
|
|
40
|
+
} satisfies Required<RCXFontStyle>;
|
|
41
|
+
|
|
42
|
+
const getTextFromChildren = (children: RCXChildren): string => {
|
|
43
|
+
if (
|
|
44
|
+
children === null ||
|
|
45
|
+
typeof children === 'boolean' ||
|
|
46
|
+
typeof children === 'undefined'
|
|
47
|
+
) {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof children === 'object') {
|
|
52
|
+
if (isArray(children)) {
|
|
53
|
+
return children.reduce<string>(
|
|
54
|
+
(acc, child) => acc + getTextFromChildren(child),
|
|
55
|
+
''
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return getTextFromChildren(children.props.children);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return children.toString();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const Text: RCXComponent<TextProps> = (props) => {
|
|
66
|
+
useRenderBeforeChildren((renderingContext) => {
|
|
67
|
+
const { x, y, maxWidth, children } = props;
|
|
68
|
+
const {
|
|
69
|
+
fill,
|
|
70
|
+
stroke,
|
|
71
|
+
strokeWidth,
|
|
72
|
+
strokeCap,
|
|
73
|
+
strokeJoin,
|
|
74
|
+
align,
|
|
75
|
+
baseline,
|
|
76
|
+
fontStyle = DEFAULT_FONT_STYLE.fontStyle,
|
|
77
|
+
fontWeight = DEFAULT_FONT_STYLE.fontWeight,
|
|
78
|
+
fontSize = DEFAULT_FONT_STYLE.fontSize,
|
|
79
|
+
fontFamily = DEFAULT_FONT_STYLE.fontFamily,
|
|
80
|
+
// other ctx2d properties
|
|
81
|
+
fontStretch = DEFAULT_FONT_STYLE.fontStretch,
|
|
82
|
+
fontVariant = DEFAULT_FONT_STYLE.fontVariant,
|
|
83
|
+
fontKerning = DEFAULT_FONT_STYLE.fontKerning,
|
|
84
|
+
} = resolveStyles(props.style);
|
|
85
|
+
|
|
86
|
+
const text = getTextFromChildren(children);
|
|
87
|
+
|
|
88
|
+
renderingContext.ctx2d.save();
|
|
89
|
+
|
|
90
|
+
if (typeof align === 'string') {
|
|
91
|
+
renderingContext.ctx2d.textAlign = align;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof baseline === 'string') {
|
|
95
|
+
renderingContext.ctx2d.textBaseline = baseline;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
renderingContext.ctx2d.font = [
|
|
99
|
+
fontStyle,
|
|
100
|
+
fontWeight,
|
|
101
|
+
withPx(fontSize),
|
|
102
|
+
fontFamily,
|
|
103
|
+
].join(' ');
|
|
104
|
+
|
|
105
|
+
renderingContext.ctx2d.fontStretch = fontStretch;
|
|
106
|
+
renderingContext.ctx2d.fontVariantCaps = fontVariant;
|
|
107
|
+
renderingContext.ctx2d.fontKerning = fontKerning;
|
|
108
|
+
|
|
109
|
+
if (isValidFillOrStrokeStyle(fill)) {
|
|
110
|
+
renderingContext.ctx2d.fillStyle = fill;
|
|
111
|
+
renderingContext.ctx2d.fillText(text, x, y, maxWidth);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof strokeWidth === 'number') {
|
|
115
|
+
renderingContext.ctx2d.lineWidth = strokeWidth;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isValidStrokeCap(strokeCap)) {
|
|
119
|
+
renderingContext.ctx2d.lineCap = strokeCap;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isValidStrokeJoin(strokeJoin)) {
|
|
123
|
+
renderingContext.ctx2d.lineJoin = strokeJoin;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (isValidFillOrStrokeStyle(stroke)) {
|
|
127
|
+
renderingContext.ctx2d.strokeStyle = stroke;
|
|
128
|
+
renderingContext.ctx2d.strokeText(text, x, y, maxWidth);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
renderingContext.ctx2d.restore();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
Text.displayName = 'Text';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useRenderAfterChildren,
|
|
3
|
+
useRenderBeforeChildren,
|
|
4
|
+
} from '../../hooks/use-render.ts';
|
|
5
|
+
import { RCXComponent, RCXPropsWithChildren } from '../../types.ts';
|
|
6
|
+
|
|
7
|
+
export type RotateProps = RCXPropsWithChildren<{
|
|
8
|
+
rotation: number;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
export const Rotate: RCXComponent<RotateProps> = (props) => {
|
|
12
|
+
useRenderBeforeChildren((renderingContext) => {
|
|
13
|
+
const { rotation } = props;
|
|
14
|
+
|
|
15
|
+
renderingContext.ctx2d.save();
|
|
16
|
+
renderingContext.ctx2d.rotate(rotation);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
useRenderAfterChildren((renderingContext) => {
|
|
20
|
+
renderingContext.ctx2d.restore();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return props.children;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
Rotate.displayName = 'Rotate';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useRenderAfterChildren,
|
|
3
|
+
useRenderBeforeChildren,
|
|
4
|
+
} from '../../hooks/use-render.ts';
|
|
5
|
+
import { RCXComponent, RCXPropsWithChildren } from '../../types.ts';
|
|
6
|
+
|
|
7
|
+
export type ScaleProps =
|
|
8
|
+
| RCXPropsWithChildren<{
|
|
9
|
+
scale: number;
|
|
10
|
+
scaleX?: never;
|
|
11
|
+
scaleY?: never;
|
|
12
|
+
}>
|
|
13
|
+
| RCXPropsWithChildren<{
|
|
14
|
+
scale: never;
|
|
15
|
+
scaleX: number;
|
|
16
|
+
scaleY: number;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export const Scale: RCXComponent<ScaleProps> = (props) => {
|
|
20
|
+
useRenderBeforeChildren((renderingContext) => {
|
|
21
|
+
const { scale, scaleX, scaleY } = props;
|
|
22
|
+
|
|
23
|
+
renderingContext.ctx2d.save();
|
|
24
|
+
renderingContext.ctx2d.scale(scaleX ?? scale, scaleY ?? scale);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
useRenderAfterChildren((renderingContext) => {
|
|
28
|
+
renderingContext.ctx2d.restore();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return props.children;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
Scale.displayName = 'Scale';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useRenderAfterChildren,
|
|
3
|
+
useRenderBeforeChildren,
|
|
4
|
+
} from '../../hooks/use-render.ts';
|
|
5
|
+
import { RCXComponent, RCXPropsWithChildren } from '../../types.ts';
|
|
6
|
+
|
|
7
|
+
export type TranslateProps = RCXPropsWithChildren<{
|
|
8
|
+
x?: number;
|
|
9
|
+
y?: number;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export const Translate: RCXComponent<TranslateProps> = (props) => {
|
|
13
|
+
useRenderBeforeChildren((renderingContext) => {
|
|
14
|
+
const { x = 0, y = 0 } = props;
|
|
15
|
+
|
|
16
|
+
renderingContext.ctx2d.save();
|
|
17
|
+
renderingContext.ctx2d.translate(x, y);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
useRenderAfterChildren((renderingContext) => {
|
|
21
|
+
renderingContext.ctx2d.restore();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return props.children;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
Translate.displayName = 'Translate';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cxGlobal } from '../internal/global.ts';
|
|
2
|
+
import type {
|
|
3
|
+
AnyObject,
|
|
4
|
+
RCXComponent,
|
|
5
|
+
RCXPropsWithChildren,
|
|
6
|
+
} from '../types.ts';
|
|
7
|
+
|
|
8
|
+
export type ProviderProps<T> = RCXPropsWithChildren<{
|
|
9
|
+
value: T;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export const createContext = <T extends AnyObject>(name?: string) => {
|
|
13
|
+
const symbol = Symbol(name);
|
|
14
|
+
|
|
15
|
+
const useProvide = (value: T) => {
|
|
16
|
+
const { currentNode } = cxGlobal;
|
|
17
|
+
if (!currentNode) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
'useProvide must be called inside the body of a component'
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
currentNode.context[symbol] = value;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const useInject = () => {
|
|
27
|
+
const { currentNode } = cxGlobal;
|
|
28
|
+
if (!currentNode) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'useInject must be called inside the body of a component'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return currentNode.context[symbol] as Readonly<T> | undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const Provider: RCXComponent<ProviderProps<T>> = ({ children, value }) => {
|
|
38
|
+
useProvide(value);
|
|
39
|
+
return children;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
Provider.displayName = 'Provider';
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
Provider,
|
|
46
|
+
useProvide,
|
|
47
|
+
useInject,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './create-context.ts';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './use-canvas-context.ts';
|
|
2
|
+
export * from './use-loop.ts';
|
|
3
|
+
export * from './use-on.ts';
|
|
4
|
+
export * from './use-render.ts';
|
|
5
|
+
export * from './use-state.ts';
|
|
6
|
+
export * from './use-window-size.ts';
|
|
7
|
+
export * from './use-linear-gradient.ts';
|
|
8
|
+
export * from './use-radial-gradient.ts';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { canvasContext } from '../components/canvas/context.ts';
|
|
2
|
+
|
|
3
|
+
export const useCanvasContext = () => {
|
|
4
|
+
const context = canvasContext.useInject();
|
|
5
|
+
|
|
6
|
+
if (!context) {
|
|
7
|
+
throw new Error('useCanvasContext must be used below a Canvas component');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return context;
|
|
11
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { renderingContext } from '../components/canvas/context.ts';
|
|
2
|
+
import type { RCXColorStop } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
export interface LinearGradientConfig {
|
|
5
|
+
startX: number;
|
|
6
|
+
startY: number;
|
|
7
|
+
endX: number;
|
|
8
|
+
endY: number;
|
|
9
|
+
stops: readonly RCXColorStop[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const useLinearGradient = ({
|
|
13
|
+
startX,
|
|
14
|
+
startY,
|
|
15
|
+
endX,
|
|
16
|
+
endY,
|
|
17
|
+
stops,
|
|
18
|
+
}: LinearGradientConfig) => {
|
|
19
|
+
const renderingContextState = renderingContext.useInject();
|
|
20
|
+
|
|
21
|
+
if (!renderingContextState) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'useLinearGradient must be called inside the body of a component'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const gradient = renderingContextState.ctx2d.createLinearGradient(
|
|
28
|
+
startX,
|
|
29
|
+
startY,
|
|
30
|
+
endX,
|
|
31
|
+
endY
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
stops.forEach(({ offset, color }) => {
|
|
35
|
+
gradient.addColorStop(offset, color);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return gradient;
|
|
39
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useUnreactive } from './use-state.ts';
|
|
2
|
+
|
|
3
|
+
export const useLoop = (callback: () => void) => {
|
|
4
|
+
const unreactive = useUnreactive<{ raf: number | null }>({ raf: null });
|
|
5
|
+
|
|
6
|
+
if (typeof unreactive.raf === 'number') {
|
|
7
|
+
window.cancelAnimationFrame(unreactive.raf);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
unreactive.raf = window.requestAnimationFrame(callback);
|
|
11
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { registerHook } from '../internal/hooks.ts';
|
|
2
|
+
import type { RCXOnMountHook } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
export const useOnMount = (callback: () => void | (() => void)) => {
|
|
5
|
+
const hook: RCXOnMountHook = {
|
|
6
|
+
onMount: () => {
|
|
7
|
+
const onUnmount = callback();
|
|
8
|
+
if (onUnmount) {
|
|
9
|
+
hook.onUnmount = onUnmount;
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
registerHook('useOnMount', hook);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const useOnUnmount = (callback: () => void) => {
|
|
17
|
+
registerHook('useOnUnmount', callback);
|
|
18
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { renderingContext } from '../components/canvas/context.ts';
|
|
2
|
+
import type { RCXColorStop } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
export interface RadialGradientConfig {
|
|
5
|
+
startX: number;
|
|
6
|
+
startY: number;
|
|
7
|
+
startRadius: number;
|
|
8
|
+
endX: number;
|
|
9
|
+
endY: number;
|
|
10
|
+
endRadius: number;
|
|
11
|
+
stops: readonly RCXColorStop[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useRadialGradient = ({
|
|
15
|
+
startX,
|
|
16
|
+
startY,
|
|
17
|
+
startRadius,
|
|
18
|
+
endX,
|
|
19
|
+
endY,
|
|
20
|
+
endRadius,
|
|
21
|
+
stops,
|
|
22
|
+
}: RadialGradientConfig) => {
|
|
23
|
+
const renderingContextState = renderingContext.useInject();
|
|
24
|
+
|
|
25
|
+
if (!renderingContextState) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'useRadialGradient must be called inside the body of a component'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const gradient = renderingContextState.ctx2d.createRadialGradient(
|
|
32
|
+
startX,
|
|
33
|
+
startY,
|
|
34
|
+
startRadius,
|
|
35
|
+
endX,
|
|
36
|
+
endY,
|
|
37
|
+
endRadius
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
stops.forEach(({ offset, color }) => {
|
|
41
|
+
gradient.addColorStop(offset, color);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return gradient;
|
|
45
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { registerHook } from '../internal/hooks.ts';
|
|
2
|
+
import { RCXRenderingContext } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
export const useRenderBeforeChildren = (
|
|
5
|
+
callback: (renderingContext: RCXRenderingContext) => void
|
|
6
|
+
) => {
|
|
7
|
+
registerHook('useRenderBeforeChildren', callback);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const useRenderAfterChildren = (
|
|
11
|
+
callback: (renderingContext: RCXRenderingContext) => void
|
|
12
|
+
) => {
|
|
13
|
+
registerHook('useRenderAfterChildren', callback);
|
|
14
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { registerHook } from '../internal/hooks.ts';
|
|
2
|
+
import { reactive } from '../internal/reactive.ts';
|
|
3
|
+
import type { AnyObject } from '../types.ts';
|
|
4
|
+
|
|
5
|
+
export const useReactive = <T extends AnyObject>(initialState: T) =>
|
|
6
|
+
registerHook('useReactive', reactive(initialState));
|
|
7
|
+
|
|
8
|
+
export const useUnreactive = <T extends AnyObject>(initialState: T) =>
|
|
9
|
+
registerHook('useUnreactive', initialState);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useOnMount } from './use-on.ts';
|
|
2
|
+
import { useReactive } from './use-state.ts';
|
|
3
|
+
|
|
4
|
+
export const useWindowSize = () => {
|
|
5
|
+
const size = useReactive({
|
|
6
|
+
width: window.innerWidth,
|
|
7
|
+
height: window.innerHeight,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
useOnMount(() => {
|
|
11
|
+
const onResize = () => {
|
|
12
|
+
size.width = window.innerWidth;
|
|
13
|
+
size.height = window.innerHeight;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
window.addEventListener('resize', onResize);
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
window.removeEventListener('resize', onResize);
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return size;
|
|
24
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AnyFunction, AnyObject } from '../types.ts';
|
|
2
|
+
|
|
3
|
+
type Handler<T extends AnyObject, K extends keyof T> = (
|
|
4
|
+
data: T[K],
|
|
5
|
+
event: K
|
|
6
|
+
) => void;
|
|
7
|
+
|
|
8
|
+
class Emitter<T extends AnyObject> {
|
|
9
|
+
private handlers: {
|
|
10
|
+
[K in keyof T]?: Handler<T, K>[];
|
|
11
|
+
} = {};
|
|
12
|
+
|
|
13
|
+
public on = <K extends keyof T>(event: K, handler: Handler<T, K>) => {
|
|
14
|
+
this.handlers[event] = this.handlers[event] || [];
|
|
15
|
+
this.handlers[event]?.push(handler);
|
|
16
|
+
|
|
17
|
+
return () => this.off(event, handler);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
public off = <K extends keyof T>(event: K, handler: Handler<T, K>) => {
|
|
21
|
+
const index = this.handlers[event]?.indexOf(handler);
|
|
22
|
+
|
|
23
|
+
if (typeof index === 'number' && index >= 0) {
|
|
24
|
+
this.handlers[event]?.splice(index, 1);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
public emit = <K extends keyof T>(event: K, data: T[K]) => {
|
|
29
|
+
this.handlers[event]?.forEach((handler) => handler(data, event));
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const emitter = new Emitter<{
|
|
34
|
+
render: null;
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
registerState: any;
|
|
37
|
+
registerBeforeChildren: AnyFunction;
|
|
38
|
+
registerAfterChildren: AnyFunction;
|
|
39
|
+
}>();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RCXHook, RCXHookMap } from '../types.ts';
|
|
2
|
+
import { cxGlobal } from './global.ts';
|
|
3
|
+
|
|
4
|
+
export const registerHook = <
|
|
5
|
+
H extends keyof RCXHookMap,
|
|
6
|
+
T extends RCXHookMap[H],
|
|
7
|
+
>(
|
|
8
|
+
hookType: H,
|
|
9
|
+
value: T
|
|
10
|
+
) => {
|
|
11
|
+
const { currentNode, hookIndex } = cxGlobal;
|
|
12
|
+
|
|
13
|
+
if (!currentNode) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`${hookType} must be called inside the body of a component`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const prev = currentNode.hooks[hookIndex];
|
|
20
|
+
|
|
21
|
+
if (!!prev && prev.type !== hookType) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`${hookType} was called at a different time (conditionally, or in a loop)`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const originalValue = prev ? prev.value : value;
|
|
28
|
+
const hook = { type: hookType, value: originalValue } as RCXHook;
|
|
29
|
+
currentNode.hooks[hookIndex] = hook;
|
|
30
|
+
cxGlobal.hookIndex += 1;
|
|
31
|
+
return hook.value as T;
|
|
32
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { reactive } from './reactive.ts';
|
|
2
|
+
|
|
3
|
+
describe('reactive', () => {
|
|
4
|
+
it('wraps an object in a proxy and allows accessing raw values', () => {
|
|
5
|
+
const result = reactive({ foo: 'foo', bar: 123 });
|
|
6
|
+
|
|
7
|
+
expect(typeof result.foo).toBe('string');
|
|
8
|
+
expect(result.foo).toBe('foo');
|
|
9
|
+
expect(result.foo + 'bar').toBe('foobar');
|
|
10
|
+
expect(typeof result.bar).toBe('number');
|
|
11
|
+
expect(result.bar).toBe(123);
|
|
12
|
+
expect(result.bar * 2).toBe(246);
|
|
13
|
+
expect(JSON.stringify(result)).toBe('{"foo":"foo","bar":123}');
|
|
14
|
+
|
|
15
|
+
expect(Object.entries(result)).toEqual([
|
|
16
|
+
['foo', 'foo'],
|
|
17
|
+
['bar', 123],
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnyObject } from '../types.ts';
|
|
2
|
+
import { emitter } from './emitter.ts';
|
|
3
|
+
|
|
4
|
+
export const reactive = <T extends AnyObject>(initialState: T) =>
|
|
5
|
+
new Proxy(initialState, {
|
|
6
|
+
set(target, key, value) {
|
|
7
|
+
Reflect.set(target, key, value);
|
|
8
|
+
emitter.emit('render', null);
|
|
9
|
+
return true;
|
|
10
|
+
},
|
|
11
|
+
get(target, key) {
|
|
12
|
+
const value = Reflect.get(target, key);
|
|
13
|
+
|
|
14
|
+
if (typeof value === 'object' && !!value) {
|
|
15
|
+
return reactive(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return value;
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnyObject,
|
|
3
|
+
RCXComponent,
|
|
4
|
+
RCXElement,
|
|
5
|
+
RCXFragmentProps,
|
|
6
|
+
} from './types.ts';
|
|
7
|
+
|
|
8
|
+
export const jsx = <C extends RCXComponent<P>, P extends AnyObject>(
|
|
9
|
+
type: C,
|
|
10
|
+
props: P
|
|
11
|
+
): RCXElement<C, P> => ({
|
|
12
|
+
type,
|
|
13
|
+
props,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const jsxs = jsx;
|
|
17
|
+
|
|
18
|
+
export const Fragment: RCXComponent<RCXFragmentProps> = ({ children }) =>
|
|
19
|
+
children;
|
|
20
|
+
|
|
21
|
+
Fragment.displayName = 'Fragment';
|