Added meteor scroll.

This commit is contained in:
MaksSlyzar 2026-04-03 23:42:11 +03:00
parent e5b0891e77
commit f2ed608bfc
7 changed files with 333 additions and 168 deletions

View File

@ -0,0 +1,53 @@
.meteor-region-wrapper {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
}
.meteor-region-content {
flex: 1;
overflow-y: auto;
padding-right: 15px;
scrollbar-width: none;
-ms-overflow-style: none;
}
.meteor-region-content::-webkit-scrollbar {
display: none;
}
.meteor-track-local {
position: absolute;
right: 6px;
top: 10px;
bottom: 10px;
width: 1px;
background: rgba(0, 210, 255, 0.1);
pointer-events: none;
}
.meteor-slider-local {
position: absolute;
top: 0;
left: 50%;
margin-left: -1px;
width: 2px;
height: 60px;
background: linear-gradient(to bottom, transparent, #00d2ff, #fff);
transition: transform 0.1s linear;
will-change: transform;
}
.meteor-glow-local {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 6px;
height: 6px;
background: #fff;
border-radius: 50%;
box-shadow: 0 0 10px #00d2ff;
}

View File

@ -0,0 +1,58 @@
import React, { useState, useRef, useEffect, useCallback } from "react";
import "./MeteorRegion.css";
const MeteorRegion = ({ children, className = "", maxHeight }) => {
const [scrollProgress, setScrollProgress] = useState(0);
const [isVisible, setIsVisible] = useState(false);
const containerRef = useRef(null);
const updateScroll = useCallback(() => {
const el = containerRef.current;
if (!el) return;
const { scrollTop, scrollHeight, clientHeight } = el;
const progress = scrollTop / (scrollHeight - clientHeight);
setScrollProgress(isNaN(progress) ? 0 : progress);
setIsVisible(scrollHeight > clientHeight);
}, []);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const resizeObserver = new ResizeObserver(updateScroll);
resizeObserver.observe(el);
el.addEventListener("scroll", updateScroll);
updateScroll();
return () => {
resizeObserver.disconnect();
el.removeEventListener("scroll", updateScroll);
};
}, [updateScroll]);
return (
<div className={`meteor-region-wrapper ${className}`} style={{ maxHeight }}>
<div className="meteor-region-content" ref={containerRef}>
{children}
</div>
{isVisible && (
<div className="meteor-track-local">
<div
className="meteor-slider-local"
style={{
transform: `translateY(${scrollProgress * (containerRef.current?.clientHeight - 70)}px)`,
}}
>
<div className="meteor-glow-local" />
</div>
</div>
)}
</div>
);
};
export default MeteorRegion;

View File

@ -312,3 +312,13 @@
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
} }
*::-webkit-scrollbar {
width: 0px;
background: transparent;
}
* {
scrollbar-width: none;
-ms-overflow-style: none;
}

View File

@ -40,3 +40,13 @@ body {
transparent 50% transparent 50%
); );
} }
*::-webkit-scrollbar {
width: 0px;
background: transparent;
}
* {
scrollbar-width: none;
-ms-overflow-style: none;
}

View File

@ -1,137 +1,161 @@
/* Galaxy Strike Online - Main Styles */ /* Galaxy Strike Online - Main Styles */
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
:root { :root {
--primary-color: #00d4ff; --primary-color: #00d4ff;
--secondary-color: #ff6b35; --secondary-color: #ff6b35;
--accent-color: #ff00ff; --accent-color: #ff00ff;
--bg-primary: #0a0e1a; --bg-primary: #0a0e1a;
--bg-secondary: #151923; --bg-secondary: #151923;
--bg-tertiary: #1e2433; --bg-tertiary: #1e2433;
--text-primary: #ffffff; --text-primary: #ffffff;
--text-secondary: #b8c5d6; --text-secondary: #b8c5d6;
--text-muted: #6b7c93; --text-muted: #6b7c93;
--border-color: #2a3241; --border-color: #2a3241;
--success-color: #00ff88; --success-color: #00ff88;
--warning-color: #ffaa00; --warning-color: #ffaa00;
--error-color: #ff3366; --error-color: #ff3366;
--card-bg: rgba(30, 36, 51, 0.8); --card-bg: rgba(30, 36, 51, 0.8);
--hover-bg: rgba(0, 212, 255, 0.1); --hover-bg: rgba(0, 212, 255, 0.1);
--gradient-primary: linear-gradient(135deg, #00d4ff, #0099cc); --gradient-primary: linear-gradient(135deg, #00d4ff, #0099cc);
--gradient-secondary: linear-gradient(135deg, #ff6b35, #ff4500); --gradient-secondary: linear-gradient(135deg, #ff6b35, #ff4500);
} }
body { body {
font-family: 'Space Mono', monospace; font-family: "Space Mono", monospace;
background: var(--bg-primary); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
overflow: hidden; overflow: hidden;
background-image: background-image:
radial-gradient(circle at 20% 50%, rgba(0, 212, 255, 0.1) 0%, transparent 50%), radial-gradient(
radial-gradient(circle at 80% 80%, rgba(255, 107, 53, 0.1) 0%, transparent 50%), circle at 20% 50%,
radial-gradient(circle at 40% 20%, rgba(255, 0, 255, 0.05) 0%, transparent 50%); rgba(0, 212, 255, 0.1) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 80%,
rgba(255, 107, 53, 0.1) 0%,
transparent 50%
),
radial-gradient(
circle at 40% 20%,
rgba(255, 0, 255, 0.05) 0%,
transparent 50%
);
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.server-controls { .server-controls {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.server-filters { .server-filters {
justify-content: center; justify-content: center;
} }
.server-confirmation { .server-confirmation {
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
} }
.confirm-actions-left, .confirm-actions-right { .confirm-actions-left,
width: 100%; .confirm-actions-right {
max-width: 300px; width: 100%;
} max-width: 300px;
}
.server-details {
flex-direction: column; .server-details {
gap: 8px; flex-direction: column;
} gap: 8px;
}
} }
/* Animations */ /* Animations */
@keyframes fadeInUp { @keyframes fadeInUp {
from { from {
opacity: 0; opacity: 0;
transform: translateY(30px); transform: translateY(30px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%,
opacity: 1; 100% {
} opacity: 1;
50% { }
opacity: 0.5; 50% {
} opacity: 0.5;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.dashboard-grid { .dashboard-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.dungeons-container { .dungeons-container {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.base-container { .base-container {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.inventory-container { .inventory-container {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.main-nav { .main-nav {
overflow-x: scroll; overflow-x: scroll;
} }
.resources { .resources {
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
} }
.resource { .resource {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
font-size: 0.8rem; font-size: 0.8rem;
} }
.game-title { .game-title {
font-size: 2rem; font-size: 2rem;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.header-center { .header-center {
display: none; display: none;
} }
.nav-btn span { .nav-btn span {
display: none; display: none;
} }
.nav-btn { .nav-btn {
padding: 0.5rem; padding: 0.5rem;
width: 100px; width: 100px;
justify-content: center; justify-content: center;
} }
}
*::-webkit-scrollbar {
width: 0px;
background: transparent;
}
* {
scrollbar-width: none;
-ms-overflow-style: none;
} }

View File

@ -1,10 +1,11 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { useSocket } from "../../../hooks/useSocket"; import { useSocket } from "../../../hooks/useSocket";
import GameDataManager from "../../../services/GameDataManager.js"; import GameDataManager from "../../../services/GameDataManager.js";
import ItemModal from "./components/ItemModal"; import ItemModal from "./components/ItemModal";
import "./styles/InventoryTab.css"; import "./styles/InventoryTab.css";
import { getServerUrl } from "../../../config/api.js"; import { getServerUrl } from "../../../config/api.js";
import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
const InventoryTab = () => { const InventoryTab = () => {
const { socket } = useSocket(); const { socket } = useSocket();
@ -12,6 +13,7 @@ const InventoryTab = () => {
const [equipment, setEquipment] = useState({}); const [equipment, setEquipment] = useState({});
const [selectedItem, setSelectedItem] = useState(null); const [selectedItem, setSelectedItem] = useState(null);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const CONNECT_URL = getServerUrl(); const CONNECT_URL = getServerUrl();
const ASSET_BASE_URL = `${CONNECT_URL}/static/`; const ASSET_BASE_URL = `${CONNECT_URL}/static/`;
@ -88,32 +90,32 @@ const InventoryTab = () => {
const equipmentSlots = { const equipmentSlots = {
personal: Object.keys(coreSystems) personal: Object.keys(coreSystems)
.filter( .filter(
(key) => (k) =>
key.startsWith("original:personal_") && k.startsWith("original:personal_") &&
!key.includes("accessory") && !k.includes("accessory") &&
key !== "original:personal_weapons", k !== "original:personal_weapons",
) )
.map((key) => ({ .map((k) => ({
id: key, id: k,
label: GameDataManager.t(coreSystems[key].displayName), label: GameDataManager.t(coreSystems[k].displayName),
})), })),
weapons: Object.keys(coreSystems) weapons: Object.keys(coreSystems)
.filter((key) => key === "original:personal_weapons") .filter((k) => k === "original:personal_weapons")
.map((key) => ({ .map((k) => ({
id: key, id: k,
label: GameDataManager.t(coreSystems[key].displayName), label: GameDataManager.t(coreSystems[k].displayName),
})), })),
accessories: Object.keys(coreSystems) accessories: Object.keys(coreSystems)
.filter((key) => key.includes("personal_accessory")) .filter((k) => k.includes("personal_accessory"))
.map((key) => ({ .map((k) => ({
id: key, id: k,
label: GameDataManager.t(coreSystems[key].displayName), label: GameDataManager.t(coreSystems[k].displayName),
})), })),
ship: Object.keys(coreSystems) ship: Object.keys(coreSystems)
.filter((key) => key.startsWith("original:ship_")) .filter((k) => k.startsWith("original:ship_"))
.map((key) => ({ .map((k) => ({
id: key, id: k,
label: GameDataManager.t(coreSystems[key].displayName), label: GameDataManager.t(coreSystems[k].displayName),
})), })),
}; };
@ -152,43 +154,49 @@ const InventoryTab = () => {
return ( return (
<div className="inv-adaptive-container"> <div className="inv-adaptive-container">
<div className="inv-layout-wrapper"> <div className="inv-layout-wrapper">
<section className="inv-panel loadout overflow-y"> <section className="inv-panel loadout">
{renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)} <MeteorRegion>
{renderSlotGroup("WEAPONRY", equipmentSlots.weapons)} {renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
{renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)} {renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
<div className="separator" /> {renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
{renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)} <div className="separator" />
{renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
</MeteorRegion>
</section> </section>
<section className="inv-panel cargo"> <section className="inv-panel cargo">
<div className="cargo-grid-v2"> <MeteorRegion>
{items.map((item, idx) => { <div className="cargo-grid-v2">
const isEquipped = Object.values(equipment).some( {items.map((item, idx) => {
(e) => e?.id === item.id, const isEquipped = Object.values(equipment).some(
); (e) => e?.id === item.id,
return ( );
<div return (
key={`${item.id}-${idx}`} <div
className={`item-slot ${item.rarity} ${isEquipped ? "equipped-in-storage" : ""}`} key={`${item.id}-${idx}`}
onClick={() => { className={`item-slot ${item.rarity} ${isEquipped ? "equipped-in-storage" : ""}`}
setSelectedItem(item); onClick={() => {
setShowModal(true); setSelectedItem(item);
}} setShowModal(true);
> }}
<img >
src={item.textureUrl} <img
width={62} src={item.textureUrl}
className="item-img-grid" width={62}
/> className="item-img-grid"
{isEquipped && <div className="equipped-tag">E</div>} />
{item.quantity > 1 && ( {isEquipped && <div className="equipped-tag">E</div>}
<span className="qty-label">{item.quantity}</span> {item.quantity > 1 && (
)} <span className="qty-label">{item.quantity}</span>
</div> )}
); </div>
})} );
</div> })}
</div>
</MeteorRegion>
</section> </section>
</div> </div>
{showModal && {showModal &&
selectedItem && selectedItem &&
ReactDOM.createPortal( ReactDOM.createPortal(

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
import GameDataManager from "../../../services/GameDataManager.js"; import GameDataManager from "../../../services/GameDataManager.js";
import "./styles/ItemListTab.css"; import "./styles/ItemListTab.css";
import { config } from "../../../config/api.js"; import { config } from "../../../config/api.js";
import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
const ItemListTab = () => { const ItemListTab = () => {
const [allItems, setAllItems] = useState([]); const [allItems, setAllItems] = useState([]);
@ -83,7 +84,7 @@ const ItemListTab = () => {
</div> </div>
</div> </div>
<div className="inspector-grid"> <MeteorRegion className="inspector-grid">
<div className="inspector-section"> <div className="inspector-section">
<div className="section-title">DATA_DESCRIPTION</div> <div className="section-title">DATA_DESCRIPTION</div>
<p>{selectedItem.description}</p> <p>{selectedItem.description}</p>
@ -103,7 +104,7 @@ const ItemListTab = () => {
</div> </div>
</div> </div>
)} )}
</div> </MeteorRegion>
</div> </div>
)} )}
</div> </div>
@ -131,7 +132,8 @@ const ItemListTab = () => {
</button> </button>
))} ))}
</div> </div>
<div className="items-list-scroll">
<MeteorRegion className="items-list-scroll">
{filteredItems.map((item) => ( {filteredItems.map((item) => (
<div <div
key={item.id} key={item.id}
@ -147,7 +149,7 @@ const ItemListTab = () => {
</div> </div>
</div> </div>
))} ))}
</div> </MeteorRegion>
</div> </div>
{(!isMobile || (isMobile && selectedItem)) && renderInspector()} {(!isMobile || (isMobile && selectedItem)) && renderInspector()}
</div> </div>