@buchorg/ui-core 1.0.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.
Files changed (2) hide show
  1. package/package.json +14 -0
  2. package/src/index.tsx +257 -0
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@buchorg/ui-core",
3
+ "version": "1.0.0",
4
+ "description": "Componentes React compartidos: Tag, StatCard, FlagBar, etc.",
5
+ "main": "src/index.tsx",
6
+ "types": "src/index.tsx",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "dependencies": {
11
+ "@buchorg/plugin-sdk": "*"
12
+ },
13
+ "peerDependencies": { "react": ">=18" }
14
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,257 @@
1
+ // ============================================================
2
+ // @buchorg/ui-core — Componentes React compartidos
3
+ // Agnósticos al país; consumen CSS variables del tema activo.
4
+ // ============================================================
5
+
6
+ import React, { type ReactNode, type HTMLAttributes } from 'react';
7
+
8
+ // -----------------------------------------------------------
9
+ // Tipos comunes
10
+ // -----------------------------------------------------------
11
+ export type TagVariant = 'primary' | 'success' | 'warning' | 'danger' | 'neutral';
12
+
13
+ // -----------------------------------------------------------
14
+ // Tag / Badge
15
+ // -----------------------------------------------------------
16
+ const tagStyles: Record<TagVariant, React.CSSProperties> = {
17
+ primary: { background: 'var(--country-primary)', color: '#fff' },
18
+ success: { background: '#d1fae5', color: '#065f46' },
19
+ warning: { background: '#fef3c7', color: '#92400e' },
20
+ danger: { background: '#fee2e2', color: '#991b1b' },
21
+ neutral: { background: '#f1f5f9', color: '#475569' },
22
+ };
23
+
24
+ interface TagProps { text: string; variant?: TagVariant; className?: string; }
25
+
26
+ export function Tag({ text, variant = 'neutral', className }: TagProps) {
27
+ return (
28
+ <span
29
+ className={className}
30
+ style={{
31
+ display: 'inline-flex', alignItems: 'center',
32
+ fontSize: 11, fontWeight: 600, letterSpacing: '.04em',
33
+ padding: '3px 9px', borderRadius: 20, textTransform: 'uppercase',
34
+ ...tagStyles[variant],
35
+ }}
36
+ >
37
+ {text}
38
+ </span>
39
+ );
40
+ }
41
+
42
+ // -----------------------------------------------------------
43
+ // StatCard — tarjeta de métrica
44
+ // -----------------------------------------------------------
45
+ interface StatCardProps {
46
+ icon: string;
47
+ title: string;
48
+ value: string;
49
+ subtitle?: string;
50
+ accent?: boolean;
51
+ }
52
+
53
+ export function StatCard({ icon, title, value, subtitle, accent }: StatCardProps) {
54
+ return (
55
+ <div
56
+ style={{
57
+ background: 'var(--country-surface)',
58
+ border: `1px solid var(--country-border)`,
59
+ borderLeft: accent ? '4px solid var(--country-primary)' : undefined,
60
+ borderRadius: 12,
61
+ padding: '20px 24px',
62
+ transition: 'transform .2s, box-shadow .2s',
63
+ cursor: 'default',
64
+ fontFamily: 'var(--country-font, inherit)',
65
+ }}
66
+ onMouseEnter={e => {
67
+ (e.currentTarget as HTMLElement).style.transform = 'translateY(-2px)';
68
+ (e.currentTarget as HTMLElement).style.boxShadow =
69
+ '0 8px 24px color-mix(in srgb, var(--country-primary) 14%, transparent)';
70
+ }}
71
+ onMouseLeave={e => {
72
+ (e.currentTarget as HTMLElement).style.transform = '';
73
+ (e.currentTarget as HTMLElement).style.boxShadow = '';
74
+ }}
75
+ >
76
+ <div style={{ fontSize: 28, marginBottom: 12 }}>{icon}</div>
77
+ <div style={{ fontSize: 12, fontWeight: 500, color: 'var(--country-text-secondary)', marginBottom: 4 }}>
78
+ {title}
79
+ </div>
80
+ <div style={{ fontSize: 26, fontWeight: 700, color: 'var(--country-text)', lineHeight: 1.1 }}>
81
+ {value}
82
+ </div>
83
+ {subtitle && (
84
+ <div style={{ fontSize: 12, color: 'var(--country-text-secondary)', marginTop: 4 }}>
85
+ {subtitle}
86
+ </div>
87
+ )}
88
+ </div>
89
+ );
90
+ }
91
+
92
+ // -----------------------------------------------------------
93
+ // SectionTitle — título de sección con línea decorativa
94
+ // -----------------------------------------------------------
95
+ interface SectionTitleProps {
96
+ children: ReactNode;
97
+ accentLeft?: boolean;
98
+ style?: React.CSSProperties;
99
+ }
100
+
101
+ export function SectionTitle({ children, accentLeft, style }: SectionTitleProps) {
102
+ return (
103
+ <div
104
+ style={{
105
+ fontSize: 11, fontWeight: 700, letterSpacing: '.1em',
106
+ textTransform: 'uppercase', color: 'var(--country-primary)',
107
+ marginBottom: 14, display: 'flex', alignItems: 'center', gap: 8,
108
+ ...(accentLeft ? {
109
+ paddingLeft: 10,
110
+ borderLeft: '3px solid var(--country-accent)',
111
+ } : {}),
112
+ ...style,
113
+ }}
114
+ >
115
+ {children}
116
+ {!accentLeft && (
117
+ <span style={{ flex: 1, height: 1, background: 'color-mix(in srgb, var(--country-primary) 20%, transparent)' }} />
118
+ )}
119
+ </div>
120
+ );
121
+ }
122
+
123
+ // -----------------------------------------------------------
124
+ // FlagBar — franja de bandera configurable
125
+ // -----------------------------------------------------------
126
+ interface FlagBarProps {
127
+ colors: [string, string, string];
128
+ weights: [number, number, number];
129
+ height?: number;
130
+ }
131
+
132
+ export function FlagBar({ colors, weights, height = 6 }: FlagBarProps) {
133
+ const [w1, w2] = weights;
134
+ return (
135
+ <div style={{
136
+ height,
137
+ background: `linear-gradient(to right,
138
+ ${colors[0]} ${w1}%,
139
+ ${colors[1]} ${w1}% ${w1 + w2}%,
140
+ ${colors[2]} ${w1 + w2}%
141
+ )`,
142
+ }} />
143
+ );
144
+ }
145
+
146
+ // -----------------------------------------------------------
147
+ // TaxInfoBox — caja de información tributaria
148
+ // -----------------------------------------------------------
149
+ interface TaxInfoBoxProps {
150
+ icon: string;
151
+ label: string;
152
+ value: string;
153
+ borderColor: string;
154
+ bgColor: string;
155
+ }
156
+
157
+ export function TaxInfoBox({ icon, label, value, borderColor, bgColor }: TaxInfoBoxProps) {
158
+ return (
159
+ <div style={{
160
+ marginTop: 28, background: bgColor,
161
+ border: `1px solid color-mix(in srgb, ${borderColor} 30%, transparent)`,
162
+ borderLeft: `4px solid ${borderColor}`,
163
+ borderRadius: 10, padding: '16px 20px',
164
+ display: 'flex', alignItems: 'center', gap: 16,
165
+ }}>
166
+ <span style={{ fontSize: 28 }}>{icon}</span>
167
+ <div>
168
+ <div style={{ fontSize: 13, color: 'var(--country-text-secondary)' }}>{label}</div>
169
+ <div style={{ fontSize: 18, fontWeight: 700, color: borderColor }}>{value}</div>
170
+ </div>
171
+ </div>
172
+ );
173
+ }
174
+
175
+ // -----------------------------------------------------------
176
+ // OrdersTable — tabla genérica de pedidos
177
+ // -----------------------------------------------------------
178
+ export interface OrderRow {
179
+ id: string;
180
+ customer: string;
181
+ amount: string; // ya formateado
182
+ status: TagVariant;
183
+ statusText: string;
184
+ docType: string;
185
+ }
186
+
187
+ interface OrdersTableProps {
188
+ rows: OrderRow[];
189
+ headerBg: string; // color de fondo de cabecera
190
+ idColor: string;
191
+ docBg: string;
192
+ docColor: string;
193
+ docBorder: string;
194
+ }
195
+
196
+ export function OrdersTable({ rows, headerBg, idColor, docBg, docColor, docBorder }: OrdersTableProps) {
197
+ return (
198
+ <div style={{ overflowX: 'auto', background: 'var(--country-surface)', borderRadius: 12, border: '1px solid var(--country-border)' }}>
199
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
200
+ <thead>
201
+ <tr>
202
+ {['N° Pedido', 'Cliente', 'Comprobante', 'Monto', 'Estado'].map((h, i) => (
203
+ <th key={h} style={{
204
+ background: headerBg, color: '#fff',
205
+ fontSize: 11, fontWeight: 700, letterSpacing: '.06em',
206
+ textTransform: 'uppercase', padding: '11px 14px', textAlign: 'left',
207
+ borderRadius: i === 0 ? '8px 0 0 0' : i === 4 ? '0 8px 0 0' : undefined,
208
+ }}>{h}</th>
209
+ ))}
210
+ </tr>
211
+ </thead>
212
+ <tbody>
213
+ {rows.map((row, idx) => (
214
+ <tr key={row.id} style={{ background: idx % 2 === 1 ? 'color-mix(in srgb, var(--country-primary) 2%, transparent)' : undefined }}>
215
+ <td style={{ padding: '12px 14px', borderBottom: '1px solid var(--country-border)' }}>
216
+ <span style={{ fontFamily: 'monospace', fontSize: 12, fontWeight: 700, color: idColor }}>{row.id}</span>
217
+ </td>
218
+ <td style={{ padding: '12px 14px', fontSize: 13, borderBottom: '1px solid var(--country-border)', color: 'var(--country-text)' }}>{row.customer}</td>
219
+ <td style={{ padding: '12px 14px', borderBottom: '1px solid var(--country-border)' }}>
220
+ <span style={{
221
+ fontSize: 10, fontWeight: 600, padding: '2px 7px', borderRadius: 4,
222
+ background: docBg, color: docColor,
223
+ border: `1px solid ${docBorder}`,
224
+ textTransform: 'uppercase', letterSpacing: '.04em',
225
+ }}>{row.docType}</span>
226
+ </td>
227
+ <td style={{ padding: '12px 14px', fontSize: 13, fontWeight: 600, borderBottom: '1px solid var(--country-border)', color: 'var(--country-text)' }}>{row.amount}</td>
228
+ <td style={{ padding: '12px 14px', borderBottom: '1px solid var(--country-border)' }}>
229
+ <Tag text={row.statusText} variant={row.status} />
230
+ </td>
231
+ </tr>
232
+ ))}
233
+ </tbody>
234
+ </table>
235
+ </div>
236
+ );
237
+ }
238
+
239
+ // -----------------------------------------------------------
240
+ // Skeleton
241
+ // -----------------------------------------------------------
242
+ interface SkeletonProps extends HTMLAttributes<HTMLDivElement> { lines?: number; }
243
+
244
+ export function Skeleton({ lines = 3, style, ...rest }: SkeletonProps) {
245
+ return (
246
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: '12px 0', ...style }} {...rest}>
247
+ {Array.from({ length: lines }).map((_, i) => (
248
+ <div key={i} style={{
249
+ height: 14, borderRadius: 4,
250
+ width: `${70 + Math.random() * 28}%`,
251
+ background: 'var(--country-surface)',
252
+ animation: 'skeleton-shimmer 1.4s infinite',
253
+ }} />
254
+ ))}
255
+ </div>
256
+ );
257
+ }