@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.d.mts CHANGED
@@ -280,7 +280,7 @@ declare const YourOrders: React.ForwardRefExoticComponent<React.HTMLAttributes<H
280
280
  renderOrderActions?: (order: YourOrder) => React.ReactNode;
281
281
  } & React.RefAttributes<HTMLDivElement>>;
282
282
 
283
- type PriceChartRange = "1D" | "1W" | "1M" | "3M" | "1Y";
283
+ type PriceChartRange = "30s" | "1m" | "5m" | "15m" | "1h" | "4h" | "24h" | "1W" | "1M";
284
284
  type PriceChartCandle = {
285
285
  time: LightweightCharts.Time;
286
286
  open: number;
package/dist/index.d.ts CHANGED
@@ -280,7 +280,7 @@ declare const YourOrders: React.ForwardRefExoticComponent<React.HTMLAttributes<H
280
280
  renderOrderActions?: (order: YourOrder) => React.ReactNode;
281
281
  } & React.RefAttributes<HTMLDivElement>>;
282
282
 
283
- type PriceChartRange = "1D" | "1W" | "1M" | "3M" | "1Y";
283
+ type PriceChartRange = "30s" | "1m" | "5m" | "15m" | "1h" | "4h" | "24h" | "1W" | "1M";
284
284
  type PriceChartCandle = {
285
285
  time: LightweightCharts.Time;
286
286
  open: number;
package/dist/index.js CHANGED
@@ -3300,7 +3300,7 @@ function DesktopOrderbookLayout({
3300
3300
  )
3301
3301
  ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-3", children: rightHeader })
3302
3302
  ] }),
3303
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 pb-3 pt-2", children: [
3303
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 pt-2", children: [
3304
3304
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-3 px-3 py-2 text-xs text-white/60", children: [
3305
3305
  /* @__PURE__ */ jsxRuntime.jsx("div", { children: priceLabel }),
3306
3306
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-right", children: amountLabel })
@@ -3354,7 +3354,7 @@ function DesktopOrderbookLayout({
3354
3354
  ))
3355
3355
  }
3356
3356
  ),
3357
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 grid grid-cols-2 items-center gap-3 bg-[#0b1a24] px-3 py-2", children: [
3357
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 items-center gap-3 bg-[#0b1a24] px-3 py-2", children: [
3358
3358
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("text-lg font-semibold tabular-nums", midClass), children: [
3359
3359
  "$",
3360
3360
  formatNumber(midPrice, precision),
@@ -4567,7 +4567,119 @@ var YourOrders = o__namespace.forwardRef(
4567
4567
  }
4568
4568
  );
4569
4569
  YourOrders.displayName = "YourOrders";
4570
- var defaultRanges = ["1D", "1W", "1M", "3M", "1Y"];
4570
+ var timeToDate = (time) => {
4571
+ if (typeof time === "number") {
4572
+ return new Date(time * 1e3);
4573
+ }
4574
+ if (typeof time === "object" && time !== null && "year" in time && "month" in time && "day" in time) {
4575
+ const { year, month, day } = time;
4576
+ return new Date(Date.UTC(year, month - 1, day));
4577
+ }
4578
+ return null;
4579
+ };
4580
+ var createTickFormatter = (formatOptions) => {
4581
+ return (time, _tickMarkType, locale) => {
4582
+ const date = timeToDate(time);
4583
+ if (!date) return "";
4584
+ return date.toLocaleString(locale || void 0, formatOptions);
4585
+ };
4586
+ };
4587
+ var getTimeScaleOptions = (range) => {
4588
+ switch (range) {
4589
+ case "30s":
4590
+ return {
4591
+ timeVisible: true,
4592
+ secondsVisible: true,
4593
+ borderColor: "rgba(255,255,255,0.06)",
4594
+ tickMarkFormatter: createTickFormatter({ hour: "2-digit", minute: "2-digit", second: "2-digit" })
4595
+ };
4596
+ case "1m":
4597
+ return {
4598
+ timeVisible: true,
4599
+ secondsVisible: true,
4600
+ borderColor: "rgba(255,255,255,0.06)",
4601
+ tickMarkFormatter: createTickFormatter({ hour: "2-digit", minute: "2-digit", second: "2-digit" })
4602
+ };
4603
+ case "5m":
4604
+ return {
4605
+ timeVisible: true,
4606
+ secondsVisible: false,
4607
+ borderColor: "rgba(255,255,255,0.06)",
4608
+ tickMarkFormatter: createTickFormatter({ hour: "2-digit", minute: "2-digit" })
4609
+ };
4610
+ case "15m":
4611
+ return {
4612
+ timeVisible: true,
4613
+ secondsVisible: false,
4614
+ borderColor: "rgba(255,255,255,0.06)",
4615
+ tickMarkFormatter: createTickFormatter({ hour: "2-digit", minute: "2-digit" })
4616
+ };
4617
+ case "1h":
4618
+ return {
4619
+ timeVisible: true,
4620
+ secondsVisible: false,
4621
+ borderColor: "rgba(255,255,255,0.06)",
4622
+ tickMarkFormatter: createTickFormatter({ hour: "2-digit", minute: "2-digit" })
4623
+ };
4624
+ case "4h":
4625
+ return {
4626
+ timeVisible: true,
4627
+ secondsVisible: false,
4628
+ borderColor: "rgba(255,255,255,0.06)",
4629
+ tickMarkFormatter: createTickFormatter({ weekday: "short", hour: "2-digit" })
4630
+ };
4631
+ case "24h":
4632
+ return {
4633
+ timeVisible: true,
4634
+ secondsVisible: false,
4635
+ borderColor: "rgba(255,255,255,0.06)",
4636
+ tickMarkFormatter: createTickFormatter({ month: "short", day: "numeric" })
4637
+ };
4638
+ case "1W":
4639
+ return {
4640
+ timeVisible: true,
4641
+ secondsVisible: false,
4642
+ borderColor: "rgba(255,255,255,0.06)",
4643
+ tickMarkFormatter: createTickFormatter({ month: "short", day: "numeric" })
4644
+ };
4645
+ case "1M":
4646
+ return {
4647
+ timeVisible: true,
4648
+ secondsVisible: false,
4649
+ borderColor: "rgba(255,255,255,0.06)",
4650
+ tickMarkFormatter: createTickFormatter({ month: "short", year: "2-digit" })
4651
+ };
4652
+ default:
4653
+ return {
4654
+ timeVisible: true,
4655
+ secondsVisible: false,
4656
+ borderColor: "rgba(255,255,255,0.06)"
4657
+ };
4658
+ }
4659
+ };
4660
+ var getPriceScaleOptions = (data) => {
4661
+ if (!data.length) return {
4662
+ borderColor: "rgba(230, 200, 126, 0.25)",
4663
+ textColor: "rgba(230, 200, 126, 0.7)"
4664
+ };
4665
+ const prices = data.flatMap((d2) => [d2.open, d2.high, d2.low, d2.close]);
4666
+ const minPrice = Math.min(...prices);
4667
+ const maxPrice = Math.max(...prices);
4668
+ const priceRange = maxPrice - minPrice;
4669
+ let scaleMargins = { top: 0.1, bottom: 0.1 };
4670
+ if (priceRange < 1) {
4671
+ scaleMargins = { top: 0.2, bottom: 0.2 };
4672
+ } else if (priceRange > 1e3) {
4673
+ scaleMargins = { top: 0.05, bottom: 0.05 };
4674
+ }
4675
+ return {
4676
+ borderColor: "rgba(230, 200, 126, 0.25)",
4677
+ textColor: "rgba(230, 200, 126, 0.7)",
4678
+ scaleMargins
4679
+ };
4680
+ };
4681
+ var defaultRanges = ["30s", "1m", "5m", "15m", "1h", "4h", "24h", "1W", "1M"];
4682
+ var VISIBLE_RANGE_COUNT = 4;
4571
4683
  var formatPrice = (value, currencySymbol) => {
4572
4684
  return `${currencySymbol}${value.toLocaleString(void 0, {
4573
4685
  minimumFractionDigits: 2,
@@ -4604,6 +4716,21 @@ var PriceChart = o__namespace.forwardRef(
4604
4716
  const seriesRef = o__namespace.useRef(null);
4605
4717
  const priceLineRef = o__namespace.useRef(null);
4606
4718
  const [hoveredRange, setHoveredRange] = o__namespace.useState(null);
4719
+ const [dropdownOpen, setDropdownOpen] = o__namespace.useState(false);
4720
+ const dropdownRef = o__namespace.useRef(null);
4721
+ const isAutoScrollRef = o__namespace.useRef(true);
4722
+ const visibleRanges = ranges.slice(0, VISIBLE_RANGE_COUNT);
4723
+ const dropdownRanges = ranges.slice(VISIBLE_RANGE_COUNT);
4724
+ const selectedInDropdown = dropdownRanges.includes(selectedRange);
4725
+ o__namespace.useEffect(() => {
4726
+ const handleClickOutside = (e) => {
4727
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
4728
+ setDropdownOpen(false);
4729
+ }
4730
+ };
4731
+ document.addEventListener("mousedown", handleClickOutside);
4732
+ return () => document.removeEventListener("mousedown", handleClickOutside);
4733
+ }, []);
4607
4734
  const resolvedPrice = o__namespace.useMemo(() => {
4608
4735
  if (price != null) return price;
4609
4736
  const last = data.at(-1);
@@ -4636,15 +4763,8 @@ var PriceChart = o__namespace.forwardRef(
4636
4763
  vertLines: { color: "rgba(255,255,255,0.06)" },
4637
4764
  horzLines: { color: "rgba(255,255,255,0.06)" }
4638
4765
  },
4639
- rightPriceScale: {
4640
- borderColor: "rgba(230, 200, 126, 0.25)",
4641
- textColor: "rgba(230, 200, 126, 0.7)"
4642
- },
4643
- timeScale: {
4644
- borderColor: "rgba(255,255,255,0.06)",
4645
- timeVisible: true,
4646
- secondsVisible: false
4647
- },
4766
+ rightPriceScale: getPriceScaleOptions(data),
4767
+ timeScale: getTimeScaleOptions(selectedRange || defaultRanges[0]),
4648
4768
  crosshair: {
4649
4769
  vertLine: { color: "rgba(255,255,255,0.12)" },
4650
4770
  horzLine: { color: "rgba(255,255,255,0.12)" }
@@ -4659,17 +4779,37 @@ var PriceChart = o__namespace.forwardRef(
4659
4779
  });
4660
4780
  chartRef.current = chart;
4661
4781
  seriesRef.current = series;
4782
+ const handleVisibleRangeChange = () => {
4783
+ const timeScale2 = chart.timeScale();
4784
+ const position2 = timeScale2.scrollPosition();
4785
+ const atRightEdge = position2 <= 0.01;
4786
+ isAutoScrollRef.current = atRightEdge;
4787
+ };
4788
+ const timeScale = chart.timeScale();
4789
+ timeScale.subscribeVisibleLogicalRangeChange(handleVisibleRangeChange);
4662
4790
  return () => {
4663
4791
  chartRef.current = null;
4664
4792
  seriesRef.current = null;
4793
+ timeScale.unsubscribeVisibleLogicalRangeChange(handleVisibleRangeChange);
4665
4794
  chart.remove();
4666
4795
  };
4667
4796
  }, []);
4797
+ o__namespace.useEffect(() => {
4798
+ const chart = chartRef.current;
4799
+ if (!chart) return;
4800
+ const effectiveRange = selectedRange ?? ranges?.[0] ?? "1D";
4801
+ chart.applyOptions({
4802
+ timeScale: getTimeScaleOptions(effectiveRange)
4803
+ });
4804
+ }, [selectedRange, ranges]);
4668
4805
  o__namespace.useEffect(() => {
4669
4806
  const chart = chartRef.current;
4670
4807
  const series = seriesRef.current;
4671
4808
  if (!chart || !series) return;
4672
4809
  series.setData(data);
4810
+ chart.applyOptions({
4811
+ rightPriceScale: getPriceScaleOptions(data)
4812
+ });
4673
4813
  if (priceLineRef.current) {
4674
4814
  series.removePriceLine(priceLineRef.current);
4675
4815
  priceLineRef.current = null;
@@ -4685,7 +4825,9 @@ var PriceChart = o__namespace.forwardRef(
4685
4825
  title: resolvedPrice.toFixed(2)
4686
4826
  });
4687
4827
  }
4688
- chart.timeScale().fitContent();
4828
+ if (isAutoScrollRef.current) {
4829
+ chart.timeScale().scrollToPosition(0, true);
4830
+ }
4689
4831
  }, [data, resolvedPrice]);
4690
4832
  const sign = inferredChangePercent == null ? null : inferredChangePercent >= 0 ? "+" : "";
4691
4833
  const changeClass = inferredChangePercent == null ? "" : inferredChangePercent >= 0 ? "text-[#0ecb81]" : "text-[#f6465d]";
@@ -4717,30 +4859,97 @@ var PriceChart = o__namespace.forwardRef(
4717
4859
  /* @__PURE__ */ jsxRuntime.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: [
4718
4860
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full flex-col gap-3 md:w-auto", children: [
4719
4861
  /* @__PURE__ */ jsxRuntime.jsx(CardTitle, { className: "m-0 text-[1.1rem] font-semibold text-white", children: title }),
4720
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: ranges.map((r2) => {
4721
- const active = r2 === selectedRange;
4722
- const hovered = hoveredRange === r2;
4723
- const style = {
4724
- ...btnBaseStyle,
4725
- ...hovered ? btnHoverStyle : null,
4726
- ...active ? btnActiveStyle : null
4727
- };
4728
- return /* @__PURE__ */ jsxRuntime.jsx(
4862
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
4863
+ visibleRanges.map((r2) => {
4864
+ const active = r2 === selectedRange;
4865
+ const hovered = hoveredRange === r2;
4866
+ const style = {
4867
+ ...btnBaseStyle,
4868
+ ...hovered ? btnHoverStyle : null,
4869
+ ...active ? btnActiveStyle : null
4870
+ };
4871
+ return /* @__PURE__ */ jsxRuntime.jsx(
4872
+ "button",
4873
+ {
4874
+ type: "button",
4875
+ onClick: () => onRangeChange?.(r2),
4876
+ onMouseEnter: () => setHoveredRange(r2),
4877
+ onMouseLeave: () => setHoveredRange((prev2) => prev2 === r2 ? null : prev2),
4878
+ style,
4879
+ className: cn(
4880
+ "rounded border px-3 py-1 text-[0.85rem] font-medium transition-all duration-200"
4881
+ ),
4882
+ children: r2
4883
+ },
4884
+ r2
4885
+ );
4886
+ }),
4887
+ selectedInDropdown && selectedRange ? /* @__PURE__ */ jsxRuntime.jsx(
4729
4888
  "button",
4730
4889
  {
4731
4890
  type: "button",
4732
- onClick: () => onRangeChange?.(r2),
4733
- onMouseEnter: () => setHoveredRange(r2),
4734
- onMouseLeave: () => setHoveredRange((prev2) => prev2 === r2 ? null : prev2),
4735
- style,
4891
+ style: {
4892
+ ...btnBaseStyle,
4893
+ ...btnActiveStyle
4894
+ },
4736
4895
  className: cn(
4737
4896
  "rounded border px-3 py-1 text-[0.85rem] font-medium transition-all duration-200"
4738
4897
  ),
4739
- children: r2
4740
- },
4741
- r2
4742
- );
4743
- }) })
4898
+ disabled: true,
4899
+ children: selectedRange
4900
+ }
4901
+ ) : null,
4902
+ dropdownRanges.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", ref: dropdownRef, children: [
4903
+ /* @__PURE__ */ jsxRuntime.jsxs(
4904
+ "button",
4905
+ {
4906
+ type: "button",
4907
+ onClick: () => setDropdownOpen((prev2) => !prev2),
4908
+ onMouseEnter: () => setHoveredRange("__dropdown__"),
4909
+ onMouseLeave: () => setHoveredRange((prev2) => prev2 === "__dropdown__" ? null : prev2),
4910
+ style: {
4911
+ ...btnBaseStyle,
4912
+ ...hoveredRange === "__dropdown__" ? btnHoverStyle : null
4913
+ },
4914
+ className: cn(
4915
+ "flex items-center gap-1 rounded border px-3 py-1 text-[0.85rem] font-medium transition-all duration-200"
4916
+ ),
4917
+ children: [
4918
+ "More",
4919
+ /* @__PURE__ */ jsxRuntime.jsx(
4920
+ "svg",
4921
+ {
4922
+ className: cn("h-3 w-3 transition-transform", dropdownOpen && "rotate-180"),
4923
+ fill: "none",
4924
+ stroke: "currentColor",
4925
+ viewBox: "0 0 24 24",
4926
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" })
4927
+ }
4928
+ )
4929
+ ]
4930
+ }
4931
+ ),
4932
+ dropdownOpen && /* @__PURE__ */ jsxRuntime.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) => {
4933
+ const active = r2 === selectedRange;
4934
+ return /* @__PURE__ */ jsxRuntime.jsx(
4935
+ "button",
4936
+ {
4937
+ type: "button",
4938
+ onClick: () => {
4939
+ onRangeChange?.(r2);
4940
+ setDropdownOpen(false);
4941
+ },
4942
+ className: cn(
4943
+ "block w-full px-3 py-1.5 text-left text-[0.85rem] font-medium transition-colors hover:bg-white/10",
4944
+ active ? "bg-[#e6c87e]/20 text-[#e6c87e]" : "text-white/70"
4945
+ ),
4946
+ children: r2
4947
+ },
4948
+ r2
4949
+ );
4950
+ }) })
4951
+ ] })
4952
+ ] })
4744
4953
  ] }),
4745
4954
  resolvedPrice == null && inferredChangePercent == null ? null : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center" }, children: [
4746
4955
  resolvedPrice == null ? null : /* @__PURE__ */ jsxRuntime.jsx(