mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"news": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Welcome to Nitro local mode",
|
||||
"body": "This news card is loaded from public/configuration/news.json while yarn start is running.",
|
||||
"image": "${image.library.url}web_promo_small/spromo_Canal_Bundle.png",
|
||||
"link": "",
|
||||
"linkText": "Read more"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Local development ready",
|
||||
"body": "API and socket use localhost:2096. Remote assets and gamedata stay available so you do not need to copy the full client folder locally.",
|
||||
"image": "${image.library.url}web_promo_small/spromo_hween12_vampire.png",
|
||||
"link": "",
|
||||
"linkText": "Ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+13
@@ -1,4 +1,17 @@
|
||||
(() => {
|
||||
const API_BASE = "http://localhost:2096";
|
||||
|
||||
const ensureMobileViewport = () => {
|
||||
let viewport = document.querySelector('meta[name="viewport"]');
|
||||
if(!viewport) {
|
||||
viewport = document.createElement("meta");
|
||||
viewport.name = "viewport";
|
||||
document.head.appendChild(viewport);
|
||||
}
|
||||
viewport.content = "width=device-width, initial-scale=1, viewport-fit=cover";
|
||||
};
|
||||
|
||||
ensureMobileViewport();
|
||||
const FALLBACK_API_BASE = "";
|
||||
|
||||
const getBase = () => {
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
"login.server_key.endpoint": "${api.url}/api/auth/server-key",
|
||||
"login.sso-token.endpoint": "${api.url}/api/auth/sso-token",
|
||||
"login.refresh.endpoint": "${api.url}/api/auth/refresh",
|
||||
"login.health.method": "GET",
|
||||
"login.news.url": "${asset.url}/news/news.json",
|
||||
"badges.custom.list.endpoint": "${api.url}/api/badges/custom",
|
||||
"badges.custom.create.endpoint": "${api.url}/api/badges/custom",
|
||||
"badges.custom.update.endpoint": "${api.url}/api/badges/custom/%badgeId%",
|
||||
|
||||
@@ -215,6 +215,11 @@ const ASSET_LOADER_JS = `(() => {
|
||||
return new URL(".", source);
|
||||
};
|
||||
|
||||
const getDeployBase = () => {
|
||||
try { return new URL("..", getBase()); }
|
||||
catch { return new URL("/", location.href); }
|
||||
};
|
||||
|
||||
const withCacheBust = (url) => {
|
||||
url.searchParams.set("v", Date.now().toString(36));
|
||||
return url;
|
||||
@@ -242,9 +247,14 @@ const ASSET_LOADER_JS = `(() => {
|
||||
|
||||
const resolveAssetCandidates = (path) => {
|
||||
const base = getBase();
|
||||
const deploy = getDeployBase();
|
||||
const normalized = path.replace(/^\\.\\//, "");
|
||||
const file = normalized.split("/").pop();
|
||||
const relative = normalized.replace(/^\\//, "");
|
||||
const urls = [
|
||||
new URL("src/assets/" + file, deploy),
|
||||
new URL("assets/" + file, deploy),
|
||||
new URL(relative, deploy),
|
||||
new URL("./src/assets/" + file, base),
|
||||
new URL("./assets/" + file, base),
|
||||
new URL("/src/assets/" + file, base.origin),
|
||||
@@ -376,7 +386,10 @@ const ASSET_LOADER_JS = `(() => {
|
||||
|
||||
const fetchManifest = async () => {
|
||||
const base = getBase();
|
||||
const deploy = getDeployBase();
|
||||
const candidates = [
|
||||
new URL(".vite/manifest.json", deploy),
|
||||
new URL("manifest.json", deploy),
|
||||
new URL(".vite/manifest.json", base.origin + "/"),
|
||||
new URL("manifest.json", base.origin + "/"),
|
||||
new URL(".vite/manifest.json", base),
|
||||
@@ -392,7 +405,11 @@ const ASSET_LOADER_JS = `(() => {
|
||||
const json = await response.json();
|
||||
if(json && typeof json === "object") {
|
||||
debug("loader: manifest from " + candidate.href);
|
||||
return { manifest: json, base: new URL(".", candidate.href) };
|
||||
let manifestBase = new URL(".", candidate.href);
|
||||
if(/\\/\\.vite\\/manifest\\.json$/.test(candidate.pathname)) {
|
||||
manifestBase = new URL("..", manifestBase);
|
||||
}
|
||||
return { manifest: json, base: manifestBase };
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
@@ -418,18 +435,24 @@ const ASSET_LOADER_JS = `(() => {
|
||||
const resolveManifestPath = (manifestBase, file) => {
|
||||
if(/^https?:\\/\\//i.test(file)) return file;
|
||||
if(file.startsWith("/")) return file;
|
||||
return new URL(file, manifestBase.origin + "/").pathname;
|
||||
return new URL(file, manifestBase).pathname;
|
||||
};
|
||||
|
||||
const isLoaderUrl = (href) => /(?:^|\\/)bootstrap\\.js(?:$|\\?|#)/i.test(href) || /(?:^|\\/)asset-loader\\.js(?:$|\\?|#)/i.test(href);
|
||||
|
||||
const fetchEntryFromIndexHtml = async () => {
|
||||
const base = getBase();
|
||||
const deploy = getDeployBase();
|
||||
const candidates = [
|
||||
new URL("index.html", deploy),
|
||||
new URL("./", deploy),
|
||||
new URL("/index.html", base.origin + "/"),
|
||||
new URL("/", base.origin + "/")
|
||||
];
|
||||
const seen = new Set();
|
||||
for(const candidate of candidates) {
|
||||
if(seen.has(candidate.href)) continue;
|
||||
seen.add(candidate.href);
|
||||
try {
|
||||
const response = await fetch(withCacheBust(new URL(candidate.href)), { cache: "no-store" });
|
||||
if(!response.ok) continue;
|
||||
|
||||
+40
-2
@@ -10,6 +10,23 @@ import { useMessageEvent, useNitroEvent } from './hooks';
|
||||
|
||||
NitroVersion.UI_VERSION = GetUIVersion();
|
||||
|
||||
const getViewportDimensions = () =>
|
||||
{
|
||||
const viewport = window.visualViewport;
|
||||
const width = Math.max(1, Math.floor(viewport?.width ?? window.innerWidth));
|
||||
const height = Math.max(1, Math.floor(viewport?.height ?? window.innerHeight));
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
const syncViewportCssVars = () =>
|
||||
{
|
||||
const { width, height } = getViewportDimensions();
|
||||
|
||||
document.documentElement.style.setProperty('--nitro-app-width', `${ width }px`);
|
||||
document.documentElement.style.setProperty('--nitro-app-height', `${ height }px`);
|
||||
};
|
||||
|
||||
const preloadUrl = async (url: string): Promise<void> =>
|
||||
{
|
||||
if(!url) return;
|
||||
@@ -268,6 +285,25 @@ export const App: FC<{}> = props =>
|
||||
return warmupPromiseRef.current;
|
||||
}, [ startRenderer ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
syncViewportCssVars();
|
||||
|
||||
const handleViewportResize = () => syncViewportCssVars();
|
||||
const viewport = window.visualViewport;
|
||||
|
||||
window.addEventListener('resize', handleViewportResize);
|
||||
viewport?.addEventListener('resize', handleViewportResize);
|
||||
viewport?.addEventListener('scroll', handleViewportResize);
|
||||
|
||||
return () =>
|
||||
{
|
||||
window.removeEventListener('resize', handleViewportResize);
|
||||
viewport?.removeEventListener('resize', handleViewportResize);
|
||||
viewport?.removeEventListener('scroll', handleViewportResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const prepare = async (width: number, height: number) =>
|
||||
@@ -370,7 +406,9 @@ export const App: FC<{}> = props =>
|
||||
}
|
||||
};
|
||||
|
||||
prepare(window.innerWidth, window.innerHeight);
|
||||
const { width, height } = getViewportDimensions();
|
||||
|
||||
prepare(width, height);
|
||||
|
||||
return () =>
|
||||
{
|
||||
@@ -380,7 +418,7 @@ export const App: FC<{}> = props =>
|
||||
}, [ prepareTrigger, startWarmup, startRenderer, tryRememberLogin, applySsoTicket, rotateRememberLogin ]);
|
||||
|
||||
return (
|
||||
<Base fit overflow="hidden" className={ !(window.devicePixelRatio % 1) && 'image-rendering-pixelated' }>
|
||||
<Base fit overflow="hidden" className={ `nitro-app-root ${ !(window.devicePixelRatio % 1) ? 'image-rendering-pixelated' : '' }` }>
|
||||
{ !isReady && !showLogin &&
|
||||
<LoadingView isError={ errorMessage.length > 0 } message={ errorMessage } homeUrl={ homeUrl } /> }
|
||||
{ !isReady && showLogin && <LoginView onAuthenticated={ handleAuthenticated } isEntering={ isEnteringHotel } /> }
|
||||
|
||||
@@ -36,6 +36,8 @@ export class AvatarInfoFurni implements IAvatarInfo
|
||||
public allowLay: boolean = false;
|
||||
public allowWalk: boolean = false;
|
||||
public teleportTargetId: number = 0;
|
||||
public spriteId: number = -1;
|
||||
public productType: string = 's';
|
||||
|
||||
constructor(public readonly type: string)
|
||||
{}
|
||||
|
||||
@@ -118,6 +118,8 @@ export class AvatarInfoUtilities
|
||||
{
|
||||
furniInfo.name = furnitureData.name;
|
||||
furniInfo.description = furnitureData.description;
|
||||
furniInfo.spriteId = furnitureData.id;
|
||||
furniInfo.productType = ((category === RoomObjectCategory.WALL) ? 'i' : 's');
|
||||
furniInfo.purchaseOfferId = furnitureData.purchaseOfferId;
|
||||
furniInfo.purchaseCouldBeUsedForBuyout = furnitureData.purchaseCouldBeUsedForBuyout;
|
||||
furniInfo.rentOfferId = furnitureData.rentOfferId;
|
||||
|
||||
@@ -80,11 +80,97 @@ const applyWiredTextMarkup = (content: string) =>
|
||||
return result;
|
||||
};
|
||||
|
||||
const FONT_NAMED_COLORS = new Set([
|
||||
'red', 'green', 'blue', 'yellow', 'white', 'black',
|
||||
'orange', 'cyan', 'brown', 'purple', 'pink', 'magenta',
|
||||
'violet', 'gray', 'grey', 'lime', 'teal', 'gold',
|
||||
'silver', 'navy', 'maroon', 'olive', 'indigo'
|
||||
]);
|
||||
|
||||
export const sanitizeFontColor = (raw: string | null | undefined): string | null =>
|
||||
{
|
||||
if(!raw) return null;
|
||||
if(raw.length > 20) return null;
|
||||
|
||||
const value = raw.trim().toLowerCase();
|
||||
|
||||
if(/^#([0-9a-f]{3}|[0-9a-f]{6})$/.test(value)) return value;
|
||||
if(FONT_NAMED_COLORS.has(value)) return value;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export type FontSegment = { color: string | null; text: string };
|
||||
|
||||
const FONT_COLOR_ATTR = /color\s*=\s*(?:"([^"]{1,32})"|'([^']{1,32})'|([^\s"'>]{1,32}))/i;
|
||||
|
||||
export const parseFontSegments = (input: string): FontSegment[] =>
|
||||
{
|
||||
if(!input) return [];
|
||||
|
||||
const pattern = /<font\b([^>]{0,200}?)>([\s\S]{0,200}?)<\/font>/gi;
|
||||
const segments: FontSegment[] = [];
|
||||
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while((match = pattern.exec(input)) !== null)
|
||||
{
|
||||
if(match.index > lastIndex)
|
||||
{
|
||||
segments.push({ color: null, text: input.slice(lastIndex, match.index) });
|
||||
}
|
||||
|
||||
const colorMatch = FONT_COLOR_ATTR.exec(match[1] || '');
|
||||
const rawColor = colorMatch ? (colorMatch[1] || colorMatch[2] || colorMatch[3]) : null;
|
||||
const color = sanitizeFontColor(rawColor);
|
||||
|
||||
segments.push({ color, text: match[2] });
|
||||
lastIndex = pattern.lastIndex;
|
||||
}
|
||||
|
||||
if(lastIndex < input.length)
|
||||
{
|
||||
segments.push({ color: null, text: input.slice(lastIndex) });
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
|
||||
const applyFontMarkup = (content: string) =>
|
||||
{
|
||||
const fontPattern = /<font\b([^&]{0,200}?)>([\s\S]{0,4000}?)<\/font>/gi;
|
||||
const colorAttr = /color\s*=\s*(?:"([^"]{1,32})"|'([^']{1,32})'|([^\s"'>]{1,32}))/i;
|
||||
|
||||
let previous = '';
|
||||
let next = content;
|
||||
let guard = 0;
|
||||
|
||||
while((previous !== next) && (guard < 20))
|
||||
{
|
||||
previous = next;
|
||||
next = next.replace(fontPattern, (_match, attrs: string, inner: string) =>
|
||||
{
|
||||
const colorMatch = colorAttr.exec(attrs || '');
|
||||
const rawColor = colorMatch ? (colorMatch[1] || colorMatch[2] || colorMatch[3]) : null;
|
||||
const color = sanitizeFontColor(rawColor);
|
||||
|
||||
if(!color) return inner;
|
||||
|
||||
return `<span style="color:${ color }">${ inner }</span>`;
|
||||
});
|
||||
guard++;
|
||||
}
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
export const RoomChatFormatter = (content: string) =>
|
||||
{
|
||||
let result = '';
|
||||
|
||||
content = encodeHTML(content);
|
||||
content = applyFontMarkup(content);
|
||||
content = applyWiredTextMarkup(content);
|
||||
//content = (joypixels.shortnameToUnicode(content) as string)
|
||||
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import { configFileUrl, getClientMode, installSecureFetch } from './secure-assets';
|
||||
|
||||
const ensureMobileViewport = () =>
|
||||
{
|
||||
let viewport = document.querySelector<HTMLMetaElement>('meta[name="viewport"]');
|
||||
|
||||
if(!viewport)
|
||||
{
|
||||
viewport = document.createElement('meta');
|
||||
viewport.name = 'viewport';
|
||||
document.head.appendChild(viewport);
|
||||
}
|
||||
|
||||
viewport.content = 'width=device-width, initial-scale=1, viewport-fit=cover';
|
||||
};
|
||||
|
||||
ensureMobileViewport();
|
||||
installSecureFetch();
|
||||
|
||||
const setBootDebug = (message: string) =>
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { FC, Fragment, ReactNode, useMemo } from 'react';
|
||||
import { GetNickIconUrl } from '../assets/images/user_custom/nick_icons';
|
||||
import { PREFIX_EFFECT_KEYFRAMES, getPrefixEffectStyle, getPrefixFontStyle, parsePrefixColors } from '../api';
|
||||
import { PREFIX_EFFECT_KEYFRAMES, getPrefixEffectStyle, getPrefixFontStyle, parseFontSegments, parsePrefixColors } from '../api';
|
||||
|
||||
const renderInlineFontMarkup = (text: string): ReactNode =>
|
||||
{
|
||||
if(!text) return text;
|
||||
if(text.indexOf('<font') === -1) return text;
|
||||
|
||||
const segments = parseFontSegments(text);
|
||||
|
||||
if(!segments.length) return text;
|
||||
|
||||
return segments.map((segment, index) =>
|
||||
{
|
||||
if(segment.color) return <span key={ index } style={ { color: segment.color } }>{ segment.text }</span>;
|
||||
|
||||
return <Fragment key={ index }>{ segment.text }</Fragment>;
|
||||
});
|
||||
};
|
||||
|
||||
interface UserIdentityViewProps
|
||||
{
|
||||
@@ -87,7 +104,7 @@ export const UserIdentityView: FC<UserIdentityViewProps> = ({
|
||||
</span>
|
||||
);
|
||||
case 'name':
|
||||
return <span key="identity-name" className={ `${ nameClassName } whitespace-nowrap` }>{ username }{ showColon ? ':' : '' }{ showColon ? ' ' : '' }</span>;
|
||||
return <span key="identity-name" className={ `${ nameClassName } whitespace-nowrap` }>{ renderInlineFontMarkup(username) }{ showColon ? ':' : '' }{ showColon ? ' ' : '' }</span>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GetConfiguration } from '@nitrots/nitro-renderer';
|
||||
import { AvatarScaleType, AvatarSetType, GetAvatarRenderManager, GetConfiguration, IAvatarImage } from '@nitrots/nitro-renderer';
|
||||
import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ClearRememberLogin, GetConfigurationValue, GetOptionalConfigurationValue, GetRememberLogin, StoreRememberLoginFromPayload } from '../../api';
|
||||
import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, StoreRememberLoginFromPayload } from '../../api';
|
||||
import { configFileUrl } from '../../secure-assets';
|
||||
import flagBr from '../../assets/images/flag_icon/flag_icon_br.png';
|
||||
import flagDe from '../../assets/images/flag_icon/flag_icon_de.png';
|
||||
@@ -195,25 +195,19 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
||||
const [ localeApplying, setLocaleApplying ] = useState(false);
|
||||
const [ localeError, setLocaleError ] = useState('');
|
||||
const [ loginViewConfig, setLoginViewConfig ] = useState<Record<string, unknown>>(() => GetConfigurationValue<Record<string, unknown>>('loginview', {}));
|
||||
const [ , setLocalizationVersion ] = useState(0);
|
||||
const submitTimeRef = useRef(0);
|
||||
const preloadedLoginImagesRef = useRef<Set<string>>(new Set());
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const refreshLocalization = () => setLocalizationVersion(value => (value + 1));
|
||||
window.addEventListener('nitro-localization-updated', refreshLocalization);
|
||||
return () => window.removeEventListener('nitro-localization-updated', refreshLocalization);
|
||||
}, []);
|
||||
|
||||
const configuredLoginImages = useMemo<Record<string, string>>(() =>
|
||||
(loginViewConfig?.['images'] as Record<string, string>) ?? {}, [ loginViewConfig ]);
|
||||
const loginImages = useMemo<Record<string, string>>(() =>
|
||||
{
|
||||
const configured = (loginViewConfig?.['images'] as Record<string, string>) ?? {};
|
||||
return { ...getDefaultLoginImages(), ...configured };
|
||||
}, [ loginViewConfig ]);
|
||||
({ ...getDefaultLoginImages(), ...configuredLoginImages }), [ configuredLoginImages ]);
|
||||
|
||||
const configuredLoginWidgets = useMemo<Record<string, unknown>>(() =>
|
||||
(loginViewConfig?.['widgets'] as Record<string, unknown>) ?? {}, [ loginViewConfig ]);
|
||||
|
||||
const loginWidgetSlots = useMemo(() =>
|
||||
{
|
||||
const configuredLoginWidgets = (loginViewConfig?.['widgets'] as Record<string, unknown>) ?? {};
|
||||
return Object.entries(configuredLoginWidgets)
|
||||
.filter(([ key, value ]) => key.startsWith('slot.') && key.endsWith('.widget') && typeof value === 'string' && value.length > 0)
|
||||
.map(([ key, value ]) =>
|
||||
@@ -225,7 +219,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
||||
})
|
||||
.filter(slot => slot.slotNum > 0)
|
||||
.sort((a, b) => a.slotNum - b.slotNum);
|
||||
}, [ loginViewConfig ]);
|
||||
}, [ configuredLoginWidgets ]);
|
||||
|
||||
const backgroundColor = (loginImages['background.colour'] || GetConfigurationValue<string>('login_background.colour', '#6eadc8'));
|
||||
const background = interpolate(loginImages['background'] || GetConfigurationValue<string>('login_background', ''));
|
||||
@@ -234,11 +228,15 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
||||
const left = interpolate(loginImages['left'] || GetConfigurationValue<string>('login_left', ''));
|
||||
const rightRepeat = interpolate(loginImages['right.repeat'] || GetConfigurationValue<string>('login_right.repeat', ''));
|
||||
const right = interpolate(loginImages['right'] || GetConfigurationValue<string>('login_right', ''));
|
||||
const widgetImageUrls = useMemo(() => loginWidgetSlots
|
||||
.map(slot => typeof slot.conf.image === 'string' ? interpolate(slot.conf.image) : '')
|
||||
.filter(Boolean), [ loginWidgetSlots ]);
|
||||
const loginImageUrls = useMemo(() => [ background, sun, drape, left, rightRepeat, right, ...widgetImageUrls ].filter(Boolean), [ background, sun, drape, left, rightRepeat, right, widgetImageUrls ]);
|
||||
const [ loginImagesVersion, setLoginImagesVersion ] = useState(0);
|
||||
const loginUrl = GetConfigurationValue<string>('login.endpoint', '/api/auth/login');
|
||||
const registerUrl = GetConfigurationValue<string>('login.register.endpoint', '/api/auth/register');
|
||||
const forgotUrl = GetConfigurationValue<string>('login.forgot.endpoint', '/api/auth/forgot-password');
|
||||
const configuredNewsUrl = interpolate(GetOptionalConfigurationValue<string>('login.news.url', ''));
|
||||
const newsUrl = configuredNewsUrl || configFileUrl('news.json');
|
||||
const newsUrl = interpolate(GetConfigurationValue<string>('login.news.url', ''));
|
||||
const turnstileSiteKey = GetConfigurationValue<string>('login.turnstile.sitekey', '');
|
||||
const rawTurnstileEnabled = GetConfigurationValue<unknown>('login.turnstile.enabled', false);
|
||||
const turnstileEnabled = (rawTurnstileEnabled === true
|
||||
@@ -314,6 +312,44 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
||||
}
|
||||
}, [ localeApplying, selectedLocale ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!loginImageUrls.length) return;
|
||||
|
||||
let cancelled = false;
|
||||
let remaining = 0;
|
||||
|
||||
loginImageUrls
|
||||
.filter(url =>
|
||||
{
|
||||
if(preloadedLoginImagesRef.current.has(url)) return false;
|
||||
|
||||
preloadedLoginImagesRef.current.add(url);
|
||||
|
||||
return true;
|
||||
})
|
||||
.forEach(url =>
|
||||
{
|
||||
remaining++;
|
||||
|
||||
const image = new Image();
|
||||
|
||||
image.onload = image.onerror = () =>
|
||||
{
|
||||
remaining--;
|
||||
|
||||
if(!cancelled && remaining <= 0) setLoginImagesVersion(version => version + 1);
|
||||
};
|
||||
|
||||
image.src = url;
|
||||
});
|
||||
|
||||
return () =>
|
||||
{
|
||||
cancelled = true;
|
||||
};
|
||||
}, [ loginImageUrls ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!info) return;
|
||||
@@ -367,7 +403,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
||||
}, []);
|
||||
|
||||
const healthUrl = GetConfigurationValue<string>('login.health.endpoint', '');
|
||||
const healthMethodRaw = GetOptionalConfigurationValue<string>('login.health.method', 'GET');
|
||||
const healthMethodRaw = GetConfigurationValue<string>('login.health.method', 'GET');
|
||||
const healthMethod = (healthMethodRaw || 'GET').toUpperCase();
|
||||
const checkServerReachable = useCallback(async (): Promise<boolean> =>
|
||||
{
|
||||
@@ -481,7 +517,6 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
||||
|
||||
const checkEmailUrl = GetConfigurationValue<string>('login.check-email.endpoint', '/api/auth/check-email');
|
||||
const checkUsernameUrl = GetConfigurationValue<string>('login.check-username.endpoint', '/api/auth/check-username');
|
||||
const imagingUrl = GetOptionalConfigurationValue<string>('login.register.imaging.url', '');
|
||||
const interpretAvailability = (ok: boolean, status: number, payload: Record<string, unknown>): { available: boolean; error?: string } =>
|
||||
{
|
||||
const isTrue = (v: unknown) => v === true || v === 'true' || v === 1 || v === '1';
|
||||
@@ -629,6 +664,9 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
||||
{ left ? <img className="login-left login-layer login-layer-img" src={ left } alt="" draggable={ false } /> : null }
|
||||
{ rightRepeat ? <div className="login-right-repeat login-layer" style={ { backgroundImage: `url(${ rightRepeat })` } } /> : null }
|
||||
{ right ? <img className="login-right login-layer login-layer-img" src={ right } alt="" draggable={ false } /> : null }
|
||||
<div className="login-image-preloader" aria-hidden="true" data-version={ loginImagesVersion }>
|
||||
{ loginImageUrls.map(url => <img key={ url } src={ url } decoding="async" loading="eager" alt="" />) }
|
||||
</div>
|
||||
|
||||
{ loginWidgetSlots.length > 0 &&
|
||||
<div className="login-widgets">
|
||||
@@ -769,7 +807,6 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = fa
|
||||
onCheckEmail={ checkEmailAvailable }
|
||||
onCheckUsername={ checkUsernameAvailable }
|
||||
onCheckServer={ checkServerReachable }
|
||||
imagingUrl={ imagingUrl }
|
||||
submitting={ submitting }
|
||||
error={ error }
|
||||
info={ info }
|
||||
@@ -807,7 +844,6 @@ interface RegisterDialogProps extends DialogSharedProps
|
||||
onCheckEmail: (email: string) => Promise<{ available: boolean; error?: string }>;
|
||||
onCheckUsername: (username: string) => Promise<{ available: boolean; error?: string }>;
|
||||
onCheckServer: () => Promise<boolean>;
|
||||
imagingUrl: string;
|
||||
}
|
||||
|
||||
type RegisterStep = 'credentials' | 'avatar';
|
||||
@@ -868,60 +904,160 @@ const buildFigureString = (selection: FigureSelection): string =>
|
||||
return parts.join('.');
|
||||
};
|
||||
|
||||
const buildImagingUrl = (template: string, figure: string, gender: GenderKey): string =>
|
||||
template
|
||||
.replace(/\{figure\}/g, encodeURIComponent(figure))
|
||||
.replace(/\{gender\}/g, gender)
|
||||
.replace(/\{direction\}/g, '2');
|
||||
|
||||
const HEAD_ONLY_PARTS = new Set([ 'hr', 'hd' ]);
|
||||
|
||||
const buildPartPreviewUrl = (
|
||||
template: string,
|
||||
setType: string,
|
||||
selection: FigureSelection,
|
||||
gender: GenderKey
|
||||
): string =>
|
||||
const buildPartPreviewFigure = (setType: string, selection: FigureSelection, gender: GenderKey): string =>
|
||||
{
|
||||
const defaults = FALLBACK_DEFAULTS[gender];
|
||||
const partSel = selection[setType] ?? defaults[setType];
|
||||
const tail = (partSel.colors && partSel.colors.length) ? `-${ partSel.colors.join('-') }` : '';
|
||||
const isHeadOnly = HEAD_ONLY_PARTS.has(setType);
|
||||
const hd = defaults.hd;
|
||||
const head = `hd-${ hd.partId }-${ hd.colors.join('-') }`;
|
||||
const part = `${ setType }-${ partSel.partId }${ tail }`;
|
||||
|
||||
let parts: string[];
|
||||
if(isHeadOnly)
|
||||
return setType === 'hd' ? part : `${ head }.${ part }`;
|
||||
};
|
||||
|
||||
const AVATAR_PREVIEW_CACHE = new Map<string, string>();
|
||||
const AVATAR_PREVIEW_CACHE_MAX = 200;
|
||||
|
||||
const AVATAR_PREVIEW_MAX_ATTEMPTS = 4;
|
||||
const AVATAR_PREVIEW_TIMEOUT_MS = 8000;
|
||||
|
||||
const renderAvatarPreview = (figure: string, gender: GenderKey, setType: string): Promise<string> =>
|
||||
{
|
||||
if(!figure) return Promise.resolve('');
|
||||
|
||||
const cacheKey = `${ gender }|${ setType }|${ figure }`;
|
||||
const cached = AVATAR_PREVIEW_CACHE.get(cacheKey);
|
||||
if(cached) return Promise.resolve(cached);
|
||||
|
||||
return new Promise<string>(resolve =>
|
||||
{
|
||||
const hd = defaults.hd;
|
||||
const pieces = new Map<string, string>();
|
||||
pieces.set('hd', `hd-${ hd.partId }-${ hd.colors.join('-') }`);
|
||||
pieces.set(setType, `${ setType }-${ partSel.partId }${ tail }`);
|
||||
parts = Array.from(pieces.values());
|
||||
}
|
||||
else
|
||||
let avatarImage: IAvatarImage | null = null;
|
||||
let resolved = false;
|
||||
let attempts = 0;
|
||||
let timer: number | null = null;
|
||||
|
||||
const finish = (url: string) =>
|
||||
{
|
||||
if(resolved) return;
|
||||
resolved = true;
|
||||
if(timer !== null) window.clearTimeout(timer);
|
||||
try { avatarImage?.dispose(); } catch {}
|
||||
avatarImage = null;
|
||||
if(url)
|
||||
{
|
||||
AVATAR_PREVIEW_CACHE.set(cacheKey, url);
|
||||
if(AVATAR_PREVIEW_CACHE.size > AVATAR_PREVIEW_CACHE_MAX)
|
||||
{
|
||||
const firstKey = AVATAR_PREVIEW_CACHE.keys().next().value;
|
||||
if(firstKey) AVATAR_PREVIEW_CACHE.delete(firstKey);
|
||||
}
|
||||
}
|
||||
resolve(url);
|
||||
};
|
||||
|
||||
timer = window.setTimeout(() => finish(''), AVATAR_PREVIEW_TIMEOUT_MS);
|
||||
|
||||
const attempt = () =>
|
||||
{
|
||||
if(resolved) return;
|
||||
if(attempts >= AVATAR_PREVIEW_MAX_ATTEMPTS) { finish(''); return; }
|
||||
attempts++;
|
||||
|
||||
try { avatarImage?.dispose(); } catch {}
|
||||
avatarImage = null;
|
||||
|
||||
try
|
||||
{
|
||||
avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, {
|
||||
resetFigure: () => attempt(),
|
||||
dispose: () => {},
|
||||
disposed: false
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish('');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!avatarImage) { finish(''); return; }
|
||||
|
||||
if(avatarImage.isPlaceholder()) return;
|
||||
|
||||
try
|
||||
{
|
||||
const url = avatarImage.processAsImageUrl(setType);
|
||||
if(url) finish(url);
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish('');
|
||||
}
|
||||
};
|
||||
|
||||
attempt();
|
||||
});
|
||||
};
|
||||
|
||||
const useAvatarPreview = (figure: string, gender: GenderKey, setType: string): string =>
|
||||
{
|
||||
const [ url, setUrl ] = useState<string>(() =>
|
||||
AVATAR_PREVIEW_CACHE.get(`${ gender }|${ setType }|${ figure }`) ?? '');
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const hd = defaults.hd;
|
||||
parts = [
|
||||
`hd-${ hd.partId }-${ hd.colors.join('-') }`,
|
||||
`${ setType }-${ partSel.partId }${ tail }`
|
||||
];
|
||||
}
|
||||
const cacheKey = `${ gender }|${ setType }|${ figure }`;
|
||||
const cached = AVATAR_PREVIEW_CACHE.get(cacheKey);
|
||||
if(cached)
|
||||
{
|
||||
setUrl(cached);
|
||||
return;
|
||||
}
|
||||
|
||||
const figure = parts.join('.');
|
||||
let url = template
|
||||
.replace(/\{figure\}/g, encodeURIComponent(figure))
|
||||
.replace(/\{gender\}/g, gender)
|
||||
.replace(/\{direction\}/g, '2');
|
||||
|
||||
url = url.replace(/size=l/, 'size=s').replace(/size=m/, 'size=s');
|
||||
if(!/size=/.test(url)) url += (url.includes('?') ? '&' : '?') + 'size=s';
|
||||
if(isHeadOnly && !/headonly=/.test(url)) url += '&headonly=1';
|
||||
let cancelled = false;
|
||||
setUrl('');
|
||||
renderAvatarPreview(figure, gender, setType).then(result =>
|
||||
{
|
||||
if(!cancelled) setUrl(result);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [ figure, gender, setType ]);
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
interface AvatarPartRowProps
|
||||
{
|
||||
setType: string;
|
||||
selection: FigureSelection;
|
||||
gender: GenderKey;
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
const AvatarPartRow: FC<AvatarPartRowProps> = ({ setType, selection, gender, onPrev, onNext }) =>
|
||||
{
|
||||
const figure = useMemo(() => buildPartPreviewFigure(setType, selection, gender), [ setType, selection, gender ]);
|
||||
const previewSetType = HEAD_ONLY_PARTS.has(setType) ? AvatarSetType.HEAD : AvatarSetType.FULL;
|
||||
const url = useAvatarPreview(figure, gender, previewSetType);
|
||||
|
||||
return (
|
||||
<div className="avatar-part-row">
|
||||
<button type="button" className="arrow-btn" aria-label={ `Previous ${ setType }` } onClick={ onPrev }>‹</button>
|
||||
<div className={ `part-preview part-preview-${ setType }` }>
|
||||
{ url && <img src={ url } alt={ `${ setType } preview` } onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> }
|
||||
</div>
|
||||
<button type="button" className="arrow-btn" aria-label={ `Next ${ setType }` } onClick={ onNext }>›</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RegisterDialog: FC<RegisterDialogProps> = props =>
|
||||
{
|
||||
const { onCancel, onSubmit, onCheckEmail, onCheckUsername, onCheckServer, imagingUrl, submitting, error, info, turnstileEnabled, turnstileSiteKey } = props;
|
||||
const { onCancel, onSubmit, onCheckEmail, onCheckUsername, onCheckServer, submitting, error, info, turnstileEnabled, turnstileSiteKey } = props;
|
||||
|
||||
const [ step, setStep ] = useState<RegisterStep>('credentials');
|
||||
const [ email, setEmail ] = useState('');
|
||||
@@ -1207,7 +1343,7 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
||||
};
|
||||
|
||||
const figure = buildFigureString(selection);
|
||||
const previewSrc = buildImagingUrl(imagingUrl, figure, gender);
|
||||
const previewSrc = useAvatarPreview(figure, gender, AvatarSetType.FULL);
|
||||
|
||||
const handleAvatarSubmit = async (event: FormEvent<HTMLFormElement>) =>
|
||||
{
|
||||
@@ -1345,24 +1481,20 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
||||
|
||||
<div className="avatar-builder">
|
||||
<div className="avatar-part-col">
|
||||
{ PART_ROWS.map(setType => {
|
||||
const partPreviewSrc = buildPartPreviewUrl(imagingUrl, setType, selection, gender);
|
||||
return (
|
||||
<div className="avatar-part-row" key={ `part-${ setType }` }>
|
||||
<button type="button" className="arrow-btn" aria-label={ `Previous ${ setType }` }
|
||||
onClick={ () => cyclePart(setType, -1) }>‹</button>
|
||||
<div className={ `part-preview part-preview-${ setType }` }>
|
||||
<img src={ partPreviewSrc } alt={ `${ setType } preview` } onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } />
|
||||
</div>
|
||||
<button type="button" className="arrow-btn" aria-label={ `Next ${ setType }` }
|
||||
onClick={ () => cyclePart(setType, 1) }>›</button>
|
||||
</div>
|
||||
);
|
||||
}) }
|
||||
{ PART_ROWS.map(setType => (
|
||||
<AvatarPartRow
|
||||
key={ `part-${ setType }` }
|
||||
setType={ setType }
|
||||
selection={ selection }
|
||||
gender={ gender }
|
||||
onPrev={ () => cyclePart(setType, -1) }
|
||||
onNext={ () => cyclePart(setType, 1) }
|
||||
/>
|
||||
)) }
|
||||
</div>
|
||||
|
||||
<div className="avatar-preview">
|
||||
<img src={ previewSrc } alt="Habbo preview" onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } />
|
||||
{ previewSrc && <img src={ previewSrc } alt="Habbo preview" onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> }
|
||||
</div>
|
||||
|
||||
<div className="avatar-color-col">
|
||||
|
||||
@@ -47,7 +47,9 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
|
||||
{
|
||||
if(!newsUrl) { setFailed(true); return; }
|
||||
let cancelled = false;
|
||||
fetch(newsUrl, { credentials: 'omit' })
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(newsUrl, { credentials: 'omit', signal: controller.signal })
|
||||
.then(async r =>
|
||||
{
|
||||
if(!r.ok) throw new Error('status ' + r.status);
|
||||
@@ -62,7 +64,11 @@ export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
|
||||
setItems(rawList.map((raw, idx) => normalizeNewsItem(raw, idx + 1)));
|
||||
})
|
||||
.catch(() => { if(!cancelled) setFailed(true); });
|
||||
return () => { cancelled = true; };
|
||||
return () =>
|
||||
{
|
||||
cancelled = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, [ newsUrl ]);
|
||||
|
||||
useEffect(() =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GetRenderer, RoomSession } from '@nitrots/nitro-renderer';
|
||||
import { GetEventDispatcher, GetRenderer, RoomObjectMouseEvent, RoomObjectTileMouseEvent, RoomSession } from '@nitrots/nitro-renderer';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import { DispatchMouseEvent, DispatchTouchEvent } from '../../api';
|
||||
@@ -30,6 +30,68 @@ export const RoomView: FC<{}> = (props) =>
|
||||
canvas.ontouchend = (event) => DispatchTouchEvent(event);
|
||||
canvas.ontouchcancel = (event) => DispatchTouchEvent(event);
|
||||
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let touchMoved = false;
|
||||
let lastTileTap: { x: number; y: number; time: number } = null;
|
||||
|
||||
const isMobileTouch = () => window.matchMedia('(pointer: coarse), (hover: none)').matches;
|
||||
|
||||
const onTouchStart = (event: TouchEvent) =>
|
||||
{
|
||||
const touch = event.touches[0];
|
||||
|
||||
if(!touch || !isMobileTouch()) return;
|
||||
|
||||
touchStartX = touch.clientX;
|
||||
touchStartY = touch.clientY;
|
||||
touchMoved = false;
|
||||
};
|
||||
|
||||
const onTouchMove = (event: TouchEvent) =>
|
||||
{
|
||||
const touch = event.touches[0];
|
||||
|
||||
if(!touch || !isMobileTouch()) return;
|
||||
|
||||
if(Math.abs(touch.clientX - touchStartX) > 8 || Math.abs(touch.clientY - touchStartY) > 8) touchMoved = true;
|
||||
};
|
||||
|
||||
const onTouchEnd = (event: TouchEvent) =>
|
||||
{
|
||||
const touch = event.changedTouches[0];
|
||||
|
||||
if(!touch || touchMoved || !isMobileTouch()) return;
|
||||
|
||||
lastTileTap = { x: touch.clientX, y: touch.clientY, time: Date.now() };
|
||||
};
|
||||
|
||||
const showTouchFeedback = () =>
|
||||
{
|
||||
if(!lastTileTap || ((Date.now() - lastTileTap.time) > 250)) return;
|
||||
|
||||
const feedback = document.createElement('div');
|
||||
|
||||
feedback.className = 'nitro-room-touch-feedback';
|
||||
feedback.style.left = `${ lastTileTap.x }px`;
|
||||
feedback.style.top = `${ lastTileTap.y }px`;
|
||||
|
||||
document.body.appendChild(feedback);
|
||||
window.setTimeout(() => feedback.remove(), 420);
|
||||
|
||||
lastTileTap = null;
|
||||
};
|
||||
|
||||
const onTileClick = (event: RoomObjectMouseEvent) =>
|
||||
{
|
||||
if(event instanceof RoomObjectTileMouseEvent) window.setTimeout(showTouchFeedback, 0);
|
||||
};
|
||||
|
||||
canvas.addEventListener('touchstart', onTouchStart, { passive: true });
|
||||
canvas.addEventListener('touchmove', onTouchMove, { passive: true });
|
||||
canvas.addEventListener('touchend', onTouchEnd, { passive: true });
|
||||
GetEventDispatcher().addEventListener(RoomObjectMouseEvent.CLICK, onTileClick);
|
||||
|
||||
const element = elementRef.current;
|
||||
|
||||
if(!element) return;
|
||||
@@ -37,6 +99,14 @@ export const RoomView: FC<{}> = (props) =>
|
||||
canvas.classList.add('bg-black');
|
||||
|
||||
element.appendChild(canvas);
|
||||
|
||||
return () =>
|
||||
{
|
||||
canvas.removeEventListener('touchstart', onTouchStart);
|
||||
canvas.removeEventListener('touchmove', onTouchMove);
|
||||
canvas.removeEventListener('touchend', onTouchEnd);
|
||||
GetEventDispatcher().removeEventListener(RoomObjectMouseEvent.CLICK, onTileClick);
|
||||
};
|
||||
}, [roomSession]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AddLinkEventTracker, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomEnterEffect, RoomSessionDanceEvent } from '@nitrots/nitro-renderer';
|
||||
import { AddLinkEventTracker, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomEngineObjectEvent, RoomEnterEffect, RoomSessionDanceEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { AvatarInfoFurni, AvatarInfoPet, AvatarInfoRentableBot, AvatarInfoUser, GetConfigurationValue, RoomWidgetUpdateRentableBotChatEvent } from '../../../../api';
|
||||
import { Column } from '../../../../common';
|
||||
import { Column, LayoutFurniIconImageView } from '../../../../common';
|
||||
import { useAvatarInfoWidget, useNitroEvent, useRoom, useUiEvent } from '../../../../hooks';
|
||||
import { AvatarInfoPetTrainingPanelView } from './AvatarInfoPetTrainingPanelView';
|
||||
import { AvatarInfoRentableBotChatView } from './AvatarInfoRentableBotChatView';
|
||||
@@ -27,6 +27,9 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
const BLOCK_ROTATE_WINDOW_MS = 500;
|
||||
const [ isGameMode, setGameMode ] = useState(false);
|
||||
const [ isDancing, setIsDancing ] = useState(false);
|
||||
const [ isTouchLayout, setIsTouchLayout ] = useState(false);
|
||||
const [ mobileFurniDetailsOpen, setMobileFurniDetailsOpen ] = useState(false);
|
||||
const [ mobileUserDetailsOpen, setMobileUserDetailsOpen ] = useState(false);
|
||||
const [ rentableBotChatEvent, setRentableBotChatEvent ] = useState<RoomWidgetUpdateRentableBotChatEvent>(null);
|
||||
const { avatarInfo = null, setAvatarInfo = null, activeNameBubble = null, setActiveNameBubble = null, nameBubbles = [], removeNameBubble = null, productBubbles = [], confirmingProduct = null, updateConfirmingProduct = null, removeProductBubble = null, isDecorating = false, setIsDecorating = null } = useAvatarInfoWidget();
|
||||
const { roomSession = null } = useRoom();
|
||||
@@ -56,6 +59,17 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
if(!isGameMode) setGameMode(true);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const query = window.matchMedia('(pointer: coarse), (hover: none)');
|
||||
const updateTouchLayout = () => setIsTouchLayout(query.matches);
|
||||
|
||||
updateTouchLayout();
|
||||
query.addEventListener('change', updateTouchLayout);
|
||||
|
||||
return () => query.removeEventListener('change', updateTouchLayout);
|
||||
}, []);
|
||||
|
||||
useNitroEvent<RoomSessionDanceEvent>(RoomSessionDanceEvent.RSDE_DANCE, event =>
|
||||
{
|
||||
if(event.roomIndex !== roomSession.ownRoomIndex) return;
|
||||
@@ -65,6 +79,13 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
|
||||
useUiEvent<RoomWidgetUpdateRentableBotChatEvent>(RoomWidgetUpdateRentableBotChatEvent.UPDATE_CHAT, event => setRentableBotChatEvent(event));
|
||||
|
||||
useNitroEvent<RoomEngineObjectEvent>(RoomEngineObjectEvent.REQUEST_MANIPULATION, event =>
|
||||
{
|
||||
if(event.category !== avatarInfo?.category || event.objectId !== avatarInfo?.id) return;
|
||||
|
||||
setMobileFurniDetailsOpen(false);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
@@ -100,6 +121,19 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, [ roomSession, setActiveNameBubble, setAvatarInfo ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(avatarInfo?.type !== AvatarInfoFurni.FURNI)
|
||||
{
|
||||
setMobileFurniDetailsOpen(false);
|
||||
}
|
||||
|
||||
if(avatarInfo?.type !== AvatarInfoUser.OWN_USER && avatarInfo?.type !== AvatarInfoUser.PEER)
|
||||
{
|
||||
setMobileUserDetailsOpen(false);
|
||||
}
|
||||
}, [ avatarInfo ]);
|
||||
|
||||
const getMenuView = () =>
|
||||
{
|
||||
if(!roomSession || isGameMode) return null;
|
||||
@@ -120,6 +154,9 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
case AvatarInfoUser.OWN_USER:
|
||||
case AvatarInfoUser.PEER: {
|
||||
const info = (avatarInfo as AvatarInfoUser);
|
||||
|
||||
if(isTouchLayout && !mobileUserDetailsOpen) return null;
|
||||
|
||||
if(GetConfigurationValue('user.tags.enabled')) GetSessionDataManager().getUserTags(info.roomIndex);
|
||||
|
||||
if(info.isSpectatorMode) return null;
|
||||
@@ -156,9 +193,41 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
|
||||
switch(avatarInfo.type)
|
||||
{
|
||||
case AvatarInfoFurni.FURNI:
|
||||
if(isTouchLayout && !isDecorating)
|
||||
{
|
||||
const info = (avatarInfo as AvatarInfoFurni);
|
||||
|
||||
if(!mobileFurniDetailsOpen)
|
||||
{
|
||||
return (
|
||||
<button className="nitro-mobile-furni-infostand-trigger" type="button" onClick={ () => setMobileFurniDetailsOpen(true) }>
|
||||
<LayoutFurniIconImageView productType={ info.productType } productClassId={ info.spriteId } />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <InfoStandWidgetFurniView avatarInfo={ (avatarInfo as AvatarInfoFurni) } onClose={ () => setAvatarInfo(null) } />;
|
||||
case AvatarInfoUser.OWN_USER:
|
||||
case AvatarInfoUser.PEER:
|
||||
if(isTouchLayout)
|
||||
{
|
||||
const info = (avatarInfo as AvatarInfoUser);
|
||||
const figure = encodeURIComponent(info.figure || '');
|
||||
const avatarHeadUrl = `https://www.habbo.com/habbo-imaging/avatarimage?figure=${ figure }&direction=2&head_direction=2&gesture=sml&size=m&headonly=1`;
|
||||
|
||||
if(!mobileUserDetailsOpen)
|
||||
{
|
||||
return (
|
||||
<button className="nitro-mobile-furni-infostand-trigger nitro-mobile-user-infostand-trigger" type="button" onClick={ () => setMobileUserDetailsOpen(true) }>
|
||||
<div className="nitro-mobile-user-infostand-avatar">
|
||||
<img className="nitro-mobile-user-infostand-avatar-image" src={ avatarHeadUrl } alt={ info.name } draggable={ false } />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <InfoStandWidgetUserView avatarInfo={ (avatarInfo as AvatarInfoUser) } setAvatarInfo={ setAvatarInfo } onClose={ () => setAvatarInfo(null) } />;
|
||||
case AvatarInfoUser.BOT:
|
||||
return <InfoStandWidgetBotView avatarInfo={ (avatarInfo as AvatarInfoUser) } onClose={ () => setAvatarInfo(null) } />;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, GetSessionDataManager, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType, UpdateFurniturePositionComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaCrosshairs, FaRulerVertical, FaTimes } from 'react-icons/fa';
|
||||
import { FaCrosshairs, FaTimes } from 'react-icons/fa';
|
||||
import { GrFormNextLink, GrRotateLeft, GrRotateRight } from 'react-icons/gr';
|
||||
import { AvatarInfoFurni, GetGroupInformation, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, Column, Flex, LayoutBadgeImageView, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, LayoutRoomObjectImageView, Text, UserProfileIconView } from '../../../../../common';
|
||||
@@ -487,17 +487,23 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
<div className="absolute inset-e-0">
|
||||
<LayoutRarityLevelView level={ avatarInfo.stuffData.rarityLevel } />
|
||||
</div> }
|
||||
<Flex center fullWidth>
|
||||
<LayoutRoomObjectImageView category={ avatarInfo.category } objectId={ avatarInfo.id } roomId={ roomSession.roomId } />
|
||||
<Flex center fullWidth className="min-h-[74px] max-h-[86px] overflow-hidden">
|
||||
<LayoutRoomObjectImageView
|
||||
category={ avatarInfo.category }
|
||||
objectId={ avatarInfo.id }
|
||||
roomId={ roomSession.roomId }
|
||||
style={ {
|
||||
maxWidth: 120,
|
||||
maxHeight: 82,
|
||||
backgroundSize: 'contain',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
} } />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
|
||||
</div>
|
||||
}
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text fullWidth small textBreak wrap variant="white">{ avatarInfo.description }</Text>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
{ showOwnerProfileIcon && <UserProfileIconView userId={ avatarInfo.ownerId } /> }
|
||||
@@ -551,13 +557,9 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
{ (itemLocation.x > -1) &&
|
||||
<>
|
||||
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 min-w-0">
|
||||
<FaCrosshairs className="fa-icon shrink-0" />
|
||||
<Text small wrap variant="white">X: { itemLocation.x } · Y: { itemLocation.y }</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<FaRulerVertical className="fa-icon shrink-0" />
|
||||
<Text small wrap variant="white">{ LocalizeText('stack.magic.tile.height.label') }: { itemLocation.z < 0.01 ? 0 : itemLocation.z }</Text>
|
||||
<Text small textBreak variant="white">X: { itemLocation.x } · Y: { itemLocation.y } · H: { itemLocation.z < 0.01 ? 0 : itemLocation.z }</Text>
|
||||
</div>
|
||||
</> }
|
||||
{ godMode &&
|
||||
|
||||
@@ -208,7 +208,7 @@ export const AvatarInfoWidgetAvatarView: FC<AvatarInfoWidgetAvatarViewProps> = p
|
||||
}, [ avatarInfo ]);
|
||||
|
||||
return (
|
||||
<ContextMenuView category={ RoomObjectCategory.UNIT } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ avatarInfo.userType } onClose={ onClose }>
|
||||
<ContextMenuView category={ RoomObjectCategory.UNIT } classNames={ [ 'nitro-avatar-action-menu' ] } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ avatarInfo.userType } onClose={ onClose }>
|
||||
<ContextMenuHeaderView className="cursor-pointer" onClick={ event => GetUserProfile(avatarInfo.webID) } dangerouslySetInnerHTML={ { __html: `${ avatarInfo.name }` } }></ContextMenuHeaderView>
|
||||
{ (mode === MODE_NORMAL) &&
|
||||
<>
|
||||
|
||||
@@ -58,6 +58,9 @@ export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProp
|
||||
case 'avatar_effect':
|
||||
CreateLinkEvent('avatar-effects/show');
|
||||
break;
|
||||
case 'customize_nick':
|
||||
CreateLinkEvent('customize/show');
|
||||
break;
|
||||
case 'expressions':
|
||||
hideMenu = false;
|
||||
setMode(MODE_EXPRESSIONS);
|
||||
@@ -122,7 +125,7 @@ export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProp
|
||||
const isRidingHorse = IsRidingHorse();
|
||||
|
||||
return (
|
||||
<ContextMenuView category={ RoomObjectCategory.UNIT } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ avatarInfo.userType } onClose={ onClose }>
|
||||
<ContextMenuView category={ RoomObjectCategory.UNIT } classNames={ [ 'nitro-avatar-action-menu' ] } collapsable={ true } objectId={ avatarInfo.roomIndex } userType={ avatarInfo.userType } onClose={ onClose }>
|
||||
|
||||
<ContextMenuHeaderView className="cursor-pointer" onClick={ event => GetUserProfile(avatarInfo.webID) }>
|
||||
{ avatarInfo.name }
|
||||
@@ -143,6 +146,9 @@ export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProp
|
||||
<ContextMenuListItemView onClick={ event => processAction('avatar_effect') }>
|
||||
{ LocalizeText('product.type.effect') }
|
||||
</ContextMenuListItemView>
|
||||
<ContextMenuListItemView onClick={ event => processAction('customize_nick') }>
|
||||
Nick Custom
|
||||
</ContextMenuListItemView>
|
||||
{ (HasHabboClub() && !isRidingHorse) &&
|
||||
<ContextMenuListItemView onClick={ event => processAction('dance_menu') }>
|
||||
<FaChevronRight className="right fa-icon" />
|
||||
|
||||
@@ -12,7 +12,7 @@ export const ContextMenuCaretView: FC<CaretViewProps> = props =>
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'menu-footer' ];
|
||||
const newClassNames: string[] = [ 'menu-footer nitro-context-menu-footer' ];
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export const ContextMenuHeaderView: FC<FlexProps> = props =>
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'bg-[#3d5f6e] text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ];
|
||||
const newClassNames: string[] = [ 'nitro-context-menu-header', 'bg-[#3d5f6e] text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ];
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export const ContextMenuListItemView: FC<ContextMenuListItemViewProps> = props =
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] bg-[repeating-linear-gradient(#131e25,#131e25_50%,#0d171d_50%,#0d171d_100%)] cursor-pointer' ];
|
||||
const newClassNames: string[] = [ 'nitro-context-menu-item', 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] bg-[repeating-linear-gradient(#131e25,#131e25_50%,#0d171d_50%,#0d171d_100%)] cursor-pointer' ];
|
||||
|
||||
if(disabled) newClassNames.push('disabled');
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
|
||||
|
||||
const getClassNames = useMemo(() => {
|
||||
const classes = [
|
||||
'nitro-context-menu',
|
||||
'p-[2px]!',
|
||||
'bg-[#1c323f]',
|
||||
'border-2',
|
||||
|
||||
@@ -25,6 +25,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
const { isInRoom } = props;
|
||||
const [ isMeExpanded, setMeExpanded ] = useState(false);
|
||||
const [ isToolbarOpen, setIsToolbarOpen ] = useState(false);
|
||||
const [ isTouchLayout, setIsTouchLayout ] = useState(false);
|
||||
const [ useGuideTool, setUseGuideTool ] = useState(false);
|
||||
const [ youtubeEnabled, setYoutubeEnabled ] = useState(false);
|
||||
const { userFigure = null } = useSessionInfo();
|
||||
@@ -36,6 +37,14 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
const isMod = GetSessionDataManager().isModerator;
|
||||
const hasDesktopUnifiedShell = (isInRoom && isToolbarOpen);
|
||||
const showDesktopShell = (isToolbarOpen || !isInRoom);
|
||||
const desktopToolbarFrameClasses = isTouchLayout ? '' : 'md:left-1/2 md:right-auto md:h-[52px] md:w-[420px] md:-translate-x-1/2 md:items-center md:px-[6px] md:py-[4px] lg:w-[460px]';
|
||||
const desktopToolbarOpenClasses = isTouchLayout ? '' : 'md:rounded-none md:border-0 md:bg-transparent md:shadow-none';
|
||||
const desktopToggleClasses = isTouchLayout ? '' : 'md:mb-0';
|
||||
const desktopToggleIconClasses = isTouchLayout ? '' : (isToolbarOpen ? 'md:-rotate-90' : 'md:rotate-90');
|
||||
const desktopChatInputClasses = isTouchLayout ? '' : 'md:px-0';
|
||||
const mobileOnlyClasses = isTouchLayout ? '' : 'md:hidden';
|
||||
const desktopBlockClasses = isTouchLayout ? 'hidden' : 'hidden md:block';
|
||||
const desktopFlexClasses = isTouchLayout ? 'hidden' : 'hidden md:flex';
|
||||
|
||||
useMessageEvent<YouTubeRoomSettingsEvent>(YouTubeRoomSettingsEvent, event =>
|
||||
{
|
||||
@@ -53,6 +62,17 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
}
|
||||
}, [ isInRoom ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const query = window.matchMedia('(pointer: coarse), (hover: none)');
|
||||
const updateTouchLayout = () => setIsTouchLayout(query.matches);
|
||||
|
||||
updateTouchLayout();
|
||||
query.addEventListener('change', updateTouchLayout);
|
||||
|
||||
return () => query.removeEventListener('change', updateTouchLayout);
|
||||
}, []);
|
||||
|
||||
const openYouTubePlayer = () => window.dispatchEvent(new CustomEvent('youtube:toggle'));
|
||||
|
||||
useMessageEvent<PerkAllowancesMessageEvent>(PerkAllowancesMessageEvent, event =>
|
||||
@@ -103,13 +123,13 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
{ youtubeEnabled && <YouTubePlayerView /> }
|
||||
|
||||
{ isInRoom &&
|
||||
<div className={ `fixed bottom-0 left-0 right-0 z-40 flex h-[52px] items-end px-0 pt-[2px] pb-0 pointer-events-none md:left-1/2 md:right-auto md:h-[52px] md:w-[420px] md:-translate-x-1/2 md:items-center md:px-[6px] md:py-[4px] lg:w-[460px] ${ isToolbarOpen ? (hasDesktopUnifiedShell ? 'md:rounded-none md:border-0 md:bg-transparent md:shadow-none rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : 'rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]') : 'border-0 bg-transparent shadow-none md:border-0 md:bg-transparent md:shadow-none' }` }>
|
||||
<div className={ `fixed bottom-0 left-0 right-0 z-40 flex h-[52px] items-end px-0 pt-[2px] pb-0 pointer-events-none ${ desktopToolbarFrameClasses } ${ isToolbarOpen ? (hasDesktopUnifiedShell ? `${ desktopToolbarOpenClasses } rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]` : 'rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]') : `border-0 bg-transparent shadow-none ${ desktopToolbarOpenClasses }` }` }>
|
||||
<motion.div
|
||||
className="tb-toggle pointer-events-auto mr-2 mb-[4px] flex-shrink-0 md:mb-0"
|
||||
className={ `tb-toggle pointer-events-auto mr-2 mb-[4px] flex-shrink-0 ${ desktopToggleClasses }` }
|
||||
onClick={ () => setIsToolbarOpen(value => !value) }
|
||||
whileTap={ { scale: 0.9 } }>
|
||||
<svg
|
||||
className={ `h-3.5 w-3.5 text-white/70 transition-transform duration-300 ${ isToolbarOpen ? 'rotate-180 md:-rotate-90' : 'rotate-0 md:rotate-90' }` }
|
||||
className={ `h-3.5 w-3.5 text-white/70 transition-transform duration-300 ${ isToolbarOpen ? 'rotate-180' : 'rotate-0' } ${ desktopToggleIconClasses }` }
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
@@ -119,9 +139,9 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
className="pointer-events-auto h-full w-full min-w-0 flex-1 px-[6px] md:px-0"
|
||||
className={ `pointer-events-auto h-full w-full min-w-0 flex-1 px-[6px] ${ desktopChatInputClasses }` }
|
||||
id="toolbar-chat-input-container" />
|
||||
<div className="pointer-events-auto relative mr-[6px] shrink-0 md:hidden">
|
||||
<div className={ `pointer-events-auto relative mr-[6px] shrink-0 ${ mobileOnlyClasses }` }>
|
||||
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
|
||||
{ (requests.length > 0) &&
|
||||
<LayoutItemCountView count={ requests.length } className="absolute -right-1 top-0" /> }
|
||||
@@ -138,7 +158,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
animate={ { opacity: 1, y: 0 } }
|
||||
exit={ { opacity: 0, y: 8 } }
|
||||
transition={ { type: 'spring', stiffness: 260, damping: 26 } }
|
||||
className="pointer-events-none fixed bottom-0 left-0 right-0 z-[39] hidden h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] md:block" /> }
|
||||
className={ `pointer-events-none fixed bottom-0 left-0 right-0 z-[39] h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] ${ desktopBlockClasses }` } /> }
|
||||
|
||||
<motion.div
|
||||
key="left-nav"
|
||||
@@ -146,7 +166,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
animate={ { opacity: 1, x: 0, y: 0 } }
|
||||
exit={ { opacity: 0, x: isInRoom ? -10 : 0, y: isInRoom ? 0 : 8 } }
|
||||
transition={ { type: 'spring', stiffness: 300, damping: 28 } }
|
||||
className="fixed bottom-0 left-0 z-40 hidden h-[52px] max-w-[calc(50vw-242px)] items-center overflow-visible pl-3 pointer-events-auto md:flex">
|
||||
className={ `fixed bottom-0 left-0 z-40 h-[52px] max-w-[calc(50vw-242px)] items-center overflow-visible pl-3 pointer-events-auto ${ desktopFlexClasses }` }>
|
||||
<motion.div variants={ containerVariants } initial="hidden" animate="visible" exit="exit" className={ `tb-open-shell flex h-[52px] max-w-full items-center gap-2 overflow-visible px-[8px] pt-[10px] pb-[2px] ${ showDesktopShell ? 'bg-transparent' : 'rounded-t-[10px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' }` }>
|
||||
<motion.div variants={ itemVariants }>
|
||||
{ isInRoom
|
||||
@@ -218,7 +238,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
animate={ { opacity: 1, x: 0 } }
|
||||
exit={ { opacity: 0, x: 10 } }
|
||||
transition={ { type: 'spring', stiffness: 300, damping: 28 } }
|
||||
className={ `fixed bottom-0 z-40 hidden h-[52px] max-w-[calc(50vw-242px)] items-center overflow-visible pr-3 pointer-events-auto md:flex ${ isInRoom ? 'right-0' : 'right-3' }` }>
|
||||
className={ `fixed bottom-0 z-40 h-[52px] max-w-[calc(50vw-242px)] items-center overflow-visible pr-3 pointer-events-auto ${ desktopFlexClasses } ${ isInRoom ? 'right-0' : 'right-3' }` }>
|
||||
<motion.div variants={ containerVariants } initial="hidden" animate="visible" exit="exit" className="tb-open-shell flex h-[52px] max-w-full items-center gap-3 overflow-visible bg-transparent px-[8px] pt-[10px] pb-[2px]">
|
||||
<motion.div variants={ itemVariants } className="relative">
|
||||
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
|
||||
@@ -229,8 +249,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView className={ `tb-icon ${ iconState === MessengerIconState.UNREAD ? 'is-unseen animate-pulse' : '' }` } icon="message" onClick={ () => OpenMessengerChat() } />
|
||||
</motion.div> }
|
||||
<div className="mx-1 hidden h-5 w-[1px] bg-white/20 md:block" />
|
||||
<div className="hidden h-full shrink-0 md:block" id="toolbar-friend-bar-container-desktop" />
|
||||
<div className={ `mx-1 h-5 w-[1px] bg-white/20 ${ desktopBlockClasses }` } />
|
||||
<div className={ `h-full shrink-0 ${ desktopBlockClasses }` } id="toolbar-friend-bar-container-desktop" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
@@ -240,7 +260,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
animate={ { opacity: 1, y: 0 } }
|
||||
exit={ { opacity: 0, y: 8 } }
|
||||
transition={ { type: 'spring', stiffness: 300, damping: 28 } }
|
||||
className={ `fixed left-1/2 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible pointer-events-auto md:hidden ${ isInRoom ? 'bottom-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] px-[6px] py-[4px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : 'bottom-0' }` }>
|
||||
className={ `fixed left-1/2 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible pointer-events-auto ${ mobileOnlyClasses } ${ isInRoom ? 'bottom-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] px-[6px] py-[4px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : 'bottom-0' }` }>
|
||||
<motion.div variants={ containerVariants } initial="hidden" animate="visible" exit="exit" className="tb-bar-scroll flex h-full min-w-0 flex-1 items-center gap-2 overflow-x-auto overflow-y-visible px-1">
|
||||
<motion.div variants={ itemVariants }>
|
||||
{ isInRoom
|
||||
|
||||
@@ -166,7 +166,7 @@ export const useAvailableUserSources = (trigger: Triggerable, userSources: Wired
|
||||
|
||||
if(!trigger) return;
|
||||
|
||||
const intervalId = window.setInterval(refreshStackSources, 100);
|
||||
const intervalId = window.setInterval(refreshStackSources, 1000);
|
||||
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, [ refreshStackSources, trigger ]);
|
||||
|
||||
@@ -45,18 +45,70 @@ body {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
overscroll-behavior: none;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #6d7b84 #c8d0d4;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: var(--nitro-app-width, 100vw);
|
||||
height: var(--nitro-app-height, 100vh);
|
||||
min-height: var(--nitro-app-height, 100vh);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nitro-app-root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: var(--nitro-app-width, 100vw) !important;
|
||||
height: var(--nitro-app-height, 100vh) !important;
|
||||
min-height: var(--nitro-app-height, 100vh) !important;
|
||||
overflow: hidden !important;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
@supports (height: 100dvh) {
|
||||
html,
|
||||
body,
|
||||
#root,
|
||||
.nitro-app-root {
|
||||
height: var(--nitro-app-height, 100dvh) !important;
|
||||
min-height: var(--nitro-app-height, 100dvh) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.image-rendering-pixelated {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.nitro-room-touch-feedback {
|
||||
position: fixed;
|
||||
z-index: 35;
|
||||
width: 34px;
|
||||
height: 18px;
|
||||
margin-left: -17px;
|
||||
margin-top: -9px;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
border: 2px solid rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
transform: scale(0.35);
|
||||
opacity: 0.9;
|
||||
animation: nitroRoomTouchFeedback 0.42s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes nitroRoomTouchFeedback {
|
||||
to {
|
||||
transform: scale(1.15);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*:focus,
|
||||
*:hover {
|
||||
|
||||
@@ -708,6 +708,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 600px) and (max-width: 1100px) {
|
||||
.nitro-login-view .login-stack {
|
||||
right: 16px;
|
||||
width: auto;
|
||||
max-width: min(540px, calc(100vw - 32px));
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 12px;
|
||||
row-gap: 14px;
|
||||
}
|
||||
|
||||
.nitro-login-view .login-stack > .nitro-login-card:nth-child(3) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Login News Window (Habbo flavour) ─── */
|
||||
|
||||
.nitro-login-view .login-news-stack {
|
||||
@@ -1065,6 +1081,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.nitro-login-view .login-news-stack {
|
||||
left: 24px;
|
||||
top: 45%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.nitro-login-view .login-news-stack {
|
||||
display: none;
|
||||
|
||||
@@ -249,3 +249,127 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-mobile-furni-infostand-trigger {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
padding: 2px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.55);
|
||||
border-radius: 8px;
|
||||
background: rgba(27, 40, 52, 0.92);
|
||||
box-shadow:
|
||||
inset 1px 1px 0 rgba(255, 255, 255, 0.25),
|
||||
0 4px 10px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.nitro-mobile-furni-infostand-trigger .object-preview {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.nitro-mobile-user-infostand-avatar {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 54px;
|
||||
max-height: 62px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.nitro-mobile-user-infostand-avatar-image {
|
||||
display: block;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: 54px;
|
||||
max-height: 62px;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nitro-mobile-user-infostand-trigger {
|
||||
position: relative;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu {
|
||||
min-width: 132px;
|
||||
padding: 4px !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
border-radius: 12px;
|
||||
background: rgba(10, 10, 12, 0.58) !important;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-header {
|
||||
min-width: 0;
|
||||
height: 28px;
|
||||
max-height: 28px;
|
||||
margin-bottom: 4px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 9px;
|
||||
background: rgba(10, 10, 12, 0.58);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-item {
|
||||
min-height: 26px;
|
||||
max-height: none;
|
||||
margin-bottom: 2px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-item:hover {
|
||||
background: rgba(18, 18, 22, 0.72);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-item.disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-item .right.fa-icon,
|
||||
.nitro-avatar-action-menu .nitro-context-menu-item .left.fa-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nitro-avatar-action-menu .nitro-context-menu-footer {
|
||||
height: 20px;
|
||||
margin-top: 2px;
|
||||
border-radius: 8px;
|
||||
background: rgba(10, 10, 12, 0.58);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,16 @@ import { useBetween } from 'use-between';
|
||||
import { CanManipulateFurniture, DispatchUiEvent, GetRoomSession, IsFurnitureSelectionDisabled, ProcessRoomObjectOperation, RoomWidgetUpdateBackgroundColorPreviewEvent, RoomWidgetUpdateRoomObjectEvent, SetActiveRoomId, StartRoomSession } from '../../api';
|
||||
import { useMessageEvent, useNitroEvent, useUiEvent } from '../events';
|
||||
|
||||
const getViewportSize = () =>
|
||||
{
|
||||
const viewport = window.visualViewport;
|
||||
|
||||
return {
|
||||
width: Math.max(1, Math.floor(viewport?.width ?? window.innerWidth)),
|
||||
height: Math.max(1, Math.floor(viewport?.height ?? window.innerHeight))
|
||||
};
|
||||
};
|
||||
|
||||
const useRoomState = () =>
|
||||
{
|
||||
const [roomSession, setRoomSession] = useState<IRoomSession>(null);
|
||||
@@ -215,8 +225,7 @@ const useRoomState = () =>
|
||||
const roomEngine = GetRoomEngine();
|
||||
const roomId = roomSession.roomId;
|
||||
const canvasId = 1;
|
||||
const width = Math.floor(window.innerWidth);
|
||||
const height = Math.floor(window.innerHeight);
|
||||
const { width, height } = getViewportSize();
|
||||
const renderer = GetRenderer();
|
||||
|
||||
if (renderer) renderer.resize(width, height);
|
||||
@@ -266,10 +275,9 @@ const useRoomState = () =>
|
||||
|
||||
SetActiveRoomId(roomSession.roomId);
|
||||
|
||||
const resize = (event: UIEvent) =>
|
||||
const resize = () =>
|
||||
{
|
||||
const newWidth = Math.floor(window.innerWidth);
|
||||
const newHeight = Math.floor(window.innerHeight);
|
||||
const { width: newWidth, height: newHeight } = getViewportSize();
|
||||
|
||||
const offsetX = canvas.screenOffsetX - (newWidth - canvas.width) / 2;
|
||||
const offsetY = canvas.screenOffsetY - (newHeight - canvas.height) / 2;
|
||||
@@ -284,7 +292,11 @@ const useRoomState = () =>
|
||||
canvas.screenOffsetY = ~~offsetY;
|
||||
};
|
||||
|
||||
const viewport = window.visualViewport;
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
viewport?.addEventListener('resize', resize);
|
||||
viewport?.addEventListener('scroll', resize);
|
||||
|
||||
return () =>
|
||||
{
|
||||
@@ -293,6 +305,8 @@ const useRoomState = () =>
|
||||
setOriginalRoomBackgroundColor(0);
|
||||
|
||||
window.removeEventListener('resize', resize);
|
||||
viewport?.removeEventListener('resize', resize);
|
||||
viewport?.removeEventListener('scroll', resize);
|
||||
};
|
||||
}, [roomSession]);
|
||||
|
||||
|
||||
+18
-1
@@ -110,6 +110,7 @@ const textDecoder = new TextDecoder();
|
||||
let secureSessionPromise: Promise<SecureSession> = null;
|
||||
let installed = false;
|
||||
const secureResponseCache = new Map<string, Promise<Response>>();
|
||||
const SECURE_RESPONSE_CACHE_LIMIT = 128;
|
||||
let secureSessionCreatedAt = 0;
|
||||
const SECURE_SESSION_TTL_MS = 5 * 60 * 1000;
|
||||
const REKEY_ENDPOINTS = new Set([
|
||||
@@ -366,6 +367,22 @@ const cloneCachedResponse = async (responsePromise: Promise<Response>): Promise<
|
||||
return response.clone();
|
||||
};
|
||||
|
||||
const cacheSecureResponse = (cacheKey: string, responsePromise: Promise<Response>): void =>
|
||||
{
|
||||
secureResponseCache.set(cacheKey, responsePromise);
|
||||
|
||||
responsePromise.catch(() => secureResponseCache.delete(cacheKey));
|
||||
|
||||
while(secureResponseCache.size > SECURE_RESPONSE_CACHE_LIMIT)
|
||||
{
|
||||
const oldestKey = secureResponseCache.keys().next().value;
|
||||
|
||||
if(!oldestKey) break;
|
||||
|
||||
secureResponseCache.delete(oldestKey);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeSecureCacheKey = (requestUrl: string): string =>
|
||||
{
|
||||
try
|
||||
@@ -518,7 +535,7 @@ export const installSecureFetch = (): void =>
|
||||
return response;
|
||||
})();
|
||||
|
||||
if(cacheKey) secureResponseCache.set(cacheKey, responsePromise);
|
||||
if(cacheKey) cacheSecureResponse(cacheKey, responsePromise);
|
||||
|
||||
return cloneCachedResponse(responsePromise);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
picocolors "^1.1.1"
|
||||
|
||||
"@babel/compat-data@^7.28.6":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d"
|
||||
integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==
|
||||
version "7.29.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.3.tgz#e3f5347f0589596c91d227ccb6a541d37fb1307b"
|
||||
integrity sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==
|
||||
|
||||
"@babel/core@^7.24.4":
|
||||
version "7.29.0"
|
||||
@@ -110,9 +110,9 @@
|
||||
"@babel/types" "^7.29.0"
|
||||
|
||||
"@babel/parser@^7.24.4", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0":
|
||||
version "7.29.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.2.tgz#58bd50b9a7951d134988a1ae177a35ef9a703ba1"
|
||||
integrity sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==
|
||||
version "7.29.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.3.tgz#116f70a77958307fceac27747573032f8a62f88e"
|
||||
integrity sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.29.0"
|
||||
|
||||
@@ -856,9 +856,9 @@
|
||||
integrity sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==
|
||||
|
||||
"@tybys/wasm-util@^0.10.1":
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
|
||||
integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
|
||||
version "0.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz#12b3a1b33db1f9cad4ddff1f604ab7dd00bf464e"
|
||||
integrity sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
@@ -901,100 +901,100 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.59.1", "@typescript-eslint/eslint-plugin@^8.59.1":
|
||||
version "8.59.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz#781bc6f9002982cfaf75a185240e24ad7276628a"
|
||||
integrity sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==
|
||||
"@typescript-eslint/eslint-plugin@8.59.2", "@typescript-eslint/eslint-plugin@^8.59.1":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz#f37b2c189a0177141fe3de3b08f2a83991bfdbfa"
|
||||
integrity sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.59.1"
|
||||
"@typescript-eslint/type-utils" "8.59.1"
|
||||
"@typescript-eslint/utils" "8.59.1"
|
||||
"@typescript-eslint/visitor-keys" "8.59.1"
|
||||
"@typescript-eslint/scope-manager" "8.59.2"
|
||||
"@typescript-eslint/type-utils" "8.59.2"
|
||||
"@typescript-eslint/utils" "8.59.2"
|
||||
"@typescript-eslint/visitor-keys" "8.59.2"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/parser@8.59.1", "@typescript-eslint/parser@^8.59.1":
|
||||
version "8.59.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.1.tgz#835d20a62350659a082a1ae2a60b822c40488905"
|
||||
integrity sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==
|
||||
"@typescript-eslint/parser@8.59.2", "@typescript-eslint/parser@^8.59.1":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.2.tgz#e2fd0084baa5dd0c24cd789af1c72cbc3a7a1c62"
|
||||
integrity sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.59.1"
|
||||
"@typescript-eslint/types" "8.59.1"
|
||||
"@typescript-eslint/typescript-estree" "8.59.1"
|
||||
"@typescript-eslint/visitor-keys" "8.59.1"
|
||||
"@typescript-eslint/scope-manager" "8.59.2"
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
"@typescript-eslint/typescript-estree" "8.59.2"
|
||||
"@typescript-eslint/visitor-keys" "8.59.2"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.59.1":
|
||||
version "8.59.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.1.tgz#49efe87c37ef84262f23df8bf62fdc56698ca6fe"
|
||||
integrity sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==
|
||||
"@typescript-eslint/project-service@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz#f8b8cbf8692e3a51c2c394acf8cf6900f7e755af"
|
||||
integrity sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.59.1"
|
||||
"@typescript-eslint/types" "^8.59.1"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.59.2"
|
||||
"@typescript-eslint/types" "^8.59.2"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.59.1":
|
||||
version "8.59.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz#ed90d054fc3db2d0c81464db3a953a94fb85bb58"
|
||||
integrity sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==
|
||||
"@typescript-eslint/scope-manager@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz#63cbd0af2e3180949d6be81122cc555bc71e736d"
|
||||
integrity sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.1"
|
||||
"@typescript-eslint/visitor-keys" "8.59.1"
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
"@typescript-eslint/visitor-keys" "8.59.2"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.59.1", "@typescript-eslint/tsconfig-utils@^8.59.1":
|
||||
version "8.59.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz#ba2a779a444f1d5cb92a606f9b209d239fd4cab1"
|
||||
integrity sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==
|
||||
"@typescript-eslint/tsconfig-utils@8.59.2", "@typescript-eslint/tsconfig-utils@^8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz#6e92bc412083753185a79c9f1431e78169d9232f"
|
||||
integrity sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==
|
||||
|
||||
"@typescript-eslint/type-utils@8.59.1":
|
||||
version "8.59.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz#9c83d3f2ed9187a815e8120f72c08317e513e409"
|
||||
integrity sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==
|
||||
"@typescript-eslint/type-utils@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz#a60a1192a804fa472a92c41656853ac6a9ba7176"
|
||||
integrity sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.1"
|
||||
"@typescript-eslint/typescript-estree" "8.59.1"
|
||||
"@typescript-eslint/utils" "8.59.1"
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
"@typescript-eslint/typescript-estree" "8.59.2"
|
||||
"@typescript-eslint/utils" "8.59.2"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/types@8.59.1", "@typescript-eslint/types@^8.59.1":
|
||||
version "8.59.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.1.tgz#c1d014d3f03a97e0113a8899fc9d4e45a7fb0ca9"
|
||||
integrity sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==
|
||||
"@typescript-eslint/types@8.59.2", "@typescript-eslint/types@^8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.2.tgz#01caabcd7e4715c33ad5e11cab260829714d6b9c"
|
||||
integrity sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.59.1":
|
||||
version "8.59.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz#4391fadf98a22c869c5b6522dbf4e491e53e351a"
|
||||
integrity sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==
|
||||
"@typescript-eslint/typescript-estree@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz#6a217ef65b18dbd12c718fc86a675d1d7a1414cc"
|
||||
integrity sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.59.1"
|
||||
"@typescript-eslint/tsconfig-utils" "8.59.1"
|
||||
"@typescript-eslint/types" "8.59.1"
|
||||
"@typescript-eslint/visitor-keys" "8.59.1"
|
||||
"@typescript-eslint/project-service" "8.59.2"
|
||||
"@typescript-eslint/tsconfig-utils" "8.59.2"
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
"@typescript-eslint/visitor-keys" "8.59.2"
|
||||
debug "^4.4.3"
|
||||
minimatch "^10.2.2"
|
||||
semver "^7.7.3"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/utils@8.59.1":
|
||||
version "8.59.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.1.tgz#cf6204d69701bbbc5b150f98c18aeef0a42c10bd"
|
||||
integrity sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==
|
||||
"@typescript-eslint/utils@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.2.tgz#ff619a6a3075f4017fa91b8610b752a8ca3366aa"
|
||||
integrity sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.59.1"
|
||||
"@typescript-eslint/types" "8.59.1"
|
||||
"@typescript-eslint/typescript-estree" "8.59.1"
|
||||
"@typescript-eslint/scope-manager" "8.59.2"
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
"@typescript-eslint/typescript-estree" "8.59.2"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.59.1":
|
||||
version "8.59.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz#b5cba576287a3eeb0b400b62813189abcc3f976a"
|
||||
integrity sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==
|
||||
"@typescript-eslint/visitor-keys@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz#5ccc486913cd347883d69158836b1189a660bfe6"
|
||||
integrity sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.1"
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@vitejs/plugin-react@^6.0.1":
|
||||
@@ -1132,9 +1132,9 @@ balanced-match@^4.0.2:
|
||||
integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
|
||||
|
||||
baseline-browser-mapping@^2.10.12:
|
||||
version "2.10.23"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz#3a1a55d1a691a8c8d74688af7f1fd17eac23c184"
|
||||
integrity sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==
|
||||
version "2.10.27"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz#fee941c2a0b42cdf83c6427e4c830b1d0bdab2c3"
|
||||
integrity sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.14"
|
||||
@@ -1309,9 +1309,9 @@ doctrine@^2.1.0:
|
||||
esutils "^2.0.2"
|
||||
|
||||
dompurify@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.1.tgz#521d04483ac12631b2aedf434a5f5390933b8789"
|
||||
integrity sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==
|
||||
version "3.4.2"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.2.tgz#f0ff81be682c485505097ba8195a058d8f575218"
|
||||
integrity sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==
|
||||
optionalDependencies:
|
||||
"@types/trusted-types" "^2.0.7"
|
||||
|
||||
@@ -1325,9 +1325,9 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1:
|
||||
gopd "^1.2.0"
|
||||
|
||||
electron-to-chromium@^1.5.328:
|
||||
version "1.5.344"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz#6437cc08a7d9b914a98120e182f37793c9eaffd4"
|
||||
integrity sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==
|
||||
version "1.5.351"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.351.tgz#7314fbb5b4835a1869feaec09665541b6a84cd37"
|
||||
integrity sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA==
|
||||
|
||||
emoji-mart@^5.6.0:
|
||||
version "5.6.0"
|
||||
@@ -1538,9 +1538,9 @@ eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1:
|
||||
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
|
||||
|
||||
eslint@^10.2.1:
|
||||
version "10.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.2.1.tgz#224b2a6caeb34473eddcf918762363e2e063222a"
|
||||
integrity sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==
|
||||
version "10.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.3.0.tgz#ed5b810eb8e0191bf24bddcf9cdb45b974e0a16d"
|
||||
integrity sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.8.0"
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
@@ -1801,7 +1801,7 @@ has-tostringtag@^1.0.2:
|
||||
dependencies:
|
||||
has-symbols "^1.0.3"
|
||||
|
||||
hasown@^2.0.2:
|
||||
hasown@^2.0.2, hasown@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.3.tgz#5e5c2b15b60370a4c7930c383dfb76bf17bc403c"
|
||||
integrity sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==
|
||||
@@ -1890,11 +1890,11 @@ is-callable@^1.2.7:
|
||||
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
|
||||
|
||||
is-core-module@^2.16.1:
|
||||
version "2.16.1"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
|
||||
integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
|
||||
version "2.16.2"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.2.tgz#3e07450a8080ebce3fbf0cac494f4d2ab324e082"
|
||||
integrity sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==
|
||||
dependencies:
|
||||
hasown "^2.0.2"
|
||||
hasown "^2.0.3"
|
||||
|
||||
is-data-view@^1.0.1, is-data-view@^1.0.2:
|
||||
version "1.0.2"
|
||||
@@ -2050,9 +2050,9 @@ iterator.prototype@^1.1.5:
|
||||
set-function-name "^2.0.2"
|
||||
|
||||
jiti@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92"
|
||||
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64"
|
||||
integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
@@ -2263,9 +2263,9 @@ ms@^2.1.3:
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
nanoid@^3.3.11:
|
||||
version "3.3.11"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||
version "3.3.12"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05"
|
||||
integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
@@ -2430,9 +2430,9 @@ postcss-selector-parser@^7.0.0:
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss@^8.5.10, postcss@^8.5.12, postcss@^8.5.6:
|
||||
version "8.5.12"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.12.tgz#cd0c0f667f7cb0521e2313234ea6e707a9ec1ddb"
|
||||
integrity sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==
|
||||
version "8.5.14"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c"
|
||||
integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==
|
||||
dependencies:
|
||||
nanoid "^3.3.11"
|
||||
picocolors "^1.1.1"
|
||||
@@ -2884,14 +2884,14 @@ typed-array-length@^1.0.7:
|
||||
reflect.getprototypeof "^1.0.6"
|
||||
|
||||
typescript-eslint@^8.59.1:
|
||||
version "8.59.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.1.tgz#244a9fcbf27057ebbc2281d408239f1861b55b78"
|
||||
integrity sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz#e24b4f7232e20112e40572dba162a829a738ce98"
|
||||
integrity sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.59.1"
|
||||
"@typescript-eslint/parser" "8.59.1"
|
||||
"@typescript-eslint/typescript-estree" "8.59.1"
|
||||
"@typescript-eslint/utils" "8.59.1"
|
||||
"@typescript-eslint/eslint-plugin" "8.59.2"
|
||||
"@typescript-eslint/parser" "8.59.2"
|
||||
"@typescript-eslint/typescript-estree" "8.59.2"
|
||||
"@typescript-eslint/utils" "8.59.2"
|
||||
|
||||
typescript@^6.0.3:
|
||||
version "6.0.3"
|
||||
@@ -3047,6 +3047,6 @@ yocto-queue@^0.1.0:
|
||||
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
|
||||
|
||||
"zod@^3.25.0 || ^4.0.0":
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a"
|
||||
integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356"
|
||||
integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==
|
||||
|
||||
Reference in New Issue
Block a user