@hfunlabs/hypurr-connect 0.1.14 → 0.1.16

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.
@@ -9,15 +9,24 @@ import {
9
9
  type MouseEvent as ReactMouseEvent,
10
10
  type ReactNode,
11
11
  } from "react";
12
+ import {
13
+ AgentExpiryWarningIcon,
14
+ EXPIRED_AGENT_COLOR,
15
+ } from "./AgentExpiryWarning";
12
16
  import { useHypurrConnectInternal } from "./HypurrConnectProvider";
13
17
  import { UserProfileModal, type SlippageOption } from "./UserProfileModal";
18
+ import { getAgentExpiryTitle } from "./agentWallet";
14
19
  import {
20
+ Bot,
15
21
  Copy,
16
22
  Crown,
23
+ Eye,
17
24
  Folder,
25
+ KeyRound,
18
26
  LayoutDashboard,
19
27
  LogOut,
20
28
  Plus,
29
+ RefreshCw,
21
30
  Star,
22
31
  User,
23
32
  Wallet,
@@ -36,6 +45,8 @@ export interface WalletSelectorDropdownProps {
36
45
  onAddWallet?: () => void;
37
46
  /** Called when the user clicks the portfolio icon on a wallet row. */
38
47
  onShowPortfolio?: (wallet: HyperliquidWallet) => void;
48
+ /** Called when the user clicks an agent wallet renew action. Host should connect the owner wallet and call `renewAgentWallet`. */
49
+ onRenewAgentWallet?: (wallet: HyperliquidWallet) => void;
39
50
  /**
40
51
  * Called before the SDK's `logout()` runs. Host can do extra cleanup
41
52
  * (e.g. wagmi `disconnect()`).
@@ -67,6 +78,21 @@ export interface WalletSelectorDropdownProps {
67
78
  principalColors?: PrincipalColorOverrides;
68
79
  /** CSS color used as the dropdown panel background. Defaults to `rgba(20,20,20,0.95)`. */
69
80
  backgroundColor?: string;
81
+ /** Extra footer rows rendered between "Profile & Settings" and "Logout". */
82
+ extraFooterItems?: ExtraFooterItem[];
83
+ }
84
+
85
+ export interface ExtraFooterItem {
86
+ /** Stable key for React reconciliation. */
87
+ key: string;
88
+ /** Leading icon (e.g. an icon from `./icons/lucide`). */
89
+ icon: ReactNode;
90
+ /** Row label. */
91
+ label: string;
92
+ /** Click handler. The dropdown is NOT auto-closed — call `onClose` yourself if desired. */
93
+ onClick: () => void;
94
+ /** Optional hover text color (matches the "Logout" red-on-hover pattern). */
95
+ hoverColor?: string;
70
96
  }
71
97
 
72
98
  const DEFAULT_BACKGROUND_COLOR = "rgba(20,20,20,0.95)";
@@ -75,6 +101,10 @@ interface DropdownWallet {
75
101
  id: number;
76
102
  name?: string | null;
77
103
  ethereumAddress?: string;
104
+ agentEthereumAddress?: HyperliquidWallet["agentEthereumAddress"];
105
+ isAgent?: boolean;
106
+ isReadOnly?: boolean;
107
+ agentExpiresAt?: HyperliquidWallet["agentExpiresAt"];
78
108
  }
79
109
 
80
110
  interface WalletListItem {
@@ -171,6 +201,36 @@ const formatCompactAddress = (addr: string | undefined) => {
171
201
  return `${addr.slice(0, 4)}...${addr.slice(-2)}`;
172
202
  };
173
203
 
204
+ type WalletTypeMeta = {
205
+ label: string;
206
+ color: string;
207
+ icon: ReactNode;
208
+ };
209
+
210
+ function getWalletTypeMeta(wallet: DropdownWallet): WalletTypeMeta {
211
+ if (wallet.isAgent) {
212
+ return {
213
+ label: "Agent wallet",
214
+ color: "#38bdf8",
215
+ icon: <Bot size={12} />,
216
+ };
217
+ }
218
+
219
+ if (wallet.isReadOnly) {
220
+ return {
221
+ label: "Read-only wallet",
222
+ color: "#a78bfa",
223
+ icon: <Eye size={12} />,
224
+ };
225
+ }
226
+
227
+ return {
228
+ label: "Private-key wallet",
229
+ color: "#34d399",
230
+ icon: <KeyRound size={12} />,
231
+ };
232
+ }
233
+
174
234
  const rootStyle: CSSProperties = {
175
235
  position: "absolute",
176
236
  right: 0,
@@ -217,6 +277,7 @@ export function WalletSelectorDropdown({
217
277
  onClose,
218
278
  onAddWallet,
219
279
  onShowPortfolio,
280
+ onRenewAgentWallet,
220
281
  onLogout,
221
282
  onNotify,
222
283
  vipThreshold = 10,
@@ -230,6 +291,7 @@ export function WalletSelectorDropdown({
230
291
  accentColor,
231
292
  principalColors,
232
293
  backgroundColor = DEFAULT_BACKGROUND_COLOR,
294
+ extraFooterItems,
233
295
  }: WalletSelectorDropdownProps): ReactNode {
234
296
  const { user, wallets, selectedWalletId, selectWallet, logout, authMethod } =
235
297
  useHypurrConnectInternal();
@@ -270,12 +332,17 @@ export function WalletSelectorDropdown({
270
332
  const { wallet, label } = item;
271
333
  const isSelected = wallet.id === selectedWalletId;
272
334
  const compactAddress = formatCompactAddress(wallet.ethereumAddress);
335
+ const agentExpiryTitle = getAgentExpiryTitle({
336
+ isAgent: !!wallet.isAgent,
337
+ agentExpiresAt: wallet.agentExpiresAt,
338
+ });
273
339
  return (
274
340
  <WalletRow
275
341
  key={wallet.id}
276
342
  depth={depth}
277
343
  isSelected={isSelected}
278
344
  label={label}
345
+ walletType={getWalletTypeMeta(wallet)}
279
346
  compactAddress={compactAddress}
280
347
  onSelect={() => {
281
348
  selectWallet(wallet.id);
@@ -290,6 +357,15 @@ export function WalletSelectorDropdown({
290
357
  : undefined
291
358
  }
292
359
  onCopy={() => handleCopyAddress(wallet.ethereumAddress)}
360
+ agentExpiryTitle={agentExpiryTitle}
361
+ onRenewAgentWallet={
362
+ wallet.isAgent && onRenewAgentWallet
363
+ ? () => {
364
+ onRenewAgentWallet(wallet as HyperliquidWallet);
365
+ onClose();
366
+ }
367
+ : undefined
368
+ }
293
369
  colors={colors}
294
370
  />
295
371
  );
@@ -557,6 +633,15 @@ export function WalletSelectorDropdown({
557
633
  icon={<User size={14} />}
558
634
  label="Profile & Settings"
559
635
  />
636
+ {extraFooterItems?.map((item) => (
637
+ <FooterBtn
638
+ key={item.key}
639
+ onClick={item.onClick}
640
+ icon={item.icon}
641
+ label={item.label}
642
+ hoverColor={item.hoverColor}
643
+ />
644
+ ))}
560
645
  <FooterBtn
561
646
  onClick={handleLogout}
562
647
  icon={<LogOut size={14} />}
@@ -580,6 +665,7 @@ export function WalletSelectorDropdown({
580
665
  principalColors={principalColors}
581
666
  onWalletDeleted={onWalletDeleted}
582
667
  onWalletRenamed={onWalletRenamed}
668
+ onRenewAgentWallet={onRenewAgentWallet}
583
669
  onShowPortfolio={
584
670
  onShowPortfolio
585
671
  ? (wallet) => {
@@ -628,21 +714,30 @@ function WalletRow({
628
714
  depth,
629
715
  isSelected,
630
716
  label,
717
+ walletType,
631
718
  compactAddress,
632
719
  onSelect,
633
720
  onShowPortfolio,
634
721
  onCopy,
722
+ agentExpiryTitle,
723
+ onRenewAgentWallet,
635
724
  colors,
636
725
  }: {
637
726
  depth: number;
638
727
  isSelected: boolean;
639
728
  label: string | null;
729
+ walletType: WalletTypeMeta;
640
730
  compactAddress: string;
641
731
  onSelect: () => void;
642
732
  onShowPortfolio?: () => void;
643
733
  onCopy: () => void;
734
+ agentExpiryTitle?: string | null;
735
+ onRenewAgentWallet?: () => void;
644
736
  colors: PrincipalColors;
645
737
  }) {
738
+ const isAgentExpired = !!agentExpiryTitle;
739
+ const walletTextColor = isAgentExpired ? EXPIRED_AGENT_COLOR : "#d1d5db";
740
+ const mutedTextColor = isAgentExpired ? "rgba(245,158,11,0.78)" : "#6b7280";
646
741
  return (
647
742
  <div
648
743
  style={{
@@ -652,7 +747,7 @@ function WalletRow({
652
747
  paddingTop: 6,
653
748
  paddingBottom: 6,
654
749
  fontSize: 14,
655
- color: "#d1d5db",
750
+ color: walletTextColor,
656
751
  display: "flex",
657
752
  alignItems: "center",
658
753
  background: isSelected ? "rgba(255,255,255,0.06)" : "transparent",
@@ -689,6 +784,19 @@ function WalletRow({
689
784
  padding: 0,
690
785
  }}
691
786
  >
787
+ <span
788
+ title={walletType.label}
789
+ aria-label={walletType.label}
790
+ style={{
791
+ display: "inline-flex",
792
+ alignItems: "center",
793
+ justifyContent: "center",
794
+ flexShrink: 0,
795
+ color: isAgentExpired ? walletTextColor : walletType.color,
796
+ }}
797
+ >
798
+ {walletType.icon}
799
+ </span>
692
800
  {label ? (
693
801
  <>
694
802
  <span
@@ -696,7 +804,11 @@ function WalletRow({
696
804
  overflow: "hidden",
697
805
  textOverflow: "ellipsis",
698
806
  whiteSpace: "nowrap",
699
- color: isSelected ? "#fff" : undefined,
807
+ color: isAgentExpired
808
+ ? walletTextColor
809
+ : isSelected
810
+ ? "#fff"
811
+ : undefined,
700
812
  fontWeight: isSelected ? 500 : undefined,
701
813
  }}
702
814
  >
@@ -707,7 +819,7 @@ function WalletRow({
707
819
  fontSize: 12,
708
820
  fontWeight: 400,
709
821
  flexShrink: 0,
710
- color: "#6b7280",
822
+ color: mutedTextColor,
711
823
  }}
712
824
  >
713
825
  ({compactAddress})
@@ -717,7 +829,11 @@ function WalletRow({
717
829
  <span
718
830
  style={{
719
831
  fontSize: 12,
720
- color: isSelected ? "#fff" : "#9ca3af",
832
+ color: isAgentExpired
833
+ ? walletTextColor
834
+ : isSelected
835
+ ? "#fff"
836
+ : "#9ca3af",
721
837
  fontWeight: isSelected ? 500 : undefined,
722
838
  }}
723
839
  >
@@ -725,6 +841,12 @@ function WalletRow({
725
841
  </span>
726
842
  )}
727
843
  </button>
844
+ {agentExpiryTitle && (
845
+ <AgentExpiryWarningIcon
846
+ message={agentExpiryTitle}
847
+ onClick={onRenewAgentWallet}
848
+ />
849
+ )}
728
850
  <div
729
851
  data-row-actions
730
852
  style={{
@@ -734,6 +856,18 @@ function WalletRow({
734
856
  transition: "opacity 120ms",
735
857
  }}
736
858
  >
859
+ {onRenewAgentWallet && (
860
+ <RowIconBtn
861
+ title="Renew agent wallet"
862
+ hoverColor={isAgentExpired ? EXPIRED_AGENT_COLOR : colors.accent}
863
+ onClick={(e) => {
864
+ e.stopPropagation();
865
+ onRenewAgentWallet();
866
+ }}
867
+ >
868
+ <RefreshCw size={12} />
869
+ </RowIconBtn>
870
+ )}
737
871
  {onShowPortfolio && (
738
872
  <RowIconBtn
739
873
  title="View portfolio"
package/src/agent.ts CHANGED
@@ -50,14 +50,14 @@ interface ExtraAgent {
50
50
  validUntil: number;
51
51
  }
52
52
 
53
- /**
54
- * Query the Hyperliquid info API for the named agents registered to a user.
55
- * Returns the matching entry for AGENT_NAME if it exists and is still valid.
56
- */
57
- export async function fetchActiveAgent(
53
+ function isNamedAgent(agent: ExtraAgent): boolean {
54
+ return agent.name.replace(/ valid_until \d+$/, "") === AGENT_NAME;
55
+ }
56
+
57
+ async function fetchExtraAgents(
58
58
  userAddress: string,
59
59
  isTestnet: boolean,
60
- ): Promise<ExtraAgent | null> {
60
+ ): Promise<ExtraAgent[]> {
61
61
  const url = isTestnet
62
62
  ? "https://api.hyperliquid-testnet.xyz/info"
63
63
  : "https://api.hyperliquid.xyz/info";
@@ -68,17 +68,43 @@ export async function fetchActiveAgent(
68
68
  body: JSON.stringify({ type: "extraAgents", user: userAddress }),
69
69
  });
70
70
 
71
- if (!res.ok) return null;
71
+ if (!res.ok) return [];
72
72
 
73
73
  const agents: unknown = await res.json();
74
- if (!Array.isArray(agents)) return null;
74
+ if (!Array.isArray(agents)) return [];
75
+
76
+ return (agents as ExtraAgent[]).map((agent) => ({
77
+ ...agent,
78
+ validUntil: agent.validUntil * 1000,
79
+ }));
80
+ }
75
81
 
82
+ /**
83
+ * Query the Hyperliquid info API for the named agents registered to a user.
84
+ * Returns the matching entry for AGENT_NAME if it exists and is still valid.
85
+ */
86
+ export async function fetchActiveAgent(
87
+ userAddress: string,
88
+ isTestnet: boolean,
89
+ ): Promise<ExtraAgent | null> {
76
90
  const nowMs = Date.now();
77
- const match = (agents as ExtraAgent[]).find(
78
- (a) => a.name === AGENT_NAME && a.validUntil * 1000 > nowMs,
79
- );
91
+ const agents = await fetchExtraAgents(userAddress, isTestnet);
92
+ const match = agents.find((a) => isNamedAgent(a) && a.validUntil > nowMs);
80
93
  if (!match) return null;
81
- return { ...match, validUntil: match.validUntil * 1000 };
94
+ return match;
95
+ }
96
+
97
+ export async function fetchAgentByAddress(
98
+ userAddress: string,
99
+ agentAddress: string,
100
+ isTestnet: boolean,
101
+ ): Promise<ExtraAgent | null> {
102
+ const agents = await fetchExtraAgents(userAddress, isTestnet);
103
+ return (
104
+ agents.find(
105
+ (agent) => agent.address.toLowerCase() === agentAddress.toLowerCase(),
106
+ ) ?? null
107
+ );
82
108
  }
83
109
 
84
110
  /**
@@ -0,0 +1,86 @@
1
+ import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
2
+
3
+ const MS_PER_SECOND = 1_000;
4
+ const NS_PER_MS = 1_000_000;
5
+ const MS_PER_DAY = 24 * 60 * 60 * 1_000;
6
+ const TELEGRAM_AGENT_APPROVAL_NAME = "xyz";
7
+
8
+ export interface AgentApprovalDurationOption {
9
+ label: string;
10
+ durationMs: number;
11
+ }
12
+
13
+ export const DEFAULT_AGENT_APPROVAL_DURATION_MS = MS_PER_DAY;
14
+
15
+ export const AGENT_APPROVAL_DURATION_OPTIONS: AgentApprovalDurationOption[] = [
16
+ { label: "1 day", durationMs: MS_PER_DAY },
17
+ { label: "7 days", durationMs: 7 * MS_PER_DAY },
18
+ { label: "30 days", durationMs: 30 * MS_PER_DAY },
19
+ { label: "90 days", durationMs: 90 * MS_PER_DAY },
20
+ ];
21
+
22
+ type TimestampLike =
23
+ | {
24
+ seconds?: number | string | bigint;
25
+ nanos?: number | string | bigint;
26
+ }
27
+ | string
28
+ | number
29
+ | Date;
30
+
31
+ function toFiniteNumber(value: number | string | bigint | undefined): number {
32
+ if (value === undefined) return 0;
33
+ const numeric = typeof value === "bigint" ? Number(value) : Number(value);
34
+ return Number.isFinite(numeric) ? numeric : 0;
35
+ }
36
+
37
+ export function normalizeAgentApprovalDurationMs(durationMs?: number): number {
38
+ return typeof durationMs === "number" &&
39
+ Number.isFinite(durationMs) &&
40
+ durationMs > 0
41
+ ? durationMs
42
+ : DEFAULT_AGENT_APPROVAL_DURATION_MS;
43
+ }
44
+
45
+ export function createTelegramAgentApprovalName(durationMs?: number): string {
46
+ const expiresAt = Date.now() + normalizeAgentApprovalDurationMs(durationMs);
47
+ return `${TELEGRAM_AGENT_APPROVAL_NAME} valid_until ${expiresAt}`;
48
+ }
49
+
50
+ type WalletWithAgentExpiry = Pick<HyperliquidWallet, "isAgent"> & {
51
+ agentExpiresAt?: unknown;
52
+ };
53
+
54
+ export function agentExpiresAtMs(wallet: WalletWithAgentExpiry): number | null {
55
+ if (!wallet.isAgent || !wallet.agentExpiresAt) return null;
56
+
57
+ const timestamp = wallet.agentExpiresAt as TimestampLike;
58
+ if (timestamp instanceof Date) return timestamp.getTime();
59
+ if (typeof timestamp === "number") return timestamp;
60
+ if (typeof timestamp === "string") {
61
+ const parsed = Date.parse(timestamp);
62
+ return Number.isFinite(parsed) ? parsed : null;
63
+ }
64
+
65
+ const seconds = toFiniteNumber(timestamp.seconds);
66
+ const nanos = toFiniteNumber(timestamp.nanos);
67
+ const ms = seconds * MS_PER_SECOND + Math.floor(nanos / NS_PER_MS);
68
+ return Number.isFinite(ms) && ms > 0 ? ms : null;
69
+ }
70
+
71
+ export function formatAgentExpiry(expiresAtMs: number): string {
72
+ return new Intl.DateTimeFormat(undefined, {
73
+ dateStyle: "medium",
74
+ timeStyle: "short",
75
+ }).format(new Date(expiresAtMs));
76
+ }
77
+
78
+ export function getAgentExpiryTitle(
79
+ wallet: WalletWithAgentExpiry,
80
+ ): string | null {
81
+ const expiresAtMs = agentExpiresAtMs(wallet);
82
+ if (!expiresAtMs || expiresAtMs > Date.now()) return null;
83
+
84
+ const expiresAt = formatAgentExpiry(expiresAtMs);
85
+ return `Agent approval expired ${expiresAt}. Connect the owner wallet to renew this agent.`;
86
+ }
package/src/css.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module "*.css";
@@ -70,6 +70,67 @@ export function Wallet(props: IconProps) {
70
70
  );
71
71
  }
72
72
 
73
+ export function Bot(props: IconProps) {
74
+ return (
75
+ <svg {...svgBase(props)}>
76
+ <path d="M12 8V4H8" />
77
+ <rect width="16" height="12" x="4" y="8" rx="2" />
78
+ <path d="M2 14h2" />
79
+ <path d="M20 14h2" />
80
+ <path d="M15 13v2" />
81
+ <path d="M9 13v2" />
82
+ </svg>
83
+ );
84
+ }
85
+
86
+ export function Eye(props: IconProps) {
87
+ return (
88
+ <svg {...svgBase(props)}>
89
+ <path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
90
+ <circle cx="12" cy="12" r="3" />
91
+ </svg>
92
+ );
93
+ }
94
+
95
+ export function EyeOff(props: IconProps) {
96
+ return (
97
+ <svg {...svgBase(props)}>
98
+ <path d="M10.733 5.076a10.74 10.74 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.8 10.8 0 0 1-1.444 2.49" />
99
+ <path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
100
+ <path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
101
+ <path d="m2 2 20 20" />
102
+ </svg>
103
+ );
104
+ }
105
+
106
+ export function Check(props: IconProps) {
107
+ return (
108
+ <svg {...svgBase(props)}>
109
+ <path d="M20 6 9 17l-5-5" />
110
+ </svg>
111
+ );
112
+ }
113
+
114
+ export function KeyRound(props: IconProps) {
115
+ return (
116
+ <svg {...svgBase(props)}>
117
+ <path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" />
118
+ <circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
119
+ </svg>
120
+ );
121
+ }
122
+
123
+ export function RefreshCw(props: IconProps) {
124
+ return (
125
+ <svg {...svgBase(props)}>
126
+ <path d="M3 12a9 9 0 0 1 15.18-6.49" />
127
+ <path d="M21 3v6h-6" />
128
+ <path d="M21 12a9 9 0 0 1-15.18 6.49" />
129
+ <path d="M3 21v-6h6" />
130
+ </svg>
131
+ );
132
+ }
133
+
73
134
  export function TrendingUp(props: IconProps) {
74
135
  return (
75
136
  <svg {...svgBase(props)}>
package/src/index.ts CHANGED
@@ -1,11 +1,20 @@
1
+ import "./styles.css";
2
+
1
3
  export {
2
4
  HypurrConnectProvider,
3
5
  useHypurrConnect,
4
6
  } from "./HypurrConnectProvider";
5
7
  export { LoginModal } from "./LoginModal";
6
8
  export type { LoginModalProps } from "./LoginModal";
9
+ export { AddWalletModal } from "./AddWalletModal";
10
+ export type { AddWalletModalProps } from "./AddWalletModal";
11
+ export { RenewAgentModal } from "./RenewAgentModal";
12
+ export type { RenewAgentModalProps } from "./RenewAgentModal";
7
13
  export { WalletSelectorDropdown } from "./WalletSelectorDropdown";
8
- export type { WalletSelectorDropdownProps } from "./WalletSelectorDropdown";
14
+ export type {
15
+ WalletSelectorDropdownProps,
16
+ ExtraFooterItem,
17
+ } from "./WalletSelectorDropdown";
9
18
  export { UserProfileModal } from "./UserProfileModal";
10
19
  export type { UserProfileModalProps, SlippageOption } from "./UserProfileModal";
11
20
  export { DeleteWalletModal } from "./DeleteWalletModal";
@@ -17,6 +26,12 @@ export { GrpcExchangeTransport } from "./GrpcExchangeTransport";
17
26
  export type { GrpcExchangeTransportConfig } from "./GrpcExchangeTransport";
18
27
  export { createTelegramClient, createStaticClient } from "./grpc";
19
28
  export { PrivateKeySigner } from "./privateKeySigner";
29
+ export {
30
+ AGENT_APPROVAL_DURATION_OPTIONS,
31
+ DEFAULT_AGENT_APPROVAL_DURATION_MS,
32
+ createTelegramAgentApprovalName,
33
+ } from "./agentWallet";
34
+ export type { AgentApprovalDurationOption } from "./agentWallet";
20
35
  export { createEoaSigner } from "./types";
21
36
  export type {
22
37
  AuthMethod,
@@ -29,6 +44,7 @@ export type {
29
44
  HypurrConnectConfig,
30
45
  HypurrConnectState,
31
46
  HypurrUser,
47
+ RenewAgentWalletParams,
32
48
  ScaleCreateParams,
33
49
  SignEvmTransactionFn,
34
50
  SignTypedDataFn,
@@ -37,6 +37,64 @@ export interface PrincipalColors {
37
37
 
38
38
  export type PrincipalColorOverrides = Partial<PrincipalColors>;
39
39
 
40
+ const parseSrgb = (color: string): [number, number, number] | null => {
41
+ const value = color.trim();
42
+ const shortHex = /^#([0-9a-f]{3})$/i.exec(value);
43
+ if (shortHex) {
44
+ const [r, g, b] = shortHex[1].split("").map((c) => parseInt(c + c, 16)) as [
45
+ number,
46
+ number,
47
+ number,
48
+ ];
49
+ return [r, g, b];
50
+ }
51
+ const longHex = /^#([0-9a-f]{6})$/i.exec(value);
52
+ if (longHex) {
53
+ const v = longHex[1];
54
+ return [
55
+ parseInt(v.slice(0, 2), 16),
56
+ parseInt(v.slice(2, 4), 16),
57
+ parseInt(v.slice(4, 6), 16),
58
+ ];
59
+ }
60
+ const rgb =
61
+ /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*[\d.]+\s*)?\)$/i.exec(
62
+ value,
63
+ );
64
+ if (rgb) {
65
+ return [parseInt(rgb[1], 10), parseInt(rgb[2], 10), parseInt(rgb[3], 10)];
66
+ }
67
+ return null;
68
+ };
69
+
70
+ /**
71
+ * WCAG-style relative luminance in [0, 1]. Returns `null` for color formats
72
+ * the parser doesn't understand (named colors, hsl, color-mix, …).
73
+ */
74
+ export const relativeLuminance = (color: string): number | null => {
75
+ const rgb = parseSrgb(color);
76
+ if (!rgb) return null;
77
+ const [r, g, b] = rgb.map((c) => {
78
+ const v = c / 255;
79
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
80
+ }) as [number, number, number];
81
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
82
+ };
83
+
84
+ /**
85
+ * Picks a readable foreground for `background`. Falls back to `lightFg` when
86
+ * the background can't be parsed.
87
+ */
88
+ export const pickContrastColor = (
89
+ background: string,
90
+ lightFg: string = profileColors.text,
91
+ darkFg: string = "#0a0a0a",
92
+ ): string => {
93
+ const lum = relativeLuminance(background);
94
+ if (lum === null) return lightFg;
95
+ return lum > 0.5 ? darkFg : lightFg;
96
+ };
97
+
40
98
  const colorWithAlpha = (color: string, alpha: number): string => {
41
99
  const hex = color.trim();
42
100
  const shortHex = /^#([0-9a-f]{3})$/i.exec(hex);
package/src/styles.css ADDED
@@ -0,0 +1 @@
1
+ .hypurr-connect .btn-raised{background-color:#0d1219;background-image:linear-gradient(180deg,hsla(0,0%,100%,.03),transparent);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1),inset 0 -1px 0 rgba(0,0,0,.35),inset 0 0 0 1px #262a30;color:#aab1c1;transition:background-color .15s,color .15s,background-image .15s,box-shadow .15s}.hypurr-connect .btn-raised:hover:not(:disabled){background-color:#15171a;background-image:linear-gradient(180deg,hsla(0,0%,100%,.04),transparent);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.14),inset 0 -1px 0 rgba(0,0,0,.4),inset 0 0 0 1px #444548;color:#d1d5db}.hypurr-connect .btn-raised-active{background-color:#1e2124;background-image:linear-gradient(180deg,hsla(0,0%,100%,.05),transparent);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.18),inset 0 -1px 0 rgba(0,0,0,.45),inset 0 0 0 1px #4b4d50,0 1px 2px rgba(0,0,0,.5);color:#fff}.hypurr-connect .btn-raised-active,.hypurr-connect .btn-raised-disabled{transition:background-color .15s,color .15s,background-image .15s,box-shadow .15s}.hypurr-connect .btn-raised-disabled{background-color:#0d1219;background-image:linear-gradient(180deg,hsla(0,0%,100%,.02),transparent);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.05),inset 0 -1px 0 rgba(0,0,0,.25),inset 0 0 0 1px #1c2026;color:#7d8597;cursor:not-allowed}.hypurr-connect .\!visible{visibility:visible!important}.hypurr-connect .visible{visibility:visible}.hypurr-connect .fixed{position:fixed}.hypurr-connect .absolute{position:absolute}.hypurr-connect .relative{position:relative}.hypurr-connect .inset-0{inset:0}.hypurr-connect .right-2{right:.5rem}.hypurr-connect .right-6{right:1.5rem}.hypurr-connect .top-1\/2{top:50%}.hypurr-connect .z-\[100\]{z-index:100}.hypurr-connect .z-\[101\]{z-index:101}.hypurr-connect .z-\[110\]{z-index:110}.hypurr-connect .z-\[111\]{z-index:111}.hypurr-connect .mb-1\.5{margin-bottom:.375rem}.hypurr-connect .mb-2{margin-bottom:.5rem}.hypurr-connect .mt-0\.5{margin-top:.125rem}.hypurr-connect .mt-1{margin-top:.25rem}.hypurr-connect .mt-1\.5{margin-top:.375rem}.hypurr-connect .block{display:block}.hypurr-connect .inline-block{display:inline-block}.hypurr-connect .inline{display:inline}.hypurr-connect .flex{display:flex}.hypurr-connect .inline-flex{display:inline-flex}.hypurr-connect .grid{display:grid}.hypurr-connect .contents{display:contents}.hypurr-connect .hidden{display:none}.hypurr-connect .h-8{height:2rem}.hypurr-connect .w-8{width:2rem}.hypurr-connect .w-full{width:100%}.hypurr-connect .min-w-0{min-width:0}.hypurr-connect .max-w-md{max-width:28rem}.hypurr-connect .flex-1{flex:1 1 0%}.hypurr-connect .flex-shrink-0{flex-shrink:0}.hypurr-connect .-translate-y-1\/2{--tw-translate-y:-50%}.hypurr-connect .-translate-y-1\/2,.hypurr-connect .transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hypurr-connect .cursor-not-allowed{cursor:not-allowed}.hypurr-connect .grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.hypurr-connect .items-start{align-items:flex-start}.hypurr-connect .items-center{align-items:center}.hypurr-connect .justify-center{justify-content:center}.hypurr-connect .justify-between{justify-content:space-between}.hypurr-connect .gap-1\.5{gap:.375rem}.hypurr-connect .gap-2{gap:.5rem}.hypurr-connect .gap-3{gap:.75rem}.hypurr-connect :is(.space-y-2>:not([hidden])~:not([hidden])){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.hypurr-connect :is(.space-y-3>:not([hidden])~:not([hidden])){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.hypurr-connect :is(.space-y-4>:not([hidden])~:not([hidden])){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.hypurr-connect .overflow-hidden{overflow:hidden}.hypurr-connect .truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hypurr-connect .break-all{word-break:break-all}.hypurr-connect .rounded{border-radius:.25rem}.hypurr-connect .rounded-lg{border-radius:.5rem}.hypurr-connect .rounded-md{border-radius:.375rem}.hypurr-connect .border{border-width:1px}.hypurr-connect .border-b{border-bottom-width:1px}.hypurr-connect .border-amber-500\/25{border-color:rgba(245,158,11,.25)}.hypurr-connect .border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity,1))}.hypurr-connect .border-purple-500{--tw-border-opacity:1;border-color:rgb(185 125 241/var(--tw-border-opacity,1))}.hypurr-connect .border-surface-bd{--tw-border-opacity:1;border-color:rgb(38 42 48/var(--tw-border-opacity,1))}.hypurr-connect .border-trade-down\/20{border-color:hsla(0,91%,71%,.2)}.hypurr-connect .border-trade-down\/50{border-color:hsla(0,91%,71%,.5)}.hypurr-connect .border-white\/\[0\.06\]{border-color:hsla(0,0%,100%,.06)}.hypurr-connect .border-white\/\[0\.08\]{border-color:hsla(0,0%,100%,.08)}.hypurr-connect .bg-amber-500\/\[0\.08\]{background-color:rgba(245,158,11,.08)}.hypurr-connect .bg-black\/70{background-color:rgba(0,0,0,.7)}.hypurr-connect .bg-surface-btn\/90{background-color:rgba(13,18,25,.9)}.hypurr-connect .bg-surface-modal{--tw-bg-opacity:1;background-color:rgb(14 18 24/var(--tw-bg-opacity,1))}.hypurr-connect .bg-trade-down\/\[0\.08\]{background-color:hsla(0,91%,71%,.08)}.hypurr-connect .bg-transparent{background-color:transparent}.hypurr-connect .bg-white\/\[0\.03\]{background-color:hsla(0,0%,100%,.03)}.hypurr-connect .bg-white\/\[0\.06\]{background-color:hsla(0,0%,100%,.06)}.hypurr-connect .p-1\.5{padding:.375rem}.hypurr-connect .p-3{padding:.75rem}.hypurr-connect .p-4{padding:1rem}.hypurr-connect .px-3{padding-left:.75rem;padding-right:.75rem}.hypurr-connect .px-6{padding-left:1.5rem;padding-right:1.5rem}.hypurr-connect .py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.hypurr-connect .py-2{padding-top:.5rem;padding-bottom:.5rem}.hypurr-connect .py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.hypurr-connect .py-5{padding-top:1.25rem}.hypurr-connect .pb-5,.hypurr-connect .py-5{padding-bottom:1.25rem}.hypurr-connect .pb-6{padding-bottom:1.5rem}.hypurr-connect .pr-11{padding-right:2.75rem}.hypurr-connect .pt-5{padding-top:1.25rem}.hypurr-connect .pt-6{padding-top:1.5rem}.hypurr-connect .font-mono{font-family:Google Sans Code,Roboto Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}.hypurr-connect .font-sans{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.hypurr-connect .text-base{font-size:12.5px;line-height:1rem}.hypurr-connect .text-lg{font-size:14px;line-height:1.25rem}.hypurr-connect .font-medium{font-weight:500}.hypurr-connect .font-semibold{font-weight:600}.hypurr-connect .uppercase{text-transform:uppercase}.hypurr-connect .tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.hypurr-connect .tracking-\[0\.1em\]{letter-spacing:.1em}.hypurr-connect .text-amber-200{--tw-text-opacity:1;color:rgb(253 230 138/var(--tw-text-opacity,1))}.hypurr-connect .text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.hypurr-connect .text-gray-400{--tw-text-opacity:1;color:rgb(170 177 193/var(--tw-text-opacity,1))}.hypurr-connect .text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.hypurr-connect .text-gray-600{--tw-text-opacity:1;color:rgb(125 133 151/var(--tw-text-opacity,1))}.hypurr-connect .text-purple-400{--tw-text-opacity:1;color:rgb(216 180 254/var(--tw-text-opacity,1))}.hypurr-connect .text-trade-down{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.hypurr-connect .text-trade-up{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.hypurr-connect .text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hypurr-connect .placeholder-gray-600::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(125 133 151/var(--tw-placeholder-opacity,1))}.hypurr-connect .placeholder-gray-600::placeholder{--tw-placeholder-opacity:1;color:rgb(125 133 151/var(--tw-placeholder-opacity,1))}.hypurr-connect .shadow-modal{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.6);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hypurr-connect .outline{outline-style:solid}.hypurr-connect .blur{--tw-blur:blur(8px)}.hypurr-connect .blur,.hypurr-connect .drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hypurr-connect .drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.hypurr-connect .filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hypurr-connect .backdrop-blur-sm{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.hypurr-connect .transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hypurr-connect .transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hypurr-connect .hover\:bg-purple-500\/10:hover{background-color:rgba(185,125,241,.1)}.hypurr-connect .hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.hypurr-connect .hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hypurr-connect .focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.hypurr-connect .disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.hypurr-connect .disabled\:opacity-40:disabled{opacity:.4}.hypurr-connect .disabled\:opacity-50:disabled{opacity:.5}