@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/engine.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// The gui engine, named once.
|
|
2
|
+
//
|
|
3
|
+
// @hanzo/gui is a facade over ~55 `@hanzogui/*` packages, and several of them
|
|
4
|
+
// (`popper`, `popover`, `core`, `floating`) publish React contexts at MODULE
|
|
5
|
+
// scope. Two physical copies of such a package are two different context
|
|
6
|
+
// objects, so a provider from copy A is invisible to a consumer from copy B and
|
|
7
|
+
// the consumer silently falls back to the context's default value.
|
|
8
|
+
//
|
|
9
|
+
// That is not a theoretical hazard, it is the shape of the loudest bug this
|
|
10
|
+
// package has shipped: `PopperAnchor` reads `refs` off the Popper context and
|
|
11
|
+
// calls `refs.setReference(node)` from a ref callback with no guard, so when the
|
|
12
|
+
// context is the empty default the browser logs
|
|
13
|
+
//
|
|
14
|
+
// Cannot read properties of undefined (reading 'setReference')
|
|
15
|
+
//
|
|
16
|
+
// once per tooltip trigger, uncaught (it runs inside `startTransition`, so no
|
|
17
|
+
// error boundary sees it). A page with a tooltip per table row logs it hundreds
|
|
18
|
+
// of times. The gui engine itself diagnoses the same root cause out loud when
|
|
19
|
+
// the duplication reaches its config lookup: "Can't find Hanzo GUI
|
|
20
|
+
// configuration … due to having mis-matched versions of Hanzo GUI dependencies,
|
|
21
|
+
// or bundlers somehow duplicating them."
|
|
22
|
+
//
|
|
23
|
+
// So the engine list is a VALUE, declared once here, and both bundler wrappers
|
|
24
|
+
// (`@luxfi/ui/vite`, `@luxfi/ui/next`) dedupe against it. A consumer that uses
|
|
25
|
+
// the wrapper cannot end up with two copies, and does not have to hand-write a
|
|
26
|
+
// per-app `pnpm.overrides` block pinning three dozen packages to stop it.
|
|
27
|
+
|
|
28
|
+
/** Every package `@hanzo/gui` re-exports — the engine, in full. */
|
|
29
|
+
export const GUI_PACKAGES: ReadonlyArray<string> = [
|
|
30
|
+
'@hanzo/gui',
|
|
31
|
+
'@hanzogui/accordion',
|
|
32
|
+
'@hanzogui/adapt',
|
|
33
|
+
'@hanzogui/alert-dialog',
|
|
34
|
+
'@hanzogui/animate',
|
|
35
|
+
'@hanzogui/animate-presence',
|
|
36
|
+
'@hanzogui/avatar',
|
|
37
|
+
'@hanzogui/button',
|
|
38
|
+
'@hanzogui/card',
|
|
39
|
+
'@hanzogui/checkbox',
|
|
40
|
+
'@hanzogui/collapsible',
|
|
41
|
+
'@hanzogui/component-helpers',
|
|
42
|
+
'@hanzogui/compose-refs',
|
|
43
|
+
'@hanzogui/config',
|
|
44
|
+
'@hanzogui/context-menu',
|
|
45
|
+
'@hanzogui/core',
|
|
46
|
+
'@hanzogui/create-context',
|
|
47
|
+
'@hanzogui/create-menu',
|
|
48
|
+
'@hanzogui/dialog',
|
|
49
|
+
'@hanzogui/dismissable',
|
|
50
|
+
'@hanzogui/element',
|
|
51
|
+
'@hanzogui/elements',
|
|
52
|
+
'@hanzogui/floating',
|
|
53
|
+
'@hanzogui/focus-scope',
|
|
54
|
+
'@hanzogui/font-size',
|
|
55
|
+
'@hanzogui/form',
|
|
56
|
+
'@hanzogui/get-token',
|
|
57
|
+
'@hanzogui/group',
|
|
58
|
+
'@hanzogui/helpers',
|
|
59
|
+
'@hanzogui/image',
|
|
60
|
+
'@hanzogui/input',
|
|
61
|
+
'@hanzogui/label',
|
|
62
|
+
'@hanzogui/list-item',
|
|
63
|
+
'@hanzogui/menu',
|
|
64
|
+
'@hanzogui/popover',
|
|
65
|
+
'@hanzogui/popper',
|
|
66
|
+
'@hanzogui/portal',
|
|
67
|
+
'@hanzogui/progress',
|
|
68
|
+
'@hanzogui/radio-group',
|
|
69
|
+
'@hanzogui/react-native-media-driver',
|
|
70
|
+
'@hanzogui/remove-scroll',
|
|
71
|
+
'@hanzogui/scroll-view',
|
|
72
|
+
'@hanzogui/select',
|
|
73
|
+
'@hanzogui/separator',
|
|
74
|
+
'@hanzogui/shapes',
|
|
75
|
+
'@hanzogui/sheet',
|
|
76
|
+
'@hanzogui/slider',
|
|
77
|
+
'@hanzogui/spacer',
|
|
78
|
+
'@hanzogui/spinner',
|
|
79
|
+
'@hanzogui/stacks',
|
|
80
|
+
'@hanzogui/switch',
|
|
81
|
+
'@hanzogui/tabs',
|
|
82
|
+
'@hanzogui/text',
|
|
83
|
+
'@hanzogui/theme',
|
|
84
|
+
'@hanzogui/toast',
|
|
85
|
+
'@hanzogui/toggle-group',
|
|
86
|
+
'@hanzogui/tooltip',
|
|
87
|
+
'@hanzogui/use-controllable-state',
|
|
88
|
+
'@hanzogui/use-debounce',
|
|
89
|
+
'@hanzogui/use-force-update',
|
|
90
|
+
'@hanzogui/use-presence',
|
|
91
|
+
'@hanzogui/use-window-dimensions',
|
|
92
|
+
'@hanzogui/visually-hidden',
|
|
93
|
+
'@hanzogui/web',
|
|
94
|
+
'@hanzogui/z-index-stack',
|
|
95
|
+
'@luxfi/ui',
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The engine branches on these at module scope; without them a browser bundle
|
|
100
|
+
* takes the native path.
|
|
101
|
+
*/
|
|
102
|
+
export const GUI_DEFINES: Readonly<Record<string, string>> = {
|
|
103
|
+
'process.env.EXPO_OS': JSON.stringify('web'),
|
|
104
|
+
'process.env.IS_WEB': JSON.stringify('true'),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Exact-match aliases pinning every engine package to ONE entry file, for
|
|
109
|
+
* bundlers that have no `dedupe` of their own (webpack).
|
|
110
|
+
*
|
|
111
|
+
* Keys carry webpack's `$` exact-match suffix so only the bare specifier is
|
|
112
|
+
* redirected — a deep import keeps resolving normally. Packages that are not
|
|
113
|
+
* physically installed are skipped rather than throwing: the list is the
|
|
114
|
+
* engine's full surface and no single app pulls all of it.
|
|
115
|
+
*/
|
|
116
|
+
export function guiAliases(from: string): Record<string, string> {
|
|
117
|
+
// Deferred so this module stays importable from a browser bundle (the
|
|
118
|
+
// wrappers are build-time only, but `engine.ts` is plain values).
|
|
119
|
+
const { createRequire } = require('node:module') as typeof import('node:module');
|
|
120
|
+
const require_ = createRequire(`${ from.replace(/\/?$/, '/') }noop.js`);
|
|
121
|
+
const alias: Record<string, string> = {};
|
|
122
|
+
for (const pkg of GUI_PACKAGES) {
|
|
123
|
+
try {
|
|
124
|
+
alias[`${ pkg }$`] = require_.resolve(pkg);
|
|
125
|
+
} catch {
|
|
126
|
+
// Not installed in this app — nothing to dedupe.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return alias;
|
|
130
|
+
}
|
|
@@ -66,8 +66,9 @@ function ExpirationPill({ exp, isSelected, onClick }: ExpirationPillProps) {
|
|
|
66
66
|
</span>
|
|
67
67
|
<span className={cn(
|
|
68
68
|
'font-mono tabular-nums text-[10px]',
|
|
69
|
-
isSelected ? 'text-
|
|
70
|
-
|
|
69
|
+
isSelected ? 'text-[var(--foreground)]' : 'text-[var(--muted-foreground)]',
|
|
70
|
+
// Near expiry is a notice, and a notice in Lux is the loudest neutral.
|
|
71
|
+
exp.dte <= 7 && !isSelected && 'text-[var(--color-status-warn)]',
|
|
71
72
|
)}>
|
|
72
73
|
{exp.dte}d
|
|
73
74
|
</span>
|
package/src/greeks-display.tsx
CHANGED
|
@@ -32,13 +32,16 @@ interface GreekDef {
|
|
|
32
32
|
maxMagnitude: number;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// A greek is identified by its symbol and its label, never by a hue. Six series
|
|
36
|
+
// on one achromatic ramp (tokens.css --chart-*, plus --foreground for the sixth),
|
|
37
|
+
// so the panel obeys the brand atom and reskins with the host.
|
|
35
38
|
const GREEKS: GreekDef[] = [
|
|
36
|
-
{ key: 'delta', label: 'Delta', symbol: '\u0394', color: 'text-
|
|
37
|
-
{ key: 'gamma', label: 'Gamma', symbol: '\u0393', color: 'text-
|
|
38
|
-
{ key: 'theta', label: 'Theta', symbol: '\u0398', color: 'text-
|
|
39
|
-
{ key: 'vega', label: 'Vega', symbol: '\u03BD', color: 'text-
|
|
40
|
-
{ key: 'rho', label: 'Rho', symbol: '\u03C1', color: 'text-
|
|
41
|
-
{ key: 'iv', label: 'IV', symbol: '\u03C3', color: 'text-
|
|
39
|
+
{ key: 'delta', label: 'Delta', symbol: '\u0394', color: 'text-[var(--foreground)]', barColor: 'bg-[var(--foreground)]', decimals: 4, maxMagnitude: 1 },
|
|
40
|
+
{ key: 'gamma', label: 'Gamma', symbol: '\u0393', color: 'text-[var(--chart-1)]', barColor: 'bg-[var(--chart-1)]', decimals: 4, maxMagnitude: 0.1 },
|
|
41
|
+
{ key: 'theta', label: 'Theta', symbol: '\u0398', color: 'text-[var(--chart-4)]', barColor: 'bg-[var(--chart-4)]', decimals: 4, maxMagnitude: 1 },
|
|
42
|
+
{ key: 'vega', label: 'Vega', symbol: '\u03BD', color: 'text-[var(--chart-2)]', barColor: 'bg-[var(--chart-2)]', decimals: 4, maxMagnitude: 1 },
|
|
43
|
+
{ key: 'rho', label: 'Rho', symbol: '\u03C1', color: 'text-[var(--chart-3)]', barColor: 'bg-[var(--chart-3)]', decimals: 4, maxMagnitude: 1 },
|
|
44
|
+
{ key: 'iv', label: 'IV', symbol: '\u03C3', color: 'text-[var(--chart-5)]', barColor: 'bg-[var(--chart-5)]', decimals: 1, maxMagnitude: 100 },
|
|
42
45
|
];
|
|
43
46
|
|
|
44
47
|
// ---------------------------------------------------------------------------
|
package/src/gui.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The engine, re-exported.
|
|
2
|
+
//
|
|
3
|
+
// `@luxfi/ui/*` is the Lux SKIN; `@luxfi/ui/gui` is the @hanzo/gui ENGINE it is
|
|
4
|
+
// built on. Both ship from one package so a Lux surface never has to reach past
|
|
5
|
+
// its design system to lay out a screen: `styled`, `View`, `Text`, `XStack`,
|
|
6
|
+
// `Sheet`, `Theme`, `useMedia` and the rest are all one import away.
|
|
7
|
+
//
|
|
8
|
+
// It is a separate subpath rather than part of the root barrel on purpose. The
|
|
9
|
+
// engine and the skin both define `Button`, `Input`, `Image`, `Switch`,
|
|
10
|
+
// `Separator`, `Select`, `Tabs`, `Avatar`, `Checkbox`, `Slider` and `Progress`;
|
|
11
|
+
// merging them would collide. Two paths, two clear meanings, no ambiguity:
|
|
12
|
+
//
|
|
13
|
+
// import { Button } from '@luxfi/ui/button' // the Lux-skinned Button
|
|
14
|
+
// import { XStack, Sheet } from '@luxfi/ui/gui' // the raw gui primitive
|
|
15
|
+
//
|
|
16
|
+
// Anything the engine gains, Lux surfaces gain — no wrapper to write first.
|
|
17
|
+
export * from '@hanzo/gui';
|
package/src/heading.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { H1, H2, H3 } from '@hanzo/gui';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
|
|
3
4
|
import { cn } from './utils';
|
|
@@ -9,7 +10,9 @@ export interface HeadingProps extends React.ComponentPropsWithoutRef<'h1'> {
|
|
|
9
10
|
size?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// gui's heading primitives — real h1/h2/h3 on web, sized Text on native, both
|
|
14
|
+
// inheriting the theme's heading font. The Lux type ramp stays in the classes.
|
|
15
|
+
const LEVEL_TAG = { '1': H1, '2': H2, '3': H3 } as const;
|
|
13
16
|
|
|
14
17
|
// base (mobile) + lg (desktop) responsive text styles per level
|
|
15
18
|
//
|
|
@@ -26,18 +29,19 @@ const BASE_CLASSES = 'font-heading text-heading';
|
|
|
26
29
|
|
|
27
30
|
export const Heading = React.forwardRef<HTMLHeadingElement, HeadingProps>(
|
|
28
31
|
function Heading({ level, className, mb, size: _size, style: styleProp, ...rest }, ref) {
|
|
29
|
-
const Tag = level ? LEVEL_TAG[level] :
|
|
32
|
+
const Tag = level ? LEVEL_TAG[level] : H2;
|
|
30
33
|
const levelClasses = level ? LEVEL_CLASSES[level] : undefined;
|
|
31
|
-
const mergedStyle = mb !== undefined
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const mergedStyle = mb !== undefined ?
|
|
35
|
+
{ ...styleProp, marginBottom: typeof mb === 'number' ? `${ mb * 4 }px` : mb } :
|
|
36
|
+
styleProp;
|
|
34
37
|
|
|
35
38
|
return (
|
|
36
39
|
<Tag
|
|
37
|
-
ref={ ref }
|
|
40
|
+
ref={ ref as never }
|
|
41
|
+
unstyled
|
|
38
42
|
className={ cn(BASE_CLASSES, levelClasses, className) }
|
|
39
|
-
style={ mergedStyle }
|
|
40
|
-
{ ...rest }
|
|
43
|
+
style={ mergedStyle as never }
|
|
44
|
+
{ ...(rest as object) }
|
|
41
45
|
/>
|
|
42
46
|
);
|
|
43
47
|
},
|
package/src/iam.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Hanzo IAM read surface — the ONE identity source every Lux surface reads.
|
|
2
|
+
//
|
|
3
|
+
// Who am I, which orgs am I in, which projects does this org have. Every
|
|
4
|
+
// surface needs those three answers and every surface had grown its own way to
|
|
5
|
+
// get them (a hardcoded ORGS table here, a bespoke token store there, a
|
|
6
|
+
// per-app `/branding` map somewhere else). They are IAM's answers, so they are
|
|
7
|
+
// read from IAM.
|
|
8
|
+
//
|
|
9
|
+
// The endpoints are the Casdoor-compat verbs IAM opens to a registered browser
|
|
10
|
+
// origin (see hanzoai/iam internal/cors: get-account, get-organizations,
|
|
11
|
+
// get-organization, get-users, get-organization-projects). CORS decides which
|
|
12
|
+
// ORIGIN may read; the bearer decides WHO — a caller only ever sees what its
|
|
13
|
+
// own principal could already see, so there is nothing to gate here.
|
|
14
|
+
//
|
|
15
|
+
// VALUES ONLY: no React, no storage policy, no redirect. `fetch` is the single
|
|
16
|
+
// effect, and the base URL comes from the host-resolved white-label, so this
|
|
17
|
+
// module is identical on the server, in the browser and in a test.
|
|
18
|
+
|
|
19
|
+
import { resolveWhiteLabel, type WhiteLabel } from './white-label';
|
|
20
|
+
|
|
21
|
+
/** A signed-in principal, as IAM describes it. */
|
|
22
|
+
export interface IamAccount {
|
|
23
|
+
readonly owner: string;
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly displayName?: string;
|
|
26
|
+
readonly email?: string;
|
|
27
|
+
readonly avatar?: string;
|
|
28
|
+
readonly isAdmin?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** An organization the principal can see. */
|
|
32
|
+
export interface IamOrg {
|
|
33
|
+
readonly owner: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly displayName?: string;
|
|
36
|
+
readonly logo?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A project inside an organization (IAM's Org → Workspace → Project tier). */
|
|
40
|
+
export interface IamProject {
|
|
41
|
+
readonly owner: string;
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly displayName?: string;
|
|
44
|
+
readonly organization?: string;
|
|
45
|
+
readonly workspace?: string;
|
|
46
|
+
readonly isDefault?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** IAM's response envelope. `data2` carries the total for paged reads. */
|
|
50
|
+
interface Envelope<T> {
|
|
51
|
+
readonly status?: string;
|
|
52
|
+
readonly msg?: string;
|
|
53
|
+
readonly data?: T;
|
|
54
|
+
readonly data2?: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A refusal from IAM, carrying enough to tell "not signed in" apart from
|
|
59
|
+
* "signed in, and the answer is genuinely empty" — the distinction a switcher
|
|
60
|
+
* has to render honestly.
|
|
61
|
+
*/
|
|
62
|
+
export class IamError extends Error {
|
|
63
|
+
readonly status: number;
|
|
64
|
+
readonly unauthenticated: boolean;
|
|
65
|
+
constructor(message: string, status: number, unauthenticated: boolean) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = 'IamError';
|
|
68
|
+
this.status = status;
|
|
69
|
+
this.unauthenticated = unauthenticated;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** IAM answers 401, or 200 with a sign-in message. Both mean "no session". */
|
|
74
|
+
const SIGNED_OUT = /please sign in|authentication required|unauthorized/i;
|
|
75
|
+
|
|
76
|
+
function isSignedOut(status: number, msg: string): boolean {
|
|
77
|
+
return status === 401 || status === 403 || SIGNED_OUT.test(msg);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The IAM API root for a host — `lux.cloud` → `https://lux.id/v1/iam`. */
|
|
81
|
+
export function iamBase(host?: string | null | WhiteLabel): string {
|
|
82
|
+
const wl = typeof host === 'object' && host !== null ? host : resolveWhiteLabel(host as string | null | undefined);
|
|
83
|
+
return `${ wl.issuer }/v1/iam`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** How a read reaches IAM: where, and with whose bearer. */
|
|
87
|
+
export interface IamCall {
|
|
88
|
+
readonly base: string;
|
|
89
|
+
readonly token?: string | null;
|
|
90
|
+
readonly signal?: AbortSignal;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function read<T>(call: IamCall, verb: string, params: Record<string, string | number>): Promise<T> {
|
|
94
|
+
const url = new URL(`${ call.base }/${ verb }`);
|
|
95
|
+
for (const [ k, v ] of Object.entries(params)) url.searchParams.set(k, String(v));
|
|
96
|
+
|
|
97
|
+
let res: Response;
|
|
98
|
+
try {
|
|
99
|
+
res = await fetch(url.toString(), {
|
|
100
|
+
method: 'GET',
|
|
101
|
+
// The bearer is the whole credential. IAM does not allow credentialed
|
|
102
|
+
// CORS, so sending cookies would only get the response blocked.
|
|
103
|
+
headers: call.token ? { Authorization: `Bearer ${ call.token }` } : {},
|
|
104
|
+
signal: call.signal,
|
|
105
|
+
});
|
|
106
|
+
} catch (e) {
|
|
107
|
+
// A network/CORS failure is NOT "no orgs" — say so, so the caller can show
|
|
108
|
+
// an error instead of an empty switcher.
|
|
109
|
+
throw new IamError(e instanceof Error ? e.message : 'IAM unreachable', 0, false);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const body = (await res.json().catch(() => ({}))) as Envelope<T>;
|
|
113
|
+
const msg = typeof body.msg === 'string' ? body.msg : '';
|
|
114
|
+
if (!res.ok || body.status === 'error') {
|
|
115
|
+
throw new IamError(msg || `IAM ${ verb } failed (${ res.status })`, res.status, isSignedOut(res.status, msg));
|
|
116
|
+
}
|
|
117
|
+
return body.data as T;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Rows per page for the org list — small first paint, more on demand. */
|
|
121
|
+
export const IAM_PAGE_SIZE = 20;
|
|
122
|
+
|
|
123
|
+
export const iam = {
|
|
124
|
+
/** The signed-in principal, or null when there is no session. */
|
|
125
|
+
async account(call: IamCall): Promise<IamAccount | null> {
|
|
126
|
+
try {
|
|
127
|
+
const data = await read<IamAccount | null>(call, 'get-account', {});
|
|
128
|
+
return data && data.name ? data : null;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (e instanceof IamError && e.unauthenticated) return null;
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* One page of the orgs the principal can see. IAM scopes the read to the
|
|
137
|
+
* caller's authority, so a normal member gets their own org and a super
|
|
138
|
+
* admin gets the tenant list — the page never has to decide.
|
|
139
|
+
*/
|
|
140
|
+
async organizations(call: IamCall, page = 0, query = '', pageSize = IAM_PAGE_SIZE): Promise<Array<IamOrg>> {
|
|
141
|
+
const params: Record<string, string | number> = {
|
|
142
|
+
owner: 'admin', // IAM orgs live under the reserved `admin` owner
|
|
143
|
+
p: page + 1, // the backend page is 1-based
|
|
144
|
+
pageSize,
|
|
145
|
+
sortField: 'name',
|
|
146
|
+
sortOrder: 'ascending',
|
|
147
|
+
};
|
|
148
|
+
const q = query.trim();
|
|
149
|
+
if (q) {
|
|
150
|
+
params.field = 'name';
|
|
151
|
+
params.value = q;
|
|
152
|
+
}
|
|
153
|
+
return (await read<Array<IamOrg> | null>(call, 'get-organizations', params)) ?? [];
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/** One organization by name. */
|
|
157
|
+
async organization(call: IamCall, name: string): Promise<IamOrg | null> {
|
|
158
|
+
return (await read<IamOrg | null>(call, 'get-organization', { id: `admin/${ name }` })) ?? null;
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/** Members of an organization. */
|
|
162
|
+
async users(call: IamCall, org: string): Promise<Array<IamAccount>> {
|
|
163
|
+
return (await read<Array<IamAccount> | null>(call, 'get-users', { owner: org })) ?? [];
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/** The organization's projects — IAM keys this read by `organization`. */
|
|
167
|
+
async projects(call: IamCall, org: string): Promise<Array<IamProject>> {
|
|
168
|
+
return (await read<Array<IamProject> | null>(call, 'get-organization-projects', { organization: org })) ?? [];
|
|
169
|
+
},
|
|
170
|
+
};
|
package/src/icons.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The Lux icon set — one set, one import path.
|
|
2
|
+
//
|
|
3
|
+
// Lucide, exactly as @hanzo/gui standardises on it. @hanzo/gui ships the same
|
|
4
|
+
// drawings twice, once per runtime — `@hanzogui/lucide-icons-2` for React
|
|
5
|
+
// Native, `lucide-react` for the DOM — and the geometry is identical
|
|
6
|
+
// path-for-path, so this is one set and not two. Lux surfaces are DOM, so the
|
|
7
|
+
// DOM delivery is what `@luxfi/ui/icons` hands them.
|
|
8
|
+
//
|
|
9
|
+
// import { Box, Zap, Info } from '@luxfi/ui/icons';
|
|
10
|
+
//
|
|
11
|
+
// The theming is the set's own contract, and it is why nothing here wraps it:
|
|
12
|
+
// every Lucide glyph is a 24x24, 2px, round-cap outline stroked in
|
|
13
|
+
// `currentColor`, so it takes the colour of the text it sits next to and stays
|
|
14
|
+
// monochrome by construction. Size it with `className="size-4"`, colour it by
|
|
15
|
+
// colouring its container — never by reaching for a different drawing.
|
|
16
|
+
//
|
|
17
|
+
// A second icon package inside a Lux surface is a bug, not a preference.
|
|
18
|
+
export * from 'lucide-react';
|