@luxfi/ui 7.4.0 → 7.4.2

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.
Files changed (130) hide show
  1. package/dist/avatar.cjs +7 -6
  2. package/dist/avatar.js +7 -5
  3. package/dist/badge.cjs +30 -32
  4. package/dist/badge.js +30 -32
  5. package/dist/bank.d.cts +3 -3
  6. package/dist/bank.d.ts +3 -3
  7. package/dist/checkbox.cjs +4 -4
  8. package/dist/checkbox.d.cts +3 -2
  9. package/dist/checkbox.d.ts +3 -2
  10. package/dist/checkbox.js +5 -4
  11. package/dist/chrome.cjs +950 -0
  12. package/dist/chrome.d.cts +93 -0
  13. package/dist/chrome.d.ts +93 -0
  14. package/dist/chrome.js +921 -0
  15. package/dist/drawer.cjs +86 -20
  16. package/dist/drawer.js +85 -19
  17. package/dist/expiration-selector.cjs +3 -2
  18. package/dist/expiration-selector.js +3 -2
  19. package/dist/greeks-display.cjs +6 -6
  20. package/dist/greeks-display.js +6 -6
  21. package/dist/gui.cjs +13 -0
  22. package/dist/gui.d.cts +2 -0
  23. package/dist/gui.d.ts +2 -0
  24. package/dist/gui.js +2 -0
  25. package/dist/heading.cjs +4 -2
  26. package/dist/heading.js +4 -2
  27. package/dist/iam.cjs +206 -0
  28. package/dist/iam.d.cts +65 -0
  29. package/dist/iam.d.ts +65 -0
  30. package/dist/iam.js +201 -0
  31. package/dist/icons.cjs +13 -0
  32. package/dist/icons.d.cts +1 -0
  33. package/dist/icons.d.ts +1 -0
  34. package/dist/icons.js +2 -0
  35. package/dist/identity.cjs +420 -0
  36. package/dist/identity.d.cts +64 -0
  37. package/dist/identity.d.ts +64 -0
  38. package/dist/identity.js +398 -0
  39. package/dist/index.cjs +1597 -833
  40. package/dist/index.d.cts +7 -3
  41. package/dist/index.d.ts +7 -3
  42. package/dist/index.js +1574 -828
  43. package/dist/input.cjs +3 -1
  44. package/dist/input.d.cts +3 -5
  45. package/dist/input.d.ts +3 -5
  46. package/dist/input.js +3 -1
  47. package/dist/lux.config.cjs +23 -2
  48. package/dist/lux.config.d.cts +28 -10
  49. package/dist/lux.config.d.ts +28 -10
  50. package/dist/lux.config.js +23 -2
  51. package/dist/menu.cjs +4 -4
  52. package/dist/menu.js +4 -4
  53. package/dist/next.cjs +99 -0
  54. package/dist/next.d.cts +29 -0
  55. package/dist/next.d.ts +29 -0
  56. package/dist/next.js +96 -0
  57. package/dist/option-chain.cjs +1 -1
  58. package/dist/option-chain.js +1 -1
  59. package/dist/option-position.cjs +2 -1
  60. package/dist/option-position.js +2 -1
  61. package/dist/pnl-diagram.cjs +8 -6
  62. package/dist/pnl-diagram.js +8 -6
  63. package/dist/progress.cjs +7 -6
  64. package/dist/progress.js +7 -5
  65. package/dist/provider.cjs +136 -4
  66. package/dist/provider.d.cts +15 -3
  67. package/dist/provider.d.ts +15 -3
  68. package/dist/provider.js +136 -5
  69. package/dist/radio.cjs +9 -7
  70. package/dist/radio.js +10 -7
  71. package/dist/separator.cjs +3 -1
  72. package/dist/separator.js +3 -1
  73. package/dist/slider.cjs +28 -16
  74. package/dist/slider.js +28 -15
  75. package/dist/strategy-builder.cjs +2 -1
  76. package/dist/strategy-builder.js +2 -1
  77. package/dist/switch.cjs +11 -12
  78. package/dist/switch.js +12 -12
  79. package/dist/tag.cjs +31 -33
  80. package/dist/tag.js +30 -32
  81. package/dist/textarea.cjs +3 -1
  82. package/dist/textarea.js +3 -1
  83. package/dist/tooltip.cjs +25 -27
  84. package/dist/tooltip.js +24 -26
  85. package/dist/use-narrow.cjs +46 -0
  86. package/dist/use-narrow.d.cts +12 -0
  87. package/dist/use-narrow.d.ts +12 -0
  88. package/dist/use-narrow.js +20 -0
  89. package/dist/vite.cjs +40 -0
  90. package/dist/vite.d.cts +29 -0
  91. package/dist/vite.d.ts +29 -0
  92. package/dist/vite.js +38 -0
  93. package/dist/white-label.cjs +132 -0
  94. package/dist/white-label.d.cts +59 -0
  95. package/dist/white-label.d.ts +59 -0
  96. package/dist/white-label.js +124 -0
  97. package/package.json +85 -19
  98. package/src/avatar.tsx +22 -16
  99. package/src/checkbox.tsx +15 -10
  100. package/src/chrome.tsx +459 -0
  101. package/src/drawer.tsx +83 -10
  102. package/src/expiration-selector.tsx +3 -2
  103. package/src/greeks-display.tsx +9 -6
  104. package/src/gui.ts +17 -0
  105. package/src/heading.tsx +12 -8
  106. package/src/iam.ts +170 -0
  107. package/src/icons.ts +18 -0
  108. package/src/identity.tsx +318 -0
  109. package/src/index.ts +44 -1
  110. package/src/input.tsx +13 -9
  111. package/src/lux.config.ts +16 -15
  112. package/src/menu.tsx +4 -4
  113. package/src/next.ts +142 -0
  114. package/src/option-chain.tsx +1 -1
  115. package/src/option-position.tsx +2 -1
  116. package/src/pnl-diagram.tsx +11 -6
  117. package/src/progress.tsx +15 -8
  118. package/src/provider.tsx +48 -7
  119. package/src/radio.tsx +15 -11
  120. package/src/separator.tsx +8 -3
  121. package/src/slider.tsx +35 -19
  122. package/src/strategy-builder.tsx +3 -2
  123. package/src/switch.tsx +17 -16
  124. package/src/textarea.tsx +8 -4
  125. package/src/tooltip.tsx +2 -21
  126. package/src/use-narrow.ts +40 -0
  127. package/src/vite.ts +78 -0
  128. package/src/white-label.ts +240 -0
  129. package/tokens.css +544 -319
  130. package/src/tokens.css +0 -438
@@ -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
- * Tamagui / React-Native convention: called with the text value on every
39
- * change. Bridged to the native `onChange`, so callers using EITHER the web
40
- * `onChange(e)` API OR the `onChangeText(text)` API get updates. Without this
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
- <input
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
- // Branding = VALUE, not place.
8
- // The ONE spot brand hex is allowed to live (project rule: never hardcode hex
9
- // in components). Accents sourced from the work-board brand palette.
10
- // ---------------------------------------------------------------------------
11
- export const LUX_BRAND = {
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 { LuCheck, LuChevronRight } from 'react-icons/lu';
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
- <LuCheck className="h-4 w-4"/>
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
- <LuCheck className="h-4 w-4"/>
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
- <LuChevronRight className="h-4 w-4"/>
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,142 @@
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
+ /** Every package `@hanzo/gui` re-exports — the engine, in full. */
22
+ const GUI_PACKAGES = [
23
+ '@hanzo/gui',
24
+ '@hanzogui/accordion',
25
+ '@hanzogui/adapt',
26
+ '@hanzogui/alert-dialog',
27
+ '@hanzogui/animate',
28
+ '@hanzogui/animate-presence',
29
+ '@hanzogui/avatar',
30
+ '@hanzogui/button',
31
+ '@hanzogui/card',
32
+ '@hanzogui/checkbox',
33
+ '@hanzogui/collapsible',
34
+ '@hanzogui/component-helpers',
35
+ '@hanzogui/compose-refs',
36
+ '@hanzogui/config',
37
+ '@hanzogui/context-menu',
38
+ '@hanzogui/core',
39
+ '@hanzogui/create-context',
40
+ '@hanzogui/create-menu',
41
+ '@hanzogui/dialog',
42
+ '@hanzogui/element',
43
+ '@hanzogui/elements',
44
+ '@hanzogui/font-size',
45
+ '@hanzogui/form',
46
+ '@hanzogui/group',
47
+ '@hanzogui/image',
48
+ '@hanzogui/input',
49
+ '@hanzogui/label',
50
+ '@hanzogui/list-item',
51
+ '@hanzogui/menu',
52
+ '@hanzogui/popover',
53
+ '@hanzogui/popper',
54
+ '@hanzogui/portal',
55
+ '@hanzogui/progress',
56
+ '@hanzogui/radio-group',
57
+ '@hanzogui/react-native-media-driver',
58
+ '@hanzogui/scroll-view',
59
+ '@hanzogui/select',
60
+ '@hanzogui/separator',
61
+ '@hanzogui/shapes',
62
+ '@hanzogui/sheet',
63
+ '@hanzogui/slider',
64
+ '@hanzogui/spacer',
65
+ '@hanzogui/spinner',
66
+ '@hanzogui/stacks',
67
+ '@hanzogui/switch',
68
+ '@hanzogui/tabs',
69
+ '@hanzogui/text',
70
+ '@hanzogui/theme',
71
+ '@hanzogui/toast',
72
+ '@hanzogui/toggle-group',
73
+ '@hanzogui/tooltip',
74
+ '@hanzogui/use-controllable-state',
75
+ '@hanzogui/use-debounce',
76
+ '@hanzogui/use-force-update',
77
+ '@hanzogui/use-window-dimensions',
78
+ '@hanzogui/visually-hidden',
79
+ '@hanzogui/web',
80
+ '@luxfi/ui',
81
+ ];
82
+
83
+ /** The engine branches on these at module scope. */
84
+ const DEFINES: Record<string, string> = {
85
+ 'process.env.EXPO_OS': JSON.stringify('web'),
86
+ 'process.env.IS_WEB': JSON.stringify('true'),
87
+ };
88
+
89
+ interface WebpackConfig {
90
+ resolve?: { alias?: Record<string, unknown> };
91
+ plugins?: Array<unknown>;
92
+ [key: string]: unknown;
93
+ }
94
+
95
+ /** The structural slice of a Next config this wrapper touches. */
96
+ export interface LuxUiNextConfig {
97
+ transpilePackages?: Array<string>;
98
+ turbopack?: { resolveAlias?: Record<string, string>; [key: string]: unknown };
99
+ env?: Record<string, string>;
100
+ webpack?: (config: WebpackConfig, context: unknown) => WebpackConfig;
101
+ [key: string]: unknown;
102
+ }
103
+
104
+ /**
105
+ * Wrap a Next config so `@luxfi/ui` and the gui engine build.
106
+ *
107
+ * Additive: an existing `transpilePackages`, `turbopack.resolveAlias`,
108
+ * `env` and `webpack()` are all preserved and extended, never replaced.
109
+ */
110
+ export function withLuxUi(config: LuxUiNextConfig = {}): LuxUiNextConfig {
111
+ const userWebpack = config.webpack;
112
+ return {
113
+ ...config,
114
+ transpilePackages: [ ...new Set([ ...(config.transpilePackages ?? []), ...GUI_PACKAGES ]) ],
115
+ turbopack: {
116
+ ...config.turbopack,
117
+ resolveAlias: {
118
+ 'react-native': 'react-native-web',
119
+ ...config.turbopack?.resolveAlias,
120
+ },
121
+ },
122
+ // Next inlines `env` into the client bundle, which is how the engine's
123
+ // module-scope platform checks get their answer under Turbopack too.
124
+ env: { EXPO_OS: 'web', IS_WEB: 'true', ...config.env },
125
+ webpack(webpackConfig: WebpackConfig, context: unknown) {
126
+ const next = userWebpack ? userWebpack(webpackConfig, context) : webpackConfig;
127
+ next.resolve = {
128
+ ...next.resolve,
129
+ alias: { ...next.resolve?.alias, 'react-native$': 'react-native-web' },
130
+ };
131
+ // `context` is Next's `{ webpack }` bag — use ITS webpack instance so the
132
+ // DefinePlugin is the one running the build.
133
+ const webpackModule = (context as { webpack?: { DefinePlugin?: new (d: Record<string, string>) => unknown } })?.webpack;
134
+ if (webpackModule?.DefinePlugin) {
135
+ next.plugins = [ ...(next.plugins ?? []), new webpackModule.DefinePlugin(DEFINES) ];
136
+ }
137
+ return next;
138
+ },
139
+ };
140
+ }
141
+
142
+ export { GUI_PACKAGES };
@@ -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-amber-400 bg-zinc-800/80' : 'text-zinc-300',
229
+ isATM ? 'text-[var(--foreground)] bg-[var(--secondary)]' : 'text-[var(--muted-foreground)]',
230
230
  )}
231
231
  >
232
232
  {fmt(row.strike)}
@@ -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
- isCall ? 'bg-blue-900/40 text-blue-400' : 'bg-orange-900/40 text-orange-400',
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>