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;
}
}
*::-webkit-scrollbar {
width: 0px;
background: transparent;
}
* {
scrollbar-width: none;
-ms-overflow-style: none;
}

View File

@ -40,3 +40,13 @@ body {
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 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #00d4ff;
--secondary-color: #ff6b35;
--accent-color: #ff00ff;
--bg-primary: #0a0e1a;
--bg-secondary: #151923;
--bg-tertiary: #1e2433;
--text-primary: #ffffff;
--text-secondary: #b8c5d6;
--text-muted: #6b7c93;
--border-color: #2a3241;
--success-color: #00ff88;
--warning-color: #ffaa00;
--error-color: #ff3366;
--card-bg: rgba(30, 36, 51, 0.8);
--hover-bg: rgba(0, 212, 255, 0.1);
--gradient-primary: linear-gradient(135deg, #00d4ff, #0099cc);
--gradient-secondary: linear-gradient(135deg, #ff6b35, #ff4500);
--primary-color: #00d4ff;
--secondary-color: #ff6b35;
--accent-color: #ff00ff;
--bg-primary: #0a0e1a;
--bg-secondary: #151923;
--bg-tertiary: #1e2433;
--text-primary: #ffffff;
--text-secondary: #b8c5d6;
--text-muted: #6b7c93;
--border-color: #2a3241;
--success-color: #00ff88;
--warning-color: #ffaa00;
--error-color: #ff3366;
--card-bg: rgba(30, 36, 51, 0.8);
--hover-bg: rgba(0, 212, 255, 0.1);
--gradient-primary: linear-gradient(135deg, #00d4ff, #0099cc);
--gradient-secondary: linear-gradient(135deg, #ff6b35, #ff4500);
}
body {
font-family: 'Space Mono', monospace;
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
background-image:
radial-gradient(circle at 20% 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%);
font-family: "Space Mono", monospace;
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
background-image:
radial-gradient(
circle at 20% 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) {
.server-controls {
flex-direction: column;
align-items: stretch;
}
.server-filters {
justify-content: center;
}
.server-confirmation {
flex-direction: column;
gap: 20px;
}
.confirm-actions-left, .confirm-actions-right {
width: 100%;
max-width: 300px;
}
.server-details {
flex-direction: column;
gap: 8px;
}
.server-controls {
flex-direction: column;
align-items: stretch;
}
.server-filters {
justify-content: center;
}
.server-confirmation {
flex-direction: column;
gap: 20px;
}
.confirm-actions-left,
.confirm-actions-right {
width: 100%;
max-width: 300px;
}
.server-details {
flex-direction: column;
gap: 8px;
}
}
/* Animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@media (max-width: 768px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
.dungeons-container {
grid-template-columns: 1fr;
}
.base-container {
grid-template-columns: 1fr;
}
.inventory-container {
grid-template-columns: 1fr;
}
.main-nav {
overflow-x: scroll;
}
.resources {
flex-direction: column;
gap: 0.5rem;
}
.resource {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
.game-title {
font-size: 2rem;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
.dungeons-container {
grid-template-columns: 1fr;
}
.base-container {
grid-template-columns: 1fr;
}
.inventory-container {
grid-template-columns: 1fr;
}
.main-nav {
overflow-x: scroll;
}
.resources {
flex-direction: column;
gap: 0.5rem;
}
.resource {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
.game-title {
font-size: 2rem;
}
}
@media (max-width: 480px) {
.header-center {
display: none;
}
.nav-btn span {
display: none;
}
.nav-btn {
padding: 0.5rem;
width: 100px;
justify-content: center;
}
.header-center {
display: none;
}
.nav-btn span {
display: none;
}
.nav-btn {
padding: 0.5rem;
width: 100px;
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 { useSocket } from "../../../hooks/useSocket";
import GameDataManager from "../../../services/GameDataManager.js";
import ItemModal from "./components/ItemModal";
import "./styles/InventoryTab.css";
import { getServerUrl } from "../../../config/api.js";
import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
const InventoryTab = () => {
const { socket } = useSocket();
@ -12,6 +13,7 @@ const InventoryTab = () => {
const [equipment, setEquipment] = useState({});
const [selectedItem, setSelectedItem] = useState(null);
const [showModal, setShowModal] = useState(false);
const CONNECT_URL = getServerUrl();
const ASSET_BASE_URL = `${CONNECT_URL}/static/`;
@ -88,32 +90,32 @@ const InventoryTab = () => {
const equipmentSlots = {
personal: Object.keys(coreSystems)
.filter(
(key) =>
key.startsWith("original:personal_") &&
!key.includes("accessory") &&
key !== "original:personal_weapons",
(k) =>
k.startsWith("original:personal_") &&
!k.includes("accessory") &&
k !== "original:personal_weapons",
)
.map((key) => ({
id: key,
label: GameDataManager.t(coreSystems[key].displayName),
.map((k) => ({
id: k,
label: GameDataManager.t(coreSystems[k].displayName),
})),
weapons: Object.keys(coreSystems)
.filter((key) => key === "original:personal_weapons")
.map((key) => ({
id: key,
label: GameDataManager.t(coreSystems[key].displayName),
.filter((k) => k === "original:personal_weapons")
.map((k) => ({
id: k,
label: GameDataManager.t(coreSystems[k].displayName),
})),
accessories: Object.keys(coreSystems)
.filter((key) => key.includes("personal_accessory"))
.map((key) => ({
id: key,
label: GameDataManager.t(coreSystems[key].displayName),
.filter((k) => k.includes("personal_accessory"))
.map((k) => ({
id: k,
label: GameDataManager.t(coreSystems[k].displayName),
})),
ship: Object.keys(coreSystems)
.filter((key) => key.startsWith("original:ship_"))
.map((key) => ({
id: key,
label: GameDataManager.t(coreSystems[key].displayName),
.filter((k) => k.startsWith("original:ship_"))
.map((k) => ({
id: k,
label: GameDataManager.t(coreSystems[k].displayName),
})),
};
@ -152,43 +154,49 @@ const InventoryTab = () => {
return (
<div className="inv-adaptive-container">
<div className="inv-layout-wrapper">
<section className="inv-panel loadout overflow-y">
{renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
{renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
{renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
<div className="separator" />
{renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
<section className="inv-panel loadout">
<MeteorRegion>
{renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
{renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
{renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
<div className="separator" />
{renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
</MeteorRegion>
</section>
<section className="inv-panel cargo">
<div className="cargo-grid-v2">
{items.map((item, idx) => {
const isEquipped = Object.values(equipment).some(
(e) => e?.id === item.id,
);
return (
<div
key={`${item.id}-${idx}`}
className={`item-slot ${item.rarity} ${isEquipped ? "equipped-in-storage" : ""}`}
onClick={() => {
setSelectedItem(item);
setShowModal(true);
}}
>
<img
src={item.textureUrl}
width={62}
className="item-img-grid"
/>
{isEquipped && <div className="equipped-tag">E</div>}
{item.quantity > 1 && (
<span className="qty-label">{item.quantity}</span>
)}
</div>
);
})}
</div>
<MeteorRegion>
<div className="cargo-grid-v2">
{items.map((item, idx) => {
const isEquipped = Object.values(equipment).some(
(e) => e?.id === item.id,
);
return (
<div
key={`${item.id}-${idx}`}
className={`item-slot ${item.rarity} ${isEquipped ? "equipped-in-storage" : ""}`}
onClick={() => {
setSelectedItem(item);
setShowModal(true);
}}
>
<img
src={item.textureUrl}
width={62}
className="item-img-grid"
/>
{isEquipped && <div className="equipped-tag">E</div>}
{item.quantity > 1 && (
<span className="qty-label">{item.quantity}</span>
)}
</div>
);
})}
</div>
</MeteorRegion>
</section>
</div>
{showModal &&
selectedItem &&
ReactDOM.createPortal(

View File

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