@angadie/chittie-react 0.1.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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Asyncdot Engineering
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ Portions of this software are vendored from MIT-licensed projects and retain
16
+ their original copyright notices; see VENDOR.md.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # @angadie/chittie-react
2
+
3
+ Pure JSX receipt authoring → ESC/POS bytes. **RN-safe**: no `react-dom`, no HTML host elements — components render straight onto the [`chittie-core`](../chittie-core) builder, so the same `<Printer>` tree works on web and React Native.
4
+
5
+ > Most apps install [`@angadie/chittie`](../chittie) (which re-exports this). Install this directly only if you don't want the builder re-export.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @angadie/chittie-react react
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```tsx
16
+ import { Printer, Text, Row, Line, Cut, render } from '@angadie/chittie-react';
17
+
18
+ const bytes = render(
19
+ <Printer width={48}>
20
+ <Text align="center" bold size={{ width: 2, height: 2 }}>SHOP</Text>
21
+ <Line />
22
+ <Row left="Item" right="Rs. 100" />
23
+ <Cut />
24
+ </Printer>
25
+ );
26
+ // bytes: Uint8Array — hand to any transport
27
+ ```
28
+
29
+ `render(element, options?)` returns a `Uint8Array`. It is synchronous and pure.
30
+
31
+ ## Components
32
+
33
+ | Component | Props | Emits |
34
+ |---|---|---|
35
+ | `<Printer>` | `width` (columns, default 48) | the root; sets line width |
36
+ | `<Text>` | `align`, `bold`, `underline`, `invert`, `size` ({width,height} multipliers), `inline` | text (or image — see below) + newline |
37
+ | `<Row>` | `left`, `right` | a two-column justified row |
38
+ | `<Line>` | — | a horizontal rule |
39
+ | `<Br>` | `lines` | blank line(s) |
40
+ | `<Cut>` | `partial` | paper cut |
41
+ | `<Cashdraw>` | `device` | cash-drawer kick pulse |
42
+ | `<Barcode>` | `value`, `symbology`, `height` | a barcode |
43
+ | `<QRCode>` | `value`, `size`, `model` | a QR code |
44
+ | `<Image>` | `image` (ImageData), `align`, `dither`, `threshold`, `width`, `height` | a raster image (logo, etc.) |
45
+
46
+ ### `<Image>` — logos and bitmaps
47
+
48
+ Pass `ImageData` (from a `<canvas>`, a decoded PNG, or a rasterizer). Dimensions are auto-padded to multiples of 8 (the ESC/POS raster requirement), so any size works. Use `dither="threshold"` for crisp line-art/logos, or `floydsteinberg`/`atkinson` for photos.
49
+
50
+ ```tsx
51
+ const logo = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height);
52
+ render(
53
+ <Printer width={48}>
54
+ <Image image={logo} align="center" dither="threshold" />
55
+ <Text align="center" bold>ARTISAN HAUS</Text>
56
+ </Printer>
57
+ );
58
+ ```
59
+
60
+ User-defined wrapper components are supported — `render` walks function components and fragments.
61
+
62
+ ## Custom components
63
+
64
+ Compose your own components — they're plain functions returning chittie elements. `render` invokes them and walks their output, so `.map()`, fragments, and conditionals all work:
65
+
66
+ ```tsx
67
+ const LineItem = ({ name, qty, price }: Item) => (
68
+ <Row left={`${qty}x ${name}`} right={`Rs. ${qty * price}`} />
69
+ );
70
+
71
+ function Receipt({ items }: { items: Item[] }) {
72
+ return (
73
+ <Printer width={48}>
74
+ <Text align="center" bold>MY SHOP</Text>
75
+ <Line />
76
+ {items.map((it) => <LineItem key={it.id} {...it} />)}
77
+ {items.length === 0 && <Text align="center">No items</Text>}
78
+ <Cut />
79
+ </Printer>
80
+ );
81
+ }
82
+
83
+ // a whole receipt can be ONE component — render resolves it down to <Printer>
84
+ render(<Receipt items={cart} />);
85
+ ```
86
+
87
+ **Constraints** (it's a pure renderer, not the React reconciler):
88
+ - Components must be pure `props → elements`. **No hooks** (`useState`/`useEffect`/`useContext`) and no `react-dom` — there's no component tree, state, or lifecycle, by design (a receipt is a one-shot render).
89
+ - `<Text>` accepts only **text** (strings/numbers). Nesting a component (`<Text><Row/></Text>`) **throws a clear error** — put `<Row>`, `<Image>`, etc. as siblings. (Fragments wrapping text are fine.)
90
+
91
+ ## Non-Latin scripts (Sinhala / Tamil / …)
92
+
93
+ `<Text>` content that a code page can't represent is auto-rasterized **when you pass a rasterizer**; otherwise `render` throws (never a silent `?`). See [`@angadie/chittie-text`](../chittie-text).
94
+
95
+ ```tsx
96
+ render(<Printer><Text>ආයුබෝවන්</Text></Printer>, {
97
+ rasterizer: myRasterizer, // TextRasterizer
98
+ codepage: 'cp437', // what counts as "encodable as text"
99
+ });
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT.
@@ -0,0 +1,132 @@
1
+ import { ReactElement, ReactNode } from "react";
2
+ import { Codepage, Codepage as Codepage$1, RasterOptions, TextRasterizer, TextRasterizer as TextRasterizer$1 } from "@angadie/chittie-text";
3
+ import ReceiptPrinterEncoder, { BarcodeSymbology, DitherAlgorithm } from "@angadie/chittie-core";
4
+
5
+ //#region src/components.d.ts
6
+ type Alignment = 'left' | 'center' | 'right';
7
+ /** Character magnification (width/height multipliers, e.g. 2 = double). */
8
+ type TextScale = {
9
+ width: number;
10
+ height: number;
11
+ };
12
+ /**
13
+ * Pure recursive text extraction — no react-dom, no DOM. Strings/numbers pass
14
+ * through; fragments are transparent. A component element is a mistake (it would
15
+ * be silently dropped — its print() never runs from here), so we throw loudly.
16
+ */
17
+ declare function toText(node: ReactNode): string;
18
+ interface PrinterProps {
19
+ /** Printer model passed to the encoder, e.g. 'epson'. */
20
+ type?: string;
21
+ /** Characters per line (default 48 / 80mm). */
22
+ width?: number;
23
+ children?: ReactNode;
24
+ }
25
+ /** Root element — render() reads its props; it has no print() of its own. */
26
+ declare const Printer: (_props: PrinterProps) => null;
27
+ interface TextProps {
28
+ align?: Alignment;
29
+ bold?: boolean;
30
+ underline?: boolean | number;
31
+ invert?: boolean;
32
+ size?: TextScale;
33
+ /** If true, don't append a newline after the text. */
34
+ inline?: boolean;
35
+ children?: ReactNode;
36
+ }
37
+ declare const Text: (_props: TextProps) => null;
38
+ interface RowProps {
39
+ left?: ReactNode;
40
+ right?: ReactNode;
41
+ children?: ReactNode;
42
+ }
43
+ declare const Row: (_props: RowProps) => null;
44
+ declare const Line: (_props: {
45
+ children?: ReactNode;
46
+ }) => null;
47
+ interface BrProps {
48
+ lines?: number;
49
+ children?: ReactNode;
50
+ }
51
+ declare const Br: (_props: BrProps) => null;
52
+ interface CutProps {
53
+ partial?: boolean;
54
+ children?: ReactNode;
55
+ }
56
+ declare const Cut: (_props: CutProps) => null;
57
+ interface CashdrawProps {
58
+ device?: number;
59
+ children?: ReactNode;
60
+ }
61
+ declare const Cashdraw: (_props: CashdrawProps) => null;
62
+ interface BarcodeProps {
63
+ value: string;
64
+ symbology?: BarcodeSymbology | number;
65
+ height?: number;
66
+ children?: ReactNode;
67
+ }
68
+ declare const Barcode: (_props: BarcodeProps) => null;
69
+ interface QRCodeProps {
70
+ value: string;
71
+ size?: number;
72
+ model?: number;
73
+ children?: ReactNode;
74
+ }
75
+ declare const QRCode: (_props: QRCodeProps) => null;
76
+ interface ImageProps {
77
+ /** Pixel data (from a <canvas>, decoded PNG, or a rasterizer). */
78
+ image: ImageData;
79
+ /** Output width/height in dots; default the image's own (padded to /8). */
80
+ width?: number;
81
+ height?: number;
82
+ align?: Alignment;
83
+ /** Dithering: 'threshold' (crisp line-art/logos) | 'bayer' | 'floydsteinberg' | 'atkinson' (photos). */
84
+ dither?: DitherAlgorithm;
85
+ threshold?: number;
86
+ children?: ReactNode;
87
+ }
88
+ declare const Image: (_props: ImageProps) => null;
89
+ //#endregion
90
+ //#region src/render.d.ts
91
+ interface RenderOptions {
92
+ /** Characters per line; overridden by <Printer width>. */
93
+ columns?: number;
94
+ /** Printable width in dots; defaults to columns × 12 (203 DPI, font A). */
95
+ dotWidth?: number;
96
+ /** Supply to print non-encodable scripts (Sinhala/Tamil/…) as images. */
97
+ rasterizer?: TextRasterizer$1;
98
+ /** Code page used to decide what's encodable as text (default cp437). */
99
+ codepage?: Codepage$1;
100
+ }
101
+ /** Common thermal printer profiles: characters/line + printable dot width (203 DPI). */
102
+ declare const PRINTER_PROFILES: {
103
+ readonly '58mm': {
104
+ readonly columns: 32;
105
+ readonly dotWidth: 384;
106
+ };
107
+ readonly '80mm': {
108
+ readonly columns: 48;
109
+ readonly dotWidth: 576;
110
+ };
111
+ };
112
+ /**
113
+ * Render a <Printer> element tree to ESC/POS bytes by driving the vendored
114
+ * builder. Pure: no react-dom, no DOM host elements.
115
+ */
116
+ declare function render(element: ReactElement, options?: RenderOptions): Uint8Array;
117
+ //#endregion
118
+ //#region src/printable.d.ts
119
+ /** The vendored builder instance the components drive. */
120
+ type Encoder = InstanceType<typeof ReceiptPrinterEncoder>;
121
+ /** Render-time context threaded to every component's print(). */
122
+ interface RenderContext {
123
+ columns: number;
124
+ /** Printable width in dots (e.g. 384 for 58mm, 576 for 80mm) — used when rasterizing. */
125
+ dotWidth: number;
126
+ /** When set, <Text>/<Row> with non-encodable scripts (Sinhala/Tamil/…) is rasterized. */
127
+ rasterizer?: TextRasterizer$1;
128
+ /** Code page used to decide what's encodable as text (default cp437). */
129
+ codepage?: Codepage$1;
130
+ }
131
+ //#endregion
132
+ export { type Alignment, Barcode, type BarcodeProps, Br, type BrProps, Cashdraw, type CashdrawProps, type Codepage, Cut, type CutProps, type Encoder, Image, type ImageProps, Line, PRINTER_PROFILES, Printer, type PrinterProps, QRCode, type QRCodeProps, type RasterOptions, type RenderContext, type RenderOptions, Row, type RowProps, Text, type TextProps, type TextRasterizer, type TextScale, render, toText };
package/dist/index.mjs ADDED
@@ -0,0 +1,157 @@
1
+ import { Children, Fragment, isValidElement } from "react";
2
+ import { needsRaster, padTo8, rasterizeRow, smartText } from "@angadie/chittie-text";
3
+ import ReceiptPrinterEncoder from "@angadie/chittie-core";
4
+ //#region src/components.ts
5
+ /**
6
+ * Pure recursive text extraction — no react-dom, no DOM. Strings/numbers pass
7
+ * through; fragments are transparent. A component element is a mistake (it would
8
+ * be silently dropped — its print() never runs from here), so we throw loudly.
9
+ */
10
+ function toText(node) {
11
+ if (node == null || typeof node === "boolean") return "";
12
+ if (typeof node === "string") return node;
13
+ if (typeof node === "number" || typeof node === "bigint") return String(node);
14
+ if (Array.isArray(node)) return node.map(toText).join("");
15
+ if (isValidElement(node)) {
16
+ if (node.type === Fragment) return toText(node.props.children);
17
+ throw new Error("chittie: <Text> (and <Row>) accept only string/number text, not components. Put <Row>, <Image>, etc. as siblings — not nested inside <Text>.");
18
+ }
19
+ return "";
20
+ }
21
+ const printable = (fn) => {
22
+ const comp = (_props) => null;
23
+ comp.print = fn;
24
+ return comp;
25
+ };
26
+ /** Root element — render() reads its props; it has no print() of its own. */
27
+ const Printer = (_props) => null;
28
+ const Text = printable((e, p, ctx) => {
29
+ if (p.align) e.align(p.align);
30
+ if (p.bold) e.bold(true);
31
+ if (p.underline) e.underline(p.underline);
32
+ if (p.invert) e.invert(true);
33
+ if (p.size) e.size(p.size.width, p.size.height);
34
+ smartText(e, toText(p.children), {
35
+ rasterizer: ctx.rasterizer,
36
+ codepage: ctx.codepage,
37
+ raster: {
38
+ bold: p.bold,
39
+ fontSize: p.size ? p.size.height * 24 : void 0,
40
+ maxWidth: ctx.dotWidth
41
+ }
42
+ });
43
+ if (!p.inline) e.newline();
44
+ if (p.size) e.size(1, 1);
45
+ if (p.invert) e.invert(false);
46
+ if (p.underline) e.underline(false);
47
+ if (p.bold) e.bold(false);
48
+ if (p.align) e.align("left");
49
+ });
50
+ const Row = printable((e, p, ctx) => {
51
+ const left = toText(p.left);
52
+ const right = toText(p.right);
53
+ if (needsRaster(left, ctx.codepage) || needsRaster(right, ctx.codepage)) {
54
+ if (!ctx.rasterizer) throw new Error("chittie: <Row> contains non-encodable text (e.g. Sinhala/Tamil). Pass a rasterizer to render(), or use code-page text.");
55
+ const img = rasterizeRow(ctx.rasterizer, left, right, { dotWidth: ctx.dotWidth });
56
+ e.image(img, img.width, img.height);
57
+ return;
58
+ }
59
+ const rightW = Math.min(right.length, ctx.columns);
60
+ const leftW = Math.max(0, ctx.columns - rightW);
61
+ e.table([{
62
+ width: leftW,
63
+ align: "left"
64
+ }, {
65
+ width: rightW,
66
+ align: "right"
67
+ }], [[left, right]]);
68
+ });
69
+ const Line = printable((e) => {
70
+ e.rule();
71
+ });
72
+ const Br = printable((e, p) => {
73
+ e.newline(p.lines ?? 1);
74
+ });
75
+ const Cut = printable((e, p) => {
76
+ e.cut(p.partial ? "partial" : "full");
77
+ });
78
+ const Cashdraw = printable((e, p) => {
79
+ e.pulse(p.device);
80
+ });
81
+ const Barcode = printable((e, p) => {
82
+ e.barcode(p.value, p.symbology ?? "code128", p.height);
83
+ });
84
+ const QRCode = printable((e, p) => {
85
+ e.qrcode(p.value, p.model, p.size);
86
+ });
87
+ const Image = printable((e, p) => {
88
+ const img = padTo8(p.image);
89
+ if (p.align) e.align(p.align);
90
+ e.image(img, p.width ?? img.width, p.height ?? img.height, p.dither, p.threshold);
91
+ if (p.align) e.align("left");
92
+ });
93
+ //#endregion
94
+ //#region src/printable.ts
95
+ function isPrintable(type) {
96
+ return (typeof type === "function" || typeof type === "object") && type !== null && typeof type.print === "function";
97
+ }
98
+ //#endregion
99
+ //#region src/render.ts
100
+ /**
101
+ * Resolve a custom function-component root down to the <Printer> element it
102
+ * returns, so a whole receipt can be authored as one component:
103
+ * `render(<MyReceipt items={…} />)`. Stops at <Printer> or any printable.
104
+ */
105
+ function resolveRoot(element) {
106
+ let current = element;
107
+ for (let i = 0; i < 100; i++) {
108
+ if (!isValidElement(current)) break;
109
+ const type = current.type;
110
+ if (type === Printer || isPrintable(type) || typeof type !== "function") break;
111
+ current = type(current.props);
112
+ }
113
+ return isValidElement(current) ? current : element;
114
+ }
115
+ /** Common thermal printer profiles: characters/line + printable dot width (203 DPI). */
116
+ const PRINTER_PROFILES = {
117
+ "58mm": {
118
+ columns: 32,
119
+ dotWidth: 384
120
+ },
121
+ "80mm": {
122
+ columns: 48,
123
+ dotWidth: 576
124
+ }
125
+ };
126
+ /**
127
+ * Render a <Printer> element tree to ESC/POS bytes by driving the vendored
128
+ * builder. Pure: no react-dom, no DOM host elements.
129
+ */
130
+ function render(element, options = {}) {
131
+ const props = resolveRoot(element).props ?? {};
132
+ const columns = props.width ?? options.columns ?? 48;
133
+ const dotWidth = options.dotWidth ?? columns * 12;
134
+ const encoder = new ReceiptPrinterEncoder({ columns });
135
+ encoder.initialize();
136
+ walk(props.children, encoder, {
137
+ columns,
138
+ dotWidth,
139
+ rasterizer: options.rasterizer,
140
+ codepage: options.codepage
141
+ });
142
+ return encoder.encode();
143
+ }
144
+ function walk(node, encoder, ctx) {
145
+ for (const child of Children.toArray(node)) {
146
+ if (!isValidElement(child)) continue;
147
+ const type = child.type;
148
+ if (isPrintable(type)) {
149
+ type.print(encoder, child.props ?? {}, ctx);
150
+ continue;
151
+ }
152
+ if (typeof type === "function") walk(type(child.props), encoder, ctx);
153
+ else walk(child.props.children, encoder, ctx);
154
+ }
155
+ }
156
+ //#endregion
157
+ export { Barcode, Br, Cashdraw, Cut, Image, Line, PRINTER_PROFILES, Printer, QRCode, Row, Text, render, toText };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@angadie/chittie-react",
3
+ "version": "0.1.0",
4
+ "description": "Pure JSX receipt authoring → ESC/POS bytes. RN-safe (no react-dom, no HTML host elements). Renders onto chittie-core.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "types": "./dist/index.d.mts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "import": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "dependencies": {
18
+ "@angadie/chittie-core": "0.1.0",
19
+ "@angadie/chittie-text": "0.1.0"
20
+ },
21
+ "peerDependencies": {
22
+ "react": "^18 || ^19"
23
+ },
24
+ "devDependencies": {
25
+ "@canvas/image-data": "^1.1.0"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "build": "tsdown",
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "tsx spikes/jsx.spike.tsx"
34
+ }
35
+ }