@m3000/market 0.0.1
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 +21 -0
- package/dist/components/blocks/auction/Auction.d.ts +49 -0
- package/dist/components/blocks/auction/Auction.js +44 -0
- package/dist/components/blocks/auction/AuctionBidForm.d.ts +11 -0
- package/dist/components/blocks/auction/AuctionBidForm.js +88 -0
- package/dist/components/blocks/auction/AuctionBidInput.d.ts +9 -0
- package/dist/components/blocks/auction/AuctionBidInput.js +99 -0
- package/dist/components/blocks/auction/AuctionContext.d.ts +71 -0
- package/dist/components/blocks/auction/AuctionContext.js +228 -0
- package/dist/components/blocks/auction/AuctionInfo.d.ts +9 -0
- package/dist/components/blocks/auction/AuctionInfo.js +37 -0
- package/dist/components/blocks/auction/AuctionLayout.d.ts +63 -0
- package/dist/components/blocks/auction/AuctionLayout.js +80 -0
- package/dist/components/blocks/auction/AuctionRankings.d.ts +16 -0
- package/dist/components/blocks/auction/AuctionRankings.js +334 -0
- package/dist/components/blocks/auction/AuctionStatusTag.d.ts +15 -0
- package/dist/components/blocks/auction/AuctionStatusTag.js +60 -0
- package/dist/components/blocks/auction/AuctionSuggestedBids.d.ts +38 -0
- package/dist/components/blocks/auction/AuctionSuggestedBids.js +116 -0
- package/dist/components/blocks/auction/AuctionYourBidCard.d.ts +27 -0
- package/dist/components/blocks/auction/AuctionYourBidCard.js +94 -0
- package/dist/components/blocks/auction/AuctionYourBids.d.ts +9 -0
- package/dist/components/blocks/auction/AuctionYourBids.js +49 -0
- package/dist/components/blocks/auction/index.d.ts +12 -0
- package/dist/components/blocks/index.d.ts +12 -0
- package/dist/components/index.d.ts +28 -0
- package/dist/components/primitives/Button.d.ts +31 -0
- package/dist/components/primitives/Button.js +117 -0
- package/dist/components/primitives/Drawer.d.ts +43 -0
- package/dist/components/primitives/Drawer.js +51 -0
- package/dist/components/primitives/Feedback.d.ts +28 -0
- package/dist/components/primitives/Feedback.js +147 -0
- package/dist/components/primitives/MorphDialog.d.ts +39 -0
- package/dist/components/primitives/MorphDialog.js +87 -0
- package/dist/components/primitives/Price.d.ts +84 -0
- package/dist/components/primitives/Price.js +255 -0
- package/dist/components/primitives/PriceInput.d.ts +33 -0
- package/dist/components/primitives/PriceInput.js +25 -0
- package/dist/components/primitives/Receipt.d.ts +164 -0
- package/dist/components/primitives/Receipt.js +344 -0
- package/dist/components/primitives/Scale.d.ts +67 -0
- package/dist/components/primitives/Scale.js +132 -0
- package/dist/components/primitives/Separator.d.ts +22 -0
- package/dist/components/primitives/Separator.js +62 -0
- package/dist/components/primitives/Skeleton.d.ts +14 -0
- package/dist/components/primitives/Skeleton.js +20 -0
- package/dist/components/primitives/SteppedInput.d.ts +94 -0
- package/dist/components/primitives/SteppedInput.js +154 -0
- package/dist/components/primitives/Tabs.d.ts +37 -0
- package/dist/components/primitives/Tabs.js +99 -0
- package/dist/components/primitives/Tag.d.ts +24 -0
- package/dist/components/primitives/Tag.js +22 -0
- package/dist/components/primitives/Text.d.ts +32 -0
- package/dist/components/primitives/Text.js +65 -0
- package/dist/components/primitives/countdown/Countdown.d.ts +24 -0
- package/dist/components/primitives/countdown/Countdown.js +22 -0
- package/dist/components/primitives/framed-image/FramedImage.d.ts +13 -0
- package/dist/components/primitives/framed-image/FramedImage.js +37 -0
- package/dist/components/primitives/index.d.ts +17 -0
- package/dist/components/primitives/ranked-list/Ranking.d.ts +117 -0
- package/dist/components/primitives/ranked-list/Ranking.js +219 -0
- package/dist/components/primitives/ranked-list/index.d.ts +1 -0
- package/dist/hooks/useCountdown.d.ts +20 -0
- package/dist/hooks/useCountdown.js +75 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +36 -0
- package/dist/lib/cn.d.ts +6 -0
- package/dist/lib/cn.js +75 -0
- package/dist/lib/motion.d.ts +19 -0
- package/dist/lib/motion.js +43 -0
- package/dist/types/index.d.ts +120 -0
- package/dist/utils/format.d.ts +38 -0
- package/dist/utils/format.js +103 -0
- package/dist/utils/rank-utils.d.ts +34 -0
- package/dist/utils/rank-utils.js +80 -0
- package/dist/utils/tick-validation.d.ts +22 -0
- package/dist/utils/tick-validation.js +40 -0
- package/package.json +92 -0
- package/src/components/blocks/auction/Auction.tsx +74 -0
- package/src/components/blocks/auction/AuctionArtwork.tsx +4 -0
- package/src/components/blocks/auction/AuctionBidForm.tsx +138 -0
- package/src/components/blocks/auction/AuctionBidInput.tsx +166 -0
- package/src/components/blocks/auction/AuctionContext.tsx +401 -0
- package/src/components/blocks/auction/AuctionInfo.tsx +36 -0
- package/src/components/blocks/auction/AuctionLayout.tsx +200 -0
- package/src/components/blocks/auction/AuctionRankings.tsx +435 -0
- package/src/components/blocks/auction/AuctionStatusTag.tsx +98 -0
- package/src/components/blocks/auction/AuctionSuggestedBids.tsx +203 -0
- package/src/components/blocks/auction/AuctionYourBidCard.tsx +125 -0
- package/src/components/blocks/auction/AuctionYourBids.tsx +61 -0
- package/src/components/blocks/auction/index.ts +42 -0
- package/src/components/blocks/index.ts +1 -0
- package/src/components/index.ts +2 -0
- package/src/components/primitives/Button.tsx +183 -0
- package/src/components/primitives/Drawer.tsx +125 -0
- package/src/components/primitives/Feedback.tsx +185 -0
- package/src/components/primitives/MorphDialog.tsx +160 -0
- package/src/components/primitives/Price.tsx +394 -0
- package/src/components/primitives/PriceInput.tsx +48 -0
- package/src/components/primitives/Receipt.tsx +711 -0
- package/src/components/primitives/Scale.tsx +287 -0
- package/src/components/primitives/Separator.tsx +87 -0
- package/src/components/primitives/Skeleton.tsx +33 -0
- package/src/components/primitives/SteppedInput.tsx +313 -0
- package/src/components/primitives/Tabs.tsx +161 -0
- package/src/components/primitives/Tag.tsx +48 -0
- package/src/components/primitives/Text.tsx +102 -0
- package/src/components/primitives/countdown/Countdown.tsx +43 -0
- package/src/components/primitives/countdown/index.ts +2 -0
- package/src/components/primitives/framed-image/FramedImage.tsx +51 -0
- package/src/components/primitives/framed-image/index.ts +1 -0
- package/src/components/primitives/index.ts +42 -0
- package/src/components/primitives/ranked-list/RankedList.tsx +9 -0
- package/src/components/primitives/ranked-list/Ranking.tsx +454 -0
- package/src/components/primitives/ranked-list/index.ts +8 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useCountdown.ts +91 -0
- package/src/index.ts +130 -0
- package/src/lib/cn.ts +81 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/motion.ts +55 -0
- package/src/public/lea-83-time-walk.png +0 -0
- package/src/public/lea-83-time-walk.webp +0 -0
- package/src/stories/Auction.stories.tsx +658 -0
- package/src/stories/AuctionLayout.stories.tsx +313 -0
- package/src/stories/AuctionStatusTag.stories.tsx +166 -0
- package/src/stories/AuctionYourBidCard.stories.tsx +257 -0
- package/src/stories/Button.stories.tsx +306 -0
- package/src/stories/Countdown.stories.tsx +158 -0
- package/src/stories/Feedback.stories.tsx +80 -0
- package/src/stories/FramedImage.stories.tsx +46 -0
- package/src/stories/MorphDialog.stories.tsx +88 -0
- package/src/stories/Price.stories.tsx +292 -0
- package/src/stories/RankedList.stories.tsx +190 -0
- package/src/stories/Receipt.stories.tsx +221 -0
- package/src/stories/Scale.stories.tsx +578 -0
- package/src/stories/Separator.stories.tsx +188 -0
- package/src/stories/Skeleton.stories.tsx +138 -0
- package/src/stories/SteppedInput.stories.tsx +321 -0
- package/src/stories/Tabs.stories.tsx +215 -0
- package/src/stories/Tag.stories.tsx +138 -0
- package/src/stories/Text.stories.tsx +245 -0
- package/src/styles/globals.css +39 -0
- package/src/styles/index.css +4 -0
- package/src/styles/theme/animation.css +11 -0
- package/src/styles/theme/color.css +185 -0
- package/src/styles/theme/index.css +3 -0
- package/src/styles/theme/typography.css +3 -0
- package/src/styles/utility.css +8 -0
- package/src/types/index.ts +149 -0
- package/src/utils/format.ts +130 -0
- package/src/utils/index.ts +16 -0
- package/src/utils/rank-utils.ts +131 -0
- package/src/utils/tick-validation.ts +65 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { cn } from "@/lib";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// BigIntDecimal – pure bigint decimal arithmetic
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
function bigIntPow(base: bigint, exp: number): bigint {
|
|
10
|
+
let result = 1n;
|
|
11
|
+
let b = base;
|
|
12
|
+
let e = exp;
|
|
13
|
+
while (e > 0) {
|
|
14
|
+
if (e & 1) result *= b;
|
|
15
|
+
b *= b;
|
|
16
|
+
e >>= 1;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class BigIntDecimal {
|
|
22
|
+
constructor(
|
|
23
|
+
public int: bigint,
|
|
24
|
+
public decimals: number,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
clone(): BigIntDecimal {
|
|
28
|
+
return new BigIntDecimal(this.int, this.decimals);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Remove trailing zeros after the decimal point without losing precision.
|
|
33
|
+
*/
|
|
34
|
+
optimizeDecimals(): void {
|
|
35
|
+
const base = 10n;
|
|
36
|
+
while (this.decimals > 0 && this.int % base === 0n) {
|
|
37
|
+
this.decimals--;
|
|
38
|
+
this.int = this.int / base;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Round UP (ceil) to `precision` decimal places. If the current number of
|
|
44
|
+
* decimals is already ≤ precision, only trailing zeros are stripped.
|
|
45
|
+
*/
|
|
46
|
+
ceil(precision: number): void {
|
|
47
|
+
if (precision < 0) throw new Error("decimal precision < 0");
|
|
48
|
+
if (this.decimals === 0) return;
|
|
49
|
+
if (this.decimals <= precision) {
|
|
50
|
+
this.optimizeDecimals();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const base = 10n;
|
|
54
|
+
while (this.decimals > precision) {
|
|
55
|
+
const r = this.int % base;
|
|
56
|
+
if (r > 0n) this.int = this.int + base;
|
|
57
|
+
this.decimals--;
|
|
58
|
+
this.int = this.int / base;
|
|
59
|
+
}
|
|
60
|
+
this.optimizeDecimals();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Full-precision decimal string, e.g. "1.001" or "0" or "42".
|
|
65
|
+
*/
|
|
66
|
+
toString(): string {
|
|
67
|
+
const isNegative = this.int < 0n;
|
|
68
|
+
const abs = isNegative ? -this.int : this.int;
|
|
69
|
+
let full = abs.toString();
|
|
70
|
+
if (this.decimals === 0) return `${isNegative ? "-" : ""}${full}`;
|
|
71
|
+
if (this.decimals >= full.length) {
|
|
72
|
+
full = full.padStart(this.decimals + 1, "0");
|
|
73
|
+
}
|
|
74
|
+
const decIdx = full.length - this.decimals;
|
|
75
|
+
return `${isNegative ? "-" : ""}${full.slice(0, decIdx)}.${full.slice(decIdx)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Abbreviate large numbers: 100K, 2.5M, etc.
|
|
80
|
+
* Values < 1 are not abbreviated but still have maxDecimals applied.
|
|
81
|
+
*/
|
|
82
|
+
abbreviate(maxDecimals = 3): string {
|
|
83
|
+
const symbols: { exponent: number; symbol: string }[] = [
|
|
84
|
+
{ exponent: 9, symbol: "B" },
|
|
85
|
+
{ exponent: 6, symbol: "M" },
|
|
86
|
+
{ exponent: 3, symbol: "K" },
|
|
87
|
+
];
|
|
88
|
+
const abs = this.int < 0n ? -this.int : this.int;
|
|
89
|
+
const base = abs / bigIntPow(10n, this.decimals);
|
|
90
|
+
if (base < 1n) {
|
|
91
|
+
this.ceil(maxDecimals);
|
|
92
|
+
return this.toString();
|
|
93
|
+
}
|
|
94
|
+
for (const { exponent, symbol } of symbols) {
|
|
95
|
+
const threshold = bigIntPow(10n, exponent);
|
|
96
|
+
if (base >= threshold) {
|
|
97
|
+
this.decimals += exponent;
|
|
98
|
+
this.ceil(maxDecimals);
|
|
99
|
+
return this.toString() + symbol;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.ceil(maxDecimals);
|
|
103
|
+
return this.toString();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Formatting utilities
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
function toBigInt(value: bigint | number | string): bigint {
|
|
112
|
+
if (typeof value === "bigint") return value;
|
|
113
|
+
try {
|
|
114
|
+
return BigInt(value);
|
|
115
|
+
} catch {
|
|
116
|
+
return BigInt(0);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type FormatPriceOptions = {
|
|
121
|
+
/**
|
|
122
|
+
* Enable K / M / B abbreviation for large values.
|
|
123
|
+
* @default false
|
|
124
|
+
*/
|
|
125
|
+
abbreviate?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Maximum decimal digits to display. Excess precision is ceiled (rounded up)
|
|
128
|
+
* for safety in price contexts. When `undefined`, full precision is shown
|
|
129
|
+
* (trailing zeros stripped).
|
|
130
|
+
*/
|
|
131
|
+
maxDecimals?: number;
|
|
132
|
+
/**
|
|
133
|
+
* Minimum decimal digits to display. Pads trailing zeros when needed.
|
|
134
|
+
*/
|
|
135
|
+
minDecimals?: number;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
function padMinimumDecimals(formatted: string, minDecimals: number): string {
|
|
139
|
+
if (minDecimals <= 0) return formatted;
|
|
140
|
+
|
|
141
|
+
const suffixMatch = formatted.match(/([KMB])$/);
|
|
142
|
+
const suffix = suffixMatch ? suffixMatch[1] : "";
|
|
143
|
+
const numericPart = suffix ? formatted.slice(0, -1) : formatted;
|
|
144
|
+
const [integerPart, fractionPart = ""] = numericPart.split(".");
|
|
145
|
+
|
|
146
|
+
if (fractionPart.length >= minDecimals) return formatted;
|
|
147
|
+
|
|
148
|
+
const paddedFraction = fractionPart.padEnd(minDecimals, "0");
|
|
149
|
+
return `${integerPart}.${paddedFraction}${suffix}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Format a bigint price value into a human-readable string.
|
|
154
|
+
* All arithmetic stays in bigint to preserve precision.
|
|
155
|
+
*/
|
|
156
|
+
export function formatPrice(
|
|
157
|
+
value: bigint | number | string,
|
|
158
|
+
decimals: number,
|
|
159
|
+
options?: FormatPriceOptions,
|
|
160
|
+
): string {
|
|
161
|
+
const raw = toBigInt(value);
|
|
162
|
+
const num = new BigIntDecimal(raw, decimals);
|
|
163
|
+
num.optimizeDecimals();
|
|
164
|
+
|
|
165
|
+
if (options?.abbreviate) {
|
|
166
|
+
return num.abbreviate(options.maxDecimals ?? 3);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (typeof options?.maxDecimals === "number") {
|
|
170
|
+
num.ceil(options.maxDecimals);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const formatted = num.toString();
|
|
174
|
+
|
|
175
|
+
if (typeof options?.minDecimals === "number") {
|
|
176
|
+
return padMinimumDecimals(formatted, options.minDecimals);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return formatted;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Price context – shared config across compound children
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
type PriceContextValue = {
|
|
187
|
+
value: bigint | number | string;
|
|
188
|
+
decimals: number;
|
|
189
|
+
abbreviate: boolean;
|
|
190
|
+
maxDecimals: number | undefined;
|
|
191
|
+
minDecimals: number | undefined;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const PriceContext = React.createContext<PriceContextValue | null>(null);
|
|
195
|
+
|
|
196
|
+
function usePriceContext(): PriceContextValue {
|
|
197
|
+
const ctx = React.useContext(PriceContext);
|
|
198
|
+
if (!ctx) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
"Price compound components must be used within a <Price> root",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return ctx;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Compound components
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
export interface PriceProps extends React.ComponentProps<"span"> {
|
|
211
|
+
/** The raw integer price value. */
|
|
212
|
+
value: bigint | number | string;
|
|
213
|
+
/** Number of decimal places for the value. */
|
|
214
|
+
decimals: number;
|
|
215
|
+
/** Enable K / M / B abbreviation for large values. */
|
|
216
|
+
abbreviate?: boolean;
|
|
217
|
+
/** Max decimal digits to show. Excess is ceiled (rounded up). */
|
|
218
|
+
maxDecimals?: number;
|
|
219
|
+
/** Minimum decimal digits to show. Pads trailing zeros when needed. */
|
|
220
|
+
minDecimals?: number;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Root component. Can be used standalone (renders the formatted number) or
|
|
225
|
+
* with compound children for full control over symbol placement and layout.
|
|
226
|
+
*
|
|
227
|
+
* @example Standalone
|
|
228
|
+
* ```tsx
|
|
229
|
+
* <Price value={500000000000000000n} decimals={18} maxDecimals={4} />
|
|
230
|
+
* ```
|
|
231
|
+
*
|
|
232
|
+
* @example Composed
|
|
233
|
+
* ```tsx
|
|
234
|
+
* <Price value={500000000000000000n} decimals={18} maxDecimals={4}>
|
|
235
|
+
* <Price.Symbol>USD</Price.Symbol>
|
|
236
|
+
* <Price.Value />
|
|
237
|
+
* </Price>
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
function PriceRoot({
|
|
241
|
+
value,
|
|
242
|
+
decimals,
|
|
243
|
+
abbreviate = false,
|
|
244
|
+
maxDecimals,
|
|
245
|
+
minDecimals,
|
|
246
|
+
children,
|
|
247
|
+
className,
|
|
248
|
+
...props
|
|
249
|
+
}: PriceProps): React.ReactElement {
|
|
250
|
+
const ctx = useMemo<PriceContextValue>(
|
|
251
|
+
() => ({ value, decimals, abbreviate, maxDecimals, minDecimals }),
|
|
252
|
+
[value, decimals, abbreviate, maxDecimals, minDecimals],
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const hasChildren =
|
|
256
|
+
children !== undefined &&
|
|
257
|
+
children !== null &&
|
|
258
|
+
children !== true &&
|
|
259
|
+
children !== false;
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<PriceContext value={ctx}>
|
|
263
|
+
<span className={cn("tabular-nums", className)} {...props}>
|
|
264
|
+
{hasChildren ? children : <PriceValue />}
|
|
265
|
+
</span>
|
|
266
|
+
</PriceContext>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// --- Price.Value ---
|
|
271
|
+
|
|
272
|
+
export interface PriceValueProps extends React.ComponentProps<"span"> {
|
|
273
|
+
/**
|
|
274
|
+
* A BCP 47 locale tag (e.g. `"de-DE"`, `"ja-JP"`) used to localise
|
|
275
|
+
* decimal and thousands separators. When omitted the raw formatted string
|
|
276
|
+
* is rendered as-is (dot decimal separator, no grouping).
|
|
277
|
+
*/
|
|
278
|
+
locale?: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Localise a formatted price string by replacing the decimal separator and
|
|
283
|
+
* inserting thousands separators according to the given locale. Delegates
|
|
284
|
+
* integer grouping to `Intl.NumberFormat` so locale-specific patterns (e.g.
|
|
285
|
+
* Indian lakh grouping) are handled correctly. Works on abbreviated strings
|
|
286
|
+
* like "2.5K" too.
|
|
287
|
+
*/
|
|
288
|
+
function localizeFormatted(formatted: string, locale: string): string {
|
|
289
|
+
// Split potential abbreviation suffix (K / M / B)
|
|
290
|
+
const suffixMatch = formatted.match(/([KMB])$/);
|
|
291
|
+
const suffix = suffixMatch ? suffixMatch[1] : "";
|
|
292
|
+
const numPart = suffix ? formatted.slice(0, -1) : formatted;
|
|
293
|
+
|
|
294
|
+
const [intPart, fracPart] = numPart.split(".");
|
|
295
|
+
const isNegative = intPart.startsWith("-");
|
|
296
|
+
const absInt = isNegative ? intPart.slice(1) : intPart;
|
|
297
|
+
|
|
298
|
+
// Use Intl to format the integer part with locale-correct grouping
|
|
299
|
+
const intNum = Number(absInt);
|
|
300
|
+
const intFmt = new Intl.NumberFormat(locale, {
|
|
301
|
+
useGrouping: true,
|
|
302
|
+
maximumFractionDigits: 0,
|
|
303
|
+
});
|
|
304
|
+
const grouped = (isNegative ? "-" : "") + intFmt.format(intNum);
|
|
305
|
+
|
|
306
|
+
if (fracPart === undefined) return grouped + suffix;
|
|
307
|
+
|
|
308
|
+
// Discover the locale's decimal separator
|
|
309
|
+
const parts = new Intl.NumberFormat(locale).formatToParts(1.1);
|
|
310
|
+
const decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
|
|
311
|
+
|
|
312
|
+
return `${grouped}${decimal}${fracPart}${suffix}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Renders the formatted numeric value. Reads config from the parent `<Price>`.
|
|
317
|
+
*/
|
|
318
|
+
function PriceValue({
|
|
319
|
+
locale,
|
|
320
|
+
className,
|
|
321
|
+
...props
|
|
322
|
+
}: PriceValueProps): React.ReactElement {
|
|
323
|
+
const ctx = usePriceContext();
|
|
324
|
+
|
|
325
|
+
const formatted = useMemo(() => {
|
|
326
|
+
const raw = formatPrice(ctx.value, ctx.decimals, {
|
|
327
|
+
abbreviate: ctx.abbreviate,
|
|
328
|
+
maxDecimals: ctx.maxDecimals,
|
|
329
|
+
minDecimals: ctx.minDecimals,
|
|
330
|
+
});
|
|
331
|
+
if (locale) return localizeFormatted(raw, locale);
|
|
332
|
+
return raw;
|
|
333
|
+
}, [
|
|
334
|
+
ctx.value,
|
|
335
|
+
ctx.decimals,
|
|
336
|
+
ctx.abbreviate,
|
|
337
|
+
ctx.maxDecimals,
|
|
338
|
+
ctx.minDecimals,
|
|
339
|
+
locale,
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<span className={className} {...props}>
|
|
344
|
+
{formatted}
|
|
345
|
+
</span>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// --- Price.Symbol ---
|
|
350
|
+
|
|
351
|
+
export interface PriceSymbolProps extends React.ComponentProps<"span"> {}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Renders a currency symbol or label. Just a styled `<span>` — you control
|
|
355
|
+
* placement by ordering it before or after `<Price.Value>`.
|
|
356
|
+
*
|
|
357
|
+
* @example Suffix (default pattern)
|
|
358
|
+
* ```tsx
|
|
359
|
+
* <Price value={1000000n} decimals={6}>
|
|
360
|
+
* <Price.Value /> <Price.Symbol>USD</Price.Symbol>
|
|
361
|
+
* </Price>
|
|
362
|
+
* ```
|
|
363
|
+
*
|
|
364
|
+
* @example Prefix
|
|
365
|
+
* ```tsx
|
|
366
|
+
* <Price value={12345n} decimals={2}>
|
|
367
|
+
* <Price.Symbol>$</Price.Symbol><Price.Value />
|
|
368
|
+
* </Price>
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
function PriceSymbol({
|
|
372
|
+
children,
|
|
373
|
+
className,
|
|
374
|
+
...props
|
|
375
|
+
}: PriceSymbolProps): React.ReactElement {
|
|
376
|
+
return (
|
|
377
|
+
<span className={className} {...props}>
|
|
378
|
+
{children}
|
|
379
|
+
</span>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Compound export
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
export const Price: {
|
|
388
|
+
(props: PriceProps): React.ReactElement;
|
|
389
|
+
Value: typeof PriceValue;
|
|
390
|
+
Symbol: typeof PriceSymbol;
|
|
391
|
+
} = Object.assign(PriceRoot, {
|
|
392
|
+
Value: PriceValue,
|
|
393
|
+
Symbol: PriceSymbol,
|
|
394
|
+
}) as typeof Price;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
import { SteppedInput } from "./SteppedInput";
|
|
5
|
+
|
|
6
|
+
interface PriceInputRootProps {
|
|
7
|
+
value: bigint;
|
|
8
|
+
onChange: (value: bigint) => void;
|
|
9
|
+
min?: bigint;
|
|
10
|
+
max?: bigint;
|
|
11
|
+
getTickSize: (currentValue: bigint) => bigint;
|
|
12
|
+
decimals?: number;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
snapToTick?: "up" | "down" | "nearest" | false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function Root({
|
|
20
|
+
decimals = 2,
|
|
21
|
+
...props
|
|
22
|
+
}: PriceInputRootProps): React.ReactElement {
|
|
23
|
+
return <SteppedInput.Root {...props} decimals={decimals} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PriceInputComponent {
|
|
27
|
+
Root: typeof Root;
|
|
28
|
+
Group: typeof SteppedInput.Group;
|
|
29
|
+
Decrement: typeof SteppedInput.Decrement;
|
|
30
|
+
Increment: typeof SteppedInput.Increment;
|
|
31
|
+
ScrubArea: typeof SteppedInput.ScrubArea;
|
|
32
|
+
ScrubAreaCursor: typeof SteppedInput.ScrubAreaCursor;
|
|
33
|
+
Input: typeof SteppedInput.Input;
|
|
34
|
+
Value: typeof SteppedInput.Value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const PriceInput = {
|
|
38
|
+
Root,
|
|
39
|
+
Group: SteppedInput.Group,
|
|
40
|
+
Decrement: SteppedInput.Decrement,
|
|
41
|
+
Increment: SteppedInput.Increment,
|
|
42
|
+
ScrubArea: SteppedInput.ScrubArea,
|
|
43
|
+
ScrubAreaCursor: SteppedInput.ScrubAreaCursor,
|
|
44
|
+
Input: SteppedInput.Input,
|
|
45
|
+
Value: SteppedInput.Value,
|
|
46
|
+
} as PriceInputComponent;
|
|
47
|
+
|
|
48
|
+
export { PriceInput };
|