@attrus-ds/core 0.2.0 → 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.
- package/LICENSE +27 -27
- package/dist/assets/flags/Brazil-rounded.svg +15 -15
- package/dist/assets/flags/Brazil-square.svg +16 -16
- package/dist/assets/flags/Chile-rounded.svg +10 -10
- package/dist/assets/flags/Chile-square.svg +10 -10
- package/dist/assets/flags/Colombia-rounded.svg +9 -9
- package/dist/assets/flags/Colombia-square.svg +9 -9
- package/dist/assets/flags/EUA-rounded.svg +10 -10
- package/dist/assets/flags/EUA-square.svg +10 -10
- package/dist/assets/flags/EUR-rounded.svg +8 -8
- package/dist/assets/flags/EUR-square.svg +8 -8
- package/dist/assets/flags/Mexico-rounded.svg +18 -18
- package/dist/assets/flags/Mexico-square.svg +18 -18
- package/dist/assets/icon-attrus-dark.svg +2 -2
- package/dist/assets/icon-attrus-light.svg +2 -2
- package/dist/assets/logo-attrus-dark.svg +7 -7
- package/dist/assets/logo-attrus-light.svg +7 -7
- package/dist/css/components/buttons.css +301 -301
- package/dist/css/components/callout.css +195 -195
- package/dist/css/components/cards.css +272 -272
- package/dist/css/components/combobox.css +319 -319
- package/dist/css/components/empty-states.css +61 -61
- package/dist/css/components/inputs.css +975 -975
- package/dist/css/components/number-input.css +138 -138
- package/dist/css/components/overlays.css +374 -374
- package/dist/css/components/primitives.css +314 -314
- package/dist/css/components/sidebar.css +179 -179
- package/dist/css/components/smart-search.css +91 -91
- package/dist/css/components/status-pills.css +207 -207
- package/dist/css/components/stepper.css +309 -309
- package/dist/css/components/table.css +602 -602
- package/dist/css/components/tabs.css +319 -319
- package/dist/css/components/toast.css +243 -243
- package/dist/css/components/tooltip.css +231 -231
- package/dist/css/components/topbar.css +148 -148
- package/dist/css/tokens.css +713 -666
- package/dist/react/AppSidebar/AppSidebar.d.ts +50 -50
- package/dist/react/AppSidebar/AppSidebar.tsx +197 -197
- package/dist/react/AppTopbar/AppTopbar.d.ts +58 -58
- package/dist/react/AppTopbar/AppTopbar.tsx +203 -203
- package/dist/react/Atoms/Atoms.d.ts +149 -149
- package/dist/react/Atoms/Atoms.tsx +407 -407
- package/dist/react/Button/Button.d.ts +25 -25
- package/dist/react/Button/Button.tsx +83 -83
- package/dist/react/Callout/Callout.d.ts +29 -29
- package/dist/react/Callout/Callout.tsx +98 -98
- package/dist/react/Card/Card.d.ts +38 -38
- package/dist/react/Card/Card.tsx +93 -93
- package/dist/react/Combobox/Combobox.d.ts +52 -52
- package/dist/react/Combobox/Combobox.tsx +254 -254
- package/dist/react/DataTable/DataTable.d.ts +81 -81
- package/dist/react/DataTable/DataTable.tsx +308 -308
- package/dist/react/Drawer/Drawer.d.ts +35 -35
- package/dist/react/Drawer/Drawer.tsx +226 -226
- package/dist/react/Input/Input.d.ts +155 -155
- package/dist/react/Input/Input.tsx +478 -478
- package/dist/react/Modal/Modal.d.ts +27 -27
- package/dist/react/Modal/Modal.tsx +173 -173
- package/dist/react/NumberInput/NumberInput.d.ts +25 -25
- package/dist/react/NumberInput/NumberInput.tsx +117 -117
- package/dist/react/Pill/Pill.d.ts +31 -31
- package/dist/react/Pill/Pill.tsx +104 -104
- package/dist/react/SearchPill/SearchPill.d.ts +14 -14
- package/dist/react/SearchPill/SearchPill.tsx +40 -40
- package/dist/react/SmartSearch/SmartSearch.d.ts +65 -65
- package/dist/react/SmartSearch/SmartSearch.tsx +271 -271
- package/dist/react/Stepper/Stepper.d.ts +25 -25
- package/dist/react/Stepper/Stepper.tsx +94 -94
- package/dist/react/Tabs/Tabs.d.ts +32 -32
- package/dist/react/Tabs/Tabs.tsx +147 -147
- package/dist/react/Toast/Toast.d.ts +30 -30
- package/dist/react/Toast/Toast.tsx +94 -94
- package/dist/react/Tooltip/Tooltip.d.ts +23 -23
- package/dist/react/Tooltip/Tooltip.tsx +80 -80
- package/dist/react/ZeroState/ZeroState.d.ts +17 -17
- package/dist/react/ZeroState/ZeroState.tsx +45 -45
- package/dist/react/index.ts +35 -35
- package/dist/tokens/attrus-tokens.flat.json +1234 -1234
- package/dist/tokens/attrus-tokens.json +1089 -1089
- package/package.json +66 -64
|
@@ -1,407 +1,407 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ATTRUS Atoms — typed wrappers over primitives.css atoms.
|
|
5
|
-
* Money · Metric · Avatar/AvatarGroup · Eyebrow · Flag · Icon.
|
|
6
|
-
* CSS stays the visual source of truth; these compose class names.
|
|
7
|
-
* (Module named Atoms — "Primitives" collides with the legacy
|
|
8
|
-
* ui_kits/dashboard/Primitives.jsx still consumed by the app.)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/* ---------- Money — mono, inline (tables, rows, receipts) ---------- */
|
|
12
|
-
|
|
13
|
-
export type MoneySize = 'sm' | 'md' | 'lg' | 'xl' | 'display';
|
|
14
|
-
export type MoneyTone = 'pos' | 'neg' | 'muted';
|
|
15
|
-
|
|
16
|
-
export interface MoneyProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
17
|
-
/** Currency code, dimmed uppercase prefix (e.g. "USD"). */
|
|
18
|
-
cur?: string;
|
|
19
|
-
/** Main figure including separators (e.g. "1,000"). */
|
|
20
|
-
amount: React.ReactNode;
|
|
21
|
-
/** Dimmed decimals, rendered after the amount (e.g. ".00"). */
|
|
22
|
-
frac?: React.ReactNode;
|
|
23
|
-
size?: MoneySize;
|
|
24
|
-
tone?: MoneyTone;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const Money: React.FC<MoneyProps> = ({ cur, amount, frac, size = 'md', tone, className, ...rest }) => (
|
|
28
|
-
<span
|
|
29
|
-
className={['money', size !== 'md' ? `money-${size}` : '', tone ? `money-${tone}` : '', className || '']
|
|
30
|
-
.filter(Boolean)
|
|
31
|
-
.join(' ')}
|
|
32
|
-
{...rest}
|
|
33
|
-
>
|
|
34
|
-
{cur ? <span className="money-cur">{cur}</span> : null}
|
|
35
|
-
{amount}
|
|
36
|
-
{frac != null ? <span className="money-frac">{frac}</span> : null}
|
|
37
|
-
</span>
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
/* ---------- Metric — sans display number (heroes, KPIs) ---------- */
|
|
41
|
-
|
|
42
|
-
export type MetricSize = 'sm' | 'md' | 'lg';
|
|
43
|
-
export type MetricTone = 'pos' | 'neg';
|
|
44
|
-
|
|
45
|
-
export interface MetricProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
46
|
-
/** Currency code, dimmed prefix. */
|
|
47
|
-
cur?: string;
|
|
48
|
-
/** Main figure (include the trailing dot when using frac: "48,920."). */
|
|
49
|
-
amount: React.ReactNode;
|
|
50
|
-
/** Dimmed decimals (e.g. "56"). */
|
|
51
|
-
frac?: React.ReactNode;
|
|
52
|
-
size?: MetricSize;
|
|
53
|
-
/** Colour inherits by default (works on light + inverse). */
|
|
54
|
-
tone?: MetricTone;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export const Metric: React.FC<MetricProps> = ({ cur, amount, frac, size = 'md', tone, className, ...rest }) => (
|
|
58
|
-
<span
|
|
59
|
-
className={['metric', size !== 'md' ? `metric-${size}` : '', tone ? `metric-${tone}` : '', className || '']
|
|
60
|
-
.filter(Boolean)
|
|
61
|
-
.join(' ')}
|
|
62
|
-
{...rest}
|
|
63
|
-
>
|
|
64
|
-
{cur ? <span className="metric-cur">{cur}</span> : null}
|
|
65
|
-
{amount}
|
|
66
|
-
{frac != null ? <span className="metric-frac">{frac}</span> : null}
|
|
67
|
-
</span>
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
/* ---------- Avatar ---------- */
|
|
71
|
-
|
|
72
|
-
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
73
|
-
export type AvatarTone = 'brand' | 'success' | 'warning' | 'danger' | 'info' | 'neutral';
|
|
74
|
-
export type AvatarDot = boolean | 'busy' | 'away' | 'off';
|
|
75
|
-
|
|
76
|
-
export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
77
|
-
/** Full name — initials are derived (first + last word). */
|
|
78
|
-
name?: string;
|
|
79
|
-
/** Image avatar; falls back to initials while absent. */
|
|
80
|
-
src?: string;
|
|
81
|
-
size?: AvatarSize;
|
|
82
|
-
tone?: AvatarTone;
|
|
83
|
-
/** Presence dot: true=online, 'busy' | 'away' | 'off'. */
|
|
84
|
-
dot?: AvatarDot;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function initials(name?: string): string {
|
|
88
|
-
if (!name) return '';
|
|
89
|
-
const parts = name.trim().split(/\s+/);
|
|
90
|
-
const first = parts[0] ? parts[0].charAt(0) : '';
|
|
91
|
-
const last = parts.length > 1 ? parts[parts.length - 1].charAt(0) : '';
|
|
92
|
-
return (first + last).toUpperCase();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export const Avatar: React.FC<AvatarProps> = ({ name, src, size = 'md', tone = 'brand', dot, className, children, ...rest }) => (
|
|
96
|
-
<span
|
|
97
|
-
className={['av', `av-${size}`, `av-${tone}`, className || ''].filter(Boolean).join(' ')}
|
|
98
|
-
aria-label={name}
|
|
99
|
-
{...rest}
|
|
100
|
-
>
|
|
101
|
-
{src ? <img src={src} alt={name || ''} /> : children != null ? children : initials(name)}
|
|
102
|
-
{dot ? <span className={['av-dot', typeof dot === 'string' ? `av-dot-${dot}` : ''].filter(Boolean).join(' ')} /> : null}
|
|
103
|
-
</span>
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
export interface AvatarGroupProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
107
|
-
/** Render "+N" overflow chip after the avatars. */
|
|
108
|
-
more?: number;
|
|
109
|
-
/** Size applied to the overflow chip. */
|
|
110
|
-
size?: AvatarSize;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export const AvatarGroup: React.FC<AvatarGroupProps> = ({ more, size = 'md', className, children, ...rest }) => (
|
|
114
|
-
<span className={['av-group', className || ''].filter(Boolean).join(' ')} {...rest}>
|
|
115
|
-
{children}
|
|
116
|
-
{more ? <span className={`av av-${size} av-more`}>+{more}</span> : null}
|
|
117
|
-
</span>
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
/* ---------- Eyebrow — uppercase micro-label ---------- */
|
|
121
|
-
|
|
122
|
-
export type EyebrowSize = 'xs' | 'sm' | 'md';
|
|
123
|
-
export type EyebrowTone = 'muted' | 'secondary' | 'default' | 'brand' | 'inverse';
|
|
124
|
-
|
|
125
|
-
export interface EyebrowProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
126
|
-
size?: EyebrowSize;
|
|
127
|
-
tone?: EyebrowTone;
|
|
128
|
-
/** Inline icon — adds .has-icon for the flex layout. */
|
|
129
|
-
icon?: React.ReactNode;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export const Eyebrow: React.FC<EyebrowProps> = ({ size = 'md', tone = 'muted', icon, className, children, ...rest }) => (
|
|
133
|
-
<span
|
|
134
|
-
className={['eyebrow', `eyebrow-${size}`, `eyebrow-${tone}`, icon != null ? 'has-icon' : '', className || '']
|
|
135
|
-
.filter(Boolean)
|
|
136
|
-
.join(' ')}
|
|
137
|
-
{...rest}
|
|
138
|
-
>
|
|
139
|
-
{icon}
|
|
140
|
-
{children}
|
|
141
|
-
</span>
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
/* ---------- Flag — brand flag SVGs from assets/flags ---------- */
|
|
145
|
-
|
|
146
|
-
export type FlagCountry = 'BR' | 'MX' | 'CO' | 'CL' | 'US' | 'EU';
|
|
147
|
-
export type FlagShape = 'rounded' | 'square';
|
|
148
|
-
|
|
149
|
-
const FLAG_FILE: Record<FlagCountry, string> = {
|
|
150
|
-
BR: 'Brazil',
|
|
151
|
-
MX: 'Mexico',
|
|
152
|
-
CO: 'Colombia',
|
|
153
|
-
CL: 'Chile',
|
|
154
|
-
US: 'EUA',
|
|
155
|
-
EU: 'EUR',
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
export interface FlagProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> {
|
|
159
|
-
country: FlagCountry;
|
|
160
|
-
/** Pixel size (width = height). Default 16. */
|
|
161
|
-
size?: number;
|
|
162
|
-
/** rounded (circular) | square. Default rounded. */
|
|
163
|
-
shape?: FlagShape;
|
|
164
|
-
/** Base path to assets/flags — default works two levels below root
|
|
165
|
-
(component benches and ui_kits/dashboard). Override elsewhere. */
|
|
166
|
-
assetsBase?: string;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export const Flag: React.FC<FlagProps> = ({ country, size = 16, shape = 'rounded', assetsBase = '../../assets/flags', style, ...rest }) => (
|
|
170
|
-
<img
|
|
171
|
-
src={`${assetsBase}/${FLAG_FILE[country]}-${shape}.svg`}
|
|
172
|
-
alt={country}
|
|
173
|
-
width={size}
|
|
174
|
-
height={size}
|
|
175
|
-
style={{ borderRadius: shape === 'rounded' ? '50%' : 'var(--radius-2xs)', display: 'inline-block', verticalAlign: '-0.18em', ...style }}
|
|
176
|
-
{...rest}
|
|
177
|
-
/>
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
/* ---------- Icon — canonical Lucide glyphs ---------- */
|
|
181
|
-
|
|
182
|
-
export interface IconProps extends React.SVGAttributes<SVGSVGElement> {
|
|
183
|
-
/** Lucide name — kebab-case ("arrow-left-right") or PascalCase. */
|
|
184
|
-
name: string;
|
|
185
|
-
/** Pixel size. Default 18. */
|
|
186
|
-
size?: number;
|
|
187
|
-
/** Stroke width. Default 1.75 (matches the system). */
|
|
188
|
-
stroke?: number;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
type LucideNode = Array<unknown>;
|
|
192
|
-
|
|
193
|
-
function lucideLookup(name: string): LucideNode | null {
|
|
194
|
-
const lucide = (window as unknown as { lucide?: { icons?: Record<string, LucideNode> } }).lucide;
|
|
195
|
-
if (!lucide || !lucide.icons) return null;
|
|
196
|
-
const pascal = name
|
|
197
|
-
.split('-')
|
|
198
|
-
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
199
|
-
.join('');
|
|
200
|
-
return lucide.icons[pascal] || lucide.icons[name] || null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/* Lucide UMD ships two icon-data shapes depending on version:
|
|
204
|
-
- pair list: [['path', {...}], ['line', {...}]] (newer)
|
|
205
|
-
- triple: ['svg', {...attrs}, [['path', {...}], ...]] (0.4xx)
|
|
206
|
-
Render both, recursively (children may themselves be triples). */
|
|
207
|
-
function renderLucideChildren(nodes: unknown, keyPrefix: string): React.ReactNode[] {
|
|
208
|
-
if (!Array.isArray(nodes)) return [];
|
|
209
|
-
return (nodes as Array<unknown>).map((child, i) => {
|
|
210
|
-
if (!Array.isArray(child) || typeof child[0] !== 'string') return null;
|
|
211
|
-
const tag = child[0] as string;
|
|
212
|
-
const attrs = (child[1] && typeof child[1] === 'object' ? child[1] : {}) as Record<string, string>;
|
|
213
|
-
const kids = renderLucideChildren(child[2], `${keyPrefix}${i}-`);
|
|
214
|
-
return React.createElement(tag, { ...attrs, key: `${keyPrefix}${i}` }, kids.length ? kids : undefined);
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export const Icon: React.FC<IconProps> = ({ name, size = 18, stroke = 1.75, ...rest }) => {
|
|
219
|
-
const node = lucideLookup(name);
|
|
220
|
-
if (!node) return <span style={{ display: 'inline-block', width: size, height: size }} aria-hidden="true" />;
|
|
221
|
-
// Triple form: ['svg', attrs, children] — unwrap to the children list.
|
|
222
|
-
const children =
|
|
223
|
-
node.length >= 2 && node[0] === 'svg' ? (node[2] as unknown) : (node as unknown);
|
|
224
|
-
return (
|
|
225
|
-
<svg
|
|
226
|
-
width={size}
|
|
227
|
-
height={size}
|
|
228
|
-
viewBox="0 0 24 24"
|
|
229
|
-
fill="none"
|
|
230
|
-
stroke="currentColor"
|
|
231
|
-
strokeWidth={stroke}
|
|
232
|
-
strokeLinecap="round"
|
|
233
|
-
strokeLinejoin="round"
|
|
234
|
-
aria-hidden="true"
|
|
235
|
-
{...rest}
|
|
236
|
-
>
|
|
237
|
-
{renderLucideChildren(children, 'n')}
|
|
238
|
-
</svg>
|
|
239
|
-
);
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
/* ---------- Divider ---------- */
|
|
243
|
-
|
|
244
|
-
export type DividerWeight = 'default' | 'subtle' | 'strong';
|
|
245
|
-
|
|
246
|
-
export interface DividerProps extends React.HTMLAttributes<HTMLElement> {
|
|
247
|
-
weight?: DividerWeight;
|
|
248
|
-
dashed?: boolean;
|
|
249
|
-
/** Vertical hairline for inline rows (1px × 1em). */
|
|
250
|
-
vertical?: boolean;
|
|
251
|
-
/** Midline label — renders the `.divider-labelled` layout. */
|
|
252
|
-
label?: React.ReactNode;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export const Divider: React.FC<DividerProps> = ({ weight = 'default', dashed = false, vertical = false, label, className, ...rest }) => {
|
|
256
|
-
const mods = [weight !== 'default' ? `divider-${weight}` : '', dashed ? 'divider-dashed' : '', className || ''];
|
|
257
|
-
if (label != null) {
|
|
258
|
-
return (
|
|
259
|
-
<div className={['divider-labelled', ...mods].filter(Boolean).join(' ')} {...rest}>
|
|
260
|
-
<span>{label}</span>
|
|
261
|
-
</div>
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
if (vertical) return <span className={['divider-vertical', ...mods].filter(Boolean).join(' ')} {...(rest as React.HTMLAttributes<HTMLSpanElement>)} />;
|
|
265
|
-
return <hr className={['divider', ...mods].filter(Boolean).join(' ')} {...(rest as React.HTMLAttributes<HTMLHRElement>)} />;
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
/* ---------- FilterChip — pill button for filter rows (40px) ---------- */
|
|
269
|
-
|
|
270
|
-
export interface FilterChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
271
|
-
/** Selected state (aria-pressed + active styling from CSS). */
|
|
272
|
-
selected?: boolean;
|
|
273
|
-
icon?: React.ReactNode;
|
|
274
|
-
/** Trailing dropdown indicator (`.chip-chevron`). */
|
|
275
|
-
chevron?: boolean;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export const FilterChip: React.FC<FilterChipProps> = ({ selected = false, icon, chevron = false, className, children, ...rest }) => (
|
|
279
|
-
<button type="button" className={['filter-chip', className || ''].filter(Boolean).join(' ')} aria-pressed={selected} {...rest}>
|
|
280
|
-
{icon}
|
|
281
|
-
{children}
|
|
282
|
-
{chevron ? (
|
|
283
|
-
<svg className="chip-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
284
|
-
<polyline points="6 9 12 15 18 9" />
|
|
285
|
-
</svg>
|
|
286
|
-
) : null}
|
|
287
|
-
</button>
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
/* ---------- ProgressBar — slim track + data-driven fill ---------- */
|
|
291
|
-
|
|
292
|
-
export interface ProgressBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
293
|
-
/** 0–100. */
|
|
294
|
-
value: number;
|
|
295
|
-
/** Fill color — any token/var; defaults to the CSS fallback. */
|
|
296
|
-
fill?: string;
|
|
297
|
-
/** Track height px — feeds `--progress-h` (default 6). */
|
|
298
|
-
height?: number;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export const ProgressBar: React.FC<ProgressBarProps> = ({ value, fill, height, style, className, ...rest }) => (
|
|
302
|
-
<div
|
|
303
|
-
className={['progress', className || ''].filter(Boolean).join(' ')}
|
|
304
|
-
role="progressbar"
|
|
305
|
-
aria-valuenow={Math.round(value)}
|
|
306
|
-
aria-valuemin={0}
|
|
307
|
-
aria-valuemax={100}
|
|
308
|
-
style={{ ...(height != null ? ({ '--progress-h': `${height}px` } as React.CSSProperties) : null), ...style }}
|
|
309
|
-
{...rest}
|
|
310
|
-
>
|
|
311
|
-
<span className="progress-fill" style={{ width: `${Math.max(0, Math.min(100, value))}%`, ...(fill ? { background: fill } : null) }} />
|
|
312
|
-
</div>
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
/* ---------- DescTerm — labelled term/definition pair ---------- */
|
|
316
|
-
|
|
317
|
-
export interface DescTermProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
318
|
-
label: React.ReactNode;
|
|
319
|
-
children?: React.ReactNode;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
export const DescTerm: React.FC<DescTermProps> = ({ label, className, children, ...rest }) => (
|
|
323
|
-
<div className={['desc-term', className || ''].filter(Boolean).join(' ')} {...rest}>
|
|
324
|
-
<span className="desc-term-label">{label}</span>
|
|
325
|
-
<span className="desc-term-value">{children}</span>
|
|
326
|
-
</div>
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
/* ---------- Stat — standalone KPI tile ---------- */
|
|
330
|
-
|
|
331
|
-
export type StatDeltaDirection = 'up' | 'down' | 'flat';
|
|
332
|
-
|
|
333
|
-
export interface StatProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
334
|
-
/** Mono overline label. */
|
|
335
|
-
label: React.ReactNode;
|
|
336
|
-
/** Big tabular figure. */
|
|
337
|
-
value: React.ReactNode;
|
|
338
|
-
/** Delta text, e.g. "+8.2%". Arrow glyph derives from deltaDirection. */
|
|
339
|
-
delta?: React.ReactNode;
|
|
340
|
-
/** up=success ↑ · down=danger ↓ · flat=muted. Default: 'flat'. */
|
|
341
|
-
deltaDirection?: StatDeltaDirection;
|
|
342
|
-
/** Footnote line (.stat-foot) — context under the figure. */
|
|
343
|
-
foot?: React.ReactNode;
|
|
344
|
-
/** Leading icon beside the label. */
|
|
345
|
-
icon?: React.ReactNode;
|
|
346
|
-
/** No tile chrome (bg/border/padding) — for use INSIDE a Card. */
|
|
347
|
-
bare?: boolean;
|
|
348
|
-
/** Dense paddings (.stat-compact). */
|
|
349
|
-
compact?: boolean;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const STAT_ARROW: Record<StatDeltaDirection, string> = { up: '↑ ', down: '↓ ', flat: '' };
|
|
353
|
-
|
|
354
|
-
export const Stat: React.FC<StatProps> = ({
|
|
355
|
-
label,
|
|
356
|
-
value,
|
|
357
|
-
delta,
|
|
358
|
-
deltaDirection = 'flat',
|
|
359
|
-
foot,
|
|
360
|
-
icon,
|
|
361
|
-
bare = false,
|
|
362
|
-
compact = false,
|
|
363
|
-
className,
|
|
364
|
-
children,
|
|
365
|
-
...rest
|
|
366
|
-
}) => (
|
|
367
|
-
<div
|
|
368
|
-
className={['stat', bare ? 'is-bare' : '', compact ? 'stat-compact' : '', className || ''].filter(Boolean).join(' ')}
|
|
369
|
-
{...rest}
|
|
370
|
-
>
|
|
371
|
-
<span className="stat-label" style={icon != null ? { display: 'inline-flex', alignItems: 'center', gap: 'var(--space-2)' } : undefined}>
|
|
372
|
-
{icon}
|
|
373
|
-
{label}
|
|
374
|
-
</span>
|
|
375
|
-
<span style={{ display: 'flex', alignItems: 'baseline', gap: 'var(--space-3)' }}>
|
|
376
|
-
<span className="stat-value">{value}</span>
|
|
377
|
-
{delta != null ? (
|
|
378
|
-
<span className={`stat-delta stat-delta-${deltaDirection}`}>
|
|
379
|
-
{STAT_ARROW[deltaDirection]}
|
|
380
|
-
{delta}
|
|
381
|
-
</span>
|
|
382
|
-
) : null}
|
|
383
|
-
</span>
|
|
384
|
-
{foot != null ? <div className="stat-foot">{foot}</div> : null}
|
|
385
|
-
{children}
|
|
386
|
-
</div>
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
export interface StatRowProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
390
|
-
/** Equal columns — feeds `--stat-cols`. Default 3. */
|
|
391
|
-
cols?: number;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export const StatRow: React.FC<StatRowProps> = ({ cols, className, style, children, ...rest }) => (
|
|
395
|
-
<div
|
|
396
|
-
className={['stat-row', className || ''].filter(Boolean).join(' ')}
|
|
397
|
-
style={cols != null ? ({ '--stat-cols': cols, ...style } as React.CSSProperties) : style}
|
|
398
|
-
{...rest}
|
|
399
|
-
>
|
|
400
|
-
{children}
|
|
401
|
-
</div>
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
/* Collection export — the bundler requires the module-name symbol. */
|
|
405
|
-
export const Atoms = { Money, Metric, Avatar, AvatarGroup, Eyebrow, Flag, Icon, Divider, FilterChip, ProgressBar, DescTerm, Stat, StatRow };
|
|
406
|
-
|
|
407
|
-
export default Atoms;
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ATTRUS Atoms — typed wrappers over primitives.css atoms.
|
|
5
|
+
* Money · Metric · Avatar/AvatarGroup · Eyebrow · Flag · Icon.
|
|
6
|
+
* CSS stays the visual source of truth; these compose class names.
|
|
7
|
+
* (Module named Atoms — "Primitives" collides with the legacy
|
|
8
|
+
* ui_kits/dashboard/Primitives.jsx still consumed by the app.)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/* ---------- Money — mono, inline (tables, rows, receipts) ---------- */
|
|
12
|
+
|
|
13
|
+
export type MoneySize = 'sm' | 'md' | 'lg' | 'xl' | 'display';
|
|
14
|
+
export type MoneyTone = 'pos' | 'neg' | 'muted';
|
|
15
|
+
|
|
16
|
+
export interface MoneyProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
17
|
+
/** Currency code, dimmed uppercase prefix (e.g. "USD"). */
|
|
18
|
+
cur?: string;
|
|
19
|
+
/** Main figure including separators (e.g. "1,000"). */
|
|
20
|
+
amount: React.ReactNode;
|
|
21
|
+
/** Dimmed decimals, rendered after the amount (e.g. ".00"). */
|
|
22
|
+
frac?: React.ReactNode;
|
|
23
|
+
size?: MoneySize;
|
|
24
|
+
tone?: MoneyTone;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Money: React.FC<MoneyProps> = ({ cur, amount, frac, size = 'md', tone, className, ...rest }) => (
|
|
28
|
+
<span
|
|
29
|
+
className={['money', size !== 'md' ? `money-${size}` : '', tone ? `money-${tone}` : '', className || '']
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(' ')}
|
|
32
|
+
{...rest}
|
|
33
|
+
>
|
|
34
|
+
{cur ? <span className="money-cur">{cur}</span> : null}
|
|
35
|
+
{amount}
|
|
36
|
+
{frac != null ? <span className="money-frac">{frac}</span> : null}
|
|
37
|
+
</span>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
/* ---------- Metric — sans display number (heroes, KPIs) ---------- */
|
|
41
|
+
|
|
42
|
+
export type MetricSize = 'sm' | 'md' | 'lg';
|
|
43
|
+
export type MetricTone = 'pos' | 'neg';
|
|
44
|
+
|
|
45
|
+
export interface MetricProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
46
|
+
/** Currency code, dimmed prefix. */
|
|
47
|
+
cur?: string;
|
|
48
|
+
/** Main figure (include the trailing dot when using frac: "48,920."). */
|
|
49
|
+
amount: React.ReactNode;
|
|
50
|
+
/** Dimmed decimals (e.g. "56"). */
|
|
51
|
+
frac?: React.ReactNode;
|
|
52
|
+
size?: MetricSize;
|
|
53
|
+
/** Colour inherits by default (works on light + inverse). */
|
|
54
|
+
tone?: MetricTone;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Metric: React.FC<MetricProps> = ({ cur, amount, frac, size = 'md', tone, className, ...rest }) => (
|
|
58
|
+
<span
|
|
59
|
+
className={['metric', size !== 'md' ? `metric-${size}` : '', tone ? `metric-${tone}` : '', className || '']
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join(' ')}
|
|
62
|
+
{...rest}
|
|
63
|
+
>
|
|
64
|
+
{cur ? <span className="metric-cur">{cur}</span> : null}
|
|
65
|
+
{amount}
|
|
66
|
+
{frac != null ? <span className="metric-frac">{frac}</span> : null}
|
|
67
|
+
</span>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/* ---------- Avatar ---------- */
|
|
71
|
+
|
|
72
|
+
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
73
|
+
export type AvatarTone = 'brand' | 'success' | 'warning' | 'danger' | 'info' | 'neutral';
|
|
74
|
+
export type AvatarDot = boolean | 'busy' | 'away' | 'off';
|
|
75
|
+
|
|
76
|
+
export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
77
|
+
/** Full name — initials are derived (first + last word). */
|
|
78
|
+
name?: string;
|
|
79
|
+
/** Image avatar; falls back to initials while absent. */
|
|
80
|
+
src?: string;
|
|
81
|
+
size?: AvatarSize;
|
|
82
|
+
tone?: AvatarTone;
|
|
83
|
+
/** Presence dot: true=online, 'busy' | 'away' | 'off'. */
|
|
84
|
+
dot?: AvatarDot;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function initials(name?: string): string {
|
|
88
|
+
if (!name) return '';
|
|
89
|
+
const parts = name.trim().split(/\s+/);
|
|
90
|
+
const first = parts[0] ? parts[0].charAt(0) : '';
|
|
91
|
+
const last = parts.length > 1 ? parts[parts.length - 1].charAt(0) : '';
|
|
92
|
+
return (first + last).toUpperCase();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const Avatar: React.FC<AvatarProps> = ({ name, src, size = 'md', tone = 'brand', dot, className, children, ...rest }) => (
|
|
96
|
+
<span
|
|
97
|
+
className={['av', `av-${size}`, `av-${tone}`, className || ''].filter(Boolean).join(' ')}
|
|
98
|
+
aria-label={name}
|
|
99
|
+
{...rest}
|
|
100
|
+
>
|
|
101
|
+
{src ? <img src={src} alt={name || ''} /> : children != null ? children : initials(name)}
|
|
102
|
+
{dot ? <span className={['av-dot', typeof dot === 'string' ? `av-dot-${dot}` : ''].filter(Boolean).join(' ')} /> : null}
|
|
103
|
+
</span>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
export interface AvatarGroupProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
107
|
+
/** Render "+N" overflow chip after the avatars. */
|
|
108
|
+
more?: number;
|
|
109
|
+
/** Size applied to the overflow chip. */
|
|
110
|
+
size?: AvatarSize;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const AvatarGroup: React.FC<AvatarGroupProps> = ({ more, size = 'md', className, children, ...rest }) => (
|
|
114
|
+
<span className={['av-group', className || ''].filter(Boolean).join(' ')} {...rest}>
|
|
115
|
+
{children}
|
|
116
|
+
{more ? <span className={`av av-${size} av-more`}>+{more}</span> : null}
|
|
117
|
+
</span>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
/* ---------- Eyebrow — uppercase micro-label ---------- */
|
|
121
|
+
|
|
122
|
+
export type EyebrowSize = 'xs' | 'sm' | 'md';
|
|
123
|
+
export type EyebrowTone = 'muted' | 'secondary' | 'default' | 'brand' | 'inverse';
|
|
124
|
+
|
|
125
|
+
export interface EyebrowProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
126
|
+
size?: EyebrowSize;
|
|
127
|
+
tone?: EyebrowTone;
|
|
128
|
+
/** Inline icon — adds .has-icon for the flex layout. */
|
|
129
|
+
icon?: React.ReactNode;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const Eyebrow: React.FC<EyebrowProps> = ({ size = 'md', tone = 'muted', icon, className, children, ...rest }) => (
|
|
133
|
+
<span
|
|
134
|
+
className={['eyebrow', `eyebrow-${size}`, `eyebrow-${tone}`, icon != null ? 'has-icon' : '', className || '']
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.join(' ')}
|
|
137
|
+
{...rest}
|
|
138
|
+
>
|
|
139
|
+
{icon}
|
|
140
|
+
{children}
|
|
141
|
+
</span>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
/* ---------- Flag — brand flag SVGs from assets/flags ---------- */
|
|
145
|
+
|
|
146
|
+
export type FlagCountry = 'BR' | 'MX' | 'CO' | 'CL' | 'US' | 'EU';
|
|
147
|
+
export type FlagShape = 'rounded' | 'square';
|
|
148
|
+
|
|
149
|
+
const FLAG_FILE: Record<FlagCountry, string> = {
|
|
150
|
+
BR: 'Brazil',
|
|
151
|
+
MX: 'Mexico',
|
|
152
|
+
CO: 'Colombia',
|
|
153
|
+
CL: 'Chile',
|
|
154
|
+
US: 'EUA',
|
|
155
|
+
EU: 'EUR',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export interface FlagProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> {
|
|
159
|
+
country: FlagCountry;
|
|
160
|
+
/** Pixel size (width = height). Default 16. */
|
|
161
|
+
size?: number;
|
|
162
|
+
/** rounded (circular) | square. Default rounded. */
|
|
163
|
+
shape?: FlagShape;
|
|
164
|
+
/** Base path to assets/flags — default works two levels below root
|
|
165
|
+
(component benches and ui_kits/dashboard). Override elsewhere. */
|
|
166
|
+
assetsBase?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const Flag: React.FC<FlagProps> = ({ country, size = 16, shape = 'rounded', assetsBase = '../../assets/flags', style, ...rest }) => (
|
|
170
|
+
<img
|
|
171
|
+
src={`${assetsBase}/${FLAG_FILE[country]}-${shape}.svg`}
|
|
172
|
+
alt={country}
|
|
173
|
+
width={size}
|
|
174
|
+
height={size}
|
|
175
|
+
style={{ borderRadius: shape === 'rounded' ? '50%' : 'var(--radius-2xs)', display: 'inline-block', verticalAlign: '-0.18em', ...style }}
|
|
176
|
+
{...rest}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
/* ---------- Icon — canonical Lucide glyphs ---------- */
|
|
181
|
+
|
|
182
|
+
export interface IconProps extends React.SVGAttributes<SVGSVGElement> {
|
|
183
|
+
/** Lucide name — kebab-case ("arrow-left-right") or PascalCase. */
|
|
184
|
+
name: string;
|
|
185
|
+
/** Pixel size. Default 18. */
|
|
186
|
+
size?: number;
|
|
187
|
+
/** Stroke width. Default 1.75 (matches the system). */
|
|
188
|
+
stroke?: number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
type LucideNode = Array<unknown>;
|
|
192
|
+
|
|
193
|
+
function lucideLookup(name: string): LucideNode | null {
|
|
194
|
+
const lucide = (window as unknown as { lucide?: { icons?: Record<string, LucideNode> } }).lucide;
|
|
195
|
+
if (!lucide || !lucide.icons) return null;
|
|
196
|
+
const pascal = name
|
|
197
|
+
.split('-')
|
|
198
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
199
|
+
.join('');
|
|
200
|
+
return lucide.icons[pascal] || lucide.icons[name] || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Lucide UMD ships two icon-data shapes depending on version:
|
|
204
|
+
- pair list: [['path', {...}], ['line', {...}]] (newer)
|
|
205
|
+
- triple: ['svg', {...attrs}, [['path', {...}], ...]] (0.4xx)
|
|
206
|
+
Render both, recursively (children may themselves be triples). */
|
|
207
|
+
function renderLucideChildren(nodes: unknown, keyPrefix: string): React.ReactNode[] {
|
|
208
|
+
if (!Array.isArray(nodes)) return [];
|
|
209
|
+
return (nodes as Array<unknown>).map((child, i) => {
|
|
210
|
+
if (!Array.isArray(child) || typeof child[0] !== 'string') return null;
|
|
211
|
+
const tag = child[0] as string;
|
|
212
|
+
const attrs = (child[1] && typeof child[1] === 'object' ? child[1] : {}) as Record<string, string>;
|
|
213
|
+
const kids = renderLucideChildren(child[2], `${keyPrefix}${i}-`);
|
|
214
|
+
return React.createElement(tag, { ...attrs, key: `${keyPrefix}${i}` }, kids.length ? kids : undefined);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const Icon: React.FC<IconProps> = ({ name, size = 18, stroke = 1.75, ...rest }) => {
|
|
219
|
+
const node = lucideLookup(name);
|
|
220
|
+
if (!node) return <span style={{ display: 'inline-block', width: size, height: size }} aria-hidden="true" />;
|
|
221
|
+
// Triple form: ['svg', attrs, children] — unwrap to the children list.
|
|
222
|
+
const children =
|
|
223
|
+
node.length >= 2 && node[0] === 'svg' ? (node[2] as unknown) : (node as unknown);
|
|
224
|
+
return (
|
|
225
|
+
<svg
|
|
226
|
+
width={size}
|
|
227
|
+
height={size}
|
|
228
|
+
viewBox="0 0 24 24"
|
|
229
|
+
fill="none"
|
|
230
|
+
stroke="currentColor"
|
|
231
|
+
strokeWidth={stroke}
|
|
232
|
+
strokeLinecap="round"
|
|
233
|
+
strokeLinejoin="round"
|
|
234
|
+
aria-hidden="true"
|
|
235
|
+
{...rest}
|
|
236
|
+
>
|
|
237
|
+
{renderLucideChildren(children, 'n')}
|
|
238
|
+
</svg>
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/* ---------- Divider ---------- */
|
|
243
|
+
|
|
244
|
+
export type DividerWeight = 'default' | 'subtle' | 'strong';
|
|
245
|
+
|
|
246
|
+
export interface DividerProps extends React.HTMLAttributes<HTMLElement> {
|
|
247
|
+
weight?: DividerWeight;
|
|
248
|
+
dashed?: boolean;
|
|
249
|
+
/** Vertical hairline for inline rows (1px × 1em). */
|
|
250
|
+
vertical?: boolean;
|
|
251
|
+
/** Midline label — renders the `.divider-labelled` layout. */
|
|
252
|
+
label?: React.ReactNode;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export const Divider: React.FC<DividerProps> = ({ weight = 'default', dashed = false, vertical = false, label, className, ...rest }) => {
|
|
256
|
+
const mods = [weight !== 'default' ? `divider-${weight}` : '', dashed ? 'divider-dashed' : '', className || ''];
|
|
257
|
+
if (label != null) {
|
|
258
|
+
return (
|
|
259
|
+
<div className={['divider-labelled', ...mods].filter(Boolean).join(' ')} {...rest}>
|
|
260
|
+
<span>{label}</span>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (vertical) return <span className={['divider-vertical', ...mods].filter(Boolean).join(' ')} {...(rest as React.HTMLAttributes<HTMLSpanElement>)} />;
|
|
265
|
+
return <hr className={['divider', ...mods].filter(Boolean).join(' ')} {...(rest as React.HTMLAttributes<HTMLHRElement>)} />;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/* ---------- FilterChip — pill button for filter rows (40px) ---------- */
|
|
269
|
+
|
|
270
|
+
export interface FilterChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
271
|
+
/** Selected state (aria-pressed + active styling from CSS). */
|
|
272
|
+
selected?: boolean;
|
|
273
|
+
icon?: React.ReactNode;
|
|
274
|
+
/** Trailing dropdown indicator (`.chip-chevron`). */
|
|
275
|
+
chevron?: boolean;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export const FilterChip: React.FC<FilterChipProps> = ({ selected = false, icon, chevron = false, className, children, ...rest }) => (
|
|
279
|
+
<button type="button" className={['filter-chip', className || ''].filter(Boolean).join(' ')} aria-pressed={selected} {...rest}>
|
|
280
|
+
{icon}
|
|
281
|
+
{children}
|
|
282
|
+
{chevron ? (
|
|
283
|
+
<svg className="chip-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
284
|
+
<polyline points="6 9 12 15 18 9" />
|
|
285
|
+
</svg>
|
|
286
|
+
) : null}
|
|
287
|
+
</button>
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
/* ---------- ProgressBar — slim track + data-driven fill ---------- */
|
|
291
|
+
|
|
292
|
+
export interface ProgressBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
293
|
+
/** 0–100. */
|
|
294
|
+
value: number;
|
|
295
|
+
/** Fill color — any token/var; defaults to the CSS fallback. */
|
|
296
|
+
fill?: string;
|
|
297
|
+
/** Track height px — feeds `--progress-h` (default 6). */
|
|
298
|
+
height?: number;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export const ProgressBar: React.FC<ProgressBarProps> = ({ value, fill, height, style, className, ...rest }) => (
|
|
302
|
+
<div
|
|
303
|
+
className={['progress', className || ''].filter(Boolean).join(' ')}
|
|
304
|
+
role="progressbar"
|
|
305
|
+
aria-valuenow={Math.round(value)}
|
|
306
|
+
aria-valuemin={0}
|
|
307
|
+
aria-valuemax={100}
|
|
308
|
+
style={{ ...(height != null ? ({ '--progress-h': `${height}px` } as React.CSSProperties) : null), ...style }}
|
|
309
|
+
{...rest}
|
|
310
|
+
>
|
|
311
|
+
<span className="progress-fill" style={{ width: `${Math.max(0, Math.min(100, value))}%`, ...(fill ? { background: fill } : null) }} />
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
/* ---------- DescTerm — labelled term/definition pair ---------- */
|
|
316
|
+
|
|
317
|
+
export interface DescTermProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
318
|
+
label: React.ReactNode;
|
|
319
|
+
children?: React.ReactNode;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export const DescTerm: React.FC<DescTermProps> = ({ label, className, children, ...rest }) => (
|
|
323
|
+
<div className={['desc-term', className || ''].filter(Boolean).join(' ')} {...rest}>
|
|
324
|
+
<span className="desc-term-label">{label}</span>
|
|
325
|
+
<span className="desc-term-value">{children}</span>
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
/* ---------- Stat — standalone KPI tile ---------- */
|
|
330
|
+
|
|
331
|
+
export type StatDeltaDirection = 'up' | 'down' | 'flat';
|
|
332
|
+
|
|
333
|
+
export interface StatProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
334
|
+
/** Mono overline label. */
|
|
335
|
+
label: React.ReactNode;
|
|
336
|
+
/** Big tabular figure. */
|
|
337
|
+
value: React.ReactNode;
|
|
338
|
+
/** Delta text, e.g. "+8.2%". Arrow glyph derives from deltaDirection. */
|
|
339
|
+
delta?: React.ReactNode;
|
|
340
|
+
/** up=success ↑ · down=danger ↓ · flat=muted. Default: 'flat'. */
|
|
341
|
+
deltaDirection?: StatDeltaDirection;
|
|
342
|
+
/** Footnote line (.stat-foot) — context under the figure. */
|
|
343
|
+
foot?: React.ReactNode;
|
|
344
|
+
/** Leading icon beside the label. */
|
|
345
|
+
icon?: React.ReactNode;
|
|
346
|
+
/** No tile chrome (bg/border/padding) — for use INSIDE a Card. */
|
|
347
|
+
bare?: boolean;
|
|
348
|
+
/** Dense paddings (.stat-compact). */
|
|
349
|
+
compact?: boolean;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const STAT_ARROW: Record<StatDeltaDirection, string> = { up: '↑ ', down: '↓ ', flat: '' };
|
|
353
|
+
|
|
354
|
+
export const Stat: React.FC<StatProps> = ({
|
|
355
|
+
label,
|
|
356
|
+
value,
|
|
357
|
+
delta,
|
|
358
|
+
deltaDirection = 'flat',
|
|
359
|
+
foot,
|
|
360
|
+
icon,
|
|
361
|
+
bare = false,
|
|
362
|
+
compact = false,
|
|
363
|
+
className,
|
|
364
|
+
children,
|
|
365
|
+
...rest
|
|
366
|
+
}) => (
|
|
367
|
+
<div
|
|
368
|
+
className={['stat', bare ? 'is-bare' : '', compact ? 'stat-compact' : '', className || ''].filter(Boolean).join(' ')}
|
|
369
|
+
{...rest}
|
|
370
|
+
>
|
|
371
|
+
<span className="stat-label" style={icon != null ? { display: 'inline-flex', alignItems: 'center', gap: 'var(--space-2)' } : undefined}>
|
|
372
|
+
{icon}
|
|
373
|
+
{label}
|
|
374
|
+
</span>
|
|
375
|
+
<span style={{ display: 'flex', alignItems: 'baseline', gap: 'var(--space-3)' }}>
|
|
376
|
+
<span className="stat-value">{value}</span>
|
|
377
|
+
{delta != null ? (
|
|
378
|
+
<span className={`stat-delta stat-delta-${deltaDirection}`}>
|
|
379
|
+
{STAT_ARROW[deltaDirection]}
|
|
380
|
+
{delta}
|
|
381
|
+
</span>
|
|
382
|
+
) : null}
|
|
383
|
+
</span>
|
|
384
|
+
{foot != null ? <div className="stat-foot">{foot}</div> : null}
|
|
385
|
+
{children}
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
export interface StatRowProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
390
|
+
/** Equal columns — feeds `--stat-cols`. Default 3. */
|
|
391
|
+
cols?: number;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export const StatRow: React.FC<StatRowProps> = ({ cols, className, style, children, ...rest }) => (
|
|
395
|
+
<div
|
|
396
|
+
className={['stat-row', className || ''].filter(Boolean).join(' ')}
|
|
397
|
+
style={cols != null ? ({ '--stat-cols': cols, ...style } as React.CSSProperties) : style}
|
|
398
|
+
{...rest}
|
|
399
|
+
>
|
|
400
|
+
{children}
|
|
401
|
+
</div>
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
/* Collection export — the bundler requires the module-name symbol. */
|
|
405
|
+
export const Atoms = { Money, Metric, Avatar, AvatarGroup, Eyebrow, Flag, Icon, Divider, FilterChip, ProgressBar, DescTerm, Stat, StatRow };
|
|
406
|
+
|
|
407
|
+
export default Atoms;
|