mirror of
https://github.com/NebulaServices/Nebula.git
synced 2025-05-17 05:20:01 -04:00
feat: framer motion settings
This commit is contained in:
parent
83f1083d75
commit
90a858d1e3
16 changed files with 461 additions and 3 deletions
7
framer-motion.d.ts
vendored
Normal file
7
framer-motion.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
import * as React from "preact/compat";
|
||||
|
||||
declare module "framer-motion" {
|
||||
export interface AnimatePresenceProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,8 @@
|
|||
"dependencies": {
|
||||
"@titaniumnetwork-dev/ultraviolet": "^2.0.0",
|
||||
"@tomphttp/bare-server-node": "^2.0.1",
|
||||
"classnames": "^2.3.2",
|
||||
"framer-motion": "^10.16.16",
|
||||
"i18next": "^23.7.9",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"million": "^2.6.4",
|
||||
|
|
42
pnpm-lock.yaml
generated
42
pnpm-lock.yaml
generated
|
@ -11,6 +11,12 @@ dependencies:
|
|||
'@tomphttp/bare-server-node':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
classnames:
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2
|
||||
framer-motion:
|
||||
specifier: ^10.16.16
|
||||
version: 10.16.16(react@18.2.0)
|
||||
i18next:
|
||||
specifier: ^23.7.9
|
||||
version: 23.7.10
|
||||
|
@ -359,6 +365,20 @@ packages:
|
|||
'@babel/helper-validator-identifier': 7.22.20
|
||||
to-fast-properties: 2.0.0
|
||||
|
||||
/@emotion/is-prop-valid@0.8.8:
|
||||
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@emotion/memoize': 0.7.4
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@emotion/memoize@0.7.4:
|
||||
resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm64@0.19.9:
|
||||
resolution: {integrity: sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -1220,6 +1240,10 @@ packages:
|
|||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
/classnames@2.3.2:
|
||||
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
|
||||
dev: false
|
||||
|
||||
/cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -1817,6 +1841,23 @@ packages:
|
|||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
dev: true
|
||||
|
||||
/framer-motion@10.16.16(react@18.2.0):
|
||||
resolution: {integrity: sha512-je6j91rd7NmUX7L1XHouwJ4v3R+SO4umso2LUcgOct3rHZ0PajZ80ETYZTajzEXEl9DlKyzjyt4AvGQ+lrebOw==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
tslib: 2.6.2
|
||||
optionalDependencies:
|
||||
'@emotion/is-prop-valid': 0.8.8
|
||||
dev: false
|
||||
|
||||
/fs-extra@11.2.0:
|
||||
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
@ -3252,7 +3293,6 @@ packages:
|
|||
|
||||
/tslib@2.6.2:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
dev: true
|
||||
|
||||
/tsutils@3.21.0(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Home } from "./pages/Home";
|
|||
import { NotFound } from "./pages/_404.jsx";
|
||||
import { DiscordPage } from "./pages/discord.jsx";
|
||||
import { ProxyFrame } from "./ProxyFrame.js";
|
||||
import { Settings } from "./pages/Settings/index.js";
|
||||
|
||||
import "./style.css";
|
||||
import "./themes/main.css";
|
||||
|
@ -18,6 +19,7 @@ export function App() {
|
|||
<Route path="/" component={Home} />
|
||||
<Route path="/discord" component={DiscordPage} />
|
||||
<Route path="/proxyframe/:id" component={ProxyFrame} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route default component={NotFound} />
|
||||
</Router>
|
||||
</LocationProvider>
|
||||
|
|
|
@ -17,5 +17,13 @@
|
|||
"sub": "Would you like to open this via proxy?",
|
||||
"button1": "Open Normally",
|
||||
"button2": "Use Proxy"
|
||||
},
|
||||
"settings": {
|
||||
"tabs": {
|
||||
"proxy": "Proxy",
|
||||
"tab": "Tab",
|
||||
"custom": "Customization",
|
||||
"misc": "Misc"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,5 +17,13 @@
|
|||
"sub": "プロキシ経由で開きますか?",
|
||||
"button1": "通常通り開く",
|
||||
"button2": "プロキシを使用する"
|
||||
},
|
||||
"settings": {
|
||||
"tabs": {
|
||||
"proxy": "プロキシ",
|
||||
"tab": "タブ",
|
||||
"custom": "カスタマイズ",
|
||||
"misc": "その他"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
19
src/pages/Settings/Customization.tsx
Normal file
19
src/pages/Settings/Customization.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { motion } from "framer-motion";
|
||||
import { tabContentVariant, settingsPageVariant } from "./Variants";
|
||||
|
||||
const Customization = ({ id, active }) => (
|
||||
<motion.div
|
||||
role="tabpanel"
|
||||
id={id}
|
||||
className="tab-content"
|
||||
variants={tabContentVariant}
|
||||
animate={active ? "active" : "inactive"}
|
||||
initial="inactive"
|
||||
>
|
||||
<motion.div variants={settingsPageVariant} className="content-card">
|
||||
<h1>Customization</h1>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
export default Customization;
|
19
src/pages/Settings/Misc.tsx
Normal file
19
src/pages/Settings/Misc.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { motion } from "framer-motion";
|
||||
import { tabContentVariant, settingsPageVariant } from "./Variants";
|
||||
|
||||
const Misc = ({ id, active }) => (
|
||||
<motion.div
|
||||
role="tabpanel"
|
||||
id={id}
|
||||
className="tab-content"
|
||||
variants={tabContentVariant}
|
||||
animate={active ? "active" : "inactive"}
|
||||
initial="inactive"
|
||||
>
|
||||
<motion.div variants={settingsPageVariant} className="content-card">
|
||||
<h1>Misc settings</h1>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
export default Misc;
|
19
src/pages/Settings/Proxy.tsx
Normal file
19
src/pages/Settings/Proxy.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { motion } from "framer-motion";
|
||||
import { tabContentVariant, settingsPageVariant } from "./Variants";
|
||||
|
||||
const Proxy = ({ id, active }) => (
|
||||
<motion.div
|
||||
role="tabpanel"
|
||||
id={id}
|
||||
className="tab-content"
|
||||
variants={tabContentVariant}
|
||||
animate={active ? "active" : "inactive"}
|
||||
initial="inactive"
|
||||
>
|
||||
<motion.div variants={settingsPageVariant} className="content-card">
|
||||
<h1>Porxy</h1>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
export default Proxy;
|
106
src/pages/Settings/TabComponent.tsx
Normal file
106
src/pages/Settings/TabComponent.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
import { useState, useEffect } from "preact/hooks";
|
||||
import cn from "classnames";
|
||||
import { motion } from "framer-motion";
|
||||
import "./styles.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const tabVariant = {
|
||||
active: {
|
||||
width: "55%",
|
||||
transition: {
|
||||
type: "tween",
|
||||
duration: 0.4
|
||||
}
|
||||
},
|
||||
inactive: {
|
||||
width: "15%",
|
||||
transition: {
|
||||
type: "tween",
|
||||
duration: 0.4
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tabTextVariant = {
|
||||
active: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
display: "block",
|
||||
transition: {
|
||||
type: "tween",
|
||||
duration: 0.3,
|
||||
delay: 0.3
|
||||
}
|
||||
},
|
||||
inactive: {
|
||||
opacity: 0,
|
||||
x: -30,
|
||||
transition: {
|
||||
type: "tween",
|
||||
duration: 0.3,
|
||||
delay: 0.1
|
||||
},
|
||||
transitionEnd: { display: "none" }
|
||||
}
|
||||
};
|
||||
|
||||
const TabComponent = ({ tabs, defaultIndex = 0 }) => {
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(defaultIndex);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
"--active-color",
|
||||
tabs[activeTabIndex].color
|
||||
);
|
||||
}, [activeTabIndex, tabs]);
|
||||
|
||||
// Default to a tab based on the URL hash value
|
||||
useEffect(() => {
|
||||
const tabFromHash = tabs.findIndex(
|
||||
(tab) => `#${tab.id}` === window.location.hash
|
||||
);
|
||||
setActiveTabIndex(tabFromHash !== -1 ? tabFromHash : defaultIndex);
|
||||
}, [tabs, defaultIndex]);
|
||||
|
||||
const onTabClick = (index) => {
|
||||
setActiveTabIndex(index);
|
||||
};
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div class="flex flex-col items-center">
|
||||
<div className="container h-full w-full">
|
||||
<div className="tabs-component">
|
||||
<ul className="tab-links" role="tablist">
|
||||
{tabs.map((tab, index) => (
|
||||
<motion.li
|
||||
key={tab.id}
|
||||
className={cn("tab", { active: activeTabIndex === index })}
|
||||
role="presentation"
|
||||
variants={tabVariant}
|
||||
animate={activeTabIndex === index ? "active" : "inactive"}
|
||||
>
|
||||
<a href={`#${tab.id}`} onClick={() => onTabClick(index)}>
|
||||
{tab.icon}
|
||||
<motion.span variants={tabTextVariant}>
|
||||
{t(tab.title)}
|
||||
</motion.span>
|
||||
</a>
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
{tabs.map((tab, index) => (
|
||||
<tab.content
|
||||
key={tab.id}
|
||||
id={`${tab.id}-content`}
|
||||
active={activeTabIndex === index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabComponent;
|
19
src/pages/Settings/TabSettings.tsx
Normal file
19
src/pages/Settings/TabSettings.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { motion } from "framer-motion";
|
||||
import { tabContentVariant, settingsPageVariant } from "./Variants";
|
||||
|
||||
const TabSettings = ({ id, active }) => (
|
||||
<motion.div
|
||||
role="tabpanel"
|
||||
id={id}
|
||||
className="tab-content"
|
||||
variants={tabContentVariant}
|
||||
animate={active ? "active" : "inactive"}
|
||||
initial="inactive"
|
||||
>
|
||||
<motion.div variants={settingsPageVariant} className="content-card">
|
||||
<h1>Tab settings</h1>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
export default TabSettings;
|
30
src/pages/Settings/Variants.tsx
Normal file
30
src/pages/Settings/Variants.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
const tabContentVariant = {
|
||||
active: {
|
||||
display: "block",
|
||||
transition: {
|
||||
staggerChildren: 0.2
|
||||
}
|
||||
},
|
||||
inactive: {
|
||||
display: "none"
|
||||
}
|
||||
};
|
||||
|
||||
const settingsPageVariant = {
|
||||
active: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.5
|
||||
}
|
||||
},
|
||||
inactive: {
|
||||
opacity: 0,
|
||||
y: 10,
|
||||
transition: {
|
||||
duration: 0.5
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { settingsPageVariant, tabContentVariant };
|
11
src/pages/Settings/index.tsx
Normal file
11
src/pages/Settings/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import TabComponent from "./TabComponent";
|
||||
import { HeaderRoute } from "../../components/HeaderRoute";
|
||||
import tabs from "./tabs";
|
||||
|
||||
export function Settings() {
|
||||
return (
|
||||
<HeaderRoute>
|
||||
<TabComponent tabs={tabs} />
|
||||
</HeaderRoute>
|
||||
);
|
||||
}
|
128
src/pages/Settings/styles.css
Normal file
128
src/pages/Settings/styles.css
Normal file
|
@ -0,0 +1,128 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--white: #fff;
|
||||
--black: #333;
|
||||
--active-color: #f1f1f1;
|
||||
--border-radius: 40px;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background: var(--active-color);
|
||||
transition: background 1.5s ease;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tabs-component {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
padding: 40px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.tab-links {
|
||||
padding: 0;
|
||||
margin: 0 auto 20px;
|
||||
list-style: none;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab a {
|
||||
text-decoration: none;
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.tab::before {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.2;
|
||||
position: absolute;
|
||||
border-radius: var(--border-radius);
|
||||
background: none;
|
||||
transition: background 0.5s ease;
|
||||
}
|
||||
|
||||
.tab svg {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
min-width: 30px;
|
||||
fill: var(--black);
|
||||
transition: fill 0.5s ease;
|
||||
}
|
||||
|
||||
.tab.active::before {
|
||||
background: var(--active-color);
|
||||
}
|
||||
|
||||
.tab span {
|
||||
font-weight: 700;
|
||||
margin-left: 10px;
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
|
||||
.tab.active span {
|
||||
color: var(--active-color);
|
||||
}
|
||||
|
||||
.tab.active svg {
|
||||
fill: var(--active-color);
|
||||
}
|
||||
|
||||
.tab a {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
width: 48%;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.content-card .info::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
bottom: -5px;
|
||||
background: var(--active-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.content-card img {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.content-card h3 {
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
|
||||
.content-card .info {
|
||||
padding: 10px 0;
|
||||
}
|
42
src/pages/Settings/tabs.tsx
Normal file
42
src/pages/Settings/tabs.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import Proxy from "./Proxy";
|
||||
import TabSettings from "./TabSettings";
|
||||
import Misc from "./Misc";
|
||||
import Customization from "./Customization";
|
||||
|
||||
import { GoBrowser } from "react-icons/go";
|
||||
import { AiOutlineLaptop } from "react-icons/ai";
|
||||
import { FaPalette } from "react-icons/fa";
|
||||
import { FaGear } from "react-icons/fa6";
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: "settings.tabs.proxy",
|
||||
id: "proxy",
|
||||
icon: <AiOutlineLaptop />,
|
||||
color: "#5d5dff",
|
||||
content: Proxy
|
||||
},
|
||||
{
|
||||
title: "settings.tabs.tab",
|
||||
id: "tab",
|
||||
icon: <GoBrowser />,
|
||||
color: "#67bb67",
|
||||
content: TabSettings
|
||||
},
|
||||
{
|
||||
title: "settings.tabs.custom",
|
||||
id: "custom",
|
||||
icon: <FaPalette />,
|
||||
color: "#63a7c7",
|
||||
content: Customization
|
||||
},
|
||||
{
|
||||
title: "settings.tabs.misc",
|
||||
id: "misc",
|
||||
icon: <FaGear />,
|
||||
color: "#f56868",
|
||||
content: Misc
|
||||
}
|
||||
];
|
||||
|
||||
export default tabs;
|
|
@ -8,8 +8,6 @@
|
|||
"jsxImportSource": "preact",
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"react": ["./node_modules/preact/compat/"],
|
||||
"react-dom": ["./node_modules/preact/compat/"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue