b44ui 0.2.6 → 0.3.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/core.tsx +89 -0
- package/lib.tsx +123 -0
- package/package.json +14 -36
- package/readme.md +10 -56
- package/server.tsx +50 -0
- package/showcase.tsx +126 -0
- package/tsconfig.json +14 -0
- package/dist/example.d.ts +0 -1
- package/dist/example.js +0 -9
- package/dist/index.d.ts +0 -1800
- package/dist/index.js +0 -206
- package/dist/styles.css +0 -898
- package/dist/vite.config.d.ts +0 -2
- package/dist/vite.config.js +0 -5
- package/index.js +0 -3
- package/tailwind.css +0 -3
package/core.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React, { useState, type CSSProperties, type ReactNode, type Ref } from "react"
|
|
2
|
+
|
|
3
|
+
export type Color = 'purple'
|
|
4
|
+
|
|
5
|
+
const units = {
|
|
6
|
+
p: 'padding', m: 'margin', wd: 'width', ht: 'height',
|
|
7
|
+
minHt: 'minHeight', maxHt: 'maxHeight', minWd: 'minWidth', maxWd: 'maxWidth'
|
|
8
|
+
} as const
|
|
9
|
+
|
|
10
|
+
const passthrough = [
|
|
11
|
+
'font', 'fontSize', 'fontFamily', 'fontWeight', 'lineHeight',
|
|
12
|
+
'border', 'borderLeft', 'borderTop', 'borderRadius',
|
|
13
|
+
'background', 'color', 'opacity',
|
|
14
|
+
'gap', 'cursor', 'overflow', 'overflowX', 'overflowY',
|
|
15
|
+
'transition', 'userSelect', 'resize',
|
|
16
|
+
'position', 'inset', 'top', 'bottom', 'left', 'right', 'zIndex',
|
|
17
|
+
'alignSelf', 'flex', 'flexDirection', 'outline',
|
|
18
|
+
'display', 'gridTemplateColumns',
|
|
19
|
+
] as const
|
|
20
|
+
|
|
21
|
+
type Passthrough = typeof passthrough[number]
|
|
22
|
+
|
|
23
|
+
// props that are consumed by Core and must NOT be forwarded to the DOM
|
|
24
|
+
const consumed = new Set(['children', 'style', 'ref', 'contentEditable', 'click', 'hover', 'input', 'href', 'grow', 'row', 'col', 'align', 'textAlign'])
|
|
25
|
+
|
|
26
|
+
export type Props = {
|
|
27
|
+
children?: ReactNode
|
|
28
|
+
ref?: Ref<HTMLDivElement>
|
|
29
|
+
contentEditable?: boolean
|
|
30
|
+
click?: (e: MouseEvent) => void
|
|
31
|
+
hover?: Partial<Record<Passthrough, string | number>>
|
|
32
|
+
input?: (v: string) => void
|
|
33
|
+
href?: string
|
|
34
|
+
grow?: boolean
|
|
35
|
+
row?: boolean
|
|
36
|
+
col?: boolean
|
|
37
|
+
align?: string
|
|
38
|
+
[key: string]: unknown
|
|
39
|
+
} & { [u in keyof typeof units]?: string | number }
|
|
40
|
+
& { [p in Passthrough]?: string | number }
|
|
41
|
+
|
|
42
|
+
const maybePx = (v: string | number) => typeof v == 'number' ? `${v}px` : v
|
|
43
|
+
|
|
44
|
+
export const Core = (props: Props) => {
|
|
45
|
+
const [isHover, setIsHover] = useState(false)
|
|
46
|
+
|
|
47
|
+
const style: CSSProperties = { ...(props.style as CSSProperties ?? {}) }
|
|
48
|
+
const rest: Record<string, unknown> = {}
|
|
49
|
+
|
|
50
|
+
if (props.contentEditable) rest.suppressContentEditableWarning = true
|
|
51
|
+
|
|
52
|
+
rest.onClick = (e: MouseEvent) => {
|
|
53
|
+
if (props.click) props.click(e)
|
|
54
|
+
if (props.href) window.location.href = props.href
|
|
55
|
+
}
|
|
56
|
+
rest.onInput = (e: Event) => {
|
|
57
|
+
if (props.input) props.input((e.currentTarget as HTMLElement).textContent ?? '')
|
|
58
|
+
}
|
|
59
|
+
rest.onMouseEnter = () => setIsHover(true)
|
|
60
|
+
rest.onMouseLeave = () => setIsHover(false)
|
|
61
|
+
|
|
62
|
+
for (const key in props) {
|
|
63
|
+
if (consumed.has(key)) continue
|
|
64
|
+
const keyu = key as keyof typeof units
|
|
65
|
+
const keyp = key as Passthrough
|
|
66
|
+
if (keyu in units) style[units[keyu]] = maybePx(props[keyu] as string | number)
|
|
67
|
+
else if (passthrough.includes(keyp)) style[keyp as keyof CSSProperties] = props[keyp] as never
|
|
68
|
+
else rest[key] = props[key]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (props.grow) style.flex = 1, style.minHeight = 0
|
|
72
|
+
if (props.row) style.display = 'flex', style.flexDirection = 'row'
|
|
73
|
+
if (props.col) style.display = 'flex', style.flexDirection = 'column'
|
|
74
|
+
|
|
75
|
+
for (const word of (props.align ?? '').split(' ')) {
|
|
76
|
+
if (word === 'between') style.justifyContent = 'space-between'
|
|
77
|
+
const x = props.row ? 'justifyContent' : 'alignItems', y = props.row ? 'alignItems' : 'justifyContent'
|
|
78
|
+
if (word === 'left') style[x] = 'flex-start'
|
|
79
|
+
if (word === 'center') style[x] = 'center'
|
|
80
|
+
if (word === 'right') style[x] = 'flex-end'
|
|
81
|
+
if (word === 'top') style[y] = 'flex-start'
|
|
82
|
+
if (word === 'mid') style[y] = 'center'
|
|
83
|
+
if (word === 'bot') style[y] = 'flex-end'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isHover && props.hover) Object.assign(style, props.hover)
|
|
87
|
+
|
|
88
|
+
return <div style={style} {...rest}>{props.children as ReactNode}</div>
|
|
89
|
+
}
|
package/lib.tsx
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Core, type Props } from "./core"
|
|
3
|
+
import { useEffect, useRef, type ReactNode } from "react"
|
|
4
|
+
import Markdown from 'marked-react'
|
|
5
|
+
import { Prism } from 'react-syntax-highlighter'
|
|
6
|
+
|
|
7
|
+
export const D = (props: Props) => <Core gap={8} {...props} />
|
|
8
|
+
|
|
9
|
+
export const A = (props: Props & { href: string }) => <D {...props} />
|
|
10
|
+
|
|
11
|
+
export const App = (props: Props) =>
|
|
12
|
+
<D col wd='100vw' p={32} gap={16} minHt='100vh' background='#111' color='#eee' fontFamily='Inter, sans-serif' align='center mid' {...props}>{props.children}</D>
|
|
13
|
+
|
|
14
|
+
export const Card = (props: Props) =>
|
|
15
|
+
<D col borderRadius={4} border='1px solid #333' background='#222' gap={16} p={16} {...props} />
|
|
16
|
+
|
|
17
|
+
export const Btn = ({ purple, sm, ...props }: Props & { purple?: boolean, sm?: boolean }) => {
|
|
18
|
+
if (sm) props.gap = 6, props.p = '4px 12px', props.fontSize = 13
|
|
19
|
+
else props.gap = 8, props.p = '8px 16px', props.fontSize = 14
|
|
20
|
+
props.border = '1px solid ' + (purple ? '#63a' : '#444')
|
|
21
|
+
props.background = purple ? '#63a' : '#333'
|
|
22
|
+
props.hover = { background: purple ? '#74b' : '#444', border: '1px solid ' + (purple ? '#74b' : '#444') } as Props
|
|
23
|
+
return <D row borderRadius={4} cursor='pointer' align='center mid' transition='background 0.15s, border-color 0.15s' {...props} />
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const Chip = ({ ...props }: Props) =>
|
|
27
|
+
<D row align='center mid' p='4px 10px' borderRadius={999}
|
|
28
|
+
border='1px solid #3f3f46' background='#222' fontSize={13}
|
|
29
|
+
cursor='pointer' userSelect='none' transition='background 0.15s'
|
|
30
|
+
hover={{ background: '#333' } as Props} {...props} />
|
|
31
|
+
|
|
32
|
+
export const Code = ({ lang, children }: Props & { lang: string }) =>
|
|
33
|
+
<Prism language={lang}>{children as string}</Prism>
|
|
34
|
+
|
|
35
|
+
export const Dropzone = ({ onFiles, accept, multiple, dashed, ...props }:
|
|
36
|
+
Props & { onFiles?: (f: File[]) => void, accept?: string, multiple?: boolean, dashed?: boolean }) => {
|
|
37
|
+
const ref = useRef<HTMLInputElement>(null)
|
|
38
|
+
const push = (l: FileList | null) => { if (l?.length) onFiles?.(Array.from(l)) }
|
|
39
|
+
return <D cursor='pointer' transition='border-color 0.2s' border={dashed ? '2px dashed #3f3f46' : undefined} borderRadius={dashed ? 6 : undefined}
|
|
40
|
+
hover={dashed ? { border: '2px dashed #6633aa' } : {}} click={() => ref.current?.click()} {...props}>
|
|
41
|
+
<input ref={ref} type='file' hidden accept={accept} multiple={multiple} onChange={e => push(e.target.files)} />
|
|
42
|
+
{props.children}
|
|
43
|
+
</D>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Grid = ({ cols, ...props }: Props & { cols?: number }) =>
|
|
47
|
+
<D display='grid' gridTemplateColumns={`repeat(${cols || 2}, 1fr)`} gap={16} {...props} />
|
|
48
|
+
|
|
49
|
+
export const Hr = ({ vertical: vert, ...props }: Props & { vertical?: boolean }) =>
|
|
50
|
+
vert ? <D alignSelf='stretch' borderLeft='1px solid #444' wd='1.5px' {...props} />
|
|
51
|
+
: <D wd='100%' borderTop='1px solid #444' {...props} />
|
|
52
|
+
|
|
53
|
+
export const Input = ({ state: [x, setX], ...props }: Props & { state: [string, (v: string) => void] }) => {
|
|
54
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
55
|
+
useEffect(() => { if (ref.current && document.activeElement != ref.current) ref.current.textContent = x }, [x])
|
|
56
|
+
return <Core p='8px 12px' borderRadius={4} border='1px solid #333' background='#111' color='#eee' fontSize={14} outline='none' minHt='1em' {...props} ref={ref} contentEditable input={setX} />
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const Md = (props: Props) =>
|
|
60
|
+
<D {...props}><Markdown>{props.children as string}</Markdown></D>
|
|
61
|
+
|
|
62
|
+
export const Modal = ({ open, onClose, bare, ...props }: Props & { open: boolean, onClose?: () => void, bare?: boolean }) =>
|
|
63
|
+
open && <D position='fixed' inset='0' background={bare ? 'transparent' : '#0004'} zIndex={50} wd='100vw' ht='100%' row align='center mid' click={() => onClose?.()} {...(bare ? props : {})}>
|
|
64
|
+
{bare ? props.children : <Card p='24px' {...props}>{props.children}</Card>}
|
|
65
|
+
</D>
|
|
66
|
+
|
|
67
|
+
export const Muted = (props: Props) =>
|
|
68
|
+
<D color='#71717a' fontSize={13} {...props} />
|
|
69
|
+
|
|
70
|
+
export const Popover = ({ text, ...props }: Props & { text: ReactNode }) =>
|
|
71
|
+
<D position='relative' display='inline-flex' {...props}>
|
|
72
|
+
{props.children}
|
|
73
|
+
{props.hover && <D position='absolute' top='100%' left='0' zIndex={20} p='6px 0 0'>
|
|
74
|
+
<Card p='8px 12px' maxWd='260px' fontSize={13}>{text}</Card>
|
|
75
|
+
</D>}
|
|
76
|
+
</D>
|
|
77
|
+
|
|
78
|
+
export const Progress = ({ dot, value, ht = 6 }: { ht?: number, value: number | boolean, dot?: number }) =>
|
|
79
|
+
<D ht={ht} wd={dot ? ht : '100%'} borderRadius={ht} background='#222' overflow='hidden'>
|
|
80
|
+
<D ht='100%' borderRadius={ht} background='#63a' transition='width 0.3s'
|
|
81
|
+
wd={`${Math.max(0, Math.min(1, Number(value))) * 100}%`} />
|
|
82
|
+
</D>
|
|
83
|
+
|
|
84
|
+
export const Scroll = (props: Props) =>
|
|
85
|
+
<D grow overflow='auto' minHt='0' {...props} />
|
|
86
|
+
|
|
87
|
+
export const Select = <T,>({ state: [x, setX], options, ...props }: Props & { options: Record<string, T>, state: [T, (v: T) => void] }) =>
|
|
88
|
+
<select value={String(x)} onChange={e => { const entry = Object.entries(options).find(([, v]) => String(v) === e.target.value); if (entry) setX(entry[1] as T) }} style={{
|
|
89
|
+
background: '#111', color: '#eee', border: '1px solid #333',
|
|
90
|
+
borderRadius: 4, padding: '6px 10px', fontSize: 14, outline: 'none', cursor: 'pointer',
|
|
91
|
+
}}>
|
|
92
|
+
{Object.entries(options).map(([k, v]) => <option key={k} value={String(v)}>{k}</option>)}
|
|
93
|
+
</select>
|
|
94
|
+
|
|
95
|
+
export const Tab = ({ active, click, ...props }: Props & { active?: boolean, click?: () => void }) =>
|
|
96
|
+
<D p='6px 14px' borderRadius={4} cursor='pointer' fontSize={14}
|
|
97
|
+
userSelect='none' transition='all 0.15s' click={click}
|
|
98
|
+
background={active ? '#222' : 'transparent'}
|
|
99
|
+
border={active ? '1px solid #333' : '1px solid transparent'}
|
|
100
|
+
hover={!active ? { background: '#111' } as Props : undefined}
|
|
101
|
+
{...props} />
|
|
102
|
+
|
|
103
|
+
export const Tabs = ({ state: [active, setActive], list, ...props }: Props & {
|
|
104
|
+
state: [string, (v: string) => void], list: string[]
|
|
105
|
+
}) =>
|
|
106
|
+
<D row gap={4} p='4px' border='1px solid #222' {...props}>
|
|
107
|
+
{list.map(t => <Btn purple={active == t} click={() => setActive(t)}>{t}</Btn>)}
|
|
108
|
+
</D>
|
|
109
|
+
|
|
110
|
+
export const Textarea = ({ state: [x, setX] = ['', () => { }], ...props }: Props & { state?: [string, (v: string) => void] }) =>
|
|
111
|
+
<textarea value={x} onChange={e => setX(e.target.value)}
|
|
112
|
+
style={{
|
|
113
|
+
background: '#111', color: '#eee', border: '1px solid #333',
|
|
114
|
+
borderRadius: 4, padding: '8px 12px', fontSize: 14, outline: 'none',
|
|
115
|
+
resize: 'vertical', fontFamily: 'inherit', lineHeight: 1.6
|
|
116
|
+
}} />
|
|
117
|
+
|
|
118
|
+
export const H1 = (props: Props) => <D fontWeight={800} fontSize={32} {...props} />
|
|
119
|
+
export const H2 = (props: Props) => <D fontWeight={800} fontSize={24} {...props} />
|
|
120
|
+
export const H3 = (props: Props) => <D fontWeight={700} fontSize={20} {...props} />
|
|
121
|
+
export const H4 = (props: Props) => <D fontWeight={700} fontSize={18} {...props} />
|
|
122
|
+
export const H5 = (props: Props) => <D fontWeight={700} fontSize={16} {...props} />
|
|
123
|
+
export const H6 = (props: Props) => <D fontWeight={700} fontSize={14} {...props} />
|
package/package.json
CHANGED
|
@@ -1,48 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "b44ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"sideEffects": [
|
|
6
|
-
"./index.js",
|
|
7
|
-
"./dist/styles.css"
|
|
8
|
-
],
|
|
9
|
-
"files": [
|
|
10
|
-
"index.js",
|
|
11
|
-
"dist",
|
|
12
|
-
"tailwind.css"
|
|
13
|
-
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "npm run build:js && npm run build:css",
|
|
16
|
-
"build:css": "node scripts/build-css.mjs",
|
|
17
|
-
"build:js": "tsc -p tsconfig.json",
|
|
18
|
-
"dev": "vite",
|
|
19
|
-
"prepare": "npm run build",
|
|
20
|
-
"prepublishOnly": "npm run build"
|
|
21
|
-
},
|
|
22
5
|
"exports": {
|
|
23
|
-
".":
|
|
24
|
-
|
|
25
|
-
"import": "./index.js"
|
|
26
|
-
},
|
|
27
|
-
"./tailwind.css": "./tailwind.css",
|
|
28
|
-
"./styles.css": "./dist/styles.css"
|
|
29
|
-
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"@vitejs/plugin-react": "^6.0.1",
|
|
32
|
-
"clsx": "^2.1.1",
|
|
33
|
-
"highlight.js": "^11.11.0",
|
|
34
|
-
"marked": "^17.0.0",
|
|
35
|
-
"marked-highlight": "^2.2.3",
|
|
36
|
-
"tailwind-merge": "^3.0.0",
|
|
37
|
-
"vite": "^8.0.2"
|
|
6
|
+
".": "./lib.tsx",
|
|
7
|
+
"./server": "./server.tsx"
|
|
38
8
|
},
|
|
39
9
|
"peerDependencies": {
|
|
40
|
-
"react": "
|
|
10
|
+
"react": ">=19",
|
|
11
|
+
"react-dom": ">=19"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"esbuild": "^0.28.0",
|
|
15
|
+
"marked": "^18.0.3",
|
|
16
|
+
"marked-react": "^4.0.0",
|
|
17
|
+
"react-syntax-highlighter": "^16.1.1"
|
|
41
18
|
},
|
|
42
19
|
"devDependencies": {
|
|
20
|
+
"@types/node": "^25.6.2",
|
|
43
21
|
"@types/react": "^19.2.14",
|
|
44
22
|
"@types/react-dom": "^19.2.3",
|
|
45
|
-
"react-
|
|
46
|
-
"
|
|
23
|
+
"@types/react-syntax-highlighter": "^15.5.13",
|
|
24
|
+
"typescript": "^6.0.3"
|
|
47
25
|
}
|
|
48
26
|
}
|
package/readme.md
CHANGED
|
@@ -1,64 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# b4ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
nice react components. also bundles a server cause why not
|
|
4
4
|
|
|
5
|
-
```
|
|
6
|
-
|
|
5
|
+
```tsx
|
|
6
|
+
import { Card, React, server } from "./more"
|
|
7
|
+
server(() => <Card>hello world</Card>)
|
|
7
8
|
```
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
@import "b44ui/tailwind.css";
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
if your app already has `@import "tailwindcss";`, replace that line with the one above.
|
|
10
|
+
# components
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
### D
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
import { defineConfig } from "vite"
|
|
19
|
-
import react from "@vitejs/plugin-react"
|
|
20
|
-
import tailwindcss from "@tailwindcss/vite"
|
|
14
|
+
the div replacement. supports a bunch of shorthands:
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
16
|
+
```jsx
|
|
17
|
+
<D row wd="50%"></D>
|
|
25
18
|
```
|
|
26
|
-
|
|
27
|
-
shared `d` props on `d`-based components:
|
|
28
|
-
|
|
29
|
-
- layout: `grow`, `gap`, `p`, `wd`, `ht`, `row`, `col`, `align`
|
|
30
|
-
- styling: `cn`
|
|
31
|
-
- dom: native rest props like `style`, `onClick`, `data-*`
|
|
32
|
-
|
|
33
|
-
`ratio` is only on `D`.
|
|
34
|
-
|
|
35
|
-
| Component | Extra Props | Description |
|
|
36
|
-
|-----------|-------------|-------------|
|
|
37
|
-
| `App` | `center`, `width`, `htScreen` | Root layout, dark background, wraps strings in `Md` |
|
|
38
|
-
| `Centered` | `width` | Centered max-width container |
|
|
39
|
-
| `D` | `ratio` | Plain div, also handles row/col layout |
|
|
40
|
-
| `H1` `H2` `H3` `H4` `B` `I` | none | Semantic html tags with shared `D` props |
|
|
41
|
-
| `Scroll` | `grow=true` | Vertical scroll container for growable layouts |
|
|
42
|
-
| `Code` | `highlight` | Code block |
|
|
43
|
-
| `Grid` | `cols` | CSS grid, defaults to one column per child |
|
|
44
|
-
| `Card` | none | Bordered zinc-900 card, column by default |
|
|
45
|
-
| `Block` | `label`, `dashed` | Padded container with optional label |
|
|
46
|
-
| `BlockSm` | `dashed` | Smaller padded container |
|
|
47
|
-
| `TabList` | none | Simple tab row wrapper |
|
|
48
|
-
| `Tab` | `title`, `active`, `click` | String-first tab primitive |
|
|
49
|
-
| `Hr` | `vertical`, `color` | Horizontal or vertical divider |
|
|
50
|
-
| `Progress` | `value`, `color`, `dot` | Progress bar or dot |
|
|
51
|
-
| `Dropzone` | `onFiles`, `multiple`, `accept` | Hidden file input wrapper for click/drop |
|
|
52
|
-
| `Btn` | `click`, `color`, `ghost`, `sm` | Button with optional layout props from `D` |
|
|
53
|
-
| `A` | `href`, `click` | Link-styled anchor, works with `onClick` or `href` |
|
|
54
|
-
| `Chip` | `click` | Small inline badge, clickable if `click` provided |
|
|
55
|
-
| `Tint` | `color` | Tinted background block |
|
|
56
|
-
| `Muted` | none | Small muted text |
|
|
57
|
-
| `Input` | `state`, native input attrs | Styled text input, `state={[value, setValue]}` supported |
|
|
58
|
-
| `Textarea` | native textarea attrs | Styled textarea |
|
|
59
|
-
| `Select` | native select attrs | Styled select |
|
|
60
|
-
| `Modal` | `open` | Fixed overlay modal |
|
|
61
|
-
| `Popover` | `text`, `color` | Hover popover |
|
|
62
|
-
| `Md` | `className` | Markdown renderer with syntax highlighting |
|
|
63
|
-
|
|
64
|
-
`Color` = `red | blue | orange | purple | yellow | green`
|
package/server.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createServer } from 'node:http'
|
|
2
|
+
import { build } from 'esbuild'
|
|
3
|
+
import { writeFileSync } from 'node:fs'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
import { App } from './lib'
|
|
7
|
+
|
|
8
|
+
export const server = async (Component: () => ReactNode, port = 6767, Wrapper = App) => {
|
|
9
|
+
const serverPath = resolve('./server')
|
|
10
|
+
|
|
11
|
+
writeFileSync(resolve('./.shim.tsx'), `
|
|
12
|
+
import React, { createElement } from 'react'
|
|
13
|
+
import { createRoot } from 'react-dom/client'
|
|
14
|
+
import { App } from './lib'
|
|
15
|
+
|
|
16
|
+
export const server = (C: () => React.ReactNode, p?: number, W: any = App) =>
|
|
17
|
+
createRoot(document.getElementById('root')!).render(createElement(W, null, createElement(C)))
|
|
18
|
+
`)
|
|
19
|
+
|
|
20
|
+
const shim = {
|
|
21
|
+
name: 'shim',
|
|
22
|
+
setup(build: any) {
|
|
23
|
+
build.onResolve({ filter: /.*/ }, (args: any) => {
|
|
24
|
+
const abs = resolve(args.resolveDir, args.path)
|
|
25
|
+
if (abs === serverPath || abs === serverPath + '.tsx')
|
|
26
|
+
return { path: resolve('./.shim.tsx') }
|
|
27
|
+
})
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = await build({ entryPoints: [resolve(process.argv[1])], bundle: true, write: false, platform: 'browser', plugins: [shim] })
|
|
32
|
+
|
|
33
|
+
const clientJs = result.outputFiles[0].text
|
|
34
|
+
const html = `<html lang="en">
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="UTF-8">
|
|
37
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
38
|
+
<style>* { box-sizing: border-box; } body { margin: 0; padding: 0; }</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div id="root"></div>
|
|
42
|
+
<script type="module">${clientJs}</script>
|
|
43
|
+
</body>
|
|
44
|
+
</html>`
|
|
45
|
+
|
|
46
|
+
createServer((req, res) => {
|
|
47
|
+
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
48
|
+
res.end(html)
|
|
49
|
+
}).listen(port, () => console.log(`listening on ${port}`))
|
|
50
|
+
}
|
package/showcase.tsx
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
D, App, Card, Grid, Hr, Btn, Chip, Tabs, H1, H2, H3, H4, H5, H6, Muted,
|
|
3
|
+
Input, Textarea, Select, Progress, Modal, Dropzone
|
|
4
|
+
} from "./lib"
|
|
5
|
+
import { server } from "./server"
|
|
6
|
+
import React from "react"
|
|
7
|
+
|
|
8
|
+
server(() => {
|
|
9
|
+
const [tab, setTab] = React.useState('buttons')
|
|
10
|
+
const [inputVal, setInputVal] = React.useState('editable text')
|
|
11
|
+
const [textarea, setTextarea] = React.useState('some notes here')
|
|
12
|
+
const [select, setSelect] = React.useState(2)
|
|
13
|
+
const [progress, setProgress] = React.useState(0.6)
|
|
14
|
+
const [modal, setModal] = React.useState(false)
|
|
15
|
+
const [dropped, setDropped] = React.useState<string[]>([])
|
|
16
|
+
const [count, setCount] = React.useState(0)
|
|
17
|
+
|
|
18
|
+
return <App align='top' wd='50%'>
|
|
19
|
+
<H1>b44ui</H1>
|
|
20
|
+
<D color='#888'>component showcase</D>
|
|
21
|
+
|
|
22
|
+
<Hr />
|
|
23
|
+
|
|
24
|
+
<D color='#888'>typography</D>
|
|
25
|
+
<H1>heading no. 1</H1>
|
|
26
|
+
<H2>heading no. 2</H2>
|
|
27
|
+
<H3>heading no. 3</H3>
|
|
28
|
+
<H4>heading no. 4</H4>
|
|
29
|
+
<H5>heading no. 5</H5>
|
|
30
|
+
<H6>heading no. 6</H6>
|
|
31
|
+
<Muted>also, muted</Muted>
|
|
32
|
+
|
|
33
|
+
<Hr />
|
|
34
|
+
|
|
35
|
+
<D color='#888'>buttons</D>
|
|
36
|
+
<D row>
|
|
37
|
+
<Btn click={() => setCount(c => c + 1)}>default</Btn>
|
|
38
|
+
<Btn purple click={() => setCount(c => c + 1)}>purple</Btn>
|
|
39
|
+
<Btn sm click={() => setCount(c => c + 1)}>small</Btn>
|
|
40
|
+
<Btn sm purple click={() => setCount(c => c + 1)}>small purple</Btn>
|
|
41
|
+
</D>
|
|
42
|
+
<Muted>clicks: {count}</Muted>
|
|
43
|
+
|
|
44
|
+
<Hr />
|
|
45
|
+
|
|
46
|
+
<D color='#888'>chips</D>
|
|
47
|
+
<D row>
|
|
48
|
+
<Chip>design</Chip>
|
|
49
|
+
<Chip>react</Chip>
|
|
50
|
+
<Chip>typescript</Chip>
|
|
51
|
+
<Chip>ui library</Chip>
|
|
52
|
+
</D>
|
|
53
|
+
|
|
54
|
+
<Hr />
|
|
55
|
+
|
|
56
|
+
<D color='#888'>tabs</D>
|
|
57
|
+
<Tabs state={[tab, setTab]} list={['buttons', 'inputs', 'layout']} />
|
|
58
|
+
<Card>
|
|
59
|
+
{tab === 'buttons' && <Muted>Buttons tab content</Muted>}
|
|
60
|
+
{tab === 'inputs' && <Muted>Inputs tab content</Muted>}
|
|
61
|
+
{tab === 'layout' && <Muted>Layout tab content</Muted>}
|
|
62
|
+
</Card>
|
|
63
|
+
|
|
64
|
+
<Hr />
|
|
65
|
+
|
|
66
|
+
<D color='#888'>input / textarea / select</D>
|
|
67
|
+
<D col>
|
|
68
|
+
<Muted>Input</Muted>
|
|
69
|
+
<Input state={[inputVal, setInputVal]} />
|
|
70
|
+
</D>
|
|
71
|
+
<D col>
|
|
72
|
+
<Muted>Textarea</Muted>
|
|
73
|
+
<Textarea state={[textarea, setTextarea]} />
|
|
74
|
+
</D>
|
|
75
|
+
<D col>
|
|
76
|
+
<Muted>Select</Muted>
|
|
77
|
+
<Select state={[select, setSelect]} options={{ 'use 1': 1, 'use 2': 2, 'use 3': 3 }} />
|
|
78
|
+
<Muted>Selected: {select}</Muted>
|
|
79
|
+
</D>
|
|
80
|
+
|
|
81
|
+
<Hr />
|
|
82
|
+
|
|
83
|
+
<D color='#888'>progress</D>
|
|
84
|
+
<Progress value={progress} />
|
|
85
|
+
<D row >
|
|
86
|
+
<Btn sm click={() => setProgress(p => Math.max(0, p - 0.1))}>−10%</Btn>
|
|
87
|
+
<Btn sm click={() => setProgress(p => Math.min(1, p + 0.1))}>+10%</Btn>
|
|
88
|
+
<Muted>{Math.round(progress * 100)}%</Muted>
|
|
89
|
+
</D>
|
|
90
|
+
|
|
91
|
+
<Hr />
|
|
92
|
+
|
|
93
|
+
<D color='#888'>grid</D>
|
|
94
|
+
<Grid cols={3} gap={16}>
|
|
95
|
+
{['one', 'two', 'three', 'four', 'five', 'six'].map(n => <Card key={n}>{n}</Card>)}
|
|
96
|
+
</Grid>
|
|
97
|
+
|
|
98
|
+
<Hr />
|
|
99
|
+
|
|
100
|
+
<D color='#888'>dropzone</D>
|
|
101
|
+
<Dropzone dashed onFiles={files => setDropped(files.map(f => f.name))}
|
|
102
|
+
p={32} borderRadius={6} align='center mid' col >
|
|
103
|
+
<Muted>Drop files here or click to upload</Muted>
|
|
104
|
+
{dropped.length > 0 && dropped.map(n => <Chip key={n}>{n}</Chip>)}
|
|
105
|
+
</Dropzone>
|
|
106
|
+
|
|
107
|
+
<Hr />
|
|
108
|
+
|
|
109
|
+
<D color='#888'>modal</D>
|
|
110
|
+
<Btn click={() => setModal(true)}>Open Modal</Btn>
|
|
111
|
+
<Modal open={modal} onClose={() => setModal(false)} gap={16}>
|
|
112
|
+
<H3>my cool modal</H3>
|
|
113
|
+
<Muted>you can write things here if you want</Muted>
|
|
114
|
+
<Btn click={() => setModal(false)}>nice</Btn>
|
|
115
|
+
</Modal>
|
|
116
|
+
|
|
117
|
+
<Hr />
|
|
118
|
+
|
|
119
|
+
<D color='#888'>vertical hr</D>
|
|
120
|
+
<D row ht={40} gap={16} align='left mid'>
|
|
121
|
+
<Muted>left</Muted>
|
|
122
|
+
<Hr vertical />
|
|
123
|
+
<Muted>right</Muted>
|
|
124
|
+
</D>
|
|
125
|
+
</App>
|
|
126
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "esnext",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"types": ["node"],
|
|
10
|
+
"lib": ["esnext", "dom"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["**/*.tsx", "**/*.ts"],
|
|
13
|
+
"exclude": ["node_modules"]
|
|
14
|
+
}
|
package/dist/example.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/example.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { App, Card, D, Input, Scroll } from './index';
|
|
4
|
-
const Example = () => {
|
|
5
|
-
let [text, setText] = useState('');
|
|
6
|
-
return _jsxs(App, { htScreen: true, children: [_jsxs(Card, { row: true, children: [_jsx("h1", { children: "b44ui" }), _jsx(Input, { state: [text, setText], placeholder: 'input supports useState natively' })] }), _jsx(Card, { grow: true, children: _jsx(Scroll, { children: Array.from({ length: 24 }, (_, i) => _jsxs(D, { children: ["item ", i + 1] }, i)) }) })] });
|
|
7
|
-
};
|
|
8
|
-
import { createRoot } from 'react-dom/client';
|
|
9
|
-
createRoot(document.getElementById('root')).render(_jsx(Example, {}));
|