From 7007752e9167d025ff2149767921d5e27d5110bf Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 4 Jun 2026 11:32:55 +0200 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=86=99=20Mention=20Now=20in=20UI-Conf?= =?UTF-8?q?ig=20and=20UITexts=20and=20not=20hardcoded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/configuration/UITexts_en.json5.example | 43 +++++--- public/configuration/UITexts_it.json5.example | 43 +++++--- public/configuration/UITexts_nl.json5.example | 41 ++++--- src/App.tsx | 3 - .../RoomWidgetUpdateChatInputContentEvent.ts | 1 - .../ChatInputCommandSelectorView.tsx | 18 ++- .../room/widgets/chat-input/ChatInputView.tsx | 104 ++++++++++++------ .../rooms/widgets/useChatCommandSelector.ts | 63 ++++++----- 8 files changed, 182 insertions(+), 134 deletions(-) diff --git a/public/configuration/UITexts_en.json5.example b/public/configuration/UITexts_en.json5.example index 3bfbe4b..9a91bca 100644 --- a/public/configuration/UITexts_en.json5.example +++ b/public/configuration/UITexts_en.json5.example @@ -640,6 +640,8 @@ 'wheel.extra': 'Extra spins: %count%', 'wheel.spin': 'SPIN', 'wheel.buy': 'Buy spin', + 'wheel.settings': 'Settings', + 'wheel.settings.title': 'Wheel of Fortune Settings', 'wheel.winners': 'Latest winners', 'wheel.winners.empty': 'No winners yet', 'wheel.win.title': 'You won!', @@ -707,24 +709,6 @@ 'chatcmd.client.info': 'Client info', // ------------------------------------------------------------------------ - // Mentions - // ------------------------------------------------------------------------ - 'mentions.window.title': 'Mentions', - 'mentions.window.empty': 'No mentions', - 'mentions.window.markall': 'Mark all as read', - 'mentions.tab.title': 'Mentions', - 'mentions.notification': '%sender% mentioned you in %room%', - 'mentions.filter.all': 'All', - 'mentions.filter.unread': 'Unread', - 'mentions.filter.direct': 'Direct', - 'mentions.filter.room': 'Room', - 'mentions.group.today': 'Today', - 'mentions.group.yesterday': 'Yesterday', - 'mentions.group.older': 'Earlier', - 'mentions.type.direct': 'Direct mention', - 'mentions.type.room': 'Room mention', - 'mentions.action.goto': 'Go to room', - 'mentions.action.remove': 'Remove', // Me-menu settings + User account settings window // ------------------------------------------------------------------------ 'usersettings.tab.general': "General", @@ -790,4 +774,27 @@ 'usersettings.success.password': "Password updated successfully.", 'usersettings.success.email': "Email updated successfully.", 'usersettings.success.username': "Username updated. Please log in again with your new name.", + + // ------------------------------------------------------------------------ + // @-mention autocomplete (chat input) + // ------------------------------------------------------------------------ + 'mentions.window.title': "Mentions", + 'mentions.window.empty': "No mentions", + 'mentions.window.markall': "Mark all as read", + 'mentions.tab.title': "Mentions", + 'mentions.notification': "%sender% mentioned you in %room%", + 'mentions.filter.all': "All", + 'mentions.filter.unread': "Unread", + 'mentions.filter.direct': "Direct", + 'mentions.filter.room': "Room", + 'mentions.group.today': "Today", + 'mentions.group.yesterday': "Yesterday", + 'mentions.group.older': "Earlier", + 'mentions.type.direct': "Direct mention", + 'mentions.type.room': "Room mention", + 'mentions.action.goto': "Go to room", + 'mentions.action.remove': "Remove", + 'mentions.alias.description.everyone': "Everyone in the hotel", + 'mentions.alias.description.friends': "Your online friends", + 'mentions.alias.description.room': "Everyone in this room", } diff --git a/public/configuration/UITexts_it.json5.example b/public/configuration/UITexts_it.json5.example index 593498e..81bd966 100644 --- a/public/configuration/UITexts_it.json5.example +++ b/public/configuration/UITexts_it.json5.example @@ -640,6 +640,8 @@ 'wheel.extra': 'Giri extra: %count%', 'wheel.spin': 'GIRA', 'wheel.buy': 'Acquista giro', + 'wheel.settings': 'Configurações', + 'wheel.settings.title': 'Configuração de Sistema da Roleta', 'wheel.winners': 'Ultimi vincitori', 'wheel.winners.empty': 'Ancora nessun vincitore', 'wheel.win.title': 'Hai vinto!', @@ -707,24 +709,6 @@ 'chatcmd.client.info': 'Info client', // ------------------------------------------------------------------------ - // Mentions - // ------------------------------------------------------------------------ - 'mentions.window.title': 'Menzioni', - 'mentions.window.empty': 'Nessuna menzione', - 'mentions.window.markall': 'Segna tutte come lette', - 'mentions.tab.title': 'Menzioni', - 'mentions.notification': '%sender% ti ha menzionato in %room%', - 'mentions.filter.all': 'Tutte', - 'mentions.filter.unread': 'Non lette', - 'mentions.filter.direct': 'Dirette', - 'mentions.filter.room': 'Stanza', - 'mentions.group.today': 'Oggi', - 'mentions.group.yesterday': 'Ieri', - 'mentions.group.older': 'Precedenti', - 'mentions.type.direct': 'Menzione diretta', - 'mentions.type.room': 'Menzione di stanza', - 'mentions.action.goto': 'Vai alla stanza', - 'mentions.action.remove': 'Rimuovi', // Me-menu settings + User account settings window // ------------------------------------------------------------------------ 'usersettings.tab.general': "Generale", @@ -790,4 +774,27 @@ 'usersettings.success.password': "Password aggiornata con successo.", 'usersettings.success.email': "Email aggiornata con successo.", 'usersettings.success.username': "Nome utente aggiornato. Accedi di nuovo con il tuo nuovo nome.", + + // ------------------------------------------------------------------------ + // @-mention autocomplete (chat input) + // ------------------------------------------------------------------------ + 'mentions.window.title': "Menzioni", + 'mentions.window.empty': "Nessuna menzione", + 'mentions.window.markall': "Segna tutte come lette", + 'mentions.tab.title': "Menzioni", + 'mentions.notification': "%sender% ti ha menzionato in %room%", + 'mentions.filter.all': "Tutte", + 'mentions.filter.unread': "Non lette", + 'mentions.filter.direct': "Dirette", + 'mentions.filter.room': "Stanza", + 'mentions.group.today': "Oggi", + 'mentions.group.yesterday': "Ieri", + 'mentions.group.older': "Precedenti", + 'mentions.type.direct': "Menzione diretta", + 'mentions.type.room': "Menzione di stanza", + 'mentions.action.goto': "Vai alla stanza", + 'mentions.action.remove': "Rimuovi", + 'mentions.alias.description.everyone': "Tutti nell'hotel", + 'mentions.alias.description.friends': "I tuoi amici online", + 'mentions.alias.description.room': "Tutti in questa stanza", } diff --git a/public/configuration/UITexts_nl.json5.example b/public/configuration/UITexts_nl.json5.example index 62f963f..ebd3616 100644 --- a/public/configuration/UITexts_nl.json5.example +++ b/public/configuration/UITexts_nl.json5.example @@ -709,24 +709,6 @@ 'chatcmd.client.info': 'Client info', // ------------------------------------------------------------------------ - // Mentions - // ------------------------------------------------------------------------ - 'mentions.window.title': 'Vermeldingen', - 'mentions.window.empty': 'Geen vermeldingen', - 'mentions.window.markall': 'Alles als gelezen markeren', - 'mentions.tab.title': 'Vermeldingen', - 'mentions.notification': '%sender% heeft je genoemd in %room%', - 'mentions.filter.all': 'Alle', - 'mentions.filter.unread': 'Ongelezen', - 'mentions.filter.direct': 'Direct', - 'mentions.filter.room': 'Kamer', - 'mentions.group.today': 'Vandaag', - 'mentions.group.yesterday': 'Gisteren', - 'mentions.group.older': 'Eerder', - 'mentions.type.direct': 'Directe vermelding', - 'mentions.type.room': 'Kamervermelding', - 'mentions.action.goto': 'Ga naar kamer', - 'mentions.action.remove': 'Verwijderen', // Me-menu settings + User account settings window // ------------------------------------------------------------------------ 'usersettings.tab.general': "Algemeen", @@ -792,4 +774,27 @@ 'usersettings.success.password': "Wachtwoord succesvol bijgewerkt.", 'usersettings.success.email': "E-mail succesvol bijgewerkt.", 'usersettings.success.username': "Gebruikersnaam bijgewerkt. Log opnieuw in met je nieuwe naam.", + + // ------------------------------------------------------------------------ + // @-mention autocomplete (chat input) + // ------------------------------------------------------------------------ + 'mentions.window.title': "Vermeldingen", + 'mentions.window.empty': "Geen vermeldingen", + 'mentions.window.markall': "Markeer alles als gelezen", + 'mentions.tab.title': "Vermeldingen", + 'mentions.notification': "%sender% noemde je in %room%", + 'mentions.filter.all': "Alles", + 'mentions.filter.unread': "Ongelezen", + 'mentions.filter.direct': "Direct", + 'mentions.filter.room': "Kamer", + 'mentions.group.today': "Vandaag", + 'mentions.group.yesterday': "Gisteren", + 'mentions.group.older': "Eerder", + 'mentions.type.direct': "Directe vermelding", + 'mentions.type.room': "Kamervermelding", + 'mentions.action.goto': "Ga naar kamer", + 'mentions.action.remove': "Verwijderen", + 'mentions.alias.description.everyone': "Iedereen in het hotel", + 'mentions.alias.description.friends': "Je vrienden online", + 'mentions.alias.description.room': "Iedereen in deze kamer", } diff --git a/src/App.tsx b/src/App.tsx index babffe1..4b799ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,6 @@ import { LoginView } from './components/login/LoginView'; import { MainView } from './components/MainView'; import { ReconnectView } from './components/reconnect/ReconnectView'; import { useMessageEvent, useNitroEvent } from './hooks'; -import { ensureChatCommandListener } from './hooks/rooms/widgets/useChatCommandSelector'; NitroVersion.UI_VERSION = GetUIVersion(); @@ -563,9 +562,7 @@ export const App: FC<{}> = props => bumpProgress(85, taskLabel('loading.task.rooms', 'Loading rooms...')); await GetRoomEngine().init(); bumpProgress(92, taskLabel('loading.task.engine', 'Loading graphics engine...')); - ensureChatCommandListener(); await GetCommunication().init(); - ensureChatCommandListener(); bumpProgress(98, taskLabel('generic.reconnecting', 'Connecting to server...')); })(); } diff --git a/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts b/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts index aea7193..9352372 100644 --- a/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts +++ b/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts @@ -5,7 +5,6 @@ export class RoomWidgetUpdateChatInputContentEvent extends RoomWidgetUpdateEvent public static CHAT_INPUT_CONTENT: string = 'RWUCICE_CHAT_INPUT_CONTENT'; public static WHISPER: string = 'whisper'; public static SHOUT: string = 'shout'; - public static TEXT: string = 'text'; private _chatMode: string = ''; private _userName: string = ''; diff --git a/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx index d3e0eae..5eae3d2 100644 --- a/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx @@ -1,10 +1,9 @@ import { FC, useEffect, useRef } from 'react'; -import type { CommandDefinition } from '../../../../api'; -import type { RankedCommandDefinition } from '../../../../hooks/rooms/widgets/useChatCommandSelector.helpers'; +import { CommandDefinition } from '../../../../api'; interface ChatInputCommandSelectorViewProps { - commands: RankedCommandDefinition[]; + commands: CommandDefinition[]; selectedIndex: number; onSelect: (command: CommandDefinition) => void; onHover: (index: number) => void; @@ -25,18 +24,17 @@ export const ChatInputCommandSelectorView: FC }, [ selectedIndex ]); return ( -
+
{ commands.map((cmd, 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 08ffe8e..85d3198 100644 --- a/src/components/room/widgets/chat-input/ChatInputView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputView.tsx @@ -12,15 +12,40 @@ import { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView'; const USER_TYPE_REAL_USER = 1; const MAX_MENTION_SUGGESTIONS = 8; -const MENTION_ALIASES: ReadonlyArray<{ key: string; label: string; description?: string }> = [ - { key: 'all', label: 'all', description: 'Everyone in the hotel' }, - { key: 'everyone', label: 'everyone', description: 'Everyone in the hotel' }, - { key: 'tutti', label: 'tutti', description: 'Everyone in the hotel' }, - { key: 'friends', label: 'friends', description: 'Your online friends' }, - { key: 'amici', label: 'amici', description: 'Your online friends' }, - { key: 'room', label: 'room', description: 'Everyone in this room' }, - { key: 'stanza', label: 'stanza', description: 'Everyone in this room' } -]; + +type MentionAliasScope = 'everyone' | 'friends' | 'room'; + +const MENTION_ALIAS_CONFIG_KEY: Record = { + everyone: 'mentions_ui.aliases.everyone', + friends: 'mentions_ui.aliases.friends', + room: 'mentions_ui.aliases.room' +}; + +const MENTION_ALIAS_DEFAULTS: Record = { + everyone: [ 'all', 'everyone', 'tutti' ], + friends: [ 'friends', 'amici' ], + room: [ 'room', 'stanza' ] +}; + +const MENTION_ALIAS_DESCRIPTION_KEY: Record = { + everyone: 'mentions.alias.description.everyone', + friends: 'mentions.alias.description.friends', + room: 'mentions.alias.description.room' +}; + +const sanitizeAliasList = (raw: unknown, fallback: string[]): string[] => +{ + if(!Array.isArray(raw)) return fallback; + const out: string[] = []; + for(const entry of raw) + { + if(typeof entry !== 'string') continue; + const trimmed = entry.trim(); + if(!trimmed) continue; + out.push(trimmed); + } + return out; +}; export const ChatInputView: FC<{}> = props => { @@ -30,6 +55,7 @@ export const ChatInputView: FC<{}> = props => const { roomSession = null } = useRoom(); const inputRef = useRef(null); const { isVisible: commandSelectorVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close: closeCommandSelector } = useChatCommandSelector(chatValue); + const roomUserList = useRoomUserListSnapshot(); const [ mentionSelectedIndex, setMentionSelectedIndex ] = useState(0); @@ -46,12 +72,38 @@ export const ChatInputView: FC<{}> = props => if(at > 0 && !/\s/.test(upToCaret.charAt(at - 1))) return null; const query = upToCaret.slice(at + 1); - if(/\s/.test(query)) return null; return { atIndex: at, replaceFrom: at, replaceTo: caret, query }; }, [ chatValue, commandSelectorVisible ]); + const mentionAliases = useMemo>(() => + { + const out: { key: string; scope: MentionAliasScope; description: string }[] = []; + const seen = new Set(); + + const scopes: MentionAliasScope[] = [ 'everyone', 'friends', 'room' ]; + for(const scope of scopes) + { + const list = sanitizeAliasList( + GetConfigurationValue(MENTION_ALIAS_CONFIG_KEY[scope], MENTION_ALIAS_DEFAULTS[scope]), + MENTION_ALIAS_DEFAULTS[scope] + ); + + for(const key of list) + { + const lower = key.toLowerCase(); + + if(seen.has(lower)) continue; + seen.add(lower); + + out.push({ key, scope, description: LocalizeText(MENTION_ALIAS_DESCRIPTION_KEY[scope]) }); + } + } + + return out; + }, []); + const mentionSuggestions = useMemo(() => { if(!mentionContext) return []; @@ -76,14 +128,14 @@ export const ChatInputView: FC<{}> = props => if(out.length >= MAX_MENTION_SUGGESTIONS) break; } - for(const alias of MENTION_ALIASES) + for(const alias of mentionAliases) { if(query.length > 0 && !alias.key.toLowerCase().startsWith(query)) continue; out.push({ key: `alias:${ alias.key }`, kind: 'alias', - name: alias.label, + name: alias.key, insertToken: alias.key, description: alias.description }); @@ -92,7 +144,7 @@ export const ChatInputView: FC<{}> = props => } return out; - }, [ mentionContext, roomUserList ]); + }, [ mentionContext, roomUserList, mentionAliases ]); const mentionSelectorVisible = mentionSuggestions.length > 0; @@ -148,23 +200,6 @@ export const ChatInputView: FC<{}> = props => inputRef.current.setSelectionRange((inputRef.current.value.length * 2), (inputRef.current.value.length * 2)); }, [ inputRef ]); - const setChatInputValue = useCallback((value: string, markTyping: boolean = true) => - { - setChatValue(value); - - if(markTyping) - { - setIsTyping(!!value.length); - setIsIdle(!!value.length); - } - - requestAnimationFrame(() => - { - inputRef.current?.focus(); - inputRef.current?.setSelectionRange(value.length, value.length); - }); - }, [ setIsTyping, setIsIdle ]); - const checkSpecialKeywordForInput = useCallback(() => { setChatValue(prevValue => @@ -279,7 +314,7 @@ export const ChatInputView: FC<{}> = props => if(selected) { event.preventDefault(); - setChatInputValue(':' + selected.key + ' '); + setChatValue(':' + selected.key + ' '); return; } break; @@ -319,7 +354,7 @@ export const ChatInputView: FC<{}> = props => case 'Escape': event.preventDefault(); setMentionSelectedIndex(0); - + if(mentionContext) { const before = chatValue.slice(0, mentionContext.replaceFrom); @@ -361,9 +396,6 @@ export const ChatInputView: FC<{}> = props => { switch(event.chatMode) { - case RoomWidgetUpdateChatInputContentEvent.TEXT: - setChatInputValue(event.userName); - return; case RoomWidgetUpdateChatInputContentEvent.WHISPER: { setChatValue(`${ chatModeIdWhisper } ${ event.userName } `); return; @@ -450,7 +482,7 @@ export const ChatInputView: FC<{}> = props => selectedIndex={ selectedIndex } onSelect={ (cmd) => { - setChatInputValue(':' + cmd.key + ' '); + setChatValue(':' + cmd.key + ' '); inputRef.current?.focus(); } } onHover={ setSelectedIndex } /> } diff --git a/src/hooks/rooms/widgets/useChatCommandSelector.ts b/src/hooks/rooms/widgets/useChatCommandSelector.ts index 55785a2..c999dec 100644 --- a/src/hooks/rooms/widgets/useChatCommandSelector.ts +++ b/src/hooks/rooms/widgets/useChatCommandSelector.ts @@ -3,9 +3,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { CommandDefinition, LocalizeText } from '../../../api'; import { createNitroStore } from '../../../state/createNitroStore'; import { useMessageEvent } from '../../events'; -import { getChatCommandQuery, getRankedCommandSuggestions } from './useChatCommandSelector.helpers'; - -const MAX_VISIBLE_COMMANDS = 8; // Client-only commands are static; safe to keep at module scope. The // `descriptionKey` is a LocalizeText slot resolved at merge time so @@ -65,7 +62,7 @@ const useChatCommandStore = createNitroStore()((set) => ({ markListenerRegistered: () => set({ isListenerRegistered: true }) })); -export const ensureChatCommandListener = (): void => +const ensureGlobalListener = (): void => { if(useChatCommandStore.getState().isListenerRegistered) return; @@ -89,20 +86,20 @@ export const ensureChatCommandListener = (): void => // Try once at module load so the server's response landing before any // React mount still hits the cache. -ensureChatCommandListener(); +ensureGlobalListener(); export const useChatCommandSelector = (chatValue: string) => { const serverCommands = useChatCommandStore(s => s.serverCommands); const setServerCommands = useChatCommandStore(s => s.setServerCommands); const [ selectedIndex, setSelectedIndex ] = useState(0); - const [ dismissedQuery, setDismissedQuery ] = useState(null); + const [ dismissed, setDismissed ] = useState(false); useEffect(() => { // Cover the case where the module-level registration failed // because GetCommunication() wasn't ready at import time. - ensureChatCommandListener(); + ensureGlobalListener(); }, []); // Late updates (rank change, etc.) — go through the store so all @@ -126,55 +123,61 @@ export const useChatCommandSelector = (chatValue: string) => return merged.sort((a, b) => a.key.localeCompare(b.key)); }, [ serverCommands ]); - const filterText = useMemo(() => getChatCommandQuery(chatValue), [ chatValue ]); + const filterText = useMemo(() => + { + if(!chatValue.startsWith(':') || chatValue.includes(' ')) return ''; + + return chatValue.slice(1).toLowerCase(); + }, [ chatValue ]); const filteredCommands = useMemo(() => { - if(filterText === null) return []; + if(!filterText && !chatValue.startsWith(':')) return []; - return getRankedCommandSuggestions(allCommands, filterText, MAX_VISIBLE_COMMANDS); - }, [ allCommands, filterText ]); + return allCommands.filter(cmd => cmd.key.toLowerCase().startsWith(filterText)); + }, [ allCommands, filterText, chatValue ]); const isVisible = useMemo(() => { - return filterText !== null && filteredCommands.length > 0 && dismissedQuery !== filterText; - }, [ filterText, filteredCommands, dismissedQuery ]); - - const boundedSelectedIndex = useMemo(() => - { - if(!filteredCommands.length) return 0; - - return Math.min(selectedIndex, filteredCommands.length - 1); - }, [ filteredCommands.length, selectedIndex ]); + return chatValue.startsWith(':') && !chatValue.includes(' ') && filteredCommands.length > 0 && !dismissed; + }, [ chatValue, filteredCommands, dismissed ]); const moveUp = useCallback(() => { - if(!filteredCommands.length) return; - - setSelectedIndex(prev => ((prev <= 0 || prev >= filteredCommands.length) ? filteredCommands.length - 1 : prev - 1)); + setSelectedIndex(prev => (prev <= 0 ? filteredCommands.length - 1 : prev - 1)); }, [ filteredCommands.length ]); const moveDown = useCallback(() => { - if(!filteredCommands.length) return; - setSelectedIndex(prev => (prev >= filteredCommands.length - 1 ? 0 : prev + 1)); }, [ filteredCommands.length ]); const selectCurrent = useCallback((): CommandDefinition | null => { - if(boundedSelectedIndex >= 0 && boundedSelectedIndex < filteredCommands.length) + if(selectedIndex >= 0 && selectedIndex < filteredCommands.length) { - return filteredCommands[boundedSelectedIndex]; + return filteredCommands[selectedIndex]; } return null; - }, [ boundedSelectedIndex, filteredCommands ]); + }, [ selectedIndex, filteredCommands ]); const close = useCallback(() => { - setDismissedQuery(filterText); + 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: boundedSelectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close }; + return { isVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close }; }; From 47453db5ee819705c14314d173edcca54a3a910b Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 4 Jun 2026 13:43:04 +0200 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=86=99=20Bug=20fixed=20in=20localstor?= =?UTF-8?q?age?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/MainView.tsx | 4 +- src/components/mentions/index.ts | 1 - .../NotificationDefaultAlertView.tsx | 73 +---------- .../ChatInputMentionSelectorView.tsx | 9 ++ .../room/widgets/chat-input/ChatInputView.tsx | 40 +++++- .../room/widgets/chat/highlightMentions.tsx | 69 +++++++++- .../notification/NotificationCenterView.css | 73 +---------- src/hooks/catalog/useCatalog.ts | 31 +---- src/hooks/chat-history/useChatHistory.ts | 20 ++- .../mentions/__tests__/mentionsStore.test.ts | 15 +++ src/hooks/mentions/index.ts | 1 - src/hooks/mentions/mentionsStore.ts | 24 +++- src/hooks/mentions/useMentionMessages.ts | 50 +++++-- src/hooks/navigator/navigatorUiStore.ts | 2 +- src/hooks/rooms/widgets/useChatWidget.ts | 41 +++--- src/hooks/useLocalStorage.ts | 124 +++++++++++++++++- src/index.tsx | 1 - 17 files changed, 368 insertions(+), 210 deletions(-) diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 32eb535..caa04ab 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -48,7 +48,7 @@ import { UserAccountSettingsView } from './user-settings/UserAccountSettingsView import { UserSettingsView } from './user-settings/UserSettingsView'; import { WiredView } from './wired/WiredView'; import { WiredCreatorToolsView } from './wired-tools/WiredCreatorToolsView'; -import { MentionsView, MentionToastsView } from './mentions'; +import { MentionsView } from './mentions'; export const MainView: FC<{}> = props => { @@ -242,8 +242,6 @@ export const MainView: FC<{}> = props => { GetConfigurationValue('radio_ui.enabled', false) && } { (GetConfigurationValue('mentions_ui.enabled', true) && mentionsVisible) && setMentionsVisible(false) } /> } - { GetConfigurationValue('mentions_ui.enabled', true) && - } ); diff --git a/src/components/mentions/index.ts b/src/components/mentions/index.ts index 8c125c1..5eaab12 100644 --- a/src/components/mentions/index.ts +++ b/src/components/mentions/index.ts @@ -1,6 +1,5 @@ export * from './MentionMessageView'; export * from './MentionRowView'; export * from './MentionsView'; -export * from './MentionToastsView'; export * from './mentionsFormat'; export * from './useMentionActions'; diff --git a/src/components/notification-center/views/alert-layouts/NotificationDefaultAlertView.tsx b/src/components/notification-center/views/alert-layouts/NotificationDefaultAlertView.tsx index 14c289d..d9905e6 100644 --- a/src/components/notification-center/views/alert-layouts/NotificationDefaultAlertView.tsx +++ b/src/components/notification-center/views/alert-layouts/NotificationDefaultAlertView.tsx @@ -1,5 +1,5 @@ -import { FC, useMemo, useState } from 'react'; -import { DispatchUiEvent, LocalizeText, NotificationAlertItem, NotificationAlertType, OpenUrl, RoomWidgetUpdateChatInputContentEvent } from '../../../../api'; +import { FC, useState } from 'react'; +import { LocalizeText, NotificationAlertItem, NotificationAlertType, OpenUrl } from '../../../../api'; import { Button, Column, Flex, LayoutNotificationAlertView, LayoutNotificationAlertViewProps } from '../../../../common'; interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewProps @@ -7,57 +7,11 @@ interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewP item: NotificationAlertItem; } -const COMMAND_LINE_PATTERN = /^\s*:[\w.-]+(?:\s.*)?$/; - -interface CommandTemplateEntry -{ - command: string; - description: string; -} - export const NotificationDefaultAlertView: FC = props => { - const { item = null, title = ((props.item && props.item.title) || ''), onClose = null, classNames = [], ...rest } = props; + const { item = null, title = ((props.item && props.item.title) || ''), onClose = null, ...rest } = props; const [ imageFailed, setImageFailed ] = useState(false); - const alertLines = useMemo(() => item.messages.flatMap(message => message.split(/\r\n|\r|\n/g)), [ item.messages ]); - const hasCommandTemplate = useMemo(() => - { - const commandLines = alertLines.filter(line => COMMAND_LINE_PATTERN.test(line)); - - return commandLines.length >= 4 || alertLines.some(line => /^Your Commands\(\d+\):?/i.test(line.trim())); - }, [ alertLines ]); - const commandTemplateContent = useMemo(() => - { - const intro: string[] = []; - const commands: CommandTemplateEntry[] = []; - - for(const rawLine of alertLines) - { - const text = rawLine.trim(); - - if(!text.length) continue; - - if(COMMAND_LINE_PATTERN.test(text)) - { - commands.push({ command: text, description: '' }); - continue; - } - - if(commands.length) - { - const lastCommand = commands[commands.length - 1]; - - lastCommand.description = lastCommand.description ? `${ lastCommand.description } ${ text }` : text; - continue; - } - - intro.push(text); - } - - return { intro, commands }; - }, [ alertLines ]); - const visitUrl = () => { OpenUrl(item.clickUrl); @@ -65,18 +19,10 @@ export const NotificationDefaultAlertView: FC onClose(); }; - const copyCommandToChatInput = (command: string) => - { - const chatValue = command.endsWith(' ') ? command : `${ command } `; - - DispatchUiEvent(new RoomWidgetUpdateChatInputContentEvent(RoomWidgetUpdateChatInputContentEvent.TEXT, chatValue)); - }; - const hasFrank = (item.alertType === NotificationAlertType.DEFAULT); - const alertClassNames = hasCommandTemplate ? [ ...classNames, 'nitro-alert-command-list' ] : classNames; return ( - + { hasFrank && !item.imageUrl &&
} { item.imageUrl && !imageFailed && { @@ -84,16 +30,7 @@ export const NotificationDefaultAlertView: FC setImageFailed(true); } } /> }
- { hasCommandTemplate &&
- { commandTemplateContent.intro.map((text, index) => -
{ text }
) } - { commandTemplateContent.commands.map((entry, index) => - ) } -
} - { !hasCommandTemplate && (item.messages.length > 0) && item.messages.map((message, index) => + { (item.messages.length > 0) && item.messages.map((message, index) => { const htmlText = message.replace(/\r\n|\r|\n/g, '
'); diff --git a/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx index 370786f..3bf5913 100644 --- a/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx @@ -7,9 +7,13 @@ export interface MentionSuggestion { key: string; kind: MentionSuggestionKind; + /** Display name shown in the row (e.g. "DuckieTM" or "all"). */ name: string; + /** Token that's actually inserted into the chat input (without the @). */ insertToken: string; + /** Figure string for the avatar tile - only set for 'user' rows. */ figure?: string; + /** Optional sub-label, e.g. for "Staff Chat". */ description?: string; } @@ -21,6 +25,11 @@ interface ChatInputMentionSelectorViewProps onHover: (index: number) => void; } +/** + * @-autocomplete popover. Suggestion list comes pre-filtered from the parent: + * real users (RoomObjectUserType.USER = 1) only, never pets / bots / rentable + * bots / monster plants, plus the configured broadcast aliases. + */ export const ChatInputMentionSelectorView: FC = props => { const { suggestions = [], selectedIndex = 0, onSelect = null, onHover = null } = props; diff --git a/src/components/room/widgets/chat-input/ChatInputView.tsx b/src/components/room/widgets/chat-input/ChatInputView.tsx index 85d3198..41f14f6 100644 --- a/src/components/room/widgets/chat-input/ChatInputView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputView.tsx @@ -10,9 +10,15 @@ import { ChatInputEmojiSelectorView } from './ChatInputEmojiSelectorView'; import { ChatInputMentionSelectorView, MentionSuggestion } from './ChatInputMentionSelectorView'; import { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView'; +// RoomObjectUserType.AVATAR_TYPES: USER = 1, PET = 2, BOT = 3, RENTABLE_BOT = 4. +// Only real users can be mentioned, so the @-autocomplete keeps just type 1. const USER_TYPE_REAL_USER = 1; const MAX_MENTION_SUGGESTIONS = 8; +// Broadcast alias categories. The actual alias strings live in +// ui-config.json (mentions_ui.aliases.everyone / .friends / .room) so an +// admin can edit them without a rebuild and keep them in sync with the +// gameserver's mentions.*.aliases config keys. type MentionAliasScope = 'everyone' | 'friends' | 'room'; const MENTION_ALIAS_CONFIG_KEY: Record = { @@ -27,12 +33,18 @@ const MENTION_ALIAS_DEFAULTS: Record = { room: [ 'room', 'stanza' ] }; +// Localization keys for the description shown next to each alias in the +// picker. The actual translations live in UITexts (see +// text/UITexts_*.json5.example) so admins can localize without a rebuild. const MENTION_ALIAS_DESCRIPTION_KEY: Record = { everyone: 'mentions.alias.description.everyone', friends: 'mentions.alias.description.friends', room: 'mentions.alias.description.room' }; +// Coerces a configured value to a clean string[] - tolerates a missing +// config key, a non-array value, and non-string entries (so a typo in +// ui-config.json can't crash the picker). const sanitizeAliasList = (raw: unknown, fallback: string[]): string[] => { if(!Array.isArray(raw)) return fallback; @@ -59,6 +71,13 @@ export const ChatInputView: FC<{}> = props => const roomUserList = useRoomUserListSnapshot(); const [ mentionSelectedIndex, setMentionSelectedIndex ] = useState(0); + /** + * Detect an open @-mention token at the input caret. Returns the active + * query (text after @) plus the offsets we'll replace on selection, or + * null when the caret is not inside an @-token. Triggers only when the + * @ is at the start of the input or follows whitespace, so an email-like + * "foo@bar" doesn't pop the picker. + */ const mentionContext = useMemo(() => { if(!chatValue) return null; @@ -69,14 +88,19 @@ export const ChatInputView: FC<{}> = props => const at = upToCaret.lastIndexOf('@'); if(at < 0) return null; + // @ must be at start or follow whitespace. if(at > 0 && !/\s/.test(upToCaret.charAt(at - 1))) return null; const query = upToCaret.slice(at + 1); + // Bail if the query already contains whitespace - the token has ended. if(/\s/.test(query)) return null; return { atIndex: at, replaceFrom: at, replaceTo: caret, query }; }, [ chatValue, commandSelectorVisible ]); + // Flattened, config-driven alias list. Each scope's aliases are loaded + // from ui-config.json (with a typed fallback in case the key is missing + // or corrupted) and stitched in with the per-scope description. const mentionAliases = useMemo>(() => { const out: { key: string; scope: MentionAliasScope; description: string }[] = []; @@ -93,7 +117,8 @@ export const ChatInputView: FC<{}> = props => for(const key of list) { const lower = key.toLowerCase(); - + // First-wins dedupe so an alias accidentally listed in two + // scopes shows once - same precedence the gameserver uses. if(seen.has(lower)) continue; seen.add(lower); @@ -111,6 +136,8 @@ export const ChatInputView: FC<{}> = props => const query = mentionContext.query.toLowerCase(); const out: MentionSuggestion[] = []; + // 1. Real users in the room. Pets, bots, rentable bots and monster + // plants are filtered out by type. for(const user of roomUserList) { if(!user || user.type !== USER_TYPE_REAL_USER) continue; @@ -128,6 +155,9 @@ export const ChatInputView: FC<{}> = props => if(out.length >= MAX_MENTION_SUGGESTIONS) break; } + // 2. Broadcast aliases. The server permission-gates these (sender needs + // acc_mention_everyone / acc_mention_friends to actually fire) - the + // picker just surfaces them. for(const alias of mentionAliases) { if(query.length > 0 && !alias.key.toLowerCase().startsWith(query)) continue; @@ -148,6 +178,8 @@ export const ChatInputView: FC<{}> = props => const mentionSelectorVisible = mentionSuggestions.length > 0; + // Reset / clamp the highlighted row whenever the suggestion list changes + // so arrow-up/down doesn't keep an index that's now out of range. useEffect(() => { if(mentionSelectedIndex >= mentionSuggestions.length) setMentionSelectedIndex(0); @@ -164,6 +196,8 @@ export const ChatInputView: FC<{}> = props => setChatValue(next); + // Move the caret to right after the inserted mention so subsequent + // typing continues the message instead of editing the mention. requestAnimationFrame(() => { if(!inputRef.current) return; @@ -307,6 +341,7 @@ export const ChatInputView: FC<{}> = props => return; case 'Tab': event.preventDefault(); + // fall through case 'NumpadEnter': case 'Enter': { const selected = selectCurrent(); @@ -354,7 +389,8 @@ export const ChatInputView: FC<{}> = props => case 'Escape': event.preventDefault(); setMentionSelectedIndex(0); - + // Closing without picking: drop the bare "@" so the + // picker doesn't immediately reopen on next render. if(mentionContext) { const before = chatValue.slice(0, mentionContext.replaceFrom); diff --git a/src/components/room/widgets/chat/highlightMentions.tsx b/src/components/room/widgets/chat/highlightMentions.tsx index 6c8620e..f418fab 100644 --- a/src/components/room/widgets/chat/highlightMentions.tsx +++ b/src/components/room/widgets/chat/highlightMentions.tsx @@ -1,11 +1,48 @@ +/** + * Cosmetic-only mention highlighting for in-room chat bubbles. + * + * The bubble text is rendered through {@link RoomChatFormatter}, which emits + * an HTML string (wired markup ``/``/``, font-colour + * ``, `
`, plus HTML-entity-encoded special characters) and + * is injected via `dangerouslySetInnerHTML`. We therefore operate on the + * already-formatted HTML string and wrap mention tokens that appear in the + * TEXT regions (never inside a ``), returning a new HTML string. This + * keeps every existing formatting behaviour intact and is purely visual — it + * does not touch `chat.text`, parsing, chat history, or any wire payload. + * + * Token detection mirrors the server's `MentionManager.process` exactly: + * - split on whitespace + * - a candidate token has length >= 2 and starts with `@` + * - strip the `@`, remove every char that is not [A-Za-z0-9_], lowercase + * - match against the local username or a room-broadcast alias + * + * This means `@Bob!`, `@bob,` etc. all match the nick `Bob` (case-insensitive) + * just like the server, while the highlighted span keeps the original token + * text (`@` + original casing + trailing punctuation) verbatim. + */ + +// Mirror of the three alias config keys in Arcturus +// (com.eu.habbo.habbohotel.mentions.MentionManager). +// Highlighting is purely visual - the server still gates @everyone / +// @friends on acc_mention_everyone / acc_mention_friends, so a normal +// user typing @all just gets a highlighted chat bubble with no actual +// notifications fired. export const MENTION_ROOM_ALIASES: ReadonlyArray = [ + // mentions.everyone.aliases default 'all', 'everyone', 'tutti', + // mentions.friends.aliases default 'friends', 'amici', + // mentions.room.aliases default 'room', 'stanza' ]; const NON_NICK_CHARS = /[^A-Za-z0-9_]/g; +/** + * Normalise a raw `@token` the same way the server does: drop the leading `@`, + * strip any non-nick characters (trailing punctuation, etc.), lowercase. + * Returns an empty string when nothing usable remains. + */ const normalizeToken = (token: string): string => { if(!token || token.length < 2 || token.charAt(0) !== '@') return ''; @@ -13,7 +50,10 @@ const normalizeToken = (token: string): string => return token.substring(1).replace(NON_NICK_CHARS, '').toLowerCase(); }; - +/** + * Whether the given raw whitespace-delimited token mentions the local user + * or a room-broadcast alias. + */ const isMentionToken = (token: string, ownUsernameLower: string, aliases: ReadonlySet): boolean => { const nick = normalizeToken(token); @@ -25,6 +65,11 @@ const isMentionToken = (token: string, ownUsernameLower: string, aliases: Readon return aliases.has(nick); }; +/** + * Public predicate: does this raw whitespace-delimited token mention the given + * user or a room-broadcast alias? Mirrors the server's detection. Reusable by + * UI that renders mention previews as React nodes (e.g. the mentions box). + */ export const tokenIsMention = ( token: string, ownUsername: string, @@ -38,10 +83,17 @@ export const tokenIsMention = ( const HIGHLIGHT_OPEN = ''; const HIGHLIGHT_CLOSE = ''; +/** + * Wrap mention tokens in a single text chunk (no HTML tags inside it). + * Whitespace runs between tokens are preserved verbatim by re-using the + * original substrings around each match. + */ const highlightTextChunk = (chunk: string, ownUsernameLower: string, aliases: ReadonlySet): string => { if(chunk.indexOf('@') < 0) return chunk; + // Split into alternating [whitespace, token, whitespace, token, ...] + // segments so the exact original spacing is rebuilt unchanged. const segments = chunk.split(/(\s+)/); let result = ''; @@ -50,6 +102,7 @@ const highlightTextChunk = (chunk: string, ownUsernameLower: string, aliases: Re { if(segment.length === 0) continue; + // Whitespace runs and non-mention tokens pass through untouched. if(/^\s+$/.test(segment) || !isMentionToken(segment, ownUsernameLower, aliases)) { result += segment; @@ -62,6 +115,15 @@ const highlightTextChunk = (chunk: string, ownUsernameLower: string, aliases: Re return result; }; +/** + * Take the formatted bubble HTML and return new HTML where every mention + * token (own nick or room alias) in the text regions is wrapped in + * ``. HTML tags are passed through + * untouched so existing markup keeps working. + * + * Returns the input unchanged when there is no `@`, no own username, and no + * possibility of a match (fast path), or when nothing matches. + */ export const highlightMentions = ( formattedHtml: string, ownUsername: string, @@ -73,8 +135,10 @@ export const highlightMentions = ( const ownUsernameLower = (ownUsername || '').replace(NON_NICK_CHARS, '').toLowerCase(); const aliasSet = new Set(aliases.map(a => a.toLowerCase())); + // Nothing could ever match → return verbatim. if(!ownUsernameLower && aliasSet.size === 0) return formattedHtml; + // Walk the string, only highlighting inside text regions (outside `<...>`). let result = ''; let cursor = 0; @@ -88,6 +152,7 @@ export const highlightMentions = ( break; } + // Text region before the next tag. if(tagStart > cursor) { result += highlightTextChunk(formattedHtml.slice(cursor, tagStart), ownUsernameLower, aliasSet); @@ -97,10 +162,12 @@ export const highlightMentions = ( if(tagEnd < 0) { + // Malformed trailing `<` with no closing `>` — emit the rest verbatim. result += formattedHtml.slice(tagStart); break; } + // Emit the tag (including the angle brackets) untouched. result += formattedHtml.slice(tagStart, tagEnd + 1); cursor = tagEnd + 1; } diff --git a/src/css/notification/NotificationCenterView.css b/src/css/notification/NotificationCenterView.css index 2d43f52..c24cb67 100644 --- a/src/css/notification/NotificationCenterView.css +++ b/src/css/notification/NotificationCenterView.css @@ -20,77 +20,6 @@ } } - &.nitro-alert-command-list { - width: min(430px, calc(100vw - 18px)); - min-height: 210px; - max-height: min(520px, calc(100vh - 24px)); - - .content-area { - padding: 9px 10px 8px; - } - - .notification-text { - min-width: 0; - padding-right: 3px; - font-family: Ubuntu, sans-serif; - line-height: 1.25; - } - - .notification-command-template { - display: flex; - flex-direction: column; - gap: 4px; - padding-bottom: 2px; - } - - .notification-command-heading { - font-weight: 700; - color: #101010; - margin-bottom: 3px; - } - - .notification-command-copy { - color: #262626; - margin-bottom: 6px; - } - - .notification-command-row { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 2px; - min-height: 34px; - padding: 5px 8px; - color: #123b4c; - background: linear-gradient(180deg, #ffffff 0%, #dceaf0 100%); - border: 1px solid #8ca6b1; - border-radius: 4px; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85); - text-align: left; - word-break: break-word; - } - - .notification-command-row:hover { - background: linear-gradient(180deg, #ffffff 0%, #cfe2eb 100%); - border-color: #4f879b; - } - - .notification-command-name { - font-weight: 700; - color: #123b4c; - } - - .notification-command-description { - font-size: 11px; - line-height: 1.2; - color: #3d4a50; - } - - .notification-command-spacer { - height: 3px; - } - } - &.nitro-alert-credits { width: 370px; .notification-text { @@ -461,4 +390,4 @@ position: relative; background-image: url("@/assets/images/notifications/nitro_v3.png"); background-repeat: no-repeat; -} +} \ No newline at end of file diff --git a/src/hooks/catalog/useCatalog.ts b/src/hooks/catalog/useCatalog.ts index 807ef66..807b98e 100644 --- a/src/hooks/catalog/useCatalog.ts +++ b/src/hooks/catalog/useCatalog.ts @@ -1,4 +1,4 @@ -import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetConfiguration, GetRoomContentLoader, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomEngineObjectPlacedEvent, RoomObjectPlacementSource, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer'; +import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomEngineObjectPlacedEvent, RoomObjectPlacementSource, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useBetween } from 'use-between'; import { BuilderFurniPlaceableStatus, CatalogPage, CatalogType, DispatchUiEvent, FurniCategory, GetFurnitureData, GetProductDataForLocalization, GetRoomSession, ICatalogNode, ICatalogPage, IPageLocalization, IProduct, IPurchasableOffer, IPurchaseOptions, LocalizeText, NotificationAlertType, Offer, PageLocalization, PlacedObjectPurchaseData, PlaySound, Product, ProductTypeEnum, RequestedPage, SearchResult, SendMessageComposer, SoundNames } from '../../api'; @@ -89,27 +89,6 @@ const useCatalogStore = () => setCurrentType(normalizeCatalogType(type)); }, []); - // Real-time furni importati: ri-mergia il chunk custom/imported.json5 nelle Map - // furnidata + RoomContentLoader all'apertura del catalogo, SENZA reload del client. - const refreshImportedFurnidata = useCallback(() => - { - try - { - const base = GetConfiguration().getValue('furnidata.url'); - - if(!base || !base.length) return; - - const importedUrl = base.replace(/\/+$/, '') + '/custom/imported.json5'; - - GetSessionDataManager().mergeFurnitureDataFromUrl(importedUrl).then(added => - { - if(added && added.length) GetRoomContentLoader().processFurnitureData(added); - }).catch(() => {}); - } - catch - {} - }, []); - const openCatalogByType = useCallback((type?: string) => { const catalogType = normalizeCatalogType(type); @@ -119,10 +98,8 @@ const useCatalogStore = () => resetVisibleCatalogState(catalogType); } - refreshImportedFurnidata(); - setIsVisible(true); - }, [ currentType, resetVisibleCatalogState, refreshImportedFurnidata ]); + }, [ currentType, resetVisibleCatalogState ]); const toggleCatalogByType = useCallback((type?: string) => { @@ -140,10 +117,8 @@ const useCatalogStore = () => resetVisibleCatalogState(catalogType); } - refreshImportedFurnidata(); - setIsVisible(true); - }, [ isVisible, currentType, resetVisibleCatalogState, refreshImportedFurnidata ]); + }, [ isVisible, currentType, resetVisibleCatalogState ]); const getBuilderFurniPlaceableStatus = useCallback((offer: IPurchasableOffer) => { diff --git a/src/hooks/chat-history/useChatHistory.ts b/src/hooks/chat-history/useChatHistory.ts index 2cd021f..c26057a 100644 --- a/src/hooks/chat-history/useChatHistory.ts +++ b/src/hooks/chat-history/useChatHistory.ts @@ -12,11 +12,27 @@ const MESSENGER_HISTORY_MAX = 1000; let CHAT_HISTORY_COUNTER: number = 0; let MESSENGER_HISTORY_COUNTER: number = 0; +/** + * Project a list of chat entries to the slim shape we want to persist in + * localStorage. `imageUrl` is a base64 data URL of the avatar / pet head + * (10-50 KB each) - keeping it in storage blows past the browser quota + * inside minutes in a pet-heavy room. The avatar can always be re-rendered + * from `look` via ChatBubbleUtilities.getUserImage(), and pet images are + * regenerated from the bubble flow when needed; we just don't restore + * head thumbnails for entries loaded from a previous session. + * + * `style` / `chatType` / `color` are kept because they're tiny but + * meaningful for re-rendering the bubble. Translation fields are kept + * because they're already text. + */ +const slimChatEntriesForStorage = (entries: IChatEntry[]): IChatEntry[] => + entries.map(entry => entry.imageUrl ? { ...entry, imageUrl: undefined } : entry); + const useChatHistoryState = () => { - const [ chatHistory, setChatHistory ] = useLocalStorage('chatHistory', []); + const [ chatHistory, setChatHistory ] = useLocalStorage('chatHistory', [], { toStorage: slimChatEntriesForStorage }); const [ roomHistory, setRoomHistory ] = useLocalStorage('roomHistory', []); - const [ messengerHistory, setMessengerHistory ] = useLocalStorage('messengerHistory', []); + const [ messengerHistory, setMessengerHistory ] = useLocalStorage('messengerHistory', [], { toStorage: slimChatEntriesForStorage }); const [ needsRoomInsert, setNeedsRoomInsert ] = useLocalStorage('needsRoomInsert', false); const addChatEntry = (entry: IChatEntry) => diff --git a/src/hooks/mentions/__tests__/mentionsStore.test.ts b/src/hooks/mentions/__tests__/mentionsStore.test.ts index e85b5fb..2ddf3a8 100644 --- a/src/hooks/mentions/__tests__/mentionsStore.test.ts +++ b/src/hooks/mentions/__tests__/mentionsStore.test.ts @@ -40,4 +40,19 @@ describe('mentionsStore', () => expect(getUnreadCount()).toBe(1); expect(getMentionsSnapshot().find(m => m.mentionId === 1)!.read).toBe(true); }); + + it('drops mentions with non-positive id (defensive against id=0 spam)', () => + { + addMention(make(0)); + addMention(make(-1)); + expect(getMentionsSnapshot()).toHaveLength(0); + }); + + it('dedupes duplicate ids even after the legacy id !== 0 carve-out is gone', () => + { + addMention(make(7)); + addMention(make(7)); + addMention(make(7)); + expect(getMentionsSnapshot()).toHaveLength(1); + }); }); diff --git a/src/hooks/mentions/index.ts b/src/hooks/mentions/index.ts index f8b9e5f..3486da8 100644 --- a/src/hooks/mentions/index.ts +++ b/src/hooks/mentions/index.ts @@ -1,3 +1,2 @@ export * from './useMentionsSnapshot'; export * from './useMentionMessages'; -export * from './useMentionAutocomplete'; diff --git a/src/hooks/mentions/mentionsStore.ts b/src/hooks/mentions/mentionsStore.ts index 23d2b17..d2614e9 100644 --- a/src/hooks/mentions/mentionsStore.ts +++ b/src/hooks/mentions/mentionsStore.ts @@ -1,10 +1,21 @@ import { IMentionEntry } from '../../api/mentions'; +// Hard cap on how many mentions we hold in memory at once. The server's +// initial list is capped (mentions.store.limit, default 50) but live +// MentionReceived packets feed into addMention unbounded - so a server bug +// or a hostile/injected stream could otherwise grow the array and the DOM +// forever. 200 is comfortably more than any realistic active user has and +// well below anything that would inflate memory. +const MAX_MENTIONS = 200; + let mentions: IMentionEntry[] = []; const listeners = new Set<() => void>(); const emit = () => { for(const l of listeners) l(); }; +const cap = (list: IMentionEntry[]): IMentionEntry[] => + (list.length > MAX_MENTIONS) ? list.slice(0, MAX_MENTIONS) : list; + export const subscribeMentions = (onChange: () => void): (() => void) => { listeners.add(onChange); @@ -17,14 +28,21 @@ export const getUnreadCount = (): number => mentions.reduce((n, m) => n + (m.rea export const setMentions = (list: IMentionEntry[]): void => { - mentions = [...list].sort((a, b) => b.mentionId - a.mentionId); + mentions = cap([...list].sort((a, b) => b.mentionId - a.mentionId)); emit(); }; export const addMention = (entry: IMentionEntry): void => { - if(mentions.some(m => m.mentionId === entry.mentionId && entry.mentionId !== 0)) return; - mentions = [entry, ...mentions]; + // Drop entries the server failed to persist (generatedId 0 / negative). + // The server hardening already refuses to push these, but the client + // stays defensive in case a stale gameserver or an injected packet sends + // one - without this guard, the old "id !== 0" dedup carve-out let + // every duplicate through. + if(!entry || !Number.isFinite(entry.mentionId) || entry.mentionId <= 0) return; + if(mentions.some(m => m.mentionId === entry.mentionId)) return; + + mentions = cap([entry, ...mentions]); emit(); }; diff --git a/src/hooks/mentions/useMentionMessages.ts b/src/hooks/mentions/useMentionMessages.ts index 09cf0dd..6e4796b 100644 --- a/src/hooks/mentions/useMentionMessages.ts +++ b/src/hooks/mentions/useMentionMessages.ts @@ -1,15 +1,29 @@ import { MentionReceivedEvent, MentionsListEvent, RequestMentionsComposer } from '@nitrots/nitro-renderer'; -import { useCallback, useEffect } from 'react'; -import { GetConfigurationValue, IMentionEntry, PlaySound, SendMessageComposer } from '../../api'; +import { useCallback, useEffect, useRef } from 'react'; +import { GetConfigurationValue, IMentionEntry, LocalizeText, NotificationBubbleType, PlaySound, SendMessageComposer } from '../../api'; import { useMessageEvent } from '../events'; +import { useNotificationActions } from '../notification'; import { addMention, setMentions } from './mentionsStore'; -import { pushMentionToast } from './mentionToastsStore'; // Dedicated mention chime served from nitro-assets/sounds/.mp3. const MENTION_SOUND_SAMPLE = 'mentions_notification'; +// Floor on the gap between bubble/chime notifications. Even if the server +// (or an injected packet stream) pushes mentions faster than this, the user +// gets at most one chime + bubble per window. The mentions list itself +// still updates in real time - this only throttles the in-screen feedback. +const NOTIFICATION_THROTTLE_MS = 1500; +// Drop any single mention packet whose mention id we've already seen this +// session, so a replay attack can't re-trigger the bubble + sound even if +// the client store dropped the entry already. +const SEEN_IDS_MAX = 500; + export const useMentionMessages = (): void => { + const { showSingleBubble } = useNotificationActions(); + const lastNotificationRef = useRef(0); + const seenIdsRef = useRef>(new Set()); + const onMentionsList = useCallback((event: MentionsListEvent) => { const list = event.getParser().mentions; @@ -18,7 +32,7 @@ export const useMentionMessages = (): void => mentionId: m.mentionId, senderId: m.senderId, senderUsername: m.senderUsername, - senderFigure: m.senderFigure, + senderFigure: m.senderFigure ?? '', roomId: m.roomId, roomName: m.roomName, message: m.message, @@ -34,11 +48,22 @@ export const useMentionMessages = (): void => const m = event.getParser().mention; + if(!m || !Number.isFinite(m.mentionId) || m.mentionId <= 0) return; + + const seen = seenIdsRef.current; + if(seen.has(m.mentionId)) return; + seen.add(m.mentionId); + if(seen.size > SEEN_IDS_MAX) + { + const first = seen.values().next().value as number | undefined; + if(first !== undefined) seen.delete(first); + } + const entry: IMentionEntry = { mentionId: m.mentionId, senderId: m.senderId, senderUsername: m.senderUsername, - senderFigure: m.senderFigure, + senderFigure: m.senderFigure ?? '', roomId: m.roomId, roomName: m.roomName, message: m.message, @@ -49,11 +74,20 @@ export const useMentionMessages = (): void => addMention(entry); + const now = Date.now(); + if((now - lastNotificationRef.current) < NOTIFICATION_THROTTLE_MS) return; + lastNotificationRef.current = now; + if(GetConfigurationValue('mentions_ui.sound', true)) PlaySound(MENTION_SOUND_SAMPLE); - // Notifica laterale custom (avatar + messaggio + dismiss) invece del bubble generico. - pushMentionToast(entry); - }, []); + showSingleBubble( + LocalizeText('mentions.notification', [ 'sender', 'room' ], [ entry.senderUsername, entry.roomName ]), + NotificationBubbleType.INFO, + null, + 'mentions/toggle', + entry.senderUsername + ); + }, [ showSingleBubble ]); useMessageEvent(MentionsListEvent, onMentionsList); useMessageEvent(MentionReceivedEvent, onMentionReceived); diff --git a/src/hooks/navigator/navigatorUiStore.ts b/src/hooks/navigator/navigatorUiStore.ts index c4899d7..709207a 100644 --- a/src/hooks/navigator/navigatorUiStore.ts +++ b/src/hooks/navigator/navigatorUiStore.ts @@ -64,6 +64,6 @@ export const useNavigatorUiStore = createNitroStore set({ needsInit: false }), requestSearch: () => set({ needsSearch: true }), consumeSearchRequest: () => set({ needsSearch: false }), - setTab: (code) => set({ currentTabCode: code, currentFilter: '', isCreatorOpen: false }), + setTab: (code) => set({ currentTabCode: code, currentFilter: '' }), setFilter: (value) => set({ currentFilter: value }) })); diff --git a/src/hooks/rooms/widgets/useChatWidget.ts b/src/hooks/rooms/widgets/useChatWidget.ts index e05fd36..ed0b9ed 100644 --- a/src/hooks/rooms/widgets/useChatWidget.ts +++ b/src/hooks/rooms/widgets/useChatWidget.ts @@ -230,22 +230,31 @@ const useChatWidgetState = () => return newValue; }); - const chatEntryId = addChatEntry({ - id: -1, - webId: userData.webID, - entityId: userData.roomIndex, - name: username, - imageUrl, - style: styleId, - chatType: chatType, - entityType: userData.type, - message: formattedText, - timestamp: ChatHistoryCurrentDate(), - type: ChatEntryType.TYPE_CHAT, - roomId: roomSession.roomId, - color, - ...(outgoingTranslation ? buildTranslatedEntryPatch(outgoingTranslation.originalText, outgoingTranslation.translatedText, outgoingTranslation.detectedLanguage, outgoingTranslation.targetLanguage) : {}) - }); + + // Pet, Bot and Rentable Bot chat is fire-and-forget ("UDP-style"): + // the live bubble already rendered above, but we deliberately skip + // addChatEntry so the entry never lands in localStorage. A pet-heavy + // room used to push 30+ KB per message (base64 head data URL) into + // the chat history, exhausting the localStorage quota in minutes. + // Real users still go through the full persisted path. + const chatEntryId = (userType === RoomObjectType.USER) + ? addChatEntry({ + id: -1, + webId: userData.webID, + entityId: userData.roomIndex, + name: username, + imageUrl, + style: styleId, + chatType: chatType, + entityType: userData.type, + message: formattedText, + timestamp: ChatHistoryCurrentDate(), + type: ChatEntryType.TYPE_CHAT, + roomId: roomSession.roomId, + color, + ...(outgoingTranslation ? buildTranslatedEntryPatch(outgoingTranslation.originalText, outgoingTranslation.translatedText, outgoingTranslation.detectedLanguage, outgoingTranslation.targetLanguage) : {}) + }) + : -1; if(!settings.enabled || outgoingTranslation || !isTranslatableChatType || !text.trim().length) return; diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index cd73ced..13c00cc 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -1,10 +1,43 @@ import { NitroLogger } from '@nitrots/nitro-renderer'; -import { Dispatch, SetStateAction, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import { GetLocalStorage, SetLocalStorage } from '../api'; const userId = new URLSearchParams(window.location.search).get('userid') || 0; -const useLocalStorageState = (key: string, initialValue: T): [ T, Dispatch>] => +const STORAGE_WRITE_DEBOUNCE_MS = 250; +const QUOTA_TRIM_FACTOR = 0.5; // on quota error, keep the newest 50%. +const MIN_RETAINED_ENTRIES = 50; + +const isQuotaError = (error: unknown): boolean => +{ + if(!error || typeof error !== 'object') return false; + const name = (error as { name?: string }).name; + if(name === 'QuotaExceededError') return true; + // Firefox legacy: + if(name === 'NS_ERROR_DOM_QUOTA_REACHED') return true; + return false; +}; + +const trimArrayForQuota = (value: T): T => +{ + if(!Array.isArray(value)) return value; + if(value.length <= MIN_RETAINED_ENTRIES) return [] as unknown as T; + const keep = Math.max(MIN_RETAINED_ENTRIES, Math.floor(value.length * QUOTA_TRIM_FACTOR)); + return value.slice(value.length - keep) as unknown as T; +}; + +interface UseLocalStorageOptions +{ + /** + * Optional projection applied right before the value is written to + * localStorage. The in-memory React state is unaffected. Use this to + * strip heavy ephemeral fields (e.g. base64 image URLs) that would + * otherwise blow past the storage quota. + */ + toStorage?: (value: T) => unknown; +} + +const useLocalStorageState = (key: string, initialValue: T, options: UseLocalStorageOptions = {}): [ T, Dispatch>] => { key = userId ? `${ key }.${ userId }` : key; @@ -22,6 +55,91 @@ const useLocalStorageState = (key: string, initialValue: T): [ T, Dispatch(null); + const writeTimerRef = useRef | null>(null); + const optionsRef = useRef(options); + + // Keep the latest toStorage projection without re-running effects. + optionsRef.current = options; + + const flushWrite = (value: T) => + { + if(typeof window === 'undefined') return; + + const project = optionsRef.current.toStorage; + const projected = project ? project(value) : value; + + try + { + SetLocalStorage(key, projected); + return; + } + catch(error) + { + if(!isQuotaError(error)) + { + NitroLogger.error(error); + return; + } + } + + // Quota exceeded - trim and retry once. Anything that isn't an + // array gets cleared, since we have no generic trimming rule. + try + { + const trimmed = trimArrayForQuota(projected as T); + SetLocalStorage(key, trimmed); + NitroLogger.warn(`[useLocalStorage] quota exceeded for ${ key }, trimmed payload`); + } + catch(retryError) + { + NitroLogger.error(retryError); + // Last resort: drop the key entirely so future writes have room. + try { window.localStorage.removeItem(key); } catch(_) { /* ignore */ } + } + }; + + // Debounce: high-frequency chat would otherwise trigger one full + // JSON.stringify + setItem per message. We coalesce bursts into one + // write per STORAGE_WRITE_DEBOUNCE_MS window with the latest value. + const scheduleWrite = (value: T) => + { + pendingWriteRef.current = value; + if(writeTimerRef.current) clearTimeout(writeTimerRef.current); + writeTimerRef.current = setTimeout(() => + { + writeTimerRef.current = null; + if(pendingWriteRef.current !== null) + { + flushWrite(pendingWriteRef.current); + pendingWriteRef.current = null; + } + }, STORAGE_WRITE_DEBOUNCE_MS); + }; + + // Flush a pending write on tab close / hide so we don't lose the last + // burst of activity. + useEffect(() => + { + const flushOnLeave = () => + { + if(pendingWriteRef.current === null) return; + if(writeTimerRef.current) clearTimeout(writeTimerRef.current); + writeTimerRef.current = null; + flushWrite(pendingWriteRef.current); + pendingWriteRef.current = null; + }; + + window.addEventListener('pagehide', flushOnLeave); + window.addEventListener('beforeunload', flushOnLeave); + + return () => + { + window.removeEventListener('pagehide', flushOnLeave); + window.removeEventListener('beforeunload', flushOnLeave); + }; + }, []); + const setValue = (value: T) => { try @@ -30,7 +148,7 @@ const useLocalStorageState = (key: string, initialValue: T): [ T, Dispatch Date: Thu, 4 Jun 2026 13:43:29 +0200 Subject: [PATCH 3/9] =?UTF-8?q?Revert=20"=F0=9F=86=99=20Bug=20fixed=20in?= =?UTF-8?q?=20localstorage"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 47453db5ee819705c14314d173edcca54a3a910b. --- src/components/MainView.tsx | 4 +- src/components/mentions/index.ts | 1 + .../NotificationDefaultAlertView.tsx | 73 ++++++++++- .../ChatInputMentionSelectorView.tsx | 9 -- .../room/widgets/chat-input/ChatInputView.tsx | 40 +----- .../room/widgets/chat/highlightMentions.tsx | 69 +--------- .../notification/NotificationCenterView.css | 73 ++++++++++- src/hooks/catalog/useCatalog.ts | 31 ++++- src/hooks/chat-history/useChatHistory.ts | 20 +-- .../mentions/__tests__/mentionsStore.test.ts | 15 --- src/hooks/mentions/index.ts | 1 + src/hooks/mentions/mentionsStore.ts | 24 +--- src/hooks/mentions/useMentionMessages.ts | 50 ++----- src/hooks/navigator/navigatorUiStore.ts | 2 +- src/hooks/rooms/widgets/useChatWidget.ts | 41 +++--- src/hooks/useLocalStorage.ts | 124 +----------------- src/index.tsx | 1 + 17 files changed, 210 insertions(+), 368 deletions(-) diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index caa04ab..32eb535 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -48,7 +48,7 @@ import { UserAccountSettingsView } from './user-settings/UserAccountSettingsView import { UserSettingsView } from './user-settings/UserSettingsView'; import { WiredView } from './wired/WiredView'; import { WiredCreatorToolsView } from './wired-tools/WiredCreatorToolsView'; -import { MentionsView } from './mentions'; +import { MentionsView, MentionToastsView } from './mentions'; export const MainView: FC<{}> = props => { @@ -242,6 +242,8 @@ export const MainView: FC<{}> = props => { GetConfigurationValue('radio_ui.enabled', false) && } { (GetConfigurationValue('mentions_ui.enabled', true) && mentionsVisible) && setMentionsVisible(false) } /> } + { GetConfigurationValue('mentions_ui.enabled', true) && + } ); diff --git a/src/components/mentions/index.ts b/src/components/mentions/index.ts index 5eaab12..8c125c1 100644 --- a/src/components/mentions/index.ts +++ b/src/components/mentions/index.ts @@ -1,5 +1,6 @@ export * from './MentionMessageView'; export * from './MentionRowView'; export * from './MentionsView'; +export * from './MentionToastsView'; export * from './mentionsFormat'; export * from './useMentionActions'; diff --git a/src/components/notification-center/views/alert-layouts/NotificationDefaultAlertView.tsx b/src/components/notification-center/views/alert-layouts/NotificationDefaultAlertView.tsx index d9905e6..14c289d 100644 --- a/src/components/notification-center/views/alert-layouts/NotificationDefaultAlertView.tsx +++ b/src/components/notification-center/views/alert-layouts/NotificationDefaultAlertView.tsx @@ -1,5 +1,5 @@ -import { FC, useState } from 'react'; -import { LocalizeText, NotificationAlertItem, NotificationAlertType, OpenUrl } from '../../../../api'; +import { FC, useMemo, useState } from 'react'; +import { DispatchUiEvent, LocalizeText, NotificationAlertItem, NotificationAlertType, OpenUrl, RoomWidgetUpdateChatInputContentEvent } from '../../../../api'; import { Button, Column, Flex, LayoutNotificationAlertView, LayoutNotificationAlertViewProps } from '../../../../common'; interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewProps @@ -7,11 +7,57 @@ interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewP item: NotificationAlertItem; } +const COMMAND_LINE_PATTERN = /^\s*:[\w.-]+(?:\s.*)?$/; + +interface CommandTemplateEntry +{ + command: string; + description: string; +} + export const NotificationDefaultAlertView: FC = props => { - const { item = null, title = ((props.item && props.item.title) || ''), onClose = null, ...rest } = props; + const { item = null, title = ((props.item && props.item.title) || ''), onClose = null, classNames = [], ...rest } = props; const [ imageFailed, setImageFailed ] = useState(false); + const alertLines = useMemo(() => item.messages.flatMap(message => message.split(/\r\n|\r|\n/g)), [ item.messages ]); + const hasCommandTemplate = useMemo(() => + { + const commandLines = alertLines.filter(line => COMMAND_LINE_PATTERN.test(line)); + + return commandLines.length >= 4 || alertLines.some(line => /^Your Commands\(\d+\):?/i.test(line.trim())); + }, [ alertLines ]); + const commandTemplateContent = useMemo(() => + { + const intro: string[] = []; + const commands: CommandTemplateEntry[] = []; + + for(const rawLine of alertLines) + { + const text = rawLine.trim(); + + if(!text.length) continue; + + if(COMMAND_LINE_PATTERN.test(text)) + { + commands.push({ command: text, description: '' }); + continue; + } + + if(commands.length) + { + const lastCommand = commands[commands.length - 1]; + + lastCommand.description = lastCommand.description ? `${ lastCommand.description } ${ text }` : text; + continue; + } + + intro.push(text); + } + + return { intro, commands }; + }, [ alertLines ]); + const visitUrl = () => { OpenUrl(item.clickUrl); @@ -19,10 +65,18 @@ export const NotificationDefaultAlertView: FC onClose(); }; + const copyCommandToChatInput = (command: string) => + { + const chatValue = command.endsWith(' ') ? command : `${ command } `; + + DispatchUiEvent(new RoomWidgetUpdateChatInputContentEvent(RoomWidgetUpdateChatInputContentEvent.TEXT, chatValue)); + }; + const hasFrank = (item.alertType === NotificationAlertType.DEFAULT); + const alertClassNames = hasCommandTemplate ? [ ...classNames, 'nitro-alert-command-list' ] : classNames; return ( - + { hasFrank && !item.imageUrl &&
} { item.imageUrl && !imageFailed && { @@ -30,7 +84,16 @@ export const NotificationDefaultAlertView: FC setImageFailed(true); } } /> }
- { (item.messages.length > 0) && item.messages.map((message, index) => + { hasCommandTemplate &&
+ { commandTemplateContent.intro.map((text, index) => +
{ text }
) } + { commandTemplateContent.commands.map((entry, index) => + ) } +
} + { !hasCommandTemplate && (item.messages.length > 0) && item.messages.map((message, index) => { const htmlText = message.replace(/\r\n|\r|\n/g, '
'); diff --git a/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx index 3bf5913..370786f 100644 --- a/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx @@ -7,13 +7,9 @@ export interface MentionSuggestion { key: string; kind: MentionSuggestionKind; - /** Display name shown in the row (e.g. "DuckieTM" or "all"). */ name: string; - /** Token that's actually inserted into the chat input (without the @). */ insertToken: string; - /** Figure string for the avatar tile - only set for 'user' rows. */ figure?: string; - /** Optional sub-label, e.g. for "Staff Chat". */ description?: string; } @@ -25,11 +21,6 @@ interface ChatInputMentionSelectorViewProps onHover: (index: number) => void; } -/** - * @-autocomplete popover. Suggestion list comes pre-filtered from the parent: - * real users (RoomObjectUserType.USER = 1) only, never pets / bots / rentable - * bots / monster plants, plus the configured broadcast aliases. - */ export const ChatInputMentionSelectorView: FC = props => { const { suggestions = [], selectedIndex = 0, onSelect = null, onHover = null } = props; diff --git a/src/components/room/widgets/chat-input/ChatInputView.tsx b/src/components/room/widgets/chat-input/ChatInputView.tsx index 41f14f6..85d3198 100644 --- a/src/components/room/widgets/chat-input/ChatInputView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputView.tsx @@ -10,15 +10,9 @@ import { ChatInputEmojiSelectorView } from './ChatInputEmojiSelectorView'; import { ChatInputMentionSelectorView, MentionSuggestion } from './ChatInputMentionSelectorView'; import { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView'; -// RoomObjectUserType.AVATAR_TYPES: USER = 1, PET = 2, BOT = 3, RENTABLE_BOT = 4. -// Only real users can be mentioned, so the @-autocomplete keeps just type 1. const USER_TYPE_REAL_USER = 1; const MAX_MENTION_SUGGESTIONS = 8; -// Broadcast alias categories. The actual alias strings live in -// ui-config.json (mentions_ui.aliases.everyone / .friends / .room) so an -// admin can edit them without a rebuild and keep them in sync with the -// gameserver's mentions.*.aliases config keys. type MentionAliasScope = 'everyone' | 'friends' | 'room'; const MENTION_ALIAS_CONFIG_KEY: Record = { @@ -33,18 +27,12 @@ const MENTION_ALIAS_DEFAULTS: Record = { room: [ 'room', 'stanza' ] }; -// Localization keys for the description shown next to each alias in the -// picker. The actual translations live in UITexts (see -// text/UITexts_*.json5.example) so admins can localize without a rebuild. const MENTION_ALIAS_DESCRIPTION_KEY: Record = { everyone: 'mentions.alias.description.everyone', friends: 'mentions.alias.description.friends', room: 'mentions.alias.description.room' }; -// Coerces a configured value to a clean string[] - tolerates a missing -// config key, a non-array value, and non-string entries (so a typo in -// ui-config.json can't crash the picker). const sanitizeAliasList = (raw: unknown, fallback: string[]): string[] => { if(!Array.isArray(raw)) return fallback; @@ -71,13 +59,6 @@ export const ChatInputView: FC<{}> = props => const roomUserList = useRoomUserListSnapshot(); const [ mentionSelectedIndex, setMentionSelectedIndex ] = useState(0); - /** - * Detect an open @-mention token at the input caret. Returns the active - * query (text after @) plus the offsets we'll replace on selection, or - * null when the caret is not inside an @-token. Triggers only when the - * @ is at the start of the input or follows whitespace, so an email-like - * "foo@bar" doesn't pop the picker. - */ const mentionContext = useMemo(() => { if(!chatValue) return null; @@ -88,19 +69,14 @@ export const ChatInputView: FC<{}> = props => const at = upToCaret.lastIndexOf('@'); if(at < 0) return null; - // @ must be at start or follow whitespace. if(at > 0 && !/\s/.test(upToCaret.charAt(at - 1))) return null; const query = upToCaret.slice(at + 1); - // Bail if the query already contains whitespace - the token has ended. if(/\s/.test(query)) return null; return { atIndex: at, replaceFrom: at, replaceTo: caret, query }; }, [ chatValue, commandSelectorVisible ]); - // Flattened, config-driven alias list. Each scope's aliases are loaded - // from ui-config.json (with a typed fallback in case the key is missing - // or corrupted) and stitched in with the per-scope description. const mentionAliases = useMemo>(() => { const out: { key: string; scope: MentionAliasScope; description: string }[] = []; @@ -117,8 +93,7 @@ export const ChatInputView: FC<{}> = props => for(const key of list) { const lower = key.toLowerCase(); - // First-wins dedupe so an alias accidentally listed in two - // scopes shows once - same precedence the gameserver uses. + if(seen.has(lower)) continue; seen.add(lower); @@ -136,8 +111,6 @@ export const ChatInputView: FC<{}> = props => const query = mentionContext.query.toLowerCase(); const out: MentionSuggestion[] = []; - // 1. Real users in the room. Pets, bots, rentable bots and monster - // plants are filtered out by type. for(const user of roomUserList) { if(!user || user.type !== USER_TYPE_REAL_USER) continue; @@ -155,9 +128,6 @@ export const ChatInputView: FC<{}> = props => if(out.length >= MAX_MENTION_SUGGESTIONS) break; } - // 2. Broadcast aliases. The server permission-gates these (sender needs - // acc_mention_everyone / acc_mention_friends to actually fire) - the - // picker just surfaces them. for(const alias of mentionAliases) { if(query.length > 0 && !alias.key.toLowerCase().startsWith(query)) continue; @@ -178,8 +148,6 @@ export const ChatInputView: FC<{}> = props => const mentionSelectorVisible = mentionSuggestions.length > 0; - // Reset / clamp the highlighted row whenever the suggestion list changes - // so arrow-up/down doesn't keep an index that's now out of range. useEffect(() => { if(mentionSelectedIndex >= mentionSuggestions.length) setMentionSelectedIndex(0); @@ -196,8 +164,6 @@ export const ChatInputView: FC<{}> = props => setChatValue(next); - // Move the caret to right after the inserted mention so subsequent - // typing continues the message instead of editing the mention. requestAnimationFrame(() => { if(!inputRef.current) return; @@ -341,7 +307,6 @@ export const ChatInputView: FC<{}> = props => return; case 'Tab': event.preventDefault(); - // fall through case 'NumpadEnter': case 'Enter': { const selected = selectCurrent(); @@ -389,8 +354,7 @@ export const ChatInputView: FC<{}> = props => case 'Escape': event.preventDefault(); setMentionSelectedIndex(0); - // Closing without picking: drop the bare "@" so the - // picker doesn't immediately reopen on next render. + if(mentionContext) { const before = chatValue.slice(0, mentionContext.replaceFrom); diff --git a/src/components/room/widgets/chat/highlightMentions.tsx b/src/components/room/widgets/chat/highlightMentions.tsx index f418fab..6c8620e 100644 --- a/src/components/room/widgets/chat/highlightMentions.tsx +++ b/src/components/room/widgets/chat/highlightMentions.tsx @@ -1,48 +1,11 @@ -/** - * Cosmetic-only mention highlighting for in-room chat bubbles. - * - * The bubble text is rendered through {@link RoomChatFormatter}, which emits - * an HTML string (wired markup ``/``/``, font-colour - * ``, `
`, plus HTML-entity-encoded special characters) and - * is injected via `dangerouslySetInnerHTML`. We therefore operate on the - * already-formatted HTML string and wrap mention tokens that appear in the - * TEXT regions (never inside a ``), returning a new HTML string. This - * keeps every existing formatting behaviour intact and is purely visual — it - * does not touch `chat.text`, parsing, chat history, or any wire payload. - * - * Token detection mirrors the server's `MentionManager.process` exactly: - * - split on whitespace - * - a candidate token has length >= 2 and starts with `@` - * - strip the `@`, remove every char that is not [A-Za-z0-9_], lowercase - * - match against the local username or a room-broadcast alias - * - * This means `@Bob!`, `@bob,` etc. all match the nick `Bob` (case-insensitive) - * just like the server, while the highlighted span keeps the original token - * text (`@` + original casing + trailing punctuation) verbatim. - */ - -// Mirror of the three alias config keys in Arcturus -// (com.eu.habbo.habbohotel.mentions.MentionManager). -// Highlighting is purely visual - the server still gates @everyone / -// @friends on acc_mention_everyone / acc_mention_friends, so a normal -// user typing @all just gets a highlighted chat bubble with no actual -// notifications fired. export const MENTION_ROOM_ALIASES: ReadonlyArray = [ - // mentions.everyone.aliases default 'all', 'everyone', 'tutti', - // mentions.friends.aliases default 'friends', 'amici', - // mentions.room.aliases default 'room', 'stanza' ]; const NON_NICK_CHARS = /[^A-Za-z0-9_]/g; -/** - * Normalise a raw `@token` the same way the server does: drop the leading `@`, - * strip any non-nick characters (trailing punctuation, etc.), lowercase. - * Returns an empty string when nothing usable remains. - */ const normalizeToken = (token: string): string => { if(!token || token.length < 2 || token.charAt(0) !== '@') return ''; @@ -50,10 +13,7 @@ const normalizeToken = (token: string): string => return token.substring(1).replace(NON_NICK_CHARS, '').toLowerCase(); }; -/** - * Whether the given raw whitespace-delimited token mentions the local user - * or a room-broadcast alias. - */ + const isMentionToken = (token: string, ownUsernameLower: string, aliases: ReadonlySet): boolean => { const nick = normalizeToken(token); @@ -65,11 +25,6 @@ const isMentionToken = (token: string, ownUsernameLower: string, aliases: Readon return aliases.has(nick); }; -/** - * Public predicate: does this raw whitespace-delimited token mention the given - * user or a room-broadcast alias? Mirrors the server's detection. Reusable by - * UI that renders mention previews as React nodes (e.g. the mentions box). - */ export const tokenIsMention = ( token: string, ownUsername: string, @@ -83,17 +38,10 @@ export const tokenIsMention = ( const HIGHLIGHT_OPEN = ''; const HIGHLIGHT_CLOSE = ''; -/** - * Wrap mention tokens in a single text chunk (no HTML tags inside it). - * Whitespace runs between tokens are preserved verbatim by re-using the - * original substrings around each match. - */ const highlightTextChunk = (chunk: string, ownUsernameLower: string, aliases: ReadonlySet): string => { if(chunk.indexOf('@') < 0) return chunk; - // Split into alternating [whitespace, token, whitespace, token, ...] - // segments so the exact original spacing is rebuilt unchanged. const segments = chunk.split(/(\s+)/); let result = ''; @@ -102,7 +50,6 @@ const highlightTextChunk = (chunk: string, ownUsernameLower: string, aliases: Re { if(segment.length === 0) continue; - // Whitespace runs and non-mention tokens pass through untouched. if(/^\s+$/.test(segment) || !isMentionToken(segment, ownUsernameLower, aliases)) { result += segment; @@ -115,15 +62,6 @@ const highlightTextChunk = (chunk: string, ownUsernameLower: string, aliases: Re return result; }; -/** - * Take the formatted bubble HTML and return new HTML where every mention - * token (own nick or room alias) in the text regions is wrapped in - * ``. HTML tags are passed through - * untouched so existing markup keeps working. - * - * Returns the input unchanged when there is no `@`, no own username, and no - * possibility of a match (fast path), or when nothing matches. - */ export const highlightMentions = ( formattedHtml: string, ownUsername: string, @@ -135,10 +73,8 @@ export const highlightMentions = ( const ownUsernameLower = (ownUsername || '').replace(NON_NICK_CHARS, '').toLowerCase(); const aliasSet = new Set(aliases.map(a => a.toLowerCase())); - // Nothing could ever match → return verbatim. if(!ownUsernameLower && aliasSet.size === 0) return formattedHtml; - // Walk the string, only highlighting inside text regions (outside `<...>`). let result = ''; let cursor = 0; @@ -152,7 +88,6 @@ export const highlightMentions = ( break; } - // Text region before the next tag. if(tagStart > cursor) { result += highlightTextChunk(formattedHtml.slice(cursor, tagStart), ownUsernameLower, aliasSet); @@ -162,12 +97,10 @@ export const highlightMentions = ( if(tagEnd < 0) { - // Malformed trailing `<` with no closing `>` — emit the rest verbatim. result += formattedHtml.slice(tagStart); break; } - // Emit the tag (including the angle brackets) untouched. result += formattedHtml.slice(tagStart, tagEnd + 1); cursor = tagEnd + 1; } diff --git a/src/css/notification/NotificationCenterView.css b/src/css/notification/NotificationCenterView.css index c24cb67..2d43f52 100644 --- a/src/css/notification/NotificationCenterView.css +++ b/src/css/notification/NotificationCenterView.css @@ -20,6 +20,77 @@ } } + &.nitro-alert-command-list { + width: min(430px, calc(100vw - 18px)); + min-height: 210px; + max-height: min(520px, calc(100vh - 24px)); + + .content-area { + padding: 9px 10px 8px; + } + + .notification-text { + min-width: 0; + padding-right: 3px; + font-family: Ubuntu, sans-serif; + line-height: 1.25; + } + + .notification-command-template { + display: flex; + flex-direction: column; + gap: 4px; + padding-bottom: 2px; + } + + .notification-command-heading { + font-weight: 700; + color: #101010; + margin-bottom: 3px; + } + + .notification-command-copy { + color: #262626; + margin-bottom: 6px; + } + + .notification-command-row { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + min-height: 34px; + padding: 5px 8px; + color: #123b4c; + background: linear-gradient(180deg, #ffffff 0%, #dceaf0 100%); + border: 1px solid #8ca6b1; + border-radius: 4px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85); + text-align: left; + word-break: break-word; + } + + .notification-command-row:hover { + background: linear-gradient(180deg, #ffffff 0%, #cfe2eb 100%); + border-color: #4f879b; + } + + .notification-command-name { + font-weight: 700; + color: #123b4c; + } + + .notification-command-description { + font-size: 11px; + line-height: 1.2; + color: #3d4a50; + } + + .notification-command-spacer { + height: 3px; + } + } + &.nitro-alert-credits { width: 370px; .notification-text { @@ -390,4 +461,4 @@ position: relative; background-image: url("@/assets/images/notifications/nitro_v3.png"); background-repeat: no-repeat; -} \ No newline at end of file +} diff --git a/src/hooks/catalog/useCatalog.ts b/src/hooks/catalog/useCatalog.ts index 807b98e..807ef66 100644 --- a/src/hooks/catalog/useCatalog.ts +++ b/src/hooks/catalog/useCatalog.ts @@ -1,4 +1,4 @@ -import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomEngineObjectPlacedEvent, RoomObjectPlacementSource, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer'; +import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetConfiguration, GetRoomContentLoader, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomEngineObjectPlacedEvent, RoomObjectPlacementSource, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useBetween } from 'use-between'; import { BuilderFurniPlaceableStatus, CatalogPage, CatalogType, DispatchUiEvent, FurniCategory, GetFurnitureData, GetProductDataForLocalization, GetRoomSession, ICatalogNode, ICatalogPage, IPageLocalization, IProduct, IPurchasableOffer, IPurchaseOptions, LocalizeText, NotificationAlertType, Offer, PageLocalization, PlacedObjectPurchaseData, PlaySound, Product, ProductTypeEnum, RequestedPage, SearchResult, SendMessageComposer, SoundNames } from '../../api'; @@ -89,6 +89,27 @@ const useCatalogStore = () => setCurrentType(normalizeCatalogType(type)); }, []); + // Real-time furni importati: ri-mergia il chunk custom/imported.json5 nelle Map + // furnidata + RoomContentLoader all'apertura del catalogo, SENZA reload del client. + const refreshImportedFurnidata = useCallback(() => + { + try + { + const base = GetConfiguration().getValue('furnidata.url'); + + if(!base || !base.length) return; + + const importedUrl = base.replace(/\/+$/, '') + '/custom/imported.json5'; + + GetSessionDataManager().mergeFurnitureDataFromUrl(importedUrl).then(added => + { + if(added && added.length) GetRoomContentLoader().processFurnitureData(added); + }).catch(() => {}); + } + catch + {} + }, []); + const openCatalogByType = useCallback((type?: string) => { const catalogType = normalizeCatalogType(type); @@ -98,8 +119,10 @@ const useCatalogStore = () => resetVisibleCatalogState(catalogType); } + refreshImportedFurnidata(); + setIsVisible(true); - }, [ currentType, resetVisibleCatalogState ]); + }, [ currentType, resetVisibleCatalogState, refreshImportedFurnidata ]); const toggleCatalogByType = useCallback((type?: string) => { @@ -117,8 +140,10 @@ const useCatalogStore = () => resetVisibleCatalogState(catalogType); } + refreshImportedFurnidata(); + setIsVisible(true); - }, [ isVisible, currentType, resetVisibleCatalogState ]); + }, [ isVisible, currentType, resetVisibleCatalogState, refreshImportedFurnidata ]); const getBuilderFurniPlaceableStatus = useCallback((offer: IPurchasableOffer) => { diff --git a/src/hooks/chat-history/useChatHistory.ts b/src/hooks/chat-history/useChatHistory.ts index c26057a..2cd021f 100644 --- a/src/hooks/chat-history/useChatHistory.ts +++ b/src/hooks/chat-history/useChatHistory.ts @@ -12,27 +12,11 @@ const MESSENGER_HISTORY_MAX = 1000; let CHAT_HISTORY_COUNTER: number = 0; let MESSENGER_HISTORY_COUNTER: number = 0; -/** - * Project a list of chat entries to the slim shape we want to persist in - * localStorage. `imageUrl` is a base64 data URL of the avatar / pet head - * (10-50 KB each) - keeping it in storage blows past the browser quota - * inside minutes in a pet-heavy room. The avatar can always be re-rendered - * from `look` via ChatBubbleUtilities.getUserImage(), and pet images are - * regenerated from the bubble flow when needed; we just don't restore - * head thumbnails for entries loaded from a previous session. - * - * `style` / `chatType` / `color` are kept because they're tiny but - * meaningful for re-rendering the bubble. Translation fields are kept - * because they're already text. - */ -const slimChatEntriesForStorage = (entries: IChatEntry[]): IChatEntry[] => - entries.map(entry => entry.imageUrl ? { ...entry, imageUrl: undefined } : entry); - const useChatHistoryState = () => { - const [ chatHistory, setChatHistory ] = useLocalStorage('chatHistory', [], { toStorage: slimChatEntriesForStorage }); + const [ chatHistory, setChatHistory ] = useLocalStorage('chatHistory', []); const [ roomHistory, setRoomHistory ] = useLocalStorage('roomHistory', []); - const [ messengerHistory, setMessengerHistory ] = useLocalStorage('messengerHistory', [], { toStorage: slimChatEntriesForStorage }); + const [ messengerHistory, setMessengerHistory ] = useLocalStorage('messengerHistory', []); const [ needsRoomInsert, setNeedsRoomInsert ] = useLocalStorage('needsRoomInsert', false); const addChatEntry = (entry: IChatEntry) => diff --git a/src/hooks/mentions/__tests__/mentionsStore.test.ts b/src/hooks/mentions/__tests__/mentionsStore.test.ts index 2ddf3a8..e85b5fb 100644 --- a/src/hooks/mentions/__tests__/mentionsStore.test.ts +++ b/src/hooks/mentions/__tests__/mentionsStore.test.ts @@ -40,19 +40,4 @@ describe('mentionsStore', () => expect(getUnreadCount()).toBe(1); expect(getMentionsSnapshot().find(m => m.mentionId === 1)!.read).toBe(true); }); - - it('drops mentions with non-positive id (defensive against id=0 spam)', () => - { - addMention(make(0)); - addMention(make(-1)); - expect(getMentionsSnapshot()).toHaveLength(0); - }); - - it('dedupes duplicate ids even after the legacy id !== 0 carve-out is gone', () => - { - addMention(make(7)); - addMention(make(7)); - addMention(make(7)); - expect(getMentionsSnapshot()).toHaveLength(1); - }); }); diff --git a/src/hooks/mentions/index.ts b/src/hooks/mentions/index.ts index 3486da8..f8b9e5f 100644 --- a/src/hooks/mentions/index.ts +++ b/src/hooks/mentions/index.ts @@ -1,2 +1,3 @@ export * from './useMentionsSnapshot'; export * from './useMentionMessages'; +export * from './useMentionAutocomplete'; diff --git a/src/hooks/mentions/mentionsStore.ts b/src/hooks/mentions/mentionsStore.ts index d2614e9..23d2b17 100644 --- a/src/hooks/mentions/mentionsStore.ts +++ b/src/hooks/mentions/mentionsStore.ts @@ -1,21 +1,10 @@ import { IMentionEntry } from '../../api/mentions'; -// Hard cap on how many mentions we hold in memory at once. The server's -// initial list is capped (mentions.store.limit, default 50) but live -// MentionReceived packets feed into addMention unbounded - so a server bug -// or a hostile/injected stream could otherwise grow the array and the DOM -// forever. 200 is comfortably more than any realistic active user has and -// well below anything that would inflate memory. -const MAX_MENTIONS = 200; - let mentions: IMentionEntry[] = []; const listeners = new Set<() => void>(); const emit = () => { for(const l of listeners) l(); }; -const cap = (list: IMentionEntry[]): IMentionEntry[] => - (list.length > MAX_MENTIONS) ? list.slice(0, MAX_MENTIONS) : list; - export const subscribeMentions = (onChange: () => void): (() => void) => { listeners.add(onChange); @@ -28,21 +17,14 @@ export const getUnreadCount = (): number => mentions.reduce((n, m) => n + (m.rea export const setMentions = (list: IMentionEntry[]): void => { - mentions = cap([...list].sort((a, b) => b.mentionId - a.mentionId)); + mentions = [...list].sort((a, b) => b.mentionId - a.mentionId); emit(); }; export const addMention = (entry: IMentionEntry): void => { - // Drop entries the server failed to persist (generatedId 0 / negative). - // The server hardening already refuses to push these, but the client - // stays defensive in case a stale gameserver or an injected packet sends - // one - without this guard, the old "id !== 0" dedup carve-out let - // every duplicate through. - if(!entry || !Number.isFinite(entry.mentionId) || entry.mentionId <= 0) return; - if(mentions.some(m => m.mentionId === entry.mentionId)) return; - - mentions = cap([entry, ...mentions]); + if(mentions.some(m => m.mentionId === entry.mentionId && entry.mentionId !== 0)) return; + mentions = [entry, ...mentions]; emit(); }; diff --git a/src/hooks/mentions/useMentionMessages.ts b/src/hooks/mentions/useMentionMessages.ts index 6e4796b..09cf0dd 100644 --- a/src/hooks/mentions/useMentionMessages.ts +++ b/src/hooks/mentions/useMentionMessages.ts @@ -1,29 +1,15 @@ import { MentionReceivedEvent, MentionsListEvent, RequestMentionsComposer } from '@nitrots/nitro-renderer'; -import { useCallback, useEffect, useRef } from 'react'; -import { GetConfigurationValue, IMentionEntry, LocalizeText, NotificationBubbleType, PlaySound, SendMessageComposer } from '../../api'; +import { useCallback, useEffect } from 'react'; +import { GetConfigurationValue, IMentionEntry, PlaySound, SendMessageComposer } from '../../api'; import { useMessageEvent } from '../events'; -import { useNotificationActions } from '../notification'; import { addMention, setMentions } from './mentionsStore'; +import { pushMentionToast } from './mentionToastsStore'; // Dedicated mention chime served from nitro-assets/sounds/.mp3. const MENTION_SOUND_SAMPLE = 'mentions_notification'; -// Floor on the gap between bubble/chime notifications. Even if the server -// (or an injected packet stream) pushes mentions faster than this, the user -// gets at most one chime + bubble per window. The mentions list itself -// still updates in real time - this only throttles the in-screen feedback. -const NOTIFICATION_THROTTLE_MS = 1500; -// Drop any single mention packet whose mention id we've already seen this -// session, so a replay attack can't re-trigger the bubble + sound even if -// the client store dropped the entry already. -const SEEN_IDS_MAX = 500; - export const useMentionMessages = (): void => { - const { showSingleBubble } = useNotificationActions(); - const lastNotificationRef = useRef(0); - const seenIdsRef = useRef>(new Set()); - const onMentionsList = useCallback((event: MentionsListEvent) => { const list = event.getParser().mentions; @@ -32,7 +18,7 @@ export const useMentionMessages = (): void => mentionId: m.mentionId, senderId: m.senderId, senderUsername: m.senderUsername, - senderFigure: m.senderFigure ?? '', + senderFigure: m.senderFigure, roomId: m.roomId, roomName: m.roomName, message: m.message, @@ -48,22 +34,11 @@ export const useMentionMessages = (): void => const m = event.getParser().mention; - if(!m || !Number.isFinite(m.mentionId) || m.mentionId <= 0) return; - - const seen = seenIdsRef.current; - if(seen.has(m.mentionId)) return; - seen.add(m.mentionId); - if(seen.size > SEEN_IDS_MAX) - { - const first = seen.values().next().value as number | undefined; - if(first !== undefined) seen.delete(first); - } - const entry: IMentionEntry = { mentionId: m.mentionId, senderId: m.senderId, senderUsername: m.senderUsername, - senderFigure: m.senderFigure ?? '', + senderFigure: m.senderFigure, roomId: m.roomId, roomName: m.roomName, message: m.message, @@ -74,20 +49,11 @@ export const useMentionMessages = (): void => addMention(entry); - const now = Date.now(); - if((now - lastNotificationRef.current) < NOTIFICATION_THROTTLE_MS) return; - lastNotificationRef.current = now; - if(GetConfigurationValue('mentions_ui.sound', true)) PlaySound(MENTION_SOUND_SAMPLE); - showSingleBubble( - LocalizeText('mentions.notification', [ 'sender', 'room' ], [ entry.senderUsername, entry.roomName ]), - NotificationBubbleType.INFO, - null, - 'mentions/toggle', - entry.senderUsername - ); - }, [ showSingleBubble ]); + // Notifica laterale custom (avatar + messaggio + dismiss) invece del bubble generico. + pushMentionToast(entry); + }, []); useMessageEvent(MentionsListEvent, onMentionsList); useMessageEvent(MentionReceivedEvent, onMentionReceived); diff --git a/src/hooks/navigator/navigatorUiStore.ts b/src/hooks/navigator/navigatorUiStore.ts index 709207a..c4899d7 100644 --- a/src/hooks/navigator/navigatorUiStore.ts +++ b/src/hooks/navigator/navigatorUiStore.ts @@ -64,6 +64,6 @@ export const useNavigatorUiStore = createNitroStore set({ needsInit: false }), requestSearch: () => set({ needsSearch: true }), consumeSearchRequest: () => set({ needsSearch: false }), - setTab: (code) => set({ currentTabCode: code, currentFilter: '' }), + setTab: (code) => set({ currentTabCode: code, currentFilter: '', isCreatorOpen: false }), setFilter: (value) => set({ currentFilter: value }) })); diff --git a/src/hooks/rooms/widgets/useChatWidget.ts b/src/hooks/rooms/widgets/useChatWidget.ts index ed0b9ed..e05fd36 100644 --- a/src/hooks/rooms/widgets/useChatWidget.ts +++ b/src/hooks/rooms/widgets/useChatWidget.ts @@ -230,31 +230,22 @@ const useChatWidgetState = () => return newValue; }); - - // Pet, Bot and Rentable Bot chat is fire-and-forget ("UDP-style"): - // the live bubble already rendered above, but we deliberately skip - // addChatEntry so the entry never lands in localStorage. A pet-heavy - // room used to push 30+ KB per message (base64 head data URL) into - // the chat history, exhausting the localStorage quota in minutes. - // Real users still go through the full persisted path. - const chatEntryId = (userType === RoomObjectType.USER) - ? addChatEntry({ - id: -1, - webId: userData.webID, - entityId: userData.roomIndex, - name: username, - imageUrl, - style: styleId, - chatType: chatType, - entityType: userData.type, - message: formattedText, - timestamp: ChatHistoryCurrentDate(), - type: ChatEntryType.TYPE_CHAT, - roomId: roomSession.roomId, - color, - ...(outgoingTranslation ? buildTranslatedEntryPatch(outgoingTranslation.originalText, outgoingTranslation.translatedText, outgoingTranslation.detectedLanguage, outgoingTranslation.targetLanguage) : {}) - }) - : -1; + const chatEntryId = addChatEntry({ + id: -1, + webId: userData.webID, + entityId: userData.roomIndex, + name: username, + imageUrl, + style: styleId, + chatType: chatType, + entityType: userData.type, + message: formattedText, + timestamp: ChatHistoryCurrentDate(), + type: ChatEntryType.TYPE_CHAT, + roomId: roomSession.roomId, + color, + ...(outgoingTranslation ? buildTranslatedEntryPatch(outgoingTranslation.originalText, outgoingTranslation.translatedText, outgoingTranslation.detectedLanguage, outgoingTranslation.targetLanguage) : {}) + }); if(!settings.enabled || outgoingTranslation || !isTranslatableChatType || !text.trim().length) return; diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index 13c00cc..cd73ced 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -1,43 +1,10 @@ import { NitroLogger } from '@nitrots/nitro-renderer'; -import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; +import { Dispatch, SetStateAction, useState } from 'react'; import { GetLocalStorage, SetLocalStorage } from '../api'; const userId = new URLSearchParams(window.location.search).get('userid') || 0; -const STORAGE_WRITE_DEBOUNCE_MS = 250; -const QUOTA_TRIM_FACTOR = 0.5; // on quota error, keep the newest 50%. -const MIN_RETAINED_ENTRIES = 50; - -const isQuotaError = (error: unknown): boolean => -{ - if(!error || typeof error !== 'object') return false; - const name = (error as { name?: string }).name; - if(name === 'QuotaExceededError') return true; - // Firefox legacy: - if(name === 'NS_ERROR_DOM_QUOTA_REACHED') return true; - return false; -}; - -const trimArrayForQuota = (value: T): T => -{ - if(!Array.isArray(value)) return value; - if(value.length <= MIN_RETAINED_ENTRIES) return [] as unknown as T; - const keep = Math.max(MIN_RETAINED_ENTRIES, Math.floor(value.length * QUOTA_TRIM_FACTOR)); - return value.slice(value.length - keep) as unknown as T; -}; - -interface UseLocalStorageOptions -{ - /** - * Optional projection applied right before the value is written to - * localStorage. The in-memory React state is unaffected. Use this to - * strip heavy ephemeral fields (e.g. base64 image URLs) that would - * otherwise blow past the storage quota. - */ - toStorage?: (value: T) => unknown; -} - -const useLocalStorageState = (key: string, initialValue: T, options: UseLocalStorageOptions = {}): [ T, Dispatch>] => +const useLocalStorageState = (key: string, initialValue: T): [ T, Dispatch>] => { key = userId ? `${ key }.${ userId }` : key; @@ -55,91 +22,6 @@ const useLocalStorageState = (key: string, initialValue: T, options: UseLocal } }); - const pendingWriteRef = useRef(null); - const writeTimerRef = useRef | null>(null); - const optionsRef = useRef(options); - - // Keep the latest toStorage projection without re-running effects. - optionsRef.current = options; - - const flushWrite = (value: T) => - { - if(typeof window === 'undefined') return; - - const project = optionsRef.current.toStorage; - const projected = project ? project(value) : value; - - try - { - SetLocalStorage(key, projected); - return; - } - catch(error) - { - if(!isQuotaError(error)) - { - NitroLogger.error(error); - return; - } - } - - // Quota exceeded - trim and retry once. Anything that isn't an - // array gets cleared, since we have no generic trimming rule. - try - { - const trimmed = trimArrayForQuota(projected as T); - SetLocalStorage(key, trimmed); - NitroLogger.warn(`[useLocalStorage] quota exceeded for ${ key }, trimmed payload`); - } - catch(retryError) - { - NitroLogger.error(retryError); - // Last resort: drop the key entirely so future writes have room. - try { window.localStorage.removeItem(key); } catch(_) { /* ignore */ } - } - }; - - // Debounce: high-frequency chat would otherwise trigger one full - // JSON.stringify + setItem per message. We coalesce bursts into one - // write per STORAGE_WRITE_DEBOUNCE_MS window with the latest value. - const scheduleWrite = (value: T) => - { - pendingWriteRef.current = value; - if(writeTimerRef.current) clearTimeout(writeTimerRef.current); - writeTimerRef.current = setTimeout(() => - { - writeTimerRef.current = null; - if(pendingWriteRef.current !== null) - { - flushWrite(pendingWriteRef.current); - pendingWriteRef.current = null; - } - }, STORAGE_WRITE_DEBOUNCE_MS); - }; - - // Flush a pending write on tab close / hide so we don't lose the last - // burst of activity. - useEffect(() => - { - const flushOnLeave = () => - { - if(pendingWriteRef.current === null) return; - if(writeTimerRef.current) clearTimeout(writeTimerRef.current); - writeTimerRef.current = null; - flushWrite(pendingWriteRef.current); - pendingWriteRef.current = null; - }; - - window.addEventListener('pagehide', flushOnLeave); - window.addEventListener('beforeunload', flushOnLeave); - - return () => - { - window.removeEventListener('pagehide', flushOnLeave); - window.removeEventListener('beforeunload', flushOnLeave); - }; - }, []); - const setValue = (value: T) => { try @@ -148,7 +30,7 @@ const useLocalStorageState = (key: string, initialValue: T, options: UseLocal setStoredValue(valueToStore); - scheduleWrite(valueToStore); + if(typeof window !== 'undefined') SetLocalStorage(key, valueToStore); } catch(error) diff --git a/src/index.tsx b/src/index.tsx index 3825168..d1f1b0f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,6 +23,7 @@ import './css/catalog/CatalogClassicView.css'; import './css/emustats/EmuStatsView.css'; import './css/chat/Chats.css'; +import './css/mentions/MentionToasts.css'; import './css/common/Buttons.css'; From 9982c96b6383fd505576aed929f834e79d30da50 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 4 Jun 2026 13:50:40 +0200 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=86=99=20Bug=20fixed=20in=20localstor?= =?UTF-8?q?age?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/chat-history/useChatHistory.ts | 7 +- src/hooks/rooms/widgets/useChatWidget.ts | 40 ++++----- src/hooks/useLocalStorage.ts | 108 ++++++++++++++++++++++- 3 files changed, 129 insertions(+), 26 deletions(-) diff --git a/src/hooks/chat-history/useChatHistory.ts b/src/hooks/chat-history/useChatHistory.ts index 2cd021f..e935296 100644 --- a/src/hooks/chat-history/useChatHistory.ts +++ b/src/hooks/chat-history/useChatHistory.ts @@ -12,11 +12,14 @@ const MESSENGER_HISTORY_MAX = 1000; let CHAT_HISTORY_COUNTER: number = 0; let MESSENGER_HISTORY_COUNTER: number = 0; +const slimChatEntriesForStorage = (entries: IChatEntry[]): IChatEntry[] => + entries.map(entry => entry.imageUrl ? { ...entry, imageUrl: undefined } : entry); + const useChatHistoryState = () => { - const [ chatHistory, setChatHistory ] = useLocalStorage('chatHistory', []); + const [ chatHistory, setChatHistory ] = useLocalStorage('chatHistory', [], { toStorage: slimChatEntriesForStorage }); const [ roomHistory, setRoomHistory ] = useLocalStorage('roomHistory', []); - const [ messengerHistory, setMessengerHistory ] = useLocalStorage('messengerHistory', []); + const [ messengerHistory, setMessengerHistory ] = useLocalStorage('messengerHistory', [], { toStorage: slimChatEntriesForStorage }); const [ needsRoomInsert, setNeedsRoomInsert ] = useLocalStorage('needsRoomInsert', false); const addChatEntry = (entry: IChatEntry) => diff --git a/src/hooks/rooms/widgets/useChatWidget.ts b/src/hooks/rooms/widgets/useChatWidget.ts index e05fd36..ea61c6f 100644 --- a/src/hooks/rooms/widgets/useChatWidget.ts +++ b/src/hooks/rooms/widgets/useChatWidget.ts @@ -23,11 +23,6 @@ const useChatWidgetState = () => const { addChatEntry, updateChatEntry } = useChatHistory(); const { settings, translateIncoming, consumeOutgoingTranslation } = useTranslation(); const isDisposed = useRef(false); - // Reactive: re-renders if the session-data snapshot flips (e.g. - // reconnect under a different user id). Safe to call here — - // useChatWidget is NOT wrapped in useBetween (see export below), - // so the real React dispatcher is in scope and - // useSyncExternalStore installs correctly. const ownUserId = (useUserDataSnapshot().userId || -1); const applyTranslationToBubble = useCallback((chatMessage: ChatBubbleMessage, originalText: string, translatedText: string, detectedLanguage: string, targetLanguage: string) => @@ -230,22 +225,25 @@ const useChatWidgetState = () => return newValue; }); - const chatEntryId = addChatEntry({ - id: -1, - webId: userData.webID, - entityId: userData.roomIndex, - name: username, - imageUrl, - style: styleId, - chatType: chatType, - entityType: userData.type, - message: formattedText, - timestamp: ChatHistoryCurrentDate(), - type: ChatEntryType.TYPE_CHAT, - roomId: roomSession.roomId, - color, - ...(outgoingTranslation ? buildTranslatedEntryPatch(outgoingTranslation.originalText, outgoingTranslation.translatedText, outgoingTranslation.detectedLanguage, outgoingTranslation.targetLanguage) : {}) - }); + + const chatEntryId = (userType === RoomObjectType.USER) + ? addChatEntry({ + id: -1, + webId: userData.webID, + entityId: userData.roomIndex, + name: username, + imageUrl, + style: styleId, + chatType: chatType, + entityType: userData.type, + message: formattedText, + timestamp: ChatHistoryCurrentDate(), + type: ChatEntryType.TYPE_CHAT, + roomId: roomSession.roomId, + color, + ...(outgoingTranslation ? buildTranslatedEntryPatch(outgoingTranslation.originalText, outgoingTranslation.translatedText, outgoingTranslation.detectedLanguage, outgoingTranslation.targetLanguage) : {}) + }) + : -1; if(!settings.enabled || outgoingTranslation || !isTranslatableChatType || !text.trim().length) return; diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index cd73ced..1e760c8 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -1,10 +1,36 @@ import { NitroLogger } from '@nitrots/nitro-renderer'; -import { Dispatch, SetStateAction, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import { GetLocalStorage, SetLocalStorage } from '../api'; const userId = new URLSearchParams(window.location.search).get('userid') || 0; -const useLocalStorageState = (key: string, initialValue: T): [ T, Dispatch>] => +const STORAGE_WRITE_DEBOUNCE_MS = 250; +const QUOTA_TRIM_FACTOR = 0.5; // on quota error, keep the newest 50%. +const MIN_RETAINED_ENTRIES = 50; + +const isQuotaError = (error: unknown): boolean => +{ + if(!error || typeof error !== 'object') return false; + const name = (error as { name?: string }).name; + if(name === 'QuotaExceededError') return true; + if(name === 'NS_ERROR_DOM_QUOTA_REACHED') return true; + return false; +}; + +const trimArrayForQuota = (value: T): T => +{ + if(!Array.isArray(value)) return value; + if(value.length <= MIN_RETAINED_ENTRIES) return [] as unknown as T; + const keep = Math.max(MIN_RETAINED_ENTRIES, Math.floor(value.length * QUOTA_TRIM_FACTOR)); + return value.slice(value.length - keep) as unknown as T; +}; + +interface UseLocalStorageOptions +{ + toStorage?: (value: T) => unknown; +} + +const useLocalStorageState = (key: string, initialValue: T, options: UseLocalStorageOptions = {}): [ T, Dispatch>] => { key = userId ? `${ key }.${ userId }` : key; @@ -22,6 +48,82 @@ const useLocalStorageState = (key: string, initialValue: T): [ T, Dispatch(null); + const writeTimerRef = useRef | null>(null); + const optionsRef = useRef(options); + + optionsRef.current = options; + + const flushWrite = (value: T) => + { + if(typeof window === 'undefined') return; + + const project = optionsRef.current.toStorage; + const projected = project ? project(value) : value; + + try + { + SetLocalStorage(key, projected); + return; + } + catch(error) + { + if(!isQuotaError(error)) + { + NitroLogger.error(error); + return; + } + } + + try + { + const trimmed = trimArrayForQuota(projected as T); + SetLocalStorage(key, trimmed); + NitroLogger.warn(`[useLocalStorage] quota exceeded for ${ key }, trimmed payload`); + } + catch(retryError) + { + NitroLogger.error(retryError); + try { window.localStorage.removeItem(key); } catch(_) { } + } + }; + + const scheduleWrite = (value: T) => + { + pendingWriteRef.current = value; + if(writeTimerRef.current) clearTimeout(writeTimerRef.current); + writeTimerRef.current = setTimeout(() => + { + writeTimerRef.current = null; + if(pendingWriteRef.current !== null) + { + flushWrite(pendingWriteRef.current); + pendingWriteRef.current = null; + } + }, STORAGE_WRITE_DEBOUNCE_MS); + }; + + useEffect(() => + { + const flushOnLeave = () => + { + if(pendingWriteRef.current === null) return; + if(writeTimerRef.current) clearTimeout(writeTimerRef.current); + writeTimerRef.current = null; + flushWrite(pendingWriteRef.current); + pendingWriteRef.current = null; + }; + + window.addEventListener('pagehide', flushOnLeave); + window.addEventListener('beforeunload', flushOnLeave); + + return () => + { + window.removeEventListener('pagehide', flushOnLeave); + window.removeEventListener('beforeunload', flushOnLeave); + }; + }, []); + const setValue = (value: T) => { try @@ -30,7 +132,7 @@ const useLocalStorageState = (key: string, initialValue: T): [ T, Dispatch Date: Thu, 4 Jun 2026 18:16:43 +0200 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=86=99=20Take=20#1=201:1=20Habbo=20in?= =?UTF-8?q?=20old=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/NotificationCenterView.css | 73 +------------------ 1 file changed, 1 insertion(+), 72 deletions(-) diff --git a/src/css/notification/NotificationCenterView.css b/src/css/notification/NotificationCenterView.css index 2d43f52..c24cb67 100644 --- a/src/css/notification/NotificationCenterView.css +++ b/src/css/notification/NotificationCenterView.css @@ -20,77 +20,6 @@ } } - &.nitro-alert-command-list { - width: min(430px, calc(100vw - 18px)); - min-height: 210px; - max-height: min(520px, calc(100vh - 24px)); - - .content-area { - padding: 9px 10px 8px; - } - - .notification-text { - min-width: 0; - padding-right: 3px; - font-family: Ubuntu, sans-serif; - line-height: 1.25; - } - - .notification-command-template { - display: flex; - flex-direction: column; - gap: 4px; - padding-bottom: 2px; - } - - .notification-command-heading { - font-weight: 700; - color: #101010; - margin-bottom: 3px; - } - - .notification-command-copy { - color: #262626; - margin-bottom: 6px; - } - - .notification-command-row { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 2px; - min-height: 34px; - padding: 5px 8px; - color: #123b4c; - background: linear-gradient(180deg, #ffffff 0%, #dceaf0 100%); - border: 1px solid #8ca6b1; - border-radius: 4px; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85); - text-align: left; - word-break: break-word; - } - - .notification-command-row:hover { - background: linear-gradient(180deg, #ffffff 0%, #cfe2eb 100%); - border-color: #4f879b; - } - - .notification-command-name { - font-weight: 700; - color: #123b4c; - } - - .notification-command-description { - font-size: 11px; - line-height: 1.2; - color: #3d4a50; - } - - .notification-command-spacer { - height: 3px; - } - } - &.nitro-alert-credits { width: 370px; .notification-text { @@ -461,4 +390,4 @@ position: relative; background-image: url("@/assets/images/notifications/nitro_v3.png"); background-repeat: no-repeat; -} +} \ No newline at end of file From d73656bbfea74973c0625d8b4a3b9e5352d9c66b Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Thu, 4 Jun 2026 22:15:12 +0200 Subject: [PATCH 6/9] chore: update react player and frontend deps --- package.json | 46 +- .../furniture/FurnitureYoutubeDisplayView.tsx | 11 +- src/components/toolbar/YouTubePlayerView.tsx | 11 +- src/components/youtube/YoutubeReactPlayer.ts | 28 + vite.config.mjs | 3 + yarn.lock | 1265 ++++++++++------- 6 files changed, 838 insertions(+), 526 deletions(-) create mode 100644 src/components/youtube/YoutubeReactPlayer.ts diff --git a/package.json b/package.json index 85c6c78..583190b 100644 --- a/package.json +++ b/package.json @@ -18,55 +18,55 @@ "test:watch": "vitest" }, "dependencies": { - "@babel/runtime": "^7.29.2", + "@babel/runtime": "^7.29.7", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-slider": "^1.3.6", "@tanstack/react-query": "5", "@tanstack/react-query-devtools": "5", - "@tanstack/react-virtual": "^3.13.24", - "dompurify": "^3.4.2", + "@tanstack/react-virtual": "^3.14.2", + "dompurify": "^3.4.8", "emoji-mart": "^5.6.0", "emoji-toolkit": "10.0.0", - "framer-motion": "^12.38.0", + "framer-motion": "^12.40.0", "json5": "^2.2.3", - "react": "^19.2.5", + "react": "^19.2.7", "react-colorful": "^5.7.0", - "react-dom": "^19.2.5", - "react-error-boundary": "^6.1.1", + "react-dom": "^19.2.7", + "react-error-boundary": "^6.1.2", "react-icons": "^5.6.0", - "react-player": "^2.16.0", + "react-player": "^3.4.0", "use-between": "^1.4.0", - "zustand": "^5.0.13" + "zustand": "^5.0.14" }, "devDependencies": { "@tailwindcss/forms": "^0.5.11", - "@tailwindcss/postcss": "^4.2.4", + "@tailwindcss/postcss": "^4.3.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", - "@types/node": "^25.6.0", - "@types/react": "^19.2.14", + "@types/node": "^25.9.1", + "@types/react": "^19.2.16", "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "^8.59.1", - "@typescript-eslint/parser": "^8.59.1", - "@typescript/native-preview": "^7.0.0-dev.20260509.2", - "@vitejs/plugin-react": "^6.0.1", + "@typescript-eslint/eslint-plugin": "^8.60.1", + "@typescript-eslint/parser": "^8.60.1", + "@typescript/native-preview": "^7.0.0-dev.20260604.1", + "@vitejs/plugin-react": "^6.0.2", "babel-plugin-react-compiler": "^1.0.0", - "eslint": "^10.2.1", + "eslint": "^10.4.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-compiler": "19.1.0-rc.2", "eslint-plugin-react-hooks": "^7.1.1", "jsdom": "^29.1.1", - "postcss": "^8.5.12", + "postcss": "^8.5.15", "postcss-nested": "^7.0.2", - "sass": "^1.99.0", + "sass": "^1.100.0", "sirv": "^3.0.2", - "tailwindcss": "^4.2.4", + "tailwindcss": "^4.3.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.1", - "vite": "^8.0.10", - "vitest": "^3" + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", + "vitest": "^3.2.6" } } diff --git a/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx b/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx index 82a4b8c..a2fa601 100644 --- a/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx +++ b/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx @@ -1,8 +1,8 @@ import { FC, useRef } from 'react'; -import ReactPlayer from 'react-player/youtube'; import { LocalizeText, YoutubeVideoPlaybackStateEnum } from '../../../../api'; import { AutoGrid, AutoGridProps, LayoutGridItem, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { useFurnitureYoutubeWidget } from '../../../../hooks'; +import ReactPlayer from '../../../youtube/YoutubeReactPlayer'; interface FurnitureYoutubeDisplayViewProps extends AutoGridProps { @@ -12,7 +12,7 @@ interface FurnitureYoutubeDisplayViewProps extends AutoGridProps export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewProps => { const { objectId = -1, videoId = null, videoStart = 0, videoEnd = 0, currentVideoState = null, selectedVideo = null, playlists = [], onClose = null, previous = null, next = null, pause = null, play = null, selectVideo = null } = useFurnitureYoutubeWidget(); - const playerRef = useRef(null); + const playerRef = useRef(null); const handlePlay = () => { @@ -39,7 +39,7 @@ export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewPr { (videoId && videoId.length > 0) && = FurnitureYoutubeDisplayViewPr onPlay={ handlePlay } onPause={ handlePause } config={ { - playerVars: { - autoplay: 1, + youtube: { disablekb: 1, - controls: 0, origin: window.origin, - modestbranding: 1, start: videoStart, end: videoEnd } diff --git a/src/components/toolbar/YouTubePlayerView.tsx b/src/components/toolbar/YouTubePlayerView.tsx index e4c986a..c4c5849 100644 --- a/src/components/toolbar/YouTubePlayerView.tsx +++ b/src/components/toolbar/YouTubePlayerView.tsx @@ -1,9 +1,9 @@ import { ControlYoutubeDisplayPlaybackMessageComposer, YouTubeRoomBroadcastEvent, YouTubeRoomPlayComposer, YouTubeRoomSettingsEvent, YouTubeRoomWatchersEvent, YouTubeRoomWatchingComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useRef, useState } from 'react'; -import ReactPlayer from 'react-player/youtube'; import { GetRoomSession, getYoutubeRoomEnabled, LocalizeText, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from '../../api'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView, LayoutAvatarImageView } from '../../common'; import { useFurnitureYoutubeWidget, useHasPermission, useMessageEvent } from '../../hooks'; +import ReactPlayer from '../youtube/YoutubeReactPlayer'; const CONTROL_COMMAND_PREVIOUS_VIDEO = 0; const CONTROL_COMMAND_NEXT_VIDEO = 1; @@ -38,7 +38,7 @@ export const YouTubePlayerView: FC<{}> = () => const [playlist, setPlaylist] = useState([]); const [history, setHistory] = useState([]); const [showVolumeSlider, setShowVolumeSlider] = useState(true); - const playerRef = useRef(null); + const playerRef = useRef(null); const { objectId: youtubeObjectId, videoId: roomVideoId, currentVideoState, hasControl } = useFurnitureYoutubeWidget(); const [spectators, setSpectators] = useState< { id: number; name: string; look: string }[] >([]); const [broadcastVideo, setBroadcastVideo] = useState(''); @@ -380,7 +380,7 @@ export const YouTubePlayerView: FC<{}> = () => { playerRef.current = ref; }} - url={`https://www.youtube.com/watch?v=${videoId}`} + src={`https://www.youtube.com/watch?v=${videoId}`} width="100%" height={isFullscreen ? '100%' : 280} playing @@ -388,10 +388,7 @@ export const YouTubePlayerView: FC<{}> = () => loop={isLooping} volume={Math.max(0, Math.min(1, volume / 100))} config={{ - playerVars: { - autoplay: 1, - loop: isLooping ? 1 : 0, - }, + youtube: {}, }} onReady={() => addToHistory(videoId)} /> diff --git a/src/components/youtube/YoutubeReactPlayer.ts b/src/components/youtube/YoutubeReactPlayer.ts new file mode 100644 index 0000000..0d55cf1 --- /dev/null +++ b/src/components/youtube/YoutubeReactPlayer.ts @@ -0,0 +1,28 @@ +import { ComponentType, LazyExoticComponent, lazy } from 'react'; +import HtmlPlayer from 'react-player/HtmlPlayer'; +import { canPlay } from 'react-player/patterns'; +import { PlayerEntry } from 'react-player/players'; +import { createReactPlayer } from 'react-player/ReactPlayer'; +import { VideoElementProps } from 'react-player/types'; + +const YoutubeElement = lazy(() => import('youtube-video-element/react')) as LazyExoticComponent>; + +const YoutubeReactPlayer = createReactPlayer( + [ + { + key: 'youtube', + name: 'YouTube', + canPlay: canPlay.youtube, + player: YoutubeElement + } + ] satisfies PlayerEntry[], + { + key: 'html', + name: 'html', + canPlay: canPlay.html, + canEnablePIP: () => true, + player: HtmlPlayer + } +); + +export default YoutubeReactPlayer; diff --git a/vite.config.mjs b/vite.config.mjs index e51a1a0..a5824be 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -162,6 +162,9 @@ export default defineConfig({ chunkSizeWarningLimit: 200000, manifest: true, rollupOptions: { + checks: { + pluginTimings: false + }, output: { assetFileNames: 'src/assets/[name]-[hash].[ext]', // Granular chunking: split the monolithic vendor / nitro-renderer diff --git a/yarn.lock b/yarn.lock index 80a5588..52000d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -218,11 +218,16 @@ "@babel/helper-create-class-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.29.2": +"@babel/runtime@^7.12.5": version "7.29.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e" integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g== +"@babel/runtime@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768" + integrity sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw== + "@babel/template@^7.28.6": version "7.28.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" @@ -293,7 +298,7 @@ resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz#798a33950d11226a0ebb6acafa60f5594424967f" integrity sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA== -"@emnapi/core@1.10.0", "@emnapi/core@^1.8.1": +"@emnapi/core@1.10.0", "@emnapi/core@^1.10.0": version "1.10.0" resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.10.0.tgz#380ccc8f2412ea22d1d972df7f8ee23a3b9c7467" integrity sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw== @@ -301,14 +306,14 @@ "@emnapi/wasi-threads" "1.2.1" tslib "^2.4.0" -"@emnapi/runtime@1.10.0", "@emnapi/runtime@^1.8.1": +"@emnapi/runtime@1.10.0", "@emnapi/runtime@^1.10.0": version "1.10.0" resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c" integrity sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA== dependencies: tslib "^2.4.0" -"@emnapi/wasi-threads@1.2.1", "@emnapi/wasi-threads@^1.1.0": +"@emnapi/wasi-threads@1.2.1", "@emnapi/wasi-threads@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548" integrity sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w== @@ -476,10 +481,10 @@ debug "^4.3.1" minimatch "^10.2.4" -"@eslint/config-helpers@^0.5.5": - version "0.5.5" - resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.5.5.tgz#ae16134e4792ac5fbdc533548a24ac1ea9f7f3ae" - integrity sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w== +"@eslint/config-helpers@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.6.0.tgz#ef9a36881d39dfd5dbeac22b0da997fabfb08b03" + integrity sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA== dependencies: "@eslint/core" "^1.2.1" @@ -495,10 +500,10 @@ resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.5.tgz#88e9bf4d11d2b19c082e78ebe7ce88724a5eb091" integrity sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw== -"@eslint/plugin-kit@^0.7.1": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz#c4125fd015eceeb09b793109fdbcd4dd0a02d346" - integrity sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ== +"@eslint/plugin-kit@^0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz#4b0962f3f2c7ce8bc98b3ecfe34525c09d2cb729" + integrity sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A== dependencies: "@eslint/core" "^1.2.1" levn "^0.4.1" @@ -600,17 +605,62 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@napi-rs/wasm-runtime@^1.1.1", "@napi-rs/wasm-runtime@^1.1.4": +"@mux/mux-data-google-ima@^0.3.4": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@mux/mux-data-google-ima/-/mux-data-google-ima-0.3.17.tgz#ed3db384c35573ed77340b860db103160d024eb8" + integrity sha512-4wpH6dYybyZhqLn9qGn/+67Z8MZnQRAdqTFEEZw2bx61M9q01uPYYHxd8qwOnYtUGEeafsdTwVHVxKHGD3oc1A== + dependencies: + mux-embed "5.18.1" + +"@mux/mux-player-react@^3.8.0": + version "3.13.0" + resolved "https://registry.yarnpkg.com/@mux/mux-player-react/-/mux-player-react-3.13.0.tgz#4b3f01690bef52904d0a010965d96be9be32e3d9" + integrity sha512-7IkImo1H3rUYeuWHI/L0L7sGUqBvZCvptx3+4igc+P/V3WgqNFjOyQlcyxzxgXNtNNhGmkn5NaF3TU9DPbOmAQ== + dependencies: + "@mux/mux-player" "3.13.0" + "@mux/playback-core" "0.35.0" + prop-types "^15.8.1" + +"@mux/mux-player@3.13.0": + version "3.13.0" + resolved "https://registry.yarnpkg.com/@mux/mux-player/-/mux-player-3.13.0.tgz#4ad2a43fadd7360a2c7be4bd7572614d627d7524" + integrity sha512-vh4CIMahUa29gys+mlfsOFKYKAKXxE07jSWk9WZxkdpGBW1fKfCQXlNxBoAIQlPa9Uk4MZuM3HVgqdW6aTuaZg== + dependencies: + "@mux/mux-video" "0.31.0" + "@mux/playback-core" "0.35.0" + media-chrome "~4.19.0" + player.style "^0.3.0" + +"@mux/mux-video@0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@mux/mux-video/-/mux-video-0.31.0.tgz#df9beef9f90127667a0313b766e2a5cb9e23dbc0" + integrity sha512-DvO2GynIJhPDc0LMuWvC144lCF+E07NI+chrg/vcRQlCf2fPdNNqCSMoaRdWu2S2v/Orsc85giT9K6GRbjbzgA== + dependencies: + "@mux/mux-data-google-ima" "^0.3.4" + "@mux/playback-core" "0.35.0" + castable-video "~1.1.13" + custom-media-element "~1.4.6" + media-tracks "~0.3.5" + +"@mux/playback-core@0.35.0": + version "0.35.0" + resolved "https://registry.yarnpkg.com/@mux/playback-core/-/playback-core-0.35.0.tgz#1504fb565b76d52add3c43919f5cba5d670f424f" + integrity sha512-7Zi1EJ9sQNIUlQVBjJCXV0CB+rUVEsU3vNRElRV4xnD7dbpoioIAhe1SjZNBifjnK5aBKLDwQHypbnL3Cw3a5A== + dependencies: + hls.js "~1.6.15" + mux-embed "^5.16.1" + +"@napi-rs/wasm-runtime@^1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz#a46bbfedc29751b7170c5d23bc1d8ee8c7e3c1e1" integrity sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow== dependencies: "@tybys/wasm-util" "^0.10.1" -"@oxc-project/types@=0.127.0": - version "0.127.0" - resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.127.0.tgz#8374fcdfb4a641861218daa5700c447c00b66663" - integrity sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ== +"@oxc-project/types@=0.133.0": + version "0.133.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.133.0.tgz#2e282ef9e1d26e06b68ccd14b73f310a3b2cf7f8" + integrity sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA== "@parcel/watcher-android-arm64@2.5.6": version "2.5.6" @@ -920,94 +970,89 @@ resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== -"@rolldown/binding-android-arm64@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz#0a502a88c39d0ffa81aa30b561dade6f6217dcc5" - integrity sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ== +"@rolldown/binding-android-arm64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz#54ce8f8382213f4a314a0c2f7ba83f81ffeae592" + integrity sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw== -"@rolldown/binding-darwin-arm64@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz#8b7f05ac9000ab19161a79a0346b1b64a1bc7ba3" - integrity sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw== +"@rolldown/binding-darwin-arm64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz#388fca1566c14c00c4b446fc3928630e7f0d95fc" + integrity sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA== -"@rolldown/binding-darwin-x64@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz#f8b465b3a4e992053890b162f1ae19e4f1719a6a" - integrity sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw== +"@rolldown/binding-darwin-x64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz#53f57de1f599ecf1db13823cfc88c18fb80954ad" + integrity sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg== -"@rolldown/binding-freebsd-x64@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz#a8281e14fa9c243fe22dc2d0e54900e66b31935e" - integrity sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw== +"@rolldown/binding-freebsd-x64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz#6f3fdda1b7aeaac9d268a526804b4fb96e4e35f1" + integrity sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g== -"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz#cd29cf869ddd4fac8d6929abf94b91ddb0494650" - integrity sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ== +"@rolldown/binding-linux-arm-gnueabihf@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz#d87a454bf585cc9676849377e91d6e375297326f" + integrity sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw== -"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz#91c331236ec3728366218d61a62f0bd226546c6c" - integrity sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q== +"@rolldown/binding-linux-arm64-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz#419fd6bf612cf348f10528cbcd94ebab9607d8d1" + integrity sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw== -"@rolldown/binding-linux-arm64-musl@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz#80108957db752e7826836e22240e56b8140e9684" - integrity sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg== +"@rolldown/binding-linux-arm64-musl@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz#fcc6918696bb76844877e1e4930a18fd0d374069" + integrity sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q== -"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz#1dce51148cbc6bab3c3f9157b5323d2a31aac924" - integrity sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA== +"@rolldown/binding-linux-ppc64-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz#32aecb7c8dae5d4f2a8cde57a058ec86991542f8" + integrity sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg== -"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz#d4a0d2e01d8d441e4ac3af3fa68eec17a7d0e9cd" - integrity sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA== +"@rolldown/binding-linux-s390x-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz#bed9346ea81e6bb8b93cf11f5d88b77db890b763" + integrity sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg== -"@rolldown/binding-linux-x64-gnu@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz#0ac8b3139cefeea798ad147f30ea70572b133af1" - integrity sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA== +"@rolldown/binding-linux-x64-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz#64c2d26f75dffd9b5a1f97557a00ae77250c8cb7" + integrity sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg== -"@rolldown/binding-linux-x64-musl@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz#2af61bee087571728f58f1c47734bbbd41dd7050" - integrity sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw== +"@rolldown/binding-linux-x64-musl@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz#5a45132e8a47659eeaaf3b540c2954a97c860ff3" + integrity sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow== -"@rolldown/binding-openharmony-arm64@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz#56c1afbf6c592819abf47b4a983987dc288b30c1" - integrity sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA== +"@rolldown/binding-openharmony-arm64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz#290513068c55e849dc8457a32afee1d7b0acb309" + integrity sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg== -"@rolldown/binding-wasm32-wasi@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz#5d112ff4dd0d268a60fb4e0eb3077e3ea2531f0d" - integrity sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA== +"@rolldown/binding-wasm32-wasi@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz#3d9972dbf1a953d3c7afaa4a0f20ef2b2e39f31b" + integrity sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg== dependencies: "@emnapi/core" "1.10.0" "@emnapi/runtime" "1.10.0" "@napi-rs/wasm-runtime" "^1.1.4" -"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz#5125a85222d64a543201d28e16a395cc45bf4d17" - integrity sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA== +"@rolldown/binding-win32-arm64-msvc@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz#a004ab607a16d6f03bcb555728ff888af75773ad" + integrity sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g== -"@rolldown/binding-win32-x64-msvc@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz#fc6b78e759a0bb2054b5c0a3489da12b2cae54b4" - integrity sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg== +"@rolldown/binding-win32-x64-msvc@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz#e2a25b34691a1cc8a1209d7de709063026dd0cdb" + integrity sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA== -"@rolldown/pluginutils@1.0.0-rc.17": - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz#a89b30833fb628bc834fe2e89fea93a2da9fa69a" - integrity sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg== - -"@rolldown/pluginutils@1.0.0-rc.7": - version "1.0.0-rc.7" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz#0414869467f0e471a6515d4f506c85fde867e022" - integrity sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA== +"@rolldown/pluginutils@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" + integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== "@rollup/rollup-android-arm-eabi@4.60.3": version "4.60.3" @@ -1134,6 +1179,41 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz#cc6f094a3ffe5556bb4a831ee6fb572b8cd81a75" integrity sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA== +"@svta/cml-608@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@svta/cml-608/-/cml-608-1.0.2.tgz#b8d1839fe0abc1591eb8da28712279e2c2a777ff" + integrity sha512-ZEJ68330gcLKfvVv6Qifr1HR7+GldDUxzkjqSbqRK7jHtHSLSV1JAyDwlYe8+C7ABuY1bp8MpgJ4/Gg+jl+pLQ== + +"@svta/cml-cmcd@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@svta/cml-cmcd/-/cml-cmcd-2.3.2.tgz#ddcff43ab30af1b211a48d59a942c603b9f59434" + integrity sha512-SKBBjLmci0WK8HMjuv+36tVIMktonoOoxsXblOFZmB+ePPV2zjRMTD+2ZmE/1VEPJkKHENyhSjSHgJyeOlvZ1A== + +"@svta/cml-cmsd@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@svta/cml-cmsd/-/cml-cmsd-1.0.6.tgz#edcc26ebf971f84b6784e72b1fd2508f1344e57c" + integrity sha512-LUORV6bb0TbU4rSC2HoPqUCix1igLrXkRQXWiIyJo2OMzb14kAK/1jsW0mzY6up6w1GrjKQcjc6OwqJdo/zd/g== + +"@svta/cml-dash@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@svta/cml-dash/-/cml-dash-1.0.6.tgz#98ecbd8ce0fab043a55138a7cb9f58de8be2f1fc" + integrity sha512-4XtHYlPrzL/dRe/8XmRQoLnTo9S86tISgrl67eUqKl5MtNTpZBYTncuPrspslPZPZROBBWNrBuepYfYUtU9CKA== + +"@svta/cml-id3@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@svta/cml-id3/-/cml-id3-1.0.6.tgz#febfe4a8c6dbbefcb3a23cf451a07f2b34751543" + integrity sha512-63j8gkAnPOmOBWlp0hIZPvsIioZttdbg6/TgwITqMYbSLYVJ+6QGa/UtIP0I84NsfstANw6QdCn7i8SS08kn1A== + +"@svta/cml-request@1.0.12": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@svta/cml-request/-/cml-request-1.0.12.tgz#14c43a8f1da32714a82229d6001cccf36cf1e77d" + integrity sha512-4sJvnnoNpq58j2mCGP8k+MF6wVy/qa4gbt6kfT1dPIKmn3mPxw+JVfilhcWsUi+peK2yCZxOJJYyHj1cAcQE1w== + +"@svta/cml-xml@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@svta/cml-xml/-/cml-xml-1.1.4.tgz#1acafe8d02551cb9b69e1b72c9125298a5406b21" + integrity sha512-jbixqjiJIc16SGxylHwiOzO+DuhkGfuP+fJ9AHeVJKdFDKnabgfCDpnp6dvZpZnjMj4nHvzVtuUV7RISPIwYXw== + "@tailwindcss/forms@^0.5.11": version "0.5.11" resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.11.tgz#e77039e96fa7b87c3d001a991f77f9418e666700" @@ -1141,150 +1221,150 @@ dependencies: mini-svg-data-uri "^1.2.3" -"@tailwindcss/node@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.2.4.tgz#1f7fc0c1741037ded1fa92fbe62a786a197771ce" - integrity sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA== +"@tailwindcss/node@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.3.0.tgz#9dc5312bf41c48658529f36021e0b466c4eb7860" + integrity sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g== dependencies: "@jridgewell/remapping" "^2.3.5" - enhanced-resolve "^5.19.0" + enhanced-resolve "^5.21.0" jiti "^2.6.1" lightningcss "1.32.0" magic-string "^0.30.21" source-map-js "^1.2.1" - tailwindcss "4.2.4" + tailwindcss "4.3.0" -"@tailwindcss/oxide-android-arm64@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz#d533e52ee98d58f55d1d4753774251513ba8a911" - integrity sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g== +"@tailwindcss/oxide-android-arm64@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz#e4533b6125236fe81a899cf5a82028c85244def8" + integrity sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng== -"@tailwindcss/oxide-darwin-arm64@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz#2a6250aa7d8791fc1b5797e64e09e51da57514a6" - integrity sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg== +"@tailwindcss/oxide-darwin-arm64@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz#96b074ef64ec6c41d580063740c8d36cf5c459ce" + integrity sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ== -"@tailwindcss/oxide-darwin-x64@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz#d647299812946b6ab5140c61a334c8ebc8d877de" - integrity sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg== +"@tailwindcss/oxide-darwin-x64@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz#0d9638d06d38684339b2dc06631966a7296bb64e" + integrity sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA== -"@tailwindcss/oxide-freebsd-x64@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz#019b7fce37aaf5ddfed0f231c536108292e87ffb" - integrity sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw== +"@tailwindcss/oxide-freebsd-x64@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz#efc7acd17cd38d7585c07cb938a4f1b703f79d7a" + integrity sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ== -"@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz#c88a95d69095e84f811b302daa66f5287ad8ce0f" - integrity sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA== +"@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz#e41c945e529670cd93fd6ed0c6a2880de5c40333" + integrity sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA== -"@tailwindcss/oxide-linux-arm64-gnu@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz#1292f1c222994bfe4a5e990ac0a701de6487dd02" - integrity sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw== +"@tailwindcss/oxide-linux-arm64-gnu@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz#6bb608b16ba7146d61097c2f4c7ee927d1f3580a" + integrity sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg== -"@tailwindcss/oxide-linux-arm64-musl@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz#afb6492b22616f0d9d3346d39c1a6e285f994a08" - integrity sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g== +"@tailwindcss/oxide-linux-arm64-musl@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz#1bb443aa371bb99b50cb39d4d688151fadcd8a63" + integrity sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ== -"@tailwindcss/oxide-linux-x64-gnu@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz#400b0ccfc53937c7804ed8e0e9652b42bd86f2eb" - integrity sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA== +"@tailwindcss/oxide-linux-x64-gnu@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz#5267c0bb2597426c0d2e759acb5389cde2aa71fd" + integrity sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ== -"@tailwindcss/oxide-linux-x64-musl@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz#5c23c476e5de4ed9cd6ab39c2718b9a4be2bbb2b" - integrity sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA== +"@tailwindcss/oxide-linux-x64-musl@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz#fb2da97c67b218e5c7c723cb32782d55d7e4a5d5" + integrity sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg== -"@tailwindcss/oxide-wasm32-wasi@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz#21b7f53ba7c6c03f26ccb8cef5d09f5c2973ae5e" - integrity sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw== +"@tailwindcss/oxide-wasm32-wasi@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz#3f6538e511066d67d8683863dcaeeb16c22de849" + integrity sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA== dependencies: - "@emnapi/core" "^1.8.1" - "@emnapi/runtime" "^1.8.1" - "@emnapi/wasi-threads" "^1.1.0" - "@napi-rs/wasm-runtime" "^1.1.1" + "@emnapi/core" "^1.10.0" + "@emnapi/runtime" "^1.10.0" + "@emnapi/wasi-threads" "^1.2.1" + "@napi-rs/wasm-runtime" "^1.1.4" "@tybys/wasm-util" "^0.10.1" tslib "^2.8.1" -"@tailwindcss/oxide-win32-arm64-msvc@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz#13bc1cf3818e3345a965d36b40c237817124d070" - integrity sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ== +"@tailwindcss/oxide-win32-arm64-msvc@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz#ec45fba773c76759338c05d4fe5cf42c4eea2e4e" + integrity sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ== -"@tailwindcss/oxide-win32-x64-msvc@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz#5476dbbbf6b8934d58452340cec737fdaa5ec8c6" - integrity sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw== +"@tailwindcss/oxide-win32-x64-msvc@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz#58cdd6e06adbe2e3160274edfcd0b0b43e17fee4" + integrity sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA== -"@tailwindcss/oxide@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.2.4.tgz#e2ca51d04e8ad94d569222fa727de479b097db39" - integrity sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q== +"@tailwindcss/oxide@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.3.0.tgz#cc1c61e88f62c0e9f56062de3e7873acaa2159d4" + integrity sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg== optionalDependencies: - "@tailwindcss/oxide-android-arm64" "4.2.4" - "@tailwindcss/oxide-darwin-arm64" "4.2.4" - "@tailwindcss/oxide-darwin-x64" "4.2.4" - "@tailwindcss/oxide-freebsd-x64" "4.2.4" - "@tailwindcss/oxide-linux-arm-gnueabihf" "4.2.4" - "@tailwindcss/oxide-linux-arm64-gnu" "4.2.4" - "@tailwindcss/oxide-linux-arm64-musl" "4.2.4" - "@tailwindcss/oxide-linux-x64-gnu" "4.2.4" - "@tailwindcss/oxide-linux-x64-musl" "4.2.4" - "@tailwindcss/oxide-wasm32-wasi" "4.2.4" - "@tailwindcss/oxide-win32-arm64-msvc" "4.2.4" - "@tailwindcss/oxide-win32-x64-msvc" "4.2.4" + "@tailwindcss/oxide-android-arm64" "4.3.0" + "@tailwindcss/oxide-darwin-arm64" "4.3.0" + "@tailwindcss/oxide-darwin-x64" "4.3.0" + "@tailwindcss/oxide-freebsd-x64" "4.3.0" + "@tailwindcss/oxide-linux-arm-gnueabihf" "4.3.0" + "@tailwindcss/oxide-linux-arm64-gnu" "4.3.0" + "@tailwindcss/oxide-linux-arm64-musl" "4.3.0" + "@tailwindcss/oxide-linux-x64-gnu" "4.3.0" + "@tailwindcss/oxide-linux-x64-musl" "4.3.0" + "@tailwindcss/oxide-wasm32-wasi" "4.3.0" + "@tailwindcss/oxide-win32-arm64-msvc" "4.3.0" + "@tailwindcss/oxide-win32-x64-msvc" "4.3.0" -"@tailwindcss/postcss@^4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@tailwindcss/postcss/-/postcss-4.2.4.tgz#548ed07584a41411574e8b1ec5f1543d09c439a4" - integrity sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg== +"@tailwindcss/postcss@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/postcss/-/postcss-4.3.0.tgz#58a087d8c6f06c6aa81e8a3f6c1e7282b8ee94d9" + integrity sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w== dependencies: "@alloc/quick-lru" "^5.2.0" - "@tailwindcss/node" "4.2.4" - "@tailwindcss/oxide" "4.2.4" - postcss "^8.5.6" - tailwindcss "4.2.4" + "@tailwindcss/node" "4.3.0" + "@tailwindcss/oxide" "4.3.0" + postcss "^8.5.10" + tailwindcss "4.3.0" -"@tanstack/query-core@5.100.10": - version "5.100.10" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.100.10.tgz#aeb34d301fd4ff9762e67dfa018adc33b7a18be4" - integrity sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w== +"@tanstack/query-core@5.101.0": + version "5.101.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.101.0.tgz#45391ce143c270b7e7a55bd1a6696918f20782d1" + integrity sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow== -"@tanstack/query-devtools@5.100.10": - version "5.100.10" - resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.100.10.tgz#1972789fdc7c4cb9ec2062d51f25bc4dc655a27b" - integrity sha512-3DmJf25hDPus5IpVvp6ujXv6bKV2zPzI9vpbAmpJigsL/H6DPvPjmf7/Q9yVKEke//8fgeQ45abjgnLuyYxAiw== +"@tanstack/query-devtools@5.101.0": + version "5.101.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.101.0.tgz#807c5fae297a20f1b1cb8c3e0adddf9ff98e5068" + integrity sha512-MVqw17k08RQtGGLEL654+dX/btbX9p/8WjkznO//zusLTMaObxi3Q+MoFwGVkC9K3tqjn8qrrNhJevXx4fJTeQ== "@tanstack/react-query-devtools@5": - version "5.100.10" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.100.10.tgz#cca3479cc2c8b434637c31f8119fe6ff93e5832c" - integrity sha512-zes0+o9ef5rAZXJ9f/SeaLs2nufJaeVkZkl/Or9NGrWVF41kL9Od9ED9nCwtQlgiF2VGtrzhEw5AU/igAO+aAg== + version "5.101.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.101.0.tgz#b7894eff3fcd78c892e062dcf0a0c479667bf138" + integrity sha512-cpZA0+WqKXwrwMfiWZEGGF6QrIWVQFbhBtxqDF5sQsAfrFf47HIE6fiPbQU3wyAUEN2+7UNqLCQe7oG6m3f93w== dependencies: - "@tanstack/query-devtools" "5.100.10" + "@tanstack/query-devtools" "5.101.0" "@tanstack/react-query@5": - version "5.100.10" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.100.10.tgz#3bf1844efd76f5f68f9f39da2917fc4c6023e726" - integrity sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q== + version "5.101.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.101.0.tgz#2dd56c5b96ec816d6b6ec9cee5c80ed298974733" + integrity sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg== dependencies: - "@tanstack/query-core" "5.100.10" + "@tanstack/query-core" "5.101.0" -"@tanstack/react-virtual@^3.13.24": - version "3.13.24" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz#77af3d5dcf77358d805b7b3b06d3221af7bd3f6f" - integrity sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg== +"@tanstack/react-virtual@^3.14.2": + version "3.14.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.14.2.tgz#ff1f97cd52f2bf4bda7f6d141bc9af80010daf45" + integrity sha512-IpWnmCLvuymRfeeLNVXIzNEYBFLpd3drVIS91sqV78VTZFyldlChkOocZRCPp1B+Wnk09bcLNme8WaMU/9/9bQ== dependencies: - "@tanstack/virtual-core" "3.14.0" + "@tanstack/virtual-core" "3.17.0" -"@tanstack/virtual-core@3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz#c8839d0d702b8af47c0e57d4ab72fc3ba8bbf3da" - integrity sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q== +"@tanstack/virtual-core@3.17.0": + version "3.17.0" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.17.0.tgz#2949d4aaeaf73f89bab10bf86e22905f5a668acc" + integrity sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ== "@testing-library/dom@^10.4.1": version "10.4.1" @@ -1364,22 +1444,22 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/node@^25.6.0": - version "25.6.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca" - integrity sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ== +"@types/node@^25.9.1": + version "25.9.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.1.tgz#3bda556db500ae4319c08e7fc9ab94f19013ba0b" + integrity sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg== dependencies: - undici-types "~7.19.0" + undici-types ">=7.24.0 <7.24.7" "@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@^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== +"@types/react@^19.2.16": + version "19.2.16" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.16.tgz#9868b153fd9e34e0117afcd5d7e372b8179337e1" + integrity sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w== dependencies: csstype "^3.2.2" @@ -1388,215 +1468,223 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== -"@typescript-eslint/eslint-plugin@8.59.2", "@typescript-eslint/eslint-plugin@^8.59.1": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz#f37b2c189a0177141fe3de3b08f2a83991bfdbfa" - integrity sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ== +"@typescript-eslint/eslint-plugin@8.60.1", "@typescript-eslint/eslint-plugin@^8.60.1": + version "8.60.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz#c1060bb8fa4be80624d3f3dec8dd9caca373af76" + integrity sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.59.2" - "@typescript-eslint/type-utils" "8.59.2" - "@typescript-eslint/utils" "8.59.2" - "@typescript-eslint/visitor-keys" "8.59.2" + "@typescript-eslint/scope-manager" "8.60.1" + "@typescript-eslint/type-utils" "8.60.1" + "@typescript-eslint/utils" "8.60.1" + "@typescript-eslint/visitor-keys" "8.60.1" ignore "^7.0.5" natural-compare "^1.4.0" ts-api-utils "^2.5.0" -"@typescript-eslint/parser@8.59.2", "@typescript-eslint/parser@^8.59.1": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.2.tgz#e2fd0084baa5dd0c24cd789af1c72cbc3a7a1c62" - integrity sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ== +"@typescript-eslint/parser@8.60.1", "@typescript-eslint/parser@^8.60.1": + version "8.60.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.1.tgz#a9d7f30850384d34b41f4687dd8944823c09e289" + integrity sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA== dependencies: - "@typescript-eslint/scope-manager" "8.59.2" - "@typescript-eslint/types" "8.59.2" - "@typescript-eslint/typescript-estree" "8.59.2" - "@typescript-eslint/visitor-keys" "8.59.2" + "@typescript-eslint/scope-manager" "8.60.1" + "@typescript-eslint/types" "8.60.1" + "@typescript-eslint/typescript-estree" "8.60.1" + "@typescript-eslint/visitor-keys" "8.60.1" debug "^4.4.3" -"@typescript-eslint/project-service@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz#f8b8cbf8692e3a51c2c394acf8cf6900f7e755af" - integrity sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw== +"@typescript-eslint/project-service@8.60.1": + version "8.60.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.1.tgz#eb29712f58d72c222fc727162e92f2ab4670971b" + integrity sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.59.2" - "@typescript-eslint/types" "^8.59.2" + "@typescript-eslint/tsconfig-utils" "^8.60.1" + "@typescript-eslint/types" "^8.60.1" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz#63cbd0af2e3180949d6be81122cc555bc71e736d" - integrity sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg== +"@typescript-eslint/scope-manager@8.60.1": + version "8.60.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz#2f875962eaad0a0789cc3c36aea9b4ddeb2dd9c8" + integrity sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w== dependencies: - "@typescript-eslint/types" "8.59.2" - "@typescript-eslint/visitor-keys" "8.59.2" + "@typescript-eslint/types" "8.60.1" + "@typescript-eslint/visitor-keys" "8.60.1" -"@typescript-eslint/tsconfig-utils@8.59.2", "@typescript-eslint/tsconfig-utils@^8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz#6e92bc412083753185a79c9f1431e78169d9232f" - integrity sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw== +"@typescript-eslint/tsconfig-utils@8.60.1", "@typescript-eslint/tsconfig-utils@^8.60.1": + version "8.60.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93" + integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA== -"@typescript-eslint/type-utils@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz#a60a1192a804fa472a92c41656853ac6a9ba7176" - integrity sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ== +"@typescript-eslint/type-utils@8.60.1": + version "8.60.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz#1ae45f0f2a701354beea4a58c2161e40a5e3c379" + integrity sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A== dependencies: - "@typescript-eslint/types" "8.59.2" - "@typescript-eslint/typescript-estree" "8.59.2" - "@typescript-eslint/utils" "8.59.2" + "@typescript-eslint/types" "8.60.1" + "@typescript-eslint/typescript-estree" "8.60.1" + "@typescript-eslint/utils" "8.60.1" debug "^4.4.3" ts-api-utils "^2.5.0" -"@typescript-eslint/types@8.59.2", "@typescript-eslint/types@^8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.2.tgz#01caabcd7e4715c33ad5e11cab260829714d6b9c" - integrity sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q== +"@typescript-eslint/types@8.60.1", "@typescript-eslint/types@^8.60.1": + version "8.60.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4" + integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w== -"@typescript-eslint/typescript-estree@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz#6a217ef65b18dbd12c718fc86a675d1d7a1414cc" - integrity sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg== +"@typescript-eslint/typescript-estree@8.60.1": + version "8.60.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz#016630b119228bf483ddc652703a6a038f3fdd74" + integrity sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew== dependencies: - "@typescript-eslint/project-service" "8.59.2" - "@typescript-eslint/tsconfig-utils" "8.59.2" - "@typescript-eslint/types" "8.59.2" - "@typescript-eslint/visitor-keys" "8.59.2" + "@typescript-eslint/project-service" "8.60.1" + "@typescript-eslint/tsconfig-utils" "8.60.1" + "@typescript-eslint/types" "8.60.1" + "@typescript-eslint/visitor-keys" "8.60.1" debug "^4.4.3" minimatch "^10.2.2" semver "^7.7.3" tinyglobby "^0.2.15" ts-api-utils "^2.5.0" -"@typescript-eslint/utils@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.2.tgz#ff619a6a3075f4017fa91b8610b752a8ca3366aa" - integrity sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q== +"@typescript-eslint/utils@8.60.1": + version "8.60.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.1.tgz#31cf566095602d9fe8ad91837d2eb520b8de762b" + integrity sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.59.2" - "@typescript-eslint/types" "8.59.2" - "@typescript-eslint/typescript-estree" "8.59.2" + "@typescript-eslint/scope-manager" "8.60.1" + "@typescript-eslint/types" "8.60.1" + "@typescript-eslint/typescript-estree" "8.60.1" -"@typescript-eslint/visitor-keys@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz#5ccc486913cd347883d69158836b1189a660bfe6" - integrity sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA== +"@typescript-eslint/visitor-keys@8.60.1": + version "8.60.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz#165d1d8901137b944efaf18f00ab5ecb57f06995" + integrity sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag== dependencies: - "@typescript-eslint/types" "8.59.2" + "@typescript-eslint/types" "8.60.1" eslint-visitor-keys "^5.0.0" -"@typescript/native-preview-darwin-arm64@7.0.0-dev.20260509.2": - version "7.0.0-dev.20260509.2" - resolved "https://registry.yarnpkg.com/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260509.2.tgz#e759c5815bcc7d1605da78c63d3960c366b24d5c" - integrity sha512-oG9KahiCpx4q70Ood/rRJhYio4oIMHEHfX0g0LhfenlSIjIonitZWjUmUVG9N9q1ev9QWcM8pWpDrGGP0Osp3Q== +"@typescript/native-preview-darwin-arm64@7.0.0-dev.20260604.1": + version "7.0.0-dev.20260604.1" + resolved "https://registry.yarnpkg.com/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260604.1.tgz#52cb9c8e098036c6cb321554caefd78424df542c" + integrity sha512-zs616um9UuaODLsNlCu5Aw95rFcTV4u3hVt090r6k0lVvTxfaJOv8HKA6BpIotcEYlZlMQowrMSYCCdedo7iyA== -"@typescript/native-preview-darwin-x64@7.0.0-dev.20260509.2": - version "7.0.0-dev.20260509.2" - resolved "https://registry.yarnpkg.com/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260509.2.tgz#70750de45f9791972eaa0d2054eec5964657f52a" - integrity sha512-xdEkp23Gu8I7PJCMmSMYtSLX76NKODWj74AoWFPi6MM59ICsjnTSqZf/HmXKSvuNZ5MGb4CMpP3c40dLjGB2PQ== +"@typescript/native-preview-darwin-x64@7.0.0-dev.20260604.1": + version "7.0.0-dev.20260604.1" + resolved "https://registry.yarnpkg.com/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260604.1.tgz#3e31508c9dbe5d759587efc31a12d419dca60932" + integrity sha512-pOdNAf2pwc9JBjo8gUsvLs5uqg7d0AhYXfE8/3zvPKBlIZG+mTcvEWW1hPawlWxXzf/vxnP4dgYwUHAMDghhKA== -"@typescript/native-preview-linux-arm64@7.0.0-dev.20260509.2": - version "7.0.0-dev.20260509.2" - resolved "https://registry.yarnpkg.com/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260509.2.tgz#e81a79ddd39efe42a872b0e5191fdbd0f80a023a" - integrity sha512-rd+bMRtUAFBClOAKi9p2rOu6jPmnrjZVljoFyxHw+6bIRLerEQlxP+nIH1olC3HOZPyZ6/x75WtfzTHYeqffiQ== +"@typescript/native-preview-linux-arm64@7.0.0-dev.20260604.1": + version "7.0.0-dev.20260604.1" + resolved "https://registry.yarnpkg.com/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260604.1.tgz#6ed9ba6ae91e8dc1eb7ccf217a9520e5b6d2982c" + integrity sha512-GjIrt6YHP3bbOWBCCE08SlBSDf84Lnjn3Td822/lOX9nm6ODlA/HI7rtGh7KzS/fxehep2Vy4dXU4Il12X1s5A== -"@typescript/native-preview-linux-arm@7.0.0-dev.20260509.2": - version "7.0.0-dev.20260509.2" - resolved "https://registry.yarnpkg.com/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260509.2.tgz#c05f9d7e6a546f2d2844808535bc216753913bba" - integrity sha512-ar5HN/V/4HLF4FZCoVVFj+ET1Soi758hb4WhhzYQfSUXQ/bpVGUGP86JAy8EhVMoeN6qxqWet93MkLSszJOIVg== +"@typescript/native-preview-linux-arm@7.0.0-dev.20260604.1": + version "7.0.0-dev.20260604.1" + resolved "https://registry.yarnpkg.com/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260604.1.tgz#042eb684fcd1673bb4b284b7122ed1999567d461" + integrity sha512-IKaZL3i5HKmKqwb2IZEXW1j68fVg1HsvAaXkbrkOIG/J5Eyksu5tEnTzucrIY1oPdzgHT+y2HpDIYp2sGeHFvw== -"@typescript/native-preview-linux-x64@7.0.0-dev.20260509.2": - version "7.0.0-dev.20260509.2" - resolved "https://registry.yarnpkg.com/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260509.2.tgz#64bc09d219b194cbc8e40fa3dec973ee63585844" - integrity sha512-lB26mGzdolYIZiOdBII8roVJCxCUR8zkYszvvHyjB1IPs7d5fmOhT6OzI1zYPYujiSRJi4HVYM1iXTcIfp7KDg== +"@typescript/native-preview-linux-x64@7.0.0-dev.20260604.1": + version "7.0.0-dev.20260604.1" + resolved "https://registry.yarnpkg.com/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260604.1.tgz#68f92898cb00b186453b6ade6818839990cf08e9" + integrity sha512-twQ7XDjsmaHIevN1MjeRYIVLUPL0fBm8A0jg1FhGYPckhTxEBiHIpJf9E//eFidAdrEHjeTSBP1jJw3GNAensg== -"@typescript/native-preview-win32-arm64@7.0.0-dev.20260509.2": - version "7.0.0-dev.20260509.2" - resolved "https://registry.yarnpkg.com/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260509.2.tgz#14341a0a6561107fad33e0686bd267b4775b120e" - integrity sha512-gH3UmtyxHiRNEP0LgQXCVlB5+ZN/U+/Z7jM/zULQtTOxIIFK3Y4b8gbGLvP7uW3u2cqYOg2hc2nuN8OdsCmOig== +"@typescript/native-preview-win32-arm64@7.0.0-dev.20260604.1": + version "7.0.0-dev.20260604.1" + resolved "https://registry.yarnpkg.com/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260604.1.tgz#59eda11a804eae1deeae47c91d8b5ddf91a7c1e3" + integrity sha512-QBRxaVT3SFiNfOhwYb/56ddpHWPMFdfiJ4zkFJIjaAXeZ/ssWNHM9lH7yR++GrE9VTsUZ4eVSZfOmQbzRERhgw== -"@typescript/native-preview-win32-x64@7.0.0-dev.20260509.2": - version "7.0.0-dev.20260509.2" - resolved "https://registry.yarnpkg.com/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260509.2.tgz#41a91f82067717781c47e5698bff0d1abb14ac51" - integrity sha512-kZV0Vh64hp10saOghPlFZE1qahonqvRgU3iubt8pUY4XLe8IQIofwWCN5vzNNeULE4W4mRtAJbHuvP/muOFomw== +"@typescript/native-preview-win32-x64@7.0.0-dev.20260604.1": + version "7.0.0-dev.20260604.1" + resolved "https://registry.yarnpkg.com/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260604.1.tgz#d2197a281709e21e87a3a5fc130a1a278e6ab192" + integrity sha512-hR7YHoRpm88Q86gK6/IMayWUA+ROdHGzOPKK8EBp1xD/Cg2Bh5AXbut8HcyUDx/bcGQvnuht/XsW47bT3to9mg== -"@typescript/native-preview@^7.0.0-dev.20260509.2": - version "7.0.0-dev.20260509.2" - resolved "https://registry.yarnpkg.com/@typescript/native-preview/-/native-preview-7.0.0-dev.20260509.2.tgz#19767f26410f7a211384b9c77e5d53893cc489c5" - integrity sha512-JAJpEX0yBaEle2zzbX5z9QAhmEfML1SyQafLwbKCdcOtnkGdk5xD8NKIVxq+nTwYjRwuV7kKnQ+fqU3gpWY0qQ== +"@typescript/native-preview@^7.0.0-dev.20260604.1": + version "7.0.0-dev.20260604.1" + resolved "https://registry.yarnpkg.com/@typescript/native-preview/-/native-preview-7.0.0-dev.20260604.1.tgz#7d5430a8d6fb3f543ea719529b4995c54a56adc9" + integrity sha512-A3/9yZTt2V5NlDURcVJ4mN2YjfeQTXCRyLuENKrNdGhO+y59mC/2UDr7UvpB3Li+83TRAuhDN8SBoM+7gkHdzQ== optionalDependencies: - "@typescript/native-preview-darwin-arm64" "7.0.0-dev.20260509.2" - "@typescript/native-preview-darwin-x64" "7.0.0-dev.20260509.2" - "@typescript/native-preview-linux-arm" "7.0.0-dev.20260509.2" - "@typescript/native-preview-linux-arm64" "7.0.0-dev.20260509.2" - "@typescript/native-preview-linux-x64" "7.0.0-dev.20260509.2" - "@typescript/native-preview-win32-arm64" "7.0.0-dev.20260509.2" - "@typescript/native-preview-win32-x64" "7.0.0-dev.20260509.2" + "@typescript/native-preview-darwin-arm64" "7.0.0-dev.20260604.1" + "@typescript/native-preview-darwin-x64" "7.0.0-dev.20260604.1" + "@typescript/native-preview-linux-arm" "7.0.0-dev.20260604.1" + "@typescript/native-preview-linux-arm64" "7.0.0-dev.20260604.1" + "@typescript/native-preview-linux-x64" "7.0.0-dev.20260604.1" + "@typescript/native-preview-win32-arm64" "7.0.0-dev.20260604.1" + "@typescript/native-preview-win32-x64" "7.0.0-dev.20260604.1" -"@vitejs/plugin-react@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz#d9113b71a0a592714913eafd9e5e63bcafd0ff15" - integrity sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ== +"@vimeo/player@2.29.0": + version "2.29.0" + resolved "https://registry.yarnpkg.com/@vimeo/player/-/player-2.29.0.tgz#f620f4f936706f92c99a3807e7b7ae4a7be9452f" + integrity sha512-9JjvjeqUndb9otCCFd0/+2ESsLk7VkDE6sxOBy9iy2ukezuQbplVRi+g9g59yAurKofbmTi/KcKxBGO/22zWRw== dependencies: - "@rolldown/pluginutils" "1.0.0-rc.7" + native-promise-only "0.8.1" + weakmap-polyfill "2.0.4" -"@vitest/expect@3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.2.4.tgz#8362124cd811a5ee11c5768207b9df53d34f2433" - integrity sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig== +"@vitejs/plugin-react@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz#f70cb8ed0ce225dbc3055d78070f820d8aa35eda" + integrity sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg== + dependencies: + "@rolldown/pluginutils" "^1.0.0" + +"@vitest/expect@3.2.6": + version "3.2.6" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.2.6.tgz#dc3a617acc1f29c132ed6be15456adb75c94a7a4" + integrity sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ== dependencies: "@types/chai" "^5.2.2" - "@vitest/spy" "3.2.4" - "@vitest/utils" "3.2.4" + "@vitest/spy" "3.2.6" + "@vitest/utils" "3.2.6" chai "^5.2.0" tinyrainbow "^2.0.0" -"@vitest/mocker@3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.2.4.tgz#4471c4efbd62db0d4fa203e65cc6b058a85cabd3" - integrity sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ== +"@vitest/mocker@3.2.6": + version "3.2.6" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.2.6.tgz#fd0f2bc2a86d82b6013b3456ad662d04a3551434" + integrity sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw== dependencies: - "@vitest/spy" "3.2.4" + "@vitest/spy" "3.2.6" estree-walker "^3.0.3" magic-string "^0.30.17" -"@vitest/pretty-format@3.2.4", "@vitest/pretty-format@^3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.2.4.tgz#3c102f79e82b204a26c7a5921bf47d534919d3b4" - integrity sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA== +"@vitest/pretty-format@3.2.6", "@vitest/pretty-format@^3.2.6": + version "3.2.6" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.2.6.tgz#5d1272700abe317f24b88009337b2b5cdaa68bf6" + integrity sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA== dependencies: tinyrainbow "^2.0.0" -"@vitest/runner@3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.2.4.tgz#5ce0274f24a971f6500f6fc166d53d8382430766" - integrity sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ== +"@vitest/runner@3.2.6": + version "3.2.6" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.2.6.tgz#6d9fb047b1430431987782e779aa889819a2e964" + integrity sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q== dependencies: - "@vitest/utils" "3.2.4" + "@vitest/utils" "3.2.6" pathe "^2.0.3" strip-literal "^3.0.0" -"@vitest/snapshot@3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.2.4.tgz#40a8bc0346ac0aee923c0eefc2dc005d90bc987c" - integrity sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ== +"@vitest/snapshot@3.2.6": + version "3.2.6" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.2.6.tgz#3a9cb56389289028a511e97bbfd41d914004f3f5" + integrity sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw== dependencies: - "@vitest/pretty-format" "3.2.4" + "@vitest/pretty-format" "3.2.6" magic-string "^0.30.17" pathe "^2.0.3" -"@vitest/spy@3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.4.tgz#cc18f26f40f3f028da6620046881f4e4518c2599" - integrity sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw== +"@vitest/spy@3.2.6": + version "3.2.6" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.6.tgz#c255ab84df924b28d6fbec60a37a7f693db4ea41" + integrity sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg== dependencies: tinyspy "^4.0.3" -"@vitest/utils@3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.2.4.tgz#c0813bc42d99527fb8c5b138c7a88516bca46fea" - integrity sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA== +"@vitest/utils@3.2.6": + version "3.2.6" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.2.6.tgz#6fad3368e48b7a1d058827eb9b4bbc650a3f9402" + integrity sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg== dependencies: - "@vitest/pretty-format" "3.2.4" + "@vitest/pretty-format" "3.2.6" loupe "^3.1.4" tinyrainbow "^2.0.0" @@ -1766,6 +1854,11 @@ baseline-browser-mapping@^2.10.12: resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz#fee941c2a0b42cdf83c6427e4c830b1d0bdab2c3" integrity sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA== +bcp-47-match@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/bcp-47-match/-/bcp-47-match-2.0.3.tgz#603226f6e5d3914a581408be33b28a53144b09d0" + integrity sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ== + bidi-js@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/bidi-js/-/bidi-js-1.0.3.tgz#6f8bcf3c877c4d9220ddf49b9bb6930c88f877d2" @@ -1835,6 +1928,18 @@ caniuse-lite@^1.0.30001782: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz#dfb93d85c40ad380c57123e72e10f3c575786b51" integrity sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ== +castable-video@~1.1.13: + version "1.1.16" + resolved "https://registry.yarnpkg.com/castable-video/-/castable-video-1.1.16.tgz#47b32497610af172f2d610125094c7f54a312a1e" + integrity sha512-wBhe2dZu2afhewL3EaGgVYTyDsa9HvNhY98clMZkNzDrLelOValSrTaoMos9YX7PPBCrgpd1j6YmNyyI2Vbq3w== + dependencies: + custom-media-element "~1.4.6" + +ce-la-react@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ce-la-react/-/ce-la-react-0.3.2.tgz#66c1454e024c3b9f65ebac05724d0f50756ff057" + integrity sha512-QJ6k4lOD/btI08xG8jBPxRCGXvCnusGGkTsiXk0u3NqUu/W+BXRnFD4PYjwtqh8AWmGa5LDbGk0fLQsqr0nSMA== + chai@^5.2.0: version "5.3.3" resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06" @@ -1851,12 +1956,22 @@ check-error@^2.1.1: resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.3.tgz#2427361117b70cca8dc89680ead32b157019caf5" integrity sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA== -chokidar@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" - integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== +chokidar@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-5.0.0.tgz#949c126a9238a80792be9a0265934f098af369a5" + integrity sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw== dependencies: - readdirp "^4.0.1" + readdirp "^5.0.0" + +cloudflare-video-element@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/cloudflare-video-element/-/cloudflare-video-element-1.3.5.tgz#e69a50c8be6ce9f50b00ee1caf45dbc4b7f60010" + integrity sha512-zj9gjJa6xW8MNrfc4oKuwgGS0njRLpOlQjdifbuNxvy8k4Y3pKCyKCMG2XIsjd2iQGhgjS57b1P5VWdJlxcXBw== + +codem-isoboxer@0.3.10: + version "0.3.10" + resolved "https://registry.yarnpkg.com/codem-isoboxer/-/codem-isoboxer-0.3.10.tgz#75182f8b1754163dfdbfd9bc975d7c7994d99e47" + integrity sha512-eNk3TRV+xQMJ1PEj0FQGY8KD4m0GPxT487XJ+Iftm7mVa9WpPFDMWqPt+46buiP5j5Wzqe5oMIhqBcAeKfygSA== concat-map@0.0.1: version "0.0.1" @@ -1900,6 +2015,40 @@ csstype@^3.2.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== +custom-media-element@^1.4.6, custom-media-element@~1.4.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/custom-media-element/-/custom-media-element-1.4.6.tgz#b5a49bbc3fd221b789674f7c0cebc87dda7dec38" + integrity sha512-/HRYqJOa1ob5ik4q7FIJVYxTJCFs/FL3+cQPAJjUf2uiqrDEzbTgB315gQ2rG8oK3w094W9m5tcB8S5Qah+caA== + +dash-video-element@^0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/dash-video-element/-/dash-video-element-0.3.2.tgz#797079abb17f9eebcdf067e20cbf871727367719" + integrity sha512-eN1IqgtTAbq4zVkbt82BDwQtybQ6WOXyQU5HbftUSnqo+SHAPbZCif1Y7uViAhgvuEPvZtbiXDiacvvTeGxc/g== + dependencies: + custom-media-element "^1.4.6" + dashjs "^5.0.3" + media-tracks "^0.3.5" + +dashjs@^5.0.3: + version "5.2.0" + resolved "https://registry.yarnpkg.com/dashjs/-/dashjs-5.2.0.tgz#f525071da302e3bafe6dbc1f1f68b1815654f29a" + integrity sha512-2W2KHFN53Sk7+rtdnIfSUK/3Oov+hraMTeVZwDOTSCNKM1cQtFiJdblNzRMnl59a/QDseG7JW+PC9mh/B+CMcg== + dependencies: + "@svta/cml-608" "1.0.2" + "@svta/cml-cmcd" "2.3.2" + "@svta/cml-cmsd" "1.0.6" + "@svta/cml-dash" "1.0.6" + "@svta/cml-id3" "1.0.6" + "@svta/cml-request" "1.0.12" + "@svta/cml-xml" "1.1.4" + bcp-47-match "^2.0.3" + codem-isoboxer "0.3.10" + fast-deep-equal "3.1.3" + html-entities "^2.6.0" + imsc "^1.1.5" + localforage "^1.10.0" + path-browserify "^1.0.1" + data-urls@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-7.0.0.tgz#6dce8b63226a1ecfdd907ce18a8ccfb1eee506d3" @@ -1957,11 +2106,6 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@^4.0.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - 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" @@ -2012,10 +2156,10 @@ dom-accessibility-api@^0.6.3: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== -dompurify@^3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.2.tgz#f0ff81be682c485505097ba8195a058d8f575218" - integrity sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA== +dompurify@^3.4.8: + version "3.4.8" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.8.tgz#6c54f8c207160e7f83fcb7f4fd05a82ac36b1cdc" + integrity sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ== optionalDependencies: "@types/trusted-types" "^2.0.7" @@ -2043,10 +2187,10 @@ emoji-toolkit@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.21.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz#bb8e6fabaf74930de70e61397798750429e5b1ae" - integrity sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA== +enhanced-resolve@^5.21.0: + version "5.22.2" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.22.2.tgz#b8ff1a9207130b9f5497031ec68d9acb040656e9" + integrity sha512-0rxICaFZ7NQho/sHely2bvOPRP0Eu2B0NZ9zM54YvRvWMn7jfz3DmnOZDR9LlXDdDcqntAVc6Hfy4gr/tdH/Ag== dependencies: graceful-fs "^4.2.4" tapable "^2.3.3" @@ -2295,17 +2439,17 @@ eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^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.2.1: - version "10.3.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.3.0.tgz#ed5b810eb8e0191bf24bddcf9cdb45b974e0a16d" - integrity sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw== +eslint@^10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.4.1.tgz#f6640b176e0912246d9ddbf8fcfa5e8b7f02445a" + integrity sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw== dependencies: "@eslint-community/eslint-utils" "^4.8.0" "@eslint-community/regexpp" "^4.12.2" "@eslint/config-array" "^0.23.5" - "@eslint/config-helpers" "^0.5.5" + "@eslint/config-helpers" "^0.6.0" "@eslint/core" "^1.2.1" - "@eslint/plugin-kit" "^0.7.1" + "@eslint/plugin-kit" "^0.7.2" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/retry" "^0.4.2" @@ -2376,7 +2520,7 @@ expect-type@^1.2.1: resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68" integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: +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== @@ -2431,13 +2575,13 @@ for-each@^0.3.3, for-each@^0.3.5: dependencies: is-callable "^1.2.7" -framer-motion@^12.38.0: - version "12.38.0" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.38.0.tgz#cf28e072a95942881ca4e33fd33be41192fd146b" - integrity sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g== +framer-motion@^12.40.0: + version "12.40.0" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.40.0.tgz#68e53aecd51c8a8a62b565f059b418bef2add0e2" + integrity sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg== dependencies: - motion-dom "^12.38.0" - motion-utils "^12.36.0" + motion-dom "^12.40.0" + motion-utils "^12.39.0" tslib "^2.4.0" fsevents@~2.3.2, fsevents@~2.3.3: @@ -2590,6 +2734,20 @@ hermes-parser@^0.25.1: dependencies: hermes-estree "0.25.1" +hls-video-element@^1.5.9: + version "1.5.11" + resolved "https://registry.yarnpkg.com/hls-video-element/-/hls-video-element-1.5.11.tgz#d9a0526b7d56bee274fc33dd3725a8eb3513dc38" + integrity sha512-tJJ65/52CDxj8XFyIve6zT9nVVdUIc6mqvKR25X0ycPKHk07rpjp4xxVteeCefDUBSf/tFLhlICFmn3KWj37xA== + dependencies: + custom-media-element "^1.4.6" + hls.js "^1.6.5" + media-tracks "^0.3.5" + +hls.js@^1.6.5, hls.js@~1.6.15: + version "1.6.16" + resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.6.16.tgz#e2e7e79b68357cb3f8aa82dec0a36c9c2701f5c0" + integrity sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA== + html-encoding-sniffer@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz#f8d9390b3b348b50d4f61c16dd2ef5c05980a882" @@ -2597,6 +2755,11 @@ html-encoding-sniffer@^6.0.0: dependencies: "@exodus/bytes" "^1.6.0" +html-entities@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.6.0.tgz#7c64f1ea3b36818ccae3d3fb48b6974208e984f8" + integrity sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ== + ignore@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -2607,11 +2770,23 @@ ignore@^7.0.5: resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + 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== +imsc@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/imsc/-/imsc-1.1.5.tgz#7e52690fbfc8a122c1480bc3349336f4bb997034" + integrity sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ== + dependencies: + sax "1.2.1" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -2928,6 +3103,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== + dependencies: + immediate "~3.0.5" + lightningcss-android-arm64@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968" @@ -3002,10 +3184,12 @@ lightningcss@1.32.0, lightningcss@^1.32.0: lightningcss-win32-arm64-msvc "1.32.0" lightningcss-win32-x64-msvc "1.32.0" -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== +localforage@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" locate-path@^6.0.0: version "6.0.0" @@ -3060,10 +3244,22 @@ mdn-data@2.27.1: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.27.1.tgz#e37b9c50880b75366c4d40ac63d9bbcacdb61f0e" integrity sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ== -memoize-one@^5.1.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" - integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +media-chrome@~4.19.0: + version "4.19.1" + resolved "https://registry.yarnpkg.com/media-chrome/-/media-chrome-4.19.1.tgz#d439878c5b4d56307ecbd7865f2dc3ab2b9f4507" + integrity sha512-1+x2l0mNulHKZN0lBxGJwJ+TV2W/KzLjaAd//UCGZz8GE5O5YNafFskWTcv/D6Ty0d9drX9SSfimOzGwob8eVQ== + dependencies: + ce-la-react "^0.3.2" + +media-played-ranges-mixin@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/media-played-ranges-mixin/-/media-played-ranges-mixin-0.1.0.tgz#fce7e3c1449eedc4a15748c82ffa7da6eeeafc64" + integrity sha512-zTsvkleu5sAyTsPVxDI+KUbCwy/lXwHgOPi3ER9S3lhtAWhGTQH6qxvfrVMym1cvoLU36SPbVr6Qe8Zxyc0WpA== + +media-tracks@^0.3.5, media-tracks@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/media-tracks/-/media-tracks-0.3.5.tgz#5b850f6789cdb61792d172b4f7873be0bde70793" + integrity sha512-l54rkKXlLBt3ob3zOLWHcnjvwUmX5bNEZ70igyapOZZC9imzqBmq1oz8p2roiV04KhjblFIi2hetLPF1oYVLRA== min-indent@^1.0.0: version "1.0.1" @@ -3089,17 +3285,17 @@ minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -motion-dom@^12.38.0: - version "12.38.0" - resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.38.0.tgz#9ef3253ea0fb28b6757588327073848d940e9aab" - integrity sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA== +motion-dom@^12.40.0: + version "12.40.0" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.40.0.tgz#95fb411ac72e8adbaf5f1b17b8f07783da223bee" + integrity sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg== dependencies: - motion-utils "^12.36.0" + motion-utils "^12.39.0" -motion-utils@^12.36.0: - version "12.36.0" - resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.36.0.tgz#cff2df2a28c3fe53a3de7e0103ba7f73ff7d77a7" - integrity sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg== +motion-utils@^12.39.0: + version "12.39.0" + resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.39.0.tgz#e1c66f0e912999804bc5e69b4630c3bc794ef29f" + integrity sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ== mrmime@^2.0.0: version "2.0.1" @@ -3111,11 +3307,21 @@ ms@^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: +mux-embed@5.18.1, mux-embed@^5.16.1: + version "5.18.1" + resolved "https://registry.yarnpkg.com/mux-embed/-/mux-embed-5.18.1.tgz#c33094979f8573d7e6743cbfe08f3fecee33e9fc" + integrity sha512-ePsHjiEKY+FgrSBiMmaF+LOtTQSSBWv/1zqpREQFN96JE93xlsArT/MEi30yKOE06MgjOlL70YI750molu3y7g== + +nanoid@^3.3.11, nanoid@^3.3.12: version "3.3.12" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05" integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ== +native-promise-only@0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" + integrity sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -3240,6 +3446,11 @@ parse5@^8.0.1: dependencies: entities "^8.0.0" +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -3275,6 +3486,13 @@ picomatch@^4.0.2, picomatch@^4.0.3, picomatch@^4.0.4: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== +player.style@^0.3.0: + version "0.3.4" + resolved "https://registry.yarnpkg.com/player.style/-/player.style-0.3.4.tgz#89ae398391ba80fc0f60ddbb2db0fcc52133fb81" + integrity sha512-5O9bkbq0APQIkhptyZwp0gUgheCeImGPFo4hvPF2M5xBmgsUYzsUPHCfMye2xyeRE1zvQBKtziaC3jPgnHE1tw== + dependencies: + media-chrome "~4.19.0" + 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" @@ -3295,7 +3513,7 @@ postcss-selector-parser@^7.0.0: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss@^8.5.10, postcss@^8.5.12, postcss@^8.5.6: +postcss@^8.5.10, postcss@^8.5.6: version "8.5.14" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c" integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg== @@ -3304,6 +3522,15 @@ postcss@^8.5.10, postcss@^8.5.12, postcss@^8.5.6: picocolors "^1.1.1" source-map-js "^1.2.1" +postcss@^8.5.15: + version "8.5.15" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c" + integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A== + dependencies: + nanoid "^3.3.12" + 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" @@ -3318,7 +3545,7 @@ pretty-format@^27.0.2: ansi-styles "^5.0.0" react-is "^17.0.1" -prop-types@^15.7.2, prop-types@^15.8.1: +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== @@ -3337,22 +3564,17 @@ react-colorful@^5.7.0: resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.7.0.tgz#82c0f31311606161ed13423979067c865e4a7877" integrity sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg== -react-dom@^19.2.5: - version "19.2.5" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.5.tgz#b8768b10837d0b8e9ca5b9e2d58dff3d880ea25e" - integrity sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag== +react-dom@^19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.7.tgz#0450dc9ae9ddbff76ef196401cd8b8c7fb466ccc" + integrity sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ== dependencies: scheduler "^0.27.0" -react-error-boundary@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-6.1.1.tgz#491d655e86c32434ede852755bb649119fdddd89" - integrity sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w== - -react-fast-compare@^3.0.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" - integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== +react-error-boundary@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-6.1.2.tgz#d213780329ceb3678cec7813a76a00e2a7584f48" + integrity sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng== react-icons@^5.6.0: version "5.6.0" @@ -3369,16 +3591,21 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-player@^2.16.0: - version "2.16.1" - resolved "https://registry.yarnpkg.com/react-player/-/react-player-2.16.1.tgz#f157600bd04a641d9a2c53685235d34dcfabac6b" - integrity sha512-mxP6CqjSWjidtyDoMOSHVPdhX0pY16aSvw5fVr44EMaT7X5Xz46uQ4b/YBm1v2x+3hHkB9PmjEEkmbHb9PXQ4w== +react-player@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/react-player/-/react-player-3.4.0.tgz#2d01f4833b52c221f0ae63e90770612edae71861" + integrity sha512-QpQSHXtnMBKjQVNeaCYMtTVcynWQ0DDDhz/FJu1OR9PHLC1Aih94UqNstywzSHbJ6Oc7lI8/7kDDqcIvyTI6zQ== dependencies: - deepmerge "^4.0.0" - load-script "^1.0.0" - memoize-one "^5.1.1" - prop-types "^15.7.2" - react-fast-compare "^3.0.1" + "@mux/mux-player-react" "^3.8.0" + cloudflare-video-element "^1.3.4" + dash-video-element "^0.3.0" + hls-video-element "^1.5.9" + spotify-audio-element "^1.0.3" + tiktok-video-element "^0.1.1" + twitch-video-element "^0.1.5" + vimeo-video-element "^1.6.1" + wistia-video-element "^1.3.5" + youtube-video-element "^1.8.0" react-remove-scroll-bar@^2.3.7: version "2.3.8" @@ -3407,15 +3634,15 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: get-nonce "^1.0.0" tslib "^2.0.0" -react@^19.2.5: - version "19.2.5" - resolved "https://registry.yarnpkg.com/react/-/react-19.2.5.tgz#c888ab8b8ef33e2597fae8bdb2d77edbdb42858b" - integrity sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA== +react@^19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.7.tgz#1f47a1bfc06f8ec885752c6f4af14369a9f8260b" + integrity sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ== -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== +readdirp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-5.0.0.tgz#fbf1f71a727891d685bb1786f9ba74084f6e2f91" + integrity sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ== redent@^3.0.0: version "3.0.0" @@ -3468,29 +3695,29 @@ resolve@^2.0.0-next.5: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -rolldown@1.0.0-rc.17: - version "1.0.0-rc.17" - resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.17.tgz#c524fc22f6bb37b5588aec862ab1ee11382610f3" - integrity sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA== +rolldown@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac" + integrity sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g== dependencies: - "@oxc-project/types" "=0.127.0" - "@rolldown/pluginutils" "1.0.0-rc.17" + "@oxc-project/types" "=0.133.0" + "@rolldown/pluginutils" "^1.0.0" optionalDependencies: - "@rolldown/binding-android-arm64" "1.0.0-rc.17" - "@rolldown/binding-darwin-arm64" "1.0.0-rc.17" - "@rolldown/binding-darwin-x64" "1.0.0-rc.17" - "@rolldown/binding-freebsd-x64" "1.0.0-rc.17" - "@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.17" - "@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.17" - "@rolldown/binding-linux-arm64-musl" "1.0.0-rc.17" - "@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.17" - "@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.17" - "@rolldown/binding-linux-x64-gnu" "1.0.0-rc.17" - "@rolldown/binding-linux-x64-musl" "1.0.0-rc.17" - "@rolldown/binding-openharmony-arm64" "1.0.0-rc.17" - "@rolldown/binding-wasm32-wasi" "1.0.0-rc.17" - "@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.17" - "@rolldown/binding-win32-x64-msvc" "1.0.0-rc.17" + "@rolldown/binding-android-arm64" "1.0.3" + "@rolldown/binding-darwin-arm64" "1.0.3" + "@rolldown/binding-darwin-x64" "1.0.3" + "@rolldown/binding-freebsd-x64" "1.0.3" + "@rolldown/binding-linux-arm-gnueabihf" "1.0.3" + "@rolldown/binding-linux-arm64-gnu" "1.0.3" + "@rolldown/binding-linux-arm64-musl" "1.0.3" + "@rolldown/binding-linux-ppc64-gnu" "1.0.3" + "@rolldown/binding-linux-s390x-gnu" "1.0.3" + "@rolldown/binding-linux-x64-gnu" "1.0.3" + "@rolldown/binding-linux-x64-musl" "1.0.3" + "@rolldown/binding-openharmony-arm64" "1.0.3" + "@rolldown/binding-wasm32-wasi" "1.0.3" + "@rolldown/binding-win32-arm64-msvc" "1.0.3" + "@rolldown/binding-win32-x64-msvc" "1.0.3" rollup@^4.43.0: version "4.60.3" @@ -3554,17 +3781,22 @@ safe-regex-test@^1.1.0: es-errors "^1.3.0" is-regex "^1.2.1" -sass@^1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.99.0.tgz#ff9d1594da4886249dfaafabbeea2dea2dc74b26" - integrity sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q== +sass@^1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.100.0.tgz#b4cab1bed286fe22ac6c879c514f71cd36aa06c8" + integrity sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ== dependencies: - chokidar "^4.0.0" + chokidar "^5.0.0" immutable "^5.1.5" source-map-js ">=0.6.2 <2.0.0" optionalDependencies: "@parcel/watcher" "^2.4.1" +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== + saxes@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" @@ -3689,6 +3921,11 @@ sirv@^3.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +spotify-audio-element@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spotify-audio-element/-/spotify-audio-element-1.0.4.tgz#6753d568875a85c0a8740bd5eea863980b0ef0ab" + integrity sha512-QdKrJPkYCzaNwwz2vN2eDGyoW0KmQFmnwVprB41mpMzj4qujbqr6pegEchQeTn0b5PceKiLoVu0pp2QDpTcWnw== + stackback@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" @@ -3780,6 +4017,11 @@ strip-literal@^3.0.0: dependencies: js-tokens "^9.0.1" +super-media-element@~1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/super-media-element/-/super-media-element-1.4.2.tgz#833e41e721461e54ef64a9daad6db578a5767cc6" + integrity sha512-9pP/CVNp4NF2MNlRzLwQkjiTgKKe9WYXrLh9+8QokWmMxz+zt2mf1utkWLco26IuA3AfVcTb//qtlTIjY3VHxA== + 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" @@ -3790,16 +4032,21 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -tailwindcss@4.2.4, tailwindcss@^4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.2.4.tgz#f7e3090edb22d56394db4d68e6464d2628dc2aa9" - integrity sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA== +tailwindcss@4.3.0, tailwindcss@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.3.0.tgz#0a874e044a859cf6de413f3a59e76a9bedf05264" + integrity sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q== tapable@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160" integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A== +tiktok-video-element@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tiktok-video-element/-/tiktok-video-element-0.1.2.tgz#22c93f4148afb99882fec050ca30c34673155f93" + integrity sha512-w6TboLm236XJKKiIXIhCbYCnUxbixBbaAoty0etaEAZ/2kHkVIdfZdv2oouMU/HGMsWCHI/VjQ3wU3MJ+s192Q== + tinybench@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" @@ -3810,7 +4057,7 @@ tinyexec@^0.3.2: resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== -tinyglobby@^0.2.14, tinyglobby@^0.2.15, tinyglobby@^0.2.16: +tinyglobby@^0.2.14, tinyglobby@^0.2.15: version "0.2.16" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== @@ -3818,6 +4065,14 @@ tinyglobby@^0.2.14, tinyglobby@^0.2.15, tinyglobby@^0.2.16: fdir "^6.5.0" picomatch "^4.0.4" +tinyglobby@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + tinypool@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" @@ -3874,6 +4129,11 @@ tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +twitch-video-element@^0.1.5: + version "0.1.6" + resolved "https://registry.yarnpkg.com/twitch-video-element/-/twitch-video-element-0.1.6.tgz#986668a6b676b5b42f2abab365014208e2f6edff" + integrity sha512-X7l8gy+DEFKJ/EztUwaVnAYwQN9fUJxPkOVJj2sE62sGvGU4DNLyvmOsmVulM+8Plc5dMg6hYIMNRAPaH+39Uw== + 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" @@ -3926,15 +4186,15 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" -typescript-eslint@^8.59.1: - version "8.59.2" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz#e24b4f7232e20112e40572dba162a829a738ce98" - integrity sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ== +typescript-eslint@^8.60.1: + version "8.60.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.1.tgz#13db05c6eabb89669deec44545b788a0e9aee640" + integrity sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA== dependencies: - "@typescript-eslint/eslint-plugin" "8.59.2" - "@typescript-eslint/parser" "8.59.2" - "@typescript-eslint/typescript-estree" "8.59.2" - "@typescript-eslint/utils" "8.59.2" + "@typescript-eslint/eslint-plugin" "8.60.1" + "@typescript-eslint/parser" "8.60.1" + "@typescript-eslint/typescript-estree" "8.60.1" + "@typescript-eslint/utils" "8.60.1" typescript@^6.0.3: version "6.0.3" @@ -3951,10 +4211,10 @@ unbox-primitive@^1.1.0: has-symbols "^1.1.0" which-boxed-primitive "^1.1.1" -undici-types@~7.19.0: - version "7.19.2" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.19.2.tgz#1b67fc26d0f157a0cba3a58a5b5c1e2276b8ba2a" - integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg== +"undici-types@>=7.24.0 <7.24.7": + version "7.24.6" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.24.6.tgz#61275b485d7fd4e9d269c7cf04ec2873c9cc0f91" + integrity sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg== undici@^7.25.0: version "7.25.0" @@ -4001,6 +4261,14 @@ util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +vimeo-video-element@^1.6.1: + version "1.7.2" + resolved "https://registry.yarnpkg.com/vimeo-video-element/-/vimeo-video-element-1.7.2.tgz#c128bff549931191731fc8c1552537a5023b3812" + integrity sha512-7QM7fvSZvTTSq4igxBuO6Gc+0u3Exgk4IaLNixVzilCPzHEf7SN8b6YLXSM5QCs0ineTJI4XjiUCSoIbabHvwg== + dependencies: + "@vimeo/player" "2.29.0" + media-played-ranges-mixin "^0.1.0" + vite-node@3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07" @@ -4026,32 +4294,32 @@ vite-node@3.2.4: optionalDependencies: fsevents "~2.3.3" -vite@^8.0.10: - version "8.0.10" - resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.10.tgz#fb31868526ec874101fac084172a2cdc6776319b" - integrity sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw== +vite@^8.0.16: + version "8.0.16" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.16.tgz#ae073866c06563d6634a90169a496e11bd84f1a6" + integrity sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw== dependencies: lightningcss "^1.32.0" picomatch "^4.0.4" - postcss "^8.5.10" - rolldown "1.0.0-rc.17" - tinyglobby "^0.2.16" + postcss "^8.5.15" + rolldown "1.0.3" + tinyglobby "^0.2.17" optionalDependencies: fsevents "~2.3.3" -vitest@^3: - version "3.2.4" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.2.4.tgz#0637b903ad79d1539a25bc34c0ed54b5c67702ea" - integrity sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A== +vitest@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.2.6.tgz#de42ebfb58e16faba4e5a314fe35e146e03cb340" + integrity sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw== dependencies: "@types/chai" "^5.2.2" - "@vitest/expect" "3.2.4" - "@vitest/mocker" "3.2.4" - "@vitest/pretty-format" "^3.2.4" - "@vitest/runner" "3.2.4" - "@vitest/snapshot" "3.2.4" - "@vitest/spy" "3.2.4" - "@vitest/utils" "3.2.4" + "@vitest/expect" "3.2.6" + "@vitest/mocker" "3.2.6" + "@vitest/pretty-format" "^3.2.6" + "@vitest/runner" "3.2.6" + "@vitest/snapshot" "3.2.6" + "@vitest/spy" "3.2.6" + "@vitest/utils" "3.2.6" chai "^5.2.0" debug "^4.4.1" expect-type "^1.2.1" @@ -4075,6 +4343,11 @@ w3c-xmlserializer@^5.0.0: dependencies: xml-name-validator "^5.0.0" +weakmap-polyfill@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/weakmap-polyfill/-/weakmap-polyfill-2.0.4.tgz#bcc301e4c8eb4eda3e406f08f1a691093e407884" + integrity sha512-ZzxBf288iALJseijWelmECm/1x7ZwQn3sMYIkDr2VvZp7r6SEKuT8D0O9Wiq6L9Nl5mazrOMcmiZE/2NCenaxw== + webidl-conversions@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz#0657e571fe6f06fcb15ca50ed1fdbcb495cd1686" @@ -4162,6 +4435,13 @@ why-is-node-running@^2.3.0: siginfo "^2.0.0" stackback "0.0.2" +wistia-video-element@^1.3.5: + version "1.4.0" + resolved "https://registry.yarnpkg.com/wistia-video-element/-/wistia-video-element-1.4.0.tgz#33a52245346da85a076c4ae1a59ff618011ca540" + integrity sha512-udI8/yiMZ+KIGwYK1qNOFiXl+s/wffiY+XkwTXlBFICZylFalOS0QYEQqYOmuBsm3jNfGUS4O50tLuN7Ejqm3A== + dependencies: + super-media-element "~1.4.2" + word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" @@ -4187,6 +4467,13 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +youtube-video-element@^1.8.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/youtube-video-element/-/youtube-video-element-1.9.0.tgz#f0a786819b935bae96b12a9e11d67c5dbb091b82" + integrity sha512-Hh0dbQM+FVlUaYUbpYkZNUvdKxTNcSNvTGzkQKYShltnX+LRHEp2eYvC2Zm43eU8Np+CBZuoNR2i+seCYzzAyg== + dependencies: + media-played-ranges-mixin "^0.1.0" + zod-validation-error@^3.0.3: version "3.5.4" resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.5.4.tgz#9072f829e4b45b9e27317c3002408c0c4cdd2bb4" @@ -4207,7 +4494,7 @@ zod@^3.22.4: resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356" integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== -zustand@^5.0.13: - version "5.0.13" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.13.tgz#06995c126e8903cd27100af04da91c36ae3051ed" - integrity sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ== +zustand@^5.0.14: + version "5.0.14" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.14.tgz#18216c24fcb980cf36898f9c57520e67b1f77855" + integrity sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g== From f4d41dd3c9bb458618becbba779590caa1bad718 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 5 Jun 2026 14:32:55 +0200 Subject: [PATCH 7/9] Take #2 Desktop cacta 100% --- .../images/catalog/bitmap/catalog_skin1.png | Bin 0 -> 15445 bytes .../images/catalog/bitmap/catalog_skin2.png | Bin 0 -> 7807 bytes .../images/catalog/bitmap/catalog_skin3.png | Bin 0 -> 24279 bytes .../images/catalog/bitmap/catalog_skin4.png | Bin 0 -> 24279 bytes .../images/catalog/bitmap/catalog_skin5.png | Bin 0 -> 24279 bytes .../images/catalog/bitmap/catalog_skin6.png | Bin 0 -> 2614 bytes .../images/catalog/buttons/btn_secondary.png | Bin 0 -> 182 bytes .../buttons/btn_secondary_disabled.png | Bin 0 -> 179 bytes .../catalog/buttons/btn_secondary_hover.png | Bin 0 -> 202 bytes .../catalog/buttons/btn_secondary_pressed.png | Bin 0 -> 197 bytes src/assets/images/catalog/buttons/buy.png | Bin 0 -> 317 bytes .../images/catalog/buttons/buy_disabled.png | Bin 0 -> 552 bytes .../images/catalog/buttons/buy_hover.png | Bin 0 -> 317 bytes .../images/catalog/buttons/buy_pressed.png | Bin 0 -> 317 bytes src/assets/images/catalog/buttons/close.png | Bin 0 -> 269 bytes .../images/catalog/buttons/close_hover.png | Bin 0 -> 263 bytes .../images/catalog/buttons/close_pressed.png | Bin 0 -> 250 bytes src/assets/images/catalog/buttons/help.png | Bin 0 -> 236 bytes .../images/catalog/buttons/help_hover.png | Bin 0 -> 228 bytes .../images/catalog/buttons/help_pressed.png | Bin 0 -> 231 bytes src/assets/images/catalog/buttons/minus.png | Bin 0 -> 195 bytes .../images/catalog/buttons/minus_disabled.png | Bin 0 -> 187 bytes .../images/catalog/buttons/minus_hover.png | Bin 0 -> 202 bytes .../images/catalog/buttons/minus_pressed.png | Bin 0 -> 199 bytes src/assets/images/catalog/buttons/plus.png | Bin 0 -> 237 bytes .../images/catalog/buttons/plus_disabled.png | Bin 0 -> 221 bytes .../images/catalog/buttons/plus_hover.png | Bin 0 -> 248 bytes .../images/catalog/buttons/plus_pressed.png | Bin 0 -> 232 bytes .../catalog/scrollbar/scroll_h_left.png | Bin 0 -> 178 bytes .../scrollbar/scroll_h_left_disabled.png | Bin 0 -> 181 bytes .../scrollbar/scroll_h_left_pressed.png | Bin 0 -> 172 bytes .../catalog/scrollbar/scroll_h_right.png | Bin 0 -> 188 bytes .../scrollbar/scroll_h_right_disabled.png | Bin 0 -> 193 bytes .../scrollbar/scroll_h_right_pressed.png | Bin 0 -> 194 bytes .../catalog/scrollbar/scroll_h_thumb.png | Bin 0 -> 183 bytes .../scrollbar/scroll_h_thumb_disabled.png | Bin 0 -> 183 bytes .../scrollbar/scroll_h_thumb_pressed.png | Bin 0 -> 192 bytes .../catalog/scrollbar/scroll_v_down.png | Bin 0 -> 175 bytes .../scrollbar/scroll_v_down_disabled.png | Bin 0 -> 177 bytes .../scrollbar/scroll_v_down_pressed.png | Bin 0 -> 196 bytes .../catalog/scrollbar/scroll_v_thumb.png | Bin 0 -> 188 bytes .../scrollbar/scroll_v_thumb_disabled.png | Bin 0 -> 188 bytes .../scrollbar/scroll_v_thumb_pressed.png | Bin 0 -> 183 bytes .../images/catalog/scrollbar/scroll_v_up.png | Bin 0 -> 175 bytes .../scrollbar/scroll_v_up_disabled.png | Bin 0 -> 177 bytes .../catalog/scrollbar/scroll_v_up_pressed.png | Bin 0 -> 181 bytes .../catalog/CatalogAdminContext.tsx | 76 +- src/components/catalog/CatalogClassicView.tsx | 52 +- .../views/admin/CatalogAdminOfferEditView.tsx | 12 +- .../views/admin/CatalogAdminPageEditView.tsx | 44 +- .../navigation/CatalogNavigationItemView.tsx | 6 +- .../page/common/CatalogGridOfferView.tsx | 68 +- .../page/layout/CatalogLayoutDefaultView.tsx | 121 +- .../CatalogLayoutGuildCustomFurniView.tsx | 12 +- .../page/layout/CatalogLayoutRoomAdsView.tsx | 13 +- .../layout/CatalogLayoutSingleBundleView.tsx | 2 +- .../views/page/layout/GetCatalogLayout.tsx | 3 - .../page/layout/pets/CatalogLayoutPetView.tsx | 10 +- .../CatalogGuildSelectorWidgetView.tsx | 2 +- .../widgets/CatalogPriceDisplayWidgetView.tsx | 12 +- .../widgets/CatalogPurchaseWidgetView.tsx | 28 +- .../page/widgets/CatalogSpinnerWidgetView.tsx | 16 +- .../widgets/CatalogViewProductWidgetView.tsx | 2 +- .../ChatInputCommandSelectorView.tsx | 78 +- .../ChatInputMentionSelectorView.tsx | 136 +- .../room/widgets/chat-input/ChatInputView.tsx | 9 +- src/css/backgrounds/BackgroundsView.css | 68 +- src/css/catalog/CatalogClassicView.css | 1917 ++++++++++++----- src/css/chat/ChatInputMentionSelectorView.css | 366 ++++ src/css/chat/Chats.css | 594 ----- src/css/common/Buttons.css | 67 + src/css/friends/FriendsView.css | 42 +- src/css/habbo/HabboSwfSkin.css | 168 ++ src/css/icons/icons.css | 7 - src/css/index.css | 125 +- src/css/login/LoginView.css | 2 +- src/css/navigator/HabboNavigatorDesktop.css | 242 +++ src/hooks/chat-history/useChatHistory.ts | 13 + src/hooks/rooms/widgets/useChatWidget.ts | 11 + src/hooks/useLocalStorage.ts | 18 +- src/index.tsx | 5 + 81 files changed, 2898 insertions(+), 1449 deletions(-) create mode 100644 src/assets/images/catalog/bitmap/catalog_skin1.png create mode 100644 src/assets/images/catalog/bitmap/catalog_skin2.png create mode 100644 src/assets/images/catalog/bitmap/catalog_skin3.png create mode 100644 src/assets/images/catalog/bitmap/catalog_skin4.png create mode 100644 src/assets/images/catalog/bitmap/catalog_skin5.png create mode 100644 src/assets/images/catalog/bitmap/catalog_skin6.png create mode 100644 src/assets/images/catalog/buttons/btn_secondary.png create mode 100644 src/assets/images/catalog/buttons/btn_secondary_disabled.png create mode 100644 src/assets/images/catalog/buttons/btn_secondary_hover.png create mode 100644 src/assets/images/catalog/buttons/btn_secondary_pressed.png create mode 100644 src/assets/images/catalog/buttons/buy.png create mode 100644 src/assets/images/catalog/buttons/buy_disabled.png create mode 100644 src/assets/images/catalog/buttons/buy_hover.png create mode 100644 src/assets/images/catalog/buttons/buy_pressed.png create mode 100644 src/assets/images/catalog/buttons/close.png create mode 100644 src/assets/images/catalog/buttons/close_hover.png create mode 100644 src/assets/images/catalog/buttons/close_pressed.png create mode 100644 src/assets/images/catalog/buttons/help.png create mode 100644 src/assets/images/catalog/buttons/help_hover.png create mode 100644 src/assets/images/catalog/buttons/help_pressed.png create mode 100644 src/assets/images/catalog/buttons/minus.png create mode 100644 src/assets/images/catalog/buttons/minus_disabled.png create mode 100644 src/assets/images/catalog/buttons/minus_hover.png create mode 100644 src/assets/images/catalog/buttons/minus_pressed.png create mode 100644 src/assets/images/catalog/buttons/plus.png create mode 100644 src/assets/images/catalog/buttons/plus_disabled.png create mode 100644 src/assets/images/catalog/buttons/plus_hover.png create mode 100644 src/assets/images/catalog/buttons/plus_pressed.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_h_left.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_h_left_disabled.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_h_left_pressed.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_h_right.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_h_right_disabled.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_h_right_pressed.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_h_thumb.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_h_thumb_disabled.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_h_thumb_pressed.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_v_down.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_v_down_disabled.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_v_down_pressed.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_v_thumb.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_v_thumb_disabled.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_v_thumb_pressed.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_v_up.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_v_up_disabled.png create mode 100644 src/assets/images/catalog/scrollbar/scroll_v_up_pressed.png create mode 100644 src/css/chat/ChatInputMentionSelectorView.css create mode 100644 src/css/habbo/HabboSwfSkin.css create mode 100644 src/css/navigator/HabboNavigatorDesktop.css diff --git a/src/assets/images/catalog/bitmap/catalog_skin1.png b/src/assets/images/catalog/bitmap/catalog_skin1.png new file mode 100644 index 0000000000000000000000000000000000000000..388d8519655a67b41823f67b3ec3879e19f3ceb5 GIT binary patch literal 15445 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ĥq3+d5x5FgpL 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

3QVTZuA15Os1uK|?@xb;Pry ztZhcY(twG=%fo(fF+L&~sfAhCs5M~~%DytVT2{rs#7bfh7Zgtvd7;i1UKHL5d)m-H zVa@Vhw^PMWf-Xq?waKS@gUx%tFP$EXtjiwXws_+3;>LqedaB4?6b;eChaaHsT^}gI ztMQd0_-nE`F(-77hhH5H#bB^={)yFG-Ct=1!t1_O^w00ecZ^ctk_EC5;|Bwh?l1%U z39_^OsW0tJU)~PA*jj3-EU>=K&FO60h(Y*cL+ z1axPE847OkacuevVX2a-PNKstm``_wwjZrsfq2I!HS!Q&B7S)d;m&Lz5gC#iY6U*9 z`Hj2IXsft!GUOm5&`Z@GLZY!g(sP|zy4I{w#KghG|Ol&9}q06rIi5B?*Dbee1yTlrh9&mUrX{Q?aK?vAV%iB_Wg_KW$!9QPHot^HiT z>�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 PcMjdqG0-m5vU%};^bEs} literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/bitmap/catalog_skin4.png b/src/assets/images/catalog/bitmap/catalog_skin4.png new file mode 100644 index 0000000000000000000000000000000000000000..f79cf9cac6a9ad34f6bfe8209821e7f92ec7d3e7 GIT binary patch literal 24279 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 PcMjdqG0-m5vU%};^bEs} literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/bitmap/catalog_skin5.png b/src/assets/images/catalog/bitmap/catalog_skin5.png new file mode 100644 index 0000000000000000000000000000000000000000..f79cf9cac6a9ad34f6bfe8209821e7f92ec7d3e7 GIT binary patch literal 24279 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 PcMjdqG0-m5vU%};^bEs} literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/bitmap/catalog_skin6.png b/src/assets/images/catalog/bitmap/catalog_skin6.png new file mode 100644 index 0000000000000000000000000000000000000000..b89f9080ff90b47704b53f68bfa424480ad6496c GIT binary patch literal 2614 zcmai0c{r478=sk`yv8P7anrC}|W71{1QjGIs&r zHSlCW`@nn4QJ4XP@h@7No4B2aFO}Lx%36q}*7pfdt6jEC*>fOAPE_>6T?D^`#C1t4 zG_os09EHZ1knn_ydyB(<{lmtJBrGKnc8Gv^aX>-4L@+_WmiRzts&Kw;VVqMwlebc~ zV!S!YisAYl(=lq^33bBtIt3eyZLSUvKM#cEhQWh1cH7=L?u8}M7 z!br}oily$pl@nsh`T+~&1$KQhwJGb_(VsL$m0s9-JSW4Mz}5S$tTfTtRtTfL_NU7e zX4O=Kk29y$`d^I>@BEzJ6WsLJ7jLJ#gUO!rssZxbw>NiD4QlYOA6Rk5$F~yv^ z+cnh-l51;k&97~4-0u~4P7?bNHOf3zgR_}$zO=w+cctj{8EyK)XxPt?Wy^=#o4--@ zD%x3z`ahU0V)HhJv=J0HqMIQ z(aY+jz6^-L-!$v~Mrrx-4a2J3vhyZvX}BwS<3D2UxvQOFgj^kX9(i#8u_hC|1iV`| zQ=?N)lxTz{AL%gE<)ioWqi6d)qm+?$AVhV+ zvqW`>H+E@lZ1BA9s=pVZ-$TY-GAW`)l@=GjDt@0+>fq=QykwzMa-&AKr-6&0kDxz_ z&lI$BUeUO(Tt^2KwrpCd;orH+@2ff$oRl7WJjB0OD=p0?tYxw1HtDG5-_O2P>j}W;>WCN6 z;Ab@6k#KPZfe@@tTzEdam3VX<#Ua4x`ba2RE{-bzH$y^Aw;orBV@OS+M9Eq0u5W># z=QFUEruwplvO6{>-(9+GdHVXw&CBi6dg}OU{IxT=}XPS<}_;9bfaBx83SBGtJ7wRdXLCV#%Esf3_O<8)vb6 zT0ZQwoO|xu5!K_{MtkMU+w`Xfg@>!m`_Z1*maTdSacnXfA!sWzwmBm*|9ywEjrY&7 znjNHDVupt#{h#1Jq+=6W2DH z?PuCAgGcKy9R?^;1rDx59+8^6_`N@2vq$8u&_UW$gF9*zH1P!6jbT)*qpq&SY}fXN zrtR3L7}5I%!_es#NyI*d(6NHaKY{3)d_2+?6yV+5$L^srCo2DE#UT_KzQ!UR*rZ ziO(g#vE+;I{^Fa+mNVdPERRn-2p{KeZ^!4V#yE@JEHIpDkgmJ=UjL9NawKuY&DBus z-buf>{^g0bQB4hvhej7R9!|7T+kD&3wUioA2bCt&MQHu5lXbVe?4MivLRXcY)|eTp zYi*y}r*LuK$jyno9h=O|%!t1`6HJt}Kh;aux##U~He)M+G$ab;X0YeCI437$SRJ#% z^2STTa{AN4IKz*^vd4&dF|2u#-Y%L* zx-35+^*ftg@yVFrPa|f#OLW30mq>O1U=`nXLT)aM>;M(Jpwq3Bh?bR^nn)KcP|HkJ zm5E^0K3gM*sREqNn2q?%>Ce+*PU|hPf{=9Xu2k6HVWG#Ffm>-y7;Wga@-E>+M&wQn z|8N~Cx~IB0`qCdD1vNR%&)0_ARo%ffCAJfY*?>nmq1U6)N$e3Mv^H|}Ap+5WCo#P5 z+Uf4ULK-wq(VOG^_P_xHjLs_}?RgN;S^-2MN$+cWc5|t6DRiV-`N6mb%)*_pe}$b37gGtaCxP0Dhy)8U7`i6tN4k zllUQPdM1HpTbGI_66hYj6qSLpB7~&bWVt5tta5C&tsPqB7cw)VpmULTvlZ_M*j&mkm4DTkaS<( zHgh${p;%^)h@5o$Bs{;r7YQY zjK0JDdZ4DEh!k4r2_HD!12{*+uhm)hX9elcL0Ks(?FhPuC|LA4Qk2F91*Yj>{pcih zpnMN4ZwGrF+XH};V%G8aAtG>v&j=?R$QwQDr1XVO>H+Zg2#~f)AYB6iazy@d4gq}? zMix#JRfKeHlZZ#3l`o*76G=oqvkVNfNEv-(;Q63wEEEHJ_{!H+op=ZE1Bd#4jZ#o1 zgN|*%axAbfhc72CxN@EZ-z==!)e``#T=3jeul}@3G|~oK(`UmuVX!yQ1UlBVX06BEBX%hs@TBa){{;%0 z4m+X*YcSw&wZC)L zq9Ts{@xuZ^=B=|b>U*5;hu!)kF1F%9PiU6^#MRA=4UxN@pL9&s*eJ12?{oG+1HYv{ zla{FNQ7i6T^rGqHyzE!3Y2vB#?(JE8Q8o6GYW?IRFHb5w)12_kUiAFmviUz)D~;7W hg#+pY$eCcPr_5$rvP000&U1^@s6HNQ8u0001yNkl zIg(3AA4~l5Kk2lUnX9i(-;7iqK&(nG6SPFIkcGTh0onP43E5JoU;qFB07*qoM6N<$ Ef{m{zN^yG(u;e01?VmYPgg&ebxsLQ0P)&OSpWb4 literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/buttons/buy.png b/src/assets/images/catalog/buttons/buy.png new file mode 100644 index 0000000000000000000000000000000000000000..de4bf35f3037a2251a5637ea5c724b0c73e36a63 GIT binary patch literal 317 zcmV-D0mA-?P)ow z7f~Jk;@i7KpT5)pdJ_Y1J?y`hdWpZji)p=yVVIhyWOtvufC1!qI1IR^fX4B_;h3(3-Fa#*Odv03{#jv zE}4Ihs0+!iTns>G3v-n>>2AF;2b7QxgM2v2uku>Sky3r{02q11NO|$?DAvKk9;t9 z7Ur*m82~~E0DzPdM>9X4=A4`Hd7deLO_oCl@ZJLeVvKDj8@Q0M`E^}2X?an9kwXZ$ z-|x8HZpb;~{eCyu8)LfvopU?lloFO@DLexs0)Ta0k#la8)VAsQl+w<2=N!gy1h&!$ z8NBy}D5V5rOxG$FItfiUO;dRuM3sd2EdV`55;Ct76P2QFvo{xIF~(wD4fGd+o=N~1 zhM_2BG>b}cJixn2Mxlk(kg0T_8x1h&*$??)&tkTOrciFx>Uw?HnQyW7_+aJ z0K=0QSXL~w8U|d~jN$HT#xgov*k{BInf_Q?#7KKPNF6K*s;nkFQZE1NjesR~TC=lV zz78SM?>DV~WQSzgQ+Q@?)B3&EJ*p>5kTuUvnG9NMn-}$SZxPqaqUpj-*;kVq0b@sh z??lkgI(boM=6T*732Xstf|`hxz&Qu+J(gv`>-7QvLu+QOJ>8;uwG^Vu<$@Svu@I>4 zAlK`)k*?1;*4FjXu-3wRj~FAK&u39plL0^;#}U&sHJ6uviO1ssW6X|-kpbJ~WV@&U q_-S2jhYmKkku>;6+rodp5q|^Upnv&*AaUIQ0000ow z7f~Jk;@i7KpT5)pdJ_Y1J?y`hdWpZji)p=yVVIhyWOtvufC1!qI1IR^fX4B_;h3(3-Fa#*Odv03{#jv zE}4Ihs0+!iTns>G3v-n>>2AF;2b7QxgM2v2uku>Sky3r{ow z7f~Jk;@i7KpT5)pdJ_Y1J?y`hdWpZji)p=yVVIhyWOtvufC1!qI1IR^fX4B_;h3(3-Fa#*Odv03{#jv zE}4Ihs0+!iTns>G3v-n>>2AF;2b7QxgM2v2uku>Sky3r{VQ?)ECI#7O35265oVo-bzU@RF}1ST z^bMlI4y#21;2<8)s!RfEAQdJVH4p&Q;pKFHB*E?Z_OZ6kV>h29dB#lj$~cGv;tz0% zFbR|cRL?AKNotX+mNqq$7W>cdv}tc-sJM^N4#gkLvwk=hlX_8Tlhm{f5CFUZR3b&K Typkc&00000NkvXXu0mjfNKn5 zE*ZxAJ^Eoc&rO9C<_o$GY!SSN(*z9J9`PEN=qG+wGDL`_m+!l})nQJ+$zCCc6^GBa81~ZU} zEZsN&3pPRBV^`qp%T_uB#bJP;%U}i|8-xoGf!*(LY@LBn7M5n~|MM2kx!Ap@zJov2!B zFfu?3MJkkMh{BOFS$teyE|9PG;yICAO zaNxuJO}WZ9)nwOyN>2cS6c@7>znfR9NFQ0bfY*EirsN7azNoc8@ jEV$lCi6;r@90mq&2^~w0WYJkb=QDV^`njxgN@xNAd_h`q literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/buttons/help_hover.png b/src/assets/images/catalog/buttons/help_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..901d0e9ef130ee9e8a68dccd45db84870c60738e GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S1|*9D%+3I*)t)YnAr*6yQyd!p^E_d8vp9I* zz=!z<4LWKTpIe`t00b#6W*%{&b9FYeuvSRwomrKX(8M_*TfitmX-!MTfhPY?4_B3m z?5l}TjXUg7B)RM13x@TlC&W6PP+()LYLjGUUe(RfDtTu^d;a|g=2tC6mrFS^#yyHT z_F#@z04I+C-{!rW6=!I*O=NiS##e~pXq%*l_Mw{}KAllm+nd9z53;UdTDpNlp(i)P YKUL+7bJyS40iDg@>FVdQ&MBb@0M*A=1ONa4 literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/buttons/help_pressed.png b/src/assets/images/catalog/buttons/help_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..71477b64a431af035b9e2e76b87f40ca443c1793 GIT binary patch literal 231 zcmeAS@N?(olHy`uVBq!ia0vp^!ayv-!3HGrL)l7z)H+WW$B>FS$teyE|9PG;yICAO zaNxuL|Mj2FCZ)8@INFgD@ak5yinNJ!1aF=Kw~s;TTirI}-Pb1NL})o1+;I?#={k3Q z%@M7<8Ta}wC#VI*Hk}sQ#B-Rvpi20z^^X{r3kFHN4s0sb!tpE%cD(u9e!hY?h-3XB z?f&btTTCXW+-RPft0L)lb&2Erl4A!Ps%8Xky^@g9Al4P4abBQR`KFrUOwMiww>_si f3_2Pc8yOiAV+vPixHbUA~ktDnm{r-UW|Jjq&5 literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/buttons/minus.png b/src/assets/images/catalog/buttons/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..0a6484571a9493086d2d5d67ba7973c24d2869db GIT binary patch literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fU7jwEAr*7pUOmWrSb?YY;r-C{ zItQ5UO=P?=k#!FP^OAP9szN2DpOduSA2`_LFE+{XN2;=#k$6J(+SV1TxD<>o%qix` zNn4?{Uf}FW=KB(L)klL)sV=#;i;a27lxY|4?T((mVWzHQ+@8(tvht;yW?5dESn2jl v{=wg?{3ka&yDp+uul!i+Ny%+jkq6AX_bP-7uw0D>x{ATm)z4*}Q$iB}ih502 literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/buttons/minus_disabled.png b/src/assets/images/catalog/buttons/minus_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..ab8f0e1202623d3cb3002f1c65be8993e19c2c43 GIT binary patch literal 187 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fO`a}}Ar*7pUfsxf#DJ&u;eEy4 z>pNuMWWIShZ&_j438}oxDg~{ULVhevl~1+pS_CvUEWftvfq*Axg58Cnu6FJX9$p6} zSr43<5p#6?o)66rD?_$jvkRXv=W`VoTZiF1r|S2I|J@7yGwX?d#g@&hEAQ>x`+;}E ml9aT(1v3>jTNi(*TPCe0q?X59zfT$H8U{~SKbLh*2~7ZZdrboX literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/buttons/minus_hover.png b/src/assets/images/catalog/buttons/minus_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..c3108ba28944a3a6f01761a1ca4b4f468292d9d9 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4f6FprVLn`LHy=u#KK!Jz#!SqX4 z9Y3_qJ8hg+AZC5#SFr4*NlP!7TcrHp@KRc+>Z~5Z&tRD@JN-q8r|{d4zM*eE9hPtV z9%|U~f#c)7(+La)$2u-ZJl z&vt#=w(eXNtLh}X}QZ?7<}00000NkvXXu0mjfioak_ literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/buttons/plus_hover.png b/src/assets/images/catalog/buttons/plus_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..842451e18e089b8580e5a1bc5e17645e5a6c4f9e GIT binary patch literal 248 zcmVP)0jLn25=>NbUO;L>TGEXVpo>4m{A`p?b{4E)encnS7nNOQH%X3`c z_g$>(;9Bd#<66?$spHW}b2@XV0*4ST^PKZ?U(!pin9+_LN;6aY@?i7b9fdD4ae0000DlcR+!IwbJ+f zr+4)-dwF6rrM34@n`(HbflcB4g|l-dBOI^V@H?DiCDR3^>YyBjH`{mIT=_QrthC_bMC8{ d*g^G+tiJ|rY?z z2dDlu7nZSAT`~(3i1Iu3K`w*g3t#k|osE(vr#~~icvHY0ojsN9#R-*_S}Rv+bF8jT zSk){m!)3HV`^2Gj4le&VwI+QKHlFlW=8ejN%ogp*C(a~`ui3=B*s8#uk%6(})AdQm hciuas@4RV978JRES++Y_kaQq^TH$r zwT~W^DJo()@g5y&7J_wAyOy~ens(oeugTeHi VlC`+7y5mVrP(G(b=C*+(Ym%s|KCU6i7SRpo0YyBU13R}P`{0h8tJ>aX7Y>Duh9Jy$otOSciwR`TvcZJSXpP4M?t(qdlG%szE%!T#kC(jsc z*1KT#ipkW(>78c{*O>_ySOisA&P*_vQ2dXt=a6E8SRB)8gOJrScaKcDB)TWO>bLHm tx%v*rUp-RzvHoWLxzcHGIWzt>3kNG3omyzNs~YGg22WQ%mvv4FO#rAVN+|#U literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/scrollbar/scroll_h_right_pressed.png b/src/assets/images/catalog/scrollbar/scroll_h_right_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..744458694080f3a8c92a9178d3aaed35a1524959 GIT binary patch literal 194 zcmeAS@N?(olHy`uVBq!ia0vp^0zfRt!3HF+tk*dLq&hua978JRES++Z_kaS2%lT!@ zcU5|xuq#`3O5c>f9irT&TYu!v;{XE&jT_w;)LldGzLH30wlZ7o92&?i#$fTDd6J&P zxt%B5`CM3(j94bHoIHMX3oG}gk`2s-v5$Bj)vN=HP7tt?_dzHQSIBCTs^V)ozjG$Rj(c> z7GBl9v&VYjsui!E|LVN1RB!AWwalZNA#9r0`H!u&(>pxodhAZA@(@|&vgd}7rhNZ& hmNlI_B4s4fvcwkcH$1+Idj-g844$rjF6*2UngG) literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/scrollbar/scroll_h_thumb_disabled.png b/src/assets/images/catalog/scrollbar/scroll_h_thumb_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..d75689dd4a9cd5d3a0e5f0bf399bb6d989de84b9 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^NgikQ@(#X i%bLy|kunl#Sz`6;P1~al%M^evVeoYIb6Mw<&;$S?w?-EL literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/scrollbar/scroll_h_thumb_pressed.png b/src/assets/images/catalog/scrollbar/scroll_h_thumb_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..0627403845eea4d528b7b5cf769b5ca8f45add5b GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^N(n57lgw-)DlGGyyAG=#Nr>`mR3*fUe; zK(%~kl;EN#u1}LcXYw67lkn{NE;ln}e~qQPN_yXKO*5&VFy&d|)u{59Qh~0QZs)12 r3=f+)Gtr4-NBjZNn0rjK|G4|z4AfTazHrtw literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/scrollbar/scroll_v_down.png b/src/assets/images/catalog/scrollbar/scroll_v_down.png new file mode 100644 index 0000000000000000000000000000000000000000..3962e1c02e85e1bef191d8620403e1907651b46c GIT binary patch literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^f%q2K_|bzDNAuRUd&Kb`g!u~xBpvG a-?OJ(Qx-Wtv+5?$1q`09elF{r5}E*`RY-w)BgTulrC^@2lA+4U>ixN9$DTyVDvMMJEqdKnoR_)j z+CKG6r&qCM*LJHcUJzJs;g>r@(C@s8WVo_c$NIeKD?&cm?v{bP0l+XkKIT}X+ literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/scrollbar/scroll_v_thumb.png b/src/assets/images/catalog/scrollbar/scroll_v_thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1670e61380cb3096089fcc2c17b64695d81df2 GIT binary patch literal 188 zcmV;t07L(YP)Wbs<59Gb;{kJU2~TvE_Es`@b)9 zs#vosmrcBWROI3cb+!&eGwwOX@_s?D%Z`3=F}A(v<8Si+=af3FpuBBjsq+{zOT-WF ny5h^{#`?{(?f8v~qc>O?idE$mZGZ6qox|Yi>gTe~DWM4frw~TP literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/scrollbar/scroll_v_thumb_pressed.png b/src/assets/images/catalog/scrollbar/scroll_v_thumb_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..7fbaffb38907a45f04f800fe7b5fc0c32b5d47a6 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^fiq3skF+BEjw!lQMB-@D*{wmjmPuAP1POi~> jsoXSeUU5a@iAcs9f^uKIRh%k-E@AL=^>bP0l+XkKvAahk literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/scrollbar/scroll_v_up.png b/src/assets/images/catalog/scrollbar/scroll_v_up.png new file mode 100644 index 0000000000000000000000000000000000000000..1fb60715edbf31d3e75371c2aaea1c625d28ab9e GIT binary patch literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^f4qs z&W8UM8+`s=%2*iV`$B$7btZ>E^39a<;)`1u8)llFP(Nm#G_#=L444u6RwYLh=`Kij;3WEA|2n@U3%?apYA?;<%3_=+WixCC+9eJ+*u!c cZJMWEcDPPj$jNWhfo@>%boFyt=akR{0A8;}B>(^b literal 0 HcmV?d00001 diff --git a/src/assets/images/catalog/scrollbar/scroll_v_up_pressed.png b/src/assets/images/catalog/scrollbar/scroll_v_up_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..aa2a47dc927afca54f7d034b30205377a7539fb2 GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^fVdiTA$+}(TSi(l5-^%MQo he)21I)c(zl-KQm&cPM%KA)q@LJYD@<);T3K0RRAZM)Lpw literal 0 HcmV?d00001 diff --git a/src/components/catalog/CatalogAdminContext.tsx b/src/components/catalog/CatalogAdminContext.tsx index a0d70fb..8de4180 100644 --- a/src/components/catalog/CatalogAdminContext.tsx +++ b/src/components/catalog/CatalogAdminContext.tsx @@ -1,4 +1,4 @@ -import { CatalogAdminCreateOfferComposer, CatalogAdminCreatePageComposer, CatalogAdminDeleteOfferComposer, CatalogAdminDeletePageComposer, CatalogAdminMoveOfferComposer, CatalogAdminMovePageComposer, CatalogAdminPublishComposer, CatalogAdminResultEvent, CatalogAdminSaveOfferComposer, CatalogAdminSavePageComposer } from '@nitrots/nitro-renderer'; +import { CatalogAdminCreateOfferComposer, CatalogAdminCreatePageComposer, CatalogAdminDeleteOfferComposer, CatalogAdminDeletePageComposer, CatalogAdminLoadOfferComposer, CatalogAdminLoadPageComposer, CatalogAdminMoveOfferComposer, CatalogAdminMovePageComposer, CatalogAdminOfferDetailsEvent, CatalogAdminPageDetailsEvent, CatalogAdminPublishComposer, CatalogAdminResultEvent, CatalogAdminSaveOfferComposer, CatalogAdminSavePageComposer } from '@nitrots/nitro-renderer'; import { createContext, FC, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { ICatalogNode, IPurchasableOffer, NotificationAlertType, SendMessageComposer } from '../../api'; import { useCatalogUiState, useMessageEvent, useNotification } from '../../hooks'; @@ -44,12 +44,34 @@ export interface IOfferEditData orderNumber: number; } +export interface IEditingOfferDetails +{ + offerId: number; + offerIdGroup: number; + limitedStack: number; + orderNumber: number; +} + +export interface IEditingPageDetails +{ + pageId: number; + caption: string; + captionSave: string; + minRank: number; + orderNum: number; + visible: boolean; + enabled: boolean; +} + interface ICatalogAdminContext { adminMode: boolean; setAdminMode: (value: boolean) => void; editingOffer: IPurchasableOffer | null; setEditingOffer: (offer: IPurchasableOffer | null) => void; + editingOfferDetails: IEditingOfferDetails | null; + editingPageDetails: IEditingPageDetails | null; + requestPageDetails: (pageId: number) => void; editingPageData: boolean; setEditingPageData: (value: boolean) => void; editingRootPage: boolean; @@ -80,7 +102,9 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children }) { const { currentType } = useCatalogUiState(); const [ adminMode, setAdminMode ] = useState(false); - const [ editingOffer, setEditingOffer ] = useState(null); + const [ editingOffer, setEditingOfferState ] = useState(null); + const [ editingOfferDetails, setEditingOfferDetails ] = useState(null); + const [ editingPageDetails, setEditingPageDetails ] = useState(null); const [ editingPageData, setEditingPageData ] = useState(false); const [ editingRootPage, setEditingRootPage ] = useState(false); const [ editingPageNode, setEditingPageNode ] = useState(null); @@ -90,6 +114,51 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children }) const pendingActionRef = useRef(null); const { simpleAlert = null } = useNotification(); + const setEditingOffer = useCallback((offer: IPurchasableOffer | null) => + { + setEditingOfferState(offer); + setEditingOfferDetails(null); + + if(offer && offer.offerId !== -1) + { + SendMessageComposer(new CatalogAdminLoadOfferComposer(offer.offerId, currentType)); + } + }, [ currentType ]); + + useMessageEvent(CatalogAdminOfferDetailsEvent, (event: CatalogAdminOfferDetailsEvent) => + { + const parser = event.getParser(); + + setEditingOfferDetails({ + offerId: parser.offerId, + offerIdGroup: parser.offerIdGroup, + limitedStack: parser.limitedStack, + orderNumber: parser.orderNumber + }); + }); + + useMessageEvent(CatalogAdminPageDetailsEvent, (event: CatalogAdminPageDetailsEvent) => + { + const parser = event.getParser(); + + setEditingPageDetails({ + pageId: parser.pageId, + caption: parser.caption, + captionSave: parser.captionSave, + minRank: parser.minRank, + orderNum: parser.orderNum, + visible: parser.visible, + enabled: parser.enabled + }); + }); + + const requestPageDetails = useCallback((pageId: number) => + { + setEditingPageDetails(null); + if(pageId == null || pageId < 0) return; + SendMessageComposer(new CatalogAdminLoadPageComposer(pageId, currentType)); + }, [ currentType ]); + useEffect(() => { if(!adminMode) return; @@ -288,7 +357,8 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children }) return ( = () => { - const { rootNode = null, currentPage = null, searchResult = null } = useCatalogData(); + const { rootNode = null, currentPage = null, currentOffer = null, searchResult = null } = useCatalogData(); const { isVisible = false, setIsVisible = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], setSearchResult = null, currentType = CatalogType.NORMAL } = useCatalogUiState(); const { openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null } = useCatalogActions(); const catalogAdmin = useCatalogAdmin(); @@ -34,6 +34,11 @@ const CatalogClassicViewInner: FC<{}> = () => const [ mobileMenuOpen, setMobileMenuOpen ] = useState(false); const { purse = null } = usePurse(); const displayedCurrencies = GetConfigurationValue('system.currency.types', []); + const activeCatalogNode = activeNodes?.[activeNodes.length - 1] ?? null; + // Strip SWF-style suffixes like "(BC)" or "(Hot)" but keep the + // pageId hint the gameserver appends when the viewer has + // ACC_CATALOG_IDS - that's a pure-numeric "(6)" trailer. + const getSwfTabLabel = (label: string) => (label || '').replace(/\s*\(\D[^)]*\)\s*$/g, '').trim(); const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER) ? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' } : undefined; @@ -122,7 +127,7 @@ const CatalogClassicViewInner: FC<{}> = () => return ( <> { isVisible && - + setIsVisible(false) } style={ buildersClubHeaderStyle } />

{ isMod && @@ -161,20 +166,19 @@ const CatalogClassicViewInner: FC<{}> = () =>
{ adminMode && -
- Admin Mode - -
} + } { rootNode && (rootNode.children.length > 0) && rootNode.children.map((child, index) => { if(!adminMode && !child.isVisible) return null; + if(!adminMode && (index === 0) && getSwfTabLabel(child.localization).toLowerCase().includes('rari')) return null; const isHidden = !child.isVisible; @@ -186,8 +190,9 @@ const CatalogClassicViewInner: FC<{}> = () => activateNode(child); } }>
- - { child.localization } + { (child.iconId > 0) && + } + { getSwfTabLabel(child.localization) } { adminMode && isHidden && } { adminMode &&
e.stopPropagation() }> @@ -215,6 +220,20 @@ const CatalogClassicViewInner: FC<{}> = () => } +
+
+
+ +
+
+
+ { currentType === CatalogType.BUILDER ? LocalizeText('builder.header.title') : getSwfTabLabel(activeCatalogNode?.localization ?? LocalizeText('catalog.title')) } +
+ { currentType === CatalogType.BUILDER + ?
{ LocalizeText('builder.header.status.membership') }
+ :
} +
+
{ adminMode && rootNode && @@ -252,8 +271,7 @@ const CatalogClassicViewInner: FC<{}> = () =>
- { /* info_duckets renders its own logo in the body (BcInfoView) — don't duplicate it in the hero */ } - { (currentPage?.layoutCode !== 'info_duckets') && !!currentPage?.localization?.getImage(0) && } + { !!currentPage?.localization?.getImage(0) && }
diff --git a/src/components/catalog/views/admin/CatalogAdminOfferEditView.tsx b/src/components/catalog/views/admin/CatalogAdminOfferEditView.tsx index c61c14b..0259589 100644 --- a/src/components/catalog/views/admin/CatalogAdminOfferEditView.tsx +++ b/src/components/catalog/views/admin/CatalogAdminOfferEditView.tsx @@ -10,6 +10,7 @@ export const CatalogAdminOfferEditView: FC<{}> = () => const { currentPage = null } = useCatalogData(); const catalogAdmin = useCatalogAdmin(); const editingOffer = catalogAdmin?.editingOffer ?? null; + const editingOfferDetails = catalogAdmin?.editingOfferDetails ?? null; const setEditingOffer = catalogAdmin?.setEditingOffer; const saveOffer = catalogAdmin?.saveOffer; const deleteOffer = catalogAdmin?.deleteOffer; @@ -62,12 +63,21 @@ export const CatalogAdminOfferEditView: FC<{}> = () => setClubOnly(editingOffer.clubLevel > 0 ? '1' : '0'); setExtradata(editingOffer.product?.extraParam || ''); setHaveOffer(editingOffer.haveOffer ? '1' : '0'); - setOfferIdGroup(editingOffer.offerId || -1); + setOfferIdGroup(0); setLimitedStack(0); setOrderNumber(0); } }, [ editingOffer ]); + useEffect(() => + { + if(!editingOfferDetails) return; + + setOfferIdGroup(editingOfferDetails.offerIdGroup); + setLimitedStack(editingOfferDetails.limitedStack); + setOrderNumber(editingOfferDetails.orderNumber); + }, [ editingOfferDetails ]); + if(!editingOffer) return null; const handleSave = async () => diff --git a/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx b/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx index 94b2326..1b4c201 100644 --- a/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx +++ b/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx @@ -28,6 +28,8 @@ export const CatalogAdminPageEditView: FC<{}> = () => const editingPageData = catalogAdmin?.editingPageData ?? false; const editingRootPage = catalogAdmin?.editingRootPage ?? false; const editingPageNode = catalogAdmin?.editingPageNode ?? null; + const editingPageDetails = catalogAdmin?.editingPageDetails ?? null; + const requestPageDetails = catalogAdmin?.requestPageDetails; const loading = catalogAdmin?.loading ?? false; const [ caption, setCaption ] = useState(''); @@ -67,21 +69,22 @@ export const CatalogAdminPageEditView: FC<{}> = () => { if(!editingPageData || !targetNode) return; - // The server appends " (pageId)" to the caption for mods/admins (see - // CatalogPagesListComposer). Strip that exact suffix before seeding the - // edit field, otherwise saving folds the id back into the stored - // caption and it multiplies on every edit ("Wired (1114) (1114) ..."). - const rawCaption = (targetNode.localization || '').replace(new RegExp(`\\s*\\(${ targetNode.pageId }\\)\\s*$`), ''); + // Don't read the decorated caption out of the catalog index - + // the gameserver appends " (id)" when ACC_CATALOG_IDS is on and + // we don't want that round-tripping back into the DB. Wait for + // the admin page-details event to land instead; it carries the + // raw caption / caption_save / min_rank / order_num / enabled. + setCaption(''); + setCaptionSave(''); + setMinRank(1); + setOrderNum(0); + setEnabled('1'); - setCaption(rawCaption); - setCaptionSave(targetNode.pageName || rawCaption); setCatalogMode(currentType === CatalogType.BUILDER ? 'BUILDER' : 'NORMAL'); setPageLayout(currentPage?.layoutCode || 'default_3x3'); setIconImage(targetNode.iconId ?? 0); setVisible(targetNode.isVisible ? '1' : '0'); - setEnabled('1'); - setMinRank(1); - setOrderNum(0); + const matchesLoadedPage = currentPage && targetPageId === currentPage.pageId; const existingText1 = matchesLoadedPage && currentPage.localization ? currentPage.localization.getText(0) @@ -94,7 +97,22 @@ export const CatalogAdminPageEditView: FC<{}> = () => setParentId(typeof wireParentId === 'number' && wireParentId !== -1 ? wireParentId : (targetNode.parent ? targetNode.parent.pageId : -1)); - }, [ editingPageData, targetNode, currentPage, currentType ]); + + if(targetPageId != null && targetPageId >= 0) requestPageDetails?.(targetPageId); + }, [ editingPageData, targetNode, currentPage, currentType, targetPageId, requestPageDetails ]); + + useEffect(() => + { + if(!editingPageDetails) return; + if(targetPageId != null && editingPageDetails.pageId !== targetPageId) return; + + setCaption(editingPageDetails.caption); + setCaptionSave(editingPageDetails.captionSave); + setMinRank(editingPageDetails.minRank); + setOrderNum(editingPageDetails.orderNum); + setVisible(editingPageDetails.visible ? '1' : '0'); + setEnabled(editingPageDetails.enabled ? '1' : '0'); + }, [ editingPageDetails, targetPageId ]); if(!editingPageData || !targetNode) return null; @@ -168,7 +186,7 @@ export const CatalogAdminPageEditView: FC<{}> = () => const handleDelete = async () => { if(!catalogAdmin?.deletePage || isRoot) return; - if(!confirm(LocalizeText('catalog.admin.delete.page.confirm', [ 'name' ], [ targetNode.localization ]))) return; + if(!confirm(LocalizeText('catalog.admin.delete.page.confirm', [ 'name' ], [ editingPageDetails?.caption ?? '' ]))) return; catalogAdmin.deletePage(targetPageId); @@ -179,7 +197,7 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
- { isRoot ? LocalizeText('catalog.admin.edit.root') : `${ LocalizeText('catalog.admin.edit') } ${ targetNode.localization }` } + { isRoot ? LocalizeText('catalog.admin.edit.root') : `${ LocalizeText('catalog.admin.edit') } ${ editingPageDetails?.caption ?? '' }` }
diff --git a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx index 973179c..18e68af 100644 --- a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx +++ b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx @@ -23,6 +23,10 @@ export const CatalogNavigationItemView: FC = pro const isFav = node ? isFavoritePage(node.pageId) : false; const [ isDragOver, setIsDragOver ] = useState(false); const dragRef = useRef(null); + // Strip SWF-style suffixes like "(BC)" or "(Hot)" but keep the + // pageId hint the gameserver appends when the viewer has + // ACC_CATALOG_IDS - that's a pure-numeric "(6)" trailer. + const swfLabel = (node?.localization || '').replace(/\s*\(\D[^)]*\)\s*$/g, '').trim(); const handleDragStart = useCallback((e: React.DragEvent) => { @@ -90,7 +94,7 @@ export const CatalogNavigationItemView: FC = pro
- { node.localization } + { swfLabel } { adminMode &&
= props => return null; } - return offer.product?.getIconUrl(offer) ?? null; + const product = offer.product; + + if(!product) return null; + + if((product.productType === ProductTypeEnum.FLOOR) || (product.productType === ProductTypeEnum.WALL)) + { + const className = product.furnitureData?.className; + + if(className?.length) + { + const param = (product.productType === ProductTypeEnum.WALL && product.extraParam?.length) ? `_${ product.extraParam }` : ''; + const configuredIconUrl = GetConfigurationValue('furni.asset.icon.url', ''); + + if(configuredIconUrl?.length) + { + return configuredIconUrl + .replace('%libname%', className) + .replace('%param%', param); + } + } + } + + return product.getIconUrl(offer) ?? null; }, [ offer ]); + const prices = useMemo(() => + { + if(!offer) return []; + + const values: { amount: number; type: number }[] = []; + + if(offer.priceInCredits > 0) values.push({ amount: offer.priceInCredits, type: -1 }); + if(offer.priceInActivityPoints > 0) values.push({ amount: offer.priceInActivityPoints, type: offer.activityPointType }); + + return values; + }, [ offer ]); + + const getCurrencyIconUrl = (type: number) => + { + const configuredCurrencyUrl = GetConfigurationValue('currency.asset.icon.url', ''); + + return configuredCurrencyUrl.replace('%type%', type.toString()); + }; + const onMouseEvent = (event: MouseEvent) => { switch(event.type) @@ -74,9 +115,30 @@ export const CatalogGridOfferView: FC = props => { ...rest } > { iconUrl && !(offer.product.productType === ProductTypeEnum.ROBOT) && -
} + + { + const fallbackIconUrl = product.getIconUrl(offer); + + if(fallbackIconUrl && (event.currentTarget.src !== fallbackIconUrl)) event.currentTarget.src = fallbackIconUrl; + } } /> } { (offer.product.productType === ProductTypeEnum.ROBOT) && } + { (prices.length > 0) && + 1 ? 'is-multi-price' : 'is-single-price' }` }> + { prices.map((price, index) => + + { index > 0 && + } + { price.amount } + + ) } + }
diff --git a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx index 09411d1..d8517f5 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { FaEdit, FaPlus, FaPowerOff, FaSyncAlt } from 'react-icons/fa'; +import { FaEdit, FaExchangeAlt, FaPlus, FaSyncAlt } from 'react-icons/fa'; import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SanitizeHtml } from '../../../../../api'; import { Text } from '../../../../../common'; import { useCatalogData } from '../../../../../hooks'; @@ -20,6 +20,7 @@ export const CatalogLayoutDefaultView: FC = props => const { currentOffer = null, currentPage = null, roomPreviewer = null } = useCatalogData(); const catalogAdmin = useCatalogAdmin(); const adminMode = catalogAdmin?.adminMode ?? false; + const offerName = currentOffer?.localizationName?.replace(/\s*\([^)]*\)\s*$/g, ''); return (
@@ -40,63 +41,87 @@ export const CatalogLayoutDefaultView: FC = props => > { LocalizeText('catalog.admin.offer.new') } + { currentOffer && + }
} - { currentOffer && -
-
- { (currentOffer.product.productType !== ProductTypeEnum.BADGE) && - <> - - - - - } - { (currentOffer.product.productType === ProductTypeEnum.BADGE) && - } -
-
-
-
- { currentOffer.localizationName } +
+ { currentOffer && +
+
+ { offerName } + { (currentOffer.product.productType !== ProductTypeEnum.BADGE) && + <> + + + + + } + { (currentOffer.product.productType === ProductTypeEnum.BADGE) && + } +
+
+
+
+ { offerName } + { adminMode && + catalogAdmin.setEditingOffer(currentOffer) } + /> } +
{ adminMode && - catalogAdmin.setEditingOffer(currentOffer) } - /> } +
+ ID: { currentOffer.product.productClassId } + Offer: { currentOffer.offerId } + { currentOffer.product.productType.toUpperCase() } +
} +
- { adminMode && -
- ID: { currentOffer.product.productClassId } - Offer: { currentOffer.offerId } - { currentOffer.product.productType.toUpperCase() } -
} -
- - -
- -
-
-
} +
} - { !currentOffer && -
- { !!page.localization.getImage(1) && - } - -
} + { !currentOffer && +
+ { !!page.localization.getImage(1) && + } + +
} +
{ GetConfigurationValue('catalog.headers') && } - +
+ + { currentOffer && +
+
+ +
+
+ +
+
} + + { currentOffer && +
+
+ +
+
}
); }; diff --git a/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx index 6d28e1e..145620d 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx @@ -17,14 +17,14 @@ export const CatalogLayouGuildCustomFurniView: FC = props => return ( - - + + - + { !currentOffer && <> - { !!page.localization.getImage(1) && } - + { !!page.localization.getImage(1) && } + } { currentOffer && <> @@ -33,7 +33,7 @@ export const CatalogLayouGuildCustomFurniView: FC = props =>
- { currentOffer.localizationName } + { currentOffer.localizationName }
diff --git a/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx index ade3125..5432bb2 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx @@ -20,6 +20,7 @@ export const CatalogLayoutRoomAdsView: FC = props => const { categories } = useNavigatorData(); const { setIsVisible = null } = useCatalogUiState(); const { promoteInformation, isExtended, setIsExtended } = useRoomPromote(); + const promoteData = promoteInformation?.data ?? null; const { data: availableRooms = [] } = useNitroQuery({ key: [ 'nitro', 'catalog', 'room-ad-purchase-info' ], @@ -31,17 +32,17 @@ export const CatalogLayoutRoomAdsView: FC = props => useEffect(() => { - if(isExtended) + if(isExtended && promoteData) { - setRoomId(promoteInformation.data.flatId); - setEventName(promoteInformation.data.eventName); - setEventDesc(promoteInformation.data.eventDescription); - setCategoryId(promoteInformation.data.categoryId); + setRoomId(promoteData.flatId); + setEventName(promoteData.eventName); + setEventDesc(promoteData.eventDescription); + setCategoryId(promoteData.categoryId); setExtended(isExtended); // This is for sending to packet setIsExtended(false); // This is from hook useRoomPromotte } - }, [ isExtended, eventName, eventDesc, categoryId, promoteInformation.data, setIsExtended ]); + }, [ isExtended, promoteData, setIsExtended ]); const resetData = () => { diff --git a/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx index 7b8f1ec..a3cab10 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx @@ -28,7 +28,7 @@ export const CatalogLayoutSingleBundleView: FC = props => { page.localization.getText(1) } } { !!page.localization.getImage(1) && - } + } diff --git a/src/components/catalog/views/page/layout/GetCatalogLayout.tsx b/src/components/catalog/views/page/layout/GetCatalogLayout.tsx index ce68f37..6ccf2a1 100644 --- a/src/components/catalog/views/page/layout/GetCatalogLayout.tsx +++ b/src/components/catalog/views/page/layout/GetCatalogLayout.tsx @@ -1,7 +1,6 @@ import { ICatalogPage } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; import { CatalogLayoutBadgeDisplayView } from './CatalogLayoutBadgeDisplayView'; -import { CatalogLayoutBcInfoView } from './CatalogLayoutBcInfoView'; import { CatalogLayoutBuildersClubBuyView } from './CatalogLayoutBuildersClubBuyView'; import { CatalogLayoutColorGroupingView } from './CatalogLayoutColorGroupingView'; import { CatalogLayoutCustomPrefixView } from './CatalogLayoutCustomPrefixView'; @@ -35,8 +34,6 @@ export const GetCatalogLayout = (page: ICatalogPage, hideNavigation: () => void) { case 'frontpage_featured': return null; - case 'info_duckets': - return ; case 'frontpage4': return ; case 'pets': diff --git a/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx b/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx index 7081428..96dff71 100644 --- a/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx +++ b/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx @@ -206,7 +206,7 @@ export const CatalogLayoutPetView: FC = props =>
} { /* Top card: preview + name + purchase */ } -
+
{ /* Pet preview */ }
@@ -240,12 +240,12 @@ export const CatalogLayoutPetView: FC = props => Offer: { currentOffer.offerId }
} { !!page.localization.getText(0) && -

} +

}

{ /* Name input */ }
- +
0 ? 'border-danger bg-danger/5' : approvalResult === 0 ? 'border-success bg-success/5' : 'border-card-grid-item-border focus:border-primary bg-white' }` } @@ -267,7 +267,7 @@ export const CatalogLayoutPetView: FC = props =>
diff --git a/src/components/catalog/views/page/widgets/CatalogPriceDisplayWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogPriceDisplayWidgetView.tsx index a83a981..80bcd00 100644 --- a/src/components/catalog/views/page/widgets/CatalogPriceDisplayWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogPriceDisplayWidgetView.tsx @@ -19,17 +19,17 @@ export const CatalogPriceDisplayWidgetView: FC +
{ (offer.priceInCredits > 0) && -
- { (offer.priceInCredits * quantity) } +
+ { (offer.priceInCredits * quantity) }
} { separator && (offer.priceInCredits > 0) && (offer.priceInActivityPoints > 0) && - } + } { (offer.priceInActivityPoints > 0) && -
- { (offer.priceInActivityPoints * quantity) } +
+ { (offer.priceInActivityPoints * quantity) }
}
diff --git a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx index e04cac0..44feade 100644 --- a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx @@ -171,6 +171,8 @@ export const CatalogPurchaseWidgetView: FC = pro const PurchaseButton = () => { + const swfButtonClassNames = [ 'nitro-catalog-swf-button' ]; + if(isBuildersClubPlaceable) { const hasMissingExtraParam = (purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length)); @@ -198,10 +200,10 @@ export const CatalogPurchaseWidgetView: FC = pro return (
- -
@@ -220,37 +222,37 @@ export const CatalogPurchaseWidgetView: FC = pro const priceCredits = (currentOffer.priceInCredits * purchaseOptions.quantity); const pricePoints = (currentOffer.priceInActivityPoints * purchaseOptions.quantity); - if(GetClubMemberLevel() < currentOffer.clubLevel) return ; + if(GetClubMemberLevel() < currentOffer.clubLevel) return ; - if(isLimitedSoldOut) return ; + if(isLimitedSoldOut) return ; - if(priceCredits > getCurrencyAmount(-1)) return ; + if(priceCredits > getCurrencyAmount(-1)) return ; - if(pricePoints > getCurrencyAmount(currentOffer.activityPointType)) return ; + if(pricePoints > getCurrencyAmount(currentOffer.activityPointType)) return ; switch(purchaseState) { case CatalogPurchaseState.CONFIRM: - return ; + return ; case CatalogPurchaseState.PURCHASE: - return ; + return ; case CatalogPurchaseState.FAILED: - return ; + return ; case CatalogPurchaseState.SOLD_OUT: - return ; + return ; case CatalogPurchaseState.NONE: default: - return ; + return ; } }; return ( <> - { (!isBuildersClubOffer && !noGiftOption && !currentOffer.isRentOffer) && - } + ); }; diff --git a/src/components/catalog/views/page/widgets/CatalogSpinnerWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogSpinnerWidgetView.tsx index 573465e..6ba296b 100644 --- a/src/components/catalog/views/page/widgets/CatalogSpinnerWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogSpinnerWidgetView.tsx @@ -34,26 +34,26 @@ export const CatalogSpinnerWidgetView: FC<{}> = props => if(!currentOffer || !currentOffer.bundlePurchaseAllowed) return null; return ( -
- { LocalizeText('catalog.bundlewidget.spinner.select.amount') } -
+
+ { LocalizeText('catalog.bundlewidget.quantity') } +
updateQuantity(event.target.valueAsNumber) } />
diff --git a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx index f63f721..8beb08e 100644 --- a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx @@ -119,5 +119,5 @@ export const CatalogViewProductWidgetView: FC<{}> = props => ); } - return ; + return ; }; diff --git a/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx index 5eae3d2..f3458c5 100644 --- a/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx @@ -7,11 +7,25 @@ interface ChatInputCommandSelectorViewProps selectedIndex: number; onSelect: (command: CommandDefinition) => void; onHover: (index: number) => void; + /** + * When true, render the flat minimalist look (gray list, dark-blue + * selection). When false / undefined (default) the picker wears the + * Habbo NitroCard chrome with the green :command header strip. + */ + newStyle?: boolean; } +/** + * :command autocomplete popover. Two visual modes, both driven by the + * "New style" toggle in user settings (memenu.settings.other.catalog.classic.style): + * + * - newStyle = false (default): cream cardstock, habbo-green header, + * UbuntuCondensed names, green ":" tile, custom Habbo scrollbar. + * - newStyle = true: flat gray list, dark-blue selection, plain text rows. + */ export const ChatInputCommandSelectorView: FC = props => { - const { commands = [], selectedIndex = 0, onSelect = null, onHover = null } = props; + const { commands = [], selectedIndex = 0, onSelect = null, onHover = null, newStyle = false } = props; const listRef = useRef(null); useEffect(() => @@ -23,19 +37,57 @@ export const ChatInputCommandSelectorView: FC if(selected) selected.scrollIntoView({ block: 'nearest' }); }, [ selectedIndex ]); + if(newStyle) + { + return ( +
+ { commands.map((cmd, index) => ( +
onSelect(cmd) } + onMouseEnter={ () => onHover(index) } + > + :{ cmd.key } + { cmd.description } +
+ )) } +
+ ); + } + return ( -
- { commands.map((cmd, index) => ( -
onSelect(cmd) } - onMouseEnter={ () => onHover(index) } - > - :{ cmd.key } - { cmd.description } -
- )) } +
+
+ + : Command +
+
+ { commands.map((cmd, index) => + { + const isSelected = (index === selectedIndex); + const rowClass = [ + 'chat-input-command-row', + isSelected ? 'is-selected' : '' + ].filter(Boolean).join(' '); + + return ( +
onSelect(cmd) } + onMouseEnter={ () => onHover(index) } + > +
:
+
+ :{ cmd.key } + { cmd.description && + { cmd.description } } +
+
+ ); + }) } +
); }; diff --git a/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx index 370786f..0c860ad 100644 --- a/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx @@ -19,11 +19,28 @@ interface ChatInputMentionSelectorViewProps selectedIndex: number; onSelect: (suggestion: MentionSuggestion) => void; onHover: (index: number) => void; + /** + * When true, render the flat minimalist look (gray list, dark-blue + * selection, no header / no kind chip). When false / undefined (default) + * the picker wears the Habbo NitroCard chrome. + */ + newStyle?: boolean; } +/** + * @-mention autocomplete popover. Two visual modes, both driven by the + * "New style" toggle in user settings (memenu.settings.other.catalog.classic.style): + * + * - newStyle = false (default): cream cardstock, habbo-blue header, + * UbuntuCondensed names, kind chips, custom Habbo scrollbar. + * - newStyle = true: flat gray list, dark-blue selection, plain text rows. + * + * Both modes share the same suggestion structure and keyboard contract - + * the difference is purely cosmetic. + */ export const ChatInputMentionSelectorView: FC = props => { - const { suggestions = [], selectedIndex = 0, onSelect = null, onHover = null } = props; + const { suggestions = [], selectedIndex = 0, onSelect = null, onHover = null, newStyle = false } = props; const listRef = useRef(null); useEffect(() => @@ -37,39 +54,92 @@ export const ChatInputMentionSelectorView: FC if(suggestions.length === 0) return null; - return ( -
- { suggestions.map((suggestion, index) => - { - const isSelected = (index === selectedIndex); - const rowClass = `px-3 py-1.5 cursor-pointer text-sm flex items-center gap-2 ${ isSelected ? 'bg-[#283F5D] text-white' : 'hover:bg-gray-300' }`; + if(newStyle) + { + return ( +
+ { suggestions.map((suggestion, index) => + { + const isSelected = (index === selectedIndex); + const rowClass = `px-3 py-1.5 cursor-pointer text-sm flex items-center gap-2 ${ isSelected ? 'bg-[#283F5D] text-white' : 'hover:bg-gray-300' }`; - return ( -
onSelect(suggestion) } - onMouseEnter={ () => onHover(index) } - > - { suggestion.kind === 'user' && suggestion.figure - ? ( -
- -
- ) - : ( -
@
- ) } - @{ suggestion.name } - { suggestion.description && { suggestion.description } } -
- ); - }) } + return ( +
onSelect(suggestion) } + onMouseEnter={ () => onHover(index) } + > + { suggestion.kind === 'user' && suggestion.figure + ? ( +
+ +
+ ) + : ( +
@
+ ) } + @{ suggestion.name } + { suggestion.description && { suggestion.description } } +
+ ); + }) } +
+ ); + } + + return ( +
+
+ + @ Mention +
+
+ { suggestions.map((suggestion, index) => + { + const isSelected = (index === selectedIndex); + const rowClass = [ + 'chat-input-mention-row', + isSelected ? 'is-selected' : '' + ].filter(Boolean).join(' '); + + return ( +
onSelect(suggestion) } + onMouseEnter={ () => onHover(index) } + > + { suggestion.kind === 'user' && suggestion.figure + ? ( +
+ +
+ ) + : ( +
@
+ ) } +
+ @{ suggestion.name } + { suggestion.description && + { suggestion.description } } +
+ + { suggestion.kind === 'alias' ? 'Broadcast' : 'User' } + +
+ ); + }) } +
); }; diff --git a/src/components/room/widgets/chat-input/ChatInputView.tsx b/src/components/room/widgets/chat-input/ChatInputView.tsx index 85d3198..551795a 100644 --- a/src/components/room/widgets/chat-input/ChatInputView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputView.tsx @@ -3,7 +3,7 @@ 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 { useChatCommandSelector, useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks'; +import { useCatalogClassicStyle, useChatCommandSelector, useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks'; import { useRoomUserListSnapshot } from '../../../../hooks/session/useSessionSnapshots'; import { ChatInputCommandSelectorView } from './ChatInputCommandSelectorView'; import { ChatInputEmojiSelectorView } from './ChatInputEmojiSelectorView'; @@ -58,6 +58,11 @@ export const ChatInputView: FC<{}> = props => const roomUserList = useRoomUserListSnapshot(); const [ mentionSelectedIndex, setMentionSelectedIndex ] = useState(0); + // The "New style" user-setting (memenu.settings.other.catalog.classic.style) + // drives BOTH the catalog layout and the mention-picker chrome: + // false (default) = Habbo old-school NitroCard cardstock look + // true = flat minimalist gray look + const [ newStyle ] = useCatalogClassicStyle(); const mentionContext = useMemo(() => { @@ -485,6 +490,7 @@ export const ChatInputView: FC<{}> = props => setChatValue(':' + cmd.key + ' '); inputRef.current?.focus(); } } onHover={ setSelectedIndex } + newStyle={ newStyle } /> } { mentionSelectorVisible && !commandSelectorVisible && = props => selectedIndex={ mentionSelectedIndex } onSelect={ applyMentionSuggestion } onHover={ setMentionSelectedIndex } + newStyle={ newStyle } /> }
{ !floodBlocked && diff --git a/src/css/backgrounds/BackgroundsView.css b/src/css/backgrounds/BackgroundsView.css index d08b2b1..a5e1fa8 100644 --- a/src/css/backgrounds/BackgroundsView.css +++ b/src/css/backgrounds/BackgroundsView.css @@ -1629,6 +1629,9 @@ .infostand-border.border-15 { border-color: #cbd5e1; } /* Silver */ .infostand-border.border-16 { border-color: #1f2937; } /* Black */ +/* Image-based borders (17-25). These override the colour-border insets and + strip the CSS border so the artwork sits ~22px outside the card and + stretches to fill the frame area. */ .infostand-border.border-17, .infostand-border.border-18, .infostand-border.border-19, @@ -1659,6 +1662,8 @@ .infostand-border.border-24 { background-image: url('@/assets/images/backgrounds/borders/border_24.webp'); } .infostand-border.border-25 { background-image: url('@/assets/images/backgrounds/borders/border_25.webp'); } +/* Picker thumbnails inside the BackgroundsView "Borders" tab. + Each thumbnail is a small rounded box outlined in its border colour. */ .profile-border { width: 60px; height: 76px; @@ -1669,7 +1674,9 @@ background: rgba(255, 255, 255, 0.05); } +/* border-0 = no border (default) — show as a dashed translucent outline */ .profile-border.border-0 { border: 2px dashed rgba(255, 255, 255, 0.25); } + .profile-border.border-1 { border-color: #ef4444; } .profile-border.border-2 { border-color: #f97316; } .profile-border.border-3 { border-color: #eab308; } @@ -1686,6 +1693,8 @@ .profile-border.border-14 { border-color: #d4a020; } .profile-border.border-15 { border-color: #cbd5e1; } .profile-border.border-16 { border-color: #1f2937; } + +/* Image-border picker thumbnails — drop the CSS frame and show the artwork. */ .profile-border.border-17, .profile-border.border-18, .profile-border.border-19, @@ -1710,61 +1719,4 @@ .profile-border.border-22 { background-image: url('@/assets/images/backgrounds/borders/border_22.webp'); } .profile-border.border-23 { background-image: url('@/assets/images/backgrounds/borders/border_23.webp'); } .profile-border.border-24 { background-image: url('@/assets/images/backgrounds/borders/border_24.webp'); } -.profile-border.border-25 { background-image: url('@/assets/images/backgrounds/borders/border_25.webp'); } - -.card-background-2, -.card-background-3, -.card-background-4, -.card-background-5, -.card-background-6, -.card-background-7, -.card-background-12, -.card-background-13, -.card-background-28, -.card-background-30, -.card-background-35, -.card-background-52, -.card-background-55, -.card-background-56, -.card-background-58, -.card-background-60, -.card-background-95, -.card-background-116, -.card-background-122, -.card-background-127, -.card-background-129, -.card-background-131, -.card-background-144, -.card-background-149, -.card-background-150, -.card-background-161, -.card-background-185, -.card-background-187 { - --profile-card-text: #1a1a1a; - --profile-card-shadow: 0 1px 1px rgba(255, 255, 255, 0.65); -} - -[class*="card-background-"] .nitro-extended-profile__username .username, -[class*="card-background-"] .nitro-extended-profile__motto, -[class*="card-background-"] .nitro-extended-profile__meta, -[class*="card-background-"] .nitro-extended-profile__meta b, -[class*="card-background-"] .nitro-extended-profile__meta span, -[class*="card-background-"] .nitro-extended-profile__status-text, -[class*="card-background-"] .nitro-extended-profile__relationships-label, -[class*="card-background-"] .nitro-extended-profile__relationship-subcopy, -[class*="card-background-"] .nitro-extended-profile__link, -[class*="card-background-"] .nitro-extended-profile__right > p, -[class*="card-background-"] .nitro-extended-profile__right > p b { - color: var(--profile-card-text, #fff) !important; - text-shadow: var(--profile-card-shadow, 0 1px 2px rgba(0, 0, 0, 0.55)); -} - -.profile-card-background[class*="card-background-"]:not(.nitro-extended-profile-window__content) { - color: var(--profile-card-text, #fff); - text-shadow: var(--profile-card-shadow, 0 1px 2px rgba(0, 0, 0, 0.55)); -} - -.profile-card-background[class*="card-background-"]:not(.nitro-extended-profile-window__content) .text-white { - color: var(--profile-card-text, #fff) !important; -} - +.profile-border.border-25 { background-image: url('@/assets/images/backgrounds/borders/border_25.webp'); } \ No newline at end of file diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index 0a146e7..6a1f23e 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -1,110 +1,532 @@ .nitro-catalog-classic-window { - --cat-blue: #4a7d8c; - --cat-blue-dark: #315863; - --cat-ink: #2c2c2a; - --cat-strip: #e2e0d6; - --cat-tab: #c7c5ba; - --cat-tab-border: #8f8f8b; - --cat-panel: #e8e7df; - --cat-sub: #dedcd2; - --cat-line: #8899a2; - --cat-canvas: #d8d8d2; - --cat-canvas-2: #ccccc4; - --cat-select: #4a7d8c; - --cat-select-bg: #f1efe6; - --cat-gold: #f7d673; - --cat-gold-border: #d4af37; - --cat-gold-ink: #4a3300; - --cat-buy: #5ca843; + --catalog-swf-bg: #ecece4; + --catalog-swf-panel: #f7f7f2; + --catalog-swf-panel-2: #e7e7df; + --catalog-swf-border: #9d9d96; + --catalog-swf-border-dark: #6f6f6a; + --catalog-swf-text: #222222; + --catalog-swf-muted: #666666; + --catalog-swf-blue: #2f8097; + --catalog-swf-blue-dark: #1c596c; + --catalog-swf-select: #63c5e9; + --catalog-swf-select-outer: #82d1ed; + --catalog-swf-bc: #ff8d00; + --catalog-swf-bc-outer: #ffb53c; + --habbo-skin-ubuntu: url("../../assets/images/catalog/swf/skins/habbo_skin_ubuntu.png"); + --habbo-skin-blue: url("../../assets/images/catalog/swf/skins/habbo_skin_blue.png"); + --habbo-skin-illumina-light: url("../../assets/images/catalog/swf/skins/habbo_skin_illumina_light.png"); + --habbo-skin-illumina-dark: url("../../assets/images/catalog/swf/skins/habbo_skin_illumina_dark.png"); + --habbo-slice-frame: url("../../assets/images/catalog/swf/ubuntu_frame3_26x55.png"); + --habbo-slice-tab-default: url("../../assets/images/catalog/swf/ubuntu_tab3_default_22x32.png"); + --habbo-slice-tab-selected: url("../../assets/images/catalog/swf/ubuntu_tab3_selected_22x32.png"); + --habbo-slice-tab-hover: url("../../assets/images/catalog/swf/ubuntu_tab3_hover_22x32.png"); + /* Light gray secondary button - cropped from catalog_skin1.png + at (10, 190, 25x22). Drives the gift button "Cadeau", the + preview-room control button and the generic .nitro-catalog-swf- + button via border-image-slice 3 3 3 3 fill. */ + --habbo-slice-button-default: url("../../assets/images/catalog/buttons/btn_secondary.png"); + --habbo-slice-button-hover: url("../../assets/images/catalog/buttons/btn_secondary_hover.png"); + --habbo-slice-button-pressed: url("../../assets/images/catalog/buttons/btn_secondary_pressed.png"); + --habbo-slice-button-disabled: url("../../assets/images/catalog/buttons/btn_secondary_disabled.png"); + /* Classic Habbo "Osta!" Buy button - cropped from catalog_skin3.png + yellow band. The historical name says "green" but the user's + skin sheet ships yellow for the action colour, so that's what + we paint. The 27x34 sprite border-image-slices nicely at 6px + since the rounded corner is ~5px. */ + --habbo-slice-button-buy: url("../../assets/images/catalog/buttons/buy.png"); + --habbo-slice-button-large: url("../../assets/images/catalog/buttons/buy.png"); + --habbo-slice-button-large-hover: url("../../assets/images/catalog/buttons/buy_hover.png"); + --habbo-slice-button-large-pressed: url("../../assets/images/catalog/buttons/buy_pressed.png"); + --habbo-slice-button-large-disabled: url("../../assets/images/catalog/buttons/buy_disabled.png"); + --habbo-button-green: url("../../assets/images/catalog/buttons/buy.png"); + --habbo-button-green-hover: url("../../assets/images/catalog/buttons/buy_hover.png"); + --habbo-button-green-pressed: url("../../assets/images/catalog/buttons/buy_pressed.png"); + --habbo-button-green-disabled: url("../../assets/images/catalog/buttons/buy_disabled.png"); + --habbo-grid-default: url("../../assets/images/catalog/swf/habbo_grid.png"); + --habbo-grid-hover: url("../../assets/images/catalog/swf/habbo_grid_hover.png"); + --habbo-grid-selected: url("../../assets/images/catalog/swf/habbo_grid_selected.png"); + --habbo-grid-selected-inactive: url("../../assets/images/catalog/swf/habbo_grid_selected_inactive.png"); + --habbo-close: url("../../assets/images/catalog/buttons/close.png"); + --habbo-close-hover: url("../../assets/images/catalog/buttons/close_hover.png"); + --habbo-close-pressed: url("../../assets/images/catalog/buttons/close_pressed.png"); + --habbo-help: url("../../assets/images/catalog/buttons/help.png"); + --habbo-help-hover: url("../../assets/images/catalog/buttons/help_hover.png"); + --habbo-help-pressed: url("../../assets/images/catalog/buttons/help_pressed.png"); + --habbo-stepper-plus: url("../../assets/images/catalog/buttons/plus.png"); + --habbo-stepper-plus-hover: url("../../assets/images/catalog/buttons/plus_hover.png"); + --habbo-stepper-plus-pressed: url("../../assets/images/catalog/buttons/plus_pressed.png"); + --habbo-stepper-plus-disabled: url("../../assets/images/catalog/buttons/plus_disabled.png"); + --habbo-stepper-minus: url("../../assets/images/catalog/buttons/minus.png"); + --habbo-stepper-minus-hover: url("../../assets/images/catalog/buttons/minus_hover.png"); + --habbo-stepper-minus-pressed: url("../../assets/images/catalog/buttons/minus_pressed.png"); + --habbo-stepper-minus-disabled: url("../../assets/images/catalog/buttons/minus_disabled.png"); + /* Scrollbar sprites cropped from catalog_skin1.png. The single-piece + thumb has caps + grip baked into one 17x34 image - stretch it + full-height with background-size: 17px 100%. */ + --habbo-scrollbar-up: url("../../assets/images/catalog/scrollbar/scroll_v_up.png"); + --habbo-scrollbar-up-pressed: url("../../assets/images/catalog/scrollbar/scroll_v_up_pressed.png"); + --habbo-scrollbar-down: url("../../assets/images/catalog/scrollbar/scroll_v_down.png"); + --habbo-scrollbar-down-pressed: url("../../assets/images/catalog/scrollbar/scroll_v_down_pressed.png"); + --habbo-scrollbar-thumb-v: url("../../assets/images/catalog/scrollbar/scroll_v_thumb.png"); + --habbo-scrollbar-thumb-v-pressed: url("../../assets/images/catalog/scrollbar/scroll_v_thumb_pressed.png"); + --habbo-scrollbar-h-left: url("../../assets/images/catalog/scrollbar/scroll_h_left.png"); + --habbo-scrollbar-h-left-pressed: url("../../assets/images/catalog/scrollbar/scroll_h_left_pressed.png"); + --habbo-scrollbar-h-right: url("../../assets/images/catalog/scrollbar/scroll_h_right.png"); + --habbo-scrollbar-h-right-pressed: url("../../assets/images/catalog/scrollbar/scroll_h_right_pressed.png"); + --habbo-scrollbar-thumb-h: url("../../assets/images/catalog/scrollbar/scroll_h_thumb.png"); + --habbo-scrollbar-thumb-h-pressed: url("../../assets/images/catalog/scrollbar/scroll_h_thumb_pressed.png"); - width: 640px !important; - height: 600px !important; - max-width: 640px !important; - min-width: 640px !important; - min-height: 600px !important; - max-height: 600px !important; - background: var(--cat-strip) !important; - border-radius: 10px !important; + width: 660px !important; + height: 720px !important; + min-width: 660px !important; + max-width: 660px !important; + min-height: 640px !important; + max-height: calc(100vh - 24px) !important; + position: relative; + color: var(--catalog-swf-text); + background: var(--catalog-swf-bg) !important; + border: 1px solid #000 !important; + border-radius: 7px 7px 0 0 !important; + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.35); overflow: hidden; + box-sizing: border-box; } -.nitro-catalog-classic-window .nitro-card-title { - font-size: 18px; - letter-spacing: 0.2px; +.draggable-window:has(> .nitro-catalog-classic-window) { + width: 660px !important; + height: 720px !important; + min-width: 660px !important; + max-width: 660px !important; } -.nitro-catalog-classic-window .nitro-card-header-shell { - min-height: 38px; - max-height: 38px; +.nitro-catalog-classic-window::before { + display: none; +} + +.nitro-catalog-classic-window > * { + position: relative; + z-index: 4; +} + +.nitro-catalog-classic-window, +.nitro-catalog-classic-window * { + image-rendering: pixelated; } .nitro-catalog-classic-window .nitro-card-header { - background: var(--cat-blue); - border-color: var(--cat-blue); - border-bottom-color: var(--cat-blue); - box-shadow: inset 0 2px 0 #709da9, inset 0 -2px 0 var(--cat-blue-dark); + height: 35px; + min-height: 35px; + background: var(--catalog-swf-blue) !important; + border: 0 !important; + border-bottom: 1px solid #000 !important; } -.nitro-catalog-classic-admin-banner { - border-bottom: 1px solid rgba(0, 0, 0, 0.18); - background: linear-gradient(180deg, #f4d45d 0%, #d8b43e 100%); +.nitro-catalog-classic-window .nitro-card-header-shell { + min-height: 35px; + max-height: 35px; +} + +.nitro-catalog-classic-window .nitro-card-title { + color: #ffffff; + font-size: 16px; + font-weight: 700; + line-height: 35px; + text-align: center; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.8); +} + +.nitro-catalog-classic-window .nitro-card-close-button { + top: 7px !important; + right: 7px !important; + width: 19px !important; + min-width: 19px !important; + height: 20px !important; + min-height: 20px !important; + padding: 0 !important; + background-image: url("../../assets/images/catalog/buttons/close.png") !important; + background-color: transparent !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: 19px 20px !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + image-rendering: pixelated !important; + opacity: 1 !important; + visibility: visible !important; + display: block !important; +} + +.nitro-catalog-classic-window .nitro-card-close-button:hover { + background-image: url("../../assets/images/catalog/buttons/close_hover.png") !important; + filter: none; +} + +.nitro-catalog-classic-window .nitro-card-close-button:active { + background-image: url("../../assets/images/catalog/buttons/close_pressed.png") !important; +} + +.nitro-catalog-classic-mobile-header, +.nitro-catalog-classic-admin-banner, +.nitro-catalog-classic-layout-header-shell, +.nitro-catalog-classic-layout-hero, +.nitro-catalog-classic-window .nitro-catalog-header, +.nitro-catalog-classic-window .builders-club-status-shell { + display: none !important; +} + +/* Publish button: lives inside the catalog window, absolutely + positioned in the header just to the left of the close X. Renders + only when adminMode is true (see CatalogClassicView.tsx). Uses the + Habbo yellow buy-button skin so it matches the Koop button. */ +.nitro-catalog-classic-window .nitro-catalog-classic-header-publish { + position: absolute !important; + top: 5px !important; + right: 32px !important; + width: auto !important; + min-width: 0 !important; + height: 22px !important; + padding: 0 10px !important; + font-size: 10px !important; + font-weight: 700 !important; + letter-spacing: 0.5px !important; + line-height: 22px !important; + z-index: 10 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-header-publish.has-pending { + animation: nitroPublishPulse 1.4s ease-in-out infinite; +} + +@keyframes nitroPublishPulse { + 0%, 100% { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65), inset 0 -2px 0 rgba(140, 75, 0, 0.35), 0 0 0 rgba(255, 200, 0, 0); } + 50% { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65), inset 0 -2px 0 rgba(140, 75, 0, 0.35), 0 0 8px rgba(255, 200, 0, 0.75); } +} + +/* Catalog default-layout admin row (Pagina bewerken / Nieuwe + aanbieding / Aanbieding bewerken). These are inline text buttons + but the .habbo-swf-window button + .habbo-swf-window + button[class*="success"] global rules were dressing them up as + SWF skin buttons (one yellow!) and forcing min-height: 22px which + broke the layout. Reset and re-skin as compact pill chips. */ +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin { + /* Keep all admin buttons on one row so the product-view doesn't + get pushed down into the absolutely-positioned grid-shell. If + a future label makes them overflow, the row scrolls + horizontally instead of wrapping. */ + flex-wrap: nowrap !important; + align-items: center !important; + gap: 6px !important; + margin-bottom: 4px !important; + overflow-x: auto !important; + overflow-y: visible !important; + scrollbar-width: thin; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin::-webkit-scrollbar { + height: 4px !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button { + display: inline-flex !important; + align-items: center !important; + gap: 4px !important; + height: 22px !important; + min-height: 22px !important; + padding: 0 10px !important; + border: 1px solid #6f8db5 !important; + border-image: none !important; + border-image-source: none !important; + border-radius: 4px !important; + background: linear-gradient(180deg, #ffffff 0%, #e7eef8 100%) !important; + background-image: linear-gradient(180deg, #ffffff 0%, #e7eef8 100%) !important; + color: #1a3a6b !important; + font-size: 10px !important; + font-weight: 600 !important; + line-height: 1 !important; + text-shadow: none !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8), 0 1px 0 rgba(0, 0, 0, 0.12) !important; + cursor: pointer !important; + white-space: nowrap !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button:hover { + background: linear-gradient(180deg, #ffffff 0%, #d6e2f3 100%) !important; + background-image: linear-gradient(180deg, #ffffff 0%, #d6e2f3 100%) !important; + color: #0b2347 !important; + border-color: #4a72b8 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button:active { + background: linear-gradient(180deg, #d6e2f3 0%, #ffffff 100%) !important; + background-image: linear-gradient(180deg, #d6e2f3 0%, #ffffff 100%) !important; + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.08), 0 0 0 rgba(0, 0, 0, 0) !important; +} + +/* The "Nieuwe aanbieding" button uses text-success - give it the + habbo-yellow buy-button palette to mark it as the create action. */ +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button.text-success, +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button[class*="success"] { + border-color: #8a5b00 !important; + background: linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; + background-image: linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; + color: #4a2b00 !important; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55) !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button.text-success:hover, +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button[class*="success"]:hover { + background: linear-gradient(180deg, #fff080 0%, #ffd54d 45%, #f5ab1c 100%) !important; + background-image: linear-gradient(180deg, #fff080 0%, #ffd54d 45%, #f5ab1c 100%) !important; + color: #4a2b00 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button svg { + width: 11px !important; + height: 11px !important; + fill: currentColor !important; + flex-shrink: 0 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button .font-mono { + color: #4a4a44 !important; + font-weight: 600 !important; + margin-left: 4px !important; +} + +/* Admin cog tab at the end of the tab strip - only renders when the + user is a mod, so leaving it visible at all times is safe. Style + it as a compact square that sits flush with the other tabs + instead of stretching to flex: 1 like a category tab. */ +.nitro-catalog-classic-tabs-shell .nitro-card-tab-item.nitro-catalog-classic-admin-tab { + flex: 0 0 auto !important; + width: 32px !important; + min-width: 32px !important; + max-width: 32px !important; + padding: 6px 4px 7px !important; + margin-left: 4px !important; +} + +.nitro-catalog-classic-swf-header { + position: relative; + flex: 0 0 90px; + height: 90px; + margin: 0 1px; + border: 1px solid #376275; + background: #0e3f52; + overflow: hidden; +} + +.nitro-catalog-classic-swf-header-bg { + position: absolute; + inset: 0; + opacity: 0.1; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + background: + linear-gradient(135deg, transparent 0 46%, rgba(255, 255, 255, 0.35) 47% 49%, transparent 50%), + radial-gradient(circle at 18% 28%, rgba(255, 255, 255, 0.55), transparent 18%), + linear-gradient(180deg, #60a6bd, #0e3f52); + filter: grayscale(1); +} + +.nitro-catalog-classic-swf-header-icon { + position: absolute; + left: 24px; + top: 30px; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 35px; + transform: scale(2); + transform-origin: center; +} + +.nitro-catalog-classic-swf-header-copy { + position: absolute; + left: 80px; + top: 11px; + right: 15px; + color: #fff; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.7); +} + +.nitro-catalog-classic-swf-header-title { + min-height: 24px; + font-size: 24px; + font-weight: 700; + line-height: 24px; +} + +.nitro-catalog-classic-swf-header-description { + margin-top: 5px; + font-size: 12px; + line-height: 15px; } .nitro-catalog-classic-tabs-shell { - flex-wrap: nowrap; - gap: 2px; - min-height: 32px; - max-height: 32px; - padding: 4px 6px 0; + /* Strip just tall enough to hold the 36px tab + 4px of breathing + room above. Trims the dead blue band between the header and + the tabs so the catalog body doesn't lose vertical space. */ + flex: 0 0 40px; + height: 40px; + min-height: 40px; + max-height: 40px; + gap: 0; + padding: 4px 6px 0 !important; + align-items: flex-end; + /* Horizontal scroll so every category tab stays reachable when the + card is narrower than the total tab width. */ overflow-x: auto; overflow-y: hidden; - align-items: end; - background: var(--cat-strip); - border-bottom: 2px solid var(--cat-ink); + flex-wrap: nowrap; + scrollbar-width: thin; + scrollbar-color: #8da3b3 transparent; + background: var(--catalog-swf-bg) !important; + border: 0 !important; + border-bottom: 1px solid #c8c8bf; +} + +/* The tabs strip uses a slim 6px scrollbar - opt it out of the + 17px Habbo-sprite scrollbar applied to the rest of the catalog. */ +.nitro-catalog-classic-tabs-shell::-webkit-scrollbar { + width: 6px !important; + height: 6px !important; +} + +.nitro-catalog-classic-tabs-shell::-webkit-scrollbar-thumb { + background: #8da3b3 !important; + background-image: none !important; + border-radius: 3px !important; + box-shadow: none !important; + min-height: 0 !important; +} + +.nitro-catalog-classic-tabs-shell::-webkit-scrollbar-track { + background: transparent !important; + background-image: none !important; + box-shadow: none !important; +} + +.nitro-catalog-classic-tabs-shell::-webkit-scrollbar-button:single-button { + display: none !important; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { - min-height: 28px; - padding: 5px 12px 4px; - border: 1px solid var(--cat-tab-border); - border-bottom: 0; - border-radius: 5px 5px 0 0; - background: var(--cat-tab); - color: var(--cat-ink); - box-shadow: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + height: 36px; + min-width: 0; + /* Equal-width tabs that share the strip exactly - the right + edge of the last tab now lines up with the right edge of the + catalog window, no trailing gap. */ + flex: 1 1 0; + max-width: none; + padding: 6px 6px 7px; + margin: 0 2px 0 0; + flex-shrink: 1; + /* Classic Habbo tab: gray rounded-top rectangle with a 1px black + outline. ubuntu_tab3_*.png isn't shipped, so we draw the + habbo-look ourselves instead of border-image-slicing a missing + sprite. */ + border: 1px solid #000 !important; + border-bottom: 0 !important; + border-radius: 6px 6px 0 0 !important; + background: + linear-gradient(180deg, #d6d6cc 0%, #c1c1b7 100%) !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.55), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; + color: #2a2a2a !important; + font-size: 12px; + font-weight: 400; white-space: nowrap; - font-weight: 700; + overflow: hidden; + border-image-source: none !important; +} + +/* Bring the last tab flush with whatever follows (admin cog, edge), + instead of leaving the negative right margin tugging on empty + space. */ +.nitro-catalog-classic-tabs-shell .nitro-card-tab-item:last-of-type { + margin-right: 0; +} + +.nitro-catalog-classic-tabs-shell .nitro-card-tab-item > div { + align-items: center !important; + justify-content: center !important; + gap: 4px; + min-width: 0; + width: auto; + height: 20px; + line-height: 17px; +} + +/* Category icon that sits before the label. The blanket "hide every + img/svg inside a tab" rule is gone - we explicitly size the + classic tab icon and let everything else fall through. */ +.nitro-catalog-classic-tab-icon { + width: 18px; + height: 18px; + object-fit: contain; + image-rendering: pixelated; + flex-shrink: 0; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item:hover { - background: #d2d0c6; + background: linear-gradient(180deg, #e3e3d9 0%, #d0d0c5 100%) !important; + color: #000 !important; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item-active { - background: #ffffff; - color: #000000; - position: relative; - top: 1px; - border-color: var(--cat-ink); - box-shadow: inset 0 -1px 0 #ffffff; + z-index: 2; + /* Active tab: the catalog-header habbo-blue with the cream catalog + body bleeding up into it. Drop the bottom border so the tab + "merges" with the panel below. */ + background: + linear-gradient(180deg, #4fb3ff 0%, var(--catalog-swf-blue) 100%) !important; + color: #ffffff !important; font-weight: 700; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.55), + inset 0 -2px 0 var(--catalog-swf-blue-dark) !important; +} + +.nitro-catalog-classic-tab-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; } .nitro-catalog-classic-content-shell { + position: relative; + flex: 1 1 auto; + min-height: 0; padding: 6px 8px 8px !important; - background: #ffffff !important; + background: var(--catalog-swf-bg) !important; + overflow: hidden; } .nitro-catalog-classic-stage { display: grid; - grid-template-columns: 196px minmax(0, 1fr); + /* Sidebar pinned at 184px; the layout column takes the rest of + the stage row so the right edge of the right column lines up + with the right edge of the catalog window instead of leaving a + wide cream strip. */ + grid-template-columns: 184px 1fr; gap: 8px; - min-height: 0; + width: 100%; height: 100%; + min-height: 0; } .nitro-catalog-classic-stage.is-navigation-hidden { - grid-template-columns: minmax(0, 1fr); + grid-template-columns: 1fr; } .nitro-catalog-classic-sidebar { @@ -112,103 +534,159 @@ flex-direction: column; gap: 4px; min-height: 0; - height: 100%; + overflow: hidden; } .nitro-catalog-classic-search-shell { - padding: 4px; - border: 1px solid var(--cat-line); - border-radius: 4px; - background: var(--cat-panel); + position: relative; + /* Use flex so the input vertically centers inside the 24px shell + regardless of the input's own intrinsic baseline. */ + display: flex; + align-items: center; + height: 24px; + /* Outer padding 0, negative horizontal margin bleeds the shell a + couple of pixels past its sidebar column on each side without + touching the grid template - cheap way to look ~4px wider. */ + padding: 0; + margin: -2px -1px 0 -1px; + border: 1px solid #b7b7ae; + border-radius: 3px; + background: #f7f7f2; } +/* Clear the magnifying-glass on the left and the X-clear button on the + right. The shell's own outer padding is essentially zero, so the + input claims the full sidebar column width minus just enough to + keep the icons from overlapping the text. */ .nitro-catalog-classic-search-shell input { - height: 20px; - padding-top: 0 !important; - padding-bottom: 0 !important; - border-width: 1px !important; - border-color: var(--cat-tab-border) !important; - border-radius: 3px !important; - background: #fff !important; - box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.06); + flex: 1 1 auto; + width: 100% !important; + height: 22px !important; + min-height: 22px !important; + line-height: 22px !important; + padding: 0 16px 0 18px !important; + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; + color: var(--catalog-swf-muted) !important; + font-size: 11px !important; + box-shadow: none !important; + vertical-align: middle !important; +} + +/* The wrapping div the React component renders is 100% wide so the + input fills the shell instead of shrinking to content. */ +.nitro-catalog-classic-search-shell > div { + width: 100% !important; + height: 100% !important; } .nitro-catalog-classic-search-shell svg { - color: #5b7080 !important; + color: #888 !important; + font-size: 11px !important; +} + +/* The search icon ships with absolute + left-2; nudge it tight to the + shell edge so the input can keep its left padding small. */ +.nitro-catalog-classic-search-shell svg:first-child { + left: 4px !important; +} + +/* X-clear button on the right edge - keep it tight too. */ +.nitro-catalog-classic-search-shell button { + right: 4px !important; } .nitro-catalog-classic-navigation-shell { flex: 1 1 auto; min-height: 0; - padding: 4px 0; - border: 1px solid var(--cat-line); - border-radius: 4px; - background: var(--cat-panel); + padding: 5px 3px; + border: 1px solid #b4b4ae; + border-radius: 3px; + background: rgba(236, 236, 228, 0.5); overflow: auto; } .nitro-catalog-classic-navigation-list { display: flex; flex-direction: column; - gap: 0; } -.nitro-catalog-classic-navigation-node.is-child .nitro-catalog-classic-navigation-item { - padding-left: 22px; - background: var(--cat-sub); +.nitro-catalog-classic-navigation-node { + min-height: 21px; } .nitro-catalog-classic-navigation-item { + position: relative; display: flex; align-items: center; gap: 6px; - min-height: 28px; - padding: 4px 10px; - border: 0; - border-left: 4px solid transparent; - border-radius: 0; + height: 21px; + min-height: 21px; + padding: 1px 18px 1px 0; + border: 0 !important; + border-radius: 0 !important; background: transparent; - color: var(--cat-ink); + color: var(--catalog-swf-muted); + font-size: 11px; font-weight: 700; + line-height: 17px; cursor: pointer; - transition: background-color 0.12s ease; + text-shadow: 0 1px 0 #b4b4ae; } .nitro-catalog-classic-navigation-node.is-child .nitro-catalog-classic-navigation-item { - font-weight: 400; + padding-left: 15px; + color: #52819a; + font-style: italic; } -.nitro-catalog-classic-navigation-item:hover { - background: #dcdacf; +.nitro-catalog-classic-navigation-item::before { + position: absolute; + inset: 0 1px 1px 1px; + z-index: 0; + display: none; + content: ""; + border: 1px solid var(--catalog-swf-select-outer); + background: linear-gradient(180deg, var(--catalog-swf-select-outer) 0 2px, var(--catalog-swf-select) 2px calc(100% - 2px), var(--catalog-swf-select-outer) calc(100% - 2px)); } -.nitro-catalog-classic-navigation-item.is-active { - background: #ffffff; - border-left-color: var(--cat-blue); - color: #000000; - font-weight: 700; +.nitro-catalog-classic-window .builders-club-card-header ~ .nitro-catalog-classic-tabs-shell, +.nitro-catalog-classic-window:has(.builders-club-card-header) .nitro-catalog-classic-navigation-item::before { + --catalog-swf-select: var(--catalog-swf-bc); + --catalog-swf-select-outer: var(--catalog-swf-bc-outer); } -.nitro-catalog-classic-navigation-item.is-drag-over { - outline: 2px solid rgba(74, 125, 140, 0.4); - outline-offset: -2px; +.nitro-catalog-classic-navigation-item:hover::before, +.nitro-catalog-classic-navigation-item.is-active::before { + display: block; +} + +.nitro-catalog-classic-navigation-icon, +.nitro-catalog-classic-navigation-label, +.nitro-catalog-classic-navigation-caret, +.nitro-catalog-classic-navigation-favorite, +.nitro-catalog-classic-navigation-admin, +.nitro-catalog-classic-navigation-drag { + position: relative; + z-index: 1; } .nitro-catalog-classic-navigation-icon { display: flex; align-items: center; justify-content: center; - width: 18px; - min-width: 18px; - height: 18px; + width: 20px; + min-width: 20px; + height: 20px; } .nitro-catalog-classic-navigation-icon img, .nitro-catalog-classic-navigation-icon canvas { width: auto; height: auto; - max-width: 18px; - max-height: 18px; + max-width: 20px; + max-height: 20px; } .nitro-catalog-classic-navigation-label { @@ -217,19 +695,14 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: 11px; - line-height: 1; -} - -.nitro-catalog-classic-navigation-caret, -.nitro-catalog-classic-navigation-favorite, -.nitro-catalog-classic-navigation-admin, -.nitro-catalog-classic-navigation-drag { - flex-shrink: 0; } .nitro-catalog-classic-navigation-caret { - color: #5b7080 !important; + position: absolute; + right: 5px; + top: 5px; + color: #777 !important; + font-size: 9px; } .nitro-catalog-classic-layout-shell { @@ -238,378 +711,879 @@ min-width: 0; min-height: 0; height: 100%; - border: 1px solid var(--cat-line); - border-radius: 4px; - background: #ffffff; + background: transparent; + border: 0; overflow: hidden; } -.nitro-catalog-classic-layout-header-shell { - display: flex; - flex-direction: column; - gap: 3px; - flex-shrink: 0; - min-height: 0; - padding: 6px 8px; - border-bottom: 1px solid var(--cat-line); - background: #ffffff; -} - -.nitro-catalog-classic-layout-hero { - display: flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - min-height: 32px; - overflow: visible; -} - -.nitro-catalog-classic-layout-hero img { - display: block; - width: auto; - height: auto; - max-width: 100%; - max-height: none; - object-fit: contain; -} - .nitro-catalog-classic-layout-container { flex: 1 1 auto; min-height: 0; - padding: 6px; - background: #ffffff; + padding: 0; + background: transparent; overflow: hidden; } .nitro-catalog-classic-default-layout { - gap: 8px; + position: relative; + display: block !important; + /* Fill the layout container in both axes - the stage was + previously 552px wide and this column was pinned at 360px, but + now that the stage uses 1fr, hardcoding 360px would leave a + wide blank strip on the right of every default-layout catalog + page. */ + width: 100%; + height: 100%; + min-height: 460px; } -.nitro-catalog-classic-offer-panel, -.nitro-catalog-classic-welcome { - border: 1px solid var(--cat-line); - border-radius: 6px; - background: #ffffff; +/* The product-view, grid-shell, price-row and purchase-row inside + the default-layout were each pinned at 360px to match the old + stage width. Widen them in lockstep so they fill the new + 1fr-sized container. */ +.nitro-catalog-classic-product-view, +.nitro-catalog-classic-grid-shell, +.nitro-catalog-classic-price-row, +.nitro-catalog-classic-purchase-row { + width: 100% !important; } -.nitro-catalog-classic-offer-panel { - min-height: 132px; +.nitro-catalog-classic-product-view { + width: calc(100% + 3px) !important; +} + +.nitro-catalog-classic-product-view, +.nitro-catalog-classic-grid-shell { + border: 1px solid #b4b4ae; + border-radius: 5px; + background: var(--catalog-swf-panel); +} + +.nitro-catalog-classic-product-view { + position: relative; + top: 0; + left: 0; + width: 363px; + height: 240px; + min-height: 0; overflow: hidden; } +.nitro-catalog-classic-product-view::before { + display: none; +} + +.nitro-catalog-classic-offer-panel { + position: relative; + z-index: 1; + width: 100%; + height: 100%; +} + .nitro-catalog-classic-offer-preview { - width: 190px; - min-width: 190px; - padding: 8px; - border-right: 1px solid var(--cat-line); - background-color: var(--cat-canvas); + position: relative; + width: 360px; + min-width: 360px; + height: 100%; + padding: 0; + overflow: hidden; + background: #000; +} + +.nitro-catalog-classic-preview-title { + position: absolute; + top: 12px; + left: 12px; + z-index: 5; + color: #ffffff !important; + font-size: 12px !important; + font-weight: 700 !important; + line-height: 15px !important; + text-shadow: 0 1px 0 #000; +} + +.nitro-catalog-classic-offer-preview .shadow-room-previewer, +.nitro-catalog-classic-offer-preview canvas, +.nitro-catalog-classic-offer-preview img { + width: 100% !important; + height: 100% !important; + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 0 !important; + box-shadow: none !important; + background-position: center bottom !important; + background-repeat: no-repeat !important; + background-size: contain !important; } .nitro-catalog-classic-offer-info { - padding: 10px; + display: none !important; } -.nitro-catalog-classic-offer-actions { - justify-content: flex-end; +.nitro-catalog-classic-offer-info .nitro-text, +.nitro-catalog-classic-offer-info span, +.nitro-catalog-classic-offer-info div { + color: inherit; } -.nitro-catalog-classic-offer-info .rounded-full { - background: var(--cat-gold) !important; - border-color: var(--cat-gold-border) !important; -} - -.nitro-catalog-classic-offer-info .rounded-full, -.nitro-catalog-classic-offer-info .rounded-full * { - color: var(--cat-gold-ink) !important; -} .nitro-catalog-classic-welcome { - min-height: 128px; - padding: 10px; + position: relative; + z-index: 1; + width: 100%; + height: 100%; + padding: 18px; + color: var(--catalog-swf-muted); +} + +.nitro-catalog-classic-welcome img { + width: auto !important; + height: auto !important; + max-width: 96px; + max-height: 120px; + border: 0; + background: transparent; } .nitro-catalog-classic-grid-shell { - min-height: 150px; - padding: 6px; - border: 1px solid var(--cat-line); - border-radius: 6px; - background: #ffffff; - height: auto; - flex: 1 1 auto; + position: absolute; + left: 0; + top: 245px; + /* Stretch down to just above the price + purchase rows so the + grid soaks up any extra height the bigger window gives us. */ + bottom: 68px; + width: 360px; + min-height: 0; + padding: 3px 2px 3px 4px; + overflow: auto; +} + +/* When the admin row is rendered above the product-view it adds + ~30px (22px button + flex gap) to the flex column, but the + grid-shell is absolutely positioned and doesn't shift on its own. + Push it (and the bottom-anchored price/purchase rows stay put) + down so the preview panel no longer bleeds into the grid. */ +.nitro-catalog-classic-default-layout:has(.nitro-catalog-classic-default-admin) .nitro-catalog-classic-grid-shell { + top: 280px !important; } .nitro-catalog-classic-grid { - gap: 6px !important; + /* Don't pin a fixed column track here - AutoGrid sets the inline + grid-template-columns from its columnMinWidth prop. The earlier + `repeat(6, 53px) !important` was clobbering that and freezing + the row at 6 tiles regardless of what the React layout passed. */ + grid-auto-rows: var(--nitro-grid-column-min-height, 70px); align-content: start; + justify-content: start; + gap: 3px !important; + overflow: visible !important; } .nitro-catalog-classic-window .layout-grid-item { - height: 54px; - border: 1px solid var(--cat-line) !important; - border-radius: 4px !important; - background-color: #ffffff; - background-image: none; - box-shadow: none; - transition: background-color 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease; + /* Let the tile flex to whatever min/max width the AutoGrid sets + via repeat(auto-fill, minmax(N, 1fr)) - hard-pinning 53x74 was + overriding the layout's columnMinWidth prop, so the row count + never changed when we reduced it. Width is now 100% of the + column cell, height tracks --nitro-grid-column-min-height. */ + width: 100% !important; + height: var(--nitro-grid-column-min-height, 70px) !important; + min-width: 0 !important; + min-height: var(--nitro-grid-column-min-height, 70px) !important; + border: 0 !important; + border-radius: 0 !important; + background-color: transparent !important; + background-image: none !important; + box-shadow: none !important; + overflow: visible !important; } .nitro-catalog-classic-window .layout-grid-item:hover { - background-color: var(--cat-select-bg) !important; - border-color: var(--cat-select) !important; - box-shadow: 0 0 0 1px rgba(74, 125, 140, 0.2); + background-image: none !important; + box-shadow: inset 0 0 0 1px #a1a19b !important; } .nitro-catalog-classic-window .layout-grid-item.is-active { - background-color: var(--cat-select-bg) !important; - border-color: var(--cat-select) !important; - border-width: 2px !important; - box-shadow: 0 0 0 1px rgba(74, 125, 140, 0.35); + background-image: none !important; + box-shadow: + inset 0 0 0 1px #63c5e9, + inset 2px 2px 0 #ecece4, + inset -2px -2px 0 #ecece4 !important; } .nitro-catalog-classic-grid-offer-icon { position: absolute; - inset: 4px; - background-repeat: no-repeat; - background-position: center; + left: 50%; + top: 20px; + width: auto !important; + height: auto !important; + max-width: 36px; + max-height: 36px; + object-fit: contain; + transform: translate(-50%, -50%); pointer-events: none; } -.nitro-catalog-classic-window .nitro-catalog-header { - display: none; -} - -.nitro-catalog-classic-offer-info .bg-\[\#00800b\] { - background-color: var(--cat-buy) !important; - border-color: #007a00 !important; -} - -.nitro-catalog-classic-breadcrumb { +.nitro-catalog-classic-grid-price { + position: absolute; + left: 2px; + right: 2px; + top: 36px; + bottom: auto; display: flex; + flex-direction: column; align-items: center; - gap: 5px; - min-height: 16px; - overflow: hidden; - color: #5b7080; - font-size: 10px; - line-height: 1; + justify-content: flex-start; + gap: 0; + min-height: 24px; + color: #000; + font-size: 11px; + font-weight: 700; + line-height: 12px; + pointer-events: none; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.75); +} + +/* When the tile shows a full-tile bot/pet avatar (instead of a small + icon), pin the price strip to the bottom of the tile and give it a + translucent backdrop so it doesn't overlap with the avatar body. */ +.nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price, +.nitro-catalog-classic-grid .layout-grid-item:has(> .avatar-image) > .nitro-catalog-classic-grid-price, +.nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price { + top: auto !important; + /* Re-anchor horizontally too: the parent rule's left: 2px / + right: 2px combined with content-sized inner flex was visually + parking the pill at the left side of the tile. Center it via + explicit left/right + transform so it lands smack in the + middle regardless of inner content width. */ + left: 50% !important; + right: auto !important; + bottom: 4px !important; + transform: translateX(-50%) !important; + width: auto !important; + background: rgba(255, 255, 255, 0.85) !important; + border-radius: 3px !important; + padding: 2px 6px !important; + min-height: 0 !important; + height: auto !important; + display: inline-flex !important; + flex-direction: row !important; + align-items: center !important; + justify-content: center !important; + z-index: 5 !important; +} + +/* Tighten the price entry inside the avatar-tile pill so the number + and currency icon center on the same baseline (the global + .grid-price-entry height: 13px clipped the 15px wallet icon and + pushed it visually below the number). */ +.nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price-entry, +.nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price .nitro-catalog-classic-grid-price-entry { + height: auto !important; + line-height: 1 !important; + align-items: center !important; + justify-content: center !important; + gap: 3px !important; +} + +.nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price-currency, +.nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price .nitro-catalog-classic-grid-price-currency { + width: 13px !important; + height: 13px !important; + object-fit: contain !important; + vertical-align: middle !important; + display: inline-block !important; +} + +.nitro-catalog-classic-grid-price.is-single-price { + height: 19px; +} + +.nitro-catalog-classic-grid-price.is-multi-price { + height: 38px; +} + +.nitro-catalog-classic-grid-price-entry { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 1px; + height: 13px; white-space: nowrap; } -.nitro-catalog-classic-breadcrumb-segment { +.nitro-catalog-classic-grid-price-plus { + padding-right: 1px; +} + +.nitro-catalog-classic-grid-price-currency { + width: auto !important; + height: auto !important; + max-width: none !important; + max-height: none !important; + min-width: 0 !important; + min-height: 0 !important; + object-fit: none !important; +} + +.nitro-catalog-classic-price-row { + position: absolute; + left: 0; + /* Anchored from the bottom so the Aantal/Prezzo row sits just + above the Cadeau/Koop buttons regardless of layout height. */ + bottom: 38px; + width: 360px; + height: 25px; + padding: 0; + overflow: visible; +} + +.nitro-catalog-classic-spinner-slot { + position: absolute; + left: 0; + top: 0; + width: 200px; + height: 25px; +} + +.nitro-catalog-classic-total-price-slot { + position: absolute; + /* Anchored to the right of the now-100% wide price row so the + Prezzo + amount stays flush with the right edge of the panel. */ + right: 2px; + top: 0; + width: auto; + min-width: 180px; + height: 25px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding-right: 2px; +} + +.nitro-catalog-classic-total-price-slot::before { + content: "Prezzo"; + color: #666; + font-size: 11px; + line-height: 17px; +} + +.nitro-catalog-classic-purchase-row { + position: absolute; + left: 0; + /* Anchored to the bottom of the panel with a 4px breathing strip + so the Cadeau / Koop buttons stay flush at the bottom of the + window no matter how tall the catalog is. */ + bottom: 4px; + width: 360px; + height: 30px; + padding: 0; + overflow: hidden; +} + +.nitro-catalog-classic-offer-actions { + display: flex !important; + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + gap: 10px !important; + /* Fill the now-100% wide purchase row instead of staying pinned at + 330px (which used to match the old 360px column - 15px each + side). */ + width: auto; + height: 24px; + margin-left: 15px; + margin-right: 15px; + padding: 0; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-button, +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + min-height: 22px; + padding: 2px 12px !important; + border: 3px solid transparent !important; + border-radius: 0 !important; + border-image-source: var(--habbo-slice-button-default) !important; + border-image-slice: 3 3 3 3 fill !important; + border-image-width: 3px !important; + border-image-repeat: stretch !important; + background: transparent !important; + background-color: transparent !important; + background-image: none !important; + color: #222 !important; + font-size: 11px !important; + font-weight: 700 !important; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); + box-shadow: none !important; + transition: none !important; +} + +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn *, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn svg { + border-radius: 0 !important; + box-shadow: none !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-button:hover, +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button:hover, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn:hover { + border-image-source: var(--habbo-slice-button-hover) !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-button:active, +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button:active, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn:active { + border-image-source: var(--habbo-slice-button-pressed) !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-button.pointer-events-none, +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button:disabled, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn:disabled { + border-image-source: var(--habbo-slice-button-disabled) !important; + color: #888 !important; + opacity: 1 !important; +} + +/* Buy / Gift buttons - pure CSS. border-image-slicing the bitmap + sprites produced thin highlight/shadow stripes at the top and + bottom because the source rounded corners are ~5-6px tall but the + buttons render at 22-24px, so the slice rows stretched into a + visible band. CSS gradients give a crisp pixel-art classic-habbo + look without those artefacts. */ + +.nitro-catalog-classic-window .nitro-catalog-swf-buy-button { + width: 160px !important; + min-width: 160px !important; + height: 24px !important; + padding: 0 8px !important; + border: 1px solid #000 !important; + border-radius: 4px !important; + border-image: none !important; + border-image-source: none !important; + /* Yellow body with the same #f0a318 / #ffd54d tones as the + skin3-yellow Buy sprite. */ + background: + linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.65), + inset 0 -2px 0 rgba(140, 75, 0, 0.35) !important; + color: #4a2b00 !important; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55) !important; + font-weight: 700 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-buy-button:hover { + background: + linear-gradient(180deg, #fff080 0%, #ffd54d 45%, #f5ab1c 100%) !important; + filter: brightness(1.04); +} + +.nitro-catalog-classic-window .nitro-catalog-swf-buy-button:active { + background: + linear-gradient(180deg, #f0a318 0%, #d98c0a 100%) !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.25), + inset 0 2px 0 rgba(140, 75, 0, 0.45) !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-buy-button.pointer-events-none, +.nitro-catalog-classic-window .nitro-catalog-swf-buy-button:disabled { + /* Stay yellow when disabled - the user wants the action colour + to be recognisable regardless of state. Drop opacity + flip + the cursor so it still reads as non-interactive. */ + background: + linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; + color: #4a2b00 !important; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55) !important; + opacity: 0.55 !important; + cursor: not-allowed !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-gift-button { + width: 160px !important; + min-width: 160px !important; + height: 22px !important; + padding: 0 8px !important; + border: 1px solid #000 !important; + border-radius: 4px !important; + border-image: none !important; + border-image-source: none !important; + /* Cream / light-gray body matching the catalog cardstock. */ + background: + linear-gradient(180deg, #ececec 0%, #cfcfc4 100%) !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; + color: #2a2a2a !important; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55) !important; + font-weight: 700 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-gift-button:hover { + background: + linear-gradient(180deg, #f4f4ed 0%, #dcdcd0 100%) !important; + filter: brightness(1.02); +} + +.nitro-catalog-classic-window .nitro-catalog-swf-gift-button:active { + background: + linear-gradient(180deg, #c0c0b6 0%, #aaaaa0 100%) !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.2), + inset 0 2px 0 rgba(0, 0, 0, 0.25) !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-gift-button.pointer-events-none, +.nitro-catalog-classic-window .nitro-catalog-swf-gift-button:disabled { + background: + linear-gradient(180deg, #e3e3dc 0%, #c5c5bb 100%) !important; + color: #6a6a64 !important; + text-shadow: none !important; +} + +/* Pet purchase card lives in a tight flex row alongside the price, + so the main 160px Buy button doesn't fit. Shrink it down here. */ +.nitro-catalog-classic-pet-card .nitro-catalog-swf-buy-button { + width: auto !important; + min-width: 0 !important; + padding: 0 14px !important; +} + +/* All catalog grids must scroll vertically only - horizontal overflow + produces a stray horizontal scrollbar at the bottom of the items + strip on narrow columns (e.g. guild_furni). minmax(N, 1fr) usually + contains content but the safety net stops any odd item from + triggering a horizontal bar. */ +.nitro-catalog-classic-window .layout-grid, +.nitro-catalog-classic-window [class*="grid-cols-["] { + overflow-x: hidden !important; +} + +.nitro-catalog-swf-spinner { + display: flex; + align-items: center; + gap: 5px; + height: 25px; + color: #666; + font-size: 11px; +} + +.nitro-catalog-swf-spinner-label { + max-width: 62px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.nitro-catalog-swf-spinner-control { + display: grid; + grid-template-columns: 22px 34px 22px; + align-items: center; + height: 25px; + gap: 2px; +} + +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button { + width: 22px !important; + min-width: 22px !important; + height: 22px !important; + min-height: 22px !important; + padding: 0 !important; + border: 0 !important; + border-radius: 0 !important; + border-image: none !important; + background-color: transparent !important; + background-repeat: no-repeat !important; + background-position: center center !important; + background-size: auto !important; + box-shadow: none !important; + image-rendering: pixelated !important; +} + +/* react-icons FaMinus/FaPlus glyphs ride inside these buttons; hide + them - the sprite already contains the +/- mark. */ +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button svg { + display: none !important; +} + +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-less { + background-image: var(--habbo-stepper-minus) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-less:hover { + background-image: var(--habbo-stepper-minus-hover) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-less:active { + background-image: var(--habbo-stepper-minus-pressed) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-less:disabled { + background-image: var(--habbo-stepper-minus-disabled) !important; +} + +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-more { + background-image: var(--habbo-stepper-plus) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-more:hover { + background-image: var(--habbo-stepper-plus-hover) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-more:active { + background-image: var(--habbo-stepper-plus-pressed) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-more:disabled { + background-image: var(--habbo-stepper-plus-disabled) !important; +} + +.nitro-catalog-swf-spinner-input { + width: 30px !important; + height: 25px !important; + padding: 4px 2px !important; + border: 1px solid #9d9d96 !important; + border-radius: 0 !important; + background: #fff !important; + color: #222 !important; + font-size: 11px !important; + font-weight: 700 !important; + line-height: 15px !important; + text-align: center !important; + outline: none !important; + appearance: textfield; +} + +.nitro-catalog-swf-spinner-input::-webkit-outer-spin-button, +.nitro-catalog-swf-spinner-input::-webkit-inner-spin-button { + appearance: none; + margin: 0; +} + +.nitro-catalog-swf-price-display { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 3px; + min-height: 24px; + color: #222; + font-size: 11px; +} + +.nitro-catalog-swf-price-pill { display: inline-flex; align-items: center; - gap: 5px; - min-width: 0; + gap: 2px; + height: 22px; + min-width: 34px; + padding: 0 2px; + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; } -.nitro-catalog-classic-breadcrumb-separator { - color: #94a7b3; +.nitro-catalog-swf-price-text { + color: #222 !important; + font-size: 11px !important; + font-weight: 700 !important; + line-height: 17px !important; } -.nitro-catalog-classic-navigation-shell::-webkit-scrollbar, -.nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar { - width: 12px; -} - -.nitro-catalog-classic-navigation-shell::-webkit-scrollbar-track, -.nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-track { - border-left: 1px solid var(--cat-line); - background: var(--cat-panel); -} - -.nitro-catalog-classic-navigation-shell::-webkit-scrollbar-thumb, -.nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-thumb { - border: 1px solid var(--cat-tab-border); - border-radius: 6px; - background: linear-gradient(180deg, #a9bcc9 0%, #89a0ae 100%); -} - -.nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:decrement, -.nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:decrement { - height: 12px; - background: var(--cat-panel); -} - -.nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:increment, -.nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:increment { - height: 12px; - background: var(--cat-panel); -} - -@media (max-width: 1024px) and (min-width: 641px) { - .nitro-catalog-classic-window { - width: min(calc(100vw - 24px), 720px) !important; - min-width: 0 !important; - max-width: calc(100vw - 24px) !important; - height: min(calc(100vh - 24px), 720px) !important; - min-height: 0 !important; - max-height: calc(100vh - 24px) !important; - } - - .nitro-catalog-classic-stage { - grid-template-columns: minmax(0, 1fr); - } - - .nitro-catalog-classic-sidebar { - max-height: 200px; - } -} - -.nitro-catalog-classic-mobile-header { - position: absolute; - top: 0; - left: 0; - right: 0; - z-index: 5; - display: flex; - align-items: center; - height: 38px; - padding: 0 44px 0 8px; - pointer-events: none; -} - -.nitro-catalog-classic-mobile-burger { - position: relative; - pointer-events: auto; -} - -.nitro-catalog-classic-burger-btn { - display: flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border: 0; - border-radius: 5px; - background: rgba(0, 0, 0, 0.2); - color: #fff; - font-size: 13px; - cursor: pointer; -} - -.nitro-catalog-classic-burger-btn:hover { - background: rgba(0, 0, 0, 0.3); -} - -.nitro-catalog-classic-burger-btn:active { - background: rgba(0, 0, 0, 0.36); -} - -.nitro-catalog-classic-burger-menu { - position: absolute; - top: 32px; - left: 0; - z-index: 60; - display: flex; - flex-direction: column; - gap: 4px; - min-width: 150px; - padding: 6px; - border: 1px solid var(--cat-line); - border-radius: 6px; - background: #fff; - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28); -} - -.nitro-catalog-classic-burger-menu button { - padding: 8px 10px; - border: 0; - border-radius: 4px; - background: var(--cat-strip); - color: var(--cat-ink); - font-weight: 700; - text-align: left; - cursor: pointer; -} - -.nitro-catalog-classic-burger-menu button:disabled { - opacity: 0.6; -} - -.nitro-catalog-classic-mobile-currency { - margin-left: auto; - display: flex; - align-items: center; - gap: 5px; - pointer-events: auto; -} - -.nitro-catalog-classic-coin { - display: flex; - align-items: center; - gap: 3px; - padding: 3px 7px; - border-radius: 11px; - background: rgba(0, 0, 0, 0.25); - color: #fff; - font-size: 10px; - font-weight: 700; -} - -.nitro-catalog-classic-coin span { - color: #fff; -} - -.nitro-catalog-classic-admin-tab { - display: none !important; +.nitro-catalog-swf-price-plus { + width: 7px; + height: 7px; + color: #666; } .nitro-catalog-classic-preview-btn { position: absolute; - top: 8px; z-index: 4; - display: inline-flex; - align-items: center; - gap: 5px; - padding: 5px 10px; - border: 1px solid #555; - border-radius: 5px; - background: rgba(0, 0, 0, 0.7); - color: #fff; - font-size: 11px; - font-weight: 700; - white-space: nowrap; - cursor: pointer; + width: 25px; + height: 24px; + min-width: 25px; + padding: 0 !important; + overflow: hidden; + /* font-size: 0 was killing the SVG: react-icons emits + , so 0em -> 0x0. Use a real + font-size and pin the SVG to explicit pixels below. */ + font-size: 14px !important; + line-height: 1 !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; } -.nitro-catalog-classic-preview-btn:hover { - background: rgba(0, 0, 0, 0.82); -} - -.nitro-catalog-classic-preview-btn:active { - background: rgba(0, 0, 0, 0.9); +.nitro-catalog-classic-preview-btn svg { + width: 14px !important; + height: 14px !important; + color: #111 !important; + fill: #111 !important; + flex-shrink: 0 !important; + display: block !important; } .nitro-catalog-classic-preview-rotate { - left: 8px; + top: 8px; + right: 35px; } .nitro-catalog-classic-preview-state { - right: 8px; + top: 8px; + right: 6px; } -@media (max-width: 640px) { - .nitro-catalog-classic-window { - --cat-blue: #4b7a94; - --cat-blue-dark: #385d73; - --cat-ink: #233a47; - --cat-strip: #d9e2e8; - --cat-tab: #b7c7d1; - --cat-tab-border: #7a9cb0; - --cat-panel: #eef2f5; - --cat-sub: #e1e7ec; - --cat-line: #b7c7d1; - --cat-select: #3a82a7; - --cat-select-bg: #f0f5f8; - --cat-buy: #009900; +.nitro-catalog-classic-window .nitro-catalog-classic-navigation-shell::-webkit-scrollbar, +.nitro-catalog-classic-window .nitro-catalog-classic-navigation-list::-webkit-scrollbar, +.nitro-catalog-classic-window .nitro-catalog-classic-grid-shell::-webkit-scrollbar, +.nitro-catalog-classic-window .nitro-catalog-classic-grid::-webkit-scrollbar, +.nitro-catalog-classic-window .nitro-catalog-classic-layout-container::-webkit-scrollbar { + width: 17px; + height: 17px; +} +/* ===== Classic catalog scrollbar (pure CSS, no sprites) ===== + Drew this with CSS gradients instead of stretching the 17x34 + skin1 thumb sprite. The sprite version pixelated into visible + horizontal bands on tall scroll areas because every source row + stretched 5-10x. CSS gradients stay crisp at any height. */ + +.nitro-catalog-classic-window * { + scrollbar-color: auto !important; + scrollbar-width: auto; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar { + width: 17px !important; + height: 17px !important; + background-color: #e7e5d8 !important; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar-track { + background-image: none !important; + background-color: #e7e5d8 !important; + box-shadow: inset 1px 0 0 #b9b6a5, inset -1px 0 0 #ffffff !important; + border: 0 !important; +} + +/* Habbo thumb: symmetric light-edges -> darker-middle gradient (the + "pinched in the middle" look of the classic Ubuntu scrollbar), + 1px near-black outline, three central grip lines via SVG centered + no-repeat. */ +.nitro-catalog-classic-window *::-webkit-scrollbar-thumb { + min-height: 28px !important; + border: 1px solid #2a2a26 !important; + border-radius: 2px !important; + background: + url("data:image/svg+xml;utf8,") center center / 10px 9px no-repeat, + linear-gradient(180deg, #d6d6cc 0%, #b4b4aa 30%, #9a9a90 50%, #b4b4aa 70%, #d6d6cc 100%) !important; + background-color: #a8a89e !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.55), + inset 0 -1px 0 rgba(255, 255, 255, 0.4) !important; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar-thumb:hover { + background: + url("data:image/svg+xml;utf8,") center center / 10px 9px no-repeat, + linear-gradient(180deg, #e0e0d6 0%, #bebeb4 30%, #a4a49a 50%, #bebeb4 70%, #e0e0d6 100%) !important; + background-color: #b2b2a8 !important; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar-thumb:active { + background: + linear-gradient(180deg, #c6c6bc 0%, #a4a49a 30%, #8a8a82 50%, #a4a49a 70%, #c6c6bc 100%) !important; + background-color: #9a9a90 !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.35), + inset 0 -1px 0 rgba(255, 255, 255, 0.25) !important; +} + +/* Arrow buttons: cream cap with a 1px black outline + dark inset + chevron. SVG glyphs so they stay crisp at any zoom. */ +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement { + display: block !important; + width: 17px !important; + height: 16px !important; + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + linear-gradient(180deg, #f3f1e6 0%, #d8d6c8 100%) !important; + background-color: #e7e5d8 !important; + border: 1px solid #0b2d3a !important; + border-radius: 2px !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; +} +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement:active { + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + linear-gradient(180deg, #c7c5b8 0%, #aeaca0 100%) !important; + background-color: #c7c5b8 !important; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:increment { + display: block !important; + width: 17px !important; + height: 16px !important; + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + linear-gradient(180deg, #f3f1e6 0%, #d8d6c8 100%) !important; + background-color: #e7e5d8 !important; + border: 1px solid #0b2d3a !important; + border-radius: 2px !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; +} +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:increment:active { + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + linear-gradient(180deg, #c7c5b8 0%, #aeaca0 100%) !important; + background-color: #c7c5b8 !important; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:horizontal:decrement { + display: block !important; + width: 16px !important; + height: 17px !important; + background: + url("data:image/svg+xml;utf8,") center center / 6px 9px no-repeat, + linear-gradient(180deg, #f3f1e6 0%, #d8d6c8 100%) !important; + background-color: #e7e5d8 !important; + border: 1px solid #0b2d3a !important; + border-radius: 2px !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; +} +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:horizontal:increment { + display: block !important; + width: 16px !important; + height: 17px !important; + background: + url("data:image/svg+xml;utf8,") center center / 6px 9px no-repeat, + linear-gradient(180deg, #f3f1e6 0%, #d8d6c8 100%) !important; + background-color: #e7e5d8 !important; + border: 1px solid #0b2d3a !important; + border-radius: 2px !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; +} + +.nitro-catalog-classic-breadcrumb { + display: none; +} + +@media (max-width: 640px), (orientation: landscape) and (max-height: 520px) { + .nitro-catalog-classic-window { width: 100vw !important; min-width: 0 !important; max-width: 100vw !important; height: 100vh !important; min-height: 0 !important; max-height: 100vh !important; - border-radius: 10px !important; + border-radius: 0 !important; } .draggable-window:has(> .nitro-catalog-classic-window) { @@ -618,131 +1592,38 @@ top: 0 !important; } - .nitro-catalog-classic-window .nitro-card-title { - display: block; - } - - .nitro-catalog-classic-window .nitro-card-header { - border-bottom-color: transparent; - box-shadow: none; - } - - .nitro-catalog-classic-mobile-currency { - position: absolute; - top: 38px; - left: 0; - right: 0; - height: 30px; - margin: 0; - transform: none; - justify-content: center; - gap: 6px; - background: #30728c; - z-index: 5; - } - .nitro-catalog-classic-tabs-shell { - margin-top: 30px; - min-height: 56px; - max-height: 56px; - padding: 4px 4px 0; - -webkit-overflow-scrolling: touch; - } - - .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { - min-height: 52px; - padding: 5px 8px; - justify-content: center; - } - - .nitro-catalog-classic-tabs-shell .nitro-card-tab-item div:has(> .nitro-catalog-classic-tab-label) { - flex-direction: column; - gap: 2px; - } - - .nitro-catalog-classic-tab-label { - font-size: 9px; - line-height: 1; + overflow-x: auto; + overflow-y: hidden; } .nitro-catalog-classic-content-shell { padding: 6px !important; } - .nitro-catalog-classic-layout-hero { - display: none; - } - - .nitro-catalog-classic-offer-panel { - flex-direction: column; - } - - .nitro-catalog-classic-offer-preview { - width: 100%; - min-width: 0; - border-right: 0; - border-bottom: 1px solid var(--cat-line); - } - - .nitro-catalog-classic-stage { + .nitro-catalog-classic-stage, + .nitro-catalog-classic-stage.is-navigation-hidden { grid-template-columns: minmax(0, 1fr); - gap: 6px; + grid-template-rows: auto minmax(0, 1fr); + width: 100%; } .nitro-catalog-classic-sidebar { - max-height: 33vh; + max-height: 34vh; } - .nitro-catalog-classic-search-shell input { - height: 28px; - font-size: 13px; - } - - .nitro-catalog-classic-navigation-item { - min-height: 40px; - padding: 6px 12px; - } - - .nitro-catalog-classic-navigation-label { - font-size: 13px; - } - - .nitro-catalog-classic-window .layout-grid-item { - height: 64px; - } - - .nitro-catalog-classic-window .nitro-card-header-shell, - .nitro-catalog-classic-window .nitro-card-content-shell { - border-radius: 0 !important; - } -} - -@media (max-height: 480px) { - .nitro-catalog-classic-window { - height: 100vh !important; - max-height: 100vh !important; - } - - .nitro-catalog-classic-window .nitro-card-header-shell { - min-height: 32px; - max-height: 32px; - } - - .nitro-catalog-classic-tabs-shell { - min-height: 38px; - max-height: 38px; - } - - .nitro-catalog-classic-layout-header-shell { + .nitro-catalog-classic-layout-shell, + .nitro-catalog-classic-default-layout { + width: 100%; + height: 100%; min-height: 0; - padding: 3px 6px; } - .nitro-catalog-classic-layout-hero { - display: none; + .nitro-catalog-classic-default-layout { + grid-template-rows: minmax(150px, 1fr) minmax(120px, 38vh) 30px; } - .nitro-catalog-classic-sidebar { - max-height: 26vh; + .nitro-catalog-classic-grid { + grid-template-columns: repeat(auto-fill, minmax(47px, 47px)) !important; } } diff --git a/src/css/chat/ChatInputMentionSelectorView.css b/src/css/chat/ChatInputMentionSelectorView.css new file mode 100644 index 0000000..d9b0cc8 --- /dev/null +++ b/src/css/chat/ChatInputMentionSelectorView.css @@ -0,0 +1,366 @@ +/* ============================================================================ + Chat-bar @-mention autocomplete - Habbo style + ---------------------------------------------------------------------------- + Mirrors the NitroCard look (cream cardstock, habbo-blue header, black 2px + border, drop shadow) and the in-room infostand row chrome. The popover + appears above the chat input, anchored to its bottom-left and the same + width as the input, with the bottom corners flush so it visually merges + with the input edge. + ============================================================================ */ + +.chat-input-mention-popover { + position: absolute; + bottom: 100%; + left: 0; + width: 100%; + margin-bottom: 4px; + background: #f2f2eb; + border: 2px solid #000; + border-radius: 10px; + box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28); + overflow: hidden; + z-index: 1070; + font-family: Volter, Volter_Goldfish, "Ubuntu", sans-serif; + image-rendering: pixelated; +} + +.chat-input-mention-popover-header { + height: 24px; + padding: 0 8px; + background: #30728c; + border-bottom: 2px solid #000; + color: #fff; + text-shadow: 1px 1px 1px #000; + font-family: UbuntuCondensed, Ubuntu, sans-serif; + font-size: 13px; + line-height: 22px; + letter-spacing: 0.3px; + display: flex; + align-items: center; + gap: 6px; +} + +.chat-input-mention-popover-header-dot { + width: 8px; + height: 8px; + background: #ffdc4c; + border: 1px solid #000; + border-radius: 50%; + box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.55); +} + +.chat-input-mention-popover-list { + max-height: 220px; + overflow-y: auto; + padding: 4px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.chat-input-mention-row { + position: relative; + display: flex; + align-items: center; + gap: 8px; + padding: 4px 6px; + border-radius: 6px; + border: 1px solid transparent; + cursor: pointer; + background: transparent; + transition: background 0.08s linear, border-color 0.08s linear; +} + +.chat-input-mention-row:hover { + background: rgba(48, 114, 140, 0.12); +} + +.chat-input-mention-row.is-selected { + background: #30728c; + border-color: #000; + color: #fff; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.45); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18); +} + +.chat-input-mention-row-tile { + position: relative; + width: 36px; + height: 36px; + flex-shrink: 0; + border-radius: 6px; + border: 1px solid #000; + background: #cfcfc4; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6), inset 0 -1px 0 rgba(0, 0, 0, 0.12); + overflow: hidden; +} + +.chat-input-mention-row.is-selected .chat-input-mention-row-tile { + background: #1f5a72; +} + +.chat-input-mention-row-tile.is-alias { + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(180deg, #ffdc4c 0%, #f0a91c 100%); + color: #4a2b00; + font-weight: 700; + font-size: 16px; + text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.45); +} + +.chat-input-mention-row.is-selected .chat-input-mention-row-tile.is-alias { + background: linear-gradient(180deg, #ffe97a 0%, #f9bd44 100%); +} + +.chat-input-mention-row-tile .avatar-image { + position: absolute !important; + inset: 0 !important; + width: 100% !important; + height: 100% !important; + left: 0 !important; + background-repeat: no-repeat; + background-position: -22px -32px; + background-size: auto; +} + +.chat-input-mention-row-body { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1 1 auto; + gap: 0; +} + +.chat-input-mention-row-name { + font-family: UbuntuCondensed, Ubuntu, sans-serif; + font-size: 14px; + line-height: 1.1; + color: #2c2c2c; + font-weight: 700; + letter-spacing: 0.2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-input-mention-row.is-selected .chat-input-mention-row-name { + color: #fff; +} + +.chat-input-mention-row-desc { + font-size: 10px; + line-height: 1.1; + color: #6b6b6b; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-input-mention-row.is-selected .chat-input-mention-row-desc { + color: #d6e6ee; +} + +.chat-input-mention-row-kind { + font-size: 9px; + line-height: 1; + padding: 2px 5px; + border-radius: 8px; + border: 1px solid #000; + background: #cfe6ef; + color: #1c3d4c; + text-transform: uppercase; + letter-spacing: 0.5px; + flex-shrink: 0; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6); +} + +.chat-input-mention-row-kind.is-alias { + background: #ffe97a; + color: #5a3a00; +} + +.chat-input-mention-row.is-selected .chat-input-mention-row-kind { + background: #ffdc4c; + color: #4a2b00; +} + +/* Habbo-style scrollbar, matching NitroCardContentView scroll chrome. */ +.chat-input-mention-popover-list::-webkit-scrollbar { + width: 8px; +} + +.chat-input-mention-popover-list::-webkit-scrollbar-track { + background: #d8d8cf; + border-left: 1px solid #000; +} + +.chat-input-mention-popover-list::-webkit-scrollbar-thumb { + background: #30728c; + border: 1px solid #000; + border-radius: 3px; +} + +.chat-input-mention-popover-list::-webkit-scrollbar-thumb:hover { + background: #3c88a6; +} + +/* ============================================================================ + :command popover - same Habbo NitroCard chrome as the @-mention picker. + Header is green to distinguish it visually from the mention popover, + which uses the standard habbo-blue. + ============================================================================ */ + +.chat-input-command-popover { + position: absolute; + bottom: 100%; + left: 0; + width: 100%; + margin-bottom: 4px; + background: #f2f2eb; + border: 2px solid #000; + border-radius: 10px; + box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28); + overflow: hidden; + z-index: 1070; + font-family: Volter, Volter_Goldfish, "Ubuntu", sans-serif; + image-rendering: pixelated; +} + +.chat-input-command-popover-header { + height: 24px; + padding: 0 8px; + background: #2f8d4a; + border-bottom: 2px solid #000; + color: #fff; + text-shadow: 1px 1px 1px #000; + font-family: UbuntuCondensed, Ubuntu, sans-serif; + font-size: 13px; + line-height: 22px; + letter-spacing: 0.3px; + display: flex; + align-items: center; + gap: 6px; +} + +.chat-input-command-popover-header-dot { + width: 8px; + height: 8px; + background: #ffdc4c; + border: 1px solid #000; + border-radius: 50%; + box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.55); +} + +.chat-input-command-popover-list { + max-height: 220px; + overflow-y: auto; + padding: 4px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.chat-input-command-row { + position: relative; + display: flex; + align-items: center; + gap: 8px; + padding: 4px 6px; + border-radius: 6px; + border: 1px solid transparent; + cursor: pointer; + background: transparent; + transition: background 0.08s linear, border-color 0.08s linear; +} + +.chat-input-command-row:hover { + background: rgba(47, 141, 74, 0.12); +} + +.chat-input-command-row.is-selected { + background: #2f8d4a; + border-color: #000; + color: #fff; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.45); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18); +} + +.chat-input-command-row-tile { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + border-radius: 6px; + border: 1px solid #000; + background: linear-gradient(180deg, #b6e6c4 0%, #5ec07d 100%); + color: #1a4a28; + font-weight: 700; + font-size: 14px; + font-family: UbuntuCondensed, Ubuntu, sans-serif; + text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.45); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6), inset 0 -1px 0 rgba(0, 0, 0, 0.12); +} + +.chat-input-command-row.is-selected .chat-input-command-row-tile { + background: linear-gradient(180deg, #c8f0d5 0%, #7ed79a 100%); +} + +.chat-input-command-row-body { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1 1 auto; + gap: 0; +} + +.chat-input-command-row-name { + font-family: UbuntuCondensed, Ubuntu, sans-serif; + font-size: 14px; + line-height: 1.1; + color: #2c2c2c; + font-weight: 700; + letter-spacing: 0.2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-input-command-row.is-selected .chat-input-command-row-name { + color: #fff; +} + +.chat-input-command-row-desc { + font-size: 10px; + line-height: 1.1; + color: #6b6b6b; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-input-command-row.is-selected .chat-input-command-row-desc { + color: #d9efde; +} + +.chat-input-command-popover-list::-webkit-scrollbar { + width: 8px; +} + +.chat-input-command-popover-list::-webkit-scrollbar-track { + background: #d8d8cf; + border-left: 1px solid #000; +} + +.chat-input-command-popover-list::-webkit-scrollbar-thumb { + background: #2f8d4a; + border: 1px solid #000; + border-radius: 3px; +} + +.chat-input-command-popover-list::-webkit-scrollbar-thumb:hover { + background: #3aa55b; +} diff --git a/src/css/chat/Chats.css b/src/css/chat/Chats.css index 8dd0096..43f093f 100644 --- a/src/css/chat/Chats.css +++ b/src/css/chat/Chats.css @@ -807,435 +807,6 @@ } } - &.bubble-253 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_253.png'); - border-image-slice: 16 22 15 27 fill; - border-image-width: 16px 22px 15px 27px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_253_pointer.png'); - } - } - - &.bubble-254 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_254.png'); - border-image-slice: 7 28 15 25 fill; - border-image-width: 7px 28px 15px 25px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_254_pointer.png'); - } - } - - &.bubble-255 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_255.png'); - border-image-slice: 12 19 22 30 fill; - border-image-width: 12px 19px 22px 30px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_255_pointer.png'); - } - } - - &.bubble-256 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_256.png'); - border-image-slice: 24 18 10 31 fill; - border-image-width: 24px 18px 10px 31px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_256_pointer.png'); - } - } - - &.bubble-257 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_257.png'); - border-image-slice: 6 17 19 36 fill; - border-image-width: 6px 17px 19px 36px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_257_pointer.png'); - } - } - - &.bubble-258 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_258.png'); - border-image-slice: 22 27 10 27 fill; - border-image-width: 22px 27px 10px 27px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_258_pointer.png'); - } - } - - &.bubble-259 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_259.png'); - border-image-slice: 21 27 18 37 fill; - border-image-width: 21px 27px 18px 37px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_259_pointer.png'); - } - } - - &.bubble-260 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_260.png'); - border-image-slice: 6 22 16 27 fill; - border-image-width: 6px 22px 16px 27px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_260_pointer.png'); - } - } - - &.bubble-261 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_261.png'); - border-image-slice: 18 27 5 22 fill; - border-image-width: 18px 27px 5px 22px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_261_pointer.png'); - } - } - - &.bubble-262 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_262.png'); - border-image-slice: 33 31 11 34 fill; - border-image-width: 33px 31px 11px 34px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_262_pointer.png'); - } - } - - &.bubble-263 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_263.png'); - border-image-slice: 15 19 10 32 fill; - border-image-width: 15px 19px 10px 32px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_263_pointer.png'); - } - } - - &.bubble-264 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_264.png'); - border-image-slice: 18 24 16 25 fill; - border-image-width: 18px 24px 16px 25px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_264_pointer.png'); - } - } - - &.bubble-265 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_265.png'); - border-image-slice: 41 40 17 18 fill; - border-image-width: 41px 40px 17px 18px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_265_pointer.png'); - } - } - - &.bubble-266 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_266.png'); - border-image-slice: 13 34 22 27 fill; - border-image-width: 13px 34px 22px 27px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_266_pointer.png'); - } - } - - &.bubble-267 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_267.png'); - border-image-slice: 17 30 22 25 fill; - border-image-width: 17px 30px 22px 25px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_267_pointer.png'); - } - } - - &.bubble-268 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_268.png'); - border-image-slice: 7 30 21 24 fill; - border-image-width: 7px 30px 21px 24px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_268_pointer.png'); - } - } - - &.bubble-269 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_269.png'); - border-image-slice: 10 23 25 35 fill; - border-image-width: 10px 23px 25px 35px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_269_pointer.png'); - } - } - - &.bubble-270 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_270.png'); - border-image-slice: 13 30 14 26 fill; - border-image-width: 13px 30px 14px 26px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_270_pointer.png'); - } - } - - &.bubble-271 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_271.png'); - border-image-slice: 23 23 9 35 fill; - border-image-width: 23px 23px 9px 35px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_271_pointer.png'); - } - } - - &.bubble-272 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_272.png'); - border-image-slice: 9 31 24 25 fill; - border-image-width: 9px 31px 24px 25px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_272_pointer.png'); - } - } - - &.bubble-273 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_273.png'); - border-image-slice: 11 16 25 37 fill; - border-image-width: 11px 16px 25px 37px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_273_pointer.png'); - } - } - - &.bubble-274 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_274.png'); - border-image-slice: 7 22 19 27 fill; - border-image-width: 7px 22px 19px 27px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_274_pointer.png'); - } - } - - &.bubble-275 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_275.png'); - border-image-slice: 8 23 14 26 fill; - border-image-width: 8px 23px 14px 26px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_275_pointer.png'); - } - } - - &.bubble-276 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_276.png'); - border-image-slice: 12 40 17 17 fill; - border-image-width: 12px 40px 17px 17px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_276_pointer.png'); - } - } - - &.bubble-277 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_277.png'); - border-image-slice: 6 39 18 17 fill; - border-image-width: 6px 39px 18px 17px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_277_pointer.png'); - } - } - - &.bubble-278 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_278.png'); - border-image-slice: 16 38 6 19 fill; - border-image-width: 16px 38px 6px 19px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_278_pointer.png'); - } - } - - &.bubble-279 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_279.png'); - border-image-slice: 6 26 16 23 fill; - border-image-width: 6px 26px 16px 23px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_279_pointer.png'); - } - } - - &.bubble-280 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_280.png'); - border-image-slice: 23 29 6 15 fill; - border-image-width: 23px 29px 6px 15px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_280_pointer.png'); - } - } - - &.bubble-281 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_281.png'); - border-image-slice: 18 42 9 18 fill; - border-image-width: 18px 42px 9px 18px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_281_pointer.png'); - } - } - - &.bubble-282 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_282.png'); - border-image-slice: 18 42 9 18 fill; - border-image-width: 18px 42px 9px 18px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_282_pointer.png'); - } - } - - &.bubble-283 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_283.png'); - border-image-slice: 17 26 13 31 fill; - border-image-width: 17px 26px 13px 31px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_283_pointer.png'); - } - } - - &.bubble-284 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_284.png'); - border-image-slice: 9 26 23 26 fill; - border-image-width: 9px 26px 23px 26px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_284_pointer.png'); - } - } - - &.bubble-285 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_285.png'); - border-image-slice: 16 35 15 15 fill; - border-image-width: 16px 35px 15px 15px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_285_pointer.png'); - } - } - - &.bubble-286 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_286.png'); - border-image-slice: 18 22 4 23 fill; - border-image-width: 18px 22px 4px 23px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_286_pointer.png'); - } - } - - &.bubble-287 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_287.png'); - border-image-slice: 6 22 18 26 fill; - border-image-width: 6px 22px 18px 26px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_287_pointer.png'); - } - } - - &.bubble-288 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_288.png'); - border-image-slice: 18 31 11 24 fill; - border-image-width: 18px 31px 11px 24px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_288_pointer.png'); - } - } - - &.bubble-289 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_289.png'); - border-image-slice: 7 54 17 24 fill; - border-image-width: 7px 54px 17px 24px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_289_pointer.png'); - } - } - - &.bubble-290 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_290.png'); - border-image-slice: 18 24 14 29 fill; - border-image-width: 18px 24px 14px 29px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_290_pointer.png'); - } - } - - &.bubble-291 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_291.png'); - border-image-slice: 9 26 11 35 fill; - border-image-width: 9px 26px 11px 35px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_291_pointer.png'); - } - } - &.bubble-200, &.bubble-201, &.bubble-202, @@ -2239,169 +1810,4 @@ background: center / contain no-repeat url('@/assets/images/chat/chatbubbles/bubble_252_extra.png'); } } - &.bubble-253 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_253.png'); - } - - &.bubble-254 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_254.png'); - } - - &.bubble-255 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_255.png'); - } - - &.bubble-256 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_256.png'); - } - - &.bubble-257 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_257.png'); - } - - &.bubble-258 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_258.png'); - } - - &.bubble-259 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_259.png'); - } - - &.bubble-260 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_260.png'); - } - - &.bubble-261 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_261.png'); - } - - &.bubble-262 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_262.png'); - } - - &.bubble-263 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_263.png'); - } - - &.bubble-264 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_264.png'); - } - - &.bubble-265 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_265.png'); - } - - &.bubble-266 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_266.png'); - } - - &.bubble-267 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_267.png'); - } - - &.bubble-268 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_268.png'); - } - - &.bubble-269 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_269.png'); - } - - &.bubble-270 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_270.png'); - } - - &.bubble-271 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_271.png'); - } - - &.bubble-272 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_272.png'); - } - - &.bubble-273 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_273.png'); - } - - &.bubble-274 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_274.png'); - } - - &.bubble-275 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_275.png'); - } - - &.bubble-276 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_276.png'); - } - - &.bubble-277 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_277.png'); - } - - &.bubble-278 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_278.png'); - } - - &.bubble-279 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_279.png'); - } - - &.bubble-280 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_280.png'); - } - - &.bubble-281 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_281.png'); - } - - &.bubble-282 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_282.png'); - } - - &.bubble-283 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_283.png'); - } - - &.bubble-284 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_284.png'); - } - - &.bubble-285 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_285.png'); - } - - &.bubble-286 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_286.png'); - } - - &.bubble-287 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_287.png'); - } - - &.bubble-288 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_288.png'); - } - - &.bubble-289 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_289.png'); - } - - &.bubble-290 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_290.png'); - } - - &.bubble-291 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_291.png'); - } - -} - -/* Mention highlight inside chat bubbles (cosmetic) */ -.mention-highlight { - font-weight: 700; - color: #1e7295; - background-color: rgba(30, 114, 149, 0.16); - border-radius: 3px; - padding: 0 2px; } diff --git a/src/css/common/Buttons.css b/src/css/common/Buttons.css index 106a3cc..7188a50 100644 --- a/src/css/common/Buttons.css +++ b/src/css/common/Buttons.css @@ -1,3 +1,70 @@ +.nitro-swf-button { + min-height: 22px !important; + height: 22px; + padding: 2px 10px !important; + border: 3px solid transparent !important; + border-radius: 0 !important; + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_default_9x22.png") !important; + border-image-slice: 3 3 3 3 fill !important; + border-image-width: 3px !important; + border-image-repeat: stretch !important; + background: transparent !important; + background-color: transparent !important; + background-image: none !important; + box-shadow: none !important; + color: #222 !important; + font-size: 11px !important; + font-weight: 700 !important; + line-height: 16px !important; + text-shadow: 0 1px 0 rgba(255,255,255,.75) !important; + transition: none !important; +} + +.nitro-swf-button:hover { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_hover_9x22.png") !important; + background: transparent !important; + background-color: transparent !important; +} + +.nitro-swf-button:active, +.nitro-swf-button.active { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_pressed_9x22.png") !important; + background: transparent !important; + background-color: transparent !important; +} + +.nitro-swf-button.pointer-events-none, +.nitro-swf-button:disabled { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_disabled_9x22.png") !important; + color: #888 !important; + opacity: 1 !important; +} + +.nitro-swf-button-success { + height: 24px; + min-height: 24px !important; + border: 6px solid transparent !important; + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_24x24.png") !important; + border-image-slice: 6 6 6 6 fill !important; + border-image-width: 6px !important; + color: #fff !important; + text-shadow: 0 1px 0 rgba(0,0,0,.55) !important; +} + +.nitro-swf-button-success:hover { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_hover_24x24.png") !important; +} + +.nitro-swf-button-success:active, +.nitro-swf-button-success.active { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_pressed_24x24.png") !important; +} + +.nitro-swf-button-success.pointer-events-none, +.nitro-swf-button-success:disabled { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_disabled_24x24.png") !important; +} + .btn-sm { min-height: 28px; } diff --git a/src/css/friends/FriendsView.css b/src/css/friends/FriendsView.css index 33a625d..43be89c 100644 --- a/src/css/friends/FriendsView.css +++ b/src/css/friends/FriendsView.css @@ -1,3 +1,4 @@ +/* ── Friends spritesheet icons ── */ .nitro-friends-spritesheet { background: url('@/assets/images/friends/friends-spritesheet.png') transparent no-repeat; @@ -138,8 +139,8 @@ & .nitro-card-accordion-set-content, & .nitro-card-content-shell { - scrollbar-width: thin; - scrollbar-color: #6d7b84 #cdd4d8; + scrollbar-width: auto; + scrollbar-color: auto; } & .nitro-card-accordion-set-content::-webkit-scrollbar, @@ -175,12 +176,9 @@ border: 0 !important; } - & .nitro-card-accordion-set-header span, - & .nitro-card-accordion-set-header > div { - font-size: 12px !important; - font-weight: 700; + & .nitro-card-accordion-set-header span { + font-size: 12px; color: #111 !important; - line-height: 1.1; } & .nitro-card-accordion-set-header .fa-icon { @@ -463,8 +461,8 @@ & .nitro-card-content-shell, & .chat-messages { - scrollbar-width: thin; - scrollbar-color: #6d7b84 #cdd4d8; + scrollbar-width: auto; + scrollbar-color: auto; } & .nitro-card-content-shell::-webkit-scrollbar, @@ -802,29 +800,3 @@ } } } - -.nitro-friends .friends-list-avatar { - position: relative !important; - width: 32px; - height: 36px; - flex-shrink: 0; - overflow: hidden; -} - -.nitro-friends .friends-list-avatar .avatar-image { - position: absolute !important; - inset: 0 !important; - width: 100% !important; - height: 100% !important; - margin: 0 !important; - background-size: 66px auto !important; - background-position: -16px -21px !important; - transform: none !important; -} - -.nitro-friends .nitro-card-accordion-set-header > div, -.nitro-friends .nitro-card-accordion-set-header span { - font-size: 12px !important; - font-weight: 700 !important; - line-height: 1.15 !important; -} diff --git a/src/css/habbo/HabboSwfSkin.css b/src/css/habbo/HabboSwfSkin.css new file mode 100644 index 0000000..7b2d7d9 --- /dev/null +++ b/src/css/habbo/HabboSwfSkin.css @@ -0,0 +1,168 @@ +.habbo-swf-window { + --habbo-swf-ubuntu: url("../../assets/images/catalog/swf/habbo_skin_ubuntu.png"); + --habbo-swf-blue: url("../../assets/images/catalog/swf/skins/habbo_skin_blue.png"); + --habbo-swf-bg: #ecece4; + --habbo-swf-panel: #f7f7f2; + --habbo-swf-border: #9d9d96; + --habbo-swf-header: #2f8097; + --habbo-swf-header-dark: #1a5364; + --habbo-swf-text: #111; + --habbo-swf-close: url("../../assets/images/catalog/buttons/close.png"); + --habbo-swf-close-hover: url("../../assets/images/catalog/buttons/close_hover.png"); + --habbo-swf-close-pressed: url("../../assets/images/catalog/buttons/close_pressed.png"); + --habbo-swf-button: url("../../assets/images/catalog/buttons/btn_secondary.png"); + --habbo-swf-button-hover: url("../../assets/images/catalog/buttons/btn_secondary_hover.png"); + --habbo-swf-button-pressed: url("../../assets/images/catalog/buttons/btn_secondary_pressed.png"); + --habbo-swf-button-disabled: url("../../assets/images/catalog/buttons/btn_secondary_disabled.png"); + --habbo-swf-button-green: url("../../assets/images/catalog/buttons/buy.png"); + color: var(--habbo-swf-text) !important; + background: var(--habbo-swf-bg) !important; + border: 1px solid #000 !important; + border-radius: 7px 7px 0 0 !important; + font-family: Ubuntu, Arial, sans-serif !important; + image-rendering: pixelated; +} + +.habbo-swf-window, +.habbo-swf-window * { + box-sizing: border-box; + image-rendering: pixelated; +} + +.habbo-swf-window .nitro-card-header-shell, +.habbo-swf-window .nitro-card-header { + min-height: 35px !important; + max-height: 35px !important; + height: 35px !important; + background: var(--habbo-swf-header) !important; + border: 0 !important; + border-bottom: 1px solid #000 !important; + border-radius: 6px 6px 0 0 !important; +} + +.habbo-swf-window .nitro-card-title { + color: #fff !important; + font-family: Ubuntu, Arial, sans-serif !important; + font-size: 16px !important; + font-weight: 700 !important; + line-height: 35px !important; + text-align: center !important; + text-shadow: 0 1px 0 #000 !important; +} + +.habbo-swf-window .nitro-card-close-button { + top: 7px !important; + right: 7px !important; + width: 19px !important; + min-width: 19px !important; + height: 20px !important; + min-height: 20px !important; + padding: 0 !important; + border: 0 !important; + border-radius: 0 !important; + background-color: transparent !important; + /* Direct URL instead of var(--habbo-swf-close) - some browser / + bundler combinations don't resolve relative url()s inside CSS + custom properties consistently (they're spec'd to resolve from + the document, not the stylesheet). Inlining the path makes + this immune. */ + background-image: url("../../assets/images/catalog/buttons/close.png") !important; + background-repeat: no-repeat !important; + background-position: center !important; + background-size: 19px 20px !important; + box-shadow: none !important; + image-rendering: pixelated !important; + opacity: 1 !important; + visibility: visible !important; + display: block !important; +} + +.habbo-swf-window .nitro-card-close-button:hover { + background-image: url("../../assets/images/catalog/buttons/close_hover.png") !important; +} + +.habbo-swf-window .nitro-card-close-button:active { + background-image: url("../../assets/images/catalog/buttons/close_pressed.png") !important; +} + +.habbo-swf-window .nitro-card-close-button::before, +.habbo-swf-window .nitro-card-close-button::after { + display: none !important; +} + +.habbo-swf-window .nitro-card-content-shell, +.habbo-swf-window .nitro-card-content { + background: var(--habbo-swf-bg) !important; + color: var(--habbo-swf-text) !important; + border-radius: 0 !important; +} + +.habbo-swf-window button, +.habbo-swf-window .btn, +.habbo-swf-window .nitro-swf-button { + min-height: 22px !important; + border: 4px solid transparent !important; + border-radius: 0 !important; + border-image-source: var(--habbo-swf-button) !important; + border-image-slice: 4 4 4 4 fill !important; + border-image-width: 4px !important; + border-image-repeat: stretch !important; + background: transparent !important; + box-shadow: none !important; + color: #111 !important; + font-family: Ubuntu, Arial, sans-serif !important; + font-size: 12px !important; + font-weight: 700 !important; + line-height: 14px !important; +} + +.habbo-swf-window button:hover, +.habbo-swf-window .btn:hover, +.habbo-swf-window .nitro-swf-button:hover { + border-image-source: var(--habbo-swf-button-hover) !important; +} + +.habbo-swf-window button:active, +.habbo-swf-window .btn:active, +.habbo-swf-window .nitro-swf-button:active { + border-image-source: var(--habbo-swf-button-pressed) !important; +} + +.habbo-swf-window button:disabled, +.habbo-swf-window .btn:disabled, +.habbo-swf-window .nitro-swf-button:disabled { + border-image-source: var(--habbo-swf-button-disabled) !important; + color: #8d8d87 !important; +} + +.habbo-swf-window .btn-success, +.habbo-swf-window button[class*="success"], +.habbo-swf-window .nitro-swf-button-success { + border-image-source: var(--habbo-swf-button-green) !important; + color: #fff !important; + text-shadow: 0 1px 0 #004b00 !important; +} + +.habbo-swf-window input, +.habbo-swf-window textarea, +.habbo-swf-window select { + min-height: 22px !important; + border: 1px solid #b7b7ae !important; + border-radius: 3px !important; + background: #fff !important; + color: #333 !important; + box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.08) !important; + font-family: Ubuntu, Arial, sans-serif !important; + font-size: 12px !important; +} + +.habbo-swf-window .nitro-card-tabs-shell, +.habbo-swf-window .nitro-catalog-classic-tabs-shell { + background: var(--habbo-swf-bg) !important; + border-bottom: 1px solid #000 !important; +} + +.habbo-swf-window .nitro-card-tab-item { + color: #111 !important; + text-shadow: none !important; +} diff --git a/src/css/icons/icons.css b/src/css/icons/icons.css index 7245da9..17a903f 100644 --- a/src/css/icons/icons.css +++ b/src/css/icons/icons.css @@ -229,13 +229,6 @@ height: 32px; } -.nitro-icon.icon-mentions { - background-image: url("@/assets/images/toolbar/icons/mentions.png"); - background-size: contain; - width: 36px; - height: 32px; -} - .nitro-icon.icon-wired-tools { background-image: url("@/assets/images/wiredtools/wired_menu.png"); background-size: contain; diff --git a/src/css/index.css b/src/css/index.css index 8be4f78..46f219d 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -51,8 +51,8 @@ body { -webkit-user-select: none; user-select: none; overscroll-behavior: none; - scrollbar-width: thin; - scrollbar-color: #6d7b84 #c8d0d4; + scrollbar-width: auto; + scrollbar-color: auto; } #root { @@ -115,77 +115,98 @@ body { @apply outline-0; } -::-webkit-scrollbar { - width: .875rem; +*::-webkit-scrollbar { + width: 17px !important; + height: 17px !important; } -::-webkit-scrollbar:horizontal { - height: .875rem; +*::-webkit-scrollbar:horizontal { + height: 17px !important; } -::-webkit-scrollbar:not(:horizontal) { - width: .875rem; +*::-webkit-scrollbar:not(:horizontal) { + width: 17px !important; } -::-webkit-scrollbar-track { - background: linear-gradient(180deg, #dfe5e8 0%, #c9d1d5 100%); - border-left: 1px solid #7a858b; - border-right: 1px solid #eef3f5; - border-radius: 0; +/* App-wide Habbo scrollbar (sprites cropped from catalog_skin1.png). + Thumb sprite is 17x34 with caps + grip baked in; stretched full + height via background-size: 17px 100%. Arrow buttons are natural + 17x16 sprites. */ + +*::-webkit-scrollbar-track { + background-color: #e7e5d8 !important; + background-image: none !important; + box-shadow: inset 1px 0 0 #b9b6a5, inset -1px 0 0 #ffffff !important; + border: 0 !important; + border-radius: 0 !important; } -::-webkit-scrollbar-thumb { - background: linear-gradient(180deg, #8fb5c7 0%, #5d8ea5 100%); - border: 1px solid #446879; - border-radius: 2px; - box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.28); +*::-webkit-scrollbar-thumb { + min-height: 24px !important; + background-color: transparent !important; + background-image: url("../assets/images/catalog/scrollbar/scroll_v_thumb.png") !important; + background-repeat: no-repeat !important; + background-position: center center !important; + background-size: 17px 100% !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + image-rendering: pixelated !important; } -::-webkit-scrollbar-thumb:hover { - background: linear-gradient(180deg, #99c2d5 0%, #689ab0 100%); +*::-webkit-scrollbar-thumb:hover, +*::-webkit-scrollbar-thumb:active { + background-image: url("../assets/images/catalog/scrollbar/scroll_v_thumb_pressed.png") !important; } -::-webkit-scrollbar-thumb:active { - background: linear-gradient(180deg, #5c889d 0%, #436977 100%); +*::-webkit-scrollbar-corner { + background: transparent !important; } -::-webkit-scrollbar-corner { - background: #c9d1d5; +*::-webkit-scrollbar-button:single-button { + display: block !important; + width: 17px !important; + height: 16px !important; + /* Cream fill so the arrow sprite's transparent rounded corners + paint over the track colour, not whatever is behind the + scrollbar (which can render black). */ + background-color: #e7e5d8 !important; + background-repeat: no-repeat !important; + background-position: center !important; + border: 0 !important; + image-rendering: pixelated !important; } -::-webkit-scrollbar-button:single-button { - display: block; - width: .875rem; - height: .875rem; - background-color: #d8dfe3; - background-repeat: no-repeat; - background-position: center; - border-left: 1px solid #7a858b; - border-right: 1px solid #eef3f5; +*::-webkit-scrollbar-button:single-button:vertical:decrement { + background-image: url("../assets/images/catalog/scrollbar/scroll_v_up.png") !important; +} +*::-webkit-scrollbar-button:single-button:vertical:decrement:active { + background-image: url("../assets/images/catalog/scrollbar/scroll_v_up_pressed.png") !important; } -::-webkit-scrollbar-button:single-button:vertical:decrement { - background-image: linear-gradient(135deg, transparent 50%, #35586a 50%), linear-gradient(225deg, transparent 50%, #35586a 50%); - background-size: 6px 6px; - background-position: calc(50% - 3px) 55%, calc(50% + 3px) 55%; +*::-webkit-scrollbar-button:single-button:vertical:increment { + background-image: url("../assets/images/catalog/scrollbar/scroll_v_down.png") !important; +} +*::-webkit-scrollbar-button:single-button:vertical:increment:active { + background-image: url("../assets/images/catalog/scrollbar/scroll_v_down_pressed.png") !important; } -::-webkit-scrollbar-button:single-button:vertical:increment { - background-image: linear-gradient(315deg, transparent 50%, #35586a 50%), linear-gradient(45deg, transparent 50%, #35586a 50%); - background-size: 6px 6px; - background-position: calc(50% - 3px) 45%, calc(50% + 3px) 45%; +*::-webkit-scrollbar-button:single-button:horizontal:decrement { + width: 16px !important; + height: 17px !important; + background-image: url("../assets/images/catalog/scrollbar/scroll_h_left.png") !important; +} +*::-webkit-scrollbar-button:single-button:horizontal:decrement:active { + background-image: url("../assets/images/catalog/scrollbar/scroll_h_left_pressed.png") !important; } -::-webkit-scrollbar-button:single-button:horizontal:decrement { - background-image: linear-gradient(45deg, transparent 50%, #35586a 50%), linear-gradient(135deg, transparent 50%, #35586a 50%); - background-size: 6px 6px; - background-position: 58% calc(50% - 3px), 58% calc(50% + 3px); +*::-webkit-scrollbar-button:single-button:horizontal:increment { + width: 16px !important; + height: 17px !important; + background-image: url("../assets/images/catalog/scrollbar/scroll_h_right.png") !important; } - -::-webkit-scrollbar-button:single-button:horizontal:increment { - background-image: linear-gradient(225deg, transparent 50%, #35586a 50%), linear-gradient(315deg, transparent 50%, #35586a 50%); - background-size: 6px 6px; - background-position: 42% calc(50% - 3px), 42% calc(50% + 3px); +*::-webkit-scrollbar-button:single-button:horizontal:increment:active { + background-image: url("../assets/images/catalog/scrollbar/scroll_h_right_pressed.png") !important; } @layer components { @@ -306,7 +327,7 @@ body { .nitro-card-shell:not(.nitro-wired) .nitro-card-header-shell { border: 2px solid #3c88a6; - border-bottom-color: #30728c; + border-bottom-color: #000; border-radius: 8px 8px 0 0; background: #30728c; padding: 5px; @@ -314,7 +335,7 @@ body { .nitro-card-shell:not(.nitro-wired) .nitro-card-header-shell.builders-club-card-header { border-color: #d79d2e; - border-bottom-color: #c68515; + border-bottom-color: #000; background: linear-gradient(180deg, #d89f2d 0%, #c68515 100%); } diff --git a/src/css/login/LoginView.css b/src/css/login/LoginView.css index fae3328..af2263d 100644 --- a/src/css/login/LoginView.css +++ b/src/css/login/LoginView.css @@ -1190,7 +1190,7 @@ overflow-x: hidden; padding-right: 4px; margin-top: 2px; - scrollbar-width: thin; + scrollbar-width: auto; } .nitro-login-card .room-template-option { diff --git a/src/css/navigator/HabboNavigatorDesktop.css b/src/css/navigator/HabboNavigatorDesktop.css new file mode 100644 index 0000000..dd75c44 --- /dev/null +++ b/src/css/navigator/HabboNavigatorDesktop.css @@ -0,0 +1,242 @@ +.habbo-navigator-desktop { + border: 1px solid #000; + border-radius: 7px; + background: #e9e9e1; + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.35); + color: #111; + font-family: Ubuntu, Arial, sans-serif; +} + +.habbo-navigator-desktop .nitro-card-header-shell { + min-height: 32px; + max-height: 32px; + background: #418db0; + border-bottom: 1px solid #000; + border-radius: 6px 6px 0 0; +} + +.habbo-navigator-desktop .nitro-card-title { + color: #fff; + font-family: UbuntuCondensed, Ubuntu, Arial, sans-serif; + font-size: 18px; + font-weight: 700; + line-height: 1; + text-shadow: 1px 1px 0 #000; +} + +.habbo-navigator-desktop .nitro-card-close-button { + width: 19px; + height: 20px; + right: 6px; + border: 2px solid #000; + border-radius: 4px; + background: #c73a32; + box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.35); +} + +.habbo-navigator-desktop .nitro-card-close-button::before, +.habbo-navigator-desktop .nitro-card-close-button::after { + content: ""; + position: absolute; + width: 10px; + height: 2px; + background: #fff; + box-shadow: 1px 1px 0 #64120f; +} + +.habbo-navigator-desktop .nitro-card-close-button::before { + transform: rotate(45deg); +} + +.habbo-navigator-desktop .nitro-card-close-button::after { + transform: rotate(-45deg); +} + +.habbo-navigator-desktop .nitro-card-tabs-shell { + justify-content: flex-start; + gap: 0; + min-height: 27px; + max-height: 27px; + padding: 4px 8px 0; + background: #e9e9e1; + border-bottom: 1px solid #b8b8ad; +} + +.habbo-navigator-desktop .nitro-card-tab-item { + min-height: 23px; + margin-right: -1px; + padding: 4px 12px 3px; + border: 1px solid #555; + border-bottom: 0; + border-radius: 6px 6px 0 0; + background-color: #d5d8cf; + background-image: url("../../assets/images/navigator/swf/tab_bg_unsel.png"); + background-repeat: repeat-x; + background-size: auto 100%; + color: #111; + font-size: 12px; + font-weight: 400; + line-height: 1; + box-shadow: inset 1px 1px 0 #fff; +} + +.habbo-navigator-desktop .nitro-card-tab-item:hover { + background-color: #e7e8df; + background-image: url("../../assets/images/navigator/swf/tab_bg_hilite.png"); +} + +.habbo-navigator-desktop .nitro-card-tab-item-active { + z-index: 2; + margin-bottom: -1px; + background-color: #f4f4ed; + background-image: url("../../assets/images/navigator/swf/tab_bg_sel.png"); + border-bottom: 1px solid #f4f4ed; + font-weight: 700; +} + +.habbo-navigator-desktop .habbo-navigator-desktop-content { + padding: 8px 9px 9px; + overflow: hidden; + background: #f4f4ed; + border-radius: 0 0 6px 6px; + color: #111; +} + +.habbo-navigator-desktop .habbo-navigator-desktop-content input, +.habbo-navigator-desktop .habbo-navigator-desktop-content select, +.habbo-navigator-desktop .habbo-navigator-desktop-content textarea { + height: 22px; + border: 1px solid #a0a49c; + border-radius: 3px; + background-color: #fff; + background-image: url("../../assets/images/navigator/swf/hdr_search.png"); + background-repeat: repeat-x; + color: #333; + font-size: 12px; + box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.08); +} + +.habbo-navigator-desktop .habbo-navigator-desktop-content button, +.habbo-navigator-desktop .habbo-navigator-desktop-content .btn { + min-height: 22px; + border: 1px solid #3a3a3a; + border-radius: 4px; + background-color: #d6d6d1; + background-image: url("../../assets/images/navigator/swf/button.png"); + background-repeat: repeat-x; + background-size: auto 100%; + color: #111; + font-size: 12px; + font-weight: 700; + box-shadow: inset 1px 1px 0 #fff; +} + +.habbo-navigator-desktop .nitro-card-panel { + border: 1px solid #babdb4; + border-radius: 6px; + background: #efefe8; + box-shadow: inset 1px 1px 0 #fff; + overflow: hidden; +} + +.habbo-navigator-desktop .nitro-card-panel > .flex:first-child { + min-height: 28px; + padding: 5px 8px; + border-bottom: 1px solid #d3d5cd; + background: #efefe8; +} + +.habbo-navigator-desktop .navigator-grid { + padding: 0 4px 5px; + background: #fff; +} + +.habbo-navigator-desktop .navigator-item { + min-height: 28px; + margin: 2px 0; + border: 1px solid transparent; + border-radius: 5px; + background: #f5f5ef; + color: #111; +} + +.habbo-navigator-desktop .navigator-item:nth-child(even) { + background: #e7e8e0; +} + +.habbo-navigator-desktop .navigator-item:hover { + border-color: #777; + background: #fff; +} + +.habbo-navigator-desktop .nitro-navigator-search-saves-result { + width: 155px; + min-width: 155px; + height: 100%; + border: 1px solid #babdb4; + border-radius: 6px; + background: #efefe8; + padding: 4px; +} + +.habbo-navigator-desktop .nitro-navigator-search-saves-result > .flex:first-child { + min-height: 24px; + border: 1px solid #d58e00; + border-radius: 4px; + background: #f8a900; + box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.45); +} + +.habbo-navigator-desktop .nitro-navigator-search-saves-result span { + color: #111; + font-weight: 700; +} + +.habbo-navigator-desktop .nitro-icon.icon-navigator-info { + width: 16px; + height: 16px; +} + +.habbo-navigator-desktop ::-webkit-scrollbar { + width: 17px !important; + height: 17px !important; +} + +.habbo-navigator-desktop ::-webkit-scrollbar-track { + border: 0 !important; + background-color: transparent !important; + background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_track_v_17x2.png") !important; + background-repeat: repeat-y !important; + background-position: center top !important; +} + +.habbo-navigator-desktop ::-webkit-scrollbar-thumb { + min-height: 24px !important; + border: 0 !important; + border-radius: 0 !important; + background-color: transparent !important; + background-image: + url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_grip_7x10.png"), + url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_top_17x2.png"), + url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_bottom_17x2.png"), + url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_mid_17x1.png") !important; + background-repeat: repeat-y, no-repeat, no-repeat, repeat-y !important; + background-position: center center, center top, center bottom, center top !important; + background-size: 7px 10px, 17px 2px, 17px 2px, 17px 1px !important; + box-shadow: none !important; +} + +.habbo-navigator-desktop ::-webkit-scrollbar-button { + width: 17px !important; + height: 16px !important; + background-color: transparent !important; + border: 0 !important; +} + +.habbo-navigator-desktop ::-webkit-scrollbar-button:single-button:vertical:decrement { + background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_up_17x16.png") !important; +} + +.habbo-navigator-desktop ::-webkit-scrollbar-button:single-button:vertical:increment { + background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_down_17x16.png") !important; +} diff --git a/src/hooks/chat-history/useChatHistory.ts b/src/hooks/chat-history/useChatHistory.ts index e935296..c26057a 100644 --- a/src/hooks/chat-history/useChatHistory.ts +++ b/src/hooks/chat-history/useChatHistory.ts @@ -12,6 +12,19 @@ const MESSENGER_HISTORY_MAX = 1000; let CHAT_HISTORY_COUNTER: number = 0; let MESSENGER_HISTORY_COUNTER: number = 0; +/** + * Project a list of chat entries to the slim shape we want to persist in + * localStorage. `imageUrl` is a base64 data URL of the avatar / pet head + * (10-50 KB each) - keeping it in storage blows past the browser quota + * inside minutes in a pet-heavy room. The avatar can always be re-rendered + * from `look` via ChatBubbleUtilities.getUserImage(), and pet images are + * regenerated from the bubble flow when needed; we just don't restore + * head thumbnails for entries loaded from a previous session. + * + * `style` / `chatType` / `color` are kept because they're tiny but + * meaningful for re-rendering the bubble. Translation fields are kept + * because they're already text. + */ const slimChatEntriesForStorage = (entries: IChatEntry[]): IChatEntry[] => entries.map(entry => entry.imageUrl ? { ...entry, imageUrl: undefined } : entry); diff --git a/src/hooks/rooms/widgets/useChatWidget.ts b/src/hooks/rooms/widgets/useChatWidget.ts index ea61c6f..ed0b9ed 100644 --- a/src/hooks/rooms/widgets/useChatWidget.ts +++ b/src/hooks/rooms/widgets/useChatWidget.ts @@ -23,6 +23,11 @@ const useChatWidgetState = () => const { addChatEntry, updateChatEntry } = useChatHistory(); const { settings, translateIncoming, consumeOutgoingTranslation } = useTranslation(); const isDisposed = useRef(false); + // Reactive: re-renders if the session-data snapshot flips (e.g. + // reconnect under a different user id). Safe to call here — + // useChatWidget is NOT wrapped in useBetween (see export below), + // so the real React dispatcher is in scope and + // useSyncExternalStore installs correctly. const ownUserId = (useUserDataSnapshot().userId || -1); const applyTranslationToBubble = useCallback((chatMessage: ChatBubbleMessage, originalText: string, translatedText: string, detectedLanguage: string, targetLanguage: string) => @@ -226,6 +231,12 @@ const useChatWidgetState = () => return newValue; }); + // Pet, Bot and Rentable Bot chat is fire-and-forget ("UDP-style"): + // the live bubble already rendered above, but we deliberately skip + // addChatEntry so the entry never lands in localStorage. A pet-heavy + // room used to push 30+ KB per message (base64 head data URL) into + // the chat history, exhausting the localStorage quota in minutes. + // Real users still go through the full persisted path. const chatEntryId = (userType === RoomObjectType.USER) ? addChatEntry({ id: -1, diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index 1e760c8..13c00cc 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -13,6 +13,7 @@ const isQuotaError = (error: unknown): boolean => if(!error || typeof error !== 'object') return false; const name = (error as { name?: string }).name; if(name === 'QuotaExceededError') return true; + // Firefox legacy: if(name === 'NS_ERROR_DOM_QUOTA_REACHED') return true; return false; }; @@ -27,6 +28,12 @@ const trimArrayForQuota = (value: T): T => interface UseLocalStorageOptions { + /** + * Optional projection applied right before the value is written to + * localStorage. The in-memory React state is unaffected. Use this to + * strip heavy ephemeral fields (e.g. base64 image URLs) that would + * otherwise blow past the storage quota. + */ toStorage?: (value: T) => unknown; } @@ -52,6 +59,7 @@ const useLocalStorageState = (key: string, initialValue: T, options: UseLocal const writeTimerRef = useRef | null>(null); const optionsRef = useRef(options); + // Keep the latest toStorage projection without re-running effects. optionsRef.current = options; const flushWrite = (value: T) => @@ -75,6 +83,8 @@ const useLocalStorageState = (key: string, initialValue: T, options: UseLocal } } + // Quota exceeded - trim and retry once. Anything that isn't an + // array gets cleared, since we have no generic trimming rule. try { const trimmed = trimArrayForQuota(projected as T); @@ -84,10 +94,14 @@ const useLocalStorageState = (key: string, initialValue: T, options: UseLocal catch(retryError) { NitroLogger.error(retryError); - try { window.localStorage.removeItem(key); } catch(_) { } + // Last resort: drop the key entirely so future writes have room. + try { window.localStorage.removeItem(key); } catch(_) { /* ignore */ } } }; + // Debounce: high-frequency chat would otherwise trigger one full + // JSON.stringify + setItem per message. We coalesce bursts into one + // write per STORAGE_WRITE_DEBOUNCE_MS window with the latest value. const scheduleWrite = (value: T) => { pendingWriteRef.current = value; @@ -103,6 +117,8 @@ const useLocalStorageState = (key: string, initialValue: T, options: UseLocal }, STORAGE_WRITE_DEBOUNCE_MS); }; + // Flush a pending write on tab close / hide so we don't lose the last + // burst of activity. useEffect(() => { const flushOnLeave = () => diff --git a/src/index.tsx b/src/index.tsx index d1f1b0f..6fb5997 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,6 +23,7 @@ import './css/catalog/CatalogClassicView.css'; import './css/emustats/EmuStatsView.css'; import './css/chat/Chats.css'; +import './css/chat/ChatInputMentionSelectorView.css'; import './css/mentions/MentionToasts.css'; import './css/common/Buttons.css'; @@ -32,6 +33,8 @@ import './css/forms/form_select.css'; import './css/friends/FriendsView.css'; +import './css/habbo/HabboSwfSkin.css'; + import './css/hotelview/HotelView.css'; import './css/login/LoginView.css'; @@ -43,6 +46,8 @@ import './css/inventory/InventoryView.css'; import './css/layout/LayoutTrophy.css'; +import './css/navigator/HabboNavigatorDesktop.css'; + import './css/nitrocard/NitroCardView.css'; From fff4c0bca6594c020f4e25cdcd35744534ce49d3 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 5 Jun 2026 16:31:59 +0200 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=86=99=20Take=20#3=20desktop=20view?= =?UTF-8?q?=20catalog=20is=20now=20100%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/layout/LayoutRoomPreviewerView.tsx | 52 ++++-- .../layout/CatalogLayoutColorGroupingView.tsx | 101 +++++++----- .../page/layout/pets/CatalogLayoutPetView.tsx | 10 +- .../widgets/CatalogViewProductWidgetView.tsx | 153 ++++++++++-------- src/css/catalog/CatalogClassicView.css | 109 +++++++++++-- src/css/common/Buttons.css | 67 -------- src/css/habbo/HabboSwfSkin.css | 2 - src/index.tsx | 2 - 8 files changed, 283 insertions(+), 213 deletions(-) diff --git a/src/common/layout/LayoutRoomPreviewerView.tsx b/src/common/layout/LayoutRoomPreviewerView.tsx index 4355e40..30deb10 100644 --- a/src/common/layout/LayoutRoomPreviewerView.tsx +++ b/src/common/layout/LayoutRoomPreviewerView.tsx @@ -1,4 +1,4 @@ -import { GetRenderer, GetTicker, NitroTicker, RoomPreviewer, TextureUtils } from '@nitrots/nitro-renderer'; +import { GetRenderer, GetTicker, NitroLogger, NitroTicker, RoomPreviewer, TextureUtils } from '@nitrots/nitro-renderer'; import { FC, MouseEvent, useEffect, useRef } from 'react'; export const LayoutRoomPreviewerView: FC<{ @@ -8,6 +8,13 @@ export const LayoutRoomPreviewerView: FC<{ { const { roomPreviewer = null, height = 0 } = props; const elementRef = useRef(null); + // Latch that disables further renders once Pixi throws inside this + // previewer. The crash (e.g. blackhole furni's filter chain that + // accesses .alphaMode on a null texture) repeats every animation + // frame as long as the ticker keeps firing, flooding the console + // and locking the catalog. One catch and we stop trying for the + // lifetime of this previewer instance. + const renderFailedRef = useRef(false); const onClick = (event: MouseEvent) => { @@ -21,37 +28,58 @@ export const LayoutRoomPreviewerView: FC<{ { if(!elementRef) return; + renderFailedRef.current = false; + const width = elementRef.current.parentElement.clientWidth; const texture = TextureUtils.createRenderTexture(width, height); const paintToDOM = () => { + if(renderFailedRef.current) return; if(!roomPreviewer || !elementRef.current) return; const renderingCanvas = roomPreviewer.getRenderingCanvas(); if(!renderingCanvas) return; - GetRenderer().render({ - target: texture, - container: renderingCanvas.master, - clear: true - }); + try + { + GetRenderer().render({ + target: texture, + container: renderingCanvas.master, + clear: true + }); - const canvas = GetRenderer().texture.generateCanvas(texture); - const base64 = canvas.toDataURL('image/png'); + const canvas = GetRenderer().texture.generateCanvas(texture); + const base64 = canvas.toDataURL('image/png'); - canvas.width = 0; - canvas.height = 0; + canvas.width = 0; + canvas.height = 0; - elementRef.current.style.backgroundImage = `url(${ base64 })`; + elementRef.current.style.backgroundImage = `url(${ base64 })`; + } + catch(error) + { + renderFailedRef.current = true; + NitroLogger.error('LayoutRoomPreviewerView paint failed; disabling further renders for this preview', error); + } }; const update = (ticker: NitroTicker) => { + if(renderFailedRef.current) return; if(!roomPreviewer || !elementRef.current) return; - roomPreviewer.updatePreviewRoomView(); + try + { + roomPreviewer.updatePreviewRoomView(); + } + catch(error) + { + renderFailedRef.current = true; + NitroLogger.error('LayoutRoomPreviewerView update failed; disabling further renders for this preview', error); + return; + } const renderingCanvas = roomPreviewer.getRenderingCanvas(); diff --git a/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx index cb4c53a..0c585bf 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx @@ -1,8 +1,8 @@ import { ColorConverter } from '@nitrots/nitro-renderer'; import { FC, useMemo, useState } from 'react'; -import { FaFillDrip } from 'react-icons/fa'; -import { IPurchasableOffer, SanitizeHtml } from '../../../../../api'; -import { AutoGrid, Button, Column, Grid, LayoutGridItem, Text } from '../../../../../common'; +import { FaExchangeAlt, FaFillDrip, FaSyncAlt } from 'react-icons/fa'; +import { IPurchasableOffer, ProductTypeEnum, SanitizeHtml } from '../../../../../api'; +import { AutoGrid, Button, Column, LayoutGridItem, Text } from '../../../../../common'; import { useCatalogData, useCatalogUiState } from '../../../../../hooks'; import { CatalogGridOfferView } from '../common/CatalogGridOfferView'; import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView'; @@ -22,7 +22,7 @@ export const CatalogLayoutColorGroupingView: FC>(new Map()); - const { currentOffer = null } = useCatalogData(); + const { currentOffer = null, roomPreviewer = null } = useCatalogData(); const { setCurrentOffer = null } = useCatalogUiState(); const [ colorsShowing, setColorsShowing ] = useState(false); @@ -132,46 +132,59 @@ export const CatalogLayoutColorGroupingView: FC - - - { (!colorsShowing || !currentOffer || !colorableItems.has(currentOffer.product.furnitureData.className)) && - offers.map((offer, index) => ) - } - { (colorsShowing && currentOffer && colorableItems.has(currentOffer.product.furnitureData.className)) && - colorableItems.get(currentOffer.product.furnitureData.className).map((color, index) => selectColor(index, currentOffer.product.furnitureData.className) } />) - } - - - - { !currentOffer && - <> - { !!page.localization.getImage(1) && } - - } - { currentOffer && - <> -
- - - { currentOffer.product.furnitureData.hasIndexedColor && - } + + { /* Top: two visible rows of furni tiles. Tile is 70px tall + and the AutoGrid handles its own overflow if there are + more than two rows worth of offers. */ } +
+ { (!colorsShowing || !currentOffer || !colorableItems.has(currentOffer.product.furnitureData.className)) && + + { offers.map((offer, index) => ) } + } + { (colorsShowing && currentOffer && colorableItems.has(currentOffer.product.furnitureData.className)) && +
+ { colorableItems.get(currentOffer.product.furnitureData.className).map((color, index) => selectColor(index, currentOffer.product.furnitureData.className) } />) } +
} +
+ + { /* Bottom: preview pane stacked under the grid. Mirrors the + default-3x3 split (preview on the left, offer info on the + right) so the rotate/state buttons and Buy/Gift actions + sit where the user expects. */ } + { !currentOffer && + + { !!page.localization.getImage(1) && } + + } + { currentOffer && +
+
+ { (currentOffer.product.productType !== ProductTypeEnum.BADGE) && + <> + + + } + + + { currentOffer.product.furnitureData.hasIndexedColor && + } +
+
+ + { currentOffer.localizationName } +
+ +
- - - { currentOffer.localizationName } -
-
- -
- -
- -
- } - - + +
+
} +
); }; diff --git a/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx b/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx index 96dff71..3eba176 100644 --- a/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx +++ b/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx @@ -291,7 +291,7 @@ export const CatalogLayoutPetView: FC = props => { LocalizeText('catalog.pets.back.breeds') } }
-
+
{ !colorsShowing && (sellablePalettes.length > 0) && sellablePalettes.map((palette, index) => ( = props => )) } { colorsShowing && (sellableColors.length > 0) && sellableColors.map((colorSet, index) => ( -
setSelectedColorIndex(index) } /> )) } diff --git a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx index 8beb08e..df4cc24 100644 --- a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx @@ -22,85 +22,98 @@ export const CatalogViewProductWidgetView: FC<{}> = props => roomPreviewer.updateObjectRoom('default', 'default', 'default'); roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); - switch(product.productType) + const populate = () => { - case ProductTypeEnum.FLOOR: { - if(!product.furnitureData) return; + switch(product.productType) + { + case ProductTypeEnum.FLOOR: { + if(!product.furnitureData) return; - const furniData = GetSessionDataManager().getFloorItemData(product.furnitureData.id); - const isPurchasableClothing = (product.furnitureData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET); - const hasResolvableFigureSets = (() => - { - if(!furniData || !furniData.customParams || !furniData.customParams.length) return false; - - const parts = furniData.customParams.split(',').map(value => parseInt(value)); - - for(const part of parts) + const furniData = GetSessionDataManager().getFloorItemData(product.furnitureData.id); + const isPurchasableClothing = (product.furnitureData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET); + const hasResolvableFigureSets = (() => { - if(isNaN(part)) continue; + if(!furniData || !furniData.customParams || !furniData.customParams.length) return false; - if(GetAvatarRenderManager().structureData?.getFigurePartSet(part)) return true; - } + const parts = furniData.customParams.split(',').map(value => parseInt(value)); - return false; - })(); + for(const part of parts) + { + if(isNaN(part)) continue; - if(isPurchasableClothing || hasResolvableFigureSets) - { - const customParts = furniData.customParams.split(',').map(value => parseInt(value)); - const figureSets: number[] = []; + if(GetAvatarRenderManager().structureData?.getFigurePartSet(part)) return true; + } - for(const part of customParts) + return false; + })(); + + if(isPurchasableClothing || hasResolvableFigureSets) { - if(isNaN(part)) continue; + const customParts = furniData.customParams.split(',').map(value => parseInt(value)); + const figureSets: number[] = []; - if(GetAvatarRenderManager().isValidFigureSetForGender(part, GetSessionDataManager().gender)) figureSets.push(part); + for(const part of customParts) + { + if(isNaN(part)) continue; + + if(GetAvatarRenderManager().isValidFigureSetForGender(part, GetSessionDataManager().gender)) figureSets.push(part); + } + + const figureString = BuildPurchasableClothingFigure(GetSessionDataManager().figure, figureSets); + + roomPreviewer.addAvatarIntoRoom(figureString, product.productClassId); } - - const figureString = BuildPurchasableClothingFigure(GetSessionDataManager().figure, figureSets); - - roomPreviewer.addAvatarIntoRoom(figureString, product.productClassId); - } - else - { - roomPreviewer.addFurnitureIntoRoom(product.productClassId, new Vector3d(90), previewStuffData, product.extraParam); - } - return; - } - case ProductTypeEnum.WALL: { - if(!product.furnitureData) return; - - roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); - - switch(product.furnitureData.specialType) - { - case FurniCategory.FLOOR: - roomPreviewer.updateObjectRoom(product.extraParam); - return; - case FurniCategory.WALL_PAPER: - roomPreviewer.updateObjectRoom(null, product.extraParam); - return; - case FurniCategory.LANDSCAPE: { - roomPreviewer.updateObjectRoom(null, null, product.extraParam); - - const furniData = GetSessionDataManager().getWallItemDataByName('window_double_default'); - - if(furniData) roomPreviewer.addWallItemIntoRoom(furniData.id, new Vector3d(90), furniData.customParams); - return; + else + { + roomPreviewer.addFurnitureIntoRoom(product.productClassId, new Vector3d(90), previewStuffData, product.extraParam); } - default: - roomPreviewer.updateObjectRoom('default', 'default', 'default'); - roomPreviewer.addWallItemIntoRoom(product.productClassId, new Vector3d(90), product.extraParam); - return; + return; } + case ProductTypeEnum.WALL: { + if(!product.furnitureData) return; + + roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); + + switch(product.furnitureData.specialType) + { + case FurniCategory.FLOOR: + roomPreviewer.updateObjectRoom(product.extraParam); + return; + case FurniCategory.WALL_PAPER: + roomPreviewer.updateObjectRoom(null, product.extraParam); + return; + case FurniCategory.LANDSCAPE: { + roomPreviewer.updateObjectRoom(null, null, product.extraParam); + + const furniData = GetSessionDataManager().getWallItemDataByName('window_double_default'); + + if(furniData) roomPreviewer.addWallItemIntoRoom(furniData.id, new Vector3d(90), furniData.customParams); + return; + } + default: + roomPreviewer.updateObjectRoom('default', 'default', 'default'); + roomPreviewer.addWallItemIntoRoom(product.productClassId, new Vector3d(90), product.extraParam); + return; + } + } + case ProductTypeEnum.ROBOT: + roomPreviewer.addAvatarIntoRoom(product.extraParam, 0); + return; + case ProductTypeEnum.EFFECT: + roomPreviewer.addAvatarIntoRoom(GetSessionDataManager().figure, product.productClassId); + return; } - case ProductTypeEnum.ROBOT: - roomPreviewer.addAvatarIntoRoom(product.extraParam, 0); - return; - case ProductTypeEnum.EFFECT: - roomPreviewer.addAvatarIntoRoom(GetSessionDataManager().figure, product.productClassId); - return; - } + }; + + populate(); + + // RoomPreviewer.addFurnitureIntoRoom / addAvatarIntoRoom flip + // _automaticStateChange to true, which makes the ticker advance + // the room object's state every AUTOMATIC_STATE_CHANGE_INTERVAL. + // In the catalog we want the preview to sit still until the + // user clicks the state button explicitly - turn it back off + // after populate() runs. + roomPreviewer.setAutomaticStateChange(false); }, [ currentOffer, previewStuffData, roomPreviewer ]); if(!currentOffer) return null; @@ -119,5 +132,11 @@ export const CatalogViewProductWidgetView: FC<{}> = props => ); } - return ; + // Re-mount the previewer whenever the offer changes so the render + // latch / texture handle in LayoutRoomPreviewerView resets cleanly. + // Without this a single broken offer (e.g. blackhole's Pixi filter + // crash) latches the previewer permanently and every following + // offer paints nothing - the singleton roomPreviewer + 240px height + // keep the same component mounted otherwise. + return ; }; diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index 6a1f23e..8c3fb12 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -12,14 +12,6 @@ --catalog-swf-select-outer: #82d1ed; --catalog-swf-bc: #ff8d00; --catalog-swf-bc-outer: #ffb53c; - --habbo-skin-ubuntu: url("../../assets/images/catalog/swf/skins/habbo_skin_ubuntu.png"); - --habbo-skin-blue: url("../../assets/images/catalog/swf/skins/habbo_skin_blue.png"); - --habbo-skin-illumina-light: url("../../assets/images/catalog/swf/skins/habbo_skin_illumina_light.png"); - --habbo-skin-illumina-dark: url("../../assets/images/catalog/swf/skins/habbo_skin_illumina_dark.png"); - --habbo-slice-frame: url("../../assets/images/catalog/swf/ubuntu_frame3_26x55.png"); - --habbo-slice-tab-default: url("../../assets/images/catalog/swf/ubuntu_tab3_default_22x32.png"); - --habbo-slice-tab-selected: url("../../assets/images/catalog/swf/ubuntu_tab3_selected_22x32.png"); - --habbo-slice-tab-hover: url("../../assets/images/catalog/swf/ubuntu_tab3_hover_22x32.png"); /* Light gray secondary button - cropped from catalog_skin1.png at (10, 190, 25x22). Drives the gift button "Cadeau", the preview-room control button and the generic .nitro-catalog-swf- @@ -42,10 +34,6 @@ --habbo-button-green-hover: url("../../assets/images/catalog/buttons/buy_hover.png"); --habbo-button-green-pressed: url("../../assets/images/catalog/buttons/buy_pressed.png"); --habbo-button-green-disabled: url("../../assets/images/catalog/buttons/buy_disabled.png"); - --habbo-grid-default: url("../../assets/images/catalog/swf/habbo_grid.png"); - --habbo-grid-hover: url("../../assets/images/catalog/swf/habbo_grid_hover.png"); - --habbo-grid-selected: url("../../assets/images/catalog/swf/habbo_grid_selected.png"); - --habbo-grid-selected-inactive: url("../../assets/images/catalog/swf/habbo_grid_selected_inactive.png"); --habbo-close: url("../../assets/images/catalog/buttons/close.png"); --habbo-close-hover: url("../../assets/images/catalog/buttons/close_hover.png"); --habbo-close-pressed: url("../../assets/images/catalog/buttons/close_pressed.png"); @@ -782,14 +770,24 @@ .nitro-catalog-classic-offer-preview { position: relative; - width: 360px; - min-width: 360px; height: 100%; padding: 0; overflow: hidden; background: #000; } +/* The default-3x3 layout puts the preview next to .offer-info inside + .offer-panel and needs the 360px column. Scope the pin to that + context so other layouts (color-grouping, etc.) can put the same + preview class inside a flex/grid column and let it track the + container width. Without this scoping the absolute-positioned + rotate/state buttons sit past the column's right edge and get + clipped by overflow: hidden. */ +.nitro-catalog-classic-offer-panel > .nitro-catalog-classic-offer-preview { + width: 360px; + min-width: 360px; +} + .nitro-catalog-classic-preview-title { position: absolute; top: 12px; @@ -892,12 +890,19 @@ min-height: var(--nitro-grid-column-min-height, 70px) !important; border: 0 !important; border-radius: 0 !important; - background-color: transparent !important; background-image: none !important; box-shadow: none !important; overflow: visible !important; } +/* Furni tiles drive their look from the icon image and need a clear + background. Color-grouping swatches use itemHighlight (.has-highlight) + to ask LayoutGridItem for a solid colour via inline backgroundColor - + keep the transparent override off those so the swatch is visible. */ +.nitro-catalog-classic-window .layout-grid-item:not(.has-highlight) { + background-color: transparent !important; +} + .nitro-catalog-classic-window .layout-grid-item:hover { background-image: none !important; box-shadow: inset 0 0 0 1px #a1a19b !important; @@ -911,6 +916,42 @@ inset -2px -2px 0 #ecece4 !important; } +/* Habbo-classic colour swatches: small chip with a 1px dark border + and a subtle inner highlight so light tones still read as buttons. + Hover lifts the border; the selected swatch is "pressed" with a + sunken inner shadow and a bright cyan ring matching the catalog + selection accent. The cream inset from the generic .is-active rule + above would wash out the swatch colour, so we replace it here. */ +.nitro-catalog-classic-window .layout-grid-item.has-highlight { + width: 26px !important; + height: 26px !important; + min-width: 26px !important; + min-height: 26px !important; + margin: 1px !important; + border: 1px solid #2a2a2a !important; + border-radius: 2px !important; + box-shadow: + inset 1px 1px 0 rgba(255, 255, 255, 0.35), + inset -1px -1px 0 rgba(0, 0, 0, 0.18) !important; + cursor: pointer !important; +} + +.nitro-catalog-classic-window .layout-grid-item.has-highlight:hover { + border-color: #000 !important; + box-shadow: + inset 1px 1px 0 rgba(255, 255, 255, 0.5), + inset -1px -1px 0 rgba(0, 0, 0, 0.25), + 0 0 0 1px rgba(0, 0, 0, 0.45) !important; +} + +.nitro-catalog-classic-window .layout-grid-item.has-highlight.is-active { + border-color: #000 !important; + box-shadow: + inset 0 0 0 2px #ffffff, + inset 0 0 0 3px #000, + 0 0 0 1px #63c5e9 !important; +} + .nitro-catalog-classic-grid-offer-icon { position: absolute; left: 50%; @@ -1432,6 +1473,44 @@ right: 6px; } +/* Bulletproof override for the rotate/state buttons. The shared SWF + button rule above lays a transparent body + border-image skin on + top, which works only when the catalog/buttons/btn_secondary*.png + sprites resolve - if they're missing the button renders 0x0 + invisible. Pin the box and paint a visible gradient + outline so + the controls are always discoverable, and force z-index above the + room-previewer DIV so they sit on top of the rendered scene. */ +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn { + width: 28px !important; + height: 26px !important; + min-width: 28px !important; + min-height: 26px !important; + padding: 0 !important; + border: 1px solid #2a2a2a !important; + border-image: none !important; + border-image-source: none !important; + border-radius: 3px !important; + background: linear-gradient(180deg, #f6f6f0 0%, #d3d3c8 100%) !important; + background-color: #ecece4 !important; + background-image: linear-gradient(180deg, #f6f6f0 0%, #d3d3c8 100%) !important; + box-shadow: + inset 1px 1px 0 rgba(255, 255, 255, 0.7), + 0 1px 0 rgba(0, 0, 0, 0.35) !important; + z-index: 10 !important; +} + +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn:hover { + background: linear-gradient(180deg, #ffffff 0%, #dedeD2 100%) !important; + background-image: linear-gradient(180deg, #ffffff 0%, #dedeD2 100%) !important; + border-color: #000 !important; +} + +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn:active { + background: linear-gradient(180deg, #d3d3c8 0%, #f6f6f0 100%) !important; + background-image: linear-gradient(180deg, #d3d3c8 0%, #f6f6f0 100%) !important; + box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.18) !important; +} + .nitro-catalog-classic-window .nitro-catalog-classic-navigation-shell::-webkit-scrollbar, .nitro-catalog-classic-window .nitro-catalog-classic-navigation-list::-webkit-scrollbar, .nitro-catalog-classic-window .nitro-catalog-classic-grid-shell::-webkit-scrollbar, diff --git a/src/css/common/Buttons.css b/src/css/common/Buttons.css index 7188a50..106a3cc 100644 --- a/src/css/common/Buttons.css +++ b/src/css/common/Buttons.css @@ -1,70 +1,3 @@ -.nitro-swf-button { - min-height: 22px !important; - height: 22px; - padding: 2px 10px !important; - border: 3px solid transparent !important; - border-radius: 0 !important; - border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_default_9x22.png") !important; - border-image-slice: 3 3 3 3 fill !important; - border-image-width: 3px !important; - border-image-repeat: stretch !important; - background: transparent !important; - background-color: transparent !important; - background-image: none !important; - box-shadow: none !important; - color: #222 !important; - font-size: 11px !important; - font-weight: 700 !important; - line-height: 16px !important; - text-shadow: 0 1px 0 rgba(255,255,255,.75) !important; - transition: none !important; -} - -.nitro-swf-button:hover { - border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_hover_9x22.png") !important; - background: transparent !important; - background-color: transparent !important; -} - -.nitro-swf-button:active, -.nitro-swf-button.active { - border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_pressed_9x22.png") !important; - background: transparent !important; - background-color: transparent !important; -} - -.nitro-swf-button.pointer-events-none, -.nitro-swf-button:disabled { - border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_disabled_9x22.png") !important; - color: #888 !important; - opacity: 1 !important; -} - -.nitro-swf-button-success { - height: 24px; - min-height: 24px !important; - border: 6px solid transparent !important; - border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_24x24.png") !important; - border-image-slice: 6 6 6 6 fill !important; - border-image-width: 6px !important; - color: #fff !important; - text-shadow: 0 1px 0 rgba(0,0,0,.55) !important; -} - -.nitro-swf-button-success:hover { - border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_hover_24x24.png") !important; -} - -.nitro-swf-button-success:active, -.nitro-swf-button-success.active { - border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_pressed_24x24.png") !important; -} - -.nitro-swf-button-success.pointer-events-none, -.nitro-swf-button-success:disabled { - border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_disabled_24x24.png") !important; -} - .btn-sm { min-height: 28px; } diff --git a/src/css/habbo/HabboSwfSkin.css b/src/css/habbo/HabboSwfSkin.css index 7b2d7d9..4cc82a0 100644 --- a/src/css/habbo/HabboSwfSkin.css +++ b/src/css/habbo/HabboSwfSkin.css @@ -1,6 +1,4 @@ .habbo-swf-window { - --habbo-swf-ubuntu: url("../../assets/images/catalog/swf/habbo_skin_ubuntu.png"); - --habbo-swf-blue: url("../../assets/images/catalog/swf/skins/habbo_skin_blue.png"); --habbo-swf-bg: #ecece4; --habbo-swf-panel: #f7f7f2; --habbo-swf-border: #9d9d96; diff --git a/src/index.tsx b/src/index.tsx index 6fb5997..91c4147 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -46,8 +46,6 @@ import './css/inventory/InventoryView.css'; import './css/layout/LayoutTrophy.css'; -import './css/navigator/HabboNavigatorDesktop.css'; - import './css/nitrocard/NitroCardView.css'; From cdf962a7d2d246e285621480820d8c01de69b03a Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 5 Jun 2026 17:21:53 +0200 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=86=99=20Small=20fixing=20alphablend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bootstrap.ts | 2 + src/common/layout/LayoutRoomPreviewerView.tsx | 32 +++-- .../page/layout/pets/CatalogLayoutPetView.tsx | 2 +- src/css/catalog/CatalogClassicView.css | 124 +++++------------- src/pixiPatch.ts | 83 ++++++++++++ 5 files changed, 135 insertions(+), 108 deletions(-) create mode 100644 src/pixiPatch.ts diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 82a8f70..c0e86da 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -1,3 +1,5 @@ +import './pixiPatch'; + import { GetConfiguration } from '@nitrots/nitro-renderer'; import JSON5 from 'json5'; import { configFileUrl, getClientMode, installSecureFetch } from './secure-assets'; diff --git a/src/common/layout/LayoutRoomPreviewerView.tsx b/src/common/layout/LayoutRoomPreviewerView.tsx index 30deb10..bee10e8 100644 --- a/src/common/layout/LayoutRoomPreviewerView.tsx +++ b/src/common/layout/LayoutRoomPreviewerView.tsx @@ -8,13 +8,8 @@ export const LayoutRoomPreviewerView: FC<{ { const { roomPreviewer = null, height = 0 } = props; const elementRef = useRef(null); - // Latch that disables further renders once Pixi throws inside this - // previewer. The crash (e.g. blackhole furni's filter chain that - // accesses .alphaMode on a null texture) repeats every animation - // frame as long as the ticker keeps firing, flooding the console - // and locking the catalog. One catch and we stop trying for the - // lifetime of this previewer instance. - const renderFailedRef = useRef(false); + const renderFailuresRef = useRef(0); + const MAX_RENDER_FAILURES = 6; const onClick = (event: MouseEvent) => { @@ -28,14 +23,24 @@ export const LayoutRoomPreviewerView: FC<{ { if(!elementRef) return; - renderFailedRef.current = false; + renderFailuresRef.current = 0; const width = elementRef.current.parentElement.clientWidth; const texture = TextureUtils.createRenderTexture(width, height); + const noteFailure = (label: string, error: unknown) => + { + renderFailuresRef.current += 1; + + if(renderFailuresRef.current >= MAX_RENDER_FAILURES) + { + NitroLogger.error(`LayoutRoomPreviewerView ${ label } failed ${ renderFailuresRef.current } times; disabling further renders for this preview`, error); + } + }; + const paintToDOM = () => { - if(renderFailedRef.current) return; + if(renderFailuresRef.current >= MAX_RENDER_FAILURES) return; if(!roomPreviewer || !elementRef.current) return; const renderingCanvas = roomPreviewer.getRenderingCanvas(); @@ -57,17 +62,17 @@ export const LayoutRoomPreviewerView: FC<{ canvas.height = 0; elementRef.current.style.backgroundImage = `url(${ base64 })`; + renderFailuresRef.current = 0; } catch(error) { - renderFailedRef.current = true; - NitroLogger.error('LayoutRoomPreviewerView paint failed; disabling further renders for this preview', error); + noteFailure('paint', error); } }; const update = (ticker: NitroTicker) => { - if(renderFailedRef.current) return; + if(renderFailuresRef.current >= MAX_RENDER_FAILURES) return; if(!roomPreviewer || !elementRef.current) return; try @@ -76,8 +81,7 @@ export const LayoutRoomPreviewerView: FC<{ } catch(error) { - renderFailedRef.current = true; - NitroLogger.error('LayoutRoomPreviewerView update failed; disabling further renders for this preview', error); + noteFailure('update', error); return; } diff --git a/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx b/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx index 3eba176..2c7d147 100644 --- a/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx +++ b/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx @@ -291,7 +291,7 @@ export const CatalogLayoutPetView: FC = props => { LocalizeText('catalog.pets.back.breeds') } }
-
+
{ !colorsShowing && (sellablePalettes.length > 0) && sellablePalettes.map((palette, index) => (