@luxfi/ui 7.3.2 → 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.
- package/dist/alert.cjs +2 -2
- package/dist/alert.js +1 -1
- package/dist/avatar.cjs +7 -6
- package/dist/avatar.js +7 -5
- package/dist/badge.cjs +36 -38
- package/dist/badge.js +31 -33
- package/dist/bank.cjs +13 -13
- package/dist/bank.d.cts +31 -24
- package/dist/bank.d.ts +31 -24
- package/dist/bank.js +1 -1
- package/dist/button.cjs +3 -3
- package/dist/button.js +1 -1
- 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 +950 -0
- package/dist/chrome.d.cts +93 -0
- package/dist/chrome.d.ts +93 -0
- package/dist/chrome.js +921 -0
- package/dist/close-button.cjs +2 -2
- package/dist/close-button.js +1 -1
- package/dist/dialog.cjs +13 -14
- package/dist/dialog.d.cts +58 -5
- package/dist/dialog.d.ts +58 -5
- package/dist/dialog.js +1 -2
- package/dist/drawer.cjs +87 -21
- 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 +206 -0
- package/dist/iam.d.cts +65 -0
- package/dist/iam.d.ts +65 -0
- package/dist/iam.js +201 -0
- package/dist/icon-button.cjs +2 -2
- package/dist/icon-button.js +1 -1
- 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 +420 -0
- package/dist/identity.d.cts +64 -0
- package/dist/identity.d.ts +64 -0
- package/dist/identity.js +398 -0
- package/dist/index.cjs +1699 -860
- package/dist/index.d.cts +12 -4
- package/dist/index.d.ts +12 -4
- package/dist/index.js +1618 -802
- package/dist/input.cjs +13 -3
- package/dist/input.d.cts +6 -0
- package/dist/input.d.ts +6 -0
- package/dist/input.js +13 -3
- package/dist/lux.config.cjs +77 -0
- package/dist/lux.config.d.cts +433 -0
- package/dist/lux.config.d.ts +433 -0
- package/dist/lux.config.js +69 -0
- package/dist/menu.cjs +4 -4
- package/dist/menu.js +4 -4
- package/dist/next.cjs +99 -0
- package/dist/next.d.cts +29 -0
- package/dist/next.d.ts +29 -0
- package/dist/next.js +96 -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/popover.cjs +8 -9
- package/dist/popover.js +1 -2
- package/dist/progress.cjs +7 -6
- package/dist/progress.js +7 -5
- package/dist/provider.cjs +199 -5
- package/dist/provider.d.cts +16 -3
- package/dist/provider.d.ts +16 -3
- package/dist/provider.js +195 -6
- 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 +38 -41
- package/dist/tag.js +31 -34
- package/dist/textarea.cjs +13 -3
- package/dist/textarea.d.cts +2 -0
- package/dist/textarea.d.ts +2 -0
- package/dist/textarea.js +13 -3
- package/dist/tooltip.cjs +31 -33
- package/dist/tooltip.js +25 -27
- 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 +40 -0
- package/dist/vite.d.cts +29 -0
- package/dist/vite.d.ts +29 -0
- package/dist/vite.js +38 -0
- package/dist/white-label.cjs +132 -0
- package/dist/white-label.d.cts +59 -0
- package/dist/white-label.d.ts +59 -0
- package/dist/white-label.js +124 -0
- package/package.json +95 -17
- package/src/avatar.tsx +22 -16
- package/src/bank.tsx +2 -2
- package/src/button.tsx +1 -1
- package/src/checkbox.tsx +15 -10
- package/src/chrome.tsx +459 -0
- package/src/close-button.tsx +1 -1
- package/src/dialog.tsx +1 -1
- package/src/drawer.tsx +83 -10
- 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/icon-button.tsx +1 -1
- package/src/icons.ts +18 -0
- package/src/identity.tsx +318 -0
- package/src/index.ts +50 -1
- package/src/input.tsx +24 -4
- package/src/lux.config.ts +82 -0
- package/src/menu.tsx +4 -4
- package/src/next.ts +142 -0
- package/src/option-chain.tsx +1 -1
- package/src/option-position.tsx +2 -1
- package/src/pnl-diagram.tsx +11 -6
- package/src/popover.tsx +1 -1
- package/src/progress.tsx +15 -8
- package/src/provider.tsx +92 -12
- 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 +18 -4
- package/src/tooltip.tsx +4 -23
- package/src/use-narrow.ts +40 -0
- package/src/vite.ts +78 -0
- package/src/white-label.ts +240 -0
- package/tokens.css +544 -319
- package/src/tokens.css +0 -438
package/src/slider.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Slider as GuiSlider } from '@hanzo/gui';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
|
|
4
4
|
import { cn } from './utils';
|
|
@@ -20,9 +20,10 @@ export interface SliderProps {
|
|
|
20
20
|
readonly className?: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// gui's Slider: pointer, touch and keyboard from one implementation, with the
|
|
24
|
+
// drag handled as a pan gesture by the engine rather than by mouse events — so it
|
|
25
|
+
// works under a finger, not only under a cursor. `role=slider` and the arrow-key
|
|
26
|
+
// contract come from the primitive.
|
|
26
27
|
export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
|
|
27
28
|
function Slider(props, ref) {
|
|
28
29
|
const {
|
|
@@ -35,7 +36,7 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
|
|
|
35
36
|
max = 100,
|
|
36
37
|
step,
|
|
37
38
|
disabled,
|
|
38
|
-
orientation,
|
|
39
|
+
orientation = 'horizontal',
|
|
39
40
|
name,
|
|
40
41
|
onValueChange,
|
|
41
42
|
onValueCommit,
|
|
@@ -51,6 +52,17 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
|
|
|
51
52
|
|
|
52
53
|
const hasMarkLabel = Boolean(marks?.some((mark) => mark.label));
|
|
53
54
|
|
|
55
|
+
// gui signals the end of a drag without echoing the value, so keep the last
|
|
56
|
+
// one to satisfy the Chakra-shaped `onValueCommit(value)` contract.
|
|
57
|
+
const latest = React.useRef<Array<number>>(value ?? defaultValue ?? []);
|
|
58
|
+
const handleValueChange = React.useCallback((next: Array<number>) => {
|
|
59
|
+
latest.current = next;
|
|
60
|
+
onValueChange?.(next);
|
|
61
|
+
}, [ onValueChange ]);
|
|
62
|
+
const handleSlideEnd = React.useCallback(() => {
|
|
63
|
+
onValueCommit?.(latest.current);
|
|
64
|
+
}, [ onValueCommit ]);
|
|
65
|
+
|
|
54
66
|
return (
|
|
55
67
|
<div className={ cn('flex flex-col gap-1', className) }>
|
|
56
68
|
{ label && !showValue && (
|
|
@@ -69,8 +81,9 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
|
|
|
69
81
|
</div>
|
|
70
82
|
) }
|
|
71
83
|
|
|
72
|
-
<
|
|
73
|
-
ref={ ref }
|
|
84
|
+
<GuiSlider
|
|
85
|
+
ref={ ref as never }
|
|
86
|
+
unstyled
|
|
74
87
|
className={ cn(
|
|
75
88
|
'relative flex w-full touch-none select-none items-center',
|
|
76
89
|
hasMarkLabel && 'mb-6',
|
|
@@ -83,23 +96,26 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
|
|
|
83
96
|
disabled={ disabled }
|
|
84
97
|
orientation={ orientation }
|
|
85
98
|
name={ name }
|
|
86
|
-
onValueChange={
|
|
87
|
-
|
|
99
|
+
onValueChange={ handleValueChange }
|
|
100
|
+
onSlideEnd={ handleSlideEnd }
|
|
88
101
|
>
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`h-[${ TRACK_HEIGHT }px]`,
|
|
93
|
-
) }
|
|
102
|
+
<GuiSlider.Track
|
|
103
|
+
unstyled
|
|
104
|
+
className="relative grow h-1.5 rounded-full bg-border-divider"
|
|
94
105
|
>
|
|
95
|
-
<
|
|
96
|
-
|
|
106
|
+
<GuiSlider.TrackActive
|
|
107
|
+
unstyled
|
|
108
|
+
className="absolute h-full rounded-full bg-link-primary"
|
|
109
|
+
/>
|
|
110
|
+
</GuiSlider.Track>
|
|
97
111
|
|
|
98
112
|
{ resolvedValue?.map((_, index) => (
|
|
99
|
-
<
|
|
113
|
+
<GuiSlider.Thumb
|
|
100
114
|
key={ index }
|
|
115
|
+
index={ index }
|
|
116
|
+
unstyled
|
|
101
117
|
className={ cn(
|
|
102
|
-
|
|
118
|
+
'block h-5 w-5',
|
|
103
119
|
'rounded-full border-2 border-link-primary bg-white',
|
|
104
120
|
'shadow-sm transition-colors',
|
|
105
121
|
'hover:border-link-primary-hover',
|
|
@@ -108,7 +124,7 @@ export const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
|
|
|
108
124
|
) }
|
|
109
125
|
/>
|
|
110
126
|
)) }
|
|
111
|
-
</
|
|
127
|
+
</GuiSlider>
|
|
112
128
|
|
|
113
129
|
<SliderMarks marks={ marks } min={ min } max={ max }/>
|
|
114
130
|
</div>
|
package/src/strategy-builder.tsx
CHANGED
|
@@ -207,9 +207,10 @@ function LegRow({ leg, index, strikes, expirations, removable, onChange, onRemov
|
|
|
207
207
|
type="button"
|
|
208
208
|
className={cn(
|
|
209
209
|
'px-2 py-0.5 rounded text-xs font-medium transition-colors',
|
|
210
|
+
// Call vs put is a rank difference, not a hue difference.
|
|
210
211
|
leg.type === 'call'
|
|
211
|
-
? 'bg-
|
|
212
|
-
: 'bg-
|
|
212
|
+
? 'bg-[var(--secondary)] text-[var(--foreground)]'
|
|
213
|
+
: 'bg-[var(--muted)] text-[var(--muted-foreground)]',
|
|
213
214
|
)}
|
|
214
215
|
onClick={() => update({ type: leg.type === 'call' ? 'put' : 'call' })}
|
|
215
216
|
>
|
package/src/switch.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
// chakra() HOC removed — pure Radix + Tailwind
|
|
1
|
+
import { Switch as GuiSwitch } from '@hanzo/gui';
|
|
3
2
|
import * as React from 'react';
|
|
4
3
|
|
|
5
4
|
import { cn } from './utils';
|
|
@@ -7,20 +6,23 @@ import { cn } from './utils';
|
|
|
7
6
|
const NOOP = () => { /* noop */ };
|
|
8
7
|
|
|
9
8
|
// ─── Size mappings ────────────────────────────────────────────────────
|
|
9
|
+
// Paint and box size only. Thumb travel is NOT a hardcoded translate any more:
|
|
10
|
+
// gui measures the track and the knob and moves the knob by the difference, so
|
|
11
|
+
// the switch stays correct at any size and on native.
|
|
10
12
|
const SIZE_CLASSES = {
|
|
11
13
|
sm: {
|
|
12
14
|
root: 'h-4 w-7',
|
|
13
|
-
thumb: 'h-3 w-3
|
|
15
|
+
thumb: 'h-3 w-3',
|
|
14
16
|
label: 'text-xs',
|
|
15
17
|
},
|
|
16
18
|
md: {
|
|
17
19
|
root: 'h-5 w-9',
|
|
18
|
-
thumb: 'h-4 w-4
|
|
20
|
+
thumb: 'h-4 w-4',
|
|
19
21
|
label: 'text-sm',
|
|
20
22
|
},
|
|
21
23
|
lg: {
|
|
22
24
|
root: 'h-6 w-11',
|
|
23
|
-
thumb: 'h-5 w-5
|
|
25
|
+
thumb: 'h-5 w-5',
|
|
24
26
|
label: 'text-base',
|
|
25
27
|
},
|
|
26
28
|
} as const;
|
|
@@ -72,11 +74,8 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
|
|
|
72
74
|
setInternalChecked(nextChecked);
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// We call with a single object arg; if the consumer ignores it, that's fine.
|
|
78
|
-
(onCheckedChange as (details: { checked: boolean }) => void)({ checked: nextChecked });
|
|
79
|
-
}
|
|
77
|
+
// Chakra-shaped callback ({ checked }) — the contract consumers already use.
|
|
78
|
+
onCheckedChange?.({ checked: nextChecked });
|
|
80
79
|
},
|
|
81
80
|
[ isControlled, onCheckedChange ],
|
|
82
81
|
);
|
|
@@ -97,7 +96,8 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
|
|
|
97
96
|
data-disabled={ disabled || undefined }
|
|
98
97
|
{ ...rest }
|
|
99
98
|
>
|
|
100
|
-
<
|
|
99
|
+
<GuiSwitch
|
|
100
|
+
unstyled
|
|
101
101
|
checked={ checkedState }
|
|
102
102
|
onCheckedChange={ handleCheckedChange }
|
|
103
103
|
disabled={ disabled }
|
|
@@ -115,11 +115,12 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
|
|
|
115
115
|
{ checkedState ? trackLabel.on : trackLabel.off }
|
|
116
116
|
</span>
|
|
117
117
|
) }
|
|
118
|
-
<
|
|
118
|
+
<GuiSwitch.Thumb
|
|
119
|
+
unstyled
|
|
120
|
+
data-thumb
|
|
121
|
+
transition="quicker"
|
|
119
122
|
className={ cn(
|
|
120
123
|
'block rounded-full bg-white shadow-sm',
|
|
121
|
-
'transition-transform duration-200',
|
|
122
|
-
'translate-x-0.5',
|
|
123
124
|
sizeClasses.thumb,
|
|
124
125
|
) }
|
|
125
126
|
>
|
|
@@ -128,8 +129,8 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
|
|
|
128
129
|
{ checkedState ? thumbLabel.on : thumbLabel.off }
|
|
129
130
|
</span>
|
|
130
131
|
) }
|
|
131
|
-
</
|
|
132
|
-
</
|
|
132
|
+
</GuiSwitch.Thumb>
|
|
133
|
+
</GuiSwitch>
|
|
133
134
|
{ /* Hidden native input for form compat and ref forwarding */ }
|
|
134
135
|
<input
|
|
135
136
|
ref={ ref }
|
package/src/textarea.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TextArea as GuiTextArea } from '@hanzo/gui';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
|
|
3
4
|
import { cn } from './utils';
|
|
@@ -34,15 +35,28 @@ type TextareaVariant = 'outline' | 'filled' | 'unstyled';
|
|
|
34
35
|
export interface TextareaProps extends Omit<React.ComponentPropsWithRef<'textarea'>, 'size'> {
|
|
35
36
|
readonly size?: TextareaSize;
|
|
36
37
|
readonly variant?: TextareaVariant;
|
|
38
|
+
/** Tamagui / React-Native convention — bridged to native `onChange` (see Input). */
|
|
39
|
+
readonly onChangeText?: (text: string) => void;
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
// gui's TextArea — multiline TextInput on native, `<textarea>` on web — wearing
|
|
43
|
+
// the Lux size/variant classes.
|
|
39
44
|
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
40
|
-
({ size = 'md', variant = 'outline', className, ...rest }, ref) => {
|
|
45
|
+
({ size = 'md', variant = 'outline', className, onChange, onChangeText, ...rest }, ref) => {
|
|
46
|
+
const handleChange = React.useCallback(
|
|
47
|
+
(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
|
48
|
+
onChange?.(event);
|
|
49
|
+
onChangeText?.(event.target.value);
|
|
50
|
+
},
|
|
51
|
+
[ onChange, onChangeText ],
|
|
52
|
+
);
|
|
41
53
|
return (
|
|
42
|
-
<
|
|
43
|
-
ref={ ref }
|
|
54
|
+
<GuiTextArea
|
|
55
|
+
ref={ ref as never }
|
|
56
|
+
unstyled
|
|
44
57
|
className={ cn(TEXTAREA_BASE, TEXTAREA_SIZE_CLASSES[size], TEXTAREA_VARIANT_CLASSES[variant], className) }
|
|
45
|
-
{ ...rest }
|
|
58
|
+
{ ...(rest as object) }
|
|
59
|
+
onChange={ handleChange as never }
|
|
46
60
|
/>
|
|
47
61
|
);
|
|
48
62
|
},
|
package/src/tooltip.tsx
CHANGED
|
@@ -1,36 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Tooltip as GuiTooltip,
|
|
3
3
|
TooltipGroup as GuiTooltipGroup,
|
|
4
|
-
} from '@
|
|
4
|
+
} from '@hanzo/gui';
|
|
5
5
|
import { useClickAway } from '@uidotdev/usehooks';
|
|
6
6
|
import * as React from 'react';
|
|
7
7
|
|
|
8
|
+
import { useIsTouch } from './use-narrow';
|
|
8
9
|
import { cn } from './utils';
|
|
9
10
|
|
|
10
|
-
// Inline useIsMobile hook (matches lg breakpoint = 1024px)
|
|
11
|
-
const LG_BREAKPOINT = 1024;
|
|
12
|
-
|
|
13
|
-
function useIsMobile(): boolean {
|
|
14
|
-
const [ isMobile, setIsMobile ] = React.useState(() => {
|
|
15
|
-
if (typeof window === 'undefined') return false;
|
|
16
|
-
return window.innerWidth < LG_BREAKPOINT;
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
React.useEffect(() => {
|
|
20
|
-
const mql = window.matchMedia(`(max-width: ${ LG_BREAKPOINT - 1 }px)`);
|
|
21
|
-
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
|
|
22
|
-
setIsMobile(mql.matches);
|
|
23
|
-
mql.addEventListener('change', handler);
|
|
24
|
-
return () => mql.removeEventListener('change', handler);
|
|
25
|
-
}, []);
|
|
26
|
-
|
|
27
|
-
return isMobile;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
11
|
/** Map placement string to @hanzogui Popper placement prop. */
|
|
31
12
|
function mapPlacement(p: string | undefined): string | undefined {
|
|
32
13
|
if (!p) return undefined;
|
|
33
|
-
// @
|
|
14
|
+
// @hanzo/gui uses same placement strings as floating-ui
|
|
34
15
|
return p;
|
|
35
16
|
}
|
|
36
17
|
|
|
@@ -86,7 +67,7 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
|
|
86
67
|
const [ open, setOpen ] = React.useState<boolean>(defaultOpen);
|
|
87
68
|
const timeoutRef = React.useRef<number | null>(null);
|
|
88
69
|
|
|
89
|
-
const isMobile =
|
|
70
|
+
const isMobile = useIsTouch();
|
|
90
71
|
|
|
91
72
|
const handleOpenChange = React.useCallback((nextOpen: boolean) => {
|
|
92
73
|
setOpen(nextOpen);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
// ONE viewport hook for the whole design system.
|
|
6
|
+
//
|
|
7
|
+
// Three components used to carry their own copy of the same `matchMedia`
|
|
8
|
+
// dance at three different breakpoints, so "is this phone-sized?" had three
|
|
9
|
+
// answers. It has one now, and the breakpoints are named values rather than
|
|
10
|
+
// magic numbers buried in a component.
|
|
11
|
+
//
|
|
12
|
+
// SSR-safe by construction: the first render is always `false` on both the
|
|
13
|
+
// server and the client (there is no viewport during prerender, and guessing
|
|
14
|
+
// one produces a hydration mismatch), then the real answer arrives in an
|
|
15
|
+
// effect before paint.
|
|
16
|
+
|
|
17
|
+
/** Below this the UI is a phone: menus become sheets, drawers drag. */
|
|
18
|
+
export const NARROW_PX = 640;
|
|
19
|
+
|
|
20
|
+
/** Below this the pointer is a finger: hover-only affordances are suppressed. */
|
|
21
|
+
export const TOUCH_PX = 1024;
|
|
22
|
+
|
|
23
|
+
/** True while the viewport is narrower than `px`. */
|
|
24
|
+
export function useBelow(px: number): boolean {
|
|
25
|
+
const [ below, setBelow ] = React.useState(false);
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
const mql = window.matchMedia(`(max-width: ${ px - 1 }px)`);
|
|
28
|
+
const apply = () => setBelow(mql.matches);
|
|
29
|
+
apply();
|
|
30
|
+
mql.addEventListener('change', apply);
|
|
31
|
+
return () => mql.removeEventListener('change', apply);
|
|
32
|
+
}, [ px ]);
|
|
33
|
+
return below;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Phone-sized: present menus as sheets. */
|
|
37
|
+
export const useIsNarrow = (): boolean => useBelow(NARROW_PX);
|
|
38
|
+
|
|
39
|
+
/** Touch-sized: no hover, so hover-only affordances must not be the only path. */
|
|
40
|
+
export const useIsTouch = (): boolean => useBelow(TOUCH_PX);
|
package/src/vite.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The bundler wiring the gui engine needs on web, as ONE plugin.
|
|
2
|
+
//
|
|
3
|
+
// @hanzo/gui is authored against React Native primitives, so a web bundle has to
|
|
4
|
+
// alias `react-native` → `react-native-web`, pre-bundle the CJS-only pieces, keep
|
|
5
|
+
// exactly one physical copy of React and of the engine, and define the platform
|
|
6
|
+
// flags the engine reads at module scope. Every Lux surface needs the identical
|
|
7
|
+
// six-part incantation, and every surface that hand-rolled it got a different
|
|
8
|
+
// subset — which is how you end up debugging `createContext of undefined` in one
|
|
9
|
+
// app and a duplicate-React theme context in another.
|
|
10
|
+
//
|
|
11
|
+
// import { luxUi } from '@luxfi/ui/vite'
|
|
12
|
+
// export default defineConfig({ plugins: [react(), luxUi()] })
|
|
13
|
+
//
|
|
14
|
+
// Node-only (it is a build-time module) and dependency-free: it returns a plain
|
|
15
|
+
// Vite plugin object, so it does not force `vite` into anyone's runtime graph.
|
|
16
|
+
|
|
17
|
+
interface LuxUiViteOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Extra packages to keep to a single physical copy. React, the gui engine and
|
|
20
|
+
* TanStack Query are always deduped.
|
|
21
|
+
*/
|
|
22
|
+
readonly dedupe?: ReadonlyArray<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Extra packages to pre-bundle. The engine, react-native-web and the CJS-only
|
|
25
|
+
* colour helper are always included.
|
|
26
|
+
*/
|
|
27
|
+
readonly include?: ReadonlyArray<string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Minimal structural shape of a Vite plugin — avoids a hard `vite` type dep. */
|
|
31
|
+
interface LuxUiVitePlugin {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly config: () => {
|
|
34
|
+
define: Record<string, string>;
|
|
35
|
+
resolve: { alias: Record<string, string>; dedupe: Array<string> };
|
|
36
|
+
optimizeDeps: { include: Array<string> };
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEDUPE = [
|
|
41
|
+
'react',
|
|
42
|
+
'react-dom',
|
|
43
|
+
'@tanstack/react-query',
|
|
44
|
+
'@hanzo/gui',
|
|
45
|
+
'@hanzogui/core',
|
|
46
|
+
'@hanzogui/web',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const INCLUDE = [
|
|
50
|
+
'react-native-web',
|
|
51
|
+
'@react-native/normalize-color',
|
|
52
|
+
'@hanzo/gui',
|
|
53
|
+
'@hanzogui/core',
|
|
54
|
+
'@luxfi/ui',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export function luxUi(options: LuxUiViteOptions = {}): LuxUiVitePlugin {
|
|
58
|
+
return {
|
|
59
|
+
name: '@luxfi/ui',
|
|
60
|
+
config: () => ({
|
|
61
|
+
// The engine branches on these at module scope; without them it takes the
|
|
62
|
+
// native path in a browser bundle.
|
|
63
|
+
define: {
|
|
64
|
+
'process.env.EXPO_OS': JSON.stringify('web'),
|
|
65
|
+
'process.env.IS_WEB': JSON.stringify('true'),
|
|
66
|
+
},
|
|
67
|
+
resolve: {
|
|
68
|
+
alias: { 'react-native': 'react-native-web' },
|
|
69
|
+
dedupe: [ ...new Set([ ...DEDUPE, ...(options.dedupe ?? []) ]) ],
|
|
70
|
+
},
|
|
71
|
+
optimizeDeps: {
|
|
72
|
+
include: [ ...new Set([ ...INCLUDE, ...(options.include ?? []) ]) ],
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type { LuxUiViteOptions };
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// White-label resolution — BY HOSTNAME, never per deployment.
|
|
2
|
+
//
|
|
3
|
+
// One image, many brands. Brand, gui theme row, IAM client id and OIDC issuer
|
|
4
|
+
// are all a pure function of the Host header, so the same container serves
|
|
5
|
+
// lux.cloud, zoo.network, hanzo.ai and pars.id without a rebuild, an env var or
|
|
6
|
+
// a per-deployment override. Nothing here reads process.env.
|
|
7
|
+
//
|
|
8
|
+
// This module is VALUES ONLY (no React, no DOM) so it resolves identically in
|
|
9
|
+
// three places that must agree:
|
|
10
|
+
// - the Next/SSR server, from `req.headers.host`
|
|
11
|
+
// - the browser, from `location.host`
|
|
12
|
+
// - an edge/middleware rewrite, from the incoming URL
|
|
13
|
+
//
|
|
14
|
+
// Logos, SEO copy and marketing palettes live in @luxfi/brand. This is the
|
|
15
|
+
// runtime identity the UI layer needs and nothing more.
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Branding = VALUE, not place.
|
|
19
|
+
// The ONE spot brand hex is allowed to live (project rule: never hardcode hex
|
|
20
|
+
// in components). It lives HERE, beside the identity table it colours, rather
|
|
21
|
+
// than in ./lux.config — that module builds the gui engine, and an identity
|
|
22
|
+
// value that drags a Tamagui config behind it cannot be read by a server
|
|
23
|
+
// component, a route handler or the `@luxfi/ui/iam` client. lux.config imports
|
|
24
|
+
// these; nothing imports lux.config to get them.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
export const LUX_BRAND = {
|
|
27
|
+
lux: '#7000FF',
|
|
28
|
+
zoo: '#6C5EFB',
|
|
29
|
+
hanzo: '#EA580C',
|
|
30
|
+
pars: '#1C3879',
|
|
31
|
+
// bootno.de is its own white label with its own IdP (id.bootno.de) — accent
|
|
32
|
+
// taken from the bootnode console's own brand default, not invented here.
|
|
33
|
+
bootnode: '#3B82F6',
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export const PARS_GOLD = '#C8A45C';
|
|
37
|
+
|
|
38
|
+
export type LuxBrand = keyof typeof LUX_BRAND;
|
|
39
|
+
|
|
40
|
+
export type Org = LuxBrand;
|
|
41
|
+
|
|
42
|
+
export interface OrgIdentity {
|
|
43
|
+
/** Org id — also the gui child-theme suffix (`dark_lux`, `dark_zoo`, …). */
|
|
44
|
+
readonly org: Org;
|
|
45
|
+
/** Display name for chrome (header, title, sign-in copy). */
|
|
46
|
+
readonly name: string;
|
|
47
|
+
/** Canonical apex domain. */
|
|
48
|
+
readonly domain: string;
|
|
49
|
+
/** IAM/OIDC host. The issuer is `https://` + this. */
|
|
50
|
+
readonly iamDomain: string;
|
|
51
|
+
/** Brand accent hex — the same value the gui child-theme row is built from. */
|
|
52
|
+
readonly accent: string;
|
|
53
|
+
/** Foreground used on top of the accent. */
|
|
54
|
+
readonly accentForeground: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The ONE table. Every hostname in the fleet resolves into one of these rows.
|
|
58
|
+
const ORGS: Readonly<Record<Org, OrgIdentity>> = {
|
|
59
|
+
lux: {
|
|
60
|
+
org: 'lux',
|
|
61
|
+
name: 'Lux',
|
|
62
|
+
domain: 'lux.network',
|
|
63
|
+
iamDomain: 'lux.id',
|
|
64
|
+
accent: LUX_BRAND.lux,
|
|
65
|
+
accentForeground: '#FFFFFF',
|
|
66
|
+
},
|
|
67
|
+
zoo: {
|
|
68
|
+
org: 'zoo',
|
|
69
|
+
name: 'Zoo',
|
|
70
|
+
domain: 'zoo.ngo',
|
|
71
|
+
// Zoo's IdP is `id.zoo.network`, not a `zoo.id` apex — the one row in this
|
|
72
|
+
// table where the issuer is not `<org>.id`, and the one every hand-rolled
|
|
73
|
+
// per-app branding map got wrong.
|
|
74
|
+
iamDomain: 'id.zoo.network',
|
|
75
|
+
accent: LUX_BRAND.zoo,
|
|
76
|
+
accentForeground: '#FFFFFF',
|
|
77
|
+
},
|
|
78
|
+
bootnode: {
|
|
79
|
+
org: 'bootnode',
|
|
80
|
+
name: 'Bootnode',
|
|
81
|
+
domain: 'bootno.de',
|
|
82
|
+
iamDomain: 'id.bootno.de',
|
|
83
|
+
accent: LUX_BRAND.lux,
|
|
84
|
+
accentForeground: '#FFFFFF',
|
|
85
|
+
},
|
|
86
|
+
hanzo: {
|
|
87
|
+
org: 'hanzo',
|
|
88
|
+
name: 'Hanzo',
|
|
89
|
+
domain: 'hanzo.ai',
|
|
90
|
+
iamDomain: 'hanzo.id',
|
|
91
|
+
accent: LUX_BRAND.hanzo,
|
|
92
|
+
accentForeground: '#FFFFFF',
|
|
93
|
+
},
|
|
94
|
+
pars: {
|
|
95
|
+
org: 'pars',
|
|
96
|
+
name: 'Pars',
|
|
97
|
+
domain: 'pars.id',
|
|
98
|
+
iamDomain: 'pars.id',
|
|
99
|
+
accent: LUX_BRAND.pars,
|
|
100
|
+
accentForeground: PARS_GOLD,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Registrable domains that belong to each org. Matched against the hostname
|
|
105
|
+
// SUFFIX on a label boundary, so `mpc.lux.cloud` and `lux.cloud` both hit `lux`
|
|
106
|
+
// while `notlux.cloud` does not.
|
|
107
|
+
//
|
|
108
|
+
// The third slot names the app at the domain's APEX. It is usually derivable
|
|
109
|
+
// (`lux.cloud` → `cloud` → client `lux-cloud`), so it is only written where the
|
|
110
|
+
// registered client says otherwise — `bootno.de` is `bootnode-platform`, not
|
|
111
|
+
// `bootnode-de`. A client id is a registration, not a guess.
|
|
112
|
+
const DOMAIN_ORG: ReadonlyArray<readonly [string, Org, string?]> = [
|
|
113
|
+
[ 'lux.network', 'lux' ],
|
|
114
|
+
[ 'lux.cloud', 'lux' ],
|
|
115
|
+
[ 'lux.id', 'lux' ],
|
|
116
|
+
[ 'lux.finance', 'lux' ],
|
|
117
|
+
[ 'zoo.ngo', 'zoo' ],
|
|
118
|
+
[ 'zoo.network', 'zoo' ],
|
|
119
|
+
[ 'zoo.cloud', 'zoo' ],
|
|
120
|
+
[ 'zoo.id', 'zoo' ],
|
|
121
|
+
[ 'hanzo.ai', 'hanzo' ],
|
|
122
|
+
[ 'hanzo.cloud', 'hanzo' ],
|
|
123
|
+
[ 'hanzo.id', 'hanzo' ],
|
|
124
|
+
[ 'pars.id', 'pars' ],
|
|
125
|
+
[ 'parsdao.org', 'pars' ],
|
|
126
|
+
[ 'bootno.de', 'bootnode', 'platform' ],
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
export const DEFAULT_ORG: Org = 'lux';
|
|
130
|
+
|
|
131
|
+
export interface WhiteLabel extends OrgIdentity {
|
|
132
|
+
/** Hostname the identity was resolved from, port stripped, lowercased. */
|
|
133
|
+
readonly host: string;
|
|
134
|
+
/** App slug taken from the hostname — `mpc.lux.cloud` → `mpc`. */
|
|
135
|
+
readonly app: string;
|
|
136
|
+
/** gui child-theme row to hand GuiProvider — `dark_lux`, `dark_zoo`, … */
|
|
137
|
+
readonly theme: string;
|
|
138
|
+
/** OIDC issuer — `https://lux.id`. */
|
|
139
|
+
readonly issuer: string;
|
|
140
|
+
/** IAM client id, `<org>-<app>` per the fleet naming scheme — `lux-mpc`. */
|
|
141
|
+
readonly clientId: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Strip the port and any trailing dot, lowercase. `LUX.cloud:3000` → `lux.cloud`. */
|
|
145
|
+
function normalizeHost(host: string): string {
|
|
146
|
+
const noPort = host.trim().toLowerCase().split(':')[0] ?? '';
|
|
147
|
+
return noPort.replace(/\.$/, '');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** true when `host` is `domain` or a subdomain of it (label boundary respected). */
|
|
151
|
+
function isUnder(host: string, domain: string): boolean {
|
|
152
|
+
return host === domain || host.endsWith(`.${ domain }`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The registrable domain this host sits under, or undefined for unknown hosts. */
|
|
156
|
+
function matchDomain(host: string): readonly [string, Org, string?] | undefined {
|
|
157
|
+
// Longest domain first so `zoo.network` wins over a hypothetical `network`.
|
|
158
|
+
let best: readonly [string, Org, string?] | undefined;
|
|
159
|
+
for (const entry of DOMAIN_ORG) {
|
|
160
|
+
if (isUnder(host, entry[0]) && (!best || entry[0].length > best[0].length)) {
|
|
161
|
+
best = entry;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return best;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* App slug for the IAM client id.
|
|
169
|
+
*
|
|
170
|
+
* `mpc.lux.cloud` → `mpc` (leftmost label)
|
|
171
|
+
* `lux.cloud` → `cloud` (apex: the service name, not the org)
|
|
172
|
+
* `www.lux.network`→ `network` (www is chrome, not an app)
|
|
173
|
+
* `localhost` → `local`
|
|
174
|
+
*/
|
|
175
|
+
function appSlug(host: string, domain: string, apexApp?: string): string {
|
|
176
|
+
const sub = host === domain ? '' : host.slice(0, -(domain.length + 1));
|
|
177
|
+
const leftmost = sub.split('.')[0] ?? '';
|
|
178
|
+
if (leftmost && leftmost !== 'www') return leftmost;
|
|
179
|
+
// Apex (or www) — the registered name when the table gives one, else the
|
|
180
|
+
// domain's own service label.
|
|
181
|
+
if (apexApp) return apexApp;
|
|
182
|
+
const label = domain.split('.')[0] ?? '';
|
|
183
|
+
const tld = domain.split('.').slice(1).join('.');
|
|
184
|
+
// `lux.network` → `network`, `lux.cloud` → `cloud`, `pars.id` → `id`.
|
|
185
|
+
return tld.split('.')[0] || label;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Loopback / LAN / *.local hosts a developer actually types. */
|
|
189
|
+
function isLocal(host: string): boolean {
|
|
190
|
+
return (
|
|
191
|
+
host === 'localhost' ||
|
|
192
|
+
host.endsWith('.localhost') ||
|
|
193
|
+
host === '127.0.0.1' ||
|
|
194
|
+
host === '0.0.0.0' ||
|
|
195
|
+
host === '[::1]' ||
|
|
196
|
+
host.endsWith('.local')
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Resolve the full white-label identity from a Host header value.
|
|
202
|
+
*
|
|
203
|
+
* Pure and total: an unknown or missing host falls back to the Lux row rather
|
|
204
|
+
* than throwing, so a preview URL or a health probe never blanks the UI.
|
|
205
|
+
*
|
|
206
|
+
* resolveWhiteLabel('mpc.lux.cloud') // org lux, clientId lux-mpc, issuer https://lux.id
|
|
207
|
+
* resolveWhiteLabel('zoo.network') // org zoo, clientId zoo-network, issuer https://zoo.id
|
|
208
|
+
* resolveWhiteLabel('id.pars.id') // org pars, clientId pars-id, issuer https://pars.id
|
|
209
|
+
*/
|
|
210
|
+
export function resolveWhiteLabel(host?: string | null): WhiteLabel {
|
|
211
|
+
const h = normalizeHost(host ?? '');
|
|
212
|
+
const match = h ? matchDomain(h) : undefined;
|
|
213
|
+
const org = match ? match[1] : DEFAULT_ORG;
|
|
214
|
+
const identity = ORGS[org];
|
|
215
|
+
const app = match ? appSlug(h, match[0], match[2]) : (h && isLocal(h) ? 'local' : 'app');
|
|
216
|
+
return {
|
|
217
|
+
...identity,
|
|
218
|
+
host: h,
|
|
219
|
+
app,
|
|
220
|
+
theme: `dark_${ org }`,
|
|
221
|
+
issuer: `https://${ identity.iamDomain }`,
|
|
222
|
+
clientId: `${ org }-${ app }`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The current host in a browser, or '' during SSR / prerender.
|
|
228
|
+
* Server code must pass `req.headers.host` to resolveWhiteLabel explicitly —
|
|
229
|
+
* there is no ambient request here, and guessing would bake in a brand.
|
|
230
|
+
*/
|
|
231
|
+
export function currentHost(): string {
|
|
232
|
+
return typeof window === 'undefined' ? '' : window.location.host;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Identity for the org id itself, for the rare surface that is not host-routed. */
|
|
236
|
+
export function orgIdentity(org: Org): OrgIdentity {
|
|
237
|
+
return ORGS[org];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export { ORGS as orgs };
|