@moeve-ui/ui 0.1.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/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @moeve-ui/ui
2
+
3
+ Moeve/Redinamic component library, published as `@moeve-ui/ui` under the `moeve-ui` npm organization. This is an **independent** project, not tied to any particular app - today it's used by `kairos-frontend` (Impact Simulator), but it's meant to be reused across future projects, the same way an equivalent Vite + MUI library was built before.
4
+
5
+ It's built with [`tsup`](https://tsup.egoist.dev/) into a compiled `dist/` (ESM + CJS + `.d.ts`), so consumers just `npm install @moeve-ui/ui` like any other package - no `transpilePackages` or knowledge of our TS setup required on their end.
6
+
7
+ ## ⚠️ About this delivery's tests (read before running `npm test`)
8
+
9
+ Same as with the rest of the project, this was written without access to the npm registry (blocked in the environment I work in), so **I was never able to install vitest or @testing-library, and I never actually ran a single one of these 15 tests.** I'm not going to tell you "it's tested" without having seen it pass.
10
+
11
+ What I did do:
12
+
13
+ - Wrote each test by reading the actual code of the component it tests (no invented behavior) - assertions based on the classes/text the component actually renders.
14
+ - Built a minimal type shim for `vitest` and `@testing-library/react` (in the same manual type-checker I use for the whole project) and ran `tsc --noEmit` over the 15 tests - 0 syntax/type errors.
15
+ - That review already found and fixed 3 real issues before delivering this to you:
16
+ 1. **A real bug in `MiniMapCard`**: it rendered `{overlayLinkLabel} ({competitorCount})`, and whoever uses it in `kairos-frontend` (`DecisionFactorsCard.tsx`) already includes the count inside `overlayLinkLabel` - the result on screen was `"Basado en 3 competidores clave (3)"`, duplicated. The component was fixed (see `src/molecules/MiniMapCard/MiniMapCard.tsx`).
17
+ 2. One of my `CompetitorRow` tests had the `trend` color inverted (I assumed "competitor's price going up = bad = red", but the component intentionally paints it the other way around: if the competitor raises their price it's a good competitive signal for us = green). I fixed it to reflect the real behavior, not what I expected.
18
+ 3. A `PriceRangeSlider` test with `value` equal to `currentPrice` would have failed with "multiple elements found" (the component renders that number twice on screen - the tooltip and the current price). Just like a real UI bug, only in the test.
19
+
20
+ What I **couldn't** verify: that the tests actually pass (`vitest run`), or that the type shim captures 100% of those libraries' real API. Run `npm install && npm test` on your Mac (regular Terminal) and let me know what breaks - at this level of review I expect it to be little, but I won't claim it without having run it.
21
+
22
+ ## What's included
23
+
24
+ - **Design tokens** (`src/tokens/colors.ts`) - the single source of truth for brand colors, extracted from the "Redinamic-tec" Figma file. `tailwind.preset.ts` exposes them as a Tailwind preset.
25
+ - **Custom icon set** (`src/icons/`) - plain SVG, no external dependencies.
26
+ - **15 components organized by Atomic Design** (Brad Frost) - see the section below.
27
+
28
+ ## Structure: Atomic Design (atoms / molecules / organisms)
29
+
30
+ ```
31
+ src/
32
+ ├── atoms/ Avatar, Badge, Button, Card, ProgressBar
33
+ ├── molecules/ SegmentedTabs, NavItem, StatTile, DecisionFactorRow,
34
+ │ CompetitorRow, PriceRangeSlider, MiniMapCard
35
+ └── organisms/ Sidebar, TopBar, ConfirmDialog
36
+ ```
37
+
38
+ - **atoms/** - don't compose any other component in the library. The smallest building blocks: a button, a badge, an avatar circle.
39
+ - **molecules/** - compose 1 or more atoms (`StatTile` uses `Badge` + `ProgressBar`; `CompetitorRow` uses `Avatar`), or are a self-contained control with its own interaction logic (`PriceRangeSlider`, `SegmentedTabs`).
40
+ - **organisms/** - assemble a full screen section by combining molecules/atoms (`Sidebar` = `NavItem` × N + `Avatar`; `ConfirmDialog` = a complete modal with `Button`).
41
+
42
+ `tokens/` and `icons/` are deliberately left out of this hierarchy - they aren't "components" in the Atomic Design sense (they have no visual composition of their own; they're the raw materials the atoms/molecules/organisms consume).
43
+
44
+ **This doesn't contradict Fractal Component Design (FCD)**: each component still lives in its own self-contained file, with the same shape (props interface + responsibility comment + the component), regardless of which folder it's in - the only thing that changed is that folder now also documents its composition tier. See `../kairos-frontend/ARCHITECTURE.md` for the rest of the principles (SOLID, Clean Code) with examples.
45
+
46
+ ## Tests
47
+
48
+ Each component has its test right next to it (`Badge.tsx` + `Badge.test.tsx` in the same folder) using [Vitest](https://vitest.dev/) + [Testing Library](https://testing-library.com/react). What actually matters about each one was tested: what it renders, which classes it applies based on its props/variants, and that callbacks (`onClick`, `onChange`, etc.) fire with the correct arguments - no giant snapshots, no testing internal implementation.
49
+
50
+ ```bash
51
+ npm test # runs all tests once
52
+ npm run test:watch
53
+ ```
54
+
55
+ ## How a consuming project installs it
56
+
57
+ Published to the public npm registry under the `moeve-ui` org:
58
+
59
+ ```bash
60
+ npm install @moeve-ui/ui
61
+ ```
62
+
63
+ ```jsonc
64
+ // package.json of the project that consumes it
65
+ {
66
+ "dependencies": {
67
+ "@moeve-ui/ui": "^0.1.0"
68
+ }
69
+ }
70
+ ```
71
+
72
+ Its Tailwind preset is a separate entry point:
73
+
74
+ ```js
75
+ // tailwind.config.js of the consuming project
76
+ module.exports = {
77
+ presets: [require("@moeve-ui/ui/tailwind.preset")],
78
+ // ...
79
+ };
80
+ ```
81
+
82
+ ### Local development against an unpublished change
83
+
84
+ While iterating on a change here before publishing a new version, a consumer can still point at this folder instead of the registry:
85
+
86
+ ```jsonc
87
+ {
88
+ "dependencies": {
89
+ "@moeve-ui/ui": "file:../kairos-ui"
90
+ }
91
+ }
92
+ ```
93
+
94
+ Run `npm run build` here first (the consumer picks up `dist/`, not `src/`), then `npm install` again on their side after each change.
95
+
96
+ ## Development
97
+
98
+ ```bash
99
+ npm install # installs devDependencies: React, TS, Tailwind, Vitest, Testing Library, tsup, Storybook
100
+ npm run typecheck # tsc --noEmit
101
+ npm test # vitest run
102
+ npm run build # tsup -> dist/ (index + tailwind.preset, ESM + CJS + .d.ts)
103
+ npm run storybook # visual playground on http://localhost:6006
104
+ ```
105
+
106
+ ## Publishing a new version
107
+
108
+ ```bash
109
+ npm login # once per machine, needs to be a member of the moeve-ui org
110
+ npm version patch # or minor/major - bumps package.json + creates a git tag
111
+ npm publish # runs prepublishOnly (typecheck + test + build) automatically
112
+ ```
113
+
114
+ `publishConfig.access` is set to `"public"` in `package.json`, so the scoped package publishes as public (free on npm - a *private* scoped package needs a paid npm org plan). If that's not what you want, override it once with `npm publish --access restricted`.
@@ -0,0 +1,46 @@
1
+ // src/tokens/colors.ts
2
+ var colors = {
3
+ brand: {
4
+ sidebar: "#004656",
5
+ sidebarHover: "#005a6e",
6
+ accent: "#047dba",
7
+ primary: "#006395",
8
+ logo: "#8af9b5"
9
+ },
10
+ surface: {
11
+ page: "#f4fafe",
12
+ card: "#ffffff",
13
+ subtle: "#eef4f8",
14
+ muted: "#e9eff3"
15
+ },
16
+ text: {
17
+ primary: "#161c1f",
18
+ secondary: "#404850",
19
+ tertiary: "#707881",
20
+ onDark: "#ffffff",
21
+ onDarkMuted: "rgba(255,255,255,0.6)",
22
+ onDarkFaint: "rgba(255,255,255,0.5)"
23
+ },
24
+ success: {
25
+ DEFAULT: "#008851",
26
+ dark: "#006c3f",
27
+ bg: "#8af9b5"
28
+ },
29
+ danger: {
30
+ DEFAULT: "#ba1a1a",
31
+ dark: "#93000a",
32
+ bg: "#ffdad6"
33
+ },
34
+ info: {
35
+ DEFAULT: "#006395",
36
+ dark: "#2e6576"
37
+ },
38
+ border: {
39
+ DEFAULT: "#dde3e7"
40
+ }
41
+ };
42
+
43
+ export {
44
+ colors
45
+ };
46
+ //# sourceMappingURL=chunk-HUXDCAF5.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tokens/colors.ts"],"sourcesContent":["/**\n * Design tokens - extracted directly from the \"Redinamic-tec\" Figma file\n * (get_design_context on the \"Simulador de Impacto\" node), not made up.\n *\n * This is the ONLY source of truth for the library's colors. The Tailwind\n * preset (`packages/ui/tailwind.preset.ts`) reads from here, so a brand\n * change happens in a single place and propagates through the whole app -\n * it's the same Open/Closed principle: extend by adding/changing tokens,\n * not by editing every component that uses a color.\n */\n\nexport const colors = {\n brand: {\n sidebar: \"#004656\",\n sidebarHover: \"#005a6e\",\n accent: \"#047dba\",\n primary: \"#006395\",\n logo: \"#8af9b5\",\n },\n surface: {\n page: \"#f4fafe\",\n card: \"#ffffff\",\n subtle: \"#eef4f8\",\n muted: \"#e9eff3\",\n },\n text: {\n primary: \"#161c1f\",\n secondary: \"#404850\",\n tertiary: \"#707881\",\n onDark: \"#ffffff\",\n onDarkMuted: \"rgba(255,255,255,0.6)\",\n onDarkFaint: \"rgba(255,255,255,0.5)\",\n },\n success: {\n DEFAULT: \"#008851\",\n dark: \"#006c3f\",\n bg: \"#8af9b5\",\n },\n danger: {\n DEFAULT: \"#ba1a1a\",\n dark: \"#93000a\",\n bg: \"#ffdad6\",\n },\n info: {\n DEFAULT: \"#006395\",\n dark: \"#2e6576\",\n },\n border: {\n DEFAULT: \"#dde3e7\",\n },\n} as const;\n\nexport type ColorTokens = typeof colors;\n"],"mappings":";AAWO,IAAM,SAAS;AAAA,EACpB,OAAO;AAAA,IACL,SAAS;AAAA,IACT,cAAc;AAAA,IACd,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,EACT;AAAA,EACA,MAAM;AAAA,IACJ,SAAS;AAAA,IACT,WAAW;AAAA,IACX,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,aAAa;AAAA,EACf;AAAA,EACA,SAAS;AAAA,IACP,SAAS;AAAA,IACT,MAAM;AAAA,IACN,IAAI;AAAA,EACN;AAAA,EACA,QAAQ;AAAA,IACN,SAAS;AAAA,IACT,MAAM;AAAA,IACN,IAAI;AAAA,EACN;AAAA,EACA,MAAM;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,EACR;AAAA,EACA,QAAQ;AAAA,IACN,SAAS;AAAA,EACX;AACF;","names":[]}
@@ -0,0 +1,395 @@
1
+ import * as react from 'react';
2
+ import { SVGProps, ReactNode, ButtonHTMLAttributes, HTMLAttributes } from 'react';
3
+
4
+ /**
5
+ * Design tokens - extracted directly from the "Redinamic-tec" Figma file
6
+ * (get_design_context on the "Simulador de Impacto" node), not made up.
7
+ *
8
+ * This is the ONLY source of truth for the library's colors. The Tailwind
9
+ * preset (`packages/ui/tailwind.preset.ts`) reads from here, so a brand
10
+ * change happens in a single place and propagates through the whole app -
11
+ * it's the same Open/Closed principle: extend by adding/changing tokens,
12
+ * not by editing every component that uses a color.
13
+ */
14
+ declare const colors: {
15
+ readonly brand: {
16
+ readonly sidebar: "#004656";
17
+ readonly sidebarHover: "#005a6e";
18
+ readonly accent: "#047dba";
19
+ readonly primary: "#006395";
20
+ readonly logo: "#8af9b5";
21
+ };
22
+ readonly surface: {
23
+ readonly page: "#f4fafe";
24
+ readonly card: "#ffffff";
25
+ readonly subtle: "#eef4f8";
26
+ readonly muted: "#e9eff3";
27
+ };
28
+ readonly text: {
29
+ readonly primary: "#161c1f";
30
+ readonly secondary: "#404850";
31
+ readonly tertiary: "#707881";
32
+ readonly onDark: "#ffffff";
33
+ readonly onDarkMuted: "rgba(255,255,255,0.6)";
34
+ readonly onDarkFaint: "rgba(255,255,255,0.5)";
35
+ };
36
+ readonly success: {
37
+ readonly DEFAULT: "#008851";
38
+ readonly dark: "#006c3f";
39
+ readonly bg: "#8af9b5";
40
+ };
41
+ readonly danger: {
42
+ readonly DEFAULT: "#ba1a1a";
43
+ readonly dark: "#93000a";
44
+ readonly bg: "#ffdad6";
45
+ };
46
+ readonly info: {
47
+ readonly DEFAULT: "#006395";
48
+ readonly dark: "#2e6576";
49
+ };
50
+ readonly border: {
51
+ readonly DEFAULT: "#dde3e7";
52
+ };
53
+ };
54
+ type ColorTokens = typeof colors;
55
+
56
+ /**
57
+ * Custom icon set, in plain SVG (no external library like
58
+ * lucide-react/heroicons) - so packages/ui doesn't depend on anything but
59
+ * React, and there's no separate icon package to install/update.
60
+ *
61
+ * NOTE ON DESIGN FIDELITY: the Figma design ships its own icons exported
62
+ * as images; those assets live on Figma's servers and the URLs expire in
63
+ * ~7 days, so they can't be "committed" as-is. These are an equivalent set
64
+ * (same visual meaning - pin, arrow, check, etc.) redrawn by hand in SVG,
65
+ * not an exact pixel-for-pixel copy of the original vector. If the exact
66
+ * Figma set is wanted later on, it can be exported as SVG from the file
67
+ * and used to replace these.
68
+ *
69
+ * All of them accept the standard SVG props (className, etc.) so size/color
70
+ * can be controlled with Tailwind from wherever they're used.
71
+ */
72
+ type IconProps = SVGProps<SVGSVGElement>;
73
+ declare function BoltIcon(props: IconProps): react.JSX.Element;
74
+ declare function MapPinIcon(props: IconProps): react.JSX.Element;
75
+ declare function ChevronDownIcon(props: IconProps): react.JSX.Element;
76
+ declare function ArrowUpIcon(props: IconProps): react.JSX.Element;
77
+ declare function ArrowDownIcon(props: IconProps): react.JSX.Element;
78
+ declare function RefreshIcon(props: IconProps): react.JSX.Element;
79
+ declare function UserCircleIcon(props: IconProps): react.JSX.Element;
80
+ declare function LogOutIcon(props: IconProps): react.JSX.Element;
81
+ declare function TagIcon(props: IconProps): react.JSX.Element;
82
+ declare function TargetIcon(props: IconProps): react.JSX.Element;
83
+ declare function TrendUpIcon(props: IconProps): react.JSX.Element;
84
+ declare function TrendDownIcon(props: IconProps): react.JSX.Element;
85
+ declare function FuelIcon(props: IconProps): react.JSX.Element;
86
+ declare function WalletIcon(props: IconProps): react.JSX.Element;
87
+ declare function CheckCircleIcon(props: IconProps): react.JSX.Element;
88
+ declare function XIcon(props: IconProps): react.JSX.Element;
89
+ /**
90
+ * Solid/filled icon set, exported directly from the "Redinamic-tec" Figma
91
+ * file (the "Gestion de Precios" stat cards) - unlike the outline set
92
+ * above, these are pixel-accurate copies of the Figma vectors, not
93
+ * hand-redrawn equivalents. Each keeps its own natural viewBox/aspect
94
+ * ratio instead of being forced into the outline set's 24x24 grid.
95
+ */
96
+ declare function FuelPumpSolidIcon(props: IconProps): react.JSX.Element;
97
+ declare function WalletSolidIcon(props: IconProps): react.JSX.Element;
98
+ declare function TrendUpSolidIcon(props: IconProps): react.JSX.Element;
99
+
100
+ interface AvatarProps {
101
+ /** letter or icon shown inside the circle */
102
+ children: ReactNode;
103
+ /** background color of the circle (hex or another Tailwind class via className) */
104
+ className?: string;
105
+ size?: "sm" | "md";
106
+ }
107
+ /**
108
+ * Avatar/initials circle - used both by the session user (sidebar) and
109
+ * each competitor in the "Nearby Competitors" list. It does not decide
110
+ * the background OR text color: it receives them via className
111
+ * (e.g. "bg-danger-bg text-danger-dark" or "bg-success text-white"), so
112
+ * the same component works for any combination without having to touch it.
113
+ */
114
+ declare function Avatar({ children, className, size }: AvatarProps): react.JSX.Element;
115
+
116
+ type BadgeTone = "success" | "danger" | "neutral";
117
+ interface BadgeProps {
118
+ children: ReactNode;
119
+ tone?: BadgeTone;
120
+ className?: string;
121
+ }
122
+ /**
123
+ * Small pill for deltas ("+0.7%", "-800L") and status labels. The set of
124
+ * variants ("tone") is the only extension point - adding a new tone
125
+ * doesn't touch the rest of the component (Open/Closed).
126
+ */
127
+ declare function Badge({ children, tone, className }: BadgeProps): react.JSX.Element;
128
+
129
+ type ButtonVariant = "primary" | "ghost" | "text";
130
+ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
131
+ children: ReactNode;
132
+ variant?: ButtonVariant;
133
+ icon?: ReactNode;
134
+ }
135
+ /**
136
+ * Single button with 3 variants (the ones that appear in the design: the
137
+ * solid blue pill for "Aplicar precio"/"Actualizar datos", the text link
138
+ * for "Restablecer a Actual", and a "ghost" variant for future cases).
139
+ * Same as Badge: extending the set of variants doesn't require touching
140
+ * the rest of the component.
141
+ */
142
+ declare function Button({ children, variant, icon, className, ...rest }: ButtonProps): react.JSX.Element;
143
+
144
+ interface CardProps extends HTMLAttributes<HTMLDivElement> {
145
+ children: ReactNode;
146
+ /** removes the default padding, for when the content manages its own */
147
+ noPadding?: boolean;
148
+ }
149
+ /**
150
+ * Base white, rounded container with a soft shadow - the same pattern
151
+ * that wraps every dashboard block in the design (Simulator Panel,
152
+ * Decision Factors, Nearby Competitors, etc.). Anything specific to a
153
+ * given block goes through composition (children), not new props here -
154
+ * single responsibility principle: Card only knows how to be "the white box".
155
+ */
156
+ declare function Card({ children, className, noPadding, ...rest }: CardProps): react.JSX.Element;
157
+
158
+ interface ProgressBarProps {
159
+ /** 0 to 100 */
160
+ value: number;
161
+ /** color of the filled bar - Tailwind class, e.g. "bg-info" */
162
+ fillClassName?: string;
163
+ /** bar height in px (the design uses 4px in stat tiles and 6px in Decision Factors) */
164
+ heightClassName?: string;
165
+ /** number of equal segments the bar is divided into (the design uses 3 in "Supply Cost") */
166
+ segments?: number;
167
+ className?: string;
168
+ }
169
+ /**
170
+ * Simple progress bar. It knows nothing about "what" it represents
171
+ * (elasticity, margin, inventory...) - that's decided by whoever uses it
172
+ * via `value` and `fillClassName`. Keeping it "dumb" is what allows it to
173
+ * be reused in the stat tiles AND in the Decision Factors panel without
174
+ * duplicating code.
175
+ */
176
+ declare function ProgressBar({ value, fillClassName, heightClassName, segments, className, }: ProgressBarProps): react.JSX.Element;
177
+
178
+ type SnackbarTone = "success" | "danger" | "neutral";
179
+ interface SnackbarProps {
180
+ message: string;
181
+ tone?: SnackbarTone;
182
+ onClose?: () => void;
183
+ className?: string;
184
+ }
185
+ /**
186
+ * Toast/confirmation bar (e.g. "Datos de mercado actualizados
187
+ * correctamente" after a refresh). Fully controlled - it has no timer
188
+ * or visibility state of its own; whoever uses it decides when it's on
189
+ * screen (mount/unmount it, or drive a boolean) and for how long, same
190
+ * philosophy as ConfirmDialog's `open`.
191
+ */
192
+ declare function Snackbar({ message, tone, onClose, className }: SnackbarProps): react.JSX.Element;
193
+
194
+ interface SegmentedTabOption {
195
+ value: string;
196
+ label: string;
197
+ }
198
+ interface SegmentedTabsProps {
199
+ options: SegmentedTabOption[];
200
+ value: string;
201
+ onChange: (value: string) => void;
202
+ className?: string;
203
+ }
204
+ /**
205
+ * The Simulator's "Regular | Premium | Diesel" control. It receives the
206
+ * options via props instead of having them fixed - the same component
207
+ * works for any list of tabs, in any future screen.
208
+ */
209
+ declare function SegmentedTabs({ options, value, onChange, className }: SegmentedTabsProps): react.JSX.Element;
210
+
211
+ interface NavItemProps {
212
+ icon: ReactNode;
213
+ label: string;
214
+ active?: boolean;
215
+ href?: string;
216
+ }
217
+ /**
218
+ * A sidebar navigation link. `active` is the only variant -
219
+ * the rest of the appearance (icon, text) is composed from outside.
220
+ */
221
+ declare function NavItem({ icon, label, active, href }: NavItemProps): react.JSX.Element;
222
+
223
+ interface StatTileProps {
224
+ icon: ReactNode;
225
+ label: string;
226
+ value: string;
227
+ /** unit suffix rendered next to `value` in a lighter/smaller style, e.g. "L" */
228
+ unit?: string;
229
+ /** delta badge text, e.g. "+2.4% vs prom. diario" */
230
+ delta?: string;
231
+ deltaTone?: BadgeTone;
232
+ /** optional 0-100 value for the mini bar below (not every tile has one) */
233
+ progressValue?: number;
234
+ progressClassName?: string;
235
+ }
236
+ /**
237
+ * One of the result cards ("Volumen Proyectado", "Margen Bruto Proy.",
238
+ * "Ganancia Proyectada"). Purely presentational - it doesn't know where
239
+ * the numbers come from, it just renders them (Dependency Inversion:
240
+ * StatTile depends on props, not on the API or the model).
241
+ */
242
+ declare function StatTile({ icon, label, value, unit, delta, deltaTone, progressValue, progressClassName, }: StatTileProps): react.JSX.Element;
243
+
244
+ interface DecisionFactorRowProps {
245
+ label: string;
246
+ /** the short value on the right, e.g. "Alta", "Estable", "65%" */
247
+ statusText: string;
248
+ statusClassName?: string;
249
+ /** 0-100, the weight/strength of this factor */
250
+ value: number;
251
+ fillClassName?: string;
252
+ segments?: number;
253
+ description: string;
254
+ }
255
+ /**
256
+ * A row in the "Decision Factors" panel (Elasticity, Supply Cost,
257
+ * Inventory...). Designed to be populated directly from the `factors`
258
+ * array returned by the backend's POST /predict - each factor from the
259
+ * API maps 1 to 1 to one of these rows.
260
+ */
261
+ declare function DecisionFactorRow({ label, statusText, statusClassName, value, fillClassName, segments, description, }: DecisionFactorRowProps): react.JSX.Element;
262
+
263
+ type CompetitorTrend = "up" | "down" | "stable";
264
+ interface CompetitorRowProps {
265
+ name: string;
266
+ /** initial shown in the avatar, e.g. "O" */
267
+ avatarLetter: string;
268
+ avatarBgClassName: string;
269
+ avatarTextClassName: string;
270
+ distanceKm: number;
271
+ price: string;
272
+ trend: CompetitorTrend;
273
+ /** e.g. "Hace 2h" or "Sin cambios (24h)" */
274
+ trendLabel: string;
275
+ }
276
+ /**
277
+ * A row in the "Nearby Competitors" panel. Designed to be populated
278
+ * directly from the array returned by the backend's GET /competitors.
279
+ */
280
+ declare function CompetitorRow({ name, avatarLetter, avatarBgClassName, avatarTextClassName, distanceKm, price, trend, trendLabel, }: CompetitorRowProps): react.JSX.Element;
281
+
282
+ interface PriceRangeSliderProps {
283
+ min: number;
284
+ max: number;
285
+ step?: number;
286
+ /** the value the user is currently dragging */
287
+ value: number;
288
+ onChange: (value: number) => void;
289
+ /** reference "current" price, shown at the top left */
290
+ currentPrice: number;
291
+ /** price suggested by the model, shown at the top right */
292
+ recommendedPrice: number;
293
+ currencyFormatter: (value: number) => string;
294
+ }
295
+ /**
296
+ * The Simulator's "Adjust the price to evaluate the projected impact"
297
+ * slider. Fully controlled (value/onChange come from the parent) - it
298
+ * holds no state of its own, so it doesn't need "use client" by itself:
299
+ * it inherits it from the screen component that does manage the state.
300
+ */
301
+ declare function PriceRangeSlider({ min, max, step, value, onChange, currentPrice, recommendedPrice, currencyFormatter, }: PriceRangeSliderProps): react.JSX.Element;
302
+
303
+ interface MiniMapCardProps {
304
+ overlayTitle: string;
305
+ /** text already assembled with the count included, e.g. "Basado en 3 competidores clave" - don't repeat it in competitorCount. */
306
+ overlayLinkLabel: string;
307
+ /** used only for accessibility (aria-label), not rendered on screen again - overlayLinkLabel already has the number in the right place in the sentence. */
308
+ competitorCount: number;
309
+ }
310
+ /**
311
+ * The decorative map card below "Decision Factors".
312
+ *
313
+ * *** PLACEHOLDER ***: the design uses a screenshot of a real map; here an
314
+ * abstract SVG map is drawn instead (no dependencies or API key) that
315
+ * occupies the same space and keeps the text overlay identical. Once a
316
+ * real integration (Google Maps / Mapbox) exists with the location and
317
+ * geolocated competitors, this is the component that gets replaced -
318
+ * the rest of the screen doesn't change because it only depends on this
319
+ * interface.
320
+ */
321
+ declare function MiniMapCard({ overlayTitle, overlayLinkLabel, competitorCount }: MiniMapCardProps): react.JSX.Element;
322
+
323
+ interface SidebarStation {
324
+ id: string;
325
+ label: string;
326
+ }
327
+ interface SidebarNavItem {
328
+ key: string;
329
+ label: string;
330
+ icon: ReactNode;
331
+ active?: boolean;
332
+ href?: string;
333
+ }
334
+ interface SidebarUser {
335
+ name: string;
336
+ role: string;
337
+ }
338
+ interface SidebarProps {
339
+ brandName?: string;
340
+ stations: SidebarStation[];
341
+ selectedStationId: string;
342
+ onSelectStation: (stationId: string) => void;
343
+ navItems: SidebarNavItem[];
344
+ user: SidebarUser;
345
+ onLogout?: () => void;
346
+ }
347
+ /**
348
+ * The fixed 260px sidebar. It receives EVERYTHING via props - stations,
349
+ * nav items, user - instead of having anything hardcoded (beyond the
350
+ * default brand name). This is what lets it serve "multiple users":
351
+ * whoever uses it decides which user/stations to show, this component
352
+ * just knows how to render them. The station list currently uses a
353
+ * native <select> instead of a custom dropdown - it's the simplest and
354
+ * most accessible option; it can be replaced by a more elaborate menu
355
+ * without touching the rest of the screen.
356
+ */
357
+ declare function Sidebar({ brandName, stations, selectedStationId, onSelectStation, navItems, user, onLogout, }: SidebarProps): react.JSX.Element;
358
+
359
+ interface TopBarProps {
360
+ stationName: string;
361
+ lastUpdatedLabel: string;
362
+ onRefresh?: () => void;
363
+ refreshing?: boolean;
364
+ /** optional content rendered between the station info and the refresh button - e.g. product tabs. */
365
+ middleContent?: ReactNode;
366
+ }
367
+ /**
368
+ * The fixed top bar (80px, with blur), to the right of the sidebar.
369
+ * It only knows the active station's name and the "last updated"
370
+ * state - the concrete data comes from the screen that uses it.
371
+ */
372
+ declare function TopBar({ stationName, lastUpdatedLabel, onRefresh, refreshing, middleContent, }: TopBarProps): react.JSX.Element;
373
+
374
+ interface ConfirmDialogProps {
375
+ open: boolean;
376
+ onClose: () => void;
377
+ onConfirm: () => void;
378
+ title: string;
379
+ productLabel: string;
380
+ description: ReactNode;
381
+ currentPrice: string;
382
+ newPrice: string;
383
+ marginImpactLabel: string;
384
+ volumeImpactLabel: string;
385
+ confirming?: boolean;
386
+ }
387
+ /**
388
+ * The "Aplicar precio recomendado" / "Confirmar y aplicar" modal. Fully
389
+ * controlled by `open` - it has no state of its own, so the screen that
390
+ * uses it decides when to show it (e.g. after a successful /simulate
391
+ * response) and what happens on confirm (calling /prices/apply).
392
+ */
393
+ declare function ConfirmDialog({ open, onClose, onConfirm, title, productLabel, description, currentPrice, newPrice, marginImpactLabel, volumeImpactLabel, confirming, }: ConfirmDialogProps): react.JSX.Element | null;
394
+
395
+ export { ArrowDownIcon, ArrowUpIcon, Avatar, type AvatarProps, Badge, type BadgeProps, type BadgeTone, BoltIcon, Button, type ButtonProps, type ButtonVariant, Card, type CardProps, CheckCircleIcon, ChevronDownIcon, type ColorTokens, CompetitorRow, type CompetitorRowProps, type CompetitorTrend, ConfirmDialog, type ConfirmDialogProps, DecisionFactorRow, type DecisionFactorRowProps, FuelIcon, FuelPumpSolidIcon, LogOutIcon, MapPinIcon, MiniMapCard, type MiniMapCardProps, NavItem, type NavItemProps, PriceRangeSlider, type PriceRangeSliderProps, ProgressBar, type ProgressBarProps, RefreshIcon, type SegmentedTabOption, SegmentedTabs, type SegmentedTabsProps, Sidebar, type SidebarNavItem, type SidebarProps, type SidebarStation, type SidebarUser, Snackbar, type SnackbarProps, type SnackbarTone, StatTile, type StatTileProps, TagIcon, TargetIcon, TopBar, type TopBarProps, TrendDownIcon, TrendUpIcon, TrendUpSolidIcon, UserCircleIcon, WalletIcon, WalletSolidIcon, XIcon, colors };