Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions examples/hello-world-react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@examples/hello-world-react",
"private": true,
"type": "module",
"scripts": {
"start": "tsx src/index.tsx",
"dev": "tsx watch src/index.tsx"
},
"dependencies": {
"@bomb.sh/tty": "^0.7.0",
"@clack/ui": "workspace:*",
"@clack/ui-react": "workspace:*",
"react": "^19.2.0"
},
"devDependencies": {
"@types/node": "^22.20.0",
"@types/react": "^19.2.0",
"tsx": "^4.19.0"
}
}
44 changes: 44 additions & 0 deletions examples/hello-world-react/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// oxlint-disable bombshell-dev/exported-function-async -- React components render synchronously.
import { fixed, grow, percent } from '@bomb.sh/tty';
import { rgba } from '@clack/ui-react';
import { type ReactElement, useState } from 'react';

/** Render the interactive hello-world example. */
export function HelloWorld(): ReactElement {
const [say, setSay] = useState('Hello');
const [to, setTo] = useState('World');

return (
<box
layout={{
direction: 'ttb',
gap: 1,
padding: { top: 1, bottom: 1, left: 2, right: 2 },
width: fixed(40),
}}
border={{ color: blue, top: 1, right: 1, bottom: 1, left: 1 }}
>
<text color={cyan}>
{say}, {to}!
</text>
<box layout={{ direction: 'ttb', width: grow() }}>
<box layout={{ direction: 'ltr', gap: 1, width: grow() }}>
<box layout={{ width: percent(0.3) }}>
<text color={gray}>say:</text>
</box>
<box layout={{ width: percent(0.3) }}>
<text color={gray}>to:</text>
</box>
</box>
<box layout={{ direction: 'ltr', gap: 1, width: grow() }}>
<input onInput={(event) => setSay(event.value)} />
<input onInput={(event) => setTo(event.value)} />
</box>
</box>
</box>
);
}

const blue = rgba(0, 0, 238);
const cyan = rgba(0, 205, 205);
const gray = rgba(127, 127, 127);
9 changes: 9 additions & 0 deletions examples/hello-world-react/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { stdin, stdout } from 'node:process';
import { createUI } from '@clack/ui';
import { createRoot } from '@clack/ui-react';
import { HelloWorld } from './app.tsx';

await using ui = await createUI({ input: stdin, output: stdout });
const root = createRoot(ui.host.element);
root.render(<HelloWorld />);
await ui.main();
12 changes: 12 additions & 0 deletions examples/hello-world-react/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "@bomb.sh/tools/tsconfig.json",
"compilerOptions": {
"composite": false,
"declaration": false,
"declarationMap": false,
"jsx": "react-jsx",
"jsxImportSource": "@clack/ui-react",
"noEmit": true,
"types": ["node"]
}
}
80 changes: 80 additions & 0 deletions packages/ui-react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"name": "@clack/ui-react",
"version": "0.0.0",
"keywords": [
"bombshell",
"cli",
"react"
],
"homepage": "https://bomb.sh/docs/ui",
"license": "MIT",
"author": {
"name": "Bombshell",
"email": "oss@bomb.sh",
"url": "https://bomb.sh"
},
"repository": {
"type": "git",
"url": "git+https://github.com/bombshell-dev/ui.git"
},
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
},
"./jsx-runtime": {
"types": "./dist/jsx-runtime.d.mts",
"import": "./dist/jsx-runtime.mjs"
},
"./jsx-dev-runtime": {
"types": "./dist/jsx-dev-runtime.d.mts",
"import": "./dist/jsx-dev-runtime.mjs"
},
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "bsh dev",
"build": "bsh build --dts",
"format": "bsh format",
"lint": "bsh lint",
"test": "bsh test"
},
"dependencies": {
"@clack/ui": "workspace:*",
"react-reconciler": "^0.33.0"
},
"devDependencies": {
"@bomb.sh/tools": "^0.5.4",
"@types/node": "^22.20.0",
"@types/react": "^19.2.0",
"@types/react-reconciler": "^0.33.0",
"react": "^19.2.0",
"vitest": "^4.1.9"
},
"peerDependencies": {
"react": "^19.2.0"
},
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "^10.7.0",
"onFail": "error"
},
"runtime": {
"name": "node",
"version": "^22.14.0",
"onFail": "error"
}
},
"knip": {
"entry": [
"src/index.ts",
"src/jsx-runtime.ts",
"src/jsx-dev-runtime.ts"
]
}
}
10 changes: 10 additions & 0 deletions packages/ui-react/src/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { type FunctionComponent, createElement } from 'react';
import type { BoxProps, TextProps } from './elements.ts';

export type { BoxProps, TextProps } from './elements.ts';

/** A layout container backed by the Host's `box` element. */
export const Box: FunctionComponent<BoxProps> = (props) => createElement('box', props);

/** A styled text scope backed by the Host's `text` element. */
export const Text: FunctionComponent<TextProps> = (props) => createElement('text', props);
47 changes: 47 additions & 0 deletions packages/ui-react/src/elements.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { HostElements as CoreHostElements, InputEvent } from '@clack/ui';
import type { ReactNode } from 'react';
import { describe, expectTypeOf, test } from 'vitest';
import type { JSX } from './jsx-runtime.ts';

interface ReactProbeEvent {
readonly value: number;
}

interface ReactProbeProps {
count: number;
onprobe?: (event: ReactProbeEvent) => void;
}

declare module '@clack/ui/elements' {
interface HostElements {
'@test/react-probe': ReactProbeProps;
}
}

describe('React host elements', () => {
test('maps box and text properties from the core registry', () => {
expectTypeOf<JSX.IntrinsicElements['box']['layout']>().toEqualTypeOf<
CoreHostElements['box']['layout']
>();
expectTypeOf<JSX.IntrinsicElements['text']['color']>().toEqualTypeOf<
CoreHostElements['text']['color']
>();
expectTypeOf<JSX.IntrinsicElements['box']['children']>().toEqualTypeOf<ReactNode | undefined>();
});

test('maps lowercase host event properties to React casing', () => {
expectTypeOf<NonNullable<JSX.IntrinsicElements['input']['onInput']>>()
.parameter(0)
.toEqualTypeOf<InputEvent>();
expectTypeOf<
'oninput' extends keyof JSX.IntrinsicElements['input'] ? true : false
>().toEqualTypeOf<false>();
});

test('derives custom elements from core registry augmentation', () => {
expectTypeOf<JSX.IntrinsicElements['@test/react-probe']['count']>().toEqualTypeOf<number>();
expectTypeOf<NonNullable<JSX.IntrinsicElements['@test/react-probe']['onProbe']>>()
.parameter(0)
.toEqualTypeOf<ReactProbeEvent>();
});
});
23 changes: 23 additions & 0 deletions packages/ui-react/src/elements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { HostElements as CoreHostElements } from '@clack/ui';
import type { HostElement } from '@clack/ui/elements';
import type { Key, ReactNode, Ref } from 'react';

type ReactProperties<Properties> = {
[Name in keyof Properties as Name extends string
? Name extends `on${infer Type}`
? `on${Capitalize<Type>}`
: Name
: Name]: Properties[Name];
} & {
children?: ReactNode;
key?: Key | null;
ref?: Ref<HostElement>;
};

export type HostElements = {
[Name in keyof CoreHostElements]: ReactProperties<CoreHostElements[Name]>;
};

export type BoxProps = HostElements['box'];
export type InputProps = HostElements['input'];
export type TextProps = HostElements['text'];
4 changes: 4 additions & 0 deletions packages/ui-react/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { rgba } from '@clack/ui';
export { Box, Text } from './components.ts';
export type { BoxProps, HostElements, InputProps, TextProps } from './elements.ts';
export { createRoot, type Root, type RootOptions } from './root.ts';
20 changes: 20 additions & 0 deletions packages/ui-react/src/jsx-dev-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type * as React from 'react';
import type { HostElements } from './index.ts';

export { Fragment, jsxDEV } from 'react/jsx-dev-runtime';

export declare namespace JSX {
type ElementType = React.JSX.ElementType;
interface Element extends React.JSX.Element {}
interface ElementClass extends React.JSX.ElementClass {}
interface ElementAttributesProperty extends React.JSX.ElementAttributesProperty {}
interface ElementChildrenAttribute extends React.JSX.ElementChildrenAttribute {}
type LibraryManagedAttributes<Component, Props> = React.JSX.LibraryManagedAttributes<
Component,
Props
>;
interface IntrinsicAttributes extends React.JSX.IntrinsicAttributes {}
interface IntrinsicClassAttributes<Instance> extends React.JSX
.IntrinsicClassAttributes<Instance> {}
interface IntrinsicElements extends HostElements {}
}
20 changes: 20 additions & 0 deletions packages/ui-react/src/jsx-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type * as React from 'react';
import type { HostElements } from './index.ts';

export { Fragment, jsx, jsxs } from 'react/jsx-runtime';

export declare namespace JSX {
type ElementType = React.JSX.ElementType;
interface Element extends React.JSX.Element {}
interface ElementClass extends React.JSX.ElementClass {}
interface ElementAttributesProperty extends React.JSX.ElementAttributesProperty {}
interface ElementChildrenAttribute extends React.JSX.ElementChildrenAttribute {}
type LibraryManagedAttributes<Component, Props> = React.JSX.LibraryManagedAttributes<
Component,
Props
>;
interface IntrinsicAttributes extends React.JSX.IntrinsicAttributes {}
interface IntrinsicClassAttributes<Instance> extends React.JSX
.IntrinsicClassAttributes<Instance> {}
type IntrinsicElements = HostElements;
}
Loading
Loading