@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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/blocks/auction/Auction.d.ts +49 -0
  3. package/dist/components/blocks/auction/Auction.js +44 -0
  4. package/dist/components/blocks/auction/AuctionBidForm.d.ts +11 -0
  5. package/dist/components/blocks/auction/AuctionBidForm.js +88 -0
  6. package/dist/components/blocks/auction/AuctionBidInput.d.ts +9 -0
  7. package/dist/components/blocks/auction/AuctionBidInput.js +99 -0
  8. package/dist/components/blocks/auction/AuctionContext.d.ts +71 -0
  9. package/dist/components/blocks/auction/AuctionContext.js +228 -0
  10. package/dist/components/blocks/auction/AuctionInfo.d.ts +9 -0
  11. package/dist/components/blocks/auction/AuctionInfo.js +37 -0
  12. package/dist/components/blocks/auction/AuctionLayout.d.ts +63 -0
  13. package/dist/components/blocks/auction/AuctionLayout.js +80 -0
  14. package/dist/components/blocks/auction/AuctionRankings.d.ts +16 -0
  15. package/dist/components/blocks/auction/AuctionRankings.js +334 -0
  16. package/dist/components/blocks/auction/AuctionStatusTag.d.ts +15 -0
  17. package/dist/components/blocks/auction/AuctionStatusTag.js +60 -0
  18. package/dist/components/blocks/auction/AuctionSuggestedBids.d.ts +38 -0
  19. package/dist/components/blocks/auction/AuctionSuggestedBids.js +116 -0
  20. package/dist/components/blocks/auction/AuctionYourBidCard.d.ts +27 -0
  21. package/dist/components/blocks/auction/AuctionYourBidCard.js +94 -0
  22. package/dist/components/blocks/auction/AuctionYourBids.d.ts +9 -0
  23. package/dist/components/blocks/auction/AuctionYourBids.js +49 -0
  24. package/dist/components/blocks/auction/index.d.ts +12 -0
  25. package/dist/components/blocks/index.d.ts +12 -0
  26. package/dist/components/index.d.ts +28 -0
  27. package/dist/components/primitives/Button.d.ts +31 -0
  28. package/dist/components/primitives/Button.js +117 -0
  29. package/dist/components/primitives/Drawer.d.ts +43 -0
  30. package/dist/components/primitives/Drawer.js +51 -0
  31. package/dist/components/primitives/Feedback.d.ts +28 -0
  32. package/dist/components/primitives/Feedback.js +147 -0
  33. package/dist/components/primitives/MorphDialog.d.ts +39 -0
  34. package/dist/components/primitives/MorphDialog.js +87 -0
  35. package/dist/components/primitives/Price.d.ts +84 -0
  36. package/dist/components/primitives/Price.js +255 -0
  37. package/dist/components/primitives/PriceInput.d.ts +33 -0
  38. package/dist/components/primitives/PriceInput.js +25 -0
  39. package/dist/components/primitives/Receipt.d.ts +164 -0
  40. package/dist/components/primitives/Receipt.js +344 -0
  41. package/dist/components/primitives/Scale.d.ts +67 -0
  42. package/dist/components/primitives/Scale.js +132 -0
  43. package/dist/components/primitives/Separator.d.ts +22 -0
  44. package/dist/components/primitives/Separator.js +62 -0
  45. package/dist/components/primitives/Skeleton.d.ts +14 -0
  46. package/dist/components/primitives/Skeleton.js +20 -0
  47. package/dist/components/primitives/SteppedInput.d.ts +94 -0
  48. package/dist/components/primitives/SteppedInput.js +154 -0
  49. package/dist/components/primitives/Tabs.d.ts +37 -0
  50. package/dist/components/primitives/Tabs.js +99 -0
  51. package/dist/components/primitives/Tag.d.ts +24 -0
  52. package/dist/components/primitives/Tag.js +22 -0
  53. package/dist/components/primitives/Text.d.ts +32 -0
  54. package/dist/components/primitives/Text.js +65 -0
  55. package/dist/components/primitives/countdown/Countdown.d.ts +24 -0
  56. package/dist/components/primitives/countdown/Countdown.js +22 -0
  57. package/dist/components/primitives/framed-image/FramedImage.d.ts +13 -0
  58. package/dist/components/primitives/framed-image/FramedImage.js +37 -0
  59. package/dist/components/primitives/index.d.ts +17 -0
  60. package/dist/components/primitives/ranked-list/Ranking.d.ts +117 -0
  61. package/dist/components/primitives/ranked-list/Ranking.js +219 -0
  62. package/dist/components/primitives/ranked-list/index.d.ts +1 -0
  63. package/dist/hooks/useCountdown.d.ts +20 -0
  64. package/dist/hooks/useCountdown.js +75 -0
  65. package/dist/index.d.ts +36 -0
  66. package/dist/index.js +36 -0
  67. package/dist/lib/cn.d.ts +6 -0
  68. package/dist/lib/cn.js +75 -0
  69. package/dist/lib/motion.d.ts +19 -0
  70. package/dist/lib/motion.js +43 -0
  71. package/dist/types/index.d.ts +120 -0
  72. package/dist/utils/format.d.ts +38 -0
  73. package/dist/utils/format.js +103 -0
  74. package/dist/utils/rank-utils.d.ts +34 -0
  75. package/dist/utils/rank-utils.js +80 -0
  76. package/dist/utils/tick-validation.d.ts +22 -0
  77. package/dist/utils/tick-validation.js +40 -0
  78. package/package.json +92 -0
  79. package/src/components/blocks/auction/Auction.tsx +74 -0
  80. package/src/components/blocks/auction/AuctionArtwork.tsx +4 -0
  81. package/src/components/blocks/auction/AuctionBidForm.tsx +138 -0
  82. package/src/components/blocks/auction/AuctionBidInput.tsx +166 -0
  83. package/src/components/blocks/auction/AuctionContext.tsx +401 -0
  84. package/src/components/blocks/auction/AuctionInfo.tsx +36 -0
  85. package/src/components/blocks/auction/AuctionLayout.tsx +200 -0
  86. package/src/components/blocks/auction/AuctionRankings.tsx +435 -0
  87. package/src/components/blocks/auction/AuctionStatusTag.tsx +98 -0
  88. package/src/components/blocks/auction/AuctionSuggestedBids.tsx +203 -0
  89. package/src/components/blocks/auction/AuctionYourBidCard.tsx +125 -0
  90. package/src/components/blocks/auction/AuctionYourBids.tsx +61 -0
  91. package/src/components/blocks/auction/index.ts +42 -0
  92. package/src/components/blocks/index.ts +1 -0
  93. package/src/components/index.ts +2 -0
  94. package/src/components/primitives/Button.tsx +183 -0
  95. package/src/components/primitives/Drawer.tsx +125 -0
  96. package/src/components/primitives/Feedback.tsx +185 -0
  97. package/src/components/primitives/MorphDialog.tsx +160 -0
  98. package/src/components/primitives/Price.tsx +394 -0
  99. package/src/components/primitives/PriceInput.tsx +48 -0
  100. package/src/components/primitives/Receipt.tsx +711 -0
  101. package/src/components/primitives/Scale.tsx +287 -0
  102. package/src/components/primitives/Separator.tsx +87 -0
  103. package/src/components/primitives/Skeleton.tsx +33 -0
  104. package/src/components/primitives/SteppedInput.tsx +313 -0
  105. package/src/components/primitives/Tabs.tsx +161 -0
  106. package/src/components/primitives/Tag.tsx +48 -0
  107. package/src/components/primitives/Text.tsx +102 -0
  108. package/src/components/primitives/countdown/Countdown.tsx +43 -0
  109. package/src/components/primitives/countdown/index.ts +2 -0
  110. package/src/components/primitives/framed-image/FramedImage.tsx +51 -0
  111. package/src/components/primitives/framed-image/index.ts +1 -0
  112. package/src/components/primitives/index.ts +42 -0
  113. package/src/components/primitives/ranked-list/RankedList.tsx +9 -0
  114. package/src/components/primitives/ranked-list/Ranking.tsx +454 -0
  115. package/src/components/primitives/ranked-list/index.ts +8 -0
  116. package/src/hooks/index.ts +1 -0
  117. package/src/hooks/useCountdown.ts +91 -0
  118. package/src/index.ts +130 -0
  119. package/src/lib/cn.ts +81 -0
  120. package/src/lib/index.ts +2 -0
  121. package/src/lib/motion.ts +55 -0
  122. package/src/public/lea-83-time-walk.png +0 -0
  123. package/src/public/lea-83-time-walk.webp +0 -0
  124. package/src/stories/Auction.stories.tsx +658 -0
  125. package/src/stories/AuctionLayout.stories.tsx +313 -0
  126. package/src/stories/AuctionStatusTag.stories.tsx +166 -0
  127. package/src/stories/AuctionYourBidCard.stories.tsx +257 -0
  128. package/src/stories/Button.stories.tsx +306 -0
  129. package/src/stories/Countdown.stories.tsx +158 -0
  130. package/src/stories/Feedback.stories.tsx +80 -0
  131. package/src/stories/FramedImage.stories.tsx +46 -0
  132. package/src/stories/MorphDialog.stories.tsx +88 -0
  133. package/src/stories/Price.stories.tsx +292 -0
  134. package/src/stories/RankedList.stories.tsx +190 -0
  135. package/src/stories/Receipt.stories.tsx +221 -0
  136. package/src/stories/Scale.stories.tsx +578 -0
  137. package/src/stories/Separator.stories.tsx +188 -0
  138. package/src/stories/Skeleton.stories.tsx +138 -0
  139. package/src/stories/SteppedInput.stories.tsx +321 -0
  140. package/src/stories/Tabs.stories.tsx +215 -0
  141. package/src/stories/Tag.stories.tsx +138 -0
  142. package/src/stories/Text.stories.tsx +245 -0
  143. package/src/styles/globals.css +39 -0
  144. package/src/styles/index.css +4 -0
  145. package/src/styles/theme/animation.css +11 -0
  146. package/src/styles/theme/color.css +185 -0
  147. package/src/styles/theme/index.css +3 -0
  148. package/src/styles/theme/typography.css +3 -0
  149. package/src/styles/utility.css +8 -0
  150. package/src/types/index.ts +149 -0
  151. package/src/utils/format.ts +130 -0
  152. package/src/utils/index.ts +16 -0
  153. package/src/utils/rank-utils.ts +131 -0
  154. package/src/utils/tick-validation.ts +65 -0
@@ -0,0 +1,658 @@
1
+ import { useEffect } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Ranking, Separator, Text } from "@/components";
4
+ import {
5
+ Auction,
6
+ AuctionArtwork,
7
+ AuctionBiddingPanel,
8
+ AuctionBidForm,
9
+ AuctionDetails,
10
+ AuctionDetailsBody,
11
+ AuctionDetailsHeader,
12
+ AuctionLayout,
13
+ AuctionRankings,
14
+ AuctionRankingsContainer,
15
+ AuctionStatusTag,
16
+ useAuctionContext,
17
+ } from "@/components/blocks/auction";
18
+ import type { AuctionData, AuctionUserBid, RankableBid } from "@/types";
19
+ import { formatShortRelative } from "@/utils";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Artwork constants
23
+ // ---------------------------------------------------------------------------
24
+
25
+ // Multi-edition: Hokusai's "The Great Wave" – a natural fit for a limited edition art print
26
+ const EDITION_ARTWORK_URL =
27
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Tsunami_by_hokusai_19th_century.jpg/1280px-Tsunami_by_hokusai_19th_century.jpg";
28
+ const EDITION_ARTWORK_ALT = "The Great Wave off Kanagawa";
29
+
30
+ // Single item: MTG Alpha "Time Walk" – a 1-of-1 collectible
31
+ const SINGLE_ARTWORK_URL = "/lea-83-time-walk.png";
32
+ const SINGLE_ARTWORK_ALT = "LEA-83 Time Walk";
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Shared helpers
36
+ // ---------------------------------------------------------------------------
37
+
38
+ const mockBidders = [
39
+ { id: "0xalice", name: "Alice" },
40
+ { id: "0xbob", name: "Bob" },
41
+ { id: "0xcharlie", name: "Charlie" },
42
+ { id: "0xdiana", name: "Diana" },
43
+ { id: "0xeve", name: "Eve" },
44
+ { id: "0xfrank", name: "Frank" },
45
+ { id: "0xgrace", name: "Grace" },
46
+ { id: "0xhenry", name: "Henry" },
47
+ { id: "0xivy", name: "Ivy" },
48
+ { id: "0xjack", name: "Jack" },
49
+ { id: "0xkate", name: "Kate" },
50
+ { id: "0xleo", name: "Leo" },
51
+ { id: "0xmia", name: "Mia" },
52
+ { id: "0xnathan", name: "Nathan" },
53
+ { id: "0xolivia", name: "Olivia" },
54
+ { id: "0xpeter", name: "Peter" },
55
+ { id: "0xquinn", name: "Quinn" },
56
+ { id: "0xrachel", name: "Rachel" },
57
+ { id: "0xsam", name: "Sam" },
58
+ { id: "0xtina", name: "Tina" },
59
+ { id: "0xuma", name: "Uma" },
60
+ { id: "0xvictor", name: "Victor" },
61
+ { id: "0xwendy", name: "Wendy" },
62
+ { id: "0xxavier", name: "Xavier" },
63
+ { id: "0xyara", name: "Yara" },
64
+ { id: "0xzack", name: "Zack" },
65
+ ];
66
+
67
+ function formatPrice(price: bigint) {
68
+ const dollars = Number(price) / 100;
69
+ return dollars.toLocaleString("en-US", {
70
+ minimumFractionDigits: 2,
71
+ maximumFractionDigits: 2,
72
+ });
73
+ }
74
+
75
+ const dollarFormatters = {
76
+ formatPrice,
77
+ currencySymbol: "$",
78
+ inputDecimals: 2,
79
+ };
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Multi-edition mock data (20 editions of an art print)
83
+ // ---------------------------------------------------------------------------
84
+
85
+ // Dollar auction using cents as the base unit (1 dollar = 100 cents)
86
+ const mockAuction: AuctionData = {
87
+ id: "0x1234567890abcdef1234567890abcdef12345678",
88
+ reservePrice: 10000n, // $100 in cents
89
+ opensAt: new Date(Date.now() - 86400000),
90
+ endsAt: new Date(Date.now() + 86400000 * 3),
91
+ maxTotalItems: 20,
92
+ tickConfig: {
93
+ threshold: 100000n, // $1000 in cents
94
+ smallTickSize: 1000n, // $10 in cents
95
+ largeTickSize: 10000n, // $100 in cents
96
+ },
97
+ };
98
+
99
+ const mockBids: RankableBid[] = Array.from({ length: 26 }, (_, i) => ({
100
+ id: String(i + 1),
101
+ price: (50000n - BigInt(i) * 1500n).toString(),
102
+ created_at: new Date(Date.now() - (3600000 + i * 1200000)).toISOString(),
103
+ bidder: mockBidders[i],
104
+ }));
105
+
106
+ // User bid matching bid #9 in the rankings (price $380 = 38000 cents)
107
+ const mockUserBids: AuctionUserBid[] = [
108
+ {
109
+ id: "9",
110
+ price: 38000n, // $380 in cents
111
+ createdAt: new Date(Date.now() - 18000000),
112
+ bidder: { id: "0xivy", name: "You" },
113
+ globalBidId: 9n,
114
+ status: "active",
115
+ isWinning: true,
116
+ },
117
+ ];
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Single-item mock data (1-of-1 collectible card)
121
+ // ---------------------------------------------------------------------------
122
+
123
+ const mockSingleAuction: AuctionData = {
124
+ id: "0xabcdef1234567890abcdef1234567890abcdef12",
125
+ reservePrice: 50000n, // $500 in cents
126
+ opensAt: new Date(Date.now() - 86400000 * 2),
127
+ endsAt: new Date(Date.now() + 86400000),
128
+ maxTotalItems: 1,
129
+ tickConfig: {
130
+ threshold: 500000n, // $5000 in cents
131
+ smallTickSize: 5000n, // $50 in cents
132
+ largeTickSize: 50000n, // $500 in cents
133
+ },
134
+ };
135
+
136
+ const mockSingleBids: RankableBid[] = [
137
+ {
138
+ id: "s1",
139
+ price: "280000", // $2,800
140
+ created_at: new Date(Date.now() - 1800000).toISOString(),
141
+ bidder: { id: "0xalice", name: "Alice" },
142
+ },
143
+ {
144
+ id: "s2",
145
+ price: "250000", // $2,500
146
+ created_at: new Date(Date.now() - 3600000).toISOString(),
147
+ bidder: { id: "0xbob", name: "Bob" },
148
+ },
149
+ {
150
+ id: "s3",
151
+ price: "220000", // $2,200
152
+ created_at: new Date(Date.now() - 7200000).toISOString(),
153
+ bidder: { id: "0xcharlie", name: "Charlie" },
154
+ },
155
+ {
156
+ id: "s4",
157
+ price: "180000", // $1,800
158
+ created_at: new Date(Date.now() - 14400000).toISOString(),
159
+ bidder: { id: "0xdiana", name: "Diana" },
160
+ },
161
+ {
162
+ id: "s5",
163
+ price: "150000", // $1,500
164
+ created_at: new Date(Date.now() - 21600000).toISOString(),
165
+ bidder: { id: "0xeve", name: "Eve" },
166
+ },
167
+ {
168
+ id: "s6",
169
+ price: "120000", // $1,200
170
+ created_at: new Date(Date.now() - 36000000).toISOString(),
171
+ bidder: { id: "0xfrank", name: "Frank" },
172
+ },
173
+ {
174
+ id: "s7",
175
+ price: "100000", // $1,000
176
+ created_at: new Date(Date.now() - 50400000).toISOString(),
177
+ bidder: { id: "0xgrace", name: "Grace" },
178
+ },
179
+ {
180
+ id: "s8",
181
+ price: "75000", // $750
182
+ created_at: new Date(Date.now() - 64800000).toISOString(),
183
+ bidder: { id: "0xhenry", name: "Henry" },
184
+ },
185
+ {
186
+ id: "s9",
187
+ price: "50000", // $500 (reserve price)
188
+ created_at: new Date(Date.now() - 86400000).toISOString(),
189
+ bidder: { id: "0xivy", name: "Ivy" },
190
+ },
191
+ ];
192
+
193
+ // User bid at $2,200 (rank #3) for the SingleItemWithBid story
194
+ const mockSingleUserBids: AuctionUserBid[] = [
195
+ {
196
+ id: "s3",
197
+ price: 220000n, // $2,200 in cents
198
+ createdAt: new Date(Date.now() - 7200000),
199
+ bidder: { id: "0xcharlie", name: "You" },
200
+ globalBidId: 3n,
201
+ status: "active",
202
+ isWinning: false,
203
+ },
204
+ ];
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Story meta
208
+ // ---------------------------------------------------------------------------
209
+
210
+ const meta: Meta<typeof Auction> = {
211
+ title: "Blocks/Auction",
212
+ component: Auction,
213
+ parameters: {
214
+ layout: "fullscreen",
215
+ },
216
+ };
217
+
218
+ export default meta;
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Shared sub-components
222
+ // ---------------------------------------------------------------------------
223
+
224
+ function AuctionStatus() {
225
+ const { auction } = useAuctionContext();
226
+ return (
227
+ <div className="text-xs">
228
+ <AuctionStatusTag
229
+ opensAt={auction.opensAt}
230
+ endsAt={auction.endsAt}
231
+ background="transparent"
232
+ />
233
+ </div>
234
+ );
235
+ }
236
+
237
+ function AutoStartBidding() {
238
+ const { isBiddingActive, startBidding, setShowBidPreview } =
239
+ useAuctionContext();
240
+
241
+ useEffect(() => {
242
+ if (isBiddingActive) return;
243
+ startBidding();
244
+ setShowBidPreview(true);
245
+ }, [isBiddingActive, setShowBidPreview, startBidding]);
246
+
247
+ return null;
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Multi-edition stories (20 editions of an art print)
252
+ // ---------------------------------------------------------------------------
253
+
254
+ export const Default: StoryObj<typeof Auction> = {
255
+ render: () => (
256
+ <div className="h-screen w-full bg-muted p-8">
257
+ <div className="mx-auto flex h-full max-w-7xl items-center justify-center">
258
+ <Auction
259
+ auction={mockAuction}
260
+ bids={mockBids}
261
+ userBids={[]}
262
+ formatters={dollarFormatters}
263
+ onPlaceBid={async (price, qty) => {
264
+ console.log("Place bid:", price, "cents", qty);
265
+ return true;
266
+ }}
267
+ onTopUpBid={async (bidId, newPrice, value) => {
268
+ console.log("Top up:", bidId, newPrice, "cents", value);
269
+ return true;
270
+ }}
271
+ className="size-full overflow-hidden rounded-lg border border-border bg-background shadow-lg"
272
+ >
273
+ <AuctionLayout>
274
+ <AuctionDetails>
275
+ <AuctionDetailsHeader>
276
+ <AuctionStatus />
277
+ <h2 className="mt-2 text-lg font-semibold">
278
+ The Great Wave off Kanagawa
279
+ </h2>
280
+ <Text color="secondary">Katsushika Hokusai</Text>
281
+ <AuctionArtwork
282
+ className="mt-4"
283
+ src={EDITION_ARTWORK_URL}
284
+ alt={EDITION_ARTWORK_ALT}
285
+ />
286
+ </AuctionDetailsHeader>
287
+ <AuctionDetailsBody>
288
+ <div className="space-y-4">
289
+ <div className="flex justify-between">
290
+ <Text size="2" color="tertiary">
291
+ Editions
292
+ </Text>
293
+ <Text size="2">20</Text>
294
+ </div>
295
+ <div className="flex justify-between">
296
+ <Text size="2" color="tertiary">
297
+ Reserve Price
298
+ </Text>
299
+ <Text size="2" tabularNums>
300
+ $100.00
301
+ </Text>
302
+ </div>
303
+ <Separator />
304
+ <Text size="2" color="secondary">
305
+ Bid on one of 20 limited edition prints. The top 20 bidders
306
+ each win an edition.
307
+ </Text>
308
+ </div>
309
+ </AuctionDetailsBody>
310
+ </AuctionDetails>
311
+ <AuctionRankingsContainer>
312
+ <AuctionRankings />
313
+ <AuctionBiddingPanel>
314
+ <AuctionBidForm.Root />
315
+ </AuctionBiddingPanel>
316
+ </AuctionRankingsContainer>
317
+ </AuctionLayout>
318
+ </Auction>
319
+ </div>
320
+ </div>
321
+ ),
322
+ };
323
+
324
+ export const WithTopUp: StoryObj<typeof Auction> = {
325
+ render: () => (
326
+ <div className="h-screen w-full bg-muted p-8">
327
+ <div className="mx-auto flex h-full max-w-7xl items-center justify-center">
328
+ <Auction
329
+ auction={mockAuction}
330
+ bids={mockBids}
331
+ userBids={mockUserBids}
332
+ formatters={dollarFormatters}
333
+ onPlaceBid={async (price, qty) => {
334
+ console.log("Place bid:", price, "cents", qty);
335
+ return true;
336
+ }}
337
+ onTopUpBid={async (bidId, newPrice, value) => {
338
+ console.log("Top up:", bidId, newPrice, "cents", value);
339
+ return true;
340
+ }}
341
+ className="size-full overflow-hidden rounded-lg border border-border bg-background shadow-lg"
342
+ >
343
+ <AuctionLayout>
344
+ <AuctionDetails>
345
+ <AuctionDetailsHeader>
346
+ <AuctionStatus />
347
+ <h2 className="mt-2 text-lg font-semibold">
348
+ The Great Wave off Kanagawa
349
+ </h2>
350
+ <Text color="secondary">Katsushika Hokusai</Text>
351
+ <AuctionArtwork
352
+ className="mt-4"
353
+ src={EDITION_ARTWORK_URL}
354
+ alt={EDITION_ARTWORK_ALT}
355
+ />
356
+ </AuctionDetailsHeader>
357
+ <AuctionDetailsBody>
358
+ <div className="space-y-4">
359
+ <div className="flex justify-between">
360
+ <Text size="2" color="tertiary">
361
+ Editions
362
+ </Text>
363
+ <Text size="2">20</Text>
364
+ </div>
365
+ <div className="flex justify-between">
366
+ <Text size="2" color="tertiary">
367
+ Reserve Price
368
+ </Text>
369
+ <Text size="2" tabularNums>
370
+ $100.00
371
+ </Text>
372
+ </div>
373
+ <Separator />
374
+ <Text size="2" color="secondary">
375
+ Bid on one of 20 limited edition prints. The top 20 bidders
376
+ each win an edition.
377
+ </Text>
378
+ </div>
379
+ </AuctionDetailsBody>
380
+ </AuctionDetails>
381
+ <AuctionRankingsContainer>
382
+ <AuctionRankings />
383
+ <AuctionBiddingPanel>
384
+ <AuctionBidForm.Root />
385
+ </AuctionBiddingPanel>
386
+ </AuctionRankingsContainer>
387
+ </AuctionLayout>
388
+ </Auction>
389
+ </div>
390
+ </div>
391
+ ),
392
+ };
393
+
394
+ export const MobileBiddingOverlay: StoryObj<typeof Auction> = {
395
+ render: () => (
396
+ <div className="h-screen w-full bg-muted p-4 lg:p-8">
397
+ <div className="mx-auto flex h-full max-w-sm items-center justify-center">
398
+ <Auction
399
+ auction={mockAuction}
400
+ bids={mockBids}
401
+ userBids={[]}
402
+ formatters={dollarFormatters}
403
+ onPlaceBid={async (price, qty) => {
404
+ console.log("Place bid:", price, "cents", qty);
405
+ return true;
406
+ }}
407
+ onTopUpBid={async (bidId, newPrice, value) => {
408
+ console.log("Top up:", bidId, newPrice, "cents", value);
409
+ return true;
410
+ }}
411
+ className="size-full max-h-180 overflow-hidden rounded-lg border border-border bg-background shadow-lg"
412
+ >
413
+ <AutoStartBidding />
414
+ <AuctionLayout>
415
+ <AuctionDetails>
416
+ <AuctionDetailsHeader>
417
+ <AuctionStatus />
418
+ <h2 className="mt-2 text-lg font-semibold">
419
+ The Great Wave off Kanagawa
420
+ </h2>
421
+ <Text color="secondary">Katsushika Hokusai</Text>
422
+ <AuctionArtwork
423
+ className="mt-4"
424
+ src={EDITION_ARTWORK_URL}
425
+ alt={EDITION_ARTWORK_ALT}
426
+ />
427
+ </AuctionDetailsHeader>
428
+ <AuctionDetailsBody>
429
+ <div className="space-y-4">
430
+ <div className="flex justify-between">
431
+ <Text size="2" color="tertiary">
432
+ Editions
433
+ </Text>
434
+ <Text size="2">20</Text>
435
+ </div>
436
+ <div className="flex justify-between">
437
+ <Text size="2" color="tertiary">
438
+ Reserve Price
439
+ </Text>
440
+ <Text size="2" tabularNums>
441
+ $100.00
442
+ </Text>
443
+ </div>
444
+ <Separator />
445
+ <Text size="2" color="secondary">
446
+ Bid on one of 20 limited edition prints. The top 20 bidders
447
+ each win an edition.
448
+ </Text>
449
+ </div>
450
+ </AuctionDetailsBody>
451
+ </AuctionDetails>
452
+ <AuctionRankingsContainer>
453
+ <AuctionRankings />
454
+ <AuctionBiddingPanel>
455
+ <AuctionBidForm.Root />
456
+ </AuctionBiddingPanel>
457
+ </AuctionRankingsContainer>
458
+ </AuctionLayout>
459
+ </Auction>
460
+ </div>
461
+ </div>
462
+ ),
463
+ };
464
+
465
+ // ---------------------------------------------------------------------------
466
+ // Single-item stories (1-of-1 collectible)
467
+ // ---------------------------------------------------------------------------
468
+
469
+ interface SingleItemBid {
470
+ id: string;
471
+ name: string;
472
+ price: string;
473
+ time: string;
474
+ }
475
+
476
+ const singleItemBids: SingleItemBid[] = mockSingleBids.map((b) => ({
477
+ id: b.id,
478
+ name: b.bidder?.name || `${b.bidder?.id.slice(0, 6)}...`,
479
+ price: formatPrice(BigInt(b.price)),
480
+ time: formatShortRelative(new Date(b.created_at)),
481
+ }));
482
+
483
+ /**
484
+ * Renders a preview slot in the Ranking component that shows where the user's
485
+ * current bid (from the bid form) would land. Reads bidValue/showBidPreview
486
+ * from the auction context so it updates live as the user adjusts the input.
487
+ */
488
+ function BidPreviewSlot() {
489
+ const {
490
+ bidValue,
491
+ minBidValue,
492
+ showBidPreview,
493
+ isBiddingActive,
494
+ formatPrice: ctxFormatPrice,
495
+ currencySymbol,
496
+ getProjectedRank,
497
+ } = useAuctionContext();
498
+
499
+ if (!isBiddingActive || !showBidPreview || bidValue < minBidValue)
500
+ return null;
501
+
502
+ const { rank } = getProjectedRank(bidValue);
503
+ const atIndex = rank ? rank - 1 : 0;
504
+
505
+ return (
506
+ <Ranking.Slot slotKey="bid-preview" atIndex={atIndex}>
507
+ {(ctx) => (
508
+ <>
509
+ <div className="relative">
510
+ <div className="absolute inset-0 animate-[pulse_2s_ease-in-out_infinite] bg-success/10" />
511
+ <div className="relative flex items-center justify-between gap-2 px-6 py-4">
512
+ <div className="flex min-w-0 items-center gap-3">
513
+ <Text color="tertiary" className="w-8 shrink-0" size="2">
514
+ #{ctx.rank}
515
+ </Text>
516
+ <Text color="secondary" size="3" weight="medium">
517
+ Your bid
518
+ </Text>
519
+ </div>
520
+ <div className="flex shrink-0 items-center gap-3">
521
+ <Text tabularNums size="3" weight="medium">
522
+ {ctxFormatPrice(bidValue)} {currencySymbol}
523
+ </Text>
524
+ <span className="min-w-9" />
525
+ </div>
526
+ </div>
527
+ </div>
528
+ {!ctx.isLastInGroup && <Separator />}
529
+ </>
530
+ )}
531
+ </Ranking.Slot>
532
+ );
533
+ }
534
+
535
+ function SingleItemContent({ userBids }: { userBids: AuctionUserBid[] }) {
536
+ return (
537
+ <div className="h-screen w-full bg-muted p-8">
538
+ <div className="mx-auto flex h-full max-w-3xl items-center justify-center">
539
+ <Auction
540
+ auction={mockSingleAuction}
541
+ bids={mockSingleBids}
542
+ userBids={userBids}
543
+ formatters={dollarFormatters}
544
+ onPlaceBid={async (price, qty) => {
545
+ console.log("Place bid:", price, "cents", qty);
546
+ return true;
547
+ }}
548
+ onTopUpBid={async (bidId, newPrice, value) => {
549
+ console.log("Top up:", bidId, newPrice, "cents", value);
550
+ return true;
551
+ }}
552
+ className="flex h-full max-h-180 w-full overflow-hidden rounded-lg border border-border bg-background shadow-lg"
553
+ >
554
+ <div className="hidden min-w-0 flex-1 flex-col gap-6 p-6 lg:flex">
555
+ <div className="shrink-0">
556
+ <AuctionStatus />
557
+ <h2 className="mt-2 text-lg font-semibold">Time Walk</h2>
558
+ <Text color="secondary">
559
+ Magic: The Gathering &middot; Alpha Edition
560
+ </Text>
561
+ </div>
562
+ <AuctionArtwork
563
+ className="min-h-0 flex-1"
564
+ src={SINGLE_ARTWORK_URL}
565
+ alt={SINGLE_ARTWORK_ALT}
566
+ />
567
+ <div className="shrink-0 space-y-2 border-t border-border pt-6">
568
+ <div className="flex justify-between">
569
+ <Text size="2" color="tertiary">
570
+ Edition
571
+ </Text>
572
+ <Text size="2">1 of 1</Text>
573
+ </div>
574
+ <div className="flex justify-between">
575
+ <Text size="2" color="tertiary">
576
+ Reserve Price
577
+ </Text>
578
+ <Text size="2" tabularNums>
579
+ $500.00
580
+ </Text>
581
+ </div>
582
+ </div>
583
+ </div>
584
+ <div className="flex min-w-0 flex-1 flex-col lg:border-l lg:border-border">
585
+ <div className="min-h-0 flex-1 overflow-y-auto">
586
+ <Ranking.Root items={singleItemBids} getKey={(bid) => bid.id}>
587
+ <BidPreviewSlot />
588
+ <Ranking.Group>
589
+ <Ranking.GroupItem>
590
+ <Ranking.GroupItemValue>
591
+ {(bid: SingleItemBid, ctx) => {
592
+ const isHighest = ctx.globalIndex === 0;
593
+ return (
594
+ <>
595
+ <div
596
+ className={`flex items-center justify-between gap-2 px-6 ${
597
+ isHighest ? "py-4" : "py-2 opacity-50"
598
+ }`}
599
+ >
600
+ <div className="flex min-w-0 items-center gap-3">
601
+ <Text
602
+ color="tertiary"
603
+ className="w-8 shrink-0"
604
+ size={isHighest ? "2" : "1"}
605
+ >
606
+ #{ctx.globalIndex + 1}
607
+ </Text>
608
+ <Text
609
+ className="truncate"
610
+ size={isHighest ? "3" : undefined}
611
+ weight={isHighest ? "medium" : undefined}
612
+ >
613
+ {bid.name}
614
+ </Text>
615
+ </div>
616
+ <div className="flex shrink-0 items-center gap-3">
617
+ <Text
618
+ tabularNums
619
+ size={isHighest ? "3" : undefined}
620
+ weight={isHighest ? "medium" : undefined}
621
+ >
622
+ {bid.price} $
623
+ </Text>
624
+ <Text
625
+ size="1"
626
+ color="tertiary"
627
+ className="min-w-9 text-right"
628
+ >
629
+ {bid.time}
630
+ </Text>
631
+ </div>
632
+ </div>
633
+ {!ctx.isLastInGroup && <Separator />}
634
+ </>
635
+ );
636
+ }}
637
+ </Ranking.GroupItemValue>
638
+ </Ranking.GroupItem>
639
+ </Ranking.Group>
640
+ </Ranking.Root>
641
+ </div>
642
+ <AuctionBiddingPanel>
643
+ <AuctionBidForm.Root />
644
+ </AuctionBiddingPanel>
645
+ </div>
646
+ </Auction>
647
+ </div>
648
+ </div>
649
+ );
650
+ }
651
+
652
+ export const SingleItem: StoryObj<typeof Auction> = {
653
+ render: () => <SingleItemContent userBids={[]} />,
654
+ };
655
+
656
+ export const SingleItemWithBid: StoryObj<typeof Auction> = {
657
+ render: () => <SingleItemContent userBids={mockSingleUserBids} />,
658
+ };