completed modules

This commit is contained in:
Km.Van
2025-08-10 19:57:03 +08:00
parent 29089a4de4
commit c12f7bea7d
73 changed files with 608 additions and 607 deletions

View File

@@ -0,0 +1,37 @@
:root {
--x-card-legend-arrow-fg: var(--x-card-legend-fg);
--x-card-legend-arrow-bg-hover: hsl(0 0% 0% / 0.05);
--x-card-legend-arrow-bg-active: hsl(0 0% 0% / 0.1);
@media (prefers-color-scheme: dark) {
--x-card-legend-arrow-fg: var(--x-card-legend-fg);
--x-card-legend-arrow-bg-hover: hsl(0 0% 100% / 0.05);
--x-card-legend-arrow-bg-active: hsl(0 0% 100% / 0.1);
}
}
.arrow {
display: flex;
align-items: center;
cursor: pointer;
border: none;
border-radius: var(--x-radius);
background: transparent;
padding: var(--x-gutter-sm);
color: var(--x-card-legend-arrow-fg);
&:hover {
background: var(--x-card-legend-arrow-bg-hover);
color: var(--x-card-legend-arrow-fg);
}
&:active {
background: var(--x-card-legend-arrow-bg-active);
color: var(--x-card-legend-arrow-fg);
}
&[data-disabled],
&[data-disabled]:hover {
opacity: 0.1;
cursor: not-allowed;
}
svg {
width: 1rem;
height: 1rem;
}
}

View File

@@ -0,0 +1,38 @@
import { ChevronDown, ChevronUp } from 'lucide-react';
import { observer } from 'mobx-react-lite';
import { type FC, type MouseEvent, useCallback } from 'react';
import { gettext } from '@/Components/Language/index.ts';
import styles from './arrow.module.scss';
import { ModuleStore } from './store.ts';
export const ModuleArrow: FC<{
isDown: boolean;
id: string;
}> = observer(({ isDown, id }) => {
const { disabledMoveUpId, disabledMoveDownId, moveUp, moveDown } =
ModuleStore;
const disabled = isDown ? disabledMoveDownId === id : disabledMoveUpId === id;
const handleMove = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
if (isDown) {
moveDown(id);
return;
}
moveUp(id);
},
[isDown, moveDown, moveUp, id]
);
return (
<button
className={styles.arrow}
data-disabled={disabled || undefined}
disabled={disabled}
onClick={handleMove}
title={isDown ? gettext('Move down') : gettext('Move up')}
type="button"
>
{isDown ? <ChevronDown /> : <ChevronUp />}
</button>
);
});

View File

@@ -0,0 +1,36 @@
:root {
--x-card-group-label-fg: var(--x-fg);
--x-card-group-split-color: hsl(0 0% 0% / 0.1);
--x-card-group-bg-hover: hsl(0 0% 0% / 0.05);
@media (prefers-color-scheme: dark) {
--x-card-group-label-fg: var(--x-fg);
--x-card-group-split-color: hsl(0 0% 100% / 0.1);
--x-card-group-bg-hover: hsl(0 0% 100% / 0.05);
}
}
.main {
display: grid;
grid-template-columns: minmax(5rem, 10rem) 1fr;
gap: var(--x-gutter-sm);
border-radius: var(--x-radius);
&:hover {
background: var(--x-card-group-bg-hover);
}
}
.label {
// display: flex;
// align-items: center;
color: var(--x-card-group-label-fg);
font-family: var(--x-text-font-family);
text-align: right;
word-break: normal;
&::after {
content: ":";
}
}
.content {
display: flex;
flex-wrap: wrap;
// align-items: center;
gap: var(--x-gutter-sm);
}

View File

@@ -0,0 +1,16 @@
import type { FC, ReactNode } from 'react';
import styles from './group.module.scss';
export const ModuleGroup: FC<{
label?: ReactNode;
children: ReactNode;
title?: string;
}> = ({ label = '', title = '', children }) => (
<div className={styles.main}>
{Boolean(label) && (
<div className={styles.label} title={title}>
{label}
</div>
)}
<div className={styles.content}>{children}</div>
</div>
);

View File

@@ -0,0 +1,5 @@
.container {
display: grid;
gap: var(--x-gutter);
padding: 0 var(--x-gutter);
}

View File

@@ -0,0 +1,34 @@
import { observer } from 'mobx-react-lite';
import { type FC, useEffect } from 'react';
import { ModulePriority } from '@/Components/Module/components/priority.ts';
import styles from './index.module.scss';
import { ModulePreset } from './preset.ts';
import { ModuleStorage } from './storage.ts';
import { ModuleStore } from './store.ts';
import type { SortedModuleProps } from './typings.ts';
export const Modules: FC = observer(() => {
const { setSortedModules, availableModules } = ModuleStore;
useEffect(() => {
const storageItems = ModuleStorage.getItems();
const sorted: SortedModuleProps[] = [];
for (const preset of ModulePreset.items) {
sorted.push({
id: preset.id,
priority:
Number(storageItems?.[preset.id]) ||
ModulePriority.indexOf(preset.id),
});
}
setSortedModules(sorted);
}, [setSortedModules]);
if (!availableModules.length) {
return null;
}
return (
<div className={styles.container}>
{availableModules.map(({ id, content: C }) => {
return <C key={id} />;
})}
</div>
);
});

View File

@@ -0,0 +1,51 @@
@use "../../Style/components/device.scss" as m;
:root {
--x-module-bg: hsl(0 0% 0% / 0.95);
--x-module-header-bg: hsl(0 0% 100% / 0.75);
--x-module-header-fg: hsl(0 0% 0%);
--x-module-header-title-fg: hsl(0 0% 0% / 0.7);
--x-module-header-title-bg: hsl(0 0% 0% / 0.1);
--x-module-body-bg: var(--x-module-header-bg);
--x-module-box-shadow: hsla(0 0% 20% 0.3) 0px -1px 0px hsl(0 0% 100%) 0px 1px 0px inset,
hsla(0 0% 20% 0.3) 0px -1px 0px inset hsl(0 0% 100%) 0px 1px 0px;
@media (prefers-color-scheme: dark) {
--x-module-bg: hsl(0 0% 15% / 0.95);
--x-module-header-bg: hsl(0 0% 100% / 0.1);
--x-module-header-fg: hsl(0 0% 100% / 0.7);
--x-module-header-title-fg: hsl(0 0% 100% / 0.7);
--x-module-header-title-bg: hsl(0 0% 100% / 0.1);
--x-module-body-bg: var(--x-module-header-bg);
--x-module-box-shadow: 0px 0px 0px 1px hsl(0 0% 0%) inset;
}
}
.main {
position: relative;
flex-grow: 1;
scroll-margin-top: 0;
}
.header {
display: flex;
align-items: center;
// z-index: 10;
border-radius: var(--x-radius) var(--x-radius) 0 0;
background: var(--x-module-header-bg);
// backdrop-filter: blur(5px);
// background: var(--x-module-header-bg);
padding: 1px;
// position: sticky;
// top: 0;
width: fit-content;
color: var(--x-module-header-fg);
font-size: 1rem;
white-space: nowrap;
}
.title {
font-weight: normal;
}
.body {
display: grid;
gap: var(--x-gutter-sm);
border-radius: 0 var(--x-radius) var(--x-radius) var(--x-radius);
background: var(--x-module-body-bg);
padding: var(--x-gutter);
}

View File

@@ -0,0 +1,28 @@
import type { FC, ReactNode } from 'react';
import { ModuleArrow } from '@/Components/Module/components/arrow.tsx';
import styles from './item.module.scss';
const ModuleItemTitle: FC<{
id: string;
title: string;
}> = ({ id, title }) => {
return (
<h2 className={styles.header}>
<ModuleArrow id={id} isDown={false} />
<span className={styles.title}>{title}</span>
<ModuleArrow id={id} isDown />
</h2>
);
};
export const ModuleItem: FC<{
id: string;
title: string;
children: ReactNode;
}> = ({ id, title, children, ...props }) => {
return (
<div className={styles.main} id={id} {...props}>
<ModuleItemTitle id={id} title={title} />
<div className={styles.body}>{children}</div>
</div>
);
};

View File

@@ -0,0 +1,29 @@
import { DatabaseLoader } from '@/Components/Database/components/loader.ts';
import { DiskUsageLoader } from '@/Components/DiskUsage/components/loader.ts';
import { MyInfoLoader } from '@/Components/MyInfo/components/loader.ts';
import { NetworkStatsLoader } from '@/Components/NetworkStats/components/loader.ts';
import { NodesLoader } from '@/Components/Nodes/components/loader.ts';
import { PhpExtensionsLoader } from '@/Components/PhpExtensions/components/loader.ts';
import { PhpInfoLoader } from '@/Components/PhpInfo/components/loader.ts';
import { PingLoader } from '@/Components/Ping/components/loader.ts';
import { ServerBenchmarkLoader } from '@/Components/ServerBenchmark/components/loader.ts';
import { ServerInfoLoader } from '@/Components/ServerInfo/components/loader.ts';
import { ServerStatusLoader } from '@/Components/ServerStatus/components/loader.ts';
import { TemperatureSensorLoader } from '@/Components/TemperatureSensor/components/loader.ts';
export const ModulePreset = {
items: [
NodesLoader,
TemperatureSensorLoader,
ServerStatusLoader,
NetworkStatsLoader,
DiskUsageLoader,
PingLoader,
ServerInfoLoader,
PhpInfoLoader,
PhpExtensionsLoader,
DatabaseLoader,
MyInfoLoader,
ServerBenchmarkLoader,
],
};

View File

@@ -0,0 +1,27 @@
import { PingConstants } from '@/Components/Ping/components/constants.ts';
import { DatabaseConstants } from '../../Database/components/constants.ts';
import { DiskUsageConstants } from '../../DiskUsage/components/constants.ts';
import { MyInfoConstants } from '../../MyInfo/components/constants.ts';
import { NetworkStatsConstants } from '../../NetworkStats/components/constants.ts';
import { NodesConstants } from '../../Nodes/components/constants.ts';
import { PhpExtensionsConstants } from '../../PhpExtensions/components/constants.ts';
import { PhpInfoConstants } from '../../PhpInfo/components/constants.ts';
import { ServerBenchmarkConstants } from '../../ServerBenchmark/components/constants.ts';
import { ServerInfoConstants } from '../../ServerInfo/components/constants.ts';
import { ServerStatusConstants } from '../../ServerStatus/components/constants.ts';
import { TemperatureSensorConstants } from '../../TemperatureSensor/components/constants.ts';
export const ModulePriority = [
NodesConstants.id,
TemperatureSensorConstants.id,
ServerStatusConstants.id,
NetworkStatsConstants.id,
DiskUsageConstants.id,
ServerInfoConstants.id,
PingConstants.id,
PhpInfoConstants.id,
PhpExtensionsConstants.id,
DatabaseConstants.id,
ServerBenchmarkConstants.id,
MyInfoConstants.id,
];

View File

@@ -0,0 +1,27 @@
import type { StoragePriorityItemProps } from './store.ts';
const STORAGE_KEY = 'module-priority';
export const ModuleStorage = {
getItems(): Record<string, number> {
const items = localStorage.getItem(STORAGE_KEY);
if (!items) {
return {};
}
try {
return JSON.parse(items) as Record<string, number>;
} catch {
return {};
}
},
setItems(items: Record<string, number>) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
},
getPriority(id: string): number {
return this.getItems()[id] || 0;
},
setPriority({ id, priority }: StoragePriorityItemProps) {
const items = this.getItems();
items[id] = priority;
this.setItems(items);
},
};

View File

@@ -0,0 +1,84 @@
import { configure, makeAutoObservable } from 'mobx';
import { ModulePriority } from '@/Components/Module/components/priority.ts';
import { PollStore } from '@/Components/Poll/components/store.ts';
import type { PollDataProps } from '@/Components/Poll/components/typings.ts';
import { ModulePreset } from './preset.ts';
import { ModuleStorage } from './storage.ts';
import type { ModuleProps, SortedModuleProps } from './typings.ts';
configure({
enforceActions: 'observed',
});
export interface StoragePriorityItemProps {
id: string;
priority: number;
}
const saveSortedStorage = (items: SortedModuleProps[]) => {
const sorted: Record<string, number> = {};
for (const item of items) {
sorted[item.id] = item.priority;
}
ModuleStorage.setItems(sorted);
};
class Main {
sortedModules: SortedModuleProps[] = [];
constructor() {
makeAutoObservable(this);
}
setSortedModules = (modules: SortedModuleProps[]) => {
this.sortedModules = modules.toSorted((a, b) => {
return a.priority - b.priority;
});
};
get availableModules(): ModuleProps[] {
const { pollData } = PollStore;
const items = ModulePreset.items
.filter(({ id }) => Boolean(pollData?.[id as keyof PollDataProps]))
.toSorted((a, b) => {
const moduleA = this.sortedModules.find((item) => item.id === a.id);
const moduleB = this.sortedModules.find((item) => item.id === b.id);
return (
Number(moduleA?.priority ?? ModulePriority.indexOf(a.id)) -
Number(moduleB?.priority ?? ModulePriority.indexOf(b.id))
);
});
return items;
}
moveUp = (id: string) => {
const i = this.sortedModules.findIndex((item) => item.id === id);
if (i === 0) {
return;
}
[this.sortedModules[i].priority, this.sortedModules[i - 1].priority] = [
this.sortedModules[i - 1].priority,
this.sortedModules[i].priority,
];
saveSortedStorage(this.sortedModules);
};
moveDown = (id: string) => {
const i = this.sortedModules.findIndex((item) => item.id === id);
if (i === this.sortedModules.length - 1) {
return;
}
[this.sortedModules[i].priority, this.sortedModules[i + 1].priority] = [
this.sortedModules[i + 1].priority,
this.sortedModules[i].priority,
];
saveSortedStorage(this.sortedModules);
};
get disabledMoveUpId(): string {
const items = this.availableModules;
if (items.length <= 1) {
return '';
}
return items[0].id;
}
get disabledMoveDownId(): string {
const items = this.availableModules;
if (items.length <= 1) {
return '';
}
return items.at(-1)?.id ?? '';
}
}
export const ModuleStore = new Main();

View File

@@ -0,0 +1,11 @@
import type { FC } from 'react';
export interface ModuleProps {
id: string;
content: FC;
nav: FC;
}
export interface SortedModuleProps {
id: string;
priority: number;
}