From a11987e1e00e8ba53ad9df31fb63a84d87c832f0 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 15 Mar 2026 00:25:57 +0100 Subject: [PATCH 01/33] Add FurniEditor tool with Next.js API integration - FurniEditor component with Search/Edit tabs (NitroCard UI) - useFurniEditor hook connecting to Next.js API routes via Vite proxy - Edit Furni button in room infostand (godMode) with sprite ID lookup - Toolbar: 3-column flex layout (icons | chat | friends) - Heroicons SVG for ID/Sprite display in infostand and edit view - Vite config: proxy /api to Next.js, aliases for renderer3 packages --- index.html | 2 +- src/components/MainView.tsx | 2 + .../furni-editor/FurniEditorView.tsx | 140 ++++++++++ .../views/FurniEditorCreateView.tsx | 159 +++++++++++ .../views/FurniEditorEditView.tsx | 249 ++++++++++++++++++ .../views/FurniEditorSearchView.tsx | 134 ++++++++++ .../infostand/InfoStandWidgetFurniView.tsx | 29 +- src/components/toolbar/ToolbarView.tsx | 60 ++--- src/hooks/furni-editor/index.ts | 1 + src/hooks/furni-editor/useFurniEditor.ts | 239 +++++++++++++++++ vite.config.mjs | 38 ++- 11 files changed, 1019 insertions(+), 34 deletions(-) create mode 100644 src/components/furni-editor/FurniEditorView.tsx create mode 100644 src/components/furni-editor/views/FurniEditorCreateView.tsx create mode 100644 src/components/furni-editor/views/FurniEditorEditView.tsx create mode 100644 src/components/furni-editor/views/FurniEditorSearchView.tsx create mode 100644 src/hooks/furni-editor/index.ts create mode 100644 src/hooks/furni-editor/useFurniEditor.ts diff --git a/index.html b/index.html index 09b372f..4e6a87e 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@ - + diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 3fef0cc..d9594bc 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -9,6 +9,7 @@ import { CampaignView } from './campaign/CampaignView'; import { CatalogView } from './catalog/CatalogView'; import { ChatHistoryView } from './chat-history/ChatHistoryView'; import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView'; +import { FurniEditorView } from './furni-editor/FurniEditorView'; import { FriendsView } from './friends/FriendsView'; import { GameCenterView } from './game-center/GameCenterView'; import { GroupsView } from './groups/GroupsView'; @@ -115,6 +116,7 @@ export const MainView: FC<{}> = props => + ); diff --git a/src/components/furni-editor/FurniEditorView.tsx b/src/components/furni-editor/FurniEditorView.tsx new file mode 100644 index 0000000..a013554 --- /dev/null +++ b/src/components/furni-editor/FurniEditorView.tsx @@ -0,0 +1,140 @@ +import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; +import { useFurniEditor } from '../../hooks/furni-editor'; +import { FurniEditorEditView } from './views/FurniEditorEditView'; +import { FurniEditorSearchView } from './views/FurniEditorSearchView'; + +const TAB_SEARCH = 0; +const TAB_EDIT = 1; + +export const FurniEditorView: FC<{}> = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ activeTab, setActiveTab ] = useState(TAB_SEARCH); + + const { + items, total, page, loading, error, clearError, + selectedItem, catalogItems, furniDataEntry, + interactions, + searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions + } = useFurniEditor(); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': + setIsVisible(true); + return; + case 'hide': + setIsVisible(false); + return; + case 'toggle': + setIsVisible(prev => !prev); + return; + } + }, + eventUrlPrefix: 'furni-editor/' + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + useEffect(() => + { + if(isVisible) loadInteractions(); + }, [ isVisible ]); + + useEffect(() => + { + const handler = async (e: CustomEvent<{ spriteId: number }>) => + { + const { spriteId } = e.detail; + + const ok = await loadBySpriteId(spriteId); + + if(ok) setActiveTab(TAB_EDIT); + }; + + window.addEventListener('furni-editor:open', handler as EventListener); + + return () => window.removeEventListener('furni-editor:open', handler as EventListener); + }, [ loadBySpriteId ]); + + const handleSelect = useCallback(async (id: number) => + { + const ok = await loadDetail(id); + + if(ok) setActiveTab(TAB_EDIT); + }, [ loadDetail ]); + + const handleBack = useCallback(() => + { + setActiveTab(TAB_SEARCH); + }, []); + + const handleClose = useCallback(() => + { + setIsVisible(false); + }, []); + + if(!isVisible) return null; + + return ( + + + + setActiveTab(TAB_SEARCH) }> + Search + + selectedItem && setActiveTab(TAB_EDIT) }> + Edit + + + + { error && +
+ { error } + x +
+ } + + { activeTab === TAB_SEARCH && + + } + + { activeTab === TAB_EDIT && selectedItem && + + } + +
+
+ ); +}; diff --git a/src/components/furni-editor/views/FurniEditorCreateView.tsx b/src/components/furni-editor/views/FurniEditorCreateView.tsx new file mode 100644 index 0000000..f47530c --- /dev/null +++ b/src/components/furni-editor/views/FurniEditorCreateView.tsx @@ -0,0 +1,159 @@ +import { FC, useCallback, useState } from 'react'; +import { Button, Column, Flex, Text } from '../../../common'; + +interface FurniEditorCreateViewProps +{ + interactions: string[]; + loading: boolean; + onCreate: (fields: Record) => Promise; + onCreated: (id: number) => void; +} + +export const FurniEditorCreateView: FC = props => +{ + const { interactions, loading, onCreate, onCreated } = props; + const [ success, setSuccess ] = useState(null); + + const [ form, setForm ] = useState({ + itemName: '', + publicName: '', + spriteId: 0, + type: 's' as 's' | 'i', + width: 1, + length: 1, + stackHeight: 0, + allowStack: true, + allowSit: false, + allowLay: false, + allowWalk: false, + allowGift: true, + allowTrade: true, + allowRecycle: true, + allowMarketplaceSell: true, + allowInventoryStack: true, + interactionType: '', + interactionModesCount: 1, + customparams: '', + }); + + const setField = useCallback((key: string, value: unknown) => + { + setForm(prev => ({ ...prev, [key]: value })); + setSuccess(null); + }, []); + + const handleCreate = useCallback(async () => + { + if(!form.itemName || !form.publicName) return; + + const id = await onCreate(form); + + if(id) + { + setSuccess(id); + setTimeout(() => onCreated(id), 1000); + } + }, [ form, onCreate, onCreated ]); + + const inputClass = 'form-control form-control-sm'; + const labelClass = 'text-[11px] font-bold text-[#333] mb-0'; + + return ( + + { success && +
+ Item created with ID #{ success }! +
+ } + +
+ Basic Info +
+
+ + setField('itemName', e.target.value) } placeholder="my_custom_furni" /> +
+
+ + setField('publicName', e.target.value) } placeholder="My Custom Furni" /> +
+
+ + setField('spriteId', Number(e.target.value)) } /> +
+
+ + +
+
+
+ +
+ Dimensions +
+
+ + setField('width', Number(e.target.value)) } /> +
+
+ + setField('length', Number(e.target.value)) } /> +
+
+ + setField('stackHeight', Number(e.target.value)) } /> +
+
+
+ +
+ Permissions +
+ { [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay', 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell', 'allowInventoryStack' ].map(key => ( + + )) } +
+
+ +
+ Interaction +
+
+ + +
+
+ + setField('interactionModesCount', Number(e.target.value)) } /> +
+
+
+ + setField('customparams', e.target.value) } /> +
+
+ + + + +
+ ); +}; diff --git a/src/components/furni-editor/views/FurniEditorEditView.tsx b/src/components/furni-editor/views/FurniEditorEditView.tsx new file mode 100644 index 0000000..dc648ef --- /dev/null +++ b/src/components/furni-editor/views/FurniEditorEditView.tsx @@ -0,0 +1,249 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import { Button, Column, Flex, Text } from '../../../common'; +import { CatalogRef, FurniDetail } from '../../../hooks/furni-editor'; + +interface FurniEditorEditViewProps +{ + item: FurniDetail; + catalogItems: CatalogRef[]; + furniDataEntry: Record | null; + interactions: string[]; + loading: boolean; + onUpdate: (id: number, fields: Record) => Promise; + onDelete: (id: number) => Promise; + onBack: () => void; + onRefresh: (id: number) => void; +} + +export const FurniEditorEditView: FC = props => +{ + const { item, catalogItems, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack, onRefresh } = props; + + const [ form, setForm ] = useState({ + itemName: '', + publicName: '', + spriteId: 0, + type: 's', + width: 1, + length: 1, + stackHeight: 0, + allowStack: true, + allowWalk: false, + allowSit: false, + allowLay: false, + allowGift: true, + allowTrade: true, + allowRecycle: true, + allowMarketplaceSell: true, + allowInventoryStack: true, + interactionType: '', + interactionModesCount: 0, + customparams: '', + }); + + const [ confirmDelete, setConfirmDelete ] = useState(false); + + useEffect(() => + { + if(!item) return; + + setForm({ + itemName: item.itemName || '', + publicName: item.publicName || '', + spriteId: item.spriteId || 0, + type: item.type || 's', + width: item.width || 1, + length: item.length || 1, + stackHeight: item.stackHeight || 0, + allowStack: !!item.allowStack, + allowWalk: !!item.allowWalk, + allowSit: !!item.allowSit, + allowLay: !!item.allowLay, + allowGift: !!item.allowGift, + allowTrade: !!item.allowTrade, + allowRecycle: !!item.allowRecycle, + allowMarketplaceSell: !!item.allowMarketplaceSell, + allowInventoryStack: !!item.allowInventoryStack, + interactionType: item.interactionType || '', + interactionModesCount: item.interactionModesCount || 0, + customparams: item.customparams || '', + }); + + setConfirmDelete(false); + }, [ item ]); + + const setField = useCallback((key: string, value: unknown) => + { + setForm(prev => ({ ...prev, [key]: value })); + }, []); + + const handleSave = useCallback(async () => + { + const ok = await onUpdate(item.id, form); + + if(ok) onRefresh(item.id); + }, [ item, form, onUpdate, onRefresh ]); + + const handleDelete = useCallback(async () => + { + if(!confirmDelete) return setConfirmDelete(true); + + const ok = await onDelete(item.id); + + if(ok) onBack(); + }, [ confirmDelete, item, onDelete, onBack ]); + + const inputClass = 'form-control form-control-sm'; + const labelClass = 'text-[11px] font-bold text-[#333] mb-0'; + + return ( + + + + + + + + { item.id } + | + + + + { item.spriteId } + + ({ item.usageCount } in use) + + + { /* Basic Info */ } +
+ Basic Info +
+
+ + setField('itemName', e.target.value) } /> +
+
+ + setField('publicName', e.target.value) } /> +
+
+ + setField('spriteId', Number(e.target.value)) } /> +
+
+ + +
+
+
+ + { /* Dimensions */ } +
+ Dimensions +
+
+ + setField('width', Number(e.target.value)) } /> +
+
+ + setField('length', Number(e.target.value)) } /> +
+
+ + setField('stackHeight', Number(e.target.value)) } /> +
+
+
+ + { /* Permissions */ } +
+ Permissions +
+ { [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay', 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell', 'allowInventoryStack' ].map(key => ( + + )) } +
+
+ + { /* Interaction */ } +
+ Interaction +
+
+ + +
+
+ + setField('interactionModesCount', Number(e.target.value)) } /> +
+
+
+ + setField('customparams', e.target.value) } /> +
+
+ + { /* Catalog References */ } + { catalogItems.length > 0 && +
+ Catalog ({ catalogItems.length }) +
+ { catalogItems.map(ci => ( +
+ { ci.catalogName } (page: { ci.pageName }) + { ci.costCredits }c + { ci.costPoints }p +
+ )) } +
+
+ } + + { /* FurniData.json Entry */ } + { furniDataEntry && +
+ FurniData.json +
+ { Object.entries(furniDataEntry).map(([ key, value ]) => ( +
+ { key } + { String(value ?? '') } +
+ )) } +
+
+ } + + { /* Actions */ } + + + + +
+ ); +}; diff --git a/src/components/furni-editor/views/FurniEditorSearchView.tsx b/src/components/furni-editor/views/FurniEditorSearchView.tsx new file mode 100644 index 0000000..7b3f8c6 --- /dev/null +++ b/src/components/furni-editor/views/FurniEditorSearchView.tsx @@ -0,0 +1,134 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import { Button, Column, Flex, Text } from '../../../common'; +import { FurniItem } from '../../../hooks/furni-editor'; + +interface FurniEditorSearchViewProps +{ + items: FurniItem[]; + total: number; + page: number; + loading: boolean; + onSearch: (query: string, type: string, page: number) => void; + onSelect: (id: number) => void; +} + +export const FurniEditorSearchView: FC = props => +{ + const { items, total, page, loading, onSearch, onSelect } = props; + const [ query, setQuery ] = useState(''); + const [ typeFilter, setTypeFilter ] = useState(''); + + useEffect(() => + { + onSearch('', '', 1); + }, []); + + const handleSearch = useCallback(() => + { + onSearch(query, typeFilter, 1); + }, [ query, typeFilter, onSearch ]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => + { + if(e.key === 'Enter') handleSearch(); + }, [ handleSearch ]); + + const totalPages = Math.ceil(total / 20); + + return ( + + + + Search + setQuery(e.target.value) } + onKeyDown={ handleKeyDown } + /> + + + Type + + + + + + + + + + + + + + + + + + + { items.map(item => ( + onSelect(item.id) } + > + + + + + + + + )) } + { items.length === 0 && !loading && + + + + } + +
IDSpriteNamePublic NameTypeInteraction
{ item.id }{ item.spriteId }{ item.itemName }{ item.publicName } + + { item.type === 's' ? 'Floor' : 'Wall' } + + { item.interactionType || '-' }
No items found
+
+ + { totalPages > 1 && + + + { total } items - Page { page }/{ totalPages } + + + + + + + } +
+ ); +}; diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index d58a720..468247c 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -552,7 +552,21 @@ export const InfoStandWidgetFurniView: FC = props { godMode && <>
- { canSeeFurniId && ID: { avatarInfo.id } } + { canSeeFurniId && +
+
+ + + + ID: { avatarInfo.id } +
+
+ + + + Sprite: { (() => { const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?'; })() } +
+
} { (!avatarInfo.isWallItem && canMove) && <> + { dropdownOpen &&
{ /* Left panel: position + rotation */ } diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 7d6743a..fbb5ae8 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -69,38 +69,38 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => )} - - - - - { - setMeExpanded(!isMeExpanded); - event.stopPropagation(); - } }> - - { (getTotalUnseen > 0) && - } - - { isInRoom && - VisitDesktop() } /> } - { !isInRoom && - CreateLinkEvent('navigator/goto/home') } /> } - CreateLinkEvent('navigator/toggle') } /> - { GetConfigurationValue('game.center.enabled') && - CreateLinkEvent('games/toggle') } /> } - CreateLinkEvent('catalog/toggle') } /> - CreateLinkEvent('inventory/toggle') }> - { (getFullCount > 0) && - } - - { isInRoom && - CreateLinkEvent('camera/toggle') } /> } - { isMod && - CreateLinkEvent('mod-tools/toggle') } /> } + + + + { + setMeExpanded(!isMeExpanded); + event.stopPropagation(); + } }> + + { (getTotalUnseen > 0) && + } - + { isInRoom && + VisitDesktop() } /> } + { !isInRoom && + CreateLinkEvent('navigator/goto/home') } /> } + CreateLinkEvent('navigator/toggle') } /> + { GetConfigurationValue('game.center.enabled') && + CreateLinkEvent('games/toggle') } /> } + CreateLinkEvent('catalog/toggle') } /> + CreateLinkEvent('inventory/toggle') }> + { (getFullCount > 0) && + } + + { isInRoom && + CreateLinkEvent('camera/toggle') } /> } + { isMod && + CreateLinkEvent('mod-tools/toggle') } /> } + { isMod && + CreateLinkEvent('furni-editor/toggle') } /> } - + + CreateLinkEvent('friends/toggle') }> { (requests.length > 0) && diff --git a/src/hooks/furni-editor/index.ts b/src/hooks/furni-editor/index.ts new file mode 100644 index 0000000..47ce6ef --- /dev/null +++ b/src/hooks/furni-editor/index.ts @@ -0,0 +1 @@ +export * from './useFurniEditor'; diff --git a/src/hooks/furni-editor/useFurniEditor.ts b/src/hooks/furni-editor/useFurniEditor.ts new file mode 100644 index 0000000..e4258e5 --- /dev/null +++ b/src/hooks/furni-editor/useFurniEditor.ts @@ -0,0 +1,239 @@ +import { useCallback, useState } from 'react'; + +export interface FurniItem +{ + id: number; + spriteId: number; + itemName: string; + publicName: string; + type: string; + width: number; + length: number; + stackHeight: number; + allowStack: boolean; + allowWalk: boolean; + allowSit: boolean; + allowLay: boolean; + interactionType: string; + interactionModesCount: number; +} + +export interface FurniDetail extends FurniItem +{ + allowGift: boolean; + allowTrade: boolean; + allowRecycle: boolean; + allowMarketplaceSell: boolean; + allowInventoryStack: boolean; + vendingIds: string; + customparams: string; + effectIdMale: number; + effectIdFemale: number; + clothingOnWalk: string; + multiheight: string; + description: string; + usageCount: number; +} + +export interface CatalogRef +{ + id: number; + catalogName: string; + costCredits: number; + costPoints: number; + pointsType: number; + pageId: number; + pageName: string; +} + +const API_BASE = '/api/admin/furni-editor'; + +async function apiFetch(url: string, options?: RequestInit): Promise +{ + const res = await fetch(url, { credentials: 'include', ...options }); + const data = await res.json(); + + if(!res.ok || data.error) throw new Error(data.error || 'API error'); + + return data; +} + +export const useFurniEditor = () => +{ + const [ items, setItems ] = useState([]); + const [ total, setTotal ] = useState(0); + const [ page, setPage ] = useState(1); + const [ loading, setLoading ] = useState(false); + const [ error, setError ] = useState(null); + const [ selectedItem, setSelectedItem ] = useState(null); + const [ catalogItems, setCatalogItems ] = useState([]); + const [ interactions, setInteractions ] = useState([]); + const [ furniDataEntry, setFurniDataEntry ] = useState | null>(null); + + const clearError = useCallback(() => setError(null), []); + + const searchItems = useCallback(async (query: string, type: string, pg: number) => + { + setLoading(true); + setError(null); + + try + { + const params = new URLSearchParams({ q: query, limit: '20', page: String(pg) }); + + if(type) params.set('type', type); + + const data = await apiFetch<{ items: FurniItem[]; total: number; page: number }>(`${ API_BASE }?${ params }`); + + setItems(data.items); + setTotal(data.total); + setPage(data.page); + } + catch(e: any) + { + setError(e.message); + } + finally + { + setLoading(false); + } + }, []); + + const loadDetail = useCallback(async (id: number): Promise => + { + setLoading(true); + setError(null); + + try + { + const data = await apiFetch<{ item: FurniDetail; catalogItems: CatalogRef[]; furniDataEntry: Record | null }>(`${ API_BASE }/detail?id=${ id }`); + + setSelectedItem(data.item); + setCatalogItems(data.catalogItems); + setFurniDataEntry(data.furniDataEntry); + + return true; + } + catch(e: any) + { + setError(e.message); + + return false; + } + finally + { + setLoading(false); + } + }, []); + + const updateItem = useCallback(async (id: number, fields: Record) => + { + setLoading(true); + setError(null); + + try + { + await apiFetch(`${ API_BASE }/update?id=${ id }`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(fields) + }); + + return true; + } + catch(e: any) + { + setError(e.message); + + return false; + } + finally + { + setLoading(false); + } + }, []); + + const createItem = useCallback(async (fields: Record) => + { + setLoading(true); + setError(null); + + try + { + const data = await apiFetch<{ id: number }>(`${ API_BASE }`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(fields) + }); + + return data.id; + } + catch(e: any) + { + setError(e.message); + + return null; + } + finally + { + setLoading(false); + } + }, []); + + const deleteItem = useCallback(async (id: number) => + { + setLoading(true); + setError(null); + + try + { + await apiFetch(`${ API_BASE }/delete?id=${ id }`, { method: 'POST' }); + + return true; + } + catch(e: any) + { + setError(e.message); + + return false; + } + finally + { + setLoading(false); + } + }, []); + + const loadInteractions = useCallback(async () => + { + try + { + const data = await apiFetch<{ interactions: Array }>(`${ API_BASE }/interactions`); + + setInteractions(data.interactions.map(i => typeof i === 'string' ? i : i.name)); + } + catch {} + }, []); + + const loadBySpriteId = useCallback(async (spriteId: number): Promise => + { + try + { + const data = await apiFetch<{ id: number }>(`${ API_BASE }/by-sprite?spriteId=${ spriteId }`); + + return await loadDetail(data.id); + } + catch(e: any) + { + setError(e.message); + + return false; + } + }, [ loadDetail ]); + + return { + items, total, page, loading, error, clearError, + selectedItem, setSelectedItem, catalogItems, furniDataEntry, + interactions, + searchItems, loadDetail, loadBySpriteId, updateItem, createItem, deleteItem, loadInteractions + }; +}; diff --git a/vite.config.mjs b/vite.config.mjs index 3267645..7e92144 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -3,12 +3,46 @@ import { resolve } from 'path'; import { defineConfig } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; +const renderer3 = resolve(__dirname, '..', 'renderer3'); + export default defineConfig({ plugins: [ react(), tsconfigPaths() ], + server: { + fs: { + allow: [ + resolve(__dirname), // nitro3 itself + renderer3, // renderer3 source + packages + ] + }, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + } + } + }, resolve: { alias: { '@': resolve(__dirname, 'src'), - '~': resolve(__dirname, 'node_modules') + '~': resolve(__dirname, 'node_modules'), + // Renderer3 workspace packages → point to their src/index.ts + '@nitrots/api': resolve(renderer3, 'packages/api/src/index.ts'), + '@nitrots/assets': resolve(renderer3, 'packages/assets/src/index.ts'), + '@nitrots/avatar': resolve(renderer3, 'packages/avatar/src/index.ts'), + '@nitrots/camera': resolve(renderer3, 'packages/camera/src/index.ts'), + '@nitrots/communication': resolve(renderer3, 'packages/communication/src/index.ts'), + '@nitrots/configuration': resolve(renderer3, 'packages/configuration/src/index.ts'), + '@nitrots/events': resolve(renderer3, 'packages/events/src/index.ts'), + '@nitrots/localization': resolve(renderer3, 'packages/localization/src/index.ts'), + '@nitrots/room': resolve(renderer3, 'packages/room/src/index.ts'), + '@nitrots/session': resolve(renderer3, 'packages/session/src/index.ts'), + '@nitrots/sound': resolve(renderer3, 'packages/sound/src/index.ts'), + '@nitrots/utils/src': resolve(renderer3, 'packages/utils/src'), + '@nitrots/utils': resolve(renderer3, 'packages/utils/src/index.ts'), + // Resolve pixi.js and pixi-filters from renderer3's node_modules + 'pixi.js': resolve(renderer3, 'node_modules/pixi.js'), + 'pixi-filters': resolve(renderer3, 'node_modules/pixi-filters'), + 'howler': resolve(renderer3, 'node_modules/howler'), } }, build: { @@ -21,7 +55,7 @@ export default defineConfig({ { if(id.includes('node_modules')) { - if(id.includes('@nitrots/nitro-renderer')) return 'nitro-renderer'; + if(id.includes('@nitrots/nitro-renderer') || id.includes('renderer3')) return 'nitro-renderer'; return 'vendor'; } From f0133a8e99936084b7de4ff342f3147cb2566071 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 15 Mar 2026 11:08:04 +0100 Subject: [PATCH 02/33] =?UTF-8?q?=F0=9F=90=9B=20Fix=20crackableHits=20unde?= =?UTF-8?q?fined=20TypeError=20in=20InfoStandWidgetFurniView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: medievalshell --- .../avatar-info/infostand/InfoStandWidgetFurniView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index 468247c..48e9b84 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -220,8 +220,8 @@ export const InfoStandWidgetFurniView: FC = props canUse = true; isCrackable = true; - crackableHits = stuffData.hits; - crackableTarget = stuffData.target; + crackableHits = stuffData?.hits ?? 0; + crackableTarget = stuffData?.target ?? 0; } else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) @@ -527,7 +527,7 @@ export const InfoStandWidgetFurniView: FC = props { isCrackable && <>
- { LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ crackableHits.toString(), crackableTarget.toString() ]) } + { LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ (crackableHits ?? 0).toString(), (crackableTarget ?? 0).toString() ]) } } { avatarInfo.groupId > 0 && <> From e694f34e342a609cf574e428554981630e16062a Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 15 Mar 2026 12:45:54 +0100 Subject: [PATCH 03/33] somethings --- .claude/settings.local.json | 30 + public/renderer-config.example | 10 +- public/renderer-config.json | 110 ++ public/ui-config.example | 4 +- public/ui-config.json | 2427 ++++++++++++++++++++++++ yarn.lock | 3238 ++++++++++++++++++++++++++++++++ 6 files changed, 5812 insertions(+), 7 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 public/renderer-config.json create mode 100644 public/ui-config.json create mode 100644 yarn.lock diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..bbe542f --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,30 @@ +{ + "permissions": { + "allow": [ + "Bash(cp -r nitro3_temp/* nitro3/)", + "Bash(cp -r nitro3_temp/.* nitro3/)", + "Bash(rm -rf nitro3_temp)", + "Bash(yarn install:*)", + "Bash(find /E/www/habbo-next/public/nitro3 -type f -iname *furni*editor*)", + "Bash(mkdir -p /e/www/habbo-next/public/nitro3/src/components/furni-editor/views)", + "Bash(mkdir -p /e/www/habbo-next/public/nitro3/src/hooks/furni-editor)", + "Bash(find /e/www/habbo-next/src -type f \\\\\\(-name *client* \\\\\\))", + "Bash(grep -E \"\\\\.\\(tsx|ts|jsx|js\\)$\")", + "Bash(ls -la /e/www/habbo-next/src/app/[locale]/\\\\\\(client\\\\\\)/)", + "Bash(grep -r \"NITRO_URL\\\\|/client\" /e/www/habbo-next/.env*)", + "Bash(grep -r \"NEXT\\\\|API_URL\\\\|BASE_URL\\\\|apiUrl\" /e/www/habbo-next/public/nitro3/src/ --include=*.ts --include=*.tsx -l)", + "Bash(find /e/www/habbo-next/src/lib -name *catalog* -o -name *furni*)", + "Bash(npx tsc:*)", + "Bash(npx next:*)", + "Bash(find /e/www/habbo-next -name *.prisma -o -name schema.prisma)", + "Bash(pushd E:/www/habbo-next)", + "Bash(popd)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(find /e/www/habbo-next/src/app -type f -path *catalog*)", + "Bash(echo \"EXIT:$?\")", + "Bash(find /E/www/habbo-next/src -type f -name *prisma*)" + ] + } +} diff --git a/public/renderer-config.example b/public/renderer-config.example index 54b7a47..62dc183 100644 --- a/public/renderer-config.example +++ b/public/renderer-config.example @@ -1,8 +1,8 @@ { - "socket.url": "ws://## YOUR HOST ##:2096", - "asset.url": "http://## YOUR HOST ##/gamedata", - "image.library.url": "http://## YOUR HOST ##/gamedata/c_images/", - "hof.furni.url": "http://## YOUR HOST ##", + "socket.url": "ws:localhost:2097", + "asset.url": "http://localhost:3000/public\nitro-assets\gamedata", + "image.library.url": "http://localhost:3000/swf/gamedata/c_images/", + "hof.furni.url": "http://localhost:3000/", "images.url": "${asset.url}/images", "gamedata.url": "${asset.url}", "sounds.url": "${asset.url}/sounds/%sample%.mp3", @@ -585,4 +585,4 @@ "${images.url}/clear_icon.png", "${images.url}/big_arrow.png" ] -} \ No newline at end of file +} diff --git a/public/renderer-config.json b/public/renderer-config.json new file mode 100644 index 0000000..114bf6d --- /dev/null +++ b/public/renderer-config.json @@ -0,0 +1,110 @@ +{ + "socket.url": "ws://localhost:2097", + "asset.url": "http://localhost:3000/nitro-assets", + "image.library.url": "http://localhost:3000/swf/c_images/", + "hof.furni.url": "http://localhost:3000/swf/dcr/hof_furni", + "images.url": "${asset.url}/images", + "gamedata.url": "${asset.url}/gamedata", + "sounds.url": "${asset.url}/sounds/%sample%.mp3", + "external.texts.url": [ "${gamedata.url}/ExternalTexts.json", "${gamedata.url}/UITexts.json" ], + "external.samples.url": "${hof.furni.url}/mp3/sound_machine_sample_%sample%.mp3", + "furnidata.url": "${gamedata.url}/FurnitureData.json", + "productdata.url": "${gamedata.url}/ProductData.json", + "avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json", + "avatar.figuredata.url": "${gamedata.url}/FigureData.json", + "avatar.figuremap.url": "${gamedata.url}/FigureMap.json", + "avatar.effectmap.url": "${gamedata.url}/EffectMap.json", + "avatar.asset.url": "${asset.url}/bundled/figure/%libname%.nitro", + "avatar.asset.effect.url": "${asset.url}/bundled/effect/%libname%.nitro", + "furni.asset.url": "${asset.url}/bundled/furniture/%libname%.nitro", + "furni.asset.icon.url": "${hof.furni.url}/icons/%libname%%param%_icon.png", + "pet.asset.url": "${asset.url}/bundled/pet/%libname%.nitro", + "generic.asset.url": "${asset.url}/bundled/generic/%libname%.nitro", + "badge.asset.url": "${image.library.url}album1584/%badgename%.gif", + "furni.rotation.bounce.steps": 20, + "furni.rotation.bounce.height": 0.0625, + "enable.avatar.arrow": false, + "system.log.debug": false, + "system.log.warn": false, + "system.log.error": false, + "system.log.events": false, + "system.log.packets": false, + "system.fps.animation": 24, + "system.fps.max": 60, + "system.pong.manually": true, + "system.pong.interval.ms": 20000, + "room.color.skip.transition": true, + "room.landscapes.enabled": true, + "avatar.mandatory.libraries": [ + "bd:1", + "li:0" + ], + "avatar.mandatory.effect.libraries": [ + "dance.1", + "dance.2", + "dance.3", + "dance.4" + ], + "avatar.default.figuredata": {"palettes":[{"id":1,"colors":[{"id":99999,"index":1001,"club":0,"selectable":false,"hexCode":"DDDDDD"},{"id":99998,"index":1001,"club":0,"selectable":false,"hexCode":"FAFAFA"}]},{"id":3,"colors":[{"id":10001,"index":1001,"club":0,"selectable":false,"hexCode":"EEEEEE"},{"id":10002,"index":1002,"club":0,"selectable":false,"hexCode":"FA3831"},{"id":10003,"index":1003,"club":0,"selectable":false,"hexCode":"FD92A0"},{"id":10004,"index":1004,"club":0,"selectable":false,"hexCode":"2AC7D2"},{"id":10005,"index":1005,"club":0,"selectable":false,"hexCode":"35332C"},{"id":10006,"index":1006,"club":0,"selectable":false,"hexCode":"EFFF92"},{"id":10007,"index":1007,"club":0,"selectable":false,"hexCode":"C6FF98"},{"id":10008,"index":1008,"club":0,"selectable":false,"hexCode":"FF925A"},{"id":10009,"index":1009,"club":0,"selectable":false,"hexCode":"9D597E"},{"id":10010,"index":1010,"club":0,"selectable":false,"hexCode":"B6F3FF"},{"id":10011,"index":1011,"club":0,"selectable":false,"hexCode":"6DFF33"},{"id":10012,"index":1012,"club":0,"selectable":false,"hexCode":"3378C9"},{"id":10013,"index":1013,"club":0,"selectable":false,"hexCode":"FFB631"},{"id":10014,"index":1014,"club":0,"selectable":false,"hexCode":"DFA1E9"},{"id":10015,"index":1015,"club":0,"selectable":false,"hexCode":"F9FB32"},{"id":10016,"index":1016,"club":0,"selectable":false,"hexCode":"CAAF8F"},{"id":10017,"index":1017,"club":0,"selectable":false,"hexCode":"C5C6C5"},{"id":10018,"index":1018,"club":0,"selectable":false,"hexCode":"47623D"},{"id":10019,"index":1019,"club":0,"selectable":false,"hexCode":"8A8361"},{"id":10020,"index":1020,"club":0,"selectable":false,"hexCode":"FF8C33"},{"id":10021,"index":1021,"club":0,"selectable":false,"hexCode":"54C627"},{"id":10022,"index":1022,"club":0,"selectable":false,"hexCode":"1E6C99"},{"id":10023,"index":1023,"club":0,"selectable":false,"hexCode":"984F88"},{"id":10024,"index":1024,"club":0,"selectable":false,"hexCode":"77C8FF"},{"id":10025,"index":1025,"club":0,"selectable":false,"hexCode":"FFC08E"},{"id":10026,"index":1026,"club":0,"selectable":false,"hexCode":"3C4B87"},{"id":10027,"index":1027,"club":0,"selectable":false,"hexCode":"7C2C47"},{"id":10028,"index":1028,"club":0,"selectable":false,"hexCode":"D7FFE3"},{"id":10029,"index":1029,"club":0,"selectable":false,"hexCode":"8F3F1C"},{"id":10030,"index":1030,"club":0,"selectable":false,"hexCode":"FF6393"},{"id":10031,"index":1031,"club":0,"selectable":false,"hexCode":"1F9B79"},{"id":10032,"index":1032,"club":0,"selectable":false,"hexCode":"FDFF33"}]}],"setTypes":[{"type":"hd","paletteId":1,"mandatory_f_0":true,"mandatory_f_1":true,"mandatory_m_0":true,"mandatory_m_1":true,"sets":[{"id":99999,"gender":"U","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":1,"type":"bd","colorable":true,"index":0,"colorindex":1},{"id":1,"type":"hd","colorable":true,"index":0,"colorindex":1},{"id":1,"type":"lh","colorable":true,"index":0,"colorindex":1},{"id":1,"type":"rh","colorable":true,"index":0,"colorindex":1}]}]},{"type":"bds","paletteId":1,"mandatory_f_0":false,"mandatory_f_1":false,"mandatory_m_0":false,"mandatory_m_1":false,"sets":[{"id":10001,"gender":"U","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":10001,"type":"bds","colorable":true,"index":0,"colorindex":1},{"id":10001,"type":"lhs","colorable":true,"index":0,"colorindex":1},{"id":10001,"type":"rhs","colorable":true,"index":0,"colorindex":1}],"hiddenLayers":[{"partType":"bd"},{"partType":"rh"},{"partType":"lh"}]}]},{"type":"ss","paletteId":3,"mandatory_f_0":false,"mandatory_f_1":false,"mandatory_m_0":false,"mandatory_m_1":false,"sets":[{"id":10010,"gender":"F","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":10001,"type":"ss","colorable":true,"index":0,"colorindex":1}],"hiddenLayers":[{"partType":"ch"},{"partType":"lg"},{"partType":"ca"},{"partType":"wa"},{"partType":"sh"},{"partType":"ls"},{"partType":"rs"},{"partType":"lc"},{"partType":"rc"},{"partType":"cc"},{"partType":"cp"}]},{"id":10011,"gender":"M","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":10002,"type":"ss","colorable":true,"index":0,"colorindex":1}],"hiddenLayers":[{"partType":"ch"},{"partType":"lg"},{"partType":"ca"},{"partType":"wa"},{"partType":"sh"},{"partType":"ls"},{"partType":"rs"},{"partType":"lc"},{"partType":"rc"},{"partType":"cc"},{"partType":"cp"}]}]}]}, + "avatar.default.actions": { + "actions": [ + { + "id": "Default", + "state": "std", + "precedence": 1000, + "main": true, + "isDefault": true, + "geometryType": "vertical", + "activePartSet": "figure", + "assetPartDefinition": "std" + } + ] + }, + "pet.types": [ + "dog", + "cat", + "croco", + "terrier", + "bear", + "pig", + "lion", + "rhino", + "spider", + "turtle", + "chicken", + "frog", + "dragon", + "monster", + "monkey", + "horse", + "monsterplant", + "bunnyeaster", + "bunnyevil", + "bunnydepressed", + "bunnylove", + "pigeongood", + "pigeonevil", + "demonmonkey", + "bearbaby", + "terrierbaby", + "gnome", + "leprechaun", + "kittenbaby", + "puppybaby", + "pigletbaby", + "haloompa", + "fools", + "pterosaur", + "velociraptor", + "cow", + "dragondog" + ], + "preload.assets.urls": [ + "${asset.url}/bundled/generic/avatar_additions.nitro", + "${asset.url}/bundled/generic/group_badge.nitro", + "${asset.url}/bundled/generic/floor_editor.nitro", + "${images.url}/loading_icon.png", + "${images.url}/clear_icon.png", + "${images.url}/big_arrow.png" + ] +} diff --git a/public/ui-config.example b/public/ui-config.example index e02d544..08fce1f 100644 --- a/public/ui-config.example +++ b/public/ui-config.example @@ -1,8 +1,8 @@ { "image.library.notifications.url": "${image.library.url}notifications/%image%.png", "achievements.images.url": "${image.library.url}Quests/%image%.png", - "camera.url": "http://## YOUR HOST ##/camera/photo/", - "thumbnails.url": "http://## YOUR HOST ##/camera/photo/temp/thumb/%thumbnail%.png", + "camera.url": "http://localhost:3000/camera/photo/", + "thumbnails.url": "http://localhost:3000/camera/photo/temp/thumb/%thumbnail%.png", "url.prefix": "", "habbopages.url": "/gamedata/habbopages/", "group.homepage.url": "${url.prefix}/groups/%groupid%/id", diff --git a/public/ui-config.json b/public/ui-config.json new file mode 100644 index 0000000..55dbad6 --- /dev/null +++ b/public/ui-config.json @@ -0,0 +1,2427 @@ +{ + "image.library.notifications.url": "${image.library.url}notifications/%image%.png", + "achievements.images.url": "${image.library.url}Quests/%image%.png", + "camera.url": "/swf/usercontent/camera/", + "thumbnails.url": "/swf/usercontent/thumbnails/%thumbnail%.png", + "url.prefix": "", + "habbopages.url": "/swf/habbopages/", + "group.homepage.url": "${url.prefix}/groups/%groupid%/id", + "guide.help.alpha.groupid": 0, + "chat.viewer.height.percentage": 0.4, + "widget.dimmer.colorwheel": false, + "avatar.wardrobe.max.slots": 10, + "user.badges.max.slots": 5, + "user.tags.enabled": false, + "camera.publish.disabled": false, + "hc.disabled": false, + "badge.descriptions.enabled": true, + "motto.max.length": 38, + "bot.name.max.length": 15, + "pet.package.name.max.length": 15, + "wired.action.bot.talk.to.avatar.max.length": 64, + "wired.action.bot.talk.max.length": 64, + "wired.action.chat.max.length": 100, + "wired.action.kick.from.room.max.length": 100, + "wired.action.mute.user.max.length": 100, + "game.center.enabled": false, + "guides.enabled": true, + "toolbar.hide.quests": true, + "navigator.room.models": [{ + "clubLevel": 0, + "tileSize": 104, + "name": "a" + }, { + "clubLevel": 0, + "tileSize": 94, + "name": "b" + }, { + "clubLevel": 0, + "tileSize": 36, + "name": "c" + }, { + "clubLevel": 0, + "tileSize": 84, + "name": "d" + }, { + "clubLevel": 0, + "tileSize": 80, + "name": "e" + }, { + "clubLevel": 0, + "tileSize": 80, + "name": "f" + }, { + "clubLevel": 0, + "tileSize": 416, + "name": "i" + }, { + "clubLevel": 0, + "tileSize": 320, + "name": "j" + }, { + "clubLevel": 0, + "tileSize": 448, + "name": "k" + }, { + "clubLevel": 0, + "tileSize": 352, + "name": "l" + }, { + "clubLevel": 0, + "tileSize": 384, + "name": "m" + }, { + "clubLevel": 0, + "tileSize": 372, + "name": "n" + }, { + "clubLevel": 1, + "tileSize": 80, + "name": "g" + }, { + "clubLevel": 1, + "tileSize": 74, + "name": "h" + }, { + "clubLevel": 1, + "tileSize": 416, + "name": "o" + }, { + "clubLevel": 1, + "tileSize": 352, + "name": "p" + }, { + "clubLevel": 1, + "tileSize": 304, + "name": "q" + }, { + "clubLevel": 1, + "tileSize": 336, + "name": "r" + }, { + "clubLevel": 1, + "tileSize": 748, + "name": "u" + }, { + "clubLevel": 1, + "tileSize": 438, + "name": "v" + }, { + "clubLevel": 2, + "tileSize": 540, + "name": "t" + }, { + "clubLevel": 2, + "tileSize": 512, + "name": "w" + }, { + "clubLevel": 2, + "tileSize": 396, + "name": "x" + }, { + "clubLevel": 2, + "tileSize": 440, + "name": "y" + }, { + "clubLevel": 2, + "tileSize": 456, + "name": "z" + }, { + "clubLevel": 2, + "tileSize": 208, + "name": "0" + }, { + "clubLevel": 2, + "tileSize": 1009, + "name": "1" + }, { + "clubLevel": 2, + "tileSize": 1044, + "name": "2" + }, { + "clubLevel": 2, + "tileSize": 183, + "name": "3" + }, { + "clubLevel": 2, + "tileSize": 254, + "name": "4" + }, { + "clubLevel": 2, + "tileSize": 1024, + "name": "5" + }, { + "clubLevel": 2, + "tileSize": 801, + "name": "6" + }, { + "clubLevel": 2, + "tileSize": 354, + "name": "7" + }, { + "clubLevel": 2, + "tileSize": 888, + "name": "8" + }, { + "clubLevel": 2, + "tileSize": 926, + "name": "9" + } + ], + "backgrounds.data": [{ + "backgroundId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 2, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 3, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 4, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 5, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 6, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 7, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 8, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 9, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 10, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 11, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 12, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 13, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 14, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 15, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 16, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 17, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 18, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 19, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 20, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 21, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 22, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 23, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 24, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 25, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 26, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 27, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 28, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 29, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 30, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 31, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 32, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 33, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 34, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 35, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 36, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 37, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 38, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 39, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 40, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 41, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 42, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 43, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 44, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 45, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 46, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 47, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 48, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 49, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 50, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 51, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 52, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 53, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 54, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 55, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 56, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 57, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 58, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 59, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 60, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 61, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 62, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 63, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 64, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 65, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 66, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 67, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 68, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 69, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 70, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 71, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 72, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 73, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 74, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 75, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 76, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 77, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 78, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 79, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 80, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 81, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 82, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 83, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 84, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 85, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 86, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 87, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 88, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 89, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 90, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 91, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 92, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 93, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 94, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 95, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 96, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 97, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 98, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 99, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 100, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 101, + "minRank": 2, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 102, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 103, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 104, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 105, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 106, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 107, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 108, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 109, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 110, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 111, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 112, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 113, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 114, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 115, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 116, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 117, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 118, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 119, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 120, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 121, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 122, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 123, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 124, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 125, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 126, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 127, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 128, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 129, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 130, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 131, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 132, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 133, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 134, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 135, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 136, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 137, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 138, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 139, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 140, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 141, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 142, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 143, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 144, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 145, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 146, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 147, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 148, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 149, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 150, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 151, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 152, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 153, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 154, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 155, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 156, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 157, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 158, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 159, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 160, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 161, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 162, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 163, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 164, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 165, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 166, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 167, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 168, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 169, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 170, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 171, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 172, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 173, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 174, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 175, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 176, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 177, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 178, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 179, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 180, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 181, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 182, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 183, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 184, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 185, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 186, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 187, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "stands.data": [{ + "standId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 2, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 3, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 4, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 5, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 6, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 7, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 8, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 9, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 10, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 11, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 12, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 13, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 14, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 15, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 16, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 17, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 18, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 19, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 20, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 21, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "overlays.data": [{ + "overlayId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "overlayId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "overlayId": 2, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 3, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 4, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 5, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 6, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 7, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 8, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "hotelview": { + "room.pool": "791", + "room.picnic": "2193", + "room.rooftop": "", + "room.rooftop.pool": "", + "room.peaceful": "", + "room.infobus": "5956", + "room.lobby": "1450", + "show.avatar": true, + "widgets": { + "slot.1.widget": "", + "slot.1.conf": {}, + "slot.2.widget": "", + "slot.2.conf": { + "image": "", + "texts": "", + "btnLink": "" + }, + "slot.3.widget": "", + "slot.3.conf": {}, + "slot.4.widget": "", + "slot.4.conf": {}, + "slot.5.widget": "", + "slot.5.conf": {}, + "slot.6.widget": "", + "slot.6.conf": { + "campaign": "" + }, + "slot.7.widget": "", + "slot.7.conf": {} + }, + "images": { + "background": "${asset.url}/images/reception/stretch_blue.png", + "background.colour": "#8ee0f0", + "sun": "${asset.url}/images/reception/sun.png", + "drape": "${asset.url}/images/reception/drape.png", + "left": "", + "right": "", + "right.repeat": "" + } + }, + "achievements.unseen.ignored": [ + "ACH_AllTimeHotelPresence" + ], + "avatareditor.show.clubitems.dimmed": true, + "avatareditor.show.clubitems.first": true, + "chat.history.max.items": 100, + "system.currency.types": [ + -1, + 0, + 5 + ], + "catalog.links": { + "hc.buy_hc": "habbo_club", + "hc.hc_gifts": "club_gifts", + "pets.buy_food": "pet_food", + "pets.buy_saddle": "saddles" + }, + "hc.center": { + "benefits.info": true, + "payday.info": true, + "gift.info": true, + "benefits.habbopage": "habboclub", + "payday.habbopage": "hcpayday" + }, + "respect.options": { + "enabled": false, + "sound": "sound_respect_received" + }, + "currency.display.number.short": false, + "currency.asset.icon.url": "${images.url}/wallet/%type%.png", + "catalog.asset.url": "${image.library.url}catalogue", + "catalog.asset.image.url": "${catalog.asset.url}/%name%.gif", + "catalog.asset.icon.url": "${catalog.asset.url}/icon_%name%.png", + "catalog.tab.icons": false, + "catalog.headers": false, + "chat.input.maxlength": 100, + "chat.styles.disabled": [], + "chat.styles": [{ + "styleId": 0, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 1, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 2, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 3, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 4, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 5, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 6, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 7, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 8, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 9, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 10, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 11, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 12, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 13, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 14, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 15, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 16, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 17, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 18, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 19, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 20, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 21, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 22, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 23, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 24, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 25, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 26, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 27, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 28, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 29, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 30, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 31, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 32, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 33, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 34, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 35, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 36, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 37, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 38, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 39, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 40, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 41, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 42, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 43, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 44, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 45, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 46, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 47, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 48, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 49, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 50, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 51, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 52, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 53, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + } + ], + "camera.available.effects": [{ + "name": "dark_sepia", + "colorMatrix": [ + 0.4, + 0.4, + 0.1, + 0, + 110, + 0.3, + 0.4, + 0.1, + 0, + 30, + 0.3, + 0.2, + 0.1, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 0, + "enabled": true + }, { + "name": "increase_saturation", + "colorMatrix": [ + 2, + -0.5, + -0.5, + 0, + 0, + -0.5, + 2, + -0.5, + 0, + 0, + -0.5, + -0.5, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 0, + "enabled": true + }, { + "name": "increase_contrast", + "colorMatrix": [ + 1.5, + 0, + 0, + 0, + -50, + 0, + 1.5, + 0, + 0, + -50, + 0, + 0, + 1.5, + 0, + -50, + 0, + 0, + 0, + 1.5, + 0 + ], + "minLevel": 0, + "enabled": true + }, { + "name": "shadow_multiply_02", + "colorMatrix": [], + "minLevel": 0, + "blendMode": 2, + "enabled": true + }, { + "name": "color_1", + "colorMatrix": [ + 0.393, + 0.769, + 0.189, + 0, + 0, + 0.349, + 0.686, + 0.168, + 0, + 0, + 0.272, + 0.534, + 0.131, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 1, + "enabled": true + }, { + "name": "hue_bright_sat", + "colorMatrix": [ + 1, + 0.6, + 0.2, + 0, + -50, + 0.2, + 1, + 0.6, + 0, + -50, + 0.6, + 0.2, + 1, + 0, + -50, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 1, + "enabled": true + }, { + "name": "hearts_hardlight_02", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 9, + "enabled": true + }, { + "name": "texture_overlay", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 4, + "enabled": true + }, { + "name": "pinky_nrm", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 0, + "enabled": true + }, { + "name": "color_2", + "colorMatrix": [ + 0.333, + 0.333, + 0.333, + 0, + 0, + 0.333, + 0.333, + 0.333, + 0, + 0, + 0.333, + 0.333, + 0.333, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 2, + "enabled": true + }, { + "name": "night_vision", + "colorMatrix": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1.1, + 0, + 0, + -50, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 2, + "enabled": true + }, { + "name": "stars_hardlight_02", + "colorMatrix": [], + "minLevel": 2, + "blendMode": 9, + "enabled": true + }, { + "name": "coffee_mpl", + "colorMatrix": [], + "minLevel": 2, + "blendMode": 2, + "enabled": true + }, { + "name": "security_hardlight", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 9, + "enabled": true + }, { + "name": "bluemood_mpl", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 2, + "enabled": true + }, { + "name": "rusty_mpl", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 2, + "enabled": true + }, { + "name": "decr_conrast", + "colorMatrix": [ + 0.5, + 0, + 0, + 0, + 50, + 0, + 0.5, + 0, + 0, + 50, + 0, + 0, + 0.5, + 0, + 50, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 4, + "enabled": true + }, { + "name": "green_2", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + 90, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 4, + "enabled": true + }, { + "name": "alien_hrd", + "colorMatrix": [], + "minLevel": 4, + "blendMode": 9, + "enabled": true + }, { + "name": "color_3", + "colorMatrix": [ + 0.609, + 0.609, + 0.082, + 0, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 5, + "enabled": true + }, { + "name": "color_4", + "colorMatrix": [ + 0.8, + -0.8, + 1, + 0, + 70, + 0.8, + -0.8, + 1, + 0, + 70, + 0.8, + -0.8, + 1, + 0, + 70, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 5, + "enabled": true + }, { + "name": "toxic_hrd", + "colorMatrix": [], + "minLevel": 5, + "blendMode": 9, + "enabled": true + }, { + "name": "hypersaturated", + "colorMatrix": [ + 2, + -1, + 0, + 0, + 0, + -1, + 2, + 0, + 0, + 0, + 0, + -1, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 6, + "enabled": true + }, { + "name": "Yellow", + "colorMatrix": [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 6, + "enabled": true + }, { + "name": "misty_hrd", + "colorMatrix": [], + "minLevel": 6, + "blendMode": 9, + "enabled": true + }, { + "name": "x_ray", + "colorMatrix": [ + 0, + 1.2, + 0, + 0, + -100, + 0, + 2, + 0, + 0, + -120, + 0, + 2, + 0, + 0, + -120, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 7, + "enabled": true + }, { + "name": "decrease_saturation", + "colorMatrix": [ + 0.7, + 0.2, + 0.2, + 0, + 0, + 0.2, + 0.7, + 0.2, + 0, + 0, + 0.2, + 0.2, + 0.7, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 7, + "enabled": true + }, { + "name": "drops_mpl", + "colorMatrix": [], + "minLevel": 8, + "blendMode": 2, + "enabled": true + }, { + "name": "shiny_hrd", + "colorMatrix": [], + "minLevel": 9, + "blendMode": 9, + "enabled": true + }, { + "name": "glitter_hrd", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 9, + "enabled": true + }, { + "name": "frame_gold", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "frame_gray_4", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "frame_black_2", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "frame_wood_2", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "finger_nrm", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "color_5", + "colorMatrix": [ + 3.309, + 0.609, + 1.082, + 0.2, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 1.309, + 0.609, + 0.082, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "black_white_negative", + "colorMatrix": [ + -0.5, + -0.5, + -0.5, + 0, + 0, + -0.5, + -0.5, + -0.5, + 0, + 0, + -0.5, + -0.5, + -0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "blue", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + -255, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "red", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "green", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + } + ], + "notification": { + "notification.admin.transient": { + "display": "POP_UP", + "image": "${image.library.url}/album1358/frank_wave_001.gif" + }, + "notification.builders_club.membership_expired": { + "display": "POP_UP" + }, + "notification.builders_club.membership_expires": { + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.membership_extended": { + "delivery": "PERSISTENT", + "display": "POP_UP" + }, + "notification.builders_club.membership_made": { + "delivery": "PERSISTENT", + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_membership_extended.png" + }, + "notification.builders_club.membership_renewed": { + "delivery": "PERSISTENT", + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_membership_extended.png" + }, + "notification.builders_club.room_locked": { + "display": "BUBBLE", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.room_unlocked": { + "display": "BUBBLE" + }, + "notification.builders_club.visit_denied_for_owner": { + "display": "BUBBLE", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.visit_denied_for_visitor": { + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_room_locked.png" + }, + "notification.campaign.credit.donation": { + "display": "BUBBLE" + }, + "notification.campaign.product.donation": { + "display": "BUBBLE" + }, + "notification.casino.too_many_dice.placement": { + "display": "POP_UP" + }, + "notification.casino.too_many_dice": { + "display": "POP_UP" + }, + "notification.cfh.created": { + "display": "POP_UP", + "title": "" + }, + "notification.feed.enabled": false, + "notification.floorplan_editor.error": { + "display": "POP_UP" + }, + "notification.forums.delivered": { + "delivery": "PERSISTENT", + "display": "POP_UP" + }, + "notification.forums.forum_settings_updated": { + "display": "BUBBLE" + }, + "notification.forums.message.hidden": { + "display": "BUBBLE" + }, + "notification.forums.message.restored": { + "display": "BUBBLE" + }, + "notification.forums.thread.hidden": { + "display": "BUBBLE" + }, + "notification.forums.thread.locked": { + "display": "BUBBLE" + }, + "notification.forums.thread.pinned": { + "display": "BUBBLE" + }, + "notification.forums.thread.restored": { + "display": "BUBBLE" + }, + "notification.forums.thread.unlocked": { + "display": "BUBBLE" + }, + "notification.forums.thread.unpinned": { + "display": "BUBBLE" + }, + "notification.furni_placement_error": { + "display": "BUBBLE" + }, + "notification.gifting.valentine": { + "delivery": "PERSISTENT", + "display": "BUBBLE", + "image": "${image.library.url}/notifications/polaroid_photo.png" + }, + "notification.items.enabled": true, + "notification.mute.forbidden.time": { + "display": "BUBBLE" + }, + "notification.npc.gift.received": { + "display": "BUBBLE", + "image": "${image.library.url}/album1584/X1517.gif" + } + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..059359d --- /dev/null +++ b/yarn.lock @@ -0,0 +1,3238 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + +"@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + 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== + +"@babel/core@^7.24.4", "@babel/core@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.29.0": + version "7.29.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" + integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== + dependencies: + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== + dependencies: + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" + +"@babel/helper-plugin-utils@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" + integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helpers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7" + integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw== + dependencies: + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.4", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== + dependencies: + "@babel/types" "^7.29.0" + +"@babel/plugin-transform-react-jsx-self@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz#af678d8506acf52c577cac73ff7fe6615c85fc92" + integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-source@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz#dcfe2c24094bb757bf73960374e7c55e434f19f0" + integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/runtime@^7.24.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.26.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b" + integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== + +"@babel/template@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + debug "^4.3.1" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@emnapi/core@^1.7.1", "@emnapi/core@^1.8.1": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.9.0.tgz#4a54213b208fcf288cce25076c74e0f7613e6100" + integrity sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w== + dependencies: + "@emnapi/wasi-threads" "1.2.0" + tslib "^2.4.0" + +"@emnapi/runtime@^1.7.1", "@emnapi/runtime@^1.8.1": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.9.0.tgz#91c54a6e77c36154c125e873409472e2b70efd5b" + integrity sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw== + dependencies: + tslib "^2.4.0" + +"@emnapi/wasi-threads@1.2.0", "@emnapi/wasi-threads@^1.1.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz#a19d9772cc3d195370bf6e2a805eec40aa75e18e" + integrity sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg== + dependencies: + tslib "^2.4.0" + +"@emoji-mart/data@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.2.1.tgz#0ad70c662e3bc603e23e7d98413bd1e64c4fcb6c" + integrity sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw== + +"@emoji-mart/react@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@emoji-mart/react/-/react-1.1.1.tgz#ddad52f93a25baf31c5383c3e7e4c6e05554312a" + integrity sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g== + +"@esbuild/aix-ppc64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz#4c585002f7ad694d38fe0e8cbf5cfd939ccff327" + integrity sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q== + +"@esbuild/android-arm64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz#7625d0952c3b402d3ede203a16c9f2b78f8a4827" + integrity sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw== + +"@esbuild/android-arm@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.4.tgz#9a0cf1d12997ec46dddfb32ce67e9bca842381ac" + integrity sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ== + +"@esbuild/android-x64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.4.tgz#06e1fdc6283fccd6bc6aadd6754afce6cf96f42e" + integrity sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw== + +"@esbuild/darwin-arm64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz#6c550ee6c0273bcb0fac244478ff727c26755d80" + integrity sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ== + +"@esbuild/darwin-x64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz#ed7a125e9f25ce0091b9aff783ee943f6ba6cb86" + integrity sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw== + +"@esbuild/freebsd-arm64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz#597dc8e7161dba71db4c1656131c1f1e9d7660c6" + integrity sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw== + +"@esbuild/freebsd-x64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz#ea171f9f4f00efaa8e9d3fe8baa1b75d757d1b36" + integrity sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ== + +"@esbuild/linux-arm64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz#e52d57f202369386e6dbcb3370a17a0491ab1464" + integrity sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA== + +"@esbuild/linux-arm@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz#5e0c0b634908adbce0a02cebeba8b3acac263fb6" + integrity sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg== + +"@esbuild/linux-ia32@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz#5f90f01f131652473ec06b038a14c49683e14ec7" + integrity sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA== + +"@esbuild/linux-loong64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz#63bacffdb99574c9318f9afbd0dd4fff76a837e3" + integrity sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA== + +"@esbuild/linux-mips64el@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz#c4b6952eca6a8efff67fee3671a3536c8e67b7eb" + integrity sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw== + +"@esbuild/linux-ppc64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz#6dea67d3d98c6986f1b7769e4f1848e5ae47ad58" + integrity sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA== + +"@esbuild/linux-riscv64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz#9ad2b4c3c0502c6bada9c81997bb56c597853489" + integrity sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw== + +"@esbuild/linux-s390x@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz#c43d3cfd073042ca6f5c52bb9bc313ed2066ce28" + integrity sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA== + +"@esbuild/linux-x64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz#45fa173e0591ac74d80d3cf76704713e14e2a4a6" + integrity sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA== + +"@esbuild/netbsd-arm64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz#366b0ef40cdb986fc751cbdad16e8c25fe1ba879" + integrity sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q== + +"@esbuild/netbsd-x64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz#e985d49a3668fd2044343071d52e1ae815112b3e" + integrity sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg== + +"@esbuild/openbsd-arm64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz#6fb4ab7b73f7e5572ce5ec9cf91c13ff6dd44842" + integrity sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow== + +"@esbuild/openbsd-x64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz#641f052040a0d79843d68898f5791638a026d983" + integrity sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ== + +"@esbuild/openharmony-arm64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz#fc1d33eac9d81ae0a433b3ed1dd6171a20d4e317" + integrity sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg== + +"@esbuild/sunos-x64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz#af2cd5ca842d6d057121f66a192d4f797de28f53" + integrity sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g== + +"@esbuild/win32-arm64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz#78ec7e59bb06404583d4c9511e621db31c760de3" + integrity sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg== + +"@esbuild/win32-ia32@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz#0e616aa488b7ee5d2592ab070ff9ec06a9fddf11" + integrity sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw== + +"@esbuild/win32-x64@0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz#1f7ba71a3d6155d44a6faa8dbe249c62ab3e408c" + integrity sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg== + +"@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.12.2": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== + +"@eslint/config-array@^0.23.3": + version "0.23.3" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.3.tgz#3f4a93dd546169c09130cbd10f2415b13a20a219" + integrity sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw== + dependencies: + "@eslint/object-schema" "^3.0.3" + debug "^4.3.1" + minimatch "^10.2.4" + +"@eslint/config-helpers@^0.5.2": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.5.3.tgz#721fe6bbb90d74b0c80d6ff2428e5bbcb002becb" + integrity sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw== + dependencies: + "@eslint/core" "^1.1.1" + +"@eslint/core@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-1.1.1.tgz#450f3d2be2d463ccd51119544092256b4e88df32" + integrity sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/object-schema@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.3.tgz#5bf671e52e382e4adc47a9906f2699374637db6b" + integrity sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ== + +"@eslint/plugin-kit@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz#eb9e6689b56ce8bc1855bb33090e63f3fc115e8e" + integrity sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ== + dependencies: + "@eslint/core" "^1.1.1" + levn "^0.4.1" + +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.7" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26" + integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.4.0" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@napi-rs/wasm-runtime@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz#c3705ab549d176b8dc5172723d6156c3dc426af2" + integrity sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A== + dependencies: + "@emnapi/core" "^1.7.1" + "@emnapi/runtime" "^1.7.1" + "@tybys/wasm-util" "^0.10.1" + +"@parcel/watcher-android-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz#5f32e0dba356f4ac9a11068d2a5c134ca3ba6564" + integrity sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A== + +"@parcel/watcher-darwin-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz#88d3e720b59b1eceffce98dac46d7c40e8be5e8e" + integrity sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA== + +"@parcel/watcher-darwin-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz#bf05d76a78bc15974f15ec3671848698b0838063" + integrity sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg== + +"@parcel/watcher-freebsd-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz#8bc26e9848e7303ac82922a5ae1b1ef1bdb48a53" + integrity sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng== + +"@parcel/watcher-linux-arm-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz#1328fee1deb0c2d7865079ef53a2ba4cc2f8b40a" + integrity sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ== + +"@parcel/watcher-linux-arm-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz#bad0f45cb3e2157746db8b9d22db6a125711f152" + integrity sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg== + +"@parcel/watcher-linux-arm64-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz#b75913fbd501d9523c5f35d420957bf7d0204809" + integrity sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA== + +"@parcel/watcher-linux-arm64-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz#da5621a6a576070c8c0de60dea8b46dc9c3827d4" + integrity sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA== + +"@parcel/watcher-linux-x64-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz#ce437accdc4b30f93a090b4a221fd95cd9b89639" + integrity sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ== + +"@parcel/watcher-linux-x64-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz#02400c54b4a67efcc7e2327b249711920ac969e2" + integrity sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg== + +"@parcel/watcher-win32-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz#caae3d3c7583ca0a7171e6bd142c34d20ea1691e" + integrity sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q== + +"@parcel/watcher-win32-ia32@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz#9ac922550896dfe47bfc5ae3be4f1bcaf8155d6d" + integrity sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g== + +"@parcel/watcher-win32-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz#73fdafba2e21c448f0e456bbe13178d8fe11739d" + integrity sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw== + +"@parcel/watcher@^2.4.1": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.6.tgz#3f932828c894f06d0ad9cfefade1756ecc6ef1f1" + integrity sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ== + dependencies: + detect-libc "^2.0.3" + is-glob "^4.0.3" + node-addon-api "^7.0.0" + picomatch "^4.0.3" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.6" + "@parcel/watcher-darwin-arm64" "2.5.6" + "@parcel/watcher-darwin-x64" "2.5.6" + "@parcel/watcher-freebsd-x64" "2.5.6" + "@parcel/watcher-linux-arm-glibc" "2.5.6" + "@parcel/watcher-linux-arm-musl" "2.5.6" + "@parcel/watcher-linux-arm64-glibc" "2.5.6" + "@parcel/watcher-linux-arm64-musl" "2.5.6" + "@parcel/watcher-linux-x64-glibc" "2.5.6" + "@parcel/watcher-linux-x64-musl" "2.5.6" + "@parcel/watcher-win32-arm64" "2.5.6" + "@parcel/watcher-win32-ia32" "2.5.6" + "@parcel/watcher-win32-x64" "2.5.6" + +"@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@react-aria/ssr@^3.5.0": + version "3.9.10" + resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.10.tgz#7fdc09e811944ce0df1d7e713de1449abd7435e6" + integrity sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ== + dependencies: + "@swc/helpers" "^0.5.0" + +"@restart/hooks@^0.4.9": + version "0.4.16" + resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.16.tgz#95ae8ac1cc7e2bd4fed5e39800ff85604c6d59fb" + integrity sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w== + dependencies: + dequal "^2.0.3" + +"@restart/hooks@^0.5.0": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.5.1.tgz#6776b3859e33aea72b23b81fc47021edf17fd247" + integrity sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q== + dependencies: + dequal "^2.0.3" + +"@restart/ui@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@restart/ui/-/ui-1.9.4.tgz#9d61f56f2647f5ab8a33d87b278b9ce183511a26" + integrity sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA== + dependencies: + "@babel/runtime" "^7.26.0" + "@popperjs/core" "^2.11.8" + "@react-aria/ssr" "^3.5.0" + "@restart/hooks" "^0.5.0" + "@types/warning" "^3.0.3" + dequal "^2.0.3" + dom-helpers "^5.2.0" + uncontrollable "^8.0.4" + warning "^4.0.3" + +"@rolldown/pluginutils@1.0.0-rc.3": + version "1.0.0-rc.3" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz#8a88cc92a0f741befc7bc109cb1a4c6b9408e1c5" + integrity sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q== + +"@rollup/rollup-android-arm-eabi@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz#a6742c74c7d9d6d604ef8a48f99326b4ecda3d82" + integrity sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg== + +"@rollup/rollup-android-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz#97247be098de4df0c11971089fd2edf80a5da8cf" + integrity sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q== + +"@rollup/rollup-darwin-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz#674852cf14cf11b8056e0b1a2f4e872b523576cf" + integrity sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg== + +"@rollup/rollup-darwin-x64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz#36dfd7ed0aaf4d9d89d9ef983af72632455b0246" + integrity sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w== + +"@rollup/rollup-freebsd-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz#2f87c2074b4220260fdb52a9996246edfc633c22" + integrity sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA== + +"@rollup/rollup-freebsd-x64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz#9b5a26522a38a95dc06616d1939d4d9a76937803" + integrity sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg== + +"@rollup/rollup-linux-arm-gnueabihf@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz#86aa4859385a8734235b5e40a48e52d770758c3a" + integrity sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw== + +"@rollup/rollup-linux-arm-musleabihf@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz#cbe70e56e6ece8dac83eb773b624fc9e5a460976" + integrity sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA== + +"@rollup/rollup-linux-arm64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz#d14992a2e653bc3263d284bc6579b7a2890e1c45" + integrity sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA== + +"@rollup/rollup-linux-arm64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz#2fdd1ddc434ea90aeaa0851d2044789b4d07f6da" + integrity sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA== + +"@rollup/rollup-linux-loong64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz#8a181e6f89f969f21666a743cd411416c80099e7" + integrity sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg== + +"@rollup/rollup-linux-loong64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz#904125af2babc395f8061daa27b5af1f4e3f2f78" + integrity sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q== + +"@rollup/rollup-linux-ppc64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz#a57970ac6864c9a3447411a658224bdcf948be22" + integrity sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA== + +"@rollup/rollup-linux-ppc64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz#bb84de5b26870567a4267666e08891e80bb56a63" + integrity sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA== + +"@rollup/rollup-linux-riscv64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz#72d00d2c7fb375ce3564e759db33f17a35bffab9" + integrity sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg== + +"@rollup/rollup-linux-riscv64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz#4c166ef58e718f9245bd31873384ba15a5c1a883" + integrity sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg== + +"@rollup/rollup-linux-s390x-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz#bb5025cde9a61db478c2ca7215808ad3bce73a09" + integrity sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w== + +"@rollup/rollup-linux-x64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz#9b66b1f9cd95c6624c788f021c756269ffed1552" + integrity sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg== + +"@rollup/rollup-linux-x64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz#b007ca255dc7166017d57d7d2451963f0bd23fd9" + integrity sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg== + +"@rollup/rollup-openbsd-x64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz#e8b357b2d1aa2c8d76a98f5f0d889eabe93f4ef9" + integrity sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ== + +"@rollup/rollup-openharmony-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz#96c2e3f4aacd3d921981329831ff8dde492204dc" + integrity sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA== + +"@rollup/rollup-win32-arm64-msvc@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz#2d865149d706d938df8b4b8f117e69a77646d581" + integrity sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A== + +"@rollup/rollup-win32-ia32-msvc@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz#abe1593be0fa92325e9971c8da429c5e05b92c36" + integrity sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA== + +"@rollup/rollup-win32-x64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz#c4af3e9518c9a5cd4b1c163dc81d0ad4d82e7eab" + integrity sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA== + +"@rollup/rollup-win32-x64-msvc@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz#4584a8a87b29188a4c1fe987a9fcf701e256d86c" + integrity sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA== + +"@swc/helpers@^0.5.0": + version "0.5.19" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.19.tgz#9a8c8a0bdaecfdfb9b8ae5421c0c8e09246dfee9" + integrity sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA== + dependencies: + tslib "^2.8.0" + +"@tailwindcss/forms@^0.5.11": + version "0.5.11" + resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.11.tgz#e77039e96fa7b87c3d001a991f77f9418e666700" + integrity sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA== + dependencies: + mini-svg-data-uri "^1.2.3" + +"@tailwindcss/node@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.2.1.tgz#e963ac242a885353a4660e7e3e9c695cde7d3fc9" + integrity sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg== + dependencies: + "@jridgewell/remapping" "^2.3.5" + enhanced-resolve "^5.19.0" + jiti "^2.6.1" + lightningcss "1.31.1" + magic-string "^0.30.21" + source-map-js "^1.2.1" + tailwindcss "4.2.1" + +"@tailwindcss/oxide-android-arm64@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz#a7c24919b607e7f884e6ab97799d12c7fb5b47bd" + integrity sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg== + +"@tailwindcss/oxide-darwin-arm64@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz#6f6e91ff0e1b5476cc0dad0da1ea8474f4563212" + integrity sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw== + +"@tailwindcss/oxide-darwin-x64@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz#1e59ef0665f6cb9e658bf0ebcb3cb50f21b2c175" + integrity sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw== + +"@tailwindcss/oxide-freebsd-x64@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz#6b0c75e9dac7f1a241cb9a5eaa89f0d9664835b6" + integrity sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA== + +"@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz#717044d8fe746b1f0760485946c0c9a900174f7b" + integrity sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw== + +"@tailwindcss/oxide-linux-arm64-gnu@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz#f544b0faf166d80791347911b2dd4372a893129d" + integrity sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ== + +"@tailwindcss/oxide-linux-arm64-musl@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz#9fbaf8dc00b858a2b955526abb15d88f5678d1ef" + integrity sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ== + +"@tailwindcss/oxide-linux-x64-gnu@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz#6ab4e6b8d308d037a1155b8443df5941dbfa6aa1" + integrity sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g== + +"@tailwindcss/oxide-linux-x64-musl@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz#52c55593394dff85f1fa88172f69f8fdcde182b6" + integrity sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g== + +"@tailwindcss/oxide-wasm32-wasi@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz#7401e35f881d3654b6180badd1243d75a2702ea5" + integrity sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q== + dependencies: + "@emnapi/core" "^1.8.1" + "@emnapi/runtime" "^1.8.1" + "@emnapi/wasi-threads" "^1.1.0" + "@napi-rs/wasm-runtime" "^1.1.1" + "@tybys/wasm-util" "^0.10.1" + tslib "^2.8.1" + +"@tailwindcss/oxide-win32-arm64-msvc@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz#63a502e7b696dcd976aa356b94ce0f4f8f832c44" + integrity sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA== + +"@tailwindcss/oxide-win32-x64-msvc@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz#8cc59b28ebc4dc866c0c14d7057f07f0ed04c4a8" + integrity sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ== + +"@tailwindcss/oxide@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.2.1.tgz#43ae4217268a7e8b4736f1c056d0ef6f393c79d3" + integrity sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw== + optionalDependencies: + "@tailwindcss/oxide-android-arm64" "4.2.1" + "@tailwindcss/oxide-darwin-arm64" "4.2.1" + "@tailwindcss/oxide-darwin-x64" "4.2.1" + "@tailwindcss/oxide-freebsd-x64" "4.2.1" + "@tailwindcss/oxide-linux-arm-gnueabihf" "4.2.1" + "@tailwindcss/oxide-linux-arm64-gnu" "4.2.1" + "@tailwindcss/oxide-linux-arm64-musl" "4.2.1" + "@tailwindcss/oxide-linux-x64-gnu" "4.2.1" + "@tailwindcss/oxide-linux-x64-musl" "4.2.1" + "@tailwindcss/oxide-wasm32-wasi" "4.2.1" + "@tailwindcss/oxide-win32-arm64-msvc" "4.2.1" + "@tailwindcss/oxide-win32-x64-msvc" "4.2.1" + +"@tailwindcss/postcss@^4.2.0": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/postcss/-/postcss-4.2.1.tgz#efce3b23608b23324ed4848ff1aae657adfe0c5f" + integrity sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw== + dependencies: + "@alloc/quick-lru" "^5.2.0" + "@tailwindcss/node" "4.2.1" + "@tailwindcss/oxide" "4.2.1" + postcss "^8.5.6" + tailwindcss "4.2.1" + +"@tanstack/react-virtual@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.2.0.tgz#fb70f9c6baee753a5a0f7618ac886205d5a02af9" + integrity sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg== + dependencies: + "@tanstack/virtual-core" "3.2.0" + +"@tanstack/virtual-core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.2.0.tgz#874d36135e4badce2719e7bdc556ce240cbaff14" + integrity sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ== + +"@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== + dependencies: + tslib "^2.4.0" + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== + dependencies: + "@babel/types" "^7.28.2" + +"@types/esrecurse@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec" + integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw== + +"@types/estree@1.0.8", "@types/estree@^1.0.6", "@types/estree@^1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/node@^25.3.0": + version "25.5.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.5.0.tgz#5c99f37c443d9ccc4985866913f1ed364217da31" + integrity sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw== + dependencies: + undici-types "~7.18.0" + +"@types/prop-types@^15.7.12": + version "15.7.15" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" + integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== + +"@types/react-dom@^19.2.3": + version "19.2.3" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" + integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== + +"@types/react-slider@^1.3.6": + version "1.3.6" + resolved "https://registry.yarnpkg.com/@types/react-slider/-/react-slider-1.3.6.tgz#6f5602be93ab1cb3d273428c87aa227ad2ff68ff" + integrity sha512-RS8XN5O159YQ6tu3tGZIQz1/9StMLTg/FCIPxwqh2gwVixJnlfIodtVx+fpXVMZHe7A58lAX1Q4XTgAGOQaCQg== + dependencies: + "@types/react" "*" + +"@types/react-transition-group@^4.4.10", "@types/react-transition-group@^4.4.6": + version "4.4.12" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" + integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== + +"@types/react@*", "@types/react@>=16.9.11", "@types/react@^19.2.14": + version "19.2.14" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad" + integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== + dependencies: + csstype "^3.2.2" + +"@types/trusted-types@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + +"@types/warning@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.3.tgz#d1884c8cc4a426d1ac117ca2611bf333834c6798" + integrity sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q== + +"@typescript-eslint/eslint-plugin@8.57.0", "@typescript-eslint/eslint-plugin@^8.56.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz#6e4085604ab63f55b3dcc61ce2c16965b2c36374" + integrity sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ== + dependencies: + "@eslint-community/regexpp" "^4.12.2" + "@typescript-eslint/scope-manager" "8.57.0" + "@typescript-eslint/type-utils" "8.57.0" + "@typescript-eslint/utils" "8.57.0" + "@typescript-eslint/visitor-keys" "8.57.0" + ignore "^7.0.5" + natural-compare "^1.4.0" + ts-api-utils "^2.4.0" + +"@typescript-eslint/parser@8.57.0", "@typescript-eslint/parser@^8.56.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.57.0.tgz#444c57a943e8b04f255cda18a94c8e023b46b08c" + integrity sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g== + dependencies: + "@typescript-eslint/scope-manager" "8.57.0" + "@typescript-eslint/types" "8.57.0" + "@typescript-eslint/typescript-estree" "8.57.0" + "@typescript-eslint/visitor-keys" "8.57.0" + debug "^4.4.3" + +"@typescript-eslint/project-service@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.57.0.tgz#2014ed527bcd0eff8aecb7e44879ae3150604ab3" + integrity sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.57.0" + "@typescript-eslint/types" "^8.57.0" + debug "^4.4.3" + +"@typescript-eslint/scope-manager@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz#7d2a2aeaaef2ae70891b21939fadb4cb0b19f840" + integrity sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw== + dependencies: + "@typescript-eslint/types" "8.57.0" + "@typescript-eslint/visitor-keys" "8.57.0" + +"@typescript-eslint/tsconfig-utils@8.57.0", "@typescript-eslint/tsconfig-utils@^8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz#cf2f2822af3887d25dd325b6bea6c3f60a83a0b4" + integrity sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA== + +"@typescript-eslint/type-utils@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz#2877af4c2e8f0998b93a07dad1c34ce1bb669448" + integrity sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ== + dependencies: + "@typescript-eslint/types" "8.57.0" + "@typescript-eslint/typescript-estree" "8.57.0" + "@typescript-eslint/utils" "8.57.0" + debug "^4.4.3" + ts-api-utils "^2.4.0" + +"@typescript-eslint/types@8.57.0", "@typescript-eslint/types@^8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.57.0.tgz#4fa5385ffd1cd161fa5b9dce93e0493d491b8dc6" + integrity sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg== + +"@typescript-eslint/typescript-estree@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz#e0e4a89bfebb207de314826df876e2dabc7dea04" + integrity sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q== + dependencies: + "@typescript-eslint/project-service" "8.57.0" + "@typescript-eslint/tsconfig-utils" "8.57.0" + "@typescript-eslint/types" "8.57.0" + "@typescript-eslint/visitor-keys" "8.57.0" + debug "^4.4.3" + minimatch "^10.2.2" + semver "^7.7.3" + tinyglobby "^0.2.15" + ts-api-utils "^2.4.0" + +"@typescript-eslint/utils@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.57.0.tgz#c7193385b44529b788210d20c94c11de79ad3498" + integrity sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ== + dependencies: + "@eslint-community/eslint-utils" "^4.9.1" + "@typescript-eslint/scope-manager" "8.57.0" + "@typescript-eslint/types" "8.57.0" + "@typescript-eslint/typescript-estree" "8.57.0" + +"@typescript-eslint/visitor-keys@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz#23aea662279bb66209700854453807a119350f85" + integrity sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg== + dependencies: + "@typescript-eslint/types" "8.57.0" + eslint-visitor-keys "^5.0.0" + +"@vitejs/plugin-react@^5.1.4": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz#108bd0f566f288ce3566982df4eff137ded7b15f" + integrity sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw== + dependencies: + "@babel/core" "^7.29.0" + "@babel/plugin-transform-react-jsx-self" "^7.27.1" + "@babel/plugin-transform-react-jsx-source" "^7.27.1" + "@rolldown/pluginutils" "1.0.0-rc.3" + "@types/babel__core" "^7.20.5" + react-refresh "^0.18.0" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.16.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + +ajv@^6.14.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a" + integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== + dependencies: + call-bound "^1.0.3" + is-array-buffer "^3.0.5" + +array-includes@^3.1.6, array-includes@^3.1.8: + version "3.1.9" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a" + integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-abstract "^1.24.0" + es-object-atoms "^1.1.1" + get-intrinsic "^1.3.0" + is-string "^1.1.1" + math-intrinsics "^1.1.0" + +array.prototype.findlast@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.flat@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" + integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" + +array.prototype.flatmap@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" + +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + is-array-buffer "^3.0.4" + +async-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" + integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== + +autoprefixer@^10.4.24: + version "10.4.27" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.27.tgz#51ea301a5c3c5f8642f8e564759c4f573be486f2" + integrity sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA== + dependencies: + browserslist "^4.28.1" + caniuse-lite "^1.0.30001774" + fraction.js "^5.3.4" + picocolors "^1.1.1" + postcss-value-parser "^4.2.0" + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + +baseline-browser-mapping@^2.9.0: + version "2.10.8" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz#23d1cea1a85b181c2b8660b6cfe626dc2fb15630" + integrity sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ== + +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^5.0.2: + version "5.0.4" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336" + integrity sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg== + dependencies: + balanced-match "^4.0.2" + +browserslist@^4.24.0, browserslist@^4.28.1: + version "4.28.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" + integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== + dependencies: + baseline-browser-mapping "^2.9.0" + caniuse-lite "^1.0.30001759" + electron-to-chromium "^1.5.263" + node-releases "^2.0.27" + update-browserslist-db "^1.2.0" + +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001774: + version "1.0.30001778" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz#79a8a124e3473a20b70616497b987c30d06570a0" + integrity sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg== + +chokidar@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +classnames@^2.3.2: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +csstype@^3.0.2, csstype@^3.2.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + +data-view-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-offset@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@^2.6.6: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +detect-libc@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +dompurify@^3.1.5: + version "3.3.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.3.tgz#680cae8af3e61320ddf3666a3bc843f7b291b2b6" + integrity sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA== + optionalDependencies: + "@types/trusted-types" "^2.0.7" + +dunder-proto@^1.0.0, dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +electron-to-chromium@^1.5.263: + version "1.5.313" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz#193e9ae2c2ab6915acb41e833068381e4ef0b3e4" + integrity sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA== + +emoji-mart@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023" + integrity sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow== + +emoji-toolkit@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/emoji-toolkit/-/emoji-toolkit-10.0.0.tgz#4a4dc29c86c30cea9bb1e5ef1d45bb36f6858397" + integrity sha512-GkIAvgutEVbkqcT2HjBzV002SWvpdNaT3aP9q/YjQ6hlgDq8HhE9GcqxWkyYkRRQnLADGpwDoj1heTw9KzO9wQ== + +enhanced-resolve@^5.19.0: + version "5.20.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz#323c2a70d2aa7fb4bdfd6d3c24dfc705c581295d" + integrity sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.3.0" + +es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9, es-abstract@^1.24.0, es-abstract@^1.24.1: + version "1.24.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.1.tgz#f0c131ed5ea1bb2411134a8dd94def09c46c7899" + integrity sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw== + dependencies: + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.3.0" + get-proto "^1.0.1" + get-symbol-description "^1.1.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" + is-callable "^1.2.7" + is-data-view "^1.0.2" + is-negative-zero "^2.0.3" + is-regex "^1.2.1" + is-set "^2.0.3" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.1" + math-intrinsics "^1.1.0" + object-inspect "^1.13.4" + object-keys "^1.1.1" + object.assign "^4.1.7" + own-keys "^1.0.1" + regexp.prototype.flags "^1.5.4" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + stop-iteration-iterator "^1.1.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.19" + +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-iterator-helpers@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz#3be0f4e63438d6c5a1fb5f33b891aaad3f7dae06" + integrity sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-abstract "^1.24.1" + es-errors "^1.3.0" + es-set-tostringtag "^2.1.0" + function-bind "^1.1.2" + get-intrinsic "^1.3.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + iterator.prototype "^1.1.5" + math-intrinsics "^1.1.0" + safe-array-concat "^1.1.3" + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +es-shim-unscopables@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" + integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== + dependencies: + hasown "^2.0.2" + +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== + dependencies: + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" + +esbuild@^0.27.0: + version "0.27.4" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.4.tgz#b9591dd7e0ab803a11c9c3b602850403bef22f00" + integrity sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ== + optionalDependencies: + "@esbuild/aix-ppc64" "0.27.4" + "@esbuild/android-arm" "0.27.4" + "@esbuild/android-arm64" "0.27.4" + "@esbuild/android-x64" "0.27.4" + "@esbuild/darwin-arm64" "0.27.4" + "@esbuild/darwin-x64" "0.27.4" + "@esbuild/freebsd-arm64" "0.27.4" + "@esbuild/freebsd-x64" "0.27.4" + "@esbuild/linux-arm" "0.27.4" + "@esbuild/linux-arm64" "0.27.4" + "@esbuild/linux-ia32" "0.27.4" + "@esbuild/linux-loong64" "0.27.4" + "@esbuild/linux-mips64el" "0.27.4" + "@esbuild/linux-ppc64" "0.27.4" + "@esbuild/linux-riscv64" "0.27.4" + "@esbuild/linux-s390x" "0.27.4" + "@esbuild/linux-x64" "0.27.4" + "@esbuild/netbsd-arm64" "0.27.4" + "@esbuild/netbsd-x64" "0.27.4" + "@esbuild/openbsd-arm64" "0.27.4" + "@esbuild/openbsd-x64" "0.27.4" + "@esbuild/openharmony-arm64" "0.27.4" + "@esbuild/sunos-x64" "0.27.4" + "@esbuild/win32-arm64" "0.27.4" + "@esbuild/win32-ia32" "0.27.4" + "@esbuild/win32-x64" "0.27.4" + +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-plugin-react-hooks@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169" + integrity sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA== + dependencies: + "@babel/core" "^7.24.4" + "@babel/parser" "^7.24.4" + hermes-parser "^0.25.1" + zod "^3.25.0 || ^4.0.0" + zod-validation-error "^3.5.0 || ^4.0.0" + +eslint-plugin-react@^7.37.5: + version "7.37.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" + integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== + dependencies: + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" + array.prototype.flatmap "^1.3.3" + array.prototype.tosorted "^1.1.4" + doctrine "^2.1.0" + es-iterator-helpers "^1.2.1" + estraverse "^5.3.0" + hasown "^2.0.2" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.9" + object.fromentries "^2.0.8" + object.values "^1.2.1" + prop-types "^15.8.1" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.12" + string.prototype.repeat "^1.0.0" + +eslint-scope@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.2.tgz#b9de6ace2fab1cff24d2e58d85b74c8fcea39802" + integrity sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ== + dependencies: + "@types/esrecurse" "^4.3.1" + "@types/estree" "^1.0.8" + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be" + integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== + +eslint@^10.0.0: + version "10.0.3" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.0.3.tgz#360a7de7f2706eb8a32caa17ca983f0089efe694" + integrity sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ== + dependencies: + "@eslint-community/eslint-utils" "^4.8.0" + "@eslint-community/regexpp" "^4.12.2" + "@eslint/config-array" "^0.23.3" + "@eslint/config-helpers" "^0.5.2" + "@eslint/core" "^1.1.1" + "@eslint/plugin-kit" "^0.6.1" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + ajv "^6.14.0" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^9.1.2" + eslint-visitor-keys "^5.0.1" + espree "^11.1.1" + esquery "^1.7.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + minimatch "^10.2.4" + natural-compare "^1.4.0" + optionator "^0.9.3" + +espree@^11.1.1: + version "11.2.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-11.2.0.tgz#01d5e47dc332aaba3059008362454a8cc34ccaa5" + integrity sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw== + dependencies: + acorn "^8.16.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^5.0.1" + +esquery@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" + integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@3.1.3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + +flatted@^3.2.9: + version "3.4.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.1.tgz#84ccd9579e76e9cc0d246c11d8be0beb019143e6" + integrity sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ== + +for-each@^0.3.3, for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + +fraction.js@^5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a" + integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ== + +framer-motion@^11.2.12: + version "11.18.2" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.18.2.tgz#0c6bd05677f4cfd3b3bdead4eb5ecdd5ed245718" + integrity sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w== + dependencies: + motion-dom "^11.18.1" + motion-utils "^11.18.1" + tslib "^2.4.0" + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +generator-function@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/generator-function/-/generator-function-2.0.1.tgz#0e75dd410d1243687a0ba2e951b94eedb8f737a2" + integrity sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.0, get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +get-symbol-description@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-bigints@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hermes-estree@0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480" + integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw== + +hermes-parser@^0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1" + integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA== + dependencies: + hermes-estree "0.25.1" + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +ignore@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + +immutable@^5.1.5: + version "5.1.5" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.5.tgz#93ee4db5c2a9ab42a4a783069f3c5d8847d40165" + integrity sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + +is-async-function@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" + integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== + dependencies: + async-function "^1.0.0" + call-bound "^1.0.3" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== + dependencies: + has-bigints "^1.0.2" + +is-boolean-object@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" + integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + 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== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1, is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== + dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + is-typed-array "^1.1.13" + +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== + dependencies: + call-bound "^1.0.3" + +is-generator-function@^1.0.10: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.2.tgz#ae3b61e3d5ea4e4839b90bad22b02335051a17d5" + integrity sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== + dependencies: + call-bound "^1.0.4" + generator-function "^2.0.0" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== + dependencies: + call-bound "^1.0.3" + +is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== + dependencies: + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" + +is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2, is-weakref@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" + integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== + dependencies: + call-bound "^1.0.3" + +is-weakset@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== + dependencies: + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +iterator.prototype@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39" + integrity sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g== + dependencies: + define-data-property "^1.1.4" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + get-proto "^1.0.0" + has-symbols "^1.1.0" + 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== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +"jsx-ast-utils@^2.4.1 || ^3.0.0": + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lightningcss-android-arm64@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz#609ff48332adff452a8157a7c2842fd692a8eac4" + integrity sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg== + +lightningcss-darwin-arm64@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz#a13da040a7929582bab3ace9a67bdc146e99fc2d" + integrity sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg== + +lightningcss-darwin-x64@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz#f7482c311273571ec0c2bd8277c1f5f6e90e03a4" + integrity sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA== + +lightningcss-freebsd-x64@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz#91df1bb290f1cb7bb2af832d7d0d8809225e0124" + integrity sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A== + +lightningcss-linux-arm-gnueabihf@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz#c3cad5ae8b70045f21600dc95295ab6166acf57e" + integrity sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g== + +lightningcss-linux-arm64-gnu@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz#a5c4f6a5ac77447093f61b209c0bd7fef1f0a3e3" + integrity sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg== + +lightningcss-linux-arm64-musl@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz#af26ab8f829b727ada0a200938a6c8796ff36900" + integrity sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg== + +lightningcss-linux-x64-gnu@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz#a891d44e84b71c0d88959feb9a7522bbf61450ee" + integrity sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA== + +lightningcss-linux-x64-musl@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz#8c8b21def851f4d477fa897b80cb3db2b650bc6e" + integrity sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA== + +lightningcss-win32-arm64-msvc@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz#79000fb8c57e94a91b8fc643e74d5a54407d7080" + integrity sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w== + +lightningcss-win32-x64-msvc@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz#7f025274c81c7d659829731e09c8b6f442209837" + integrity sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw== + +lightningcss@1.31.1: + version "1.31.1" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.31.1.tgz#1a19dd327b547a7eda1d5c296ebe1e72df5a184b" + integrity sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-android-arm64 "1.31.1" + lightningcss-darwin-arm64 "1.31.1" + lightningcss-darwin-x64 "1.31.1" + lightningcss-freebsd-x64 "1.31.1" + lightningcss-linux-arm-gnueabihf "1.31.1" + lightningcss-linux-arm64-gnu "1.31.1" + lightningcss-linux-arm64-musl "1.31.1" + lightningcss-linux-x64-gnu "1.31.1" + lightningcss-linux-x64-musl "1.31.1" + lightningcss-win32-arm64-msvc "1.31.1" + lightningcss-win32-x64-msvc "1.31.1" + +load-script@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" + integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +loose-envify@^1.0.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +magic-string@^0.30.21: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +mini-svg-data-uri@^1.2.3: + version "1.4.4" + resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939" + integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== + +minimatch@^10.2.2, minimatch@^10.2.4: + version "10.2.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde" + integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg== + dependencies: + brace-expansion "^5.0.2" + +minimatch@^3.1.2: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + +motion-dom@^11.18.1: + version "11.18.1" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-11.18.1.tgz#e7fed7b7dc6ae1223ef1cce29ee54bec826dc3f2" + integrity sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw== + dependencies: + motion-utils "^11.18.1" + +motion-utils@^11.18.1: + version "11.18.1" + resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-11.18.1.tgz#671227669833e991c55813cf337899f41327db5b" + integrity sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + 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== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + +node-exports-info@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/node-exports-info/-/node-exports-info-1.6.0.tgz#1aedafb01a966059c9a5e791a94a94d93f5c2a13" + integrity sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw== + dependencies: + array.prototype.flatmap "^1.3.3" + es-errors "^1.3.0" + object.entries "^1.1.9" + semver "^6.3.1" + +node-releases@^2.0.27: + version "2.0.36" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.36.tgz#99fd6552aaeda9e17c4713b57a63964a2e325e9d" + integrity sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3, object-inspect@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4, object.assign@^4.1.7: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + +object.entries@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3" + integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-object-atoms "^1.1.1" + +object.fromentries@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.values@^1.1.6, object.values@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +own-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== + dependencies: + get-intrinsic "^1.2.6" + object-keys "^1.1.1" + safe-push-apply "^1.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + +possible-typed-array-names@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== + +postcss-nested@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-7.0.2.tgz#863d83a6b5df0a2894560394be93d5383ea37a65" + integrity sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-selector-parser@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz#e75d2e0d843f620e5df69076166f4e16f891cb9f" + integrity sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.5.6: + version "8.5.8" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.8.tgz#6230ecc8fb02e7a0f6982e53990937857e13f399" + integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prop-types-extra@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b" + integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew== + dependencies: + react-is "^16.3.2" + warning "^4.0.0" + +prop-types@15.8.1, prop-types@^15.6.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +react-bootstrap@^2.10.10: + version "2.10.10" + resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-2.10.10.tgz#be0b0d951a69987152d75c0e6986c80425efdf21" + integrity sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ== + dependencies: + "@babel/runtime" "^7.24.7" + "@restart/hooks" "^0.4.9" + "@restart/ui" "^1.9.4" + "@types/prop-types" "^15.7.12" + "@types/react-transition-group" "^4.4.6" + classnames "^2.3.2" + dom-helpers "^5.2.1" + invariant "^2.2.4" + prop-types "^15.8.1" + prop-types-extra "^1.1.0" + react-transition-group "^4.4.5" + uncontrollable "^7.2.1" + warning "^4.0.3" + +react-dom@^19.2.4: + version "19.2.4" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.4.tgz#6fac6bd96f7db477d966c7ec17c1a2b1ad8e6591" + integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ== + dependencies: + scheduler "^0.27.0" + +react-icons@^5.5.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.6.0.tgz#27bcc4acbc836e762548d76041cf9b9fef4e3837" + integrity sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA== + +react-is@^16.13.1, react-is@^16.3.2: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-refresh@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" + integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== + +react-slider@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-2.0.6.tgz#8c7ff0301211f7c3ff32aa0163b33bdab6258559" + integrity sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA== + dependencies: + prop-types "^15.8.1" + +react-tiny-popover@^8.1.6: + version "8.1.6" + resolved "https://registry.yarnpkg.com/react-tiny-popover/-/react-tiny-popover-8.1.6.tgz#82fad10eb8f0d8197ce0944031fd03a524b78c29" + integrity sha512-jeZnGqHxb5TX7pCzpqLoVJned7DTVnLrLoCQQGFTyvlxXB/QUaet7O0krG22t5FReMBH035SLnzThKvk8tIfsg== + +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react-youtube@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/react-youtube/-/react-youtube-10.1.0.tgz#7e5670c764f12eb408166e8eb438d788dc64e8b5" + integrity sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg== + dependencies: + fast-deep-equal "3.1.3" + prop-types "15.8.1" + youtube-player "5.5.2" + +react@^19.2.4: + version "19.2.4" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a" + integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ== + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.7" + get-proto "^1.0.1" + which-builtin-type "^1.2.1" + +regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" + +resolve@^2.0.0-next.5: + version "2.0.0-next.6" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.6.tgz#b3961812be69ace7b3bc35d5bf259434681294af" + integrity sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + node-exports-info "^1.6.0" + object-keys "^1.1.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rollup@^4.43.0: + version "4.59.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.59.0.tgz#cf74edac17c1486f562d728a4d923a694abdf06f" + integrity sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg== + dependencies: + "@types/estree" "1.0.8" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.59.0" + "@rollup/rollup-android-arm64" "4.59.0" + "@rollup/rollup-darwin-arm64" "4.59.0" + "@rollup/rollup-darwin-x64" "4.59.0" + "@rollup/rollup-freebsd-arm64" "4.59.0" + "@rollup/rollup-freebsd-x64" "4.59.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.59.0" + "@rollup/rollup-linux-arm-musleabihf" "4.59.0" + "@rollup/rollup-linux-arm64-gnu" "4.59.0" + "@rollup/rollup-linux-arm64-musl" "4.59.0" + "@rollup/rollup-linux-loong64-gnu" "4.59.0" + "@rollup/rollup-linux-loong64-musl" "4.59.0" + "@rollup/rollup-linux-ppc64-gnu" "4.59.0" + "@rollup/rollup-linux-ppc64-musl" "4.59.0" + "@rollup/rollup-linux-riscv64-gnu" "4.59.0" + "@rollup/rollup-linux-riscv64-musl" "4.59.0" + "@rollup/rollup-linux-s390x-gnu" "4.59.0" + "@rollup/rollup-linux-x64-gnu" "4.59.0" + "@rollup/rollup-linux-x64-musl" "4.59.0" + "@rollup/rollup-openbsd-x64" "4.59.0" + "@rollup/rollup-openharmony-arm64" "4.59.0" + "@rollup/rollup-win32-arm64-msvc" "4.59.0" + "@rollup/rollup-win32-ia32-msvc" "4.59.0" + "@rollup/rollup-win32-x64-gnu" "4.59.0" + "@rollup/rollup-win32-x64-msvc" "4.59.0" + fsevents "~2.3.2" + +safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" + isarray "^2.0.5" + +safe-push-apply@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== + dependencies: + es-errors "^1.3.0" + isarray "^2.0.5" + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +sass@^1.97.3: + version "1.98.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.98.0.tgz#924ce85a3745ccaccd976262fdc1bc0c13aa8e57" + integrity sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A== + dependencies: + chokidar "^4.0.0" + immutable "^5.1.5" + source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" + +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.7.3: + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +set-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== + dependencies: + dunder-proto "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +sister@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/sister/-/sister-3.0.2.tgz#bb3e39f07b1f75bbe1945f29a27ff1e5a2f26be4" + integrity sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA== + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +stop-iteration-iterator@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" + integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== + dependencies: + es-errors "^1.3.0" + internal-slot "^1.1.0" + +string.prototype.matchall@^4.0.12: + version "4.0.12" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0" + integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-abstract "^1.23.6" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + gopd "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + regexp.prototype.flags "^1.5.3" + set-function-name "^2.0.2" + side-channel "^1.1.0" + +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" + +string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tailwindcss@4.2.1, tailwindcss@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.2.1.tgz#018c4720b58baf98a6bf56b0a12aa797c6cfef1d" + integrity sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw== + +tapable@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" + integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== + +tinyglobby@^0.2.15: + version "0.2.15" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" + integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.3" + +ts-api-utils@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" + integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== + +tsconfck@^3.0.3: + version "3.1.6" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.6.tgz#da1f0b10d82237ac23422374b3fce1edb23c3ead" + integrity sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w== + +tslib@^2.4.0, tslib@^2.8.0, tslib@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-typed-array "^1.1.14" + +typed-array-byte-length@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== + dependencies: + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" + +typed-array-byte-offset@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" + +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" + +typescript-eslint@^8.56.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.57.0.tgz#82764795d316ed1c72a489727c43c3a87373f100" + integrity sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA== + dependencies: + "@typescript-eslint/eslint-plugin" "8.57.0" + "@typescript-eslint/parser" "8.57.0" + "@typescript-eslint/typescript-estree" "8.57.0" + "@typescript-eslint/utils" "8.57.0" + +typescript@^5.9.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +unbox-primitive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== + dependencies: + call-bound "^1.0.3" + has-bigints "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" + +uncontrollable@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738" + integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== + dependencies: + "@babel/runtime" "^7.6.3" + "@types/react" ">=16.9.11" + invariant "^2.2.4" + react-lifecycles-compat "^3.0.4" + +uncontrollable@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-8.0.4.tgz#a0a8307f638795162fafd0550f4a1efa0f8c5eb6" + integrity sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ== + +undici-types@~7.18.0: + version "7.18.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9" + integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w== + +update-browserslist-db@^1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +use-between@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/use-between/-/use-between-1.4.0.tgz#d1e3b95095be2c2305709c15ed5265ee6c692935" + integrity sha512-MpLUnRHxZd3CNa5EeXaMadK1+oSd2Kst57WfU15TQbsLu3vgMcfh4gjAJWKaox02pOf+7Lx1ZHK5tMXHEVH1Qw== + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +vite-tsconfig-paths@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz#d5c28cba79c89ebf76489ef1040024b21df6da3a" + integrity sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^3.0.3" + +vite@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507" + integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== + dependencies: + esbuild "^0.27.0" + fdir "^6.5.0" + picomatch "^4.0.3" + postcss "^8.5.6" + rollup "^4.43.0" + tinyglobby "^0.2.15" + optionalDependencies: + fsevents "~2.3.3" + +warning@^4.0.0, warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== + dependencies: + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" + +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== + dependencies: + call-bound "^1.0.2" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" + is-generator-function "^1.0.10" + is-regex "^1.2.1" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.1.0" + which-collection "^1.0.2" + which-typed-array "^1.1.16" + +which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.16, which-typed-array@^1.1.19: + version "1.1.20" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.20.tgz#3fdb7adfafe0ea69157b1509f3a1cd892bd1d122" + integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +youtube-player@5.5.2: + version "5.5.2" + resolved "https://registry.yarnpkg.com/youtube-player/-/youtube-player-5.5.2.tgz#052b86b1eabe21ff331095ffffeae285fa7f7cb5" + integrity sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ== + dependencies: + debug "^2.6.6" + load-script "^1.0.0" + sister "^3.0.0" + +"zod-validation-error@^3.5.0 || ^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918" + 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== From 2a29d3d08c735179467eadc239f0ea0b3c7903f8 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 15 Mar 2026 14:01:25 +0100 Subject: [PATCH 04/33] ui plugins --- src/components/MainView.tsx | 2 + .../plugins/ExternalPluginLoader.tsx | 61 ++++++ src/components/plugins/NitroPluginApi.ts | 193 ++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 src/components/plugins/ExternalPluginLoader.tsx create mode 100644 src/components/plugins/NitroPluginApi.ts diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index d9594bc..3f1db7e 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -22,6 +22,7 @@ import { ModToolsView } from './mod-tools/ModToolsView'; import { NavigatorView } from './navigator/NavigatorView'; import { NitrobubbleHiddenView } from './nitrobubblehidden/NitrobubbleHiddenView'; import { NitropediaView } from './nitropedia/NitropediaView'; +import { ExternalPluginLoader } from './plugins/ExternalPluginLoader'; import { RightSideView } from './right-side/RightSideView'; import { RoomView } from './room/RoomView'; import { ToolbarView } from './toolbar/ToolbarView'; @@ -118,6 +119,7 @@ export const MainView: FC<{}> = props => + ); }; diff --git a/src/components/plugins/ExternalPluginLoader.tsx b/src/components/plugins/ExternalPluginLoader.tsx new file mode 100644 index 0000000..b97fb41 --- /dev/null +++ b/src/components/plugins/ExternalPluginLoader.tsx @@ -0,0 +1,61 @@ +import { FC, useEffect, useState } from 'react'; +import { GetConfigurationValue } from '../../api'; +import { subscribePlugins } from './NitroPluginApi'; + +// Force the global API to be initialized +import './NitroPluginApi'; + +export const ExternalPluginLoader: FC<{}> = () => +{ + const [, forceUpdate] = useState(0); + + useEffect(() => + { + return subscribePlugins(() => forceUpdate(n => n + 1)); + }, []); + + // MainView only renders after isReady=true in App.tsx, + // so the configuration is guaranteed to be loaded at this point. + useEffect(() => + { + const scripts: HTMLScriptElement[] = []; + + let pluginUrls: string[] = []; + + try + { + pluginUrls = GetConfigurationValue('external.plugins', []); + } + catch (e) + { + console.warn('[NitroPlugins] Could not read external.plugins config:', e); + return; + } + + if (!pluginUrls || pluginUrls.length === 0) + { + console.log('[NitroPlugins] No external plugins configured'); + return; + } + + console.log('[NitroPlugins] Loading external plugins:', pluginUrls); + + for (const url of pluginUrls) + { + const script = document.createElement('script'); + script.src = url; + script.async = true; + script.onload = () => console.log(`[NitroPlugins] Loaded: ${url}`); + script.onerror = () => console.warn(`[NitroPlugins] Failed to load: ${url}`); + document.head.appendChild(script); + scripts.push(script); + } + + return () => + { + scripts.forEach(s => s.remove()); + }; + }, []); + + return null; +}; diff --git a/src/components/plugins/NitroPluginApi.ts b/src/components/plugins/NitroPluginApi.ts new file mode 100644 index 0000000..d52ffa0 --- /dev/null +++ b/src/components/plugins/NitroPluginApi.ts @@ -0,0 +1,193 @@ +import { GetRoomEngine } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent, GetRoomSession, SendMessageComposer } from '../../api'; + +/** + * Plugin descriptor registered by external plugin scripts. + */ +export interface INitroPlugin +{ + /** Unique plugin name */ + name: string; + /** Label shown on the button in room tools */ + label: string; + /** CSS class for the icon (nitro-icon class) */ + icon?: string; + /** Called when the plugin button is clicked */ + onOpen: () => void; + /** Called to close/destroy the plugin UI */ + onClose?: () => void; + /** Called when the plugin is first loaded, receives the Nitro API */ + onInit?: (api: INitroPluginApi) => void; +} + +/** + * API exposed to external plugins via window.NitroPlugins + */ +export interface INitroPluginApi +{ + /** Register a plugin */ + register: (plugin: INitroPlugin) => void; + /** Unregister a plugin by name */ + unregister: (name: string) => void; + /** Get all registered plugins */ + getPlugins: () => INitroPlugin[]; + /** Fire a Nitro link event (e.g., 'navigator/toggle-room-info') */ + createLinkEvent: (link: string) => void; + /** Get the room engine instance */ + getRoomEngine: () => ReturnType; + /** Get the current room session */ + getRoomSession: () => ReturnType; + /** Send a message composer to the server */ + sendMessage: typeof SendMessageComposer; + /** Create a draggable floating window and return its container element */ + createWindow: (id: string, title: string, width: number) => HTMLDivElement | null; + /** Destroy a floating window by id */ + destroyWindow: (id: string) => void; +} + +// Internal plugin storage +const _plugins: INitroPlugin[] = []; +const _listeners: Array<() => void> = []; + +function notifyListeners() +{ + _listeners.forEach(fn => fn()); +} + +const pluginApi: INitroPluginApi = { + register(plugin: INitroPlugin) + { + if (_plugins.some(p => p.name === plugin.name)) return; + _plugins.push(plugin); + plugin.onInit?.(pluginApi); + notifyListeners(); + }, + + unregister(name: string) + { + const index = _plugins.findIndex(p => p.name === name); + if (index >= 0) + { + _plugins[index].onClose?.(); + _plugins.splice(index, 1); + notifyListeners(); + } + }, + + getPlugins() + { + return [..._plugins]; + }, + + createLinkEvent(link: string) + { + CreateLinkEvent(link); + }, + + getRoomEngine() + { + return GetRoomEngine(); + }, + + getRoomSession() + { + return GetRoomSession(); + }, + + sendMessage: SendMessageComposer, + + createWindow(id: string, title: string, width: number): HTMLDivElement | null + { + // Remove existing window with same id + pluginApi.destroyWindow(id); + + // Create overlay container + const overlay = document.createElement('div'); + overlay.id = `nitro-plugin-window-${id}`; + overlay.style.cssText = `position:fixed;z-index:500;top:50%;left:50%;transform:translate(-50%,-50%)`; + + // Card wrapper + const card = document.createElement('div'); + card.style.cssText = `width:${width}px;background:#2c3e50;border:1px solid #283F5D;border-radius:6px;box-shadow:0 8px 32px rgba(0,0,0,0.5);overflow:hidden;font-family:Ubuntu,sans-serif`; + + // Header (draggable) + const header = document.createElement('div'); + header.style.cssText = `display:flex;align-items:center;justify-content:center;position:relative;min-height:33px;background:linear-gradient(180deg,#3c6a8e 0%,#2a4f6e 100%);cursor:move;user-select:none`; + + const titleEl = document.createElement('span'); + titleEl.textContent = title; + titleEl.style.cssText = `color:#fff;font-size:16px;text-shadow:0 1px 2px rgba(0,0,0,0.5)`; + + const closeBtn = document.createElement('div'); + closeBtn.style.cssText = `position:absolute;right:8px;width:20px;height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;border-radius:50%;background:rgba(255,255,255,0.1)`; + closeBtn.innerHTML = '✕'; + closeBtn.addEventListener('click', () => pluginApi.destroyWindow(id)); + + header.appendChild(titleEl); + header.appendChild(closeBtn); + + // Make draggable + let isDragging = false; + let offsetX = 0, offsetY = 0; + + header.addEventListener('mousedown', (e: MouseEvent) => + { + isDragging = true; + const rect = overlay.getBoundingClientRect(); + offsetX = e.clientX - rect.left; + offsetY = e.clientY - rect.top; + overlay.style.transform = 'none'; + overlay.style.left = rect.left + 'px'; + overlay.style.top = rect.top + 'px'; + }); + + document.addEventListener('mousemove', (e: MouseEvent) => + { + if (!isDragging) return; + overlay.style.left = (e.clientX - offsetX) + 'px'; + overlay.style.top = (e.clientY - offsetY) + 'px'; + }); + + document.addEventListener('mouseup', () => { isDragging = false; }); + + // Content area + const content = document.createElement('div'); + content.style.cssText = `padding:16px`; + + card.appendChild(header); + card.appendChild(content); + overlay.appendChild(card); + document.body.appendChild(overlay); + + return content; + }, + + destroyWindow(id: string) + { + const existing = document.getElementById(`nitro-plugin-window-${id}`); + if (existing) existing.remove(); + } +}; + +/** + * Subscribe to plugin list changes. Returns unsubscribe function. + */ +export function subscribePlugins(listener: () => void): () => void +{ + _listeners.push(listener); + return () => + { + const idx = _listeners.indexOf(listener); + if (idx >= 0) _listeners.splice(idx, 1); + }; +} + +export function getRegisteredPlugins(): INitroPlugin[] +{ + return [..._plugins]; +} + +// Expose globally so external scripts can use it +(window as any).NitroPlugins = pluginApi; + +export { pluginApi }; From 38f38d72095923faee0ac7e52d28dfaba06279ea Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 15 Mar 2026 20:48:05 +0100 Subject: [PATCH 05/33] Add badge drag & drop system for InfoStand and inventory - Drag & drop badges between slots in InfoStand (own user only) - Mini badge picker on empty slot click with search - Swap badges between occupied slots - Hover animation (scale, glow) on badge slots - Configurable group slot (user.badges.group.slot.enabled) - Support for 6 badge slots when group slot disabled - Race condition fix with localChangeRef - Fixed-size array logic to prevent badge disappearing Co-Authored-By: medievalshell --- .claude/settings.local.json | 15 +- public/plugins/room-builder.js | 587 ++++++++++++++++++ public/ui-config.json | 6 +- .../views/badge/InventoryBadgeItemView.tsx | 16 +- .../views/badge/InventoryBadgeView.tsx | 141 ++++- src/components/plugins/NitroPluginApi.ts | 56 +- .../infostand/InfoStandBadgeSlotView.tsx | 172 +++++ .../infostand/InfoStandWidgetUserView.tsx | 63 +- .../room-tools/RoomToolsWidgetView.tsx | 23 +- src/hooks/inventory/useInventoryBadges.ts | 108 +++- src/hooks/rooms/widgets/useChatInputWidget.ts | 20 +- 11 files changed, 1152 insertions(+), 55 deletions(-) create mode 100644 public/plugins/room-builder.js create mode 100644 src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bbe542f..04ffefa 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -24,7 +24,20 @@ "Bash(git push:*)", "Bash(find /e/www/habbo-next/src/app -type f -path *catalog*)", "Bash(echo \"EXIT:$?\")", - "Bash(find /E/www/habbo-next/src -type f -name *prisma*)" + "Bash(find /E/www/habbo-next/src -type f -name *prisma*)", + "Bash(node -e \"const mysql = require\\(''mysql2/promise''\\); \\(async\\(\\)=>{const c=await mysql.createConnection\\({host:''localhost'',user:''root'',database:''habbo''}\\); const [r]=await c.query\\(''DESCRIBE items_base''\\); console.log\\(r.map\\(x=>x.Field\\).join\\(''\\\\n''\\)\\); await c.end\\(\\);}\\)\\(\\)\")", + "Bash(node -e \"const mysql = require\\(''mysql2/promise''\\); \\(async\\(\\)=>{const c=await mysql.createConnection\\({host:''localhost'',user:''habbo'',password:''habbo'',database:''next''}\\); const [r]=await c.query\\(''DESCRIBE items_base''\\); console.log\\(r.map\\(x=>x.Field\\).join\\(''\\\\n''\\)\\); const [r2]=await c.query\\(''DESCRIBE catalog_items''\\); console.log\\(''---''\\); console.log\\(r2.map\\(x=>x.Field\\).join\\(''\\\\n''\\)\\); await c.end\\(\\);}\\)\\(\\)\")", + "Bash(node -e \"const mysql = require\\(''mysql2/promise''\\); \\(async\\(\\)=>{const c=await mysql.createConnection\\({host:''localhost'',user:''habbo'',password:''habbo'',database:''next''}\\); const [r]=await c.query\\(''DESCRIBE items_base''\\); const cols=r.map\\(x=>x.Field\\); console.log\\(''items_base columns:'', JSON.stringify\\(cols\\)\\); const [r2]=await c.query\\(''DESCRIBE catalog_items''\\); const cols2=r2.map\\(x=>x.Field\\); console.log\\(''catalog_items columns:'', JSON.stringify\\(cols2\\)\\); await c.end\\(\\);}\\)\\(\\)\")", + "Bash(node -e \":*)", + "Bash(npx prisma:*)", + "WebFetch(domain:www.habbo.it)", + "Bash(grep -r \"slider\\\\|height\\\\|rotation\\\\|state\\\\|speed\" /e/www/habbo-next/public/nitro3/src/components/room/widgets/furniture/*.tsx)", + "Bash(grep -r \"processAction\\\\|handleAction\\\\|dispatch\" /e/www/habbo-next/public/nitro3/src/components/room/widgets/furniture/*.tsx)", + "Bash(xargs ls:*)", + "Bash(find /e/www/habbo-next/public/nitro3/src -type f \\\\\\(-name *Modif* -o -name *Manip* -o -name *Floorplan* -o -name *Builder* \\\\\\))", + "Bash(mkdir -p \"E:/www/habbo-next/public/nitro3/src/api/plugins\")", + "Bash(mkdir -p \"E:/www/habbo-next/public/nitro3/src/components/plugins/room-builder\")", + "Bash(ls \"E:/www/habbo-next/public/nitro3/vite.config\"*)" ] } } diff --git a/public/plugins/room-builder.js b/public/plugins/room-builder.js new file mode 100644 index 0000000..b9ae098 --- /dev/null +++ b/public/plugins/room-builder.js @@ -0,0 +1,587 @@ +/** + * Room Builder Plugin - Menu Costruzioni + * + * Plugin esterno per Nitro Client. + * Richiede il RoomBuilderPlugin.jar lato server (Arcturus). + * + * Se rimuovi questo file, il bottone scompare automaticamente dalla UI. + * + * Colori e stili uniformati al tema Nitro Client. + */ +(function () +{ + 'use strict'; + + // ─── Nitro Theme Colors ─── + var THEME = { + // Card / Window + headerBg: '#1E7295', + headerText: '#FFFFFF', + tabsBg: '#185D79', + cardBorder: '#283F5D', + contentBg: '#DFDFDF', + + // Buttons + btnPrimary: '#3c6d82', + btnPrimaryBrd: '#1a617f', + btnPrimaryHov: '#4a8199', + btnActive: '#185D79', + btnActiveBrd: '#0f4a63', + btnActiveHov: '#1E7295', + btnDanger: '#a81a12', + btnDangerBrd: '#b9322a', + btnDangerHov: '#c43a32', + btnSuccess: '#00800b', + btnSuccessBrd: '#006d09', + btnWarning: '#ffc107', + btnWarningBrd: '#f3c12a', + + // Dark panel (infostand style) + darkBg: '#212131', + darkBorder: '#383853', + darkShadow: 'inset 0 5px rgba(38,38,57,0.6), inset 0 -4px rgba(25,25,37,0.6)', + + // Grid items + gridBg: '#CDD3D9', + gridBorder: '#B6BEC5', + gridActiveBg: '#ECECEC', + gridActiveBrd: '#FFFFFF', + + // Text + textLight: '#FFFFFF', + textDark: '#212529', + textMuted: '#B6BEC5', + + // Typography + fontFamily: 'Ubuntu, sans-serif', + fontSm: '0.7875rem', + fontBase: '0.9rem', + + // Misc + borderRadius: '0.5rem', + borderRadiusSm: '0.25rem', + scrollThumb: 'rgba(30, 114, 149, 0.4)', + scrollThumbHov: 'rgba(30, 114, 149, 0.8)' + }; + + function waitForApi(callback, maxRetries) + { + if (maxRetries === undefined) maxRetries = 50; + if (window.NitroPlugins) + { + callback(window.NitroPlugins); + return; + } + if (maxRetries <= 0) + { + console.warn('[RoomBuilder] NitroPlugins API not found after retries'); + return; + } + setTimeout(function () { waitForApi(callback, maxRetries - 1); }, 200); + } + + // ─── Constants ─── + var FLOOR = 10; + var WALL = 20; + + // ─── Send chat command to server via proper API ─── + function sendCommand(api, command) + { + try + { + api.sendChat(':' + command); + } + catch (e) + { + console.warn('[RoomBuilder] sendCommand error:', e); + } + } + + // ─── Section Label Helper ─── + function createSectionLabel(container, text) + { + var label = document.createElement('div'); + label.textContent = text; + label.style.cssText = 'font-family:' + THEME.fontFamily + ';font-size:' + THEME.fontSm + ';font-weight:bold;color:' + THEME.textDark + ';margin:10px 0 6px 0;padding-bottom:4px;border-bottom:1px solid ' + THEME.gridBorder + ';text-transform:uppercase;letter-spacing:0.5px'; + container.appendChild(label); + } + + // ─── Slider Helper ─── + function createSlider(container, label, min, max, value, step, onChange) + { + var row = document.createElement('div'); + row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:6px;padding:4px 6px;background:' + THEME.gridBg + ';border:1px solid ' + THEME.gridBorder + ';border-radius:' + THEME.borderRadiusSm; + + var lbl = document.createElement('span'); + lbl.textContent = label; + lbl.style.cssText = 'width:70px;color:' + THEME.textDark + ';font-family:' + THEME.fontFamily + ';font-size:' + THEME.fontSm + ';font-weight:bold;flex-shrink:0'; + + var slider = document.createElement('input'); + slider.type = 'range'; + slider.min = min; + slider.max = max; + slider.step = step || 1; + slider.value = value; + slider.style.cssText = 'flex:1;height:6px;cursor:pointer;accent-color:' + THEME.headerBg; + + var valDisplay = document.createElement('span'); + valDisplay.textContent = value; + valDisplay.style.cssText = 'width:28px;color:' + THEME.textDark + ';font-family:' + THEME.fontFamily + ';font-size:' + THEME.fontSm + ';text-align:center;font-weight:bold'; + + var saveBtn = document.createElement('button'); + saveBtn.innerHTML = '💾'; + saveBtn.title = 'Salva'; + saveBtn.style.cssText = 'width:28px;height:28px;min-height:28px;display:flex;align-items:center;justify-content:center;background:' + THEME.btnSuccess + ';border:2px solid ' + THEME.btnSuccessBrd + ';border-radius:' + THEME.borderRadius + ';cursor:pointer;font-size:11px;color:' + THEME.textLight; + + var resetBtn = document.createElement('button'); + resetBtn.innerHTML = '↩'; + resetBtn.title = 'Ripristina'; + resetBtn.style.cssText = 'width:28px;height:28px;min-height:28px;display:flex;align-items:center;justify-content:center;background:' + THEME.btnDanger + ';border:2px solid ' + THEME.btnDangerBrd + ';border-radius:' + THEME.borderRadius + ';color:' + THEME.textLight + ';cursor:pointer;font-size:13px;font-weight:bold'; + + slider.addEventListener('input', function () + { + valDisplay.textContent = slider.value; + }); + + saveBtn.addEventListener('click', function () + { + if (onChange) onChange(Number(slider.value)); + }); + + resetBtn.addEventListener('click', function () + { + slider.value = value; + valDisplay.textContent = value; + if (onChange) onChange(Number(value)); + }); + + row.appendChild(lbl); + row.appendChild(slider); + row.appendChild(valDisplay); + row.appendChild(saveBtn); + row.appendChild(resetBtn); + container.appendChild(row); + + return { slider: slider, valDisplay: valDisplay }; + } + + // ─── Button Helper ─── + function createButton(container, label, onClick, opts) + { + opts = opts || {}; + var isActive = opts.active || false; + var isDanger = opts.danger || false; + + var bgColor = isDanger ? THEME.btnDanger : (isActive ? THEME.btnActive : THEME.btnPrimary); + var borderColor = isDanger ? THEME.btnDangerBrd : (isActive ? THEME.btnActiveBrd : THEME.btnPrimaryBrd); + var hoverColor = isDanger ? THEME.btnDangerHov : (isActive ? THEME.btnActiveHov : THEME.btnPrimaryHov); + + var btn = document.createElement('button'); + btn.textContent = label; + btn.style.cssText = 'padding:0.25rem 0.5rem;border-radius:' + THEME.borderRadius + ';color:' + THEME.textLight + ';font-family:' + THEME.fontFamily + ';font-size:' + THEME.fontSm + ';font-weight:500;cursor:pointer;border:2px solid ' + borderColor + ';transition:background .15s;background:' + bgColor + ';min-height:28px;box-shadow:none;' + (opts.fullWidth ? 'width:100%;' : '') + (opts.extraStyle || ''); + + btn.addEventListener('mouseenter', function () { btn.style.background = hoverColor; }); + btn.addEventListener('mouseleave', function () { btn.style.background = bgColor; }); + btn.addEventListener('click', function () + { + if (onClick) onClick(btn); + }); + if (container) container.appendChild(btn); + return btn; + } + + // ─── Grid Row Helper ─── + function createButtonRow(container, cols, buttons) + { + var row = document.createElement('div'); + row.style.cssText = 'display:grid;grid-template-columns:repeat(' + cols + ',1fr);gap:6px;margin-bottom:6px'; + buttons.forEach(function (b) { createButton(row, b.label, b.onClick, b); }); + container.appendChild(row); + } + + // ─── Utility: iterate room floor objects ─── + function forEachFloorObject(api, callback) + { + var session = api.getRoomSession(); + var engine = api.getRoomEngine(); + if (!session || !engine) return; + var objects = engine.getRoomObjects(session.roomId, FLOOR); + for (var i = 0; i < objects.length; i++) + { + var obj = engine.getRoomObject(session.roomId, objects[i].id, FLOOR); + if (obj) callback(obj, objects[i].id, session.roomId, engine); + } + } + + // ─── State ─── + var state = { + hidePyramids: false, + hideCarpets: false, + hideWalls: false, + hideWired: false, + frozen: false, + teleporting: false + }; + + // ─── Stack clipboard (client-side memory) ─── + var stackClipboard = null; + + // ─── Plugin Init ─── + waitForApi(function (api) + { + api.register({ + name: 'room-builder', + label: 'Menu costruzioni', + icon: 'icon-cog', + + onOpen: function () + { + var content = api.createWindow('room-builder', 'Menu costruzioni', 440); + if (!content) return; + + // Apply Nitro content area style to the content container + content.style.cssText = 'padding:10px;font-family:' + THEME.fontFamily + ';font-size:' + THEME.fontBase; + + // ─── Warning banner (Nitro tabs style) ─── + var banner = document.createElement('div'); + banner.style.cssText = 'display:flex;align-items:center;justify-content:center;gap:6px;background:' + THEME.tabsBg + ';border:1px solid ' + THEME.cardBorder + ';border-radius:' + THEME.borderRadiusSm + ';padding:8px 12px;margin-bottom:12px'; + banner.innerHTML = 'Assicurati di non spammare per non essere mutato'; + content.appendChild(banner); + + // ─── Sliders Section ─── + createSectionLabel(content, 'Controlli'); + + var slidersDiv = document.createElement('div'); + slidersDiv.style.marginBottom = '8px'; + + createSlider(slidersDiv, 'Altezza', -10, 40, 0, 1, function (val) + { + try + { + sendCommand(api, 'autotile ' + val); + } + catch (e) { console.warn('[RoomBuilder] Height:', e); } + }); + + createSlider(slidersDiv, 'Velocita', 0, 10, 4, 1, function (val) + { + sendCommand(api, 'rb_speed ' + val); + }); + + content.appendChild(slidersDiv); + + // ─── Screenshot ─── + var ssDiv = document.createElement('div'); + ssDiv.style.marginBottom = '8px'; + createButton(ssDiv, 'Fai lo screenshot della stanza', function () + { + api.takeScreenshot(); + }, { fullWidth: true, extraStyle: 'padding:6px 12px;' }); + content.appendChild(ssDiv); + + // ─── Avatar Section ─── + createSectionLabel(content, 'Avatar'); + + createButtonRow(content, 2, [ + { + label: state.frozen ? '\u2713 Avatar bloccato' : 'Blocca avatar', + active: state.frozen, + onClick: function (btn) + { + state.frozen = !state.frozen; + btn.textContent = state.frozen ? '\u2713 Avatar bloccato' : 'Blocca avatar'; + btn.style.background = state.frozen ? THEME.btnActive : THEME.btnPrimary; + btn.style.borderColor = state.frozen ? THEME.btnActiveBrd : THEME.btnPrimaryBrd; + sendCommand(api, 'blocca'); + } + }, + { + label: state.teleporting ? '\u2713 Teletrasporto ON' : 'Teletrasporto', + active: state.teleporting, + onClick: function (btn) + { + state.teleporting = !state.teleporting; + btn.textContent = state.teleporting ? '\u2713 Teletrasporto ON' : 'Teletrasporto'; + btn.style.background = state.teleporting ? THEME.btnActive : THEME.btnPrimary; + btn.style.borderColor = state.teleporting ? THEME.btnActiveBrd : THEME.btnPrimaryBrd; + sendCommand(api, 'rb_teleport'); + } + } + ]); + + // ─── Visibility Section ─── + createSectionLabel(content, 'Visibilita'); + + createButtonRow(content, 3, [ + { + label: state.hidePyramids ? 'Mostra piramidi' : 'Nascondi piramidi', + active: state.hidePyramids, + onClick: function (btn) + { + state.hidePyramids = !state.hidePyramids; + btn.style.background = state.hidePyramids ? THEME.btnActive : THEME.btnPrimary; + btn.style.borderColor = state.hidePyramids ? THEME.btnActiveBrd : THEME.btnPrimaryBrd; + btn.textContent = state.hidePyramids ? 'Mostra piramidi' : 'Nascondi piramidi'; + try + { + forEachFloorObject(api, function (obj, objId, roomId, engine) + { + if (obj.type && obj.type.toLowerCase().indexOf('pyramid') >= 0) + { + engine.changeObjectModelData(roomId, objId, FLOOR, 'furniture_alpha_multiplier', state.hidePyramids ? 0 : 1); + } + }); + } + catch (e) { console.warn('[RoomBuilder]', e); } + } + }, + { + label: state.hideCarpets ? 'Mostra tappeti' : 'Nascondi tappeti', + active: state.hideCarpets, + onClick: function (btn) + { + state.hideCarpets = !state.hideCarpets; + btn.style.background = state.hideCarpets ? THEME.btnActive : THEME.btnPrimary; + btn.style.borderColor = state.hideCarpets ? THEME.btnActiveBrd : THEME.btnPrimaryBrd; + btn.textContent = state.hideCarpets ? 'Mostra tappeti' : 'Nascondi tappeti'; + try + { + forEachFloorObject(api, function (obj, objId, roomId, engine) + { + if (obj.model) + { + var sizeZ = obj.model.getValue('furniture_size_z'); + if (sizeZ !== undefined && sizeZ <= 0.01) + { + engine.changeObjectModelData(roomId, objId, FLOOR, 'furniture_alpha_multiplier', state.hideCarpets ? 0 : 1); + } + } + }); + } + catch (e) { console.warn('[RoomBuilder]', e); } + } + }, + { + label: state.hideWalls ? 'Mostra mura' : 'Nascondi mura', + active: state.hideWalls, + onClick: function (btn) + { + state.hideWalls = !state.hideWalls; + btn.style.background = state.hideWalls ? THEME.btnActive : THEME.btnPrimary; + btn.style.borderColor = state.hideWalls ? THEME.btnActiveBrd : THEME.btnPrimaryBrd; + btn.textContent = state.hideWalls ? 'Mostra mura' : 'Nascondi mura'; + try + { + var session = api.getRoomSession(); + var engine = api.getRoomEngine(); + if (!session || !engine) return; + var objects = engine.getRoomObjects(session.roomId, WALL); + for (var i = 0; i < objects.length; i++) + { + engine.changeObjectModelData(session.roomId, objects[i].id, WALL, 'furniture_alpha_multiplier', state.hideWalls ? 0 : 1); + } + } + catch (e) { console.warn('[RoomBuilder]', e); } + } + } + ]); + + // ─── Stack Section ─── + createSectionLabel(content, 'Pila (Stack)'); + + createButtonRow(content, 4, [ + { + label: 'Annulla pila', + onClick: function () + { + sendCommand(api, 'autotile'); + } + }, + { + label: 'Seleziona pila', + onClick: function () + { + try + { + var session = api.getRoomSession(); + var engine = api.getRoomEngine(); + if (!session || !engine) return; + var objects = engine.getRoomObjects(session.roomId, FLOOR); + stackClipboard = []; + for (var i = 0; i < objects.length; i++) + { + var obj = engine.getRoomObject(session.roomId, objects[i].id, FLOOR); + if (obj && obj.location) + { + stackClipboard.push({ + id: objects[i].id, + x: Math.floor(obj.location.x), + y: Math.floor(obj.location.y), + z: obj.location.z, + dir: obj.direction ? obj.direction.x : 0 + }); + } + } + console.log('[RoomBuilder] Pila selezionata: ' + stackClipboard.length + ' oggetti'); + } + catch (e) { console.warn('[RoomBuilder]', e); } + } + }, + { + label: 'Copia pila', + onClick: function () + { + if (!stackClipboard || stackClipboard.length === 0) + { + console.log('[RoomBuilder] Nessuna pila selezionata'); + return; + } + console.log('[RoomBuilder] Pila copiata: ' + stackClipboard.length + ' oggetti'); + } + }, + { + label: 'Posiziona pila', + onClick: function () + { + if (!stackClipboard || stackClipboard.length === 0) + { + console.log('[RoomBuilder] Nessuna pila da posizionare'); + return; + } + try + { + for (var i = 0; i < stackClipboard.length; i++) + { + var item = stackClipboard[i]; + api.sendStackHeight(item.id, Math.round(item.z * 100)); + } + console.log('[RoomBuilder] Pila posizionata: ' + stackClipboard.length + ' oggetti'); + } + catch (e) { console.warn('[RoomBuilder]', e); } + } + } + ]); + + // ─── Room Management Section ─── + createSectionLabel(content, 'Gestione stanza'); + + createButtonRow(content, 3, [ + { + label: 'Impostazioni', + onClick: function () { api.createLinkEvent('navigator/toggle-room-info'); } + }, + { + label: 'Reload stanza', + onClick: function () + { + try + { + var session = api.getRoomSession(); + if (session) api.createLinkEvent('navigator/goto/' + session.roomId); + } + catch (e) { } + } + }, + { + label: 'Unload stanza', + onClick: function () + { + api.visitDesktop(); + } + } + ]); + + // ─── Floor Tools Section ─── + createSectionLabel(content, 'Strumenti pavimento'); + + createButtonRow(content, 4, [ + { + label: 'Max Tile', + onClick: function () + { + sendCommand(api, 'maxtile'); + } + }, + { + label: 'Auto Tile', + onClick: function () + { + sendCommand(api, 'autotile'); + } + }, + { + label: 'No Item Floor', + danger: true, + onClick: function () + { + if (confirm('Sei sicuro? Tutti i furni verranno rimossi dal pavimento!')) + { + sendCommand(api, 'noitemfloor'); + } + } + }, + { + label: 'Edit Floorplan', + onClick: function () { api.createLinkEvent('floor-editor/toggle'); } + } + ]); + + // ─── Wired Section ─── + createSectionLabel(content, 'Wired'); + + createButtonRow(content, 2, [ + { + label: state.hideWired ? 'Mostra wired' : 'Nascondi wired', + active: state.hideWired, + onClick: function (btn) + { + state.hideWired = !state.hideWired; + btn.style.background = state.hideWired ? THEME.btnActive : THEME.btnPrimary; + btn.style.borderColor = state.hideWired ? THEME.btnActiveBrd : THEME.btnPrimaryBrd; + btn.textContent = state.hideWired ? 'Mostra wired' : 'Nascondi wired'; + try + { + forEachFloorObject(api, function (obj, objId, roomId, engine) + { + if (obj.type && (obj.type.toLowerCase().indexOf('wf_') >= 0 || obj.type.toLowerCase().indexOf('wired') >= 0)) + { + engine.changeObjectModelData(roomId, objId, FLOOR, 'furniture_alpha_multiplier', state.hideWired ? 0 : 1); + } + }); + } + catch (e) { console.warn('[RoomBuilder]', e); } + } + }, + { + label: 'Prendi tutti gli wired', + danger: true, + onClick: function () + { + sendCommand(api, 'pickwired'); + } + } + ]); + + // ─── Spacer ─── + var spacer = document.createElement('div'); + spacer.style.cssText = 'height:1px;background:' + THEME.gridBorder + ';margin:10px 0'; + content.appendChild(spacer); + + // ─── Back button (danger style like close button) ─── + var backDiv = document.createElement('div'); + createButton(backDiv, 'Torna indietro', function () + { + api.destroyWindow('room-builder'); + }, { fullWidth: true, danger: true, extraStyle: 'padding:6px 12px;' }); + content.appendChild(backDiv); + }, + + onClose: function () + { + if (window.NitroPlugins) window.NitroPlugins.destroyWindow('room-builder'); + } + }); + + console.log('[NitroPlugins] Room Builder plugin loaded (Nitro theme)'); + }); +})(); diff --git a/public/ui-config.json b/public/ui-config.json index 55dbad6..9692188 100644 --- a/public/ui-config.json +++ b/public/ui-config.json @@ -1,4 +1,7 @@ { + "external.plugins": [ + "plugins/room-builder.js" + ], "image.library.notifications.url": "${image.library.url}notifications/%image%.png", "achievements.images.url": "${image.library.url}Quests/%image%.png", "camera.url": "/swf/usercontent/camera/", @@ -10,7 +13,8 @@ "chat.viewer.height.percentage": 0.4, "widget.dimmer.colorwheel": false, "avatar.wardrobe.max.slots": 10, - "user.badges.max.slots": 5, + "user.badges.max.slots": 6, + "user.badges.group.slot.enabled": false, "user.tags.enabled": false, "camera.publish.disabled": false, "hc.disabled": false, diff --git a/src/components/inventory/views/badge/InventoryBadgeItemView.tsx b/src/components/inventory/views/badge/InventoryBadgeItemView.tsx index 4553621..4bf666b 100644 --- a/src/components/inventory/views/badge/InventoryBadgeItemView.tsx +++ b/src/components/inventory/views/badge/InventoryBadgeItemView.tsx @@ -11,8 +11,22 @@ export const InventoryBadgeItemView: FC const { isUnseen = null } = useInventoryUnseenTracker(); const unseen = isUnseen(UnseenItemCategory.BADGE, getBadgeId(badgeCode)); + const onDragStart = (event: React.DragEvent) => + { + event.dataTransfer.setData('badgeCode', badgeCode); + event.dataTransfer.setData('source', 'inventory'); + event.dataTransfer.effectAllowed = 'move'; + }; + return ( - toggleBadge(selectedBadgeCode) } onMouseDown={ event => setSelectedBadgeCode(badgeCode) } { ...rest }> + toggleBadge(selectedBadgeCode) } + onDragStart={ onDragStart } + onMouseDown={ event => setSelectedBadgeCode(badgeCode) } + { ...rest }> { children } diff --git a/src/components/inventory/views/badge/InventoryBadgeView.tsx b/src/components/inventory/views/badge/InventoryBadgeView.tsx index 8a60522..1bf6d13 100644 --- a/src/components/inventory/views/badge/InventoryBadgeView.tsx +++ b/src/components/inventory/views/badge/InventoryBadgeView.tsx @@ -1,5 +1,5 @@ import { DeleteBadgeMessageComposer } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; +import { FC, useCallback, useEffect, useState } from 'react'; import { FaTrashAlt } from 'react-icons/fa'; import { LocalizeBadgeName, LocalizeText, SendMessageComposer, UnseenItemCategory } from '../../../../api'; import { LayoutBadgeImageView } from '../../../../common'; @@ -7,14 +7,74 @@ import { useInventoryBadges, useInventoryUnseenTracker, useNotification } from ' import { InfiniteGrid, NitroButton } from '../../../../layout'; import { InventoryBadgeItemView } from './InventoryBadgeItemView'; +const ActiveBadgeSlot: FC<{ + slotIndex: number; + badgeCode?: string; + onDropBadge: (badgeCode: string, slotIndex: number, sourceSlot?: number) => void; + onRemoveBadge: (badgeCode: string) => void; + onDragStartFromSlot: (event: React.DragEvent, badgeCode: string, slotIndex: number) => void; + onSelectBadge: (badgeCode: string) => void; + isSelected: boolean; +}> = ({ slotIndex, badgeCode, onDropBadge, onRemoveBadge, onDragStartFromSlot, onSelectBadge, isSelected }) => +{ + const [ isDragOver, setIsDragOver ] = useState(false); + + const onDragOver = useCallback((event: React.DragEvent) => + { + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + setIsDragOver(true); + }, []); + + const onDragLeave = useCallback(() => setIsDragOver(false), []); + + const onDrop = useCallback((event: React.DragEvent) => + { + event.preventDefault(); + setIsDragOver(false); + + const droppedBadgeCode = event.dataTransfer.getData('badgeCode'); + const sourceSlotStr = event.dataTransfer.getData('activeSlot'); + const sourceSlot = sourceSlotStr ? parseInt(sourceSlotStr) : undefined; + + if(droppedBadgeCode) onDropBadge(droppedBadgeCode, slotIndex, sourceSlot); + }, [ slotIndex, onDropBadge ]); + + const onDragStart = useCallback((event: React.DragEvent) => + { + if(!badgeCode) return; + onDragStartFromSlot(event, badgeCode, slotIndex); + }, [ badgeCode, slotIndex, onDragStartFromSlot ]); + + return ( +
badgeCode && onSelectBadge(badgeCode) }> + { badgeCode + ? + : { slotIndex + 1 } } +
+ ); +}; + export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props => { const { filteredBadgeCodes = null } = props; const [ isVisible, setIsVisible ] = useState(false); - const { badgeCodes = [], activeBadgeCodes = [], selectedBadgeCode = null, isWearingBadge = null, canWearBadges = null, toggleBadge = null, getBadgeId = null, activate = null, deactivate = null } = useInventoryBadges(); + const { badgeCodes = [], activeBadgeCodes = [], selectedBadgeCode = null, isWearingBadge = null, canWearBadges = null, toggleBadge = null, getBadgeId = null, setBadgeAtSlot = null, removeBadge = null, reorderBadges = null, setSelectedBadgeCode = null, activate = null, deactivate = null } = useInventoryBadges(); const { isUnseen = null, removeUnseen = null } = useInventoryUnseenTracker(); const { showConfirm = null } = useNotification(); + const [ isDragOverInventory, setIsDragOverInventory ] = useState(false); + const maxSlots = 5; const displayCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes); const attemptDeleteBadge = () => @@ -31,6 +91,58 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props = ); }; + const handleDropOnSlot = useCallback((badgeCode: string, slotIndex: number, sourceSlot?: number) => + { + if(sourceSlot !== undefined) + { + // Reorder within active badges + reorderBadges(sourceSlot, slotIndex); + } + else + { + // Drop from inventory to active slot + setBadgeAtSlot(badgeCode, slotIndex); + } + }, [ setBadgeAtSlot, reorderBadges ]); + + const handleDragStartFromSlot = useCallback((event: React.DragEvent, badgeCode: string, slotIndex: number) => + { + event.dataTransfer.setData('badgeCode', badgeCode); + event.dataTransfer.setData('activeSlot', slotIndex.toString()); + event.dataTransfer.setData('source', 'active'); + event.dataTransfer.effectAllowed = 'move'; + }, []); + + const handleRemoveBadge = useCallback((badgeCode: string) => + { + removeBadge(badgeCode); + }, [ removeBadge ]); + + // Handle drop on inventory area (remove from active) + const onInventoryDragOver = useCallback((event: React.DragEvent) => + { + const source = event.dataTransfer.types.includes('activeslot') ? 'active' : ''; + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + setIsDragOverInventory(true); + }, []); + + const onInventoryDragLeave = useCallback(() => setIsDragOverInventory(false), []); + + const onInventoryDrop = useCallback((event: React.DragEvent) => + { + event.preventDefault(); + setIsDragOverInventory(false); + + const badgeCode = event.dataTransfer.getData('badgeCode'); + const source = event.dataTransfer.getData('source'); + + if(source === 'active' && badgeCode) + { + removeBadge(badgeCode); + } + }, [ removeBadge ]); + useEffect(() => { if(!selectedBadgeCode || !isUnseen(UnseenItemCategory.BADGE, getBadgeId(selectedBadgeCode))) return; @@ -56,7 +168,11 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props = return (
-
+
columnCount={ 5 } estimateSize={ 50 } @@ -66,11 +182,20 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
{ LocalizeText('inventory.badges.activebadges') } - - columnCount={ 3 } - estimateSize={ 50 } - itemRender={ item => } - items={ activeBadgeCodes } /> +
+ { Array.from({ length: maxSlots }).map((_, index) => ( + + )) } +
{ !!selectedBadgeCode &&
diff --git a/src/components/plugins/NitroPluginApi.ts b/src/components/plugins/NitroPluginApi.ts index d52ffa0..5fbbf8b 100644 --- a/src/components/plugins/NitroPluginApi.ts +++ b/src/components/plugins/NitroPluginApi.ts @@ -1,5 +1,5 @@ -import { GetRoomEngine } from '@nitrots/nitro-renderer'; -import { CreateLinkEvent, GetRoomSession, SendMessageComposer } from '../../api'; +import { FurnitureStackHeightComposer, GetRoomEngine, TextureUtils } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent, GetRoomSession, SendMessageComposer, VisitDesktop } from '../../api'; /** * Plugin descriptor registered by external plugin scripts. @@ -39,6 +39,14 @@ export interface INitroPluginApi getRoomSession: () => ReturnType; /** Send a message composer to the server */ sendMessage: typeof SendMessageComposer; + /** Send a chat message to the server (processed as command if starts with ':') */ + sendChat: (text: string, styleId?: number) => void; + /** Send stack height update for a furniture item (objectId, heightInCentimeters) */ + sendStackHeight: (objectId: number, height: number) => void; + /** Take a screenshot of the room and download it as PNG */ + takeScreenshot: () => Promise; + /** Leave the room and go to hotel view */ + visitDesktop: () => void; /** Create a draggable floating window and return its container element */ createWindow: (id: string, title: string, width: number) => HTMLDivElement | null; /** Destroy a floating window by id */ @@ -96,6 +104,50 @@ const pluginApi: INitroPluginApi = { sendMessage: SendMessageComposer, + sendChat(text: string, styleId: number = 0) + { + const session = GetRoomSession(); + if (!session) return; + session.sendChatMessage(text, styleId, ''); + }, + + sendStackHeight(objectId: number, height: number) + { + SendMessageComposer(new FurnitureStackHeightComposer(objectId, height)); + }, + + async takeScreenshot() + { + try + { + const session = GetRoomSession(); + if (!session) return; + + const texture = GetRoomEngine().createTextureFromRoom(session.roomId, 1); + if (!texture) return; + + const imageUrl = await TextureUtils.generateImageUrl(texture); + if (!imageUrl) return; + + // Download the image + const link = document.createElement('a'); + link.href = imageUrl; + link.download = `room_${session.roomId}_${Date.now()}.png`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + catch (e) + { + console.warn('[NitroPlugins] Screenshot failed:', e); + } + }, + + visitDesktop() + { + VisitDesktop(); + }, + createWindow(id: string, title: string, width: number): HTMLDivElement | null { // Remove existing window with same id diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx new file mode 100644 index 0000000..05c54d4 --- /dev/null +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx @@ -0,0 +1,172 @@ +import { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { FaPlus } from 'react-icons/fa'; +import { LayoutBadgeImageView } from '../../../../../common'; +import { useInventoryBadges } from '../../../../../hooks'; + +interface InfoStandBadgeSlotProps +{ + slotIndex: number; + badgeCode?: string; + isOwnUser: boolean; +} + +const BadgeMiniPicker: FC<{ + onSelect: (badgeCode: string) => void; + onClose: () => void; + activeBadgeCodes: string[]; +}> = ({ onSelect, onClose, activeBadgeCodes }) => +{ + const { badgeCodes = [], requestBadges = null } = useInventoryBadges(); + const ref = useRef(null); + const [ search, setSearch ] = useState(''); + + useEffect(() => + { + if(badgeCodes.length === 0) requestBadges(); + }, []); + + const availableBadges = badgeCodes.filter(code => !activeBadgeCodes.includes(code)); + const filtered = search.length > 0 + ? availableBadges.filter(code => code.toLowerCase().includes(search.toLowerCase())) + : availableBadges; + + useEffect(() => + { + const handleClickOutside = (event: MouseEvent) => + { + if(ref.current && !ref.current.contains(event.target as Node)) onClose(); + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [ onClose ]); + + return ( +
e.stopPropagation() }> + setSearch(e.target.value) } + /> + { badgeCodes.length === 0 + ? Caricamento... + : ( +
+ { filtered.slice(0, 40).map(code => ( +
onSelect(code) }> + +
+ )) } + { filtered.length === 0 && ( + Nessun badge + ) } +
+ ) } +
+ ); +}; + +export const InfoStandBadgeSlotView: FC = ({ slotIndex, badgeCode: badgeCodeFromProps, isOwnUser }) => +{ + const { activeBadgeCodes = [], setBadgeAtSlot = null, swapBadges = null } = useInventoryBadges(); + const [ isDragOver, setIsDragOver ] = useState(false); + const [ showPicker, setShowPicker ] = useState(false); + + // For own user, use activeBadgeCodes from the hook (updates immediately on drag/drop) + // For other users, use the badge code from props (from server via avatarInfo) + const badgeCode = isOwnUser ? (activeBadgeCodes[slotIndex] ?? null) : badgeCodeFromProps; + + const onDragStart = useCallback((event: React.DragEvent) => + { + if(!badgeCode || !isOwnUser) return; + event.dataTransfer.setData('badgeCode', badgeCode); + event.dataTransfer.setData('infostandSlot', slotIndex.toString()); + event.dataTransfer.effectAllowed = 'move'; + }, [ badgeCode, slotIndex, isOwnUser ]); + + const onDragOver = useCallback((event: React.DragEvent) => + { + if(!isOwnUser) return; + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + setIsDragOver(true); + }, [ isOwnUser ]); + + const onDragLeave = useCallback(() => setIsDragOver(false), []); + + const onDrop = useCallback((event: React.DragEvent) => + { + event.preventDefault(); + setIsDragOver(false); + if(!isOwnUser) return; + + const droppedBadgeCode = event.dataTransfer.getData('badgeCode'); + const sourceSlotStr = event.dataTransfer.getData('infostandSlot'); + + if(!droppedBadgeCode) return; + + if(sourceSlotStr !== '') + { + // Dragged from another infostand slot -> always swap (works with empty slots too) + const sourceSlot = parseInt(sourceSlotStr); + + if(sourceSlot !== slotIndex) swapBadges(sourceSlot, slotIndex); + } + else + { + // Dragged from inventory or external -> place at this slot + setBadgeAtSlot(droppedBadgeCode, slotIndex); + } + }, [ isOwnUser, slotIndex, swapBadges, setBadgeAtSlot ]); + + const handleSlotClick = useCallback(() => + { + if(!isOwnUser || badgeCode) return; + + setShowPicker(true); + }, [ isOwnUser, badgeCode ]); + + const handlePickerSelect = useCallback((code: string) => + { + setBadgeAtSlot(code, slotIndex); + setShowPicker(false); + }, [ setBadgeAtSlot, slotIndex ]); + + return ( +
+
+ { badgeCode + ? + : isOwnUser && } +
+ { showPicker && ( + setShowPicker(false) } + onSelect={ handlePickerSelect } + /> + ) } +
+ ); +}; diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx index a53e35c..1791979 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx @@ -4,6 +4,7 @@ import { FaPencilAlt, FaTimes } from 'react-icons/fa'; import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api'; import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserProfileIconView } from '../../../../../common'; import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks'; +import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView'; import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView'; import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView'; import { BackgroundsView } from '../../../../backgrounds/BackgroundsView'; @@ -158,31 +159,43 @@ export const InfoStandWidgetUserView: FC = props = /> )} -
-
- {avatarInfo.badges[0] && } -
- 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}> - {avatarInfo.groupId > 0 && - } - -
- -
- {avatarInfo.badges[1] && } -
-
- {avatarInfo.badges[2] && } -
-
- -
- {avatarInfo.badges[3] && } -
-
- {avatarInfo.badges[4] && } -
-
+ { GetConfigurationValue('user.badges.group.slot.enabled', true) + ? ( + <> +
+ + 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}> + {avatarInfo.groupId > 0 && + } + +
+ + + + + + + + + + ) + : ( + <> + + + + + + + + + + + + + + ) + }

diff --git a/src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx b/src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx index 19d382a..5105cf5 100644 --- a/src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx +++ b/src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx @@ -5,6 +5,7 @@ import { FC, useEffect, useState } from 'react'; import { GetConfigurationValue, LocalizeText, SendMessageComposer, SetLocalStorage, TryVisitRoom } from '../../../../api'; import { Text } from '../../../../common'; import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks'; +import { getRegisteredPlugins, INitroPlugin, subscribePlugins } from '../../../plugins/NitroPluginApi'; export const RoomToolsWidgetView: FC<{}> = props => { const [areBubblesMuted, setAreBubblesMuted] = useState(false); @@ -15,12 +16,20 @@ export const RoomToolsWidgetView: FC<{}> = props => { const [isOpen, setIsOpen] = useState(false); const [isOpenHistory, setIsOpenHistory] = useState(false); const [roomHistory, setRoomHistory] = useState<{ roomId: number, roomName: string }[]>([]); + const [plugins, setPlugins] = useState([]); const { navigatorData = null } = useNavigator(); const { roomSession = null } = useRoom(); + // Subscribe to external plugin changes + useEffect(() => + { + setPlugins(getRegisteredPlugins()); + return subscribePlugins(() => setPlugins(getRegisteredPlugins())); + }, []); + const handleToolClick = (action: string, value?: string) => { if (!roomSession) return; - + switch (action) { case 'settings': CreateLinkEvent('navigator/toggle-room-info'); @@ -114,12 +123,20 @@ export const RoomToolsWidgetView: FC<{}> = props => {
handleToolClick('zoom')} />
handleToolClick('chat_history')} />
handleToolClick('hiddenbubbles')} /> - + {navigatorData.canRate && (
handleToolClick('like_room')} /> )}
handleToolClick('toggle_room_link')} />
handleToolClick('room_history')} /> + {plugins.map(plugin => ( +
plugin.onOpen()} + /> + ))}
@@ -159,4 +176,4 @@ export const RoomToolsWidgetView: FC<{}> = props => {
); -}; \ No newline at end of file +}; diff --git a/src/hooks/inventory/useInventoryBadges.ts b/src/hooks/inventory/useInventoryBadges.ts index aebe155..39e0667 100644 --- a/src/hooks/inventory/useInventoryBadges.ts +++ b/src/hooks/inventory/useInventoryBadges.ts @@ -1,5 +1,5 @@ import { BadgeReceivedEvent, BadgesEvent, RequestBadgesComposer, SetActivatedBadgesComposer } from '@nitrots/nitro-renderer'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useBetween } from 'use-between'; import { GetConfigurationValue, SendMessageComposer, UnseenItemCategory } from '../../api'; import { useMessageEvent } from '../events'; @@ -17,9 +17,18 @@ const useInventoryBadgesState = () => const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker(); const maxBadgeCount = GetConfigurationValue('user.badges.max.slots', 5); + const localChangeRef = useRef(false); const isWearingBadge = (badgeCode: string) => (activeBadgeCodes.indexOf(badgeCode) >= 0); const canWearBadges = () => (activeBadgeCodes.length < maxBadgeCount); + const sendActiveBadges = (badges: string[]) => + { + localChangeRef.current = true; + const composer = new SetActivatedBadgesComposer(); + for(let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(badges[i] ?? ''); + SendMessageComposer(composer); + }; + const toggleBadge = (badgeCode: string) => { setActiveBadgeCodes(prevValue => @@ -30,7 +39,7 @@ const useInventoryBadgesState = () => if(index === -1) { - if(!canWearBadges()) return prevValue; + if(newValue.length >= maxBadgeCount) return prevValue; newValue.push(badgeCode); } @@ -39,11 +48,7 @@ const useInventoryBadgesState = () => newValue.splice(index, 1); } - const composer = new SetActivatedBadgesComposer(); - - for(let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(newValue[i] ?? ''); - - SendMessageComposer(composer); + sendActiveBadges(newValue); return newValue; }); @@ -77,7 +82,16 @@ const useInventoryBadgesState = () => return newValue; }); - setActiveBadgeCodes(parser.getActiveBadgeCodes()); + // Skip overwriting activeBadgeCodes if we recently made a local change + if(localChangeRef.current) + { + localChangeRef.current = false; + } + else + { + setActiveBadgeCodes(parser.getActiveBadgeCodes()); + } + setBadgeCodes(allBadgeCodes); }); @@ -141,7 +155,83 @@ const useInventoryBadgesState = () => setNeedsUpdate(false); }, [ isVisible, needsUpdate ]); - return { badgeCodes, activeBadgeCodes, selectedBadgeCode, setSelectedBadgeCode, isWearingBadge, canWearBadges, toggleBadge, getBadgeId, activate, deactivate }; + const setBadgeAtSlot = (badgeCode: string, slotIndex: number) => + { + setActiveBadgeCodes(prevValue => + { + // Build a fixed-size array of maxBadgeCount slots + const slots: (string | null)[] = Array.from({ length: maxBadgeCount }, (_, i) => prevValue[i] ?? null); + + // Remove badge if already in another slot + const existingIndex = slots.indexOf(badgeCode); + if(existingIndex >= 0) slots[existingIndex] = null; + + // Place badge at target slot + slots[slotIndex] = badgeCode; + + // Compact: remove nulls, keep order + const result = slots.filter(Boolean) as string[]; + + sendActiveBadges(result); + return result; + }); + }; + + const removeBadge = (badgeCode: string) => + { + setActiveBadgeCodes(prevValue => + { + const result = prevValue.filter(code => code !== badgeCode); + + sendActiveBadges(result); + return result; + }); + }; + + const reorderBadges = (fromIndex: number, toIndex: number) => + { + setActiveBadgeCodes(prevValue => + { + if(fromIndex === toIndex) return prevValue; + if(fromIndex >= prevValue.length) return prevValue; + + const newValue = [ ...prevValue ]; + const [ moved ] = newValue.splice(fromIndex, 1); + newValue.splice(toIndex, 0, moved); + + sendActiveBadges(newValue); + return newValue; + }); + }; + + const swapBadges = (fromIndex: number, toIndex: number) => + { + setActiveBadgeCodes(prevValue => + { + if(fromIndex === toIndex) return prevValue; + + // Build fixed-size array so swap works even with empty slots + const slots: (string | null)[] = Array.from({ length: maxBadgeCount }, (_, i) => prevValue[i] ?? null); + + // Swap the two slots + const temp = slots[fromIndex]; + slots[fromIndex] = slots[toIndex]; + slots[toIndex] = temp; + + // Compact: remove nulls, keep order + const result = slots.filter(Boolean) as string[]; + + sendActiveBadges(result); + return result; + }); + }; + + const requestBadges = () => + { + SendMessageComposer(new RequestBadgesComposer()); + }; + + return { badgeCodes, activeBadgeCodes, selectedBadgeCode, setSelectedBadgeCode, isWearingBadge, canWearBadges, toggleBadge, getBadgeId, setBadgeAtSlot, removeBadge, reorderBadges, swapBadges, requestBadges, activate, deactivate }; }; export const useInventoryBadges = () => useBetween(useInventoryBadgesState); diff --git a/src/hooks/rooms/widgets/useChatInputWidget.ts b/src/hooks/rooms/widgets/useChatInputWidget.ts index 3f32d3f..b21efab 100644 --- a/src/hooks/rooms/widgets/useChatInputWidget.ts +++ b/src/hooks/rooms/widgets/useChatInputWidget.ts @@ -116,12 +116,22 @@ const useChatInputWidgetState = () => (async () => { - const image = new Image(); + try + { + const imageUrl = await TextureUtils.generateImageUrl(texture); + if (!imageUrl) return; - image.src = await TextureUtils.generateImageUrl(texture); - - const newWindow = window.open(''); - newWindow.document.write(image.outerHTML); + const link = document.createElement('a'); + link.href = imageUrl; + link.download = `room_${ roomSession.roomId }_${ Date.now() }.png`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + catch (e) + { + console.warn('[Screenshot] Failed:', e); + } })(); return null; case ':pickall': From a87bb16e5ae823a79c6250ef398bcfcf422b3b41 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 15 Mar 2026 21:41:16 +0100 Subject: [PATCH 06/33] Fix badge slots showing empty on room enter Use avatarInfo badges as fallback when hook data not yet loaded Co-Authored-By: medievalshell --- .../avatar-info/infostand/InfoStandBadgeSlotView.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx index 05c54d4..951df53 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx @@ -81,9 +81,10 @@ export const InfoStandBadgeSlotView: FC = ({ slotIndex, const [ isDragOver, setIsDragOver ] = useState(false); const [ showPicker, setShowPicker ] = useState(false); - // For own user, use activeBadgeCodes from the hook (updates immediately on drag/drop) - // For other users, use the badge code from props (from server via avatarInfo) - const badgeCode = isOwnUser ? (activeBadgeCodes[slotIndex] ?? null) : badgeCodeFromProps; + // For own user: use hook data if loaded, otherwise fall back to props (avatarInfo) + // For other users: always use props + const hookBadge = activeBadgeCodes.length > 0 ? (activeBadgeCodes[slotIndex] ?? null) : null; + const badgeCode = isOwnUser ? (hookBadge ?? badgeCodeFromProps ?? null) : (badgeCodeFromProps ?? null); const onDragStart = useCallback((event: React.DragEvent) => { From d90e6591322a43583e6c242d1ed3da64ac91d228 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 15 Mar 2026 21:47:37 +0100 Subject: [PATCH 07/33] Revamp user profile with tabbed layout (Badge, Amici, Stanze, Gruppi) - Replace grid layout with NitroCard.Tabs for Badge, Amici, Stanze, Gruppi - Add BadgeInfoView component with hover tooltip showing badge name/description - Add Stanze tab that fetches and lists user rooms via NavigatorSearchComposer - Bold username in profile header - Badge tab with styled grid slots and empty state - Amici tab with loading state - Gruppi tab with full GroupsContainerView --- src/components/user-profile/BadgeInfoView.tsx | 31 ++++ .../user-profile/UserContainerView.tsx | 2 +- .../user-profile/UserProfileView.tsx | 139 +++++++++++++++--- 3 files changed, 148 insertions(+), 24 deletions(-) create mode 100644 src/components/user-profile/BadgeInfoView.tsx diff --git a/src/components/user-profile/BadgeInfoView.tsx b/src/components/user-profile/BadgeInfoView.tsx new file mode 100644 index 0000000..a06c11e --- /dev/null +++ b/src/components/user-profile/BadgeInfoView.tsx @@ -0,0 +1,31 @@ +import { FC, useState } from 'react'; +import { LocalizeBadgeDescription, LocalizeBadgeName } from '../../api'; +import { Flex, LayoutBadgeImageView } from '../../common'; + +interface BadgeInfoViewProps +{ + badgeCode: string; +} + +export const BadgeInfoView: FC = props => +{ + const { badgeCode } = props; + const [ isHovered, setIsHovered ] = useState(false); + + return ( + setIsHovered(true) } + onMouseLeave={ () => setIsHovered(false) } + > + + { isHovered && ( +
+
+
{ LocalizeBadgeName(badgeCode) }
+
{ LocalizeBadgeDescription(badgeCode) }
+
+ ) } + + ); +}; diff --git a/src/components/user-profile/UserContainerView.tsx b/src/components/user-profile/UserContainerView.tsx index d6e3fb9..ed357b0 100644 --- a/src/components/user-profile/UserContainerView.tsx +++ b/src/components/user-profile/UserContainerView.tsx @@ -37,7 +37,7 @@ export const UserContainerView: FC<{
-

{ userProfile.username }

+

{ userProfile.username }

{ userProfile.motto }

diff --git a/src/components/user-profile/UserProfileView.tsx b/src/components/user-profile/UserProfileView.tsx index f57612b..20fc0e7 100644 --- a/src/components/user-profile/UserProfileView.tsx +++ b/src/components/user-profile/UserProfileView.tsx @@ -1,24 +1,31 @@ -import { CreateLinkEvent, ExtendedProfileChangedMessageEvent, GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomEngineObjectEvent, RoomObjectCategory, RoomObjectType, UserCurrentBadgesComposer, UserCurrentBadgesEvent, UserProfileEvent, UserProfileParser, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; +import { ExtendedProfileChangedMessageEvent, GetSessionDataManager, NavigatorSearchComposer, NavigatorSearchEvent, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomDataParser, RoomEngineObjectEvent, RoomObjectCategory, RoomObjectType, UserCurrentBadgesComposer, UserCurrentBadgesEvent, UserProfileEvent, UserProfileParser, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; import { FC, useState } from 'react'; -import { GetRoomSession, GetUserProfile, LocalizeText, SendMessageComposer } from '../../api'; -import { Flex, Grid, LayoutBadgeImageView, Text } from '../../common'; +import { CreateRoomSession, GetRoomSession, GetUserProfile, LocalizeText, SendMessageComposer } from '../../api'; +import { Flex, Text } from '../../common'; +import { BadgeInfoView } from './BadgeInfoView'; import { useMessageEvent, useNitroEvent } from '../../hooks'; import { NitroCard } from '../../layout'; import { FriendsContainerView } from './FriendsContainerView'; import { GroupsContainerView } from './GroupsContainerView'; import { UserContainerView } from './UserContainerView'; +type ProfileTab = 'badge' | 'amici' | 'stanze' | 'gruppi'; + export const UserProfileView: FC<{}> = props => { const [ userProfile, setUserProfile ] = useState(null); const [ userBadges, setUserBadges ] = useState([]); const [ userRelationships, setUserRelationships ] = useState(null); + const [ activeTab, setActiveTab ] = useState('badge'); + const [ userRooms, setUserRooms ] = useState(null); const onClose = () => { setUserProfile(null); setUserBadges([]); setUserRelationships(null); + setActiveTab('badge'); + setUserRooms(null); }; const onLeaveGroup = () => @@ -28,6 +35,16 @@ export const UserProfileView: FC<{}> = props => GetUserProfile(userProfile.id); }; + const onTabClick = (tab: ProfileTab) => + { + setActiveTab(tab); + + if(tab === 'stanze' && !userRooms && userProfile) + { + SendMessageComposer(new NavigatorSearchComposer('hotel_view', `owner:${ userProfile.username }`)); + } + }; + useMessageEvent(UserCurrentBadgesEvent, event => { const parser = event.getParser(); @@ -63,6 +80,8 @@ export const UserProfileView: FC<{}> = props => { setUserBadges([]); setUserRelationships(null); + setActiveTab('badge'); + setUserRooms(null); } SendMessageComposer(new UserCurrentBadgesComposer(parser.id)); @@ -78,6 +97,28 @@ export const UserProfileView: FC<{}> = props => GetUserProfile(parser.userId); }); + useMessageEvent(NavigatorSearchEvent, event => + { + if(!userProfile || activeTab !== 'stanze') return; + + const parser = event.getParser(); + const result = parser.result; + + if(!result) return; + + const rooms: RoomDataParser[] = []; + + for(const resultList of result.results) + { + if(resultList.rooms && resultList.rooms.length) + { + for(const room of resultList.rooms) rooms.push(room); + } + } + + setUserRooms(rooms); + }); + useNitroEvent(RoomEngineObjectEvent.SELECTED, event => { if(!userProfile) return; @@ -98,27 +139,79 @@ export const UserProfileView: FC<{}> = props => - - -
- -
- { userBadges && (userBadges.length > 0) && userBadges.map((badge, index) => ) } + +
+ +
+ + onTabClick('badge') }> + Badge + + onTabClick('amici') }> + Amici + + onTabClick('stanze') }> + Stanze + + onTabClick('gruppi') }> + Gruppi + + +
+ { activeTab === 'badge' && ( +
+ { userBadges && (userBadges.length > 0) + ? userBadges.map((badge, index) => ( + + )) + : ( + + Nessun badge da mostrare + + ) + }
-
-
- { userRelationships && - } -
- - - CreateLinkEvent(`navigator/search/hotel_view/owner:${ userProfile.username }`) }> - - { LocalizeText('extendedprofile.rooms') } - - - + ) } + { activeTab === 'amici' && ( +
+ { userRelationships ? ( + + ) : ( + + Caricamento... + + ) } +
+ ) } + { activeTab === 'stanze' && ( +
+ { !userRooms && ( + + Caricamento stanze... + + ) } + { userRooms && userRooms.length === 0 && ( + + Nessuna stanza trovata + + ) } + { userRooms && userRooms.length > 0 && userRooms.map(room => ( + CreateRoomSession(room.roomId) }> +
+ { room.roomName } + { room.description && { room.description } } +
+ { room.userCount }/{ room.maxUserCount } +
+ )) } +
+ ) } + { activeTab === 'gruppi' && ( +
+ +
+ ) } +
); From 872fa79bd33352920ad922272ff7741c577df2aa Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 16 Mar 2026 18:16:20 +0100 Subject: [PATCH 08/33] Add pathfinder underpass height config --- public/ui-config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/public/ui-config.json b/public/ui-config.json index 9692188..04b4a20 100644 --- a/public/ui-config.json +++ b/public/ui-config.json @@ -11,6 +11,7 @@ "group.homepage.url": "${url.prefix}/groups/%groupid%/id", "guide.help.alpha.groupid": 0, "chat.viewer.height.percentage": 0.4, + "pathfinder.underpass.height": 1.5, "widget.dimmer.colorwheel": false, "avatar.wardrobe.max.slots": 10, "user.badges.max.slots": 6, From a3a033748200fb9e959d95cf3b712c7e569a4368 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 16 Mar 2026 18:20:07 +0100 Subject: [PATCH 09/33] Remove .claude folder from tracking and add to gitignore --- .claude/settings.local.json | 43 ------------------------------------- .gitignore | 1 + 2 files changed, 1 insertion(+), 43 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 04ffefa..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(cp -r nitro3_temp/* nitro3/)", - "Bash(cp -r nitro3_temp/.* nitro3/)", - "Bash(rm -rf nitro3_temp)", - "Bash(yarn install:*)", - "Bash(find /E/www/habbo-next/public/nitro3 -type f -iname *furni*editor*)", - "Bash(mkdir -p /e/www/habbo-next/public/nitro3/src/components/furni-editor/views)", - "Bash(mkdir -p /e/www/habbo-next/public/nitro3/src/hooks/furni-editor)", - "Bash(find /e/www/habbo-next/src -type f \\\\\\(-name *client* \\\\\\))", - "Bash(grep -E \"\\\\.\\(tsx|ts|jsx|js\\)$\")", - "Bash(ls -la /e/www/habbo-next/src/app/[locale]/\\\\\\(client\\\\\\)/)", - "Bash(grep -r \"NITRO_URL\\\\|/client\" /e/www/habbo-next/.env*)", - "Bash(grep -r \"NEXT\\\\|API_URL\\\\|BASE_URL\\\\|apiUrl\" /e/www/habbo-next/public/nitro3/src/ --include=*.ts --include=*.tsx -l)", - "Bash(find /e/www/habbo-next/src/lib -name *catalog* -o -name *furni*)", - "Bash(npx tsc:*)", - "Bash(npx next:*)", - "Bash(find /e/www/habbo-next -name *.prisma -o -name schema.prisma)", - "Bash(pushd E:/www/habbo-next)", - "Bash(popd)", - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(git push:*)", - "Bash(find /e/www/habbo-next/src/app -type f -path *catalog*)", - "Bash(echo \"EXIT:$?\")", - "Bash(find /E/www/habbo-next/src -type f -name *prisma*)", - "Bash(node -e \"const mysql = require\\(''mysql2/promise''\\); \\(async\\(\\)=>{const c=await mysql.createConnection\\({host:''localhost'',user:''root'',database:''habbo''}\\); const [r]=await c.query\\(''DESCRIBE items_base''\\); console.log\\(r.map\\(x=>x.Field\\).join\\(''\\\\n''\\)\\); await c.end\\(\\);}\\)\\(\\)\")", - "Bash(node -e \"const mysql = require\\(''mysql2/promise''\\); \\(async\\(\\)=>{const c=await mysql.createConnection\\({host:''localhost'',user:''habbo'',password:''habbo'',database:''next''}\\); const [r]=await c.query\\(''DESCRIBE items_base''\\); console.log\\(r.map\\(x=>x.Field\\).join\\(''\\\\n''\\)\\); const [r2]=await c.query\\(''DESCRIBE catalog_items''\\); console.log\\(''---''\\); console.log\\(r2.map\\(x=>x.Field\\).join\\(''\\\\n''\\)\\); await c.end\\(\\);}\\)\\(\\)\")", - "Bash(node -e \"const mysql = require\\(''mysql2/promise''\\); \\(async\\(\\)=>{const c=await mysql.createConnection\\({host:''localhost'',user:''habbo'',password:''habbo'',database:''next''}\\); const [r]=await c.query\\(''DESCRIBE items_base''\\); const cols=r.map\\(x=>x.Field\\); console.log\\(''items_base columns:'', JSON.stringify\\(cols\\)\\); const [r2]=await c.query\\(''DESCRIBE catalog_items''\\); const cols2=r2.map\\(x=>x.Field\\); console.log\\(''catalog_items columns:'', JSON.stringify\\(cols2\\)\\); await c.end\\(\\);}\\)\\(\\)\")", - "Bash(node -e \":*)", - "Bash(npx prisma:*)", - "WebFetch(domain:www.habbo.it)", - "Bash(grep -r \"slider\\\\|height\\\\|rotation\\\\|state\\\\|speed\" /e/www/habbo-next/public/nitro3/src/components/room/widgets/furniture/*.tsx)", - "Bash(grep -r \"processAction\\\\|handleAction\\\\|dispatch\" /e/www/habbo-next/public/nitro3/src/components/room/widgets/furniture/*.tsx)", - "Bash(xargs ls:*)", - "Bash(find /e/www/habbo-next/public/nitro3/src -type f \\\\\\(-name *Modif* -o -name *Manip* -o -name *Floorplan* -o -name *Builder* \\\\\\))", - "Bash(mkdir -p \"E:/www/habbo-next/public/nitro3/src/api/plugins\")", - "Bash(mkdir -p \"E:/www/habbo-next/public/nitro3/src/components/plugins/room-builder\")", - "Bash(ls \"E:/www/habbo-next/public/nitro3/vite.config\"*)" - ] - } -} diff --git a/.gitignore b/.gitignore index ae96caa..249e7db 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ Thumbs.db /build *.zip .env +.claude/ From 10ce76c0bb40cb94d338d76033851db64badf144 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 16 Mar 2026 21:07:08 +0100 Subject: [PATCH 10/33] Fix inventory badge slots not reading from config Use maxBadgeCount from hook (ui-config) instead of hardcoded 5 Co-Authored-By: medievalshell --- src/components/inventory/views/badge/InventoryBadgeView.tsx | 4 ++-- src/hooks/inventory/useInventoryBadges.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/inventory/views/badge/InventoryBadgeView.tsx b/src/components/inventory/views/badge/InventoryBadgeView.tsx index 1bf6d13..c0bd663 100644 --- a/src/components/inventory/views/badge/InventoryBadgeView.tsx +++ b/src/components/inventory/views/badge/InventoryBadgeView.tsx @@ -69,12 +69,12 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props = { const { filteredBadgeCodes = null } = props; const [ isVisible, setIsVisible ] = useState(false); - const { badgeCodes = [], activeBadgeCodes = [], selectedBadgeCode = null, isWearingBadge = null, canWearBadges = null, toggleBadge = null, getBadgeId = null, setBadgeAtSlot = null, removeBadge = null, reorderBadges = null, setSelectedBadgeCode = null, activate = null, deactivate = null } = useInventoryBadges(); + const { badgeCodes = [], activeBadgeCodes = [], selectedBadgeCode = null, isWearingBadge = null, canWearBadges = null, toggleBadge = null, getBadgeId = null, setBadgeAtSlot = null, removeBadge = null, reorderBadges = null, setSelectedBadgeCode = null, maxBadgeCount = 5, activate = null, deactivate = null } = useInventoryBadges(); const { isUnseen = null, removeUnseen = null } = useInventoryUnseenTracker(); const { showConfirm = null } = useNotification(); const [ isDragOverInventory, setIsDragOverInventory ] = useState(false); - const maxSlots = 5; + const maxSlots = maxBadgeCount; const displayCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes); const attemptDeleteBadge = () => diff --git a/src/hooks/inventory/useInventoryBadges.ts b/src/hooks/inventory/useInventoryBadges.ts index 39e0667..9d2e4d0 100644 --- a/src/hooks/inventory/useInventoryBadges.ts +++ b/src/hooks/inventory/useInventoryBadges.ts @@ -231,7 +231,7 @@ const useInventoryBadgesState = () => SendMessageComposer(new RequestBadgesComposer()); }; - return { badgeCodes, activeBadgeCodes, selectedBadgeCode, setSelectedBadgeCode, isWearingBadge, canWearBadges, toggleBadge, getBadgeId, setBadgeAtSlot, removeBadge, reorderBadges, swapBadges, requestBadges, activate, deactivate }; + return { badgeCodes, activeBadgeCodes, selectedBadgeCode, setSelectedBadgeCode, isWearingBadge, canWearBadges, toggleBadge, getBadgeId, setBadgeAtSlot, removeBadge, reorderBadges, swapBadges, requestBadges, maxBadgeCount, activate, deactivate }; }; export const useInventoryBadges = () => useBetween(useInventoryBadgesState); From 23e6b08e0651e3985cbc958e9b20cfc99a73c56d Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 16 Mar 2026 22:09:52 +0100 Subject: [PATCH 11/33] Add real-time 3D preview to floor plan editor Redesign the floor plan editor with side-by-side layout featuring: - Real-time isometric 3D preview that updates as tiles are drawn - Vertical height gradient selector with COLORMAP colors - Area counter showing total and walkable tile counts - Zoom controls (+/-) on the 2D canvas - Simplified single-row toolbar - Wall height control in the preview panel Co-Authored-By: medievalshell --- .../FloorplanEditorContext.tsx | 14 +- .../floorplan-editor/FloorplanEditorView.tsx | 156 ++++++++- .../views/FloorplanCanvasView.tsx | 105 +++--- .../views/FloorplanHeightSelector.tsx | 54 +++ .../views/FloorplanOptionsView.tsx | 247 ++++--------- .../views/FloorplanPreviewView.tsx | 328 ++++++++++++++++++ 6 files changed, 647 insertions(+), 257 deletions(-) create mode 100644 src/components/floorplan-editor/views/FloorplanHeightSelector.tsx create mode 100644 src/components/floorplan-editor/views/FloorplanPreviewView.tsx diff --git a/src/components/floorplan-editor/FloorplanEditorContext.tsx b/src/components/floorplan-editor/FloorplanEditorContext.tsx index eb528cf..1b2a3c4 100644 --- a/src/components/floorplan-editor/FloorplanEditorContext.tsx +++ b/src/components/floorplan-editor/FloorplanEditorContext.tsx @@ -8,13 +8,25 @@ interface IFloorplanEditorContext setOriginalFloorplanSettings: Dispatch>; visualizationSettings: IVisualizationSettings; setVisualizationSettings: Dispatch>; + floorHeight: number; + setFloorHeight: Dispatch>; + floorAction: number; + setFloorAction: Dispatch>; + tilemapVersion: number; + areaInfo: { total: number; walkable: number }; } const FloorplanEditorContext = createContext({ originalFloorplanSettings: null, setOriginalFloorplanSettings: null, visualizationSettings: null, - setVisualizationSettings: null + setVisualizationSettings: null, + floorHeight: 0, + setFloorHeight: null, + floorAction: 3, + setFloorAction: null, + tilemapVersion: 0, + areaInfo: { total: 0, walkable: 0 } }); export const FloorplanEditorContextProvider: FC> = props => ; diff --git a/src/components/floorplan-editor/FloorplanEditorView.tsx b/src/components/floorplan-editor/FloorplanEditorView.tsx index 59f7709..4003d13 100644 --- a/src/components/floorplan-editor/FloorplanEditorView.tsx +++ b/src/components/floorplan-editor/FloorplanEditorView.tsx @@ -1,19 +1,22 @@ import { AddLinkEventTracker, FloorHeightMapEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { FaCaretLeft, FaCaretRight } from 'react-icons/fa'; import { LocalizeText, SendMessageComposer } from '../../api'; -import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; +import { Button, ButtonGroup, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { useMessageEvent, useNitroEvent } from '../../hooks'; import { FloorplanEditorContextProvider } from './FloorplanEditorContext'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; import { IFloorplanSettings } from '@nitrots/nitro-renderer'; import { IVisualizationSettings } from '@nitrots/nitro-renderer'; -import { convertNumbersForSaving, convertSettingToNumber } from '@nitrots/nitro-renderer'; +import { convertNumbersForSaving, convertSettingToNumber, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; import { FloorplanCanvasView } from './views/FloorplanCanvasView'; import { FloorplanImportExportView } from './views/FloorplanImportExportView'; import { FloorplanOptionsView } from './views/FloorplanOptionsView'; +import { FloorplanHeightSelector } from './views/FloorplanHeightSelector'; +import { FloorplanPreviewView } from './views/FloorplanPreviewView'; - -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; +const MIN_WALL_HEIGHT = 0; +const MAX_WALL_HEIGHT = 16; export const FloorplanEditorView: FC<{}> = props => { @@ -34,7 +37,65 @@ export const FloorplanEditorView: FC<{}> = props => thicknessWall: 1, thicknessFloor: 1 }); - const [ canvasScrollHandler, setCanvasScrollHandler ] = useState<((direction: ScrollDirection) => void) | null>(null); + const [ floorHeight, setFloorHeight ] = useState(0); + const [ floorAction, setFloorAction ] = useState(FloorAction.SET); + const [ tilemapVersion, setTilemapVersion ] = useState(0); + const [ areaInfo, setAreaInfo ] = useState({ total: 0, walkable: 0 }); + + const calculateArea = useCallback(() => + { + const tilemap = FloorplanEditor.instance.tilemap; + + if(!tilemap || tilemap.length === 0) + { + setAreaInfo({ total: 0, walkable: 0 }); + + return; + } + + let total = 0; + let walkable = 0; + + for(let y = 0; y < tilemap.length; y++) + { + if(!tilemap[y]) continue; + + for(let x = 0; x < tilemap[y].length; x++) + { + if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + total++; + + if(!tilemap[y][x].isBlocked) walkable++; + } + } + + setAreaInfo({ total, walkable }); + }, []); + + // sync floorHeight/floorAction changes to the FloorplanEditor instance + useEffect(() => + { + FloorplanEditor.instance.actionSettings.currentAction = floorAction; + FloorplanEditor.instance.actionSettings.currentHeight = floorHeight.toString(36); + }, [ floorHeight, floorAction ]); + + // register onTilemapChange callback + useEffect(() => + { + if(!isVisible) return; + + FloorplanEditor.instance.onTilemapChange = () => + { + setTilemapVersion(prev => prev + 1); + calculateArea(); + }; + + return () => + { + FloorplanEditor.instance.onTilemapChange = null; + }; + }, [ isVisible, calculateArea ]); const saveFloorChanges = () => { @@ -47,16 +108,50 @@ export const FloorplanEditorView: FC<{}> = props => convertNumbersForSaving(visualizationSettings.thicknessFloor), (visualizationSettings.wallHeight - 1) )); - } + }; const revertChanges = () => { setVisualizationSettings({ wallHeight: originalFloorplanSettings.wallHeight, thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: originalFloorplanSettings.entryPointDir }); - + FloorplanEditor.instance.doorLocation = { x: originalFloorplanSettings.entryPoint[0], y: originalFloorplanSettings.entryPoint[1] }; FloorplanEditor.instance.setTilemap(originalFloorplanSettings.tilemap, originalFloorplanSettings.reservedTiles); FloorplanEditor.instance.renderTiles(); - } + }; + + const onWallHeightChange = (value: number) => + { + if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT; + + if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT; + + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; + + newValue.wallHeight = value; + + return newValue; + }); + }; + + const increaseWallHeight = () => + { + let height = (visualizationSettings.wallHeight + 1); + + if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT; + + onWallHeightChange(height); + }; + + const decreaseWallHeight = () => + { + let height = (visualizationSettings.wallHeight - 1); + + if(height <= 0) height = MIN_WALL_HEIGHT; + + onWallHeightChange(height); + }; useNitroEvent(RoomEngineEvent.DISPOSED, event => setIsVisible(false)); @@ -117,7 +212,7 @@ export const FloorplanEditorView: FC<{}> = props => const parts = url.split('/'); if(parts.length < 2) return; - + switch(parts[1]) { case 'show': @@ -140,17 +235,42 @@ export const FloorplanEditorView: FC<{}> = props => }, []); return ( - + { isVisible && - + setIsVisible(false) } /> - - canvasScrollHandler && canvasScrollHandler(direction) } /> - + + + + + + + + + { LocalizeText('floor.editor.wall.height') } + + onWallHeightChange(event.target.valueAsNumber) } /> + + + + Area: { areaInfo.total } ({ areaInfo.walkable } caselle) + + + - + - @@ -161,4 +281,4 @@ export const FloorplanEditorView: FC<{}> = props => setImportExportVisible(false) } /> } ); -} +}; diff --git a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx index e8f39a8..9db0903 100644 --- a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx +++ b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx @@ -1,25 +1,25 @@ import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useRef, useState } from 'react'; +import { FaPlus, FaMinus } from 'react-icons/fa'; import { SendMessageComposer } from '../../../api'; import { Base, Column, ColumnProps } from '../../../common'; import { useMessageEvent } from '../../../hooks'; import { useFloorplanEditorContext } from '../FloorplanEditorContext'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; - interface FloorplanCanvasViewProps extends ColumnProps { - setScrollHandler(handler: ((direction: ScrollDirection) => void) | null): void; } export const FloorplanCanvasView: FC = props => { - const { gap = 1, children = null, setScrollHandler = null, ...rest } = props; - const [ occupiedTilesReceived , setOccupiedTilesReceived ] = useState(false); + const { gap = 1, children = null, ...rest } = props; + const [ occupiedTilesReceived, setOccupiedTilesReceived ] = useState(false); const [ entryTileReceived, setEntryTileReceived ] = useState(false); + const [ zoomLevel, setZoomLevel ] = useState(1.0); const { originalFloorplanSettings = null, setOriginalFloorplanSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); const elementRef = useRef(null); + const canvasWrapperRef = useRef(null); useMessageEvent(RoomOccupiedTilesMessageEvent, event => { @@ -37,7 +37,7 @@ export const FloorplanCanvasView: FC = props => }); setOccupiedTilesReceived(true); - + elementRef.current.scrollTo((FloorplanEditor.instance.renderer.canvas.width / 3), 0); }); @@ -63,39 +63,16 @@ export const FloorplanCanvasView: FC = props => return newValue; }); - + FloorplanEditor.instance.doorLocation = { x: parser.x, y: parser.y }; setEntryTileReceived(true); }); - const onClickArrowButton = (scrollDirection: ScrollDirection) => - { - const element = elementRef.current; - - if(!element) return; - - switch(scrollDirection) - { - case 'up': - element.scrollBy({ top: -10 }); - break; - case 'down': - element.scrollBy({ top: 10 }); - break; - case 'left': - element.scrollBy({ left: -10 }); - break; - case 'right': - element.scrollBy({ left: 10 }); - break; - } - } - const onPointerEvent = (event: PointerEvent) => { event.preventDefault(); - + switch(event.type) { case 'pointerout': @@ -109,7 +86,10 @@ export const FloorplanCanvasView: FC = props => FloorplanEditor.instance.onPointerMove(event); break; } - } + }; + + const zoomIn = () => setZoomLevel(prev => Math.min(prev + 0.25, 2.0)); + const zoomOut = () => setZoomLevel(prev => Math.max(prev - 0.25, 0.5)); useEffect(() => { @@ -124,15 +104,15 @@ export const FloorplanCanvasView: FC = props => thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: prevValue.entryPointDir - } + }; }); - } + }; }, [ originalFloorplanSettings.thicknessFloor, originalFloorplanSettings.thicknessWall, originalFloorplanSettings.wallHeight, setVisualizationSettings ]); useEffect(() => { if(!entryTileReceived || !occupiedTilesReceived) return; - + FloorplanEditor.instance.renderTiles(); }, [ entryTileReceived, occupiedTilesReceived ]); @@ -144,45 +124,56 @@ export const FloorplanCanvasView: FC = props => const currentElement = elementRef.current; if(!currentElement) return; - - currentElement.appendChild(FloorplanEditor.instance.renderer.canvas); + + const wrapper = canvasWrapperRef.current; + + if(wrapper) wrapper.appendChild(FloorplanEditor.instance.renderer.canvas); currentElement.addEventListener('pointerup', onPointerEvent); - currentElement.addEventListener('pointerout', onPointerEvent); - currentElement.addEventListener('pointerdown', onPointerEvent); - currentElement.addEventListener('pointermove', onPointerEvent); - return () => + return () => { if(currentElement) { currentElement.removeEventListener('pointerup', onPointerEvent); - currentElement.removeEventListener('pointerout', onPointerEvent); - currentElement.removeEventListener('pointerdown', onPointerEvent); - currentElement.removeEventListener('pointermove', onPointerEvent); } - } + }; }, []); - useEffect(() => - { - if(!setScrollHandler) return; - - setScrollHandler(() => onClickArrowButton); - - return () => setScrollHandler(null); - }, [ setScrollHandler ]); - return ( - - + + +
+ +
+ + +
{ children } ); -} +}; diff --git a/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx b/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx new file mode 100644 index 0000000..8163c98 --- /dev/null +++ b/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react'; +import { COLORMAP, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; +import { FloorplanEditor } from '@nitrots/nitro-renderer'; +import { Column, Text } from '../../../common'; +import { useFloorplanEditorContext } from '../FloorplanEditorContext'; + +const colormap = COLORMAP as Record; + +export const FloorplanHeightSelector: FC<{}> = () => +{ + const { floorHeight, setFloorHeight, setFloorAction } = useFloorplanEditorContext(); + + const onSelectHeight = (height: number) => + { + setFloorHeight(height); + setFloorAction(FloorAction.SET); + + FloorplanEditor.instance.actionSettings.currentAction = FloorAction.SET; + FloorplanEditor.instance.actionSettings.currentHeight = height.toString(36); + }; + + const heights: number[] = []; + + for(let i = 26; i >= 0; i--) heights.push(i); + + return ( + + { floorHeight } +
+ { heights.map(h => + { + const char = HEIGHT_SCHEME[h + 1]; + const color = colormap[char] || '101010'; + const isActive = (floorHeight === h); + + return ( +
onSelectHeight(h) } + title={ `${ h }` } + /> + ); + }) } +
+ + ); +}; diff --git a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx index 5207b15..d4e7705 100644 --- a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx +++ b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx @@ -1,45 +1,32 @@ -import { FC, useState } from 'react'; -import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaCaretLeft, FaCaretRight } from 'react-icons/fa'; +import { FC } from 'react'; import { LocalizeText } from '../../../api'; -import { Button, Column, Flex, LayoutGridItem, Slider, Text } from '../../../common'; -import { COLORMAP, FloorAction } from '@nitrots/nitro-renderer'; +import { Flex, LayoutGridItem, Text } from '../../../common'; +import { FloorAction } from '@nitrots/nitro-renderer'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; import { useFloorplanEditorContext } from '../FloorplanEditorContext'; -const MIN_WALL_HEIGHT: number = 0; -const MAX_WALL_HEIGHT: number = 16; - -const MIN_FLOOR_HEIGHT: number = 0; -const MAX_FLOOR_HEIGHT: number = 26; - -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; - interface FloorplanOptionsViewProps { - onCanvasScroll?(direction: ScrollDirection): void; } export const FloorplanOptionsView: FC = props => { - const { onCanvasScroll = () => {} } = props; - const { visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); - const [ floorAction, setFloorAction ] = useState(FloorAction.SET); - const [ floorHeight, setFloorHeight ] = useState(0); - const [ isSquareSelectMode, setSquareSelectMode ] = useState(false); - + const { visualizationSettings = null, setVisualizationSettings = null, floorAction, setFloorAction } = useFloorplanEditorContext(); + const isSquareSelectMode = FloorplanEditor.instance.isSquareSelectMode; + const selectAction = (action: number) => { setFloorAction(action); FloorplanEditor.instance.actionSettings.currentAction = action; - } + }; const toggleSquareSelectMode = () => { - const nextValue = FloorplanEditor.instance.toggleSquareSelectMode(); - - setSquareSelectMode(nextValue); - } + FloorplanEditor.instance.toggleSquareSelectMode(); + // force re-render by toggling action to same value + setFloorAction(prev => prev); + }; const changeDoorDirection = () => { @@ -58,18 +45,19 @@ export const FloorplanOptionsView: FC = props => return newValue; }); - } + }; - const onFloorHeightChange = (value: number) => + const onWallThicknessChange = (value: number) => { - if(isNaN(value) || (value <= 0)) value = 0; + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; - if(value > 26) value = 26; + newValue.thicknessWall = value; - setFloorHeight(value); - - FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36); - } + return newValue; + }); + }; const onFloorThicknessChange = (value: number) => { @@ -81,157 +69,54 @@ export const FloorplanOptionsView: FC = props => return newValue; }); - } - - const onWallThicknessChange = (value: number) => - { - setVisualizationSettings(prevValue => - { - const newValue = { ...prevValue }; - - newValue.thicknessWall = value; - - return newValue; - }); - } - - const onWallHeightChange = (value: number) => - { - if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT; - - if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT; - - setVisualizationSettings(prevValue => - { - const newValue = { ...prevValue }; - - newValue.wallHeight = value; - - return newValue; - }); - } - - const increaseWallHeight = () => - { - let height = (visualizationSettings.wallHeight + 1); - - if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT; - - onWallHeightChange(height); - } - - const decreaseWallHeight = () => - { - let height = (visualizationSettings.wallHeight - 1); - - if(height <= 0) height = MIN_WALL_HEIGHT; - - onWallHeightChange(height); - } + }; return ( - - - - { LocalizeText('floor.plan.editor.draw.mode') } - - - selectAction(FloorAction.SET) }> - - - selectAction(FloorAction.UNSET) }> - - - - - selectAction(FloorAction.UP) }> - - - selectAction(FloorAction.DOWN) }> - - - - selectAction(FloorAction.DOOR) }> - - - FloorplanEditor.instance.toggleSelectAll() }> - - - - - - - - - { LocalizeText('floor.plan.editor.enter.direction') } - - - - { LocalizeText('floor.editor.wall.height') } - - - onWallHeightChange(event.target.valueAsNumber) } /> - - - - - { LocalizeText('floor.plan.editor.room.options') } - - - - - + + + { LocalizeText('floor.plan.editor.draw.mode') } + + selectAction(FloorAction.SET) }> + + + selectAction(FloorAction.UNSET) }> + + + selectAction(FloorAction.UP) }> + + + selectAction(FloorAction.DOWN) }> + + + selectAction(FloorAction.DOOR) }> + + + FloorplanEditor.instance.toggleSelectAll() }> + + + + + + - - - { LocalizeText('floor.plan.editor.tile.height') }: { floorHeight } -
- onFloorHeightChange(event) } - renderThumb={ (props, state) => - { - const { key, style, ...rest } = (props as Record); - - return
{ state.valueNow }
; - } } /> -
-
- - - - - - -
- - - - - - + + { LocalizeText('floor.plan.editor.enter.direction') } + - + + + + + ); -} \ No newline at end of file +}; diff --git a/src/components/floorplan-editor/views/FloorplanPreviewView.tsx b/src/components/floorplan-editor/views/FloorplanPreviewView.tsx new file mode 100644 index 0000000..cd82a9c --- /dev/null +++ b/src/components/floorplan-editor/views/FloorplanPreviewView.tsx @@ -0,0 +1,328 @@ +import { FC, useEffect, useRef } from 'react'; +import { COLORMAP, HEIGHT_SCHEME, FloorplanEditor } from '@nitrots/nitro-renderer'; +import { useFloorplanEditorContext } from '../FloorplanEditorContext'; + +const colormap = COLORMAP as Record; + +const PREVIEW_TILE_W = 16; +const PREVIEW_TILE_H = 8; +const PREVIEW_BLOCK_H = 5; +const WALL_HEIGHT_PX = 40; +const WALL_COLOR = '#6B7B5E'; +const WALL_SIDE_COLOR = '#5A6A4F'; +const WALL_TOP_COLOR = '#7D8E6F'; + +function hexToRgb(hex: string): [number, number, number] +{ + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + return [ r, g, b ]; +} + +function rgbToHex(r: number, g: number, b: number): string +{ + return `#${ ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) }`; +} + +function darken(hex: string, factor: number): string +{ + const [ r, g, b ] = hexToRgb(hex); + + return rgbToHex( + Math.floor(r * factor), + Math.floor(g * factor), + Math.floor(b * factor) + ); +} + +function getTilemapBounds(tilemap: any[][]): { minX: number; minY: number; maxX: number; maxY: number } +{ + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + + for(let y = 0; y < tilemap.length; y++) + { + if(!tilemap[y]) continue; + + for(let x = 0; x < tilemap[y].length; x++) + { + if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + if(x < minX) minX = x; + if(x > maxX) maxX = x; + if(y < minY) minY = y; + if(y > maxY) maxY = y; + } + } + + if(minX === Infinity) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; + + return { minX, minY, maxX, maxY }; +} + +function renderPreview(canvas: HTMLCanvasElement, wallHeight: number): void +{ + const ctx = canvas.getContext('2d'); + const tilemap = FloorplanEditor.instance.tilemap; + + if(!ctx || !tilemap || tilemap.length === 0) + { + if(ctx) + { + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + return; + } + + const bounds = getTilemapBounds(tilemap); + const tilesW = bounds.maxX - bounds.minX + 1; + const tilesH = bounds.maxY - bounds.minY + 1; + + // find max height for offset calculation + let maxTileHeight = 0; + + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!tilemap[y] || !tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + const hi = HEIGHT_SCHEME.indexOf(tilemap[y][x].height) - 1; + + if(hi > maxTileHeight) maxTileHeight = hi; + } + } + + // calculate isometric bounds + const isoW = (tilesW + tilesH) * PREVIEW_TILE_W; + const isoH = (tilesW + tilesH) * PREVIEW_TILE_H + maxTileHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX; + + // scale to fit canvas + const scaleX = (canvas.width - 20) / isoW; + const scaleY = (canvas.height - 20) / isoH; + const scale = Math.min(scaleX, scaleY, 3); + + const offsetX = (canvas.width - isoW * scale) / 2; + const offsetY = (canvas.height - isoH * scale) / 2 + WALL_HEIGHT_PX * scale * 0.5; + + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.save(); + ctx.translate(offsetX, offsetY); + ctx.scale(scale, scale); + + const tw = PREVIEW_TILE_W; + const th = PREVIEW_TILE_H; + + function isoX(gx: number, gy: number): number + { + return (gx - bounds.minX - gy + bounds.minY) * tw + (tilesH - 1) * tw; + } + + function isoY(gx: number, gy: number): number + { + return (gx - bounds.minX + gy - bounds.minY) * th; + } + + function hasActiveTile(gx: number, gy: number): boolean + { + return tilemap[gy] && tilemap[gy][gx] && tilemap[gy][gx].height !== 'x'; + } + + function getTileHeight(gx: number, gy: number): number + { + if(!hasActiveTile(gx, gy)) return 0; + + return Math.max(0, HEIGHT_SCHEME.indexOf(tilemap[gy][gx].height) - 1); + } + + // draw walls on north and west edges + const wallH = wallHeight > 0 ? wallHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX * 0.3 : WALL_HEIGHT_PX * 0.6; + + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!hasActiveTile(x, y)) continue; + + const tileH = getTileHeight(x, y) * PREVIEW_BLOCK_H; + const cx = isoX(x, y); + const cy = isoY(x, y) - tileH; + + // west wall (no tile to the left) + if(!hasActiveTile(x - 1, y)) + { + ctx.beginPath(); + ctx.moveTo(cx, cy + th); + ctx.lineTo(cx, cy + th - wallH); + ctx.lineTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw, cy); + ctx.closePath(); + ctx.fillStyle = WALL_SIDE_COLOR; + ctx.fill(); + ctx.strokeStyle = '#4A5A3F'; + ctx.lineWidth = 0.5; + ctx.stroke(); + } + + // north wall (no tile above) + if(!hasActiveTile(x, y - 1)) + { + ctx.beginPath(); + ctx.moveTo(cx + tw, cy); + ctx.lineTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw * 2, cy + th - wallH); + ctx.lineTo(cx + tw * 2, cy + th); + ctx.closePath(); + ctx.fillStyle = WALL_COLOR; + ctx.fill(); + ctx.strokeStyle = '#4A5A3F'; + ctx.lineWidth = 0.5; + ctx.stroke(); + } + + // wall top cap - corner + if(!hasActiveTile(x - 1, y) && !hasActiveTile(x, y - 1)) + { + ctx.beginPath(); + ctx.moveTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw + tw * 0.3, cy - wallH - th * 0.3); + ctx.lineTo(cx + tw, cy - wallH - th * 0.6); + ctx.lineTo(cx + tw - tw * 0.3, cy - wallH - th * 0.3); + ctx.closePath(); + ctx.fillStyle = WALL_TOP_COLOR; + ctx.fill(); + } + } + } + + // draw tiles back-to-front + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!hasActiveTile(x, y)) continue; + + const tile = tilemap[y][x]; + const heightIndex = HEIGHT_SCHEME.indexOf(tile.height) - 1; + const tileH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; + + const cx = isoX(x, y); + const cy = isoY(x, y) - tileH; + + const heightChar = tile.height; + const baseColor = colormap[heightChar] || 'aaaaaa'; + const topColor = `#${ baseColor }`; + const leftColor = darken(baseColor, 0.65); + const rightColor = darken(baseColor, 0.80); + + // draw side faces if tile has height + const blockH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; + + // left face (visible when no neighbor to south or neighbor is shorter) + const southH = getTileHeight(x, y + 1); + const leftExpose = hasActiveTile(x, y + 1) ? Math.max(0, heightIndex - southH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; + + if(leftExpose > 0) + { + ctx.beginPath(); + ctx.moveTo(cx, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx + tw, cy + th * 2 + leftExpose); + ctx.lineTo(cx, cy + th + leftExpose); + ctx.closePath(); + ctx.fillStyle = leftColor; + ctx.fill(); + } + + // right face + const eastH = getTileHeight(x + 1, y); + const rightExpose = hasActiveTile(x + 1, y) ? Math.max(0, heightIndex - eastH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; + + if(rightExpose > 0) + { + ctx.beginPath(); + ctx.moveTo(cx + tw * 2, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx + tw, cy + th * 2 + rightExpose); + ctx.lineTo(cx + tw * 2, cy + th + rightExpose); + ctx.closePath(); + ctx.fillStyle = rightColor; + ctx.fill(); + } + + // top face + ctx.beginPath(); + ctx.moveTo(cx + tw, cy); + ctx.lineTo(cx + tw * 2, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx, cy + th); + ctx.closePath(); + ctx.fillStyle = topColor; + ctx.fill(); + + // door indicator + const door = FloorplanEditor.instance.doorLocation; + + if(door.x === x && door.y === y) + { + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.fill(); + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + } + + ctx.restore(); +} + +export const FloorplanPreviewView: FC<{}> = () => +{ + const { tilemapVersion, visualizationSettings } = useFloorplanEditorContext(); + const canvasRef = useRef(null); + const rafRef = useRef(0); + + useEffect(() => + { + if(!canvasRef.current) return; + + if(rafRef.current) cancelAnimationFrame(rafRef.current); + + rafRef.current = requestAnimationFrame(() => + { + const canvas = canvasRef.current; + + if(!canvas) return; + + const parent = canvas.parentElement; + + if(parent) + { + canvas.width = parent.clientWidth; + canvas.height = parent.clientHeight; + } + + renderPreview(canvas, visualizationSettings?.wallHeight ?? 0); + }); + + return () => + { + if(rafRef.current) cancelAnimationFrame(rafRef.current); + }; + }, [ tilemapVersion, visualizationSettings?.wallHeight ]); + + return ( +
+ +
+ ); +}; From 119d12a5ea96f66c47ca6dda6dc52e2bbc610f07 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 16 Mar 2026 22:41:35 +0100 Subject: [PATCH 12/33] Add quick commands autocomplete dropdown in chat input Server-authoritative command list via packet 4050, merged with client-only commands. Supports keyboard navigation, filtering, and module-level caching to handle login-time packet timing. Co-Authored-By: medievalshell --- src/api/room/widgets/CommandDefinition.ts | 5 + src/api/room/widgets/index.ts | 1 + .../ChatInputCommandSelectorView.tsx | 41 +++++ .../room/widgets/chat-input/ChatInputView.tsx | 49 +++++- src/hooks/rooms/widgets/index.ts | 1 + .../rooms/widgets/useChatCommandSelector.ts | 162 ++++++++++++++++++ 6 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 src/api/room/widgets/CommandDefinition.ts create mode 100644 src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx create mode 100644 src/hooks/rooms/widgets/useChatCommandSelector.ts diff --git a/src/api/room/widgets/CommandDefinition.ts b/src/api/room/widgets/CommandDefinition.ts new file mode 100644 index 0000000..d1e0508 --- /dev/null +++ b/src/api/room/widgets/CommandDefinition.ts @@ -0,0 +1,5 @@ +export interface CommandDefinition +{ + key: string; + description: string; +} diff --git a/src/api/room/widgets/index.ts b/src/api/room/widgets/index.ts index 5cef378..4892937 100644 --- a/src/api/room/widgets/index.ts +++ b/src/api/room/widgets/index.ts @@ -7,6 +7,7 @@ export * from './AvatarInfoUser'; export * from './AvatarInfoUtilities'; export * from './BotSkillsEnum'; export * from './ChatBubbleMessage'; +export * from './CommandDefinition'; export * from './ChatBubbleUtilities'; export * from './ChatMessageTypeEnum'; export * from './DimmerFurnitureWidgetPresetItem'; diff --git a/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx new file mode 100644 index 0000000..5eae3d2 --- /dev/null +++ b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx @@ -0,0 +1,41 @@ +import { FC, useEffect, useRef } from 'react'; +import { CommandDefinition } from '../../../../api'; + +interface ChatInputCommandSelectorViewProps +{ + commands: CommandDefinition[]; + selectedIndex: number; + onSelect: (command: CommandDefinition) => void; + onHover: (index: number) => void; +} + +export const ChatInputCommandSelectorView: FC = props => +{ + const { commands = [], selectedIndex = 0, onSelect = null, onHover = null } = props; + const listRef = useRef(null); + + useEffect(() => + { + if(!listRef.current) return; + + const selected = listRef.current.children[selectedIndex] as HTMLElement; + + if(selected) selected.scrollIntoView({ block: 'nearest' }); + }, [ selectedIndex ]); + + return ( +
+ { commands.map((cmd, index) => ( +
onSelect(cmd) } + onMouseEnter={ () => onHover(index) } + > + :{ cmd.key } + { cmd.description } +
+ )) } +
+ ); +}; diff --git a/src/components/room/widgets/chat-input/ChatInputView.tsx b/src/components/room/widgets/chat-input/ChatInputView.tsx index 3abe197..4c22e85 100644 --- a/src/components/room/widgets/chat-input/ChatInputView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputView.tsx @@ -3,7 +3,8 @@ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { ChatMessageTypeEnum, GetClubMemberLevel, GetConfigurationValue, LocalizeText, RoomWidgetUpdateChatInputContentEvent } from '../../../../api'; import { Text } from '../../../../common'; -import { useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks'; +import { useChatCommandSelector, useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks'; +import { ChatInputCommandSelectorView } from './ChatInputCommandSelectorView'; import { ChatInputEmojiSelectorView } from './ChatInputEmojiSelectorView'; import { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView'; @@ -14,6 +15,7 @@ export const ChatInputView: FC<{}> = props => const { selectedUsername = '', floodBlocked = false, floodBlockedSeconds = 0, setIsTyping = null, setIsIdle = null, sendChat = null } = useChatInputWidget(); const { roomSession = null } = useRoom(); const inputRef = useRef(); + const { isVisible: commandSelectorVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close: closeCommandSelector } = useChatCommandSelector(chatValue); const chatModeIdWhisper = useMemo(() => LocalizeText('widgets.chatinput.mode.whisper'), []); const chatModeIdShout = useMemo(() => LocalizeText('widgets.chatinput.mode.shout'), []); @@ -133,6 +135,40 @@ export const ChatInputView: FC<{}> = props => if(document.activeElement !== inputRef.current) setInputFocus(); + if(commandSelectorVisible) + { + switch(event.key) + { + case 'ArrowUp': + event.preventDefault(); + moveUp(); + return; + case 'ArrowDown': + event.preventDefault(); + moveDown(); + return; + case 'Tab': + event.preventDefault(); + // fall through + case 'NumpadEnter': + case 'Enter': { + const selected = selectCurrent(); + + if(selected) + { + event.preventDefault(); + setChatValue(':' + selected.key + ' '); + return; + } + break; + } + case 'Escape': + event.preventDefault(); + closeCommandSelector(); + return; + } + } + const value = (event.target as HTMLInputElement).value; switch(event.key) @@ -158,7 +194,7 @@ export const ChatInputView: FC<{}> = props => return; } - }, [ floodBlocked, inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue ]); + }, [ floodBlocked, inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue, commandSelectorVisible, moveUp, moveDown, selectCurrent, closeCommandSelector ]); useUiEvent(RoomWidgetUpdateChatInputContentEvent.CHAT_INPUT_CONTENT, event => { @@ -243,7 +279,14 @@ export const ChatInputView: FC<{}> = props => return ( createPortal( -
+
+ { commandSelectorVisible && + { setChatValue(':' + cmd.key + ' '); inputRef.current?.focus(); } } + onHover={ setSelectedIndex } + /> }
{ !floodBlocked && updateChatInput(event.target.value) } onMouseDown={ event => setInputFocus() } /> } diff --git a/src/hooks/rooms/widgets/index.ts b/src/hooks/rooms/widgets/index.ts index 9984450..ea35008 100644 --- a/src/hooks/rooms/widgets/index.ts +++ b/src/hooks/rooms/widgets/index.ts @@ -1,5 +1,6 @@ export * from './furniture'; export * from './useAvatarInfoWidget'; +export * from './useChatCommandSelector'; export * from './useChatInputWidget'; export * from './useChatWidget'; export * from './useDoorbellWidget'; diff --git a/src/hooks/rooms/widgets/useChatCommandSelector.ts b/src/hooks/rooms/widgets/useChatCommandSelector.ts new file mode 100644 index 0000000..eb31b68 --- /dev/null +++ b/src/hooks/rooms/widgets/useChatCommandSelector.ts @@ -0,0 +1,162 @@ +import { AvailableCommandsEvent, GetCommunication } from '@nitrots/nitro-renderer'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { CommandDefinition } from '../../../api'; +import { useMessageEvent } from '../../events'; + +const CLIENT_COMMANDS: CommandDefinition[] = [ + // Effetti stanza + { key: 'shake', description: 'Scuoti la stanza' }, + { key: 'rotate', description: 'Ruota la stanza' }, + { key: 'zoom', description: 'Zoom stanza' }, + { key: 'flip', description: 'Reset zoom' }, + { key: 'iddqd', description: 'Reset zoom' }, + { key: 'screenshot', description: 'Screenshot stanza' }, + { key: 'togglefps', description: 'Toggle FPS' }, + // Espressioni + { key: 'd', description: 'Ridi (VIP)' }, + { key: 'kiss', description: 'Manda un bacio (VIP)' }, + { key: 'jump', description: 'Salta (VIP)' }, + { key: 'idle', description: 'Vai in idle' }, + { key: 'sign', description: 'Mostra cartello' }, + // Gestione stanza + { key: 'furni', description: 'Furni chooser' }, + { key: 'chooser', description: 'User chooser' }, + { key: 'floor', description: 'Floor editor' }, + { key: 'bcfloor', description: 'Floor editor' }, + { key: 'pickall', description: 'Raccogli tutti i furni' }, + { key: 'ejectall', description: 'Espelli tutti i furni' }, + { key: 'settings', description: 'Impostazioni stanza' }, + // Info + { key: 'client', description: 'Info client' }, + { key: 'nitro', description: 'Info client' }, +]; + +// Module-level cache: cattura i comandi dal server anche prima che React monti +let cachedServerCommands: CommandDefinition[] = []; +let globalListenerRegistered = false; + +function ensureGlobalListener(): void +{ + if(globalListenerRegistered) return; + globalListenerRegistered = true; + + try + { + const event = new AvailableCommandsEvent((event: AvailableCommandsEvent) => + { + const parser = event.getParser(); + cachedServerCommands = parser.commands.map(cmd => ({ key: cmd.key, description: cmd.description })); + }); + + GetCommunication().registerMessageEvent(event); + } + catch(e) + { + // Communication not ready yet, will retry on hook mount + globalListenerRegistered = false; + } +} + +// Try to register immediately at module load +ensureGlobalListener(); + +export const useChatCommandSelector = (chatValue: string) => +{ + const [ serverCommands, setServerCommands ] = useState(cachedServerCommands); + const [ selectedIndex, setSelectedIndex ] = useState(0); + const [ dismissed, setDismissed ] = useState(false); + + // Ensure global listener is registered + useEffect(() => + { + ensureGlobalListener(); + + // If cache already has data (from login), use it + if(cachedServerCommands.length > 0 && serverCommands.length === 0) + { + setServerCommands(cachedServerCommands); + } + }, []); + + // Also listen via React hook for any future updates (e.g. rank change) + useMessageEvent(AvailableCommandsEvent, event => + { + const parser = event.getParser(); + const cmds = parser.commands.map(cmd => ({ key: cmd.key, description: cmd.description })); + cachedServerCommands = cmds; + setServerCommands(cmds); + }); + + const allCommands = useMemo(() => + { + const merged = [ ...serverCommands ]; + + for(const clientCmd of CLIENT_COMMANDS) + { + if(!merged.some(cmd => cmd.key === clientCmd.key)) + { + merged.push(clientCmd); + } + } + + return merged.sort((a, b) => a.key.localeCompare(b.key)); + }, [ serverCommands ]); + + const filterText = useMemo(() => + { + if(!chatValue.startsWith(':') || chatValue.includes(' ')) return ''; + + return chatValue.slice(1).toLowerCase(); + }, [ chatValue ]); + + const filteredCommands = useMemo(() => + { + if(!filterText && !chatValue.startsWith(':')) return []; + + return allCommands.filter(cmd => cmd.key.toLowerCase().startsWith(filterText)); + }, [ allCommands, filterText, chatValue ]); + + const isVisible = useMemo(() => + { + return chatValue.startsWith(':') && !chatValue.includes(' ') && filteredCommands.length > 0 && !dismissed; + }, [ chatValue, filteredCommands, dismissed ]); + + const moveUp = useCallback(() => + { + setSelectedIndex(prev => (prev <= 0 ? filteredCommands.length - 1 : prev - 1)); + }, [ filteredCommands.length ]); + + const moveDown = useCallback(() => + { + setSelectedIndex(prev => (prev >= filteredCommands.length - 1 ? 0 : prev + 1)); + }, [ filteredCommands.length ]); + + const selectCurrent = useCallback((): CommandDefinition | null => + { + if(selectedIndex >= 0 && selectedIndex < filteredCommands.length) + { + return filteredCommands[selectedIndex]; + } + + return null; + }, [ selectedIndex, filteredCommands ]); + + const close = useCallback(() => + { + setDismissed(true); + }, []); + + // Reset dismissed when chatValue changes to a new command start + useEffect(() => + { + if(chatValue === ':' || chatValue === '') setDismissed(false); + }, [ chatValue ]); + + // Reset selectedIndex when filtered list changes + useEffect(() => + { + setSelectedIndex(0); + }, [ filterText ]); + + return { isVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close }; +}; From 435a59e647ff37d01966b7627e94ade80f7bce77 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 16 Mar 2026 23:10:27 +0100 Subject: [PATCH 13/33] Improve mod tools UI layout and usability - Fix icon alignment using flexbox instead of absolute positioning - Add active state indicators on buttons when sub-panels are open - Add min-width constraints to prevent cramped layouts - Improve user button with placeholder text and truncated username - Improve room info panel with better spacing, clickable owner, colored owner status - Improve chatlog with scrollable container, alternating row colors, compact headers - Clean up room info header and room name display --- src/components/mod-tools/ModToolsView.tsx | 28 +++++++----- .../mod-tools/views/chatlog/ChatlogView.tsx | 19 ++++---- .../views/room/ModToolsChatlogView.tsx | 6 +-- .../mod-tools/views/room/ModToolsRoomView.tsx | 45 ++++++++++--------- 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/components/mod-tools/ModToolsView.tsx b/src/components/mod-tools/ModToolsView.tsx index 2d7c5c0..d42381d 100644 --- a/src/components/mod-tools/ModToolsView.tsx +++ b/src/components/mod-tools/ModToolsView.tsx @@ -117,23 +117,31 @@ export const ModToolsView: FC<{}> = props => return () => RemoveLinkEventTracker(linkTracker); }, [ openRoomInfo, closeRoomInfo, toggleRoomInfo, openRoomChatlog, closeRoomChatlog, toggleRoomChatlog, openUserInfo, closeUserInfo, toggleUserInfo, openUserChatlog, closeUserChatlog, toggleUserChatlog ]); + const isRoomInfoOpen = currentRoomId > 0 && openRooms.includes(currentRoomId); + const isRoomChatlogOpen = currentRoomId > 0 && openRoomChatlogs.includes(currentRoomId); + const isUserInfoOpen = selectedUser && openUserInfos.includes(selectedUser.userId); + return ( <> { isVisible && - + setIsVisible(false) } /> - - - - - } diff --git a/src/components/mod-tools/views/chatlog/ChatlogView.tsx b/src/components/mod-tools/views/chatlog/ChatlogView.tsx index 3aa8650..63e5201 100644 --- a/src/components/mod-tools/views/chatlog/ChatlogView.tsx +++ b/src/components/mod-tools/views/chatlog/ChatlogView.tsx @@ -46,14 +46,11 @@ export const ChatlogView: FC = props => const RoomInfo = (props: { roomId: number, roomName: string }) => { return ( - -
- Room name: - { props.roomName } -
-
- - + + { props.roomName } +
+ +
); @@ -63,7 +60,7 @@ export const ChatlogView: FC = props => <> - +
Time
User
Message
@@ -77,8 +74,8 @@ export const ChatlogView: FC = props => { row.isRoomInfo && } { !row.isRoomInfo && - - { row.timestamp } + + { row.timestamp } CreateLinkEvent(`mod-tools/open-user-info/${ row.habboId }`) }>{ row.username } { row.message } } diff --git a/src/components/mod-tools/views/room/ModToolsChatlogView.tsx b/src/components/mod-tools/views/room/ModToolsChatlogView.tsx index 394633d..c42320d 100644 --- a/src/components/mod-tools/views/room/ModToolsChatlogView.tsx +++ b/src/components/mod-tools/views/room/ModToolsChatlogView.tsx @@ -33,9 +33,9 @@ export const ModToolsChatlogView: FC = props => if(!roomChatlog) return null; return ( - - - + + + { roomChatlog && } diff --git a/src/components/mod-tools/views/room/ModToolsRoomView.tsx b/src/components/mod-tools/views/room/ModToolsRoomView.tsx index 5793ea7..37d9fc5 100644 --- a/src/components/mod-tools/views/room/ModToolsRoomView.tsx +++ b/src/components/mod-tools/views/room/ModToolsRoomView.tsx @@ -69,47 +69,52 @@ export const ModToolsRoomView: FC = props => }, [ roomId, infoRequested, setInfoRequested ]); return ( - - onCloseClick() } /> - + + onCloseClick() } /> + + { name && +
+ { name } +
+ }
- -
- Room Owner: - { ownerName } + +
+ Owner: + CreateLinkEvent(`mod-tools/open-user-info/${ ownerId }`) }>{ ownerName }
-
- Users in room: +
+ Users in room: { usersInRoom }
-
- Owner in room: - { ownerInRoom ? 'Yes' : 'No' } +
+ Owner here: + { ownerInRoom ? 'Yes' : 'No' }
-
+
-
+
setKickUsers(event.target.checked) } /> Kick everyone out
-
+
setLockRoom(event.target.checked) } /> Enable the doorbell
-
+
setChangeRoomName(event.target.checked) } /> Change room name
- -
- - + +
+ +
From 12e50ff1cdc8eb96a87893bf99d0e7f240ab8943 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Wed, 18 Mar 2026 17:01:10 +0100 Subject: [PATCH 14/33] fix(wired-ui): clarify reward fields and mute alerts --- .../actions/WiredActionGiveRewardView.tsx | 226 +++++++++++++++--- src/hooks/notification/useNotification.ts | 24 +- src/hooks/rooms/widgets/useChatWidget.ts | 7 +- 3 files changed, 224 insertions(+), 33 deletions(-) diff --git a/src/components/wired/views/actions/WiredActionGiveRewardView.tsx b/src/components/wired/views/actions/WiredActionGiveRewardView.tsx index 91c1468..3fb239a 100644 --- a/src/components/wired/views/actions/WiredActionGiveRewardView.tsx +++ b/src/components/wired/views/actions/WiredActionGiveRewardView.tsx @@ -7,6 +7,131 @@ import { NitroInput } from '../../../../layout'; import { WiredActionBaseView } from './WiredActionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; +type RewardType = 'badge' | 'credits' | 'pixels' | 'diamonds' | 'points' | 'furni' | 'respect'; + +interface RewardEntry +{ + rewardType: RewardType; + rewardValue: string; + probability: number; + pointsType: number; +} + +const DEFAULT_PROBABILITY = 100; +const DEFAULT_POINTS_TYPE = 5; + +const REWARD_TYPES: { value: RewardType, label: string }[] = [ + { value: 'badge', label: 'Badge' }, + { value: 'credits', label: 'Credits' }, + { value: 'pixels', label: 'Pixels / Duckets' }, + { value: 'diamonds', label: 'Diamonds' }, + { value: 'points', label: 'Extra Currency' }, + { value: 'furni', label: 'Furni' }, + { value: 'respect', label: 'Respect' } +]; + +const SELECTABLE_REWARD_TYPES = REWARD_TYPES.filter(entry => (entry.value !== 'respect')); + +const createReward = (): RewardEntry => +({ + rewardType: 'furni', + rewardValue: '', + probability: DEFAULT_PROBABILITY, + pointsType: DEFAULT_POINTS_TYPE +}); + +const getRewardValuePlaceholder = (rewardType: RewardType) => +{ + switch(rewardType) + { + case 'badge': + return 'Badge code'; + case 'credits': + return 'Credits amount'; + case 'pixels': + return 'Pixels amount'; + case 'diamonds': + return 'Diamonds amount'; + case 'points': + return 'Amount'; + case 'furni': + return 'Furni base item id'; + case 'respect': + return 'Respect amount'; + } +}; + +const getExtraFieldLabel = (rewardType: RewardType) => +{ + switch(rewardType) + { + case 'points': + return 'Currency Type'; + case 'badge': + return 'Code'; + default: + return 'Info'; + } +}; + +const getExtraFieldPlaceholder = (rewardType: RewardType) => +{ + switch(rewardType) + { + case 'points': + return 'Type id (e.g. 105)'; + case 'badge': + return 'Badge'; + default: + return ''; + } +}; + +const parseRewardEntry = (rawType: string, rawCode: string, rawProbability: string): RewardEntry => +{ + const probability = Number(rawProbability); + const parsedProbability = Number.isFinite(probability) ? probability : DEFAULT_PROBABILITY; + + if(rawType === '0') + { + return { rewardType: 'badge', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + const separatorIndex = rawCode.indexOf('#'); + + if(separatorIndex === -1) + { + return { rewardType: 'furni', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + const rewardType = rawCode.slice(0, separatorIndex); + const rewardValue = rawCode.slice(separatorIndex + 1); + + if(rewardType.startsWith('points')) + { + const pointsType = Number(rewardType.slice('points'.length)); + + return { + rewardType: 'points', + rewardValue, + probability: parsedProbability, + pointsType: Number.isFinite(pointsType) ? pointsType : DEFAULT_POINTS_TYPE + }; + } + + if(REWARD_TYPES.some(entry => (entry.value === rewardType))) + { + return { rewardType: rewardType as RewardType, rewardValue, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + if(rewardType === 'cata') + { + return { rewardType: 'furni', rewardValue, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + return { rewardType: 'furni', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; +}; + export const WiredActionGiveRewardView: FC<{}> = props => { const [ limitEnabled, setLimitEnabled ] = useState(false); @@ -14,7 +139,7 @@ export const WiredActionGiveRewardView: FC<{}> = props => const [ uniqueRewards, setUniqueRewards ] = useState(false); const [ rewardsLimit, setRewardsLimit ] = useState(1); const [ limitationInterval, setLimitationInterval ] = useState(1); - const [ rewards, setRewards ] = useState<{ isBadge: boolean, itemCode: string, probability: number }[]>([]); + const [ rewards, setRewards ] = useState([]); const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); const [ userSource, setUserSource ] = useState(() => { @@ -22,7 +147,8 @@ export const WiredActionGiveRewardView: FC<{}> = props => return 0; }); - const addReward = () => setRewards(rewards => [ ...rewards, { isBadge: false, itemCode: '', probability: null } ]); + const addReward = () => setRewards(rewards => [ ...rewards, createReward() ]); + const hasCustomCurrencyReward = rewards.some(reward => (reward.rewardType === 'points')); const removeReward = (index: number) => { @@ -36,18 +162,9 @@ export const WiredActionGiveRewardView: FC<{}> = props => }); }; - const updateReward = (index: number, isBadge: boolean, itemCode: string, probability: number) => + const updateReward = (index: number, updater: (reward: RewardEntry) => RewardEntry) => { - const rewardsClone = Array.from(rewards); - const reward = rewardsClone[index]; - - if(!reward) return; - - reward.isBadge = isBadge; - reward.itemCode = itemCode; - reward.probability = probability; - - setRewards(rewardsClone); + setRewards(prevValue => prevValue.map((reward, rewardIndex) => ((rewardIndex === index) ? updater(reward) : reward))); }; const save = () => @@ -56,9 +173,20 @@ export const WiredActionGiveRewardView: FC<{}> = props => for(const reward of rewards) { - if(!reward.itemCode) continue; + const rewardValue = reward.rewardValue.trim(); - const rewardsString = [ reward.isBadge ? '0' : '1', reward.itemCode, reward.probability.toString() ]; + if(!rewardValue) continue; + + const probability = Math.max(0, Number.isFinite(reward.probability) ? reward.probability : DEFAULT_PROBABILITY); + const rewardCode = (() => + { + if(reward.rewardType === 'badge') return rewardValue; + if(reward.rewardType === 'points') return `points${ Math.max(0, reward.pointsType) }#${ rewardValue }`; + + return `${ reward.rewardType }#${ rewardValue }`; + })(); + + const rewardsString = [ reward.rewardType === 'badge' ? '0' : '1', rewardCode, (uniqueRewards ? DEFAULT_PROBABILITY : probability).toString() ]; stringRewards.push(rewardsString.join(',')); } @@ -71,9 +199,9 @@ export const WiredActionGiveRewardView: FC<{}> = props => useEffect(() => { - const readRewards: { isBadge: boolean, itemCode: string, probability: number }[] = []; + const readRewards: RewardEntry[] = []; - if(trigger.stringData.length > 0 && trigger.stringData.includes(';')) + if(trigger.stringData.length > 0) { const splittedRewards = trigger.stringData.split(';'); @@ -83,11 +211,11 @@ export const WiredActionGiveRewardView: FC<{}> = props => if(reward.length !== 3) continue; - readRewards.push({ isBadge: reward[0] === '0', itemCode: reward[1], probability: Number(reward[2]) }); + readRewards.push(parseRewardEntry(reward[0], reward[1], reward[2])); } } - if(readRewards.length === 0) readRewards.push({ isBadge: false, itemCode: '', probability: null }); + if(readRewards.length === 0) readRewards.push(createReward()); setRewardTime((trigger.intData.length > 0) ? trigger.intData[0] : 0); setUniqueRewards((trigger.intData.length > 1) ? (trigger.intData[1] === 1) : false); @@ -147,24 +275,64 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
+
+ Type + Amount / Value + { uniqueRewards ? 'Mode' : 'Chance %' } + { hasCustomCurrencyReward ? 'Currency Type' : 'Extra / Info' } + Action +
{ rewards && rewards.map((reward, index) => { + const rewardTypeOptions = (reward.rewardType === 'respect') + ? REWARD_TYPES + : SELECTABLE_REWARD_TYPES; + return ( -
-
- updateReward(index, e.target.checked, reward.itemCode, reward.probability) } /> - Badge? +
+ + updateReward(index, prevValue => ({ ...prevValue, rewardValue: event.target.value })) } /> + { uniqueRewards + ?
+ Unique +
+ : updateReward(index, prevValue => ({ ...prevValue, probability: Number(event.target.value) })) } /> } + { (reward.rewardType === 'points') + ? + updateReward(index, prevValue => ({ ...prevValue, pointsType: Number(event.target.value) })) } /> + :
+ { getExtraFieldLabel(reward.rewardType) } +
} +
+ { (index > 0) && + }
- updateReward(index, reward.isBadge, e.target.value, reward.probability) } /> - updateReward(index, reward.isBadge, reward.itemCode, Number(e.target.value)) } /> - { (index > 0) && - }
); }) }
+ + Extra Currency uses Amount as the quantity and Currency Type as the purse type id. Example: amount 200 + type 105. + ); }; diff --git a/src/hooks/notification/useNotification.ts b/src/hooks/notification/useNotification.ts index 4cd3f68..41e85d1 100644 --- a/src/hooks/notification/useNotification.ts +++ b/src/hooks/notification/useNotification.ts @@ -1,4 +1,4 @@ -import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d } from '@nitrots/nitro-renderer'; +import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d, WiredRewardResultMessageEvent } from '@nitrots/nitro-renderer'; import { useCallback, useState } from 'react'; import { useBetween } from 'use-between'; import { GetConfigurationValue, LocalizeBadgeName, LocalizeText, NotificationAlertItem, NotificationAlertType, NotificationBubbleItem, NotificationBubbleType, NotificationConfirmItem, PlaySound, ProductImageUtility, TradingNotificationType } from '../../api'; @@ -397,6 +397,28 @@ const useNotificationState = () => simpleAlert(LocalizeText(parser.alertMessage), NotificationAlertType.DEFAULT, null, null, LocalizeText(parser.titleMessage ? parser.titleMessage : 'notifications.broadcast.title')); }); + useMessageEvent(WiredRewardResultMessageEvent, event => + { + const parser = event.getParser(); + + switch(parser.reason) + { + case WiredRewardResultMessageEvent.PRODUCT_DONATED_CODE: + case WiredRewardResultMessageEvent.BADGE_DONATED_CODE: + simpleAlert(LocalizeText('wiredfurni.rewardsuccess.body'), NotificationAlertType.DEFAULT, null, null, LocalizeText('wiredfurni.rewardsuccess.title')); + return; + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 8: + simpleAlert(LocalizeText(`wiredfurni.rewardfailed.reason.${ parser.reason }`), NotificationAlertType.DEFAULT, null, null, LocalizeText('wiredfurni.rewardfailed.title')); + return; + } + }); + const onRoomEnterEvent = useCallback(() => { if(modDisclaimerShown) return; diff --git a/src/hooks/rooms/widgets/useChatWidget.ts b/src/hooks/rooms/widgets/useChatWidget.ts index d430a38..648a102 100644 --- a/src/hooks/rooms/widgets/useChatWidget.ts +++ b/src/hooks/rooms/widgets/useChatWidget.ts @@ -123,9 +123,10 @@ const useChatWidgetState = () => text = LocalizeText('widget.chatbubble.handitem', ['username', 'handitem'], [username, LocalizeText(('handitem' + event.extraParam))]); break; case RoomSessionChatEvent.CHAT_TYPE_MUTE_REMAINING: { - const hours = ((event.extraParam > 0) ? Math.floor((event.extraParam / 3600)) : 0).toString(); - const minutes = ((event.extraParam > 0) ? Math.floor((event.extraParam % 3600) / 60) : 0).toString(); - const seconds = (event.extraParam % 60).toString(); + const remainingSeconds = Math.max(0, event.extraParam); + const hours = Math.floor(remainingSeconds / 3600).toString(); + const minutes = Math.floor((remainingSeconds % 3600) / 60).toString(); + const seconds = (remainingSeconds % 60).toString(); text = LocalizeText('widget.chatbubble.mutetime', ['hours', 'minutes', 'seconds'], [hours, minutes, seconds]); break; From dbfcae523105ad87d17e45918cda5c5bc7862720 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Tue, 17 Mar 2026 01:34:33 +0100 Subject: [PATCH 15/33] feat(wired): add leave/click/action/short-period trigger views - add UI for wf_trg_leave_room, wf_trg_stuff_state, wf_trg_period_short, wf_trg_click_furni, wf_trg_click_tile, wf_trg_click_user and wf_trg_user_performs_action\n- add state snapshot mode options for wf_trg_stuff_state\n- add sign and dance filters for wf_trg_user_performs_action --- src/api/wired/WiredTriggerLayoutCode.ts | 6 + .../WiredTriggerAvatarLeaveRoomView.tsx | 39 +++++++ .../triggers/WiredTriggerClickFurniView.tsx | 8 ++ .../triggers/WiredTriggerClickTileView.tsx | 20 ++++ .../triggers/WiredTriggerClickUserView.tsx | 8 ++ ...redTriggerExecutePeriodicallyShortView.tsx | 32 ++++++ .../views/triggers/WiredTriggerLayoutView.tsx | 18 +++ .../triggers/WiredTriggerToggleFurniView.tsx | 34 +++++- .../WiredTriggerUserPerformsActionView.tsx | 103 ++++++++++++++++++ 9 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 src/components/wired/views/triggers/WiredTriggerAvatarLeaveRoomView.tsx create mode 100644 src/components/wired/views/triggers/WiredTriggerClickFurniView.tsx create mode 100644 src/components/wired/views/triggers/WiredTriggerClickTileView.tsx create mode 100644 src/components/wired/views/triggers/WiredTriggerClickUserView.tsx create mode 100644 src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyShortView.tsx create mode 100644 src/components/wired/views/triggers/WiredTriggerUserPerformsActionView.tsx diff --git a/src/api/wired/WiredTriggerLayoutCode.ts b/src/api/wired/WiredTriggerLayoutCode.ts index 683ff04..ac80e07 100644 --- a/src/api/wired/WiredTriggerLayoutCode.ts +++ b/src/api/wired/WiredTriggerLayoutCode.ts @@ -15,4 +15,10 @@ export class WiredTriggerLayout public static BOT_REACHED_STUFF: number = 13; public static BOT_REACHED_AVATAR: number = 14; public static RECEIVE_SIGNAL: number = 15; + public static AVATAR_LEAVES_ROOM: number = 16; + public static EXECUTE_PERIODICALLY_SHORT: number = 17; + public static CLICK_FURNI: number = 18; + public static CLICK_TILE: number = 19; + public static CLICK_USER: number = 20; + public static USER_PERFORMS_ACTION: number = 21; } diff --git a/src/components/wired/views/triggers/WiredTriggerAvatarLeaveRoomView.tsx b/src/components/wired/views/triggers/WiredTriggerAvatarLeaveRoomView.tsx new file mode 100644 index 0000000..36a773e --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerAvatarLeaveRoomView.tsx @@ -0,0 +1,39 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerAvatarLeaveRoomView: FC<{}> = props => +{ + const [ username, setUsername ] = useState(''); + const [ avatarMode, setAvatarMode ] = useState(0); + const { trigger = null, setStringParam = null } = useWired(); + + const save = () => setStringParam((avatarMode === 1) ? username : ''); + + useEffect(() => + { + setUsername(trigger.stringData); + setAvatarMode(trigger.stringData ? 1 : 0); + }, [ trigger ]); + + return ( + +
+ { LocalizeText('wiredfurni.params.picktriggerer') } +
+ setAvatarMode(0) } /> + { LocalizeText('wiredfurni.params.anyavatar') } +
+
+ setAvatarMode(1) } /> + { LocalizeText('wiredfurni.params.certainavatar') } +
+ { (avatarMode === 1) && + setUsername(event.target.value) } /> } +
+
+ ); +}; diff --git a/src/components/wired/views/triggers/WiredTriggerClickFurniView.tsx b/src/components/wired/views/triggers/WiredTriggerClickFurniView.tsx new file mode 100644 index 0000000..db8fbea --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerClickFurniView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerClickFurniView: FC<{}> = () => +{ + return ; +}; diff --git a/src/components/wired/views/triggers/WiredTriggerClickTileView.tsx b/src/components/wired/views/triggers/WiredTriggerClickTileView.tsx new file mode 100644 index 0000000..5a1fdc7 --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerClickTileView.tsx @@ -0,0 +1,20 @@ +import { FC, useEffect } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +const CLICK_TILE_INTERACTION_TYPES = [ 'room_invisible_click_tile' ]; + +export const WiredTriggerClickTileView: FC<{}> = () => +{ + const { setAllowedInteractionTypes } = useWired(); + + useEffect(() => + { + setAllowedInteractionTypes(CLICK_TILE_INTERACTION_TYPES); + + return () => setAllowedInteractionTypes(null); + }, [ setAllowedInteractionTypes ]); + + return ; +}; diff --git a/src/components/wired/views/triggers/WiredTriggerClickUserView.tsx b/src/components/wired/views/triggers/WiredTriggerClickUserView.tsx new file mode 100644 index 0000000..f3f2222 --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerClickUserView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerClickUserView: FC<{}> = () => +{ + return ; +}; diff --git a/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyShortView.tsx b/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyShortView.tsx new file mode 100644 index 0000000..642b5cd --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyShortView.tsx @@ -0,0 +1,32 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggeExecutePeriodicallyShortView: FC<{}> = () => +{ + const [ time, setTime ] = useState(10); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ time ]); + + useEffect(() => + { + setTime((trigger.intData.length > 0) ? trigger.intData[0] : 10); + }, [ trigger ]); + + return ( + +
+ { LocalizeText('wiredfurni.params.settime', [ 'seconds' ], [ ((time * 50) / 1000).toFixed(2) ]) } + { `${ time * 50 } ms` } + setTime(event) } /> +
+
+ ); +}; diff --git a/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx b/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx index 229464f..3b152fb 100644 --- a/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx +++ b/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx @@ -1,14 +1,20 @@ import { WiredTriggerLayout } from '../../../../api'; import { WiredTriggerAvatarEnterRoomView } from './WiredTriggerAvatarEnterRoomView'; +import { WiredTriggerAvatarLeaveRoomView } from './WiredTriggerAvatarLeaveRoomView'; import { WiredTriggerAvatarSaysSomethingView } from './WiredTriggerAvatarSaysSomethingView'; import { WiredTriggerAvatarWalksOffFurniView } from './WiredTriggerAvatarWalksOffFurniView'; import { WiredTriggerAvatarWalksOnFurniView } from './WiredTriggerAvatarWalksOnFurni'; import { WiredTriggerBotReachedAvatarView } from './WiredTriggerBotReachedAvatarView'; import { WiredTriggerBotReachedStuffView } from './WiredTriggerBotReachedStuffView'; +import { WiredTriggerClickFurniView } from './WiredTriggerClickFurniView'; +import { WiredTriggerClickTileView } from './WiredTriggerClickTileView'; +import { WiredTriggerClickUserView } from './WiredTriggerClickUserView'; import { WiredTriggerCollisionView } from './WiredTriggerCollisionView'; +import { WiredTriggerUserPerformsActionView } from './WiredTriggerUserPerformsActionView'; import { WiredTriggeExecuteOnceView } from './WiredTriggerExecuteOnceView'; import { WiredTriggeExecutePeriodicallyLongView } from './WiredTriggerExecutePeriodicallyLongView'; import { WiredTriggeExecutePeriodicallyView } from './WiredTriggerExecutePeriodicallyView'; +import { WiredTriggeExecutePeriodicallyShortView } from './WiredTriggerExecutePeriodicallyShortView'; import { WiredTriggerGameEndsView } from './WiredTriggerGameEndsView'; import { WiredTriggerGameStartsView } from './WiredTriggerGameStartsView'; import { WiredTriggeScoreAchievedView } from './WiredTriggerScoreAchievedView'; @@ -21,6 +27,8 @@ export const WiredTriggerLayoutView = (code: number) => { case WiredTriggerLayout.AVATAR_ENTERS_ROOM: return ; + case WiredTriggerLayout.AVATAR_LEAVES_ROOM: + return ; case WiredTriggerLayout.AVATAR_SAYS_SOMETHING: return ; case WiredTriggerLayout.AVATAR_WALKS_OFF_FURNI: @@ -31,12 +39,22 @@ export const WiredTriggerLayoutView = (code: number) => return ; case WiredTriggerLayout.BOT_REACHED_STUFF: return ; + case WiredTriggerLayout.CLICK_FURNI: + return ; + case WiredTriggerLayout.CLICK_TILE: + return ; + case WiredTriggerLayout.CLICK_USER: + return ; + case WiredTriggerLayout.USER_PERFORMS_ACTION: + return ; case WiredTriggerLayout.COLLISION: return ; case WiredTriggerLayout.EXECUTE_ONCE: return ; case WiredTriggerLayout.EXECUTE_PERIODICALLY: return ; + case WiredTriggerLayout.EXECUTE_PERIODICALLY_SHORT: + return ; case WiredTriggerLayout.EXECUTE_PERIODICALLY_LONG: return ; case WiredTriggerLayout.GAME_ENDS: diff --git a/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx b/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx index 4748481..01cb8a4 100644 --- a/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx +++ b/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx @@ -1,8 +1,34 @@ -import { FC } from 'react'; -import { WiredFurniType } from '../../../../api'; +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; import { WiredTriggerBaseView } from './WiredTriggerBaseView'; -export const WiredTriggerToggleFurniView: FC<{}> = props => +export const WiredTriggerToggleFurniView: FC<{}> = () => { - return ; + const [ triggerMode, setTriggerMode ] = useState(0); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ triggerMode ]); + + useEffect(() => + { + setTriggerMode((trigger?.intData?.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + +
+ { LocalizeText('wiredfurni.params.condition.state') } +
+ setTriggerMode(1) } /> + { LocalizeText('wiredfurni.params.state_trigger.1') } +
+
+ setTriggerMode(0) } /> + { LocalizeText('wiredfurni.params.state_trigger.0') } +
+
+
+ ); }; diff --git a/src/components/wired/views/triggers/WiredTriggerUserPerformsActionView.tsx b/src/components/wired/views/triggers/WiredTriggerUserPerformsActionView.tsx new file mode 100644 index 0000000..5e68651 --- /dev/null +++ b/src/components/wired/views/triggers/WiredTriggerUserPerformsActionView.tsx @@ -0,0 +1,103 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +const ACTION_WAVE = 1; +const ACTION_BLOW_KISS = 2; +const ACTION_LAUGH = 3; +const ACTION_AWAKE = 4; +const ACTION_RELAX = 5; +const ACTION_SIT = 6; +const ACTION_STAND = 7; +const ACTION_LAY = 8; +const ACTION_SIGN = 9; +const ACTION_DANCE = 10; +const ACTION_THUMB_UP = 11; + +const ACTION_OPTIONS = [ + { value: ACTION_WAVE, label: 'widget.memenu.wave' }, + { value: ACTION_BLOW_KISS, label: 'widget.memenu.blow' }, + { value: ACTION_LAUGH, label: 'widget.memenu.laugh' }, + { value: ACTION_THUMB_UP, label: 'widget.memenu.thumb' }, + { value: ACTION_AWAKE, label: 'wiredfurni.params.action.4' }, + { value: ACTION_RELAX, label: 'avatar.widget.random_walk' }, + { value: ACTION_SIT, label: 'widget.memenu.sit' }, + { value: ACTION_STAND, label: 'widget.memenu.stand' }, + { value: ACTION_LAY, label: 'wiredfurni.params.action.8' }, + { value: ACTION_SIGN, label: 'widget.memenu.sign' }, + { value: ACTION_DANCE, label: 'widget.memenu.dance' } +]; + +const SIGN_OPTIONS = Array.from({ length: 18 }, (_, value) => ({ + value, + label: `wiredfurni.params.action.sign.${ value }` +})); + +const DANCE_OPTIONS = [ + { value: 1, label: 'widget.memenu.dance1' }, + { value: 2, label: 'widget.memenu.dance2' }, + { value: 3, label: 'widget.memenu.dance3' }, + { value: 4, label: 'widget.memenu.dance4' } +]; + +export const WiredTriggerUserPerformsActionView: FC<{}> = () => +{ + const [ selectedAction, setSelectedAction ] = useState(ACTION_WAVE); + const [ signFilterEnabled, setSignFilterEnabled ] = useState(false); + const [ signId, setSignId ] = useState(0); + const [ danceFilterEnabled, setDanceFilterEnabled ] = useState(false); + const [ danceId, setDanceId ] = useState(1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ + selectedAction, + signFilterEnabled ? 1 : 0, + signId, + danceFilterEnabled ? 1 : 0, + danceId + ]); + + useEffect(() => + { + setSelectedAction((trigger?.intData?.length > 0) ? trigger.intData[0] : ACTION_WAVE); + setSignFilterEnabled((trigger?.intData?.length > 1) ? (trigger.intData[1] === 1) : false); + setSignId((trigger?.intData?.length > 2) ? trigger.intData[2] : 0); + setDanceFilterEnabled((trigger?.intData?.length > 3) ? (trigger.intData[3] === 1) : false); + setDanceId((trigger?.intData?.length > 4) ? trigger.intData[4] : 1); + }, [ trigger ]); + + return ( + +
+ Action + +
+ { (selectedAction === ACTION_SIGN) && +
+
+ setSignFilterEnabled(event.target.checked) } /> + { LocalizeText('wiredfurni.params.sign_filter') } +
+ { signFilterEnabled && + } +
} + { (selectedAction === ACTION_DANCE) && +
+
+ setDanceFilterEnabled(event.target.checked) } /> + { LocalizeText('wiredfurni.params.dance_filter') } +
+ { danceFilterEnabled && + } +
} +
+ ); +}; From f4ddf406ad89b408212cb0c2051a07c84fe68000 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Tue, 17 Mar 2026 03:28:00 +0100 Subject: [PATCH 16/33] feat(wired-ui): add freeze and furni movement action views - add UI support for FREEZE, UNFREEZE, FURNI_TO_USER, USER_TO_FURNI and FURNI_TO_FURNI - add secondary furni source 101 and dual furni-source labels for furni-to-furni targeting - extend source selectors for custom source sets and titles - add green primary and blue secondary wired highlights - clear wired highlights globally on close, reopen and save to avoid stuck selections --- src/api/wired/WiredActionLayoutCode.ts | 5 + src/api/wired/WiredSelectionVisualizer.ts | 71 +++++- src/components/wired/views/WiredBaseView.tsx | 8 +- .../wired/views/WiredSourcesSelector.tsx | 52 ++-- .../views/actions/WiredActionFreezeView.tsx | 54 +++++ .../actions/WiredActionFurniToFurniView.tsx | 222 ++++++++++++++++++ .../views/actions/WiredActionLayoutView.tsx | 13 + .../views/actions/WiredActionUnfreezeView.tsx | 26 ++ src/hooks/wired/useWired.ts | 2 + 9 files changed, 426 insertions(+), 27 deletions(-) create mode 100644 src/components/wired/views/actions/WiredActionFreezeView.tsx create mode 100644 src/components/wired/views/actions/WiredActionFurniToFurniView.tsx create mode 100644 src/components/wired/views/actions/WiredActionUnfreezeView.tsx diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index 3240a00..c9d4324 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -32,4 +32,9 @@ export class WiredActionLayoutCode public static USERS_AREA_SELECTOR: number = 31; public static USERS_NEIGHBORHOOD_SELECTOR: number = 32; public static SEND_SIGNAL: number = 33; + public static FREEZE: number = 34; + public static UNFREEZE: number = 35; + public static FURNI_TO_USER: number = 36; + public static USER_TO_FURNI: number = 37; + public static FURNI_TO_FURNI: number = 38; } diff --git a/src/api/wired/WiredSelectionVisualizer.ts b/src/api/wired/WiredSelectionVisualizer.ts index 18edbf7..06aeb57 100644 --- a/src/api/wired/WiredSelectionVisualizer.ts +++ b/src/api/wired/WiredSelectionVisualizer.ts @@ -3,25 +3,42 @@ import { GetRoomEngine, IRoomObject, IRoomObjectSpriteVisualization, RoomObjectC export class WiredSelectionVisualizer { private static _selectionShader: WiredFilter = new WiredFilter({ - lineColor: [ 1, 1, 1 ], - color: [ 0.6, 0.6, 0.6 ] + lineColor: [ 0.45, 0.95, 0.55 ], + color: [ 0.18, 0.78, 0.30 ] + }); + private static _secondarySelectionShader: WiredFilter = new WiredFilter({ + lineColor: [ 0.45, 0.78, 1 ], + color: [ 0.20, 0.52, 0.95 ] }); public static show(furniId: number): void { - WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader); } public static hide(furniId: number): void { - WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + const roomObject = WiredSelectionVisualizer.getRoomObject(furniId); + + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._selectionShader); + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._secondarySelectionShader); + } + + public static showSecondary(furniId: number): void + { + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); + } + + public static hideSecondary(furniId: number): void + { + WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); } public static clearSelectionShaderFromFurni(furniIds: number[]): void { for(const furniId of furniIds) { - WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader); } } @@ -29,7 +46,39 @@ export class WiredSelectionVisualizer { for(const furniId of furniIds) { - WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader); + } + } + + public static clearSecondarySelectionShaderFromFurni(furniIds: number[]): void + { + for(const furniId of furniIds) + { + WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); + } + } + + public static applySecondarySelectionShaderToFurni(furniIds: number[]): void + { + for(const furniId of furniIds) + { + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); + } + } + + public static clearAllSelectionShaders(): void + { + const roomEngine = GetRoomEngine(); + const roomId = roomEngine.activeRoomId; + + if(roomId < 0) return; + + const roomObjects = roomEngine.getRoomObjects(roomId, RoomObjectCategory.FLOOR); + + for(const roomObject of roomObjects) + { + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._selectionShader); + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._secondarySelectionShader); } } @@ -40,7 +89,7 @@ export class WiredSelectionVisualizer return roomEngine.getRoomObject(roomEngine.activeRoomId, objectId, RoomObjectCategory.FLOOR); } - private static applySelectionShader(roomObject: IRoomObject): void + private static applySelectionShader(roomObject: IRoomObject, filter: WiredFilter): void { if(!roomObject) return; @@ -54,13 +103,15 @@ export class WiredSelectionVisualizer if(!sprite.filters) sprite.filters = []; - sprite.filters.push(WiredSelectionVisualizer._selectionShader); + if(sprite.filters.includes(filter)) continue; + + sprite.filters.push(filter); sprite.increaseUpdateCounter(); } } - private static clearSelectionShader(roomObject: IRoomObject): void + private static clearSelectionShader(roomObject: IRoomObject, filter: WiredFilter): void { if(!roomObject) return; @@ -72,7 +123,7 @@ export class WiredSelectionVisualizer { if(!sprite.filters) continue; - const index = sprite.filters.indexOf(WiredSelectionVisualizer._selectionShader); + const index = sprite.filters.indexOf(filter); if(index >= 0) { diff --git a/src/components/wired/views/WiredBaseView.tsx b/src/components/wired/views/WiredBaseView.tsx index 4c4f0db..f9477af 100644 --- a/src/components/wired/views/WiredBaseView.tsx +++ b/src/components/wired/views/WiredBaseView.tsx @@ -24,7 +24,11 @@ export const WiredBaseView: FC> = props => const [ needsSave, setNeedsSave ] = useState(false); const { trigger = null, setTrigger = null, setIntParams = null, setStringParam = null, setFurniIds = null, setAllowsFurni = null, saveWired = null } = useWired(); - const onClose = () => setTrigger(null); + const onClose = () => + { + WiredSelectionVisualizer.clearAllSelectionShaders(); + setTrigger(null); + }; const onSave = () => { @@ -48,6 +52,8 @@ export const WiredBaseView: FC> = props => { if(!trigger) return; + WiredSelectionVisualizer.clearAllSelectionShaders(); + const spriteId = (trigger.spriteId || -1); const furniData = GetSessionDataManager().getFloorItemData(spriteId); diff --git a/src/components/wired/views/WiredSourcesSelector.tsx b/src/components/wired/views/WiredSourcesSelector.tsx index db072a6..2d5ebc1 100644 --- a/src/components/wired/views/WiredSourcesSelector.tsx +++ b/src/components/wired/views/WiredSourcesSelector.tsx @@ -16,45 +16,66 @@ export const USER_SOURCES = [ { value: 201, label: 'wiredfurni.params.sources.users.201' } ]; +export interface WiredSourceOption +{ + value: number; + label: string; +} + interface WiredSourcesSelectorProps { showFurni?: boolean; showUsers?: boolean; furniSource?: number; userSource?: number; + furniTitle?: string; + usersTitle?: string; + furniSources?: WiredSourceOption[]; + userSources?: WiredSourceOption[]; onChangeFurni?: (source: number) => void; onChangeUsers?: (source: number) => void; } export const WiredSourcesSelector: FC = props => { - const { showFurni = false, showUsers = false, furniSource = 0, userSource = 0, onChangeFurni = null, onChangeUsers = null } = props; + const { + showFurni = false, + showUsers = false, + furniSource = 0, + userSource = 0, + furniTitle = 'wiredfurni.params.sources.furni.title', + usersTitle = 'wiredfurni.params.sources.users.title', + furniSources = FURNI_SOURCES, + userSources = USER_SOURCES, + onChangeFurni = null, + onChangeUsers = null + } = props; - const furniIndex = Math.max(0, FURNI_SOURCES.findIndex(s => s.value === furniSource)); - const userIndex = Math.max(0, USER_SOURCES.findIndex(s => s.value === userSource)); + const furniIndex = Math.max(0, furniSources.findIndex(s => s.value === furniSource)); + const userIndex = Math.max(0, userSources.findIndex(s => s.value === userSource)); const prevFurni = () => { - const next = (furniIndex - 1 + FURNI_SOURCES.length) % FURNI_SOURCES.length; - onChangeFurni && onChangeFurni(FURNI_SOURCES[next].value); + const next = (furniIndex - 1 + furniSources.length) % furniSources.length; + onChangeFurni && onChangeFurni(furniSources[next].value); }; const nextFurni = () => { - const next = (furniIndex + 1) % FURNI_SOURCES.length; - onChangeFurni && onChangeFurni(FURNI_SOURCES[next].value); + const next = (furniIndex + 1) % furniSources.length; + onChangeFurni && onChangeFurni(furniSources[next].value); }; const prevUsers = () => { - const next = (userIndex - 1 + USER_SOURCES.length) % USER_SOURCES.length; - onChangeUsers && onChangeUsers(USER_SOURCES[next].value); + const next = (userIndex - 1 + userSources.length) % userSources.length; + onChangeUsers && onChangeUsers(userSources[next].value); }; const nextUsers = () => { - const next = (userIndex + 1) % USER_SOURCES.length; - onChangeUsers && onChangeUsers(USER_SOURCES[next].value); + const next = (userIndex + 1) % userSources.length; + onChangeUsers && onChangeUsers(userSources[next].value); }; if(!showFurni && !showUsers) return null; @@ -63,11 +84,11 @@ export const WiredSourcesSelector: FC = props =>
{ showFurni && <> - { LocalizeText('wiredfurni.params.sources.furni.title') } + { LocalizeText(furniTitle) }
- { LocalizeText(FURNI_SOURCES[furniIndex].label) } + { LocalizeText(furniSources[furniIndex].label) }
@@ -77,11 +98,11 @@ export const WiredSourcesSelector: FC = props => { showUsers && <> - { LocalizeText('wiredfurni.params.sources.users.title') } + { LocalizeText(usersTitle) }
- { LocalizeText(USER_SOURCES[userIndex].label) } + { LocalizeText(userSources[userIndex].label) }
@@ -89,4 +110,3 @@ export const WiredSourcesSelector: FC = props =>
); }; - diff --git a/src/components/wired/views/actions/WiredActionFreezeView.tsx b/src/components/wired/views/actions/WiredActionFreezeView.tsx new file mode 100644 index 0000000..716acaa --- /dev/null +++ b/src/components/wired/views/actions/WiredActionFreezeView.tsx @@ -0,0 +1,54 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; + +const EFFECT_OPTIONS = [ + { value: 218, label: 'fx_218' }, + { value: 12, label: 'fx_12' }, + { value: 11, label: 'fx_11' }, + { value: 53, label: 'fx_53' }, + { value: 163, label: 'fx_163' } +]; + +export const WiredActionFreezeView: FC<{}> = () => +{ + const [ effectId, setEffectId ] = useState(218); + const [ cancelOnTeleport, setCancelOnTeleport ] = useState(false); + const [ userSource, setUserSource ] = useState(0); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ + effectId, + cancelOnTeleport ? 1 : 0, + userSource + ]); + + useEffect(() => + { + setEffectId((trigger?.intData?.length > 0) ? trigger.intData[0] : 218); + setCancelOnTeleport((trigger?.intData?.length > 1) ? (trigger.intData[1] === 1) : false); + setUserSource((trigger?.intData?.length > 2) ? trigger.intData[2] : 0); + }, [ trigger ]); + + return ( + }> +
+ Effect + +
+
+ setCancelOnTeleport(event.target.checked) } /> + { LocalizeText('wiredfurni.params.freeze.cancel_on_teleport') } +
+
+ ); +}; diff --git a/src/components/wired/views/actions/WiredActionFurniToFurniView.tsx b/src/components/wired/views/actions/WiredActionFurniToFurniView.tsx new file mode 100644 index 0000000..5f7cefc --- /dev/null +++ b/src/components/wired/views/actions/WiredActionFurniToFurniView.tsx @@ -0,0 +1,222 @@ +import { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api'; +import { Button, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector, FURNI_SOURCES, WiredSourceOption } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const SOURCE_TRIGGER = 0; +const SOURCE_SELECTED = 100; +const SOURCE_SECONDARY_SELECTED = 101; +const FURNI_DELIMITER = ';'; + +const TARGET_FURNI_SOURCES: WiredSourceOption[] = [ + { value: 0, label: 'wiredfurni.params.sources.furni.0' }, + { value: SOURCE_SECONDARY_SELECTED, label: 'wiredfurni.params.sources.furni.101' }, + { value: 200, label: 'wiredfurni.params.sources.furni.200' }, + { value: 201, label: 'wiredfurni.params.sources.furni.201' } +]; + +type SelectionMode = 'move' | 'target'; + +const parseIds = (data: string): number[] => +{ + if(!data || !data.length) return []; + + const ids = new Set(); + + for(const part of data.split(/[;,\t]/)) + { + const trimmed = part.trim(); + if(!trimmed.length) continue; + + const value = parseInt(trimmed, 10); + if(!isNaN(value) && value > 0) ids.add(value); + } + + return Array.from(ids); +}; + +const serializeIds = (ids: number[]): string => +{ + if(!ids || !ids.length) return ''; + + return ids.filter(id => (id > 0)).join(FURNI_DELIMITER); +}; + +export const WiredActionFurniToFurniView: FC<{}> = () => +{ + const [ moveSource, setMoveSource ] = useState(SOURCE_TRIGGER); + const [ targetSource, setTargetSource ] = useState(SOURCE_TRIGGER); + const [ moveFurniIds, setMoveFurniIds ] = useState([]); + const [ targetFurniIds, setTargetFurniIds ] = useState([]); + const [ selectionMode, setSelectionMode ] = useState('move'); + + const highlightedIds = useRef([]); + + const { trigger = null, furniIds = [], setFurniIds, setIntParams, setStringParam, setAllowsFurni } = useWired(); + + const syncHighlights = useCallback((nextMoveIds: number[], nextTargetIds: number[]) => + { + if(highlightedIds.current.length) + { + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + } + + const targetSet = new Set(nextTargetIds); + const moveOnlyIds = nextMoveIds.filter(id => !targetSet.has(id)); + + if(moveOnlyIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(moveOnlyIds); + if(nextTargetIds.length) WiredSelectionVisualizer.applySecondarySelectionShaderToFurni(nextTargetIds); + + highlightedIds.current = Array.from(new Set([ ...nextMoveIds, ...nextTargetIds ])); + }, []); + + const switchSelection = useCallback((mode: SelectionMode) => + { + const canEditMove = (moveSource === SOURCE_SELECTED); + const canEditTarget = (targetSource === SOURCE_SECONDARY_SELECTED); + + if(mode === 'move' && !canEditMove) return; + if(mode === 'target' && !canEditTarget) return; + + setSelectionMode(mode); + setFurniIds([ ...(mode === 'move' ? moveFurniIds : targetFurniIds) ]); + }, [ moveSource, targetSource, moveFurniIds, targetFurniIds, setFurniIds ]); + + useEffect(() => + { + if(!trigger) return; + + const nextMoveIds = trigger.selectedItems ?? []; + const nextTargetIds = parseIds(trigger.stringData); + const nextMoveSource = (trigger.intData.length >= 1) + ? trigger.intData[0] + : (nextMoveIds.length ? SOURCE_SELECTED : SOURCE_TRIGGER); + const nextTargetSourceRaw = (trigger.intData.length >= 2) + ? trigger.intData[1] + : (nextTargetIds.length ? SOURCE_SECONDARY_SELECTED : SOURCE_TRIGGER); + const nextTargetSource = (nextTargetSourceRaw === SOURCE_SELECTED) ? SOURCE_SECONDARY_SELECTED : nextTargetSourceRaw; + + setMoveSource(nextMoveSource); + setTargetSource(nextTargetSource); + setMoveFurniIds(nextMoveIds); + setTargetFurniIds(nextTargetIds); + setSelectionMode('move'); + setFurniIds([ ...nextMoveIds ]); + }, [ trigger, setFurniIds ]); + + useEffect(() => + { + if(selectionMode === 'move') setMoveFurniIds(furniIds); + else setTargetFurniIds(furniIds); + }, [ furniIds, selectionMode ]); + + useEffect(() => + { + syncHighlights(moveFurniIds, targetFurniIds); + }, [ moveFurniIds, targetFurniIds, syncHighlights ]); + + useEffect(() => + { + const canEditMove = (moveSource === SOURCE_SELECTED); + const canEditTarget = (targetSource === SOURCE_SECONDARY_SELECTED); + + if(selectionMode === 'move' && !canEditMove && canEditTarget) + { + switchSelection('target'); + return; + } + + if(selectionMode === 'target' && !canEditTarget && canEditMove) + { + switchSelection('move'); + return; + } + + const canEditCurrent = ((selectionMode === 'move') ? canEditMove : canEditTarget); + setAllowsFurni(canEditCurrent ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE); + }, [ selectionMode, moveSource, targetSource, switchSelection, setAllowsFurni ]); + + useEffect(() => + { + return () => + { + if(!highlightedIds.current.length) return; + + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + highlightedIds.current = []; + }; + }, []); + + const save = useCallback(() => + { + if(selectionMode === 'target') + { + setSelectionMode('move'); + setFurniIds([ ...moveFurniIds ]); + } + + setIntParams([ + moveSource, + targetSource + ]); + + setStringParam(serializeIds(targetFurniIds)); + }, [ selectionMode, moveFurniIds, moveSource, targetSource, targetFurniIds, setFurniIds, setIntParams, setStringParam ]); + + const selectionLimit = trigger?.maximumItemSelectionCount ?? 0; + + return ( + + +
+ setTargetSource((value === SOURCE_SELECTED) ? SOURCE_SECONDARY_SELECTED : value) } /> +
+ }> +
+
+ { LocalizeText('wiredfurni.params.sources.furni.title.mv.0') } +
+ + { selectionLimit ? `${ moveFurniIds.length }/${ selectionLimit }` : moveFurniIds.length } +
+
+
+ { LocalizeText('wiredfurni.params.sources.furni.title.mv.1') } +
+ + { selectionLimit ? `${ targetFurniIds.length }/${ selectionLimit }` : targetFurniIds.length } +
+
+
+ + ); +}; diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index 23c16d1..23d853a 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -1,5 +1,7 @@ import { WiredActionLayoutCode } from '../../../../api'; import { WiredActionBotChangeFigureView } from './WiredActionBotChangeFigureView'; +import { WiredActionFreezeView } from './WiredActionFreezeView'; +import { WiredActionFurniToFurniView } from './WiredActionFurniToFurniView'; import { WiredActionSendSignalView } from './WiredActionSendSignalView'; import { WiredActionFurniAreaView } from '../selectors/WiredActionFurniAreaView'; import { WiredSelectorFurniNeighborhoodView } from '../selectors/WiredSelectorFurniNeighborhoodView'; @@ -30,6 +32,7 @@ import { WiredActionResetView } from './WiredActionResetView'; import { WiredActionSetFurniStateToView } from './WiredActionSetFurniStateToView'; import { WiredActionTeleportView } from './WiredActionTeleportView'; import { WiredActionToggleFurniStateView } from './WiredActionToggleFurniStateView'; +import { WiredActionUnfreezeView } from './WiredActionUnfreezeView'; export const WiredActionLayoutView = (code: number) => { @@ -57,6 +60,12 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.FLEE: return ; + case WiredActionLayoutCode.FREEZE: + return ; + case WiredActionLayoutCode.FURNI_TO_USER: + return ; + case WiredActionLayoutCode.FURNI_TO_FURNI: + return ; case WiredActionLayoutCode.GIVE_REWARD: return ; case WiredActionLayoutCode.GIVE_SCORE: @@ -85,6 +94,10 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.TOGGLE_FURNI_STATE: return ; + case WiredActionLayoutCode.UNFREEZE: + return ; + case WiredActionLayoutCode.USER_TO_FURNI: + return ; case WiredActionLayoutCode.FURNI_AREA_SELECTOR: return ; case WiredActionLayoutCode.FURNI_NEIGHBORHOOD_SELECTOR: diff --git a/src/components/wired/views/actions/WiredActionUnfreezeView.tsx b/src/components/wired/views/actions/WiredActionUnfreezeView.tsx new file mode 100644 index 0000000..98b31a6 --- /dev/null +++ b/src/components/wired/views/actions/WiredActionUnfreezeView.tsx @@ -0,0 +1,26 @@ +import { FC, useEffect, useState } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; + +export const WiredActionUnfreezeView: FC<{}> = () => +{ + const [ userSource, setUserSource ] = useState(0); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ userSource ]); + + useEffect(() => + { + setUserSource((trigger?.intData?.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + } /> + ); +}; diff --git a/src/hooks/wired/useWired.ts b/src/hooks/wired/useWired.ts index b0c3f32..19e6ed8 100644 --- a/src/hooks/wired/useWired.ts +++ b/src/hooks/wired/useWired.ts @@ -235,6 +235,7 @@ const useWiredState = () => { const parser = event.getParser(); + WiredSelectionVisualizer.clearAllSelectionShaders(); setTrigger(null); }); @@ -275,6 +276,7 @@ const useWiredState = () => return () => { + WiredSelectionVisualizer.clearAllSelectionShaders(); setIntParams([]); setStringParam(''); setActionDelay(0); From 95ec51b41da3a4f1c27e9949fa411afcf774195b Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Wed, 18 Mar 2026 14:38:21 +0100 Subject: [PATCH 17/33] feat(wired-ui): add altitude and relative move actions --- src/api/wired/WiredActionLayoutCode.ts | 2 + src/common/Slider.tsx | 22 ++- .../views/actions/WiredActionLayoutView.tsx | 6 + .../actions/WiredActionRelativeMoveView.tsx | 120 +++++++++++++ .../actions/WiredActionSetAltitudeView.tsx | 162 ++++++++++++++++++ 5 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 src/components/wired/views/actions/WiredActionRelativeMoveView.tsx create mode 100644 src/components/wired/views/actions/WiredActionSetAltitudeView.tsx diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index 3240a00..9e3cee2 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -32,4 +32,6 @@ export class WiredActionLayoutCode public static USERS_AREA_SELECTOR: number = 31; public static USERS_NEIGHBORHOOD_SELECTOR: number = 32; public static SEND_SIGNAL: number = 33; + public static SET_ALTITUDE: number = 39; + public static RELATIVE_MOVE: number = 40; } diff --git a/src/common/Slider.tsx b/src/common/Slider.tsx index 50cba28..206c0a5 100644 --- a/src/common/Slider.tsx +++ b/src/common/Slider.tsx @@ -11,11 +11,25 @@ export interface SliderProps extends ReactSliderProps export const Slider: FC = props => { - const { disabledButton, max, min, value, onChange, ...rest } = props; + const { disabledButton, max, min, step, value, onChange, ...rest } = props; + const currentValue = Array.isArray(value) ? value[0] : ((typeof value === 'number') ? value : 0); + const minimum = (typeof min === 'number') ? min : 0; + const maximum = (typeof max === 'number') ? max : 0; + const buttonStep = ((typeof step === 'number') && (step > 0)) ? step : 1; + + const roundToStep = (nextValue: number) => + { + if(typeof buttonStep !== 'number') return nextValue; + + const decimalStep = buttonStep.toString(); + const precision = decimalStep.includes('.') ? (decimalStep.length - decimalStep.indexOf('.') - 1) : 0; + + return parseFloat(nextValue.toFixed(precision)); + }; return - { !disabledButton && } - - { !disabledButton && } + { !disabledButton && } + + { !disabledButton && } ; } diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index 23c16d1..e4d14db 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -1,5 +1,6 @@ import { WiredActionLayoutCode } from '../../../../api'; import { WiredActionBotChangeFigureView } from './WiredActionBotChangeFigureView'; +import { WiredActionSetAltitudeView } from './WiredActionSetAltitudeView'; import { WiredActionSendSignalView } from './WiredActionSendSignalView'; import { WiredActionFurniAreaView } from '../selectors/WiredActionFurniAreaView'; import { WiredSelectorFurniNeighborhoodView } from '../selectors/WiredSelectorFurniNeighborhoodView'; @@ -26,6 +27,7 @@ import { WiredActionMoveAndRotateFurniView } from './WiredActionMoveAndRotateFur import { WiredActionMoveFurniToView } from './WiredActionMoveFurniToView'; import { WiredActionMoveFurniView } from './WiredActionMoveFurniView'; import { WiredActionMuteUserView } from './WiredActionMuteUserView'; +import { WiredActionRelativeMoveView } from './WiredActionRelativeMoveView'; import { WiredActionResetView } from './WiredActionResetView'; import { WiredActionSetFurniStateToView } from './WiredActionSetFurniStateToView'; import { WiredActionTeleportView } from './WiredActionTeleportView'; @@ -57,6 +59,8 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.FLEE: return ; + case WiredActionLayoutCode.SET_ALTITUDE: + return ; case WiredActionLayoutCode.GIVE_REWARD: return ; case WiredActionLayoutCode.GIVE_SCORE: @@ -77,6 +81,8 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.MUTE_USER: return ; + case WiredActionLayoutCode.RELATIVE_MOVE: + return ; case WiredActionLayoutCode.RESET: return ; case WiredActionLayoutCode.SET_FURNI_STATE: diff --git a/src/components/wired/views/actions/WiredActionRelativeMoveView.tsx b/src/components/wired/views/actions/WiredActionRelativeMoveView.tsx new file mode 100644 index 0000000..c6f33a1 --- /dev/null +++ b/src/components/wired/views/actions/WiredActionRelativeMoveView.tsx @@ -0,0 +1,120 @@ +import { FC, useEffect, useState } from 'react'; +import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp } from 'react-icons/fa'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const MAX_DISTANCE = 20; + +const HORIZONTAL_OPTIONS = [ + { value: 0, icon: }, + { value: 1, icon: } +]; + +const VERTICAL_OPTIONS = [ + { value: 0, icon: }, + { value: 1, icon: } +]; + +const normalizeDirection = (value: number, fallback = 1) => +{ + if(value === 0 || value === 1) return value; + + return fallback; +}; + +const normalizeDistance = (value: number) => +{ + if(isNaN(value)) return 0; + + return Math.max(0, Math.min(MAX_DISTANCE, value)); +}; + +export const WiredActionRelativeMoveView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + + const [horizontalDirection, setHorizontalDirection] = useState(1); + const [horizontalDistance, setHorizontalDistance] = useState(0); + const [verticalDirection, setVerticalDirection] = useState(1); + const [verticalDistance, setVerticalDistance] = useState(0); + const [ furniSource, setFurniSource ] = useState(() => + { + if(trigger?.intData?.length > 4) return trigger.intData[4]; + return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; + }); + + useEffect(() => + { + if(!trigger) return; + + setHorizontalDirection((trigger.intData.length > 0) ? normalizeDirection(trigger.intData[0], 1) : 1); + setHorizontalDistance((trigger.intData.length > 1) ? normalizeDistance(trigger.intData[1]) : 0); + setVerticalDirection((trigger.intData.length > 2) ? normalizeDirection(trigger.intData[2], 1) : 1); + setVerticalDistance((trigger.intData.length > 3) ? normalizeDistance(trigger.intData[3]) : 0); + + if(trigger.intData.length > 4) setFurniSource(trigger.intData[4]); + else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0); + }, [ trigger ]); + + const save = () => setIntParams([ + horizontalDirection, + horizontalDistance, + verticalDirection, + verticalDistance, + furniSource + ]); + + return ( + }> +
+ { LocalizeText('wiredfurni.params.movement.horizontal.selection') } +
+ { HORIZONTAL_OPTIONS.map(option => + { + return ( + + ); + }) } +
+ { LocalizeText('wiredfurni.params.movement.horizontal.distance', [ 'distance' ], [ horizontalDistance.toString() ]) } + setHorizontalDistance(value as number) } /> +
+
+ { LocalizeText('wiredfurni.params.movement.vertical.selection') } +
+ { VERTICAL_OPTIONS.map(option => + { + return ( + + ); + }) } +
+ { LocalizeText('wiredfurni.params.movement.vertical.distance', [ 'distance' ], [ verticalDistance.toString() ]) } + setVerticalDistance(value as number) } /> +
+
+ ); +}; diff --git a/src/components/wired/views/actions/WiredActionSetAltitudeView.tsx b/src/components/wired/views/actions/WiredActionSetAltitudeView.tsx new file mode 100644 index 0000000..3e4017b --- /dev/null +++ b/src/components/wired/views/actions/WiredActionSetAltitudeView.tsx @@ -0,0 +1,162 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const MIN_ALTITUDE = 0; +const MAX_ALTITUDE = 40; +const ALTITUDE_STEP = 0.01; +const ALTITUDE_PATTERN = /^\d*(\.\d{0,2})?$/; + +const clampAltitude = (value: number) => +{ + if(isNaN(value)) return MIN_ALTITUDE; + + const clamped = Math.min(MAX_ALTITUDE, Math.max(MIN_ALTITUDE, value)); + + return parseFloat(clamped.toFixed(2)); +}; + +const formatAltitude = (value: number) => +{ + const normalized = clampAltitude(value); + const text = normalized.toFixed(2); + + return text.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1'); +}; + +const parseAltitude = (value: string) => +{ + if(!value || !value.trim().length) return 0; + + const parsed = parseFloat(value); + + if(isNaN(parsed)) return 0; + + return clampAltitude(parsed); +}; + +const OPERATOR_OPTIONS = [ + { value: 0, label: 'wiredfurni.params.operator.0' }, + { value: 1, label: 'wiredfurni.params.operator.1' }, + { value: 2, label: 'wiredfurni.params.operator.2' } +]; + +const normalizeOperator = (value: number) => +{ + if(value < 0 || value > 2) return 2; + + return value; +}; + +export const WiredActionSetAltitudeView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + + const [ operator, setOperator ] = useState(2); + const [ furniSource, setFurniSource ] = useState(() => + { + if(trigger?.intData?.length > 1) return trigger.intData[1]; + return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; + }); + const [ altitude, setAltitude ] = useState(0); + const [ altitudeInput, setAltitudeInput ] = useState('0'); + + const normalizedAltitudeText = useMemo(() => formatAltitude(altitude), [ altitude ]); + + useEffect(() => + { + if(!trigger) return; + + setOperator((trigger.intData.length > 0) ? normalizeOperator(trigger.intData[0]) : 2); + setFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0)); + + const nextAltitude = parseAltitude(trigger.stringData); + setAltitude(nextAltitude); + setAltitudeInput(formatAltitude(nextAltitude)); + }, [ trigger ]); + + const updateAltitude = (value: number) => + { + const nextValue = clampAltitude(value); + + setAltitude(nextValue); + setAltitudeInput(formatAltitude(nextValue)); + }; + + const updateAltitudeInput = (value: string) => + { + if(!ALTITUDE_PATTERN.test(value)) return; + + setAltitudeInput(value); + + if(!value.length) + { + setAltitude(0); + return; + } + + const parsedValue = parseFloat(value); + + if(isNaN(parsedValue)) return; + + if(parsedValue > MAX_ALTITUDE) + { + updateAltitude(MAX_ALTITUDE); + return; + } + + setAltitude(clampAltitude(parsedValue)); + }; + + const save = () => + { + setIntParams([ + operator, + furniSource + ]); + + setStringParam(normalizedAltitudeText); + }; + + return ( + }> +
+ { OPERATOR_OPTIONS.map(option => + { + return ( +
+ setOperator(option.value) } /> + { LocalizeText(option.label) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.setaltitude') } + setAltitudeInput(formatAltitude(altitude)) } + onChange={ event => updateAltitudeInput(event.target.value) } /> +
+
+ updateAltitude(event as number) } /> + { normalizedAltitudeText } +
+
+ ); +}; From ea35f1994078d0b08f27550b06bde5aa6047bd62 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Wed, 18 Mar 2026 20:11:40 +0100 Subject: [PATCH 18/33] Add UI Customization Panel with full color theming - New "Interfaccia" panel with color picker (HSV + hex/RGB/alpha + 30 presets) - Profile background customization tab - Accent color propagates via CSS variables to: card headers/tabs, context menus, Button dark/primary/gray variants, InfoStand panels, toolbar, room tools, purse, .btn-primary/.btn-dark CSS classes - All elements use var(--name, fallback) for zero visual change when default - Settings persisted in localStorage - Added react-colorful dependency - Added ui-config.json with header images config keys --- package.json | 1 + public/ui-config.json | 2434 +++++++++++++++++ src/App.tsx | 16 +- src/api/ui-settings/IUiSettings.ts | 21 + src/api/ui-settings/UiSettingsContext.tsx | 164 ++ src/api/ui-settings/index.ts | 2 + src/common/Button.tsx | 40 +- src/common/card/NitroCardHeaderView.tsx | 10 +- src/common/card/tabs/NitroCardTabsView.tsx | 12 +- src/components/MainView.tsx | 7 + .../InterfaceColorTabView.tsx | 179 ++ .../InterfaceImageTabView.tsx | 52 + .../InterfaceProfileTabView.tsx | 107 + .../InterfaceSettingsView.tsx | 74 + .../infostand/InfoStandBadgeSlotView.tsx | 3 +- .../infostand/InfoStandWidgetFurniView.tsx | 37 +- .../infostand/InfoStandWidgetUserView.tsx | 20 +- .../context-menu/ContextMenuHeaderView.tsx | 11 +- .../context-menu/ContextMenuListItemView.tsx | 11 +- .../widgets/context-menu/ContextMenuView.tsx | 2 +- src/components/toolbar/ToolbarView.tsx | 60 +- src/css/common/Buttons.css | 14 +- src/css/purse/PurseView.css | 4 +- src/css/room/InfoStand.css | 2 +- src/css/room/RoomWidgets.css | 6 +- 25 files changed, 3192 insertions(+), 97 deletions(-) create mode 100644 public/ui-config.json create mode 100644 src/api/ui-settings/IUiSettings.ts create mode 100644 src/api/ui-settings/UiSettingsContext.tsx create mode 100644 src/api/ui-settings/index.ts create mode 100644 src/components/interface-settings/InterfaceColorTabView.tsx create mode 100644 src/components/interface-settings/InterfaceImageTabView.tsx create mode 100644 src/components/interface-settings/InterfaceProfileTabView.tsx create mode 100644 src/components/interface-settings/InterfaceSettingsView.tsx diff --git a/package.json b/package.json index 4dab2f5..716c4bf 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "framer-motion": "^11.2.12", "react": "^19.2.4", "react-bootstrap": "^2.10.10", + "react-colorful": "^5.6.1", "react-dom": "^19.2.4", "react-icons": "^5.5.0", "react-slider": "^2.0.6", diff --git a/public/ui-config.json b/public/ui-config.json new file mode 100644 index 0000000..cec08d1 --- /dev/null +++ b/public/ui-config.json @@ -0,0 +1,2434 @@ +{ + "external.plugins": [ + "plugins/room-builder.js" + ], + "ui.header.images.count": 30, + "ui.header.images.url": "https://image.webbo.city/image/headerImage/image{id}.gif", + "image.library.notifications.url": "${image.library.url}notifications/%image%.png", + "achievements.images.url": "${image.library.url}Quests/%image%.png", + "camera.url": "/swf/usercontent/camera/", + "thumbnails.url": "/swf/usercontent/thumbnails/%thumbnail%.png", + "url.prefix": "", + "habbopages.url": "/swf/habbopages/", + "group.homepage.url": "${url.prefix}/groups/%groupid%/id", + "guide.help.alpha.groupid": 0, + "chat.viewer.height.percentage": 0.4, + "pathfinder.underpass.height": 1.5, + "widget.dimmer.colorwheel": false, + "avatar.wardrobe.max.slots": 10, + "user.badges.max.slots": 6, + "user.badges.group.slot.enabled": false, + "user.tags.enabled": false, + "camera.publish.disabled": false, + "hc.disabled": false, + "badge.descriptions.enabled": true, + "motto.max.length": 38, + "bot.name.max.length": 15, + "pet.package.name.max.length": 15, + "wired.action.bot.talk.to.avatar.max.length": 64, + "wired.action.bot.talk.max.length": 64, + "wired.action.chat.max.length": 100, + "wired.action.kick.from.room.max.length": 100, + "wired.action.mute.user.max.length": 100, + "game.center.enabled": false, + "guides.enabled": true, + "toolbar.hide.quests": true, + "navigator.room.models": [{ + "clubLevel": 0, + "tileSize": 104, + "name": "a" + }, { + "clubLevel": 0, + "tileSize": 94, + "name": "b" + }, { + "clubLevel": 0, + "tileSize": 36, + "name": "c" + }, { + "clubLevel": 0, + "tileSize": 84, + "name": "d" + }, { + "clubLevel": 0, + "tileSize": 80, + "name": "e" + }, { + "clubLevel": 0, + "tileSize": 80, + "name": "f" + }, { + "clubLevel": 0, + "tileSize": 416, + "name": "i" + }, { + "clubLevel": 0, + "tileSize": 320, + "name": "j" + }, { + "clubLevel": 0, + "tileSize": 448, + "name": "k" + }, { + "clubLevel": 0, + "tileSize": 352, + "name": "l" + }, { + "clubLevel": 0, + "tileSize": 384, + "name": "m" + }, { + "clubLevel": 0, + "tileSize": 372, + "name": "n" + }, { + "clubLevel": 1, + "tileSize": 80, + "name": "g" + }, { + "clubLevel": 1, + "tileSize": 74, + "name": "h" + }, { + "clubLevel": 1, + "tileSize": 416, + "name": "o" + }, { + "clubLevel": 1, + "tileSize": 352, + "name": "p" + }, { + "clubLevel": 1, + "tileSize": 304, + "name": "q" + }, { + "clubLevel": 1, + "tileSize": 336, + "name": "r" + }, { + "clubLevel": 1, + "tileSize": 748, + "name": "u" + }, { + "clubLevel": 1, + "tileSize": 438, + "name": "v" + }, { + "clubLevel": 2, + "tileSize": 540, + "name": "t" + }, { + "clubLevel": 2, + "tileSize": 512, + "name": "w" + }, { + "clubLevel": 2, + "tileSize": 396, + "name": "x" + }, { + "clubLevel": 2, + "tileSize": 440, + "name": "y" + }, { + "clubLevel": 2, + "tileSize": 456, + "name": "z" + }, { + "clubLevel": 2, + "tileSize": 208, + "name": "0" + }, { + "clubLevel": 2, + "tileSize": 1009, + "name": "1" + }, { + "clubLevel": 2, + "tileSize": 1044, + "name": "2" + }, { + "clubLevel": 2, + "tileSize": 183, + "name": "3" + }, { + "clubLevel": 2, + "tileSize": 254, + "name": "4" + }, { + "clubLevel": 2, + "tileSize": 1024, + "name": "5" + }, { + "clubLevel": 2, + "tileSize": 801, + "name": "6" + }, { + "clubLevel": 2, + "tileSize": 354, + "name": "7" + }, { + "clubLevel": 2, + "tileSize": 888, + "name": "8" + }, { + "clubLevel": 2, + "tileSize": 926, + "name": "9" + } + ], + "backgrounds.data": [{ + "backgroundId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 2, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 3, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 4, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 5, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 6, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 7, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 8, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 9, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 10, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 11, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 12, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 13, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 14, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 15, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 16, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 17, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 18, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 19, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 20, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 21, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 22, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 23, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 24, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 25, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 26, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 27, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 28, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 29, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 30, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 31, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 32, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 33, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 34, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 35, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 36, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 37, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 38, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 39, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 40, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 41, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 42, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 43, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 44, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 45, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 46, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 47, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 48, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 49, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 50, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 51, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 52, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 53, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 54, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 55, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 56, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 57, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 58, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 59, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 60, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 61, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 62, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 63, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 64, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 65, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 66, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 67, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 68, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 69, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 70, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 71, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 72, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 73, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 74, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 75, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 76, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 77, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 78, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 79, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 80, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 81, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 82, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 83, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 84, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 85, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 86, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 87, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 88, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 89, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 90, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 91, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 92, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 93, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 94, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 95, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 96, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 97, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 98, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 99, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 100, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 101, + "minRank": 2, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 102, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 103, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 104, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 105, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 106, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 107, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 108, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 109, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 110, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 111, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 112, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 113, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 114, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 115, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 116, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 117, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 118, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 119, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 120, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 121, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 122, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 123, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 124, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 125, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 126, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 127, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 128, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 129, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 130, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 131, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 132, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 133, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 134, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 135, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 136, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 137, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 138, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 139, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 140, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 141, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 142, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 143, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 144, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 145, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 146, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 147, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 148, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 149, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 150, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 151, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 152, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 153, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 154, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 155, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 156, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 157, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 158, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 159, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 160, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 161, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 162, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 163, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 164, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 165, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 166, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 167, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 168, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 169, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 170, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 171, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 172, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 173, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 174, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 175, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 176, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 177, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 178, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 179, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 180, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 181, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 182, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 183, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 184, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 185, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 186, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 187, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "stands.data": [{ + "standId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 2, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 3, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 4, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 5, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 6, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 7, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 8, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 9, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 10, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 11, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 12, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 13, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 14, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 15, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 16, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 17, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 18, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 19, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 20, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 21, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "overlays.data": [{ + "overlayId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "overlayId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "overlayId": 2, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 3, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 4, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 5, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 6, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 7, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 8, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "hotelview": { + "room.pool": "791", + "room.picnic": "2193", + "room.rooftop": "", + "room.rooftop.pool": "", + "room.peaceful": "", + "room.infobus": "5956", + "room.lobby": "1450", + "show.avatar": true, + "widgets": { + "slot.1.widget": "", + "slot.1.conf": {}, + "slot.2.widget": "", + "slot.2.conf": { + "image": "", + "texts": "", + "btnLink": "" + }, + "slot.3.widget": "", + "slot.3.conf": {}, + "slot.4.widget": "", + "slot.4.conf": {}, + "slot.5.widget": "", + "slot.5.conf": {}, + "slot.6.widget": "", + "slot.6.conf": { + "campaign": "" + }, + "slot.7.widget": "", + "slot.7.conf": {} + }, + "images": { + "background": "${asset.url}/images/reception/stretch_blue.png", + "background.colour": "#8ee0f0", + "sun": "${asset.url}/images/reception/sun.png", + "drape": "${asset.url}/images/reception/drape.png", + "left": "", + "right": "", + "right.repeat": "" + } + }, + "achievements.unseen.ignored": [ + "ACH_AllTimeHotelPresence" + ], + "avatareditor.show.clubitems.dimmed": true, + "avatareditor.show.clubitems.first": true, + "chat.history.max.items": 100, + "system.currency.types": [ + -1, + 0, + 5 + ], + "catalog.links": { + "hc.buy_hc": "habbo_club", + "hc.hc_gifts": "club_gifts", + "pets.buy_food": "pet_food", + "pets.buy_saddle": "saddles" + }, + "hc.center": { + "benefits.info": true, + "payday.info": true, + "gift.info": true, + "benefits.habbopage": "habboclub", + "payday.habbopage": "hcpayday" + }, + "respect.options": { + "enabled": false, + "sound": "sound_respect_received" + }, + "currency.display.number.short": false, + "currency.asset.icon.url": "${images.url}/wallet/%type%.png", + "catalog.asset.url": "${image.library.url}catalogue", + "catalog.asset.image.url": "${catalog.asset.url}/%name%.gif", + "catalog.asset.icon.url": "${catalog.asset.url}/icon_%name%.png", + "catalog.tab.icons": false, + "catalog.headers": false, + "chat.input.maxlength": 100, + "chat.styles.disabled": [], + "chat.styles": [{ + "styleId": 0, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 1, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 2, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 3, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 4, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 5, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 6, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 7, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 8, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 9, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 10, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 11, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 12, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 13, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 14, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 15, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 16, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 17, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 18, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 19, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 20, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 21, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 22, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 23, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 24, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 25, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 26, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 27, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 28, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 29, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 30, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 31, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 32, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 33, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 34, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 35, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 36, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 37, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 38, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 39, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 40, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 41, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 42, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 43, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 44, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 45, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 46, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 47, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 48, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 49, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 50, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 51, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 52, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 53, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + } + ], + "camera.available.effects": [{ + "name": "dark_sepia", + "colorMatrix": [ + 0.4, + 0.4, + 0.1, + 0, + 110, + 0.3, + 0.4, + 0.1, + 0, + 30, + 0.3, + 0.2, + 0.1, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 0, + "enabled": true + }, { + "name": "increase_saturation", + "colorMatrix": [ + 2, + -0.5, + -0.5, + 0, + 0, + -0.5, + 2, + -0.5, + 0, + 0, + -0.5, + -0.5, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 0, + "enabled": true + }, { + "name": "increase_contrast", + "colorMatrix": [ + 1.5, + 0, + 0, + 0, + -50, + 0, + 1.5, + 0, + 0, + -50, + 0, + 0, + 1.5, + 0, + -50, + 0, + 0, + 0, + 1.5, + 0 + ], + "minLevel": 0, + "enabled": true + }, { + "name": "shadow_multiply_02", + "colorMatrix": [], + "minLevel": 0, + "blendMode": 2, + "enabled": true + }, { + "name": "color_1", + "colorMatrix": [ + 0.393, + 0.769, + 0.189, + 0, + 0, + 0.349, + 0.686, + 0.168, + 0, + 0, + 0.272, + 0.534, + 0.131, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 1, + "enabled": true + }, { + "name": "hue_bright_sat", + "colorMatrix": [ + 1, + 0.6, + 0.2, + 0, + -50, + 0.2, + 1, + 0.6, + 0, + -50, + 0.6, + 0.2, + 1, + 0, + -50, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 1, + "enabled": true + }, { + "name": "hearts_hardlight_02", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 9, + "enabled": true + }, { + "name": "texture_overlay", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 4, + "enabled": true + }, { + "name": "pinky_nrm", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 0, + "enabled": true + }, { + "name": "color_2", + "colorMatrix": [ + 0.333, + 0.333, + 0.333, + 0, + 0, + 0.333, + 0.333, + 0.333, + 0, + 0, + 0.333, + 0.333, + 0.333, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 2, + "enabled": true + }, { + "name": "night_vision", + "colorMatrix": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1.1, + 0, + 0, + -50, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 2, + "enabled": true + }, { + "name": "stars_hardlight_02", + "colorMatrix": [], + "minLevel": 2, + "blendMode": 9, + "enabled": true + }, { + "name": "coffee_mpl", + "colorMatrix": [], + "minLevel": 2, + "blendMode": 2, + "enabled": true + }, { + "name": "security_hardlight", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 9, + "enabled": true + }, { + "name": "bluemood_mpl", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 2, + "enabled": true + }, { + "name": "rusty_mpl", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 2, + "enabled": true + }, { + "name": "decr_conrast", + "colorMatrix": [ + 0.5, + 0, + 0, + 0, + 50, + 0, + 0.5, + 0, + 0, + 50, + 0, + 0, + 0.5, + 0, + 50, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 4, + "enabled": true + }, { + "name": "green_2", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + 90, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 4, + "enabled": true + }, { + "name": "alien_hrd", + "colorMatrix": [], + "minLevel": 4, + "blendMode": 9, + "enabled": true + }, { + "name": "color_3", + "colorMatrix": [ + 0.609, + 0.609, + 0.082, + 0, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 5, + "enabled": true + }, { + "name": "color_4", + "colorMatrix": [ + 0.8, + -0.8, + 1, + 0, + 70, + 0.8, + -0.8, + 1, + 0, + 70, + 0.8, + -0.8, + 1, + 0, + 70, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 5, + "enabled": true + }, { + "name": "toxic_hrd", + "colorMatrix": [], + "minLevel": 5, + "blendMode": 9, + "enabled": true + }, { + "name": "hypersaturated", + "colorMatrix": [ + 2, + -1, + 0, + 0, + 0, + -1, + 2, + 0, + 0, + 0, + 0, + -1, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 6, + "enabled": true + }, { + "name": "Yellow", + "colorMatrix": [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 6, + "enabled": true + }, { + "name": "misty_hrd", + "colorMatrix": [], + "minLevel": 6, + "blendMode": 9, + "enabled": true + }, { + "name": "x_ray", + "colorMatrix": [ + 0, + 1.2, + 0, + 0, + -100, + 0, + 2, + 0, + 0, + -120, + 0, + 2, + 0, + 0, + -120, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 7, + "enabled": true + }, { + "name": "decrease_saturation", + "colorMatrix": [ + 0.7, + 0.2, + 0.2, + 0, + 0, + 0.2, + 0.7, + 0.2, + 0, + 0, + 0.2, + 0.2, + 0.7, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 7, + "enabled": true + }, { + "name": "drops_mpl", + "colorMatrix": [], + "minLevel": 8, + "blendMode": 2, + "enabled": true + }, { + "name": "shiny_hrd", + "colorMatrix": [], + "minLevel": 9, + "blendMode": 9, + "enabled": true + }, { + "name": "glitter_hrd", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 9, + "enabled": true + }, { + "name": "frame_gold", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "frame_gray_4", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "frame_black_2", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "frame_wood_2", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "finger_nrm", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "color_5", + "colorMatrix": [ + 3.309, + 0.609, + 1.082, + 0.2, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 1.309, + 0.609, + 0.082, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "black_white_negative", + "colorMatrix": [ + -0.5, + -0.5, + -0.5, + 0, + 0, + -0.5, + -0.5, + -0.5, + 0, + 0, + -0.5, + -0.5, + -0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "blue", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + -255, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "red", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "green", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + } + ], + "notification": { + "notification.admin.transient": { + "display": "POP_UP", + "image": "${image.library.url}/album1358/frank_wave_001.gif" + }, + "notification.builders_club.membership_expired": { + "display": "POP_UP" + }, + "notification.builders_club.membership_expires": { + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.membership_extended": { + "delivery": "PERSISTENT", + "display": "POP_UP" + }, + "notification.builders_club.membership_made": { + "delivery": "PERSISTENT", + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_membership_extended.png" + }, + "notification.builders_club.membership_renewed": { + "delivery": "PERSISTENT", + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_membership_extended.png" + }, + "notification.builders_club.room_locked": { + "display": "BUBBLE", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.room_unlocked": { + "display": "BUBBLE" + }, + "notification.builders_club.visit_denied_for_owner": { + "display": "BUBBLE", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.visit_denied_for_visitor": { + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_room_locked.png" + }, + "notification.campaign.credit.donation": { + "display": "BUBBLE" + }, + "notification.campaign.product.donation": { + "display": "BUBBLE" + }, + "notification.casino.too_many_dice.placement": { + "display": "POP_UP" + }, + "notification.casino.too_many_dice": { + "display": "POP_UP" + }, + "notification.cfh.created": { + "display": "POP_UP", + "title": "" + }, + "notification.feed.enabled": false, + "notification.floorplan_editor.error": { + "display": "POP_UP" + }, + "notification.forums.delivered": { + "delivery": "PERSISTENT", + "display": "POP_UP" + }, + "notification.forums.forum_settings_updated": { + "display": "BUBBLE" + }, + "notification.forums.message.hidden": { + "display": "BUBBLE" + }, + "notification.forums.message.restored": { + "display": "BUBBLE" + }, + "notification.forums.thread.hidden": { + "display": "BUBBLE" + }, + "notification.forums.thread.locked": { + "display": "BUBBLE" + }, + "notification.forums.thread.pinned": { + "display": "BUBBLE" + }, + "notification.forums.thread.restored": { + "display": "BUBBLE" + }, + "notification.forums.thread.unlocked": { + "display": "BUBBLE" + }, + "notification.forums.thread.unpinned": { + "display": "BUBBLE" + }, + "notification.furni_placement_error": { + "display": "BUBBLE" + }, + "notification.gifting.valentine": { + "delivery": "PERSISTENT", + "display": "BUBBLE", + "image": "${image.library.url}/notifications/polaroid_photo.png" + }, + "notification.items.enabled": true, + "notification.mute.forbidden.time": { + "display": "BUBBLE" + }, + "notification.npc.gift.received": { + "display": "BUBBLE", + "image": "${image.library.url}/album1584/X1517.gif" + } + } +} diff --git a/src/App.tsx b/src/App.tsx index d3566cc..f67bd6b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { GetAssetManager, GetAvatarRenderManager, GetCommunication, GetConfiguration, GetLocalizationManager, GetRoomEngine, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, GetStage, GetTexturePool, GetTicker, HabboWebTools, LegacyExternalInterface, LoadGameUrlEvent, NitroLogger, NitroVersion, PrepareRenderer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { GetUIVersion } from './api'; +import { GetUIVersion, UiSettingsProvider } from './api'; import { Base } from './common'; import { LoadingView } from './components/loading/LoadingView'; import { MainView } from './components/MainView'; @@ -89,11 +89,13 @@ export const App: FC<{}> = props => }, []); return ( - - { !isReady && - } - { isReady && } - - + + + { !isReady && + } + { isReady && } + + + ); }; \ No newline at end of file diff --git a/src/api/ui-settings/IUiSettings.ts b/src/api/ui-settings/IUiSettings.ts new file mode 100644 index 0000000..24604b6 --- /dev/null +++ b/src/api/ui-settings/IUiSettings.ts @@ -0,0 +1,21 @@ +export interface IUiSettings +{ + colorMode: 'color' | 'image' | 'default'; + headerColor: string; + headerImageUrl: string; + headerAlpha: number; +} + +export const DEFAULT_UI_SETTINGS: IUiSettings = { + colorMode: 'default', + headerColor: '#1E7295', + headerImageUrl: '', + headerAlpha: 100 +}; + +export const PRESET_COLORS: string[] = [ + '#000000', '#444444', '#888888', '#CCCCCC', '#660000', '#CC3333', '#FF6666', '#CC6600', + '#FF3333', '#FF6633', '#FF9933', '#FFCC00', '#FFFF00', '#66FF00', '#00CC00', '#009900', + '#00FFCC', '#33CCFF', '#3366FF', '#0000CC', '#6633CC', '#9933FF', '#CC33FF', '#FF66CC', + '#FF99CC', '#1E7295', '#185D79', '#2DABC2', '#2B91A7', '#283F5D' +]; diff --git a/src/api/ui-settings/UiSettingsContext.tsx b/src/api/ui-settings/UiSettingsContext.tsx new file mode 100644 index 0000000..5d516cd --- /dev/null +++ b/src/api/ui-settings/UiSettingsContext.tsx @@ -0,0 +1,164 @@ +import { createContext, FC, PropsWithChildren, useCallback, useContext, useEffect, useState } from 'react'; +import { DEFAULT_UI_SETTINGS, IUiSettings } from './IUiSettings'; + +const STORAGE_KEY = 'nitro.ui.settings'; + +interface IUiSettingsContext +{ + settings: IUiSettings; + isCustomActive: boolean; + updateSettings: (partial: Partial) => void; + resetSettings: () => void; + getHeaderStyle: () => React.CSSProperties; + getTabsStyle: () => React.CSSProperties; + getAccentColor: () => string; +} + +const UiSettingsContext = createContext({ + settings: DEFAULT_UI_SETTINGS, + isCustomActive: false, + updateSettings: () => {}, + resetSettings: () => {}, + getHeaderStyle: () => ({}), + getTabsStyle: () => ({}), + getAccentColor: () => DEFAULT_UI_SETTINGS.headerColor +}); + +const darkenColor = (hex: string, amount: number): string => +{ + const num = parseInt(hex.replace('#', ''), 16); + const r = Math.max(0, ((num >> 16) & 0xFF) - amount); + const g = Math.max(0, ((num >> 8) & 0xFF) - amount); + const b = Math.max(0, (num & 0xFF) - amount); + + return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); +}; + +const loadSettings = (): IUiSettings => +{ + try + { + const stored = localStorage.getItem(STORAGE_KEY); + if(stored) return { ...DEFAULT_UI_SETTINGS, ...JSON.parse(stored) }; + } + catch(e) {} + + return { ...DEFAULT_UI_SETTINGS }; +}; + +const saveSettings = (settings: IUiSettings): void => +{ + try + { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + } + catch(e) {} +}; + +export const UiSettingsProvider: FC = ({ children }) => +{ + const [ settings, setSettings ] = useState(loadSettings); + + const updateSettings = useCallback((partial: Partial) => + { + setSettings(prev => + { + const updated = { ...prev, ...partial }; + saveSettings(updated); + return updated; + }); + }, []); + + const resetSettings = useCallback(() => + { + setSettings({ ...DEFAULT_UI_SETTINGS }); + saveSettings(DEFAULT_UI_SETTINGS); + }, []); + + const getHeaderStyle = useCallback((): React.CSSProperties => + { + if(settings.colorMode === 'color') + { + return { backgroundColor: settings.headerColor }; + } + + if(settings.colorMode === 'image' && settings.headerImageUrl) + { + return { + backgroundImage: `url(${ settings.headerImageUrl })`, + backgroundSize: 'cover', + backgroundPosition: 'center', + backgroundRepeat: 'repeat' + }; + } + + return {}; + }, [ settings ]); + + const getTabsStyle = useCallback((): React.CSSProperties => + { + if(settings.colorMode === 'color') + { + return { backgroundColor: darkenColor(settings.headerColor, 30) }; + } + + if(settings.colorMode === 'image' && settings.headerImageUrl) + { + return { + backgroundImage: `url(${ settings.headerImageUrl })`, + backgroundSize: 'cover', + backgroundPosition: 'center bottom', + backgroundRepeat: 'repeat' + }; + } + + return {}; + }, [ settings ]); + + const getAccentColor = useCallback((): string => + { + if(settings.colorMode === 'color') return settings.headerColor; + return DEFAULT_UI_SETTINGS.headerColor; + }, [ settings ]); + + const isCustomActive = settings.colorMode !== 'default'; + + const ALL_CSS_VARS = [ + '--ui-accent-color', '--ui-accent-dark', + '--ui-ctx-bg', '--ui-ctx-header-bg', '--ui-ctx-item-bg1', '--ui-ctx-item-bg2', + '--ui-btn-primary-bg', '--ui-btn-primary-border', + '--ui-dark-bg', '--ui-dark-border' + ]; + + useEffect(() => + { + const root = document.documentElement; + + if(settings.colorMode === 'color') + { + const c = settings.headerColor; + root.style.setProperty('--ui-accent-color', c); + root.style.setProperty('--ui-accent-dark', darkenColor(c, 30)); + root.style.setProperty('--ui-ctx-bg', darkenColor(c, 50)); + root.style.setProperty('--ui-ctx-header-bg', darkenColor(c, 20)); + root.style.setProperty('--ui-ctx-item-bg1', darkenColor(c, 60)); + root.style.setProperty('--ui-ctx-item-bg2', darkenColor(c, 70)); + root.style.setProperty('--ui-btn-primary-bg', c); + root.style.setProperty('--ui-btn-primary-border', darkenColor(c, 20)); + root.style.setProperty('--ui-dark-bg', darkenColor(c, 55)); + root.style.setProperty('--ui-dark-border', darkenColor(c, 60)); + } + else + { + ALL_CSS_VARS.forEach(v => root.style.removeProperty(v)); + } + }, [ settings ]); + + return ( + + { children } + + ); +}; + +export const useUiSettings = () => useContext(UiSettingsContext); diff --git a/src/api/ui-settings/index.ts b/src/api/ui-settings/index.ts new file mode 100644 index 0000000..255a5da --- /dev/null +++ b/src/api/ui-settings/index.ts @@ -0,0 +1,2 @@ +export * from './IUiSettings'; +export * from './UiSettingsContext'; diff --git a/src/common/Button.tsx b/src/common/Button.tsx index 6b7d454..4da0794 100644 --- a/src/common/Button.tsx +++ b/src/common/Button.tsx @@ -12,20 +12,16 @@ export interface ButtonProps extends FlexProps export const Button: FC = props => { - const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], ...rest } = props; + const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], style = {}, ...rest } = props; const getClassNames = useMemo(() => { - - // fucked up method i know (i dont have a clue what im doing because im a ninja) - const newClassNames: string[] = [ 'pointer-events-auto inline-block font-normal leading-normal text-[#fff] text-center no-underline align-middle cursor-pointer select-none border border-[solid] border-transparent px-[.75rem] py-[.375rem] text-[.9rem] rounded-[.25rem] [transition:color_.15s_ease-in-out,background-color_.15s_ease-in-out,border-color_.15s_ease-in-out,box-shadow_.15s_ease-in-out]' ]; if(variant) { - if(variant == 'primary') - newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]'); + newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white'); if(variant == 'success') newClassNames.push('text-white bg-[#00800b] border-[#00800b] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#006d09] hover:border-[#006609]'); @@ -43,11 +39,10 @@ export const Button: FC = props => newClassNames.push('text-white bg-[#185d79] border-[#185d79] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#144f67] hover:border-[#134a61]'); if(variant == 'dark') - newClassNames.push('text-white bg-dark [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#18181bfb] hover:border-[#161619fb]'); - - if(variant == 'gray') - newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]'); + newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white'); + if(variant == 'gray') + newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white'); } if(size) @@ -67,5 +62,28 @@ export const Button: FC = props => return newClassNames; }, [ variant, size, active, disabled, classNames ]); - return ; + const getStyle = useMemo(() => + { + if(variant === 'primary' || variant === 'gray') + { + return { + backgroundColor: 'var(--ui-btn-primary-bg, #1e7295)', + borderColor: 'var(--ui-btn-primary-border, #1e7295)', + ...style + }; + } + + if(variant === 'dark') + { + return { + backgroundColor: 'var(--ui-dark-bg, rgba(28, 28, 32, .98))', + borderColor: 'var(--ui-dark-border, rgba(28, 28, 32, .98))', + ...style + }; + } + + return style; + }, [ variant, style ]); + + return ; }; diff --git a/src/common/card/NitroCardHeaderView.tsx b/src/common/card/NitroCardHeaderView.tsx index 8bb354c..5bfbe2b 100644 --- a/src/common/card/NitroCardHeaderView.tsx +++ b/src/common/card/NitroCardHeaderView.tsx @@ -1,5 +1,6 @@ import { FC, MouseEvent } from 'react'; import { FaFlag } from 'react-icons/fa'; +import { useUiSettings } from '../../api'; import { Base, Column, ColumnProps, Flex } from '..'; interface NitroCardHeaderViewProps extends ColumnProps @@ -16,8 +17,7 @@ interface NitroCardHeaderViewProps extends ColumnProps export const NitroCardHeaderView: FC = props => { const { headerText = null, isGalleryPhoto = false, noCloseButton = false, isInfoToHabboPages = false, onReportPhoto = null, onClickInfoHabboPages = null, onCloseClick = null, justifyContent = 'center', alignItems = 'center', classNames = [], children = null, ...rest } = props; - - + const { isCustomActive, getHeaderStyle } = useUiSettings(); const onMouseDown = (event: MouseEvent) => { @@ -25,8 +25,12 @@ export const NitroCardHeaderView: FC = props => event.nativeEvent.stopImmediatePropagation(); }; + const headerClassName = isCustomActive + ? 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header' + : 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header bg-card-header'; + return ( - + { headerText } { isGalleryPhoto && diff --git a/src/common/card/tabs/NitroCardTabsView.tsx b/src/common/card/tabs/NitroCardTabsView.tsx index 5e14506..5c49ac4 100644 --- a/src/common/card/tabs/NitroCardTabsView.tsx +++ b/src/common/card/tabs/NitroCardTabsView.tsx @@ -1,21 +1,27 @@ import { FC, useMemo } from 'react'; +import { useUiSettings } from '../../../api'; import { Flex, FlexProps } from '../..'; export const NitroCardTabsView: FC = props => { const { justifyContent = 'center', gap = 1, classNames = [], children = null, ...rest } = props; + const { isCustomActive, getTabsStyle } = useUiSettings(); const getClassNames = useMemo(() => { - const newClassNames: string[] = [ 'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px' ]; + const base = isCustomActive + ? 'justify-center gap-0.5 flex min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px' + : 'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px'; + + const newClassNames: string[] = [ base ]; if(classNames.length) newClassNames.push(...classNames); return newClassNames; - }, [ classNames ]); + }, [ classNames, isCustomActive ]); return ( - + { children } ); diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 3fef0cc..41c320e 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -9,6 +9,7 @@ import { CampaignView } from './campaign/CampaignView'; import { CatalogView } from './catalog/CatalogView'; import { ChatHistoryView } from './chat-history/ChatHistoryView'; import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView'; +import { FurniEditorView } from './furni-editor/FurniEditorView'; import { FriendsView } from './friends/FriendsView'; import { GameCenterView } from './game-center/GameCenterView'; import { GroupsView } from './groups/GroupsView'; @@ -21,10 +22,12 @@ import { ModToolsView } from './mod-tools/ModToolsView'; import { NavigatorView } from './navigator/NavigatorView'; import { NitrobubbleHiddenView } from './nitrobubblehidden/NitrobubbleHiddenView'; import { NitropediaView } from './nitropedia/NitropediaView'; +import { ExternalPluginLoader } from './plugins/ExternalPluginLoader'; import { RightSideView } from './right-side/RightSideView'; import { RoomView } from './room/RoomView'; import { ToolbarView } from './toolbar/ToolbarView'; import { UserProfileView } from './user-profile/UserProfileView'; +import { InterfaceSettingsView } from './interface-settings/InterfaceSettingsView'; import { UserSettingsView } from './user-settings/UserSettingsView'; import { WiredView } from './wired/WiredView'; import { YoutubeTvView } from './youtube-tv/YoutubeTvView'; @@ -85,6 +88,7 @@ export const MainView: FC<{}> = props => { landingViewVisible && @@ -105,6 +109,7 @@ export const MainView: FC<{}> = props => + @@ -115,7 +120,9 @@ export const MainView: FC<{}> = props => + + ); }; diff --git a/src/components/interface-settings/InterfaceColorTabView.tsx b/src/components/interface-settings/InterfaceColorTabView.tsx new file mode 100644 index 0000000..65bf172 --- /dev/null +++ b/src/components/interface-settings/InterfaceColorTabView.tsx @@ -0,0 +1,179 @@ +import { RgbaColorPicker, RgbaColor } from 'react-colorful'; +import { FC, useCallback, useMemo, useState } from 'react'; +import { FaUndo, FaTrash, FaPen, FaFillDrip, FaSave } from 'react-icons/fa'; +import { PRESET_COLORS, useUiSettings } from '../../api'; +import { Flex, Text } from '../../common'; + +const hexToRgba = (hex: string, a = 1): RgbaColor => +{ + const num = parseInt(hex.replace('#', ''), 16); + return { r: (num >> 16) & 0xFF, g: (num >> 8) & 0xFF, b: num & 0xFF, a }; +}; + +const rgbaToHex = (rgba: RgbaColor): string => +{ + return '#' + ((1 << 24) + (rgba.r << 16) + (rgba.g << 8) + rgba.b).toString(16).slice(1); +}; + +export const InterfaceColorTabView: FC<{}> = () => +{ + const { settings, updateSettings, resetSettings } = useUiSettings(); + const [ color, setColor ] = useState(() => hexToRgba(settings.headerColor, settings.headerAlpha / 100)); + + const hexColor = useMemo(() => rgbaToHex(color), [ color ]); + const alphaPercent = useMemo(() => Math.round((color.a ?? 1) * 100), [ color ]); + + const onHexInput = useCallback((value: string) => + { + const clean = value.replace(/[^0-9a-fA-F]/g, '').slice(0, 6); + if(clean.length === 6) + { + const rgba = hexToRgba('#' + clean, color.a); + setColor(rgba); + } + }, [ color.a ]); + + const onRgbInput = useCallback((channel: 'r' | 'g' | 'b', value: number) => + { + const clamped = Math.max(0, Math.min(255, value || 0)); + setColor(prev => ({ ...prev, [channel]: clamped })); + }, []); + + const onAlphaInput = useCallback((value: number) => + { + const clamped = Math.max(0, Math.min(100, value || 0)); + setColor(prev => ({ ...prev, a: clamped / 100 })); + }, []); + + const onPresetClick = useCallback((presetHex: string) => + { + setColor(hexToRgba(presetHex, color.a)); + }, [ color.a ]); + + const onSave = useCallback(() => + { + updateSettings({ + colorMode: 'color', + headerColor: hexColor, + headerAlpha: alphaPercent + }); + }, [ updateSettings, hexColor, alphaPercent ]); + + const onReset = useCallback(() => + { + resetSettings(); + setColor(hexToRgba('#1E7295', 1)); + }, [ resetSettings ]); + + const onDelete = useCallback(() => + { + updateSettings({ colorMode: 'default' }); + setColor(hexToRgba('#1E7295', 1)); + }, [ updateSettings ]); + + return ( + +
+ +
+ + + onHexInput(e.target.value) } + maxLength={ 6 } + /> + Hex + + + onRgbInput('r', parseInt(e.target.value)) } + min={ 0 } max={ 255 } + /> + R + + + onRgbInput('g', parseInt(e.target.value)) } + min={ 0 } max={ 255 } + /> + G + + + onRgbInput('b', parseInt(e.target.value)) } + min={ 0 } max={ 255 } + /> + B + + + onAlphaInput(parseInt(e.target.value)) } + min={ 0 } max={ 100 } + /> + A + + +
+ { PRESET_COLORS.map((presetHex, i) => ( +
onPresetClick(presetHex) } + /> + )) } +
+ + + + + + + + + ); +}; diff --git a/src/components/interface-settings/InterfaceImageTabView.tsx b/src/components/interface-settings/InterfaceImageTabView.tsx new file mode 100644 index 0000000..390a390 --- /dev/null +++ b/src/components/interface-settings/InterfaceImageTabView.tsx @@ -0,0 +1,52 @@ +import { FC, useCallback, useMemo } from 'react'; +import { GetConfigurationValue, useUiSettings } from '../../api'; + +export const InterfaceImageTabView: FC<{}> = () => +{ + const { settings, updateSettings } = useUiSettings(); + + const imageCount = useMemo(() => + { + return GetConfigurationValue('ui.header.images.count', 30); + }, []); + + const baseUrl = useMemo(() => + { + return GetConfigurationValue('ui.header.images.url', 'https://image.webbo.city/image/headerImage/image{id}.gif'); + }, []); + + const images = useMemo(() => + { + const result: string[] = []; + for(let i = 1; i <= imageCount; i++) + { + result.push(baseUrl.replace('{id}', String(i))); + } + return result; + }, [ imageCount, baseUrl ]); + + const onImageSelect = useCallback((url: string) => + { + updateSettings({ + colorMode: 'image', + headerImageUrl: url + }); + }, [ updateSettings ]); + + return ( +
+ { images.map((url, i) => ( +
onImageSelect(url) } + /> + )) } +
+ ); +}; diff --git a/src/components/interface-settings/InterfaceProfileTabView.tsx b/src/components/interface-settings/InterfaceProfileTabView.tsx new file mode 100644 index 0000000..6534c3c --- /dev/null +++ b/src/components/interface-settings/InterfaceProfileTabView.tsx @@ -0,0 +1,107 @@ +import { GetSessionDataManager, HabboClubLevelEnum } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useMemo, useState } from 'react'; +import { GetClubMemberLevel, GetConfigurationValue } from '../../api'; +import { Base, Flex, Grid, LayoutCurrencyIcon, NitroCardTabsItemView, NitroCardTabsView, Text } from '../../common'; +import { useRoom } from '../../hooks'; + +interface ItemData +{ + id: number; + isHcOnly: boolean; + minRank: number; + isAmbassadorOnly: boolean; + selectable: boolean; +} + +const SUB_TABS = [ 'backgrounds', 'stands', 'overlays' ] as const; +type SubTabType = typeof SUB_TABS[number]; + +const SUB_TAB_LABELS: Record = { + backgrounds: 'Sfondi', + stands: 'Basi', + overlays: 'Overlay' +}; + +export const InterfaceProfileTabView: FC<{}> = () => +{ + const [ activeSubTab, setActiveSubTab ] = useState('backgrounds'); + const [ selectedBackground, setSelectedBackground ] = useState(0); + const [ selectedStand, setSelectedStand ] = useState(0); + const [ selectedOverlay, setSelectedOverlay ] = useState(0); + const { roomSession } = useRoom(); + + const userData = useMemo(() => ({ + isHcMember: GetClubMemberLevel() >= HabboClubLevelEnum.CLUB, + securityLevel: GetSessionDataManager().canChangeName, + isAmbassador: GetSessionDataManager().isAmbassador + }), []); + + const processData = useCallback((configData: any[], dataType: string): ItemData[] => + { + if(!configData?.length) return []; + + return configData + .filter(item => + { + const meetsRank = userData.securityLevel >= item.minRank; + const ambassadorEligible = !item.isAmbassadorOnly || userData.isAmbassador; + return item.isHcOnly || (meetsRank && ambassadorEligible); + }) + .map(item => ({ id: item[`${ dataType }Id`], ...item, selectable: !item.isHcOnly || userData.isHcMember })); + }, [ userData ]); + + const allData = useMemo(() => ({ + backgrounds: processData(GetConfigurationValue('backgrounds.data'), 'background'), + stands: processData(GetConfigurationValue('stands.data'), 'stand'), + overlays: processData(GetConfigurationValue('overlays.data'), 'overlay') + }), [ processData ]); + + const handleSelection = useCallback((id: number) => + { + if(!roomSession) return; + + const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay }; + const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay }; + + setters[activeSubTab](id); + const newValues = { ...currentValues, [activeSubTab]: id }; + roomSession.sendBackgroundMessage(newValues.backgrounds, newValues.stands, newValues.overlays); + }, [ activeSubTab, roomSession, selectedBackground, selectedStand, selectedOverlay ]); + + const renderItem = useCallback((item: ItemData, type: string) => ( + item.selectable && handleSelection(item.id) } + className={ item.selectable ? '' : 'non-selectable' } + > + + { item.isHcOnly && } + + ), [ handleSelection ]); + + return ( + + + { SUB_TABS.map(tab => ( + + )) } + + { !roomSession && ( + Entra in una stanza per modificare il profilo + ) } + { roomSession && ( + + { allData[activeSubTab].map(item => renderItem(item, activeSubTab.slice(0, -1))) } + + ) } + + ); +}; diff --git a/src/components/interface-settings/InterfaceSettingsView.tsx b/src/components/interface-settings/InterfaceSettingsView.tsx new file mode 100644 index 0000000..cd465c7 --- /dev/null +++ b/src/components/interface-settings/InterfaceSettingsView.tsx @@ -0,0 +1,74 @@ +import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsView, NitroCardTabsItemView, NitroCardView } from '../../common'; +import { InterfaceColorTabView } from './InterfaceColorTabView'; +import { InterfaceProfileTabView } from './InterfaceProfileTabView'; + +const TABS = [ 'color', 'profile' ] as const; +type TabType = typeof TABS[number]; + +const TAB_LABELS: Record = { + color: 'Colore', + profile: 'Sfondo profilo' +}; + +export const InterfaceSettingsView: FC<{}> = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ currentTab, setCurrentTab ] = useState('color'); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': + setIsVisible(true); + return; + case 'hide': + setIsVisible(false); + return; + case 'toggle': + setIsVisible(prev => !prev); + return; + case 'profile': + setCurrentTab('profile'); + setIsVisible(true); + return; + } + }, + eventUrlPrefix: 'interface-settings/' + }; + + AddLinkEventTracker(linkTracker); + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + if(!isVisible) return null; + + return ( + + setIsVisible(false) } /> + + { TABS.map(tab => ( + setCurrentTab(tab) } + > + { TAB_LABELS[tab] } + + )) } + + + { currentTab === 'color' && } + { currentTab === 'profile' && } + + + ); +}; diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx index 6a504d2..dde184d 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx @@ -45,7 +45,8 @@ const BadgeMiniPicker: FC<{ return (
e.stopPropagation() }> = props canUse = true; isCrackable = true; - crackableHits = stuffData.hits; - crackableTarget = stuffData.target; + crackableHits = stuffData?.hits ?? 0; + crackableTarget = stuffData?.target ?? 0; } else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) @@ -458,7 +458,7 @@ export const InfoStandWidgetFurniView: FC = props return ( - +
@@ -527,7 +527,7 @@ export const InfoStandWidgetFurniView: FC = props { isCrackable && <>
- { LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ crackableHits.toString(), crackableTarget.toString() ]) } + { LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ (crackableHits ?? 0).toString(), (crackableTarget ?? 0).toString() ]) } } { avatarInfo.groupId > 0 && <> @@ -552,7 +552,21 @@ export const InfoStandWidgetFurniView: FC = props { godMode && <>
- { canSeeFurniId && ID: { avatarInfo.id } } + { canSeeFurniId && +
+
+ + + + ID: { avatarInfo.id } +
+
+ + + + Sprite: { (() => { const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?'; })() } +
+
} { (!avatarInfo.isWallItem && canMove) && <> + { dropdownOpen &&
{ /* Left panel: position + rotation */ } diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx index 1791979..2dac49e 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx @@ -1,4 +1,4 @@ -import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent, GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react'; import { FaPencilAlt, FaTimes } from 'react-icons/fa'; import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api'; @@ -7,7 +7,6 @@ import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks'; import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView'; import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView'; import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView'; -import { BackgroundsView } from '../../../../backgrounds/BackgroundsView'; interface InfoStandWidgetUserViewProps { avatarInfo: AvatarInfoUser; @@ -32,7 +31,7 @@ export const InfoStandWidgetUserView: FC = props = const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]); - const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []); + const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); CreateLinkEvent('interface-settings/profile'); }, []); const saveMotto = (motto: string) => { if (!isEditingMotto || motto.length > GetConfigurationValue('motto.max.length', 38) || !roomSession) return; @@ -127,7 +126,7 @@ export const InfoStandWidgetUserView: FC = props = return ( <> - +
@@ -257,19 +256,6 @@ export const InfoStandWidgetUserView: FC = props = )} - {isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && ( -
- -
- )} ); }; \ No newline at end of file diff --git a/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx b/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx index a0513cf..ff26177 100644 --- a/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx +++ b/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx @@ -3,16 +3,21 @@ import { Flex, FlexProps } from '../../../../common'; export const ContextMenuHeaderView: FC = props => { - const { justifyContent = 'center', alignItems = 'center', classNames = [], ...rest } = props; + const { justifyContent = 'center', alignItems = 'center', classNames = [], style = {}, ...rest } = 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[] = [ 'text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ]; if(classNames.length) newClassNames.push(...classNames); return newClassNames; }, [ classNames ]); - return ; + const mergedStyle = useMemo(() => ({ + backgroundColor: 'var(--ui-ctx-header-bg, #3d5f6e)', + ...style + }), [ style ]); + + return ; }; diff --git a/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx b/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx index 0a1eacc..b012a93 100644 --- a/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx +++ b/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx @@ -8,7 +8,7 @@ interface ContextMenuListItemViewProps extends FlexProps export const ContextMenuListItemView: FC = props => { - const { disabled = false, fullWidth = true, justifyContent = 'center', alignItems = 'center', classNames = [], onClick = null, ...rest } = props; + const { disabled = false, fullWidth = true, justifyContent = 'center', alignItems = 'center', classNames = [], style = {}, onClick = null, ...rest } = props; const handleClick = (event: MouseEvent) => { @@ -19,7 +19,7 @@ export const ContextMenuListItemView: FC = 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[] = [ 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] cursor-pointer' ]; if(disabled) newClassNames.push('disabled'); @@ -28,5 +28,10 @@ export const ContextMenuListItemView: FC = props = return newClassNames; }, [ disabled, classNames ]); - return ; + const mergedStyle = useMemo(() => ({ + background: 'repeating-linear-gradient(var(--ui-ctx-item-bg1, #131e25), var(--ui-ctx-item-bg1, #131e25) 50%, var(--ui-ctx-item-bg2, #0d171d) 50%, var(--ui-ctx-item-bg2, #0d171d) 100%)', + ...style + }), [ style ]); + + return ; }; diff --git a/src/components/room/widgets/context-menu/ContextMenuView.tsx b/src/components/room/widgets/context-menu/ContextMenuView.tsx index 1ca3e83..b92dc89 100644 --- a/src/components/room/widgets/context-menu/ContextMenuView.tsx +++ b/src/components/room/widgets/context-menu/ContextMenuView.tsx @@ -76,7 +76,6 @@ export const ContextMenuView: FC = ({ const getClassNames = useMemo(() => { const classes = [ 'p-[2px]!', - 'bg-[#1c323f]', 'border-2', 'border-[solid]', 'border-[rgba(255,255,255,.5)]', @@ -98,6 +97,7 @@ export const ContextMenuView: FC = ({ top: pos.y ?? 0, transition: isFading ? 'opacity 75ms linear' : undefined, opacity, + backgroundColor: 'var(--ui-ctx-bg, #1c323f)', ...style, }), [pos, opacity, isFading, style] diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 7d6743a..62503bb 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -69,38 +69,38 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => )} - - - - - { - setMeExpanded(!isMeExpanded); - event.stopPropagation(); - } }> - - { (getTotalUnseen > 0) && - } - - { isInRoom && - VisitDesktop() } /> } - { !isInRoom && - CreateLinkEvent('navigator/goto/home') } /> } - CreateLinkEvent('navigator/toggle') } /> - { GetConfigurationValue('game.center.enabled') && - CreateLinkEvent('games/toggle') } /> } - CreateLinkEvent('catalog/toggle') } /> - CreateLinkEvent('inventory/toggle') }> - { (getFullCount > 0) && - } - - { isInRoom && - CreateLinkEvent('camera/toggle') } /> } - { isMod && - CreateLinkEvent('mod-tools/toggle') } /> } + + + + { + setMeExpanded(!isMeExpanded); + event.stopPropagation(); + } }> + + { (getTotalUnseen > 0) && + } - + { isInRoom && + VisitDesktop() } /> } + { !isInRoom && + CreateLinkEvent('navigator/goto/home') } /> } + CreateLinkEvent('navigator/toggle') } /> + { GetConfigurationValue('game.center.enabled') && + CreateLinkEvent('games/toggle') } /> } + CreateLinkEvent('catalog/toggle') } /> + CreateLinkEvent('inventory/toggle') }> + { (getFullCount > 0) && + } + + { isInRoom && + CreateLinkEvent('camera/toggle') } /> } + { isMod && + CreateLinkEvent('mod-tools/toggle') } /> } + { isMod && + CreateLinkEvent('furni-editor/toggle') } /> } - + + CreateLinkEvent('friends/toggle') }> { (requests.length > 0) && diff --git a/src/css/common/Buttons.css b/src/css/common/Buttons.css index 106a3cc..21d7f1f 100644 --- a/src/css/common/Buttons.css +++ b/src/css/common/Buttons.css @@ -24,8 +24,8 @@ input[type=number] { .btn-primary { color: #fff; - background-color: #3c6d82; - border: 2px solid #1a617f; + background-color: var(--ui-btn-primary-bg, #3c6d82); + border: 2px solid var(--ui-btn-primary-border, #1a617f); padding: 0.25rem 0.5rem; font-size: .7875rem; border-radius: 0.5rem; @@ -33,7 +33,7 @@ input[type=number] { } .btn-primary:hover { - border: 2px solid #1a617f; + border: 2px solid var(--ui-btn-primary-border, #1a617f); box-shadow: none!important; } @@ -81,16 +81,16 @@ input[type=number] { .btn-dark { color: #fff; - background-color: #212131; - border: 2px solid #1c1c2a; + background-color: var(--ui-dark-bg, #212131); + border: 2px solid var(--ui-dark-border, #1c1c2a); box-shadow: none!important; border-radius: 8px; padding: 4px 11px 4px 11px; } .btn-dark:hover{ - background-color: #212131; - border: 2px solid #1c1c2a; + background-color: var(--ui-dark-bg, #212131); + border: 2px solid var(--ui-dark-border, #1c1c2a); box-shadow: none!important; border-radius: 8px; padding: 4px 11px 4px 11px; diff --git a/src/css/purse/PurseView.css b/src/css/purse/PurseView.css index 7134551..69c1732 100644 --- a/src/css/purse/PurseView.css +++ b/src/css/purse/PurseView.css @@ -22,7 +22,7 @@ pointer-events: all; } .borderhccontent{ - background-color: #212131; + background-color: var(--ui-dark-bg, #212131); border-radius: 0.5rem!important; border: 2px solid #383853; height: calc(100% - 3px); @@ -46,7 +46,7 @@ } .nitro-purse-seasonal-currency { - background-color: #212131; + background-color: var(--ui-dark-bg, #212131); background: linear-gradient(to right, #5f5f8d, transparent); height: 30px; margin-bottom: 4px; diff --git a/src/css/room/InfoStand.css b/src/css/room/InfoStand.css index e44b062..7e1a050 100644 --- a/src/css/room/InfoStand.css +++ b/src/css/room/InfoStand.css @@ -27,7 +27,7 @@ width: clamp(160px, 20vw, 190px); /* Responsive width */ z-index: 30; pointer-events: auto; - background: #212131; + background: var(--ui-dark-bg, #212131); box-shadow: inset 0 5px rgba(38, 38, 57, 0.6), inset 0 -4px rgba(25, 25, 37, 0.6); border-radius: 0.5rem; padding: 10px; diff --git a/src/css/room/RoomWidgets.css b/src/css/room/RoomWidgets.css index 093fa67..b4d6ee2 100644 --- a/src/css/room/RoomWidgets.css +++ b/src/css/room/RoomWidgets.css @@ -4,7 +4,7 @@ left: 15px; .nitro-room-tools { - background: #212131; + background: var(--ui-dark-bg, #212131); box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4); border-top-right-radius: .25rem; border-bottom-right-radius: .25rem; @@ -54,7 +54,7 @@ } .nitro-room-history { - background: #212131; + background: var(--ui-dark-bg, #212131); box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4); transition: all .2s ease; width: 150px; @@ -63,7 +63,7 @@ } .nitro-room-tools-info { - background: #212131; + background: var(--ui-dark-bg, #212131); box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4); transition: all .2s ease; max-width: 250px; From 349498ec34e84a26e5c837fdf365fcdd7e387643 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 18 Mar 2026 20:42:53 +0100 Subject: [PATCH 19/33] Localize all hardcoded texts in mod tools using LocalizeText Replace ~70 hardcoded English strings across 15 mod-tools files with LocalizeText() calls using moderation.* keys matching the existing ExternalTexts convention. Includes mod-tools-external-texts.json with all required keys for ExternalTexts.json. --- mod-tools-external-texts.json | 96 +++++++++++++++++++ src/components/mod-tools/ModToolsView.tsx | 12 +-- .../mod-tools/views/chatlog/ChatlogView.tsx | 12 +-- .../views/room/ModToolsChatlogView.tsx | 4 +- .../mod-tools/views/room/ModToolsRoomView.tsx | 28 +++--- .../views/tickets/CfhChatlogView.tsx | 4 +- .../views/tickets/ModToolsIssueInfoView.tsx | 24 ++--- .../views/tickets/ModToolsMyIssuesTabView.tsx | 12 +-- .../tickets/ModToolsOpenIssuesTabView.tsx | 10 +- .../tickets/ModToolsPickedIssuesTabView.tsx | 9 +- .../views/tickets/ModToolsTicketsView.tsx | 15 +-- .../views/user/ModToolsUserChatlogView.tsx | 4 +- .../views/user/ModToolsUserModActionView.tsx | 56 +++++------ .../views/user/ModToolsUserRoomVisitsView.tsx | 12 +-- .../user/ModToolsUserSendMessageView.tsx | 10 +- .../mod-tools/views/user/ModToolsUserView.tsx | 38 ++++---- 16 files changed, 222 insertions(+), 124 deletions(-) create mode 100644 mod-tools-external-texts.json diff --git a/mod-tools-external-texts.json b/mod-tools-external-texts.json new file mode 100644 index 0000000..686253c --- /dev/null +++ b/mod-tools-external-texts.json @@ -0,0 +1,96 @@ +{ + "moderation.modtools.title": "Mod Tools", + "moderation.modtools.roomtool": "Room Tool", + "moderation.modtools.roomchatlogs": "Chatlog Tool", + "moderation.modtools.userinfo": "User Info", + "moderation.modtools.tickets": "Tickets", + + "moderation.roomtool.info.title": "Room Info", + "moderation.roomtool.roomowner.title": "Owner:", + "moderation.roomtool.usersinroom.title": "Users in room:", + "moderation.roomtool.ownerinroom.title": "Owner here:", + "moderation.roomtool.true.title": "Yes", + "moderation.roomtool.false.title": "No", + "moderation.roomtool.button.visit.title": "Visit Room", + "moderation.roomtool.kickall.title": "Kick everyone out", + "moderation.roomtool.closeroom.title": "Enable the doorbell", + "moderation.roomtool.inappropiatename.title": "Change room name", + "moderation.roomtool.presets.title": "Type a mandatory message...", + "moderation.roomtool.button.caution.title": "Send Caution", + "moderation.roomtool.button.message.title": "Send Alert", + + "moderation.tickets.title": "Tickets", + "moderation.tickets.open": "Open Issues", + "moderation.tickets.my": "My Issues", + "moderation.tickets.picked": "Picked Issues", + "moderation.tickets.col.type": "Type", + "moderation.tickets.col.roomPlayer": "Room/Player", + "moderation.tickets.col.opened": "Opened", + "moderation.tickets.col.picker": "Picker", + "moderation.tickets.pick": "Pick Issue", + "moderation.tickets.handle": "Handle", + "moderation.tickets.release": "Release", + + "moderation.issue.resolving": "Resolving issue %id%", + "moderation.issue.info": "Issue Information", + "moderation.issue.source": "Source", + "moderation.issue.category": "Category", + "moderation.issue.description": "Description", + "moderation.issue.caller": "Caller", + "moderation.issue.reported": "Reported User", + "moderation.issue.chatlog": "Chatlog", + "moderation.issue.close.useless": "Close as useless", + "moderation.issue.close.abusive": "Close as abusive", + "moderation.issue.close.resolved": "Close as resolved", + "moderation.issue.release": "Release", + + "moderation.chatlog.issue": "Issue Chatlog", + "moderation.chatlog.room": "Room Chatlog", + "moderation.chatlog.col.time": "Time", + "moderation.chatlog.col.user": "User", + "moderation.chatlog.col.message": "Message", + "moderation.chatlog.visit": "Visit", + "moderation.chatlog.roomtools": "Room Tools", + "moderation.chatlog.user": "User Chatlog: %username%", + + "moderation.userinfo.roomchat": "Room Chat", + "moderation.userinfo.sendmessage": "Send Message", + "moderation.userinfo.roomvisits": "Room Visits", + "moderation.userinfo.modaction": "Mod Action", + + "moderation.sendmessage.title": "Send Message", + "moderation.sendmessage.to": "Message To: %username%", + "moderation.sendmessage.send": "Send message", + "moderation.sendmessage.error.empty": "Please write a message to user.", + "moderation.error": "Error", + + "moderation.roomvisits.title": "User Visits", + "moderation.roomvisits.col.time": "Time", + "moderation.roomvisits.col.roomname": "Room name", + "moderation.roomvisits.col.visit": "Visit", + "moderation.roomvisits.visitroom": "Visit Room", + + "moderation.modaction.title": "Mod Action: %username%", + "moderation.modaction.cfhtopic": "CFH Topic", + "moderation.modaction.sanctiontype": "Sanction Type", + "moderation.modaction.message.hint": "Optional message type, overrides default", + "moderation.modaction.defaultsanction": "Default Sanction", + "moderation.modaction.sanction": "Sanction", + "moderation.modaction.alert": "Alert", + "moderation.modaction.mute1h": "Mute 1h", + "moderation.modaction.ban18h": "Ban 18h", + "moderation.modaction.ban7days": "Ban 7 days", + "moderation.modaction.ban30days.step1": "Ban 30 days (step 1)", + "moderation.modaction.ban30days.step2": "Ban 30 days (step 2)", + "moderation.modaction.ban100years": "Ban 100 years", + "moderation.modaction.banavataronly100years": "Ban avatar-only 100 years", + "moderation.modaction.kick": "Kick", + "moderation.modaction.locktrade1week": "Lock trade 1 week", + "moderation.modaction.locktradepermanent": "Lock trade permanent", + "moderation.modaction.message": "Message", + "moderation.modaction.error.notopic": "You must select a CFH topic", + "moderation.modaction.error.notopicorsanction": "You must select a CFH topic and Sanction", + "moderation.modaction.error.nopermission": "You do not have permission to do this", + "moderation.modaction.error.nosanction": "You must select a sanction", + "moderation.modaction.error.emptymessage": "Please write a message to user" +} diff --git a/src/components/mod-tools/ModToolsView.tsx b/src/components/mod-tools/ModToolsView.tsx index d42381d..2bc9680 100644 --- a/src/components/mod-tools/ModToolsView.tsx +++ b/src/components/mod-tools/ModToolsView.tsx @@ -1,6 +1,6 @@ import { AddLinkEventTracker, CreateLinkEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomId, RoomObjectCategory, RoomObjectType } from '@nitrots/nitro-renderer'; import { FC, useEffect, useRef, useState } from 'react'; -import { GetRoomSession, ISelectedUser } from '../../api'; +import { GetRoomSession, ISelectedUser, LocalizeText } from '../../api'; import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; import { useModTools, useNitroEvent, useObjectSelectedEvent } from '../../hooks'; import { ModToolsChatlogView } from './views/room/ModToolsChatlogView'; @@ -125,23 +125,23 @@ export const ModToolsView: FC<{}> = props => <> { isVisible && - setIsVisible(false) } /> + setIsVisible(false) } /> } diff --git a/src/components/mod-tools/views/chatlog/ChatlogView.tsx b/src/components/mod-tools/views/chatlog/ChatlogView.tsx index 63e5201..5e21080 100644 --- a/src/components/mod-tools/views/chatlog/ChatlogView.tsx +++ b/src/components/mod-tools/views/chatlog/ChatlogView.tsx @@ -1,6 +1,6 @@ import { ChatRecordData, CreateLinkEvent } from '@nitrots/nitro-renderer'; import { FC, useMemo } from 'react'; -import { TryVisitRoom } from '../../../../api'; +import { LocalizeText, TryVisitRoom } from '../../../../api'; import { Button, Column, Flex, Grid, InfiniteScroll, Text } from '../../../../common'; import { useModTools } from '../../../../hooks'; import { ChatlogRecord } from './ChatlogRecord'; @@ -49,8 +49,8 @@ export const ChatlogView: FC = props => { props.roomName }
- - + +
); @@ -61,9 +61,9 @@ export const ChatlogView: FC = props => -
Time
-
User
-
Message
+
{ LocalizeText('moderation.chatlog.col.time') }
+
{ LocalizeText('moderation.chatlog.col.user') }
+
{ LocalizeText('moderation.chatlog.col.message') }
{ (records && (records.length > 0)) && diff --git a/src/components/mod-tools/views/room/ModToolsChatlogView.tsx b/src/components/mod-tools/views/room/ModToolsChatlogView.tsx index c42320d..16d1adf 100644 --- a/src/components/mod-tools/views/room/ModToolsChatlogView.tsx +++ b/src/components/mod-tools/views/room/ModToolsChatlogView.tsx @@ -1,6 +1,6 @@ import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { SendMessageComposer } from '../../../../api'; +import { LocalizeText, SendMessageComposer } from '../../../../api'; import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useMessageEvent } from '../../../../hooks'; import { ChatlogView } from '../chatlog/ChatlogView'; @@ -34,7 +34,7 @@ export const ModToolsChatlogView: FC = props => return ( - + { roomChatlog && } diff --git a/src/components/mod-tools/views/room/ModToolsRoomView.tsx b/src/components/mod-tools/views/room/ModToolsRoomView.tsx index 37d9fc5..38dd324 100644 --- a/src/components/mod-tools/views/room/ModToolsRoomView.tsx +++ b/src/components/mod-tools/views/room/ModToolsRoomView.tsx @@ -1,6 +1,6 @@ import { CreateLinkEvent, GetModeratorRoomInfoMessageComposer, ModerateRoomMessageComposer, ModeratorActionMessageComposer, ModeratorRoomInfoEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { SendMessageComposer, TryVisitRoom } from '../../../../api'; +import { LocalizeText, SendMessageComposer, TryVisitRoom } from '../../../../api'; import { Button, Column, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; import { useMessageEvent } from '../../../../hooks'; @@ -70,7 +70,7 @@ export const ModToolsRoomView: FC = props => return ( - onCloseClick() } /> + onCloseClick() } /> { name &&
@@ -80,41 +80,41 @@ export const ModToolsRoomView: FC = props =>
- Owner: + { LocalizeText('moderation.roomtool.roomowner.title') } CreateLinkEvent(`mod-tools/open-user-info/${ ownerId }`) }>{ ownerName }
- Users in room: + { LocalizeText('moderation.roomtool.usersinroom.title') } { usersInRoom }
- Owner here: - { ownerInRoom ? 'Yes' : 'No' } + { LocalizeText('moderation.roomtool.ownerinroom.title') } + { ownerInRoom ? LocalizeText('moderation.roomtool.true.title') : LocalizeText('moderation.roomtool.false.title') }
- - + +
setKickUsers(event.target.checked) } /> - Kick everyone out + { LocalizeText('moderation.roomtool.kickall.title') }
setLockRoom(event.target.checked) } /> - Enable the doorbell + { LocalizeText('moderation.roomtool.closeroom.title') }
setChangeRoomName(event.target.checked) } /> - Change room name + { LocalizeText('moderation.roomtool.inappropiatename.title') }
- +
- - + +
diff --git a/src/components/mod-tools/views/tickets/CfhChatlogView.tsx b/src/components/mod-tools/views/tickets/CfhChatlogView.tsx index 9923fa9..ab1bbf3 100644 --- a/src/components/mod-tools/views/tickets/CfhChatlogView.tsx +++ b/src/components/mod-tools/views/tickets/CfhChatlogView.tsx @@ -1,6 +1,6 @@ import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { SendMessageComposer } from '../../../../api'; +import { LocalizeText, SendMessageComposer } from '../../../../api'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useMessageEvent } from '../../../../hooks'; import { ChatlogView } from '../chatlog/ChatlogView'; @@ -32,7 +32,7 @@ export const CfhChatlogView: FC = props => return ( - + { chatlogData && } diff --git a/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx b/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx index 7444a73..ac9d71b 100644 --- a/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx @@ -35,33 +35,33 @@ export const ModToolsIssueInfoView: FC = props => return ( <> - onIssueInfoClosed(issueId) } /> + onIssueInfoClosed(issueId) } /> - Issue Information + { LocalizeText('moderation.issue.info') } - + - + - + - + - + @@ -70,11 +70,11 @@ export const ModToolsIssueInfoView: FC = props =>
Source{ LocalizeText('moderation.issue.source') } { GetIssueCategoryName(ticket.categoryId) }
Category{ LocalizeText('moderation.issue.category') } { LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }
Description{ LocalizeText('moderation.issue.description') } { ticket.message }
Caller{ LocalizeText('moderation.issue.caller') } openUserInfo(ticket.reporterUserId) }>{ ticket.reporterUserName }
Reported User{ LocalizeText('moderation.issue.reported') } openUserInfo(ticket.reportedUserId) }>{ ticket.reportedUserName }
- - - - - + + + + +
diff --git a/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx b/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx index 9aaa441..07bc010 100644 --- a/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx @@ -1,6 +1,6 @@ import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useRef } from 'react'; -import { SendMessageComposer } from '../../../../api'; +import { LocalizeText, SendMessageComposer } from '../../../../api'; import { Button, Column, Grid } from '../../../../common'; interface ModToolsMyIssuesTabViewProps @@ -28,9 +28,9 @@ export const ModToolsMyIssuesTabView: FC = props = -
Type
-
Room/Player
-
Opened
+
{ LocalizeText('moderation.tickets.col.type') }
+
{ LocalizeText('moderation.tickets.col.roomPlayer') }
+
{ LocalizeText('moderation.tickets.col.opened') }
@@ -44,10 +44,10 @@ export const ModToolsMyIssuesTabView: FC = props =
{ issue.reportedUserName }
{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }
- +
- +
); diff --git a/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx b/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx index 387580b..bb7ba9a 100644 --- a/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx @@ -1,6 +1,6 @@ import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useRef } from 'react'; -import { SendMessageComposer } from '../../../../api'; +import { LocalizeText, SendMessageComposer } from '../../../../api'; import { Button, Column, Grid } from '../../../../common'; interface ModToolsOpenIssuesTabViewProps @@ -27,9 +27,9 @@ export const ModToolsOpenIssuesTabView: FC = pro -
Type
-
Room/Player
-
Opened
+
{ LocalizeText('moderation.tickets.col.type') }
+
{ LocalizeText('moderation.tickets.col.roomPlayer') }
+
{ LocalizeText('moderation.tickets.col.opened') }
@@ -42,7 +42,7 @@ export const ModToolsOpenIssuesTabView: FC = pro
{ issue.reportedUserName }
{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }
- +
); diff --git a/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx b/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx index ca6003e..2d04770 100644 --- a/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx @@ -1,5 +1,6 @@ import { IssueMessageData } from '@nitrots/nitro-renderer'; import { FC } from 'react'; +import { LocalizeText } from '../../../../api'; import { Column, Grid } from '../../../../common'; interface ModToolsPickedIssuesTabViewProps @@ -15,10 +16,10 @@ export const ModToolsPickedIssuesTabView: FC = -
Type
-
Room/Player
-
Opened
-
Picker
+
{ LocalizeText('moderation.tickets.col.type') }
+
{ LocalizeText('moderation.tickets.col.roomPlayer') }
+
{ LocalizeText('moderation.tickets.col.opened') }
+
{ LocalizeText('moderation.tickets.col.picker') }
diff --git a/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx b/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx index ab7ac35..c23286c 100644 --- a/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx @@ -1,5 +1,6 @@ import { GetSessionDataManager, IssueMessageData } from '@nitrots/nitro-renderer'; import { FC, useState } from 'react'; +import { LocalizeText } from '../../../../api'; import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../common'; import { useModTools } from '../../../../hooks'; import { ModToolsIssueInfoView } from './ModToolsIssueInfoView'; @@ -12,10 +13,10 @@ interface ModToolsTicketsViewProps onCloseClick: () => void; } -const TABS: string[] = [ - 'Open Issues', - 'My Issues', - 'Picked Issues' +const TAB_KEYS: string[] = [ + 'moderation.tickets.open', + 'moderation.tickets.my', + 'moderation.tickets.picked' ]; export const ModToolsTicketsView: FC = props => @@ -71,12 +72,12 @@ export const ModToolsTicketsView: FC = props => return ( <> - + - { TABS.map((tab, index) => + { TAB_KEYS.map((tabKey, index) => { return ( setCurrentTab(index) }> - { tab } + { LocalizeText(tabKey) } ); }) } diff --git a/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx b/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx index acae308..b38a622 100644 --- a/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx +++ b/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx @@ -1,6 +1,6 @@ import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { SendMessageComposer } from '../../../../api'; +import { LocalizeText, SendMessageComposer } from '../../../../api'; import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useMessageEvent } from '../../../../hooks'; import { ChatlogView } from '../chatlog/ChatlogView'; @@ -34,7 +34,7 @@ export const ModToolsUserChatlogView: FC = props = return ( - + { userChatlog && } diff --git a/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx b/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx index 1bea10a..c9d5075 100644 --- a/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx +++ b/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx @@ -11,18 +11,18 @@ interface ModToolsUserModActionViewProps } const MOD_ACTION_DEFINITIONS = [ - new ModActionDefinition(1, 'Alert', ModActionDefinition.ALERT, 1, 0), - new ModActionDefinition(2, 'Mute 1h', ModActionDefinition.MUTE, 2, 0), - new ModActionDefinition(3, 'Ban 18h', ModActionDefinition.BAN, 3, 0), - new ModActionDefinition(4, 'Ban 7 days', ModActionDefinition.BAN, 4, 0), - new ModActionDefinition(5, 'Ban 30 days (step 1)', ModActionDefinition.BAN, 5, 0), - new ModActionDefinition(7, 'Ban 30 days (step 2)', ModActionDefinition.BAN, 7, 0), - new ModActionDefinition(6, 'Ban 100 years', ModActionDefinition.BAN, 6, 0), - new ModActionDefinition(106, 'Ban avatar-only 100 years', ModActionDefinition.BAN, 6, 0), - new ModActionDefinition(101, 'Kick', ModActionDefinition.KICK, 0, 0), - new ModActionDefinition(102, 'Lock trade 1 week', ModActionDefinition.TRADE_LOCK, 0, 168), - new ModActionDefinition(104, 'Lock trade permanent', ModActionDefinition.TRADE_LOCK, 0, 876000), - new ModActionDefinition(105, 'Message', ModActionDefinition.MESSAGE, 0, 0), + new ModActionDefinition(1, 'moderation.modaction.alert', ModActionDefinition.ALERT, 1, 0), + new ModActionDefinition(2, 'moderation.modaction.mute1h', ModActionDefinition.MUTE, 2, 0), + new ModActionDefinition(3, 'moderation.modaction.ban18h', ModActionDefinition.BAN, 3, 0), + new ModActionDefinition(4, 'moderation.modaction.ban7days', ModActionDefinition.BAN, 4, 0), + new ModActionDefinition(5, 'moderation.modaction.ban30days.step1', ModActionDefinition.BAN, 5, 0), + new ModActionDefinition(7, 'moderation.modaction.ban30days.step2', ModActionDefinition.BAN, 7, 0), + new ModActionDefinition(6, 'moderation.modaction.ban100years', ModActionDefinition.BAN, 6, 0), + new ModActionDefinition(106, 'moderation.modaction.banavataronly100years', ModActionDefinition.BAN, 6, 0), + new ModActionDefinition(101, 'moderation.modaction.kick', ModActionDefinition.KICK, 0, 0), + new ModActionDefinition(102, 'moderation.modaction.locktrade1week', ModActionDefinition.TRADE_LOCK, 0, 168), + new ModActionDefinition(104, 'moderation.modaction.locktradepermanent', ModActionDefinition.TRADE_LOCK, 0, 876000), + new ModActionDefinition(105, 'moderation.modaction.message', ModActionDefinition.MESSAGE, 0, 0), ]; export const ModToolsUserModActionView: FC = props => @@ -60,7 +60,7 @@ export const ModToolsUserModActionView: FC = pro const category = topics[selectedTopic]; - if(selectedTopic === -1) errorMessage = 'You must select a CFH topic'; + if(selectedTopic === -1) errorMessage = LocalizeText('moderation.modaction.error.notopic'); if(errorMessage) return sendAlert(errorMessage); @@ -82,10 +82,10 @@ export const ModToolsUserModActionView: FC = pro const category = topics[selectedTopic]; const sanction = MOD_ACTION_DEFINITIONS[selectedAction]; - if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = 'You must select a CFH topic and Sanction'; - else if(!settings || !settings.cfhPermission) errorMessage = 'You do not have permission to do this'; - else if(!category) errorMessage = 'You must select a CFH topic'; - else if(!sanction) errorMessage = 'You must select a sanction'; + if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = LocalizeText('moderation.modaction.error.notopicorsanction'); + else if(!settings || !settings.cfhPermission) errorMessage = LocalizeText('moderation.modaction.error.nopermission'); + else if(!category) errorMessage = LocalizeText('moderation.modaction.error.notopic'); + else if(!sanction) errorMessage = LocalizeText('moderation.modaction.error.nosanction'); if(errorMessage) { @@ -101,7 +101,7 @@ export const ModToolsUserModActionView: FC = pro case ModActionDefinition.ALERT: { if(!settings.alertPermission) { - sendAlert('You have insufficient permissions'); + sendAlert(LocalizeText('moderation.modaction.error.nopermission')); return; } @@ -115,7 +115,7 @@ export const ModToolsUserModActionView: FC = pro case ModActionDefinition.BAN: { if(!settings.banPermission) { - sendAlert('You have insufficient permissions'); + sendAlert(LocalizeText('moderation.modaction.error.nopermission')); return; } @@ -126,7 +126,7 @@ export const ModToolsUserModActionView: FC = pro case ModActionDefinition.KICK: { if(!settings.kickPermission) { - sendAlert('You have insufficient permissions'); + sendAlert(LocalizeText('moderation.modaction.error.nopermission')); return; } @@ -142,7 +142,7 @@ export const ModToolsUserModActionView: FC = pro case ModActionDefinition.MESSAGE: { if(message.trim().length === 0) { - sendAlert('Please write a message to user'); + sendAlert(LocalizeText('moderation.modaction.error.emptymessage')); return; } @@ -161,23 +161,23 @@ export const ModToolsUserModActionView: FC = pro return ( - onCloseClick() } /> + onCloseClick() } />
- Optional message type, overrides default + { LocalizeText('moderation.modaction.message.hint') } - + ); diff --git a/src/components/mod-tools/views/user/ModToolsUserView.tsx b/src/components/mod-tools/views/user/ModToolsUserView.tsx index 6f65700..1d75d67 100644 --- a/src/components/mod-tools/views/user/ModToolsUserView.tsx +++ b/src/components/mod-tools/views/user/ModToolsUserView.tsx @@ -27,60 +27,60 @@ export const ModToolsUserView: FC = props => return [ { - localeKey: 'modtools.userinfo.userName', + localeKey: 'moderation.userinfo.userName', value: userInfo.userName, showOnline: true }, { - localeKey: 'modtools.userinfo.cfhCount', + localeKey: 'moderation.userinfo.cfhCount', value: userInfo.cfhCount.toString() }, { - localeKey: 'modtools.userinfo.abusiveCfhCount', + localeKey: 'moderation.userinfo.abusiveCfhCount', value: userInfo.abusiveCfhCount.toString() }, { - localeKey: 'modtools.userinfo.cautionCount', + localeKey: 'moderation.userinfo.cautionCount', value: userInfo.cautionCount.toString() }, { - localeKey: 'modtools.userinfo.banCount', + localeKey: 'moderation.userinfo.banCount', value: userInfo.banCount.toString() }, { - localeKey: 'modtools.userinfo.lastSanctionTime', + localeKey: 'moderation.userinfo.lastSanctionTime', value: userInfo.lastSanctionTime }, { - localeKey: 'modtools.userinfo.tradingLockCount', + localeKey: 'moderation.userinfo.tradingLockCount', value: userInfo.tradingLockCount.toString() }, { - localeKey: 'modtools.userinfo.tradingExpiryDate', + localeKey: 'moderation.userinfo.tradingExpiryDate', value: userInfo.tradingExpiryDate }, { - localeKey: 'modtools.userinfo.minutesSinceLastLogin', + localeKey: 'moderation.userinfo.minutesSinceLastLogin', value: FriendlyTime.format(userInfo.minutesSinceLastLogin * 60, '.ago', 2) }, { - localeKey: 'modtools.userinfo.lastPurchaseDate', + localeKey: 'moderation.userinfo.lastPurchaseDate', value: userInfo.lastPurchaseDate }, { - localeKey: 'modtools.userinfo.primaryEmailAddress', + localeKey: 'moderation.userinfo.primaryEmailAddress', value: userInfo.primaryEmailAddress }, { - localeKey: 'modtools.userinfo.identityRelatedBanCount', + localeKey: 'moderation.userinfo.identityRelatedBanCount', value: userInfo.identityRelatedBanCount.toString() }, { - localeKey: 'modtools.userinfo.registrationAgeInMinutes', + localeKey: 'moderation.userinfo.registrationAgeInMinutes', value: FriendlyTime.format(userInfo.registrationAgeInMinutes * 60, '.ago', 2) }, { - localeKey: 'modtools.userinfo.userClassification', + localeKey: 'moderation.userinfo.userClassification', value: userInfo.userClassification } ]; @@ -105,7 +105,7 @@ export const ModToolsUserView: FC = props => return ( <> - onCloseClick() } /> + onCloseClick() } /> @@ -130,16 +130,16 @@ export const ModToolsUserView: FC = props => From d6fbd19ee0799b2fc24142312edddd859873f7c9 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 16 Mar 2026 22:09:52 +0100 Subject: [PATCH 20/33] Add real-time 3D preview to floor plan editor Redesign the floor plan editor with side-by-side layout featuring: - Real-time isometric 3D preview that updates as tiles are drawn - Vertical height gradient selector with COLORMAP colors - Area counter showing total and walkable tile counts - Zoom controls (+/-) on the 2D canvas - Simplified single-row toolbar - Wall height control in the preview panel Co-Authored-By: medievalshell --- .../FloorplanEditorContext.tsx | 14 +- .../floorplan-editor/FloorplanEditorView.tsx | 156 ++++++++- .../views/FloorplanCanvasView.tsx | 105 +++--- .../views/FloorplanHeightSelector.tsx | 54 +++ .../views/FloorplanOptionsView.tsx | 247 ++++--------- .../views/FloorplanPreviewView.tsx | 328 ++++++++++++++++++ 6 files changed, 647 insertions(+), 257 deletions(-) create mode 100644 src/components/floorplan-editor/views/FloorplanHeightSelector.tsx create mode 100644 src/components/floorplan-editor/views/FloorplanPreviewView.tsx diff --git a/src/components/floorplan-editor/FloorplanEditorContext.tsx b/src/components/floorplan-editor/FloorplanEditorContext.tsx index eb528cf..1b2a3c4 100644 --- a/src/components/floorplan-editor/FloorplanEditorContext.tsx +++ b/src/components/floorplan-editor/FloorplanEditorContext.tsx @@ -8,13 +8,25 @@ interface IFloorplanEditorContext setOriginalFloorplanSettings: Dispatch>; visualizationSettings: IVisualizationSettings; setVisualizationSettings: Dispatch>; + floorHeight: number; + setFloorHeight: Dispatch>; + floorAction: number; + setFloorAction: Dispatch>; + tilemapVersion: number; + areaInfo: { total: number; walkable: number }; } const FloorplanEditorContext = createContext({ originalFloorplanSettings: null, setOriginalFloorplanSettings: null, visualizationSettings: null, - setVisualizationSettings: null + setVisualizationSettings: null, + floorHeight: 0, + setFloorHeight: null, + floorAction: 3, + setFloorAction: null, + tilemapVersion: 0, + areaInfo: { total: 0, walkable: 0 } }); export const FloorplanEditorContextProvider: FC> = props => ; diff --git a/src/components/floorplan-editor/FloorplanEditorView.tsx b/src/components/floorplan-editor/FloorplanEditorView.tsx index 59f7709..4003d13 100644 --- a/src/components/floorplan-editor/FloorplanEditorView.tsx +++ b/src/components/floorplan-editor/FloorplanEditorView.tsx @@ -1,19 +1,22 @@ import { AddLinkEventTracker, FloorHeightMapEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { FaCaretLeft, FaCaretRight } from 'react-icons/fa'; import { LocalizeText, SendMessageComposer } from '../../api'; -import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; +import { Button, ButtonGroup, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { useMessageEvent, useNitroEvent } from '../../hooks'; import { FloorplanEditorContextProvider } from './FloorplanEditorContext'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; import { IFloorplanSettings } from '@nitrots/nitro-renderer'; import { IVisualizationSettings } from '@nitrots/nitro-renderer'; -import { convertNumbersForSaving, convertSettingToNumber } from '@nitrots/nitro-renderer'; +import { convertNumbersForSaving, convertSettingToNumber, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; import { FloorplanCanvasView } from './views/FloorplanCanvasView'; import { FloorplanImportExportView } from './views/FloorplanImportExportView'; import { FloorplanOptionsView } from './views/FloorplanOptionsView'; +import { FloorplanHeightSelector } from './views/FloorplanHeightSelector'; +import { FloorplanPreviewView } from './views/FloorplanPreviewView'; - -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; +const MIN_WALL_HEIGHT = 0; +const MAX_WALL_HEIGHT = 16; export const FloorplanEditorView: FC<{}> = props => { @@ -34,7 +37,65 @@ export const FloorplanEditorView: FC<{}> = props => thicknessWall: 1, thicknessFloor: 1 }); - const [ canvasScrollHandler, setCanvasScrollHandler ] = useState<((direction: ScrollDirection) => void) | null>(null); + const [ floorHeight, setFloorHeight ] = useState(0); + const [ floorAction, setFloorAction ] = useState(FloorAction.SET); + const [ tilemapVersion, setTilemapVersion ] = useState(0); + const [ areaInfo, setAreaInfo ] = useState({ total: 0, walkable: 0 }); + + const calculateArea = useCallback(() => + { + const tilemap = FloorplanEditor.instance.tilemap; + + if(!tilemap || tilemap.length === 0) + { + setAreaInfo({ total: 0, walkable: 0 }); + + return; + } + + let total = 0; + let walkable = 0; + + for(let y = 0; y < tilemap.length; y++) + { + if(!tilemap[y]) continue; + + for(let x = 0; x < tilemap[y].length; x++) + { + if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + total++; + + if(!tilemap[y][x].isBlocked) walkable++; + } + } + + setAreaInfo({ total, walkable }); + }, []); + + // sync floorHeight/floorAction changes to the FloorplanEditor instance + useEffect(() => + { + FloorplanEditor.instance.actionSettings.currentAction = floorAction; + FloorplanEditor.instance.actionSettings.currentHeight = floorHeight.toString(36); + }, [ floorHeight, floorAction ]); + + // register onTilemapChange callback + useEffect(() => + { + if(!isVisible) return; + + FloorplanEditor.instance.onTilemapChange = () => + { + setTilemapVersion(prev => prev + 1); + calculateArea(); + }; + + return () => + { + FloorplanEditor.instance.onTilemapChange = null; + }; + }, [ isVisible, calculateArea ]); const saveFloorChanges = () => { @@ -47,16 +108,50 @@ export const FloorplanEditorView: FC<{}> = props => convertNumbersForSaving(visualizationSettings.thicknessFloor), (visualizationSettings.wallHeight - 1) )); - } + }; const revertChanges = () => { setVisualizationSettings({ wallHeight: originalFloorplanSettings.wallHeight, thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: originalFloorplanSettings.entryPointDir }); - + FloorplanEditor.instance.doorLocation = { x: originalFloorplanSettings.entryPoint[0], y: originalFloorplanSettings.entryPoint[1] }; FloorplanEditor.instance.setTilemap(originalFloorplanSettings.tilemap, originalFloorplanSettings.reservedTiles); FloorplanEditor.instance.renderTiles(); - } + }; + + const onWallHeightChange = (value: number) => + { + if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT; + + if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT; + + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; + + newValue.wallHeight = value; + + return newValue; + }); + }; + + const increaseWallHeight = () => + { + let height = (visualizationSettings.wallHeight + 1); + + if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT; + + onWallHeightChange(height); + }; + + const decreaseWallHeight = () => + { + let height = (visualizationSettings.wallHeight - 1); + + if(height <= 0) height = MIN_WALL_HEIGHT; + + onWallHeightChange(height); + }; useNitroEvent(RoomEngineEvent.DISPOSED, event => setIsVisible(false)); @@ -117,7 +212,7 @@ export const FloorplanEditorView: FC<{}> = props => const parts = url.split('/'); if(parts.length < 2) return; - + switch(parts[1]) { case 'show': @@ -140,17 +235,42 @@ export const FloorplanEditorView: FC<{}> = props => }, []); return ( - + { isVisible && - + setIsVisible(false) } /> - - canvasScrollHandler && canvasScrollHandler(direction) } /> - + + + + + + + + + { LocalizeText('floor.editor.wall.height') } + + onWallHeightChange(event.target.valueAsNumber) } /> + + + + Area: { areaInfo.total } ({ areaInfo.walkable } caselle) + + + - + - @@ -161,4 +281,4 @@ export const FloorplanEditorView: FC<{}> = props => setImportExportVisible(false) } /> } ); -} +}; diff --git a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx index e8f39a8..9db0903 100644 --- a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx +++ b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx @@ -1,25 +1,25 @@ import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useRef, useState } from 'react'; +import { FaPlus, FaMinus } from 'react-icons/fa'; import { SendMessageComposer } from '../../../api'; import { Base, Column, ColumnProps } from '../../../common'; import { useMessageEvent } from '../../../hooks'; import { useFloorplanEditorContext } from '../FloorplanEditorContext'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; - interface FloorplanCanvasViewProps extends ColumnProps { - setScrollHandler(handler: ((direction: ScrollDirection) => void) | null): void; } export const FloorplanCanvasView: FC = props => { - const { gap = 1, children = null, setScrollHandler = null, ...rest } = props; - const [ occupiedTilesReceived , setOccupiedTilesReceived ] = useState(false); + const { gap = 1, children = null, ...rest } = props; + const [ occupiedTilesReceived, setOccupiedTilesReceived ] = useState(false); const [ entryTileReceived, setEntryTileReceived ] = useState(false); + const [ zoomLevel, setZoomLevel ] = useState(1.0); const { originalFloorplanSettings = null, setOriginalFloorplanSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); const elementRef = useRef(null); + const canvasWrapperRef = useRef(null); useMessageEvent(RoomOccupiedTilesMessageEvent, event => { @@ -37,7 +37,7 @@ export const FloorplanCanvasView: FC = props => }); setOccupiedTilesReceived(true); - + elementRef.current.scrollTo((FloorplanEditor.instance.renderer.canvas.width / 3), 0); }); @@ -63,39 +63,16 @@ export const FloorplanCanvasView: FC = props => return newValue; }); - + FloorplanEditor.instance.doorLocation = { x: parser.x, y: parser.y }; setEntryTileReceived(true); }); - const onClickArrowButton = (scrollDirection: ScrollDirection) => - { - const element = elementRef.current; - - if(!element) return; - - switch(scrollDirection) - { - case 'up': - element.scrollBy({ top: -10 }); - break; - case 'down': - element.scrollBy({ top: 10 }); - break; - case 'left': - element.scrollBy({ left: -10 }); - break; - case 'right': - element.scrollBy({ left: 10 }); - break; - } - } - const onPointerEvent = (event: PointerEvent) => { event.preventDefault(); - + switch(event.type) { case 'pointerout': @@ -109,7 +86,10 @@ export const FloorplanCanvasView: FC = props => FloorplanEditor.instance.onPointerMove(event); break; } - } + }; + + const zoomIn = () => setZoomLevel(prev => Math.min(prev + 0.25, 2.0)); + const zoomOut = () => setZoomLevel(prev => Math.max(prev - 0.25, 0.5)); useEffect(() => { @@ -124,15 +104,15 @@ export const FloorplanCanvasView: FC = props => thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: prevValue.entryPointDir - } + }; }); - } + }; }, [ originalFloorplanSettings.thicknessFloor, originalFloorplanSettings.thicknessWall, originalFloorplanSettings.wallHeight, setVisualizationSettings ]); useEffect(() => { if(!entryTileReceived || !occupiedTilesReceived) return; - + FloorplanEditor.instance.renderTiles(); }, [ entryTileReceived, occupiedTilesReceived ]); @@ -144,45 +124,56 @@ export const FloorplanCanvasView: FC = props => const currentElement = elementRef.current; if(!currentElement) return; - - currentElement.appendChild(FloorplanEditor.instance.renderer.canvas); + + const wrapper = canvasWrapperRef.current; + + if(wrapper) wrapper.appendChild(FloorplanEditor.instance.renderer.canvas); currentElement.addEventListener('pointerup', onPointerEvent); - currentElement.addEventListener('pointerout', onPointerEvent); - currentElement.addEventListener('pointerdown', onPointerEvent); - currentElement.addEventListener('pointermove', onPointerEvent); - return () => + return () => { if(currentElement) { currentElement.removeEventListener('pointerup', onPointerEvent); - currentElement.removeEventListener('pointerout', onPointerEvent); - currentElement.removeEventListener('pointerdown', onPointerEvent); - currentElement.removeEventListener('pointermove', onPointerEvent); } - } + }; }, []); - useEffect(() => - { - if(!setScrollHandler) return; - - setScrollHandler(() => onClickArrowButton); - - return () => setScrollHandler(null); - }, [ setScrollHandler ]); - return ( - - + + +
+ +
+ + +
{ children } ); -} +}; diff --git a/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx b/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx new file mode 100644 index 0000000..8163c98 --- /dev/null +++ b/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react'; +import { COLORMAP, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; +import { FloorplanEditor } from '@nitrots/nitro-renderer'; +import { Column, Text } from '../../../common'; +import { useFloorplanEditorContext } from '../FloorplanEditorContext'; + +const colormap = COLORMAP as Record; + +export const FloorplanHeightSelector: FC<{}> = () => +{ + const { floorHeight, setFloorHeight, setFloorAction } = useFloorplanEditorContext(); + + const onSelectHeight = (height: number) => + { + setFloorHeight(height); + setFloorAction(FloorAction.SET); + + FloorplanEditor.instance.actionSettings.currentAction = FloorAction.SET; + FloorplanEditor.instance.actionSettings.currentHeight = height.toString(36); + }; + + const heights: number[] = []; + + for(let i = 26; i >= 0; i--) heights.push(i); + + return ( + + { floorHeight } +
+ { heights.map(h => + { + const char = HEIGHT_SCHEME[h + 1]; + const color = colormap[char] || '101010'; + const isActive = (floorHeight === h); + + return ( +
onSelectHeight(h) } + title={ `${ h }` } + /> + ); + }) } +
+ + ); +}; diff --git a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx index 5207b15..d4e7705 100644 --- a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx +++ b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx @@ -1,45 +1,32 @@ -import { FC, useState } from 'react'; -import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaCaretLeft, FaCaretRight } from 'react-icons/fa'; +import { FC } from 'react'; import { LocalizeText } from '../../../api'; -import { Button, Column, Flex, LayoutGridItem, Slider, Text } from '../../../common'; -import { COLORMAP, FloorAction } from '@nitrots/nitro-renderer'; +import { Flex, LayoutGridItem, Text } from '../../../common'; +import { FloorAction } from '@nitrots/nitro-renderer'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; import { useFloorplanEditorContext } from '../FloorplanEditorContext'; -const MIN_WALL_HEIGHT: number = 0; -const MAX_WALL_HEIGHT: number = 16; - -const MIN_FLOOR_HEIGHT: number = 0; -const MAX_FLOOR_HEIGHT: number = 26; - -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; - interface FloorplanOptionsViewProps { - onCanvasScroll?(direction: ScrollDirection): void; } export const FloorplanOptionsView: FC = props => { - const { onCanvasScroll = () => {} } = props; - const { visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); - const [ floorAction, setFloorAction ] = useState(FloorAction.SET); - const [ floorHeight, setFloorHeight ] = useState(0); - const [ isSquareSelectMode, setSquareSelectMode ] = useState(false); - + const { visualizationSettings = null, setVisualizationSettings = null, floorAction, setFloorAction } = useFloorplanEditorContext(); + const isSquareSelectMode = FloorplanEditor.instance.isSquareSelectMode; + const selectAction = (action: number) => { setFloorAction(action); FloorplanEditor.instance.actionSettings.currentAction = action; - } + }; const toggleSquareSelectMode = () => { - const nextValue = FloorplanEditor.instance.toggleSquareSelectMode(); - - setSquareSelectMode(nextValue); - } + FloorplanEditor.instance.toggleSquareSelectMode(); + // force re-render by toggling action to same value + setFloorAction(prev => prev); + }; const changeDoorDirection = () => { @@ -58,18 +45,19 @@ export const FloorplanOptionsView: FC = props => return newValue; }); - } + }; - const onFloorHeightChange = (value: number) => + const onWallThicknessChange = (value: number) => { - if(isNaN(value) || (value <= 0)) value = 0; + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; - if(value > 26) value = 26; + newValue.thicknessWall = value; - setFloorHeight(value); - - FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36); - } + return newValue; + }); + }; const onFloorThicknessChange = (value: number) => { @@ -81,157 +69,54 @@ export const FloorplanOptionsView: FC = props => return newValue; }); - } - - const onWallThicknessChange = (value: number) => - { - setVisualizationSettings(prevValue => - { - const newValue = { ...prevValue }; - - newValue.thicknessWall = value; - - return newValue; - }); - } - - const onWallHeightChange = (value: number) => - { - if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT; - - if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT; - - setVisualizationSettings(prevValue => - { - const newValue = { ...prevValue }; - - newValue.wallHeight = value; - - return newValue; - }); - } - - const increaseWallHeight = () => - { - let height = (visualizationSettings.wallHeight + 1); - - if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT; - - onWallHeightChange(height); - } - - const decreaseWallHeight = () => - { - let height = (visualizationSettings.wallHeight - 1); - - if(height <= 0) height = MIN_WALL_HEIGHT; - - onWallHeightChange(height); - } + }; return ( - - - - { LocalizeText('floor.plan.editor.draw.mode') } - - - selectAction(FloorAction.SET) }> - - - selectAction(FloorAction.UNSET) }> - - - - - selectAction(FloorAction.UP) }> - - - selectAction(FloorAction.DOWN) }> - - - - selectAction(FloorAction.DOOR) }> - - - FloorplanEditor.instance.toggleSelectAll() }> - - - - - - - - - { LocalizeText('floor.plan.editor.enter.direction') } - - - - { LocalizeText('floor.editor.wall.height') } - - - onWallHeightChange(event.target.valueAsNumber) } /> - - - - - { LocalizeText('floor.plan.editor.room.options') } - - - - - + + + { LocalizeText('floor.plan.editor.draw.mode') } + + selectAction(FloorAction.SET) }> + + + selectAction(FloorAction.UNSET) }> + + + selectAction(FloorAction.UP) }> + + + selectAction(FloorAction.DOWN) }> + + + selectAction(FloorAction.DOOR) }> + + + FloorplanEditor.instance.toggleSelectAll() }> + + + + + + - - - { LocalizeText('floor.plan.editor.tile.height') }: { floorHeight } -
- onFloorHeightChange(event) } - renderThumb={ (props, state) => - { - const { key, style, ...rest } = (props as Record); - - return
{ state.valueNow }
; - } } /> -
-
- - - - - - -
- - - - - - + + { LocalizeText('floor.plan.editor.enter.direction') } + - + + + + + ); -} \ No newline at end of file +}; diff --git a/src/components/floorplan-editor/views/FloorplanPreviewView.tsx b/src/components/floorplan-editor/views/FloorplanPreviewView.tsx new file mode 100644 index 0000000..cd82a9c --- /dev/null +++ b/src/components/floorplan-editor/views/FloorplanPreviewView.tsx @@ -0,0 +1,328 @@ +import { FC, useEffect, useRef } from 'react'; +import { COLORMAP, HEIGHT_SCHEME, FloorplanEditor } from '@nitrots/nitro-renderer'; +import { useFloorplanEditorContext } from '../FloorplanEditorContext'; + +const colormap = COLORMAP as Record; + +const PREVIEW_TILE_W = 16; +const PREVIEW_TILE_H = 8; +const PREVIEW_BLOCK_H = 5; +const WALL_HEIGHT_PX = 40; +const WALL_COLOR = '#6B7B5E'; +const WALL_SIDE_COLOR = '#5A6A4F'; +const WALL_TOP_COLOR = '#7D8E6F'; + +function hexToRgb(hex: string): [number, number, number] +{ + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + return [ r, g, b ]; +} + +function rgbToHex(r: number, g: number, b: number): string +{ + return `#${ ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) }`; +} + +function darken(hex: string, factor: number): string +{ + const [ r, g, b ] = hexToRgb(hex); + + return rgbToHex( + Math.floor(r * factor), + Math.floor(g * factor), + Math.floor(b * factor) + ); +} + +function getTilemapBounds(tilemap: any[][]): { minX: number; minY: number; maxX: number; maxY: number } +{ + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + + for(let y = 0; y < tilemap.length; y++) + { + if(!tilemap[y]) continue; + + for(let x = 0; x < tilemap[y].length; x++) + { + if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + if(x < minX) minX = x; + if(x > maxX) maxX = x; + if(y < minY) minY = y; + if(y > maxY) maxY = y; + } + } + + if(minX === Infinity) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; + + return { minX, minY, maxX, maxY }; +} + +function renderPreview(canvas: HTMLCanvasElement, wallHeight: number): void +{ + const ctx = canvas.getContext('2d'); + const tilemap = FloorplanEditor.instance.tilemap; + + if(!ctx || !tilemap || tilemap.length === 0) + { + if(ctx) + { + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + return; + } + + const bounds = getTilemapBounds(tilemap); + const tilesW = bounds.maxX - bounds.minX + 1; + const tilesH = bounds.maxY - bounds.minY + 1; + + // find max height for offset calculation + let maxTileHeight = 0; + + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!tilemap[y] || !tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + const hi = HEIGHT_SCHEME.indexOf(tilemap[y][x].height) - 1; + + if(hi > maxTileHeight) maxTileHeight = hi; + } + } + + // calculate isometric bounds + const isoW = (tilesW + tilesH) * PREVIEW_TILE_W; + const isoH = (tilesW + tilesH) * PREVIEW_TILE_H + maxTileHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX; + + // scale to fit canvas + const scaleX = (canvas.width - 20) / isoW; + const scaleY = (canvas.height - 20) / isoH; + const scale = Math.min(scaleX, scaleY, 3); + + const offsetX = (canvas.width - isoW * scale) / 2; + const offsetY = (canvas.height - isoH * scale) / 2 + WALL_HEIGHT_PX * scale * 0.5; + + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.save(); + ctx.translate(offsetX, offsetY); + ctx.scale(scale, scale); + + const tw = PREVIEW_TILE_W; + const th = PREVIEW_TILE_H; + + function isoX(gx: number, gy: number): number + { + return (gx - bounds.minX - gy + bounds.minY) * tw + (tilesH - 1) * tw; + } + + function isoY(gx: number, gy: number): number + { + return (gx - bounds.minX + gy - bounds.minY) * th; + } + + function hasActiveTile(gx: number, gy: number): boolean + { + return tilemap[gy] && tilemap[gy][gx] && tilemap[gy][gx].height !== 'x'; + } + + function getTileHeight(gx: number, gy: number): number + { + if(!hasActiveTile(gx, gy)) return 0; + + return Math.max(0, HEIGHT_SCHEME.indexOf(tilemap[gy][gx].height) - 1); + } + + // draw walls on north and west edges + const wallH = wallHeight > 0 ? wallHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX * 0.3 : WALL_HEIGHT_PX * 0.6; + + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!hasActiveTile(x, y)) continue; + + const tileH = getTileHeight(x, y) * PREVIEW_BLOCK_H; + const cx = isoX(x, y); + const cy = isoY(x, y) - tileH; + + // west wall (no tile to the left) + if(!hasActiveTile(x - 1, y)) + { + ctx.beginPath(); + ctx.moveTo(cx, cy + th); + ctx.lineTo(cx, cy + th - wallH); + ctx.lineTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw, cy); + ctx.closePath(); + ctx.fillStyle = WALL_SIDE_COLOR; + ctx.fill(); + ctx.strokeStyle = '#4A5A3F'; + ctx.lineWidth = 0.5; + ctx.stroke(); + } + + // north wall (no tile above) + if(!hasActiveTile(x, y - 1)) + { + ctx.beginPath(); + ctx.moveTo(cx + tw, cy); + ctx.lineTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw * 2, cy + th - wallH); + ctx.lineTo(cx + tw * 2, cy + th); + ctx.closePath(); + ctx.fillStyle = WALL_COLOR; + ctx.fill(); + ctx.strokeStyle = '#4A5A3F'; + ctx.lineWidth = 0.5; + ctx.stroke(); + } + + // wall top cap - corner + if(!hasActiveTile(x - 1, y) && !hasActiveTile(x, y - 1)) + { + ctx.beginPath(); + ctx.moveTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw + tw * 0.3, cy - wallH - th * 0.3); + ctx.lineTo(cx + tw, cy - wallH - th * 0.6); + ctx.lineTo(cx + tw - tw * 0.3, cy - wallH - th * 0.3); + ctx.closePath(); + ctx.fillStyle = WALL_TOP_COLOR; + ctx.fill(); + } + } + } + + // draw tiles back-to-front + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!hasActiveTile(x, y)) continue; + + const tile = tilemap[y][x]; + const heightIndex = HEIGHT_SCHEME.indexOf(tile.height) - 1; + const tileH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; + + const cx = isoX(x, y); + const cy = isoY(x, y) - tileH; + + const heightChar = tile.height; + const baseColor = colormap[heightChar] || 'aaaaaa'; + const topColor = `#${ baseColor }`; + const leftColor = darken(baseColor, 0.65); + const rightColor = darken(baseColor, 0.80); + + // draw side faces if tile has height + const blockH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; + + // left face (visible when no neighbor to south or neighbor is shorter) + const southH = getTileHeight(x, y + 1); + const leftExpose = hasActiveTile(x, y + 1) ? Math.max(0, heightIndex - southH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; + + if(leftExpose > 0) + { + ctx.beginPath(); + ctx.moveTo(cx, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx + tw, cy + th * 2 + leftExpose); + ctx.lineTo(cx, cy + th + leftExpose); + ctx.closePath(); + ctx.fillStyle = leftColor; + ctx.fill(); + } + + // right face + const eastH = getTileHeight(x + 1, y); + const rightExpose = hasActiveTile(x + 1, y) ? Math.max(0, heightIndex - eastH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; + + if(rightExpose > 0) + { + ctx.beginPath(); + ctx.moveTo(cx + tw * 2, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx + tw, cy + th * 2 + rightExpose); + ctx.lineTo(cx + tw * 2, cy + th + rightExpose); + ctx.closePath(); + ctx.fillStyle = rightColor; + ctx.fill(); + } + + // top face + ctx.beginPath(); + ctx.moveTo(cx + tw, cy); + ctx.lineTo(cx + tw * 2, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx, cy + th); + ctx.closePath(); + ctx.fillStyle = topColor; + ctx.fill(); + + // door indicator + const door = FloorplanEditor.instance.doorLocation; + + if(door.x === x && door.y === y) + { + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.fill(); + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + } + + ctx.restore(); +} + +export const FloorplanPreviewView: FC<{}> = () => +{ + const { tilemapVersion, visualizationSettings } = useFloorplanEditorContext(); + const canvasRef = useRef(null); + const rafRef = useRef(0); + + useEffect(() => + { + if(!canvasRef.current) return; + + if(rafRef.current) cancelAnimationFrame(rafRef.current); + + rafRef.current = requestAnimationFrame(() => + { + const canvas = canvasRef.current; + + if(!canvas) return; + + const parent = canvas.parentElement; + + if(parent) + { + canvas.width = parent.clientWidth; + canvas.height = parent.clientHeight; + } + + renderPreview(canvas, visualizationSettings?.wallHeight ?? 0); + }); + + return () => + { + if(rafRef.current) cancelAnimationFrame(rafRef.current); + }; + }, [ tilemapVersion, visualizationSettings?.wallHeight ]); + + return ( +
+ +
+ ); +}; From f3386263271c3e7759f70cb7bc1bf0f6c7605ce4 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 19 Mar 2026 09:42:40 +0100 Subject: [PATCH 21/33] =?UTF-8?q?=F0=9F=86=99=20New=20logo=20and=20small?= =?UTF-8?q?=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/loading/LoadingView.tsx | 20 +++++++++++++------ .../confirm-layouts/GetConfirmLayout.tsx | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/components/loading/LoadingView.tsx b/src/components/loading/LoadingView.tsx index b947e04..a9dc9bc 100644 --- a/src/components/loading/LoadingView.tsx +++ b/src/components/loading/LoadingView.tsx @@ -2,21 +2,29 @@ import { FC } from 'react'; import { Base, Column, Text } from '../../common'; interface LoadingViewProps { - isError: boolean; - message: string; + isError?: boolean; + message?: string; } export const LoadingView: FC = props => { const { isError = false, message = '' } = props; - + return ( - - + { !isError && + } + { isError && (message && message.length) ? - { message } + + + Something went wrong while loading + + + { message } + + : The hotel is loading ... diff --git a/src/components/notification-center/views/confirm-layouts/GetConfirmLayout.tsx b/src/components/notification-center/views/confirm-layouts/GetConfirmLayout.tsx index 3c5720c..fd142de 100644 --- a/src/components/notification-center/views/confirm-layouts/GetConfirmLayout.tsx +++ b/src/components/notification-center/views/confirm-layouts/GetConfirmLayout.tsx @@ -10,6 +10,6 @@ export const GetConfirmLayout = (item: NotificationConfirmItem, onClose: () => v switch(item.confirmType) { default: - return ; + return ; } }; From 194e8cf3a8afed915694a8a43544e96e44bac93d Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 19 Mar 2026 10:39:56 +0100 Subject: [PATCH 22/33] Revert "Merge pull request #16 from simoleo89/feature/ui-customization" This reverts commit d1a59962686367495e3f300a88b94ab9dee91f62, reversing changes made to ae4ecc42f0e879ce3576535fd188e0b85229d31a. --- package.json | 1 - public/ui-config.json | 2434 ----------------- src/App.tsx | 16 +- src/api/ui-settings/IUiSettings.ts | 21 - src/api/ui-settings/UiSettingsContext.tsx | 164 -- src/api/ui-settings/index.ts | 2 - src/common/Button.tsx | 40 +- src/common/card/NitroCardHeaderView.tsx | 10 +- src/common/card/tabs/NitroCardTabsView.tsx | 12 +- src/components/MainView.tsx | 7 - .../InterfaceColorTabView.tsx | 179 -- .../InterfaceImageTabView.tsx | 52 - .../InterfaceProfileTabView.tsx | 107 - .../InterfaceSettingsView.tsx | 74 - .../infostand/InfoStandBadgeSlotView.tsx | 3 +- .../infostand/InfoStandWidgetFurniView.tsx | 37 +- .../infostand/InfoStandWidgetUserView.tsx | 20 +- .../context-menu/ContextMenuHeaderView.tsx | 11 +- .../context-menu/ContextMenuListItemView.tsx | 11 +- .../widgets/context-menu/ContextMenuView.tsx | 2 +- src/components/toolbar/ToolbarView.tsx | 60 +- src/css/common/Buttons.css | 14 +- src/css/purse/PurseView.css | 4 +- src/css/room/InfoStand.css | 2 +- src/css/room/RoomWidgets.css | 6 +- 25 files changed, 97 insertions(+), 3192 deletions(-) delete mode 100644 public/ui-config.json delete mode 100644 src/api/ui-settings/IUiSettings.ts delete mode 100644 src/api/ui-settings/UiSettingsContext.tsx delete mode 100644 src/api/ui-settings/index.ts delete mode 100644 src/components/interface-settings/InterfaceColorTabView.tsx delete mode 100644 src/components/interface-settings/InterfaceImageTabView.tsx delete mode 100644 src/components/interface-settings/InterfaceProfileTabView.tsx delete mode 100644 src/components/interface-settings/InterfaceSettingsView.tsx diff --git a/package.json b/package.json index 716c4bf..4dab2f5 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "framer-motion": "^11.2.12", "react": "^19.2.4", "react-bootstrap": "^2.10.10", - "react-colorful": "^5.6.1", "react-dom": "^19.2.4", "react-icons": "^5.5.0", "react-slider": "^2.0.6", diff --git a/public/ui-config.json b/public/ui-config.json deleted file mode 100644 index cec08d1..0000000 --- a/public/ui-config.json +++ /dev/null @@ -1,2434 +0,0 @@ -{ - "external.plugins": [ - "plugins/room-builder.js" - ], - "ui.header.images.count": 30, - "ui.header.images.url": "https://image.webbo.city/image/headerImage/image{id}.gif", - "image.library.notifications.url": "${image.library.url}notifications/%image%.png", - "achievements.images.url": "${image.library.url}Quests/%image%.png", - "camera.url": "/swf/usercontent/camera/", - "thumbnails.url": "/swf/usercontent/thumbnails/%thumbnail%.png", - "url.prefix": "", - "habbopages.url": "/swf/habbopages/", - "group.homepage.url": "${url.prefix}/groups/%groupid%/id", - "guide.help.alpha.groupid": 0, - "chat.viewer.height.percentage": 0.4, - "pathfinder.underpass.height": 1.5, - "widget.dimmer.colorwheel": false, - "avatar.wardrobe.max.slots": 10, - "user.badges.max.slots": 6, - "user.badges.group.slot.enabled": false, - "user.tags.enabled": false, - "camera.publish.disabled": false, - "hc.disabled": false, - "badge.descriptions.enabled": true, - "motto.max.length": 38, - "bot.name.max.length": 15, - "pet.package.name.max.length": 15, - "wired.action.bot.talk.to.avatar.max.length": 64, - "wired.action.bot.talk.max.length": 64, - "wired.action.chat.max.length": 100, - "wired.action.kick.from.room.max.length": 100, - "wired.action.mute.user.max.length": 100, - "game.center.enabled": false, - "guides.enabled": true, - "toolbar.hide.quests": true, - "navigator.room.models": [{ - "clubLevel": 0, - "tileSize": 104, - "name": "a" - }, { - "clubLevel": 0, - "tileSize": 94, - "name": "b" - }, { - "clubLevel": 0, - "tileSize": 36, - "name": "c" - }, { - "clubLevel": 0, - "tileSize": 84, - "name": "d" - }, { - "clubLevel": 0, - "tileSize": 80, - "name": "e" - }, { - "clubLevel": 0, - "tileSize": 80, - "name": "f" - }, { - "clubLevel": 0, - "tileSize": 416, - "name": "i" - }, { - "clubLevel": 0, - "tileSize": 320, - "name": "j" - }, { - "clubLevel": 0, - "tileSize": 448, - "name": "k" - }, { - "clubLevel": 0, - "tileSize": 352, - "name": "l" - }, { - "clubLevel": 0, - "tileSize": 384, - "name": "m" - }, { - "clubLevel": 0, - "tileSize": 372, - "name": "n" - }, { - "clubLevel": 1, - "tileSize": 80, - "name": "g" - }, { - "clubLevel": 1, - "tileSize": 74, - "name": "h" - }, { - "clubLevel": 1, - "tileSize": 416, - "name": "o" - }, { - "clubLevel": 1, - "tileSize": 352, - "name": "p" - }, { - "clubLevel": 1, - "tileSize": 304, - "name": "q" - }, { - "clubLevel": 1, - "tileSize": 336, - "name": "r" - }, { - "clubLevel": 1, - "tileSize": 748, - "name": "u" - }, { - "clubLevel": 1, - "tileSize": 438, - "name": "v" - }, { - "clubLevel": 2, - "tileSize": 540, - "name": "t" - }, { - "clubLevel": 2, - "tileSize": 512, - "name": "w" - }, { - "clubLevel": 2, - "tileSize": 396, - "name": "x" - }, { - "clubLevel": 2, - "tileSize": 440, - "name": "y" - }, { - "clubLevel": 2, - "tileSize": 456, - "name": "z" - }, { - "clubLevel": 2, - "tileSize": 208, - "name": "0" - }, { - "clubLevel": 2, - "tileSize": 1009, - "name": "1" - }, { - "clubLevel": 2, - "tileSize": 1044, - "name": "2" - }, { - "clubLevel": 2, - "tileSize": 183, - "name": "3" - }, { - "clubLevel": 2, - "tileSize": 254, - "name": "4" - }, { - "clubLevel": 2, - "tileSize": 1024, - "name": "5" - }, { - "clubLevel": 2, - "tileSize": 801, - "name": "6" - }, { - "clubLevel": 2, - "tileSize": 354, - "name": "7" - }, { - "clubLevel": 2, - "tileSize": 888, - "name": "8" - }, { - "clubLevel": 2, - "tileSize": 926, - "name": "9" - } - ], - "backgrounds.data": [{ - "backgroundId": 0, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 1, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 2, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 3, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 4, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 5, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 6, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 7, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 8, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 9, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 10, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 11, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 12, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 13, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 14, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 15, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 16, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 17, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 18, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 19, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 20, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 21, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 22, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 23, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 24, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 25, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 26, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 27, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 28, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 29, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 30, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 31, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 32, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 33, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 34, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 35, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 36, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 37, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 38, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 39, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 40, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 41, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 42, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 43, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 44, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 45, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 46, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 47, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 48, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 49, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 50, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 51, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 52, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 53, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 54, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 55, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 56, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 57, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 58, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 59, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 60, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 61, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 62, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 63, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 64, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 65, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 66, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 67, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 68, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 69, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 70, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 71, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 72, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 73, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 74, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 75, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 76, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 77, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 78, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 79, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 80, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 81, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 82, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 83, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 84, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 85, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 86, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 87, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 88, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 89, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 90, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 91, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 92, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 93, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 94, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 95, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 96, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 97, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 98, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 99, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 100, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 101, - "minRank": 2, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "backgroundId": 102, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 103, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 104, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 105, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 106, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 107, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 108, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 109, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 110, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 111, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 112, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 113, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 114, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 115, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 116, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 117, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 118, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 119, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 120, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 121, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 122, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 123, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 124, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 125, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 126, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 127, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 128, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 129, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 130, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 131, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 132, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 133, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 134, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 135, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 136, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 137, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 138, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 139, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 140, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 141, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 142, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 143, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 144, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 145, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 146, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 147, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 148, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 149, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 150, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 151, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 152, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 153, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 154, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 155, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 156, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 157, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 158, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 159, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 160, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 161, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 162, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 163, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 164, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 165, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 166, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 167, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 168, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 169, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 170, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 171, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 172, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 173, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 174, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 175, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 176, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 177, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 178, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 179, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 180, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 181, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 182, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 183, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 184, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 185, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 186, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "backgroundId": 187, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - } - ], - "stands.data": [{ - "standId": 0, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 1, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 2, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 3, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 4, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 5, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 6, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 7, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 8, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 9, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 10, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 11, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 12, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 13, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 14, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 15, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "standId": 16, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "standId": 17, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "standId": 18, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "standId": 19, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "standId": 20, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "standId": 21, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - } - ], - "overlays.data": [{ - "overlayId": 0, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "overlayId": 1, - "minRank": 0, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "overlayId": 2, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "overlayId": 3, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "overlayId": 4, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "overlayId": 5, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "overlayId": 6, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "overlayId": 7, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "overlayId": 8, - "minRank": 0, - "isHcOnly": true, - "isAmbassadorOnly": false - } - ], - "hotelview": { - "room.pool": "791", - "room.picnic": "2193", - "room.rooftop": "", - "room.rooftop.pool": "", - "room.peaceful": "", - "room.infobus": "5956", - "room.lobby": "1450", - "show.avatar": true, - "widgets": { - "slot.1.widget": "", - "slot.1.conf": {}, - "slot.2.widget": "", - "slot.2.conf": { - "image": "", - "texts": "", - "btnLink": "" - }, - "slot.3.widget": "", - "slot.3.conf": {}, - "slot.4.widget": "", - "slot.4.conf": {}, - "slot.5.widget": "", - "slot.5.conf": {}, - "slot.6.widget": "", - "slot.6.conf": { - "campaign": "" - }, - "slot.7.widget": "", - "slot.7.conf": {} - }, - "images": { - "background": "${asset.url}/images/reception/stretch_blue.png", - "background.colour": "#8ee0f0", - "sun": "${asset.url}/images/reception/sun.png", - "drape": "${asset.url}/images/reception/drape.png", - "left": "", - "right": "", - "right.repeat": "" - } - }, - "achievements.unseen.ignored": [ - "ACH_AllTimeHotelPresence" - ], - "avatareditor.show.clubitems.dimmed": true, - "avatareditor.show.clubitems.first": true, - "chat.history.max.items": 100, - "system.currency.types": [ - -1, - 0, - 5 - ], - "catalog.links": { - "hc.buy_hc": "habbo_club", - "hc.hc_gifts": "club_gifts", - "pets.buy_food": "pet_food", - "pets.buy_saddle": "saddles" - }, - "hc.center": { - "benefits.info": true, - "payday.info": true, - "gift.info": true, - "benefits.habbopage": "habboclub", - "payday.habbopage": "hcpayday" - }, - "respect.options": { - "enabled": false, - "sound": "sound_respect_received" - }, - "currency.display.number.short": false, - "currency.asset.icon.url": "${images.url}/wallet/%type%.png", - "catalog.asset.url": "${image.library.url}catalogue", - "catalog.asset.image.url": "${catalog.asset.url}/%name%.gif", - "catalog.asset.icon.url": "${catalog.asset.url}/icon_%name%.png", - "catalog.tab.icons": false, - "catalog.headers": false, - "chat.input.maxlength": 100, - "chat.styles.disabled": [], - "chat.styles": [{ - "styleId": 0, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 1, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 2, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 3, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 4, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 5, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 6, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 7, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 8, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 9, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 10, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 11, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 12, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 13, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 14, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 15, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 16, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 17, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 18, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 19, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 20, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 21, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 22, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 23, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 24, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 25, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 26, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 27, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 28, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 29, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 30, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 31, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 32, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 33, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 34, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 35, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 36, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 37, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 38, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 39, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 40, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 41, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 42, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 43, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 44, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 45, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 46, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 47, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 48, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 49, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 50, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 51, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 52, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 53, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - } - ], - "camera.available.effects": [{ - "name": "dark_sepia", - "colorMatrix": [ - 0.4, - 0.4, - 0.1, - 0, - 110, - 0.3, - 0.4, - 0.1, - 0, - 30, - 0.3, - 0.2, - 0.1, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 0, - "enabled": true - }, { - "name": "increase_saturation", - "colorMatrix": [ - 2, - -0.5, - -0.5, - 0, - 0, - -0.5, - 2, - -0.5, - 0, - 0, - -0.5, - -0.5, - 2, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 0, - "enabled": true - }, { - "name": "increase_contrast", - "colorMatrix": [ - 1.5, - 0, - 0, - 0, - -50, - 0, - 1.5, - 0, - 0, - -50, - 0, - 0, - 1.5, - 0, - -50, - 0, - 0, - 0, - 1.5, - 0 - ], - "minLevel": 0, - "enabled": true - }, { - "name": "shadow_multiply_02", - "colorMatrix": [], - "minLevel": 0, - "blendMode": 2, - "enabled": true - }, { - "name": "color_1", - "colorMatrix": [ - 0.393, - 0.769, - 0.189, - 0, - 0, - 0.349, - 0.686, - 0.168, - 0, - 0, - 0.272, - 0.534, - 0.131, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 1, - "enabled": true - }, { - "name": "hue_bright_sat", - "colorMatrix": [ - 1, - 0.6, - 0.2, - 0, - -50, - 0.2, - 1, - 0.6, - 0, - -50, - 0.6, - 0.2, - 1, - 0, - -50, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 1, - "enabled": true - }, { - "name": "hearts_hardlight_02", - "colorMatrix": [], - "minLevel": 1, - "blendMode": 9, - "enabled": true - }, { - "name": "texture_overlay", - "colorMatrix": [], - "minLevel": 1, - "blendMode": 4, - "enabled": true - }, { - "name": "pinky_nrm", - "colorMatrix": [], - "minLevel": 1, - "blendMode": 0, - "enabled": true - }, { - "name": "color_2", - "colorMatrix": [ - 0.333, - 0.333, - 0.333, - 0, - 0, - 0.333, - 0.333, - 0.333, - 0, - 0, - 0.333, - 0.333, - 0.333, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 2, - "enabled": true - }, { - "name": "night_vision", - "colorMatrix": [ - 0, - 0, - 0, - 0, - 0, - 0, - 1.1, - 0, - 0, - -50, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 2, - "enabled": true - }, { - "name": "stars_hardlight_02", - "colorMatrix": [], - "minLevel": 2, - "blendMode": 9, - "enabled": true - }, { - "name": "coffee_mpl", - "colorMatrix": [], - "minLevel": 2, - "blendMode": 2, - "enabled": true - }, { - "name": "security_hardlight", - "colorMatrix": [], - "minLevel": 3, - "blendMode": 9, - "enabled": true - }, { - "name": "bluemood_mpl", - "colorMatrix": [], - "minLevel": 3, - "blendMode": 2, - "enabled": true - }, { - "name": "rusty_mpl", - "colorMatrix": [], - "minLevel": 3, - "blendMode": 2, - "enabled": true - }, { - "name": "decr_conrast", - "colorMatrix": [ - 0.5, - 0, - 0, - 0, - 50, - 0, - 0.5, - 0, - 0, - 50, - 0, - 0, - 0.5, - 0, - 50, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 4, - "enabled": true - }, { - "name": "green_2", - "colorMatrix": [ - 0.5, - 0.5, - 0.5, - 0, - 0, - 0.5, - 0.5, - 0.5, - 0, - 90, - 0.5, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 4, - "enabled": true - }, { - "name": "alien_hrd", - "colorMatrix": [], - "minLevel": 4, - "blendMode": 9, - "enabled": true - }, { - "name": "color_3", - "colorMatrix": [ - 0.609, - 0.609, - 0.082, - 0, - 0, - 0.309, - 0.609, - 0.082, - 0, - 0, - 0.309, - 0.609, - 0.082, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 5, - "enabled": true - }, { - "name": "color_4", - "colorMatrix": [ - 0.8, - -0.8, - 1, - 0, - 70, - 0.8, - -0.8, - 1, - 0, - 70, - 0.8, - -0.8, - 1, - 0, - 70, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 5, - "enabled": true - }, { - "name": "toxic_hrd", - "colorMatrix": [], - "minLevel": 5, - "blendMode": 9, - "enabled": true - }, { - "name": "hypersaturated", - "colorMatrix": [ - 2, - -1, - 0, - 0, - 0, - -1, - 2, - 0, - 0, - 0, - 0, - -1, - 2, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 6, - "enabled": true - }, { - "name": "Yellow", - "colorMatrix": [ - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 6, - "enabled": true - }, { - "name": "misty_hrd", - "colorMatrix": [], - "minLevel": 6, - "blendMode": 9, - "enabled": true - }, { - "name": "x_ray", - "colorMatrix": [ - 0, - 1.2, - 0, - 0, - -100, - 0, - 2, - 0, - 0, - -120, - 0, - 2, - 0, - 0, - -120, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 7, - "enabled": true - }, { - "name": "decrease_saturation", - "colorMatrix": [ - 0.7, - 0.2, - 0.2, - 0, - 0, - 0.2, - 0.7, - 0.2, - 0, - 0, - 0.2, - 0.2, - 0.7, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 7, - "enabled": true - }, { - "name": "drops_mpl", - "colorMatrix": [], - "minLevel": 8, - "blendMode": 2, - "enabled": true - }, { - "name": "shiny_hrd", - "colorMatrix": [], - "minLevel": 9, - "blendMode": 9, - "enabled": true - }, { - "name": "glitter_hrd", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 9, - "enabled": true - }, { - "name": "frame_gold", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 0, - "enabled": true - }, { - "name": "frame_gray_4", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 0, - "enabled": true - }, { - "name": "frame_black_2", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 0, - "enabled": true - }, { - "name": "frame_wood_2", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 0, - "enabled": true - }, { - "name": "finger_nrm", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 0, - "enabled": true - }, { - "name": "color_5", - "colorMatrix": [ - 3.309, - 0.609, - 1.082, - 0.2, - 0, - 0.309, - 0.609, - 0.082, - 0, - 0, - 1.309, - 0.609, - 0.082, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 10, - "enabled": true - }, { - "name": "black_white_negative", - "colorMatrix": [ - -0.5, - -0.5, - -0.5, - 0, - 0, - -0.5, - -0.5, - -0.5, - 0, - 0, - -0.5, - -0.5, - -0.5, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 10, - "enabled": true - }, { - "name": "blue", - "colorMatrix": [ - 0.5, - 0.5, - 0.5, - 0, - -255, - 0.5, - 0.5, - 0.5, - 0, - -170, - 0.5, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 10, - "enabled": true - }, { - "name": "red", - "colorMatrix": [ - 0.5, - 0.5, - 0.5, - 0, - 0, - 0.5, - 0.5, - 0.5, - 0, - -170, - 0.5, - 0.5, - 0.5, - 0, - -170, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 10, - "enabled": true - }, { - "name": "green", - "colorMatrix": [ - 0.5, - 0.5, - 0.5, - 0, - -170, - 0.5, - 0.5, - 0.5, - 0, - 0, - 0.5, - 0.5, - 0.5, - 0, - -170, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 10, - "enabled": true - } - ], - "notification": { - "notification.admin.transient": { - "display": "POP_UP", - "image": "${image.library.url}/album1358/frank_wave_001.gif" - }, - "notification.builders_club.membership_expired": { - "display": "POP_UP" - }, - "notification.builders_club.membership_expires": { - "display": "POP_UP", - "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" - }, - "notification.builders_club.membership_extended": { - "delivery": "PERSISTENT", - "display": "POP_UP" - }, - "notification.builders_club.membership_made": { - "delivery": "PERSISTENT", - "display": "POP_UP", - "image": "${image.library.url}/notifications/builders_club_membership_extended.png" - }, - "notification.builders_club.membership_renewed": { - "delivery": "PERSISTENT", - "display": "POP_UP", - "image": "${image.library.url}/notifications/builders_club_membership_extended.png" - }, - "notification.builders_club.room_locked": { - "display": "BUBBLE", - "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" - }, - "notification.builders_club.room_unlocked": { - "display": "BUBBLE" - }, - "notification.builders_club.visit_denied_for_owner": { - "display": "BUBBLE", - "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" - }, - "notification.builders_club.visit_denied_for_visitor": { - "display": "POP_UP", - "image": "${image.library.url}/notifications/builders_club_room_locked.png" - }, - "notification.campaign.credit.donation": { - "display": "BUBBLE" - }, - "notification.campaign.product.donation": { - "display": "BUBBLE" - }, - "notification.casino.too_many_dice.placement": { - "display": "POP_UP" - }, - "notification.casino.too_many_dice": { - "display": "POP_UP" - }, - "notification.cfh.created": { - "display": "POP_UP", - "title": "" - }, - "notification.feed.enabled": false, - "notification.floorplan_editor.error": { - "display": "POP_UP" - }, - "notification.forums.delivered": { - "delivery": "PERSISTENT", - "display": "POP_UP" - }, - "notification.forums.forum_settings_updated": { - "display": "BUBBLE" - }, - "notification.forums.message.hidden": { - "display": "BUBBLE" - }, - "notification.forums.message.restored": { - "display": "BUBBLE" - }, - "notification.forums.thread.hidden": { - "display": "BUBBLE" - }, - "notification.forums.thread.locked": { - "display": "BUBBLE" - }, - "notification.forums.thread.pinned": { - "display": "BUBBLE" - }, - "notification.forums.thread.restored": { - "display": "BUBBLE" - }, - "notification.forums.thread.unlocked": { - "display": "BUBBLE" - }, - "notification.forums.thread.unpinned": { - "display": "BUBBLE" - }, - "notification.furni_placement_error": { - "display": "BUBBLE" - }, - "notification.gifting.valentine": { - "delivery": "PERSISTENT", - "display": "BUBBLE", - "image": "${image.library.url}/notifications/polaroid_photo.png" - }, - "notification.items.enabled": true, - "notification.mute.forbidden.time": { - "display": "BUBBLE" - }, - "notification.npc.gift.received": { - "display": "BUBBLE", - "image": "${image.library.url}/album1584/X1517.gif" - } - } -} diff --git a/src/App.tsx b/src/App.tsx index f67bd6b..d3566cc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { GetAssetManager, GetAvatarRenderManager, GetCommunication, GetConfiguration, GetLocalizationManager, GetRoomEngine, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, GetStage, GetTexturePool, GetTicker, HabboWebTools, LegacyExternalInterface, LoadGameUrlEvent, NitroLogger, NitroVersion, PrepareRenderer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { GetUIVersion, UiSettingsProvider } from './api'; +import { GetUIVersion } from './api'; import { Base } from './common'; import { LoadingView } from './components/loading/LoadingView'; import { MainView } from './components/MainView'; @@ -89,13 +89,11 @@ export const App: FC<{}> = props => }, []); return ( - - - { !isReady && - } - { isReady && } - - - + + { !isReady && + } + { isReady && } + + ); }; \ No newline at end of file diff --git a/src/api/ui-settings/IUiSettings.ts b/src/api/ui-settings/IUiSettings.ts deleted file mode 100644 index 24604b6..0000000 --- a/src/api/ui-settings/IUiSettings.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface IUiSettings -{ - colorMode: 'color' | 'image' | 'default'; - headerColor: string; - headerImageUrl: string; - headerAlpha: number; -} - -export const DEFAULT_UI_SETTINGS: IUiSettings = { - colorMode: 'default', - headerColor: '#1E7295', - headerImageUrl: '', - headerAlpha: 100 -}; - -export const PRESET_COLORS: string[] = [ - '#000000', '#444444', '#888888', '#CCCCCC', '#660000', '#CC3333', '#FF6666', '#CC6600', - '#FF3333', '#FF6633', '#FF9933', '#FFCC00', '#FFFF00', '#66FF00', '#00CC00', '#009900', - '#00FFCC', '#33CCFF', '#3366FF', '#0000CC', '#6633CC', '#9933FF', '#CC33FF', '#FF66CC', - '#FF99CC', '#1E7295', '#185D79', '#2DABC2', '#2B91A7', '#283F5D' -]; diff --git a/src/api/ui-settings/UiSettingsContext.tsx b/src/api/ui-settings/UiSettingsContext.tsx deleted file mode 100644 index 5d516cd..0000000 --- a/src/api/ui-settings/UiSettingsContext.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { createContext, FC, PropsWithChildren, useCallback, useContext, useEffect, useState } from 'react'; -import { DEFAULT_UI_SETTINGS, IUiSettings } from './IUiSettings'; - -const STORAGE_KEY = 'nitro.ui.settings'; - -interface IUiSettingsContext -{ - settings: IUiSettings; - isCustomActive: boolean; - updateSettings: (partial: Partial) => void; - resetSettings: () => void; - getHeaderStyle: () => React.CSSProperties; - getTabsStyle: () => React.CSSProperties; - getAccentColor: () => string; -} - -const UiSettingsContext = createContext({ - settings: DEFAULT_UI_SETTINGS, - isCustomActive: false, - updateSettings: () => {}, - resetSettings: () => {}, - getHeaderStyle: () => ({}), - getTabsStyle: () => ({}), - getAccentColor: () => DEFAULT_UI_SETTINGS.headerColor -}); - -const darkenColor = (hex: string, amount: number): string => -{ - const num = parseInt(hex.replace('#', ''), 16); - const r = Math.max(0, ((num >> 16) & 0xFF) - amount); - const g = Math.max(0, ((num >> 8) & 0xFF) - amount); - const b = Math.max(0, (num & 0xFF) - amount); - - return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); -}; - -const loadSettings = (): IUiSettings => -{ - try - { - const stored = localStorage.getItem(STORAGE_KEY); - if(stored) return { ...DEFAULT_UI_SETTINGS, ...JSON.parse(stored) }; - } - catch(e) {} - - return { ...DEFAULT_UI_SETTINGS }; -}; - -const saveSettings = (settings: IUiSettings): void => -{ - try - { - localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); - } - catch(e) {} -}; - -export const UiSettingsProvider: FC = ({ children }) => -{ - const [ settings, setSettings ] = useState(loadSettings); - - const updateSettings = useCallback((partial: Partial) => - { - setSettings(prev => - { - const updated = { ...prev, ...partial }; - saveSettings(updated); - return updated; - }); - }, []); - - const resetSettings = useCallback(() => - { - setSettings({ ...DEFAULT_UI_SETTINGS }); - saveSettings(DEFAULT_UI_SETTINGS); - }, []); - - const getHeaderStyle = useCallback((): React.CSSProperties => - { - if(settings.colorMode === 'color') - { - return { backgroundColor: settings.headerColor }; - } - - if(settings.colorMode === 'image' && settings.headerImageUrl) - { - return { - backgroundImage: `url(${ settings.headerImageUrl })`, - backgroundSize: 'cover', - backgroundPosition: 'center', - backgroundRepeat: 'repeat' - }; - } - - return {}; - }, [ settings ]); - - const getTabsStyle = useCallback((): React.CSSProperties => - { - if(settings.colorMode === 'color') - { - return { backgroundColor: darkenColor(settings.headerColor, 30) }; - } - - if(settings.colorMode === 'image' && settings.headerImageUrl) - { - return { - backgroundImage: `url(${ settings.headerImageUrl })`, - backgroundSize: 'cover', - backgroundPosition: 'center bottom', - backgroundRepeat: 'repeat' - }; - } - - return {}; - }, [ settings ]); - - const getAccentColor = useCallback((): string => - { - if(settings.colorMode === 'color') return settings.headerColor; - return DEFAULT_UI_SETTINGS.headerColor; - }, [ settings ]); - - const isCustomActive = settings.colorMode !== 'default'; - - const ALL_CSS_VARS = [ - '--ui-accent-color', '--ui-accent-dark', - '--ui-ctx-bg', '--ui-ctx-header-bg', '--ui-ctx-item-bg1', '--ui-ctx-item-bg2', - '--ui-btn-primary-bg', '--ui-btn-primary-border', - '--ui-dark-bg', '--ui-dark-border' - ]; - - useEffect(() => - { - const root = document.documentElement; - - if(settings.colorMode === 'color') - { - const c = settings.headerColor; - root.style.setProperty('--ui-accent-color', c); - root.style.setProperty('--ui-accent-dark', darkenColor(c, 30)); - root.style.setProperty('--ui-ctx-bg', darkenColor(c, 50)); - root.style.setProperty('--ui-ctx-header-bg', darkenColor(c, 20)); - root.style.setProperty('--ui-ctx-item-bg1', darkenColor(c, 60)); - root.style.setProperty('--ui-ctx-item-bg2', darkenColor(c, 70)); - root.style.setProperty('--ui-btn-primary-bg', c); - root.style.setProperty('--ui-btn-primary-border', darkenColor(c, 20)); - root.style.setProperty('--ui-dark-bg', darkenColor(c, 55)); - root.style.setProperty('--ui-dark-border', darkenColor(c, 60)); - } - else - { - ALL_CSS_VARS.forEach(v => root.style.removeProperty(v)); - } - }, [ settings ]); - - return ( - - { children } - - ); -}; - -export const useUiSettings = () => useContext(UiSettingsContext); diff --git a/src/api/ui-settings/index.ts b/src/api/ui-settings/index.ts deleted file mode 100644 index 255a5da..0000000 --- a/src/api/ui-settings/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './IUiSettings'; -export * from './UiSettingsContext'; diff --git a/src/common/Button.tsx b/src/common/Button.tsx index 4da0794..6b7d454 100644 --- a/src/common/Button.tsx +++ b/src/common/Button.tsx @@ -12,16 +12,20 @@ export interface ButtonProps extends FlexProps export const Button: FC = props => { - const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], style = {}, ...rest } = props; + const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], ...rest } = props; const getClassNames = useMemo(() => { + + // fucked up method i know (i dont have a clue what im doing because im a ninja) + const newClassNames: string[] = [ 'pointer-events-auto inline-block font-normal leading-normal text-[#fff] text-center no-underline align-middle cursor-pointer select-none border border-[solid] border-transparent px-[.75rem] py-[.375rem] text-[.9rem] rounded-[.25rem] [transition:color_.15s_ease-in-out,background-color_.15s_ease-in-out,border-color_.15s_ease-in-out,box-shadow_.15s_ease-in-out]' ]; if(variant) { + if(variant == 'primary') - newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white'); + newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]'); if(variant == 'success') newClassNames.push('text-white bg-[#00800b] border-[#00800b] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#006d09] hover:border-[#006609]'); @@ -39,10 +43,11 @@ export const Button: FC = props => newClassNames.push('text-white bg-[#185d79] border-[#185d79] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#144f67] hover:border-[#134a61]'); if(variant == 'dark') - newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white'); - + newClassNames.push('text-white bg-dark [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#18181bfb] hover:border-[#161619fb]'); + if(variant == 'gray') - newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white'); + newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]'); + } if(size) @@ -62,28 +67,5 @@ export const Button: FC = props => return newClassNames; }, [ variant, size, active, disabled, classNames ]); - const getStyle = useMemo(() => - { - if(variant === 'primary' || variant === 'gray') - { - return { - backgroundColor: 'var(--ui-btn-primary-bg, #1e7295)', - borderColor: 'var(--ui-btn-primary-border, #1e7295)', - ...style - }; - } - - if(variant === 'dark') - { - return { - backgroundColor: 'var(--ui-dark-bg, rgba(28, 28, 32, .98))', - borderColor: 'var(--ui-dark-border, rgba(28, 28, 32, .98))', - ...style - }; - } - - return style; - }, [ variant, style ]); - - return ; + return ; }; diff --git a/src/common/card/NitroCardHeaderView.tsx b/src/common/card/NitroCardHeaderView.tsx index 5bfbe2b..8bb354c 100644 --- a/src/common/card/NitroCardHeaderView.tsx +++ b/src/common/card/NitroCardHeaderView.tsx @@ -1,6 +1,5 @@ import { FC, MouseEvent } from 'react'; import { FaFlag } from 'react-icons/fa'; -import { useUiSettings } from '../../api'; import { Base, Column, ColumnProps, Flex } from '..'; interface NitroCardHeaderViewProps extends ColumnProps @@ -17,7 +16,8 @@ interface NitroCardHeaderViewProps extends ColumnProps export const NitroCardHeaderView: FC = props => { const { headerText = null, isGalleryPhoto = false, noCloseButton = false, isInfoToHabboPages = false, onReportPhoto = null, onClickInfoHabboPages = null, onCloseClick = null, justifyContent = 'center', alignItems = 'center', classNames = [], children = null, ...rest } = props; - const { isCustomActive, getHeaderStyle } = useUiSettings(); + + const onMouseDown = (event: MouseEvent) => { @@ -25,12 +25,8 @@ export const NitroCardHeaderView: FC = props => event.nativeEvent.stopImmediatePropagation(); }; - const headerClassName = isCustomActive - ? 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header' - : 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header bg-card-header'; - return ( - + { headerText } { isGalleryPhoto && diff --git a/src/common/card/tabs/NitroCardTabsView.tsx b/src/common/card/tabs/NitroCardTabsView.tsx index 5c49ac4..5e14506 100644 --- a/src/common/card/tabs/NitroCardTabsView.tsx +++ b/src/common/card/tabs/NitroCardTabsView.tsx @@ -1,27 +1,21 @@ import { FC, useMemo } from 'react'; -import { useUiSettings } from '../../../api'; import { Flex, FlexProps } from '../..'; export const NitroCardTabsView: FC = props => { const { justifyContent = 'center', gap = 1, classNames = [], children = null, ...rest } = props; - const { isCustomActive, getTabsStyle } = useUiSettings(); const getClassNames = useMemo(() => { - const base = isCustomActive - ? 'justify-center gap-0.5 flex min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px' - : 'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px'; - - const newClassNames: string[] = [ base ]; + const newClassNames: string[] = [ 'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px' ]; if(classNames.length) newClassNames.push(...classNames); return newClassNames; - }, [ classNames, isCustomActive ]); + }, [ classNames ]); return ( - + { children } ); diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 41c320e..3fef0cc 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -9,7 +9,6 @@ import { CampaignView } from './campaign/CampaignView'; import { CatalogView } from './catalog/CatalogView'; import { ChatHistoryView } from './chat-history/ChatHistoryView'; import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView'; -import { FurniEditorView } from './furni-editor/FurniEditorView'; import { FriendsView } from './friends/FriendsView'; import { GameCenterView } from './game-center/GameCenterView'; import { GroupsView } from './groups/GroupsView'; @@ -22,12 +21,10 @@ import { ModToolsView } from './mod-tools/ModToolsView'; import { NavigatorView } from './navigator/NavigatorView'; import { NitrobubbleHiddenView } from './nitrobubblehidden/NitrobubbleHiddenView'; import { NitropediaView } from './nitropedia/NitropediaView'; -import { ExternalPluginLoader } from './plugins/ExternalPluginLoader'; import { RightSideView } from './right-side/RightSideView'; import { RoomView } from './room/RoomView'; import { ToolbarView } from './toolbar/ToolbarView'; import { UserProfileView } from './user-profile/UserProfileView'; -import { InterfaceSettingsView } from './interface-settings/InterfaceSettingsView'; import { UserSettingsView } from './user-settings/UserSettingsView'; import { WiredView } from './wired/WiredView'; import { YoutubeTvView } from './youtube-tv/YoutubeTvView'; @@ -88,7 +85,6 @@ export const MainView: FC<{}> = props => { landingViewVisible && @@ -109,7 +105,6 @@ export const MainView: FC<{}> = props => - @@ -120,9 +115,7 @@ export const MainView: FC<{}> = props => - - ); }; diff --git a/src/components/interface-settings/InterfaceColorTabView.tsx b/src/components/interface-settings/InterfaceColorTabView.tsx deleted file mode 100644 index 65bf172..0000000 --- a/src/components/interface-settings/InterfaceColorTabView.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { RgbaColorPicker, RgbaColor } from 'react-colorful'; -import { FC, useCallback, useMemo, useState } from 'react'; -import { FaUndo, FaTrash, FaPen, FaFillDrip, FaSave } from 'react-icons/fa'; -import { PRESET_COLORS, useUiSettings } from '../../api'; -import { Flex, Text } from '../../common'; - -const hexToRgba = (hex: string, a = 1): RgbaColor => -{ - const num = parseInt(hex.replace('#', ''), 16); - return { r: (num >> 16) & 0xFF, g: (num >> 8) & 0xFF, b: num & 0xFF, a }; -}; - -const rgbaToHex = (rgba: RgbaColor): string => -{ - return '#' + ((1 << 24) + (rgba.r << 16) + (rgba.g << 8) + rgba.b).toString(16).slice(1); -}; - -export const InterfaceColorTabView: FC<{}> = () => -{ - const { settings, updateSettings, resetSettings } = useUiSettings(); - const [ color, setColor ] = useState(() => hexToRgba(settings.headerColor, settings.headerAlpha / 100)); - - const hexColor = useMemo(() => rgbaToHex(color), [ color ]); - const alphaPercent = useMemo(() => Math.round((color.a ?? 1) * 100), [ color ]); - - const onHexInput = useCallback((value: string) => - { - const clean = value.replace(/[^0-9a-fA-F]/g, '').slice(0, 6); - if(clean.length === 6) - { - const rgba = hexToRgba('#' + clean, color.a); - setColor(rgba); - } - }, [ color.a ]); - - const onRgbInput = useCallback((channel: 'r' | 'g' | 'b', value: number) => - { - const clamped = Math.max(0, Math.min(255, value || 0)); - setColor(prev => ({ ...prev, [channel]: clamped })); - }, []); - - const onAlphaInput = useCallback((value: number) => - { - const clamped = Math.max(0, Math.min(100, value || 0)); - setColor(prev => ({ ...prev, a: clamped / 100 })); - }, []); - - const onPresetClick = useCallback((presetHex: string) => - { - setColor(hexToRgba(presetHex, color.a)); - }, [ color.a ]); - - const onSave = useCallback(() => - { - updateSettings({ - colorMode: 'color', - headerColor: hexColor, - headerAlpha: alphaPercent - }); - }, [ updateSettings, hexColor, alphaPercent ]); - - const onReset = useCallback(() => - { - resetSettings(); - setColor(hexToRgba('#1E7295', 1)); - }, [ resetSettings ]); - - const onDelete = useCallback(() => - { - updateSettings({ colorMode: 'default' }); - setColor(hexToRgba('#1E7295', 1)); - }, [ updateSettings ]); - - return ( - -
- -
- - - onHexInput(e.target.value) } - maxLength={ 6 } - /> - Hex - - - onRgbInput('r', parseInt(e.target.value)) } - min={ 0 } max={ 255 } - /> - R - - - onRgbInput('g', parseInt(e.target.value)) } - min={ 0 } max={ 255 } - /> - G - - - onRgbInput('b', parseInt(e.target.value)) } - min={ 0 } max={ 255 } - /> - B - - - onAlphaInput(parseInt(e.target.value)) } - min={ 0 } max={ 100 } - /> - A - - -
- { PRESET_COLORS.map((presetHex, i) => ( -
onPresetClick(presetHex) } - /> - )) } -
- - - - - - - - - ); -}; diff --git a/src/components/interface-settings/InterfaceImageTabView.tsx b/src/components/interface-settings/InterfaceImageTabView.tsx deleted file mode 100644 index 390a390..0000000 --- a/src/components/interface-settings/InterfaceImageTabView.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { FC, useCallback, useMemo } from 'react'; -import { GetConfigurationValue, useUiSettings } from '../../api'; - -export const InterfaceImageTabView: FC<{}> = () => -{ - const { settings, updateSettings } = useUiSettings(); - - const imageCount = useMemo(() => - { - return GetConfigurationValue('ui.header.images.count', 30); - }, []); - - const baseUrl = useMemo(() => - { - return GetConfigurationValue('ui.header.images.url', 'https://image.webbo.city/image/headerImage/image{id}.gif'); - }, []); - - const images = useMemo(() => - { - const result: string[] = []; - for(let i = 1; i <= imageCount; i++) - { - result.push(baseUrl.replace('{id}', String(i))); - } - return result; - }, [ imageCount, baseUrl ]); - - const onImageSelect = useCallback((url: string) => - { - updateSettings({ - colorMode: 'image', - headerImageUrl: url - }); - }, [ updateSettings ]); - - return ( -
- { images.map((url, i) => ( -
onImageSelect(url) } - /> - )) } -
- ); -}; diff --git a/src/components/interface-settings/InterfaceProfileTabView.tsx b/src/components/interface-settings/InterfaceProfileTabView.tsx deleted file mode 100644 index 6534c3c..0000000 --- a/src/components/interface-settings/InterfaceProfileTabView.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { GetSessionDataManager, HabboClubLevelEnum } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useMemo, useState } from 'react'; -import { GetClubMemberLevel, GetConfigurationValue } from '../../api'; -import { Base, Flex, Grid, LayoutCurrencyIcon, NitroCardTabsItemView, NitroCardTabsView, Text } from '../../common'; -import { useRoom } from '../../hooks'; - -interface ItemData -{ - id: number; - isHcOnly: boolean; - minRank: number; - isAmbassadorOnly: boolean; - selectable: boolean; -} - -const SUB_TABS = [ 'backgrounds', 'stands', 'overlays' ] as const; -type SubTabType = typeof SUB_TABS[number]; - -const SUB_TAB_LABELS: Record = { - backgrounds: 'Sfondi', - stands: 'Basi', - overlays: 'Overlay' -}; - -export const InterfaceProfileTabView: FC<{}> = () => -{ - const [ activeSubTab, setActiveSubTab ] = useState('backgrounds'); - const [ selectedBackground, setSelectedBackground ] = useState(0); - const [ selectedStand, setSelectedStand ] = useState(0); - const [ selectedOverlay, setSelectedOverlay ] = useState(0); - const { roomSession } = useRoom(); - - const userData = useMemo(() => ({ - isHcMember: GetClubMemberLevel() >= HabboClubLevelEnum.CLUB, - securityLevel: GetSessionDataManager().canChangeName, - isAmbassador: GetSessionDataManager().isAmbassador - }), []); - - const processData = useCallback((configData: any[], dataType: string): ItemData[] => - { - if(!configData?.length) return []; - - return configData - .filter(item => - { - const meetsRank = userData.securityLevel >= item.minRank; - const ambassadorEligible = !item.isAmbassadorOnly || userData.isAmbassador; - return item.isHcOnly || (meetsRank && ambassadorEligible); - }) - .map(item => ({ id: item[`${ dataType }Id`], ...item, selectable: !item.isHcOnly || userData.isHcMember })); - }, [ userData ]); - - const allData = useMemo(() => ({ - backgrounds: processData(GetConfigurationValue('backgrounds.data'), 'background'), - stands: processData(GetConfigurationValue('stands.data'), 'stand'), - overlays: processData(GetConfigurationValue('overlays.data'), 'overlay') - }), [ processData ]); - - const handleSelection = useCallback((id: number) => - { - if(!roomSession) return; - - const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay }; - const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay }; - - setters[activeSubTab](id); - const newValues = { ...currentValues, [activeSubTab]: id }; - roomSession.sendBackgroundMessage(newValues.backgrounds, newValues.stands, newValues.overlays); - }, [ activeSubTab, roomSession, selectedBackground, selectedStand, selectedOverlay ]); - - const renderItem = useCallback((item: ItemData, type: string) => ( - item.selectable && handleSelection(item.id) } - className={ item.selectable ? '' : 'non-selectable' } - > - - { item.isHcOnly && } - - ), [ handleSelection ]); - - return ( - - - { SUB_TABS.map(tab => ( - - )) } - - { !roomSession && ( - Entra in una stanza per modificare il profilo - ) } - { roomSession && ( - - { allData[activeSubTab].map(item => renderItem(item, activeSubTab.slice(0, -1))) } - - ) } - - ); -}; diff --git a/src/components/interface-settings/InterfaceSettingsView.tsx b/src/components/interface-settings/InterfaceSettingsView.tsx deleted file mode 100644 index cd465c7..0000000 --- a/src/components/interface-settings/InterfaceSettingsView.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; -import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsView, NitroCardTabsItemView, NitroCardView } from '../../common'; -import { InterfaceColorTabView } from './InterfaceColorTabView'; -import { InterfaceProfileTabView } from './InterfaceProfileTabView'; - -const TABS = [ 'color', 'profile' ] as const; -type TabType = typeof TABS[number]; - -const TAB_LABELS: Record = { - color: 'Colore', - profile: 'Sfondo profilo' -}; - -export const InterfaceSettingsView: FC<{}> = () => -{ - const [ isVisible, setIsVisible ] = useState(false); - const [ currentTab, setCurrentTab ] = useState('color'); - - useEffect(() => - { - const linkTracker: ILinkEventTracker = { - linkReceived: (url: string) => - { - const parts = url.split('/'); - if(parts.length < 2) return; - - switch(parts[1]) - { - case 'show': - setIsVisible(true); - return; - case 'hide': - setIsVisible(false); - return; - case 'toggle': - setIsVisible(prev => !prev); - return; - case 'profile': - setCurrentTab('profile'); - setIsVisible(true); - return; - } - }, - eventUrlPrefix: 'interface-settings/' - }; - - AddLinkEventTracker(linkTracker); - return () => RemoveLinkEventTracker(linkTracker); - }, []); - - if(!isVisible) return null; - - return ( - - setIsVisible(false) } /> - - { TABS.map(tab => ( - setCurrentTab(tab) } - > - { TAB_LABELS[tab] } - - )) } - - - { currentTab === 'color' && } - { currentTab === 'profile' && } - - - ); -}; diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx index dde184d..6a504d2 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx @@ -45,8 +45,7 @@ const BadgeMiniPicker: FC<{ return (
e.stopPropagation() }> = props canUse = true; isCrackable = true; - crackableHits = stuffData?.hits ?? 0; - crackableTarget = stuffData?.target ?? 0; + crackableHits = stuffData.hits; + crackableTarget = stuffData.target; } else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) @@ -458,7 +458,7 @@ export const InfoStandWidgetFurniView: FC = props return ( - +
@@ -527,7 +527,7 @@ export const InfoStandWidgetFurniView: FC = props { isCrackable && <>
- { LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ (crackableHits ?? 0).toString(), (crackableTarget ?? 0).toString() ]) } + { LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ crackableHits.toString(), crackableTarget.toString() ]) } } { avatarInfo.groupId > 0 && <> @@ -552,21 +552,7 @@ export const InfoStandWidgetFurniView: FC = props { godMode && <>
- { canSeeFurniId && -
-
- - - - ID: { avatarInfo.id } -
-
- - - - Sprite: { (() => { const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?'; })() } -
-
} + { canSeeFurniId && ID: { avatarInfo.id } } { (!avatarInfo.isWallItem && canMove) && <> - { dropdownOpen &&
{ /* Left panel: position + rotation */ } diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx index 2dac49e..1791979 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx @@ -1,4 +1,4 @@ -import { CreateLinkEvent, GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react'; import { FaPencilAlt, FaTimes } from 'react-icons/fa'; import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api'; @@ -7,6 +7,7 @@ import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks'; import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView'; import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView'; import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView'; +import { BackgroundsView } from '../../../../backgrounds/BackgroundsView'; interface InfoStandWidgetUserViewProps { avatarInfo: AvatarInfoUser; @@ -31,7 +32,7 @@ export const InfoStandWidgetUserView: FC = props = const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]); - const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); CreateLinkEvent('interface-settings/profile'); }, []); + const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []); const saveMotto = (motto: string) => { if (!isEditingMotto || motto.length > GetConfigurationValue('motto.max.length', 38) || !roomSession) return; @@ -126,7 +127,7 @@ export const InfoStandWidgetUserView: FC = props = return ( <> - +
@@ -256,6 +257,19 @@ export const InfoStandWidgetUserView: FC = props = )} + {isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && ( +
+ +
+ )} ); }; \ No newline at end of file diff --git a/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx b/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx index ff26177..a0513cf 100644 --- a/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx +++ b/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx @@ -3,21 +3,16 @@ import { Flex, FlexProps } from '../../../../common'; export const ContextMenuHeaderView: FC = props => { - const { justifyContent = 'center', alignItems = 'center', classNames = [], style = {}, ...rest } = props; + const { justifyContent = 'center', alignItems = 'center', classNames = [], ...rest } = props; const getClassNames = useMemo(() => { - const newClassNames: string[] = [ 'text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ]; + const newClassNames: string[] = [ 'bg-[#3d5f6e] text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ]; if(classNames.length) newClassNames.push(...classNames); return newClassNames; }, [ classNames ]); - const mergedStyle = useMemo(() => ({ - backgroundColor: 'var(--ui-ctx-header-bg, #3d5f6e)', - ...style - }), [ style ]); - - return ; + return ; }; diff --git a/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx b/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx index b012a93..0a1eacc 100644 --- a/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx +++ b/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx @@ -8,7 +8,7 @@ interface ContextMenuListItemViewProps extends FlexProps export const ContextMenuListItemView: FC = props => { - const { disabled = false, fullWidth = true, justifyContent = 'center', alignItems = 'center', classNames = [], style = {}, onClick = null, ...rest } = props; + const { disabled = false, fullWidth = true, justifyContent = 'center', alignItems = 'center', classNames = [], onClick = null, ...rest } = props; const handleClick = (event: MouseEvent) => { @@ -19,7 +19,7 @@ export const ContextMenuListItemView: FC = props = const getClassNames = useMemo(() => { - const newClassNames: string[] = [ 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] cursor-pointer' ]; + 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' ]; if(disabled) newClassNames.push('disabled'); @@ -28,10 +28,5 @@ export const ContextMenuListItemView: FC = props = return newClassNames; }, [ disabled, classNames ]); - const mergedStyle = useMemo(() => ({ - background: 'repeating-linear-gradient(var(--ui-ctx-item-bg1, #131e25), var(--ui-ctx-item-bg1, #131e25) 50%, var(--ui-ctx-item-bg2, #0d171d) 50%, var(--ui-ctx-item-bg2, #0d171d) 100%)', - ...style - }), [ style ]); - - return ; + return ; }; diff --git a/src/components/room/widgets/context-menu/ContextMenuView.tsx b/src/components/room/widgets/context-menu/ContextMenuView.tsx index b92dc89..1ca3e83 100644 --- a/src/components/room/widgets/context-menu/ContextMenuView.tsx +++ b/src/components/room/widgets/context-menu/ContextMenuView.tsx @@ -76,6 +76,7 @@ export const ContextMenuView: FC = ({ const getClassNames = useMemo(() => { const classes = [ 'p-[2px]!', + 'bg-[#1c323f]', 'border-2', 'border-[solid]', 'border-[rgba(255,255,255,.5)]', @@ -97,7 +98,6 @@ export const ContextMenuView: FC = ({ top: pos.y ?? 0, transition: isFading ? 'opacity 75ms linear' : undefined, opacity, - backgroundColor: 'var(--ui-ctx-bg, #1c323f)', ...style, }), [pos, opacity, isFading, style] diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 62503bb..7d6743a 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -69,38 +69,38 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => )} - - - - { - setMeExpanded(!isMeExpanded); - event.stopPropagation(); - } }> - - { (getTotalUnseen > 0) && - } + + + + + { + setMeExpanded(!isMeExpanded); + event.stopPropagation(); + } }> + + { (getTotalUnseen > 0) && + } + + { isInRoom && + VisitDesktop() } /> } + { !isInRoom && + CreateLinkEvent('navigator/goto/home') } /> } + CreateLinkEvent('navigator/toggle') } /> + { GetConfigurationValue('game.center.enabled') && + CreateLinkEvent('games/toggle') } /> } + CreateLinkEvent('catalog/toggle') } /> + CreateLinkEvent('inventory/toggle') }> + { (getFullCount > 0) && + } + + { isInRoom && + CreateLinkEvent('camera/toggle') } /> } + { isMod && + CreateLinkEvent('mod-tools/toggle') } /> } - { isInRoom && - VisitDesktop() } /> } - { !isInRoom && - CreateLinkEvent('navigator/goto/home') } /> } - CreateLinkEvent('navigator/toggle') } /> - { GetConfigurationValue('game.center.enabled') && - CreateLinkEvent('games/toggle') } /> } - CreateLinkEvent('catalog/toggle') } /> - CreateLinkEvent('inventory/toggle') }> - { (getFullCount > 0) && - } - - { isInRoom && - CreateLinkEvent('camera/toggle') } /> } - { isMod && - CreateLinkEvent('mod-tools/toggle') } /> } - { isMod && - CreateLinkEvent('furni-editor/toggle') } /> } + - - + CreateLinkEvent('friends/toggle') }> { (requests.length > 0) && diff --git a/src/css/common/Buttons.css b/src/css/common/Buttons.css index 21d7f1f..106a3cc 100644 --- a/src/css/common/Buttons.css +++ b/src/css/common/Buttons.css @@ -24,8 +24,8 @@ input[type=number] { .btn-primary { color: #fff; - background-color: var(--ui-btn-primary-bg, #3c6d82); - border: 2px solid var(--ui-btn-primary-border, #1a617f); + background-color: #3c6d82; + border: 2px solid #1a617f; padding: 0.25rem 0.5rem; font-size: .7875rem; border-radius: 0.5rem; @@ -33,7 +33,7 @@ input[type=number] { } .btn-primary:hover { - border: 2px solid var(--ui-btn-primary-border, #1a617f); + border: 2px solid #1a617f; box-shadow: none!important; } @@ -81,16 +81,16 @@ input[type=number] { .btn-dark { color: #fff; - background-color: var(--ui-dark-bg, #212131); - border: 2px solid var(--ui-dark-border, #1c1c2a); + background-color: #212131; + border: 2px solid #1c1c2a; box-shadow: none!important; border-radius: 8px; padding: 4px 11px 4px 11px; } .btn-dark:hover{ - background-color: var(--ui-dark-bg, #212131); - border: 2px solid var(--ui-dark-border, #1c1c2a); + background-color: #212131; + border: 2px solid #1c1c2a; box-shadow: none!important; border-radius: 8px; padding: 4px 11px 4px 11px; diff --git a/src/css/purse/PurseView.css b/src/css/purse/PurseView.css index 69c1732..7134551 100644 --- a/src/css/purse/PurseView.css +++ b/src/css/purse/PurseView.css @@ -22,7 +22,7 @@ pointer-events: all; } .borderhccontent{ - background-color: var(--ui-dark-bg, #212131); + background-color: #212131; border-radius: 0.5rem!important; border: 2px solid #383853; height: calc(100% - 3px); @@ -46,7 +46,7 @@ } .nitro-purse-seasonal-currency { - background-color: var(--ui-dark-bg, #212131); + background-color: #212131; background: linear-gradient(to right, #5f5f8d, transparent); height: 30px; margin-bottom: 4px; diff --git a/src/css/room/InfoStand.css b/src/css/room/InfoStand.css index 7e1a050..e44b062 100644 --- a/src/css/room/InfoStand.css +++ b/src/css/room/InfoStand.css @@ -27,7 +27,7 @@ width: clamp(160px, 20vw, 190px); /* Responsive width */ z-index: 30; pointer-events: auto; - background: var(--ui-dark-bg, #212131); + background: #212131; box-shadow: inset 0 5px rgba(38, 38, 57, 0.6), inset 0 -4px rgba(25, 25, 37, 0.6); border-radius: 0.5rem; padding: 10px; diff --git a/src/css/room/RoomWidgets.css b/src/css/room/RoomWidgets.css index b4d6ee2..093fa67 100644 --- a/src/css/room/RoomWidgets.css +++ b/src/css/room/RoomWidgets.css @@ -4,7 +4,7 @@ left: 15px; .nitro-room-tools { - background: var(--ui-dark-bg, #212131); + background: #212131; box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4); border-top-right-radius: .25rem; border-bottom-right-radius: .25rem; @@ -54,7 +54,7 @@ } .nitro-room-history { - background: var(--ui-dark-bg, #212131); + background: #212131; box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4); transition: all .2s ease; width: 150px; @@ -63,7 +63,7 @@ } .nitro-room-tools-info { - background: var(--ui-dark-bg, #212131); + background: #212131; box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4); transition: all .2s ease; max-width: 250px; From 2a5c31ad8a7817f90f4cbd88fc2d8dde7ddd70b9 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 19 Mar 2026 10:40:06 +0100 Subject: [PATCH 23/33] Revert "Merge pull request #17 from simoleo89/mod-tools-i18n" This reverts commit 3789f53ab338075d9918947566b73ddcc1a9856b, reversing changes made to 871670ec08715bfcf7585ac37a2d889487da25b6. --- mod-tools-external-texts.json | 96 ------------------- src/components/mod-tools/ModToolsView.tsx | 12 +-- .../mod-tools/views/chatlog/ChatlogView.tsx | 12 +-- .../views/room/ModToolsChatlogView.tsx | 4 +- .../mod-tools/views/room/ModToolsRoomView.tsx | 28 +++--- .../views/tickets/CfhChatlogView.tsx | 4 +- .../views/tickets/ModToolsIssueInfoView.tsx | 24 ++--- .../views/tickets/ModToolsMyIssuesTabView.tsx | 12 +-- .../tickets/ModToolsOpenIssuesTabView.tsx | 10 +- .../tickets/ModToolsPickedIssuesTabView.tsx | 9 +- .../views/tickets/ModToolsTicketsView.tsx | 15 ++- .../views/user/ModToolsUserChatlogView.tsx | 4 +- .../views/user/ModToolsUserModActionView.tsx | 56 +++++------ .../views/user/ModToolsUserRoomVisitsView.tsx | 12 +-- .../user/ModToolsUserSendMessageView.tsx | 10 +- .../mod-tools/views/user/ModToolsUserView.tsx | 38 ++++---- 16 files changed, 124 insertions(+), 222 deletions(-) delete mode 100644 mod-tools-external-texts.json diff --git a/mod-tools-external-texts.json b/mod-tools-external-texts.json deleted file mode 100644 index 686253c..0000000 --- a/mod-tools-external-texts.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "moderation.modtools.title": "Mod Tools", - "moderation.modtools.roomtool": "Room Tool", - "moderation.modtools.roomchatlogs": "Chatlog Tool", - "moderation.modtools.userinfo": "User Info", - "moderation.modtools.tickets": "Tickets", - - "moderation.roomtool.info.title": "Room Info", - "moderation.roomtool.roomowner.title": "Owner:", - "moderation.roomtool.usersinroom.title": "Users in room:", - "moderation.roomtool.ownerinroom.title": "Owner here:", - "moderation.roomtool.true.title": "Yes", - "moderation.roomtool.false.title": "No", - "moderation.roomtool.button.visit.title": "Visit Room", - "moderation.roomtool.kickall.title": "Kick everyone out", - "moderation.roomtool.closeroom.title": "Enable the doorbell", - "moderation.roomtool.inappropiatename.title": "Change room name", - "moderation.roomtool.presets.title": "Type a mandatory message...", - "moderation.roomtool.button.caution.title": "Send Caution", - "moderation.roomtool.button.message.title": "Send Alert", - - "moderation.tickets.title": "Tickets", - "moderation.tickets.open": "Open Issues", - "moderation.tickets.my": "My Issues", - "moderation.tickets.picked": "Picked Issues", - "moderation.tickets.col.type": "Type", - "moderation.tickets.col.roomPlayer": "Room/Player", - "moderation.tickets.col.opened": "Opened", - "moderation.tickets.col.picker": "Picker", - "moderation.tickets.pick": "Pick Issue", - "moderation.tickets.handle": "Handle", - "moderation.tickets.release": "Release", - - "moderation.issue.resolving": "Resolving issue %id%", - "moderation.issue.info": "Issue Information", - "moderation.issue.source": "Source", - "moderation.issue.category": "Category", - "moderation.issue.description": "Description", - "moderation.issue.caller": "Caller", - "moderation.issue.reported": "Reported User", - "moderation.issue.chatlog": "Chatlog", - "moderation.issue.close.useless": "Close as useless", - "moderation.issue.close.abusive": "Close as abusive", - "moderation.issue.close.resolved": "Close as resolved", - "moderation.issue.release": "Release", - - "moderation.chatlog.issue": "Issue Chatlog", - "moderation.chatlog.room": "Room Chatlog", - "moderation.chatlog.col.time": "Time", - "moderation.chatlog.col.user": "User", - "moderation.chatlog.col.message": "Message", - "moderation.chatlog.visit": "Visit", - "moderation.chatlog.roomtools": "Room Tools", - "moderation.chatlog.user": "User Chatlog: %username%", - - "moderation.userinfo.roomchat": "Room Chat", - "moderation.userinfo.sendmessage": "Send Message", - "moderation.userinfo.roomvisits": "Room Visits", - "moderation.userinfo.modaction": "Mod Action", - - "moderation.sendmessage.title": "Send Message", - "moderation.sendmessage.to": "Message To: %username%", - "moderation.sendmessage.send": "Send message", - "moderation.sendmessage.error.empty": "Please write a message to user.", - "moderation.error": "Error", - - "moderation.roomvisits.title": "User Visits", - "moderation.roomvisits.col.time": "Time", - "moderation.roomvisits.col.roomname": "Room name", - "moderation.roomvisits.col.visit": "Visit", - "moderation.roomvisits.visitroom": "Visit Room", - - "moderation.modaction.title": "Mod Action: %username%", - "moderation.modaction.cfhtopic": "CFH Topic", - "moderation.modaction.sanctiontype": "Sanction Type", - "moderation.modaction.message.hint": "Optional message type, overrides default", - "moderation.modaction.defaultsanction": "Default Sanction", - "moderation.modaction.sanction": "Sanction", - "moderation.modaction.alert": "Alert", - "moderation.modaction.mute1h": "Mute 1h", - "moderation.modaction.ban18h": "Ban 18h", - "moderation.modaction.ban7days": "Ban 7 days", - "moderation.modaction.ban30days.step1": "Ban 30 days (step 1)", - "moderation.modaction.ban30days.step2": "Ban 30 days (step 2)", - "moderation.modaction.ban100years": "Ban 100 years", - "moderation.modaction.banavataronly100years": "Ban avatar-only 100 years", - "moderation.modaction.kick": "Kick", - "moderation.modaction.locktrade1week": "Lock trade 1 week", - "moderation.modaction.locktradepermanent": "Lock trade permanent", - "moderation.modaction.message": "Message", - "moderation.modaction.error.notopic": "You must select a CFH topic", - "moderation.modaction.error.notopicorsanction": "You must select a CFH topic and Sanction", - "moderation.modaction.error.nopermission": "You do not have permission to do this", - "moderation.modaction.error.nosanction": "You must select a sanction", - "moderation.modaction.error.emptymessage": "Please write a message to user" -} diff --git a/src/components/mod-tools/ModToolsView.tsx b/src/components/mod-tools/ModToolsView.tsx index 2bc9680..d42381d 100644 --- a/src/components/mod-tools/ModToolsView.tsx +++ b/src/components/mod-tools/ModToolsView.tsx @@ -1,6 +1,6 @@ import { AddLinkEventTracker, CreateLinkEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomId, RoomObjectCategory, RoomObjectType } from '@nitrots/nitro-renderer'; import { FC, useEffect, useRef, useState } from 'react'; -import { GetRoomSession, ISelectedUser, LocalizeText } from '../../api'; +import { GetRoomSession, ISelectedUser } from '../../api'; import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; import { useModTools, useNitroEvent, useObjectSelectedEvent } from '../../hooks'; import { ModToolsChatlogView } from './views/room/ModToolsChatlogView'; @@ -125,23 +125,23 @@ export const ModToolsView: FC<{}> = props => <> { isVisible && - setIsVisible(false) } /> + setIsVisible(false) } /> } diff --git a/src/components/mod-tools/views/chatlog/ChatlogView.tsx b/src/components/mod-tools/views/chatlog/ChatlogView.tsx index 5e21080..63e5201 100644 --- a/src/components/mod-tools/views/chatlog/ChatlogView.tsx +++ b/src/components/mod-tools/views/chatlog/ChatlogView.tsx @@ -1,6 +1,6 @@ import { ChatRecordData, CreateLinkEvent } from '@nitrots/nitro-renderer'; import { FC, useMemo } from 'react'; -import { LocalizeText, TryVisitRoom } from '../../../../api'; +import { TryVisitRoom } from '../../../../api'; import { Button, Column, Flex, Grid, InfiniteScroll, Text } from '../../../../common'; import { useModTools } from '../../../../hooks'; import { ChatlogRecord } from './ChatlogRecord'; @@ -49,8 +49,8 @@ export const ChatlogView: FC = props => { props.roomName }
- - + +
); @@ -61,9 +61,9 @@ export const ChatlogView: FC = props => -
{ LocalizeText('moderation.chatlog.col.time') }
-
{ LocalizeText('moderation.chatlog.col.user') }
-
{ LocalizeText('moderation.chatlog.col.message') }
+
Time
+
User
+
Message
{ (records && (records.length > 0)) && diff --git a/src/components/mod-tools/views/room/ModToolsChatlogView.tsx b/src/components/mod-tools/views/room/ModToolsChatlogView.tsx index 16d1adf..c42320d 100644 --- a/src/components/mod-tools/views/room/ModToolsChatlogView.tsx +++ b/src/components/mod-tools/views/room/ModToolsChatlogView.tsx @@ -1,6 +1,6 @@ import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { LocalizeText, SendMessageComposer } from '../../../../api'; +import { SendMessageComposer } from '../../../../api'; import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useMessageEvent } from '../../../../hooks'; import { ChatlogView } from '../chatlog/ChatlogView'; @@ -34,7 +34,7 @@ export const ModToolsChatlogView: FC = props => return ( - + { roomChatlog && } diff --git a/src/components/mod-tools/views/room/ModToolsRoomView.tsx b/src/components/mod-tools/views/room/ModToolsRoomView.tsx index 38dd324..37d9fc5 100644 --- a/src/components/mod-tools/views/room/ModToolsRoomView.tsx +++ b/src/components/mod-tools/views/room/ModToolsRoomView.tsx @@ -1,6 +1,6 @@ import { CreateLinkEvent, GetModeratorRoomInfoMessageComposer, ModerateRoomMessageComposer, ModeratorActionMessageComposer, ModeratorRoomInfoEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { LocalizeText, SendMessageComposer, TryVisitRoom } from '../../../../api'; +import { SendMessageComposer, TryVisitRoom } from '../../../../api'; import { Button, Column, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; import { useMessageEvent } from '../../../../hooks'; @@ -70,7 +70,7 @@ export const ModToolsRoomView: FC = props => return ( - onCloseClick() } /> + onCloseClick() } /> { name &&
@@ -80,41 +80,41 @@ export const ModToolsRoomView: FC = props =>
- { LocalizeText('moderation.roomtool.roomowner.title') } + Owner: CreateLinkEvent(`mod-tools/open-user-info/${ ownerId }`) }>{ ownerName }
- { LocalizeText('moderation.roomtool.usersinroom.title') } + Users in room: { usersInRoom }
- { LocalizeText('moderation.roomtool.ownerinroom.title') } - { ownerInRoom ? LocalizeText('moderation.roomtool.true.title') : LocalizeText('moderation.roomtool.false.title') } + Owner here: + { ownerInRoom ? 'Yes' : 'No' }
- - + +
setKickUsers(event.target.checked) } /> - { LocalizeText('moderation.roomtool.kickall.title') } + Kick everyone out
setLockRoom(event.target.checked) } /> - { LocalizeText('moderation.roomtool.closeroom.title') } + Enable the doorbell
setChangeRoomName(event.target.checked) } /> - { LocalizeText('moderation.roomtool.inappropiatename.title') } + Change room name
- +
- - + +
diff --git a/src/components/mod-tools/views/tickets/CfhChatlogView.tsx b/src/components/mod-tools/views/tickets/CfhChatlogView.tsx index ab1bbf3..9923fa9 100644 --- a/src/components/mod-tools/views/tickets/CfhChatlogView.tsx +++ b/src/components/mod-tools/views/tickets/CfhChatlogView.tsx @@ -1,6 +1,6 @@ import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { LocalizeText, SendMessageComposer } from '../../../../api'; +import { SendMessageComposer } from '../../../../api'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useMessageEvent } from '../../../../hooks'; import { ChatlogView } from '../chatlog/ChatlogView'; @@ -32,7 +32,7 @@ export const CfhChatlogView: FC = props => return ( - + { chatlogData && } diff --git a/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx b/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx index ac9d71b..7444a73 100644 --- a/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx @@ -35,33 +35,33 @@ export const ModToolsIssueInfoView: FC = props => return ( <> - onIssueInfoClosed(issueId) } /> + onIssueInfoClosed(issueId) } /> - { LocalizeText('moderation.issue.info') } + Issue Information - + - + - + - + - + @@ -70,11 +70,11 @@ export const ModToolsIssueInfoView: FC = props =>
{ LocalizeText('moderation.issue.source') }Source { GetIssueCategoryName(ticket.categoryId) }
{ LocalizeText('moderation.issue.category') }Category { LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }
{ LocalizeText('moderation.issue.description') }Description { ticket.message }
{ LocalizeText('moderation.issue.caller') }Caller openUserInfo(ticket.reporterUserId) }>{ ticket.reporterUserName }
{ LocalizeText('moderation.issue.reported') }Reported User openUserInfo(ticket.reportedUserId) }>{ ticket.reportedUserName }
- - - - - + + + + +
diff --git a/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx b/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx index 07bc010..9aaa441 100644 --- a/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx @@ -1,6 +1,6 @@ import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useRef } from 'react'; -import { LocalizeText, SendMessageComposer } from '../../../../api'; +import { SendMessageComposer } from '../../../../api'; import { Button, Column, Grid } from '../../../../common'; interface ModToolsMyIssuesTabViewProps @@ -28,9 +28,9 @@ export const ModToolsMyIssuesTabView: FC = props = -
{ LocalizeText('moderation.tickets.col.type') }
-
{ LocalizeText('moderation.tickets.col.roomPlayer') }
-
{ LocalizeText('moderation.tickets.col.opened') }
+
Type
+
Room/Player
+
Opened
@@ -44,10 +44,10 @@ export const ModToolsMyIssuesTabView: FC = props =
{ issue.reportedUserName }
{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }
- +
- +
); diff --git a/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx b/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx index bb7ba9a..387580b 100644 --- a/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx @@ -1,6 +1,6 @@ import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useRef } from 'react'; -import { LocalizeText, SendMessageComposer } from '../../../../api'; +import { SendMessageComposer } from '../../../../api'; import { Button, Column, Grid } from '../../../../common'; interface ModToolsOpenIssuesTabViewProps @@ -27,9 +27,9 @@ export const ModToolsOpenIssuesTabView: FC = pro -
{ LocalizeText('moderation.tickets.col.type') }
-
{ LocalizeText('moderation.tickets.col.roomPlayer') }
-
{ LocalizeText('moderation.tickets.col.opened') }
+
Type
+
Room/Player
+
Opened
@@ -42,7 +42,7 @@ export const ModToolsOpenIssuesTabView: FC = pro
{ issue.reportedUserName }
{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }
- +
); diff --git a/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx b/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx index 2d04770..ca6003e 100644 --- a/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx @@ -1,6 +1,5 @@ import { IssueMessageData } from '@nitrots/nitro-renderer'; import { FC } from 'react'; -import { LocalizeText } from '../../../../api'; import { Column, Grid } from '../../../../common'; interface ModToolsPickedIssuesTabViewProps @@ -16,10 +15,10 @@ export const ModToolsPickedIssuesTabView: FC = -
{ LocalizeText('moderation.tickets.col.type') }
-
{ LocalizeText('moderation.tickets.col.roomPlayer') }
-
{ LocalizeText('moderation.tickets.col.opened') }
-
{ LocalizeText('moderation.tickets.col.picker') }
+
Type
+
Room/Player
+
Opened
+
Picker
diff --git a/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx b/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx index c23286c..ab7ac35 100644 --- a/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx +++ b/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx @@ -1,6 +1,5 @@ import { GetSessionDataManager, IssueMessageData } from '@nitrots/nitro-renderer'; import { FC, useState } from 'react'; -import { LocalizeText } from '../../../../api'; import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../common'; import { useModTools } from '../../../../hooks'; import { ModToolsIssueInfoView } from './ModToolsIssueInfoView'; @@ -13,10 +12,10 @@ interface ModToolsTicketsViewProps onCloseClick: () => void; } -const TAB_KEYS: string[] = [ - 'moderation.tickets.open', - 'moderation.tickets.my', - 'moderation.tickets.picked' +const TABS: string[] = [ + 'Open Issues', + 'My Issues', + 'Picked Issues' ]; export const ModToolsTicketsView: FC = props => @@ -72,12 +71,12 @@ export const ModToolsTicketsView: FC = props => return ( <> - + - { TAB_KEYS.map((tabKey, index) => + { TABS.map((tab, index) => { return ( setCurrentTab(index) }> - { LocalizeText(tabKey) } + { tab } ); }) } diff --git a/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx b/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx index b38a622..acae308 100644 --- a/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx +++ b/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx @@ -1,6 +1,6 @@ import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { LocalizeText, SendMessageComposer } from '../../../../api'; +import { SendMessageComposer } from '../../../../api'; import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useMessageEvent } from '../../../../hooks'; import { ChatlogView } from '../chatlog/ChatlogView'; @@ -34,7 +34,7 @@ export const ModToolsUserChatlogView: FC = props = return ( - + { userChatlog && } diff --git a/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx b/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx index c9d5075..1bea10a 100644 --- a/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx +++ b/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx @@ -11,18 +11,18 @@ interface ModToolsUserModActionViewProps } const MOD_ACTION_DEFINITIONS = [ - new ModActionDefinition(1, 'moderation.modaction.alert', ModActionDefinition.ALERT, 1, 0), - new ModActionDefinition(2, 'moderation.modaction.mute1h', ModActionDefinition.MUTE, 2, 0), - new ModActionDefinition(3, 'moderation.modaction.ban18h', ModActionDefinition.BAN, 3, 0), - new ModActionDefinition(4, 'moderation.modaction.ban7days', ModActionDefinition.BAN, 4, 0), - new ModActionDefinition(5, 'moderation.modaction.ban30days.step1', ModActionDefinition.BAN, 5, 0), - new ModActionDefinition(7, 'moderation.modaction.ban30days.step2', ModActionDefinition.BAN, 7, 0), - new ModActionDefinition(6, 'moderation.modaction.ban100years', ModActionDefinition.BAN, 6, 0), - new ModActionDefinition(106, 'moderation.modaction.banavataronly100years', ModActionDefinition.BAN, 6, 0), - new ModActionDefinition(101, 'moderation.modaction.kick', ModActionDefinition.KICK, 0, 0), - new ModActionDefinition(102, 'moderation.modaction.locktrade1week', ModActionDefinition.TRADE_LOCK, 0, 168), - new ModActionDefinition(104, 'moderation.modaction.locktradepermanent', ModActionDefinition.TRADE_LOCK, 0, 876000), - new ModActionDefinition(105, 'moderation.modaction.message', ModActionDefinition.MESSAGE, 0, 0), + new ModActionDefinition(1, 'Alert', ModActionDefinition.ALERT, 1, 0), + new ModActionDefinition(2, 'Mute 1h', ModActionDefinition.MUTE, 2, 0), + new ModActionDefinition(3, 'Ban 18h', ModActionDefinition.BAN, 3, 0), + new ModActionDefinition(4, 'Ban 7 days', ModActionDefinition.BAN, 4, 0), + new ModActionDefinition(5, 'Ban 30 days (step 1)', ModActionDefinition.BAN, 5, 0), + new ModActionDefinition(7, 'Ban 30 days (step 2)', ModActionDefinition.BAN, 7, 0), + new ModActionDefinition(6, 'Ban 100 years', ModActionDefinition.BAN, 6, 0), + new ModActionDefinition(106, 'Ban avatar-only 100 years', ModActionDefinition.BAN, 6, 0), + new ModActionDefinition(101, 'Kick', ModActionDefinition.KICK, 0, 0), + new ModActionDefinition(102, 'Lock trade 1 week', ModActionDefinition.TRADE_LOCK, 0, 168), + new ModActionDefinition(104, 'Lock trade permanent', ModActionDefinition.TRADE_LOCK, 0, 876000), + new ModActionDefinition(105, 'Message', ModActionDefinition.MESSAGE, 0, 0), ]; export const ModToolsUserModActionView: FC = props => @@ -60,7 +60,7 @@ export const ModToolsUserModActionView: FC = pro const category = topics[selectedTopic]; - if(selectedTopic === -1) errorMessage = LocalizeText('moderation.modaction.error.notopic'); + if(selectedTopic === -1) errorMessage = 'You must select a CFH topic'; if(errorMessage) return sendAlert(errorMessage); @@ -82,10 +82,10 @@ export const ModToolsUserModActionView: FC = pro const category = topics[selectedTopic]; const sanction = MOD_ACTION_DEFINITIONS[selectedAction]; - if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = LocalizeText('moderation.modaction.error.notopicorsanction'); - else if(!settings || !settings.cfhPermission) errorMessage = LocalizeText('moderation.modaction.error.nopermission'); - else if(!category) errorMessage = LocalizeText('moderation.modaction.error.notopic'); - else if(!sanction) errorMessage = LocalizeText('moderation.modaction.error.nosanction'); + if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = 'You must select a CFH topic and Sanction'; + else if(!settings || !settings.cfhPermission) errorMessage = 'You do not have permission to do this'; + else if(!category) errorMessage = 'You must select a CFH topic'; + else if(!sanction) errorMessage = 'You must select a sanction'; if(errorMessage) { @@ -101,7 +101,7 @@ export const ModToolsUserModActionView: FC = pro case ModActionDefinition.ALERT: { if(!settings.alertPermission) { - sendAlert(LocalizeText('moderation.modaction.error.nopermission')); + sendAlert('You have insufficient permissions'); return; } @@ -115,7 +115,7 @@ export const ModToolsUserModActionView: FC = pro case ModActionDefinition.BAN: { if(!settings.banPermission) { - sendAlert(LocalizeText('moderation.modaction.error.nopermission')); + sendAlert('You have insufficient permissions'); return; } @@ -126,7 +126,7 @@ export const ModToolsUserModActionView: FC = pro case ModActionDefinition.KICK: { if(!settings.kickPermission) { - sendAlert(LocalizeText('moderation.modaction.error.nopermission')); + sendAlert('You have insufficient permissions'); return; } @@ -142,7 +142,7 @@ export const ModToolsUserModActionView: FC = pro case ModActionDefinition.MESSAGE: { if(message.trim().length === 0) { - sendAlert(LocalizeText('moderation.modaction.error.emptymessage')); + sendAlert('Please write a message to user'); return; } @@ -161,23 +161,23 @@ export const ModToolsUserModActionView: FC = pro return ( - onCloseClick() } /> + onCloseClick() } />
- { LocalizeText('moderation.modaction.message.hint') } + Optional message type, overrides default - + ); diff --git a/src/components/mod-tools/views/user/ModToolsUserView.tsx b/src/components/mod-tools/views/user/ModToolsUserView.tsx index 1d75d67..6f65700 100644 --- a/src/components/mod-tools/views/user/ModToolsUserView.tsx +++ b/src/components/mod-tools/views/user/ModToolsUserView.tsx @@ -27,60 +27,60 @@ export const ModToolsUserView: FC = props => return [ { - localeKey: 'moderation.userinfo.userName', + localeKey: 'modtools.userinfo.userName', value: userInfo.userName, showOnline: true }, { - localeKey: 'moderation.userinfo.cfhCount', + localeKey: 'modtools.userinfo.cfhCount', value: userInfo.cfhCount.toString() }, { - localeKey: 'moderation.userinfo.abusiveCfhCount', + localeKey: 'modtools.userinfo.abusiveCfhCount', value: userInfo.abusiveCfhCount.toString() }, { - localeKey: 'moderation.userinfo.cautionCount', + localeKey: 'modtools.userinfo.cautionCount', value: userInfo.cautionCount.toString() }, { - localeKey: 'moderation.userinfo.banCount', + localeKey: 'modtools.userinfo.banCount', value: userInfo.banCount.toString() }, { - localeKey: 'moderation.userinfo.lastSanctionTime', + localeKey: 'modtools.userinfo.lastSanctionTime', value: userInfo.lastSanctionTime }, { - localeKey: 'moderation.userinfo.tradingLockCount', + localeKey: 'modtools.userinfo.tradingLockCount', value: userInfo.tradingLockCount.toString() }, { - localeKey: 'moderation.userinfo.tradingExpiryDate', + localeKey: 'modtools.userinfo.tradingExpiryDate', value: userInfo.tradingExpiryDate }, { - localeKey: 'moderation.userinfo.minutesSinceLastLogin', + localeKey: 'modtools.userinfo.minutesSinceLastLogin', value: FriendlyTime.format(userInfo.minutesSinceLastLogin * 60, '.ago', 2) }, { - localeKey: 'moderation.userinfo.lastPurchaseDate', + localeKey: 'modtools.userinfo.lastPurchaseDate', value: userInfo.lastPurchaseDate }, { - localeKey: 'moderation.userinfo.primaryEmailAddress', + localeKey: 'modtools.userinfo.primaryEmailAddress', value: userInfo.primaryEmailAddress }, { - localeKey: 'moderation.userinfo.identityRelatedBanCount', + localeKey: 'modtools.userinfo.identityRelatedBanCount', value: userInfo.identityRelatedBanCount.toString() }, { - localeKey: 'moderation.userinfo.registrationAgeInMinutes', + localeKey: 'modtools.userinfo.registrationAgeInMinutes', value: FriendlyTime.format(userInfo.registrationAgeInMinutes * 60, '.ago', 2) }, { - localeKey: 'moderation.userinfo.userClassification', + localeKey: 'modtools.userinfo.userClassification', value: userInfo.userClassification } ]; @@ -105,7 +105,7 @@ export const ModToolsUserView: FC = props => return ( <> - onCloseClick() } /> + onCloseClick() } /> @@ -130,16 +130,16 @@ export const ModToolsUserView: FC = props => From dffd832b7793c09b91421170c3731312c31f9266 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 19 Mar 2026 10:40:16 +0100 Subject: [PATCH 24/33] Revert "Merge pull request #20 from simoleo89/feature/floorplan-realtime-preview" This reverts commit 323c45518e8a460e29ea0ca6ba02205aa5723978, reversing changes made to d1a59962686367495e3f300a88b94ab9dee91f62. --- .../FloorplanEditorContext.tsx | 14 +- .../floorplan-editor/FloorplanEditorView.tsx | 156 +-------- .../views/FloorplanCanvasView.tsx | 105 +++--- .../views/FloorplanHeightSelector.tsx | 54 --- .../views/FloorplanOptionsView.tsx | 247 +++++++++---- .../views/FloorplanPreviewView.tsx | 328 ------------------ 6 files changed, 257 insertions(+), 647 deletions(-) delete mode 100644 src/components/floorplan-editor/views/FloorplanHeightSelector.tsx delete mode 100644 src/components/floorplan-editor/views/FloorplanPreviewView.tsx diff --git a/src/components/floorplan-editor/FloorplanEditorContext.tsx b/src/components/floorplan-editor/FloorplanEditorContext.tsx index 1b2a3c4..eb528cf 100644 --- a/src/components/floorplan-editor/FloorplanEditorContext.tsx +++ b/src/components/floorplan-editor/FloorplanEditorContext.tsx @@ -8,25 +8,13 @@ interface IFloorplanEditorContext setOriginalFloorplanSettings: Dispatch>; visualizationSettings: IVisualizationSettings; setVisualizationSettings: Dispatch>; - floorHeight: number; - setFloorHeight: Dispatch>; - floorAction: number; - setFloorAction: Dispatch>; - tilemapVersion: number; - areaInfo: { total: number; walkable: number }; } const FloorplanEditorContext = createContext({ originalFloorplanSettings: null, setOriginalFloorplanSettings: null, visualizationSettings: null, - setVisualizationSettings: null, - floorHeight: 0, - setFloorHeight: null, - floorAction: 3, - setFloorAction: null, - tilemapVersion: 0, - areaInfo: { total: 0, walkable: 0 } + setVisualizationSettings: null }); export const FloorplanEditorContextProvider: FC> = props => ; diff --git a/src/components/floorplan-editor/FloorplanEditorView.tsx b/src/components/floorplan-editor/FloorplanEditorView.tsx index 4003d13..59f7709 100644 --- a/src/components/floorplan-editor/FloorplanEditorView.tsx +++ b/src/components/floorplan-editor/FloorplanEditorView.tsx @@ -1,22 +1,19 @@ import { AddLinkEventTracker, FloorHeightMapEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useEffect, useState } from 'react'; -import { FaCaretLeft, FaCaretRight } from 'react-icons/fa'; +import { FC, useEffect, useState } from 'react'; import { LocalizeText, SendMessageComposer } from '../../api'; -import { Button, ButtonGroup, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; import { useMessageEvent, useNitroEvent } from '../../hooks'; import { FloorplanEditorContextProvider } from './FloorplanEditorContext'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; import { IFloorplanSettings } from '@nitrots/nitro-renderer'; import { IVisualizationSettings } from '@nitrots/nitro-renderer'; -import { convertNumbersForSaving, convertSettingToNumber, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; +import { convertNumbersForSaving, convertSettingToNumber } from '@nitrots/nitro-renderer'; import { FloorplanCanvasView } from './views/FloorplanCanvasView'; import { FloorplanImportExportView } from './views/FloorplanImportExportView'; import { FloorplanOptionsView } from './views/FloorplanOptionsView'; -import { FloorplanHeightSelector } from './views/FloorplanHeightSelector'; -import { FloorplanPreviewView } from './views/FloorplanPreviewView'; -const MIN_WALL_HEIGHT = 0; -const MAX_WALL_HEIGHT = 16; + +type ScrollDirection = 'up' | 'down' | 'left' | 'right'; export const FloorplanEditorView: FC<{}> = props => { @@ -37,65 +34,7 @@ export const FloorplanEditorView: FC<{}> = props => thicknessWall: 1, thicknessFloor: 1 }); - const [ floorHeight, setFloorHeight ] = useState(0); - const [ floorAction, setFloorAction ] = useState(FloorAction.SET); - const [ tilemapVersion, setTilemapVersion ] = useState(0); - const [ areaInfo, setAreaInfo ] = useState({ total: 0, walkable: 0 }); - - const calculateArea = useCallback(() => - { - const tilemap = FloorplanEditor.instance.tilemap; - - if(!tilemap || tilemap.length === 0) - { - setAreaInfo({ total: 0, walkable: 0 }); - - return; - } - - let total = 0; - let walkable = 0; - - for(let y = 0; y < tilemap.length; y++) - { - if(!tilemap[y]) continue; - - for(let x = 0; x < tilemap[y].length; x++) - { - if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue; - - total++; - - if(!tilemap[y][x].isBlocked) walkable++; - } - } - - setAreaInfo({ total, walkable }); - }, []); - - // sync floorHeight/floorAction changes to the FloorplanEditor instance - useEffect(() => - { - FloorplanEditor.instance.actionSettings.currentAction = floorAction; - FloorplanEditor.instance.actionSettings.currentHeight = floorHeight.toString(36); - }, [ floorHeight, floorAction ]); - - // register onTilemapChange callback - useEffect(() => - { - if(!isVisible) return; - - FloorplanEditor.instance.onTilemapChange = () => - { - setTilemapVersion(prev => prev + 1); - calculateArea(); - }; - - return () => - { - FloorplanEditor.instance.onTilemapChange = null; - }; - }, [ isVisible, calculateArea ]); + const [ canvasScrollHandler, setCanvasScrollHandler ] = useState<((direction: ScrollDirection) => void) | null>(null); const saveFloorChanges = () => { @@ -108,50 +47,16 @@ export const FloorplanEditorView: FC<{}> = props => convertNumbersForSaving(visualizationSettings.thicknessFloor), (visualizationSettings.wallHeight - 1) )); - }; + } const revertChanges = () => { setVisualizationSettings({ wallHeight: originalFloorplanSettings.wallHeight, thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: originalFloorplanSettings.entryPointDir }); - + FloorplanEditor.instance.doorLocation = { x: originalFloorplanSettings.entryPoint[0], y: originalFloorplanSettings.entryPoint[1] }; FloorplanEditor.instance.setTilemap(originalFloorplanSettings.tilemap, originalFloorplanSettings.reservedTiles); FloorplanEditor.instance.renderTiles(); - }; - - const onWallHeightChange = (value: number) => - { - if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT; - - if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT; - - setVisualizationSettings(prevValue => - { - const newValue = { ...prevValue }; - - newValue.wallHeight = value; - - return newValue; - }); - }; - - const increaseWallHeight = () => - { - let height = (visualizationSettings.wallHeight + 1); - - if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT; - - onWallHeightChange(height); - }; - - const decreaseWallHeight = () => - { - let height = (visualizationSettings.wallHeight - 1); - - if(height <= 0) height = MIN_WALL_HEIGHT; - - onWallHeightChange(height); - }; + } useNitroEvent(RoomEngineEvent.DISPOSED, event => setIsVisible(false)); @@ -212,7 +117,7 @@ export const FloorplanEditorView: FC<{}> = props => const parts = url.split('/'); if(parts.length < 2) return; - + switch(parts[1]) { case 'show': @@ -235,42 +140,17 @@ export const FloorplanEditorView: FC<{}> = props => }, []); return ( - + { isVisible && - + setIsVisible(false) } /> - - - - - - - - - { LocalizeText('floor.editor.wall.height') } - - onWallHeightChange(event.target.valueAsNumber) } /> - - - - Area: { areaInfo.total } ({ areaInfo.walkable } caselle) - - - + + canvasScrollHandler && canvasScrollHandler(direction) } /> + - + + @@ -281,4 +161,4 @@ export const FloorplanEditorView: FC<{}> = props => setImportExportVisible(false) } /> } ); -}; +} diff --git a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx index 9db0903..e8f39a8 100644 --- a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx +++ b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx @@ -1,25 +1,25 @@ import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useRef, useState } from 'react'; -import { FaPlus, FaMinus } from 'react-icons/fa'; import { SendMessageComposer } from '../../../api'; import { Base, Column, ColumnProps } from '../../../common'; import { useMessageEvent } from '../../../hooks'; import { useFloorplanEditorContext } from '../FloorplanEditorContext'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; +type ScrollDirection = 'up' | 'down' | 'left' | 'right'; + interface FloorplanCanvasViewProps extends ColumnProps { + setScrollHandler(handler: ((direction: ScrollDirection) => void) | null): void; } export const FloorplanCanvasView: FC = props => { - const { gap = 1, children = null, ...rest } = props; - const [ occupiedTilesReceived, setOccupiedTilesReceived ] = useState(false); + const { gap = 1, children = null, setScrollHandler = null, ...rest } = props; + const [ occupiedTilesReceived , setOccupiedTilesReceived ] = useState(false); const [ entryTileReceived, setEntryTileReceived ] = useState(false); - const [ zoomLevel, setZoomLevel ] = useState(1.0); const { originalFloorplanSettings = null, setOriginalFloorplanSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); const elementRef = useRef(null); - const canvasWrapperRef = useRef(null); useMessageEvent(RoomOccupiedTilesMessageEvent, event => { @@ -37,7 +37,7 @@ export const FloorplanCanvasView: FC = props => }); setOccupiedTilesReceived(true); - + elementRef.current.scrollTo((FloorplanEditor.instance.renderer.canvas.width / 3), 0); }); @@ -63,16 +63,39 @@ export const FloorplanCanvasView: FC = props => return newValue; }); - + FloorplanEditor.instance.doorLocation = { x: parser.x, y: parser.y }; setEntryTileReceived(true); }); + const onClickArrowButton = (scrollDirection: ScrollDirection) => + { + const element = elementRef.current; + + if(!element) return; + + switch(scrollDirection) + { + case 'up': + element.scrollBy({ top: -10 }); + break; + case 'down': + element.scrollBy({ top: 10 }); + break; + case 'left': + element.scrollBy({ left: -10 }); + break; + case 'right': + element.scrollBy({ left: 10 }); + break; + } + } + const onPointerEvent = (event: PointerEvent) => { event.preventDefault(); - + switch(event.type) { case 'pointerout': @@ -86,10 +109,7 @@ export const FloorplanCanvasView: FC = props => FloorplanEditor.instance.onPointerMove(event); break; } - }; - - const zoomIn = () => setZoomLevel(prev => Math.min(prev + 0.25, 2.0)); - const zoomOut = () => setZoomLevel(prev => Math.max(prev - 0.25, 0.5)); + } useEffect(() => { @@ -104,15 +124,15 @@ export const FloorplanCanvasView: FC = props => thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: prevValue.entryPointDir - }; + } }); - }; + } }, [ originalFloorplanSettings.thicknessFloor, originalFloorplanSettings.thicknessWall, originalFloorplanSettings.wallHeight, setVisualizationSettings ]); useEffect(() => { if(!entryTileReceived || !occupiedTilesReceived) return; - + FloorplanEditor.instance.renderTiles(); }, [ entryTileReceived, occupiedTilesReceived ]); @@ -124,56 +144,45 @@ export const FloorplanCanvasView: FC = props => const currentElement = elementRef.current; if(!currentElement) return; - - const wrapper = canvasWrapperRef.current; - - if(wrapper) wrapper.appendChild(FloorplanEditor.instance.renderer.canvas); + + currentElement.appendChild(FloorplanEditor.instance.renderer.canvas); currentElement.addEventListener('pointerup', onPointerEvent); + currentElement.addEventListener('pointerout', onPointerEvent); + currentElement.addEventListener('pointerdown', onPointerEvent); + currentElement.addEventListener('pointermove', onPointerEvent); - return () => + return () => { if(currentElement) { currentElement.removeEventListener('pointerup', onPointerEvent); + currentElement.removeEventListener('pointerout', onPointerEvent); + currentElement.removeEventListener('pointerdown', onPointerEvent); + currentElement.removeEventListener('pointermove', onPointerEvent); } - }; + } }, []); + useEffect(() => + { + if(!setScrollHandler) return; + + setScrollHandler(() => onClickArrowButton); + + return () => setScrollHandler(null); + }, [ setScrollHandler ]); + return ( - - -
- -
- - -
+ + { children } ); -}; +} diff --git a/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx b/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx deleted file mode 100644 index 8163c98..0000000 --- a/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { FC } from 'react'; -import { COLORMAP, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; -import { FloorplanEditor } from '@nitrots/nitro-renderer'; -import { Column, Text } from '../../../common'; -import { useFloorplanEditorContext } from '../FloorplanEditorContext'; - -const colormap = COLORMAP as Record; - -export const FloorplanHeightSelector: FC<{}> = () => -{ - const { floorHeight, setFloorHeight, setFloorAction } = useFloorplanEditorContext(); - - const onSelectHeight = (height: number) => - { - setFloorHeight(height); - setFloorAction(FloorAction.SET); - - FloorplanEditor.instance.actionSettings.currentAction = FloorAction.SET; - FloorplanEditor.instance.actionSettings.currentHeight = height.toString(36); - }; - - const heights: number[] = []; - - for(let i = 26; i >= 0; i--) heights.push(i); - - return ( - - { floorHeight } -
- { heights.map(h => - { - const char = HEIGHT_SCHEME[h + 1]; - const color = colormap[char] || '101010'; - const isActive = (floorHeight === h); - - return ( -
onSelectHeight(h) } - title={ `${ h }` } - /> - ); - }) } -
- - ); -}; diff --git a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx index d4e7705..5207b15 100644 --- a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx +++ b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx @@ -1,32 +1,45 @@ -import { FC } from 'react'; +import { FC, useState } from 'react'; +import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaCaretLeft, FaCaretRight } from 'react-icons/fa'; import { LocalizeText } from '../../../api'; -import { Flex, LayoutGridItem, Text } from '../../../common'; -import { FloorAction } from '@nitrots/nitro-renderer'; +import { Button, Column, Flex, LayoutGridItem, Slider, Text } from '../../../common'; +import { COLORMAP, FloorAction } from '@nitrots/nitro-renderer'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; import { useFloorplanEditorContext } from '../FloorplanEditorContext'; +const MIN_WALL_HEIGHT: number = 0; +const MAX_WALL_HEIGHT: number = 16; + +const MIN_FLOOR_HEIGHT: number = 0; +const MAX_FLOOR_HEIGHT: number = 26; + +type ScrollDirection = 'up' | 'down' | 'left' | 'right'; + interface FloorplanOptionsViewProps { + onCanvasScroll?(direction: ScrollDirection): void; } export const FloorplanOptionsView: FC = props => { - const { visualizationSettings = null, setVisualizationSettings = null, floorAction, setFloorAction } = useFloorplanEditorContext(); - const isSquareSelectMode = FloorplanEditor.instance.isSquareSelectMode; - + const { onCanvasScroll = () => {} } = props; + const { visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); + const [ floorAction, setFloorAction ] = useState(FloorAction.SET); + const [ floorHeight, setFloorHeight ] = useState(0); + const [ isSquareSelectMode, setSquareSelectMode ] = useState(false); + const selectAction = (action: number) => { setFloorAction(action); FloorplanEditor.instance.actionSettings.currentAction = action; - }; + } const toggleSquareSelectMode = () => { - FloorplanEditor.instance.toggleSquareSelectMode(); - // force re-render by toggling action to same value - setFloorAction(prev => prev); - }; + const nextValue = FloorplanEditor.instance.toggleSquareSelectMode(); + + setSquareSelectMode(nextValue); + } const changeDoorDirection = () => { @@ -45,19 +58,18 @@ export const FloorplanOptionsView: FC = props => return newValue; }); - }; + } - const onWallThicknessChange = (value: number) => + const onFloorHeightChange = (value: number) => { - setVisualizationSettings(prevValue => - { - const newValue = { ...prevValue }; + if(isNaN(value) || (value <= 0)) value = 0; - newValue.thicknessWall = value; + if(value > 26) value = 26; - return newValue; - }); - }; + setFloorHeight(value); + + FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36); + } const onFloorThicknessChange = (value: number) => { @@ -69,54 +81,157 @@ export const FloorplanOptionsView: FC = props => return newValue; }); - }; + } + + const onWallThicknessChange = (value: number) => + { + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; + + newValue.thicknessWall = value; + + return newValue; + }); + } + + const onWallHeightChange = (value: number) => + { + if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT; + + if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT; + + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; + + newValue.wallHeight = value; + + return newValue; + }); + } + + const increaseWallHeight = () => + { + let height = (visualizationSettings.wallHeight + 1); + + if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT; + + onWallHeightChange(height); + } + + const decreaseWallHeight = () => + { + let height = (visualizationSettings.wallHeight - 1); + + if(height <= 0) height = MIN_WALL_HEIGHT; + + onWallHeightChange(height); + } return ( - - - { LocalizeText('floor.plan.editor.draw.mode') } - - selectAction(FloorAction.SET) }> - - - selectAction(FloorAction.UNSET) }> - - - selectAction(FloorAction.UP) }> - - - selectAction(FloorAction.DOWN) }> - - - selectAction(FloorAction.DOOR) }> - - - FloorplanEditor.instance.toggleSelectAll() }> - - - - - - + + + + { LocalizeText('floor.plan.editor.draw.mode') } + + + selectAction(FloorAction.SET) }> + + + selectAction(FloorAction.UNSET) }> + + + + + selectAction(FloorAction.UP) }> + + + selectAction(FloorAction.DOWN) }> + + + + selectAction(FloorAction.DOOR) }> + + + FloorplanEditor.instance.toggleSelectAll() }> + + + + + + + + + { LocalizeText('floor.plan.editor.enter.direction') } + + + + { LocalizeText('floor.editor.wall.height') } + + + onWallHeightChange(event.target.valueAsNumber) } /> + + + + + { LocalizeText('floor.plan.editor.room.options') } + + + + + - - { LocalizeText('floor.plan.editor.enter.direction') } - + + + { LocalizeText('floor.plan.editor.tile.height') }: { floorHeight } +
+ onFloorHeightChange(event) } + renderThumb={ (props, state) => + { + const { key, style, ...rest } = (props as Record); + + return
{ state.valueNow }
; + } } /> +
+
+ + + + + + +
+ + + + + + - - - - - + ); -}; +} \ No newline at end of file diff --git a/src/components/floorplan-editor/views/FloorplanPreviewView.tsx b/src/components/floorplan-editor/views/FloorplanPreviewView.tsx deleted file mode 100644 index cd82a9c..0000000 --- a/src/components/floorplan-editor/views/FloorplanPreviewView.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import { FC, useEffect, useRef } from 'react'; -import { COLORMAP, HEIGHT_SCHEME, FloorplanEditor } from '@nitrots/nitro-renderer'; -import { useFloorplanEditorContext } from '../FloorplanEditorContext'; - -const colormap = COLORMAP as Record; - -const PREVIEW_TILE_W = 16; -const PREVIEW_TILE_H = 8; -const PREVIEW_BLOCK_H = 5; -const WALL_HEIGHT_PX = 40; -const WALL_COLOR = '#6B7B5E'; -const WALL_SIDE_COLOR = '#5A6A4F'; -const WALL_TOP_COLOR = '#7D8E6F'; - -function hexToRgb(hex: string): [number, number, number] -{ - const r = parseInt(hex.substring(0, 2), 16); - const g = parseInt(hex.substring(2, 4), 16); - const b = parseInt(hex.substring(4, 6), 16); - - return [ r, g, b ]; -} - -function rgbToHex(r: number, g: number, b: number): string -{ - return `#${ ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) }`; -} - -function darken(hex: string, factor: number): string -{ - const [ r, g, b ] = hexToRgb(hex); - - return rgbToHex( - Math.floor(r * factor), - Math.floor(g * factor), - Math.floor(b * factor) - ); -} - -function getTilemapBounds(tilemap: any[][]): { minX: number; minY: number; maxX: number; maxY: number } -{ - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - - for(let y = 0; y < tilemap.length; y++) - { - if(!tilemap[y]) continue; - - for(let x = 0; x < tilemap[y].length; x++) - { - if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue; - - if(x < minX) minX = x; - if(x > maxX) maxX = x; - if(y < minY) minY = y; - if(y > maxY) maxY = y; - } - } - - if(minX === Infinity) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; - - return { minX, minY, maxX, maxY }; -} - -function renderPreview(canvas: HTMLCanvasElement, wallHeight: number): void -{ - const ctx = canvas.getContext('2d'); - const tilemap = FloorplanEditor.instance.tilemap; - - if(!ctx || !tilemap || tilemap.length === 0) - { - if(ctx) - { - ctx.fillStyle = '#1a1a1a'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } - - return; - } - - const bounds = getTilemapBounds(tilemap); - const tilesW = bounds.maxX - bounds.minX + 1; - const tilesH = bounds.maxY - bounds.minY + 1; - - // find max height for offset calculation - let maxTileHeight = 0; - - for(let y = bounds.minY; y <= bounds.maxY; y++) - { - for(let x = bounds.minX; x <= bounds.maxX; x++) - { - if(!tilemap[y] || !tilemap[y][x] || tilemap[y][x].height === 'x') continue; - - const hi = HEIGHT_SCHEME.indexOf(tilemap[y][x].height) - 1; - - if(hi > maxTileHeight) maxTileHeight = hi; - } - } - - // calculate isometric bounds - const isoW = (tilesW + tilesH) * PREVIEW_TILE_W; - const isoH = (tilesW + tilesH) * PREVIEW_TILE_H + maxTileHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX; - - // scale to fit canvas - const scaleX = (canvas.width - 20) / isoW; - const scaleY = (canvas.height - 20) / isoH; - const scale = Math.min(scaleX, scaleY, 3); - - const offsetX = (canvas.width - isoW * scale) / 2; - const offsetY = (canvas.height - isoH * scale) / 2 + WALL_HEIGHT_PX * scale * 0.5; - - ctx.fillStyle = '#1a1a1a'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - ctx.save(); - ctx.translate(offsetX, offsetY); - ctx.scale(scale, scale); - - const tw = PREVIEW_TILE_W; - const th = PREVIEW_TILE_H; - - function isoX(gx: number, gy: number): number - { - return (gx - bounds.minX - gy + bounds.minY) * tw + (tilesH - 1) * tw; - } - - function isoY(gx: number, gy: number): number - { - return (gx - bounds.minX + gy - bounds.minY) * th; - } - - function hasActiveTile(gx: number, gy: number): boolean - { - return tilemap[gy] && tilemap[gy][gx] && tilemap[gy][gx].height !== 'x'; - } - - function getTileHeight(gx: number, gy: number): number - { - if(!hasActiveTile(gx, gy)) return 0; - - return Math.max(0, HEIGHT_SCHEME.indexOf(tilemap[gy][gx].height) - 1); - } - - // draw walls on north and west edges - const wallH = wallHeight > 0 ? wallHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX * 0.3 : WALL_HEIGHT_PX * 0.6; - - for(let y = bounds.minY; y <= bounds.maxY; y++) - { - for(let x = bounds.minX; x <= bounds.maxX; x++) - { - if(!hasActiveTile(x, y)) continue; - - const tileH = getTileHeight(x, y) * PREVIEW_BLOCK_H; - const cx = isoX(x, y); - const cy = isoY(x, y) - tileH; - - // west wall (no tile to the left) - if(!hasActiveTile(x - 1, y)) - { - ctx.beginPath(); - ctx.moveTo(cx, cy + th); - ctx.lineTo(cx, cy + th - wallH); - ctx.lineTo(cx + tw, cy - wallH); - ctx.lineTo(cx + tw, cy); - ctx.closePath(); - ctx.fillStyle = WALL_SIDE_COLOR; - ctx.fill(); - ctx.strokeStyle = '#4A5A3F'; - ctx.lineWidth = 0.5; - ctx.stroke(); - } - - // north wall (no tile above) - if(!hasActiveTile(x, y - 1)) - { - ctx.beginPath(); - ctx.moveTo(cx + tw, cy); - ctx.lineTo(cx + tw, cy - wallH); - ctx.lineTo(cx + tw * 2, cy + th - wallH); - ctx.lineTo(cx + tw * 2, cy + th); - ctx.closePath(); - ctx.fillStyle = WALL_COLOR; - ctx.fill(); - ctx.strokeStyle = '#4A5A3F'; - ctx.lineWidth = 0.5; - ctx.stroke(); - } - - // wall top cap - corner - if(!hasActiveTile(x - 1, y) && !hasActiveTile(x, y - 1)) - { - ctx.beginPath(); - ctx.moveTo(cx + tw, cy - wallH); - ctx.lineTo(cx + tw + tw * 0.3, cy - wallH - th * 0.3); - ctx.lineTo(cx + tw, cy - wallH - th * 0.6); - ctx.lineTo(cx + tw - tw * 0.3, cy - wallH - th * 0.3); - ctx.closePath(); - ctx.fillStyle = WALL_TOP_COLOR; - ctx.fill(); - } - } - } - - // draw tiles back-to-front - for(let y = bounds.minY; y <= bounds.maxY; y++) - { - for(let x = bounds.minX; x <= bounds.maxX; x++) - { - if(!hasActiveTile(x, y)) continue; - - const tile = tilemap[y][x]; - const heightIndex = HEIGHT_SCHEME.indexOf(tile.height) - 1; - const tileH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; - - const cx = isoX(x, y); - const cy = isoY(x, y) - tileH; - - const heightChar = tile.height; - const baseColor = colormap[heightChar] || 'aaaaaa'; - const topColor = `#${ baseColor }`; - const leftColor = darken(baseColor, 0.65); - const rightColor = darken(baseColor, 0.80); - - // draw side faces if tile has height - const blockH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; - - // left face (visible when no neighbor to south or neighbor is shorter) - const southH = getTileHeight(x, y + 1); - const leftExpose = hasActiveTile(x, y + 1) ? Math.max(0, heightIndex - southH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; - - if(leftExpose > 0) - { - ctx.beginPath(); - ctx.moveTo(cx, cy + th); - ctx.lineTo(cx + tw, cy + th * 2); - ctx.lineTo(cx + tw, cy + th * 2 + leftExpose); - ctx.lineTo(cx, cy + th + leftExpose); - ctx.closePath(); - ctx.fillStyle = leftColor; - ctx.fill(); - } - - // right face - const eastH = getTileHeight(x + 1, y); - const rightExpose = hasActiveTile(x + 1, y) ? Math.max(0, heightIndex - eastH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; - - if(rightExpose > 0) - { - ctx.beginPath(); - ctx.moveTo(cx + tw * 2, cy + th); - ctx.lineTo(cx + tw, cy + th * 2); - ctx.lineTo(cx + tw, cy + th * 2 + rightExpose); - ctx.lineTo(cx + tw * 2, cy + th + rightExpose); - ctx.closePath(); - ctx.fillStyle = rightColor; - ctx.fill(); - } - - // top face - ctx.beginPath(); - ctx.moveTo(cx + tw, cy); - ctx.lineTo(cx + tw * 2, cy + th); - ctx.lineTo(cx + tw, cy + th * 2); - ctx.lineTo(cx, cy + th); - ctx.closePath(); - ctx.fillStyle = topColor; - ctx.fill(); - - // door indicator - const door = FloorplanEditor.instance.doorLocation; - - if(door.x === x && door.y === y) - { - ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; - ctx.fill(); - ctx.strokeStyle = '#ffffff'; - ctx.lineWidth = 1; - ctx.stroke(); - } - } - } - - ctx.restore(); -} - -export const FloorplanPreviewView: FC<{}> = () => -{ - const { tilemapVersion, visualizationSettings } = useFloorplanEditorContext(); - const canvasRef = useRef(null); - const rafRef = useRef(0); - - useEffect(() => - { - if(!canvasRef.current) return; - - if(rafRef.current) cancelAnimationFrame(rafRef.current); - - rafRef.current = requestAnimationFrame(() => - { - const canvas = canvasRef.current; - - if(!canvas) return; - - const parent = canvas.parentElement; - - if(parent) - { - canvas.width = parent.clientWidth; - canvas.height = parent.clientHeight; - } - - renderPreview(canvas, visualizationSettings?.wallHeight ?? 0); - }); - - return () => - { - if(rafRef.current) cancelAnimationFrame(rafRef.current); - }; - }, [ tilemapVersion, visualizationSettings?.wallHeight ]); - - return ( -
- -
- ); -}; From 9d5dc99e69927b37b42a4d12827a7d3c66091f51 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 19 Mar 2026 11:20:48 +0100 Subject: [PATCH 25/33] =?UTF-8?q?=F0=9F=86=99=20Change=20logo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/images/notifications/coolui.png | Bin 18861 -> 0 bytes src/assets/images/notifications/nitro_v3.png | Bin 0 -> 24071 bytes src/css/notification/NotificationCenterView.css | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 src/assets/images/notifications/coolui.png create mode 100644 src/assets/images/notifications/nitro_v3.png diff --git a/src/assets/images/notifications/coolui.png b/src/assets/images/notifications/coolui.png deleted file mode 100644 index b78ce8bd8906d010021acaa611e5306ee6cd7726..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18861 zcmbTd18`;E_CFZgc5a-GZQHhO+}O5_j-7OD+v%WVr^Alfu_xd6d+*iM)PLqbGq>v8 zv(Gu7z1Chl`&6wJrKBK<2!{s;0s?|4EhVP%bx!{}6kwpf{s--@3cgOTKq+k(5D<9u ze-2QPtQ;H=5Qs}_H7!>yc{v_aM|%ciGe;A122Xq77a9bFPtX%+Y-(%n3NSIZw07Vp zz3S>C1z4N$lWMZdGsy!*&8@7Zyq(Qey%p3r_I3^~ zJf8fd|DwzDHU8%|BPrlt5La7%QsI9X0<`3n0HTi0<^XO6W_nX5CT0NV*NBaSg^Qb; z4#2|1!pg|R!N|f+&%(sR#LUCa4*2(v^oyOdnFWuEn8d%CeO>XBTDiIcc^DZzJUkdY zSQ#9hEg6})xw#pcSQuGY=)Vy3E?y3<#-8*JE@c0qAZG4j>TC^kwRUs>{6o>$#L>-_ zpY&^~|09Dv@V{goT>h=6FO4yJ8Uq=b8JPZ&>0dxI)BoatZq9c9Vs2*2Xl`e2Z|>mg z@`YvoFZN4K^78+M|1Zhf+y58s;wtX`wW5E^_P<2CsCfa+8CA?(9NnBv&BfinIFkKm zVL&TKS4S5s$Nw8S{`>g9(g1lxoz0D19i7!29qs;ODW(5V0kE($FaxMHtsTr9JzT#1 z!{y%-%*Bjd&G|{ctj0#q#757;rpC<7!@|MC&PvO~!o$S$KT!EEM=&#XHU57Gb~LlL z@cQ3E%}jYL9G&frzqDj+Z)|DK2z0Rg7dLr%9%%;`S7QfLb7?Vt(yyg5SX-O%m~)t! zurq(T1G6bB2R%0vn+3fI8yhD*8y5$=IlD14vkANDf98uhn!5dC!T-!R`~Ns!(b@Vd zgpBR}|I_)eg?L0A?Hrxu9nHS9%=(Z2{>zVeq^w=OjOF$3h)^|m`uE7r8t^aI;W0M- z$GiDSP5&{gxf$udZ(INW*3AEq^{_JkBKrSe?*Da8k%eUAUY}`)BGjZkP8T_aDNJxBCQPVPQS_h>!@l`X{LJ<;c+r zfBcBz_j%X>a;8UEQ&1?po&08or$PeIv6@!_k05+ide#3PlaXv&`Wf>c@Zwjit)qpxdS;2{jL5qWh zK>U`nt5JioEjU4m3?2fkkKh(E1M;@VK)cq(Ub(hzX zq{n>gfNEr{bHSV_f2yVtE4FPlK)yY^HdLS&IKf$AvGg+km0QE^Iz0b;GA|ibnHta# zsj!;x`|FYGz02)Yp77}!Ja}P&+~qF^{7mrI<8d%Lfl;es85bmNWCM(V*s{O)%?_T{ z1b8#eeXjRF*S&VNZ1p_hR;F+JW*wDDmYbK1yCh@uo)c^&z&?D=A(!t%rAvpW1I``5 zJ#b2hgY?B*rC5i0nGP*=w7UF1AaB-BITO4U$Dk9MypJSg=VB(Y+8!+vMJ>v+fA`O$ znk-qd6rTy&)i=_aHfW8Hj{^vhOxf@Pfcc*nuABWVv`U9zs8NaLVe#Y00qxSD>JiAr z-5*;a0bG<7JklFYIr@UUahktBUA6OFRg&dqF-|9>%>e{q93HHVbi|LDuKJS62u6sLK*TR$WM%=SEZ&DxW-lZKY9Ai0|Ort z9C=`!Y_x3-ds|ohm(Duv+?jR;`p^X*$g|S@L?(s=sZQ-Lxyv3e51LqOd#yIV!z$(! zG!+U^b72PBgaK3Xr6TZ0jG(+jno@YR>9F(|u>Jc4dwP5QXKd?w+{Hyjhpa%VmhcFe zjQWqV%IpdFJg@UwTRDd2OJ#p)x7aMH69>HAtgILYyz7>hmX1BnGzPC5$mX)=Ye<4Y zeKuS@E0$il(WIIc+S*|1)F7^b+*S@Apw zeY9X3$>H=bpGn@?Xx?7)VZ}cGy{`xY6(gyLg2qv|`rU!u*M(X_d0{!M_uCooV)yZJT5DjsGI<5vNfn|R7 zlbRplpO$c_eBFCCSW#8%$cpVKw$?{J-Z6mxQI1IEal1g^`4OHM=ixW9p5T5*>@i*7 zWxC$N6<<`>j@+12<JvWvFBlO4{09sTparMas<&l%(nY z5My}>m-U5gpY5&v0h}+t-RTI8BX2_o2r>5Ps-w_<-+;-b14=|)Xj4RASMEFW_}*$8Brlc-f!Nq0PY^(NTUC^N-f=y5DhE{%+Mcy4ZO{H@=db;q+~^dCBVCVWrp*TrJ6K~R z6F%65U2p$ULG*a6zOnjuUH1f^i2EupMYm%T8Rrd8!V#UY5+#Ho1HNl(rfu8bXJ9Z4 z{`2zj)rlw9e4$*i&wg(>>gr;%BPBf@>&vN<$YIshVZ%h8dvoj}xn5uG0hK-)O|XU#;^xttQ|X@^0jZSZHvPt8I?ki7$1u!H zPt)^Xb>DX1-`|TB&k>n^$xU%gf)@XGWgIw@y_tXkSqc88nT^xAfDD#S4l8K*T`~ua zUH=Iuf!za%tU9HA^LBdMM?VJNi$zE1PJcflg>?Ws=#SfACZ0pk+ZoXnBwbHUJip?m z`P-pmz|)=1_b8}G(Bza9YioP?m*e7u4-EqYf13VKSag48r@djpSKnSEZEakRY{cv> zq|R`0L$XSfTeCifo22Tfqv)j~-^lN)@eyd}B{!W6y}~@KC+I6y?7}40z_U#eO##Oue0# z7>2~-^zSyA);FYoeIN-$SK!)x{-IhfP(f{DuB%HMcklMJvDwVN!Th?#6->YQ%sQUQ zls&+Gfv`iw8`M1H3t`zjAODW7kQLfC9(3JolspUK8~K_}G4k z$APFT2weyTI!lgDo&^{|aB$CGc z_1a>YZ@&}$v~Blx<5L9`qMI>GYx#1?=VjO zAlARW9Iu+)mPv+1A_aJ)2ONOB0@-xmu-Cl5*vPPAi>9Ns(h24I7Ha0!Fp~Kzc^x7g z^oDury^Z!XqL>(=I)qS~w0o2=SQ~dx2{-cF?G~qP5iExoSJmOW;KM1|#T~wK4N9z# z4-VWp=>ho8gYdO96Sk%PYrU$8(av*NjwOZB8A*vza;3PyYqRv3#$~ ztJmS|ZyPPB6KOJUi49y$uPkPJY@`oj66Q3%O2}OvffNWA)3bRX_R3aE@;H2P2gqlW z@QmF|?)#&0RKGHr_iN5-yBtO?x4M{hs)wx&s>QUYbhVgHf}ITg%ipll74 zKp#wA`3|+Q`~8FuRJxP;B!(baEd2A&qGR4v|2voH&>fZ~fq|M)@_|TC93tcq*quj~ zg`SrXgGY*A?RX;y-fE#o2tZgWH;+ad;FwXxeN56TE#Ra?4~;5syQ=Tl=kfYEn;ly~ z?blN!l+;3~Ckt-B1!xzfodqy!zz#$nOE5(Et&fY$%>29O@r31!L7Vj?q)U1H>z3E! zX5W)z(!e==j(owVRB92)I0wFyva!uGvD;h%B^>V$7yo_vLG24R7zV5*5)%@SSL{Dj z5}dxP`$VF?Jl~pTD3qv%jcjowD2RGLCtu<8ZPm%{8iXrQqc45ZT3iIkPS47gtWY2L zy%7yD0=ij{+&e;!vyf~uk9%SK#VIR;?CixIVU@GL$18$a_r?B|qWBJq7;;L{U9?k< z9?|Q|J~T6aFNkWqc?htj!kM>p$dWm%F=i z<0tvfV>y5QQ~$;{9nu4{qMcp8y+scSG#(nZW2co7)eB5&*J8-w>GY8y)iv1 z8pCQDg+4kEioY25%TI*R#2z$!5OK<>A3rki<426iA2DnwmfR3}PB)Ebhhrn&&FX^K zG#veaI*nGG;`r76mbL7ak&8QZvvJo5nBRLysE*u6M4#owQK}0HWABd5@1eT z>!H5KLt24~NKsuF&c0Z|@Q_blsP27}jiKYGj2FkUi%er(^)P3oa_y%sThJZi-Xvoi z5mXtekVZFWrreL_;`1HzX5lUXx#6uCw2GIv&DG)&Bs#8I0^N(Y>AU*#V4hr$uV}*S z)QO)I{~uQ{w<2Uih_X`RSFb)Cm@C4-bQCT!qlxg~{J#Ep-LZGAI#;c~_U};J%Z^$P z_qspg6zSghD1`UUWD-r_UzI7i;sy`PWP)An-D1Gwq))QQ#G}8pC-3NB8bl7;dH5p= zRG_#Vvkab%)qgh}UiZ(Gg=Y4$ zfSh9SG@HF3Z=qyeCbV!?B~zuvFB;-2-S`t;-dFUTTkdn8#BwGh_kCxLgpzHH663>E zIU3iW7pztXQgm-X7tFdmuGDz$51aq}Uu3VE{i5qb##Ofw96QI_q21s}!@yDNbF?tP zuyOaz>ihSuHEQzV*PtDto2|hKHsiqtS23JL6r;p4mw4s)AO#7|iPZAU=h2dD`Jh~i zWU-D(07rFSx$JGe%-4<`N_*VX)Q&Tgu3!UahuLM1BMi639>G1s%b9~g&)CWFDXagt zdMF-kB%2y>gW}5dn%H_+r|J^u3bt_8pQlk52L`@2&4UR{Es{lqcVbUiedL9|7d}>n z0S=KC^{)K)j(yU<-H!Ix5T!l|h+Q59cstUi(SApz8`1qz3aN-UQyF)JO=27>{X=M! zxRz#j*u_QWSep6FH%GpFZ8fiwECwhkJAsaKR+b;L&|t1WORD&N#$TKjyGFf$Dq82- z9IBo|nmR9qaat4@M^_b>=jHyIpSEg5faZZ{n~tKWZav<)gS7gs5v+&}TZNSpEQrCb&i>L2R70kX;0o~@ zfEjsqT9t!?=UgPtGj%&W;l`YmBWL5{t|`irJ>}F;ysV(r3`yIt-fn^WRqJMz*7dmy zOWVHdgh6{ulBBZy3p8Zkr|orLfjR0H`mUFQ1r_TJqLnSA9K3QPOw`xV7wzGWe7=S` zAQx#oLJY_C<8gOYuDnzgY7HcsaDJve?t)XEd~?~eUc118__8HiDvKpY=t7VKnTIsJ z<`!ekYyK{@PyoQ;8;#jWFgUu56-dCU#|@{T1loh>i&#LNXwbhlX>8iG^9B`KTLw-q z442{Eu~Y)I4a;ckManzb-{CJ0Vv)bXlO=~}h*$M=P zgU{}U*AqK@@>*hIHy*^)cATb4=Tb?6ctpOb$GsVw!Uv4E++=_<`0HC0aTJN>V!M(Z zD^NLyL>zGO+IqC*v^%O#S2wRg-ZNzHZp8~}rUr$IX*7lzX_29zJt-k>x!02lcf+Bg zKj0y1DL(C?Lk1CMkX3M4vg zlieU@O;pf+Y=DhNinq`N#{i-h(y#(6+G z*2~_Mk6{M8!EiQ^;c|CC&LiXA)hX^;hF~-a&$S_2%y()d4&h1!_Kkk}YCUPLHWAFf zhRlK4-z%w#2>X+8sPi33?aJm-J=Q~YZM2tFhfG%bb_;OF;j>ml(9tZOP^Mo2Sy||A zMwZK7nX7y?*E!r=3nhnti?d;97nPF9u~jm2{c-~~Rc__msumc8(OdNDd#!{FfjMsa zv@;W?*}X?M93qeA%n`55T5~}sjOCDM(4In1Bkyv+ut$|dgrh9J?$+%?ObVuQwDkK> z0B>O0>rqzkF@0Zjt$u)LLg7pq$zsr6Swndso@*m+`^B@JfYtKdh(KwfS3E?;p>HT1 zGxtyspbK{XHfND;>EHM>D7V8FNCavN+P<>5YjEpS$EJY4&;tKg zZNT2hK3EzF1FotJR(g)`a9mE1={1Div!9Ya0CyM(xh?n#gddL1s#8gz%GYFogffZOLqQNX?dae~y3isX4$UXAxgX8Jicw8Xp=GipJ@nAtoO){(SewPVvgfE0k|=3_@nVHr32g zkByc(ZS}1?c#bkmsy|df2WqubfjG$yXcHk+9;nmz#7$_lMv`xed`c^YEl^oM0j7aU zkvtPv$XzQY*=QCf@j*vzaH?Ys=cPPgMkiQSh)J&wki@OwUe3;~yJb<_*4J7Q7GADb1QDwRTD;q zxCUM5dMA6w*l~eH(P0au1vtaxk|HSiB>fyfo`bs?aX!2nn^4wtI9`=tP&3-`Z}oRM zUciDjOu>uIt~rE66NqetYF}(DLHHkGCcYPlq}vz6&lOwo&(Rn$D}URC*@b<}lnz7$ zn6h9`q27e_4viYLn=7QQ(#<0wcKanjJ1>7ITTro`hm2ljW(vk-)`LjnRl_kEE=|0h4uQkFSJ#~=dgtuGV5Nb(^}3|!k9vI+NJI$Ahe|4`fEeb0W zMqlfMm*@z!g()UXh6gZ^wR+=`|0x8&03E;uq!WV}Il(&p_Iq|yqKK0>#>v--{8oc< z`WV7545W*Z1)*($$@yktI#CIt&jo_nMUBTDFP6e3mls+`C_=% zUa)3}{k`0U)*3Hu$FFI|xvQB;ST)gl13-`JE}{)7!WuE^XC;f7)@})#G4{9mp$c*w zG!Iu4!ua2GAsAE#;C2QKuHWb_1RmN#*okHWf&8-($!AS}~ zL)BA(@nlx-?U*pZqO7FB-26BbLAI@?#FuJETC_DgY@Z5b`_?BEG4Fc411AlKSDvO5EjovIsK91O2HB>B36~@)9N4b zwLS=LQtEa_aXC}0l-sXmJspClmi%5|!>+fojXxz|o=0S}FB16^qxNUa_J&Wol@SF% zE|`KP=cDG$Z4yx;U>spU(X{^*pwY-OBVm*+L!WIi6>T$VnZ%l3%M8{p{{{%9>Oeqq zMBN0UM5y~P6)5LWHu-5PW+-{UJ@=nCGJUkFu3hI~{#r&Gmy2FA?^J#aRVHMH>Iwn^ zY-%4oN7Za$C^#&FGdcJlN$z*;8+Q1>)vBK8VV$0AJKZYJo~mS|>8Jr((8KZP?EdW~ zAP)HCcTL1Gtu4`8pW10swD=jIY#Fen%jUdXl&*v?NPl>DZox%{br7$;VV^{RGxfM~ zts*o+v*305wC#EiOmmXu-v`rSZnRC(1p^S8nXsoiQ<_tZZQ$y!Nwgk{K#Ez#LC}Wx zuC&3ku?7w%J9ZYej8l}_#JLcpna{;slcq!0D2LsS8q}hrSm{HH`t!UCDx{9ncLSMd zso|PC1~%w0UZpr41J_8nGA*G^|At?$&lJashYQhap&30XDw@3Ki#*BF0LU6R+mR%p zI%HdYhIW`(ywpxHS;Nb_>BewJCg_%6I=?Nt%ScX2BUS7-8PAR_Y>CVLNXR{IfTpGJ&xw_X&g~{s3f($h>}leG--gxlMapWA zyq~)5CTUEB1dL0syNzi~*QPgDq8mFcDywre2$A9nsJE91FprS1xcpSap$>P`*(FOE z*Ap0?w^Y{=xYsYZ^}A~AYTLo~8TKDrk&(m@~T+_#zWzY=CLtvjkvFk5TB8euO`QHl9WpS z282Y+@U^YNXmSc%-oDms1yV1}2$@Q6_g(6nu{6?t<;!wlWa0UN1qtCx=o+($%nMX_ z{z4V<3TQkcqze^}(Pp63A|oalgU@SLt8n-6uoDdq)v!h$({ z=Dqx+z5@-2fKe`}Xu5K?#!7?qn~oE3#|J93CM)c7QhmkXDd7w zIdA3)>G|gOEYHmZ#523Y){~8iM$dpqt;NilP6vQ2XuwnDNN?Qf0u`URb3DHv%fpyOTyIu_I}853%hO3Jck`%p%H1P}hB){;AoSckcrKE#Qz*t8pOpTlKu?UZ{=JZ%*J* znQ33Bc#w;;@$@&+Qd>h_wU^=e%i5Q+%-tb07{HM95&SYSEa3qdSMuEUWHe9d}r7!a1s#z@bO`o*7N&npd6i*sWK)M*SEN6%xdglSn zXo{}B)1XgiRCbkhSf~T1-=3S$9AI1{myXZ%1Y@~XG?2k2ZH~b-azHs&L~(GT;xNMvxZUCA*&<~l{Hly@ z@*yl+M?IF@;9iEwUPp_}Q{Y0sQLK`1RdvD9vjVh=7J$F#Y|L@B6vs3h_^~v=!TYGQcbs;;n=}RQT@Buph?ufI|d<*cV zIFTJE>t|Qa>~0`PXl<1EoXJZjAVDL0Jh-x5pa%bHV;Kijo ziM+RySyF*=3X;D8ewdk}EMk_y=F^XTekzqT%TGccrMwSKInTJD;GdOtZU(QB670N7 z|A;=NhRj zEmk=fh*cJoJeS=bH;JH{V}v#*JX7;Z$Tk^Q0Xh8TQa!dAV<&sk3v~sm)EuCs|0j%` z9;{=WU{bX%_P|d~qbt1YPEdX7y$6VKd^mAa<9@NhQ97bkI7WNyU5=B>FP*JtedG`^{(WG*wU`dLAxMN!)fd*AEf3j$yJYsl@+n^TT&~lDmXT1keIqQz!1%m^uThC&J{qC@TX0|8c#Yf~+FR=@&Wtn) z0tgV~g2=OHZ_CUFONzsynh654(v`3Up&v(hT#*v*GxV$nj)2 z#!1_);}rIc$JhX|b zVXWo%GZekXHMG{%*h?%)er3Q>!il#E?b1F@fTar%lZqlhXDef}m1a-4 z@Y)L!zFfH!+yl7O`6dQG_ldxZ7U*PZ%dCgB`&c$spa-zMY`H7H6({sOwj_=Yyue); zgEm(nAqwDRft*=*V4`uS@&vl!VN51Mn5bjG7u44*549Nv3^*PdN?%k0|!$zNO`ks*{tu!&HgHT&G?dz zcv7sTt~UkX2O)`y7DG2MI^nR2fy6o`8dovZlL-lg1>?AG7HXv{QbYb@1O?x$?xED> z0IiK4k*H-waUNvU1*~0p+1-VWorZ^67P#AkVo9N&neNgxSl3x{dZ3^(r^=j;=3T8> zFa@KHNsxR*qO?}E>^l<^a-KKpWe#$FAKAGUJLYvvSK&Et1;SuGXu(9c`+6zF!(w8g zFiRJ3>q{iMtC2tmH8=~Z!wT9G`}mPR?ZZ1U96;0Y|LlT;eAuXv~p9XRof6v z{iRc8i1T&D6^fERo^Pr*)_i3Yg1sz7x=)0tH^V5{eWe7l%Yt=7qEXmvgGEExuTN2y z++Jvv!`rMa-_>pWe}2yY#=obCkoaE^ax!W0i%Bxtk1KCRx9C^ zc!m*v7IGaQe7fGLfPcmTZ|(5Y#3UcTFUwWiKsp4~Fz?*ozlq)I_1^G{%OL~tMA#7y zO+va5;mDz3F(j!_P|$R`P}tL97x)E3a7Q_avc2KyAa*}cl*_YMf`Rh| zdHuVtGc2xquuU08sY;W;RlQxJ*cm6no?Vxbj6Rt7a~7Nw@iyL?%G=CgifUBx!CEn@%((> zO}C-xzEwmiPGlBjoRTKpfaCCA%Oyv--aC2lfKzU>W;~zEuGnGNTcv6^1+nGnc#Vef zh~kGxr5Rb?P=V`=2B3z*mAkeEXl)EUME!4u2#?8K$5(O4{vqXB`B10`HYMEw?sgOv zsC~w5ur3afh?v$Ch0))QAb(Qo#Mn+f%y*;$2cB_=U%0K|;YfZXcrlcP%W%865nmEJ zfpcz>8^1au{=RiE*W~iNW6k z0c{S^xQ4cqS z*yI|HX{>$_)yyrYYLZNNLiz08k~-+L&4s z9o^XgbrNVFBQ=)C6KRIJvsm4$zj1oNc*89C>ApA92kFSo<_8Kt_N&||HS*_l!{Fl0 zQDNpjb4_^M1ln|)fY9D;K)5p_xwde;kPmCPe0E_6rZdZG#%9E+1(1MZN;)b^FaYEI zx5*5TV8Wk21{Lry{o9SPC~ zTaNmECOAB}MQBpYq2=3o!iW|-SSO={Q_`4tA~ zZzF8VKr2}~>IH_qlR;>R9XG;!^Yf_K!kwYASz8$xL7y8pHu9vfp5-l`AY27SE`IV=f*I=`L8ge4CjiQD;WE`goE9HGhOWA^#&7Z{ z?x&>0msE4LIp(t;K7T%|#<$+$Lu&#l9LC`GxXhG|EYe*W4Bq<4frZsBbs+3EUw~K%chZ+SFY;d5brck%8-bzi=$4(`i0NEnn!k z9Kkl^6Qre$$?bAasSK}3Rnh)Uz90FFW3K`9aVmuLzN4C;Nvx2t2rz=!=_)9E=*1!S_XEt)E^*GvCw3Jr>H-=`s z7k5sWHoo+(Pls$x+H@4*|ET-m$w%;FYj00MMix@n_rY^{*>z@#a`hjf6=nl59kN?K+eDt>^`H- zDXt>M2caunIEnG-Ah({Z_M|`1Lj#efDHfmJUp_rb>M8fO5m{no59bhQfBjio)%+78 zvArux@K#(RL8|Yx*jfA4R3arKWAeM(i;Rp zS35)D2Xb)3xdA`rUWt-Y+E8zva!hW2bd<=@BTCqBn9W}0c2AY5LwSKq}K5DEmU~5{! zeDx-$Kv=G_{IN4tR`U8VR{zludPmjrBz%BPGYLoG8VHDA@tb#UU+^@I9kHEW~!SI zRC8SC3TIg`%k*>`>K8)j5gIcJcQ| z{MXm+H-Eu%NQt&HL8WFJ%2Ah2hc(PZ;6>^+xF+1RQx&>>Qc!YL=Xcgglj8iie%8Zr zCWgXb#pMGTT8IUgisO8?%0qPn!Z50MZm;Bc8W7G_I}cV@lWq4;$XW9%(C%*}l1Q<6 zWO!B+#BIJR;lF&KMdwFd-BIHt!z_z2g{wpRu z?Y3mYSc>=0QgVeCSXlJ2U^5Z>0fP?R>mI53uhf{H1W#ngbt0iGpaBYocWY9HC`0C&~ zC_n62#!;t8`r1}R$wvToT%DOPF}_b38yiTPU+;fPJZ&^THf?A~u$u!_Tj}rkA7UN%Gd2Lb` zv&St?<=fvWlD<)86!2C$jUYz_4Q4qW@X}PdAvFmV?8f%!P@0AidFzObR}reLyURg| zw%EpO0jr>4NT||wSDabi-e8k$0j8QY3B;3w&-_vgkeMRPsFvyjMM}yC2e)RdR|gx< z9M$Npv?925_waa6JN%Rs)U|1&LjAU&6wXHwj zJ!Zk6NO*Axh!oTs*K*#yPsfAw40uke@EgNhzv9N0RY3jGR7d_gxB)~87oyM94K2D# z7|i}d+I`qbn)GnU5bDsNsi&bn0pK?hW z4Q!R_0vStsC6uzz{5otH?=KXKhv7! z_<+Nr{~BUu%2NW*+)_F~Dda}BM~B>LtT7QLt@%oAII1=b125pvueWKAWJ+7n6jYaz zUlqk}X1TQ@?aoucb2gMlYC9&{^OlNK1lI-{6!S20)S3EW8H1P5B-OOg_Y=c@sRWr* zSgM@xmw6M1aXarE$xNn`V36u(X=8#ro>)z^*JA8#Ar zk_4`&rG29;CJ0M)d?KC;$z-j!gsT(1f_f1z=lTZ*?CrY`=kvJ{1fgi;bl5bVg@(K#+?w6Zh!otD> z%dqH_=c`>Fj7Sk7r>8bNf@QhM$-(5((565h!o`V+2?AQs~$YqB$| zCp|<(t9sD(!rwKtwjm6cBQqGbJ(hVJS}KTp5Pb(k%8K_VG! zaM~Wy>uOx(>Ak;*y86Dqpc=Bw&;9%mG*BFoMNV|vwKvlT(k}OkOpJ{U)G;3ONKrBk zm*1a?!n-OFqj_cHY8ZziN|<&k((O+fA2-6E1P%#t70ubHTeH%tNxD|1OrJ}ZS0{w% zBZ5BYV6HdXlQxZFqvFq$MN5G@l51R2Z@QhnDw5o{tQd5G8NJ+{^gTZNl2cKAMd?^o zU2pW{pCZFP|K{fA0&Bd^T6Apeh+jv=U!9!&y?(F$+pDXdc>O2EuiJ^hiPBR|Sq%ZF zV^IuS&A`+xKqacAD{4JfT(Buqb8Vf`#+x>TjR&!Nfy_26g7>-oXxvOr{=%TDv_ZYt z^D57ePvFJyO6^wt$BJN$-W}zI<2wC&b1 z0s8mv|L)$sdueQJWb4+gbaZsk`K5MMRmsfEBt1QyojZ4q+q`-6l{N`}33bN08X<{Mby=;;dt{yck|JjKr-F5Buv#vRB?|Y9snisu+R$qG9{sJ}o zDEZyxG%kLV)YC4&UU(w*qI_gC9C-Fk+P5w5(O$AAVa_dV8f)R^8R;T{V@)IEIfR?3 zI9?n`UM&@zEYsTcp5BNMgPm@bHsSom8|nH4TxSOQe-o*-wY5VA3>dI;!h{LIHEY(e zWXTe&Rx62#iI~l1GBYzNDJe03^UXJt-hTV-J9h2bH5H(!sA%b&Idg6s*e`#>SD(C3 zaKvdOnMmHR^tL608Y_F0Gtf1W&S&cjH47H80$1IC;kK6^PH}(oJbKfC!}i^}VKJ>676afc z9#7Qo!&|bsS3AAm1ab!$_H3>OM@%dkWdV#pR4CDbn5`%j3q_Ho828b-LRtPVkqQur zMC_|quV(x9?WCutV>B9RXlS6Wu5RScojb4i=9_P>>%3vuuwj4A&(D8g+_-TyilS1o zWdqg|FJ;!~1onp5vBH+p>mi<9tC0<$+8`PLYc9xU*!SpDl>KS`;YX&}$$xe=<4+&_ z^x!sbjQXM7&=vmCon%QAiuFW6@1V)j$Sostm3xLgeD}}SUv}rOOWydS+W6NykLr_Y z-@59E*Ah;f&*Wqt9u*X330eO4IMV++lX}rb7d`m;>#yH)#~pWU$;imqtZAAW3v;EVpzP1Dl6TD|XhCRL{53W2K26KY4-Y#i%b;mw-~T*g$K+pE*ub+jqx{a0D!-Y2_pMh{PM>>w$@|Zl+;2Yo{bp#^EsVNo%9n*5eAXBd zM|)>1ef@vfIx?xZ-g@i3H8nMcB}Sl zz4@3a^zA$7hxsl}cQTy&D<<9f;O3iBn3o!YW?ke$j!c9vYXAR!Sn@xOR9)9O>#Vcx zEH5v=^Ugc(j9a>NslI>z{cVdwN#^fTS1ag9SJCT!^pqv9tvg*UN9-b z1+#2Kn@y03IMt?Gg<|giH;;DsSCSeC1ZZk%A`}XdoSfWhv)S-^y?DJ|8XFt&dWBP2 zI)r6}y=_=?29c4Qh#G;2S%kujiSX6_*?FJ6ckTnTy$kL;+%peLskgsM;ZHAKak7zV z4Q+%f!(tJCEODbDbTDfx6~p{oEM+G+i|)DYjBxvHr`OaoD&+I$>qxr6YTloa&YEm1 ze~ovuA}YgyfY9wCvC~^yxGN>&lkkv9+~FTus|1ZZsoJ@)Z} zes&1AF>l>%_in5=Q|Ny6=-@IiTLc2Fw8sT_ zy-z%^7i2@4&648@qwjZ9=?sFw4<2I5`u_m*z3Cs0a+YM7NLV<|R7E&I6-9ix-tER) z6k_DqhwpuPdGWNl^^0C0xMw}o?L(Jk40*$la|e+zm4(|xK0EiBfE}shMziN zmNI9;wJkLaZK-asD~h7W=O(&cNo-D5c|P64S8X-W5lc<_S1e>i@3J}lU$Zy;v*`Z; X(N=cM>u^}K00000NkvXXu0mjfJ*`(2 diff --git a/src/assets/images/notifications/nitro_v3.png b/src/assets/images/notifications/nitro_v3.png new file mode 100644 index 0000000000000000000000000000000000000000..5a30d5654b9ca2a078eb0290dea321dfe378afc2 GIT binary patch literal 24071 zcmbTc1CS`q)-Bk!ZQHhO+qP}{v~AnA-KTB4&uQD9{_cP8jfsgj^Jb2QSgTc)5Ps z|8&z65&R3{V$Dk=_>VvW4Os;OVS6W20!}(cT4M$VCIWU&Izu)?HfByX8UiK;CKh@I zW_m^@S_XD577i{ZW`choL_gx3Ow72HM8y6r?B|J>$il_Nfs3Br-QAteote(w$(){% zlarI4fr*}piS`FU>+EUgV(39@=S=(`1`$(dV<$@o7fX9Pf`1qdjqF`rc!_>g{T~}_ z9sWye=lpL!{dkPt!_a}Ak&fXXoBjnfG5#;k!PUv;U&2j{=}m1+ZB6Z5oPV&4|Hb~e zNmlm1@c$*Pt?hr&ΜEKN|hpw*MvCS;f=AlwQfy+1}O3*i_W*MW2 zu>ZfY5wi@lSIy}iwUBBk&j76K+#I!*!#bxS)Fdv|Bbf290-fT@U~ zizzSBPt@3G8JKAqIaL@qxfofwSUIQ}7`Yf2{s$`ilL#h;E{6Z_!1gAVW}g3BsEIL` znZ1*(;g6RrZ4J#$=^gCM|0PXUmP^vk*~QS#*i=%4m*_`TI!j9vE<;8J4o(&$#-Fq@ zVW4F+VKJuVG-l(VWny7vVPrBjH8W;6{?B+3dt=vsBKV*2CjTGD%R5>AOd&&?|NlJy zjS!cxy^Xz-ti8#Pmzn>`-+$#1mxQJBPp~}yJtLG&9sk|3u_X9c>Tnqv|C8OkM8^Mw z)zpOO-`$q~zdiFmwC)zBKcxQ;;r<_(v%Q&%yP=b*p!ts%|F`Kx|D!zpKlAW^?4bX@ zn)t7G|ED?oANWrp_^0{b67=)%-y&ve_fxr?eoE9Ag6L1>)98~F5mfQWZs~DPHx+li z`qXY2n#l3?y0b`Jpa=va0#9&e<3R9XoM6MKjbThT0d57vMz)b@6lF@Mpb1nE;5=r@ z%%9+0Gm)UJb%{Zn{Gdwnu#8aWUB}QRll!ZgQ(`$_*qohW1*C zbzS2~RpTa9#W_fI^e5z)-3aXUR%Hfd&L3v9$UX+P~T1EoP2}<+ZRP_v0*w&wsW2mk(q2NIH3*h%gwd~4xT?Kx zuI>*zIMBl)%b;sa83l(t*Uf#!iwWnO{^5>CvxjBuYmT+gv$^?KeXrHX*RXffrO)S1 zbaZM?<-KEe+?S{8#z*#T?)gt&RnqR)(>TmtBFhyD4e$MVHK)Oz_v2J~znlHlxLH=f zweHHB!sX9>QfcX$EB8t;>=wu#5K{Lo*H@kXDWPC|-)Fa6pC>UkTbxqd5|K-`ipzrl z6<)D0sp3m8_3ip3C%v^!S7Iw5n@qj~Zb~rujh9kb9R7LQuzr$)Pw;kb z|H3Z-S;Z8u+mQ`lf>hO;G0f_EmtyAW9*D~8hJJzqBENvmBG>l*LGa|3_qT(lwU1yW z_GQsVw4(JxD3tv5tIi|-wrI?P6}uXnvdPq#-_J8*O^he8v!U{(GzL++a%ZS-tcX3G zTo$?V)Zpp#mH-vtc5W1Cm@u(QU<1rgyg#uW6yqRQaOplFJksD-F=TYM1PiPDQIw-xzE$CY&`&Ws8^cF0TyL7TC>F!=0|1D-@VY+(nRcLneXNQ2SZdyfqEJ`Sz z82r|7pFiqf^vzAUUG~TMnXDZ59ZQ~j&^mREvxhL6wzfE}dpzIGEPfJ7DSJ&8bgB-8 zlPZv8Xa(zLN#zlYdj}?Q$l!1~BaR5}~Bo!VHpo-9qf`>2az*WpqL_wY;xg}AStCC==i#Y3II3Xx#afFs3cwCh;v~lJq%B*1ME=|`4okA-A(4!^(p?s!(O!Vc-Wn-n@55bG~ut zx9ZK@pshsCMYaI2J{S^)iu=%cuh70D_}y^n;je8&AG09}IJfov3^=`G=T9P;r4ggi z514lT4zb#9vkh(p#dSz#KGNgIKA8>N><~O}-gmX6MilB&B&SI1JD%yTyol`l8s2G~Y3X_g~; zPKrjES^5atbfuPV(XOTA<&8KXan@6#>PRThiI7ARNsK>+N$=_&fAgaTp_!L$GKhCUk4>yCC1d9V6kW!KblS<^tiea z3$Ru`otY%}r(pZN{i-kzV9)(sKY+~wBFI3&<|&5&BJ&gCDF2mm1rFZvPs;9!yw8!b zj`I+3tvM^GQL-MQy(0H#%!G?!DJ=J$J;zG~0>BV;ZVOO!u zp0WB0CO%%t(^G5?50ivro`ONqRaxhiC&7?pdY@;(-tPwH_4dvPEcjmzcU+TSkzAod zI9b`p!?5R+s6Kx)ZKLr6Vj{I;c7ZgYN>xcQbyh)Bh7mms!D7OLrPcf@*o}x{-T$M#Kd&&a^iKSw(?l8LZC0k)`R2Eh8@1GCu!O>)-;k zbBMOPo`3s=&!Nz>$tqgTh6ESQPWrG{#y*u;esUr7OUpKw`)P&KP&HPEZkXBjYD3x7 zu41I&lntoy*db;~i`Sugg595u%(9Y2o01!Tc`6dSd%ec`mpgkUqHqr(vnsRCx-GML z>8VnuuW_U<_!-JQATu;0y1uhWY9%Dq%1&1snfRFYg7(vmNc&qXD38COc^Xn>j0zr? z&g!d`2;W0`W}Jzes3v7p;H7E>zRFz9 zLM60{m1Gh}BFSn9%FMtfn_FBiVr@x*OBbWF>-t za?t6E&8q91@8dsRl&y$OW&2Yx@9@j`Ps75>msi*9+x%m)&%%HfK`L&%uk&AuMxA~ZTPrfDZGi` zPdL_d78~%r9~IbNJCLw|udtsQrK;qZ8q&?LFcL|Jf+bO2fQ}kOHnar?z)=zgn{1jz z4Y+qB(TE|JTHIR8HYKPDD=zZ>*GMk1yq#((R3rhrs(!c<9G;~ag;0u>&%&RKcO}sJ zLw%e4E!c105%cqyokaGUXC0$da#5rP$j{67xvwf}b?e0kq*mEWD5`?Pm1}r#xb6CO zy|-v`JH2{&pFw4a)`J~!n}iCwVQ)Ty%N)n-5o%&R< zgcd;P?Gn5Ii56PPFn-ks2ep(hL~s`5NY$dAiv zrRm+#dyV|E+jorfkN#RL>!Wi(un>Aj8ylgSJa!LY`vp^nof_w}$GZ*QIggWS?_Xm` z-+Q2iNvC#n{k@-u;uXNzpM+|c5RLqBU*ey)I9b~Xc?imL8RY!Z555Dx#0@`FOX>K0 zsqSR(aO0V#%kE}sVq35+HADX%riO5nY|gjiw-=0$Ty8ph>LPcHS14y`zWB_c*R&8q z?R;#DeN?8dSN1A0*RJ}t?rQAbJoT`ZB!}s7+ZxkD+lN8Yj2x$;esJfZfcQ5uoIM`K zUWmmpFB0clM}P(NaK17+ULR|$jq&Wy4R4*;-fAMp-C4}jQ~xs_6x4qAOYUmzs#>%J zI%RCO21pl>2=%V5=4U%Tyy*k7!bffQIj#JvbMAxCTv1^~)S8T12_~U>8I)++@#vCF zN|IbB$WtFE53z1oE`-gkiPvr-P_q^l1DLoz{3CEsS~UiCDyjOID%&f7jIBG+K!o>G zWY|P7EFg(25!$Bn!{1&=Z$6NMd+mbC8CIft)rVGVa)6_=Jb%$y9_qlJp(`EK`B;h_ zg3M+#5R5JMmG1r_!IQ0|HiuW+HVyu?!h3sBGXImUfSeBF9C$HNfoN=T(2)oZha^b{ zRMvdTGTSfVoOGMpqHBL0{qMKu$q$Wl8;kj*L;SOZojrW5^7`5`IlXNEL84|`fl9EX z;RxA?)x79S{a_Zf1d0)}+zdPXsJYvnbP}Pq=T9C~Xii9}J{;zSr0-Wa`#7=>nUhy^n zi^wJwCx_Wkw1iw?e442llP*+^pWUsL-G@#Hnn!W+dk!~<$EMk333GJ8GOc?s(CKvS zV3dWd-7xdnyp=)E#yyYW*MOPHoo8Ojm8UL)KbeIFatLwt(&&`8qylV<9ab>QGLL)q zC|m2C04ySAE`vO@Ixlrg!HE)2hpO{T)|U$@7Fw!kGO;}+oJM$g_IcX(g2+z_-ravM zEfGCm)P0S7zE}Al>@xBV_Ku)wgIOgDOIsO|w1Hf5NphF=-qK|9Vl%{W#ZQu$9y_Ea zR!$o(3W}JaL`o==(9|EPm&@W7uWd?O=BGU!Jt)yZn3Kdx4&p2pg=1n;HQId|dz{3+ zODv)shfllonFOx@g2^$CL%sF#1aJtv060VaB09>pc-DBLbu|+K*U52QLKyEj|Fp>u zp{wVPW1iVS>Ej$)@}A!irXyJev@$C*NagKc#;^p^|ElwEI?jG%X#fXGFe^klpaEDdE3Eh>(vTWE z(ql8aqR9K##pLgkU{7`27(F7Ke&J>-Pabt$cLICw+q%w654B55cyl!S+3{ychuC;_ zZJEYzL~+W`FCy+%@Rt%I>5>+Uiy#1aY#9QE{EBIE5>8;$A~!+R#X?0Q%KE)HhnL!} z+d@q_zrm!Uvo)lZ=LSxAWva%@i>8j_Ol!&18cFaR3=NP%)L@an1s5UCjrn}k@4^bZ&l+7`0dczlWqO^ET?-> z%rk4X*Iu)ElPd$%24~tvnHARJ8<*UKpqPNE@!Wh+xuEoMm7(QfFu4JCo~BP<&T#KQ z7Rf#4P|>@jFulLrKc}HxCoQvH!{PlSNn~s2fjs#^=}B1_g?A|*g*7_&%*Cg}n$?d!Y}Zd_K!2GY@%lYH4D`n-@b*>oNza$kdd8P$pt9v< zG?5yL4JBv?YG3y`aZM3jdw$9^2Z&W9i6)|m+ROtAg}I((OXfG%>VT73o6?8=4V6&S zCD_#5?M7EosM*={{%>mv5+c{^8XJeN<+YaJaabo7=a5ieeG=T91(!iUYYWtL)06{0 zCNYqM&OaU0!&W}28`a_1YBzOwnU6IjV@OyFG)i|!2*&SZB zX^>KzolY_TYxpmmQh9cs8-V1wTG_ddHUV!WGCM@O&+3D~Fi{C&3;My%F`_@P78Nih zJJtLPFynqtO$>m_-&E#2`%YeAZKvS;52QkBAO)mJOS$kR1V+0(hcpkz47my?kf+y- z9)%clYP&wlUA;;smbG9PfWF!AN2#$< zDTrLOLM!jTx*nL3CQvVw4jd^ggCR^x((J1uR18Fv;je%2Xc&7|gVZ zPZK2a=qEVnDn`tFtn8mwpze@N7gk}m%J6KWwRu-`cqQZ2@k~JbsY%Dughw z9o+*M5I>(41y|ZYz|-~PzN;4`*&L%8b)H~D*x_2!Bkh0Cg-X$XmR$05EI2)9=EwB1 zIG+1ThL;oFd&D-JJ#o0lcEu>7Fc|0_F-t*+u@@VYvwSar4T>g$1W5B*V5uzF{`|&8T}bC_ewTmX`eN+t;869Qt! zFSkHkr~{)(>gW9v+98V2$m^UbF!i=9TL2woPR6iKP-J1lgLKgFE2)%enJ8MW=_8WC zGvzLcuXhS|LVl1PFW@HwISQxRjH@7tAiBB=k&O+k8(^jRm_t4*_BCRtpb7}x(B(^iAU>U$UGr1jvU8@)pm*YX%6Jc1{+1oj=Jve$@6J;5!TP_HxjjaX zdzN*s*(0*V9nSJmaKjIR^Alu_0!c&7I|@NLR8kHUj<==m-4Jp-CyiGe4^=WNm~F$N z#-otC67>goM8E|XQ^dgN&~kl&iCO~{xCnOJd{RLgQq7?i!%+CokO4I2rX6WY@Jnnj}fS_9D)Du(oa8hpWvxMyet#k_J4 z&f7+UZtR2z%@Ke_p{Aok=l~yeiRe1yg(Yt}gK%d}hR1bXn|$#cqaz3W@>n=j^@wGc z5MwvWR#d&HPi2!zwtU|un3o4cJZ2K6Jp(Um^!!Awp(JtN=BZeORx^W>x6Cxsww)ze z#ch6SHs=py){7dJJ}J>>Y4dw5-|7A4+b?F0ycabw6pAjj3PNy<<(1jH&|wj)IaEzF z0ZuWq*9sHbDVezF1W~ICRHX+nQ$x6udz0$6(VXCsiaHehwf?R9mP_%n0INl(t$<)E z04jxtLd#T=p(%>JvB8MYd8EtYEru)w21~1S(=rOWCie>_31DiOtJU2sPIvE(k zwB^mNb8qZ&PgdLpBnn+wZZ-s1k08ixT{gnir&D;v%U#ev#YP0?wSVuOVjnxOA@{p0 zqS9*2z1kS^=#!>7pV9~sRp+?w$XeypMNi%U54YY&5$lX`6%Fh&eOQC&yeG2xTlc}H z5L0!{`w;}i=jI*(&hcne7=cEhpq8sgo8nmVcvQliEPny`KB-W-s8~#*CXj=eT?{4D zpoTbeGT#X;l3=(zh5&%`ktTgTSnu8O=vH2Mh)Kd*^l&j&8phNnw2IInXYc@9&;YN5rhYMU(rL;!-< z*Wrxe;&HAHFUSmpvkp5QF{{Uz*jcq5y@FerFN=U$rR*?c#j)C^2YNIhN*aJqPMZSA zw$U0#_p&{>vyKV+K+S8o;)~0G(uk*fZVV$Y>^Mo@ibQaHbdl{_x5NJBcf)5=p2UX( zcvh>RJ|yOqC^*j@F?pf1?2WP%PsUdPP)mj<8GVmR$5E9~2l$L<>H&0z^UMfM?83*# zJA)<3oC3*dfTd?XcjFkBt5raA1YPt{gSWwZMSW}9p)mhK%H^coEL zZbh(c@&uR&n{?|8R+8vv@t#UMmL-6=0I|7>#;Sv6M!05}=-334VQ6Kg)JmJ<(?v!` zsxwQK)C$?eN7&UhN~mN)X;*)eei76NMtwyZOQr_){7l-1WZ#}B>@M|FjoyhaHGfWS zO&_o3ojb!Gb(Por^icAO#zfBLX5;E)7!nc?aK89@`8~nBXx_*}t-Ae~X0cK8M-+dt zy(9cP|2N-!aRCFAV~w#DJ^7pIVL;%Q3~KKInTHYo$2!(SYaSu}cI-XP>OMzbj5CW% z5E>lAWTJ%S&kDW$Iu$TIP^ytxUB+iQ6f!g$;YC3Y4VQxVA4rDw%_;1{Hfq7%tKcivwm9U=Xh*m$F2GlPKsaIjehz|V4_c;Wf zV;(MXtPtRH&%X8Ivt7L-V|-bT4f-5>_Zi3Q&NFynE_v~#xbpGP&n{R90Jjv0lJ#Ff zeMDW_*%ZUG%dL*C@-% zPS!bnM$>f*uo8#5`bx`c|2o*@cn_^W19z0T9)<Bi_eA|jvsz0GzCr4UHe>!<sx z%ONbvG%G6(jvrdqV@)gX{l#Us=?qdtmq*c$WR5 zBmSR|?7inqkJYH7SakVYiUnbRjs~g|aEV(i4g1A+>JfGILqrH$Vz;ZQ2F;%bPG}^! zN!MduHXZ0Pg!9cp`Q;sI6T-nl7xC!RzAqft7nL-xPDBi$@OY@ERtn&kUZ1m@?+}C{ ztQcH#Ox*@Co1Gt3KC|zGl1lkPK*^rvdF>tw&Qcy=Ni14oq6X@UUr2{irw0$cE3y+8 z^+E}5JZX*@K033O5Sxwg;UU`6q*7`;xbf8UGEO;cXn)(BfmzC z9VjMSSiuM|N6kjKSr`eIB6X2XDW1kHUBevr4-E?vgxFqgRzLk<(hdZUGnwbH7T10O zNMbv6_AuF1FD~TrsXnO0z*mrz8o@(BV)&@A!fs|k?a)k4rz4S>p=HLL_FQi*`R*-# z-qkbaLw3)+q^@IyfUs& zXbqrb(R-zYJLCP`hA}=#eiwi=cQa%fCaW(77&~{G49pSFz^?Q067A7Dp_MRQm=tIj zRY1c-HQEfXew6%d!z0RCHi0?ZAf-S@%A9@5ve*qPc}LJZlzBfWzryjY72&0OVF!-` z-QfAnWH<=j!Bq#^j>*)_xxGY- z&NZeBN8A?A6-T8>A~h%&kBcpwW*2Uy@pv2GK>#T=YSgqvl4IW(TIl7e_tl5caJVKx+diqK(g4}8N`Wj{y|2x?* z?J19d4KTI23wL0-O_4tb6$9bjdGx+FaKK!bu zjCzrt6+bVfRo4f&t^pmbpsp(tBjO}+@DVV)8dySa=aY;qA3Sxp95x87Ehp^hqA?S$ zDPu4x6xV2m41}grr(y=Wklf4;aMbwM21vJEXLfG;-q48*%bx3X`2?MV(qKh`Mi|(^ ziOu{yHDXR_lJntQym&{^(gcbo6BsIIfg&hxBM!pA9lw)JD3bPJ@h+H50+CE8C;k8n z|GM9Aj|w_~GywYXy?7Xr19+!T9Vj3@bznoxbH@29VXU0>d?2Rv^HDMxHuW^Mu-0Jt zYco?|=LgOcG%7-C4otF=%CoZYa}gX1$u*Kf<7+kFlLFk zh(Uw>e_ICoh08Bb^y=ZQt)Vu^pbN?e8ih+|OJegr>%@wA-)~iXSUz{|JfBrhT{B?6>8-E?#pYLnEpM9L~1}Vv` zNRrd)s?z5O6m!A3BKu^TS1)7;@U#uzWfzcvfmaQwkF9>kAun%%z%N&G@7b~eW2^B< zzakgAYzF_y`bSmn1pYBN2fcC?id7L4I(g6vbCy@P*N>zwYY5F+=#Q%Z8=LQ4L>V@~ z)l2i3Du0lx?Vm4WBKE04fW$nP15=l0VG(EPWG}A@7?|-L?&}5J7*Te?jlEr1cU}0l z9Z=psQ^YVNn0;F+Drepbch($_w(1qvsF6x3;C)|CPDO2JG9h1wgbBxf_;OSgbf%EL zi51XQ3TzgW;7J5gH*+lt?VxyCBEiw!o7G*bDe>OsAowH5OEvj{hKEbz{4oRTupU!- z>GozjBQi)5f77Rg=!+?Bt^*?}$1)@mwyKpAR&@c}b`R>e5JerKtf9vP8%!@G*xSk{ zLAzLxLL*Up#qi&5lM+^UK))N|dtI>_J!BrZ^OPl+_ifcAl+(7Z68WpS+=uVBloXG( zWl`AB_DeZmz8L3EQL07zJeZW)ZrZ#HGV=MTh9#wn)=T|A&%8y7UN;`Szp?rN0(fvx$03lfW&i#*73h(VVy3-KkC$0%b)@nz@%h2Gvpwc+KeQYP5d&&>a~J znNCmRag+H`aj|S2t9hz!PRUJI1-yWjoY1rq1Sy~vSy&B_Q5Aq&hOiOC=vB6=B2TNG*fpQ+-O6WDf6B5`q&VF4*_WK>9Pg!GF`Z+|@gnWuJ1{p}vc_D*#y zXtiG`Y0?Wh%Lj@^3zBtIj*iP9q@v3^lXC)?pl-}G zL^^G|jsvAW3d_|{))*m+NSIHB=fMG4Gdcy=)IuAg=fbrbI4m!)`tTPU-I#7u8|q!bVK@ zVZv)ECVYVZGSjVirZ!G>z|bv{zb8pOwXb37Z=<(S|ic*Pk|d?_eH{K>4rILQ8RM}msSqM(G*X2Bfea< z0ClK8CqVc{Ji6r*N_X(s$j_H7OUv+MY18e9flLsr^Fs~RTi#ZrK4p9j+dEZ ztx+%muUq-s5X3!*l;_DdNeCedFvTAcA^q;a?BBL+l3wfT!*aWonrI0Drj7^wQXsBk zP45}u0u|=!h3o6cJfk z5=l}yVkI`Fu@pCFre*Gjs_ZlTpU?Gop40H(QS4e22*xI>;6Sg&cW35GGo}7>r1 z8(VPinUZ?QfV;sJaR*yI4!d@1{qIld_kv-74qcD7;S2CLqPxv#y^Ju4M6a@(eWZpv zsG|uSqieNcKYis|pkv{+plcvX%?YD$nWa!`$i~-u9PIdUYzJ$cZ}c@QAE8^5bL;K$ z>z@@<7?o6`@mrPXSATo{KC2%vaX#%p9t)Bevu%PjGyIk%;8e#M2I!4j=nrFjn%n;U z^_@)4|Jm_&_3SV9_ZaOKfmQ7E{FHF&*pe-4!$Ku8DPn(L^<(>eXN;D8Tg2}(w(YN1 zlw>!@Z7{>sAvQy?L?&0H0qq(}7M2epRPoeO(KcUwA7CCoR3H0=(eQ41(7n{npkR7T zzKH>wo3+Vs()!oF>T>Tg_RUZL!}$I?pCO;a$2e8)Z9P;4)g?v2c9g}+`BD$hw9ict z0S3h&t>9L*@I(B~?Xz$b9O*9M?@A8#IujtG#akn$wi+&njH#w#+}Q=D4rkzlJB0F* zn~$_XX8i$c{STTM&T|!7yB&hvuP*<;_v(6ert#OZgC^LX%T{B`ZGV}2&Eoqx=rAy+ zp7>lIu`F7aOAa`Mg*Oo|>V%aEAhMgWZ}yLyZW4gU^8+Kn-E4!`pBzTV@K z^I!0M^RDv;p6%JQl%Zi+dgk?-}%*NBO%! zH+wggB=32E-+MaChu%`eB1gmzA#?~H*ig%s#AxIb!Xj%+t~#|rB1ttOba#(^wc$8F z-9@j#+{l&GtX9D~toOw`#b^N0_BH4E2NYkWu|B6!=8enWe6-gJO~}!ZjC52KcYG#8 zu-PD`nJ6plzVj4y76poPrS}x|A+I@dviDZF!?q$2eIUzQQJ|I$L<0L&D|RF>_KIo{?I&`(^wC@5*7L>rFw{x z-1l=Cf6^~MCrWJg8bKa;^RnX;-4;1GJWTNTa@C2KgzFz{cIn@}3)@{$LD@tbyh})vm9E z6l8gG>5}Mn!j4nhd z^Ca3fqgkwAzZhDQ>0fa>&2B&2cJF1(CLY{y z;oV=Tp9H?!r;SuFMx{jIWMwYADXGTcB1RrYlw@GK?K)-A$l!n)9PHV_4()kbyYYNA zzC9%LNo_8_N25m=RTBlFB65jrRFV=-fnH`OSXCy+P<3h9r5MqV7l81pxDLs;t{iP& zzd`5TXCKFtG|WuRFtIWdcj&;ij3sDuRRHGn9g-MMR|-llg3NkY4F&;tlp~1>W&g3L z(Fxj}Fh1KFp41|AF^RcE{@JBLnxDK5<@e#N)+4^aUZ(ds zJ=F1@u6Zl<|AO*P>ld~XWh}BRHR49AWVEoIVWk+3n5Vol6&iB|xlC>R9w&F5%VT|yHTK5uk>qCuL*W+U4S$_H zha)0P{>aBQ<#6fNaB<9$x9zrNdAs4|XEWoh`@#c^mMUl~Dd~bK9-l~%fC{yVJ$_LR zG_4;%i6Wz`kV(fBRyqLvCM)RDAhC{0G6oIlf+tc2FQBi(;e@(R>b#u&79k4o|B6xx zU={ryHMgbVVQ(ccx@oMO5p-kSSXt$N zMyW`t4iO|^DSolhl66U*E5RKlfeJ+LEbgYEF!pkdaSCLW(n3sp7lW@e>gvuT5x06U zpk1pXz=MJXEg_8?I7yN)`EAPi%o};;sa*H0!npN5POzNiC5HDJ(p0eOL0fewZQ7WU1 zjZuhz;4GmyRfTY%Z1-N$R*G0PTFYw^^rKgB_+Ii5V)tOa6|hC*MoBso!Z6**Y8U2Q zS~_G@G~gYYlDvkh(Yka(qbn__4Bq|Q&Ag>55VK7i<7Y=;xsy7a&4$>D-PMS{aK-c` z{FSJ~DC2BviOCd{@);IS&-uVIFPF>fh8_@ccCn-c>+`(Le~1muZz&!3-5U9Cv}|I( z>K8KV;rog*s*Dw@ByEst7g#1Z;iI^{YAPe=bVWXqUv4In^Bslo{2y>xhqFZRjk@IZ(2KPUfMB?s^1$NI z*32BrxE72N0=0(TVhB*DCzcZAh}LPvp7nj^D6+o z5O)%h3%DT>`D~weBXcqwsGF z9D2|U{Ei`6^j~DNe#_WxS-pW{vpBp09c5!+oV!hu$r{EwCG#3GQi5R96C^RPGM zPKQi4fDhbO83KK7wb30Pr`;LnR)-FTX?l~O#%tVzbmKSUC@oK5vX;ap5%7Z*303uZ zshQS;AgUVYvSg>-6rim#W0t>=s_X7nKp=zp*!)sx~ff;l8;4hO>;4+NPQ& zE1o=O7DZqLvsPwF8>0U!SrnqP*W}hdHUCQHU6z{La(yC$6$+}$zeep?Q<^#2jPy&E zh7E!=RbaD%H5jz-tsVKz4*CvH5`|DQWCB$H^*ebG4~mTFnzT4WFS2G#D4AQ1AX2~v zR5gyagwqE;wi&#Fj5BKI&S**;D6M5B1%3*Z;sM5`FAJCd_gM|MM%$%|3XEctoU0oY zf8Ng$%X$;f`D-!uYW=lAt>|sITyc5h047w)-fntDIk!ZzxvGJM-TXsGFxp;FNVJ;V zv6?MbnFSZ)q_1e%22HcPM>?Rx%X-p^@rdf@R}G{`EnUq}5k(n{_LkSlMvo35QYxZ@ z)OwFxazliTypIpDb$^q@{oX>H%|2ylOoSlpYt-7Vv|kQ}`GvsJ?A^o}s|4WNCBZpB zGdX3h`L<2oasbXNzA6YfK= zx0~N9!v8KD%a(k&D&P%H&D_j+AWt7l9wv{2yoeE49cHjyE+O?lcw$pmMddCqfN4`0 z50iSodM=Ww#+8ulqJHJb`C~}*UuFc=S1^nTD41{6Ik4cBTHT{G5(p-)(d-?~TeV6g zfy_Q@X6BfATw>+EzqK{J$7Vs?nX=h#&O9y3m0@Zoss)!f3@^=pMqh8INLba$Zq&>a z+Le|aKMe1~9`n9Z%J+V}P11YbHV}u?>9tt3%VdgF+#(HotYaxW25O+dLOYrldr5_k z1P0w+o#YUK{|K9an7nZlXFML@``5{A31Ygo}U->m5CH+nE1IRW*EOa zq6YS-;%2VIa8D@D*ez(HBv&X+149#dxqLh(0zU=kvbI4S2v={%uyuwd zPq%9aOkzt2X5PcFVC%M}QBjQw$H)^tXsW_xREQ7p@3Ox(Y~SCJ3-*q$sP^c1Ue03l z9(J9$?vnaVToae|VKDc=dx6hm*9~`lIe#4LQhs}wdk>%wppO?c*Cb+$7~CT3%2D~V zgafFBnqetC5XsfZG_ch=QRt0?=myjCjRZ;8wyqSFm5pzV@kc6^fLo^sb%>N)ldR;m z2Yc1Xb?FtY*k!mSOW?Hh2sIdpuh>hAUH65nHHm3Z(XV^8*e;UbaJQ?g)#=V24Rchf z)p-ibQ{u44OOnM(uG_h_tf?OYEwdhf}q`H|N@3jh;PM1K(~j1Zh4 zsNC*a-%Vz9s$VZM3J?eTi|j&snjV$0D!rcfJKV2w>rj9Oh)1L=RXzdRWDXPyPSbD_9wV#e zC#;=2Ifo@WMW|_%PqAGpYR}MgUIb%FBI_-qWR3oojo$j;SAj9|v9J-z0tTd$s7 z3N_TbEWFB_*%>A83?%SlZocAyZp+KoL&*?SZ4EBE7Vb!2cIZCPHv;O!o#P2UD$8jqnX?5>)vXj-w z1%p{nBXzJPqY2Qaj96N9)eL2$$=lzV)TXFVw9;9VxL|{u+khrXQz2-s^4L@7-2lV? z9(m&Li*4H>B9yeey5z!%JqWbVm261tf1gtIfTm;HLNzDH`oNoKmW}4`XK^J6Hx1}8 z&8mK`ocG28TCPGhsH$G{$i~DKD<<1er`~<-tNYN%UC+5xC18w%AUuQa)EhrMW5gne zr}*BrUR+@b%=3uJ1ij=doz*>wj2v+o!XXu#6_#_7hCw*~hd_?CrE}#zk-K`$ZK+oA z>YUt;6#|$aDIsH~Nkugp9f=itSdm({&+(8`v8sdIs+Ej^DS6m&qSP-hR1i3PMC@If z_frw!@6)2q^|(uxoIE$|rkywjJxPlk5;c*c(QWjxlvPEcTEv0K0?YEwjvmjhc`s%I zVv)15G0hB43SKI^_bF>YVD3fD*f`{!^BT# zeGOKQ;a7PdCV8I)`?Evzslq!FQh3+x#!Fzdx(B?K>`i zOO-5~hkChnEl+v-tHL{?xrb#ymxBxtGrMX##H z!&3pt$1BpP-+5*nZR`RQY&lSZ%`%?sBe>#FitPLSVd zk3nG;b?@mtg#Z1$TBp#BW9N>gnMwu9!2~Jh;&#`Jeu#Mm>i-BS8`k98SE*DJWbGh3 zZyuF@ziO|}doD>49H+08^4@*N=KdyzCw%G?pVB}2S(am}hBbj)CDd7uz)L89#jR zeKSY)$%EHjq)AX=%jOO2*f>HfvY%?IfBU+@RcoU--dwV_6(@1Nv9#D*Yc+meDCFl0 zrP97yy>XU8{{4UZ0eRWWU#hp={eUi38)E!WkB(0>JlIEHPel(M88@qj2l2#cwbo>G zY>Krb!+Oq{JNUP+eV;3?yN#iNUI1(otKt$&*B8T&c&PnOBmT&DKs=m9sJa{yw74aN zbHHev3iCDWvDsyZ{xT2ikY1I^1?M#NGO_5CftuGm3;U!q`NuDPTW|dN9Wtn>DT(e{ zfSql0eMbBf&@#wP$L3!5+~>bI{Kf}9tdBqcG0abl%MI7wCE*|bxxV3#>++a2Wsc_Z zyx_IpVU^uG|1t50m}}+Uv(XdlK6qdM!`mjZ)}1>IZ++-eeWn3nrVWHp|^k4 zZr_*>OddJ(7uC7(a&>N8x1RSr@It-z&Yf)8vgyor%9+(_okG#chaia&<8M;qohvr} z_Suhn(jOliJ^b(Y-*eZ^ul~JPbH-_>@#!ypi}h<)BgWH*4jsezK8YgDDAC#ZCGkC9 zqSSK7eS3J(vqw1R%pJVxFF!6j_Z`u*PdkzEhaO=1$b*Eq5YR~K%SiC(0fMf0hl-HO2a*{-jy zwZA2>I=it%=+_|{Y zQ{wQ^N#63FkL&+_{ZpK>eY5<-XD(#Jx>cmsS_R1$O2x2HJ~^t+ZRiOrNuyD}2Vi<) z3{E>uhX)5_cA?7A@oDk=Kn@?9(&WfExm+LzM<+CvGzx%b=fB80sW#G6T`h^a&%fG6CeCk`GpZXS``^3BTbI*HnZ-)=Rx#<9|F zweXEdyB#~2W_vVb!DCnU!9tiXZZLtK+0#E%IAQBaUmc&TzjWuG0|S$Di?VzF5f+!K zZnnp*0MRo?y9*XYFLj6CIKZzoC;`YJ_c#|~&5x1>`eDSC{AA*=dIcnZCtM^P+O%{H@9(gYYD8itd$Dy0z9vM^s~IhN=`RoGMr zJf+o!#USa=1wN~pp-l&^SZOe`moboQ^0|H=gO}L}7 zNxy%1mWx_7w)GaOLzTSA`6k5(Ruhs|i?pW?RPZ0R6Ds-$Tkoy_l^7(dB5^a5P%f8M z(im_42n?Kn4NJc@xWpKec9V8(-mM$&`2F&^=H`=jyz`;^?kKif&20znxmLqcKbs%> zRE7q6nVVZ=d~%l6Bg6P!*K??diV5@2i9Pe2zIEGw_@Wa}d&LcR?_2eaOKx~kZ%dF8v&@maUBJH)x{Cl{3riSo${;hqPT7d!#p`(Dnh zT87^Fe%fiG-Yr)M|MOEqFQIkfGZ^Sur+@qR|Ce2R?v+zde!T<0<@YQ|7#-Mz{x;xJ#RPd;3)4v4A`J6RS_J`Z5vFIxsn=?o z0h)_57=cPpAAj+;|DsPh|19~dxBVfPU2_X*B6z;n^_g4eIz;iFve=A1?gyOF+gr&% z)Ty`FwaCDQZKyX|jE+xGYqX@dQlfXL56=^lG!diAn`VR}+&(t@m8%X(~nt4E6J@Q@88DQy(i~+7`X} zK{X+e11)AT^8|S4fat^_G~dSj-s^RJ&j3Be5;N6Bc;~BJ80ZKR{O_wVFJ7;+KOUvH zYBQJq^rv9*YP^CB_T@SCw3B3RVTpSm+^;rG8CtVep7Gcnx^V9e9DV@qomgzu5@;tW zNoqOm#EqOi*w4~p4FKP3FD+vy8{9N3l7{79@Wx99s>Z`Wm7xMP0g^`ebB9R>HvLAD zON_zG}oNzxR{N*m2?s zeDED_l&fyKlcie2mb`-VNsFipQCm_NiWB`CZ8!0Z+iTh?SWR8Z0)mK$0jjkIL;ZdH z-V2}3fB*0@z2>I7WbNub1kvYt8O> z`m9ie3LuIXHxW%;*LA`f*v!WHRkTbq$rb$ulFO|IvR1K4i+FwW-PdJ2b#H; zOjd;>E9$CY7&10F!?sQ9X>xe2i;vzJtd-_H-Kc8jDj$N#Zs!(4Y5ye7IqE zKK0ZS|LKY=zVB?8awUUDw$w#%URIlB-KaXG1)?8(#T)d?mtQMeGdsqzoY{~`Iy#@v z15vdtM42ngcR(U=s6=p1vjn$7B#MP*6VIky&pb7h)gz*t61}Ic+al>u*a`4DF8`iItndv#b@18r@dH2n1t^`+{`ce^S>l@mp)rPPwT{hd7xng(D3uDBT#1E;?vv47w>jY;k`#RN5D;mr2?y_m!w2B- z;cSy31)H$KFM})FhFgyO*mESv+O+7vB$_$5JJ}8qu0(bdbPOPhAjsrjjCQ@Unq8DF zUU$_DJtT=Sjz3CMfnI?(zm-exh4+H~)wg~?@5n0gYBkB_^Lp*gcXG!a_p<4P6J+bw zZQ5?N^umApgc&=wkBz;h%btGbws(xpEfFWKZ>+WWVc>$4V!8#ye!2L0R3Q3g$Fk(| z=MIrPlH^jw6iTSV(!RTitFw51fOuJg_;K!g2y-C|bJJ88=NK3q99vqfKGbS8HmYiF zReyyqe*K4BfBnt;>)*Xw|LNbq$>rDH!m6Qx%nhVYTGXHrt%rcpN!4 z&Z}SW4BqhS7wUI^auo-TOiFL5sO@HrR?~5a1Y}z!|DNrGrO)kLYJbvewcyARxbX`3 z@2eo+74;nV)0r<*DDPZgcBvtgpx3~03q!Nl24D*KeG%UIC$HetC!VJEhP%artH+z& zvYY83U=t^xy;VcnyQ(aRy5iAIpiddE2mI_w&*b?peU5HE?{RX)rB`aTT9sU>q?k~j z_>5=EPrq{^_uhM()M^c}&Bdv~&>MUDX&YWzDF$gHPP4M<&TSbK^BB*=rpvCT-;L+} z5swv;Ywj?~!10nxf*f&mp60|s;<*V-5F(~axPReR7EKd|SFOhLLvn@EMT)uT!MksL zu+?myGPd(pO@~jBfRybU*OJeLI=b($tXnezC^mJwX{y&vfe{Bb6qg^TfcQq#cs?`x zkFevUEqc}|+j!=y-bPPfpAHW8$$>kr*4p%_`ea!MD#wg<+Y6ao0F{EP!`cTFk9%*Vx)3hI;wlF1|Pd<-AsmLe( z{d*5+Pq|D! zmlvBxEKVFi>CzZdY$dT45wUiDfyEo|fQc%->j&Iw&!dY@J%zGd;<8gsuchezF?r_8 zAImd`ORl7j%Sm!epG^~OHa9|7!_lYteT37 zh+0+STwSjB<0K(!wYl(fALiZ%_wl-S{2jTRPoZ2=k^mzjR-ILULM8w1;bQLMhP8yD zXBsmLT=mflDXs0}*?;{^*f~w}`fEvmB3bksQZmaA4~ z=0G>ZV>%Af>;tP=waLJx^!6EKwT0cdn`hO{1t7IPb(=$SJ#y6*SIFJF?$Efl1bzT< zivo})N$&n#hrOZxO6uqGR0euo_QJ{rTDKZqf1&xmeEC*K*SXV27PAQdE6p=;=J;ef((=*(2?}I$%F*}H=^D=qhzRon2 z6d2CuzMAvBB`cP|U|YDXg}Lb^uKe09oRlxQ~>-(cMQ`<#a9vRYSMDW0!h$ z-;-|LcIo^M$8Ug^WItK_(QO-uTVA+nYmrtn(TneEF|N)KXt?v`bG_JAf_!8FuD$7c zrmIaxCnoid+isMnH)nW3fQn-84-pS4xzY3d~H;k_$shJ>|L| z1XY0Q%rR;+<9foAUM1Jva+lPqHC7J~X)c#{o_MRMpr*SklO^qpZYHr>QK*%Cj)SAe z7#p9Wx4%b%Vwu^|hnPORTfEF^nF5J18w7SHNs3js`AJ=1KDOL?*HPv+u9eD1F3@zg zA?ef-Y3M^3;M;7M09D2FI~$OzN>-QOg7H0fhRq6)7*Q1wPf^5ec4<2PTtyQj7*UZZ z(&~ue&aoQb8jCm)2dKJp((79*?xG*mg#W((F4=ScT?%j-@PEqi2Lo7&$YKg#Y+{X= z!(pMI#d4ljJ7)6e1o?8A;f-ssae~Q(1bzLPVPub+Aiq`>nhae3etcv)LqH zEFSlrJfa48gd;mGR|pC70YNTsDLQUtDXJ>@o&nu@(y99Gi?89||MR=HQ8H2P+zUpGlMHG_FyuU6nsKBTyy9)_J9Lx{Ylbw)6%cokrXYQ};74mJrB4~p zKh(AddLo6(F?=Hj%DS zHFR8TigC#6&OptKT2;!aDoNt5EriNkRe%5g{GE(1HhC&;p@!SE)k(tn2K48UV|IBo z&-an;}0q^mKA^(n$f$$`!aZrx@v!HdSnrQZAKHKV+fSAeRd;evauwyO}+@x7*;n z=RMF{$lp|t+#a?C-$1tLCC75*y*P?xDOzChuASQc{5QF(9up;r_+s4pRb4K^V8Jc= zLqtWh!4elR6t^TVGhs1~q@ikw6d4xL^L+FsE9`8ivVXoRUTU@Dlpk=?O4`7C7NNf& zd!%T-_qMWq<;Fx$+B+}gtxBN|S5%RoL}F)D%?X~_SqQy)1Hq_4+HGY0S{PZ2joJj=_Nv394TB;HRK7~SY(irbX zfTZ2z&^_17+EbsPLnoXp-~GweOixZTe)JeYE+?9)pw8@4bv0i^ggA<+R4O{MW;KB* zJ>?>bFnZsO(yYxbFM{yAF!4h0+L%O1)R4ou1a8dMB5uJAY648pF3H5#f2vmj4bW^U zpd>7VFvYnDI^K4>g0L@#`MZ??yRPFZ$jQKCf$xL-JFp4Z?!MOrK@Nb#nakp84c}~F`v*x!)**3} z#Q_8nmoQ+A=W46{YR~&GKYkl<8DnTRng z$cy&FNXU`XMuIp zZ{cqEO~7T0A&Mf}trni=Jz6&P%b)GIEBQi^T){295)sPf-d(5c*s*^9zI{J!wOZ%b zX2v91f0E{kJy<+f^ft_89!kfTT=nFxrHIld$d?EUW&AKN66BdZ@Bm4c{ZY>4ZreOO z_~mArk|r@mQ&LZ8i7?Vry0F-6jxRK#zW}%!v;?dJN`e-F9(BOVT#G9<%2=*ZnjOlr zD?tIV;-s3VT?+Dd4sz znQVIB-RK7wy`ew%^L4o*qxcw2F@be;G}o*{eIIGHmO*vgz+_>-6)*As){iAZy|#p= z3BK%YZpM0q>Z?cz8+jW`Pa~sw1J(4DgM(sAn^IeNN zxwGA3+kh&mTEZYFeh|>A*T5JWa}!;$mZ~+8C<)nyWT_>zO01SuJ*A(mE(AZjy)p9^ z3x!4CX566X6uB2yWIszm?o{XQ_QpzA!XrdxCH)2GZU>Bli$DLj!hV*wnA-;&RVOKP zNOfCZ{*#FaB|n%g2f||Aj#M#AZ8WHOB7b~B*NSLh$X zctWGuKC9Je4;|R^z!krv#}OEBX7h#(uUV=sO)b`z?)QyHGl@B&uaAwr6_<~V8F%+6T;s=;w5p0Sdw>^cjAXbcc zh0v>-I8M_bK!*nDE((S2|GA!C^cOj@wMdXdiaq?|W<&qaB$ld@v|D(d!87J}y zRjaD1))fg=OJ7g#H5)dp|M{UKhnSh2BQ_~Lg&Z3O`!JqI=zC85c(z>%!-oRjJ2ud4 z-)KN{PS^;7HLg=kwi>#-+nswS)Rh(zNpTysb~ZW?XJDuzs%oVpm945~;#YC+25!!q zmf~&o&1W{`%+0CsNL2BAD;UpUtq<6=WaC9+e5~i9*0xxH-hP(^@Cyc(T1yhg__^F8 fjH5p6e1QKC(`pD2&r6G-00000NkvXXu0mjfM&(~V literal 0 HcmV?d00001 diff --git a/src/css/notification/NotificationCenterView.css b/src/css/notification/NotificationCenterView.css index af7ba41..45e3e4b 100644 --- a/src/css/notification/NotificationCenterView.css +++ b/src/css/notification/NotificationCenterView.css @@ -59,8 +59,8 @@ .alertView_nitro-coolui-logo { width: 150px; - height: 78px; + height: 73px; position: relative; - background-image: url("@/assets/images/notifications/coolui.png"); + background-image: url("@/assets/images/notifications/nitro_v3.png"); background-repeat: no-repeat; } \ No newline at end of file From 32a16d37aadd62eb927dc9cb5b83b8f104dd8ece Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Thu, 19 Mar 2026 14:27:33 +0100 Subject: [PATCH 26/33] feat(wired-ui): add advanced condition editors --- public/UITexts.example | 76 ++++++- src/api/wired/WiredConditionLayoutCode.ts | 11 + .../WiredConditionHasAltitudeView.tsx | 185 +++++++++++++++++ .../conditions/WiredConditionLayoutView.tsx | 28 +++ .../WiredConditionMatchDateView.tsx | 195 ++++++++++++++++++ .../WiredConditionMatchTimeView.tsx | 163 +++++++++++++++ .../WiredConditionTeamHasRankView.tsx | 102 +++++++++ .../WiredConditionTeamHasScoreView.tsx | 158 ++++++++++++++ .../WiredConditionTriggererMatchView.tsx | 130 ++++++++++++ .../WiredConditionUserPerformsActionView.tsx | 151 ++++++++++++++ 10 files changed, 1191 insertions(+), 8 deletions(-) create mode 100644 src/components/wired/views/conditions/WiredConditionHasAltitudeView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionMatchDateView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionMatchTimeView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionTeamHasRankView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionTeamHasScoreView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx create mode 100644 src/components/wired/views/conditions/WiredConditionUserPerformsActionView.tsx diff --git a/public/UITexts.example b/public/UITexts.example index f4c6168..980458f 100644 --- a/public/UITexts.example +++ b/public/UITexts.example @@ -13,12 +13,72 @@ "widget.settings.interface.fps.warning": "Het zetten van FPS naar unlimited kan prestatie problemen veroorzaken!", "widget.settings.interface.secondary": "Verander de window header kleur", "widget.settings.interface.reset": "Reset header kleur naar default", - "widget.room.chat.hide_pets": "Verberg dieren", - "widget.room.chat.hide_avatars": "Verberg avatars", - "widget.room.chat.hide_balloon": "Verberg Spreekballon", - "widget.room.chat.show_balloon": "Spreekballon", - "widget.room.chat.clear_history": "leeg geschiedenis", - "widget.room.youtube.shared": "YouTube word gedeeld", - "widget.room.youtube.open_video": "Open de video", - "wiredfurni.params.area_selection.selected": "Geselecteerd gebied: Lengte=%x%, Breedte=%y%, breedte=%w%, hoogte=%h%" + "widget.room.chat.hide_pets": "Verberg dieren", + "widget.room.chat.hide_avatars": "Verberg avatars", + "widget.room.chat.hide_balloon": "Verberg Spreekballon", + "widget.room.chat.show_balloon": "Spreekballon", + "widget.room.chat.clear_history": "leeg geschiedenis", + "widget.room.youtube.shared": "YouTube word gedeeld", + "widget.room.youtube.open_video": "Open de video", + "wiredfurni.params.area_selection.selected": "Geselecteerd gebied: Lengte=%x%, Breedte=%y%, breedte=%w%, hoogte=%h%", + "wiredfurni.params.sources.collapse": "Nascondi le impostazioni avanzate", + "wiredfurni.params.sources.expand": "Mostra le impostazioni avanzate", + "wiredfurni.params.quantifier_selection": "Abbina condizione se:", + "wiredfurni.params.quantifier.users.0": "Tutti gli utenti corrispondono", + "wiredfurni.params.quantifier.users.1": "Uno qualsiasi degli utenti corrisponde", + "wiredfurni.params.quantifier.users.neg.0": "Uno qualsiasi degli utenti non corrisponde", + "wiredfurni.params.quantifier.users.neg.1": "Nessuno degli utenti corrisponde", + "wiredfurni.params.quantifier.furni.0": "Tutti i Furni corrispondono", + "wiredfurni.params.quantifier.furni.1": "Uno qualsiasi dei Furni corrisponde", + "wiredfurni.params.usertype.1": "Habbo", + "wiredfurni.params.usertype.2": "Cucciolo", + "wiredfurni.params.usertype.4": "Bot", + "wiredfurni.params.sources.users.title.match.0": "Gli utenti da abbinare:", + "wiredfurni.params.sources.users.title.match.1": "Utenti da comparare con:", + "wiredfurni.params.sources.users.101": "Usa l'utente specificato dal nome", + "wiredfurni.params.comparison.0": "Più basso di", + "wiredfurni.params.comparison.1": "È uguale a", + "wiredfurni.params.comparison.2": "Più alto di", + "wiredfurni.params.team": "Scegli una squadra", + "wiredfurni.params.team.1": "Rossa", + "wiredfurni.params.team.2": "Verde", + "wiredfurni.params.team.3": "Blu", + "wiredfurni.params.team.4": "Gialla", + "wiredfurni.params.team.triggerer": "Squadra dell'innescatore", + "wiredfurni.params.comparison_selection": "Scegli tipo:", + "wiredfurni.params.setscore2": "La squadra deve segnare:", + "wiredfurni.params.placement_selection": "Posizione:", + "wiredfurni.params.placement.1": "1.", + "wiredfurni.params.placement.2": "2.", + "wiredfurni.params.placement.3": "3.", + "wiredfurni.params.placement.4": "4.", + "wiredfurni.params.time.hour_selection": "Ore:", + "wiredfurni.params.time.minute_selection": "Minuti:", + "wiredfurni.params.time.second_selection": "Secondi:", + "wiredfurni.params.time.skip": "Non usare il filtro", + "wiredfurni.params.time.exact": "Esatto", + "wiredfurni.params.time.range": "Range", + "wiredfurni.params.time.weekday_selection": "Giorno della settimana:", + "wiredfurni.params.time.weekday.1": "Lunedì", + "wiredfurni.params.time.weekday.2": "Martedì", + "wiredfurni.params.time.weekday.3": "Mercoledì", + "wiredfurni.params.time.weekday.4": "Giovedì", + "wiredfurni.params.time.weekday.5": "Venerdì", + "wiredfurni.params.time.weekday.6": "Sabato", + "wiredfurni.params.time.weekday.7": "Domenica", + "wiredfurni.params.time.day_selection": "Giorno:", + "wiredfurni.params.time.month_selection": "Mese:", + "wiredfurni.params.time.month.10": "Ott.", + "wiredfurni.params.time.month.11": "Nov.", + "wiredfurni.params.time.month.12": "Dic.", + "wiredfurni.params.time.month.1": "Gen.", + "wiredfurni.params.time.month.2": "Feb.", + "wiredfurni.params.time.month.3": "Mar.", + "wiredfurni.params.time.month.4": "Apr.", + "wiredfurni.params.time.month.5": "Mag.", + "wiredfurni.params.time.month.6": "Giu.", + "wiredfurni.params.time.month.7": "Lug.", + "wiredfurni.params.time.month.8": "Ago.", + "wiredfurni.params.time.month.9": "Set.", + "wiredfurni.params.time.year_selection": "Anno:" } diff --git a/src/api/wired/WiredConditionLayoutCode.ts b/src/api/wired/WiredConditionLayoutCode.ts index 58cae5d..b344f8b 100644 --- a/src/api/wired/WiredConditionLayoutCode.ts +++ b/src/api/wired/WiredConditionLayoutCode.ts @@ -26,4 +26,15 @@ export class WiredConditionlayout public static NOT_ACTOR_WEARING_EFFECT: number = 23; public static DATE_RANGE_ACTIVE: number = 24; public static ACTOR_HAS_HANDITEM: number = 25; + public static COUNTER_TIME_MATCHES: number = 27; + public static USER_PERFORMS_ACTION: number = 28; + public static HAS_ALTITUDE: number = 29; + public static NOT_USER_PERFORMS_ACTION: number = 30; + public static NOT_ACTOR_HAS_HANDITEM: number = 31; + public static TRIGGERER_MATCH: number = 32; + public static NOT_TRIGGERER_MATCH: number = 33; + public static TEAM_HAS_SCORE: number = 34; + public static TEAM_HAS_RANK: number = 35; + public static MATCH_TIME: number = 36; + public static MATCH_DATE: number = 37; } diff --git a/src/components/wired/views/conditions/WiredConditionHasAltitudeView.tsx b/src/components/wired/views/conditions/WiredConditionHasAltitudeView.tsx new file mode 100644 index 0000000..91a6a82 --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionHasAltitudeView.tsx @@ -0,0 +1,185 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const COUNTER_INTERACTION_TYPES = [ 'game_upcounter' ]; +const MIN_ALTITUDE = 0; +const MAX_ALTITUDE = 40; +const ALTITUDE_STEP = 0.01; +const ALTITUDE_PATTERN = /^\d*(\.\d{0,2})?$/; + +const clampAltitude = (value: number) => +{ + if(isNaN(value)) return MIN_ALTITUDE; + + const clamped = Math.min(MAX_ALTITUDE, Math.max(MIN_ALTITUDE, value)); + + return parseFloat(clamped.toFixed(2)); +}; + +const formatAltitude = (value: number) => +{ + const normalized = clampAltitude(value); + const text = normalized.toFixed(2); + + return text.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1'); +}; + +const parseAltitude = (value: string) => +{ + if(!value || !value.trim().length) return 0; + + const parsed = parseFloat(value); + + if(isNaN(parsed)) return 0; + + return clampAltitude(parsed); +}; + +export const WiredConditionHasAltitudeView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null, setAllowedInteractionTypes = null, setAllowedInteractionErrorKey = null } = useWired(); + const [ comparison, setComparison ] = useState(1); + const [ furniSource, setFurniSource ] = useState(() => + { + if(trigger?.intData?.length > 1) return trigger.intData[1]; + return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0; + }); + const [ quantifier, setQuantifier ] = useState(0); + const [ showAdvanced, setShowAdvanced ] = useState(false); + const [ altitude, setAltitude ] = useState(0); + const [ altitudeInput, setAltitudeInput ] = useState('0'); + + useEffect(() => + { + setAllowedInteractionTypes(COUNTER_INTERACTION_TYPES); + setAllowedInteractionErrorKey('wiredfurni.error.require_counter_furni'); + + return () => + { + setAllowedInteractionTypes(null); + setAllowedInteractionErrorKey(null); + }; + }, [ setAllowedInteractionErrorKey, setAllowedInteractionTypes ]); + + useEffect(() => + { + if(!trigger) return; + + setComparison((trigger.intData.length > 0) ? trigger.intData[0] : 1); + setFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0)); + setQuantifier((trigger.intData.length > 2) ? trigger.intData[2] : 0); + setShowAdvanced((trigger.intData.length > 1) ? (trigger.intData[1] !== 0 || trigger.intData[2] !== 0) : false); + + const nextAltitude = parseAltitude(trigger.stringData); + setAltitude(nextAltitude); + setAltitudeInput(formatAltitude(nextAltitude)); + }, [ trigger ]); + + const updateAltitude = (value: number) => + { + const nextValue = clampAltitude(value); + + setAltitude(nextValue); + setAltitudeInput(formatAltitude(nextValue)); + }; + + const updateAltitudeInput = (value: string) => + { + if(!ALTITUDE_PATTERN.test(value)) return; + + setAltitudeInput(value); + + if(!value.length) + { + setAltitude(0); + return; + } + + const parsedValue = parseFloat(value); + + if(isNaN(parsedValue)) return; + + if(parsedValue > MAX_ALTITUDE) + { + updateAltitude(MAX_ALTITUDE); + return; + } + + setAltitude(clampAltitude(parsedValue)); + }; + + const save = () => + { + setIntParams([ + comparison, + furniSource, + quantifier + ]); + setStringParam(formatAltitude(altitude)); + }; + + return ( + + + { showAdvanced && + <> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( +
+ setQuantifier(value) } /> + { LocalizeText(`wiredfurni.params.quantifier.furni.${ value }`) } +
+ ); + }) } +
+ + } +
+ }> +
+ { [ 0, 1, 2 ].map(value => + { + return ( +
+ setComparison(value) } /> + { LocalizeText(`wiredfurni.params.comparison.${ value }`) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.setaltitude') } + setAltitudeInput(formatAltitude(altitude)) } + onChange={ event => updateAltitudeInput(event.target.value) } /> +
+
+ updateAltitude(event as number) } /> + { formatAltitude(altitude) } +
+ + ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx index a1a88c2..888df16 100644 --- a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx +++ b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx @@ -5,7 +5,11 @@ import { WiredConditionActorIsOnFurniView } from './WiredConditionActorIsOnFurni import { WiredConditionActorIsTeamMemberView } from './WiredConditionActorIsTeamMemberView'; import { WiredConditionActorIsWearingBadgeView } from './WiredConditionActorIsWearingBadgeView'; import { WiredConditionActorIsWearingEffectView } from './WiredConditionActorIsWearingEffectView'; +import { WiredConditionCounterTimeMatchesView } from './WiredConditionCounterTimeMatchesView'; import { WiredConditionDateRangeView } from './WiredConditionDateRangeView'; +import { WiredConditionMatchDateView } from './WiredConditionMatchDateView'; +import { WiredConditionMatchTimeView } from './WiredConditionMatchTimeView'; +import { WiredConditionHasAltitudeView } from './WiredConditionHasAltitudeView'; import { WiredConditionFurniHasAvatarOnView } from './WiredConditionFurniHasAvatarOnView'; import { WiredConditionFurniHasFurniOnView } from './WiredConditionFurniHasFurniOnView'; import { WiredConditionFurniHasNotFurniOnView } from './WiredConditionFurniHasNotFurniOnView'; @@ -13,6 +17,10 @@ import { WiredConditionFurniIsOfTypeView } from './WiredConditionFurniIsOfTypeVi import { WiredConditionFurniMatchesSnapshotView } from './WiredConditionFurniMatchesSnapshotView'; import { WiredConditionTimeElapsedLessView } from './WiredConditionTimeElapsedLessView'; import { WiredConditionTimeElapsedMoreView } from './WiredConditionTimeElapsedMoreView'; +import { WiredConditionTeamHasRankView } from './WiredConditionTeamHasRankView'; +import { WiredConditionTeamHasScoreView } from './WiredConditionTeamHasScoreView'; +import { WiredConditionTriggererMatchView } from './WiredConditionTriggererMatchView'; +import { WiredConditionUserPerformsActionView } from './WiredConditionUserPerformsActionView'; import { WiredConditionUserCountInRoomView } from './WiredConditionUserCountInRoomView'; export const WiredConditionLayoutView = (code: number) => @@ -20,7 +28,11 @@ export const WiredConditionLayoutView = (code: number) => switch(code) { case WiredConditionlayout.ACTOR_HAS_HANDITEM: + case WiredConditionlayout.NOT_ACTOR_HAS_HANDITEM: return ; + case WiredConditionlayout.TRIGGERER_MATCH: + case WiredConditionlayout.NOT_TRIGGERER_MATCH: + return ; case WiredConditionlayout.ACTOR_IS_GROUP_MEMBER: case WiredConditionlayout.NOT_ACTOR_IN_GROUP: return ; @@ -38,6 +50,10 @@ export const WiredConditionLayoutView = (code: number) => return ; case WiredConditionlayout.DATE_RANGE_ACTIVE: return ; + case WiredConditionlayout.MATCH_TIME: + return ; + case WiredConditionlayout.MATCH_DATE: + return ; case WiredConditionlayout.FURNIS_HAVE_AVATARS: case WiredConditionlayout.FURNI_NOT_HAVE_HABBO: return ; @@ -58,6 +74,18 @@ export const WiredConditionLayoutView = (code: number) => case WiredConditionlayout.USER_COUNT_IN: case WiredConditionlayout.NOT_USER_COUNT_IN: return ; + case WiredConditionlayout.COUNTER_TIME_MATCHES: + return ; + case WiredConditionlayout.USER_PERFORMS_ACTION: + return ; + case WiredConditionlayout.NOT_USER_PERFORMS_ACTION: + return ; + case WiredConditionlayout.HAS_ALTITUDE: + return ; + case WiredConditionlayout.TEAM_HAS_SCORE: + return ; + case WiredConditionlayout.TEAM_HAS_RANK: + return ; } return null; diff --git a/src/components/wired/views/conditions/WiredConditionMatchDateView.tsx b/src/components/wired/views/conditions/WiredConditionMatchDateView.tsx new file mode 100644 index 0000000..f1b9ccc --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionMatchDateView.tsx @@ -0,0 +1,195 @@ +import { ChangeEvent, FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const MODE_SKIP = 0; +const MODE_EXACT = 1; +const MODE_RANGE = 2; +const MODE_OPTIONS = [ MODE_SKIP, MODE_EXACT, MODE_RANGE ]; +const WEEKDAY_OPTIONS = [ 1, 2, 3, 4, 5, 6, 7 ]; +const MONTH_OPTIONS = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ]; + +const createMask = (values: number[]) => values.reduce((mask, value) => (mask | (1 << value)), 0); +const ALL_WEEKDAYS_MASK = createMask(WEEKDAY_OPTIONS); +const ALL_MONTHS_MASK = createMask(MONTH_OPTIONS); + +const clampValue = (value: number, min: number, max: number) => +{ + if(isNaN(value)) return min; + + return Math.max(min, Math.min(max, Math.floor(value))); +}; + +const parseInputValue = (event: ChangeEvent, min: number, max: number) => +{ + return clampValue(parseInt(event.target.value || min.toString(), 10), min, max); +}; + +const toggleMaskValue = (mask: number, value: number, enabled: boolean) => +{ + if(enabled) return (mask | (1 << value)); + + return (mask & ~(1 << value)); +}; + +const InlineNumberInput: FC<{ value: number; min: number; max: number; onChange: (value: number) => void }> = props => +{ + const { value = 0, min = 0, max = 0, onChange = null } = props; + + return ( + onChange(parseInputValue(event, min, max)) } /> + ); +}; + +interface MatchDateSectionProps +{ + sectionId: string; + titleKey: string; + mode: number; + fromValue: number; + toValue: number; + min: number; + max: number; + onModeChange: (value: number) => void; + onFromChange: (value: number) => void; + onToChange: (value: number) => void; +} + +const MatchDateSection: FC = props => +{ + const { sectionId = '', titleKey = '', mode = MODE_SKIP, fromValue = 0, toValue = 0, min = 0, max = 0, onModeChange = null, onFromChange = null, onToChange = null } = props; + + return ( +
+ { LocalizeText(titleKey) } +
+ onModeChange(MODE_SKIP) } /> + { LocalizeText('wiredfurni.params.time.skip') } +
+
+ onModeChange(MODE_EXACT) } /> + { LocalizeText('wiredfurni.params.time.exact') } + +
+
+ onModeChange(MODE_RANGE) } /> + { LocalizeText('wiredfurni.params.time.range') } + + - + +
+
+ ); +}; + +export const WiredConditionMatchDateView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + const currentYear = useMemo(() => new Date().getFullYear(), []); + const [ weekdayMask, setWeekdayMask ] = useState(ALL_WEEKDAYS_MASK); + const [ dayMode, setDayMode ] = useState(MODE_SKIP); + const [ dayFrom, setDayFrom ] = useState(1); + const [ dayTo, setDayTo ] = useState(31); + const [ monthMask, setMonthMask ] = useState(ALL_MONTHS_MASK); + const [ yearMode, setYearMode ] = useState(MODE_SKIP); + const [ yearFrom, setYearFrom ] = useState(currentYear); + const [ yearTo, setYearTo ] = useState(currentYear); + + useEffect(() => + { + if(!trigger) return; + + setWeekdayMask((trigger.intData[0] && (trigger.intData[0] > 0)) ? trigger.intData[0] : ALL_WEEKDAYS_MASK); + setDayMode(MODE_OPTIONS.includes(trigger.intData[1]) ? trigger.intData[1] : MODE_SKIP); + setDayFrom(clampValue(trigger.intData[2] ?? 1, 1, 31)); + setDayTo(clampValue(trigger.intData[3] ?? 31, 1, 31)); + setMonthMask((trigger.intData[4] && (trigger.intData[4] > 0)) ? trigger.intData[4] : ALL_MONTHS_MASK); + setYearMode(MODE_OPTIONS.includes(trigger.intData[5]) ? trigger.intData[5] : MODE_SKIP); + setYearFrom(clampValue(trigger.intData[6] ?? currentYear, 1, 9999)); + setYearTo(clampValue(trigger.intData[7] ?? currentYear, 1, 9999)); + }, [ currentYear, trigger ]); + + const save = () => + { + setIntParams([ + weekdayMask || ALL_WEEKDAYS_MASK, + dayMode, + clampValue(dayFrom, 1, 31), + clampValue(dayTo, 1, 31), + monthMask || ALL_MONTHS_MASK, + yearMode, + clampValue(yearFrom, 1, 9999), + clampValue(yearTo, 1, 9999) + ]); + }; + + return ( + +
+
+ { LocalizeText('wiredfurni.params.time.weekday_selection') } +
+ { WEEKDAY_OPTIONS.map(value => + { + const checked = ((weekdayMask & (1 << value)) !== 0); + + return ( + + ); + }) } +
+
+ setDayFrom(clampValue(value, 1, 31)) } + onModeChange={ setDayMode } + onToChange={ value => setDayTo(clampValue(value, 1, 31)) } /> +
+ { LocalizeText('wiredfurni.params.time.month_selection') } +
+ { MONTH_OPTIONS.map(value => + { + const checked = ((monthMask & (1 << value)) !== 0); + + return ( + + ); + }) } +
+
+ setYearFrom(clampValue(value, 1, 9999)) } + onModeChange={ setYearMode } + onToChange={ value => setYearTo(clampValue(value, 1, 9999)) } /> +
+
+ ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionMatchTimeView.tsx b/src/components/wired/views/conditions/WiredConditionMatchTimeView.tsx new file mode 100644 index 0000000..426ab99 --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionMatchTimeView.tsx @@ -0,0 +1,163 @@ +import { ChangeEvent, FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const MODE_SKIP = 0; +const MODE_EXACT = 1; +const MODE_RANGE = 2; +const MODE_OPTIONS = [ MODE_SKIP, MODE_EXACT, MODE_RANGE ]; + +const clampValue = (value: number, min: number, max: number) => +{ + if(isNaN(value)) return min; + + return Math.max(min, Math.min(max, Math.floor(value))); +}; + +interface TimeFilterSectionProps +{ + sectionId: string; + titleKey: string; + min: number; + max: number; + mode: number; + fromValue: number; + toValue: number; + onModeChange: (value: number) => void; + onFromChange: (value: number) => void; + onToChange: (value: number) => void; +} + +const parseInputValue = (event: ChangeEvent, min: number, max: number) => +{ + return clampValue(parseInt(event.target.value || min.toString(), 10), min, max); +}; + +const InlineNumberInput: FC<{ value: number; min: number; max: number; onChange: (value: number) => void }> = props => +{ + const { value = 0, min = 0, max = 0, onChange = null } = props; + + return ( + onChange(parseInputValue(event, min, max)) } /> + ); +}; + +const TimeFilterSection: FC = props => +{ + const { sectionId = '', titleKey = '', min = 0, max = 0, mode = MODE_SKIP, fromValue = 0, toValue = 0, onModeChange = null, onFromChange = null, onToChange = null } = props; + + return ( +
+ { LocalizeText(titleKey) } +
+ onModeChange(MODE_SKIP) } /> + { LocalizeText('wiredfurni.params.time.skip') } +
+
+ onModeChange(MODE_EXACT) } /> + { LocalizeText('wiredfurni.params.time.exact') } + +
+
+ onModeChange(MODE_RANGE) } /> + { LocalizeText('wiredfurni.params.time.range') } + + - + +
+
+ ); +}; + +export const WiredConditionMatchTimeView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + const [ hourMode, setHourMode ] = useState(MODE_SKIP); + const [ hourFrom, setHourFrom ] = useState(0); + const [ hourTo, setHourTo ] = useState(0); + const [ minuteMode, setMinuteMode ] = useState(MODE_SKIP); + const [ minuteFrom, setMinuteFrom ] = useState(0); + const [ minuteTo, setMinuteTo ] = useState(0); + const [ secondMode, setSecondMode ] = useState(MODE_SKIP); + const [ secondFrom, setSecondFrom ] = useState(0); + const [ secondTo, setSecondTo ] = useState(0); + + useEffect(() => + { + if(!trigger) return; + + setHourMode(MODE_OPTIONS.includes(trigger.intData[0]) ? trigger.intData[0] : MODE_SKIP); + setHourFrom(clampValue(trigger.intData[1] ?? 0, 0, 23)); + setHourTo(clampValue(trigger.intData[2] ?? 0, 0, 23)); + setMinuteMode(MODE_OPTIONS.includes(trigger.intData[3]) ? trigger.intData[3] : MODE_SKIP); + setMinuteFrom(clampValue(trigger.intData[4] ?? 0, 0, 59)); + setMinuteTo(clampValue(trigger.intData[5] ?? 0, 0, 59)); + setSecondMode(MODE_OPTIONS.includes(trigger.intData[6]) ? trigger.intData[6] : MODE_SKIP); + setSecondFrom(clampValue(trigger.intData[7] ?? 0, 0, 59)); + setSecondTo(clampValue(trigger.intData[8] ?? 0, 0, 59)); + }, [ trigger ]); + + const save = () => + { + setIntParams([ + hourMode, + clampValue(hourFrom, 0, 23), + clampValue(hourTo, 0, 23), + minuteMode, + clampValue(minuteFrom, 0, 59), + clampValue(minuteTo, 0, 59), + secondMode, + clampValue(secondFrom, 0, 59), + clampValue(secondTo, 0, 59) + ]); + }; + + return ( + +
+ setHourFrom(clampValue(value, 0, 23)) } + onModeChange={ setHourMode } + onToChange={ value => setHourTo(clampValue(value, 0, 23)) } /> + setMinuteFrom(clampValue(value, 0, 59)) } + onModeChange={ setMinuteMode } + onToChange={ value => setMinuteTo(clampValue(value, 0, 59)) } /> + setSecondFrom(clampValue(value, 0, 59)) } + onModeChange={ setSecondMode } + onToChange={ value => setSecondTo(clampValue(value, 0, 59)) } /> +
+
+ ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionTeamHasRankView.tsx b/src/components/wired/views/conditions/WiredConditionTeamHasRankView.tsx new file mode 100644 index 0000000..1df5347 --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionTeamHasRankView.tsx @@ -0,0 +1,102 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const TEAM_OPTIONS = [ 0, 1, 2, 3, 4 ]; +const PLACEMENT_OPTIONS = [ 1, 2, 3, 4 ]; + +export const WiredConditionTeamHasRankView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + const [ team, setTeam ] = useState(1); + const [ placement, setPlacement ] = useState(1); + const [ userSource, setUserSource ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); + const [ showAdvanced, setShowAdvanced ] = useState(false); + + useEffect(() => + { + if(!trigger) return; + + const nextTeam = (trigger.intData.length > 0) ? trigger.intData[0] : 1; + const nextPlacement = (trigger.intData.length > 1) ? trigger.intData[1] : 1; + const nextUserSource = (trigger.intData.length > 2) ? trigger.intData[2] : 0; + const nextQuantifier = (trigger.intData.length > 3) ? trigger.intData[3] : 0; + + setTeam(TEAM_OPTIONS.includes(nextTeam) ? nextTeam : 1); + setPlacement(PLACEMENT_OPTIONS.includes(nextPlacement) ? nextPlacement : 1); + setUserSource(nextUserSource); + setQuantifier((nextQuantifier === 1) ? 1 : 0); + setShowAdvanced(nextUserSource !== 0 || nextQuantifier !== 0); + }, [ trigger ]); + + const save = () => + { + setIntParams([ + team, + placement, + userSource, + quantifier + ]); + }; + + return ( + + + { showAdvanced && + <> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( +
+ setQuantifier(value) } /> + { LocalizeText(`wiredfurni.params.quantifier.users.${ value }`) } +
+ ); + }) } +
+ + } +
+ }> +
+ { LocalizeText('wiredfurni.params.team') } + { TEAM_OPTIONS.map(value => + { + const labelKey = (value === 0) ? 'wiredfurni.params.team.triggerer' : `wiredfurni.params.team.${ value }`; + + return ( +
+ setTeam(value) } /> + { LocalizeText(labelKey) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.placement_selection') } + { PLACEMENT_OPTIONS.map(value => + { + return ( +
+ setPlacement(value) } /> + { LocalizeText(`wiredfurni.params.placement.${ value }`) } +
+ ); + }) } +
+ + ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionTeamHasScoreView.tsx b/src/components/wired/views/conditions/WiredConditionTeamHasScoreView.tsx new file mode 100644 index 0000000..a2317aa --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionTeamHasScoreView.tsx @@ -0,0 +1,158 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Slider, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const TEAM_OPTIONS = [ 1, 2, 3, 4 ]; +const COMPARISON_OPTIONS = [ 0, 1, 2 ]; +const MIN_SCORE = 0; +const MAX_SCORE = 999; +const SCORE_PATTERN = /^\d*$/; + +const clampScore = (value: number) => +{ + if(isNaN(value)) return MIN_SCORE; + + return Math.max(MIN_SCORE, Math.min(MAX_SCORE, Math.floor(value))); +}; + +export const WiredConditionTeamHasScoreView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null } = useWired(); + const [ team, setTeam ] = useState(1); + const [ comparison, setComparison ] = useState(1); + const [ score, setScore ] = useState(0); + const [ scoreInput, setScoreInput ] = useState('0'); + const [ userSource, setUserSource ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); + const [ showAdvanced, setShowAdvanced ] = useState(false); + + useEffect(() => + { + if(!trigger) return; + + const nextTeam = (trigger.intData.length > 0) ? trigger.intData[0] : 1; + const nextComparison = (trigger.intData.length > 1) ? trigger.intData[1] : 1; + const nextScore = clampScore((trigger.intData.length > 2) ? trigger.intData[2] : 0); + const nextUserSource = (trigger.intData.length > 3) ? trigger.intData[3] : 0; + const nextQuantifier = (trigger.intData.length > 4) ? trigger.intData[4] : 0; + + setTeam(TEAM_OPTIONS.includes(nextTeam) ? nextTeam : 1); + setComparison(COMPARISON_OPTIONS.includes(nextComparison) ? nextComparison : 1); + setScore(nextScore); + setScoreInput(nextScore.toString()); + setUserSource(nextUserSource); + setQuantifier((nextQuantifier === 1) ? 1 : 0); + setShowAdvanced(nextUserSource !== 0 || nextQuantifier !== 0); + }, [ trigger ]); + + const updateScore = (value: number) => + { + const nextValue = clampScore(value); + + setScore(nextValue); + setScoreInput(nextValue.toString()); + }; + + const updateScoreInput = (value: string) => + { + if(!SCORE_PATTERN.test(value)) return; + + setScoreInput(value); + + if(!value.length) + { + setScore(0); + return; + } + + updateScore(parseInt(value)); + }; + + const save = () => + { + setIntParams([ + team, + comparison, + clampScore(score), + userSource, + quantifier + ]); + }; + + return ( + + + { showAdvanced && + <> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( +
+ setQuantifier(value) } /> + { LocalizeText(`wiredfurni.params.quantifier.users.${ value }`) } +
+ ); + }) } +
+ + } +
+ }> +
+ { LocalizeText('wiredfurni.params.team') } + { TEAM_OPTIONS.map(value => + { + return ( +
+ setTeam(value) } /> + { LocalizeText(`wiredfurni.params.team.${ value }`) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.comparison_selection') } + { COMPARISON_OPTIONS.map(value => + { + return ( +
+ setComparison(value) } /> + { LocalizeText(`wiredfurni.params.comparison.${ value }`) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.setscore2') } + setScoreInput(clampScore(score).toString()) } + onChange={ event => updateScoreInput(event.target.value) } /> +
+
+ updateScore(event as number) } /> + { score } +
+ + ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx b/src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx new file mode 100644 index 0000000..d66403b --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx @@ -0,0 +1,130 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredSourceOption, WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const ENTITY_HABBO = 1; +const ENTITY_PET = 2; +const ENTITY_BOT = 4; +const AVATAR_MODE_ANY = 0; +const AVATAR_MODE_CERTAIN = 1; +const SOURCE_SPECIFIED_USERNAME = 101; + +const MATCH_USER_SOURCES: WiredSourceOption[] = [ + { value: 0, label: 'wiredfurni.params.sources.users.0' }, + { value: 200, label: 'wiredfurni.params.sources.users.200' }, + { value: 201, label: 'wiredfurni.params.sources.users.201' } +]; + +const COMPARE_USER_SOURCES: WiredSourceOption[] = [ + ...MATCH_USER_SOURCES, + { value: SOURCE_SPECIFIED_USERNAME, label: 'wiredfurni.params.sources.users.101' } +]; + +export const WiredConditionTriggererMatchView: FC<{}> = () => +{ + const [ entityType, setEntityType ] = useState(ENTITY_HABBO); + const [ avatarMode, setAvatarMode ] = useState(AVATAR_MODE_ANY); + const [ username, setUsername ] = useState(''); + const [ matchUserSource, setMatchUserSource ] = useState(0); + const [ compareUserSource, setCompareUserSource ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); + const [ showAdvanced, setShowAdvanced ] = useState(false); + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + + const needsUsername = (avatarMode === AVATAR_MODE_CERTAIN) || (compareUserSource === SOURCE_SPECIFIED_USERNAME); + + const save = () => + { + setIntParams([ + entityType, + avatarMode, + matchUserSource, + compareUserSource, + quantifier + ]); + setStringParam(username); + }; + + useEffect(() => + { + if(!trigger) return; + + setEntityType((trigger.intData.length > 0) ? trigger.intData[0] : ENTITY_HABBO); + setAvatarMode((trigger.intData.length > 1) ? trigger.intData[1] : AVATAR_MODE_ANY); + setMatchUserSource((trigger.intData.length > 2) ? trigger.intData[2] : 0); + setCompareUserSource((trigger.intData.length > 3) ? trigger.intData[3] : 0); + setQuantifier((trigger.intData.length > 4) ? trigger.intData[4] : 0); + setUsername(trigger.stringData || ''); + setShowAdvanced((trigger.intData.length > 2) ? (trigger.intData[2] !== 0 || trigger.intData[3] !== 0 || trigger.intData[4] !== 0) : false); + }, [ trigger ]); + + return ( + + + { showAdvanced && + <> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( +
+ setQuantifier(value) } /> + { LocalizeText(`wiredfurni.params.quantifier.users.${ value }`) } +
+ ); + }) } +
+ + + } +
+ }> +
+ { [ ENTITY_HABBO, ENTITY_PET, ENTITY_BOT ].map(value => + { + return ( +
+ setEntityType(value) } /> + { LocalizeText(`wiredfurni.params.usertype.${ value }`) } +
+ ); + }) } +
+
+ { LocalizeText('wiredfurni.params.picktriggerer') } +
+ setAvatarMode(AVATAR_MODE_ANY) } /> + { LocalizeText('wiredfurni.params.anyavatar') } +
+
+ setAvatarMode(AVATAR_MODE_CERTAIN) } /> + { LocalizeText('wiredfurni.params.certainavatar') } +
+ { needsUsername && + setUsername(event.target.value) } /> } +
+ + ); +}; diff --git a/src/components/wired/views/conditions/WiredConditionUserPerformsActionView.tsx b/src/components/wired/views/conditions/WiredConditionUserPerformsActionView.tsx new file mode 100644 index 0000000..a26c859 --- /dev/null +++ b/src/components/wired/views/conditions/WiredConditionUserPerformsActionView.tsx @@ -0,0 +1,151 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourceOption, WiredSourcesSelector } from '../WiredSourcesSelector'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const ACTION_WAVE = 1; +const ACTION_BLOW_KISS = 2; +const ACTION_LAUGH = 3; +const ACTION_AWAKE = 4; +const ACTION_RELAX = 5; +const ACTION_SIT = 6; +const ACTION_STAND = 7; +const ACTION_LAY = 8; +const ACTION_SIGN = 9; +const ACTION_DANCE = 10; +const ACTION_THUMB_UP = 11; + +const ACTION_OPTIONS = [ + { value: ACTION_WAVE, label: 'widget.memenu.wave' }, + { value: ACTION_BLOW_KISS, label: 'widget.memenu.blow' }, + { value: ACTION_LAUGH, label: 'widget.memenu.laugh' }, + { value: ACTION_THUMB_UP, label: 'widget.memenu.thumb' }, + { value: ACTION_AWAKE, label: 'wiredfurni.params.action.4' }, + { value: ACTION_RELAX, label: 'avatar.widget.random_walk' }, + { value: ACTION_SIT, label: 'widget.memenu.sit' }, + { value: ACTION_STAND, label: 'widget.memenu.stand' }, + { value: ACTION_LAY, label: 'wiredfurni.params.action.8' }, + { value: ACTION_SIGN, label: 'widget.memenu.sign' }, + { value: ACTION_DANCE, label: 'widget.memenu.dance' } +]; + +const SIGN_OPTIONS = Array.from({ length: 18 }, (_, value) => ({ + value, + label: `wiredfurni.params.action.sign.${ value }` +})); + +const DANCE_OPTIONS = [ + { value: 1, label: 'widget.memenu.dance1' }, + { value: 2, label: 'widget.memenu.dance2' }, + { value: 3, label: 'widget.memenu.dance3' }, + { value: 4, label: 'widget.memenu.dance4' } +]; + +const USER_ACTION_SOURCES: WiredSourceOption[] = [ + { value: 0, label: 'wiredfurni.params.sources.users.0' }, + { value: 200, label: 'wiredfurni.params.sources.users.200' }, + { value: 201, label: 'wiredfurni.params.sources.users.201' } +]; + +interface WiredConditionUserPerformsActionViewProps +{ + negative?: boolean; +} + +export const WiredConditionUserPerformsActionView: FC = props => +{ + const { negative = false } = props; + const [ selectedAction, setSelectedAction ] = useState(ACTION_WAVE); + const [ signFilterEnabled, setSignFilterEnabled ] = useState(false); + const [ signId, setSignId ] = useState(0); + const [ danceFilterEnabled, setDanceFilterEnabled ] = useState(false); + const [ danceId, setDanceId ] = useState(1); + const [ userSource, setUserSource ] = useState(0); + const [ quantifier, setQuantifier ] = useState(0); + const [ showAdvanced, setShowAdvanced ] = useState(false); + const { trigger = null, setIntParams = null } = useWired(); + const quantifierKeyPrefix = negative ? 'wiredfurni.params.quantifier.users.neg' : 'wiredfurni.params.quantifier.users'; + + const save = () => setIntParams([ + selectedAction, + signFilterEnabled ? 1 : 0, + signId, + danceFilterEnabled ? 1 : 0, + danceId, + userSource, + quantifier + ]); + + useEffect(() => + { + setSelectedAction((trigger?.intData?.length > 0) ? trigger.intData[0] : ACTION_WAVE); + setSignFilterEnabled((trigger?.intData?.length > 1) ? (trigger.intData[1] === 1) : false); + setSignId((trigger?.intData?.length > 2) ? trigger.intData[2] : 0); + setDanceFilterEnabled((trigger?.intData?.length > 3) ? (trigger.intData[3] === 1) : false); + setDanceId((trigger?.intData?.length > 4) ? trigger.intData[4] : 1); + setUserSource((trigger?.intData?.length > 5) ? trigger.intData[5] : 0); + setQuantifier((trigger?.intData?.length > 6) ? trigger.intData[6] : 0); + setShowAdvanced((trigger?.intData?.length > 5) ? (trigger.intData[5] !== 0 || trigger.intData[6] !== 0) : false); + }, [ trigger ]); + + return ( + + + { showAdvanced && + <> +
+ { LocalizeText('wiredfurni.params.quantifier_selection') } + { [ 0, 1 ].map(value => + { + return ( +
+ setQuantifier(value) } /> + { LocalizeText(`${ quantifierKeyPrefix }.${ value }`) } +
+ ); + }) } +
+ + } +
+ }> +
+ Action + +
+ { (selectedAction === ACTION_SIGN) && +
+
+ setSignFilterEnabled(event.target.checked) } /> + { LocalizeText('wiredfurni.params.sign_filter') } +
+ { signFilterEnabled && + } +
} + { (selectedAction === ACTION_DANCE) && +
+
+ setDanceFilterEnabled(event.target.checked) } /> + { LocalizeText('wiredfurni.params.dance_filter') } +
+ { danceFilterEnabled && + } +
} + + ); +}; From 4f2299e492354cc419c37b77baaf48e9fe33f702 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 19 Mar 2026 15:05:41 +0100 Subject: [PATCH 27/33] =?UTF-8?q?=F0=9F=86=95=20Disconnection=20handler,?= =?UTF-8?q?=20when=20you=20got=20disconnected=20you=20automatic=20go=20bac?= =?UTF-8?q?k=20to=20the=20room?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 + src/components/MainView.tsx | 4 +- src/components/reconnect/ReconnectView.tsx | 108 +++++++++++++++++++++ src/hooks/navigator/useNavigator.ts | 4 +- 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/components/reconnect/ReconnectView.tsx diff --git a/src/App.tsx b/src/App.tsx index d3566cc..c97d6fa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { GetUIVersion } from './api'; import { Base } from './common'; import { LoadingView } from './components/loading/LoadingView'; import { MainView } from './components/MainView'; +import { ReconnectView } from './components/reconnect/ReconnectView'; import { useMessageEvent } from './hooks'; NitroVersion.UI_VERSION = GetUIVersion(); @@ -93,6 +94,7 @@ export const App: FC<{}> = props => { !isReady && } { isReady && } + ); diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 3fef0cc..50ada98 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -1,4 +1,4 @@ -import { AddLinkEventTracker, GetCommunication, HabboWebTools, ILinkEventTracker, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer'; +import { AddLinkEventTracker, GetCommunication, GetRoomSessionManager, HabboWebTools, ILinkEventTracker, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer'; import { AnimatePresence, motion } from 'framer-motion'; import { FC, useEffect, useState } from 'react'; import { useNitroEvent } from '../hooks'; @@ -41,6 +41,8 @@ export const MainView: FC<{}> = props => { setIsReady(true); + GetRoomSessionManager().tryRestoreSession(); + GetCommunication().connection.ready(); }, []); diff --git a/src/components/reconnect/ReconnectView.tsx b/src/components/reconnect/ReconnectView.tsx new file mode 100644 index 0000000..5dc2acf --- /dev/null +++ b/src/components/reconnect/ReconnectView.tsx @@ -0,0 +1,108 @@ +import { NitroEventType, ReconnectEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useState } from 'react'; +import { Base, Column, Text } from '../../common'; +import { useNitroEvent } from '../../hooks'; + +export const ReconnectView: FC<{}> = props => +{ + const [ isReconnecting, setIsReconnecting ] = useState(false); + const [ attempt, setAttempt ] = useState(0); + const [ maxAttempts, setMaxAttempts ] = useState(0); + const [ hasFailed, setHasFailed ] = useState(false); + + const onReconnecting = useCallback((event: ReconnectEvent) => + { + setIsReconnecting(true); + setHasFailed(false); + setAttempt(event.attempt); + setMaxAttempts(event.maxAttempts); + }, []); + + const onReconnected = useCallback(() => + { + setIsReconnecting(false); + setHasFailed(false); + setAttempt(0); + }, []); + + const onReconnectFailed = useCallback(() => + { + setIsReconnecting(false); + setHasFailed(true); + }, []); + + useNitroEvent(NitroEventType.SOCKET_RECONNECTING, onReconnecting); + useNitroEvent(NitroEventType.SOCKET_RECONNECTED, onReconnected); + useNitroEvent(NitroEventType.SOCKET_RECONNECT_FAILED, onReconnectFailed); + + const handleReload = useCallback(() => + { + window.location.reload(); + }, []); + + const handleGoHome = useCallback(() => + { + sessionStorage.removeItem('nitro_last_room'); + sessionStorage.removeItem('nitro_last_room_password'); + window.location.reload(); + }, []); + + if(!isReconnecting && !hasFailed) return null; + + return ( + + + { isReconnecting && ( + <> + + + Connection lost + + + Reconnecting to server... (attempt { attempt }/{ maxAttempts }) + + + + + + Please wait, your session will be restored automatically + + + ) } + + { hasFailed && ( + <> + + + Connection failed + + + Unable to reconnect to the server after multiple attempts. + + + + Reload Page + + + Go to Home + + + + ) } + + + ); +}; diff --git a/src/hooks/navigator/useNavigator.ts b/src/hooks/navigator/useNavigator.ts index 90df9fc..416c0af 100644 --- a/src/hooks/navigator/useNavigator.ts +++ b/src/hooks/navigator/useNavigator.ts @@ -1,4 +1,4 @@ -import { CanCreateRoomEventEvent, CantConnectMessageParser, CreateLinkEvent, DoorbellMessageEvent, FavouriteChangedEvent, FavouritesEvent, FlatAccessDeniedMessageEvent, FlatCreatedEvent, FollowFriendMessageComposer, GenericErrorEvent, GetGuestRoomMessageComposer, GetGuestRoomResultEvent, GetSessionDataManager, GetUserEventCatsMessageComposer, GetUserFlatCatsMessageComposer, HabboWebTools, LegacyExternalInterface, NavigatorCategoryDataParser, NavigatorEventCategoryDataParser, NavigatorHomeRoomEvent, NavigatorMetadataEvent, NavigatorOpenRoomCreatorEvent, NavigatorSavedSearch, NavigatorSearchesEvent, NavigatorSearchEvent, NavigatorSearchResultSet, NavigatorTopLevelContext, RoomDataParser, RoomDoorbellAcceptedEvent, RoomEnterErrorEvent, RoomEntryInfoMessageEvent, RoomForwardEvent, RoomScoreEvent, RoomSettingsUpdatedEvent, SecurityLevel, UserEventCatsEvent, UserFlatCatsEvent, UserInfoEvent, UserPermissionsEvent } from '@nitrots/nitro-renderer'; +import { CanCreateRoomEventEvent, CantConnectMessageParser, CreateLinkEvent, DoorbellMessageEvent, FavouriteChangedEvent, FavouritesEvent, FlatAccessDeniedMessageEvent, FlatCreatedEvent, FollowFriendMessageComposer, GenericErrorEvent, GetGuestRoomMessageComposer, GetGuestRoomResultEvent, GetRoomSessionManager, GetSessionDataManager, GetUserEventCatsMessageComposer, GetUserFlatCatsMessageComposer, HabboWebTools, LegacyExternalInterface, NavigatorCategoryDataParser, NavigatorEventCategoryDataParser, NavigatorHomeRoomEvent, NavigatorMetadataEvent, NavigatorOpenRoomCreatorEvent, NavigatorSavedSearch, NavigatorSearchesEvent, NavigatorSearchEvent, NavigatorSearchResultSet, NavigatorTopLevelContext, RoomDataParser, RoomDoorbellAcceptedEvent, RoomEnterErrorEvent, RoomEntryInfoMessageEvent, RoomForwardEvent, RoomScoreEvent, RoomSettingsUpdatedEvent, SecurityLevel, UserEventCatsEvent, UserFlatCatsEvent, UserInfoEvent, UserPermissionsEvent } from '@nitrots/nitro-renderer'; import { useState } from 'react'; import { useBetween } from 'use-between'; import { CreateRoomSession, DoorStateType, GetConfigurationValue, INavigatorData, LocalizeText, NotificationAlertType, SendMessageComposer, TryVisitRoom, VisitDesktop } from '../../api'; @@ -397,6 +397,8 @@ const useNavigatorState = () => return; } + if(GetRoomSessionManager().viewerSession) return; + let forwardType = -1; let forwardId = -1; From 1e00b919e726e54c03def2aaf124b982435590a6 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Thu, 19 Mar 2026 15:22:49 +0100 Subject: [PATCH 28/33] feat(ui): add wired creator tools shell --- src/components/MainView.tsx | 2 + .../wired-tools/WiredCreatorToolsView.tsx | 176 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 src/components/wired-tools/WiredCreatorToolsView.tsx diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 50ada98..f04a072 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -27,6 +27,7 @@ import { ToolbarView } from './toolbar/ToolbarView'; import { UserProfileView } from './user-profile/UserProfileView'; import { UserSettingsView } from './user-settings/UserSettingsView'; import { WiredView } from './wired/WiredView'; +import { WiredCreatorToolsView } from './wired-tools/WiredCreatorToolsView'; import { YoutubeTvView } from './youtube-tv/YoutubeTvView'; export const MainView: FC<{}> = props => @@ -95,6 +96,7 @@ export const MainView: FC<{}> = props => + diff --git a/src/components/wired-tools/WiredCreatorToolsView.tsx b/src/components/wired-tools/WiredCreatorToolsView.tsx new file mode 100644 index 0000000..7d5d4ed --- /dev/null +++ b/src/components/wired-tools/WiredCreatorToolsView.tsx @@ -0,0 +1,176 @@ +import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../common'; + +type WiredToolsTab = 'monitor' | 'variables' | 'inspection' | 'chests' | 'settings'; + +interface MonitorStat +{ + label: string; + value: string; +} + +interface MonitorLog +{ + type: string; + category: string; + amount: string; + latest: string; +} + +const TABS: Array<{ key: WiredToolsTab; label: string; }> = [ + { key: 'monitor', label: 'Monitor' }, + { key: 'variables', label: 'Variables' }, + { key: 'inspection', label: 'Inspection' }, + { key: 'chests', label: 'Chests' }, + { key: 'settings', label: 'Settings' } +]; + +const MONITOR_STATS: MonitorStat[] = [ + { label: 'Wired usage', value: '0/10000' }, + { label: 'Is heavy', value: 'No' }, + { label: 'Floor furni', value: '0/4000' }, + { label: 'Wall furni', value: '0/4000' }, + { label: 'Permanent furni vars', value: '0/60' } +]; + +const MONITOR_LOGS: MonitorLog[] = [ + { type: 'EXECUTION_CAP', category: 'ERROR', amount: '0', latest: '/' }, + { type: 'DELAYED_EVENTS_CAP', category: 'ERROR', amount: '0', latest: '/' }, + { type: 'EXECUTOR_OVERLOAD', category: 'ERROR', amount: '0', latest: '/' }, + { type: 'MARKED_AS_HEAVY', category: 'WARNING', amount: '0', latest: '/' }, + { type: 'KILLED', category: 'ERROR', amount: '0', latest: '/' }, + { type: 'RECURSION_TIMEOUT', category: 'ERROR', amount: '0', latest: '/' } +]; + +export const WiredCreatorToolsView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ activeTab, setActiveTab ] = useState('monitor'); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': + setIsVisible(true); + return; + case 'hide': + setIsVisible(false); + return; + case 'toggle': + setIsVisible(prevValue => !prevValue); + return; + case 'tab': + if(parts.length > 2) + { + const tab = parts[2] as WiredToolsTab; + + if(TABS.some(entry => entry.key === tab)) setActiveTab(tab); + } + setIsVisible(true); + return; + } + }, + eventUrlPrefix: 'wired-tools/' + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + const currentTabLabel = useMemo(() => TABS.find(tab => tab.key === activeTab)?.label ?? 'Monitor', [ activeTab ]); + + if(!isVisible) return null; + + return ( + + setIsVisible(false) } /> + + { TABS.map(tab => ( + setActiveTab(tab.key) }> + { tab.label } + + )) } + + +
+
+ { currentTabLabel } +
+ { (activeTab === 'monitor') && +
+
+ This is the initial shell for the Wired Creator Tools. We can now build the real functionality tab by tab. +
+
+
+ Statistics: + { MONITOR_STATS.map(stat => ( +
+ { stat.label }: + { stat.value } +
+ )) } +
+
+
+ Monitor Preview +
+ Live statistics, executor health and diagnostics can be connected here next. +
+
+
+
+
+ Logs: +
+ + + + + + + + + + + { MONITOR_LOGS.map((log, index) => ( + + + + + + + )) } + +
TypeCategoryAmountLatest occurrence
{ log.type }{ log.category }{ log.amount }{ log.latest }
+
+
+ + +
+
+
} + { (activeTab !== 'monitor') && +
+
+ { currentTabLabel } +
+ This tab is now ready to be wired into the new `:wired` tools flow. +
+
+
} +
+
+
+ ); +}; From 1943337e77aba58a2059e337b396bdbff5145898 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 20 Mar 2026 07:33:33 +0100 Subject: [PATCH 29/33] =?UTF-8?q?=F0=9F=86=99=20Oepsie=20in=20the=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wired/views/conditions/WiredConditionLayoutView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx index 888df16..7783540 100644 --- a/src/components/wired/views/conditions/WiredConditionLayoutView.tsx +++ b/src/components/wired/views/conditions/WiredConditionLayoutView.tsx @@ -5,7 +5,7 @@ import { WiredConditionActorIsOnFurniView } from './WiredConditionActorIsOnFurni import { WiredConditionActorIsTeamMemberView } from './WiredConditionActorIsTeamMemberView'; import { WiredConditionActorIsWearingBadgeView } from './WiredConditionActorIsWearingBadgeView'; import { WiredConditionActorIsWearingEffectView } from './WiredConditionActorIsWearingEffectView'; -import { WiredConditionCounterTimeMatchesView } from './WiredConditionCounterTimeMatchesView'; +// import { WiredConditionCounterTimeMatchesView } from './WiredConditionCounterTimeMatchesView'; import { WiredConditionDateRangeView } from './WiredConditionDateRangeView'; import { WiredConditionMatchDateView } from './WiredConditionMatchDateView'; import { WiredConditionMatchTimeView } from './WiredConditionMatchTimeView'; From 30bea6fca59c786835e2ab9d70bd9f47e2cf37d9 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Fri, 20 Mar 2026 14:18:45 +0100 Subject: [PATCH 30/33] feat(floorplan): add real-time 3D preview to floor plan editor - Add FloorplanPreviewView with live isometric room preview - Add FloorplanHeightSelector component for height picking - Refactor FloorplanOptionsView for cleaner layout - Update FloorplanEditorContext with preview state management - Improve FloorplanCanvasView rendering --- .../FloorplanEditorContext.tsx | 14 +- .../floorplan-editor/FloorplanEditorView.tsx | 156 ++++++++- .../views/FloorplanCanvasView.tsx | 105 +++--- .../views/FloorplanHeightSelector.tsx | 54 +++ .../views/FloorplanOptionsView.tsx | 247 ++++--------- .../views/FloorplanPreviewView.tsx | 328 ++++++++++++++++++ 6 files changed, 647 insertions(+), 257 deletions(-) create mode 100644 src/components/floorplan-editor/views/FloorplanHeightSelector.tsx create mode 100644 src/components/floorplan-editor/views/FloorplanPreviewView.tsx diff --git a/src/components/floorplan-editor/FloorplanEditorContext.tsx b/src/components/floorplan-editor/FloorplanEditorContext.tsx index eb528cf..1b2a3c4 100644 --- a/src/components/floorplan-editor/FloorplanEditorContext.tsx +++ b/src/components/floorplan-editor/FloorplanEditorContext.tsx @@ -8,13 +8,25 @@ interface IFloorplanEditorContext setOriginalFloorplanSettings: Dispatch>; visualizationSettings: IVisualizationSettings; setVisualizationSettings: Dispatch>; + floorHeight: number; + setFloorHeight: Dispatch>; + floorAction: number; + setFloorAction: Dispatch>; + tilemapVersion: number; + areaInfo: { total: number; walkable: number }; } const FloorplanEditorContext = createContext({ originalFloorplanSettings: null, setOriginalFloorplanSettings: null, visualizationSettings: null, - setVisualizationSettings: null + setVisualizationSettings: null, + floorHeight: 0, + setFloorHeight: null, + floorAction: 3, + setFloorAction: null, + tilemapVersion: 0, + areaInfo: { total: 0, walkable: 0 } }); export const FloorplanEditorContextProvider: FC> = props => ; diff --git a/src/components/floorplan-editor/FloorplanEditorView.tsx b/src/components/floorplan-editor/FloorplanEditorView.tsx index 59f7709..4003d13 100644 --- a/src/components/floorplan-editor/FloorplanEditorView.tsx +++ b/src/components/floorplan-editor/FloorplanEditorView.tsx @@ -1,19 +1,22 @@ import { AddLinkEventTracker, FloorHeightMapEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { FaCaretLeft, FaCaretRight } from 'react-icons/fa'; import { LocalizeText, SendMessageComposer } from '../../api'; -import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; +import { Button, ButtonGroup, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { useMessageEvent, useNitroEvent } from '../../hooks'; import { FloorplanEditorContextProvider } from './FloorplanEditorContext'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; import { IFloorplanSettings } from '@nitrots/nitro-renderer'; import { IVisualizationSettings } from '@nitrots/nitro-renderer'; -import { convertNumbersForSaving, convertSettingToNumber } from '@nitrots/nitro-renderer'; +import { convertNumbersForSaving, convertSettingToNumber, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; import { FloorplanCanvasView } from './views/FloorplanCanvasView'; import { FloorplanImportExportView } from './views/FloorplanImportExportView'; import { FloorplanOptionsView } from './views/FloorplanOptionsView'; +import { FloorplanHeightSelector } from './views/FloorplanHeightSelector'; +import { FloorplanPreviewView } from './views/FloorplanPreviewView'; - -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; +const MIN_WALL_HEIGHT = 0; +const MAX_WALL_HEIGHT = 16; export const FloorplanEditorView: FC<{}> = props => { @@ -34,7 +37,65 @@ export const FloorplanEditorView: FC<{}> = props => thicknessWall: 1, thicknessFloor: 1 }); - const [ canvasScrollHandler, setCanvasScrollHandler ] = useState<((direction: ScrollDirection) => void) | null>(null); + const [ floorHeight, setFloorHeight ] = useState(0); + const [ floorAction, setFloorAction ] = useState(FloorAction.SET); + const [ tilemapVersion, setTilemapVersion ] = useState(0); + const [ areaInfo, setAreaInfo ] = useState({ total: 0, walkable: 0 }); + + const calculateArea = useCallback(() => + { + const tilemap = FloorplanEditor.instance.tilemap; + + if(!tilemap || tilemap.length === 0) + { + setAreaInfo({ total: 0, walkable: 0 }); + + return; + } + + let total = 0; + let walkable = 0; + + for(let y = 0; y < tilemap.length; y++) + { + if(!tilemap[y]) continue; + + for(let x = 0; x < tilemap[y].length; x++) + { + if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + total++; + + if(!tilemap[y][x].isBlocked) walkable++; + } + } + + setAreaInfo({ total, walkable }); + }, []); + + // sync floorHeight/floorAction changes to the FloorplanEditor instance + useEffect(() => + { + FloorplanEditor.instance.actionSettings.currentAction = floorAction; + FloorplanEditor.instance.actionSettings.currentHeight = floorHeight.toString(36); + }, [ floorHeight, floorAction ]); + + // register onTilemapChange callback + useEffect(() => + { + if(!isVisible) return; + + FloorplanEditor.instance.onTilemapChange = () => + { + setTilemapVersion(prev => prev + 1); + calculateArea(); + }; + + return () => + { + FloorplanEditor.instance.onTilemapChange = null; + }; + }, [ isVisible, calculateArea ]); const saveFloorChanges = () => { @@ -47,16 +108,50 @@ export const FloorplanEditorView: FC<{}> = props => convertNumbersForSaving(visualizationSettings.thicknessFloor), (visualizationSettings.wallHeight - 1) )); - } + }; const revertChanges = () => { setVisualizationSettings({ wallHeight: originalFloorplanSettings.wallHeight, thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: originalFloorplanSettings.entryPointDir }); - + FloorplanEditor.instance.doorLocation = { x: originalFloorplanSettings.entryPoint[0], y: originalFloorplanSettings.entryPoint[1] }; FloorplanEditor.instance.setTilemap(originalFloorplanSettings.tilemap, originalFloorplanSettings.reservedTiles); FloorplanEditor.instance.renderTiles(); - } + }; + + const onWallHeightChange = (value: number) => + { + if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT; + + if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT; + + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; + + newValue.wallHeight = value; + + return newValue; + }); + }; + + const increaseWallHeight = () => + { + let height = (visualizationSettings.wallHeight + 1); + + if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT; + + onWallHeightChange(height); + }; + + const decreaseWallHeight = () => + { + let height = (visualizationSettings.wallHeight - 1); + + if(height <= 0) height = MIN_WALL_HEIGHT; + + onWallHeightChange(height); + }; useNitroEvent(RoomEngineEvent.DISPOSED, event => setIsVisible(false)); @@ -117,7 +212,7 @@ export const FloorplanEditorView: FC<{}> = props => const parts = url.split('/'); if(parts.length < 2) return; - + switch(parts[1]) { case 'show': @@ -140,17 +235,42 @@ export const FloorplanEditorView: FC<{}> = props => }, []); return ( - + { isVisible && - + setIsVisible(false) } /> - - canvasScrollHandler && canvasScrollHandler(direction) } /> - + + + + + + + + + { LocalizeText('floor.editor.wall.height') } + + onWallHeightChange(event.target.valueAsNumber) } /> + + + + Area: { areaInfo.total } ({ areaInfo.walkable } caselle) + + + - + - @@ -161,4 +281,4 @@ export const FloorplanEditorView: FC<{}> = props => setImportExportVisible(false) } /> } ); -} +}; diff --git a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx index e8f39a8..9db0903 100644 --- a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx +++ b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx @@ -1,25 +1,25 @@ import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useRef, useState } from 'react'; +import { FaPlus, FaMinus } from 'react-icons/fa'; import { SendMessageComposer } from '../../../api'; import { Base, Column, ColumnProps } from '../../../common'; import { useMessageEvent } from '../../../hooks'; import { useFloorplanEditorContext } from '../FloorplanEditorContext'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; - interface FloorplanCanvasViewProps extends ColumnProps { - setScrollHandler(handler: ((direction: ScrollDirection) => void) | null): void; } export const FloorplanCanvasView: FC = props => { - const { gap = 1, children = null, setScrollHandler = null, ...rest } = props; - const [ occupiedTilesReceived , setOccupiedTilesReceived ] = useState(false); + const { gap = 1, children = null, ...rest } = props; + const [ occupiedTilesReceived, setOccupiedTilesReceived ] = useState(false); const [ entryTileReceived, setEntryTileReceived ] = useState(false); + const [ zoomLevel, setZoomLevel ] = useState(1.0); const { originalFloorplanSettings = null, setOriginalFloorplanSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); const elementRef = useRef(null); + const canvasWrapperRef = useRef(null); useMessageEvent(RoomOccupiedTilesMessageEvent, event => { @@ -37,7 +37,7 @@ export const FloorplanCanvasView: FC = props => }); setOccupiedTilesReceived(true); - + elementRef.current.scrollTo((FloorplanEditor.instance.renderer.canvas.width / 3), 0); }); @@ -63,39 +63,16 @@ export const FloorplanCanvasView: FC = props => return newValue; }); - + FloorplanEditor.instance.doorLocation = { x: parser.x, y: parser.y }; setEntryTileReceived(true); }); - const onClickArrowButton = (scrollDirection: ScrollDirection) => - { - const element = elementRef.current; - - if(!element) return; - - switch(scrollDirection) - { - case 'up': - element.scrollBy({ top: -10 }); - break; - case 'down': - element.scrollBy({ top: 10 }); - break; - case 'left': - element.scrollBy({ left: -10 }); - break; - case 'right': - element.scrollBy({ left: 10 }); - break; - } - } - const onPointerEvent = (event: PointerEvent) => { event.preventDefault(); - + switch(event.type) { case 'pointerout': @@ -109,7 +86,10 @@ export const FloorplanCanvasView: FC = props => FloorplanEditor.instance.onPointerMove(event); break; } - } + }; + + const zoomIn = () => setZoomLevel(prev => Math.min(prev + 0.25, 2.0)); + const zoomOut = () => setZoomLevel(prev => Math.max(prev - 0.25, 0.5)); useEffect(() => { @@ -124,15 +104,15 @@ export const FloorplanCanvasView: FC = props => thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: prevValue.entryPointDir - } + }; }); - } + }; }, [ originalFloorplanSettings.thicknessFloor, originalFloorplanSettings.thicknessWall, originalFloorplanSettings.wallHeight, setVisualizationSettings ]); useEffect(() => { if(!entryTileReceived || !occupiedTilesReceived) return; - + FloorplanEditor.instance.renderTiles(); }, [ entryTileReceived, occupiedTilesReceived ]); @@ -144,45 +124,56 @@ export const FloorplanCanvasView: FC = props => const currentElement = elementRef.current; if(!currentElement) return; - - currentElement.appendChild(FloorplanEditor.instance.renderer.canvas); + + const wrapper = canvasWrapperRef.current; + + if(wrapper) wrapper.appendChild(FloorplanEditor.instance.renderer.canvas); currentElement.addEventListener('pointerup', onPointerEvent); - currentElement.addEventListener('pointerout', onPointerEvent); - currentElement.addEventListener('pointerdown', onPointerEvent); - currentElement.addEventListener('pointermove', onPointerEvent); - return () => + return () => { if(currentElement) { currentElement.removeEventListener('pointerup', onPointerEvent); - currentElement.removeEventListener('pointerout', onPointerEvent); - currentElement.removeEventListener('pointerdown', onPointerEvent); - currentElement.removeEventListener('pointermove', onPointerEvent); } - } + }; }, []); - useEffect(() => - { - if(!setScrollHandler) return; - - setScrollHandler(() => onClickArrowButton); - - return () => setScrollHandler(null); - }, [ setScrollHandler ]); - return ( - - + + +
+ +
+ + +
{ children } ); -} +}; diff --git a/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx b/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx new file mode 100644 index 0000000..8163c98 --- /dev/null +++ b/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react'; +import { COLORMAP, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; +import { FloorplanEditor } from '@nitrots/nitro-renderer'; +import { Column, Text } from '../../../common'; +import { useFloorplanEditorContext } from '../FloorplanEditorContext'; + +const colormap = COLORMAP as Record; + +export const FloorplanHeightSelector: FC<{}> = () => +{ + const { floorHeight, setFloorHeight, setFloorAction } = useFloorplanEditorContext(); + + const onSelectHeight = (height: number) => + { + setFloorHeight(height); + setFloorAction(FloorAction.SET); + + FloorplanEditor.instance.actionSettings.currentAction = FloorAction.SET; + FloorplanEditor.instance.actionSettings.currentHeight = height.toString(36); + }; + + const heights: number[] = []; + + for(let i = 26; i >= 0; i--) heights.push(i); + + return ( + + { floorHeight } +
+ { heights.map(h => + { + const char = HEIGHT_SCHEME[h + 1]; + const color = colormap[char] || '101010'; + const isActive = (floorHeight === h); + + return ( +
onSelectHeight(h) } + title={ `${ h }` } + /> + ); + }) } +
+ + ); +}; diff --git a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx index 5207b15..d4e7705 100644 --- a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx +++ b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx @@ -1,45 +1,32 @@ -import { FC, useState } from 'react'; -import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaCaretLeft, FaCaretRight } from 'react-icons/fa'; +import { FC } from 'react'; import { LocalizeText } from '../../../api'; -import { Button, Column, Flex, LayoutGridItem, Slider, Text } from '../../../common'; -import { COLORMAP, FloorAction } from '@nitrots/nitro-renderer'; +import { Flex, LayoutGridItem, Text } from '../../../common'; +import { FloorAction } from '@nitrots/nitro-renderer'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; import { useFloorplanEditorContext } from '../FloorplanEditorContext'; -const MIN_WALL_HEIGHT: number = 0; -const MAX_WALL_HEIGHT: number = 16; - -const MIN_FLOOR_HEIGHT: number = 0; -const MAX_FLOOR_HEIGHT: number = 26; - -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; - interface FloorplanOptionsViewProps { - onCanvasScroll?(direction: ScrollDirection): void; } export const FloorplanOptionsView: FC = props => { - const { onCanvasScroll = () => {} } = props; - const { visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); - const [ floorAction, setFloorAction ] = useState(FloorAction.SET); - const [ floorHeight, setFloorHeight ] = useState(0); - const [ isSquareSelectMode, setSquareSelectMode ] = useState(false); - + const { visualizationSettings = null, setVisualizationSettings = null, floorAction, setFloorAction } = useFloorplanEditorContext(); + const isSquareSelectMode = FloorplanEditor.instance.isSquareSelectMode; + const selectAction = (action: number) => { setFloorAction(action); FloorplanEditor.instance.actionSettings.currentAction = action; - } + }; const toggleSquareSelectMode = () => { - const nextValue = FloorplanEditor.instance.toggleSquareSelectMode(); - - setSquareSelectMode(nextValue); - } + FloorplanEditor.instance.toggleSquareSelectMode(); + // force re-render by toggling action to same value + setFloorAction(prev => prev); + }; const changeDoorDirection = () => { @@ -58,18 +45,19 @@ export const FloorplanOptionsView: FC = props => return newValue; }); - } + }; - const onFloorHeightChange = (value: number) => + const onWallThicknessChange = (value: number) => { - if(isNaN(value) || (value <= 0)) value = 0; + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; - if(value > 26) value = 26; + newValue.thicknessWall = value; - setFloorHeight(value); - - FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36); - } + return newValue; + }); + }; const onFloorThicknessChange = (value: number) => { @@ -81,157 +69,54 @@ export const FloorplanOptionsView: FC = props => return newValue; }); - } - - const onWallThicknessChange = (value: number) => - { - setVisualizationSettings(prevValue => - { - const newValue = { ...prevValue }; - - newValue.thicknessWall = value; - - return newValue; - }); - } - - const onWallHeightChange = (value: number) => - { - if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT; - - if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT; - - setVisualizationSettings(prevValue => - { - const newValue = { ...prevValue }; - - newValue.wallHeight = value; - - return newValue; - }); - } - - const increaseWallHeight = () => - { - let height = (visualizationSettings.wallHeight + 1); - - if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT; - - onWallHeightChange(height); - } - - const decreaseWallHeight = () => - { - let height = (visualizationSettings.wallHeight - 1); - - if(height <= 0) height = MIN_WALL_HEIGHT; - - onWallHeightChange(height); - } + }; return ( - - - - { LocalizeText('floor.plan.editor.draw.mode') } - - - selectAction(FloorAction.SET) }> - - - selectAction(FloorAction.UNSET) }> - - - - - selectAction(FloorAction.UP) }> - - - selectAction(FloorAction.DOWN) }> - - - - selectAction(FloorAction.DOOR) }> - - - FloorplanEditor.instance.toggleSelectAll() }> - - - - - - - - - { LocalizeText('floor.plan.editor.enter.direction') } - - - - { LocalizeText('floor.editor.wall.height') } - - - onWallHeightChange(event.target.valueAsNumber) } /> - - - - - { LocalizeText('floor.plan.editor.room.options') } - - - - - + + + { LocalizeText('floor.plan.editor.draw.mode') } + + selectAction(FloorAction.SET) }> + + + selectAction(FloorAction.UNSET) }> + + + selectAction(FloorAction.UP) }> + + + selectAction(FloorAction.DOWN) }> + + + selectAction(FloorAction.DOOR) }> + + + FloorplanEditor.instance.toggleSelectAll() }> + + + + + + - - - { LocalizeText('floor.plan.editor.tile.height') }: { floorHeight } -
- onFloorHeightChange(event) } - renderThumb={ (props, state) => - { - const { key, style, ...rest } = (props as Record); - - return
{ state.valueNow }
; - } } /> -
-
- - - - - - -
- - - - - - + + { LocalizeText('floor.plan.editor.enter.direction') } + - + + + + + ); -} \ No newline at end of file +}; diff --git a/src/components/floorplan-editor/views/FloorplanPreviewView.tsx b/src/components/floorplan-editor/views/FloorplanPreviewView.tsx new file mode 100644 index 0000000..cd82a9c --- /dev/null +++ b/src/components/floorplan-editor/views/FloorplanPreviewView.tsx @@ -0,0 +1,328 @@ +import { FC, useEffect, useRef } from 'react'; +import { COLORMAP, HEIGHT_SCHEME, FloorplanEditor } from '@nitrots/nitro-renderer'; +import { useFloorplanEditorContext } from '../FloorplanEditorContext'; + +const colormap = COLORMAP as Record; + +const PREVIEW_TILE_W = 16; +const PREVIEW_TILE_H = 8; +const PREVIEW_BLOCK_H = 5; +const WALL_HEIGHT_PX = 40; +const WALL_COLOR = '#6B7B5E'; +const WALL_SIDE_COLOR = '#5A6A4F'; +const WALL_TOP_COLOR = '#7D8E6F'; + +function hexToRgb(hex: string): [number, number, number] +{ + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + return [ r, g, b ]; +} + +function rgbToHex(r: number, g: number, b: number): string +{ + return `#${ ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) }`; +} + +function darken(hex: string, factor: number): string +{ + const [ r, g, b ] = hexToRgb(hex); + + return rgbToHex( + Math.floor(r * factor), + Math.floor(g * factor), + Math.floor(b * factor) + ); +} + +function getTilemapBounds(tilemap: any[][]): { minX: number; minY: number; maxX: number; maxY: number } +{ + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + + for(let y = 0; y < tilemap.length; y++) + { + if(!tilemap[y]) continue; + + for(let x = 0; x < tilemap[y].length; x++) + { + if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + if(x < minX) minX = x; + if(x > maxX) maxX = x; + if(y < minY) minY = y; + if(y > maxY) maxY = y; + } + } + + if(minX === Infinity) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; + + return { minX, minY, maxX, maxY }; +} + +function renderPreview(canvas: HTMLCanvasElement, wallHeight: number): void +{ + const ctx = canvas.getContext('2d'); + const tilemap = FloorplanEditor.instance.tilemap; + + if(!ctx || !tilemap || tilemap.length === 0) + { + if(ctx) + { + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + return; + } + + const bounds = getTilemapBounds(tilemap); + const tilesW = bounds.maxX - bounds.minX + 1; + const tilesH = bounds.maxY - bounds.minY + 1; + + // find max height for offset calculation + let maxTileHeight = 0; + + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!tilemap[y] || !tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + const hi = HEIGHT_SCHEME.indexOf(tilemap[y][x].height) - 1; + + if(hi > maxTileHeight) maxTileHeight = hi; + } + } + + // calculate isometric bounds + const isoW = (tilesW + tilesH) * PREVIEW_TILE_W; + const isoH = (tilesW + tilesH) * PREVIEW_TILE_H + maxTileHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX; + + // scale to fit canvas + const scaleX = (canvas.width - 20) / isoW; + const scaleY = (canvas.height - 20) / isoH; + const scale = Math.min(scaleX, scaleY, 3); + + const offsetX = (canvas.width - isoW * scale) / 2; + const offsetY = (canvas.height - isoH * scale) / 2 + WALL_HEIGHT_PX * scale * 0.5; + + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.save(); + ctx.translate(offsetX, offsetY); + ctx.scale(scale, scale); + + const tw = PREVIEW_TILE_W; + const th = PREVIEW_TILE_H; + + function isoX(gx: number, gy: number): number + { + return (gx - bounds.minX - gy + bounds.minY) * tw + (tilesH - 1) * tw; + } + + function isoY(gx: number, gy: number): number + { + return (gx - bounds.minX + gy - bounds.minY) * th; + } + + function hasActiveTile(gx: number, gy: number): boolean + { + return tilemap[gy] && tilemap[gy][gx] && tilemap[gy][gx].height !== 'x'; + } + + function getTileHeight(gx: number, gy: number): number + { + if(!hasActiveTile(gx, gy)) return 0; + + return Math.max(0, HEIGHT_SCHEME.indexOf(tilemap[gy][gx].height) - 1); + } + + // draw walls on north and west edges + const wallH = wallHeight > 0 ? wallHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX * 0.3 : WALL_HEIGHT_PX * 0.6; + + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!hasActiveTile(x, y)) continue; + + const tileH = getTileHeight(x, y) * PREVIEW_BLOCK_H; + const cx = isoX(x, y); + const cy = isoY(x, y) - tileH; + + // west wall (no tile to the left) + if(!hasActiveTile(x - 1, y)) + { + ctx.beginPath(); + ctx.moveTo(cx, cy + th); + ctx.lineTo(cx, cy + th - wallH); + ctx.lineTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw, cy); + ctx.closePath(); + ctx.fillStyle = WALL_SIDE_COLOR; + ctx.fill(); + ctx.strokeStyle = '#4A5A3F'; + ctx.lineWidth = 0.5; + ctx.stroke(); + } + + // north wall (no tile above) + if(!hasActiveTile(x, y - 1)) + { + ctx.beginPath(); + ctx.moveTo(cx + tw, cy); + ctx.lineTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw * 2, cy + th - wallH); + ctx.lineTo(cx + tw * 2, cy + th); + ctx.closePath(); + ctx.fillStyle = WALL_COLOR; + ctx.fill(); + ctx.strokeStyle = '#4A5A3F'; + ctx.lineWidth = 0.5; + ctx.stroke(); + } + + // wall top cap - corner + if(!hasActiveTile(x - 1, y) && !hasActiveTile(x, y - 1)) + { + ctx.beginPath(); + ctx.moveTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw + tw * 0.3, cy - wallH - th * 0.3); + ctx.lineTo(cx + tw, cy - wallH - th * 0.6); + ctx.lineTo(cx + tw - tw * 0.3, cy - wallH - th * 0.3); + ctx.closePath(); + ctx.fillStyle = WALL_TOP_COLOR; + ctx.fill(); + } + } + } + + // draw tiles back-to-front + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!hasActiveTile(x, y)) continue; + + const tile = tilemap[y][x]; + const heightIndex = HEIGHT_SCHEME.indexOf(tile.height) - 1; + const tileH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; + + const cx = isoX(x, y); + const cy = isoY(x, y) - tileH; + + const heightChar = tile.height; + const baseColor = colormap[heightChar] || 'aaaaaa'; + const topColor = `#${ baseColor }`; + const leftColor = darken(baseColor, 0.65); + const rightColor = darken(baseColor, 0.80); + + // draw side faces if tile has height + const blockH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; + + // left face (visible when no neighbor to south or neighbor is shorter) + const southH = getTileHeight(x, y + 1); + const leftExpose = hasActiveTile(x, y + 1) ? Math.max(0, heightIndex - southH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; + + if(leftExpose > 0) + { + ctx.beginPath(); + ctx.moveTo(cx, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx + tw, cy + th * 2 + leftExpose); + ctx.lineTo(cx, cy + th + leftExpose); + ctx.closePath(); + ctx.fillStyle = leftColor; + ctx.fill(); + } + + // right face + const eastH = getTileHeight(x + 1, y); + const rightExpose = hasActiveTile(x + 1, y) ? Math.max(0, heightIndex - eastH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; + + if(rightExpose > 0) + { + ctx.beginPath(); + ctx.moveTo(cx + tw * 2, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx + tw, cy + th * 2 + rightExpose); + ctx.lineTo(cx + tw * 2, cy + th + rightExpose); + ctx.closePath(); + ctx.fillStyle = rightColor; + ctx.fill(); + } + + // top face + ctx.beginPath(); + ctx.moveTo(cx + tw, cy); + ctx.lineTo(cx + tw * 2, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx, cy + th); + ctx.closePath(); + ctx.fillStyle = topColor; + ctx.fill(); + + // door indicator + const door = FloorplanEditor.instance.doorLocation; + + if(door.x === x && door.y === y) + { + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.fill(); + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + } + + ctx.restore(); +} + +export const FloorplanPreviewView: FC<{}> = () => +{ + const { tilemapVersion, visualizationSettings } = useFloorplanEditorContext(); + const canvasRef = useRef(null); + const rafRef = useRef(0); + + useEffect(() => + { + if(!canvasRef.current) return; + + if(rafRef.current) cancelAnimationFrame(rafRef.current); + + rafRef.current = requestAnimationFrame(() => + { + const canvas = canvasRef.current; + + if(!canvas) return; + + const parent = canvas.parentElement; + + if(parent) + { + canvas.width = parent.clientWidth; + canvas.height = parent.clientHeight; + } + + renderPreview(canvas, visualizationSettings?.wallHeight ?? 0); + }); + + return () => + { + if(rafRef.current) cancelAnimationFrame(rafRef.current); + }; + }, [ tilemapVersion, visualizationSettings?.wallHeight ]); + + return ( +
+ +
+ ); +}; From 961457c50815d9955345fcb4b2ed4418c416b421 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 20 Mar 2026 16:06:51 +0100 Subject: [PATCH 31/33] =?UTF-8?q?=F0=9F=86=99=20Stage=201=20reconnect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/nitro/session/VisitDesktop.ts | 4 +++- src/components/reconnect/ReconnectView.tsx | 15 +++++++++++++-- src/hooks/navigator/useNavigator.ts | 20 ++++++++++++++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/api/nitro/session/VisitDesktop.ts b/src/api/nitro/session/VisitDesktop.ts index 2309f01..364b56f 100644 --- a/src/api/nitro/session/VisitDesktop.ts +++ b/src/api/nitro/session/VisitDesktop.ts @@ -1,4 +1,4 @@ -import { GetRoomSessionManager } from '@nitrots/nitro-renderer'; +import { GetRoomSessionManager, NitroLogger } from '@nitrots/nitro-renderer'; import { GetRoomSession } from './GetRoomSession'; import { GoToDesktop } from './GoToDesktop'; @@ -6,6 +6,8 @@ export const VisitDesktop = () => { if(!GetRoomSession()) return; + NitroLogger.log('[VisitDesktop] Called (isReconnecting=' + GetRoomSessionManager().isReconnecting + ')'); + GoToDesktop(); GetRoomSessionManager().removeSession(-1); }; diff --git a/src/components/reconnect/ReconnectView.tsx b/src/components/reconnect/ReconnectView.tsx index 5dc2acf..4c13094 100644 --- a/src/components/reconnect/ReconnectView.tsx +++ b/src/components/reconnect/ReconnectView.tsx @@ -20,6 +20,16 @@ export const ReconnectView: FC<{}> = props => const onReconnected = useCallback(() => { + // Socket is open but not yet re-authenticated. + // Update attempt display but keep the overlay visible until + // re-authentication completes (SOCKET_REAUTHENTICATED). + setHasFailed(false); + }, []); + + const onReauthenticated = useCallback(() => + { + // Fully re-authenticated — dismiss the overlay so the room view + // (which stayed alive behind the overlay) is visible again. setIsReconnecting(false); setHasFailed(false); setAttempt(0); @@ -33,6 +43,7 @@ export const ReconnectView: FC<{}> = props => useNitroEvent(NitroEventType.SOCKET_RECONNECTING, onReconnecting); useNitroEvent(NitroEventType.SOCKET_RECONNECTED, onReconnected); + useNitroEvent(NitroEventType.SOCKET_REAUTHENTICATED, onReauthenticated); useNitroEvent(NitroEventType.SOCKET_RECONNECT_FAILED, onReconnectFailed); const handleReload = useCallback(() => @@ -42,8 +53,8 @@ export const ReconnectView: FC<{}> = props => const handleGoHome = useCallback(() => { - sessionStorage.removeItem('nitro_last_room'); - sessionStorage.removeItem('nitro_last_room_password'); + sessionStorage.removeItem('nitro.session.lastRoomId'); + sessionStorage.removeItem('nitro.session.lastRoomPassword'); window.location.reload(); }, []); diff --git a/src/hooks/navigator/useNavigator.ts b/src/hooks/navigator/useNavigator.ts index 416c0af..a934aff 100644 --- a/src/hooks/navigator/useNavigator.ts +++ b/src/hooks/navigator/useNavigator.ts @@ -1,8 +1,8 @@ -import { CanCreateRoomEventEvent, CantConnectMessageParser, CreateLinkEvent, DoorbellMessageEvent, FavouriteChangedEvent, FavouritesEvent, FlatAccessDeniedMessageEvent, FlatCreatedEvent, FollowFriendMessageComposer, GenericErrorEvent, GetGuestRoomMessageComposer, GetGuestRoomResultEvent, GetRoomSessionManager, GetSessionDataManager, GetUserEventCatsMessageComposer, GetUserFlatCatsMessageComposer, HabboWebTools, LegacyExternalInterface, NavigatorCategoryDataParser, NavigatorEventCategoryDataParser, NavigatorHomeRoomEvent, NavigatorMetadataEvent, NavigatorOpenRoomCreatorEvent, NavigatorSavedSearch, NavigatorSearchesEvent, NavigatorSearchEvent, NavigatorSearchResultSet, NavigatorTopLevelContext, RoomDataParser, RoomDoorbellAcceptedEvent, RoomEnterErrorEvent, RoomEntryInfoMessageEvent, RoomForwardEvent, RoomScoreEvent, RoomSettingsUpdatedEvent, SecurityLevel, UserEventCatsEvent, UserFlatCatsEvent, UserInfoEvent, UserPermissionsEvent } from '@nitrots/nitro-renderer'; +import { CanCreateRoomEventEvent, CantConnectMessageParser, CreateLinkEvent, DoorbellMessageEvent, FavouriteChangedEvent, FavouritesEvent, FlatAccessDeniedMessageEvent, FlatCreatedEvent, FollowFriendMessageComposer, GenericErrorEvent, GetGuestRoomMessageComposer, GetGuestRoomResultEvent, GetRoomSessionManager, GetSessionDataManager, GetUserEventCatsMessageComposer, GetUserFlatCatsMessageComposer, HabboWebTools, LegacyExternalInterface, NavigatorCategoryDataParser, NavigatorEventCategoryDataParser, NavigatorHomeRoomEvent, NavigatorMetadataEvent, NavigatorOpenRoomCreatorEvent, NavigatorSavedSearch, NavigatorSearchesEvent, NavigatorSearchEvent, NavigatorSearchResultSet, NavigatorTopLevelContext, NitroEventType, RoomDataParser, RoomDoorbellAcceptedEvent, RoomEnterErrorEvent, RoomEntryInfoMessageEvent, RoomForwardEvent, RoomScoreEvent, RoomSettingsUpdatedEvent, SecurityLevel, UserEventCatsEvent, UserFlatCatsEvent, UserInfoEvent, UserPermissionsEvent } from '@nitrots/nitro-renderer'; import { useState } from 'react'; import { useBetween } from 'use-between'; import { CreateRoomSession, DoorStateType, GetConfigurationValue, INavigatorData, LocalizeText, NotificationAlertType, SendMessageComposer, TryVisitRoom, VisitDesktop } from '../../api'; -import { useMessageEvent } from '../events'; +import { useMessageEvent, useNitroEvent } from '../events'; import { useNotification } from '../notification'; const useNavigatorState = () => @@ -373,6 +373,15 @@ const useNavigatorState = () => CreateRoomSession(parser.roomId); }); + // When reconnection starts, reset settingsReceived so the login sequence's + // NavigatorHomeRoomEvent is treated as a fresh login. Without this, the + // prevSettingsReceived check blocks home room navigation after reconnection, + // leaving the user stuck on hotel view. + useNitroEvent(NitroEventType.SOCKET_RECONNECTING, () => + { + setNavigatorData(prevValue => ({ ...prevValue, settingsReceived: false })); + }); + useMessageEvent(NavigatorHomeRoomEvent, event => { const parser = event.getParser(); @@ -397,6 +406,8 @@ const useNavigatorState = () => return; } + // If a room session was already restored (from a network disconnect reload), + // skip the normal home room navigation to avoid overriding it. if(GetRoomSessionManager().viewerSession) return; let forwardType = -1; @@ -458,6 +469,11 @@ const useNavigatorState = () => break; } + // During reconnection, don't navigate to desktop — the reconnection guard + // will handle retrying or cleaning up. Calling VisitDesktop here would + // remove the session from the map and send the user to hotel view. + if(GetRoomSessionManager().isReconnecting) return; + VisitDesktop(); }); From 11543bb64c84485b29a26ae7cc12fbc4c5f7a377 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Fri, 20 Mar 2026 17:07:33 +0100 Subject: [PATCH 32/33] feat: custom prefix system with effects, emoji picker and per-letter colors - Catalog page for creating custom prefixes with text, per-letter colors, emoji icon and visual effects - Emoji picker via @emoji-mart/react with createPortal + Shadow DOM blur fix - Inventory prefix management (activate/deactivate/delete) - Chat bubble rendering with multi-color prefix and effect support - Prefix utilities (getPrefixEffectStyle, parsePrefixColors, PREFIX_EFFECT_KEYFRAMES) - All UI text in English --- src/api/inventory/IPrefixItem.ts | 9 + src/api/inventory/UnseenItemCategory.ts | 1 + src/api/inventory/index.ts | 1 + src/api/room/widgets/ChatBubbleMessage.ts | 4 + src/api/room/widgets/index.ts | 1 + src/api/utils/PrefixUtils.ts | 53 ++ src/api/utils/index.ts | 1 + .../layout/CatalogLayoutCustomPrefixView.tsx | 470 ++++++++++++++++++ .../views/page/layout/GetCatalogLayout.tsx | 3 + src/components/inventory/InventoryView.tsx | 10 +- .../views/prefix/InventoryPrefixView.tsx | 136 +++++ .../widgets/chat/ChatWidgetMessageView.tsx | 23 +- src/hooks/inventory/index.ts | 1 + src/hooks/inventory/useInventoryPrefixes.ts | 126 +++++ src/hooks/rooms/widgets/index.ts | 1 + src/hooks/rooms/widgets/useChatWidget.ts | 5 + 16 files changed, 841 insertions(+), 4 deletions(-) create mode 100644 src/api/inventory/IPrefixItem.ts create mode 100644 src/api/utils/PrefixUtils.ts create mode 100644 src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx create mode 100644 src/components/inventory/views/prefix/InventoryPrefixView.tsx create mode 100644 src/hooks/inventory/useInventoryPrefixes.ts diff --git a/src/api/inventory/IPrefixItem.ts b/src/api/inventory/IPrefixItem.ts new file mode 100644 index 0000000..b65981f --- /dev/null +++ b/src/api/inventory/IPrefixItem.ts @@ -0,0 +1,9 @@ +export interface IPrefixItem +{ + id: number; + text: string; + color: string; + icon: string; + effect: string; + active: boolean; +} diff --git a/src/api/inventory/UnseenItemCategory.ts b/src/api/inventory/UnseenItemCategory.ts index cbd7e9b..0ac720c 100644 --- a/src/api/inventory/UnseenItemCategory.ts +++ b/src/api/inventory/UnseenItemCategory.ts @@ -6,4 +6,5 @@ export class UnseenItemCategory public static BADGE: number = 4; public static BOT: number = 5; public static GAMES: number = 6; + public static PREFIX: number = 7; } diff --git a/src/api/inventory/index.ts b/src/api/inventory/index.ts index 6a245d7..4e6ca21 100644 --- a/src/api/inventory/index.ts +++ b/src/api/inventory/index.ts @@ -5,6 +5,7 @@ export * from './GroupItem'; export * from './IBotItem'; export * from './IFurnitureItem'; export * from './IPetItem'; +export * from './IPrefixItem'; export * from './IUnseenItemTracker'; export * from './InventoryUtilities'; export * from './PetUtilities'; diff --git a/src/api/room/widgets/ChatBubbleMessage.ts b/src/api/room/widgets/ChatBubbleMessage.ts index 3e31e38..3fc6719 100644 --- a/src/api/room/widgets/ChatBubbleMessage.ts +++ b/src/api/room/widgets/ChatBubbleMessage.ts @@ -7,6 +7,10 @@ export class ChatBubbleMessage public height: number = 0; public elementRef: HTMLDivElement = null; public skipMovement: boolean = false; + public prefixText: string = ''; + public prefixColor: string = ''; + public prefixIcon: string = ''; + public prefixEffect: string = ''; private _top: number = 0; private _left: number = 0; diff --git a/src/api/room/widgets/index.ts b/src/api/room/widgets/index.ts index 5cef378..4892937 100644 --- a/src/api/room/widgets/index.ts +++ b/src/api/room/widgets/index.ts @@ -7,6 +7,7 @@ export * from './AvatarInfoUser'; export * from './AvatarInfoUtilities'; export * from './BotSkillsEnum'; export * from './ChatBubbleMessage'; +export * from './CommandDefinition'; export * from './ChatBubbleUtilities'; export * from './ChatMessageTypeEnum'; export * from './DimmerFurnitureWidgetPresetItem'; diff --git a/src/api/utils/PrefixUtils.ts b/src/api/utils/PrefixUtils.ts new file mode 100644 index 0000000..5da5133 --- /dev/null +++ b/src/api/utils/PrefixUtils.ts @@ -0,0 +1,53 @@ +export const PRESET_PREFIX_EFFECTS: { id: string; label: string; icon: string }[] = [ + { id: '', label: 'None', icon: '—' }, + { id: 'glow', label: 'Glow', icon: '✨' }, + { id: 'shadow', label: 'Shadow', icon: '🌑' }, + { id: 'italic', label: 'Italic', icon: '𝑰' }, + { id: 'outline', label: 'Outline', icon: '🔲' }, + { id: 'pulse', label: 'Pulse', icon: '💫' }, + { id: 'bold-glow', label: 'Neon', icon: '💡' }, +]; + +export const parsePrefixColors = (text: string, colorStr: string): string[] => +{ + if(!colorStr || !text) return []; + + const colors = colorStr.split(','); + return [ ...text ].map((_, i) => colors[Math.min(i, colors.length - 1)]); +}; + +export const getPrefixEffectStyle = (effect: string, color?: string): Record => +{ + const baseColor = color || '#FFFFFF'; + + switch(effect) + { + case 'glow': + return { textShadow: `0 0 6px ${ baseColor }, 0 0 12px ${ baseColor }80` }; + case 'shadow': + return { textShadow: '2px 2px 4px rgba(0,0,0,0.7), 1px 1px 2px rgba(0,0,0,0.5)' }; + case 'italic': + return { fontStyle: 'italic' }; + case 'outline': + return { + WebkitTextStroke: '0.5px rgba(0,0,0,0.6)', + textShadow: '1px 1px 0 rgba(0,0,0,0.3), -1px -1px 0 rgba(0,0,0,0.3), 1px -1px 0 rgba(0,0,0,0.3), -1px 1px 0 rgba(0,0,0,0.3)' + }; + case 'pulse': + return { animation: 'prefix-pulse 1.5s ease-in-out infinite' }; + case 'bold-glow': + return { + textShadow: `0 0 4px ${ baseColor }, 0 0 8px ${ baseColor }, 0 0 16px ${ baseColor }60`, + fontWeight: 900 + }; + default: + return {}; + } +}; + +export const PREFIX_EFFECT_KEYFRAMES = ` +@keyframes prefix-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} +`; diff --git a/src/api/utils/index.ts b/src/api/utils/index.ts index 1824add..4a4b221 100644 --- a/src/api/utils/index.ts +++ b/src/api/utils/index.ts @@ -11,6 +11,7 @@ export * from './LocalizeFormattedNumber'; export * from './LocalizeShortNumber'; export * from './LocalizeText'; export * from './PlaySound'; +export * from './PrefixUtils'; export * from './ProductImageUtility'; export * from './Randomizer'; export * from './RoomChatFormatter'; diff --git a/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx new file mode 100644 index 0000000..0b7f904 --- /dev/null +++ b/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx @@ -0,0 +1,470 @@ +import { PurchasePrefixComposer } from '@nitrots/nitro-renderer'; +import { createPortal } from 'react-dom'; +import { FC, useEffect, useMemo, useRef, useState } from 'react'; +import { SendMessageComposer, PRESET_PREFIX_EFFECTS, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../../api'; +import { CatalogLayoutProps } from './CatalogLayout.types'; +import data from '@emoji-mart/data'; +import Picker from '@emoji-mart/react'; + +const PRESET_COLORS: string[] = [ + '#FF0000', '#FF6600', '#FFCC00', '#33CC00', '#00CCFF', + '#0066FF', '#9933FF', '#FF33CC', '#FFFFFF', '#CCCCCC', + '#999999', '#333333', '#FF9999', '#99FF99', '#9999FF', + '#FFD700', '#FF4500', '#00CED1', '#8A2BE2', '#DC143C' +]; + +export const CatalogLayoutCustomPrefixView: FC = props => +{ + const { page = null, hideNavigation = null } = props; + + useEffect(() => + { + hideNavigation(); + }, [ page, hideNavigation ]); + + const [ prefixText, setPrefixText ] = useState(''); + const [ colorMode, setColorMode ] = useState<'single' | 'perLetter'>('single'); + const [ singleColor, setSingleColor ] = useState('#FFFFFF'); + const [ letterColors, setLetterColors ] = useState>({}); + const [ selectedLetterIndex, setSelectedLetterIndex ] = useState(null); + const [ customColorInput, setCustomColorInput ] = useState('#FFFFFF'); + const [ selectedIcon, setSelectedIcon ] = useState(''); + const [ showIconPicker, setShowIconPicker ] = useState(false); + const [ selectedEffect, setSelectedEffect ] = useState(''); + const [ purchased, setPurchased ] = useState(false); + const pickerContainerRef = useRef(null); + + // Inject style into emoji-mart Shadow DOM to remove backdrop-filter blur + useEffect(() => + { + if(!showIconPicker) return; + + const timer = setTimeout(() => + { + const container = pickerContainerRef.current; + if(!container) return; + + const emPicker = container.querySelector('em-emoji-picker'); + if(!emPicker?.shadowRoot) return; + + const existing = emPicker.shadowRoot.querySelector('#no-blur-fix'); + if(existing) return; + + const style = document.createElement('style'); + style.id = 'no-blur-fix'; + style.textContent = `.sticky { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; background-color: rgb(var(--em-rgb-background)) !important; } .menu { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; background-color: rgb(var(--em-rgb-background)) !important; }`; + emPicker.shadowRoot.appendChild(style); + }, 50); + + return () => clearTimeout(timer); + }, [ showIconPicker ]); + + const colorString = useMemo(() => + { + if(colorMode === 'single') return singleColor; + + if(!prefixText.length) return singleColor; + + return [ ...prefixText ].map((_, i) => letterColors[i] || singleColor).join(','); + }, [ colorMode, singleColor, letterColors, prefixText ]); + + const previewColors = useMemo(() => + { + return parsePrefixColors(prefixText || '...', colorString || '#FFFFFF'); + }, [ prefixText, colorString ]); + + const isValid = useMemo(() => + { + if(!prefixText.trim().length || prefixText.trim().length > 15) return false; + + if(colorMode === 'single') return /^#[0-9A-Fa-f]{6}$/.test(singleColor); + + const colors = colorString.split(','); + return colors.every(c => /^#[0-9A-Fa-f]{6}$/.test(c)); + }, [ prefixText, colorMode, singleColor, colorString ]); + + const handlePurchase = () => + { + if(!isValid) return; + + SendMessageComposer(new PurchasePrefixComposer(prefixText.trim(), colorString, selectedIcon, selectedEffect)); + setPurchased(true); + setTimeout(() => setPurchased(false), 2000); + }; + + const handleColorSelect = (color: string) => + { + if(colorMode === 'single') + { + setSingleColor(color); + setCustomColorInput(color); + } + else if(selectedLetterIndex !== null) + { + setLetterColors(prev => ({ ...prev, [selectedLetterIndex]: color })); + setCustomColorInput(color); + + // Auto-advance to next letter + if(selectedLetterIndex < prefixText.length - 1) + { + const nextIdx = selectedLetterIndex + 1; + setSelectedLetterIndex(nextIdx); + setCustomColorInput(letterColors[nextIdx] || singleColor); + } + } + }; + + const handleCustomColorChange = (value: string) => + { + setCustomColorInput(value); + if(/^#[0-9A-Fa-f]{6}$/.test(value)) + { + if(colorMode === 'single') + { + setSingleColor(value); + } + else if(selectedLetterIndex !== null) + { + setLetterColors(prev => ({ ...prev, [selectedLetterIndex]: value })); + } + } + }; + + const handleTextChange = (newText: string) => + { + setPrefixText(newText); + if(selectedLetterIndex !== null && selectedLetterIndex >= newText.length) + { + setSelectedLetterIndex(newText.length > 0 ? newText.length - 1 : null); + } + }; + + const applyColorToAll = () => + { + if(!prefixText.length) return; + + const newColors: Record = {}; + [ ...prefixText ].forEach((_, i) => { newColors[i] = customColorInput; }); + setLetterColors(newColors); + }; + + const hasMultiColor = colorMode === 'perLetter' && previewColors.length > 1 && new Set(previewColors).size > 1; + + const currentActiveColor = colorMode === 'single' + ? singleColor + : (selectedLetterIndex !== null ? (letterColors[selectedLetterIndex] || singleColor) : singleColor); + + const effectStyle = getPrefixEffectStyle(selectedEffect, previewColors[0] || '#FFFFFF'); + + return ( +
+ + + { /* Header */ } + { page.localization.getImage(0) && + } + { page.localization.getText(0) && +
} + + { /* Live Preview */ } +
+
+ + { selectedIcon && { selectedIcon } } + + {'{'} + { hasMultiColor + ? [ ...(prefixText || '...') ].map((char, i) => ( + { char } + )) + : (prefixText || '...') + } + {'}'} + + + Username +
+ + { /* Text + Icon Row */ } +
+
+ +
+ handleTextChange(e.target.value) } /> + + { prefixText.length }/15 + +
+
+
+ +
+ + { selectedIcon && + + } +
+
+
+ + { /* Emoji Picker (emoji-mart) - portaled to body, no backdrop */ } + { showIconPicker && createPortal( + <> +
setShowIconPicker(false) } /> +
+ { setSelectedIcon(emoji.native); setShowIconPicker(false); } } + theme="dark" + previewPosition="none" + skinTonePosition="search" + perLine={ 8 } + maxFrequentRows={ 2 } + emojiSize={ 22 } + emojiButtonSize={ 30 } + dynamicWidth={ false } + set="native" + /> +
+ , + document.body + ) } + + { /* Effect Selector */ } +
+ +
+ { PRESET_PREFIX_EFFECTS.map(fx => ( + + )) } +
+
+ + { /* Color Mode Toggle */ } +
+ +
+ + +
+
+ + { /* Per-Letter Selector */ } + { colorMode === 'perLetter' && prefixText.length > 0 && ( +
+
+ + Select a letter, then choose a color. Auto-advances. + + +
+
+ { [ ...prefixText ].map((char, i) => + { + const charColor = letterColors[i] || singleColor; + const isSelected = selectedLetterIndex === i; + return ( +
{ setSelectedLetterIndex(i); setCustomColorInput(charColor); } }> + + { char } + +
+
+ ); + }) } +
+
+ ) } + + { /* Color Palette */ } +
+ { colorMode === 'perLetter' && selectedLetterIndex !== null && + + Selected letter: "{ prefixText[selectedLetterIndex] || '' }" + + } +
+ { PRESET_COLORS.map((color, idx) => + { + const isActive = currentActiveColor === color; + return ( +
handleColorSelect(color) } /> + ); + }) } +
+
+ + handleCustomColorChange(e.target.value) } /> +
+
+ + { /* Purchase Footer */ } +
+
+ Price: + 5 Credits +
+ +
+
+ ); +}; diff --git a/src/components/catalog/views/page/layout/GetCatalogLayout.tsx b/src/components/catalog/views/page/layout/GetCatalogLayout.tsx index a9a03ee..cae9fd4 100644 --- a/src/components/catalog/views/page/layout/GetCatalogLayout.tsx +++ b/src/components/catalog/views/page/layout/GetCatalogLayout.tsx @@ -2,6 +2,7 @@ import { ICatalogPage } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; import { CatalogLayoutBadgeDisplayView } from './CatalogLayoutBadgeDisplayView'; import { CatalogLayoutColorGroupingView } from './CatalogLayoutColorGroupingView'; +import { CatalogLayoutCustomPrefixView } from './CatalogLayoutCustomPrefixView'; import { CatalogLayoutDefaultView } from './CatalogLayoutDefaultView'; import { CatalogLayouGuildCustomFurniView } from './CatalogLayoutGuildCustomFurniView'; import { CatalogLayouGuildForumView } from './CatalogLayoutGuildForumView'; @@ -72,6 +73,8 @@ export const GetCatalogLayout = (page: ICatalogPage, hideNavigation: () => void) return ; case 'soundmachine': return ; + case 'custom_prefix': + return ; case 'bots': case 'default_3x3': default: diff --git a/src/components/inventory/InventoryView.tsx b/src/components/inventory/InventoryView.tsx index acafff6..65c3a11 100644 --- a/src/components/inventory/InventoryView.tsx +++ b/src/components/inventory/InventoryView.tsx @@ -2,7 +2,7 @@ import { AddLinkEventTracker, BadgePointLimitsEvent, GetLocalizationManager, Get import { FC, useEffect, useState } from 'react'; import { GroupItem, LocalizeText, UnseenItemCategory, isObjectMoverRequested, setObjectMoverRequested } from '../../api'; import { NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; -import { useInventoryBadges, useInventoryFurni, useInventoryTrade, useInventoryUnseenTracker, useMessageEvent, useNitroEvent } from '../../hooks'; +import { useInventoryBadges, useInventoryFurni, useInventoryPrefixes, useInventoryTrade, useInventoryUnseenTracker, useMessageEvent, useNitroEvent } from '../../hooks'; import { InventoryCategoryFilterView } from './views/InventoryCategoryFilterView'; import { InventoryBadgeView } from './views/badge/InventoryBadgeView'; import { InventoryBotView } from './views/bot/InventoryBotView'; @@ -10,13 +10,15 @@ import { InventoryFurnitureDeleteView } from './views/furniture/InventoryFurnitu import { InventoryFurnitureView } from './views/furniture/InventoryFurnitureView'; import { InventoryTradeView } from './views/furniture/InventoryTradeView'; import { InventoryPetView } from './views/pet/InventoryPetView'; +import { InventoryPrefixView } from './views/prefix/InventoryPrefixView'; const TAB_FURNITURE: string = 'inventory.furni'; const TAB_BOTS: string = 'inventory.bots'; const TAB_PETS: string = 'inventory.furni.tab.pets'; const TAB_BADGES: string = 'inventory.badges'; -const TABS = [ TAB_FURNITURE, TAB_PETS, TAB_BADGES, TAB_BOTS ]; -const UNSEEN_CATEGORIES = [ UnseenItemCategory.FURNI, UnseenItemCategory.PET, UnseenItemCategory.BADGE, UnseenItemCategory.BOT ]; +const TAB_PREFIXES: string = 'inventory.prefixes'; +const TABS = [ TAB_FURNITURE, TAB_PETS, TAB_BADGES, TAB_PREFIXES, TAB_BOTS ]; +const UNSEEN_CATEGORIES = [ UnseenItemCategory.FURNI, UnseenItemCategory.PET, UnseenItemCategory.BADGE, UnseenItemCategory.PREFIX, UnseenItemCategory.BOT ]; export const InventoryView: FC<{}> = props => { @@ -165,6 +167,8 @@ export const InventoryView: FC<{}> = props => } { (currentTab === TAB_BADGES) && } + { (currentTab === TAB_PREFIXES) && + } { (currentTab === TAB_BOTS) && }
diff --git a/src/components/inventory/views/prefix/InventoryPrefixView.tsx b/src/components/inventory/views/prefix/InventoryPrefixView.tsx new file mode 100644 index 0000000..d959546 --- /dev/null +++ b/src/components/inventory/views/prefix/InventoryPrefixView.tsx @@ -0,0 +1,136 @@ +import { FC, useEffect, useState } from 'react'; +import { FaTrashAlt } from 'react-icons/fa'; +import { IPrefixItem, LocalizeText, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api'; +import { useInventoryPrefixes, useNotification } from '../../../../hooks'; +import { NitroButton } from '../../../../layout'; + +const PrefixPreview: FC<{ text: string; color: string; icon: string; effect?: string; className?: string; textSize?: string }> = ({ text, color, icon, effect = '', className = '', textSize = 'text-sm' }) => +{ + const colors = parsePrefixColors(text, color); + const hasMultiColor = colors.length > 1 && new Set(colors).size > 1; + const fxStyle = getPrefixEffectStyle(effect, colors[0] || '#FFFFFF'); + + return ( + + { effect === 'pulse' && } + { icon && { icon } } + + {'{'} + { hasMultiColor + ? [ ...text ].map((char, i) => ( + { char } + )) + : text + } + {'}'} + + + ); +}; + +const PrefixItemView: FC<{ + prefix: IPrefixItem; + isSelected: boolean; + onClick: () => void; +}> = ({ prefix, isSelected, onClick }) => +{ + return ( +
+ +
+ ); +}; + +export const InventoryPrefixView: FC<{}> = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const { prefixes = [], activePrefix = null, selectedPrefix = null, setSelectedPrefix = null, activatePrefix = null, deactivatePrefix = null, deletePrefix = null, activate = null, deactivate = null } = useInventoryPrefixes(); + const { showConfirm = null } = useNotification(); + + const attemptDeletePrefix = () => + { + if(!selectedPrefix) return; + + showConfirm( + `Are you sure you want to delete the prefix {${selectedPrefix.text}}?`, + () => deletePrefix(selectedPrefix.id), + null, + null, + null, + LocalizeText('inventory.delete.confirm_delete.title') + ); + }; + + useEffect(() => + { + if(!isVisible) return; + + const id = activate(); + + return () => deactivate(id); + }, [ isVisible, activate, deactivate ]); + + useEffect(() => + { + setIsVisible(true); + + return () => setIsVisible(false); + }, []); + + return ( +
+
+
+ { prefixes.map(prefix => ( + setSelectedPrefix(prefix) } /> + )) } +
+ { (!prefixes || prefixes.length === 0) && +
+ { LocalizeText('inventory.empty.title') } +
} +
+
+ { activePrefix && +
+ Active prefix +
+ +
+
} + { !activePrefix && +
+ Active prefix +
+ No active prefix +
+
} + { !!selectedPrefix && +
+
+ +
+
+ selectedPrefix.active ? deactivatePrefix() : activatePrefix(selectedPrefix.id) }> + { selectedPrefix.active ? 'Deactivate' : 'Activate' } + + { !selectedPrefix.active && + + + } +
+
} +
+
+ ); +}; diff --git a/src/components/room/widgets/chat/ChatWidgetMessageView.tsx b/src/components/room/widgets/chat/ChatWidgetMessageView.tsx index 46ef8ec..1dba1a9 100644 --- a/src/components/room/widgets/chat/ChatWidgetMessageView.tsx +++ b/src/components/room/widgets/chat/ChatWidgetMessageView.tsx @@ -1,6 +1,6 @@ import { GetRoomEngine, RoomChatSettings, RoomObjectCategory } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useRef, useState } from 'react'; -import { ChatBubbleMessage } from '../../../../api'; +import { ChatBubbleMessage, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api'; import { useOnClickChat } from '../../../../hooks'; interface ChatWidgetMessageViewProps @@ -90,6 +90,27 @@ export const ChatWidgetMessageView: FC = ({ ) }
+ { chat.prefixEffect === 'pulse' && } + { chat.prefixText && (() => { + const colors = parsePrefixColors(chat.prefixText, chat.prefixColor); + const hasMultiColor = colors.length > 1 && new Set(colors).size > 1; + const fxStyle = getPrefixEffectStyle(chat.prefixEffect, colors[0] || '#FFFFFF'); + return ( + + { chat.prefixIcon && { chat.prefixIcon } } + + {'{'} + { hasMultiColor + ? [ ...chat.prefixText ].map((char, i) => ( + { char } + )) + : chat.prefixText + } + {'}'} + + + ); + })() }
diff --git a/src/hooks/inventory/index.ts b/src/hooks/inventory/index.ts index 4e70819..ea39265 100644 --- a/src/hooks/inventory/index.ts +++ b/src/hooks/inventory/index.ts @@ -2,5 +2,6 @@ export * from './useInventoryBadges'; export * from './useInventoryBots'; export * from './useInventoryFurni'; export * from './useInventoryPets'; +export * from './useInventoryPrefixes'; export * from './useInventoryTrade'; export * from './useInventoryUnseenTracker'; diff --git a/src/hooks/inventory/useInventoryPrefixes.ts b/src/hooks/inventory/useInventoryPrefixes.ts new file mode 100644 index 0000000..1d761c1 --- /dev/null +++ b/src/hooks/inventory/useInventoryPrefixes.ts @@ -0,0 +1,126 @@ +import { ActivePrefixUpdatedEvent, PrefixReceivedEvent, RequestPrefixesComposer, SetActivePrefixComposer, DeletePrefixComposer, UserPrefixesEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { IPrefixItem, SendMessageComposer, UnseenItemCategory } from '../../api'; +import { useMessageEvent } from '../events'; +import { useSharedVisibility } from '../useSharedVisibility'; +import { useInventoryUnseenTracker } from './useInventoryUnseenTracker'; + +const useInventoryPrefixesState = () => +{ + const [ needsUpdate, setNeedsUpdate ] = useState(true); + const [ prefixes, setPrefixes ] = useState([]); + const [ activePrefix, setActivePrefix ] = useState(null); + const [ selectedPrefix, setSelectedPrefix ] = useState(null); + const { isVisible = false, activate = null, deactivate = null } = useSharedVisibility(); + const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker(); + + useMessageEvent(UserPrefixesEvent, event => + { + const parser = event.getParser(); + const newPrefixes: IPrefixItem[] = parser.prefixes.map(p => ({ + id: p.id, + text: p.text, + color: p.color, + icon: p.icon || '', + effect: p.effect || '', + active: p.active + })); + + setPrefixes(newPrefixes); + + const active = newPrefixes.find(p => p.active) || null; + setActivePrefix(active); + }); + + useMessageEvent(PrefixReceivedEvent, event => + { + const parser = event.getParser(); + const newPrefix: IPrefixItem = { + id: parser.id, + text: parser.text, + color: parser.color, + icon: parser.icon || '', + effect: parser.effect || '', + active: false + }; + + setPrefixes(prevValue => [ newPrefix, ...prevValue ]); + }); + + useMessageEvent(ActivePrefixUpdatedEvent, event => + { + const parser = event.getParser(); + + setPrefixes(prevValue => + { + return prevValue.map(p => ({ + ...p, + active: p.id === parser.prefixId + })); + }); + + if(parser.prefixId === 0) + { + setActivePrefix(null); + } + else + { + setActivePrefix(prev => + { + const found = prefixes.find(p => p.id === parser.prefixId); + if(found) return { ...found, active: true }; + return { id: parser.prefixId, text: parser.text, color: parser.color, icon: parser.icon || '', effect: parser.effect || '', active: true }; + }); + } + }); + + const activatePrefix = (prefixId: number) => + { + SendMessageComposer(new SetActivePrefixComposer(prefixId)); + }; + + const deactivatePrefix = () => + { + SendMessageComposer(new SetActivePrefixComposer(0)); + }; + + const deletePrefix = (prefixId: number) => + { + SendMessageComposer(new DeletePrefixComposer(prefixId)); + }; + + useEffect(() => + { + if(!prefixes || !prefixes.length) return; + + setSelectedPrefix(prevValue => + { + if(prevValue && prefixes.find(p => p.id === prevValue.id)) return prevValue; + return prefixes[0]; + }); + }, [ prefixes ]); + + useEffect(() => + { + if(!isVisible) return; + + return () => + { + resetCategory(UnseenItemCategory.PREFIX); + }; + }, [ isVisible, resetCategory ]); + + useEffect(() => + { + if(!isVisible || !needsUpdate) return; + + SendMessageComposer(new RequestPrefixesComposer()); + + setNeedsUpdate(false); + }, [ isVisible, needsUpdate ]); + + return { prefixes, activePrefix, selectedPrefix, setSelectedPrefix, activatePrefix, deactivatePrefix, deletePrefix, activate, deactivate }; +}; + +export const useInventoryPrefixes = () => useBetween(useInventoryPrefixesState); diff --git a/src/hooks/rooms/widgets/index.ts b/src/hooks/rooms/widgets/index.ts index 9984450..ea35008 100644 --- a/src/hooks/rooms/widgets/index.ts +++ b/src/hooks/rooms/widgets/index.ts @@ -1,5 +1,6 @@ export * from './furniture'; export * from './useAvatarInfoWidget'; +export * from './useChatCommandSelector'; export * from './useChatInputWidget'; export * from './useChatWidget'; export * from './useDoorbellWidget'; diff --git a/src/hooks/rooms/widgets/useChatWidget.ts b/src/hooks/rooms/widgets/useChatWidget.ts index 648a102..0ff6f24 100644 --- a/src/hooks/rooms/widgets/useChatWidget.ts +++ b/src/hooks/rooms/widgets/useChatWidget.ts @@ -149,6 +149,11 @@ const useChatWidgetState = () => imageUrl, color); + chatMessage.prefixText = event.prefixText || ''; + chatMessage.prefixColor = event.prefixColor || ''; + chatMessage.prefixIcon = event.prefixIcon || ''; + chatMessage.prefixEffect = event.prefixEffect || ''; + setChatMessages(prevValue => { const newValue = [ ...prevValue, chatMessage ]; From 466cc093878cd5580541be0a931dcb0dc5dcc2af Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 15 Mar 2026 11:08:04 +0100 Subject: [PATCH 33/33] =?UTF-8?q?=F0=9F=90=9B=20Fix=20crackableHits=20unde?= =?UTF-8?q?fined=20TypeError=20in=20InfoStandWidgetFurniView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: medievalshell --- .../avatar-info/infostand/InfoStandWidgetFurniView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index d58a720..4cd35a0 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -220,8 +220,8 @@ export const InfoStandWidgetFurniView: FC = props canUse = true; isCrackable = true; - crackableHits = stuffData.hits; - crackableTarget = stuffData.target; + crackableHits = stuffData?.hits ?? 0; + crackableTarget = stuffData?.target ?? 0; } else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) @@ -527,7 +527,7 @@ export const InfoStandWidgetFurniView: FC = props { isCrackable && <>
- { LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ crackableHits.toString(), crackableTarget.toString() ]) } + { LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ (crackableHits ?? 0).toString(), (crackableTarget ?? 0).toString() ]) } } { avatarInfo.groupId > 0 && <>