1
0
Fork 0

Extract modal to ui kit

This commit is contained in:
Pabloader 2026-04-10 09:05:11 +00:00
parent 810035547c
commit 3f675966a6
19 changed files with 253 additions and 281 deletions

View File

@ -0,0 +1,125 @@
@import './global.css';
.dialog {
position: fixed;
inset: 0;
width: 100%;
max-width: 100%;
height: 100%;
max-height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
outline: none;
padding: 0;
&::backdrop {
background: rgba(0, 0, 0, 0.5);
}
}
.modal {
background: var(--bg);
color: var(--text);
border-radius: 8px;
width: 90%;
max-width: 960px;
height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.title {
font-size: 18px;
font-weight: bold;
color: var(--text);
margin: 0;
}
.closeButton {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: var(--text-muted);
background: transparent;
border: none;
border-radius: var(--radius);
cursor: pointer;
&:hover {
color: var(--text);
background: var(--bg-hover);
}
}
.body {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
display: flex;
flex-direction: column;
width: 160px;
flex-shrink: 0;
border-right: 1px solid var(--border);
padding: 8px;
gap: 2px;
}
.content {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
}
.footer {
padding: 16px 20px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 8px;
flex-shrink: 0;
}
@media (max-width: 600px) {
.body {
flex-direction: column;
}
.sidebar {
flex-direction: row;
width: 100%;
overflow-x: auto;
border-right: none;
border-bottom: 1px solid var(--border);
padding: 4px 8px;
flex-shrink: 0;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.footer {
padding: 12px 16px;
}
}

View File

@ -0,0 +1,55 @@
import type { ComponentChildren } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
import { X } from "lucide-preact";
import clsx from "clsx";
import styles from '../assets/modal.module.css';
interface IProps {
open: boolean;
title?: string;
onClose?: () => void;
sidebar?: ComponentChildren;
footer?: ComponentChildren;
class?: string;
className?: string;
children?: ComponentChildren;
}
export const Modal = ({ children, open, title, onClose, sidebar, footer, ['class']: cls, className }: IProps) => {
const ref = useRef<HTMLDialogElement>(null);
useEffect(() => {
if (open) {
ref.current?.showModal();
} else {
ref.current?.close();
}
}, [open]);
const handleBackdropMouseDown = useCallback((e: MouseEvent) => {
if (e.target === ref.current) onClose?.();
}, [onClose]);
return (
<dialog ref={ref} class={styles.dialog} onMouseDown={handleBackdropMouseDown}>
<div class={clsx(styles.modal, cls ?? className)} onMouseDown={(e) => e.stopPropagation()}>
{(title != null || onClose) && (
<div class={styles.header}>
{title && <h2 class={styles.title}>{title}</h2>}
{onClose && (
<button class={styles.closeButton} onClick={onClose}>
<X size={18} />
</button>
)}
</div>
)}
<div class={styles.body}>
{sidebar && <div class={styles.sidebar}>{sidebar}</div>}
<div class={styles.content}>{children}</div>
</div>
{footer && <div class={styles.footer}>{footer}</div>}
</div>
</dialog>
);
};

View File

@ -1,42 +0,0 @@
.dialog {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: 100%;
max-width: 1000px;
height: fit-content;
max-height: 80dvh;
overflow: hidden;
background-color: var(--backgroundColor, #333333);
color: var(--color, white);
border: var(--border, 1px solid white);
outline: none;
padding: 0;
border-radius: var(--border-radius, 0);
&::backdrop {
backdrop-filter: blur(5px);
}
>.content {
display: flex;
flex-direction: column;
max-height: 80dvh;
overflow: hidden;
padding: 30px;
>.close {
font-family: var(--emojiFont, sans-serif);
font-size: 12px;
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
user-select: none;
}
}
}

View File

@ -1,40 +0,0 @@
import type { ComponentChildren } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
import styles from './Modal.module.css';
interface IProps {
open: boolean;
class?: string;
className?: string;
onClose?: () => void;
children?: ComponentChildren;
}
export const Modal = ({ children, open, onClose, ['class']: cls, className }: IProps) => {
const ref = useRef<HTMLDialogElement>(null);
const handleClickWrapper = useCallback((e: MouseEvent) => {
if (e.currentTarget instanceof HTMLDialogElement) {
onClose?.();
} else {
e.stopPropagation();
}
}, [onClose]);
useEffect(() => {
if (open) {
ref.current?.showModal();
} else {
ref.current?.close();
}
}, [open]);
return (
<dialog ref={ref} onMouseDown={handleClickWrapper} class={styles.dialog}>
<div class={`${styles.content} ${cls ?? className ?? ''}`} onMouseDown={handleClickWrapper}>
<div class={styles.close} onClick={onClose}></div>
{children}
</div>
</dialog>
);
};

View File

@ -2,7 +2,8 @@
width: 480px;
}
.loading, .error {
.loading,
.error {
color: var(--text-muted);
font-size: 14px;
text-align: center;
@ -18,7 +19,8 @@
border-collapse: collapse;
font-size: 13px;
th, td {
th,
td {
padding: 7px 10px;
text-align: left;
border-bottom: 1px solid var(--border);

View File

@ -65,7 +65,9 @@
color: var(--bg);
transition: opacity var(--transition);
&:hover { opacity: 0.85; }
&:hover {
opacity: 0.85;
}
}
.deleteBtn {
@ -77,5 +79,9 @@
margin-left: auto;
transition: all var(--transition);
&:hover { background: var(--accent); border-color: var(--accent); color: var(--bg); }
&:hover {
background: var(--accent);
border-color: var(--accent);
color: var(--bg);
}
}

View File

@ -22,7 +22,9 @@
color: var(--bg);
transition: opacity var(--transition);
&:hover { opacity: 0.85; }
&:hover {
opacity: 0.85;
}
}
.clearBtn {
@ -33,5 +35,7 @@
color: var(--text);
transition: background var(--transition);
&:hover { background: var(--bg-hover); }
&:hover {
background: var(--bg-hover);
}
}

View File

@ -59,7 +59,8 @@
}
}
.userStats, .networkStats {
.userStats,
.networkStats {
display: flex;
flex-direction: column;
}
@ -83,7 +84,8 @@
border-collapse: collapse;
font-size: 12px;
th, td {
th,
td {
padding: 5px 6px;
text-align: left;
border-bottom: 1px solid var(--border);

View File

@ -32,7 +32,8 @@ button {
}
}
input, textarea {
input,
textarea {
background: var(--bg-active);
border: 1px solid var(--border);
border-radius: var(--radius);

View File

@ -3,7 +3,7 @@ import { useEffect, useState } from "preact/hooks";
import { useHordeState } from "../../contexts/state";
import { fetchLeaderboard, type LeaderboardEntry } from "../../utils/api";
import { formatNumber, formatTime } from "@common/utils";
import { Modal } from "@common/components/modal/Modal";
import { Modal } from "@common/components/Modal";
import modalStyles from "../../assets/modal.module.css";
import styles from "../../assets/leaderboard-modal.module.css";

View File

@ -2,7 +2,7 @@ import clsx from "clsx";
import { useEffect, useState } from "preact/hooks";
import { useHordeState } from "../../contexts/state";
import { fetchWorker, updateWorker, deleteWorker, type WorkerData } from "../../utils/api";
import { Modal } from "@common/components/modal/Modal";
import { Modal } from "@common/components/Modal";
import modalStyles from "../../assets/modal.module.css";
import styles from "../../assets/manage-workers-modal.module.css";

View File

@ -2,7 +2,7 @@ import clsx from "clsx";
import { useEffect } from "preact/hooks";
import { useHordeState } from "../../contexts/state";
import { useInputState } from "@common/hooks/useInputState";
import { Modal } from "@common/components/modal/Modal";
import { Modal } from "@common/components/Modal";
import modalStyles from "../../assets/modal.module.css";
import styles from "../../assets/options-modal.module.css";

View File

@ -1,77 +1,3 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: var(--bg);
border-radius: 8px;
width: 90%;
max-width: 960px;
height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border);
}
.title {
font-size: 18px;
font-weight: bold;
color: var(--text);
margin: 0;
}
.closeButton {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
font-size: 20px;
color: var(--text-muted);
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
color: var(--text);
background: var(--bg-hover);
}
}
.body {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
display: flex;
flex-direction: column;
width: 160px;
flex-shrink: 0;
border-right: 1px solid var(--border);
padding: 8px;
gap: 2px;
}
.menuItem {
display: flex;
align-items: center;
@ -98,14 +24,6 @@
}
}
.content {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
}
.form {
composes: form from '@common/assets/ui.module.css';
}
@ -153,14 +71,6 @@
min-height: unset;
}
.footer {
padding: 16px 20px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 8px;
}
.button {
composes: button from '@common/assets/ui.module.css';
}
@ -227,25 +137,6 @@
}
@media (max-width: 600px) {
.body {
flex-direction: column;
}
.sidebar {
flex-direction: row;
width: 100%;
overflow-x: auto;
border-right: none;
border-bottom: 1px solid var(--border);
padding: 4px 8px;
flex-shrink: 0;
scrollbar-width: none;
}
.sidebar::-webkit-scrollbar {
display: none;
}
.menuItem {
flex: 1;
justify-content: center;
@ -254,8 +145,4 @@
padding: 8px;
font-size: 13px;
}
.footer {
padding: 12px 16px;
}
}

View File

@ -1,6 +1,6 @@
import clsx from "clsx";
import { X } from "lucide-preact";
import { useState } from "preact/hooks";
import { Modal } from "@common/components/Modal";
import styles from "../assets/settings-modal.module.css";
import { BannedTokensSettings } from "./settings/banned-tokens";
import { ChatSystemInstructionSettings } from "./settings/chat-system-instruction";
@ -15,72 +15,44 @@ interface Props {
type Tab = "banned-tokens" | "system-instruction" | "chat-system-instruction" | "continue-prompt" | "connection" | "user";
const TABS: { id: Tab; label: string }[] = [
{ id: "connection", label: "Connection" },
{ id: "user", label: "User" },
{ id: "system-instruction", label: "System Instruction" },
{ id: "continue-prompt", label: "Continue Prompt" },
{ id: "chat-system-instruction", label: "Chat System Instruction" },
{ id: "banned-tokens", label: "Banned Tokens" },
];
export const SettingsModal = ({ onClose }: Props) => {
const [activeTab, setActiveTab] = useState<Tab>("connection");
return (
<div class={styles.overlay} onClick={onClose}>
<div class={styles.modal} onClick={(e) => e.stopPropagation()}>
<div class={styles.header}>
<h2 class={styles.title}>Settings</h2>
<button class={styles.closeButton} onClick={onClose}>
<X size={20} />
</button>
</div>
<div class={styles.body}>
<nav class={styles.sidebar}>
<button
class={clsx(styles.menuItem, activeTab === "connection" && styles.active)}
onClick={() => setActiveTab("connection")}
>
Connection
</button>
<button
class={clsx(styles.menuItem, activeTab === "user" && styles.active)}
onClick={() => setActiveTab("user")}
>
User
</button>
<button
class={clsx(styles.menuItem, activeTab === "system-instruction" && styles.active)}
onClick={() => setActiveTab("system-instruction")}
>
System Instruction
</button>
<button
class={clsx(styles.menuItem, activeTab === "continue-prompt" && styles.active)}
onClick={() => setActiveTab("continue-prompt")}
>
Continue Prompt
</button>
<button
class={clsx(styles.menuItem, activeTab === "chat-system-instruction" && styles.active)}
onClick={() => setActiveTab("chat-system-instruction")}
>
Chat System Instruction
</button>
<button
class={clsx(styles.menuItem, activeTab === "banned-tokens" && styles.active)}
onClick={() => setActiveTab("banned-tokens")}
>
Banned Tokens
</button>
</nav>
<div class={styles.content}>
{activeTab === "user" && <UserSettings />}
{activeTab === "banned-tokens" && <BannedTokensSettings />}
{activeTab === "system-instruction" && <SystemInstructionSettings />}
{activeTab === "chat-system-instruction" && <ChatSystemInstructionSettings />}
{activeTab === "continue-prompt" && <ContinuePromptSettings />}
{activeTab === "connection" && <ConnectionSettings />}
</div>
</div>
<div class={styles.footer}>
<button onClick={onClose} class={clsx(styles.button, styles.buttonSecondary)}>
Done
</button>
</div>
</div>
</div>
<Modal
open
onClose={onClose}
title="Settings"
sidebar={TABS.map(({ id, label }) => (
<button
key={id}
class={clsx(styles.menuItem, activeTab === id && styles.active)}
onClick={() => setActiveTab(id)}
>
{label}
</button>
))}
footer={
<button onClick={onClose} class={clsx(styles.button, styles.buttonSecondary)}>
Done
</button>
}
>
{activeTab === "user" && <UserSettings />}
{activeTab === "banned-tokens" && <BannedTokensSettings />}
{activeTab === "system-instruction" && <SystemInstructionSettings />}
{activeTab === "chat-system-instruction" && <ChatSystemInstructionSettings />}
{activeTab === "continue-prompt" && <ContinuePromptSettings />}
{activeTab === "connection" && <ConnectionSettings />}
</Modal>
);
};