b44ui 0.1.0 → 0.2.1

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/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import './dist/styles.css'
2
+
3
+ export * from './dist/index.js'
package/package.json CHANGED
@@ -1,19 +1,31 @@
1
1
  {
2
2
  "name": "b44ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
+ "sideEffects": [
6
+ "./index.js",
7
+ "./dist/styles.css"
8
+ ],
5
9
  "files": [
6
- "index.tsx",
7
- "styles.css",
10
+ "index.js",
11
+ "dist",
8
12
  "tailwind.css"
9
13
  ],
10
14
  "scripts": {
11
- "dev": "npm run dev --prefix example"
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": "npm run dev --prefix example",
19
+ "prepare": "npm run build",
20
+ "prepublishOnly": "npm run build"
12
21
  },
13
22
  "exports": {
14
- ".": "./index.tsx",
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./index.js"
26
+ },
15
27
  "./tailwind.css": "./tailwind.css",
16
- "./styles.css": "./styles.css"
28
+ "./styles.css": "./dist/styles.css"
17
29
  },
18
30
  "dependencies": {
19
31
  "clsx": "^2.1.1",
@@ -23,8 +35,7 @@
23
35
  "tailwind-merge": "^3.0.0"
24
36
  },
25
37
  "peerDependencies": {
26
- "react": "^19.0.0",
27
- "tailwindcss": "^4.0.0"
38
+ "react": "^19.0.0"
28
39
  },
29
40
  "devDependencies": {
30
41
  "@types/react": "^19.2.14",
package/index.tsx DELETED
@@ -1,309 +0,0 @@
1
- import { type ReactNode, Children, useMemo, useRef, useState } from "react"
2
- import { Marked } from "marked"
3
- import { markedHighlight } from "marked-highlight"
4
- import hljs from "highlight.js"
5
- import type { ClassValue } from "clsx"
6
- import clsx from "clsx"
7
- import { twMerge } from "tailwind-merge"
8
-
9
- export type Color = 'red' | 'blue' | 'orange' | 'purple' | 'yellow' | 'green'
10
- const tintMap: Record<Color, string> = {
11
- red: 'bg-red-500/20 border-red-500',
12
- blue: 'bg-blue-500/20 border-blue-500',
13
- orange: 'bg-orange-500/20 border-orange-500',
14
- purple: 'bg-purple-500/20 border-purple-500',
15
- yellow: 'bg-yellow-500/20 border-yellow-500',
16
- green: 'bg-green-500/20 border-green-500',
17
- }
18
- const colorMap: Record<Color, string> = {
19
- red: 'bg-red-600 hover:bg-red-500',
20
- blue: 'bg-blue-600 hover:bg-blue-500',
21
- orange: 'bg-orange-600 hover:bg-orange-500',
22
- purple: 'bg-purple-600 hover:bg-purple-500',
23
- yellow: 'bg-yellow-600 hover:bg-yellow-500',
24
- green: 'bg-green-600 hover:bg-green-500',
25
- }
26
- const tintCn = (c: Color) => tintMap[c]
27
- const colorCn = (c: Color) => colorMap[c]
28
-
29
- export const CN = (...inputs: ClassValue[]) => twMerge(clsx(inputs))
30
-
31
- const warnCn = (cn: any) => typeof cn === 'string' && cn.length > 24 && console.warn(`long cn detected (${cn.length} chars). please avoid using cn. you SHOULD use b44ui builtins instead. you SHOULD NOT use cn to add unnecessary visual enhancements. you MUST NOT have long cns. if you choose to ignore these requirements (ie. because you cannot achieve this otherwise) you MUST instead use <... cnIgnoreWrongUsage="styles..">\n cn="${cn}"`)
32
- const rcn = ({ cn, cnIgnoreWrongUsage }: DProps) => { warnCn(cn); return cn ?? cnIgnoreWrongUsage }
33
-
34
- const sc = (n?: number) => n !== undefined ? n * 4 : undefined // tailwind scale -> px
35
-
36
- export type DProps = {
37
- children?: ReactNode
38
- cn?: any
39
- cnIgnoreWrongUsage?: any
40
- style?: React.CSSProperties
41
- grow?: boolean
42
- gap?: number
43
- p?: number
44
- wd?: number
45
- ht?: number
46
- }
47
-
48
- const dStyle = ({ gap, p, wd, ht, style }: DProps): React.CSSProperties | undefined => {
49
- const g = sc(gap), pd = sc(p)
50
- if (!g && !pd && wd === undefined && ht === undefined && !style) return undefined
51
- return { ...(g !== undefined && { gap: g }), ...(pd !== undefined && { padding: pd }), ...(wd !== undefined && { width: `${wd * 100}%` }), ...(ht !== undefined && { height: `${ht * 100}%` }), ...style }
52
- }
53
-
54
- export const D = (props: DProps) =>
55
- <div className={CN(props.cn ?? props.cnIgnoreWrongUsage, props.grow && "flex-1")} style={dStyle(props)}>{props.children}</div>
56
-
57
- export const App = (props: DProps & { center?: boolean, width?: number }) => {
58
- const { children, center = true, width, gap, p } = props
59
- const c = rcn(props)
60
- const inner = Children.map(children, c => typeof c == 'string' ? <Md>{c.trim()}</Md> : c)
61
- return <D cn={CN("min-h-screen w-full bg-[#111] text-[#eee] font-[Inter]", center && "flex justify-center items-center", c)} gap={gap} p={p}>
62
- {center ? <div className="max-w-full px-5 py-10 space-y-5 *:mx-auto" style={{ width }}>{inner}</div> : inner}
63
- </D>
64
- }
65
-
66
- export const Centered = (props: DProps & { width?: number }) => {
67
- const { children, grow, gap, p, wd, ht, width = 700 } = props
68
- return <D cn={CN("mx-auto max-w-full px-6", rcn(props))} grow={grow} gap={gap} p={p} wd={wd} ht={ht} style={{ width }}>{children}</D>
69
- }
70
-
71
- export const TabList = (props: DProps) => {
72
- const { children, grow, gap, p, wd, ht } = props
73
- return <D cn={CN("flex items-end gap-4", rcn(props))} grow={grow} gap={gap} p={p} wd={wd} ht={ht}>{children}</D>
74
- }
75
-
76
- export const Tab = ({ title, active, click, grow, gap, p, ...rest }: DProps & { title: ReactNode, active?: boolean, click?: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
77
- const c = rcn(rest as DProps)
78
- return <button
79
- className={CN("cursor-pointer border-b-2 pb-1.5 text-sm font-medium", active ? "border-zinc-100 text-zinc-100" : "border-transparent text-zinc-400 hover:text-zinc-200", grow && "flex-1", c)}
80
- style={dStyle({ gap, p, wd: rest.wd, style: rest.style })}
81
- onClick={click}
82
- role="tab"
83
- aria-selected={active}
84
- {...rest}
85
- >{title}</button>
86
- }
87
-
88
- export const Block = (props: DProps & { label?: ReactNode, row?: boolean, dashed?: boolean }) => {
89
- const { label, children, row, dashed, grow, gap, p, wd, ht } = props
90
- return <D cn={CN("rounded flex flex-col items-center", dashed && "border border-dashed border-zinc-700", rcn(props))} grow={grow} gap={gap ?? 4} p={p ?? 4} wd={wd} ht={ht}>
91
- {label && <span className="opacity-75">{label}</span>}
92
- <div className={CN(row ? "flex-row" : "flex-col", "flex items-center")} style={{ gap: sc(gap ?? 4) }}>{children}</div>
93
- </D>
94
- }
95
-
96
- export const BlockSm = (props: DProps & { dashed?: boolean }) => {
97
- const { children, dashed, grow, gap, p, wd, ht } = props
98
- return <D cn={CN("rounded text-sm", dashed && "border border-dashed border-zinc-700", rcn(props))} grow={grow} gap={gap} p={p ?? 2} wd={wd} ht={ht}
99
- style={{ paddingLeft: sc(3), paddingRight: sc(3) }}>{children}</D>
100
- }
101
-
102
- export const Chip = (props: DProps & { click?: () => void }) => {
103
- const { children, click, grow, gap, p, wd, ht, style } = props
104
- return <span className={CN("bg-zinc-800 rounded px-2 py-0.5 text-xs", click && "cursor-pointer hover:bg-zinc-700", rcn(props))}
105
- style={dStyle({ gap, p, wd, ht, style })}
106
- onClick={click}>{children}</span>
107
- }
108
-
109
- export const Card = (props: DProps) => {
110
- const { children, grow, gap, p, wd, ht } = props
111
- return <D cn={CN("rounded border border-zinc-700 bg-zinc-900", rcn(props))} grow={grow} gap={gap ?? 4} p={p ?? 4} wd={wd} ht={ht}
112
- style={{ display: 'flex', flexDirection: 'column' }}>{children}</D>
113
- }
114
-
115
- export const Col = (props: DProps & { align?: 'start' | 'center' | 'end' }) => {
116
- const { children, grow, gap, p, wd, ht, align } = props
117
- return <D cn={CN("flex flex-col", align === 'start' && 'items-start', align === 'center' && 'items-center', align === 'end' && 'items-end', rcn(props))} grow={grow} gap={gap ?? 4} p={p} wd={wd} ht={ht}>{children}</D>
118
- }
119
-
120
- export const Popover = (props: DProps & { text: ReactNode, color?: Color }) => {
121
- const { children, text, color } = props
122
- const c = rcn(props)
123
- const [open, setOpen] = useState(false)
124
- return <span className="relative inline-block" onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
125
- <span className={CN("cursor-pointer", color && tintCn(color), color && "border-b-2", c)}>{children}</span>
126
- {open && <div className="absolute left-0 top-full z-10 mt-1">
127
- <Card cn="w-64 shadow-lg text-sm" p={props.p ?? 3} gap={props.gap}>{text}</Card>
128
- </div>}
129
- </span>
130
- }
131
-
132
- export const Muted = (props: DProps) => {
133
- const { children, grow } = props
134
- return <span className={CN("text-sm text-zinc-400", grow && "flex-1", rcn(props))}>{children}</span>
135
- }
136
-
137
- export const A = ({
138
- children, cn, cnIgnoreWrongUsage, grow, gap, p, wd, style, click, href, onClick, onKeyDown, role, tabIndex, ...rest
139
- }: DProps & { click?: () => void } & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
140
- const c = rcn({ cn, cnIgnoreWrongUsage })
141
- const fire = (e: React.MouseEvent<HTMLAnchorElement> | React.KeyboardEvent<HTMLAnchorElement>) => {
142
- click?.()
143
- onClick?.(e as React.MouseEvent<HTMLAnchorElement>)
144
- }
145
- const buttonLike = !href
146
-
147
- return <a
148
- {...rest}
149
- href={href}
150
- role={buttonLike ? role ?? 'button' : role}
151
- tabIndex={buttonLike ? tabIndex ?? 0 : tabIndex}
152
- className={CN("inline-flex items-center gap-1 text-zinc-300 underline underline-offset-4 cursor-pointer hover:text-zinc-100", grow && "flex-1", c)}
153
- style={dStyle({ gap, p, wd, style })}
154
- onClick={fire}
155
- onKeyDown={e => {
156
- if (buttonLike && (e.key === 'Enter' || e.key === ' ')) {
157
- e.preventDefault()
158
- fire(e)
159
- }
160
- onKeyDown?.(e)
161
- }}
162
- >{children}</a>
163
- }
164
-
165
- export const Btn = ({ children, grow, gap, p, click, color, ghost, sm, ...rest }: DProps & { click?: () => void, color?: Color, ghost?: boolean, sm?: boolean } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
166
- const c = rcn(rest as DProps)
167
- return <button className={CN("rounded cursor-pointer", sm ? "px-3 py-1 text-xs" : "px-4 py-2 text-sm",
168
- ghost ? "bg-transparent text-zinc-400 hover:bg-zinc-800" : color ? colorCn(color) : "bg-zinc-800 hover:bg-zinc-700", grow && "flex-1", c)}
169
- style={dStyle({ gap, p, wd: rest.wd, style: rest.style })} onClick={click} {...rest}>{children}</button>
170
- }
171
-
172
- export const Tint = (props: DProps & { color: Color }) => {
173
- const { children, color, grow, gap, p, wd, ht } = props
174
- return <D cn={CN("rounded text-sm", tintCn(color), rcn(props))} grow={grow} gap={gap} p={p ?? 3} wd={wd} ht={ht}>{children}</D>
175
- }
176
-
177
- export const Row = (props: DProps & { ratio?: number, align?: 'mid' | 'start' | 'end' }) => {
178
- const { children, ratio, align, grow, gap, p, wd, ht, style } = props
179
- return <div className={CN("flex items-center justify-center", align === 'mid' && "justify-between", align == 'end' && "justify-end", align == 'start' && "justify-start", grow && "flex-1", rcn(props))}
180
- style={{ ...dStyle({ gap: gap ?? 4, p, wd, ht, style }), ...(ratio ? { width: `${ratio * 100}%` } : {}) }}>{children}</div>
181
- }
182
-
183
- export const Toolbar = (props: DProps) => {
184
- const { children, gap, p, wd, ht } = props
185
- return <Row cn={CN("border-b border-zinc-700", rcn(props))} gap={gap} p={p ?? 2} wd={wd} ht={ht} align="mid">{children}</Row>
186
- }
187
-
188
- export const Modal = (props: DProps & { open: boolean }) => {
189
- const { children, open, gap, p, wd, ht } = props
190
- return open ? <div className="fixed inset-0 flex items-center justify-center bg-black/50 z-50">
191
- <Card cn={CN("max-h-[80vh] overflow-y-auto w-lg", rcn(props))} gap={gap} p={p} wd={wd} ht={ht}>{children}</Card>
192
- </div> : null
193
- }
194
-
195
- export const Input = ({ cn, cnIgnoreWrongUsage, grow, ...rest }: DProps & React.InputHTMLAttributes<HTMLInputElement>) => {
196
- const c = rcn({ cn, cnIgnoreWrongUsage })
197
- return <input className={CN("rounded bg-zinc-800 border border-zinc-700 px-3 py-2 text-sm outline-none", grow && "flex-1", c)} style={dStyle({ gap: rest.gap, p: rest.p, wd: rest.wd, style: rest.style })} {...rest} />
198
- }
199
-
200
- export const Textarea = ({ cn, cnIgnoreWrongUsage, ref, grow, ...rest }: DProps & React.TextareaHTMLAttributes<HTMLTextAreaElement> & { ref?: React.RefObject<HTMLTextAreaElement | null> }) => {
201
- const c = rcn({ cn, cnIgnoreWrongUsage })
202
- return <textarea ref={ref} className={CN("rounded bg-zinc-800 border border-zinc-700 px-3 py-2 text-sm outline-none w-full", grow && "flex-1", c)} style={dStyle({ gap: rest.gap, p: rest.p, wd: rest.wd, style: rest.style })} {...rest} />
203
- }
204
-
205
- export const Select = ({ cn, cnIgnoreWrongUsage, grow, ...rest }: DProps & React.SelectHTMLAttributes<HTMLSelectElement>) => {
206
- const c = rcn({ cn, cnIgnoreWrongUsage })
207
- return <select className={CN("rounded bg-zinc-800 border border-zinc-700 px-3 py-2 text-sm outline-none cursor-pointer", grow && "flex-1", c)} style={dStyle({ gap: rest.gap, p: rest.p, wd: rest.wd, style: rest.style })} {...rest} />
208
- }
209
-
210
- export const Grid = (props: DProps & { cols?: number }) => {
211
- const { children, cols, grow, gap, p, wd, ht } = props
212
- return <D cn={CN("grid ui-grid", rcn(props))} grow={grow} p={p} wd={wd} ht={ht} style={{ gap: sc(gap ?? 4), '--cols': cols ?? Children.count(children) } as React.CSSProperties}>{children}</D>
213
- }
214
-
215
- export const Hr = ({ vertical, color = 'gray', grow, gap, p, ...rest }: DProps & { vertical?: boolean, color?: Color | 'gray' }) => {
216
- const c = rcn(rest as DProps)
217
- const border = color === 'gray' ? "border-zinc-700" : ({
218
- red: "border-red-500",
219
- blue: "border-blue-500",
220
- orange: "border-orange-500",
221
- purple: "border-purple-500",
222
- yellow: "border-yellow-500",
223
- green: "border-green-500",
224
- } satisfies Record<Color, string>)[color]
225
-
226
- return <div
227
- className={CN(vertical ? "self-stretch border-l" : "w-full border-t", border, grow && "flex-1", c)}
228
- style={dStyle({ gap, p, wd: rest.wd, style: rest.style })}
229
- aria-hidden="true"
230
- />
231
- }
232
-
233
- export const Progress = ({ value, color = 'blue', dot, grow, gap, p, ...rest }: DProps & { value: number, color?: Color, dot?: boolean }) => {
234
- const c = rcn(rest as DProps)
235
- const v = Math.max(0, Math.min(1, value))
236
- const fill = color === 'red' ? 'bg-red-500'
237
- : color === 'orange' ? 'bg-orange-500'
238
- : color === 'purple' ? 'bg-purple-500'
239
- : color === 'yellow' ? 'bg-yellow-500'
240
- : color === 'green' ? 'bg-green-500'
241
- : 'bg-blue-500'
242
-
243
- if (dot) return <div
244
- className={CN("rounded-full border border-zinc-700 bg-zinc-800 overflow-hidden", grow && "flex-1", c)}
245
- style={dStyle({ gap, p, wd: rest.wd, style: { aspectRatio: '1 / 1', ...rest.style } })}
246
- >
247
- <div className={CN("h-full rounded-full", fill)} style={{ opacity: v ? 1 : 0.2 }} />
248
- </div>
249
-
250
- return <div
251
- className={CN("h-2 overflow-hidden rounded-full bg-zinc-800 border border-zinc-700", grow && "flex-1", c)}
252
- style={dStyle({ gap, p, wd: rest.wd, style: rest.style })}
253
- >
254
- <div className={CN("h-full rounded-full", fill)} style={{ width: `${v * 100}%` }} />
255
- </div>
256
- }
257
-
258
- export const Dropzone = ({ children, onFiles, multiple, accept, click, ...rest }: DProps & { onFiles: (files: File[]) => void, multiple?: boolean, accept?: string, click?: () => void }) => {
259
- const input = useRef<HTMLInputElement>(null)
260
- const c = rcn(rest)
261
- const push = (list: FileList | null) => {
262
- const files = list ? Array.from(list) : []
263
- if (files.length) onFiles(files)
264
- }
265
-
266
- return <div
267
- className={CN("contents", c)}
268
- onClick={e => {
269
- click?.()
270
- input.current?.click()
271
- rest.onClick?.(e as any)
272
- }}
273
- onDragOver={e => {
274
- e.preventDefault()
275
- rest.onDragOver?.(e)
276
- }}
277
- onDrop={e => {
278
- e.preventDefault()
279
- push(e.dataTransfer.files)
280
- rest.onDrop?.(e)
281
- }}
282
- >
283
- <input
284
- ref={input}
285
- hidden
286
- type="file"
287
- multiple={multiple}
288
- accept={accept}
289
- onChange={e => push(e.target.files)}
290
- />
291
- {children}
292
- </div>
293
- }
294
-
295
- export const Code = (props: DProps & { highlight?: string }) => {
296
- const { children, highlight } = props
297
- const c = rcn(props)
298
- const html = useMemo(() => highlight ? hljs.highlight(children as string, { language: highlight }).value : '', [children, highlight])
299
- if (highlight) return <pre className={CN("rounded bg-zinc-900 border border-zinc-700 p-3 text-sm overflow-x-auto", c)}>
300
- <code className={`hljs language-${highlight}`} dangerouslySetInnerHTML={{ __html: html }} />
301
- </pre>
302
- return <code className={CN("bg-zinc-800 rounded px-1.5 py-0.5 text-sm font-mono", c)}>{children}</code>
303
- }
304
-
305
- const marked = new Marked(markedHighlight({ langPrefix: "hljs language-", highlight: c => hljs.highlightAuto(c).value }))
306
- export const Md = ({ children, className }: { children: string, className?: string }) => {
307
- const html = useMemo(() => marked.parse(children) as string, [children])
308
- return <div className={CN("ui-markdown", className)} dangerouslySetInnerHTML={{ __html: html }} />
309
- }
package/styles.css DELETED
@@ -1,21 +0,0 @@
1
- @import "highlight.js/styles/github-dark.css";
2
-
3
- .ui-markdown { line-height: 1.5; }
4
- h1, .ui-markdown h2, .ui-markdown h3 { font-weight: 700; margin: 0; }
5
- h1 { font-size: 1.5em; }
6
- h2 { font-size: 1.25em; }
7
- h3 { font-size: 1.1em; }
8
- p { margin: 0.4em 0; }
9
- a { color: #93c5fd; text-decoration: underline; }
10
- pre { background: #1a1a2e; border-radius: 4px; padding: 0; overflow-x: auto; margin: 0.4em 0; }
11
- code { font-family: ui-monospace, monospace; font-size: 0.9em; }
12
- blockquote { border-left: 3px solid #444; padding-left: 1em; opacity: 0.8; margin: 0.25em 0; }
13
- img { max-width: 100%; border-radius: 6px; }
14
- .ui-markdown p:first-child { margin-top: 0; }
15
- .ui-markdown p:last-child { margin-bottom: 0; }
16
- .ui-markdown :not(pre) > code { background: #1a1a2e; padding: 2px 5px; border-radius: 3px; }
17
- .ui-markdown ul, .ui-markdown ol { padding-left: 1.5em; margin: 0.25em 0; }
18
- .ui-markdown table { border-collapse: collapse; margin: 0.4em 0; }
19
- .ui-markdown th, .ui-markdown td { border: 1px solid #333; padding: 4px 10px; }
20
- .ui-grid { grid-template-columns: repeat(var(--cols), 1fr); }
21
- @media (max-width: 640px) { .ui-grid { grid-template-columns: 1fr; } }