b44ui 0.0.10 → 0.0.12

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.
Files changed (3) hide show
  1. package/index.tsx +141 -12
  2. package/package.json +1 -1
  3. package/readme.md +10 -4
package/index.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { type ReactNode, Children, useMemo, useState } from "react"
1
+ import { type ReactNode, Children, useMemo, useRef, useState } from "react"
2
2
  import { Marked } from "marked"
3
3
  import { markedHighlight } from "marked-highlight"
4
4
  import hljs from "highlight.js"
@@ -6,13 +6,14 @@ import type { ClassValue } from "clsx"
6
6
  import clsx from "clsx"
7
7
  import { twMerge } from "tailwind-merge"
8
8
 
9
- export type Color = 'red' | 'blue' | 'orange' | 'purple' | 'yellow'
9
+ export type Color = 'red' | 'blue' | 'orange' | 'purple' | 'yellow' | 'green'
10
10
  const tintMap: Record<Color, string> = {
11
11
  red: 'bg-red-500/20 border-red-500',
12
12
  blue: 'bg-blue-500/20 border-blue-500',
13
13
  orange: 'bg-orange-500/20 border-orange-500',
14
14
  purple: 'bg-purple-500/20 border-purple-500',
15
15
  yellow: 'bg-yellow-500/20 border-yellow-500',
16
+ green: 'bg-green-500/20 border-green-500',
16
17
  }
17
18
  const colorMap: Record<Color, string> = {
18
19
  red: 'bg-red-600 hover:bg-red-500',
@@ -20,6 +21,7 @@ const colorMap: Record<Color, string> = {
20
21
  orange: 'bg-orange-600 hover:bg-orange-500',
21
22
  purple: 'bg-purple-600 hover:bg-purple-500',
22
23
  yellow: 'bg-yellow-600 hover:bg-yellow-500',
24
+ green: 'bg-green-600 hover:bg-green-500',
23
25
  }
24
26
  const tintCn = (c: Color) => tintMap[c]
25
27
  const colorCn = (c: Color) => colorMap[c]
@@ -39,12 +41,13 @@ export type DProps = {
39
41
  grow?: boolean
40
42
  gap?: number
41
43
  p?: number
44
+ wd?: number
42
45
  }
43
46
 
44
- const dStyle = ({ gap, p, style }: DProps): React.CSSProperties | undefined => {
47
+ const dStyle = ({ gap, p, wd, style }: DProps): React.CSSProperties | undefined => {
45
48
  const g = sc(gap), pd = sc(p)
46
- if (!g && !pd && !style) return undefined
47
- return { ...(g !== undefined && { gap: g }), ...(pd !== undefined && { padding: pd }), ...style }
49
+ if (!g && !pd && wd === undefined && !style) return undefined
50
+ return { ...(g !== undefined && { gap: g }), ...(pd !== undefined && { padding: pd }), ...(wd !== undefined && { width: `${wd * 100}%` }), ...style }
48
51
  }
49
52
 
50
53
  export const D = (props: DProps) =>
@@ -64,6 +67,23 @@ export const Centered = (props: DProps & { width?: number }) => {
64
67
  return <D cn={CN("mx-auto max-w-full px-6", rcn(props))} grow={grow} gap={gap} p={p} style={{ width }}>{children}</D>
65
68
  }
66
69
 
70
+ export const TabList = (props: DProps) => {
71
+ const { children, grow, gap, p } = props
72
+ return <D cn={CN("flex items-end gap-4", rcn(props))} grow={grow} gap={gap} p={p}>{children}</D>
73
+ }
74
+
75
+ export const Tab = ({ title, active, click, grow, gap, p, ...rest }: DProps & { title: ReactNode, active?: boolean, click?: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
76
+ const c = rcn(rest as DProps)
77
+ return <button
78
+ 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)}
79
+ style={dStyle({ gap, p, wd: rest.wd, style: rest.style })}
80
+ onClick={click}
81
+ role="tab"
82
+ aria-selected={active}
83
+ {...rest}
84
+ >{title}</button>
85
+ }
86
+
67
87
  export const Block = (props: DProps & { label?: ReactNode, row?: boolean, dashed?: boolean }) => {
68
88
  const { label, children, row, dashed, grow, gap, p } = props
69
89
  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}>
@@ -79,8 +99,9 @@ export const BlockSm = (props: DProps & { dashed?: boolean }) => {
79
99
  }
80
100
 
81
101
  export const Chip = (props: DProps & { click?: () => void }) => {
82
- const { children, click } = props
102
+ const { children, click, grow, gap, p, wd, style } = props
83
103
  return <span className={CN("bg-zinc-800 rounded px-2 py-0.5 text-xs", click && "cursor-pointer hover:bg-zinc-700", rcn(props))}
104
+ style={dStyle({ gap, p, wd, style })}
84
105
  onClick={click}>{children}</span>
85
106
  }
86
107
 
@@ -112,11 +133,39 @@ export const Muted = (props: DProps) => {
112
133
  return <span className={CN("text-sm text-zinc-400", grow && "flex-1", rcn(props))}>{children}</span>
113
134
  }
114
135
 
136
+ export const A = ({
137
+ children, cn, cnIgnoreWrongUsage, grow, gap, p, style, click, href, onClick, onKeyDown, role, tabIndex, ...rest
138
+ }: DProps & { click?: () => void } & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
139
+ const c = rcn({ cn, cnIgnoreWrongUsage })
140
+ const fire = (e: React.MouseEvent<HTMLAnchorElement> | React.KeyboardEvent<HTMLAnchorElement>) => {
141
+ click?.()
142
+ onClick?.(e as React.MouseEvent<HTMLAnchorElement>)
143
+ }
144
+ const buttonLike = !href
145
+
146
+ return <a
147
+ {...rest}
148
+ href={href}
149
+ role={buttonLike ? role ?? 'button' : role}
150
+ tabIndex={buttonLike ? tabIndex ?? 0 : tabIndex}
151
+ 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)}
152
+ style={dStyle({ gap, p, style })}
153
+ onClick={fire}
154
+ onKeyDown={e => {
155
+ if (buttonLike && (e.key === 'Enter' || e.key === ' ')) {
156
+ e.preventDefault()
157
+ fire(e)
158
+ }
159
+ onKeyDown?.(e)
160
+ }}
161
+ >{children}</a>
162
+ }
163
+
115
164
  export const Btn = ({ children, grow, gap, p, click, color, ghost, sm, ...rest }: DProps & { click?: () => void, color?: Color, ghost?: boolean, sm?: boolean } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
116
165
  const c = rcn(rest as DProps)
117
166
  return <button className={CN("rounded cursor-pointer", sm ? "px-3 py-1 text-xs" : "px-4 py-2 text-sm",
118
167
  ghost ? "bg-transparent text-zinc-400 hover:bg-zinc-800" : color ? colorCn(color) : "bg-zinc-800 hover:bg-zinc-700", grow && "flex-1", c)}
119
- style={dStyle({ gap, p })} onClick={click} {...rest}>{children}</button>
168
+ style={dStyle({ gap, p, wd: rest.wd, style: rest.style })} onClick={click} {...rest}>{children}</button>
120
169
  }
121
170
 
122
171
  export const Tint = (props: DProps & { color: Color }) => {
@@ -125,9 +174,9 @@ export const Tint = (props: DProps & { color: Color }) => {
125
174
  }
126
175
 
127
176
  export const Row = (props: DProps & { ratio?: number, align?: 'mid' | 'start' | 'end' }) => {
128
- const { children, ratio, align, grow, gap, p } = props
177
+ const { children, ratio, align, grow, gap, p, wd, style } = props
129
178
  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))}
130
- style={{ gap: sc(gap ?? 4), ...(p !== undefined && { padding: sc(p) }), ...(ratio ? { width: `${ratio * 100}%` } : {}) }}>{children}</div>
179
+ style={{ ...dStyle({ gap: gap ?? 4, p, wd, style }), ...(ratio ? { width: `${ratio * 100}%` } : {}) }}>{children}</div>
131
180
  }
132
181
 
133
182
  export const Toolbar = (props: DProps) => {
@@ -144,17 +193,17 @@ export const Modal = (props: DProps & { open: boolean }) => {
144
193
 
145
194
  export const Input = ({ cn, cnIgnoreWrongUsage, grow, ...rest }: DProps & React.InputHTMLAttributes<HTMLInputElement>) => {
146
195
  const c = rcn({ cn, cnIgnoreWrongUsage })
147
- return <input className={CN("rounded bg-zinc-800 border border-zinc-700 px-3 py-2 text-sm outline-none", grow && "flex-1", c)} {...rest} />
196
+ 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} />
148
197
  }
149
198
 
150
199
  export const Textarea = ({ cn, cnIgnoreWrongUsage, grow, ...rest }: DProps & React.TextareaHTMLAttributes<HTMLTextAreaElement>) => {
151
200
  const c = rcn({ cn, cnIgnoreWrongUsage })
152
- return <textarea className={CN("rounded bg-zinc-800 border border-zinc-700 px-3 py-2 text-sm outline-none w-full", grow && "flex-1", c)} {...rest} />
201
+ return <textarea 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} />
153
202
  }
154
203
 
155
204
  export const Select = ({ cn, cnIgnoreWrongUsage, grow, ...rest }: DProps & React.SelectHTMLAttributes<HTMLSelectElement>) => {
156
205
  const c = rcn({ cn, cnIgnoreWrongUsage })
157
- 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)} {...rest} />
206
+ 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} />
158
207
  }
159
208
 
160
209
  export const Grid = (props: DProps & { cols?: number }) => {
@@ -162,6 +211,86 @@ export const Grid = (props: DProps & { cols?: number }) => {
162
211
  return <D cn={CN("grid ui-grid", rcn(props))} grow={grow} p={p} style={{ gap: sc(gap ?? 4), '--cols': cols ?? Children.count(children) } as React.CSSProperties}>{children}</D>
163
212
  }
164
213
 
214
+ export const Hr = ({ vertical, color = 'gray', grow, gap, p, ...rest }: DProps & { vertical?: boolean, color?: Color | 'gray' }) => {
215
+ const c = rcn(rest as DProps)
216
+ const border = color === 'gray' ? "border-zinc-700" : ({
217
+ red: "border-red-500",
218
+ blue: "border-blue-500",
219
+ orange: "border-orange-500",
220
+ purple: "border-purple-500",
221
+ yellow: "border-yellow-500",
222
+ green: "border-green-500",
223
+ } satisfies Record<Color, string>)[color]
224
+
225
+ return <div
226
+ className={CN(vertical ? "self-stretch border-l" : "w-full border-t", border, grow && "flex-1", c)}
227
+ style={dStyle({ gap, p, wd: rest.wd, style: rest.style })}
228
+ aria-hidden="true"
229
+ />
230
+ }
231
+
232
+ export const Progress = ({ value, color = 'blue', dot, grow, gap, p, ...rest }: DProps & { value: number, color?: Color, dot?: boolean }) => {
233
+ const c = rcn(rest as DProps)
234
+ const v = Math.max(0, Math.min(1, value))
235
+ const fill = color === 'red' ? 'bg-red-500'
236
+ : color === 'orange' ? 'bg-orange-500'
237
+ : color === 'purple' ? 'bg-purple-500'
238
+ : color === 'yellow' ? 'bg-yellow-500'
239
+ : color === 'green' ? 'bg-green-500'
240
+ : 'bg-blue-500'
241
+
242
+ if (dot) return <div
243
+ className={CN("rounded-full border border-zinc-700 bg-zinc-800 overflow-hidden", grow && "flex-1", c)}
244
+ style={dStyle({ gap, p, wd: rest.wd, style: { aspectRatio: '1 / 1', ...rest.style } })}
245
+ >
246
+ <div className={CN("h-full rounded-full", fill)} style={{ opacity: v ? 1 : 0.2 }} />
247
+ </div>
248
+
249
+ return <div
250
+ className={CN("h-2 overflow-hidden rounded-full bg-zinc-800 border border-zinc-700", grow && "flex-1", c)}
251
+ style={dStyle({ gap, p, wd: rest.wd, style: rest.style })}
252
+ >
253
+ <div className={CN("h-full rounded-full", fill)} style={{ width: `${v * 100}%` }} />
254
+ </div>
255
+ }
256
+
257
+ export const Dropzone = ({ children, onFiles, multiple, accept, click, ...rest }: DProps & { onFiles: (files: File[]) => void, multiple?: boolean, accept?: string, click?: () => void }) => {
258
+ const input = useRef<HTMLInputElement>(null)
259
+ const c = rcn(rest)
260
+ const push = (list: FileList | null) => {
261
+ const files = list ? Array.from(list) : []
262
+ if (files.length) onFiles(files)
263
+ }
264
+
265
+ return <div
266
+ className={CN("contents", c)}
267
+ onClick={e => {
268
+ click?.()
269
+ input.current?.click()
270
+ rest.onClick?.(e as any)
271
+ }}
272
+ onDragOver={e => {
273
+ e.preventDefault()
274
+ rest.onDragOver?.(e)
275
+ }}
276
+ onDrop={e => {
277
+ e.preventDefault()
278
+ push(e.dataTransfer.files)
279
+ rest.onDrop?.(e)
280
+ }}
281
+ >
282
+ <input
283
+ ref={input}
284
+ hidden
285
+ type="file"
286
+ multiple={multiple}
287
+ accept={accept}
288
+ onChange={e => push(e.target.files)}
289
+ />
290
+ {children}
291
+ </div>
292
+ }
293
+
165
294
  export const Code = (props: DProps & { highlight?: string }) => {
166
295
  const { children, highlight } = props
167
296
  const c = rcn(props)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "b44ui",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "index.tsx",
package/readme.md CHANGED
@@ -28,15 +28,21 @@ export default defineConfig({
28
28
  |-----------|-------|-------------|
29
29
  | `App` | `center`, `width`, `cn` | Root layout, dark background, wraps strings in `Md` |
30
30
  | `Centered` | `width`, `cn`, `grow` | Centered max-width container |
31
- | `D` | `cn`, `style`, `grow` | Plain div with `cn` |
32
- | `Row` | `align`, `ratio`, `cn`, `grow` | Flex row, `align`: `start \| mid \| end` |
33
- | `Col` | `cn`, `grow` | Flex column |
31
+ | `D` | `cn`, `style`, `grow`, `wd` | Plain div with `cn` |
32
+ | `Row` | `align`, `ratio`, `cn`, `grow`, `wd` | Flex row, `align`: `start \| mid \| end` |
33
+ | `Col` | `cn`, `grow`, `wd` | Flex column |
34
34
  | `Code` | `highlight` | Code block |
35
35
  | `Grid` | `cols`, `cn`, `grow` | CSS grid, defaults to one column per child |
36
36
  | `Card` | `cn`, `grow` | Bordered zinc-900 card |
37
37
  | `Block` | `label`, `row`, `dashed`, `cn`, `grow` | Padded container with optional label |
38
38
  | `BlockSm` | `dashed`, `cn`, `grow` | Smaller padded container |
39
+ | `TabList` | `gap`, `p`, `wd` | Simple tab row wrapper |
40
+ | `Tab` | `title`, `active`, `click`, `wd` | String-first tab primitive |
41
+ | `Hr` | `vertical`, `color`, `wd` | Horizontal or vertical divider |
42
+ | `Progress` | `value`, `color`, `dot`, `wd` | Progress bar or dot |
43
+ | `Dropzone` | `onFiles`, `multiple`, `accept` | Hidden file input wrapper for click/drop |
39
44
  | `Btn` | `click`, `color`, `ghost`, `cn`, `grow` | Button, supports `Color` and ghost style |
45
+ | `A` | `href`, `click`, `cn`, `grow` | Link-styled anchor, works with `onClick` or `href` |
40
46
  | `Chip` | `click`, `cn` | Small inline badge, clickable if `click` provided |
41
47
  | `Tint` | `color`, `cn`, `grow` | Tinted background block |
42
48
  | `Muted` | `cn`, `grow` | Small muted text |
@@ -47,6 +53,6 @@ export default defineConfig({
47
53
  | `Popover` | `text`, `color`, `cn` | Hover popover |
48
54
  | `Md` | `className` | Markdown renderer with syntax highlighting |
49
55
 
50
- `Color`: `red \| blue \| orange \| purple \| yellow`
56
+ `Color`: `red \| blue \| orange \| purple \| yellow \| green`
51
57
 
52
58
  All components accept `cn` for additional Tailwind classes (merged via `tailwind-merge`).