@loafmarkets/ui 0.0.4 → 0.0.6

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/dist/index.mjs CHANGED
@@ -282,6 +282,7 @@ function HousePositionSlider({
282
282
  pendingOrders = [],
283
283
  defaultOrderType = "market",
284
284
  orderbook,
285
+ ownershipPercentOverride,
285
286
  onConfirmOrder,
286
287
  className,
287
288
  ...props
@@ -292,6 +293,7 @@ function HousePositionSlider({
292
293
  const [deltaTokensBuy, setDeltaTokensBuy] = React5.useState(0);
293
294
  const [deltaTokensSell, setDeltaTokensSell] = React5.useState(0);
294
295
  const [isDragging, setIsDragging] = React5.useState(false);
296
+ const [visualTargetPct, setVisualTargetPct] = React5.useState(null);
295
297
  const [orderType, setOrderType] = React5.useState(defaultOrderType);
296
298
  const [limitPrice, setLimitPrice] = React5.useState(currentPrice);
297
299
  const [limitPriceInput, setLimitPriceInput] = React5.useState(currentPrice.toFixed(2));
@@ -313,9 +315,8 @@ function HousePositionSlider({
313
315
  const effectiveAvailableCash = Math.max(0, availableCash - pendingBuyValue);
314
316
  const effectiveTokensHeld = Math.max(0, tokensHeld - pendingSellTokens);
315
317
  const holdingsValue = tokensHeld * effectivePrice;
316
- const sliderHoldingsValue = effectiveTokensHeld * effectivePrice;
317
- const sliderTotalCapacity = sliderHoldingsValue + effectiveAvailableCash;
318
- const baselinePct = sliderTotalCapacity <= 0 ? 0 : sliderHoldingsValue / sliderTotalCapacity * 100;
318
+ const safeTotalTokens = totalTokens > 0 ? totalTokens : 1;
319
+ const baselineOwnershipActual = clamp(effectiveTokensHeld / safeTotalTokens * 100, 0, 100);
319
320
  let deltaTokens = 0;
320
321
  let deltaValue = 0;
321
322
  let marketAvgPrice = null;
@@ -361,29 +362,21 @@ function HousePositionSlider({
361
362
  }
362
363
  targetTokens = tokensHeld + deltaTokens;
363
364
  targetValue = targetTokens * effectivePrice;
364
- const plannedDeltaValue = (() => {
365
- if (orderMode === "buy") {
366
- if (buyTrackingMode === "dollars") {
367
- const notional = Math.min(Math.max(0, deltaDollars), effectiveAvailableCash);
368
- return notional;
369
- }
370
- const tokensPlanned = Math.max(0, deltaTokensBuy);
371
- const referencePrice = orderType === "market" ? currentPrice || limitPriceSafe : limitPriceSafe;
372
- return Math.min(tokensPlanned * referencePrice, effectiveAvailableCash);
373
- }
374
- if (orderMode === "sell") {
375
- const tokensToSell = Math.abs(Math.min(0, deltaTokensSell));
376
- const sellValue = tokensToSell * effectivePrice;
377
- return -Math.min(sellValue, sliderHoldingsValue);
378
- }
379
- return 0;
380
- })();
381
- const sliderTargetValue = clamp(sliderHoldingsValue + plannedDeltaValue, 0, sliderTotalCapacity);
382
- const targetPct = sliderTotalCapacity <= 0 ? 0 : sliderTargetValue / sliderTotalCapacity * 100;
365
+ const sliderTargetTokens = clamp(effectiveTokensHeld + deltaTokens, 0, safeTotalTokens);
366
+ const normalizedTargetPct = sliderTargetTokens / safeTotalTokens * 100;
383
367
  const isIncrease = orderMode === "buy";
384
368
  const hasChange = orderMode !== "none" && (Math.abs(deltaTokens) > 1e-3 || Math.abs(deltaValue) > 0.01);
385
- const currentOwnership = totalTokens <= 0 ? 0 : tokensHeld / totalTokens * 100;
386
- const targetOwnership = totalTokens <= 0 ? 0 : targetTokens / totalTokens * 100;
369
+ const currentOwnership = totalTokens <= 0 ? 0 : clamp(tokensHeld / totalTokens * 100, 0, 100);
370
+ const targetOwnership = totalTokens <= 0 ? 0 : clamp(targetTokens / totalTokens * 100, 0, 100);
371
+ const ownershipOverrideValue = typeof ownershipPercentOverride === "number" && Number.isFinite(ownershipPercentOverride) ? clamp(ownershipPercentOverride, 0, 100) : null;
372
+ const ownershipShift = ownershipOverrideValue != null ? ownershipOverrideValue - baselineOwnershipActual : 0;
373
+ const baselinePct = clamp(ownershipOverrideValue ?? baselineOwnershipActual, 0, 100);
374
+ const impliedTargetPct = clamp(normalizedTargetPct + ownershipShift, 0, 100);
375
+ const displayTargetPct = visualTargetPct ?? impliedTargetPct;
376
+ const targetPct = displayTargetPct;
377
+ const displayCurrentOwnership = clamp(ownershipOverrideValue ?? currentOwnership, 0, 100);
378
+ const impliedDisplayTargetOwnership = clamp(targetOwnership + ownershipShift, 0, 100);
379
+ const displayTargetOwnership = visualTargetPct ?? impliedDisplayTargetOwnership;
387
380
  const estFeeTokens = Math.abs(deltaValue) * 5e-3 / (effectivePrice || 1);
388
381
  const resetOrder = React5.useCallback(() => {
389
382
  setOrderMode("none");
@@ -391,6 +384,7 @@ function HousePositionSlider({
391
384
  setDeltaDollars(0);
392
385
  setDeltaTokensBuy(0);
393
386
  setDeltaTokensSell(0);
387
+ setVisualTargetPct(null);
394
388
  }, []);
395
389
  const updateOrderFromTargetValue = React5.useCallback(
396
390
  (newTargetValue) => {
@@ -421,6 +415,7 @@ function HousePositionSlider({
421
415
  const nextOwnership = clamp(newOwnershipPercent, 0, 100);
422
416
  const newTargetTokens = nextOwnership / 100 * totalTokens;
423
417
  updateOrderFromTargetValue(newTargetTokens * effectivePrice);
418
+ setVisualTargetPct(nextOwnership);
424
419
  },
425
420
  [effectivePrice, totalTokens, updateOrderFromTargetValue]
426
421
  );
@@ -453,12 +448,14 @@ function HousePositionSlider({
453
448
  const magnitude = Math.min(Math.abs(normalized), 1);
454
449
  if (magnitude < 0.02) {
455
450
  resetOrder();
451
+ setVisualTargetPct(null);
456
452
  return;
457
453
  }
458
454
  if (normalized > 0) {
459
455
  const notional = clamp(magnitude * effectiveAvailableCash, 0, effectiveAvailableCash);
460
456
  if (notional <= 0) {
461
457
  resetOrder();
458
+ setVisualTargetPct(null);
462
459
  return;
463
460
  }
464
461
  setOrderMode("buy");
@@ -466,12 +463,14 @@ function HousePositionSlider({
466
463
  setDeltaDollars(notional);
467
464
  setDeltaTokensBuy(0);
468
465
  setDeltaTokensSell(0);
466
+ setVisualTargetPct(clamp(pct, 0, 100));
469
467
  return;
470
468
  }
471
469
  if (normalized < 0) {
472
470
  const tokensToSell = clamp(magnitude * effectiveTokensHeld, 0, effectiveTokensHeld);
473
471
  if (tokensToSell <= 0) {
474
472
  resetOrder();
473
+ setVisualTargetPct(null);
475
474
  return;
476
475
  }
477
476
  setOrderMode("sell");
@@ -479,9 +478,11 @@ function HousePositionSlider({
479
478
  setDeltaTokensSell(-tokensToSell);
480
479
  setDeltaDollars(0);
481
480
  setDeltaTokensBuy(0);
481
+ setVisualTargetPct(clamp(pct, 0, 100));
482
482
  return;
483
483
  }
484
484
  resetOrder();
485
+ setVisualTargetPct(null);
485
486
  },
486
487
  [effectiveAvailableCash, effectiveTokensHeld, resetOrder]
487
488
  );
@@ -644,19 +645,19 @@ function HousePositionSlider({
644
645
  " Ownership"
645
646
  ] }),
646
647
  /* @__PURE__ */ jsxs("span", { className: "text-white", children: [
647
- currentOwnership.toFixed(2),
648
+ displayCurrentOwnership.toFixed(2),
648
649
  "%",
649
650
  /* @__PURE__ */ jsx("span", { className: "mx-1.5 text-white/50", children: "\u2192" }),
650
651
  /* @__PURE__ */ jsx(
651
652
  "input",
652
653
  {
653
654
  type: "text",
654
- value: ownershipInput || targetOwnership.toFixed(2),
655
+ value: ownershipInput || displayTargetOwnership.toFixed(2),
655
656
  onChange: (e) => {
656
657
  const val = e.target.value;
657
658
  if (val === "" || /^[0-9]*\.?[0-9]*$/.test(val)) setOwnershipInput(val);
658
659
  },
659
- onFocus: () => setOwnershipInput(targetOwnership.toFixed(2)),
660
+ onFocus: () => setOwnershipInput(displayTargetOwnership.toFixed(2)),
660
661
  onBlur: () => {
661
662
  const num = Number.parseFloat(ownershipInput);
662
663
  if (Number.isFinite(num)) updateOrderFromOwnership(num);
@@ -667,7 +668,7 @@ function HousePositionSlider({
667
668
  },
668
669
  className: cn(
669
670
  "w-[70px] rounded-[4px] border bg-white/10 px-2 py-1 text-right font-semibold outline-none",
670
- targetOwnership >= currentOwnership ? "border-[rgba(14,203,129,0.3)] text-[#0ecb81] focus:border-[#0ecb81]" : "border-[rgba(246,70,93,0.3)] text-[#f6465d] focus:border-[#f6465d]"
671
+ displayTargetOwnership >= displayCurrentOwnership ? "border-[rgba(14,203,129,0.3)] text-[#0ecb81] focus:border-[#0ecb81]" : "border-[rgba(246,70,93,0.3)] text-[#f6465d] focus:border-[#f6465d]"
671
672
  )
672
673
  }
673
674
  )
@@ -1227,6 +1228,144 @@ var PropertyNewsUpdates = React5.forwardRef(
1227
1228
  }
1228
1229
  );
1229
1230
  PropertyNewsUpdates.displayName = "PropertyNewsUpdates";
1231
+ var PropertyCompareBar = React5.forwardRef(
1232
+ ({
1233
+ className,
1234
+ addresses,
1235
+ selectedAddressId,
1236
+ onSelectAddress,
1237
+ compareLabel = "Compare",
1238
+ onCompareClick,
1239
+ compareIcon,
1240
+ ...props
1241
+ }, ref) => {
1242
+ const normalizedAddresses = React5.useMemo(() => {
1243
+ return addresses.map(
1244
+ (option) => typeof option === "string" ? { id: option, label: option } : option
1245
+ );
1246
+ }, [addresses]);
1247
+ const hasAddresses = normalizedAddresses.length > 0;
1248
+ const firstAddressId = normalizedAddresses[0]?.id;
1249
+ const isControlled = selectedAddressId !== void 0;
1250
+ const [internalSelectedId, setInternalSelectedId] = React5.useState(
1251
+ () => isControlled ? void 0 : firstAddressId
1252
+ );
1253
+ const resolvedSelectedId = isControlled ? selectedAddressId : internalSelectedId;
1254
+ React5.useEffect(() => {
1255
+ if (!isControlled) {
1256
+ setInternalSelectedId((current) => {
1257
+ if (current != null && normalizedAddresses.some((option) => option.id === current)) {
1258
+ return current;
1259
+ }
1260
+ return firstAddressId;
1261
+ });
1262
+ }
1263
+ }, [firstAddressId, isControlled, normalizedAddresses]);
1264
+ const selectedOption = normalizedAddresses.find((option) => option.id === resolvedSelectedId) ?? normalizedAddresses[0];
1265
+ const [isDropdownOpen, setIsDropdownOpen] = React5.useState(false);
1266
+ const dropdownRef = React5.useRef(null);
1267
+ React5.useEffect(() => {
1268
+ if (!isDropdownOpen) return;
1269
+ const handleClick = (event) => {
1270
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
1271
+ setIsDropdownOpen(false);
1272
+ }
1273
+ };
1274
+ const handleKey = (event) => {
1275
+ if (event.key === "Escape") {
1276
+ setIsDropdownOpen(false);
1277
+ }
1278
+ };
1279
+ document.addEventListener("mousedown", handleClick);
1280
+ document.addEventListener("keydown", handleKey);
1281
+ return () => {
1282
+ document.removeEventListener("mousedown", handleClick);
1283
+ document.removeEventListener("keydown", handleKey);
1284
+ };
1285
+ }, [isDropdownOpen]);
1286
+ const defaultCompareIcon = /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4h7v7H4V4zm0 9h7v7H4v-7zm9-9h7v7h-7V4zm0 9h7v7h-7v-7z" }) });
1287
+ const handleAddressSelect = (addressId) => {
1288
+ if (!isControlled) {
1289
+ setInternalSelectedId(addressId);
1290
+ }
1291
+ onSelectAddress?.(addressId);
1292
+ setIsDropdownOpen(false);
1293
+ };
1294
+ return /* @__PURE__ */ jsxs(
1295
+ "div",
1296
+ {
1297
+ ref,
1298
+ className: cn(
1299
+ "flex w-full flex-col gap-3 border border-white/10 px-4 py-3 text-white shadow-[0_18px_40px_rgba(0,0,0,0.55)] md:flex-row md:items-center md:justify-between md:gap-4",
1300
+ className
1301
+ ),
1302
+ style: { borderRadius: "16px" },
1303
+ ...props,
1304
+ children: [
1305
+ /* @__PURE__ */ jsxs("div", { className: "relative w-auto", ref: dropdownRef, children: [
1306
+ /* @__PURE__ */ jsxs(
1307
+ "button",
1308
+ {
1309
+ type: "button",
1310
+ disabled: !hasAddresses,
1311
+ onClick: () => setIsDropdownOpen((prev) => !prev),
1312
+ className: cn(
1313
+ "flex h-[42px] w-auto items-center gap-2 rounded-[12px] border border-transparent bg-transparent px-0 text-left text-[15px] font-semibold text-white transition hover:text-white/80 focus-visible:outline-none",
1314
+ !hasAddresses && "text-white/40"
1315
+ ),
1316
+ children: [
1317
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: selectedOption ? selectedOption.label : hasAddresses ? "Select address" : "No addresses available" }),
1318
+ /* @__PURE__ */ jsx("span", { className: "ml-3 flex items-center text-white/60 transition-transform", "aria-hidden": true, children: /* @__PURE__ */ jsx(
1319
+ "svg",
1320
+ {
1321
+ width: "16",
1322
+ height: "16",
1323
+ viewBox: "0 0 24 24",
1324
+ fill: "currentColor",
1325
+ className: cn("transition-transform", isDropdownOpen && "rotate-180"),
1326
+ children: /* @__PURE__ */ jsx("path", { d: "M7 10l5 5 5-5H7z" })
1327
+ }
1328
+ ) })
1329
+ ]
1330
+ }
1331
+ ),
1332
+ isDropdownOpen && hasAddresses ? /* @__PURE__ */ jsx("div", { className: "absolute left-0 top-[calc(100%+8px)] z-20 w-full rounded-[12px] border border-white/10 py-1 shadow-[0_25px_55px_rgba(0,0,0,0.6)] bg-black", children: normalizedAddresses.map((option) => {
1333
+ const active = option.id === resolvedSelectedId;
1334
+ return /* @__PURE__ */ jsx(
1335
+ "button",
1336
+ {
1337
+ type: "button",
1338
+ className: cn(
1339
+ "flex w-full items-center px-4 py-2 text-left text-[14px] text-white/80 transition hover:bg-white/5 hover:text-white",
1340
+ active && "text-white"
1341
+ ),
1342
+ onClick: () => handleAddressSelect(option.id),
1343
+ children: option.label
1344
+ },
1345
+ option.id
1346
+ );
1347
+ }) }) : null
1348
+ ] }),
1349
+ /* @__PURE__ */ jsxs(
1350
+ Button,
1351
+ {
1352
+ variant: "accentOutline",
1353
+ size: "sm",
1354
+ className: "flex items-center justify-center gap-2 rounded-[10px] border-[var(--color-accent,#e6c87e)] bg-transparent px-4 py-2 text-[14px] font-semibold text-[var(--color-accent,#e6c87e)] transition hover:bg-[rgba(230,200,126,0.08)] md:ml-auto",
1355
+ onClick: onCompareClick,
1356
+ disabled: !hasAddresses,
1357
+ children: [
1358
+ /* @__PURE__ */ jsx("span", { className: "text-base", children: compareIcon ?? defaultCompareIcon }),
1359
+ compareLabel
1360
+ ]
1361
+ }
1362
+ )
1363
+ ]
1364
+ }
1365
+ );
1366
+ }
1367
+ );
1368
+ PropertyCompareBar.displayName = "PropertyCompareBar";
1230
1369
  var clampPct = (pct) => Math.min(100, Math.max(0, pct));
1231
1370
  var EditIcon = ({ className }) => /* @__PURE__ */ jsxs(
1232
1371
  "svg",
@@ -1821,6 +1960,6 @@ var PropertySubheader = React5.forwardRef(
1821
1960
  );
1822
1961
  PropertySubheader.displayName = "PropertySubheader";
1823
1962
 
1824
- export { Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, HousePositionSlider, Orderbook, PortfolioSummary, PriceChart, PropertyHeroHeader, PropertyNewsUpdates, PropertySubheader, PropertyTour, YourOrders, badgeVariants, buttonVariants };
1963
+ export { Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, HousePositionSlider, Orderbook, PortfolioSummary, PriceChart, PropertyCompareBar, PropertyHeroHeader, PropertyNewsUpdates, PropertySubheader, PropertyTour, YourOrders, badgeVariants, buttonVariants };
1825
1964
  //# sourceMappingURL=index.mjs.map
1826
1965
  //# sourceMappingURL=index.mjs.map