refactor progress
This commit is contained in:
51
packages/admin/src/components/ErrorDisplay.tsx
Normal file
51
packages/admin/src/components/ErrorDisplay.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {VStack} from '@fluxer/admin/src/components/ui/Layout/VStack';
|
||||
import {Heading, Text} from '@fluxer/admin/src/components/ui/Typography';
|
||||
import {Alert} from '@fluxer/ui/src/components/Alert';
|
||||
import {Card} from '@fluxer/ui/src/components/Card';
|
||||
import type {FC} from 'hono/jsx';
|
||||
|
||||
interface ErrorAlertProps {
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface ErrorCardProps {
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const ErrorAlert: FC<ErrorAlertProps> = ({error}) => <Alert variant="error">{error}</Alert>;
|
||||
|
||||
export const ErrorCard: FC<ErrorCardProps> = ({title, message}) => (
|
||||
<Card padding="md">
|
||||
<VStack gap={4}>
|
||||
<Heading level={3} size="base">
|
||||
{title}
|
||||
</Heading>
|
||||
<Text size="sm" color="muted">
|
||||
{message}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Card>
|
||||
);
|
||||
91
packages/admin/src/components/Icons.tsx
Normal file
91
packages/admin/src/components/Icons.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
export function PaperclipIcon({color}: {color: string}) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class={`inline-block h-3 w-3 ${color}`}>
|
||||
<rect width="256" height="256" fill="none" />
|
||||
<path
|
||||
d="M108.71,197.23l-5.11,5.11a46.63,46.63,0,0,1-66-.05h0a46.63,46.63,0,0,1,.06-65.89L72.4,101.66a46.62,46.62,0,0,1,65.94,0h0A46.34,46.34,0,0,1,150.78,124"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
/>
|
||||
<path
|
||||
d="M147.29,58.77l5.11-5.11a46.62,46.62,0,0,1,65.94,0h0a46.62,46.62,0,0,1,0,65.94L193.94,144,183.6,154.34a46.63,46.63,0,0,1-66-.05h0A46.46,46.46,0,0,1,105.22,132"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function CheckmarkIcon({color}: {color: string}) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class={`inline-block h-4 w-4 ${color}`}>
|
||||
<rect width="256" height="256" fill="none" />
|
||||
<polyline
|
||||
points="40 144 96 200 224 72"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function XIcon({color}: {color: string}) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class={`inline-block h-4 w-4 ${color}`}>
|
||||
<rect width="256" height="256" fill="none" />
|
||||
<line
|
||||
x1="200"
|
||||
y1="56"
|
||||
x2="56"
|
||||
y2="200"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
/>
|
||||
<line
|
||||
x1="200"
|
||||
y1="200"
|
||||
x2="56"
|
||||
y2="56"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
388
packages/admin/src/components/Layout.tsx
Normal file
388
packages/admin/src/components/Layout.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {getAccessibleSections} from '@fluxer/admin/src/Navigation';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {Flash} from '@fluxer/hono/src/Flash';
|
||||
import type {UserAdminResponse} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
import {CsrfInput} from '@fluxer/ui/src/components/CsrfInput';
|
||||
import {FlashMessage} from '@fluxer/ui/src/components/Flash';
|
||||
import {formatDiscriminator, getUserAvatarUrl} from '@fluxer/ui/src/utils/FormatUser';
|
||||
import type {FC, PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
interface LayoutProps {
|
||||
title: string;
|
||||
activePage: string;
|
||||
config: Config;
|
||||
session: Session;
|
||||
currentAdmin: UserAdminResponse | undefined;
|
||||
flash: Flash | undefined;
|
||||
autoRefresh?: boolean;
|
||||
assetVersion: string;
|
||||
csrfToken: string;
|
||||
extraScripts?: string;
|
||||
inspectedVoiceRegionId?: string;
|
||||
}
|
||||
|
||||
function cacheBustedAsset(basePath: string, assetVersion: string, path: string): string {
|
||||
return `${basePath}${path}?t=${assetVersion}`;
|
||||
}
|
||||
|
||||
const Head: FC<{
|
||||
title: string;
|
||||
basePath: string;
|
||||
staticCdnEndpoint: string;
|
||||
assetVersion: string;
|
||||
autoRefresh: boolean | undefined;
|
||||
}> = ({title, basePath, staticCdnEndpoint, assetVersion, autoRefresh}) => (
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
{autoRefresh && <meta http-equiv="refresh" content="3" />}
|
||||
<title>{title} ~ Fluxer Admin</title>
|
||||
<link rel="stylesheet" href={`${staticCdnEndpoint}/fonts/ibm-plex.css`} />
|
||||
<link rel="stylesheet" href={`${staticCdnEndpoint}/fonts/bricolage.css`} />
|
||||
<link rel="stylesheet" href={cacheBustedAsset(basePath, assetVersion, '/static/app.css')} />
|
||||
<link rel="icon" type="image/x-icon" href={`${staticCdnEndpoint}/web/favicon.ico`} />
|
||||
<link rel="apple-touch-icon" href={`${staticCdnEndpoint}/web/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${staticCdnEndpoint}/web/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${staticCdnEndpoint}/web/favicon-16x16.png`} />
|
||||
</head>
|
||||
);
|
||||
|
||||
const SidebarSection: FC<PropsWithChildren<{title: string}>> = ({title, children}) => (
|
||||
<div>
|
||||
<div class="mb-2 text-neutral-400 text-xs uppercase">{title}</div>
|
||||
<div class="space-y-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SidebarItem: FC<{title: string; path: string; active: boolean; basePath: string}> = ({
|
||||
title,
|
||||
path,
|
||||
active,
|
||||
basePath,
|
||||
}) => {
|
||||
const classes = active
|
||||
? 'block px-3 py-2 rounded bg-neutral-800 text-white text-sm transition-colors'
|
||||
: 'block px-3 py-2 rounded text-neutral-300 hover:bg-neutral-800 hover:text-white text-sm transition-colors';
|
||||
|
||||
return (
|
||||
<a href={`${basePath}${path}`} class={classes} {...(active ? {'data-active': ''} : {})}>
|
||||
{title}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const Sidebar: FC<{
|
||||
activePage: string;
|
||||
adminAcls: Array<string>;
|
||||
basePath: string;
|
||||
selfHosted: boolean;
|
||||
inspectedVoiceRegionId?: string;
|
||||
}> = ({activePage, adminAcls, basePath, selfHosted, inspectedVoiceRegionId}) => {
|
||||
const sections = getAccessibleSections(adminAcls, {selfHosted, inspectedVoiceRegionId});
|
||||
|
||||
return (
|
||||
<div
|
||||
data-sidebar=""
|
||||
class="fixed inset-y-0 left-0 z-40 flex h-screen w-64 -translate-x-full transform flex-col bg-neutral-900 text-white shadow-xl transition-transform duration-200 ease-in-out lg:static lg:inset-auto lg:translate-x-0 lg:shadow-none"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3 border-neutral-800 border-b p-6">
|
||||
<a href={`${basePath}/users`}>
|
||||
<h1 class="font-semibold text-base">Fluxer Admin</h1>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
data-sidebar-close=""
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-neutral-200 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-white/40 lg:hidden"
|
||||
aria-label="Close sidebar"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-scrollbar flex-1 space-y-4 overflow-y-auto p-4">
|
||||
{sections.map((section) => (
|
||||
<SidebarSection title={section.title}>
|
||||
{section.items.map((item) => (
|
||||
<SidebarItem
|
||||
title={item.title}
|
||||
path={item.path}
|
||||
active={activePage === item.activeKey}
|
||||
basePath={basePath}
|
||||
/>
|
||||
))}
|
||||
</SidebarSection>
|
||||
))}
|
||||
</nav>
|
||||
<script
|
||||
defer
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: SIDEBAR_ACTIVE_SCROLL_SCRIPT,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Header: FC<{
|
||||
config: Config;
|
||||
session: Session;
|
||||
currentAdmin: UserAdminResponse | undefined;
|
||||
assetVersion: string;
|
||||
csrfToken: string;
|
||||
}> = ({config, session, currentAdmin, assetVersion, csrfToken}) => (
|
||||
<header class="sticky top-0 z-10 flex items-center justify-between gap-4 border-neutral-200 border-b bg-white px-4 py-4 sm:px-6 lg:px-8">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
data-sidebar-toggle=""
|
||||
class="inline-flex items-center justify-center rounded-md border border-neutral-300 p-2 text-neutral-700 hover:bg-neutral-100 focus:outline-none focus:ring-2 focus:ring-neutral-400 lg:hidden"
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
{currentAdmin ? (
|
||||
<a
|
||||
href={`${config.basePath}/users/${session.userId}`}
|
||||
class="flex items-center gap-3 transition-opacity hover:opacity-80"
|
||||
>
|
||||
<img
|
||||
src={getUserAvatarUrl(
|
||||
config.mediaEndpoint,
|
||||
config.staticCdnEndpoint,
|
||||
currentAdmin.id,
|
||||
currentAdmin.avatar,
|
||||
true,
|
||||
assetVersion,
|
||||
)}
|
||||
alt={`${currentAdmin.username}'s avatar`}
|
||||
class="h-10 w-10 rounded-full"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-neutral-900 text-sm">
|
||||
{currentAdmin.username}#{formatDiscriminator(currentAdmin.discriminator)}
|
||||
</div>
|
||||
<div class="text-neutral-500 text-xs">Admin</div>
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<div class="text-neutral-600 text-sm">
|
||||
Logged in as:{' '}
|
||||
<a
|
||||
href={`${config.basePath}/users/${session.userId}`}
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline"
|
||||
>
|
||||
{session.userId}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<form method="post" action={`${config.basePath}/logout`}>
|
||||
<CsrfInput token={csrfToken} />
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded border border-neutral-300 px-4 py-2 font-medium text-neutral-700 text-sm transition-colors hover:border-neutral-400 hover:text-neutral-900"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</header>
|
||||
);
|
||||
|
||||
const SIDEBAR_ACTIVE_SCROLL_SCRIPT = `
|
||||
(function () {
|
||||
var el = document.querySelector('[data-active]');
|
||||
if (el) el.scrollIntoView({block: 'nearest'});
|
||||
})();
|
||||
`;
|
||||
|
||||
const SIDEBAR_SCRIPT = `
|
||||
(function () {
|
||||
var sidebar = document.querySelector('[data-sidebar]');
|
||||
var overlay = document.querySelector('[data-sidebar-overlay]');
|
||||
var toggles = document.querySelectorAll('[data-sidebar-toggle]');
|
||||
var closes = document.querySelectorAll('[data-sidebar-close]');
|
||||
if (!sidebar || !overlay) return;
|
||||
|
||||
function open() {
|
||||
sidebar.classList.remove('-translate-x-full');
|
||||
overlay.classList.remove('hidden');
|
||||
document.body.classList.add('overflow-hidden');
|
||||
}
|
||||
|
||||
function close() {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
overlay.classList.add('hidden');
|
||||
document.body.classList.remove('overflow-hidden');
|
||||
}
|
||||
|
||||
toggles.forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
if (sidebar.classList.contains('-translate-x-full')) {
|
||||
open();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
closes.forEach(function (btn) {
|
||||
btn.addEventListener('click', close);
|
||||
});
|
||||
overlay.addEventListener('click', close);
|
||||
|
||||
window.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape') close();
|
||||
});
|
||||
|
||||
function syncForDesktop() {
|
||||
if (window.innerWidth >= 1024) {
|
||||
overlay.classList.add('hidden');
|
||||
document.body.classList.remove('overflow-hidden');
|
||||
sidebar.classList.remove('-translate-x-full');
|
||||
} else {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', syncForDesktop);
|
||||
syncForDesktop();
|
||||
})();
|
||||
`;
|
||||
|
||||
const SH_LINK_REWRITE_SCRIPT = `
|
||||
(function () {
|
||||
if (window.location.search.indexOf('sh=1') === -1) return;
|
||||
|
||||
function rewriteHref(el) {
|
||||
var href = el.getAttribute('href');
|
||||
if (!href || href.indexOf('sh=1') >= 0) return;
|
||||
if (href.charAt(0) === '#' || href.indexOf('javascript:') === 0 || href.indexOf('data:') === 0 || href.indexOf('mailto:') === 0) return;
|
||||
if (href.indexOf('://') >= 0) {
|
||||
try {
|
||||
var url = new URL(href);
|
||||
if (url.origin !== window.location.origin) return;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
var sep = href.indexOf('?') >= 0 ? '&' : '?';
|
||||
el.setAttribute('href', href + sep + 'sh=1');
|
||||
}
|
||||
|
||||
function rewriteAction(form) {
|
||||
var action = form.getAttribute('action');
|
||||
if (!action || action.indexOf('sh=1') >= 0) return;
|
||||
var sep = action.indexOf('?') >= 0 ? '&' : '?';
|
||||
form.setAttribute('action', action + sep + 'sh=1');
|
||||
}
|
||||
|
||||
document.querySelectorAll('a[href]').forEach(rewriteHref);
|
||||
document.querySelectorAll('form[action]').forEach(rewriteAction);
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var a = e.target.closest('a[href]');
|
||||
if (a) rewriteHref(a);
|
||||
}, true);
|
||||
|
||||
document.addEventListener('submit', function (e) {
|
||||
var form = e.target.closest('form[action]');
|
||||
if (form) rewriteAction(form);
|
||||
}, true);
|
||||
})();
|
||||
`;
|
||||
|
||||
export function Layout({
|
||||
title,
|
||||
activePage,
|
||||
config,
|
||||
session,
|
||||
currentAdmin,
|
||||
flash,
|
||||
autoRefresh,
|
||||
assetVersion,
|
||||
csrfToken,
|
||||
extraScripts,
|
||||
inspectedVoiceRegionId,
|
||||
children,
|
||||
}: PropsWithChildren<LayoutProps>) {
|
||||
const adminAcls = currentAdmin?.acls ?? [];
|
||||
|
||||
return (
|
||||
<html lang="en" data-base-path={config.basePath}>
|
||||
<Head
|
||||
title={title}
|
||||
basePath={config.basePath}
|
||||
staticCdnEndpoint={config.staticCdnEndpoint}
|
||||
assetVersion={assetVersion}
|
||||
autoRefresh={autoRefresh}
|
||||
/>
|
||||
<body class="min-h-screen overflow-hidden bg-neutral-50">
|
||||
<div class="flex h-screen">
|
||||
<Sidebar
|
||||
activePage={activePage}
|
||||
adminAcls={adminAcls}
|
||||
basePath={config.basePath}
|
||||
selfHosted={config.selfHosted}
|
||||
inspectedVoiceRegionId={inspectedVoiceRegionId}
|
||||
/>
|
||||
<div data-sidebar-overlay="" class="fixed inset-0 z-30 hidden bg-black/50 lg:hidden" />
|
||||
<div class="flex h-screen w-full flex-1 flex-col overflow-y-auto">
|
||||
<Header
|
||||
config={config}
|
||||
session={session}
|
||||
currentAdmin={currentAdmin}
|
||||
assetVersion={assetVersion}
|
||||
csrfToken={csrfToken}
|
||||
/>
|
||||
<main class="flex-1 p-4 sm:p-6 lg:p-8">
|
||||
<div class="mx-auto w-full max-w-7xl">
|
||||
{flash && (
|
||||
<div class="mb-6">
|
||||
<FlashMessage flash={flash} />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script defer dangerouslySetInnerHTML={{__html: SIDEBAR_SCRIPT}} />
|
||||
<script defer dangerouslySetInnerHTML={{__html: SH_LINK_REWRITE_SCRIPT}} />
|
||||
{extraScripts && <script defer dangerouslySetInnerHTML={{__html: extraScripts}} />}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
204
packages/admin/src/components/MessageList.tsx
Normal file
204
packages/admin/src/components/MessageList.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {PaperclipIcon} from '@fluxer/admin/src/components/Icons';
|
||||
import {CSRF_FORM_FIELD} from '@fluxer/constants/src/Cookies';
|
||||
import {formatUserTag} from '@fluxer/ui/src/utils/FormatUser';
|
||||
import type {FC} from 'hono/jsx';
|
||||
|
||||
interface Attachment {
|
||||
url: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
author_id: string;
|
||||
author_username: string;
|
||||
author_discriminator: string;
|
||||
channel_id: string;
|
||||
guild_id?: string | null;
|
||||
attachments: Array<Attachment>;
|
||||
}
|
||||
|
||||
interface MessageRowProps {
|
||||
basePath: string;
|
||||
message: Message;
|
||||
includeDeleteButton: boolean;
|
||||
}
|
||||
|
||||
function buildMessageLookupHref(basePath: string, channelId: string, messageId: string): string {
|
||||
return `${basePath}/messages?channel_id=${channelId}&message_id=${messageId}&context_limit=50`;
|
||||
}
|
||||
|
||||
const MessageRow: FC<MessageRowProps> = ({basePath, message, includeDeleteButton}) => (
|
||||
<div
|
||||
class="group flex items-start gap-3 px-4 py-2 transition-colors hover:bg-neutral-50"
|
||||
data-message-id={message.id}
|
||||
>
|
||||
<div class="flex-shrink-0 pt-0.5">
|
||||
<a
|
||||
href={`${basePath}/users/${message.author_id}`}
|
||||
class="cursor-pointer text-neutral-900 text-xs hover:underline"
|
||||
title={message.author_id}
|
||||
>
|
||||
{formatUserTag(message.author_username, message.author_discriminator)}
|
||||
</a>
|
||||
<div class="text-neutral-500 text-xs">{message.timestamp}</div>
|
||||
</div>
|
||||
<div class="message-content min-w-0 flex-1">
|
||||
<div class="whitespace-pre-wrap break-words text-neutral-900 text-sm">{message.content}</div>
|
||||
{message.attachments.length > 0 && (
|
||||
<div class="mt-2 space-y-1">
|
||||
{message.attachments.map((att) => (
|
||||
<div class="flex items-center gap-1 text-xs">
|
||||
<PaperclipIcon color="text-neutral-500" />
|
||||
<a href={att.url} target="_blank" class="text-blue-600 hover:underline">
|
||||
{att.filename}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div class="mt-1 flex flex-wrap items-center gap-2 text-neutral-400 text-xs">
|
||||
<span>ID: {message.id}</span>
|
||||
{message.channel_id && (
|
||||
<>
|
||||
<span>|</span>
|
||||
<a
|
||||
href={buildMessageLookupHref(basePath, message.channel_id, message.id)}
|
||||
class="text-neutral-500 hover:text-neutral-700 hover:underline"
|
||||
>
|
||||
Channel: {message.channel_id}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
{message.guild_id && (
|
||||
<>
|
||||
<span>|</span>
|
||||
<a
|
||||
href={`${basePath}/guilds/${message.guild_id}`}
|
||||
class="text-neutral-500 hover:text-neutral-700 hover:underline"
|
||||
>
|
||||
Guild: {message.guild_id}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{includeDeleteButton && message.channel_id && (
|
||||
<div class="flex-shrink-0 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded px-2 py-1 text-red-600 text-xs transition-colors hover:bg-red-50 hover:text-red-700"
|
||||
title="Delete message"
|
||||
onclick={`deleteMessage('${message.channel_id}', '${message.id}', this)`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export function MessageList({
|
||||
basePath,
|
||||
messages,
|
||||
includeDeleteButton,
|
||||
}: {
|
||||
basePath: string;
|
||||
messages: Array<Message>;
|
||||
includeDeleteButton: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div class="space-y-1">
|
||||
{messages.map((message) => (
|
||||
<MessageRow basePath={basePath} message={message} includeDeleteButton={includeDeleteButton} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createMessageDeletionScriptBody(csrfToken: string): string {
|
||||
return `
|
||||
function deleteMessage(channelId, messageId, button) {
|
||||
const csrfToken = ${JSON.stringify(csrfToken)};
|
||||
if (!confirm('Are you sure you want to delete this message?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('channel_id', channelId);
|
||||
formData.append('message_id', messageId);
|
||||
formData.append('${CSRF_FORM_FIELD}', csrfToken);
|
||||
|
||||
button.disabled = true;
|
||||
button.textContent = 'Deleting...';
|
||||
|
||||
const basePath = document.documentElement.dataset.basePath || '';
|
||||
fetch(basePath + '/messages?action=delete', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(async response => {
|
||||
if (response.ok) {
|
||||
const messageRow = button.closest('[data-message-id]');
|
||||
if (messageRow) {
|
||||
messageRow.style.opacity = '0.5';
|
||||
messageRow.style.pointerEvents = 'none';
|
||||
const messageContent = messageRow.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.style.textDecoration = 'line-through';
|
||||
}
|
||||
}
|
||||
const buttonContainer = button.parentElement;
|
||||
const deletedBadge = document.createElement('span');
|
||||
deletedBadge.className = 'px-2 py-1 bg-red-100 text-red-800 text-xs rounded opacity-100';
|
||||
deletedBadge.textContent = 'DELETED';
|
||||
button.replaceWith(deletedBadge);
|
||||
if (buttonContainer) {
|
||||
buttonContainer.style.opacity = '1';
|
||||
}
|
||||
} else {
|
||||
button.disabled = false;
|
||||
button.textContent = 'Delete';
|
||||
let errorMessage = 'Failed to delete message';
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
if (errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
}
|
||||
} catch (e) {}
|
||||
alert(errorMessage);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
button.disabled = false;
|
||||
button.textContent = 'Delete';
|
||||
alert('Error deleting message: ' + (error.message || 'Unknown error'));
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
124
packages/admin/src/components/UserProfileBadges.tsx
Normal file
124
packages/admin/src/components/UserProfileBadges.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import {hasBigIntFlag, parseBigIntOrZero} from '@fluxer/admin/src/utils/Bigint';
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import {UserFlags, UserPremiumTypes} from '@fluxer/constants/src/UserConstants';
|
||||
import {getFormattedShortDate} from '@fluxer/date_utils/src/DateFormatting';
|
||||
import type {UserAdminResponse} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
import {normalizeEndpoint} from '@fluxer/ui/src/utils/AvatarMediaUtils';
|
||||
|
||||
interface BadgeDefinition {
|
||||
key: string;
|
||||
iconUrl: string;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
export interface UserProfileBadgesProps {
|
||||
config: Config;
|
||||
user: UserAdminResponse;
|
||||
size?: 'sm' | 'md';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function UserProfileBadges({config, user, size = 'sm', class: className}: UserProfileBadgesProps) {
|
||||
const flags = parseBigIntOrZero(user.flags);
|
||||
const badges: Array<BadgeDefinition> = [];
|
||||
const isSelfHosted = config.selfHosted;
|
||||
const staticCdnEndpoint = normalizeEndpoint(config.staticCdnEndpoint);
|
||||
|
||||
if (hasBigIntFlag(flags, UserFlags.STAFF)) {
|
||||
badges.push({
|
||||
key: 'staff',
|
||||
iconUrl: `${staticCdnEndpoint}/badges/staff.svg`,
|
||||
tooltip: 'Fluxer Staff',
|
||||
});
|
||||
}
|
||||
|
||||
if (!isSelfHosted && hasBigIntFlag(flags, UserFlags.CTP_MEMBER)) {
|
||||
badges.push({
|
||||
key: 'ctp',
|
||||
iconUrl: `${staticCdnEndpoint}/badges/ctp.svg`,
|
||||
tooltip: 'Fluxer Community Team',
|
||||
});
|
||||
}
|
||||
|
||||
if (!isSelfHosted && hasBigIntFlag(flags, UserFlags.PARTNER)) {
|
||||
badges.push({
|
||||
key: 'partner',
|
||||
iconUrl: `${staticCdnEndpoint}/badges/partner.svg`,
|
||||
tooltip: 'Fluxer Partner',
|
||||
});
|
||||
}
|
||||
|
||||
if (!isSelfHosted && hasBigIntFlag(flags, UserFlags.BUG_HUNTER)) {
|
||||
badges.push({
|
||||
key: 'bug_hunter',
|
||||
iconUrl: `${staticCdnEndpoint}/badges/bug-hunter.svg`,
|
||||
tooltip: 'Fluxer Bug Hunter',
|
||||
});
|
||||
}
|
||||
|
||||
if (!isSelfHosted && user.premium_type && user.premium_type !== UserPremiumTypes.NONE) {
|
||||
let tooltip = 'Fluxer Plutonium';
|
||||
|
||||
if (user.premium_type === UserPremiumTypes.LIFETIME) {
|
||||
if (user.premium_since) {
|
||||
const premiumSince = getFormattedShortDate(user.premium_since);
|
||||
tooltip = `Fluxer Visionary since ${premiumSince}`;
|
||||
} else {
|
||||
tooltip = 'Fluxer Visionary';
|
||||
}
|
||||
} else if (user.premium_since) {
|
||||
const premiumSince = getFormattedShortDate(user.premium_since);
|
||||
tooltip = `Fluxer Plutonium subscriber since ${premiumSince}`;
|
||||
}
|
||||
|
||||
badges.push({
|
||||
key: 'premium',
|
||||
iconUrl: `${staticCdnEndpoint}/badges/plutonium.svg`,
|
||||
tooltip,
|
||||
});
|
||||
}
|
||||
|
||||
if (badges.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const badgeSizeClass = size === 'md' ? 'h-5 w-5' : 'h-4 w-4';
|
||||
const containerClass = cn('flex items-center', size === 'md' ? 'gap-2' : 'gap-1.5', className);
|
||||
|
||||
return (
|
||||
<div class={containerClass}>
|
||||
{badges.map((badge) => (
|
||||
<img
|
||||
key={badge.key}
|
||||
src={badge.iconUrl}
|
||||
alt={badge.tooltip}
|
||||
title={badge.tooltip}
|
||||
class={cn(badgeSizeClass, 'shrink-0')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
packages/admin/src/components/VoiceComponents.tsx
Normal file
103
packages/admin/src/components/VoiceComponents.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {UnifiedBadge as Badge} from '@fluxer/ui/src/components/Badge';
|
||||
import {Checkbox, Input} from '@fluxer/ui/src/components/Form';
|
||||
import type {FC} from 'hono/jsx';
|
||||
|
||||
export interface VoiceRestrictions {
|
||||
vip_only: boolean;
|
||||
required_guild_features: Array<string>;
|
||||
allowed_guild_ids: Array<string>;
|
||||
}
|
||||
|
||||
export interface VoiceStatusBadgesProps {
|
||||
vip_only: boolean;
|
||||
has_features: boolean;
|
||||
has_guild_ids: boolean;
|
||||
}
|
||||
|
||||
export const VoiceStatusBadges: FC<VoiceStatusBadgesProps> = ({vip_only, has_features, has_guild_ids}) => (
|
||||
<>
|
||||
{vip_only && <Badge label="VIP ONLY" tone="purple" intensity="subtle" rounded="default" />}
|
||||
{has_features && <Badge label="FEATURES" tone="orange" intensity="subtle" rounded="default" />}
|
||||
{has_guild_ids && <Badge label="GUILD IDS" tone="warning" intensity="subtle" rounded="default" />}
|
||||
</>
|
||||
);
|
||||
|
||||
export interface VoiceFeaturesListProps {
|
||||
features: Array<string>;
|
||||
}
|
||||
|
||||
export const VoiceFeaturesList: FC<VoiceFeaturesListProps> = ({features}) =>
|
||||
features.length > 0 ? (
|
||||
<div>
|
||||
<span class="font-medium text-neutral-600 text-xs">Required Features: </span>
|
||||
<span class="text-neutral-700 text-xs">{features.join(', ')}</span>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
export interface VoiceGuildIdsListProps {
|
||||
guild_ids: Array<string>;
|
||||
}
|
||||
|
||||
export const VoiceGuildIdsList: FC<VoiceGuildIdsListProps> = ({guild_ids}) =>
|
||||
guild_ids.length > 0 ? (
|
||||
<div>
|
||||
<span class="font-medium text-neutral-600 text-xs">Allowed Guilds: </span>
|
||||
<span class="text-neutral-700 text-xs">{guild_ids.join(', ')}</span>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
export interface VoiceRestrictionFieldsProps {
|
||||
id_prefix?: string;
|
||||
restrictions: VoiceRestrictions;
|
||||
}
|
||||
|
||||
export const VoiceRestrictionFields: FC<VoiceRestrictionFieldsProps> = ({id_prefix = '', restrictions}) => {
|
||||
const {vip_only, required_guild_features, allowed_guild_ids} = restrictions;
|
||||
|
||||
return (
|
||||
<div class="space-y-3 border-neutral-200 border-t pt-3">
|
||||
<h4 class="font-medium text-neutral-700 text-sm">Access Restrictions</h4>
|
||||
<Checkbox name="vip_only" value="true" label="VIP Only" checked={vip_only} />
|
||||
<Input
|
||||
label="Required Guild Features"
|
||||
name="required_guild_features"
|
||||
type="text"
|
||||
value={required_guild_features.join(', ')}
|
||||
placeholder="e.g. FEATURE_1, FEATURE_2"
|
||||
id={id_prefix ? `${id_prefix}-required-guild-features` : undefined}
|
||||
helper="Separate features with commas."
|
||||
/>
|
||||
<Input
|
||||
label="Allowed Guild IDs"
|
||||
name="allowed_guild_ids"
|
||||
type="text"
|
||||
value={allowed_guild_ids.join(', ')}
|
||||
placeholder="e.g. 123456789, 987654321"
|
||||
id={id_prefix ? `${id_prefix}-allowed-guild-ids` : undefined}
|
||||
helper="Separate guild IDs with commas."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
47
packages/admin/src/components/ui/Alert.tsx
Normal file
47
packages/admin/src/components/ui/Alert.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {Child} from 'hono/jsx';
|
||||
|
||||
interface AlertProps {
|
||||
variant?: 'success' | 'warning' | 'error' | 'info';
|
||||
title?: string;
|
||||
children: Child;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function Alert({variant = 'info', title, children, class: className}: AlertProps) {
|
||||
const variantStyles = {
|
||||
success: 'bg-green-50 border-green-200 text-green-700',
|
||||
warning: 'bg-neutral-50 border-neutral-200 text-neutral-700',
|
||||
error: 'bg-red-50 border-red-200 text-red-700',
|
||||
info: 'bg-blue-50 border-blue-200 text-blue-700',
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={cn('rounded-lg border p-4', variantStyles[variant], className)}>
|
||||
{title && <div class="mb-2 font-bold">{title}</div>}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
packages/admin/src/components/ui/Badge.tsx
Normal file
48
packages/admin/src/components/ui/Badge.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface BadgeProps {
|
||||
variant?: 'success' | 'danger' | 'warning' | 'info' | 'neutral';
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
export function Badge({variant = 'neutral', size = 'md', children}: PropsWithChildren<BadgeProps>) {
|
||||
const classes = clsx(
|
||||
'inline-flex items-center justify-center rounded-full font-medium',
|
||||
{
|
||||
'px-2 py-0.5 text-xs': size === 'sm',
|
||||
'px-2.5 py-1 text-sm': size === 'md',
|
||||
},
|
||||
{
|
||||
'bg-green-100 text-green-600': variant === 'success',
|
||||
'bg-red-100 text-red-600': variant === 'danger',
|
||||
'bg-neutral-100 text-neutral-700 border border-neutral-200': variant === 'warning',
|
||||
'bg-blue-100 text-blue-600': variant === 'info',
|
||||
'bg-neutral-100 text-neutral-600': variant === 'neutral',
|
||||
},
|
||||
);
|
||||
|
||||
return <span class={classes}>{children}</span>;
|
||||
}
|
||||
53
packages/admin/src/components/ui/Card.tsx
Normal file
53
packages/admin/src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export type CardPadding = 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
export type CardVariant = 'default' | 'bordered' | 'elevated';
|
||||
|
||||
export interface CardProps {
|
||||
padding?: CardPadding;
|
||||
variant?: CardVariant;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const paddingClasses: Record<CardPadding, string> = {
|
||||
none: 'p-0',
|
||||
sm: 'p-4',
|
||||
md: 'p-6',
|
||||
lg: 'p-8',
|
||||
xl: 'p-12',
|
||||
};
|
||||
|
||||
const variantClasses: Record<CardVariant, string> = {
|
||||
default: 'border border-neutral-200',
|
||||
bordered: 'border-2 border-neutral-300',
|
||||
elevated: 'border border-neutral-200 shadow-md',
|
||||
};
|
||||
|
||||
export function Card({padding = 'md', variant = 'default', className, children}: PropsWithChildren<CardProps>) {
|
||||
const classes = clsx('rounded-lg bg-white', variantClasses[variant], paddingClasses[padding], className);
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
34
packages/admin/src/components/ui/CardBody.tsx
Normal file
34
packages/admin/src/components/ui/CardBody.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface CardBodyProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CardBody({className, children}: PropsWithChildren<CardBodyProps>) {
|
||||
const classes = clsx('p-6', className);
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
34
packages/admin/src/components/ui/CardFooter.tsx
Normal file
34
packages/admin/src/components/ui/CardFooter.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface CardFooterProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CardFooter({className, children}: PropsWithChildren<CardFooterProps>) {
|
||||
const classes = clsx('border-neutral-200 border-t p-6', className);
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
34
packages/admin/src/components/ui/CardHeader.tsx
Normal file
34
packages/admin/src/components/ui/CardHeader.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface CardHeaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CardHeader({className, children}: PropsWithChildren<CardHeaderProps>) {
|
||||
const classes = clsx('border-neutral-200 border-b p-6', className);
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
49
packages/admin/src/components/ui/Chip.tsx
Normal file
49
packages/admin/src/components/ui/Chip.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface ChipProps {
|
||||
active?: boolean;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export function Chip({active = false, href, children}: PropsWithChildren<ChipProps>) {
|
||||
const chipClasses = clsx(
|
||||
'inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 font-medium text-sm transition-colors no-underline',
|
||||
{
|
||||
'border-brand-primary bg-brand-primary text-white': active,
|
||||
'border-neutral-300 bg-white text-neutral-700 hover:border-neutral-400 hover:bg-neutral-50': !active,
|
||||
},
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a href={href} class={chipClasses}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return <span class={chipClasses}>{children}</span>;
|
||||
}
|
||||
63
packages/admin/src/components/ui/CodeBlock.tsx
Normal file
63
packages/admin/src/components/ui/CodeBlock.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {Child} from 'hono/jsx';
|
||||
|
||||
interface CodeBlockProps {
|
||||
children: Child;
|
||||
copiable?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const COPY_CODE_SCRIPT = `
|
||||
function copyCode(button) {
|
||||
var code = button.previousElementSibling.textContent;
|
||||
navigator.clipboard.writeText(code).then(function () {
|
||||
var original = button.textContent;
|
||||
button.textContent = 'Copied!';
|
||||
setTimeout(function () { button.textContent = original; }, 2000);
|
||||
});
|
||||
}
|
||||
`;
|
||||
|
||||
export function CodeBlock({children, copiable = false, class: className}: CodeBlockProps) {
|
||||
return (
|
||||
<div class={cn('relative', className)}>
|
||||
<pre class="overflow-x-auto rounded border border-neutral-200 bg-neutral-100 p-3 font-mono text-sm">
|
||||
<code>{children}</code>
|
||||
</pre>
|
||||
{copiable && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onclick="copyCode(this)"
|
||||
class="absolute top-2 right-2 rounded border border-neutral-300 bg-white px-2 py-1 text-xs hover:bg-neutral-50"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<script dangerouslySetInnerHTML={{__html: COPY_CODE_SCRIPT}} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
packages/admin/src/components/ui/Container.tsx
Normal file
44
packages/admin/src/components/ui/Container.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export type ContainerSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||
|
||||
export interface ContainerProps {
|
||||
size?: ContainerSize;
|
||||
}
|
||||
|
||||
const sizeClasses: Record<ContainerSize, string> = {
|
||||
sm: 'max-w-2xl',
|
||||
md: 'max-w-4xl',
|
||||
lg: 'max-w-6xl',
|
||||
xl: 'max-w-7xl',
|
||||
full: 'max-w-full',
|
||||
};
|
||||
|
||||
export function Container({size = 'xl', children}: PropsWithChildren<ContainerProps>) {
|
||||
const classes = clsx('mx-auto w-full px-4 sm:px-6 lg:px-8', sizeClasses[size]);
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
40
packages/admin/src/components/ui/EmptyState.tsx
Normal file
40
packages/admin/src/components/ui/EmptyState.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {Child} from 'hono/jsx';
|
||||
|
||||
interface EmptyStateProps {
|
||||
variant?: 'empty' | 'loading' | 'error';
|
||||
children: Child;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function EmptyState({variant = 'empty', children, class: className}: EmptyStateProps) {
|
||||
const variantStyles = {
|
||||
empty: 'text-neutral-500 text-center py-8',
|
||||
loading: 'text-neutral-500 text-center py-8',
|
||||
error: 'text-red-600 text-center py-8',
|
||||
};
|
||||
|
||||
return <div class={cn(variantStyles[variant], className)}>{children}</div>;
|
||||
}
|
||||
49
packages/admin/src/components/ui/Form/FormActions.tsx
Normal file
49
packages/admin/src/components/ui/Form/FormActions.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import {Button} from '@fluxer/ui/src/components/Button';
|
||||
|
||||
export interface FormActionsProps {
|
||||
submitText?: string;
|
||||
cancelText?: string;
|
||||
loading?: boolean;
|
||||
cancelHref?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function FormActions(props: FormActionsProps) {
|
||||
const {submitText = 'Submit', cancelText = 'Cancel', loading = false, cancelHref, class: className} = props;
|
||||
|
||||
return (
|
||||
<div class={cn('flex items-center justify-end gap-3 border-gray-200 border-t pt-4', className)}>
|
||||
{cancelHref && (
|
||||
<Button variant="secondary" href={cancelHref} disabled={loading}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" variant="primary" loading={loading}>
|
||||
{submitText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
packages/admin/src/components/ui/Form/FormCard.tsx
Normal file
47
packages/admin/src/components/ui/Form/FormCard.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {Card, type CardPadding, type CardVariant} from '@fluxer/admin/src/components/ui/Card';
|
||||
import {CsrfInput} from '@fluxer/ui/src/components/CsrfInput';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface FormCardProps {
|
||||
action: string;
|
||||
method?: 'get' | 'post';
|
||||
csrfToken: string;
|
||||
padding?: CardPadding;
|
||||
variant?: CardVariant;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FormCard(props: PropsWithChildren<FormCardProps>) {
|
||||
const {action, method = 'post', csrfToken, padding = 'md', variant = 'default', className, children} = props;
|
||||
|
||||
return (
|
||||
<Card padding={padding} variant={variant} className={className}>
|
||||
<form method={method} action={action}>
|
||||
<CsrfInput token={csrfToken} />
|
||||
{children}
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
78
packages/admin/src/components/ui/Form/FormFieldGroup.tsx
Normal file
78
packages/admin/src/components/ui/Form/FormFieldGroup.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {Caption, Label} from '@fluxer/admin/src/components/ui/Typography';
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {Child} from 'hono/jsx';
|
||||
|
||||
export interface FormFieldGroupProps {
|
||||
label: string;
|
||||
htmlFor?: string;
|
||||
required?: boolean;
|
||||
error?: string;
|
||||
helper?: Child;
|
||||
children: Child;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function FormFieldGroup(props: FormFieldGroupProps) {
|
||||
const {label, htmlFor, required = false, error, helper, children, class: className} = props;
|
||||
const helperView =
|
||||
!error && helper !== undefined && helper !== null ? (
|
||||
typeof helper === 'string' || typeof helper === 'number' ? (
|
||||
<Caption>{helper}</Caption>
|
||||
) : (
|
||||
helper
|
||||
)
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div class={cn('flex flex-col gap-2', className)}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Label htmlFor={htmlFor} required={required}>
|
||||
{label}
|
||||
</Label>
|
||||
{helperView}
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
{children}
|
||||
{error && (
|
||||
<p class="flex items-center gap-1 text-red-600 text-sm">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="h-4 w-4 flex-shrink-0"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
packages/admin/src/components/ui/Form/FormRow.tsx
Normal file
51
packages/admin/src/components/ui/Form/FormRow.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface FormRowProps {
|
||||
cols?: 1 | 2 | 3 | 4;
|
||||
gap?: 2 | 3 | 4 | 5 | 6;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const colsStyles: Record<NonNullable<FormRowProps['cols']>, string> = {
|
||||
1: 'grid-cols-1',
|
||||
2: 'grid-cols-1 md:grid-cols-2',
|
||||
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
|
||||
};
|
||||
|
||||
const gapStyles: Record<NonNullable<FormRowProps['gap']>, string> = {
|
||||
2: 'gap-2',
|
||||
3: 'gap-3',
|
||||
4: 'gap-4',
|
||||
5: 'gap-5',
|
||||
6: 'gap-6',
|
||||
};
|
||||
|
||||
export function FormRow(props: PropsWithChildren<FormRowProps>) {
|
||||
const {cols = 2, gap = 4, children, class: className} = props;
|
||||
|
||||
return <div class={cn('grid', colsStyles[cols], gapStyles[gap], className)}>{children}</div>;
|
||||
}
|
||||
44
packages/admin/src/components/ui/Form/FormSection.tsx
Normal file
44
packages/admin/src/components/ui/Form/FormSection.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface FormSectionProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function FormSection(props: PropsWithChildren<FormSectionProps>) {
|
||||
const {title, description, children, class: className} = props;
|
||||
|
||||
return (
|
||||
<div class={cn('space-y-4', className)}>
|
||||
<div class="flex flex-col gap-1 border-gray-200 border-b pb-3">
|
||||
<h3 class="font-semibold text-gray-900 text-lg">{title}</h3>
|
||||
{description && <p class="text-gray-600 text-sm">{description}</p>}
|
||||
</div>
|
||||
<div class="space-y-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
packages/admin/src/components/ui/Grid.tsx
Normal file
52
packages/admin/src/components/ui/Grid.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export type GridCols = 1 | 2 | 3 | 4;
|
||||
export type GridGap = 'sm' | 'md' | 'lg';
|
||||
|
||||
export interface GridProps {
|
||||
cols?: GridCols;
|
||||
gap?: GridGap;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const colsClasses: Record<GridCols, string> = {
|
||||
1: 'md:grid-cols-1',
|
||||
2: 'md:grid-cols-2',
|
||||
3: 'md:grid-cols-3',
|
||||
4: 'md:grid-cols-4',
|
||||
};
|
||||
|
||||
const gapClasses: Record<GridGap, string> = {
|
||||
sm: 'gap-2',
|
||||
md: 'gap-4',
|
||||
lg: 'gap-6',
|
||||
};
|
||||
|
||||
export function Grid({cols = 2, gap = 'md', class: className, children}: PropsWithChildren<GridProps>) {
|
||||
const classes = cn('grid grid-cols-1', colsClasses[cols], gapClasses[gap], className);
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
38
packages/admin/src/components/ui/InlineStack.tsx
Normal file
38
packages/admin/src/components/ui/InlineStack.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {Child} from 'hono/jsx';
|
||||
|
||||
interface InlineStackProps {
|
||||
gap?: number | string;
|
||||
align?: 'start' | 'center' | 'end' | 'baseline';
|
||||
children: Child;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function InlineStack({gap = 2, align = 'center', children, class: className}: InlineStackProps) {
|
||||
const gapClass = typeof gap === 'number' ? `gap-${gap}` : gap;
|
||||
const alignClass = `items-${align}`;
|
||||
|
||||
return <div class={cn('flex', alignClass, gapClass, className)}>{children}</div>;
|
||||
}
|
||||
159
packages/admin/src/components/ui/Input.tsx
Normal file
159
packages/admin/src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {Child, FC} from 'hono/jsx';
|
||||
|
||||
export type InputSize = 'sm' | 'md' | 'lg';
|
||||
export type InputType = 'text' | 'email' | 'password' | 'tel' | 'number' | 'date' | 'datetime-local' | 'url' | 'search';
|
||||
|
||||
export interface InputProps {
|
||||
type?: InputType;
|
||||
name: string;
|
||||
id?: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
size?: InputSize;
|
||||
error?: boolean;
|
||||
fullWidth?: boolean;
|
||||
leftIcon?: Child;
|
||||
rightIcon?: Child;
|
||||
autocomplete?: string;
|
||||
min?: string | number;
|
||||
max?: string | number;
|
||||
step?: string | number;
|
||||
pattern?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const sizeClasses: Record<InputSize, string> = {
|
||||
sm: 'h-8 px-3 py-1.5 text-sm',
|
||||
md: 'h-9 px-3 py-2 text-sm',
|
||||
lg: 'h-10 px-4 py-2.5 text-base',
|
||||
};
|
||||
|
||||
function toInputId(name: string): string {
|
||||
return name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
}
|
||||
|
||||
export const Input: FC<InputProps> = ({
|
||||
type = 'text',
|
||||
name,
|
||||
id,
|
||||
value,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
required = false,
|
||||
readonly = false,
|
||||
size = 'sm',
|
||||
error = false,
|
||||
fullWidth = true,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
autocomplete,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
pattern,
|
||||
class: extraClass,
|
||||
}) => {
|
||||
const inputId = id ?? toInputId(name);
|
||||
const baseClasses = [
|
||||
'rounded-lg',
|
||||
'border',
|
||||
'border-neutral-300',
|
||||
'bg-white',
|
||||
'text-neutral-900',
|
||||
'placeholder:text-neutral-400',
|
||||
'transition-all',
|
||||
'focus:outline-none',
|
||||
'focus:ring-2',
|
||||
'disabled:opacity-50',
|
||||
'disabled:cursor-not-allowed',
|
||||
'disabled:bg-neutral-50',
|
||||
];
|
||||
|
||||
const stateClasses = [
|
||||
error
|
||||
? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
|
||||
: 'focus:border-brand-primary focus:ring-brand-primary/20',
|
||||
fullWidth ? 'w-full' : '',
|
||||
leftIcon ? 'pl-10' : '',
|
||||
rightIcon ? 'pr-10' : '',
|
||||
].filter(Boolean);
|
||||
|
||||
const classes = [...baseClasses, sizeClasses[size], ...stateClasses, extraClass || ''].filter(Boolean).join(' ');
|
||||
|
||||
if (leftIcon || rightIcon) {
|
||||
return (
|
||||
<div class={`relative ${fullWidth ? 'w-full' : 'inline-flex'}`}>
|
||||
{leftIcon && (
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-neutral-400">
|
||||
{leftIcon}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type={type}
|
||||
name={name}
|
||||
id={inputId}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
readonly={readonly}
|
||||
autocomplete={autocomplete}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
pattern={pattern}
|
||||
class={classes}
|
||||
/>
|
||||
{rightIcon && (
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-neutral-400">
|
||||
{rightIcon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
name={name}
|
||||
id={inputId}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
readonly={readonly}
|
||||
autocomplete={autocomplete}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
pattern={pattern}
|
||||
class={classes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
87
packages/admin/src/components/ui/Layout/Box.tsx
Normal file
87
packages/admin/src/components/ui/Layout/Box.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export type BoxSpacing = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||
export type BoxBackground = 'white' | 'gray-50' | 'gray-100' | 'transparent';
|
||||
export type BoxBorder = 'none' | 'gray-200' | 'gray-300';
|
||||
export type BoxRounded = 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
||||
|
||||
export interface BoxProps {
|
||||
p?: BoxSpacing;
|
||||
m?: BoxSpacing;
|
||||
bg?: BoxBackground;
|
||||
border?: BoxBorder;
|
||||
rounded?: BoxRounded;
|
||||
}
|
||||
|
||||
const backgroundClasses: Record<BoxBackground, string> = {
|
||||
white: 'bg-white',
|
||||
'gray-50': 'bg-gray-50',
|
||||
'gray-100': 'bg-gray-100',
|
||||
transparent: 'bg-transparent',
|
||||
};
|
||||
|
||||
const borderClasses: Record<BoxBorder, string> = {
|
||||
none: '',
|
||||
'gray-200': 'border border-gray-200',
|
||||
'gray-300': 'border border-gray-300',
|
||||
};
|
||||
|
||||
const roundedClasses: Record<BoxRounded, string> = {
|
||||
none: 'rounded-none',
|
||||
sm: 'rounded-sm',
|
||||
md: 'rounded-md',
|
||||
lg: 'rounded-lg',
|
||||
xl: 'rounded-xl',
|
||||
'2xl': 'rounded-2xl',
|
||||
full: 'rounded-full',
|
||||
};
|
||||
|
||||
function getPaddingClass(p: BoxSpacing): string {
|
||||
return `p-${p}`;
|
||||
}
|
||||
|
||||
function getMarginClass(m: BoxSpacing): string {
|
||||
return `m-${m}`;
|
||||
}
|
||||
|
||||
export function Box({
|
||||
p,
|
||||
m,
|
||||
bg = 'transparent',
|
||||
border = 'none',
|
||||
rounded = 'none',
|
||||
children,
|
||||
}: PropsWithChildren<BoxProps>) {
|
||||
const classes = cn(
|
||||
p !== undefined && getPaddingClass(p),
|
||||
m !== undefined && getMarginClass(m),
|
||||
backgroundClasses[bg],
|
||||
borderClasses[border],
|
||||
roundedClasses[rounded],
|
||||
);
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
39
packages/admin/src/components/ui/Layout/DetailPageLayout.tsx
Normal file
39
packages/admin/src/components/ui/Layout/DetailPageLayout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {PageContainer} from '@fluxer/admin/src/components/ui/Layout/PageContainer';
|
||||
import type {Child, PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface DetailPageLayoutProps {
|
||||
header: Child;
|
||||
tabs?: Child;
|
||||
}
|
||||
|
||||
export function DetailPageLayout({header, tabs, children}: PropsWithChildren<DetailPageLayoutProps>) {
|
||||
return (
|
||||
<PageContainer>
|
||||
{header}
|
||||
{tabs}
|
||||
{children}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
94
packages/admin/src/components/ui/Layout/Flex.tsx
Normal file
94
packages/admin/src/components/ui/Layout/Flex.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export type FlexDirection = 'row' | 'col' | 'row-reverse' | 'col-reverse';
|
||||
export type FlexAlign = 'start' | 'center' | 'end' | 'baseline' | 'stretch';
|
||||
export type FlexJustify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
|
||||
export type FlexWrap = 'wrap' | 'nowrap' | 'wrap-reverse';
|
||||
|
||||
export interface FlexProps {
|
||||
direction?: FlexDirection;
|
||||
align?: FlexAlign;
|
||||
justify?: FlexJustify;
|
||||
gap?: number | string;
|
||||
wrap?: FlexWrap;
|
||||
}
|
||||
|
||||
const directionClasses: Record<FlexDirection, string> = {
|
||||
row: 'flex-row',
|
||||
col: 'flex-col',
|
||||
'row-reverse': 'flex-row-reverse',
|
||||
'col-reverse': 'flex-col-reverse',
|
||||
};
|
||||
|
||||
const alignClasses: Record<FlexAlign, string> = {
|
||||
start: 'items-start',
|
||||
center: 'items-center',
|
||||
end: 'items-end',
|
||||
baseline: 'items-baseline',
|
||||
stretch: 'items-stretch',
|
||||
};
|
||||
|
||||
const justifyClasses: Record<FlexJustify, string> = {
|
||||
start: 'justify-start',
|
||||
center: 'justify-center',
|
||||
end: 'justify-end',
|
||||
between: 'justify-between',
|
||||
around: 'justify-around',
|
||||
evenly: 'justify-evenly',
|
||||
};
|
||||
|
||||
const wrapClasses: Record<FlexWrap, string> = {
|
||||
wrap: 'flex-wrap',
|
||||
nowrap: 'flex-nowrap',
|
||||
'wrap-reverse': 'flex-wrap-reverse',
|
||||
};
|
||||
|
||||
function getGapClass(gap: number | string): string {
|
||||
if (typeof gap === 'number') {
|
||||
return `gap-${gap}`;
|
||||
}
|
||||
return gap;
|
||||
}
|
||||
|
||||
export function Flex({
|
||||
direction = 'row',
|
||||
align = 'stretch',
|
||||
justify = 'start',
|
||||
gap,
|
||||
wrap = 'nowrap',
|
||||
children,
|
||||
}: PropsWithChildren<FlexProps>) {
|
||||
const classes = cn(
|
||||
'flex',
|
||||
directionClasses[direction],
|
||||
alignClasses[align],
|
||||
justifyClasses[justify],
|
||||
gap !== undefined && getGapClass(gap),
|
||||
wrapClasses[wrap],
|
||||
);
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
44
packages/admin/src/components/ui/Layout/FormGrid.tsx
Normal file
44
packages/admin/src/components/ui/Layout/FormGrid.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface FormGridProps {
|
||||
cols?: 2 | 3 | 4;
|
||||
gap?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const colsClasses = {
|
||||
2: 'md:grid-cols-2',
|
||||
3: 'md:grid-cols-3',
|
||||
4: 'md:grid-cols-4',
|
||||
};
|
||||
|
||||
const gapClasses = {
|
||||
sm: 'gap-2',
|
||||
md: 'gap-4',
|
||||
lg: 'gap-6',
|
||||
};
|
||||
|
||||
export function FormGrid({cols = 2, gap = 'md', children}: PropsWithChildren<FormGridProps>) {
|
||||
return <div class={`grid grid-cols-1 ${colsClasses[cols]} ${gapClasses[gap]}`}>{children}</div>;
|
||||
}
|
||||
70
packages/admin/src/components/ui/Layout/HStack.tsx
Normal file
70
packages/admin/src/components/ui/Layout/HStack.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export type HStackAlign = 'start' | 'center' | 'end' | 'baseline' | 'stretch';
|
||||
export type HStackJustify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
|
||||
|
||||
export interface HStackProps {
|
||||
gap?: number | string;
|
||||
align?: HStackAlign;
|
||||
justify?: HStackJustify;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const alignClasses: Record<HStackAlign, string> = {
|
||||
start: 'items-start',
|
||||
center: 'items-center',
|
||||
end: 'items-end',
|
||||
baseline: 'items-baseline',
|
||||
stretch: 'items-stretch',
|
||||
};
|
||||
|
||||
const justifyClasses: Record<HStackJustify, string> = {
|
||||
start: 'justify-start',
|
||||
center: 'justify-center',
|
||||
end: 'justify-end',
|
||||
between: 'justify-between',
|
||||
around: 'justify-around',
|
||||
evenly: 'justify-evenly',
|
||||
};
|
||||
|
||||
function getGapClass(gap: number | string): string {
|
||||
if (typeof gap === 'number') {
|
||||
return `gap-${gap}`;
|
||||
}
|
||||
return gap;
|
||||
}
|
||||
|
||||
export function HStack({
|
||||
gap = 4,
|
||||
align = 'center',
|
||||
justify = 'start',
|
||||
class: className,
|
||||
children,
|
||||
}: PropsWithChildren<HStackProps>) {
|
||||
const classes = cn('flex flex-row', getGapClass(gap), alignClasses[align], justifyClasses[justify], className);
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
33
packages/admin/src/components/ui/Layout/PageContainer.tsx
Normal file
33
packages/admin/src/components/ui/Layout/PageContainer.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface PageContainerProps {
|
||||
maxWidth?: 'full' | '7xl';
|
||||
}
|
||||
|
||||
export function PageContainer({maxWidth = '7xl', children}: PropsWithChildren<PageContainerProps>) {
|
||||
const widthClass = maxWidth === 'full' ? 'w-full' : 'max-w-7xl';
|
||||
|
||||
return <div class={`mx-auto ${widthClass} space-y-6`}>{children}</div>;
|
||||
}
|
||||
45
packages/admin/src/components/ui/Layout/PageHeader.tsx
Normal file
45
packages/admin/src/components/ui/Layout/PageHeader.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {Child, PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface PageHeaderProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
actions?: Child;
|
||||
}
|
||||
|
||||
export function PageHeader({title, description, actions, children}: PropsWithChildren<PageHeaderProps>) {
|
||||
return (
|
||||
<div>
|
||||
<div class={cn('flex items-start justify-between', description ? 'mb-2' : 'mb-0')}>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<h1 class="font-bold text-3xl text-gray-900">{title}</h1>
|
||||
{description && <p class="text-base text-gray-600">{description}</p>}
|
||||
</div>
|
||||
{actions && <div class="ml-4 flex-shrink-0">{actions}</div>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
packages/admin/src/components/ui/Layout/PageLayout.tsx
Normal file
50
packages/admin/src/components/ui/Layout/PageLayout.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export type PageLayoutMaxWidth = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl';
|
||||
|
||||
export interface PageLayoutProps {
|
||||
maxWidth?: PageLayoutMaxWidth;
|
||||
padding?: boolean;
|
||||
}
|
||||
|
||||
const maxWidthClasses: Record<PageLayoutMaxWidth, string> = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-md',
|
||||
lg: 'max-w-lg',
|
||||
xl: 'max-w-xl',
|
||||
'2xl': 'max-w-2xl',
|
||||
'3xl': 'max-w-3xl',
|
||||
'4xl': 'max-w-4xl',
|
||||
'5xl': 'max-w-5xl',
|
||||
'6xl': 'max-w-6xl',
|
||||
'7xl': 'max-w-7xl',
|
||||
};
|
||||
|
||||
export function PageLayout({maxWidth = '7xl', padding = false, children}: PropsWithChildren<PageLayoutProps>) {
|
||||
const classes = cn('mx-auto w-full', maxWidthClasses[maxWidth], padding && 'px-4 sm:px-6 lg:px-8');
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {PageContainer} from '@fluxer/admin/src/components/ui/Layout/PageContainer';
|
||||
import {PageHeader} from '@fluxer/admin/src/components/ui/Layout/PageHeader';
|
||||
import type {Child, PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface SearchListPageLayoutProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
actions?: Child;
|
||||
searchForm: Child;
|
||||
}
|
||||
|
||||
export function SearchListPageLayout({
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
searchForm,
|
||||
children,
|
||||
}: PropsWithChildren<SearchListPageLayoutProps>) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader title={title} description={description} actions={actions} />
|
||||
{searchForm}
|
||||
{children}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
37
packages/admin/src/components/ui/Layout/TwoColumnGrid.tsx
Normal file
37
packages/admin/src/components/ui/Layout/TwoColumnGrid.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface TwoColumnGridProps {
|
||||
gap?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const gapClasses = {
|
||||
sm: 'gap-2',
|
||||
md: 'gap-4',
|
||||
lg: 'gap-6',
|
||||
};
|
||||
|
||||
export function TwoColumnGrid({gap = 'md', children}: PropsWithChildren<TwoColumnGridProps>) {
|
||||
return <div class={`grid grid-cols-1 md:grid-cols-2 ${gapClasses[gap]}`}>{children}</div>;
|
||||
}
|
||||
52
packages/admin/src/components/ui/Layout/VStack.tsx
Normal file
52
packages/admin/src/components/ui/Layout/VStack.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export type VStackAlign = 'start' | 'center' | 'end' | 'stretch';
|
||||
|
||||
export interface VStackProps {
|
||||
gap?: number | string;
|
||||
align?: VStackAlign;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const alignClasses: Record<VStackAlign, string> = {
|
||||
start: 'items-start',
|
||||
center: 'items-center',
|
||||
end: 'items-end',
|
||||
stretch: 'items-stretch',
|
||||
};
|
||||
|
||||
function getGapClass(gap: number | string): string {
|
||||
if (typeof gap === 'number') {
|
||||
return `gap-${gap}`;
|
||||
}
|
||||
return gap;
|
||||
}
|
||||
|
||||
export function VStack({gap = 4, align = 'stretch', class: className, children}: PropsWithChildren<VStackProps>) {
|
||||
const classes = cn('flex flex-col', getGapClass(gap), alignClasses[align], className);
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
38
packages/admin/src/components/ui/MetadataRow.tsx
Normal file
38
packages/admin/src/components/ui/MetadataRow.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
|
||||
interface MetadataRowProps {
|
||||
label: string;
|
||||
value: string | number | any;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function MetadataRow({label, value, class: className}: MetadataRowProps) {
|
||||
return (
|
||||
<div class={cn('flex gap-2', className)}>
|
||||
<span class="text-neutral-500 text-sm">{label}:</span>
|
||||
<span class="text-neutral-900 text-sm">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
packages/admin/src/components/ui/NavLink.tsx
Normal file
39
packages/admin/src/components/ui/NavLink.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {Child, FC} from 'hono/jsx';
|
||||
|
||||
interface NavLinkProps {
|
||||
href: string;
|
||||
children: Child;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const NavLink: FC<NavLinkProps> = ({href, children, class: className = ''}) => {
|
||||
const baseClass = `label rounded-lg border border-neutral-300 bg-white px-3 py-2 text-neutral-700 transition-colors hover:bg-neutral-50 ${className}`;
|
||||
|
||||
return (
|
||||
<a href={href} class={baseClass}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
40
packages/admin/src/components/ui/Pill.tsx
Normal file
40
packages/admin/src/components/ui/Pill.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface PillProps {
|
||||
tone?: 'neutral' | 'success' | 'danger' | 'warning' | 'info';
|
||||
}
|
||||
|
||||
export function Pill({tone = 'neutral', children}: PropsWithChildren<PillProps>) {
|
||||
const classes = clsx('inline-block rounded-lg border px-3 py-2 font-medium text-sm shadow-sm', {
|
||||
'border-gray-200 bg-neutral-100 text-neutral-700': tone === 'neutral',
|
||||
'border-green-200 bg-green-50 text-green-700': tone === 'success',
|
||||
'border-red-200 bg-red-50 text-red-700': tone === 'danger',
|
||||
'border-neutral-200 bg-neutral-50 text-neutral-700': tone === 'warning',
|
||||
'border-blue-200 bg-blue-50 text-blue-700': tone === 'info',
|
||||
});
|
||||
|
||||
return <span class={classes}>{children}</span>;
|
||||
}
|
||||
52
packages/admin/src/components/ui/ResourceLink.tsx
Normal file
52
packages/admin/src/components/ui/ResourceLink.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {Child, FC} from 'hono/jsx';
|
||||
|
||||
interface ResourceLinkProps {
|
||||
config: Config;
|
||||
resourceType: 'user' | 'guild';
|
||||
resourceId: string;
|
||||
children: Child;
|
||||
size?: 'sm' | 'md';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const ResourceLink: FC<ResourceLinkProps> = ({
|
||||
config,
|
||||
resourceType,
|
||||
resourceId,
|
||||
children,
|
||||
size = 'sm',
|
||||
class: className = '',
|
||||
}) => {
|
||||
const sizeClass = size === 'sm' ? 'text-sm' : '';
|
||||
const baseClass = `text-neutral-900 underline decoration-neutral-300 hover:text-neutral-600 hover:decoration-neutral-500 ${sizeClass} ${className}`;
|
||||
const href = `${config.basePath}/${resourceType === 'user' ? 'users' : 'guilds'}/${resourceId}`;
|
||||
|
||||
return (
|
||||
<a href={href} class={baseClass}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
109
packages/admin/src/components/ui/Select.tsx
Normal file
109
packages/admin/src/components/ui/Select.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {FC} from 'hono/jsx';
|
||||
|
||||
export type SelectSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface SelectProps {
|
||||
name: string;
|
||||
id?: string;
|
||||
value?: string;
|
||||
options: Array<SelectOption>;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
size?: SelectSize;
|
||||
error?: boolean;
|
||||
fullWidth?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const sizeClasses: Record<SelectSize, string> = {
|
||||
sm: 'h-8 px-3 py-1.5 text-sm',
|
||||
md: 'h-9 px-3 py-2 text-sm',
|
||||
lg: 'h-10 px-4 py-2.5 text-base',
|
||||
};
|
||||
|
||||
export const Select: FC<SelectProps> = ({
|
||||
name,
|
||||
id,
|
||||
value,
|
||||
options,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
required = false,
|
||||
size = 'sm',
|
||||
error = false,
|
||||
fullWidth = true,
|
||||
class: extraClass,
|
||||
}) => {
|
||||
const baseClasses = [
|
||||
'rounded-lg',
|
||||
'border',
|
||||
'border-neutral-300',
|
||||
'bg-white',
|
||||
'text-neutral-900',
|
||||
'transition-all',
|
||||
'focus:outline-none',
|
||||
'focus:border-brand-primary',
|
||||
'focus:ring-2',
|
||||
'focus:ring-brand-primary/20',
|
||||
'disabled:opacity-50',
|
||||
'disabled:cursor-not-allowed',
|
||||
'disabled:bg-neutral-50',
|
||||
'appearance-none',
|
||||
"bg-[url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E\")]",
|
||||
'bg-[length:1.1em_1.1em]',
|
||||
'bg-[position:right_0.75rem_center]',
|
||||
'bg-no-repeat',
|
||||
'pr-10',
|
||||
];
|
||||
|
||||
const stateClasses = [
|
||||
error ? 'border-red-500 focus:border-red-500 focus:ring-2 focus:ring-red-500/20' : '',
|
||||
fullWidth ? 'w-full' : '',
|
||||
].filter(Boolean);
|
||||
|
||||
const classes = [...baseClasses, sizeClasses[size], ...stateClasses, extraClass || ''].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<select name={name} id={id} disabled={disabled} required={required} class={classes}>
|
||||
{placeholder && (
|
||||
<option value="" disabled selected={!value}>
|
||||
{placeholder}
|
||||
</option>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<option value={option.value} selected={option.value === value} disabled={option.disabled}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
59
packages/admin/src/components/ui/Stack.tsx
Normal file
59
packages/admin/src/components/ui/Stack.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export type StackGap = 'sm' | 'md' | 'lg' | number;
|
||||
export type StackAlign = 'start' | 'center' | 'end' | 'stretch';
|
||||
|
||||
export interface StackProps {
|
||||
gap?: StackGap;
|
||||
align?: StackAlign;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const gapClasses: Record<string, string> = {
|
||||
sm: 'gap-2',
|
||||
md: 'gap-4',
|
||||
lg: 'gap-6',
|
||||
};
|
||||
|
||||
const alignClasses: Record<StackAlign, string> = {
|
||||
start: 'items-start',
|
||||
center: 'items-center',
|
||||
end: 'items-end',
|
||||
stretch: 'items-stretch',
|
||||
};
|
||||
|
||||
function getGapClass(gap: StackGap): string {
|
||||
if (typeof gap === 'number') {
|
||||
return `gap-${gap}`;
|
||||
}
|
||||
return gapClasses[gap] ?? gapClasses.md;
|
||||
}
|
||||
|
||||
export function Stack({gap = 'md', align = 'stretch', class: className, children}: PropsWithChildren<StackProps>) {
|
||||
const classes = cn('flex flex-col', getGapClass(gap), alignClasses[align], className);
|
||||
|
||||
return <div class={classes}>{children}</div>;
|
||||
}
|
||||
58
packages/admin/src/components/ui/StatusBadge.tsx
Normal file
58
packages/admin/src/components/ui/StatusBadge.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {BadgeProps} from '@fluxer/admin/src/components/ui/Badge';
|
||||
import {Badge} from '@fluxer/admin/src/components/ui/Badge';
|
||||
|
||||
export interface StatusBadgeProps {
|
||||
status: 'active' | 'inactive' | 'pending' | 'approved' | 'rejected' | 'banned';
|
||||
size?: BadgeProps['size'];
|
||||
}
|
||||
|
||||
const statusVariantMap: Record<StatusBadgeProps['status'], BadgeProps['variant']> = {
|
||||
active: 'success',
|
||||
inactive: 'neutral',
|
||||
pending: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger',
|
||||
banned: 'danger',
|
||||
};
|
||||
|
||||
const statusLabelMap: Record<StatusBadgeProps['status'], string> = {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
pending: 'Pending',
|
||||
approved: 'Approved',
|
||||
rejected: 'Rejected',
|
||||
banned: 'Banned',
|
||||
};
|
||||
|
||||
export function StatusBadge({status, size}: StatusBadgeProps) {
|
||||
const variant = statusVariantMap[status];
|
||||
const label = statusLabelMap[status];
|
||||
|
||||
return (
|
||||
<Badge variant={variant} size={size}>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
36
packages/admin/src/components/ui/Table.tsx
Normal file
36
packages/admin/src/components/ui/Table.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface TableProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Table({children, className}: PropsWithChildren<TableProps>) {
|
||||
return (
|
||||
<table class={clsx('min-w-full border-collapse rounded-lg border border-neutral-200 bg-white', className)}>
|
||||
{children}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
27
packages/admin/src/components/ui/TableBody.tsx
Normal file
27
packages/admin/src/components/ui/TableBody.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export function TableBody({children}: PropsWithChildren) {
|
||||
return <tbody class="divide-y divide-neutral-200">{children}</tbody>;
|
||||
}
|
||||
49
packages/admin/src/components/ui/TableCell.tsx
Normal file
49
packages/admin/src/components/ui/TableCell.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface TableCellProps {
|
||||
align?: 'left' | 'center' | 'right';
|
||||
variant?: 'default' | 'header';
|
||||
colSpan?: number;
|
||||
}
|
||||
|
||||
export function TableCell({align = 'left', variant = 'default', colSpan, children}: PropsWithChildren<TableCellProps>) {
|
||||
const isHeader = variant === 'header';
|
||||
|
||||
return (
|
||||
<td
|
||||
class={clsx(
|
||||
'px-6 py-4 text-sm',
|
||||
align === 'left' && 'text-left',
|
||||
align === 'center' && 'text-center',
|
||||
align === 'right' && 'text-right',
|
||||
isHeader ? 'bg-neutral-50 font-bold text-neutral-900' : 'text-neutral-900',
|
||||
)}
|
||||
colspan={colSpan}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
27
packages/admin/src/components/ui/TableContainer.tsx
Normal file
27
packages/admin/src/components/ui/TableContainer.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export function TableContainer({children}: PropsWithChildren) {
|
||||
return <div class="overflow-x-auto">{children}</div>;
|
||||
}
|
||||
27
packages/admin/src/components/ui/TableHeader.tsx
Normal file
27
packages/admin/src/components/ui/TableHeader.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export function TableHeader({children}: PropsWithChildren) {
|
||||
return <thead class="bg-neutral-50">{children}</thead>;
|
||||
}
|
||||
43
packages/admin/src/components/ui/TableHeaderCell.tsx
Normal file
43
packages/admin/src/components/ui/TableHeaderCell.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface TableHeaderCellProps {
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
export function TableHeaderCell({align = 'left', children}: PropsWithChildren<TableHeaderCellProps>) {
|
||||
return (
|
||||
<th
|
||||
class={clsx(
|
||||
'whitespace-nowrap px-6 py-3 font-medium text-neutral-500 text-xs uppercase tracking-wider',
|
||||
align === 'left' && 'text-left',
|
||||
align === 'center' && 'text-center',
|
||||
align === 'right' && 'text-right',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
49
packages/admin/src/components/ui/TableRow.tsx
Normal file
49
packages/admin/src/components/ui/TableRow.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface TableRowProps {
|
||||
hover?: boolean;
|
||||
selected?: boolean;
|
||||
clickable?: boolean;
|
||||
}
|
||||
|
||||
export function TableRow({
|
||||
hover = true,
|
||||
selected = false,
|
||||
clickable = false,
|
||||
children,
|
||||
}: PropsWithChildren<TableRowProps>) {
|
||||
return (
|
||||
<tr
|
||||
class={clsx(
|
||||
hover && 'transition-colors hover:bg-neutral-50',
|
||||
selected && 'bg-blue-50',
|
||||
clickable && 'cursor-pointer',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
48
packages/admin/src/components/ui/TextLink.tsx
Normal file
48
packages/admin/src/components/ui/TextLink.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {Child, FC} from 'hono/jsx';
|
||||
|
||||
interface TextLinkProps {
|
||||
href: string;
|
||||
children: Child;
|
||||
external?: boolean;
|
||||
mono?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const TextLink: FC<TextLinkProps> = ({
|
||||
href,
|
||||
children,
|
||||
external = false,
|
||||
mono = false,
|
||||
class: className = '',
|
||||
}) => {
|
||||
const baseClass = `text-neutral-900 underline decoration-neutral-300 hover:text-neutral-600 hover:decoration-neutral-500 ${mono ? 'font-mono' : ''} ${className}`;
|
||||
const externalProps = external ? {target: '_blank', rel: 'noopener noreferrer'} : {};
|
||||
|
||||
return (
|
||||
<a href={href} class={baseClass} {...externalProps}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
121
packages/admin/src/components/ui/Textarea.tsx
Normal file
121
packages/admin/src/components/ui/Textarea.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {FC} from 'hono/jsx';
|
||||
|
||||
export type TextareaSize = 'sm' | 'md' | 'lg';
|
||||
export type TextareaResize = 'none' | 'vertical' | 'horizontal' | 'both';
|
||||
|
||||
export interface TextareaProps {
|
||||
name: string;
|
||||
id?: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
rows?: number;
|
||||
size?: TextareaSize;
|
||||
error?: boolean;
|
||||
fullWidth?: boolean;
|
||||
maxlength?: number;
|
||||
minlength?: number;
|
||||
resize?: TextareaResize;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const sizeClasses: Record<TextareaSize, string> = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-3 py-2 text-sm',
|
||||
lg: 'px-4 py-3 text-base',
|
||||
};
|
||||
|
||||
const resizeClasses: Record<TextareaResize, string> = {
|
||||
none: 'resize-none',
|
||||
vertical: 'resize-y',
|
||||
horizontal: 'resize-x',
|
||||
both: 'resize',
|
||||
};
|
||||
|
||||
function toTextareaId(name: string): string {
|
||||
return name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
}
|
||||
|
||||
export const Textarea: FC<TextareaProps> = ({
|
||||
name,
|
||||
id,
|
||||
value,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
required = false,
|
||||
readonly = false,
|
||||
rows = 4,
|
||||
size = 'sm',
|
||||
error = false,
|
||||
fullWidth = true,
|
||||
maxlength,
|
||||
minlength,
|
||||
resize = 'vertical',
|
||||
class: extraClass,
|
||||
}) => {
|
||||
const textareaId = id ?? toTextareaId(name);
|
||||
const baseClasses = [
|
||||
'rounded-lg',
|
||||
'border',
|
||||
'bg-white',
|
||||
'text-neutral-900',
|
||||
'placeholder:text-neutral-400',
|
||||
'transition-all',
|
||||
'focus:outline-none',
|
||||
'focus:ring-2',
|
||||
'focus:ring-brand-primary/20',
|
||||
'disabled:opacity-50',
|
||||
'disabled:cursor-not-allowed',
|
||||
'disabled:bg-neutral-50',
|
||||
];
|
||||
|
||||
const stateClasses = [
|
||||
error ? 'border-red-500 focus:border-red-500' : 'border-neutral-300 focus:border-brand-primary',
|
||||
fullWidth ? 'w-full' : '',
|
||||
].filter(Boolean);
|
||||
|
||||
const classes = [...baseClasses, sizeClasses[size], resizeClasses[resize], ...stateClasses, extraClass || '']
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<textarea
|
||||
name={name}
|
||||
id={textareaId}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
readonly={readonly}
|
||||
rows={rows}
|
||||
maxlength={maxlength}
|
||||
minlength={minlength}
|
||||
class={classes}
|
||||
>
|
||||
{value}
|
||||
</textarea>
|
||||
);
|
||||
};
|
||||
158
packages/admin/src/components/ui/Typography.tsx
Normal file
158
packages/admin/src/components/ui/Typography.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {cn} from '@fluxer/admin/src/utils/ClassNames';
|
||||
import type {Child, PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
export interface HeadingProps {
|
||||
level: 1 | 2 | 3 | 4 | 5 | 6;
|
||||
size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const headingSizes: Record<1 | 2 | 3 | 4 | 5 | 6, string> = {
|
||||
1: 'text-3xl font-bold',
|
||||
2: 'text-2xl font-semibold',
|
||||
3: 'text-xl font-semibold',
|
||||
4: 'text-lg font-semibold',
|
||||
5: 'text-base font-semibold',
|
||||
6: 'text-sm font-semibold',
|
||||
};
|
||||
|
||||
const customSizes: Record<NonNullable<HeadingProps['size']>, string> = {
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm',
|
||||
base: 'text-base',
|
||||
lg: 'text-lg',
|
||||
xl: 'text-xl',
|
||||
'2xl': 'text-2xl',
|
||||
'3xl': 'text-3xl',
|
||||
'4xl': 'text-4xl',
|
||||
};
|
||||
|
||||
export function Heading(props: PropsWithChildren<HeadingProps>) {
|
||||
const {level, size, children, class: className} = props;
|
||||
|
||||
const classes = cn('text-gray-900 tracking-tight', size ? customSizes[size] : headingSizes[level], className);
|
||||
|
||||
if (level === 1) return <h1 class={classes}>{children}</h1>;
|
||||
if (level === 2) return <h2 class={classes}>{children}</h2>;
|
||||
if (level === 3) return <h3 class={classes}>{children}</h3>;
|
||||
if (level === 4) return <h4 class={classes}>{children}</h4>;
|
||||
if (level === 5) return <h5 class={classes}>{children}</h5>;
|
||||
return <h6 class={classes}>{children}</h6>;
|
||||
}
|
||||
|
||||
export interface TextProps {
|
||||
size?: 'xs' | 'sm' | 'base' | 'lg';
|
||||
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
|
||||
color?: 'default' | 'muted' | 'primary' | 'danger' | 'success';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const textSizes: Record<NonNullable<TextProps['size']>, string> = {
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm',
|
||||
base: 'text-base',
|
||||
lg: 'text-lg',
|
||||
};
|
||||
|
||||
const textWeights: Record<NonNullable<TextProps['weight']>, string> = {
|
||||
normal: 'font-normal',
|
||||
medium: 'font-medium',
|
||||
semibold: 'font-semibold',
|
||||
bold: 'font-bold',
|
||||
};
|
||||
|
||||
const textColors: Record<NonNullable<TextProps['color']>, string> = {
|
||||
default: 'text-gray-900',
|
||||
muted: 'text-neutral-500',
|
||||
primary: 'text-brand-primary',
|
||||
danger: 'text-red-600',
|
||||
success: 'text-green-600',
|
||||
};
|
||||
|
||||
export function Text(props: PropsWithChildren<TextProps>) {
|
||||
const {size = 'base', weight = 'normal', color = 'default', children, class: className} = props;
|
||||
|
||||
const classes = cn(textSizes[size], textWeights[weight], textColors[color], className);
|
||||
|
||||
return <p class={classes}>{children}</p>;
|
||||
}
|
||||
|
||||
export interface LabelProps {
|
||||
htmlFor?: string;
|
||||
required?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function Label(props: PropsWithChildren<LabelProps>) {
|
||||
const {htmlFor, required = false, children, class: className} = props;
|
||||
|
||||
const classes = cn('block text-xs font-semibold uppercase tracking-wide text-neutral-500', className);
|
||||
|
||||
return (
|
||||
<label for={htmlFor} class={classes}>
|
||||
{children}
|
||||
{required && <span class="ml-1 text-red-600">*</span>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export interface CaptionProps {
|
||||
variant?: 'default' | 'error' | 'success';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const captionVariants: Record<NonNullable<CaptionProps['variant']>, string> = {
|
||||
default: 'text-gray-500',
|
||||
error: 'text-red-600',
|
||||
success: 'text-green-600',
|
||||
};
|
||||
|
||||
export function Caption(props: PropsWithChildren<CaptionProps>) {
|
||||
const {variant = 'default', children, class: className} = props;
|
||||
|
||||
const classes = cn('text-xs', captionVariants[variant], className);
|
||||
|
||||
return <p class={classes}>{children}</p>;
|
||||
}
|
||||
|
||||
export interface SectionHeadingProps {
|
||||
actions?: Child;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function SectionHeading(props: PropsWithChildren<SectionHeadingProps>) {
|
||||
const {actions, children, class: className} = props;
|
||||
|
||||
if (actions) {
|
||||
return (
|
||||
<div class={cn('mb-4 flex items-center justify-between', className)}>
|
||||
<h2 class="font-semibold text-gray-900 text-xl">{children}</h2>
|
||||
<div class="flex items-center gap-2">{actions}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <h2 class={cn('mb-4 font-semibold text-gray-900 text-xl', className)}>{children}</h2>;
|
||||
}
|
||||
Reference in New Issue
Block a user