@beemafrica/ui 0.1.12 → 0.1.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beemafrica/ui",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Shared Beem UI components built on shadcn/Radix",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -27,6 +27,7 @@
27
27
  "lucide-react": "^1.31.0",
28
28
  "next-themes": "^0.4.6",
29
29
  "radix-ui": "^1.6.7",
30
+ "recharts": "^3.8.0",
30
31
  "shadcn": "^4.16.2",
31
32
  "tailwind-merge": "^3.6.0",
32
33
  "tailwind-scrollbar": "^4.0.2",
@@ -0,0 +1,372 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "cn"
5
+ import * as RechartsPrimitive from "recharts"
6
+ import type { TooltipValueType } from "recharts"
7
+
8
+ // Format: { THEME_NAME: CSS_SELECTOR }
9
+ const THEMES = { light: "", dark: ".dark" } as const
10
+
11
+ const INITIAL_DIMENSION = { width: 320, height: 200 } as const
12
+ type TooltipNameType = number | string
13
+
14
+ export type ChartConfig = Record<
15
+ string,
16
+ {
17
+ label?: React.ReactNode
18
+ icon?: React.ComponentType
19
+ } & (
20
+ | { color?: string; theme?: never }
21
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
22
+ )
23
+ >
24
+
25
+ type ChartContextProps = {
26
+ config: ChartConfig
27
+ }
28
+
29
+ const ChartContext = React.createContext<ChartContextProps | null>(null)
30
+
31
+ function useChart() {
32
+ const context = React.useContext(ChartContext)
33
+
34
+ if (!context) {
35
+ throw new Error("useChart must be used within a <ChartContainer />")
36
+ }
37
+
38
+ return context
39
+ }
40
+
41
+ function ChartContainer({
42
+ id,
43
+ className,
44
+ children,
45
+ config,
46
+ initialDimension = INITIAL_DIMENSION,
47
+ ...props
48
+ }: React.ComponentProps<"div"> & {
49
+ config: ChartConfig
50
+ children: React.ComponentProps<
51
+ typeof RechartsPrimitive.ResponsiveContainer
52
+ >["children"]
53
+ initialDimension?: {
54
+ width: number
55
+ height: number
56
+ }
57
+ }) {
58
+ const uniqueId = React.useId()
59
+ const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`
60
+
61
+ return (
62
+ <ChartContext.Provider value={{ config }}>
63
+ <div
64
+ data-slot="chart"
65
+ data-chart={chartId}
66
+ className={cn(
67
+ "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
68
+ className
69
+ )}
70
+ {...props}
71
+ >
72
+ <ChartStyle id={chartId} config={config} />
73
+ <RechartsPrimitive.ResponsiveContainer
74
+ initialDimension={initialDimension}
75
+ >
76
+ {children}
77
+ </RechartsPrimitive.ResponsiveContainer>
78
+ </div>
79
+ </ChartContext.Provider>
80
+ )
81
+ }
82
+
83
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
84
+ const colorConfig = Object.entries(config).filter(
85
+ ([, config]) => config.theme ?? config.color
86
+ )
87
+
88
+ if (!colorConfig.length) {
89
+ return null
90
+ }
91
+
92
+ return (
93
+ <style
94
+ dangerouslySetInnerHTML={{
95
+ __html: Object.entries(THEMES)
96
+ .map(
97
+ ([theme, prefix]) => `
98
+ ${prefix} [data-chart=${id}] {
99
+ ${colorConfig
100
+ .map(([key, itemConfig]) => {
101
+ const color =
102
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ??
103
+ itemConfig.color
104
+ return color ? ` --color-${key}: ${color};` : null
105
+ })
106
+ .join("\n")}
107
+ }
108
+ `
109
+ )
110
+ .join("\n"),
111
+ }}
112
+ />
113
+ )
114
+ }
115
+
116
+ const ChartTooltip = RechartsPrimitive.Tooltip
117
+
118
+ function ChartTooltipContent({
119
+ active,
120
+ payload,
121
+ className,
122
+ indicator = "dot",
123
+ hideLabel = false,
124
+ hideIndicator = false,
125
+ label,
126
+ labelFormatter,
127
+ labelClassName,
128
+ formatter,
129
+ color,
130
+ nameKey,
131
+ labelKey,
132
+ }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
133
+ React.ComponentProps<"div"> & {
134
+ hideLabel?: boolean
135
+ hideIndicator?: boolean
136
+ indicator?: "line" | "dot" | "dashed"
137
+ nameKey?: string
138
+ labelKey?: string
139
+ } & Omit<
140
+ RechartsPrimitive.DefaultTooltipContentProps<
141
+ TooltipValueType,
142
+ TooltipNameType
143
+ >,
144
+ "accessibilityLayer"
145
+ >) {
146
+ const { config } = useChart()
147
+
148
+ const tooltipLabel = React.useMemo(() => {
149
+ if (hideLabel || !payload?.length) {
150
+ return null
151
+ }
152
+
153
+ const [item] = payload
154
+ const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`
155
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
156
+ const value =
157
+ !labelKey && typeof label === "string"
158
+ ? (config[label]?.label ?? label)
159
+ : itemConfig?.label
160
+
161
+ if (labelFormatter) {
162
+ return (
163
+ <div className={cn("font-medium", labelClassName)}>
164
+ {labelFormatter(value, payload)}
165
+ </div>
166
+ )
167
+ }
168
+
169
+ if (!value) {
170
+ return null
171
+ }
172
+
173
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>
174
+ }, [
175
+ label,
176
+ labelFormatter,
177
+ payload,
178
+ hideLabel,
179
+ labelClassName,
180
+ config,
181
+ labelKey,
182
+ ])
183
+
184
+ if (!active || !payload?.length) {
185
+ return null
186
+ }
187
+
188
+ const nestLabel = payload.length === 1 && indicator !== "dot"
189
+
190
+ return (
191
+ <div
192
+ className={cn(
193
+ "grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
194
+ className
195
+ )}
196
+ >
197
+ {!nestLabel ? tooltipLabel : null}
198
+ <div className="grid gap-1.5">
199
+ {payload
200
+ .filter((item) => item.type !== "none")
201
+ .map((item, index) => {
202
+ const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`
203
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
204
+ const indicatorColor = color ?? item.payload?.fill ?? item.color
205
+
206
+ return (
207
+ <div
208
+ key={index}
209
+ className={cn(
210
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
211
+ indicator === "dot" && "items-center"
212
+ )}
213
+ >
214
+ {formatter && item?.value !== undefined && item.name ? (
215
+ formatter(item.value, item.name, item, index, item.payload)
216
+ ) : (
217
+ <>
218
+ {itemConfig?.icon ? (
219
+ <itemConfig.icon />
220
+ ) : (
221
+ !hideIndicator && (
222
+ <div
223
+ className={cn(
224
+ "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
225
+ {
226
+ "h-2.5 w-2.5": indicator === "dot",
227
+ "w-1": indicator === "line",
228
+ "w-0 border-[1.5px] border-dashed bg-transparent":
229
+ indicator === "dashed",
230
+ "my-0.5": nestLabel && indicator === "dashed",
231
+ }
232
+ )}
233
+ style={
234
+ {
235
+ "--color-bg": indicatorColor,
236
+ "--color-border": indicatorColor,
237
+ } as React.CSSProperties
238
+ }
239
+ />
240
+ )
241
+ )}
242
+ <div
243
+ className={cn(
244
+ "flex flex-1 justify-between leading-none",
245
+ nestLabel ? "items-end" : "items-center"
246
+ )}
247
+ >
248
+ <div className="grid gap-1.5">
249
+ {nestLabel ? tooltipLabel : null}
250
+ <span className="text-muted-foreground">
251
+ {itemConfig?.label ?? item.name}
252
+ </span>
253
+ </div>
254
+ {item.value != null && (
255
+ <span className="font-mono font-medium text-foreground tabular-nums">
256
+ {typeof item.value === "number"
257
+ ? item.value.toLocaleString()
258
+ : String(item.value)}
259
+ </span>
260
+ )}
261
+ </div>
262
+ </>
263
+ )}
264
+ </div>
265
+ )
266
+ })}
267
+ </div>
268
+ </div>
269
+ )
270
+ }
271
+
272
+ const ChartLegend = RechartsPrimitive.Legend
273
+
274
+ function ChartLegendContent({
275
+ className,
276
+ hideIcon = false,
277
+ payload,
278
+ verticalAlign = "bottom",
279
+ nameKey,
280
+ }: React.ComponentProps<"div"> & {
281
+ hideIcon?: boolean
282
+ nameKey?: string
283
+ } & RechartsPrimitive.DefaultLegendContentProps) {
284
+ const { config } = useChart()
285
+
286
+ if (!payload?.length) {
287
+ return null
288
+ }
289
+
290
+ return (
291
+ <div
292
+ className={cn(
293
+ "flex items-center justify-center gap-4",
294
+ verticalAlign === "top" ? "pb-3" : "pt-3",
295
+ className
296
+ )}
297
+ >
298
+ {payload
299
+ .filter((item) => item.type !== "none")
300
+ .map((item, index) => {
301
+ const key = `${nameKey ?? item.dataKey ?? "value"}`
302
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
303
+
304
+ return (
305
+ <div
306
+ key={index}
307
+ className={cn(
308
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
309
+ )}
310
+ >
311
+ {itemConfig?.icon && !hideIcon ? (
312
+ <itemConfig.icon />
313
+ ) : (
314
+ <div
315
+ className="h-2 w-2 shrink-0 rounded-[2px]"
316
+ style={{
317
+ backgroundColor: item.color,
318
+ }}
319
+ />
320
+ )}
321
+ {itemConfig?.label}
322
+ </div>
323
+ )
324
+ })}
325
+ </div>
326
+ )
327
+ }
328
+
329
+ function getPayloadConfigFromPayload(
330
+ config: ChartConfig,
331
+ payload: unknown,
332
+ key: string
333
+ ) {
334
+ if (typeof payload !== "object" || payload === null) {
335
+ return undefined
336
+ }
337
+
338
+ const payloadPayload =
339
+ "payload" in payload &&
340
+ typeof payload.payload === "object" &&
341
+ payload.payload !== null
342
+ ? payload.payload
343
+ : undefined
344
+
345
+ let configLabelKey: string = key
346
+
347
+ if (
348
+ key in payload &&
349
+ typeof payload[key as keyof typeof payload] === "string"
350
+ ) {
351
+ configLabelKey = payload[key as keyof typeof payload] as string
352
+ } else if (
353
+ payloadPayload &&
354
+ key in payloadPayload &&
355
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
356
+ ) {
357
+ configLabelKey = payloadPayload[
358
+ key as keyof typeof payloadPayload
359
+ ] as string
360
+ }
361
+
362
+ return configLabelKey in config ? config[configLabelKey] : config[key]
363
+ }
364
+
365
+ export {
366
+ ChartContainer,
367
+ ChartTooltip,
368
+ ChartTooltipContent,
369
+ ChartLegend,
370
+ ChartLegendContent,
371
+ ChartStyle,
372
+ }
@@ -75,12 +75,12 @@ function Ticket({
75
75
  className={cn(
76
76
  container_background,
77
77
  border,
78
- "py-[14px] px-[16px] rounded-[12px] border shadow-sm flex flex-col gap-3 cursor-pointer"
78
+ "flex cursor-pointer flex-col gap-3 rounded-[12px] border px-4 py-3.5 shadow-sm"
79
79
  )}
80
80
  >
81
81
  <button type="button" onClick={click} className="w-full text-left">
82
- <div className="flex flex-row items-center justify-between">
83
- <div className="flex flex-row gap-1.5 items-center">
82
+ <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
83
+ <div className="flex flex-wrap items-center gap-1.5">
84
84
  <Avatar
85
85
  className={cn(
86
86
  avatar_background,
@@ -90,7 +90,7 @@ function Ticket({
90
90
  <AvatarFallback
91
91
  className={cn(
92
92
  avatar_background,
93
- "leading-1 flex size-full items-center justify-center text-[12px] text-white font-semibold"
93
+ "leading-1 flex size-full items-center justify-center text-[12px] font-semibold text-white"
94
94
  )}
95
95
  delayMs={600}
96
96
  >
@@ -103,16 +103,20 @@ function Ticket({
103
103
  className={cn(
104
104
  pill_background,
105
105
  pill_text_color,
106
- "py-[3px] px-[10px] rounded-[9999px] text-xs"
106
+ "rounded-[9999px] px-[10px] py-[3px] text-xs"
107
107
  )}
108
108
  >
109
109
  {ticket_type}
110
110
  </p>
111
- <small className="text-[#6b7bbd]">view full email</small>
112
- <ExternalLink size="12px" className="text-[#6B7B8D]" />
111
+ <span className="flex items-center gap-1">
112
+ <small className="text-[#6b7bbd]">view full email</small>
113
+ <ExternalLink size="12px" className="text-[#6B7B8D]" />
114
+ </span>
113
115
  </div>
114
116
 
115
- {time && <p className="text-[#98A9B8] text-[11.5px]">{time} </p>}
117
+ {time && (
118
+ <p className="shrink-0 text-[11.5px] text-[#98A9B8]">{time}</p>
119
+ )}
116
120
  </div>
117
121
  </button>
118
122
 
@@ -123,18 +127,17 @@ function Ticket({
123
127
  <CollapsibleTrigger asChild>
124
128
  <Button
125
129
  variant="ghost"
126
- className="group w-full hover:bg-transparent hover:text-inherit data-[state=open]:bg-transparent justify-start -ml-4"
130
+ className="group -ml-4 w-full justify-start hover:bg-transparent hover:text-inherit data-[state=open]:bg-transparent"
127
131
  >
128
132
  <ChevronRightIcon className="group-data-[state=open]:rotate-90" />
129
133
  Product details
130
134
  </Button>
131
135
  </CollapsibleTrigger>
132
- <CollapsibleContent className="flex flex-col items-start gap-2 p-2.5 pt-0 text-sm max-h-[150px] overflow-y-auto scrollbar scrollbar-thin scrollbar-thumb-rounded-full scrollbar-track-rounded-full">
136
+ <CollapsibleContent className="flex max-h-[150px] flex-col items-start gap-2 overflow-y-auto p-2.5 pt-0 text-sm">
133
137
  {history.map((hs, index) => {
134
138
  return (
135
139
  // biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
136
- <div key={index} className="">
137
- {" "}
140
+ <div key={index}>
138
141
  <div>{hs.label}</div>
139
142
  <div>{hs.value}</div>
140
143
  </div>