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

@ -27,14 +27,26 @@
}
body {
font-family: 'Space Mono', monospace;
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%);
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) {
@ -52,7 +64,8 @@ body {
gap: 20px;
}
.confirm-actions-left, .confirm-actions-right {
.confirm-actions-left,
.confirm-actions-right {
width: 100%;
max-width: 300px;
}
@ -76,7 +89,8 @@ body {
}
@keyframes pulse {
0%, 100% {
0%,
100% {
opacity: 1;
}
50% {
@ -135,3 +149,13 @@ body {
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,14 +154,18 @@ const InventoryTab = () => {
return (
<div className="inv-adaptive-container">
<div className="inv-layout-wrapper">
<section className="inv-panel loadout overflow-y">
<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">
<MeteorRegion>
<div className="cargo-grid-v2">
{items.map((item, idx) => {
const isEquipped = Object.values(equipment).some(
@ -187,8 +193,10 @@ const InventoryTab = () => {
);
})}
</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>