@luxfi/ui 7.4.0 → 7.4.3
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/avatar.cjs +7 -6
- package/dist/avatar.js +7 -5
- package/dist/badge.cjs +33 -35
- package/dist/badge.js +33 -35
- package/dist/bank.d.cts +3 -3
- package/dist/bank.d.ts +3 -3
- package/dist/checkbox.cjs +4 -4
- package/dist/checkbox.d.cts +3 -2
- package/dist/checkbox.d.ts +3 -2
- package/dist/checkbox.js +5 -4
- package/dist/chrome.cjs +960 -0
- package/dist/chrome.d.cts +93 -0
- package/dist/chrome.d.ts +93 -0
- package/dist/chrome.js +931 -0
- package/dist/drawer.cjs +86 -20
- package/dist/drawer.js +85 -19
- package/dist/expiration-selector.cjs +3 -2
- package/dist/expiration-selector.js +3 -2
- package/dist/greeks-display.cjs +6 -6
- package/dist/greeks-display.js +6 -6
- package/dist/gui.cjs +13 -0
- package/dist/gui.d.cts +2 -0
- package/dist/gui.d.ts +2 -0
- package/dist/gui.js +2 -0
- package/dist/heading.cjs +4 -2
- package/dist/heading.js +4 -2
- package/dist/iam.cjs +216 -0
- package/dist/iam.d.cts +65 -0
- package/dist/iam.d.ts +65 -0
- package/dist/iam.js +211 -0
- package/dist/icons.cjs +13 -0
- package/dist/icons.d.cts +1 -0
- package/dist/icons.d.ts +1 -0
- package/dist/icons.js +2 -0
- package/dist/identity.cjs +430 -0
- package/dist/identity.d.cts +64 -0
- package/dist/identity.d.ts +64 -0
- package/dist/identity.js +408 -0
- package/dist/index.cjs +1610 -836
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +1587 -831
- package/dist/input.cjs +3 -1
- package/dist/input.d.cts +3 -5
- package/dist/input.d.ts +3 -5
- package/dist/input.js +3 -1
- package/dist/lux.config.cjs +23 -2
- package/dist/lux.config.d.cts +28 -10
- package/dist/lux.config.d.ts +28 -10
- package/dist/lux.config.js +23 -2
- package/dist/menu.cjs +4 -4
- package/dist/menu.js +4 -4
- package/dist/next.cjs +132 -0
- package/dist/next.d.cts +32 -0
- package/dist/next.d.ts +32 -0
- package/dist/next.js +129 -0
- package/dist/option-chain.cjs +1 -1
- package/dist/option-chain.js +1 -1
- package/dist/option-position.cjs +2 -1
- package/dist/option-position.js +2 -1
- package/dist/pnl-diagram.cjs +8 -6
- package/dist/pnl-diagram.js +8 -6
- package/dist/progress.cjs +7 -6
- package/dist/progress.js +7 -5
- package/dist/provider.cjs +146 -4
- package/dist/provider.d.cts +15 -3
- package/dist/provider.d.ts +15 -3
- package/dist/provider.js +146 -5
- package/dist/radio.cjs +9 -7
- package/dist/radio.js +10 -7
- package/dist/separator.cjs +3 -1
- package/dist/separator.js +3 -1
- package/dist/slider.cjs +28 -16
- package/dist/slider.js +28 -15
- package/dist/strategy-builder.cjs +2 -1
- package/dist/strategy-builder.js +2 -1
- package/dist/switch.cjs +11 -12
- package/dist/switch.js +12 -12
- package/dist/tag.cjs +34 -36
- package/dist/tag.js +33 -35
- package/dist/textarea.cjs +3 -1
- package/dist/textarea.js +3 -1
- package/dist/tooltip.cjs +28 -30
- package/dist/tooltip.js +27 -29
- package/dist/use-narrow.cjs +46 -0
- package/dist/use-narrow.d.cts +12 -0
- package/dist/use-narrow.d.ts +12 -0
- package/dist/use-narrow.js +20 -0
- package/dist/vite.cjs +109 -0
- package/dist/vite.d.cts +29 -0
- package/dist/vite.d.ts +29 -0
- package/dist/vite.js +107 -0
- package/dist/white-label.cjs +142 -0
- package/dist/white-label.d.cts +59 -0
- package/dist/white-label.d.ts +59 -0
- package/dist/white-label.js +134 -0
- package/package.json +85 -19
- package/src/avatar.tsx +22 -16
- package/src/checkbox.tsx +15 -10
- package/src/chrome.tsx +459 -0
- package/src/drawer.tsx +83 -10
- package/src/engine.ts +130 -0
- package/src/expiration-selector.tsx +3 -2
- package/src/greeks-display.tsx +9 -6
- package/src/gui.ts +17 -0
- package/src/heading.tsx +12 -8
- package/src/iam.ts +170 -0
- package/src/icons.ts +18 -0
- package/src/identity.tsx +318 -0
- package/src/index.ts +44 -1
- package/src/input.tsx +13 -9
- package/src/lux.config.ts +16 -15
- package/src/menu.tsx +4 -4
- package/src/next.ts +89 -0
- package/src/option-chain.tsx +1 -1
- package/src/option-position.tsx +2 -1
- package/src/pnl-diagram.tsx +11 -6
- package/src/progress.tsx +15 -8
- package/src/provider.tsx +48 -7
- package/src/radio.tsx +15 -11
- package/src/separator.tsx +8 -3
- package/src/slider.tsx +35 -19
- package/src/strategy-builder.tsx +3 -2
- package/src/switch.tsx +17 -16
- package/src/textarea.tsx +8 -4
- package/src/tooltip.tsx +15 -24
- package/src/use-narrow.ts +40 -0
- package/src/vite.ts +80 -0
- package/src/white-label.ts +254 -0
- package/tokens.css +613 -319
- package/src/tokens.css +0 -438
package/src/identity.tsx
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
iam,
|
|
7
|
+
iamBase,
|
|
8
|
+
IamError,
|
|
9
|
+
type IamAccount,
|
|
10
|
+
type IamOrg,
|
|
11
|
+
type IamProject,
|
|
12
|
+
} from './iam';
|
|
13
|
+
import { useWhiteLabel } from './provider';
|
|
14
|
+
import type { WhiteLabel } from './white-label';
|
|
15
|
+
|
|
16
|
+
// Who is signed in, and what are they acting as.
|
|
17
|
+
//
|
|
18
|
+
// One context, read by every piece of chrome. Identity and org membership come
|
|
19
|
+
// from IAM (see ./iam); brand, theme, issuer and IAM client come from the host
|
|
20
|
+
// (see ./white-label). Nothing here is configured per deployment.
|
|
21
|
+
//
|
|
22
|
+
// The state is FOUR values, not a boolean, because a switcher renders each of
|
|
23
|
+
// them differently and conflating them is how an app ends up showing an empty
|
|
24
|
+
// account menu to a signed-out visitor:
|
|
25
|
+
//
|
|
26
|
+
// loading — the answer is on its way
|
|
27
|
+
// anonymous — nobody is signed in; offer sign-in, never an empty list
|
|
28
|
+
// ready — signed in; the lists are the real lists (possibly empty)
|
|
29
|
+
// error — IAM could not be read; say so, never render "no organizations"
|
|
30
|
+
|
|
31
|
+
export type IdentityStatus = 'loading' | 'anonymous' | 'ready' | 'error';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How the chrome gets a bearer and moves the user in and out of a session.
|
|
35
|
+
*
|
|
36
|
+
* Credentials are `@hanzo/iam`'s job, not the design system's — so this is the
|
|
37
|
+
* seam, and a surface using the SDK supplies only the two verbs:
|
|
38
|
+
*
|
|
39
|
+
* auth={{ signIn: () => void startLogin(), signOut: () => void logout() }}
|
|
40
|
+
*
|
|
41
|
+
* The bearer itself needs no wiring: the default reads the SDK's own token
|
|
42
|
+
* store, so there is exactly one token and one refresh timer per surface.
|
|
43
|
+
*/
|
|
44
|
+
export interface IdentityAuth {
|
|
45
|
+
/** The IAM access token. Defaults to the `@hanzo/iam` SDK's token store. */
|
|
46
|
+
readonly token?: () => string | null | Promise<string | null>;
|
|
47
|
+
/** Start a sign-in. */
|
|
48
|
+
readonly signIn?: () => void;
|
|
49
|
+
/** End the session. */
|
|
50
|
+
readonly signOut?: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Identity {
|
|
54
|
+
readonly status: IdentityStatus;
|
|
55
|
+
/** The signed-in principal, or null. */
|
|
56
|
+
readonly account: IamAccount | null;
|
|
57
|
+
/** Organizations the principal can act in. */
|
|
58
|
+
readonly orgs: ReadonlyArray<IamOrg>;
|
|
59
|
+
/** The organization the surface is currently scoped to, or null. */
|
|
60
|
+
readonly org: IamOrg | null;
|
|
61
|
+
/** Projects in the current organization. */
|
|
62
|
+
readonly projects: ReadonlyArray<IamProject>;
|
|
63
|
+
/** The project the surface is currently scoped to, or null. */
|
|
64
|
+
readonly project: IamProject | null;
|
|
65
|
+
/** Why the reads failed, when status is 'error'. */
|
|
66
|
+
readonly error: string | null;
|
|
67
|
+
/** Host-resolved brand, theme, issuer and IAM client. */
|
|
68
|
+
readonly whiteLabel: WhiteLabel;
|
|
69
|
+
readonly setOrg: (name: string) => void;
|
|
70
|
+
readonly setProject: (name: string) => void;
|
|
71
|
+
readonly signIn: () => void;
|
|
72
|
+
readonly signOut: () => void;
|
|
73
|
+
readonly reload: () => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- The default auth: the @hanzo/iam SDK's own token store -----------------
|
|
77
|
+
// `@hanzo/iam/browser` keeps its PKCE tokens under `hanzo_iam_*`. Reading that
|
|
78
|
+
// key IS the integration — no second SDK instance, no duplicated refresh
|
|
79
|
+
// timer, and a surface already using @hanzo/iam needs no wiring at all.
|
|
80
|
+
const IAM_TOKEN_KEY = 'hanzo_iam_access_token';
|
|
81
|
+
|
|
82
|
+
function storedToken(): string | null {
|
|
83
|
+
if (typeof window === 'undefined') return null;
|
|
84
|
+
for (const store of [ window.sessionStorage, window.localStorage ]) {
|
|
85
|
+
try {
|
|
86
|
+
const t = store.getItem(IAM_TOKEN_KEY);
|
|
87
|
+
if (t) return t;
|
|
88
|
+
} catch {
|
|
89
|
+
// Storage disabled (private mode / sandboxed frame) — treat as signed out.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function clearStoredTokens(): void {
|
|
96
|
+
if (typeof window === 'undefined') return;
|
|
97
|
+
for (const store of [ window.sessionStorage, window.localStorage ]) {
|
|
98
|
+
try {
|
|
99
|
+
for (const k of [ 'access_token', 'refresh_token', 'id_token', 'expires_at' ]) {
|
|
100
|
+
store.removeItem(`hanzo_iam_${ k }`);
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// Nothing to clear.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const defaultAuth: IdentityAuth = {};
|
|
109
|
+
|
|
110
|
+
// --- Scope persistence ------------------------------------------------------
|
|
111
|
+
// Which org/project the user last acted in, per host, so a reload does not
|
|
112
|
+
// silently move them somewhere else.
|
|
113
|
+
const scopeKey = (host: string, kind: string) => `lux-ui:${ kind }:${ host }`;
|
|
114
|
+
|
|
115
|
+
function readScope(host: string, kind: string): string | null {
|
|
116
|
+
if (typeof window === 'undefined') return null;
|
|
117
|
+
try {
|
|
118
|
+
return window.localStorage.getItem(scopeKey(host, kind));
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeScope(host: string, kind: string, value: string): void {
|
|
125
|
+
if (typeof window === 'undefined') return;
|
|
126
|
+
try {
|
|
127
|
+
window.localStorage.setItem(scopeKey(host, kind), value);
|
|
128
|
+
} catch {
|
|
129
|
+
// Selection stays live in-session; it just will not survive a reload.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const IdentityContext = React.createContext<Identity | null>(null);
|
|
134
|
+
|
|
135
|
+
export interface IdentityProviderProps {
|
|
136
|
+
readonly children: React.ReactNode;
|
|
137
|
+
/** Bearer + sign-in/out. Defaults to the `@hanzo/iam` token store. */
|
|
138
|
+
readonly auth?: IdentityAuth;
|
|
139
|
+
/** Where sign-in goes when `auth.signIn` is not supplied. */
|
|
140
|
+
readonly loginPath?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const IdentityProvider = ({
|
|
144
|
+
children,
|
|
145
|
+
auth = defaultAuth,
|
|
146
|
+
loginPath = '/login',
|
|
147
|
+
}: IdentityProviderProps): React.ReactElement => {
|
|
148
|
+
const whiteLabel = useWhiteLabel();
|
|
149
|
+
const base = React.useMemo(() => iamBase(whiteLabel), [ whiteLabel ]);
|
|
150
|
+
|
|
151
|
+
const [ status, setStatus ] = React.useState<IdentityStatus>('loading');
|
|
152
|
+
const [ account, setAccount ] = React.useState<IamAccount | null>(null);
|
|
153
|
+
const [ orgs, setOrgs ] = React.useState<ReadonlyArray<IamOrg>>([]);
|
|
154
|
+
const [ projects, setProjects ] = React.useState<ReadonlyArray<IamProject>>([]);
|
|
155
|
+
const [ error, setError ] = React.useState<string | null>(null);
|
|
156
|
+
const [ orgName, setOrgName ] = React.useState<string | null>(null);
|
|
157
|
+
const [ projectName, setProjectName ] = React.useState<string | null>(null);
|
|
158
|
+
const [ epoch, setEpoch ] = React.useState(0);
|
|
159
|
+
|
|
160
|
+
// Identity + org membership.
|
|
161
|
+
React.useEffect(() => {
|
|
162
|
+
const ac = new AbortController();
|
|
163
|
+
let live = true;
|
|
164
|
+
void (async () => {
|
|
165
|
+
const token = await (auth.token ?? storedToken)();
|
|
166
|
+
if (!live) return;
|
|
167
|
+
if (!token) {
|
|
168
|
+
setStatus('anonymous');
|
|
169
|
+
setAccount(null);
|
|
170
|
+
setOrgs([]);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const call = { base, token, signal: ac.signal };
|
|
174
|
+
try {
|
|
175
|
+
const me = await iam.account(call);
|
|
176
|
+
if (!live) return;
|
|
177
|
+
if (!me) {
|
|
178
|
+
setStatus('anonymous');
|
|
179
|
+
setAccount(null);
|
|
180
|
+
setOrgs([]);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
setAccount(me);
|
|
184
|
+
// A member who cannot list the tenant table still has their OWN org:
|
|
185
|
+
// fall back to the one the account names rather than showing nothing.
|
|
186
|
+
const rows = await iam.organizations(call).catch((e: unknown) => {
|
|
187
|
+
if (e instanceof IamError && !e.unauthenticated) return [ { owner: 'admin', name: me.owner } ];
|
|
188
|
+
throw e;
|
|
189
|
+
});
|
|
190
|
+
if (!live) return;
|
|
191
|
+
setOrgs(rows);
|
|
192
|
+
setStatus('ready');
|
|
193
|
+
setError(null);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (!live || ac.signal.aborted) return;
|
|
196
|
+
if (e instanceof IamError && e.unauthenticated) {
|
|
197
|
+
setStatus('anonymous');
|
|
198
|
+
setAccount(null);
|
|
199
|
+
setOrgs([]);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
setStatus('error');
|
|
203
|
+
setError(e instanceof Error ? e.message : 'Could not reach IAM.');
|
|
204
|
+
}
|
|
205
|
+
})();
|
|
206
|
+
return () => {
|
|
207
|
+
live = false;
|
|
208
|
+
ac.abort();
|
|
209
|
+
};
|
|
210
|
+
}, [ base, auth, epoch ]);
|
|
211
|
+
|
|
212
|
+
// The org the surface is scoped to: the persisted choice when it is still
|
|
213
|
+
// one the user has, else the account's own org, else the first row.
|
|
214
|
+
const org = React.useMemo<IamOrg | null>(() => {
|
|
215
|
+
if (orgs.length === 0) return null;
|
|
216
|
+
const saved = orgName ?? readScope(whiteLabel.host, 'org');
|
|
217
|
+
return (
|
|
218
|
+
orgs.find((o) => o.name === saved) ??
|
|
219
|
+
orgs.find((o) => o.name === account?.owner) ??
|
|
220
|
+
orgs[0]
|
|
221
|
+
);
|
|
222
|
+
}, [ orgs, orgName, account, whiteLabel.host ]);
|
|
223
|
+
|
|
224
|
+
// Projects follow the org. A project read is allowed to fail without
|
|
225
|
+
// downgrading the whole identity — the org switcher still works.
|
|
226
|
+
React.useEffect(() => {
|
|
227
|
+
if (!org) {
|
|
228
|
+
setProjects([]);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const ac = new AbortController();
|
|
232
|
+
let live = true;
|
|
233
|
+
void (async () => {
|
|
234
|
+
const token = await (auth.token ?? storedToken)();
|
|
235
|
+
if (!live || !token) return;
|
|
236
|
+
const rows = await iam
|
|
237
|
+
.projects({ base, token, signal: ac.signal }, org.name)
|
|
238
|
+
.catch(() => [] as Array<IamProject>);
|
|
239
|
+
if (live) setProjects(rows);
|
|
240
|
+
})();
|
|
241
|
+
return () => {
|
|
242
|
+
live = false;
|
|
243
|
+
ac.abort();
|
|
244
|
+
};
|
|
245
|
+
}, [ base, auth, org, epoch ]);
|
|
246
|
+
|
|
247
|
+
const project = React.useMemo<IamProject | null>(() => {
|
|
248
|
+
if (projects.length === 0) return null;
|
|
249
|
+
const saved = projectName ?? readScope(whiteLabel.host, 'project');
|
|
250
|
+
return (
|
|
251
|
+
projects.find((p) => p.name === saved) ??
|
|
252
|
+
projects.find((p) => p.isDefault) ??
|
|
253
|
+
projects[0]
|
|
254
|
+
);
|
|
255
|
+
}, [ projects, projectName, whiteLabel.host ]);
|
|
256
|
+
|
|
257
|
+
const value = React.useMemo<Identity>(() => ({
|
|
258
|
+
status,
|
|
259
|
+
account,
|
|
260
|
+
orgs,
|
|
261
|
+
org,
|
|
262
|
+
projects,
|
|
263
|
+
project,
|
|
264
|
+
error,
|
|
265
|
+
whiteLabel,
|
|
266
|
+
setOrg: (name: string) => {
|
|
267
|
+
writeScope(whiteLabel.host, 'org', name);
|
|
268
|
+
setOrgName(name);
|
|
269
|
+
setProjectName(null);
|
|
270
|
+
},
|
|
271
|
+
setProject: (name: string) => {
|
|
272
|
+
writeScope(whiteLabel.host, 'project', name);
|
|
273
|
+
setProjectName(name);
|
|
274
|
+
},
|
|
275
|
+
signIn: () => {
|
|
276
|
+
if (auth.signIn) return auth.signIn();
|
|
277
|
+
if (typeof window !== 'undefined') window.location.assign(loginPath);
|
|
278
|
+
},
|
|
279
|
+
signOut: () => {
|
|
280
|
+
if (auth.signOut) return auth.signOut();
|
|
281
|
+
clearStoredTokens();
|
|
282
|
+
if (typeof window !== 'undefined') window.location.assign(loginPath);
|
|
283
|
+
},
|
|
284
|
+
reload: () => setEpoch((n) => n + 1),
|
|
285
|
+
}), [ status, account, orgs, org, projects, project, error, whiteLabel, auth, loginPath ]);
|
|
286
|
+
|
|
287
|
+
return <IdentityContext.Provider value={ value }>{ children }</IdentityContext.Provider>;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The current identity.
|
|
292
|
+
*
|
|
293
|
+
* Outside an `<IdentityProvider>` this reports `anonymous` rather than
|
|
294
|
+
* throwing: chrome that renders before a surface has wired identity should
|
|
295
|
+
* offer sign-in, not crash the page.
|
|
296
|
+
*/
|
|
297
|
+
export function useIdentity(): Identity {
|
|
298
|
+
const ctx = React.useContext(IdentityContext);
|
|
299
|
+
const whiteLabel = useWhiteLabel();
|
|
300
|
+
const fallback = React.useMemo<Identity>(() => ({
|
|
301
|
+
status: 'anonymous',
|
|
302
|
+
account: null,
|
|
303
|
+
orgs: [],
|
|
304
|
+
org: null,
|
|
305
|
+
projects: [],
|
|
306
|
+
project: null,
|
|
307
|
+
error: null,
|
|
308
|
+
whiteLabel,
|
|
309
|
+
setOrg: () => undefined,
|
|
310
|
+
setProject: () => undefined,
|
|
311
|
+
signIn: () => {
|
|
312
|
+
if (typeof window !== 'undefined') window.location.assign('/login');
|
|
313
|
+
},
|
|
314
|
+
signOut: () => undefined,
|
|
315
|
+
reload: () => undefined,
|
|
316
|
+
}), [ whiteLabel ]);
|
|
317
|
+
return ctx ?? fallback;
|
|
318
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
|
-
export { AppProvider } from './provider';
|
|
1
|
+
export { AppProvider, useWhiteLabel } from './provider';
|
|
2
2
|
export { cn } from './utils';
|
|
3
|
+
export { useBelow, useIsNarrow, useIsTouch, NARROW_PX, TOUCH_PX } from './use-narrow';
|
|
4
|
+
|
|
5
|
+
// Identity = IAM, never a per-app copy. `useIdentity()` is the ONE answer to
|
|
6
|
+
// "who is signed in, which orgs, which projects", and the chrome below renders
|
|
7
|
+
// it. See ./iam for the endpoints and ./identity for the four honest states.
|
|
8
|
+
export { iam, iamBase, IamError, IAM_PAGE_SIZE } from './iam';
|
|
9
|
+
export type { IamAccount, IamOrg, IamProject, IamCall } from './iam';
|
|
10
|
+
export { IdentityProvider, useIdentity } from './identity';
|
|
11
|
+
export type { Identity, IdentityAuth, IdentityStatus, IdentityProviderProps } from './identity';
|
|
12
|
+
|
|
13
|
+
// The application chrome — ONE user switcher, ONE org switcher, ONE project
|
|
14
|
+
// switcher, ONE settings menu, ONE nav. All four switchers are the same
|
|
15
|
+
// `Switcher` mechanism over different values.
|
|
16
|
+
export {
|
|
17
|
+
Switcher,
|
|
18
|
+
OrgSwitcher,
|
|
19
|
+
ProjectSwitcher,
|
|
20
|
+
UserMenu,
|
|
21
|
+
SettingsMenu,
|
|
22
|
+
AppNav,
|
|
23
|
+
initials,
|
|
24
|
+
} from './chrome';
|
|
25
|
+
export type {
|
|
26
|
+
ChromeItem,
|
|
27
|
+
SwitcherProps,
|
|
28
|
+
UserMenuProps,
|
|
29
|
+
SettingsMenuProps,
|
|
30
|
+
NavLink,
|
|
31
|
+
AppNavProps,
|
|
32
|
+
} from './chrome';
|
|
33
|
+
|
|
34
|
+
// White-label = a function of the Host header, never of the deployment. Brand,
|
|
35
|
+
// gui theme row, OIDC issuer and IAM client id all come from here.
|
|
36
|
+
// (The engine itself is at '@luxfi/ui/gui'; it is a separate entry because the
|
|
37
|
+
// skin and the engine both define Button, Input, Switch, Select and friends.)
|
|
38
|
+
export {
|
|
39
|
+
resolveWhiteLabel,
|
|
40
|
+
currentHost,
|
|
41
|
+
orgIdentity,
|
|
42
|
+
orgs,
|
|
43
|
+
DEFAULT_ORG,
|
|
44
|
+
} from './white-label';
|
|
45
|
+
export type { Org, OrgIdentity, WhiteLabel } from './white-label';
|
|
3
46
|
|
|
4
47
|
// Lux gui config — the createGui() result (fed to GuiProvider) + the brand
|
|
5
48
|
// child-theme rows. Exposed so app bundlers (Vite/Next SSR extraction) and
|
package/src/input.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Input as GuiInput } from '@hanzo/gui';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
|
|
3
4
|
import { cn } from './utils';
|
|
@@ -35,15 +36,17 @@ export interface InputProps extends Omit<React.ComponentPropsWithRef<'input'>, '
|
|
|
35
36
|
readonly size?: InputSize;
|
|
36
37
|
readonly variant?: InputVariant;
|
|
37
38
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* bridge, RN/Tamagui-style code (e.g. seed-phrase / PIN entry ported from a
|
|
42
|
-
* gui-based surface) passes `onChangeText` and the field silently no-ops.
|
|
39
|
+
* React-Native convention: called with the text value on every change. The gui
|
|
40
|
+
* Input raises it natively; on web we bridge it off `onChange`, so callers
|
|
41
|
+
* using EITHER API get updates on either platform.
|
|
43
42
|
*/
|
|
44
43
|
readonly onChangeText?: (text: string) => void;
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
// gui's Input — a real `<input>` on web, a real TextInput on native (soft
|
|
47
|
+
// keyboard, `returnKeyType`, autofill, IME) — wearing the Lux size/variant
|
|
48
|
+
// classes. `size` stays the Lux scale and never reaches the primitive, whose
|
|
49
|
+
// `size` is a gui SizeToken.
|
|
47
50
|
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
48
51
|
({ size = 'md', variant = 'outline', className, onChange, onChangeText, ...rest }, ref) => {
|
|
49
52
|
const handleChange = React.useCallback(
|
|
@@ -54,11 +57,12 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
54
57
|
[ onChange, onChangeText ],
|
|
55
58
|
);
|
|
56
59
|
return (
|
|
57
|
-
<
|
|
58
|
-
ref={ ref }
|
|
60
|
+
<GuiInput
|
|
61
|
+
ref={ ref as never }
|
|
62
|
+
unstyled
|
|
59
63
|
className={ cn(INPUT_BASE, INPUT_SIZE_CLASSES[size], INPUT_VARIANT_CLASSES[variant], className) }
|
|
60
|
-
{ ...rest }
|
|
61
|
-
onChange={ handleChange }
|
|
64
|
+
{ ...(rest as object) }
|
|
65
|
+
onChange={ handleChange as never }
|
|
62
66
|
/>
|
|
63
67
|
);
|
|
64
68
|
},
|
package/src/lux.config.ts
CHANGED
|
@@ -2,22 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { createGui } from '@hanzo/gui';
|
|
4
4
|
import { defaultConfig } from '@hanzogui/config/v5';
|
|
5
|
+
import { animations } from '@hanzogui/config/v5-css';
|
|
5
6
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
lux: '#7000FF',
|
|
13
|
-
zoo: '#6C5EFB',
|
|
14
|
-
hanzo: '#EA580C',
|
|
15
|
-
pars: '#1C3879',
|
|
16
|
-
} as const;
|
|
17
|
-
|
|
18
|
-
export const PARS_GOLD = '#C8A45C';
|
|
19
|
-
|
|
20
|
-
export type LuxBrand = keyof typeof LUX_BRAND;
|
|
7
|
+
// Branding = VALUE, not place: the brand hex table lives in ./white-label,
|
|
8
|
+
// beside the identity rows it colours, so reading a brand never drags the gui
|
|
9
|
+
// engine along. Re-exported here because this module is where the themes are
|
|
10
|
+
// built from it.
|
|
11
|
+
export { LUX_BRAND, PARS_GOLD, type LuxBrand } from './white-label';
|
|
12
|
+
import { LUX_BRAND, PARS_GOLD } from './white-label';
|
|
21
13
|
|
|
22
14
|
// Shared true-black neutral base: take @hanzogui's resolved dark theme and
|
|
23
15
|
// force a pure-black canvas so every Lux surface sits on the same ground.
|
|
@@ -53,6 +45,7 @@ export const luxThemes = {
|
|
|
53
45
|
dark_zoo: brandTheme(LUX_BRAND.zoo),
|
|
54
46
|
dark_hanzo: brandTheme(LUX_BRAND.hanzo),
|
|
55
47
|
dark_pars: brandTheme(LUX_BRAND.pars, { accentColor: PARS_GOLD }),
|
|
48
|
+
dark_bootnode: brandTheme(LUX_BRAND.bootnode),
|
|
56
49
|
};
|
|
57
50
|
|
|
58
51
|
// One engine, tokens-not-forks: reuse @hanzogui's v5 defaultConfig
|
|
@@ -60,8 +53,16 @@ export const luxThemes = {
|
|
|
60
53
|
// over it. The lone settings override — `onlyAllowShorthands: false` — keeps the
|
|
61
54
|
// harvested exchange primitives (which author with longhand style props like
|
|
62
55
|
// `borderRadius` / `paddingHorizontal`) valid; v5 defaults to shorthands-only.
|
|
56
|
+
// `@hanzogui/config/v5` ships WITHOUT an animation driver — "users must provide
|
|
57
|
+
// their own". Sheet is not merely unanimated without one, it throws
|
|
58
|
+
// (`Sheet requires an animation driver to be set`), and every `transition` prop
|
|
59
|
+
// on every other primitive is inert. Feeding the CSS driver here is what earns
|
|
60
|
+
// the motion — one wiring for the whole fleet, not one per app.
|
|
61
|
+
// Native surfaces can swap `@hanzogui/config/v5-css` for `/v5-rn` without
|
|
62
|
+
// touching a component: the transition token names are identical.
|
|
63
63
|
export const config = createGui({
|
|
64
64
|
...defaultConfig,
|
|
65
|
+
animations,
|
|
65
66
|
settings: {
|
|
66
67
|
...defaultConfig.settings,
|
|
67
68
|
onlyAllowShorthands: false,
|
package/src/menu.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
|
4
4
|
import * as React from 'react';
|
|
5
|
-
import {
|
|
5
|
+
import { Check, ChevronRight } from 'lucide-react';
|
|
6
6
|
|
|
7
7
|
import { cn } from './utils';
|
|
8
8
|
|
|
@@ -356,7 +356,7 @@ export const MenuCheckboxItem = React.forwardRef<
|
|
|
356
356
|
>
|
|
357
357
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
358
358
|
<DropdownMenu.ItemIndicator>
|
|
359
|
-
<
|
|
359
|
+
<Check className="h-4 w-4"/>
|
|
360
360
|
</DropdownMenu.ItemIndicator>
|
|
361
361
|
</span>
|
|
362
362
|
{ children }
|
|
@@ -414,7 +414,7 @@ export const MenuRadioItem = React.forwardRef<
|
|
|
414
414
|
>
|
|
415
415
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
416
416
|
<DropdownMenu.ItemIndicator>
|
|
417
|
-
<
|
|
417
|
+
<Check className="h-4 w-4"/>
|
|
418
418
|
</DropdownMenu.ItemIndicator>
|
|
419
419
|
</span>
|
|
420
420
|
<span>{ children }</span>
|
|
@@ -464,7 +464,7 @@ export const MenuTriggerItem = React.forwardRef<
|
|
|
464
464
|
>
|
|
465
465
|
{ startIcon }
|
|
466
466
|
<span className="flex-1">{ children }</span>
|
|
467
|
-
<
|
|
467
|
+
<ChevronRight className="h-4 w-4"/>
|
|
468
468
|
</DropdownMenu.SubTrigger>
|
|
469
469
|
</DropdownMenu.Sub>
|
|
470
470
|
);
|
package/src/next.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// The bundler wiring the gui engine needs on Next, as ONE wrapper.
|
|
2
|
+
//
|
|
3
|
+
// The Next twin of `@luxfi/ui/vite`. @hanzo/gui is authored against React
|
|
4
|
+
// Native primitives, so a Next build has to transpile the engine (it ships
|
|
5
|
+
// untranspiled ESM), alias `react-native` → `react-native-web` in BOTH
|
|
6
|
+
// bundlers Next now uses, and define the platform flags the engine reads at
|
|
7
|
+
// module scope.
|
|
8
|
+
//
|
|
9
|
+
// const { withLuxUi } = require('@luxfi/ui/next')
|
|
10
|
+
// module.exports = withLuxUi({ /* your config */ })
|
|
11
|
+
//
|
|
12
|
+
// Every Next surface needed the identical incantation and every one that
|
|
13
|
+
// hand-rolled it transpiled a DIFFERENT subset of the engine — which is how
|
|
14
|
+
// one app renders a Sheet and the next throws `createContext of undefined` on
|
|
15
|
+
// the same component. The list below is the umbrella's own export list, in one
|
|
16
|
+
// place, so it cannot drift per app.
|
|
17
|
+
//
|
|
18
|
+
// Node-only (a build-time module) and dependency-free: it takes and returns a
|
|
19
|
+
// plain object, so it does not force `next` into anyone's type graph.
|
|
20
|
+
|
|
21
|
+
import { guiAliases, GUI_DEFINES, GUI_PACKAGES } from './engine';
|
|
22
|
+
|
|
23
|
+
interface WebpackConfig {
|
|
24
|
+
resolve?: { alias?: Record<string, unknown> };
|
|
25
|
+
plugins?: Array<unknown>;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The structural slice of a Next config this wrapper touches. */
|
|
30
|
+
export interface LuxUiNextConfig {
|
|
31
|
+
/** App root, for resolving the engine's single copy. Defaults to `cwd`. */
|
|
32
|
+
dir?: string;
|
|
33
|
+
transpilePackages?: Array<string>;
|
|
34
|
+
turbopack?: { resolveAlias?: Record<string, string>; [key: string]: unknown };
|
|
35
|
+
env?: Record<string, string>;
|
|
36
|
+
webpack?: (config: WebpackConfig, context: unknown) => WebpackConfig;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Wrap a Next config so `@luxfi/ui` and the gui engine build.
|
|
42
|
+
*
|
|
43
|
+
* Additive: an existing `transpilePackages`, `turbopack.resolveAlias`,
|
|
44
|
+
* `env` and `webpack()` are all preserved and extended, never replaced.
|
|
45
|
+
*/
|
|
46
|
+
export function withLuxUi(config: LuxUiNextConfig = {}): LuxUiNextConfig {
|
|
47
|
+
const userWebpack = config.webpack;
|
|
48
|
+
// ONE physical copy of every engine package. Webpack has no `resolve.dedupe`,
|
|
49
|
+
// so the equivalent is an exact-match alias per package, resolved from the app
|
|
50
|
+
// root — see ./engine for why two copies is a crash and not a size problem.
|
|
51
|
+
const engine = guiAliases(config.dir ?? process.cwd());
|
|
52
|
+
// `dir` is OURS, not Next's — leaving it on the returned object makes Next
|
|
53
|
+
// print `Unrecognized key(s) in object: 'dir'` on every boot of every surface
|
|
54
|
+
// that passes it. Consume it here.
|
|
55
|
+
const { dir: _dir, ...rest } = config;
|
|
56
|
+
return {
|
|
57
|
+
...rest,
|
|
58
|
+
transpilePackages: [ ...new Set([ ...(config.transpilePackages ?? []), ...GUI_PACKAGES ]) ],
|
|
59
|
+
turbopack: {
|
|
60
|
+
...config.turbopack,
|
|
61
|
+
resolveAlias: {
|
|
62
|
+
'react-native': 'react-native-web',
|
|
63
|
+
...config.turbopack?.resolveAlias,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
// Next inlines `env` into the client bundle, which is how the engine's
|
|
67
|
+
// module-scope platform checks get their answer under Turbopack too.
|
|
68
|
+
env: { EXPO_OS: 'web', IS_WEB: 'true', ...config.env },
|
|
69
|
+
webpack(webpackConfig: WebpackConfig, context: unknown) {
|
|
70
|
+
const next = userWebpack ? userWebpack(webpackConfig, context) : webpackConfig;
|
|
71
|
+
next.resolve = {
|
|
72
|
+
...next.resolve,
|
|
73
|
+
// The app's own aliases win — an app pinning a package on purpose is not
|
|
74
|
+
// something the umbrella should override.
|
|
75
|
+
alias: { 'react-native$': 'react-native-web', ...engine, ...next.resolve?.alias },
|
|
76
|
+
};
|
|
77
|
+
// `context` is Next's `{ webpack }` bag — use ITS webpack instance so the
|
|
78
|
+
// DefinePlugin is the one running the build.
|
|
79
|
+
const webpackModule = (context as { webpack?: { DefinePlugin?: new (d: Record<string, string>) => unknown } })?.webpack;
|
|
80
|
+
if (webpackModule?.DefinePlugin) {
|
|
81
|
+
next.plugins = [ ...(next.plugins ?? []), new webpackModule.DefinePlugin({ ...GUI_DEFINES }) ];
|
|
82
|
+
}
|
|
83
|
+
return next;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { GUI_PACKAGES };
|
|
89
|
+
|
package/src/option-chain.tsx
CHANGED
|
@@ -226,7 +226,7 @@ export const OptionChain = React.forwardRef<HTMLDivElement, OptionChainProps>(
|
|
|
226
226
|
<td
|
|
227
227
|
className={cn(
|
|
228
228
|
'px-2 py-1 text-center font-mono tabular-nums text-xs font-semibold',
|
|
229
|
-
isATM ? 'text-
|
|
229
|
+
isATM ? 'text-[var(--foreground)] bg-[var(--secondary)]' : 'text-[var(--muted-foreground)]',
|
|
230
230
|
)}
|
|
231
231
|
>
|
|
232
232
|
{fmt(row.strike)}
|
package/src/option-position.tsx
CHANGED
|
@@ -77,7 +77,8 @@ export const OptionPositionCard = React.forwardRef<HTMLDivElement, OptionPositio
|
|
|
77
77
|
{/* Symbol badge */}
|
|
78
78
|
<span className={cn(
|
|
79
79
|
'inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold',
|
|
80
|
-
|
|
80
|
+
// Call vs put is a rank difference, not a hue difference.
|
|
81
|
+
isCall ? 'bg-[var(--secondary)] text-[var(--foreground)]' : 'bg-[var(--muted)] text-[var(--muted-foreground)]',
|
|
81
82
|
)}>
|
|
82
83
|
{position.underlying}
|
|
83
84
|
<span className="font-mono tabular-nums">{position.strike}</span>
|
package/src/pnl-diagram.tsx
CHANGED
|
@@ -66,7 +66,12 @@ function buildPath(points: Array<{ x: number; y: number }>, vb: ViewBox, width:
|
|
|
66
66
|
// Leg colors
|
|
67
67
|
// ---------------------------------------------------------------------------
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
// Legs are told apart by position on the achromatic chart ramp, not by hue —
|
|
70
|
+
// and they resolve through tokens.css, so a host reskin moves them too.
|
|
71
|
+
const LEG_COLORS = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)'];
|
|
72
|
+
// Breakeven markers ride the same ramp; the ring is the page ground.
|
|
73
|
+
const BREAKEVEN_COLOR = 'var(--chart-1)';
|
|
74
|
+
const BREAKEVEN_RING = 'var(--background)';
|
|
70
75
|
|
|
71
76
|
// ---------------------------------------------------------------------------
|
|
72
77
|
// PnlDiagram
|
|
@@ -323,10 +328,10 @@ export const PnlDiagram = React.forwardRef<HTMLDivElement, PnlDiagramProps>(
|
|
|
323
328
|
{/* Breakeven markers */}
|
|
324
329
|
{breakevens.map((be, i) => (
|
|
325
330
|
<g key={`be-${i}`}>
|
|
326
|
-
<circle cx={scaleX(be)} cy={zeroY} r="3" fill=
|
|
331
|
+
<circle cx={scaleX(be)} cy={zeroY} r="3" fill={BREAKEVEN_COLOR} stroke={BREAKEVEN_RING} strokeWidth="1" />
|
|
327
332
|
<text
|
|
328
333
|
x={scaleX(be)} y={PADDING.top + chartH + 14}
|
|
329
|
-
fill=
|
|
334
|
+
fill={BREAKEVEN_COLOR} fontSize="8" fontFamily="monospace" textAnchor="middle"
|
|
330
335
|
>
|
|
331
336
|
{be.toFixed(1)}
|
|
332
337
|
</text>
|
|
@@ -366,7 +371,7 @@ export const PnlDiagram = React.forwardRef<HTMLDivElement, PnlDiagramProps>(
|
|
|
366
371
|
cy={scaleY(hoverPnl)}
|
|
367
372
|
r="3"
|
|
368
373
|
fill={hoverPnl >= 0 ? '#22c55e' : '#ef4444'}
|
|
369
|
-
stroke=
|
|
374
|
+
stroke={BREAKEVEN_RING}
|
|
370
375
|
strokeWidth="1"
|
|
371
376
|
/>
|
|
372
377
|
{/* Tooltip */}
|
|
@@ -376,7 +381,7 @@ export const PnlDiagram = React.forwardRef<HTMLDivElement, PnlDiagramProps>(
|
|
|
376
381
|
width="90"
|
|
377
382
|
height="28"
|
|
378
383
|
rx="3"
|
|
379
|
-
fill=
|
|
384
|
+
fill={BREAKEVEN_RING}
|
|
380
385
|
stroke="#3f3f46"
|
|
381
386
|
strokeWidth="1"
|
|
382
387
|
/>
|
|
@@ -445,7 +450,7 @@ export const PnlDiagram = React.forwardRef<HTMLDivElement, PnlDiagramProps>(
|
|
|
445
450
|
))}
|
|
446
451
|
{breakevens.length > 0 && (
|
|
447
452
|
<div className="flex items-center gap-1.5">
|
|
448
|
-
<div className="h-1.5 w-1.5 rounded-full
|
|
453
|
+
<div className="h-1.5 w-1.5 rounded-full" style={{ background: BREAKEVEN_COLOR }} />
|
|
449
454
|
<span className="text-[10px] text-zinc-500">
|
|
450
455
|
BE: {breakevens.map((b) => b.toFixed(1)).join(', ')}
|
|
451
456
|
</span>
|