@loafmarkets/ui 0.1.2 → 0.1.4

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
@@ -3278,7 +3278,7 @@ function DesktopOrderbookLayout({
3278
3278
  )
3279
3279
  ] }) : /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3", children: rightHeader })
3280
3280
  ] }),
3281
- /* @__PURE__ */ jsxs("div", { className: "px-4 pb-3 pt-2", children: [
3281
+ /* @__PURE__ */ jsxs("div", { className: "px-4 pt-2", children: [
3282
3282
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3 px-3 py-2 text-xs text-white/60", children: [
3283
3283
  /* @__PURE__ */ jsx("div", { children: priceLabel }),
3284
3284
  /* @__PURE__ */ jsx("div", { className: "text-right", children: amountLabel })
@@ -3332,7 +3332,7 @@ function DesktopOrderbookLayout({
3332
3332
  ))
3333
3333
  }
3334
3334
  ),
3335
- /* @__PURE__ */ jsxs("div", { className: "mt-2 grid grid-cols-2 items-center gap-3 bg-[#0b1a24] px-3 py-2", children: [
3335
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 items-center gap-3 bg-[#0b1a24] px-3 py-2", children: [
3336
3336
  /* @__PURE__ */ jsxs("div", { className: cn("text-lg font-semibold tabular-nums", midClass), children: [
3337
3337
  "$",
3338
3338
  formatNumber(midPrice, precision),
@@ -4545,7 +4545,119 @@ var YourOrders = o.forwardRef(
4545
4545
  }
4546
4546
  );
4547
4547
  YourOrders.displayName = "YourOrders";
4548
- var defaultRanges = ["1D", "1W", "1M", "3M", "1Y"];
4548
+ var timeToDate = (time) => {
4549
+ if (typeof time === "number") {
4550
+ return new Date(time * 1e3);
4551
+ }
4552
+ if (typeof time === "object" && time !== null && "year" in time && "month" in time && "day" in time) {
4553
+ const { year, month, day } = time;
4554
+ return new Date(Date.UTC(year, month - 1, day));
4555
+ }
4556
+ return null;
4557
+ };
4558
+ var createTickFormatter = (formatOptions) => {
4559
+ return (time, _tickMarkType, locale) => {
4560
+ const date = timeToDate(time);
4561
+ if (!date) return "";
4562
+ return date.toLocaleString(locale || void 0, formatOptions);
4563
+ };
4564
+ };
4565
+ var getTimeScaleOptions = (range) => {
4566
+ switch (range) {
4567
+ case "30s":
4568
+ return {
4569
+ timeVisible: true,
4570
+ secondsVisible: true,
4571
+ borderColor: "rgba(255,255,255,0.06)",
4572
+ tickMarkFormatter: createTickFormatter({ hour: "2-digit", minute: "2-digit", second: "2-digit" })
4573
+ };
4574
+ case "1m":
4575
+ return {
4576
+ timeVisible: true,
4577
+ secondsVisible: true,
4578
+ borderColor: "rgba(255,255,255,0.06)",
4579
+ tickMarkFormatter: createTickFormatter({ hour: "2-digit", minute: "2-digit", second: "2-digit" })
4580
+ };
4581
+ case "5m":
4582
+ return {
4583
+ timeVisible: true,
4584
+ secondsVisible: false,
4585
+ borderColor: "rgba(255,255,255,0.06)",
4586
+ tickMarkFormatter: createTickFormatter({ hour: "2-digit", minute: "2-digit" })
4587
+ };
4588
+ case "15m":
4589
+ return {
4590
+ timeVisible: true,
4591
+ secondsVisible: false,
4592
+ borderColor: "rgba(255,255,255,0.06)",
4593
+ tickMarkFormatter: createTickFormatter({ hour: "2-digit", minute: "2-digit" })
4594
+ };
4595
+ case "1h":
4596
+ return {
4597
+ timeVisible: true,
4598
+ secondsVisible: false,
4599
+ borderColor: "rgba(255,255,255,0.06)",
4600
+ tickMarkFormatter: createTickFormatter({ hour: "2-digit", minute: "2-digit" })
4601
+ };
4602
+ case "4h":
4603
+ return {
4604
+ timeVisible: true,
4605
+ secondsVisible: false,
4606
+ borderColor: "rgba(255,255,255,0.06)",
4607
+ tickMarkFormatter: createTickFormatter({ weekday: "short", hour: "2-digit" })
4608
+ };
4609
+ case "24h":
4610
+ return {
4611
+ timeVisible: true,
4612
+ secondsVisible: false,
4613
+ borderColor: "rgba(255,255,255,0.06)",
4614
+ tickMarkFormatter: createTickFormatter({ month: "short", day: "numeric" })
4615
+ };
4616
+ case "1W":
4617
+ return {
4618
+ timeVisible: true,
4619
+ secondsVisible: false,
4620
+ borderColor: "rgba(255,255,255,0.06)",
4621
+ tickMarkFormatter: createTickFormatter({ month: "short", day: "numeric" })
4622
+ };
4623
+ case "1M":
4624
+ return {
4625
+ timeVisible: true,
4626
+ secondsVisible: false,
4627
+ borderColor: "rgba(255,255,255,0.06)",
4628
+ tickMarkFormatter: createTickFormatter({ month: "short", year: "2-digit" })
4629
+ };
4630
+ default:
4631
+ return {
4632
+ timeVisible: true,
4633
+ secondsVisible: false,
4634
+ borderColor: "rgba(255,255,255,0.06)"
4635
+ };
4636
+ }
4637
+ };
4638
+ var getPriceScaleOptions = (data) => {
4639
+ if (!data.length) return {
4640
+ borderColor: "rgba(230, 200, 126, 0.25)",
4641
+ textColor: "rgba(230, 200, 126, 0.7)"
4642
+ };
4643
+ const prices = data.flatMap((d2) => [d2.open, d2.high, d2.low, d2.close]);
4644
+ const minPrice = Math.min(...prices);
4645
+ const maxPrice = Math.max(...prices);
4646
+ const priceRange = maxPrice - minPrice;
4647
+ let scaleMargins = { top: 0.1, bottom: 0.1 };
4648
+ if (priceRange < 1) {
4649
+ scaleMargins = { top: 0.2, bottom: 0.2 };
4650
+ } else if (priceRange > 1e3) {
4651
+ scaleMargins = { top: 0.05, bottom: 0.05 };
4652
+ }
4653
+ return {
4654
+ borderColor: "rgba(230, 200, 126, 0.25)",
4655
+ textColor: "rgba(230, 200, 126, 0.7)",
4656
+ scaleMargins
4657
+ };
4658
+ };
4659
+ var defaultRanges = ["30s", "1m", "5m", "15m", "1h", "4h", "24h", "1W", "1M"];
4660
+ var VISIBLE_RANGE_COUNT = 4;
4549
4661
  var formatPrice = (value, currencySymbol) => {
4550
4662
  return `${currencySymbol}${value.toLocaleString(void 0, {
4551
4663
  minimumFractionDigits: 2,
@@ -4582,6 +4694,21 @@ var PriceChart = o.forwardRef(
4582
4694
  const seriesRef = o.useRef(null);
4583
4695
  const priceLineRef = o.useRef(null);
4584
4696
  const [hoveredRange, setHoveredRange] = o.useState(null);
4697
+ const [dropdownOpen, setDropdownOpen] = o.useState(false);
4698
+ const dropdownRef = o.useRef(null);
4699
+ const isAutoScrollRef = o.useRef(true);
4700
+ const visibleRanges = ranges.slice(0, VISIBLE_RANGE_COUNT);
4701
+ const dropdownRanges = ranges.slice(VISIBLE_RANGE_COUNT);
4702
+ const selectedInDropdown = dropdownRanges.includes(selectedRange);
4703
+ o.useEffect(() => {
4704
+ const handleClickOutside = (e) => {
4705
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
4706
+ setDropdownOpen(false);
4707
+ }
4708
+ };
4709
+ document.addEventListener("mousedown", handleClickOutside);
4710
+ return () => document.removeEventListener("mousedown", handleClickOutside);
4711
+ }, []);
4585
4712
  const resolvedPrice = o.useMemo(() => {
4586
4713
  if (price != null) return price;
4587
4714
  const last = data.at(-1);
@@ -4614,15 +4741,8 @@ var PriceChart = o.forwardRef(
4614
4741
  vertLines: { color: "rgba(255,255,255,0.06)" },
4615
4742
  horzLines: { color: "rgba(255,255,255,0.06)" }
4616
4743
  },
4617
- rightPriceScale: {
4618
- borderColor: "rgba(230, 200, 126, 0.25)",
4619
- textColor: "rgba(230, 200, 126, 0.7)"
4620
- },
4621
- timeScale: {
4622
- borderColor: "rgba(255,255,255,0.06)",
4623
- timeVisible: true,
4624
- secondsVisible: false
4625
- },
4744
+ rightPriceScale: getPriceScaleOptions(data),
4745
+ timeScale: getTimeScaleOptions(selectedRange || defaultRanges[0]),
4626
4746
  crosshair: {
4627
4747
  vertLine: { color: "rgba(255,255,255,0.12)" },
4628
4748
  horzLine: { color: "rgba(255,255,255,0.12)" }
@@ -4637,17 +4757,37 @@ var PriceChart = o.forwardRef(
4637
4757
  });
4638
4758
  chartRef.current = chart;
4639
4759
  seriesRef.current = series;
4760
+ const handleVisibleRangeChange = () => {
4761
+ const timeScale2 = chart.timeScale();
4762
+ const position2 = timeScale2.scrollPosition();
4763
+ const atRightEdge = position2 <= 0.01;
4764
+ isAutoScrollRef.current = atRightEdge;
4765
+ };
4766
+ const timeScale = chart.timeScale();
4767
+ timeScale.subscribeVisibleLogicalRangeChange(handleVisibleRangeChange);
4640
4768
  return () => {
4641
4769
  chartRef.current = null;
4642
4770
  seriesRef.current = null;
4771
+ timeScale.unsubscribeVisibleLogicalRangeChange(handleVisibleRangeChange);
4643
4772
  chart.remove();
4644
4773
  };
4645
4774
  }, []);
4775
+ o.useEffect(() => {
4776
+ const chart = chartRef.current;
4777
+ if (!chart) return;
4778
+ const effectiveRange = selectedRange ?? ranges?.[0] ?? "1D";
4779
+ chart.applyOptions({
4780
+ timeScale: getTimeScaleOptions(effectiveRange)
4781
+ });
4782
+ }, [selectedRange, ranges]);
4646
4783
  o.useEffect(() => {
4647
4784
  const chart = chartRef.current;
4648
4785
  const series = seriesRef.current;
4649
4786
  if (!chart || !series) return;
4650
4787
  series.setData(data);
4788
+ chart.applyOptions({
4789
+ rightPriceScale: getPriceScaleOptions(data)
4790
+ });
4651
4791
  if (priceLineRef.current) {
4652
4792
  series.removePriceLine(priceLineRef.current);
4653
4793
  priceLineRef.current = null;
@@ -4663,7 +4803,9 @@ var PriceChart = o.forwardRef(
4663
4803
  title: resolvedPrice.toFixed(2)
4664
4804
  });
4665
4805
  }
4666
- chart.timeScale().fitContent();
4806
+ if (isAutoScrollRef.current) {
4807
+ chart.timeScale().scrollToPosition(0, true);
4808
+ }
4667
4809
  }, [data, resolvedPrice]);
4668
4810
  const sign = inferredChangePercent == null ? null : inferredChangePercent >= 0 ? "+" : "";
4669
4811
  const changeClass = inferredChangePercent == null ? "" : inferredChangePercent >= 0 ? "text-[#0ecb81]" : "text-[#f6465d]";
@@ -4695,30 +4837,97 @@ var PriceChart = o.forwardRef(
4695
4837
  /* @__PURE__ */ jsxs(CardHeader, { className: "flex flex-col gap-4 px-4 pb-2 pt-5 md:flex-row md:items-start md:justify-between md:px-6 md:pt-6", children: [
4696
4838
  /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-3 md:w-auto", children: [
4697
4839
  /* @__PURE__ */ jsx(CardTitle, { className: "m-0 text-[1.1rem] font-semibold text-white", children: title }),
4698
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: ranges.map((r2) => {
4699
- const active = r2 === selectedRange;
4700
- const hovered = hoveredRange === r2;
4701
- const style = {
4702
- ...btnBaseStyle,
4703
- ...hovered ? btnHoverStyle : null,
4704
- ...active ? btnActiveStyle : null
4705
- };
4706
- return /* @__PURE__ */ jsx(
4840
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
4841
+ visibleRanges.map((r2) => {
4842
+ const active = r2 === selectedRange;
4843
+ const hovered = hoveredRange === r2;
4844
+ const style = {
4845
+ ...btnBaseStyle,
4846
+ ...hovered ? btnHoverStyle : null,
4847
+ ...active ? btnActiveStyle : null
4848
+ };
4849
+ return /* @__PURE__ */ jsx(
4850
+ "button",
4851
+ {
4852
+ type: "button",
4853
+ onClick: () => onRangeChange?.(r2),
4854
+ onMouseEnter: () => setHoveredRange(r2),
4855
+ onMouseLeave: () => setHoveredRange((prev2) => prev2 === r2 ? null : prev2),
4856
+ style,
4857
+ className: cn(
4858
+ "rounded border px-3 py-1 text-[0.85rem] font-medium transition-all duration-200"
4859
+ ),
4860
+ children: r2
4861
+ },
4862
+ r2
4863
+ );
4864
+ }),
4865
+ selectedInDropdown && selectedRange ? /* @__PURE__ */ jsx(
4707
4866
  "button",
4708
4867
  {
4709
4868
  type: "button",
4710
- onClick: () => onRangeChange?.(r2),
4711
- onMouseEnter: () => setHoveredRange(r2),
4712
- onMouseLeave: () => setHoveredRange((prev2) => prev2 === r2 ? null : prev2),
4713
- style,
4869
+ style: {
4870
+ ...btnBaseStyle,
4871
+ ...btnActiveStyle
4872
+ },
4714
4873
  className: cn(
4715
4874
  "rounded border px-3 py-1 text-[0.85rem] font-medium transition-all duration-200"
4716
4875
  ),
4717
- children: r2
4718
- },
4719
- r2
4720
- );
4721
- }) })
4876
+ disabled: true,
4877
+ children: selectedRange
4878
+ }
4879
+ ) : null,
4880
+ dropdownRanges.length > 0 && /* @__PURE__ */ jsxs("div", { className: "relative", ref: dropdownRef, children: [
4881
+ /* @__PURE__ */ jsxs(
4882
+ "button",
4883
+ {
4884
+ type: "button",
4885
+ onClick: () => setDropdownOpen((prev2) => !prev2),
4886
+ onMouseEnter: () => setHoveredRange("__dropdown__"),
4887
+ onMouseLeave: () => setHoveredRange((prev2) => prev2 === "__dropdown__" ? null : prev2),
4888
+ style: {
4889
+ ...btnBaseStyle,
4890
+ ...hoveredRange === "__dropdown__" ? btnHoverStyle : null
4891
+ },
4892
+ className: cn(
4893
+ "flex items-center gap-1 rounded border px-3 py-1 text-[0.85rem] font-medium transition-all duration-200"
4894
+ ),
4895
+ children: [
4896
+ "More",
4897
+ /* @__PURE__ */ jsx(
4898
+ "svg",
4899
+ {
4900
+ className: cn("h-3 w-3 transition-transform", dropdownOpen && "rotate-180"),
4901
+ fill: "none",
4902
+ stroke: "currentColor",
4903
+ viewBox: "0 0 24 24",
4904
+ children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" })
4905
+ }
4906
+ )
4907
+ ]
4908
+ }
4909
+ ),
4910
+ dropdownOpen && /* @__PURE__ */ jsx("div", { className: "absolute left-0 top-full z-50 mt-1 min-w-[80px] rounded border border-white/10 bg-black/90 py-1 backdrop-blur-md", children: dropdownRanges.map((r2) => {
4911
+ const active = r2 === selectedRange;
4912
+ return /* @__PURE__ */ jsx(
4913
+ "button",
4914
+ {
4915
+ type: "button",
4916
+ onClick: () => {
4917
+ onRangeChange?.(r2);
4918
+ setDropdownOpen(false);
4919
+ },
4920
+ className: cn(
4921
+ "block w-full px-3 py-1.5 text-left text-[0.85rem] font-medium transition-colors hover:bg-white/10",
4922
+ active ? "bg-[#e6c87e]/20 text-[#e6c87e]" : "text-white/70"
4923
+ ),
4924
+ children: r2
4925
+ },
4926
+ r2
4927
+ );
4928
+ }) })
4929
+ ] })
4930
+ ] })
4722
4931
  ] }),
4723
4932
  resolvedPrice == null && inferredChangePercent == null ? null : /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center" }, children: [
4724
4933
  resolvedPrice == null ? null : /* @__PURE__ */ jsx(