@trading-game/design-intelligence-layer 0.13.2 → 0.14.0

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/README.md CHANGED
@@ -165,8 +165,9 @@ Blocks are pre-composed UI patterns built from design system primitives. They ar
165
165
  |---|---|---|
166
166
  | NavBar | Desktop, Mobile (closed), Mobile (open) | Landing page top navigation bar. Logo + ghost nav links (Products, Use Cases, Docs, Blog, FAQ) + Sign in / Sign up CTAs. |
167
167
  | Hero | Type 1 (Desktop + Mobile), Type 2 | Landing page hero sections. Type 1: tagline pill + heading + body + CTAs left, image right (responsive). Type 2: centred single-column, tagline pill + heading + body + primary CTA with arrow icon, no image. |
168
- | Sheet Open Positions | Active, Empty | Right-side sheet (desktop) / bottom drawer (mobile) listing open trading positions. Active state shows a scrollable position list with win/loss colouring and a history footer link. Empty state shows an empty state illustration. |
168
+ | Sheet Open Positions | Active (Rise-Fall, Swipe, Box-O, Digits), Empty, Edge cases (1 item, 30 items) | Right-side sheet (desktop) / bottom drawer (mobile) listing open trading positions. Each row shows bet name + stake (left) and PnL + market·duration (right). The drawer hugs its content with `max-h-[80vh]`; long lists scroll internally with header and footer pinned. |
169
169
  | Header navigation | — | Product app top header bar. Back navigation button left, account balance + Demo badge centre, history and sound action buttons right. |
170
+ | Result | Win · continuing, Loss · continuing, End of demo, Out of balance, On a roll | Fixed-width (320 px) end-of-round result card. Thumb-up/down SVG with subtle radial halo + idle motion; `Bust.` / `Called it.` title becomes `You won!` / `You lost!`; amount headline; contract + duration `standard` Badge pills; primary + (optionally) secondary CTA. Out-of-balance and on-a-roll are dialog-style variants with no icon — just title + body + 2 CTAs. |
170
171
 
171
172
  ---
172
173
 
@@ -215,6 +216,9 @@ States: hover (`bg-secondary-hover`), focus (3px `ring-ring/50`), active (`opaci
215
216
  <Badge variant="default-success" /> // Green
216
217
  <Badge variant="default-fail" /> // Red
217
218
 
219
+ // Standard (neutral grey chip) — for non-status meta like contract type, duration
220
+ <Badge variant="standard" />
221
+
218
222
  // Fill (tint background)
219
223
  <Badge variant="fill" /> // Blue tint
220
224
  <Badge variant="fill-success" /> // Green tint
@@ -595,7 +599,15 @@ Design tokens are managed in Figma and exported as CSS variables. To update:
595
599
 
596
600
  ## Changelog
597
601
 
598
- ### Unreleased
602
+ ### v0.14.0
603
+ - **New Result block:** Fixed-width (320 px) end-of-round result card with 5 variants — Win/continuing, Loss/continuing, End of demo, Out of balance, On a roll. Thumb-up/down SVG with subtle radial halo + idle motion (float for win, head-shake for loss). Title (`You won!` / `You lost!`), amount headline in semantic colour, contract + duration `standard` Badge pills, and CTA(s). Digit contracts render the picked digit inside the contract pill with a thin vertical divider. Out-of-balance and on-a-roll variants are dialog-only (no thumb / no amount) — title + body + 2 CTAs. Lives under **Blocks → Result** in the playground.
604
+ - **Sheet Open Positions redesign:** Each row now uses a 2-col layout — bet name + stake (left) / PnL + market·duration (right). Dropped per-row asset (`V100`), round number, and digit/multiplier suffix labels (the game is already implied by the sheet/tab title). Abbreviated durations (sec / min / hr / day) and dropped the `Stake ` prefix on amounts. Added an "Edge cases" row demonstrating drawer behavior with 1 item and 30 items.
605
+ - **Section nav sheet — desktop-safe:** The mobile section picker `Sheet` is now conditionally mounted (`{isMobile && …}`) so it can't be triggered at desktop widths where the permanent sidebar is already visible, avoiding redundant overlay layers.
606
+ - **Badge — new `standard` variant:** Neutral grey chip (`bg-subtle` / `text-on-prominent`) for non-status meta like contract type and duration. Existing `default` (solid blue) and all other variants are unchanged.
607
+ - **TicketCard / BoostTicketCard:**
608
+ - Left content restructured as a 2-column grid so the icon circle scales to match the **label + value height** (was a fixed `size-10` centered against the entire column).
609
+ - Boost badge now sits in row 2 of column 2 — aligned under the value, not pinned to the card's left edge.
610
+ - **2-decimal balance formatting:** the `value` prop is now normalised by `formatBalanceValue` before render, so values always display with exactly two decimal places regardless of input (e.g. `$12,450` → `$12,450.00`, `$12,450.5` → `$12,450.50`, `1234` → `1,234.00`). Currency prefix/suffix is preserved.
599
611
  - **Token convention — `on-<surface>` foreground pairs:** Added a Material-style paired foreground for every semantic surface so colour roles are self-documenting and resolve robustly across consumer setups (incl. Module Federation, where raw `var()` fallbacks were previously needed). New CSS variables and Tailwind utilities:
600
612
  - `--on-primary` (white) / `text-on-primary` — paired with `bg-primary`
601
613
  - `--primary-inverse` (white) / `bg-primary-inverse` — inverted primary surface for use on dark/coloured areas
package/dist/index.cjs CHANGED
@@ -947,6 +947,8 @@ var badgeVariants = (0, import_class_variance_authority3.cva)(
947
947
  variant: {
948
948
  // Default (solid)
949
949
  default: "bg-primary text-on-primary [a&]:hover:bg-primary/90",
950
+ // Standard (neutral chip — for non-status meta like contract type, duration)
951
+ standard: "bg-subtle text-on-prominent [a&]:hover:bg-subtle/80",
950
952
  "default-success": "bg-semantic-win text-on-semantic-win [a&]:hover:bg-semantic-win/90",
951
953
  "default-fail": "bg-semantic-loss text-on-semantic-loss [a&]:hover:bg-semantic-loss/90",
952
954
  "default-warning": "bg-semantic-warning text-on-semantic-warning [a&]:hover:bg-semantic-warning/90",
@@ -6650,6 +6652,18 @@ function TabsContent(_a) {
6650
6652
  // components/ui/ticket-card.tsx
6651
6653
  var import_lucide_react23 = require("lucide-react");
6652
6654
  var import_jsx_runtime57 = require("react/jsx-runtime");
6655
+ function formatBalanceValue(raw) {
6656
+ const match = raw.match(/^(\D*)([\d,]+(?:\.\d+)?)(\D*)$/);
6657
+ if (!match) return raw;
6658
+ const [, prefix = "", numericPart, suffix = ""] = match;
6659
+ const parsed = Number.parseFloat(numericPart.replace(/,/g, ""));
6660
+ if (Number.isNaN(parsed)) return raw;
6661
+ const formatted = parsed.toLocaleString("en-US", {
6662
+ minimumFractionDigits: 2,
6663
+ maximumFractionDigits: 2
6664
+ });
6665
+ return `${prefix}${formatted}${suffix}`;
6666
+ }
6653
6667
  function TicketCard({
6654
6668
  className,
6655
6669
  icon,
@@ -6667,7 +6681,7 @@ function TicketCard({
6667
6681
  /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("div", { className: "flex h-11 flex-col justify-center gap-1 whitespace-nowrap", children: [
6668
6682
  /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("p", { className: "text-xs text-on-subtle", children: label }),
6669
6683
  /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("p", { className: "leading-none", children: [
6670
- /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("span", { className: "text-[20px] font-bold text-on-prominent", children: value }),
6684
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("span", { className: "text-[20px] font-bold text-on-prominent", children: formatBalanceValue(value) }),
6671
6685
  currency && /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(import_jsx_runtime57.Fragment, { children: [
6672
6686
  " ",
6673
6687
  /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("span", { className: "text-sm text-on-subtle", children: currency })
@@ -6733,43 +6747,67 @@ function BoostTicketCard({
6733
6747
  boostInfo,
6734
6748
  boostInfoSide = "top",
6735
6749
  boostInfoAlign = "center",
6750
+ boostInfoTitle,
6751
+ boostInfoCloseLabel,
6736
6752
  compact = false
6737
6753
  }) {
6738
6754
  return /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("div", { className: cn("flex w-full flex-col gap-2", className), children: /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("div", { className: "relative flex w-full items-stretch justify-between rounded-sm bg-subtle", children: [
6739
- /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("div", { className: cn("flex flex-1 items-center overflow-hidden py-4", compact ? "gap-2 px-2" : "gap-4 px-4"), children: [
6740
- icon && /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-full border-2 border-primary text-primary", children: icon }),
6741
- /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("div", { className: "flex flex-col gap-2 whitespace-nowrap", children: [
6742
- /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("div", { className: "flex flex-col gap-1", children: [
6743
- /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("p", { className: "text-xs text-on-subtle", children: label }),
6744
- /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("p", { className: "leading-none", children: [
6745
- /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("span", { className: "text-[20px] font-bold text-on-prominent", children: value }),
6746
- currency && /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(import_jsx_runtime57.Fragment, { children: [
6747
- " ",
6748
- /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("span", { className: "text-sm text-on-subtle", children: currency })
6749
- ] })
6755
+ (() => {
6756
+ const boostBadge = /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(Badge, { variant: "fill-boost", size: "sm", className: "!gap-1 !font-medium !tracking-normal", children: [
6757
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(import_lucide_react23.Rocket, { className: "!size-3.5", strokeWidth: 2 }),
6758
+ "Boost: ",
6759
+ boostAmount,
6760
+ " ",
6761
+ boostCurrency,
6762
+ boostInfo && /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(TooltipProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(Tooltip2, { children: [
6763
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
6764
+ "button",
6765
+ {
6766
+ type: "button",
6767
+ "aria-label": "More info about boost",
6768
+ className: "inline-flex shrink-0 items-center justify-center rounded-full text-on-semantic-boost outline-none transition-opacity hover:opacity-70 focus-visible:opacity-70 focus-visible:ring-2 focus-visible:ring-on-semantic-boost/40 cursor-pointer",
6769
+ children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(import_lucide_react23.Info, { className: "!size-3.5", strokeWidth: 2 })
6770
+ }
6771
+ ) }),
6772
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(TooltipContent, { side: boostInfoSide, align: boostInfoAlign, title: boostInfoTitle, closeLabel: boostInfoCloseLabel, variant: "inverse", className: "max-w-xs whitespace-normal", children: boostInfo })
6773
+ ] }) })
6774
+ ] });
6775
+ const labelValue = /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(import_jsx_runtime57.Fragment, { children: [
6776
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("p", { className: "text-xs text-on-subtle", children: label }),
6777
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("p", { className: "leading-none", children: [
6778
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("span", { className: "text-[20px] font-bold text-on-prominent", children: formatBalanceValue(value) }),
6779
+ currency && /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(import_jsx_runtime57.Fragment, { children: [
6780
+ " ",
6781
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("span", { className: "text-sm text-on-subtle", children: currency })
6750
6782
  ] })
6751
- ] }),
6752
- /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(Badge, { variant: "fill-boost", size: "sm", className: "!gap-1 !font-medium !tracking-normal", children: [
6753
- /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(import_lucide_react23.Rocket, { className: "!size-3.5", strokeWidth: 2 }),
6754
- "Boost: ",
6755
- boostAmount,
6756
- " ",
6757
- boostCurrency,
6758
- boostInfo && /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(TooltipProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(Tooltip2, { children: [
6759
- /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
6760
- "button",
6761
- {
6762
- type: "button",
6763
- "aria-label": "More info about boost",
6764
- className: "inline-flex shrink-0 items-center justify-center rounded-full text-on-semantic-boost outline-none transition-opacity hover:opacity-70 focus-visible:opacity-70 focus-visible:ring-2 focus-visible:ring-on-semantic-boost/40 cursor-pointer",
6765
- children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(import_lucide_react23.Info, { className: "!size-3.5", strokeWidth: 2 })
6766
- }
6767
- ) }),
6768
- /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(TooltipContent, { side: boostInfoSide, align: boostInfoAlign, variant: "inverse", className: "max-w-xs whitespace-normal", children: boostInfo })
6769
- ] }) })
6770
6783
  ] })
6771
- ] })
6772
- ] }),
6784
+ ] });
6785
+ return /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("div", { className: cn("flex flex-1 items-center overflow-hidden py-4", compact ? "gap-2 px-2" : "gap-4 px-4"), children: [
6786
+ icon && /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-full border-2 border-primary text-primary", children: icon }),
6787
+ compact ? (
6788
+ /* Mobile / compact: badge stacked under the value */
6789
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("div", { className: "flex flex-col gap-2 whitespace-nowrap", children: [
6790
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("div", { className: "flex flex-col gap-1", children: labelValue }),
6791
+ boostBadge
6792
+ ] })
6793
+ ) : (
6794
+ /* Desktop: badge inline next to the value (16px gap = gap-4) */
6795
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("div", { className: "flex flex-col gap-1 whitespace-nowrap", children: [
6796
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("p", { className: "text-xs text-on-subtle", children: label }),
6797
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("div", { className: "flex items-center gap-4", children: [
6798
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)("p", { className: "leading-none", children: [
6799
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("span", { className: "text-[20px] font-bold text-on-prominent", children: formatBalanceValue(value) }),
6800
+ currency && /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(import_jsx_runtime57.Fragment, { children: [
6801
+ " ",
6802
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("span", { className: "text-sm text-on-subtle", children: currency })
6803
+ ] })
6804
+ ] }),
6805
+ boostBadge
6806
+ ] })
6807
+ ] })
6808
+ )
6809
+ ] });
6810
+ })(),
6773
6811
  /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("div", { "aria-hidden": true, className: "pointer-events-none absolute right-[88px] top-[-12px] z-10 size-6 rounded-full bg-prominent" }),
6774
6812
  /* @__PURE__ */ (0, import_jsx_runtime57.jsx)("div", { "aria-hidden": true, className: "pointer-events-none absolute right-[88px] bottom-[-12px] z-10 size-6 rounded-full bg-prominent" }),
6775
6813
  /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(