mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
The magnifying glass was drawn over the leading "S" of the placeholder. The cause is not the search bar: @luxfi/ui's InputGroup measures its adornments correctly and then throws the measurement away, because the clone is guarded on `displayName === 'FieldInput'` — a component that does not exist in the package — and the props it would have set (`ps`/`pe`) are Chakra style props that the Tailwind/gui Input spreads onto the DOM and ignores. Both halves are leftovers of the Chakra→Tailwind port, so EVERY InputGroup in the app is unpadded; the header is just the one you look at every day. Patched at the primitive (patches/@[email protected]) rather than hand-tuning padding at one call site: the offset is the measured width of the actual adornment, so it stays right when the adornment changes, and FilterInput / revoke's SearchInput / the showcase are fixed by the same change. Measured at 1440x900: the icon overlapped the text by 24px and the kbd chip by 52px; both are now 0. The measurement also moves to a layout effect and stops blanking value and placeholder on first render. That blanking was inert while the guard swallowed the clone; with the clone live it flips the field from controlled to uncontrolled on mount, which React warns about and which can eat a keystroke. ClearButton spread `className` over its own visibility classes, so a caller passing one clobbered `invisible` — the header showed a clear button on an empty field. Merged instead, at the component. Placeholder is now "Search or ask anything", and Cmd/Ctrl-K opens an assistant. Nothing in this repo, @luxfi/ui or @hanzo/gui ships a command palette or chat (no cmdk, no palette, one dialog primitive), so this is the smallest thing that works: the existing Dialog, one global listener, and a server-side proxy that holds the credential. The key is deliberately not NEXT_PUBLIC_, so a deployment without one gets a dialog that says exactly that instead of failing silently. Off unless NEXT_PUBLIC_AI_ASSISTANT_ENABLED is set — Ctrl+K is the browser's address-bar shortcut and claiming it to show an error would be worse than not claiming it. Validators read 5 on a network running 20. useCurrentValidators asks ONE P-Chain, the one this deployment points at, but Lux, Hanzo, Zoo and Pars are each sovereign L1s with their own P-Chain and their own validator set — measured 2026-08-06, five each, with four disjoint NodeID sets. The registry gains the node origin per chain; absence of it is the signal that a chain has no publicly reachable node (SPC and Osage are in-cluster only), and those are reported `unavailable` rather than counted as zero. Stake is deliberately NOT summed: each chain bonds its own currency. The proxy resolves `?chain=` through the registry and never from the request, so it cannot be pointed at an arbitrary origin. The new envs are registered with the envs-validator and documented in ENVS.md — the schema rejects unknown NEXT_PUBLIC_* variables and runs at container start, so an undeclared one stops the pod from booting. Co-authored-by: Hanzo Dev <[email protected]>
133 lines
4.0 KiB
TypeScript
133 lines
4.0 KiB
TypeScript
import { Input } from '@luxfi/ui/input';
|
|
import { InputGroup } from '@luxfi/ui/input-group';
|
|
import React from 'react';
|
|
import type { ChangeEvent, FormEvent, FocusEvent } from 'react';
|
|
|
|
import useIsMobile from 'lib/hooks/useIsMobile';
|
|
import { ClearButton } from 'toolkit/components/buttons/ClearButton';
|
|
import IconSvg from 'ui/shared/IconSvg';
|
|
import { useAssistantShortcut } from 'ui/snippets/searchBar/SearchBarAssistant';
|
|
|
|
interface Props extends Omit<React.HTMLAttributes<HTMLFormElement>, 'onChange'> {
|
|
onChange?: (value: string) => void;
|
|
onSubmit?: (event: FormEvent<HTMLFormElement>) => void;
|
|
onBlur?: (event: FocusEvent<HTMLFormElement>) => void;
|
|
onFocus?: () => void;
|
|
onHide?: () => void;
|
|
onClear?: () => void;
|
|
onFormClick?: (event: React.MouseEvent<HTMLFormElement>) => void;
|
|
isHeroBanner?: boolean;
|
|
isSuggestOpen?: boolean;
|
|
value?: string;
|
|
readOnly?: boolean;
|
|
mb?: number;
|
|
w?: string;
|
|
backgroundColor?: string;
|
|
borderRadius?: string;
|
|
position?: string;
|
|
zIndex?: string;
|
|
}
|
|
|
|
const SearchBarInput = (
|
|
{ onChange, onSubmit, isHeroBanner, isSuggestOpen, onFocus, onBlur, onHide, onClear, onFormClick, value, readOnly, ...rest }: Props,
|
|
ref: React.ForwardedRef<HTMLFormElement>,
|
|
) => {
|
|
const innerRef = React.useRef<HTMLFormElement>(null);
|
|
React.useImperativeHandle(ref, () => innerRef.current as HTMLFormElement, []);
|
|
const isMobile = useIsMobile();
|
|
const shortcut = useAssistantShortcut();
|
|
|
|
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
|
onChange?.(event.target.value);
|
|
}, [ onChange ]);
|
|
|
|
const handleKeyPress = React.useCallback((event: KeyboardEvent) => {
|
|
if (isMobile) {
|
|
return;
|
|
}
|
|
|
|
switch (event.key) {
|
|
case '/': {
|
|
if ([ 'INPUT', 'TEXTAREA' ].includes((event.target as HTMLElement).tagName)) {
|
|
break;
|
|
}
|
|
|
|
if (!isSuggestOpen) {
|
|
event.preventDefault();
|
|
innerRef.current?.querySelector('input')?.focus();
|
|
onFocus?.();
|
|
}
|
|
break;
|
|
}
|
|
case 'Escape': {
|
|
if (isSuggestOpen) {
|
|
innerRef.current?.querySelector('input')?.blur();
|
|
onHide?.();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}, [ isMobile, isSuggestOpen, onFocus, onHide ]);
|
|
|
|
React.useEffect(() => {
|
|
window.addEventListener('keydown', handleKeyPress);
|
|
return () => {
|
|
window.removeEventListener('keydown', handleKeyPress);
|
|
};
|
|
}, [ handleKeyPress ]);
|
|
|
|
const startElement = <IconSvg name="search" className="w-5 h-5"/>;
|
|
|
|
const endElement = (
|
|
<>
|
|
<ClearButton onClick={ onClear } visible={ Boolean(value?.length) } className="mx-2"/>
|
|
{ !isMobile && shortcut && (
|
|
<div
|
|
className={
|
|
'flex items-center justify-center h-5 px-1.5 mr-2 rounded-sm whitespace-nowrap' +
|
|
' border border-solid border-[var(--color-icon-secondary)]' +
|
|
' text-[var(--color-icon-secondary)] text-xs font-medium'
|
|
}
|
|
>
|
|
{ shortcut }
|
|
</div>
|
|
) }
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<form
|
|
ref={ innerRef }
|
|
noValidate
|
|
onSubmit={ onSubmit }
|
|
onBlur={ onBlur }
|
|
onClick={ onFormClick }
|
|
className="w-full rounded-base relative"
|
|
style={{
|
|
backgroundColor: 'var(--color-bg-primary)',
|
|
zIndex: isSuggestOpen ? 'var(--zIndex-modal)' : 'auto',
|
|
}}
|
|
{ ...rest }
|
|
>
|
|
<InputGroup
|
|
startElement={ startElement }
|
|
startElementProps={{ className: 'px-2' }}
|
|
endElement={ endElement }
|
|
>
|
|
<Input
|
|
size={ isHeroBanner ? 'md' : 'sm' }
|
|
placeholder="Search or ask anything"
|
|
value={ value }
|
|
onChange={ handleChange }
|
|
onFocus={ onFocus }
|
|
tabIndex={ readOnly ? -1 : 0 }
|
|
className="border border-solid border-[var(--color-input-border)] bg-[var(--color-input-bg)] rounded-lg"
|
|
enterKeyHint="search"
|
|
/>
|
|
</InputGroup>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
export default React.memo(React.forwardRef(SearchBarInput));
|