@luxfi/ui 7.4.6 → 7.4.8
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/alert.cjs +3 -7
- package/dist/alert.js +3 -7
- package/dist/auth.cjs +481 -0
- package/dist/auth.d.cts +80 -0
- package/dist/auth.d.ts +80 -0
- package/dist/auth.js +456 -0
- package/dist/badge.cjs +4 -8
- package/dist/badge.js +4 -8
- package/dist/button.cjs +3 -7
- package/dist/button.js +3 -7
- package/dist/collapsible.cjs +3 -7
- package/dist/collapsible.js +3 -7
- package/dist/icon-button.cjs +3 -7
- package/dist/icon-button.js +3 -7
- package/dist/icons.cjs +0 -1
- package/dist/icons.js +0 -1
- package/dist/image.cjs +3 -7
- package/dist/image.js +3 -7
- package/dist/index.cjs +9 -8
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -9
- package/dist/link.cjs +3 -7
- package/dist/link.js +3 -7
- package/dist/next.cjs +20 -1
- package/dist/next.js +20 -1
- package/dist/select.cjs +3 -7
- package/dist/select.js +3 -7
- package/dist/skeleton.cjs +8 -7
- package/dist/skeleton.d.cts +17 -1
- package/dist/skeleton.d.ts +17 -1
- package/dist/skeleton.js +8 -8
- package/dist/table.cjs +3 -7
- package/dist/table.js +3 -7
- package/dist/tag.cjs +4 -8
- package/dist/tag.js +4 -8
- package/dist/tooltip.cjs +1 -1
- package/dist/tooltip.js +1 -1
- package/dist/vite.cjs +4 -3
- package/dist/vite.js +4 -3
- package/package.json +12 -2
- package/src/auth.tsx +198 -0
- package/src/engine.ts +40 -3
- package/src/index.ts +1 -1
- package/src/next.ts +23 -0
- package/src/skeleton.tsx +36 -7
- package/src/tooltip.tsx +13 -5
- package/src/vite.ts +4 -9
- package/tokens.css +217 -0
package/dist/skeleton.js
CHANGED
|
@@ -122,11 +122,7 @@ function stripShims(props) {
|
|
|
122
122
|
for (const k of SHIM_PROPS) delete out[k];
|
|
123
123
|
return out;
|
|
124
124
|
}
|
|
125
|
-
var SKELETON_CLASSES =
|
|
126
|
-
"animate-skeleton-shimmer rounded-sm",
|
|
127
|
-
"bg-[linear-gradient(90deg,var(--color-skeleton-start)_0%,var(--color-skeleton-end)_50%,var(--color-skeleton-start)_100%)]",
|
|
128
|
-
"bg-[length:200%_100%]"
|
|
129
|
-
].join(" ");
|
|
125
|
+
var SKELETON_CLASSES = "lux-skeleton rounded-sm";
|
|
130
126
|
var HIDE_BELOW_MAP = { lg: "lg:hidden", md: "md:hidden", sm: "sm:hidden" };
|
|
131
127
|
var Skeleton = React.forwardRef(
|
|
132
128
|
function Skeleton2(props, ref) {
|
|
@@ -146,7 +142,7 @@ var Skeleton = React.forwardRef(
|
|
|
146
142
|
{
|
|
147
143
|
ref,
|
|
148
144
|
"data-loading": true,
|
|
149
|
-
className: cn(SKELETON_CLASSES, "
|
|
145
|
+
className: cn(SKELETON_CLASSES, "[&_*]:invisible", cls),
|
|
150
146
|
style: mergedStyle,
|
|
151
147
|
...htmlRest,
|
|
152
148
|
children
|
|
@@ -158,7 +154,7 @@ var Skeleton = React.forwardRef(
|
|
|
158
154
|
{
|
|
159
155
|
ref,
|
|
160
156
|
"data-loading": true,
|
|
161
|
-
className: cn(SKELETON_CLASSES, children ? "
|
|
157
|
+
className: cn(SKELETON_CLASSES, children ? "[&_*]:invisible" : "min-h-5", cls),
|
|
162
158
|
style: mergedStyle,
|
|
163
159
|
...htmlRest,
|
|
164
160
|
children
|
|
@@ -195,5 +191,9 @@ var SkeletonText = React.forwardRef(
|
|
|
195
191
|
)) });
|
|
196
192
|
}
|
|
197
193
|
);
|
|
194
|
+
function LoadingBar({ loading = true }) {
|
|
195
|
+
if (!loading) return null;
|
|
196
|
+
return /* @__PURE__ */ jsx("div", { className: "lux-loading", role: "progressbar", "aria-busy": "true", "aria-label": "Loading" });
|
|
197
|
+
}
|
|
198
198
|
|
|
199
|
-
export { Skeleton, SkeletonCircle, SkeletonText };
|
|
199
|
+
export { LoadingBar, Skeleton, SkeletonCircle, SkeletonText };
|
package/dist/table.cjs
CHANGED
|
@@ -145,11 +145,7 @@ function stripShims(props) {
|
|
|
145
145
|
for (const k of SHIM_PROPS) delete out[k];
|
|
146
146
|
return out;
|
|
147
147
|
}
|
|
148
|
-
var SKELETON_CLASSES =
|
|
149
|
-
"animate-skeleton-shimmer rounded-sm",
|
|
150
|
-
"bg-[linear-gradient(90deg,var(--color-skeleton-start)_0%,var(--color-skeleton-end)_50%,var(--color-skeleton-start)_100%)]",
|
|
151
|
-
"bg-[length:200%_100%]"
|
|
152
|
-
].join(" ");
|
|
148
|
+
var SKELETON_CLASSES = "lux-skeleton rounded-sm";
|
|
153
149
|
var HIDE_BELOW_MAP = { lg: "lg:hidden", md: "md:hidden", sm: "sm:hidden" };
|
|
154
150
|
var Skeleton = React3__namespace.forwardRef(
|
|
155
151
|
function Skeleton2(props, ref) {
|
|
@@ -169,7 +165,7 @@ var Skeleton = React3__namespace.forwardRef(
|
|
|
169
165
|
{
|
|
170
166
|
ref,
|
|
171
167
|
"data-loading": true,
|
|
172
|
-
className: cn(SKELETON_CLASSES, "
|
|
168
|
+
className: cn(SKELETON_CLASSES, "[&_*]:invisible", cls),
|
|
173
169
|
style: mergedStyle,
|
|
174
170
|
...htmlRest,
|
|
175
171
|
children
|
|
@@ -181,7 +177,7 @@ var Skeleton = React3__namespace.forwardRef(
|
|
|
181
177
|
{
|
|
182
178
|
ref,
|
|
183
179
|
"data-loading": true,
|
|
184
|
-
className: cn(SKELETON_CLASSES, children ? "
|
|
180
|
+
className: cn(SKELETON_CLASSES, children ? "[&_*]:invisible" : "min-h-5", cls),
|
|
185
181
|
style: mergedStyle,
|
|
186
182
|
...htmlRest,
|
|
187
183
|
children
|
package/dist/table.js
CHANGED
|
@@ -124,11 +124,7 @@ function stripShims(props) {
|
|
|
124
124
|
for (const k of SHIM_PROPS) delete out[k];
|
|
125
125
|
return out;
|
|
126
126
|
}
|
|
127
|
-
var SKELETON_CLASSES =
|
|
128
|
-
"animate-skeleton-shimmer rounded-sm",
|
|
129
|
-
"bg-[linear-gradient(90deg,var(--color-skeleton-start)_0%,var(--color-skeleton-end)_50%,var(--color-skeleton-start)_100%)]",
|
|
130
|
-
"bg-[length:200%_100%]"
|
|
131
|
-
].join(" ");
|
|
127
|
+
var SKELETON_CLASSES = "lux-skeleton rounded-sm";
|
|
132
128
|
var HIDE_BELOW_MAP = { lg: "lg:hidden", md: "md:hidden", sm: "sm:hidden" };
|
|
133
129
|
var Skeleton = React3.forwardRef(
|
|
134
130
|
function Skeleton2(props, ref) {
|
|
@@ -148,7 +144,7 @@ var Skeleton = React3.forwardRef(
|
|
|
148
144
|
{
|
|
149
145
|
ref,
|
|
150
146
|
"data-loading": true,
|
|
151
|
-
className: cn(SKELETON_CLASSES, "
|
|
147
|
+
className: cn(SKELETON_CLASSES, "[&_*]:invisible", cls),
|
|
152
148
|
style: mergedStyle,
|
|
153
149
|
...htmlRest,
|
|
154
150
|
children
|
|
@@ -160,7 +156,7 @@ var Skeleton = React3.forwardRef(
|
|
|
160
156
|
{
|
|
161
157
|
ref,
|
|
162
158
|
"data-loading": true,
|
|
163
|
-
className: cn(SKELETON_CLASSES, children ? "
|
|
159
|
+
className: cn(SKELETON_CLASSES, children ? "[&_*]:invisible" : "min-h-5", cls),
|
|
164
160
|
style: mergedStyle,
|
|
165
161
|
...htmlRest,
|
|
166
162
|
children
|
package/dist/tag.cjs
CHANGED
|
@@ -208,11 +208,7 @@ function stripShims(props) {
|
|
|
208
208
|
for (const k of SHIM_PROPS) delete out[k];
|
|
209
209
|
return out;
|
|
210
210
|
}
|
|
211
|
-
var SKELETON_CLASSES =
|
|
212
|
-
"animate-skeleton-shimmer rounded-sm",
|
|
213
|
-
"bg-[linear-gradient(90deg,var(--color-skeleton-start)_0%,var(--color-skeleton-end)_50%,var(--color-skeleton-start)_100%)]",
|
|
214
|
-
"bg-[length:200%_100%]"
|
|
215
|
-
].join(" ");
|
|
211
|
+
var SKELETON_CLASSES = "lux-skeleton rounded-sm";
|
|
216
212
|
var HIDE_BELOW_MAP = { lg: "lg:hidden", md: "md:hidden", sm: "sm:hidden" };
|
|
217
213
|
var Skeleton = React4__namespace.forwardRef(
|
|
218
214
|
function Skeleton2(props, ref) {
|
|
@@ -232,7 +228,7 @@ var Skeleton = React4__namespace.forwardRef(
|
|
|
232
228
|
{
|
|
233
229
|
ref,
|
|
234
230
|
"data-loading": true,
|
|
235
|
-
className: cn(SKELETON_CLASSES, "
|
|
231
|
+
className: cn(SKELETON_CLASSES, "[&_*]:invisible", cls),
|
|
236
232
|
style: mergedStyle,
|
|
237
233
|
...htmlRest,
|
|
238
234
|
children
|
|
@@ -244,7 +240,7 @@ var Skeleton = React4__namespace.forwardRef(
|
|
|
244
240
|
{
|
|
245
241
|
ref,
|
|
246
242
|
"data-loading": true,
|
|
247
|
-
className: cn(SKELETON_CLASSES, children ? "
|
|
243
|
+
className: cn(SKELETON_CLASSES, children ? "[&_*]:invisible" : "min-h-5", cls),
|
|
248
244
|
style: mergedStyle,
|
|
249
245
|
...htmlRest,
|
|
250
246
|
children
|
|
@@ -375,7 +371,7 @@ var Tooltip = React4__namespace.forwardRef(
|
|
|
375
371
|
{
|
|
376
372
|
ref: triggerRef,
|
|
377
373
|
asChild: true,
|
|
378
|
-
...isMobile ? {
|
|
374
|
+
...isMobile ? { onClick: handleTriggerClick } : {},
|
|
379
375
|
...triggerProps,
|
|
380
376
|
children
|
|
381
377
|
}
|
package/dist/tag.js
CHANGED
|
@@ -186,11 +186,7 @@ function stripShims(props) {
|
|
|
186
186
|
for (const k of SHIM_PROPS) delete out[k];
|
|
187
187
|
return out;
|
|
188
188
|
}
|
|
189
|
-
var SKELETON_CLASSES =
|
|
190
|
-
"animate-skeleton-shimmer rounded-sm",
|
|
191
|
-
"bg-[linear-gradient(90deg,var(--color-skeleton-start)_0%,var(--color-skeleton-end)_50%,var(--color-skeleton-start)_100%)]",
|
|
192
|
-
"bg-[length:200%_100%]"
|
|
193
|
-
].join(" ");
|
|
189
|
+
var SKELETON_CLASSES = "lux-skeleton rounded-sm";
|
|
194
190
|
var HIDE_BELOW_MAP = { lg: "lg:hidden", md: "md:hidden", sm: "sm:hidden" };
|
|
195
191
|
var Skeleton = React4.forwardRef(
|
|
196
192
|
function Skeleton2(props, ref) {
|
|
@@ -210,7 +206,7 @@ var Skeleton = React4.forwardRef(
|
|
|
210
206
|
{
|
|
211
207
|
ref,
|
|
212
208
|
"data-loading": true,
|
|
213
|
-
className: cn(SKELETON_CLASSES, "
|
|
209
|
+
className: cn(SKELETON_CLASSES, "[&_*]:invisible", cls),
|
|
214
210
|
style: mergedStyle,
|
|
215
211
|
...htmlRest,
|
|
216
212
|
children
|
|
@@ -222,7 +218,7 @@ var Skeleton = React4.forwardRef(
|
|
|
222
218
|
{
|
|
223
219
|
ref,
|
|
224
220
|
"data-loading": true,
|
|
225
|
-
className: cn(SKELETON_CLASSES, children ? "
|
|
221
|
+
className: cn(SKELETON_CLASSES, children ? "[&_*]:invisible" : "min-h-5", cls),
|
|
226
222
|
style: mergedStyle,
|
|
227
223
|
...htmlRest,
|
|
228
224
|
children
|
|
@@ -353,7 +349,7 @@ var Tooltip = React4.forwardRef(
|
|
|
353
349
|
{
|
|
354
350
|
ref: triggerRef,
|
|
355
351
|
asChild: true,
|
|
356
|
-
...isMobile ? {
|
|
352
|
+
...isMobile ? { onClick: handleTriggerClick } : {},
|
|
357
353
|
...triggerProps,
|
|
358
354
|
children
|
|
359
355
|
}
|
package/dist/tooltip.cjs
CHANGED
package/dist/tooltip.js
CHANGED
package/dist/vite.cjs
CHANGED
|
@@ -73,14 +73,15 @@ var GUI_DEFINES = {
|
|
|
73
73
|
"process.env.EXPO_OS": JSON.stringify("web"),
|
|
74
74
|
"process.env.IS_WEB": JSON.stringify("true")
|
|
75
75
|
};
|
|
76
|
-
|
|
77
|
-
// src/vite.ts
|
|
78
|
-
var DEDUPE = [
|
|
76
|
+
var ENGINE_DEDUPE = [
|
|
79
77
|
"react",
|
|
80
78
|
"react-dom",
|
|
81
79
|
"@tanstack/react-query",
|
|
82
80
|
...GUI_PACKAGES
|
|
83
81
|
];
|
|
82
|
+
|
|
83
|
+
// src/vite.ts
|
|
84
|
+
var DEDUPE = ENGINE_DEDUPE;
|
|
84
85
|
var INCLUDE = [
|
|
85
86
|
"react-native-web",
|
|
86
87
|
"@react-native/normalize-color",
|
package/dist/vite.js
CHANGED
|
@@ -71,14 +71,15 @@ var GUI_DEFINES = {
|
|
|
71
71
|
"process.env.EXPO_OS": JSON.stringify("web"),
|
|
72
72
|
"process.env.IS_WEB": JSON.stringify("true")
|
|
73
73
|
};
|
|
74
|
-
|
|
75
|
-
// src/vite.ts
|
|
76
|
-
var DEDUPE = [
|
|
74
|
+
var ENGINE_DEDUPE = [
|
|
77
75
|
"react",
|
|
78
76
|
"react-dom",
|
|
79
77
|
"@tanstack/react-query",
|
|
80
78
|
...GUI_PACKAGES
|
|
81
79
|
];
|
|
80
|
+
|
|
81
|
+
// src/vite.ts
|
|
82
|
+
var DEDUPE = ENGINE_DEDUPE;
|
|
82
83
|
var INCLUDE = [
|
|
83
84
|
"react-native-web",
|
|
84
85
|
"@react-native/normalize-color",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luxfi/ui",
|
|
3
|
-
"version": "7.4.
|
|
3
|
+
"version": "7.4.8",
|
|
4
4
|
"description": "Cross-platform DeFi UI components built on @hanzo/gui (Tamagui). Native mobile + web.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
"import": "./dist/iam.js",
|
|
51
51
|
"require": "./dist/iam.cjs"
|
|
52
52
|
},
|
|
53
|
+
"./auth": {
|
|
54
|
+
"types": "./dist/auth.d.ts",
|
|
55
|
+
"import": "./dist/auth.js",
|
|
56
|
+
"require": "./dist/auth.cjs"
|
|
57
|
+
},
|
|
53
58
|
"./identity": {
|
|
54
59
|
"types": "./dist/identity.d.ts",
|
|
55
60
|
"import": "./dist/identity.js",
|
|
@@ -321,14 +326,15 @@
|
|
|
321
326
|
"tailwind-merge": "^3.0.0"
|
|
322
327
|
},
|
|
323
328
|
"peerDependencies": {
|
|
324
|
-
"@tanstack/react-query": "^5.76.0",
|
|
325
329
|
"@hanzo/gui": "^7.3.0",
|
|
330
|
+
"@hanzo/iam": "^0.13.8",
|
|
326
331
|
"@hanzogui/colors": "^7.3.0",
|
|
327
332
|
"@hanzogui/config": "^7.3.0",
|
|
328
333
|
"@hanzogui/core": "^7.3.0",
|
|
329
334
|
"@hanzogui/lucide-icons-2": "^7.3.0",
|
|
330
335
|
"@hanzogui/themes": "^7.3.0",
|
|
331
336
|
"@hanzogui/web": "^7.3.0",
|
|
337
|
+
"@tanstack/react-query": "^5.76.0",
|
|
332
338
|
"react": ">=19",
|
|
333
339
|
"react-dom": ">=19"
|
|
334
340
|
},
|
|
@@ -347,10 +353,14 @@
|
|
|
347
353
|
},
|
|
348
354
|
"@hanzogui/web": {
|
|
349
355
|
"optional": true
|
|
356
|
+
},
|
|
357
|
+
"@hanzo/iam": {
|
|
358
|
+
"optional": true
|
|
350
359
|
}
|
|
351
360
|
},
|
|
352
361
|
"devDependencies": {
|
|
353
362
|
"@hanzo/gui": "7.3.0",
|
|
363
|
+
"@hanzo/iam": "^0.13.8",
|
|
354
364
|
"@hanzogui/colors": "7.3.0",
|
|
355
365
|
"@hanzogui/config": "7.3.0",
|
|
356
366
|
"@hanzogui/core": "7.3.0",
|
package/src/auth.tsx
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { configureIam, handleCallback, logout, startLogin } from '@hanzo/iam/browser';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
|
|
6
|
+
import { IdentityProvider, type IdentityAuth } from './identity';
|
|
7
|
+
import { useWhiteLabel } from './provider';
|
|
8
|
+
import type { WhiteLabel } from './white-label';
|
|
9
|
+
|
|
10
|
+
// The sign-in half of identity, in ONE place.
|
|
11
|
+
//
|
|
12
|
+
// ./identity answers "who is signed in" and ./chrome renders it (the account
|
|
13
|
+
// switcher already shows `Sign in with <iamDomain>` when anonymous and
|
|
14
|
+
// `Sign out` when ready). Neither of them can START a session — `IdentityAuth`
|
|
15
|
+
// is a seam, and every surface filled it in by hand. That is where the fleet
|
|
16
|
+
// diverged:
|
|
17
|
+
//
|
|
18
|
+
// lux/explore/lib/oidc.ts hand-built authorize URL, NO PKCE at all
|
|
19
|
+
// lux/mpc/dashboard/app/providers configureIam + startLogin + logout (right)
|
|
20
|
+
// lux/market/src/app/providers the same three lines again
|
|
21
|
+
// lux/cloud/apps/web/src/iam.ts a third shape: IAMConfig from a brand table
|
|
22
|
+
// plus VITE_IAM_URL / VITE_IAM_CLIENT_ID
|
|
23
|
+
// env overrides — i.e. brand per DEPLOYMENT
|
|
24
|
+
//
|
|
25
|
+
// Four copies, and only one of them was correct. So the flow lives here:
|
|
26
|
+
//
|
|
27
|
+
// <AppProvider> host → brand, theme, issuer, clientId
|
|
28
|
+
// <AuthProvider> the three verbs, wired to that host
|
|
29
|
+
// …and on the callback route:
|
|
30
|
+
// <AuthCallback/>
|
|
31
|
+
//
|
|
32
|
+
// MECHANISM IS NOT OURS. Authorization-Code + PKCE-S256, the state nonce, the
|
|
33
|
+
// verifier, discovery and the token store all belong to `@hanzo/iam/browser`;
|
|
34
|
+
// this module only decides WHICH issuer and WHICH client — and that is a pure
|
|
35
|
+
// function of the Host header (./white-label). A design system that reimplements
|
|
36
|
+
// PKCE is a design system with its own CVEs.
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The ONE callback route. Registered as `https://<host>/auth/callback` for
|
|
40
|
+
* every browser client in the fleet — never under `/api/`, never per app.
|
|
41
|
+
*/
|
|
42
|
+
export const CALLBACK_PATH = '/auth/callback';
|
|
43
|
+
|
|
44
|
+
/** What `handleCallback` hands back. Structural, so the SDK's types stay its own. */
|
|
45
|
+
export interface AuthResult {
|
|
46
|
+
readonly token: { readonly accessToken: string };
|
|
47
|
+
readonly redirect: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Point `@hanzo/iam` at the issuer and client this HOST resolves to.
|
|
52
|
+
*
|
|
53
|
+
* Idempotent per config, browser-only, and safe to call from anywhere — which
|
|
54
|
+
* is the point: no ordering rule between a provider's effect and a child's.
|
|
55
|
+
*/
|
|
56
|
+
export function configureAuth(wl: WhiteLabel): void {
|
|
57
|
+
if (typeof window === 'undefined') return;
|
|
58
|
+
configureIam({
|
|
59
|
+
issuer: wl.issuer,
|
|
60
|
+
clientId: wl.clientId,
|
|
61
|
+
// The SDK already defaults to `${origin}${CALLBACK_PATH}`; naming it here
|
|
62
|
+
// makes the fleet-wide invariant one readable, testable line instead of a
|
|
63
|
+
// default nobody can see.
|
|
64
|
+
redirect: `${ window.location.origin }${ CALLBACK_PATH }`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface Auth {
|
|
69
|
+
/** Start the PKCE login. `redirect` is where to land afterwards. */
|
|
70
|
+
readonly signIn: (redirect?: string) => void;
|
|
71
|
+
/** RP-initiated logout + local token clear. */
|
|
72
|
+
readonly signOut: () => void;
|
|
73
|
+
/** Finish the login on the callback route. */
|
|
74
|
+
readonly complete: () => Promise<AuthResult>;
|
|
75
|
+
/** The host-resolved brand, issuer and client these verbs are bound to. */
|
|
76
|
+
readonly whiteLabel: WhiteLabel;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The three auth verbs, bound to the current host.
|
|
81
|
+
*
|
|
82
|
+
* Configuration happens during render, before any caller can use a verb, so a
|
|
83
|
+
* callback route works whether or not it sits under an `<AuthProvider>`.
|
|
84
|
+
*/
|
|
85
|
+
export function useAuth(defaultRedirect = '/'): Auth {
|
|
86
|
+
const whiteLabel = useWhiteLabel();
|
|
87
|
+
configureAuth(whiteLabel);
|
|
88
|
+
return React.useMemo<Auth>(() => ({
|
|
89
|
+
signIn: (redirect = defaultRedirect) => void startLogin({ redirect }),
|
|
90
|
+
signOut: () => void logout(),
|
|
91
|
+
complete: () => handleCallback() as Promise<AuthResult>,
|
|
92
|
+
whiteLabel,
|
|
93
|
+
}), [ whiteLabel, defaultRedirect ]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface AuthProviderProps {
|
|
97
|
+
readonly children: React.ReactNode;
|
|
98
|
+
/** Where a successful sign-in lands. Default `/`. */
|
|
99
|
+
readonly redirect?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Anything the surface must tear down alongside the IAM session — the mirror
|
|
102
|
+
* of {@link AuthCallbackProps.onSignedIn}. A surface that mints its own
|
|
103
|
+
* downstream credential from the IAM token drops it here; the IAM session
|
|
104
|
+
* itself is never the app's to manage.
|
|
105
|
+
*/
|
|
106
|
+
readonly onSignOut?: () => void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `IdentityProvider` with the credential half filled in.
|
|
111
|
+
*
|
|
112
|
+
* A surface's root becomes `<AppProvider><AuthProvider>…` and nothing else —
|
|
113
|
+
* no `configureIam`, no `startLogin`, no per-app token keys.
|
|
114
|
+
*/
|
|
115
|
+
export const AuthProvider = ({ children, redirect = '/', onSignOut }: AuthProviderProps): React.ReactElement => {
|
|
116
|
+
const auth = useAuth(redirect);
|
|
117
|
+
const identityAuth = React.useMemo<IdentityAuth>(() => ({
|
|
118
|
+
signIn: () => auth.signIn(redirect),
|
|
119
|
+
signOut: () => {
|
|
120
|
+
onSignOut?.();
|
|
121
|
+
auth.signOut();
|
|
122
|
+
},
|
|
123
|
+
}), [ auth, redirect, onSignOut ]);
|
|
124
|
+
return <IdentityProvider auth={ identityAuth }>{ children }</IdentityProvider>;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export interface AuthCallbackProps {
|
|
128
|
+
/**
|
|
129
|
+
* Anything the surface must do with the fresh session before it navigates —
|
|
130
|
+
* e.g. the MPC dashboard mints its own API JWT from the IAM token. Throwing
|
|
131
|
+
* here does NOT fail the sign-in: the user is signed in, and a downstream
|
|
132
|
+
* service being unreachable is that service's problem to report.
|
|
133
|
+
*/
|
|
134
|
+
readonly onSignedIn?: (result: AuthResult) => void | Promise<void>;
|
|
135
|
+
/** Router hop. Defaults to a full replace, which needs no router. */
|
|
136
|
+
readonly navigate?: (to: string) => void;
|
|
137
|
+
/** Where to go when the callback carried no stashed redirect. */
|
|
138
|
+
readonly fallback?: string;
|
|
139
|
+
/** Sign-in entry point offered after a failure. */
|
|
140
|
+
readonly retryPath?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The ONE callback page body: verify state, exchange the code, go where the
|
|
145
|
+
* user was headed. Mount it at {@link CALLBACK_PATH}.
|
|
146
|
+
*/
|
|
147
|
+
export const AuthCallback = ({
|
|
148
|
+
onSignedIn,
|
|
149
|
+
navigate,
|
|
150
|
+
fallback = '/',
|
|
151
|
+
retryPath = '/login',
|
|
152
|
+
}: AuthCallbackProps): React.ReactElement => {
|
|
153
|
+
const auth = useAuth();
|
|
154
|
+
const [ error, setError ] = React.useState<string | null>(null);
|
|
155
|
+
|
|
156
|
+
React.useEffect(() => {
|
|
157
|
+
let live = true;
|
|
158
|
+
void (async () => {
|
|
159
|
+
try {
|
|
160
|
+
const result = await auth.complete();
|
|
161
|
+
if (!live) return;
|
|
162
|
+
try {
|
|
163
|
+
await onSignedIn?.(result);
|
|
164
|
+
} catch {
|
|
165
|
+
// Signed in; the surface's own downstream call failed. Not a login
|
|
166
|
+
// failure, and stranding a good session on this screen was the bug.
|
|
167
|
+
}
|
|
168
|
+
if (!live) return;
|
|
169
|
+
const to = result.redirect || fallback;
|
|
170
|
+
if (navigate) navigate(to);
|
|
171
|
+
else window.location.replace(to);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
if (live) setError(e instanceof Error ? e.message : 'Authentication failed');
|
|
174
|
+
}
|
|
175
|
+
})();
|
|
176
|
+
return () => {
|
|
177
|
+
live = false;
|
|
178
|
+
};
|
|
179
|
+
// `auth` is re-created only when the host changes; the exchange must run once.
|
|
180
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div className="flex min-h-[50vh] items-center justify-center px-4">
|
|
185
|
+
<div className="w-full max-w-sm text-center">
|
|
186
|
+
{ error ? (
|
|
187
|
+
<>
|
|
188
|
+
<p className="mb-2 text-lg font-semibold text-[var(--color-text-error)]">Sign-in failed</p>
|
|
189
|
+
<p className="mb-6 text-sm text-[var(--color-text-secondary)]">{ error }</p>
|
|
190
|
+
<a href={ retryPath } className="text-sm underline underline-offset-4">Try again</a>
|
|
191
|
+
</>
|
|
192
|
+
) : (
|
|
193
|
+
<p className="text-sm text-[var(--color-text-secondary)]">Completing sign in…</p>
|
|
194
|
+
) }
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
};
|
package/src/engine.ts
CHANGED
|
@@ -105,8 +105,45 @@ export const GUI_DEFINES: Readonly<Record<string, string>> = {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
108
|
+
* Every package that must exist exactly ONCE in a bundle — the engine, plus the
|
|
109
|
+
* three libraries outside it that also publish a module-scope React context.
|
|
110
|
+
*
|
|
111
|
+
* `react` and `react-dom` are the classic case. `@tanstack/react-query` is the
|
|
112
|
+
* same failure with a different message: lux.market resolved two copies (one
|
|
113
|
+
* under react@19.1.0, one under react@19.2.8), the app's `QueryClientProvider`
|
|
114
|
+
* came from copy A and `@luxfi/ui`'s `useQuery` read copy B's context, and the
|
|
115
|
+
* whole page died with "No QueryClient set, use QueryClientProvider to set one".
|
|
116
|
+
*
|
|
117
|
+
* ONE list, shared by both bundler wrappers, so vite and next cannot dedupe
|
|
118
|
+
* different sets — which is exactly how that market bug survived: the vite
|
|
119
|
+
* wrapper already listed these three, the Next wrapper did not.
|
|
120
|
+
*/
|
|
121
|
+
export const ENGINE_DEDUPE: ReadonlyArray<string> = [
|
|
122
|
+
'react',
|
|
123
|
+
'react-dom',
|
|
124
|
+
'@tanstack/react-query',
|
|
125
|
+
...GUI_PACKAGES,
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The subset of ENGINE_DEDUPE that may be pinned by a hard bundler ALIAS.
|
|
130
|
+
*
|
|
131
|
+
* `dedupe` is advisory hoisting; an alias is a hard redirect, and redirecting
|
|
132
|
+
* `react` / `react-dom` inside a Next app breaks the framework's own graph —
|
|
133
|
+
* the RSC client manifest is keyed on Next's copy, so the MPC dashboard died
|
|
134
|
+
* with `Could not find the module … segment-explorer-node.js#SegmentViewNode in
|
|
135
|
+
* the React Client Manifest` followed by `__webpack_modules__[moduleId] is not
|
|
136
|
+
* a function`. Next already guarantees one React; it does not guarantee one
|
|
137
|
+
* react-query, and it does not guarantee one popper.
|
|
138
|
+
*/
|
|
139
|
+
export const ALIASABLE: ReadonlyArray<string> = [
|
|
140
|
+
'@tanstack/react-query',
|
|
141
|
+
...GUI_PACKAGES,
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Exact-match aliases pinning every aliasable package to ONE entry file, for
|
|
146
|
+
* bundlers that have no `dedupe` of their own (webpack, turbopack).
|
|
110
147
|
*
|
|
111
148
|
* Keys carry webpack's `$` exact-match suffix so only the bare specifier is
|
|
112
149
|
* redirected — a deep import keeps resolving normally. Packages that are not
|
|
@@ -119,7 +156,7 @@ export function guiAliases(from: string): Record<string, string> {
|
|
|
119
156
|
const { createRequire } = require('node:module') as typeof import('node:module');
|
|
120
157
|
const require_ = createRequire(`${ from.replace(/\/?$/, '/') }noop.js`);
|
|
121
158
|
const alias: Record<string, string> = {};
|
|
122
|
-
for (const pkg of
|
|
159
|
+
for (const pkg of ALIASABLE) {
|
|
123
160
|
try {
|
|
124
161
|
alias[`${ pkg }$`] = require_.resolve(pkg);
|
|
125
162
|
} catch {
|
package/src/index.ts
CHANGED
|
@@ -199,7 +199,7 @@ export type { SelectOption, ListCollection, SelectRootProps, SelectProps, Select
|
|
|
199
199
|
export { Separator } from './separator';
|
|
200
200
|
export type { SeparatorProps } from './separator';
|
|
201
201
|
|
|
202
|
-
export { Skeleton, SkeletonCircle, SkeletonText } from './skeleton';
|
|
202
|
+
export { Skeleton, SkeletonCircle, SkeletonText, LoadingBar } from './skeleton';
|
|
203
203
|
export type { SkeletonProps, SkeletonCircleProps, SkeletonTextProps } from './skeleton';
|
|
204
204
|
|
|
205
205
|
export { Slider } from './slider';
|
package/src/next.ts
CHANGED
|
@@ -49,6 +49,28 @@ export function withLuxUi(config: LuxUiNextConfig = {}): LuxUiNextConfig {
|
|
|
49
49
|
// so the equivalent is an exact-match alias per package, resolved from the app
|
|
50
50
|
// root — see ./engine for why two copies is a crash and not a size problem.
|
|
51
51
|
const engine = guiAliases(config.dir ?? process.cwd());
|
|
52
|
+
// The same dedupe for Turbopack, which is the ONLY dev bundler from Next 16 —
|
|
53
|
+
// `webpack()` below is never called by `next dev`, so a webpack-only alias map
|
|
54
|
+
// deduped production and left development with N copies of `@hanzogui/popper`
|
|
55
|
+
// and the `Cannot read properties of undefined (reading 'setReference')`
|
|
56
|
+
// storm that comes with them.
|
|
57
|
+
//
|
|
58
|
+
// Two shape differences from webpack, both load-bearing:
|
|
59
|
+
// - no `$` exact-match suffix; a bare key is already exact.
|
|
60
|
+
// - an ABSOLUTE path is not a valid alias value. Turbopack reads it as a
|
|
61
|
+
// server-relative URL and fails with `Can't resolve
|
|
62
|
+
// './Users/.../react/index.js' — server relative imports are not
|
|
63
|
+
// implemented yet`, which takes the whole dev server down. The value has
|
|
64
|
+
// to be a project-relative `./…` specifier, so a package resolved outside
|
|
65
|
+
// the project root is skipped rather than mis-aliased.
|
|
66
|
+
const root = config.dir ?? process.cwd();
|
|
67
|
+
const path = require('node:path') as typeof import('node:path');
|
|
68
|
+
const engineTurbopack: Record<string, string> = {};
|
|
69
|
+
for (const [ key, abs ] of Object.entries(engine)) {
|
|
70
|
+
const rel = path.relative(root, abs);
|
|
71
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) continue;
|
|
72
|
+
engineTurbopack[key.replace(/\$$/, '')] = `./${ rel }`;
|
|
73
|
+
}
|
|
52
74
|
// `dir` is OURS, not Next's — leaving it on the returned object makes Next
|
|
53
75
|
// print `Unrecognized key(s) in object: 'dir'` on every boot of every surface
|
|
54
76
|
// that passes it. Consume it here.
|
|
@@ -60,6 +82,7 @@ export function withLuxUi(config: LuxUiNextConfig = {}): LuxUiNextConfig {
|
|
|
60
82
|
...config.turbopack,
|
|
61
83
|
resolveAlias: {
|
|
62
84
|
'react-native': 'react-native-web',
|
|
85
|
+
...engineTurbopack,
|
|
63
86
|
...config.turbopack?.resolveAlias,
|
|
64
87
|
},
|
|
65
88
|
},
|
package/src/skeleton.tsx
CHANGED
|
@@ -142,11 +142,19 @@ function stripShims<T extends Record<string, unknown>>(props: T): Omit<T, (typeo
|
|
|
142
142
|
return out as Omit<T, (typeof SHIM_PROPS)[number]>;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
// A skeleton reserves space. It does not move.
|
|
146
|
+
//
|
|
147
|
+
// It used to shimmer via a per-element `background-position` animation, which
|
|
148
|
+
// repaints the element's whole box every frame. Under a 4x CPU throttle that
|
|
149
|
+
// costs 33.6fps at 1460 concurrent skeletons and 13.8fps at 3000. Rewriting it
|
|
150
|
+
// as a per-element `transform` sheen — the textbook "animate transform only"
|
|
151
|
+
// fix — measured WORSE: 23.0fps and 5.4fps, because it also adds a layer and a
|
|
152
|
+
// clip per element. Holding still costs 120fps at every n.
|
|
153
|
+
//
|
|
154
|
+
// The "work is in flight" signal is <LoadingBar/> below: one animated element
|
|
155
|
+
// for the whole page instead of one per skeleton. Full numbers and reasoning
|
|
156
|
+
// live next to the CSS, in section 6c of @luxfi/ui/tokens.css.
|
|
157
|
+
const SKELETON_CLASSES = 'lux-skeleton rounded-sm';
|
|
150
158
|
|
|
151
159
|
const HIDE_BELOW_MAP: Record<string, string> = { lg: 'lg:hidden', md: 'md:hidden', sm: 'sm:hidden' };
|
|
152
160
|
|
|
@@ -176,7 +184,7 @@ export const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
|
|
|
176
184
|
<Component
|
|
177
185
|
ref={ ref }
|
|
178
186
|
data-loading
|
|
179
|
-
className={ cn(SKELETON_CLASSES, '
|
|
187
|
+
className={ cn(SKELETON_CLASSES, '[&_*]:invisible', cls) }
|
|
180
188
|
style={ mergedStyle }
|
|
181
189
|
{ ...htmlRest }
|
|
182
190
|
>
|
|
@@ -189,7 +197,7 @@ export const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
|
|
|
189
197
|
<Component
|
|
190
198
|
ref={ ref }
|
|
191
199
|
data-loading
|
|
192
|
-
className={ cn(SKELETON_CLASSES, children ? '
|
|
200
|
+
className={ cn(SKELETON_CLASSES, children ? '[&_*]:invisible' : 'min-h-5', cls) }
|
|
193
201
|
style={ mergedStyle }
|
|
194
202
|
{ ...htmlRest }
|
|
195
203
|
>
|
|
@@ -252,3 +260,24 @@ export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(
|
|
|
252
260
|
);
|
|
253
261
|
},
|
|
254
262
|
);
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// LoadingBar
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The page-level "work is in flight" signal — one animated element, however
|
|
270
|
+
* many skeletons are on screen. Mount it while any request is outstanding.
|
|
271
|
+
*
|
|
272
|
+
* It is `position: fixed` and 2px tall, so it is out of flow: it cannot shift
|
|
273
|
+
* layout (CLS 0) and it cannot tint content. Under prefers-reduced-motion the
|
|
274
|
+
* sweep stops and the track stays lit, so the state is still legible.
|
|
275
|
+
*
|
|
276
|
+
* This is what replaces a bare spinner. A spinner says "something is
|
|
277
|
+
* happening somewhere"; a skeleton plus this bar says "here is the shape of
|
|
278
|
+
* what is coming, and it is still coming".
|
|
279
|
+
*/
|
|
280
|
+
export function LoadingBar({ loading = true }: { readonly loading?: boolean }) {
|
|
281
|
+
if (!loading) return null;
|
|
282
|
+
return <div className="lux-loading" role="progressbar" aria-busy="true" aria-label="Loading"/>;
|
|
283
|
+
}
|