Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,9 @@ template/SimpleModule.Host/wwwroot/*
# Tailwind CSS module scan directory
template/SimpleModule.Host/Styles/_scan/

# Design system preview compiled CSS (derived from theme.css + Tailwind)
docs/design-system/app.css

*.stamp

# k6 load test output
Expand Down
Binary file added .verify/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"!modules/*/src/*/types.ts",
"!packages/SimpleModule.Client/src/routes.ts",
"!template/SimpleModule.Host/ClientApp/routes.ts",
"!docs/design-system",
"!test-projects"
]
}
Expand Down
1,607 changes: 1,607 additions & 0 deletions docs/design-system/index.html

Large diffs are not rendered by default.

53 changes: 40 additions & 13 deletions modules/AuditLogs/src/SimpleModule.AuditLogs/Pages/Browse.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { router } from '@inertiajs/react';
import { useTranslation } from '@simplemodule/client/use-translation';
import { Button, Card, CardContent, PageShell, TooltipProvider } from '@simplemodule/ui';
import {
Button,
Card,
CardContent,
EmptyState,
PageShell,
TooltipProvider,
} from '@simplemodule/ui';
import { type FormEvent, useState } from 'react';
import { AuditLogsKeys } from '@/Locales/keys';
import type { AuditEntry, AuditQueryRequest } from '@/types';
Expand Down Expand Up @@ -141,18 +148,38 @@ export default function Browse({ result, filters }: Props) {

{result.items.length === 0 ? (
<Card>
<CardContent className="py-12 text-center">
<p className="text-lg font-medium text-text">{t(AuditLogsKeys.Browse.EmptyTitle)}</p>
<p className="mt-1 text-sm text-text-muted">
{hasActiveFilters
? t(AuditLogsKeys.Browse.EmptyWithFilters)
: t(AuditLogsKeys.Browse.EmptyNoFilters)}
</p>
{hasActiveFilters && (
<Button variant="secondary" className="mt-4" onClick={clearFilters}>
{t(AuditLogsKeys.Browse.ClearFilters)}
</Button>
)}
<CardContent>
<EmptyState
icon={
<svg
aria-hidden="true"
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
}
title={t(AuditLogsKeys.Browse.EmptyTitle)}
description={
hasActiveFilters
? t(AuditLogsKeys.Browse.EmptyWithFilters)
: t(AuditLogsKeys.Browse.EmptyNoFilters)
}
secondaryAction={
hasActiveFilters ? (
<Button variant="secondary" onClick={clearFilters}>
{t(AuditLogsKeys.Browse.ClearFilters)}
</Button>
) : undefined
}
/>
</CardContent>
</Card>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Card, CardContent } from '@simplemodule/ui';
import { Stat } from '@simplemodule/ui';

/**
* Thin wrapper around the shared {@link Stat} component, kept for backwards
* compatibility with existing dashboard call sites. New code should reach for
* `Stat` directly.
*/
export function KpiCard({
title,
value,
Expand All @@ -14,18 +19,12 @@ export function KpiCard({
onClick?: () => void;
}) {
return (
<Card className={onClick ? 'cursor-pointer transition-shadow hover:shadow-md' : ''}>
<CardContent className="p-4 sm:p-5" onClick={onClick}>
<p className="text-xs font-medium tracking-wide text-text-muted uppercase">{title}</p>
<p
className={`mt-1 text-xl sm:text-2xl font-bold tabular-nums ${
accent === 'danger' ? 'text-danger' : 'text-text'
}`}
>
{value}
</p>
{subtitle && <p className="mt-0.5 text-xs text-text-muted">{subtitle}</p>}
</CardContent>
</Card>
<Stat
label={title}
value={accent === 'danger' ? <span className="text-danger">{value}</span> : value}
change={subtitle}
interactive={onClick != null}
onClick={onClick}
/>
);
}
8 changes: 1 addition & 7 deletions modules/Dashboard/src/SimpleModule.Dashboard/Pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,7 @@ function LandingView({ isDevelopment }: { isDevelopment: boolean }) {
return (
<Container className="flex items-center justify-center min-h-[calc(100vh-16rem)] px-4">
<div className="text-center max-w-lg mx-auto w-full">
{/* Inline style required: Tailwind gradient utilities cannot reference CSS custom properties */}
<div
className="w-14 h-14 sm:w-16 sm:h-16 rounded-2xl mx-auto mb-4 sm:mb-6 flex items-center justify-center text-white text-xl sm:text-2xl font-bold shadow-lg"
style={{
background: 'linear-gradient(135deg, var(--color-primary), var(--color-accent))',
}}
>
<div className="w-14 h-14 sm:w-16 sm:h-16 rounded-2xl mx-auto mb-4 sm:mb-6 flex items-center justify-center text-white text-xl sm:text-2xl font-bold shadow-lg bg-primary">
S
</div>
<h1 className="text-3xl sm:text-4xl font-extrabold mb-3 tracking-tight">
Expand Down
45 changes: 33 additions & 12 deletions modules/Email/src/SimpleModule.Email/Pages/History.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Button,
Card,
CardContent,
EmptyState,
PageShell,
Table,
TableBody,
Expand Down Expand Up @@ -129,18 +130,38 @@ export default function History({ result, filters }: Props) {

{result.items.length === 0 ? (
<Card>
<CardContent className="py-12 text-center">
<p className="text-lg font-medium text-text">{t(EmailKeys.History.EmptyTitle)}</p>
<p className="mt-1 text-sm text-text-muted">
{hasActiveFilters
? t(EmailKeys.History.EmptyWithFilters)
: t(EmailKeys.History.EmptyDescription)}
</p>
{hasActiveFilters && (
<Button variant="secondary" className="mt-4" onClick={clearFilters}>
{t(EmailKeys.History.FilterClear)}
</Button>
)}
<CardContent>
<EmptyState
icon={
<svg
aria-hidden="true"
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<polyline points="22,6 12,13 2,6" />
</svg>
}
title={t(EmailKeys.History.EmptyTitle)}
description={
hasActiveFilters
? t(EmailKeys.History.EmptyWithFilters)
: t(EmailKeys.History.EmptyDescription)
}
secondaryAction={
hasActiveFilters ? (
<Button variant="secondary" onClick={clearFilters}>
{t(EmailKeys.History.FilterClear)}
</Button>
) : undefined
}
/>
</CardContent>
</Card>
) : (
Expand Down
27 changes: 22 additions & 5 deletions modules/Email/src/SimpleModule.Email/Pages/Templates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
EmptyState,
Input,
PageShell,
Table,
Expand Down Expand Up @@ -129,11 +130,27 @@ export default function Templates({ result, filters }: Props) {
{/* Results Table */}
{result.items.length === 0 ? (
<Card>
<CardContent className="py-12 text-center">
<p className="text-lg font-medium text-text">{t(EmailKeys.Templates.EmptyTitle)}</p>
<p className="mt-1 text-sm text-text-muted">
{t(EmailKeys.Templates.EmptyDescription)}
</p>
<CardContent>
<EmptyState
icon={
<svg
aria-hidden="true"
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
}
title={t(EmailKeys.Templates.EmptyTitle)}
description={t(EmailKeys.Templates.EmptyDescription)}
/>
</CardContent>
</Card>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@simplemodule/ui';
import {
EmptyState,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@simplemodule/ui';
import { PolicyTypeBadge, TargetBadge } from './PolicyBadges';
import type { ActivePolicy } from './rate-limiting-types';

export function ActivePoliciesTable({ policies }: { policies: ActivePolicy[] }) {
if (policies.length === 0) {
return (
<p className="text-sm text-muted-foreground py-8 text-center">
No active rate limit policies.
</p>
);
return <EmptyState title="No active rate limit policies" />;
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Button,
EmptyState,
Switch,
Table,
TableBody,
Expand All @@ -20,9 +21,10 @@ interface Props {
export function RulesTable({ rules, onToggle, onDelete }: Props) {
if (rules.length === 0) {
return (
<p className="text-sm text-muted-foreground py-8 text-center">
No rate limit rules configured yet.
</p>
<EmptyState
title="No rate limit rules yet"
description="Configure your first rule to start protecting your endpoints."
/>
);
}

Expand Down
76 changes: 40 additions & 36 deletions modules/Settings/src/SimpleModule.Settings/Pages/MenuManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CardContent,
CardHeader,
CardTitle,
EmptyState,
PageShell,
ScrollArea,
Tooltip,
Expand Down Expand Up @@ -187,24 +188,25 @@ export default function MenuManager({ menuItems: initial, availablePages }: Menu
</CardHeader>
<CardContent className="p-0">
{menus.length === 0 ? (
<div className="flex flex-col items-center justify-center px-6 py-12 text-center">
<svg
className="mb-3 h-10 w-10 text-text-secondary opacity-40"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
<p className="text-sm font-medium text-text-secondary">
{t(SettingsKeys.MenuManager.EmptyTitle)}
</p>
<p className="mt-1 text-xs text-text-secondary">
{t(SettingsKeys.MenuManager.EmptyDescription)}
</p>
</div>
<EmptyState
icon={
<svg
aria-hidden="true"
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
}
title={t(SettingsKeys.MenuManager.EmptyTitle)}
description={t(SettingsKeys.MenuManager.EmptyDescription)}
/>
) : (
<ScrollArea className="max-h-[500px]">
<div className="px-3 pb-3">
Expand Down Expand Up @@ -238,24 +240,26 @@ export default function MenuManager({ menuItems: initial, availablePages }: Menu
onDelete={handleDelete}
/>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center">
<svg
className="mb-3 h-10 w-10 text-text-secondary opacity-40"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
<p className="text-sm font-medium text-text-secondary">
{t(SettingsKeys.MenuManager.NoItemSelectedTitle)}
</p>
<p className="mt-1 text-xs text-text-secondary">
{t(SettingsKeys.MenuManager.NoItemSelectedDescription)}
</p>
</div>
<EmptyState
icon={
<svg
aria-hidden="true"
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125" />
<path d="M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
}
title={t(SettingsKeys.MenuManager.NoItemSelectedTitle)}
description={t(SettingsKeys.MenuManager.NoItemSelectedDescription)}
/>
)}
</CardContent>
</Card>
Expand Down
Loading
Loading