Added meteor scroll.
This commit is contained in:
parent
e5b0891e77
commit
f2ed608bfc
53
client/src/components/Meteor/MeteorRegion.css
Normal file
53
client/src/components/Meteor/MeteorRegion.css
Normal 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;
|
||||||
|
}
|
||||||
58
client/src/components/Meteor/MeteorRegion.jsx
Normal file
58
client/src/components/Meteor/MeteorRegion.jsx
Normal 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;
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -40,3 +40,13 @@ body {
|
|||||||
transparent 50%
|
transparent 50%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 0px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|||||||
@ -27,14 +27,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@ -52,7 +64,8 @@ body {
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-actions-left, .confirm-actions-right {
|
.confirm-actions-left,
|
||||||
|
.confirm-actions-right {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
@ -76,7 +89,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@ -135,3 +149,13 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 0px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|||||||
@ -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,14 +154,18 @@ 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">
|
||||||
|
<MeteorRegion>
|
||||||
{renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
|
{renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
|
||||||
{renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
|
{renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
|
||||||
{renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
|
{renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
|
||||||
<div className="separator" />
|
<div className="separator" />
|
||||||
{renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
|
{renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
|
||||||
|
</MeteorRegion>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="inv-panel cargo">
|
<section className="inv-panel cargo">
|
||||||
|
<MeteorRegion>
|
||||||
<div className="cargo-grid-v2">
|
<div className="cargo-grid-v2">
|
||||||
{items.map((item, idx) => {
|
{items.map((item, idx) => {
|
||||||
const isEquipped = Object.values(equipment).some(
|
const isEquipped = Object.values(equipment).some(
|
||||||
@ -187,8 +193,10 @@ const InventoryTab = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</MeteorRegion>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showModal &&
|
{showModal &&
|
||||||
selectedItem &&
|
selectedItem &&
|
||||||
ReactDOM.createPortal(
|
ReactDOM.createPortal(
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user