feat: add full page index component with pagination and actions
- Implemented FullPagePageIndex component with mock data for database clusters. - Added pagination and bulk actions for managing database clusters. - Created navigation context hooks for detail, edit, duplicate, and create actions. - Introduced a new store for managing state in full-page and single-page modules. feat: add navigation localization files - Added English and Indonesian localization files for navigation menu items. - Included translations for various modules including CRM, Sales, Supply Chain, and more. feat: create system information shortcuts component - Developed Shortcut component to display keyboard shortcuts with search functionality. - Implemented System component to show placeholder information when system details are unavailable. - Added localization for shortcuts and system information in English and Indonesian. feat: implement global theme store - Created a Zustand store for managing theme color scheme with localStorage persistence. feat: add module page header component - Developed ModulePageHeader component for consistent page header across modules. - Included breadcrumb navigation, title, description, and action buttons. feat: define default privileges for enterprise module - Established default privileges for CRUD operations and other actions in the enterprise module.
This commit is contained in:
@@ -31,7 +31,8 @@
|
|||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-router-dom": "^7.11.0",
|
"react-router-dom": "^7.11.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"zod": "^3.25.36"
|
"zod": "^3.25.36",
|
||||||
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "workspace:*",
|
"@repo/eslint-config": "workspace:*",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { lazy, Suspense, useState } from 'react';
|
import { lazy, Suspense, useState } from 'react';
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { ThemeProvider, ColorSchemeType, DensityType } from '@repo/ui/provider';
|
import { ThemeProvider, DensityType } from '@repo/ui/provider';
|
||||||
import { NotFound, Forbidden, Maintenance, ComingSoon } from '@repo/ui/components';
|
import { NotFound, Forbidden, Maintenance, ComingSoon } from '@repo/ui/components';
|
||||||
import { LoadingScreen } from '../core/components/loading-screen';
|
import { LoadingScreen } from '../core/components/loading-screen';
|
||||||
|
import { useThemeStore } from '../core/store/theme.store';
|
||||||
|
|
||||||
const AuthModule = lazy(() => import('./auth'));
|
const AuthModule = lazy(() => import('./auth'));
|
||||||
const AppModule = lazy(() => import('./modules'));
|
const AppModule = lazy(() => import('./modules'));
|
||||||
@@ -10,7 +11,7 @@ const ShowcaseView = lazy(() => import('./showcase/showcase-view'));
|
|||||||
const ShellDemo = lazy(() => import('./showcase/shell-demo'));
|
const ShellDemo = lazy(() => import('./showcase/shell-demo'));
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [colorScheme, setColorScheme] = useState<ColorSchemeType>('light');
|
const colorScheme = useThemeStore((s) => s.colorScheme);
|
||||||
const [density, setDensity] = useState<DensityType>('compact');
|
const [density, setDensity] = useState<DensityType>('compact');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,20 +21,10 @@ export default function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/auth/*" element={<AuthModule />} />
|
<Route path="/auth/*" element={<AuthModule />} />
|
||||||
<Route path="/app/*" element={<AppModule />} />
|
<Route path="/app/*" element={<AppModule />} />
|
||||||
<Route
|
<Route path="/showcase" element={<ShowcaseView density={density} setDensity={setDensity} />} />
|
||||||
path="/showcase"
|
|
||||||
element={
|
|
||||||
<ShowcaseView
|
|
||||||
colorScheme={colorScheme}
|
|
||||||
setColorScheme={setColorScheme}
|
|
||||||
density={density}
|
|
||||||
setDensity={setDensity}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/shell-demo" element={<ShellDemo />} />
|
<Route path="/shell-demo" element={<ShellDemo />} />
|
||||||
<Route path="/404" element={<NotFound />} />
|
<Route path="/404" element={<NotFound homeUrl="/app" />} />
|
||||||
<Route path="/403" element={<Forbidden />} />
|
<Route path="/403" element={<Forbidden homeUrl="/app" />} />
|
||||||
<Route path="/maintenance" element={<Maintenance />} />
|
<Route path="/maintenance" element={<Maintenance />} />
|
||||||
<Route path="/coming-soon" element={<ComingSoon />} />
|
<Route path="/coming-soon" element={<ComingSoon />} />
|
||||||
<Route path="/" element={<Navigate to="/showcase" />} />
|
<Route path="/" element={<Navigate to="/showcase" />} />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Full Page Management",
|
"title": "Full Page",
|
||||||
|
"description": "An example module of a <1>full page layout</1> for detailed forms.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Manajemen Halaman Penuh",
|
"title": "Halaman Penuh",
|
||||||
|
"description": "Contoh modul <1>tata letak halaman penuh</1> untuk formulir detail.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"name": "Nama",
|
"name": "Nama",
|
||||||
|
|||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
import { Paper, SimpleGrid, Box, Text, Group, Badge, Divider, Breadcrumbs, Anchor, Flex, Title, ActionIcon, Button } from '@repo/ui/components';
|
||||||
|
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||||
|
import { ChevronRight, CheckCircle2, Trash2, Edit2, Play } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function FullPagePageDetail() {
|
||||||
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box pb="xl">
|
||||||
|
{/* --- INLINED PAGE HEADER --- */}
|
||||||
|
<Box mb="xl" mt="xs">
|
||||||
|
<Breadcrumbs
|
||||||
|
mb="md"
|
||||||
|
separator={<ChevronRight size={12} strokeWidth={3} style={{ color: 'var(--mantine-color-gray-5)' }} />}
|
||||||
|
>
|
||||||
|
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
||||||
|
Acme Corp
|
||||||
|
</Anchor>
|
||||||
|
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
||||||
|
Infrastructure
|
||||||
|
</Anchor>
|
||||||
|
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
||||||
|
Database Clusters
|
||||||
|
</Anchor>
|
||||||
|
<Text size="xs" fw={600} style={{ color: 'light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0))', letterSpacing: '0.2px' }}>
|
||||||
|
prod-db-01
|
||||||
|
</Text>
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
|
<Flex gap="md" align="center">
|
||||||
|
<Box>
|
||||||
|
<Group gap="xs" align="center" mb={4}>
|
||||||
|
<Title
|
||||||
|
order={2}
|
||||||
|
fw={600}
|
||||||
|
style={{
|
||||||
|
color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))',
|
||||||
|
letterSpacing: '-0.3px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
prod-db-01
|
||||||
|
</Title>
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
color="teal"
|
||||||
|
size="sm"
|
||||||
|
radius="sm"
|
||||||
|
fw={600}
|
||||||
|
leftSection={<CheckCircle2 size={10} strokeWidth={3} />}
|
||||||
|
style={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
Running
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text size="sm" c="dimmed" fw={400}>
|
||||||
|
PostgreSQL v15.4 · <Text span fw={500} c="var(--mantine-color-text)">us-east-1</Text> · Created Oct 12, 2023
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<ActionIcon variant="light" color="red" size="lg" radius="md" aria-label="Delete">
|
||||||
|
<Trash2 size={18} strokeWidth={2} />
|
||||||
|
</ActionIcon>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Edit2 size={16} strokeWidth={2} style={{ color: 'var(--mantine-color-dimmed)' }} />}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
color="indigo"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Play size={16} strokeWidth={2.5} />}
|
||||||
|
style={{ boxShadow: '0 4px 14px 0 rgba(76, 110, 245, 0.39)' }}
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider mt={28} mb={0} color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
||||||
|
</Box>
|
||||||
|
{/* --- END INLINED PAGE HEADER --- */}
|
||||||
|
|
||||||
|
<Paper withBorder shadow="sm" radius="md" p="xl">
|
||||||
|
<Text size="lg" fw={600} mb="xs" style={{ color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))', letterSpacing: '-0.3px' }}>
|
||||||
|
General Information
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" mb="xl">
|
||||||
|
View the complete details and configuration for this entity.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Divider mb="xl" color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
||||||
|
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xl" verticalSpacing="xl">
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.code')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={500}>WID-001</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.name')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={500}>Dashboard Widget</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.status')}
|
||||||
|
</Text>
|
||||||
|
<Badge variant="light" color="teal" size="sm" radius="sm" fw={600}>
|
||||||
|
ACTIVE
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.description')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={500}>Main dashboard widget</Text>
|
||||||
|
</Box>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
import { Paper, Box, Text, Divider, TextInput, Select, Textarea, Stack, Breadcrumbs, Anchor, Flex, Title, Button, Group } from '@repo/ui/components';
|
||||||
|
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||||
|
import { ChevronRight, Database } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function FullPagePageForm({ formPageType }: { formPageType: 'edit' | 'create' | 'duplicate' }) {
|
||||||
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box pb="xl">
|
||||||
|
{/* --- INLINED PAGE HEADER --- */}
|
||||||
|
<Box mb="xl" mt="xs">
|
||||||
|
<Breadcrumbs
|
||||||
|
mb="md"
|
||||||
|
separator={<ChevronRight size={12} strokeWidth={3} style={{ color: 'var(--mantine-color-gray-5)' }} />}
|
||||||
|
>
|
||||||
|
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
||||||
|
Database Clusters
|
||||||
|
</Anchor>
|
||||||
|
<Text size="xs" fw={600} style={{ color: 'light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0))', letterSpacing: '0.2px' }}>
|
||||||
|
{formPageType === 'create' ? 'Create Cluster' : 'Edit Cluster'}
|
||||||
|
</Text>
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
|
<Flex gap="md" align="center">
|
||||||
|
<Box>
|
||||||
|
<Title
|
||||||
|
order={2}
|
||||||
|
fw={600}
|
||||||
|
mb={4}
|
||||||
|
style={{
|
||||||
|
color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))',
|
||||||
|
letterSpacing: '-0.3px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formPageType === 'create' ? 'Create New Cluster' : 'Edit Cluster Configuration'}
|
||||||
|
</Title>
|
||||||
|
<Text size="sm" c="dimmed" fw={400}>
|
||||||
|
Configure and provision a new highly available database cluster.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<Button variant="default" radius="md">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
color="indigo"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Database size={16} strokeWidth={2.5} />}
|
||||||
|
style={{ boxShadow: '0 4px 14px 0 rgba(76, 110, 245, 0.39)' }}
|
||||||
|
>
|
||||||
|
{formPageType === 'create' ? 'Deploy Cluster' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider mt={28} mb={0} color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
||||||
|
</Box>
|
||||||
|
{/* --- END INLINED PAGE HEADER --- */}
|
||||||
|
|
||||||
|
<Paper withBorder shadow="sm" radius="md" p="xl">
|
||||||
|
<Text size="lg" fw={600} mb="xs" style={{ color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))', letterSpacing: '-0.3px' }}>
|
||||||
|
{formPageType === 'create' ? 'Create Entity' : 'Edit Entity'}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" mb="xl">
|
||||||
|
Fill in the required information to configure your entity properly. Fields marked with * are required.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Divider mb="xl" color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
||||||
|
|
||||||
|
<Box maw={600}>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<TextInput
|
||||||
|
label={t('fields.code')}
|
||||||
|
placeholder="e.g. WID-001"
|
||||||
|
required
|
||||||
|
defaultValue={formPageType === 'edit' ? 'WID-001' : ''}
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={t('fields.name')}
|
||||||
|
placeholder="e.g. Dashboard Widget"
|
||||||
|
required
|
||||||
|
defaultValue={formPageType === 'edit' ? 'Dashboard Widget' : ''}
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label={t('fields.status')}
|
||||||
|
placeholder="Select status"
|
||||||
|
data={['ACTIVE', 'INACTIVE']}
|
||||||
|
defaultValue="ACTIVE"
|
||||||
|
required
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
label={t('fields.description')}
|
||||||
|
placeholder="Enter a detailed description..."
|
||||||
|
minRows={4}
|
||||||
|
defaultValue={formPageType === 'edit' ? 'Main dashboard widget' : ''}
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
+424
@@ -0,0 +1,424 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Box,
|
||||||
|
Paper,
|
||||||
|
Badge,
|
||||||
|
Text,
|
||||||
|
Breadcrumbs,
|
||||||
|
Anchor,
|
||||||
|
Group,
|
||||||
|
Flex,
|
||||||
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
|
ActionIcon,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
TextInput,
|
||||||
|
Pagination,
|
||||||
|
Select,
|
||||||
|
ScrollArea,
|
||||||
|
} from '@repo/ui/components';
|
||||||
|
import { PageActions } from '@repo/ui/components';
|
||||||
|
import { useEnterpriseModuleTranslationContext, useEnterpriseModuleNavigationContext } from '@repo/ui/foundations';
|
||||||
|
import { FullPageEntity } from '../../domain/entities';
|
||||||
|
import {
|
||||||
|
Edit2,
|
||||||
|
Eye,
|
||||||
|
Copy,
|
||||||
|
Trash2,
|
||||||
|
ChevronRight,
|
||||||
|
Database,
|
||||||
|
Activity,
|
||||||
|
MoreHorizontal,
|
||||||
|
Settings,
|
||||||
|
Play,
|
||||||
|
Search,
|
||||||
|
Filter,
|
||||||
|
Plus,
|
||||||
|
CheckCircle2,
|
||||||
|
Ban,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
// TODO: Replace this mock data with real API data loaded from DataService via useEnterpriseModuleDataServiceContext
|
||||||
|
const MOCK_DATA: FullPageEntity[] = Array.from({ length: 50 }).map((_, i) => ({
|
||||||
|
id: String(i + 1),
|
||||||
|
name: `Database Cluster ${i + 1}`,
|
||||||
|
code: `DB-PROD-${String(i + 1).padStart(3, '0')}`,
|
||||||
|
status: i % 7 === 0 ? 'MAINTENANCE' : i % 4 === 0 ? 'INACTIVE' : 'ACTIVE',
|
||||||
|
description: `Managed PostgreSQL cluster instance in region us-east-${(i % 3) + 1}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function FullPagePageIndex() {
|
||||||
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
const { navigateToDetail, navigateToEdit, navigateToDuplicate, navigateToCreate } =
|
||||||
|
useEnterpriseModuleNavigationContext();
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
|
const totalRecords = MOCK_DATA.length;
|
||||||
|
const totalPages = Math.ceil(totalRecords / pageSize);
|
||||||
|
const paginatedData = MOCK_DATA.slice((page - 1) * pageSize, page * pageSize);
|
||||||
|
|
||||||
|
const getRowActions = useCallback(
|
||||||
|
(row: FullPageEntity) => [
|
||||||
|
{
|
||||||
|
key: 'view',
|
||||||
|
label: t('common:view'),
|
||||||
|
icon: <Eye size={14} />,
|
||||||
|
onClick: () => navigateToDetail(row.id as string),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
label: t('common:edit'),
|
||||||
|
icon: <Edit2 size={14} />,
|
||||||
|
onClick: () => navigateToEdit(row.id as string),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'duplicate',
|
||||||
|
label: t('common:duplicate'),
|
||||||
|
icon: <Copy size={14} />,
|
||||||
|
onClick: () => navigateToDuplicate(row.id as string),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider' as const,
|
||||||
|
key: 'div-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: t('common:delete'),
|
||||||
|
icon: <Trash2 size={14} />,
|
||||||
|
intent: 'destructive' as const,
|
||||||
|
onClick: () => {
|
||||||
|
// Mock delete action
|
||||||
|
alert(`Delete ${row.name}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t, navigateToDetail, navigateToEdit, navigateToDuplicate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows = paginatedData.map((item) => (
|
||||||
|
<Table.Tr key={item.id}>
|
||||||
|
<Table.Td>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
fw={600}
|
||||||
|
style={{ color: 'light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0))' }}
|
||||||
|
>
|
||||||
|
{item.code}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge
|
||||||
|
variant={item.status === 'ACTIVE' ? 'light' : item.status === 'MAINTENANCE' ? 'outline' : 'dot'}
|
||||||
|
color={item.status === 'ACTIVE' ? 'teal' : item.status === 'MAINTENANCE' ? 'orange' : 'gray'}
|
||||||
|
size="sm"
|
||||||
|
radius="sm"
|
||||||
|
fw={600}
|
||||||
|
>
|
||||||
|
{item.status}
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{item.description}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td ta="right">
|
||||||
|
<PageActions actions={getRowActions(item)} />
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box pb="xl">
|
||||||
|
{/* --- INLINED PAGE HEADER --- */}
|
||||||
|
<Box mb="xl" mt="xs">
|
||||||
|
<Breadcrumbs
|
||||||
|
mb="md"
|
||||||
|
separator={<ChevronRight size={12} strokeWidth={3} style={{ color: 'var(--mantine-color-gray-5)' }} />}
|
||||||
|
>
|
||||||
|
<Anchor
|
||||||
|
href="#"
|
||||||
|
c="dimmed"
|
||||||
|
size="xs"
|
||||||
|
fw={500}
|
||||||
|
style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}
|
||||||
|
>
|
||||||
|
Acme Corp
|
||||||
|
</Anchor>
|
||||||
|
<Anchor
|
||||||
|
href="#"
|
||||||
|
c="dimmed"
|
||||||
|
size="xs"
|
||||||
|
fw={500}
|
||||||
|
style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}
|
||||||
|
>
|
||||||
|
Infrastructure
|
||||||
|
</Anchor>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
fw={600}
|
||||||
|
style={{
|
||||||
|
color: 'light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0))',
|
||||||
|
letterSpacing: '0.2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Database Clusters
|
||||||
|
</Text>
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
|
<Flex gap="md" align="center">
|
||||||
|
<ThemeIcon
|
||||||
|
size={54}
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="indigo"
|
||||||
|
style={{
|
||||||
|
border: '1px solid light-dark(var(--mantine-color-indigo-1), var(--mantine-color-indigo-9))',
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.03)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Database size={26} strokeWidth={1.5} />
|
||||||
|
</ThemeIcon>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Group gap="xs" align="center" mb={4}>
|
||||||
|
<Title
|
||||||
|
order={2}
|
||||||
|
fw={600}
|
||||||
|
style={{
|
||||||
|
color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))',
|
||||||
|
letterSpacing: '-0.3px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
PostgreSQL Primary
|
||||||
|
</Title>
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
color="teal"
|
||||||
|
size="sm"
|
||||||
|
radius="sm"
|
||||||
|
fw={600}
|
||||||
|
leftSection={<Activity size={10} strokeWidth={3} />}
|
||||||
|
style={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
Healthy
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" color="gray" size="sm" radius="sm" fw={600} style={{ textTransform: 'none' }}>
|
||||||
|
v15.4
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text size="sm" c="dimmed" fw={400}>
|
||||||
|
Managed relational database cluster in{' '}
|
||||||
|
<Text span fw={500} c="var(--mantine-color-text)">
|
||||||
|
us-east-1
|
||||||
|
</Text>{' '}
|
||||||
|
region. Last automated backup was 2 hours ago.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<ActionIcon variant="default" size="lg" radius="md" aria-label="More Options">
|
||||||
|
<MoreHorizontal size={18} strokeWidth={2} style={{ color: 'var(--mantine-color-dimmed)' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Settings size={16} strokeWidth={2} style={{ color: 'var(--mantine-color-dimmed)' }} />}
|
||||||
|
>
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
color="indigo"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Play size={16} strokeWidth={2.5} />}
|
||||||
|
style={{ boxShadow: '0 4px 14px 0 rgba(76, 110, 245, 0.39)' }}
|
||||||
|
>
|
||||||
|
Start Cluster
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider mt={28} mb={0} color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
||||||
|
</Box>
|
||||||
|
{/* --- END INLINED PAGE HEADER --- */}
|
||||||
|
|
||||||
|
<Paper withBorder shadow="sm" radius="md" style={{ overflow: 'hidden' }}>
|
||||||
|
{/* --- TABLE TOOLBAR --- */}
|
||||||
|
<Box
|
||||||
|
p="md"
|
||||||
|
style={{ borderBottom: '1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-7))' }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Search items..."
|
||||||
|
leftSection={<Search size={16} strokeWidth={2} style={{ color: 'var(--mantine-color-dimmed)' }} />}
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
w={280}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Filter size={16} strokeWidth={2} style={{ color: 'var(--mantine-color-dimmed)' }} />}
|
||||||
|
>
|
||||||
|
Filters
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text size="sm" c="dimmed" fw={500} mr="xs">
|
||||||
|
Bulk Actions:
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="teal"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<CheckCircle2 size={14} strokeWidth={2.5} />}
|
||||||
|
>
|
||||||
|
Activate
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="gray"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Ban size={14} strokeWidth={2.5} />}
|
||||||
|
>
|
||||||
|
Deactivate
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Trash2 size={14} strokeWidth={2.5} />}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider
|
||||||
|
orientation="vertical"
|
||||||
|
mx="xs"
|
||||||
|
color="light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-6))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
color="indigo"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Plus size={16} strokeWidth={2.5} />}
|
||||||
|
onClick={navigateToCreate}
|
||||||
|
style={{ boxShadow: '0 4px 12px 0 rgba(76, 110, 245, 0.28)' }}
|
||||||
|
>
|
||||||
|
Create New
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
{/* --- END TABLE TOOLBAR --- */}
|
||||||
|
|
||||||
|
<ScrollArea.Autosize mah="calc(100vh - 350px)" offsetScrollbars>
|
||||||
|
<Table
|
||||||
|
stickyHeader
|
||||||
|
stickyHeaderOffset={-1}
|
||||||
|
striped
|
||||||
|
highlightOnHover
|
||||||
|
verticalSpacing="sm"
|
||||||
|
horizontalSpacing="md"
|
||||||
|
>
|
||||||
|
<Table.Thead bg="light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8))">
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th fw={700} style={{ letterSpacing: '0.5px', textTransform: 'uppercase' }} fz="xs" c="dimmed">
|
||||||
|
{t('fields.code')}
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th fw={700} style={{ letterSpacing: '0.5px', textTransform: 'uppercase' }} fz="xs" c="dimmed">
|
||||||
|
{t('fields.name')}
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th fw={700} style={{ letterSpacing: '0.5px', textTransform: 'uppercase' }} fz="xs" c="dimmed">
|
||||||
|
{t('fields.status')}
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th fw={700} style={{ letterSpacing: '0.5px', textTransform: 'uppercase' }} fz="xs" c="dimmed">
|
||||||
|
{t('fields.description')}
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th
|
||||||
|
fw={700}
|
||||||
|
style={{ letterSpacing: '0.5px', textTransform: 'uppercase' }}
|
||||||
|
fz="xs"
|
||||||
|
c="dimmed"
|
||||||
|
w={80}
|
||||||
|
ta="right"
|
||||||
|
>
|
||||||
|
Actions
|
||||||
|
</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>{rows}</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</ScrollArea.Autosize>
|
||||||
|
|
||||||
|
{/* --- PAGINATION FOOTER --- */}
|
||||||
|
<Box
|
||||||
|
p="md"
|
||||||
|
style={{ borderTop: '1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-7))' }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Text size="sm" c="dimmed" fw={500}>
|
||||||
|
Showing{' '}
|
||||||
|
<Text span fw={600} c="var(--mantine-color-text)">
|
||||||
|
{(page - 1) * pageSize + 1}
|
||||||
|
</Text>{' '}
|
||||||
|
to{' '}
|
||||||
|
<Text span fw={600} c="var(--mantine-color-text)">
|
||||||
|
{Math.min(page * pageSize, totalRecords)}
|
||||||
|
</Text>{' '}
|
||||||
|
of{' '}
|
||||||
|
<Text span fw={600} c="var(--mantine-color-text)">
|
||||||
|
{totalRecords}
|
||||||
|
</Text>{' '}
|
||||||
|
entries
|
||||||
|
</Text>
|
||||||
|
<Group gap="md">
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text size="sm" c="dimmed" fw={500}>
|
||||||
|
Rows per page:
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
data={['10', '20', '50']}
|
||||||
|
value={String(pageSize)}
|
||||||
|
onChange={(val) => {
|
||||||
|
setPageSize(Number(val));
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
w={75}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Pagination total={totalPages} value={page} onChange={setPage} size="sm" radius="md" color="indigo" />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
{/* --- END PAGINATION FOOTER --- */}
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
+123
-7
@@ -1,15 +1,131 @@
|
|||||||
|
import { Paper, SimpleGrid, Box, Text, Group, Badge, Divider, Breadcrumbs, Anchor, Flex, Title, ActionIcon, Button } from '@repo/ui/components';
|
||||||
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||||
|
import { ChevronRight, CheckCircle2, Trash2, Edit2, Play } from 'lucide-react';
|
||||||
|
|
||||||
export default function FullPagePageDetail() {
|
export default function FullPagePageDetail() {
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Box pb="xl">
|
||||||
<div>full-page.page.detail</div>
|
{/* --- INLINED PAGE HEADER --- */}
|
||||||
<div>{t('fields.code')}</div>
|
<Box mb="xl" mt="xs">
|
||||||
<div>{t('fields.name')}</div>
|
<Breadcrumbs
|
||||||
<div>{t('fields.status')}</div>
|
mb="md"
|
||||||
<div>{t('fields.description')}</div>
|
separator={<ChevronRight size={12} strokeWidth={3} style={{ color: 'var(--mantine-color-gray-5)' }} />}
|
||||||
</div>
|
>
|
||||||
|
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
||||||
|
Acme Corp
|
||||||
|
</Anchor>
|
||||||
|
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
||||||
|
Infrastructure
|
||||||
|
</Anchor>
|
||||||
|
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
||||||
|
Database Clusters
|
||||||
|
</Anchor>
|
||||||
|
<Text size="xs" fw={600} style={{ color: 'light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0))', letterSpacing: '0.2px' }}>
|
||||||
|
prod-db-01
|
||||||
|
</Text>
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
|
<Flex gap="md" align="center">
|
||||||
|
<Box>
|
||||||
|
<Group gap="xs" align="center" mb={4}>
|
||||||
|
<Title
|
||||||
|
order={2}
|
||||||
|
fw={600}
|
||||||
|
style={{
|
||||||
|
color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))',
|
||||||
|
letterSpacing: '-0.3px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
prod-db-01
|
||||||
|
</Title>
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
color="teal"
|
||||||
|
size="sm"
|
||||||
|
radius="sm"
|
||||||
|
fw={600}
|
||||||
|
leftSection={<CheckCircle2 size={10} strokeWidth={3} />}
|
||||||
|
style={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
Running
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text size="sm" c="dimmed" fw={400}>
|
||||||
|
PostgreSQL v15.4 · <Text span fw={500} c="var(--mantine-color-text)">us-east-1</Text> · Created Oct 12, 2023
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<ActionIcon variant="light" color="red" size="lg" radius="md" aria-label="Delete">
|
||||||
|
<Trash2 size={18} strokeWidth={2} />
|
||||||
|
</ActionIcon>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Edit2 size={16} strokeWidth={2} style={{ color: 'var(--mantine-color-dimmed)' }} />}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
color="indigo"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Play size={16} strokeWidth={2.5} />}
|
||||||
|
style={{ boxShadow: '0 4px 14px 0 rgba(76, 110, 245, 0.39)' }}
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider mt={28} mb={0} color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
||||||
|
</Box>
|
||||||
|
{/* --- END INLINED PAGE HEADER --- */}
|
||||||
|
|
||||||
|
<Paper withBorder shadow="sm" radius="md" p="xl">
|
||||||
|
<Text size="lg" fw={600} mb="xs" style={{ color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))', letterSpacing: '-0.3px' }}>
|
||||||
|
General Information
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" mb="xl">
|
||||||
|
View the complete details and configuration for this entity.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Divider mb="xl" color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
||||||
|
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xl" verticalSpacing="xl">
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.code')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={500}>WID-001</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.name')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={500}>Dashboard Widget</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.status')}
|
||||||
|
</Text>
|
||||||
|
<Badge variant="light" color="teal" size="sm" radius="sm" fw={600}>
|
||||||
|
ACTIVE
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.description')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={500}>Main dashboard widget</Text>
|
||||||
|
</Box>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+138
-10
@@ -1,18 +1,146 @@
|
|||||||
|
import {
|
||||||
|
Paper,
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
Divider,
|
||||||
|
TextInput,
|
||||||
|
Select,
|
||||||
|
Textarea,
|
||||||
|
Stack,
|
||||||
|
Breadcrumbs,
|
||||||
|
Anchor,
|
||||||
|
Flex,
|
||||||
|
Title,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
} from '@repo/ui/components';
|
||||||
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||||
|
import { ChevronRight, Database } from 'lucide-react';
|
||||||
|
|
||||||
export default function FullPagePageForm({ formPageType }: { formPageType: 'edit' | 'create' | 'duplicate' }) {
|
export default function FullPagePageForm({ formPageType }: { formPageType: 'edit' | 'create' | 'duplicate' }) {
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Box pb="xl">
|
||||||
<div>full-page.page.form</div>
|
{/* --- INLINED PAGE HEADER --- */}
|
||||||
<div>Form Type: {formPageType}</div>
|
<Box mb="xl" mt="xs">
|
||||||
<div>{t('fields.code')}</div>
|
<Breadcrumbs
|
||||||
<div>{t('fields.name')}</div>
|
mb="md"
|
||||||
<div>{t('fields.status')}</div>
|
separator={<ChevronRight size={12} strokeWidth={3} style={{ color: 'var(--mantine-color-gray-5)' }} />}
|
||||||
<div>{t('fields.description')}</div>
|
>
|
||||||
<div>{t('validation:required')}</div>
|
<Anchor
|
||||||
<div>{t('common:save')}</div>
|
href="#"
|
||||||
</div>
|
c="dimmed"
|
||||||
|
size="xs"
|
||||||
|
fw={500}
|
||||||
|
style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}
|
||||||
|
>
|
||||||
|
Database Clusters
|
||||||
|
</Anchor>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
fw={600}
|
||||||
|
style={{
|
||||||
|
color: 'light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0))',
|
||||||
|
letterSpacing: '0.2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formPageType === 'create' ? 'Create Cluster' : 'Edit Cluster'}
|
||||||
|
</Text>
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
|
<Flex gap="md" align="center">
|
||||||
|
<Box>
|
||||||
|
<Title
|
||||||
|
order={2}
|
||||||
|
fw={600}
|
||||||
|
mb={4}
|
||||||
|
style={{
|
||||||
|
color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))',
|
||||||
|
letterSpacing: '-0.3px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formPageType === 'create' ? 'Create New Cluster' : 'Edit Cluster Configuration'}
|
||||||
|
</Title>
|
||||||
|
<Text size="sm" c="dimmed" fw={400}>
|
||||||
|
Configure and provision a new highly available database cluster.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<Button variant="default" radius="md">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
color="indigo"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Database size={16} strokeWidth={2.5} />}
|
||||||
|
style={{ boxShadow: '0 4px 14px 0 rgba(76, 110, 245, 0.39)' }}
|
||||||
|
>
|
||||||
|
{formPageType === 'create' ? 'Deploy Cluster' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider mt={28} mb={0} color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
||||||
|
</Box>
|
||||||
|
{/* --- END INLINED PAGE HEADER --- */}
|
||||||
|
|
||||||
|
<Paper withBorder shadow="sm" radius="md" p="xl">
|
||||||
|
<Text
|
||||||
|
size="lg"
|
||||||
|
fw={600}
|
||||||
|
mb="xs"
|
||||||
|
style={{
|
||||||
|
color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))',
|
||||||
|
letterSpacing: '-0.3px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formPageType === 'create' ? 'Create Entity' : 'Edit Entity'}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" mb="xl">
|
||||||
|
Fill in the required information to configure your entity properly. Fields marked with * are required.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Divider mb="xl" color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
||||||
|
|
||||||
|
<Box maw={600}>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<TextInput
|
||||||
|
label={t('fields.code')}
|
||||||
|
placeholder="e.g. WID-001"
|
||||||
|
required
|
||||||
|
defaultValue={formPageType === 'edit' ? 'WID-001' : ''}
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={t('fields.name')}
|
||||||
|
placeholder="e.g. Dashboard Widget"
|
||||||
|
required
|
||||||
|
defaultValue={formPageType === 'edit' ? 'Dashboard Widget' : ''}
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label={t('fields.status')}
|
||||||
|
placeholder="Select status"
|
||||||
|
data={['ACTIVE', 'INACTIVE']}
|
||||||
|
defaultValue="ACTIVE"
|
||||||
|
required
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
label={t('fields.description')}
|
||||||
|
placeholder="Enter a detailed description..."
|
||||||
|
minRows={4}
|
||||||
|
defaultValue={formPageType === 'edit' ? 'Main dashboard widget' : ''}
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+380
-92
@@ -1,99 +1,387 @@
|
|||||||
import { Table, Box, Title, Paper } from '@repo/ui/components';
|
import { EnterpriseIndexPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||||
import { PageActions } from '@repo/ui/components';
|
import { Trans } from '@repo/core-i18n';
|
||||||
import { useEnterpriseModuleTranslationContext, useEnterpriseModuleNavigationContext } from '@repo/ui/foundations';
|
import { Badge, Text } from '@repo/ui/components';
|
||||||
import { FullPageEntity } from '../../domain/entities';
|
import { Activity, LayoutDashboard } from 'lucide-react';
|
||||||
import { Edit2, Eye, Copy, Trash2 } from 'lucide-react';
|
import { FullPageModuleConfig } from '../../domain/constants';
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
// TODO: Replace this mock data with real API data loaded from DataService via useEnterpriseModuleDataServiceContext
|
|
||||||
const MOCK_DATA: FullPageEntity[] = [
|
|
||||||
{ id: '1', name: 'Dashboard Widget', code: 'WID-001', status: 'ACTIVE', description: 'Main dashboard widget' },
|
|
||||||
{ id: '2', name: 'Report Generator', code: 'REP-002', status: 'INACTIVE', description: 'Generates monthly reports' },
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'User Management',
|
|
||||||
code: 'USR-003',
|
|
||||||
status: 'ACTIVE',
|
|
||||||
description: 'Manages user roles and permissions',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function FullPagePageIndex() {
|
export default function FullPagePageIndex() {
|
||||||
// Translation is scoped to ['FULL_PAGE', 'common'] — no prefix needed for module keys
|
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
|
||||||
const { navigateToDetail, navigateToEdit, navigateToDuplicate } = useEnterpriseModuleNavigationContext();
|
|
||||||
|
|
||||||
const getRowActions = useCallback(
|
|
||||||
(row: FullPageEntity) => [
|
|
||||||
{
|
|
||||||
key: 'view',
|
|
||||||
label: t('common:view'),
|
|
||||||
icon: <Eye size={14} />,
|
|
||||||
onClick: () => navigateToDetail(row.id as string),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'edit',
|
|
||||||
label: t('common:edit'),
|
|
||||||
icon: <Edit2 size={14} />,
|
|
||||||
onClick: () => navigateToEdit(row.id as string),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'duplicate',
|
|
||||||
label: t('common:duplicate'),
|
|
||||||
icon: <Copy size={14} />,
|
|
||||||
onClick: () => navigateToDuplicate(row.id as string),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'divider' as const,
|
|
||||||
key: 'div-1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'delete',
|
|
||||||
label: t('common:delete'),
|
|
||||||
icon: <Trash2 size={14} />,
|
|
||||||
intent: 'destructive' as const,
|
|
||||||
onClick: () => {
|
|
||||||
// Mock delete action
|
|
||||||
alert(`Delete ${row.name}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[t, navigateToDetail, navigateToEdit, navigateToDuplicate],
|
|
||||||
);
|
|
||||||
|
|
||||||
const rows = MOCK_DATA.map((item) => (
|
|
||||||
<Table.Tr key={item.id}>
|
|
||||||
<Table.Td>{item.code}</Table.Td>
|
|
||||||
<Table.Td>{item.name}</Table.Td>
|
|
||||||
<Table.Td>{item.status}</Table.Td>
|
|
||||||
<Table.Td>{item.description}</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<PageActions actions={getRowActions(item)} />
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box p="md">
|
<EnterpriseIndexPageProvider
|
||||||
<Title order={2} mb="md">
|
pageHeaderProps={{
|
||||||
{t('title')}
|
title: t('title'),
|
||||||
</Title>
|
description: (
|
||||||
|
<Trans
|
||||||
<Paper withBorder shadow="sm" radius="md">
|
t={t}
|
||||||
<Table striped highlightOnHover>
|
i18nKey="description"
|
||||||
<Table.Thead>
|
components={{
|
||||||
<Table.Tr>
|
1: <Text span fw={500} c="var(--mantine-color-text)" />,
|
||||||
<Table.Th>{t('fields.code')}</Table.Th>
|
}}
|
||||||
<Table.Th>{t('fields.name')}</Table.Th>
|
/>
|
||||||
<Table.Th>{t('fields.status')}</Table.Th>
|
),
|
||||||
<Table.Th>{t('fields.description')}</Table.Th>
|
badges: (
|
||||||
<Table.Th w={200}>{t('common:edit')}</Table.Th>
|
<Badge
|
||||||
</Table.Tr>
|
variant="light"
|
||||||
</Table.Thead>
|
color="teal"
|
||||||
<Table.Tbody>{rows}</Table.Tbody>
|
size="sm"
|
||||||
</Table>
|
radius="sm"
|
||||||
</Paper>
|
fw={600}
|
||||||
</Box>
|
leftSection={<Activity size={10} strokeWidth={3} />}
|
||||||
|
style={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
Healthy
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
breadcrumbs: [
|
||||||
|
{ label: t('nav:example-module'), type: 'text' },
|
||||||
|
{ label: t('nav:example-full-page'), type: 'link', href: `${FullPageModuleConfig.webUrl}/index` },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'justify' }}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Porro quae aut minus doloremque mollitia quis,
|
||||||
|
consequatur enim dolorum sunt itaque quia perferendis cumque accusantium reprehenderit laboriosam, voluptate,
|
||||||
|
voluptas voluptates neque! Lorem ipsum dolor sit amet consectetur adipisicing elit. Perferendis dolorum
|
||||||
|
excepturi sunt. Omnis eaque, laborum adipisci unde est architecto deserunt fugit eos pariatur dicta? Cumque
|
||||||
|
autem repellat distinctio ullam necessitatibus! Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet
|
||||||
|
placeat voluptas, aliquam non delectus ratione labore illo, ipsa voluptate exercitationem repellendus
|
||||||
|
consequatur blanditiis ullam autem excepturi molestias porro velit facere.
|
||||||
|
</div>
|
||||||
|
</EnterpriseIndexPageProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Burger,
|
Burger,
|
||||||
Group,
|
Group,
|
||||||
Select,
|
Select,
|
||||||
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
useCoreAppShell,
|
useCoreAppShell,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
@@ -11,8 +12,9 @@ import {
|
|||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
Box,
|
Box,
|
||||||
} from '@repo/ui/components';
|
} from '@repo/ui/components';
|
||||||
import { Globe, Bell, HelpCircle, Settings, User, Briefcase, LogOut, ChevronDown } from 'lucide-react';
|
import { Globe, Bell, HelpCircle, Settings, User, Briefcase, LogOut, ChevronDown, Sun, Moon } from 'lucide-react';
|
||||||
import { AppStorageKey, secureStorage } from '../../../../core/storage/local';
|
import { AppStorageKey, secureStorage } from '../../../../core/storage/local';
|
||||||
|
import { useThemeStore } from '../../../../core/store/theme.store';
|
||||||
import { NotificationDropdown } from './notifications/notification-dropdown';
|
import { NotificationDropdown } from './notifications/notification-dropdown';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -35,16 +37,10 @@ const getInitials = (name: string): string => {
|
|||||||
export default function HeaderLayout() {
|
export default function HeaderLayout() {
|
||||||
const { i18n, t } = useTranslation();
|
const { i18n, t } = useTranslation();
|
||||||
const { mobileOpened, toggleMobile } = useCoreAppShell();
|
const { mobileOpened, toggleMobile } = useCoreAppShell();
|
||||||
|
const { colorScheme, setColorScheme } = useThemeStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group h="100%" px="md" justify="space-between" bg="var(--mantine-color-body)" wrap="nowrap">
|
||||||
h="100%"
|
|
||||||
px="md"
|
|
||||||
justify="space-between"
|
|
||||||
bg="var(--mantine-color-body)"
|
|
||||||
style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}
|
|
||||||
wrap="nowrap"
|
|
||||||
>
|
|
||||||
{/* 1. LEFT SECTION (Navigation & Context) */}
|
{/* 1. LEFT SECTION (Navigation & Context) */}
|
||||||
<Group wrap="nowrap" gap="sm">
|
<Group wrap="nowrap" gap="sm">
|
||||||
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
||||||
@@ -57,7 +53,14 @@ export default function HeaderLayout() {
|
|||||||
|
|
||||||
{/* 3. RIGHT SECTION (Actions & Profile) */}
|
{/* 3. RIGHT SECTION (Actions & Profile) */}
|
||||||
<Group wrap="nowrap" gap="sm">
|
<Group wrap="nowrap" gap="sm">
|
||||||
<ActionIcon component={Link} to="/app/system/information" variant="subtle" color="gray" size="lg" aria-label="Help">
|
<ActionIcon
|
||||||
|
component={Link}
|
||||||
|
to="/app/system/information"
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
size="lg"
|
||||||
|
aria-label="Help"
|
||||||
|
>
|
||||||
<HelpCircle size={18} />
|
<HelpCircle size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
||||||
@@ -136,6 +139,7 @@ export default function HeaderLayout() {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
height: '22px',
|
height: '22px',
|
||||||
minHeight: '22px',
|
minHeight: '22px',
|
||||||
|
fontSize: 'var(--mantine-font-size-sm)',
|
||||||
},
|
},
|
||||||
root: {
|
root: {
|
||||||
marginTop: '-4px',
|
marginTop: '-4px',
|
||||||
@@ -145,11 +149,30 @@ export default function HeaderLayout() {
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
{/* Custom Theme Item */}
|
||||||
|
<Group justify="space-between" wrap="nowrap" px="sm" py="xs">
|
||||||
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
{colorScheme === 'dark' ? <Moon size={14} /> : <Sun size={14} />}
|
||||||
|
<Text size="sm">{t('common:theme')}</Text>
|
||||||
|
</Group>
|
||||||
|
<Switch
|
||||||
|
checked={colorScheme === 'dark'}
|
||||||
|
onChange={(event) => setColorScheme(event.currentTarget.checked ? 'dark' : 'light')}
|
||||||
|
size="sm"
|
||||||
|
color="brand"
|
||||||
|
onLabel={<Moon size={12} strokeWidth={2.5} color="var(--mantine-color-white)" />}
|
||||||
|
offLabel={<Sun size={12} strokeWidth={2.5} color="var(--mantine-color-brand-6)" />}
|
||||||
|
styles={{ track: { cursor: 'pointer' } }}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
{/* Application Section */}
|
{/* Application Section */}
|
||||||
<Menu.Label>{t('common:application')}</Menu.Label>
|
<Menu.Label>{t('common:application')}</Menu.Label>
|
||||||
<Menu.Item component={Link} to="/app/system/setting" leftSection={<Settings size={14} />}>{t('common:settings')}</Menu.Item>
|
<Menu.Item component={Link} to="/app/system/setting" leftSection={<Settings size={14} />}>
|
||||||
|
{t('common:settings')}
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item leftSection={<User size={14} />}>{t('common:myProfile')}</Menu.Item>
|
<Menu.Item leftSection={<User size={14} />}>{t('common:myProfile')}</Menu.Item>
|
||||||
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|||||||
+11
-3
@@ -78,7 +78,7 @@ export function NotificationDropdown({ children }: NotificationDropdownProps) {
|
|||||||
justify="space-between"
|
justify="space-between"
|
||||||
px="md"
|
px="md"
|
||||||
py="xs"
|
py="xs"
|
||||||
style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}
|
style={{ borderBottom: '1px solid var(--app-shell-border-color)' }}
|
||||||
>
|
>
|
||||||
<Text fw={600} size="sm">
|
<Text fw={600} size="sm">
|
||||||
{t('common:notificationsTitle')}
|
{t('common:notificationsTitle')}
|
||||||
@@ -129,7 +129,7 @@ export function NotificationDropdown({ children }: NotificationDropdownProps) {
|
|||||||
active={!item.isRead}
|
active={!item.isRead}
|
||||||
variant="light"
|
variant="light"
|
||||||
style={{
|
style={{
|
||||||
borderBottom: '1px solid var(--mantine-color-default-border)',
|
borderBottom: '1px solid var(--app-shell-border-color)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -138,7 +138,15 @@ export function NotificationDropdown({ children }: NotificationDropdownProps) {
|
|||||||
|
|
||||||
{notifications.length > 0 && (
|
{notifications.length > 0 && (
|
||||||
<Box p="xs">
|
<Box p="xs">
|
||||||
<Button component={Link} to="/app/system/notifications" variant="light" color="gray" fullWidth size="xs" radius="md">
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to="/app/system/notifications"
|
||||||
|
variant="light"
|
||||||
|
color="gray"
|
||||||
|
fullWidth
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
{t('common:viewAll')}
|
{t('common:viewAll')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const MenuItemExpanded = memo(function MenuItemExpanded({
|
|||||||
expandVersion = 0,
|
expandVersion = 0,
|
||||||
collapseVersion = 0,
|
collapseVersion = 0,
|
||||||
}: MenuItemExpandedProps) {
|
}: MenuItemExpandedProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = activeKeys.has(item.key);
|
const isActive = activeKeys.has(item.key);
|
||||||
const hasChildren = item.children && item.children.length > 0;
|
const hasChildren = item.children && item.children.length > 0;
|
||||||
@@ -66,7 +67,7 @@ const MenuItemExpanded = memo(function MenuItemExpanded({
|
|||||||
<NavLink
|
<NavLink
|
||||||
component={hasChildren ? 'button' : (Link as any)}
|
component={hasChildren ? 'button' : (Link as any)}
|
||||||
to={hasChildren ? undefined : item.path}
|
to={hasChildren ? undefined : item.path}
|
||||||
label={item.label}
|
label={t(item.label)}
|
||||||
leftSection={<Icon size={18} />}
|
leftSection={<Icon size={18} />}
|
||||||
active={isExactActive}
|
active={isExactActive}
|
||||||
opened={isOpened}
|
opened={isOpened}
|
||||||
@@ -75,7 +76,7 @@ const MenuItemExpanded = memo(function MenuItemExpanded({
|
|||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
borderRadius: 'var(--mantine-radius-md)',
|
borderRadius: 'var(--mantine-radius-md)',
|
||||||
color: isParentActive ? 'var(--mantine-primary-color-filled)' : undefined,
|
color: isParentActive ? 'light-dark(var(--mantine-color-brand-6), var(--mantine-color-brand-4))' : undefined,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -110,6 +111,7 @@ interface MenuItemFlyoutProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot = true }: MenuItemFlyoutProps) {
|
const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot = true }: MenuItemFlyoutProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = activeKeys.has(item.key);
|
const isActive = activeKeys.has(item.key);
|
||||||
const hasChildren = item.children && item.children.length > 0;
|
const hasChildren = item.children && item.children.length > 0;
|
||||||
@@ -118,14 +120,14 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
|
|||||||
if (!hasChildren) {
|
if (!hasChildren) {
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
return (
|
return (
|
||||||
<Tooltip label={item.label} position="right" withArrow transitionProps={{ transition: 'fade-right' }}>
|
<Tooltip label={t(item.label)} position="right" withArrow transitionProps={{ transition: 'fade-right' }}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
component={Link as any}
|
component={Link as any}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
variant={isActive ? 'light' : 'subtle'}
|
variant={isActive ? 'light' : 'subtle'}
|
||||||
color={isActive ? undefined : 'gray'}
|
color={isActive ? undefined : 'var(--mantine-color-text)'}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label={item.label}
|
aria-label={t(item.label)}
|
||||||
>
|
>
|
||||||
<Icon size={20} />
|
<Icon size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -137,7 +139,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
|
|||||||
component={Link as any}
|
component={Link as any}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
leftSection={<Icon size={14} />}
|
leftSection={<Icon size={14} />}
|
||||||
color={isActive ? 'var(--mantine-primary-color-filled)' : undefined}
|
color={isActive ? 'brand' : undefined}
|
||||||
styles={{
|
styles={{
|
||||||
itemLabel: {
|
itemLabel: {
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -147,7 +149,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.label}
|
{t(item.label)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -157,9 +159,9 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
|
|||||||
const Target = isRoot ? (
|
const Target = isRoot ? (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={isActive ? 'light' : 'subtle'}
|
variant={isActive ? 'light' : 'subtle'}
|
||||||
color={isActive ? undefined : 'gray'}
|
color={isActive ? undefined : 'var(--mantine-color-text)'}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label={item.label}
|
aria-label={t(item.label)}
|
||||||
>
|
>
|
||||||
<Icon size={20} />
|
<Icon size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -167,7 +169,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<Icon size={14} />}
|
leftSection={<Icon size={14} />}
|
||||||
rightSection={<ChevronsRight size={14} />}
|
rightSection={<ChevronsRight size={14} />}
|
||||||
color={isActive ? 'var(--mantine-primary-color-filled)' : undefined}
|
color={isActive ? 'brand' : undefined}
|
||||||
closeMenuOnClick={false} // Mencegah parent tertutup saat memunculkan Level 3
|
closeMenuOnClick={false} // Mencegah parent tertutup saat memunculkan Level 3
|
||||||
styles={{
|
styles={{
|
||||||
itemLabel: {
|
itemLabel: {
|
||||||
@@ -178,7 +180,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.label}
|
{t(item.label)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -197,7 +199,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
|
|||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{isRoot && (
|
{isRoot && (
|
||||||
<>
|
<>
|
||||||
<Menu.Label>{item.label}</Menu.Label>
|
<Menu.Label>{t(item.label)}</Menu.Label>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -295,7 +297,7 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
const query = searchQuery.toLowerCase();
|
const query = searchQuery.toLowerCase();
|
||||||
|
|
||||||
const filterItem = (item: MenuItemType): MenuItemType | null => {
|
const filterItem = (item: MenuItemType): MenuItemType | null => {
|
||||||
const isMatch = item.label.toLowerCase().includes(query);
|
const isMatch = t(item.label).toLowerCase().includes(query);
|
||||||
|
|
||||||
if (item.children) {
|
if (item.children) {
|
||||||
const filteredChildren = item.children.map(filterItem).filter((child): child is MenuItemType => child !== null);
|
const filteredChildren = item.children.map(filterItem).filter((child): child is MenuItemType => child !== null);
|
||||||
@@ -336,11 +338,13 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
setIsMac(typeof window !== 'undefined' && navigator.userAgent.includes('Mac'));
|
setIsMac(typeof window !== 'undefined' && navigator.userAgent.includes('Mac'));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const shortcutText = isMac ? '⌘K' : 'Ctrl+K';
|
const filterMenuShortcutText = isMac ? '⇧⌘M' : 'Ctrl+Shift+M';
|
||||||
|
const toggleShortcutText = isMac ? '⇧⌘B' : 'Ctrl+Shift+B';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
// Shortcut 1: Cmd/Ctrl + Shift + M for SEARCH MENU
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'm') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (isMini) {
|
if (isMini) {
|
||||||
handleExpandAndSearch();
|
handleExpandAndSearch();
|
||||||
@@ -348,6 +352,12 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shortcut 2: Cmd/Ctrl + Shift + B for TOGGLE SIDEBAR
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'b') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggle();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
@@ -359,20 +369,27 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
<Box h="100%" style={{ display: 'flex', flexDirection: 'column' }}>
|
<Box h="100%" style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
{withMenuFilter && (
|
{withMenuFilter && (
|
||||||
<Box
|
<Box
|
||||||
p="xs"
|
p="md"
|
||||||
style={{
|
style={{
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
backgroundColor: 'color-mix(in srgb, var(--mantine-color-body) 80%, transparent)',
|
backgroundColor: 'color-mix(in srgb, var(--mantine-color-body) 80%, transparent)',
|
||||||
backdropFilter: 'blur(8px)',
|
backdropFilter: 'blur(8px)',
|
||||||
borderBottom: '1px solid var(--mantine-color-default-border)',
|
borderBottom: '1px solid var(--app-shell-border-color)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ActionIcon variant="subtle" size="lg" onClick={handleExpandAndSearch} aria-label="Search menu">
|
<ActionIcon
|
||||||
<Search size={18} />
|
variant="subtle"
|
||||||
|
size="md"
|
||||||
|
onClick={handleExpandAndSearch}
|
||||||
|
aria-label="Search menu"
|
||||||
|
color="var(--mantine-color-text)"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<Search size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -387,7 +404,7 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Stack align="center" p="xs">
|
<Stack align="center" p="xs">
|
||||||
<Tooltip label={t('common:expandSidebar')} position="right" withArrow>
|
<Tooltip label={`${t('common:expandSidebar')} (${toggleShortcutText})`} position="right" withArrow>
|
||||||
<ActionIcon variant="subtle" color="gray" size="lg" onClick={handleToggle} aria-label="Expand sidebar">
|
<ActionIcon variant="subtle" color="gray" size="lg" onClick={handleToggle} aria-label="Expand sidebar">
|
||||||
<ChevronsRight size={18} />
|
<ChevronsRight size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -411,7 +428,7 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
backgroundColor: 'color-mix(in srgb, var(--mantine-color-body) 80%, transparent)',
|
backgroundColor: 'color-mix(in srgb, var(--mantine-color-body) 80%, transparent)',
|
||||||
backdropFilter: 'blur(8px)',
|
backdropFilter: 'blur(8px)',
|
||||||
borderBottom: '1px solid var(--mantine-color-default-border)',
|
borderBottom: '1px solid var(--app-shell-border-color)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
<Box style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
@@ -439,11 +456,11 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
padding: '2px 4px',
|
padding: '2px 4px',
|
||||||
color: 'var(--mantine-color-dimmed)',
|
color: 'var(--mantine-color-dimmed)',
|
||||||
border: '1px solid var(--mantine-color-default-border)',
|
border: '1px solid var(--app-shell-border-color)',
|
||||||
borderRadius: 'var(--mantine-radius-sm)',
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{shortcutText}
|
{filterMenuShortcutText}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -485,7 +502,7 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Box p="xs">
|
<Box p="xs">
|
||||||
<NavLink
|
<NavLink
|
||||||
label={t('common:collapse')}
|
label={`${t('common:collapse')} (${toggleShortcutText})`}
|
||||||
leftSection={<ChevronsLeft size={18} />}
|
leftSection={<ChevronsLeft size={18} />}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
|
|||||||
@@ -35,31 +35,31 @@ import type { MenuItemType } from '../types/menu.types';
|
|||||||
export const MENU_ITEMS: MenuItemType[] = [
|
export const MENU_ITEMS: MenuItemType[] = [
|
||||||
{
|
{
|
||||||
key: 'dashboard',
|
key: 'dashboard',
|
||||||
label: 'Dashboard',
|
label: 'nav:dashboard',
|
||||||
icon: LayoutDashboard,
|
icon: LayoutDashboard,
|
||||||
path: '/app/dashboard',
|
path: '/app/dashboard',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'crm',
|
key: 'crm',
|
||||||
label: 'CRM',
|
label: 'nav:crm',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
path: '/app/crm',
|
path: '/app/crm',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'crm-leads',
|
key: 'crm-leads',
|
||||||
label: 'Leads',
|
label: 'nav:crm-leads',
|
||||||
icon: Briefcase,
|
icon: Briefcase,
|
||||||
path: '/app/crm/leads',
|
path: '/app/crm/leads',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'crm-pipelines',
|
key: 'crm-pipelines',
|
||||||
label: 'Pipelines',
|
label: 'nav:crm-pipelines',
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
path: '/app/crm/pipelines',
|
path: '/app/crm/pipelines',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'crm-contacts',
|
key: 'crm-contacts',
|
||||||
label: 'Contacts',
|
label: 'nav:crm-contacts',
|
||||||
icon: Phone,
|
icon: Phone,
|
||||||
path: '/app/crm/contacts',
|
path: '/app/crm/contacts',
|
||||||
},
|
},
|
||||||
@@ -67,25 +67,25 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sales',
|
key: 'sales',
|
||||||
label: 'Sales',
|
label: 'nav:sales',
|
||||||
icon: ShoppingCart,
|
icon: ShoppingCart,
|
||||||
path: '/app/sales',
|
path: '/app/sales',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'sales-quotations',
|
key: 'sales-quotations',
|
||||||
label: 'Quotations',
|
label: 'nav:sales-quotations',
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
path: '/app/sales/quotations',
|
path: '/app/sales/quotations',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sales-orders',
|
key: 'sales-orders',
|
||||||
label: 'Sales Orders',
|
label: 'nav:sales-orders',
|
||||||
icon: Box,
|
icon: Box,
|
||||||
path: '/app/sales/orders',
|
path: '/app/sales/orders',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sales-invoices',
|
key: 'sales-invoices',
|
||||||
label: 'Invoices',
|
label: 'nav:sales-invoices',
|
||||||
icon: Receipt,
|
icon: Receipt,
|
||||||
path: '/app/sales/invoices',
|
path: '/app/sales/invoices',
|
||||||
},
|
},
|
||||||
@@ -93,31 +93,31 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'supply-chain',
|
key: 'supply-chain',
|
||||||
label: 'Supply Chain',
|
label: 'nav:supply-chain',
|
||||||
icon: Truck,
|
icon: Truck,
|
||||||
path: '/app/supply-chain',
|
path: '/app/supply-chain',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'sc-inventory',
|
key: 'sc-inventory',
|
||||||
label: 'Inventory Management',
|
label: 'nav:sc-inventory',
|
||||||
icon: Box,
|
icon: Box,
|
||||||
path: '/app/supply-chain/inventory',
|
path: '/app/supply-chain/inventory',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'sc-inventory-products',
|
key: 'sc-inventory-products',
|
||||||
label: 'Products',
|
label: 'nav:sc-inventory-products',
|
||||||
icon: Layers,
|
icon: Layers,
|
||||||
path: '/app/supply-chain/inventory/products',
|
path: '/app/supply-chain/inventory/products',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sc-inventory-categories',
|
key: 'sc-inventory-categories',
|
||||||
label: 'Categories',
|
label: 'nav:sc-inventory-categories',
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
path: '/app/supply-chain/inventory/categories',
|
path: '/app/supply-chain/inventory/categories',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sc-inventory-adjustments',
|
key: 'sc-inventory-adjustments',
|
||||||
label: 'Stock Adjustments',
|
label: 'nav:sc-inventory-adjustments',
|
||||||
icon: FileSearch,
|
icon: FileSearch,
|
||||||
path: '/app/supply-chain/inventory/adjustments',
|
path: '/app/supply-chain/inventory/adjustments',
|
||||||
},
|
},
|
||||||
@@ -125,13 +125,13 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sc-warehouses',
|
key: 'sc-warehouses',
|
||||||
label: 'Warehouses',
|
label: 'nav:sc-warehouses',
|
||||||
icon: Warehouse,
|
icon: Warehouse,
|
||||||
path: '/app/supply-chain/warehouses',
|
path: '/app/supply-chain/warehouses',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sc-logistics',
|
key: 'sc-logistics',
|
||||||
label: 'Logistics',
|
label: 'nav:sc-logistics',
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
path: '/app/supply-chain/logistics',
|
path: '/app/supply-chain/logistics',
|
||||||
},
|
},
|
||||||
@@ -139,19 +139,19 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'manufacturing',
|
key: 'manufacturing',
|
||||||
label: 'Manufacturing',
|
label: 'nav:manufacturing',
|
||||||
icon: Factory,
|
icon: Factory,
|
||||||
path: '/app/manufacturing',
|
path: '/app/manufacturing',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'mfg-bom',
|
key: 'mfg-bom',
|
||||||
label: 'Bill of Materials',
|
label: 'nav:mfg-bom',
|
||||||
icon: Layers,
|
icon: Layers,
|
||||||
path: '/app/manufacturing/bom',
|
path: '/app/manufacturing/bom',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'mfg-work-orders',
|
key: 'mfg-work-orders',
|
||||||
label: 'Work Orders',
|
label: 'nav:mfg-work-orders',
|
||||||
icon: HardHat,
|
icon: HardHat,
|
||||||
path: '/app/manufacturing/work-orders',
|
path: '/app/manufacturing/work-orders',
|
||||||
},
|
},
|
||||||
@@ -159,31 +159,31 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'hris',
|
key: 'hris',
|
||||||
label: 'HRIS',
|
label: 'nav:hris',
|
||||||
icon: Briefcase,
|
icon: Briefcase,
|
||||||
path: '/app/hris',
|
path: '/app/hris',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'hris-employees',
|
key: 'hris-employees',
|
||||||
label: 'Employees',
|
label: 'nav:hris-employees',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
path: '/app/hris/employees',
|
path: '/app/hris/employees',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'hris-attendance',
|
key: 'hris-attendance',
|
||||||
label: 'Attendance',
|
label: 'nav:hris-attendance',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
path: '/app/hris/attendance',
|
path: '/app/hris/attendance',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'hris-payroll',
|
key: 'hris-payroll',
|
||||||
label: 'Payroll',
|
label: 'nav:hris-payroll',
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
path: '/app/hris/payroll',
|
path: '/app/hris/payroll',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'hris-calendar',
|
key: 'hris-calendar',
|
||||||
label: 'Company Calendar',
|
label: 'nav:hris-calendar',
|
||||||
icon: Calendar,
|
icon: Calendar,
|
||||||
path: '/app/hris/calendar',
|
path: '/app/hris/calendar',
|
||||||
},
|
},
|
||||||
@@ -191,19 +191,19 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'accounting',
|
key: 'accounting',
|
||||||
label: 'Accounting',
|
label: 'nav:accounting',
|
||||||
icon: Calculator,
|
icon: Calculator,
|
||||||
path: '/app/accounting',
|
path: '/app/accounting',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'acc-gl',
|
key: 'acc-gl',
|
||||||
label: 'General Ledger',
|
label: 'nav:acc-gl',
|
||||||
icon: Database,
|
icon: Database,
|
||||||
path: '/app/accounting/general-ledger',
|
path: '/app/accounting/general-ledger',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'acc-taxes',
|
key: 'acc-taxes',
|
||||||
label: 'Taxes',
|
label: 'nav:acc-taxes',
|
||||||
icon: PiggyBank,
|
icon: PiggyBank,
|
||||||
path: '/app/accounting/taxes',
|
path: '/app/accounting/taxes',
|
||||||
},
|
},
|
||||||
@@ -211,25 +211,25 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'settings',
|
key: 'settings',
|
||||||
label: 'Settings & Configuration',
|
label: 'nav:settings',
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
path: '/app/settings',
|
path: '/app/settings',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'settings-general',
|
key: 'settings-general',
|
||||||
label: 'General Settings',
|
label: 'nav:settings-general',
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
path: '/app/settings/general',
|
path: '/app/settings/general',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'settings-security',
|
key: 'settings-security',
|
||||||
label: 'Security',
|
label: 'nav:settings-security',
|
||||||
icon: Shield,
|
icon: Shield,
|
||||||
path: '/app/settings/security',
|
path: '/app/settings/security',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'long-text-2',
|
key: 'long-text-2',
|
||||||
label: 'Extremely Long Menu Name To Test Text Truncation Handling Properly',
|
label: 'nav:long-text-2',
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
path: '/app/settings/long-menu-test',
|
path: '/app/settings/long-menu-test',
|
||||||
},
|
},
|
||||||
@@ -237,19 +237,19 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'example-module',
|
key: 'example-module',
|
||||||
label: 'Example Module',
|
label: 'nav:example-module',
|
||||||
icon: Database,
|
icon: Database,
|
||||||
path: '/app/example-module',
|
path: '/app/example-module',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'example-full-page',
|
key: 'example-full-page',
|
||||||
label: 'Example Full Page',
|
label: 'nav:example-full-page',
|
||||||
icon: LayoutDashboard,
|
icon: LayoutDashboard,
|
||||||
path: '/app/example/full-page/index',
|
path: '/app/example/full-page/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'example-single-page',
|
key: 'example-single-page',
|
||||||
label: 'Example Single Page',
|
label: 'nav:example-single-page',
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
path: '/app/example/single-page/index',
|
path: '/app/example/single-page/index',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"crm": "CRM",
|
||||||
|
"crm-leads": "Leads",
|
||||||
|
"crm-pipelines": "Pipelines",
|
||||||
|
"crm-contacts": "Contacts",
|
||||||
|
"sales": "Sales",
|
||||||
|
"sales-quotations": "Quotations",
|
||||||
|
"sales-orders": "Sales Orders",
|
||||||
|
"sales-invoices": "Invoices",
|
||||||
|
"supply-chain": "Supply Chain",
|
||||||
|
"sc-inventory": "Inventory Management",
|
||||||
|
"sc-inventory-products": "Products",
|
||||||
|
"sc-inventory-categories": "Categories",
|
||||||
|
"sc-inventory-adjustments": "Stock Adjustments",
|
||||||
|
"sc-warehouses": "Warehouses",
|
||||||
|
"sc-logistics": "Logistics",
|
||||||
|
"manufacturing": "Manufacturing",
|
||||||
|
"mfg-bom": "Bill of Materials",
|
||||||
|
"mfg-work-orders": "Work Orders",
|
||||||
|
"hris": "HRIS",
|
||||||
|
"hris-employees": "Employees",
|
||||||
|
"hris-attendance": "Attendance",
|
||||||
|
"hris-payroll": "Payroll",
|
||||||
|
"hris-calendar": "Company Calendar",
|
||||||
|
"accounting": "Accounting",
|
||||||
|
"acc-gl": "General Ledger",
|
||||||
|
"acc-taxes": "Taxes",
|
||||||
|
"settings": "Settings & Configuration",
|
||||||
|
"settings-general": "General Settings",
|
||||||
|
"settings-security": "Security",
|
||||||
|
"long-text-2": "Extremely Long Menu Name To Test Text Truncation Handling Properly",
|
||||||
|
"example-module": "Example Module",
|
||||||
|
"example-full-page": "Example Full Page",
|
||||||
|
"example-single-page": "Example Single Page",
|
||||||
|
"system": "System"
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"dashboard": "Dasbor",
|
||||||
|
"crm": "CRM",
|
||||||
|
"crm-leads": "Prospek",
|
||||||
|
"crm-pipelines": "Alur Penjualan",
|
||||||
|
"crm-contacts": "Kontak",
|
||||||
|
"sales": "Penjualan",
|
||||||
|
"sales-quotations": "Penawaran",
|
||||||
|
"sales-orders": "Pesanan Penjualan",
|
||||||
|
"sales-invoices": "Faktur",
|
||||||
|
"supply-chain": "Rantai Pasok",
|
||||||
|
"sc-inventory": "Manajemen Inventaris",
|
||||||
|
"sc-inventory-products": "Produk",
|
||||||
|
"sc-inventory-categories": "Kategori",
|
||||||
|
"sc-inventory-adjustments": "Penyesuaian Stok",
|
||||||
|
"sc-warehouses": "Gudang",
|
||||||
|
"sc-logistics": "Logistik",
|
||||||
|
"manufacturing": "Manufaktur",
|
||||||
|
"mfg-bom": "Daftar Material",
|
||||||
|
"mfg-work-orders": "Perintah Kerja",
|
||||||
|
"hris": "HRIS",
|
||||||
|
"hris-employees": "Karyawan",
|
||||||
|
"hris-attendance": "Kehadiran",
|
||||||
|
"hris-payroll": "Penggajian",
|
||||||
|
"hris-calendar": "Kalender Perusahaan",
|
||||||
|
"accounting": "Akuntansi",
|
||||||
|
"acc-gl": "Buku Besar",
|
||||||
|
"acc-taxes": "Pajak",
|
||||||
|
"settings": "Pengaturan & Konfigurasi",
|
||||||
|
"settings-general": "Pengaturan Umum",
|
||||||
|
"settings-security": "Keamanan",
|
||||||
|
"long-text-2": "Nama Menu Sangat Panjang Untuk Menguji Penanganan Pemotongan Teks Dengan Baik",
|
||||||
|
"example-module": "Modul Contoh",
|
||||||
|
"example-full-page": "Contoh Halaman Penuh",
|
||||||
|
"example-single-page": "Contoh Halaman Tunggal",
|
||||||
|
"system": "Sistem"
|
||||||
|
}
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
import { CoreAppShell, CoreAppShellConfig } from '@repo/ui/components';
|
import { CoreAppShell, CoreAppShellConfig } from '@repo/ui/components';
|
||||||
|
import { registerModuleNamespace } from '@repo/core-i18n';
|
||||||
import HeaderLayout from './components/header.layout';
|
import HeaderLayout from './components/header.layout';
|
||||||
import { SidebarMenu } from './components/sidebar';
|
import { SidebarMenu } from './components/sidebar';
|
||||||
import { MENU_ITEMS } from './data/menu.data';
|
import { MENU_ITEMS } from './data/menu.data';
|
||||||
|
|
||||||
|
import navEn from './locales/en/nav.json';
|
||||||
|
import navId from './locales/id/nav.json';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Namespace Registration (Module Scope)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Called once at import time — safe, idempotent, outside React render cycle.
|
||||||
|
registerModuleNamespace('nav', { en: navEn, id: navId });
|
||||||
|
|
||||||
export default function ModuleLayout({ children }: { children: React.ReactNode }) {
|
export default function ModuleLayout({ children }: { children: React.ReactNode }) {
|
||||||
const configAppShell: CoreAppShellConfig = {
|
const configAppShell: CoreAppShellConfig = {
|
||||||
variant: 'header-first',
|
variant: 'header-first',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { LucideIcon } from 'lucide-react';
|
|||||||
export interface MenuItemType {
|
export interface MenuItemType {
|
||||||
/** Unique identifier for the menu item */
|
/** Unique identifier for the menu item */
|
||||||
key: string;
|
key: string;
|
||||||
/** Display label */
|
/** i18n translation key (e.g. 'nav:dashboard'). Resolved via t() at render time. */
|
||||||
label: string;
|
label: string;
|
||||||
/** Lucide icon component */
|
/** Lucide icon component */
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export const shortcuts = [
|
||||||
|
{
|
||||||
|
label: 'information:shortcuts.create.label',
|
||||||
|
desc: 'information:shortcuts.create.desc',
|
||||||
|
macKeys: ['⌘', '⇧', 'N'],
|
||||||
|
winKeys: ['Ctrl', 'Shift', 'N'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'information:shortcuts.searchMenu.label',
|
||||||
|
desc: 'information:shortcuts.searchMenu.desc',
|
||||||
|
macKeys: ['⌘', '⇧', 'M'],
|
||||||
|
winKeys: ['Ctrl', 'Shift', 'M'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'information:shortcuts.toggleSidebar.label',
|
||||||
|
desc: 'information:shortcuts.toggleSidebar.desc',
|
||||||
|
macKeys: ['⌘', '⇧', 'B'],
|
||||||
|
winKeys: ['Ctrl', 'Shift', 'B'],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { useTranslation } from '@repo/core-i18n';
|
||||||
|
import { Group, Kbd, Table, Text, TextInput, Box, Center, ScrollArea } from '@repo/ui/components';
|
||||||
|
import { Search, Keyboard } from 'lucide-react';
|
||||||
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
|
import { shortcuts } from './data';
|
||||||
|
|
||||||
|
export function Shortcut() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isMac, setIsMac] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// OS Detection for rendering appropriate keyboard shortcuts
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMac(typeof window !== 'undefined' && navigator.userAgent.includes('Mac'));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Filter logika pencarian berdasarkan label ATAU deskripsi yang sudah diterjemahkan
|
||||||
|
const filteredShortcuts = useMemo(() => {
|
||||||
|
const query = searchQuery.toLowerCase().trim();
|
||||||
|
if (!query) return shortcuts;
|
||||||
|
|
||||||
|
return shortcuts.filter((item) => {
|
||||||
|
const labelMatch = t(item.label).toLowerCase().includes(query);
|
||||||
|
const descMatch = t(item.desc).toLowerCase().includes(query);
|
||||||
|
return labelMatch || descMatch;
|
||||||
|
});
|
||||||
|
}, [searchQuery, t]);
|
||||||
|
|
||||||
|
const rows = filteredShortcuts.map((element, index) => {
|
||||||
|
const keys = isMac ? element.macKeys : element.winKeys;
|
||||||
|
return (
|
||||||
|
<Table.Tr key={index}>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" fw={600} style={{ color: 'var(--mantine-color-text)' }}>
|
||||||
|
{t(element.label)}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed" mt={4} style={{ maxWidth: '600px', lineHeight: 1.4 }}>
|
||||||
|
{t(element.desc)}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td w={250}>
|
||||||
|
<Group gap={6} wrap="nowrap">
|
||||||
|
{keys.map((key, i) => (
|
||||||
|
<Kbd key={i} size="md" style={{ fontSize: '12px' }}>
|
||||||
|
{key}
|
||||||
|
</Kbd>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box p="md" style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}>
|
||||||
|
<TextInput
|
||||||
|
placeholder={t('information:info.searchPlaceholder')}
|
||||||
|
leftSection={<Search size={16} style={{ opacity: 0.5 }} />}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.currentTarget.value)}
|
||||||
|
w={{ base: '100%', sm: 350 }}
|
||||||
|
radius="md"
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ScrollArea.Autosize mah="calc(100vh - 350px)" offsetScrollbars>
|
||||||
|
<Table
|
||||||
|
stickyHeader
|
||||||
|
stickyHeaderOffset={-1}
|
||||||
|
striped
|
||||||
|
highlightOnHover
|
||||||
|
verticalSpacing="sm"
|
||||||
|
horizontalSpacing="md"
|
||||||
|
>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>{t('information:info.table.action')}</Table.Th>
|
||||||
|
<Table.Th>{t('information:info.table.shortcut')}</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{rows.length > 0 ? (
|
||||||
|
rows
|
||||||
|
) : (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={2}>
|
||||||
|
<Center py={40} style={{ flexDirection: 'column', gap: '8px' }}>
|
||||||
|
<Keyboard size={32} style={{ opacity: 0.2 }} />
|
||||||
|
<Text c="dimmed" size="sm" fw={500}>
|
||||||
|
{t('information:info.emptySearch')}
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</ScrollArea.Autosize>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useTranslation } from '@repo/core-i18n';
|
||||||
|
import { Group, Text } from '@repo/ui/components';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
export function System() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group justify="center" align="center" style={{ minHeight: 200, flexDirection: 'column' }}>
|
||||||
|
<Info size={48} color="var(--mantine-color-dimmed)" strokeWidth={1.5} />
|
||||||
|
<Text size="lg" fw={500} mt="md">
|
||||||
|
{t('information:info.placeholder.title')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" ta="center" maw={400}>
|
||||||
|
{t('information:info.placeholder.description')}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,17 +1,58 @@
|
|||||||
import { Container, Paper, Title, Text } from '@repo/ui/components';
|
import { Tabs, Card, CorePageContainer } from '@repo/ui/components';
|
||||||
import { useTranslation } from '@repo/core-i18n';
|
import { Keyboard, Info } from 'lucide-react';
|
||||||
|
import { ModulePageHeader } from '@repo/ui/foundations';
|
||||||
|
import { useTranslation, registerModuleNamespace } from '@repo/core-i18n';
|
||||||
|
|
||||||
|
import { Shortcut } from './components/shortcut';
|
||||||
|
import { System } from './components/system';
|
||||||
|
|
||||||
|
import informationId from './locales/id/information.json';
|
||||||
|
import informationEn from './locales/en/information.json';
|
||||||
|
|
||||||
|
registerModuleNamespace('information', {
|
||||||
|
id: informationId,
|
||||||
|
en: informationEn,
|
||||||
|
});
|
||||||
|
|
||||||
export default function InformationPage() {
|
export default function InformationPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container fluid p="md">
|
<CorePageContainer>
|
||||||
<Paper shadow="sm" p="md" radius="md">
|
<ModulePageHeader
|
||||||
<Title order={2} mb="xs">
|
icon={Info}
|
||||||
{t('common:systemInformation')}
|
title={t('information:info.pageTitle')}
|
||||||
</Title>
|
description={t('information:info.pageDescription')}
|
||||||
<Text c="dimmed">{t('common:systemInformationDesc')}</Text>
|
disableMinimize={true}
|
||||||
</Paper>
|
moduleKey="SYSTEM_INFORMATION"
|
||||||
</Container>
|
breadcrumbs={[
|
||||||
|
{ label: t('nav:system'), type: 'text' },
|
||||||
|
{ label: t('information:info.pageTitle'), type: 'link', href: '/app/system/information' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabs defaultValue="shortcuts" variant="outline" radius="md">
|
||||||
|
<Tabs.List mb="md">
|
||||||
|
<Tabs.Tab value="shortcuts" leftSection={<Keyboard size={16} />}>
|
||||||
|
{t('information:info.tabs.shortcuts')}
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="system" leftSection={<Info size={16} />}>
|
||||||
|
{t('information:info.tabs.information')}
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Panel value="shortcuts">
|
||||||
|
<Card shadow="sm" radius="md" withBorder padding={'xl'}>
|
||||||
|
<Shortcut />
|
||||||
|
</Card>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="system">
|
||||||
|
<Card shadow="sm" radius="md" withBorder padding="xl">
|
||||||
|
<System />
|
||||||
|
</Card>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
</CorePageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"pageTitle": "Information",
|
||||||
|
"pageDescription": "Central hub for system details, configurations, and user references.",
|
||||||
|
"tabs": {
|
||||||
|
"shortcuts": "Keyboard Shortcuts",
|
||||||
|
"information": "System Information"
|
||||||
|
},
|
||||||
|
"searchPlaceholder": "Search shortcuts...",
|
||||||
|
"emptySearch": "No shortcuts found",
|
||||||
|
"table": {
|
||||||
|
"action": "Action",
|
||||||
|
"shortcut": "Shortcut"
|
||||||
|
},
|
||||||
|
"placeholder": {
|
||||||
|
"title": "System Details Unavailable",
|
||||||
|
"description": "Detailed information regarding package versions, build environments, and core dependencies will be displayed here in a future update."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shortcuts": {
|
||||||
|
"create": {
|
||||||
|
"label": "Create / Add Data",
|
||||||
|
"desc": "Works on index or detail pages containing an add data action button, provided you have the required access permissions."
|
||||||
|
},
|
||||||
|
"searchMenu": {
|
||||||
|
"label": "Search Navigation Menu",
|
||||||
|
"desc": "Works globally across all pages to quickly search for menus."
|
||||||
|
},
|
||||||
|
"toggleSidebar": {
|
||||||
|
"label": "Toggle Sidebar",
|
||||||
|
"desc": "Works globally to collapse or expand the main navigation panel to maximize workspace area."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"pageTitle": "Informasi",
|
||||||
|
"pageDescription": "Pusat informasi untuk detail sistem, konfigurasi, dan referensi pengguna.",
|
||||||
|
"tabs": {
|
||||||
|
"shortcuts": "Pintasan Keyboard",
|
||||||
|
"information": "Informasi Sistem"
|
||||||
|
},
|
||||||
|
"searchPlaceholder": "Cari pintasan...",
|
||||||
|
"emptySearch": "Pintasan tidak ditemukan",
|
||||||
|
"table": {
|
||||||
|
"action": "Aksi",
|
||||||
|
"shortcut": "Pintasan"
|
||||||
|
},
|
||||||
|
"placeholder": {
|
||||||
|
"title": "Detail Sistem Belum Tersedia",
|
||||||
|
"description": "Informasi detail mengenai versi paket, environment build, dan dependensi inti akan ditampilkan di sini pada pembaruan mendatang."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shortcuts": {
|
||||||
|
"create": {
|
||||||
|
"label": "Buat / Tambah Data",
|
||||||
|
"desc": "Berfungsi di halaman indeks atau detail yang memiliki tombol aksi tambah data, serta memerlukan hak akses (permission) yang sah."
|
||||||
|
},
|
||||||
|
"searchMenu": {
|
||||||
|
"label": "Cari Menu Navigasi",
|
||||||
|
"desc": "Berfungsi secara global di seluruh halaman untuk mencari menu dengan cepat."
|
||||||
|
},
|
||||||
|
"toggleSidebar": {
|
||||||
|
"label": "Buka/Tutup Sidebar",
|
||||||
|
"desc": "Berfungsi secara global untuk melipat atau melebarkan panel navigasi utama agar ruang kerja menjadi lebih luas."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
import { Card, Title, Text, Table, Stack, Badge } from '@repo/ui/components';
|
import { Card, Title, Text, Table, Stack, Badge } from '@repo/ui/components';
|
||||||
import { PageActions, RowActions, PageAction, RowAction } from '@repo/ui/components';
|
import { PageActions, RowActions, PageActionProps, RowActionProps } from '@repo/ui/components';
|
||||||
import { Save, Printer, Trash, MoreVertical, Edit, FileText, CheckCircle, Check } from 'lucide-react';
|
import { Save, Printer, Trash, MoreVertical, Edit, FileText, CheckCircle, Check } from 'lucide-react';
|
||||||
|
|
||||||
export default function ActionToolsShowcase() {
|
export default function ActionToolsShowcase() {
|
||||||
const pageActions: PageAction[] = [
|
const pageActions: PageActionProps[] = [
|
||||||
|
{
|
||||||
|
key: 'save',
|
||||||
|
label: 'Save Changes',
|
||||||
|
icon: <Save size={16} />,
|
||||||
|
variant: 'filled',
|
||||||
|
onClick: (key) => console.log('Clicked', key),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'save',
|
key: 'save',
|
||||||
label: 'Save Changes',
|
label: 'Save Changes',
|
||||||
@@ -65,7 +72,7 @@ export default function ActionToolsShowcase() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rowActions: RowAction[] = [
|
const rowActions: RowActionProps[] = [
|
||||||
{
|
{
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
tooltip: 'Edit Record',
|
tooltip: 'Edit Record',
|
||||||
@@ -111,7 +118,7 @@ export default function ActionToolsShowcase() {
|
|||||||
<Text c="dimmed" mb="lg">
|
<Text c="dimmed" mb="lg">
|
||||||
Used in toolbars and page headers.
|
Used in toolbars and page headers.
|
||||||
</Text>
|
</Text>
|
||||||
<PageActions actions={pageActions} onClose={() => {}} />
|
<PageActions actions={pageActions} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card withBorder shadow="sm" radius="md" p="md">
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
|
|||||||
@@ -87,12 +87,7 @@ export function LiveStockGrid() {
|
|||||||
■ Stop Feed
|
■ Stop Feed
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button variant="light" color="info" onClick={() => setShowAll((s) => !s)} size="sm">
|
||||||
variant="light"
|
|
||||||
color="info"
|
|
||||||
onClick={() => setShowAll((s) => !s)}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{showAll ? `Show ${VISIBLE_ROWS} rows` : `Show all ${STOCK_COUNT} rows`}
|
{showAll ? `Show ${VISIBLE_ROWS} rows` : `Show all ${STOCK_COUNT} rows`}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -118,7 +113,7 @@ export function LiveStockGrid() {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* ── Data Grid ─────────────────────────────────────────── */}
|
{/* ── Data Grid ─────────────────────────────────────────── */}
|
||||||
<div style={{ maxHeight: 500, overflow: 'auto', border: '1px solid var(--mantine-color-default-border)' }}>
|
<div style={{ maxHeight: 500, overflow: 'auto', border: '1px solid var(--app-shell-border-color)' }}>
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr
|
<tr
|
||||||
@@ -133,11 +128,51 @@ export function LiveStockGrid() {
|
|||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<th style={{ padding: '6px 8px', textAlign: 'left', borderBottom: '2px solid var(--mantine-color-default-border)' }}>Ticker</th>
|
<th
|
||||||
<th style={{ padding: '6px 8px', textAlign: 'right', borderBottom: '2px solid var(--mantine-color-default-border)' }}>Price</th>
|
style={{
|
||||||
<th style={{ padding: '6px 8px', textAlign: 'right', borderBottom: '2px solid var(--mantine-color-default-border)' }}>Change</th>
|
padding: '6px 8px',
|
||||||
<th style={{ padding: '6px 8px', textAlign: 'right', borderBottom: '2px solid var(--mantine-color-default-border)' }}>Volume</th>
|
textAlign: 'left',
|
||||||
<th style={{ padding: '6px 8px', textAlign: 'right', borderBottom: '2px solid var(--mantine-color-default-border)' }}>Renders</th>
|
borderBottom: '2px solid var(--app-shell-border-color)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ticker
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
padding: '6px 8px',
|
||||||
|
textAlign: 'right',
|
||||||
|
borderBottom: '2px solid var(--app-shell-border-color)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Price
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
padding: '6px 8px',
|
||||||
|
textAlign: 'right',
|
||||||
|
borderBottom: '2px solid var(--app-shell-border-color)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Change
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
padding: '6px 8px',
|
||||||
|
textAlign: 'right',
|
||||||
|
borderBottom: '2px solid var(--app-shell-border-color)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Volume
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
padding: '6px 8px',
|
||||||
|
textAlign: 'right',
|
||||||
|
borderBottom: '2px solid var(--app-shell-border-color)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Renders
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export default function ShellDemo() {
|
|||||||
px="md"
|
px="md"
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
bg="gray.1"
|
bg="gray.1"
|
||||||
style={{ borderTop: '1px solid var(--mantine-color-default-border)' }}
|
style={{ borderTop: '1px solid var(--app-shell-border-color)' }}
|
||||||
>
|
>
|
||||||
<Text size="sm" fw={600}>
|
<Text size="sm" fw={600}>
|
||||||
Mock Footer (bg="gray.1")
|
Mock Footer (bg="gray.1")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, Suspense } from 'react';
|
import { useState, Suspense } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ColorSchemeType, DensityType } from '@repo/ui/provider';
|
import { ColorSchemeType, DensityType } from '@repo/ui/provider';
|
||||||
|
import { useThemeStore } from '../../core/store/theme.store';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -34,13 +35,12 @@ import FormDemoView from './example/features/form-demo';
|
|||||||
import { COMPONENTS_REGISTRY } from './registry';
|
import { COMPONENTS_REGISTRY } from './registry';
|
||||||
|
|
||||||
interface ShowcaseViewProps {
|
interface ShowcaseViewProps {
|
||||||
colorScheme: ColorSchemeType;
|
|
||||||
setColorScheme: (val: ColorSchemeType) => void;
|
|
||||||
density: DensityType;
|
density: DensityType;
|
||||||
setDensity: (val: DensityType) => void;
|
setDensity: (val: DensityType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ShowcaseView({ colorScheme, setColorScheme, density, setDensity }: ShowcaseViewProps) {
|
export default function ShowcaseView({ density, setDensity }: ShowcaseViewProps) {
|
||||||
|
const { colorScheme, setColorScheme } = useThemeStore();
|
||||||
const [activeTab, setActiveTab] = useState<string | null>('ui-components');
|
const [activeTab, setActiveTab] = useState<string | null>('ui-components');
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -103,7 +103,7 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
|
|||||||
list: {
|
list: {
|
||||||
minWidth: 260,
|
minWidth: 260,
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
borderRight: '1px solid var(--mantine-color-default-border)',
|
borderRight: '1px solid var(--app-shell-border-color)',
|
||||||
backgroundColor: 'var(--mantine-color-default-element-bg)',
|
backgroundColor: 'var(--mantine-color-default-element-bg)',
|
||||||
},
|
},
|
||||||
panel: { flex: 1, display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' },
|
panel: { flex: 1, display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' },
|
||||||
@@ -407,8 +407,6 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createLocalStorage, createIndexedDB } from '@repo/core-storage';
|
|||||||
export const AppStorageKey = {
|
export const AppStorageKey = {
|
||||||
USER_PROFILE: 'user_profile',
|
USER_PROFILE: 'user_profile',
|
||||||
LOCALE: 'app_locale',
|
LOCALE: 'app_locale',
|
||||||
|
THEME: 'app_theme',
|
||||||
ACCESS_TOKEN: 'access_token',
|
ACCESS_TOKEN: 'access_token',
|
||||||
REFRESH_TOKEN: 'refresh_token',
|
REFRESH_TOKEN: 'refresh_token',
|
||||||
MOCK_DB_COMPANY_A: 'mock_db_company_a',
|
MOCK_DB_COMPANY_A: 'mock_db_company_a',
|
||||||
@@ -19,6 +20,7 @@ export const ENCRYPTED_KEYS = new Set<AppStorageKeyValue>([
|
|||||||
|
|
||||||
export const PLAIN_KEYS = new Set<AppStorageKeyValue>([
|
export const PLAIN_KEYS = new Set<AppStorageKeyValue>([
|
||||||
AppStorageKey.LOCALE,
|
AppStorageKey.LOCALE,
|
||||||
|
AppStorageKey.THEME,
|
||||||
AppStorageKey.MOCK_DB_COMPANY_A,
|
AppStorageKey.MOCK_DB_COMPANY_A,
|
||||||
AppStorageKey.OFFLINE_DRAFT,
|
AppStorageKey.OFFLINE_DRAFT,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
|
import type { ColorSchemeType } from '@repo/ui/provider';
|
||||||
|
|
||||||
|
interface ThemeState {
|
||||||
|
colorScheme: ColorSchemeType;
|
||||||
|
setColorScheme: (scheme: ColorSchemeType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global theme store with localStorage persistence.
|
||||||
|
*
|
||||||
|
* Uses the Zustand `persist` middleware so the chosen color scheme
|
||||||
|
* survives page refreshes. The storage key (`app_theme`) is
|
||||||
|
* intentionally kept in sync with `AppStorageKey.THEME` — both
|
||||||
|
* write to the same localStorage entry.
|
||||||
|
*/
|
||||||
|
export const useThemeStore = create<ThemeState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
colorScheme: 'light',
|
||||||
|
setColorScheme: (scheme) => set({ colorScheme: scheme }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'app_theme', // matches AppStorageKey.THEME
|
||||||
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -100,12 +100,12 @@ describe('BaseRemoteDataServices (via CommonRemoteDataServices)', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('confirmProcessTransaction() resolves to /:id/confirm-data', async () => {
|
it('confirmData() resolves to /:id/confirm', async () => {
|
||||||
await services.confirmProcessTransaction('99');
|
await services.confirmData('99');
|
||||||
|
|
||||||
expect(mockClient.request).toHaveBeenCalledWith(
|
expect(mockClient.request).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
url: '/bookings/99/confirm-data',
|
url: '/bookings/99/confirm',
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -525,17 +525,13 @@ describe('BaseRemoteDataServices — Data Transformer Integration', () => {
|
|||||||
|
|
||||||
it('getMany() uses transformGetManyResponse hook (prefixes booking code)', async () => {
|
it('getMany() uses transformGetManyResponse hook (prefixes booking code)', async () => {
|
||||||
(mockClient.request as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
(mockClient.request as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||||
data: [
|
data: [{ id: '1', booking_code: 'BK001', customer_name: 'Alice' }],
|
||||||
{ id: '1', booking_code: 'BK001', customer_name: 'Alice' },
|
|
||||||
],
|
|
||||||
status: 200,
|
status: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await services.getMany();
|
const result = await services.getMany();
|
||||||
|
|
||||||
expect(result.data).toEqual([
|
expect(result.data).toEqual([{ id: '1', bookingCode: 'LIST-BK001', customerName: 'Alice' }]);
|
||||||
{ id: '1', bookingCode: 'LIST-BK001', customerName: 'Alice' },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('create() uses transformCreatePayload hook (strips id)', async () => {
|
it('create() uses transformCreatePayload hook (strips id)', async () => {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
"view": "View",
|
"view": "View",
|
||||||
"preferences": "Preferences",
|
"preferences": "Preferences",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
"theme": "Theme",
|
||||||
|
"lightMode": "Light",
|
||||||
|
"darkMode": "Dark",
|
||||||
"application": "Application",
|
"application": "Application",
|
||||||
"myProfile": "My Profile",
|
"myProfile": "My Profile",
|
||||||
"workspace": "Workspace",
|
"workspace": "Workspace",
|
||||||
@@ -37,6 +40,44 @@
|
|||||||
"collapseAll": "Collapse all menu",
|
"collapseAll": "Collapse all menu",
|
||||||
"searchMenu": "Search menu",
|
"searchMenu": "Search menu",
|
||||||
"collapse": "Collapse",
|
"collapse": "Collapse",
|
||||||
"expandSidebar": "Expand Sidebar"
|
"expandSidebar": "Expand Sidebar",
|
||||||
|
"actions": {
|
||||||
|
"create": "Create New",
|
||||||
|
"edit": "Edit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"save": "Save",
|
||||||
|
"print": "Print",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"activate": "Activate",
|
||||||
|
"deactivate": "Deactivate",
|
||||||
|
"duplicate": "Duplicate",
|
||||||
|
"back": "Back",
|
||||||
|
"reload": "Reload"
|
||||||
|
},
|
||||||
|
"confirmDialog": {
|
||||||
|
"title": "Are you sure?",
|
||||||
|
"deleteMessage": "This action cannot be undone.",
|
||||||
|
"confirmButton": "Confirm",
|
||||||
|
"cancelButton": "Cancel"
|
||||||
|
},
|
||||||
|
"draft": {
|
||||||
|
"recoveryTitle": "Draft Found",
|
||||||
|
"recoveryMessage": "You have an unsaved draft from {{date}}. Would you like to continue editing?",
|
||||||
|
"continueEditing": "Continue Editing",
|
||||||
|
"discardDraft": "Start Fresh"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"saveSuccess": "Data saved successfully",
|
||||||
|
"deleteSuccess": "Data deleted successfully",
|
||||||
|
"actionSuccess": "{{action}} completed successfully",
|
||||||
|
"actionFailed": "Failed to {{action}}: {{message}}"
|
||||||
|
},
|
||||||
|
"privilege": {
|
||||||
|
"error": {
|
||||||
|
"accessDeniedTitle": "Access Denied",
|
||||||
|
"noCreateAccess": "You do not have the required permission to create data in this module."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,9 @@
|
|||||||
"view": "Lihat",
|
"view": "Lihat",
|
||||||
"preferences": "Preferensi",
|
"preferences": "Preferensi",
|
||||||
"language": "Bahasa",
|
"language": "Bahasa",
|
||||||
|
"theme": "Tema",
|
||||||
|
"lightMode": "Terang",
|
||||||
|
"darkMode": "Gelap",
|
||||||
"application": "Aplikasi",
|
"application": "Aplikasi",
|
||||||
"myProfile": "Profil Saya",
|
"myProfile": "Profil Saya",
|
||||||
"workspace": "Ruang Kerja",
|
"workspace": "Ruang Kerja",
|
||||||
@@ -37,6 +40,44 @@
|
|||||||
"collapseAll": "Tutup semua menu",
|
"collapseAll": "Tutup semua menu",
|
||||||
"searchMenu": "Cari menu",
|
"searchMenu": "Cari menu",
|
||||||
"collapse": "Tutup",
|
"collapse": "Tutup",
|
||||||
"expandSidebar": "Perluas Sidebar"
|
"expandSidebar": "Perluas Sidebar",
|
||||||
|
"actions": {
|
||||||
|
"create": "Buat Baru",
|
||||||
|
"edit": "Ubah",
|
||||||
|
"delete": "Hapus",
|
||||||
|
"save": "Simpan",
|
||||||
|
"print": "Cetak",
|
||||||
|
"confirm": "Konfirmasi",
|
||||||
|
"cancel": "Batal",
|
||||||
|
"activate": "Aktifkan",
|
||||||
|
"deactivate": "Nonaktifkan",
|
||||||
|
"duplicate": "Duplikat",
|
||||||
|
"back": "Kembali",
|
||||||
|
"reload": "Muat Ulang"
|
||||||
|
},
|
||||||
|
"confirmDialog": {
|
||||||
|
"title": "Apakah Anda yakin?",
|
||||||
|
"deleteMessage": "Tindakan ini tidak dapat dibatalkan.",
|
||||||
|
"confirmButton": "Konfirmasi",
|
||||||
|
"cancelButton": "Batal"
|
||||||
|
},
|
||||||
|
"draft": {
|
||||||
|
"recoveryTitle": "Draf Ditemukan",
|
||||||
|
"recoveryMessage": "Anda memiliki draf yang belum disimpan dari {{date}}. Apakah Anda ingin melanjutkan?",
|
||||||
|
"continueEditing": "Lanjutkan",
|
||||||
|
"discardDraft": "Mulai Baru"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"saveSuccess": "Data berhasil disimpan",
|
||||||
|
"deleteSuccess": "Data berhasil dihapus",
|
||||||
|
"actionSuccess": "{{action}} berhasil diselesaikan",
|
||||||
|
"actionFailed": "Gagal {{action}}: {{message}}"
|
||||||
|
},
|
||||||
|
"privilege": {
|
||||||
|
"error": {
|
||||||
|
"accessDeniedTitle": "Akses Ditolak",
|
||||||
|
"noCreateAccess": "Anda tidak memiliki hak akses untuk menambah data di modul ini."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@mantine/core": "^8.3.15",
|
"@mantine/core": "^8.3.15",
|
||||||
"@mantine/hooks": "^8.3.15",
|
"@mantine/hooks": "^8.3.15",
|
||||||
|
"@mantine/notifications": "^8.3.15",
|
||||||
"@mantine/tiptap": "^9.3.2",
|
"@mantine/tiptap": "^9.3.2",
|
||||||
"@repo/core-api": "workspace:^",
|
"@repo/core-api": "workspace:^",
|
||||||
"@repo/core-i18n": "workspace:*",
|
"@repo/core-i18n": "workspace:*",
|
||||||
|
|||||||
@@ -1,51 +1,77 @@
|
|||||||
import { memo, Fragment } from 'react';
|
import { memo, Fragment } from 'react';
|
||||||
import { Group, Button, Menu, Divider, ActionIcon, Box } from '@mantine/core';
|
import { Group, Button, Menu, Divider, ActionIcon, Box, ButtonProps, Tooltip } from '@mantine/core';
|
||||||
import { ChevronDown, MoreVertical, X } from 'lucide-react';
|
import { ChevronDown, MoreVertical } from 'lucide-react';
|
||||||
import { PageAction } from './types';
|
import { PageActionProps } from './types';
|
||||||
import { getIntentColor } from './utils';
|
import { getIntentColor } from './utils';
|
||||||
|
|
||||||
export interface PageActionsProps {
|
export interface PageActionsProps {
|
||||||
/** Array of configured page-level actions. */
|
/** Array of configured page-level actions. */
|
||||||
actions: PageAction[];
|
actions?: PageActionProps[];
|
||||||
/** Optional callback triggered when the close (X) button is clicked. */
|
customButtonProps?: (action: PageActionProps) => ButtonProps;
|
||||||
onClose?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A responsive and flexible presentational component for page-level actions.
|
* A responsive and flexible presentational component for page-level actions.
|
||||||
* Automatically adapts layout based on screen size:
|
* Automatically adapts layout based on screen size.
|
||||||
* - Desktop: Renders a horizontal toolbar with buttons and dividers.
|
|
||||||
* - Mobile: Renders a single Menu dropdown containing all actions.
|
|
||||||
*
|
|
||||||
* @performance Wrapped in React.memo to prevent unnecessary re-renders.
|
|
||||||
*/
|
*/
|
||||||
export const PageActions = memo(function PageActions({ actions }: PageActionsProps) {
|
export const PageActions = memo(function PageActions({ actions = [], customButtonProps }: PageActionsProps) {
|
||||||
|
if (!actions || actions?.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultButtonStyle(isPremiumGlow: boolean) {
|
||||||
|
return {
|
||||||
|
size: 'sm',
|
||||||
|
radius: 'md',
|
||||||
|
style: isPremiumGlow
|
||||||
|
? { boxShadow: '0 4px 14px 0 color-mix(in srgb, var(--mantine-primary-color-filled) 40%, transparent)' }
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={{ display: 'inline-flex' }}>
|
<Box style={{ display: 'inline-flex' }}>
|
||||||
{/* --- DESKTOP VIEW (hidden on mobile devices) --- */}
|
{/* --- DESKTOP VIEW --- */}
|
||||||
<Group gap="xs" wrap="nowrap" visibleFrom="sm">
|
<Group gap="xs" wrap="nowrap" visibleFrom="sm">
|
||||||
{actions.map((action, index) => {
|
{actions.map((action, index) => {
|
||||||
if (action.type === 'divider') {
|
if (action.type === 'divider') {
|
||||||
return <Divider key={`divider-${index}`} orientation="vertical" mr="sm" ml="sm" />;
|
return <Divider key={`divider-${index}`} orientation="vertical" mr="sm" ml="sm" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render Dropdown Menu for actions with children
|
const isPremiumGlow = action.intent === 'primary' && action.variant === 'filled';
|
||||||
|
|
||||||
|
// 1. Button with Dropdown (Menu.Target)
|
||||||
if (action.children && action.children.length > 0) {
|
if (action.children && action.children.length > 0) {
|
||||||
return (
|
const ButtonWithDropdown = (
|
||||||
<Menu key={action.key} position="bottom-start" withArrow withinPortal trigger="hover">
|
|
||||||
<Menu.Target>
|
|
||||||
<Button
|
<Button
|
||||||
variant={action.variant || 'transparent'}
|
variant={action.variant || 'transparent'}
|
||||||
color={getIntentColor(action.intent)}
|
color={getIntentColor(action.intent)}
|
||||||
leftSection={action.icon}
|
leftSection={action.icon}
|
||||||
rightSection={<ChevronDown size={14} />}
|
rightSection={<ChevronDown size={14} />}
|
||||||
disabled={action.disabled}
|
disabled={action.disabled}
|
||||||
size="xs"
|
{...defaultButtonStyle(isPremiumGlow)}
|
||||||
pr="sm"
|
{...(customButtonProps ? customButtonProps(action) : {})}
|
||||||
pl="sm"
|
|
||||||
>
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
</Button>
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu key={action.key} position="bottom-start" withArrow withinPortal trigger="hover">
|
||||||
|
<Menu.Target>
|
||||||
|
{/* Shortcuts on the main button remain hidden in the Tooltip */}
|
||||||
|
{action.shortcutLabel ? (
|
||||||
|
<Tooltip
|
||||||
|
position="bottom"
|
||||||
|
label={`${action.label} (${action.shortcutLabel})`}
|
||||||
|
withArrow
|
||||||
|
openDelay={500}
|
||||||
|
>
|
||||||
|
{ButtonWithDropdown}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
ButtonWithDropdown
|
||||||
|
)}
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{action.children.map((child, childIndex) => {
|
{action.children.map((child, childIndex) => {
|
||||||
@@ -69,33 +95,46 @@ export const PageActions = memo(function PageActions({ actions }: PageActionsPro
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// 2. Regular Button (Standalone)
|
||||||
|
const StandaloneButton = (
|
||||||
<Button
|
<Button
|
||||||
key={action.key}
|
|
||||||
variant={action.variant || 'transparent'}
|
variant={action.variant || 'transparent'}
|
||||||
color={getIntentColor(action.intent)}
|
color={getIntentColor(action.intent)}
|
||||||
leftSection={action.icon}
|
leftSection={action.icon}
|
||||||
disabled={action.disabled}
|
disabled={action.disabled}
|
||||||
onClick={() => action.onClick?.(action.key || '')}
|
onClick={() => action.onClick?.(action.key || '')}
|
||||||
size="xs"
|
{...defaultButtonStyle(isPremiumGlow)}
|
||||||
pr="sm"
|
{...(customButtonProps ? customButtonProps(action) : {})}
|
||||||
pl="sm"
|
|
||||||
>
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return action.shortcutLabel ? (
|
||||||
|
<Tooltip
|
||||||
|
position="bottom"
|
||||||
|
key={action.key}
|
||||||
|
label={`${action.label} (${action.shortcutLabel})`}
|
||||||
|
withArrow
|
||||||
|
openDelay={500}
|
||||||
|
>
|
||||||
|
{StandaloneButton}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Fragment key={action.key}>{StandaloneButton}</Fragment>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* --- MOBILE VIEW (hidden on desktop devices) --- */}
|
{/* --- MOBILE VIEW --- */}
|
||||||
<Group gap="xs" wrap="nowrap" hiddenFrom="sm">
|
<Group gap="xs" wrap="nowrap" hiddenFrom="sm">
|
||||||
<Menu position="bottom-end" withArrow withinPortal>
|
<Menu position="bottom-end" withArrow withinPortal>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<ActionIcon variant="transparent" size="md">
|
<ActionIcon variant="outline" size="md">
|
||||||
<MoreVertical size={16} />
|
<MoreVertical size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown px={'xl'}>
|
||||||
{actions.map((action, index) => {
|
{actions.map((action, index) => {
|
||||||
if (action.type === 'divider') {
|
if (action.type === 'divider') {
|
||||||
return <Menu.Divider key={`mobile-divider-${index}`} mt="sm" mb="sm" />;
|
return <Menu.Divider key={`mobile-divider-${index}`} mt="sm" mb="sm" />;
|
||||||
@@ -116,7 +155,7 @@ export const PageActions = memo(function PageActions({ actions }: PageActionsPro
|
|||||||
color={getIntentColor(child.intent)}
|
color={getIntentColor(child.intent)}
|
||||||
disabled={child.disabled}
|
disabled={child.disabled}
|
||||||
onClick={() => child.onClick?.(child.key || '')}
|
onClick={() => child.onClick?.(child.key || '')}
|
||||||
style={{ paddingLeft: '1.5rem' }} // Indent nested items
|
style={{ paddingLeft: '1.5rem' }}
|
||||||
mt="sm"
|
mt="sm"
|
||||||
mb="sm"
|
mb="sm"
|
||||||
>
|
>
|
||||||
@@ -137,6 +176,7 @@ export const PageActions = memo(function PageActions({ actions }: PageActionsPro
|
|||||||
onClick={() => action.onClick?.(action.key || '')}
|
onClick={() => action.onClick?.(action.key || '')}
|
||||||
mt="sm"
|
mt="sm"
|
||||||
mb="sm"
|
mb="sm"
|
||||||
|
fw={600}
|
||||||
>
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Fragment, memo } from 'react';
|
import { Fragment, memo } from 'react';
|
||||||
import { Group, ActionIcon, Tooltip, Menu, Divider, Box, Button } from '@mantine/core';
|
import { Group, ActionIcon, Tooltip, Menu, Divider, Box, Button } from '@mantine/core';
|
||||||
import { RowAction } from './types';
|
import { RowActionProps } from './types';
|
||||||
import { getIntentColor } from './utils';
|
import { getIntentColor } from './utils';
|
||||||
import { MoreVertical } from 'lucide-react';
|
import { MoreVertical } from 'lucide-react';
|
||||||
|
|
||||||
export interface RowActionsProps {
|
export interface RowActionsProps {
|
||||||
/** Array of configured row-level actions. */
|
/** Array of configured row-level actions. */
|
||||||
actions: RowAction[];
|
actions: RowActionProps[];
|
||||||
showLabels?: boolean;
|
showLabels?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export const RowActions = memo(function RowActions({ actions = [], showLabels =
|
|||||||
* Helper function to render a standalone icon button.
|
* Helper function to render a standalone icon button.
|
||||||
* Wraps the icon in a Tooltip if the configuration provides one.
|
* Wraps the icon in a Tooltip if the configuration provides one.
|
||||||
*/
|
*/
|
||||||
const renderIcon = (action: RowAction, fallbackKey: string) => {
|
const renderIcon = (action: RowActionProps, fallbackKey: string) => {
|
||||||
const actionKey = action.key || fallbackKey;
|
const actionKey = action.key || fallbackKey;
|
||||||
|
|
||||||
const iconBtn = (
|
const iconBtn = (
|
||||||
|
|||||||
@@ -32,13 +32,15 @@ export interface BaseAction {
|
|||||||
* Specifically designed for toolbars, page headers, or detailed forms.
|
* Specifically designed for toolbars, page headers, or detailed forms.
|
||||||
* Enforces the presence of a text label (unless type is divider) and supports button-specific visual variants.
|
* Enforces the presence of a text label (unless type is divider) and supports button-specific visual variants.
|
||||||
*/
|
*/
|
||||||
export interface PageAction extends BaseAction {
|
export interface PageActionProps extends BaseAction {
|
||||||
/** Text label displayed on the button. Required for 'action' type. */
|
/** Text label displayed on the button. Required for 'action' type. */
|
||||||
label?: string;
|
label?: string;
|
||||||
/** Specifies the Mantine button variant. Defaults to 'subtle'. */
|
/** Specifies the Mantine button variant. Defaults to 'subtle'. */
|
||||||
variant?: 'filled' | 'light' | 'outline' | 'default' | 'subtle' | 'transparent';
|
variant?: 'filled' | 'light' | 'outline' | 'default' | 'subtle' | 'transparent';
|
||||||
/** Nested actions rendered as a Dropdown Menu below the main button. */
|
/** Nested actions rendered as a Dropdown Menu below the main button. */
|
||||||
children?: PageAction[];
|
children?: PageActionProps[];
|
||||||
|
/** Human-readable keyboard shortcut label (e.g., '⇧⌘N'). Shown in tooltip. */
|
||||||
|
shortcutLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,11 +49,11 @@ export interface PageAction extends BaseAction {
|
|||||||
* Labels are optional (utilized inside dropdowns), supports hover tooltips,
|
* Labels are optional (utilized inside dropdowns), supports hover tooltips,
|
||||||
* and allows nested action hierarchies (e.g., Kebab menus).
|
* and allows nested action hierarchies (e.g., Kebab menus).
|
||||||
*/
|
*/
|
||||||
export interface RowAction extends BaseAction {
|
export interface RowActionProps extends BaseAction {
|
||||||
/** Optional text, primarily used when rendered inside a nested menu item. */
|
/** Optional text, primarily used when rendered inside a nested menu item. */
|
||||||
label?: string;
|
label?: string;
|
||||||
/** Optional text displayed on hover. */
|
/** Optional text displayed on hover. */
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
/** Nested actions that will be rendered inside a dropdown menu. */
|
/** Nested actions that will be rendered inside a dropdown menu. */
|
||||||
children?: RowAction[];
|
children?: RowActionProps[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,24 @@ import { AppShell, Flex, Box, Text, CloseButton } from '@mantine/core';
|
|||||||
import { CoreAppShellProvider, useCoreAppShell } from './core-app-shell-context';
|
import { CoreAppShellProvider, useCoreAppShell } from './core-app-shell-context';
|
||||||
import { CoreAppShellConfig, CoreAppShellSlots, CoreAppShellDimensions } from './types';
|
import { CoreAppShellConfig, CoreAppShellSlots, CoreAppShellDimensions } from './types';
|
||||||
|
|
||||||
|
// const DEFAULT_DIMENSIONS: Required<CoreAppShellDimensions> = {
|
||||||
|
// utilityBarHeight: 32,
|
||||||
|
// headerHeight: 60,
|
||||||
|
// // sidebarWidth: 260,
|
||||||
|
// sidebarWidth: 240,
|
||||||
|
|
||||||
|
// sidebarMiniWidth: 70,
|
||||||
|
// sidebarRailWidth: 54,
|
||||||
|
// // asideWidth: 260,
|
||||||
|
// asideWidth: 240,
|
||||||
|
// };
|
||||||
const DEFAULT_DIMENSIONS: Required<CoreAppShellDimensions> = {
|
const DEFAULT_DIMENSIONS: Required<CoreAppShellDimensions> = {
|
||||||
utilityBarHeight: 32,
|
utilityBarHeight: 32,
|
||||||
headerHeight: 60,
|
headerHeight: 56,
|
||||||
sidebarWidth: 260,
|
sidebarWidth: 256,
|
||||||
sidebarMiniWidth: 80,
|
sidebarMiniWidth: 64,
|
||||||
sidebarRailWidth: 54,
|
sidebarRailWidth: 56,
|
||||||
asideWidth: 260,
|
asideWidth: 280,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CoreAppShellInnerProps {
|
interface CoreAppShellInnerProps {
|
||||||
@@ -62,7 +73,6 @@ function CoreAppShellInner({ slots = {}, children }: CoreAppShellInnerProps) {
|
|||||||
return `calc(${dims.headerHeight}${typeof dims.headerHeight === 'number' ? 'px' : ''} + ${dims.utilityBarHeight}${typeof dims.utilityBarHeight === 'number' ? 'px' : ''})`;
|
return `calc(${dims.headerHeight}${typeof dims.headerHeight === 'number' ? 'px' : ''} + ${dims.utilityBarHeight}${typeof dims.utilityBarHeight === 'number' ? 'px' : ''})`;
|
||||||
}, [dims.headerHeight, dims.utilityBarHeight, showUtilityBar]);
|
}, [dims.headerHeight, dims.utilityBarHeight, showUtilityBar]);
|
||||||
|
|
||||||
console.log({ totalHeaderHeight, dimensions });
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
layout={appShellLayout}
|
layout={appShellLayout}
|
||||||
@@ -124,7 +134,7 @@ function CoreAppShellInner({ slots = {}, children }: CoreAppShellInnerProps) {
|
|||||||
h="100%"
|
h="100%"
|
||||||
style={{
|
style={{
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
borderRight: '1px solid var(--mantine-color-default-border)',
|
borderRight: '1px solid var(--app-shell-border-color)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{slots.sidebarRail}
|
{slots.sidebarRail}
|
||||||
@@ -147,7 +157,7 @@ function CoreAppShellInner({ slots = {}, children }: CoreAppShellInnerProps) {
|
|||||||
justify="space-between"
|
justify="space-between"
|
||||||
p="md"
|
p="md"
|
||||||
pb="sm"
|
pb="sm"
|
||||||
style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}
|
style={{ borderBottom: '1px solid var(--app-shell-border-color)' }}
|
||||||
>
|
>
|
||||||
<Text fw={700}>Menu</Text>
|
<Text fw={700}>Menu</Text>
|
||||||
<CloseButton onClick={toggleMobile} size="md" aria-label="Close menu" />
|
<CloseButton onClick={toggleMobile} size="md" aria-label="Close menu" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode, useEffect, useState } from 'react';
|
||||||
import { Box, Container, Stack, ContainerProps } from '@mantine/core';
|
import { Box, Container, Stack, ContainerProps, Divider } from '@mantine/core';
|
||||||
|
|
||||||
export interface CorePageContainerProps extends ContainerProps {
|
export interface CorePageContainerProps extends ContainerProps {
|
||||||
headerSlot?: ReactNode;
|
headerSlot?: ReactNode;
|
||||||
@@ -9,34 +9,63 @@ export interface CorePageContainerProps extends ContainerProps {
|
|||||||
|
|
||||||
export function CorePageContainer({
|
export function CorePageContainer({
|
||||||
headerSlot,
|
headerSlot,
|
||||||
stickyHeader = false,
|
stickyHeader = true,
|
||||||
children,
|
children,
|
||||||
px = "md",
|
px = { base: 'md', sm: 'xl' },
|
||||||
py = "md",
|
py = { base: 'md', sm: 'lg' },
|
||||||
...others
|
...others
|
||||||
}: CorePageContainerProps) {
|
}: CorePageContainerProps) {
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stickyHeader) return;
|
||||||
|
const handleScroll = () => setIsScrolled(window.scrollY > 10);
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
handleScroll();
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, [stickyHeader]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box m="calc(var(--mantine-spacing-md) * -1)">
|
|
||||||
<Stack gap={0}>
|
|
||||||
{headerSlot && (
|
|
||||||
<Box
|
<Box
|
||||||
|
m="calc(var(--mantine-spacing-md) * -1)"
|
||||||
style={{
|
style={{
|
||||||
position: stickyHeader ? 'sticky' : 'static',
|
display: 'flex',
|
||||||
top: stickyHeader ? 'var(--app-shell-header-offset, 0px)' : undefined,
|
flexDirection: 'column',
|
||||||
zIndex: stickyHeader ? 10 : undefined,
|
minHeight: 'calc(100vh - var(--app-shell-header-offset, 0px) - var(--app-shell-footer-offset, 0px))',
|
||||||
backgroundColor: 'var(--mantine-color-body)',
|
|
||||||
borderBottom: '1px solid var(--mantine-color-default-border)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container fluid px={px} py={py}>
|
<Stack gap={0} flex={1}>
|
||||||
|
{headerSlot && (
|
||||||
|
<Box
|
||||||
|
pos={stickyHeader ? 'sticky' : 'relative'}
|
||||||
|
top={stickyHeader ? 'var(--app-shell-header-offset, 0px)' : undefined}
|
||||||
|
bg="var(--mantine-color-body)"
|
||||||
|
style={{
|
||||||
|
zIndex: stickyHeader ? 10 : 1,
|
||||||
|
boxShadow: isScrolled ? 'var(--mantine-shadow-sm)' : 'none',
|
||||||
|
transition: 'box-shadow 0.2s ease, border-color 0.2s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container fluid px={px} pt={{ base: 'sm', sm: 'sm' }}>
|
||||||
{headerSlot}
|
{headerSlot}
|
||||||
|
{!isScrolled && (
|
||||||
|
<Divider
|
||||||
|
mt={0}
|
||||||
|
mb={0}
|
||||||
|
styles={{
|
||||||
|
root: { borderColor: 'light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5))' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Container fluid px={px} py={py} w="100%" {...others}>
|
<Box flex={1} pos="relative">
|
||||||
|
<Container fluid px={px} py={py} w="100%" h="100%" {...others}>
|
||||||
{children}
|
{children}
|
||||||
</Container>
|
</Container>
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
interface ForbiddenProps {
|
interface ForbiddenProps {
|
||||||
showActionsBack?: boolean;
|
showActionsBack?: boolean;
|
||||||
showActionsHome?: boolean;
|
showActionsHome?: boolean;
|
||||||
|
onClickGoBack?(): void;
|
||||||
|
onClickBackToHome?(): void;
|
||||||
|
homeUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { Title, Text, Button, Container, Stack, Group, Center } from '@mantine/core';
|
import { Title, Text, Button, Container, Stack, Group, Center } from '@mantine/core';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export function Forbidden({ showActionsBack = true, showActionsHome = true }: ForbiddenProps) {
|
export function Forbidden({
|
||||||
|
showActionsBack = true,
|
||||||
|
showActionsHome = true,
|
||||||
|
onClickGoBack,
|
||||||
|
onClickBackToHome,
|
||||||
|
homeUrl,
|
||||||
|
}: ForbiddenProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function onBack() {
|
||||||
|
if (onClickGoBack) onClickGoBack();
|
||||||
|
else navigate(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBackHome() {
|
||||||
|
if (onClickBackToHome) onClickBackToHome();
|
||||||
|
else if (homeUrl) navigate(homeUrl, { replace: true });
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Container size="sm" h="100vh" pos="relative">
|
<Container size="sm" h="100vh" pos="relative">
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
@@ -26,13 +47,13 @@ export function Forbidden({ showActionsBack = true, showActionsHome = true }: Fo
|
|||||||
{(showActionsBack || showActionsHome) && (
|
{(showActionsBack || showActionsHome) && (
|
||||||
<Group mt="xl" justify="center">
|
<Group mt="xl" justify="center">
|
||||||
{showActionsBack && (
|
{showActionsBack && (
|
||||||
<Button variant="default" size="md" onClick={() => window.history.back()}>
|
<Button variant="default" size="md" onClick={onBack}>
|
||||||
Go Back
|
Go Back
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showActionsHome && (
|
{showActionsHome && (
|
||||||
<Button component="a" href="/" color="brand" size="md">
|
<Button color="brand" size="md" onClick={onBackHome}>
|
||||||
Back to Home
|
Back to Home
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
interface NotFoundProps {
|
interface NotFoundProps {
|
||||||
showActionsBack?: boolean;
|
showActionsBack?: boolean;
|
||||||
showActionsHome?: boolean;
|
showActionsHome?: boolean;
|
||||||
|
onClickGoBack?(): void;
|
||||||
|
onClickBackToHome?(): void;
|
||||||
|
homeUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { Title, Text, Button, Container, Stack, Group, Center } from '@mantine/core';
|
import { Title, Text, Button, Container, Stack, Group, Center } from '@mantine/core';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export function NotFound({
|
||||||
|
showActionsBack = true,
|
||||||
|
showActionsHome = true,
|
||||||
|
onClickGoBack,
|
||||||
|
onClickBackToHome,
|
||||||
|
homeUrl,
|
||||||
|
}: NotFoundProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function onBack() {
|
||||||
|
if (onClickGoBack) onClickGoBack();
|
||||||
|
else navigate(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBackHome() {
|
||||||
|
if (onClickBackToHome) onClickBackToHome();
|
||||||
|
else if (homeUrl) navigate(homeUrl, { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
export function NotFound({ showActionsBack = true, showActionsHome = true }: NotFoundProps) {
|
|
||||||
return (
|
return (
|
||||||
<Container size="sm" h="100vh" pos="relative">
|
<Container size="sm" h="100vh" pos="relative">
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
@@ -25,13 +47,13 @@ export function NotFound({ showActionsBack = true, showActionsHome = true }: Not
|
|||||||
{(showActionsBack || showActionsHome) && (
|
{(showActionsBack || showActionsHome) && (
|
||||||
<Group mt="xl" justify="center">
|
<Group mt="xl" justify="center">
|
||||||
{showActionsBack && (
|
{showActionsBack && (
|
||||||
<Button variant="default" size="md" onClick={() => window.history.back()}>
|
<Button variant="default" size="md" onClick={onBack}>
|
||||||
Go Back
|
Go Back
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showActionsHome && (
|
{showActionsHome && (
|
||||||
<Button component="a" href="/" color="brand" size="md">
|
<Button color="brand" size="md" onClick={onBackHome}>
|
||||||
Back to Home
|
Back to Home
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,310 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { Title, Breadcrumbs, Anchor, Box, Text, ThemeIcon, Flex, Divider, ActionIcon, Tooltip } from '@mantine/core';
|
||||||
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
|
import { ChevronRight, LucideIcon, Maximize2, Minimize2 } from 'lucide-react'; // <-- Update Import Icon
|
||||||
|
import { PageActions, PageActionsProps } from '../../../components';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface BreadcrumbItem {
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
type: 'text' | 'link';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModulePageHeaderProps {
|
||||||
|
title?: string;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
badges?: React.ReactNode;
|
||||||
|
breadcrumbs?: BreadcrumbItem[];
|
||||||
|
actions?: PageActionsProps['actions'];
|
||||||
|
showPageHeader?: boolean;
|
||||||
|
disableMinimize?: boolean;
|
||||||
|
moduleKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shared transition style applied to all animated wrappers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const TRANSITION_STYLE: React.CSSProperties = {
|
||||||
|
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sub-components
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface BreadcrumbBarProps {
|
||||||
|
breadcrumbs: BreadcrumbItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbBar({ breadcrumbs }: BreadcrumbBarProps) {
|
||||||
|
return (
|
||||||
|
<Breadcrumbs
|
||||||
|
style={{ flexWrap: 'wrap' }}
|
||||||
|
visibleFrom="sm"
|
||||||
|
separator={
|
||||||
|
<ChevronRight
|
||||||
|
size={12}
|
||||||
|
strokeWidth={3}
|
||||||
|
style={{
|
||||||
|
color: 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3))',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{breadcrumbs.map((item, index) => {
|
||||||
|
const isText = item.type === 'text';
|
||||||
|
const isLast = index === breadcrumbs.length - 1;
|
||||||
|
const sharedProps = {
|
||||||
|
key: index,
|
||||||
|
c: !isLast ? ('dimmed' as const) : undefined,
|
||||||
|
size: 'xs' as const,
|
||||||
|
fw: 500,
|
||||||
|
style: {
|
||||||
|
color: isLast ? 'var(--mantine-color-text)' : undefined,
|
||||||
|
transition: 'color 0.2s ease',
|
||||||
|
letterSpacing: '0.2px',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return !isText ? (
|
||||||
|
<Anchor {...sharedProps} href={item.href}>
|
||||||
|
{item.label}
|
||||||
|
</Anchor>
|
||||||
|
) : (
|
||||||
|
<Text {...sharedProps}>{item.label}</Text>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Breadcrumbs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HeaderToggle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface HeaderToggleProps {
|
||||||
|
isMinimized: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeaderToggle({ isMinimized, onToggle }: HeaderToggleProps) {
|
||||||
|
// const ToggleIcon = isMinimized ? ChevronDown : ChevronUp;
|
||||||
|
const ToggleIcon = isMinimized ? Maximize2 : Minimize2;
|
||||||
|
|
||||||
|
const label = isMinimized ? 'Expand header' : 'Collapse header';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={label} position="bottom-end" withArrow openDelay={400}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
size="sm"
|
||||||
|
radius="md"
|
||||||
|
onClick={onToggle}
|
||||||
|
aria-label={label}
|
||||||
|
style={{
|
||||||
|
opacity: 0.6,
|
||||||
|
...TRANSITION_STYLE,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleIcon size={13} strokeWidth={2} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main component
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function ModulePageHeader(_props: ModulePageHeaderProps) {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon: Icon,
|
||||||
|
badges,
|
||||||
|
breadcrumbs,
|
||||||
|
actions,
|
||||||
|
showPageHeader = true,
|
||||||
|
disableMinimize = false,
|
||||||
|
moduleKey,
|
||||||
|
} = _props;
|
||||||
|
|
||||||
|
const [isMinimized, setIsMinimized] = useLocalStorage<boolean>({
|
||||||
|
key: `page-header-minimized__${btoa(moduleKey)}`,
|
||||||
|
defaultValue: false,
|
||||||
|
getInitialValueInEffect: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!showPageHeader) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showToggle = !disableMinimize;
|
||||||
|
const handleToggle = useCallback(() => setIsMinimized((v) => !v), [setIsMinimized]);
|
||||||
|
const compactButtonProps = useMemo(() => () => ({ size: 'xs' as const }), []);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Compact / Toolbar mode
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
if (isMinimized) {
|
||||||
|
return (
|
||||||
|
<Box mb={0} mt="xs" pb={6} style={TRANSITION_STYLE} className="module-page-header">
|
||||||
|
<Flex
|
||||||
|
direction="row"
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
gap="sm"
|
||||||
|
wrap="nowrap"
|
||||||
|
style={{ minHeight: 36, ...TRANSITION_STYLE }}
|
||||||
|
>
|
||||||
|
{/* Left cluster: title + badges */}
|
||||||
|
<Flex gap="xs" align="center" style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
{title && (
|
||||||
|
<Text
|
||||||
|
fw={600}
|
||||||
|
fz={{ base: 'lg', sm: 'xl' }}
|
||||||
|
truncate="end"
|
||||||
|
style={{
|
||||||
|
color: 'var(--mantine-color-text)',
|
||||||
|
letterSpacing: '-0.2px',
|
||||||
|
lineHeight: 1.3,
|
||||||
|
...TRANSITION_STYLE,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{badges && <Box style={{ flexShrink: 0 }}>{badges}</Box>}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* Right: actions + inline toggle (separated by divider) */}
|
||||||
|
<Flex align="center" gap="sm" style={{ flexShrink: 0 }}>
|
||||||
|
{actions && <PageActions actions={actions} customButtonProps={compactButtonProps} />}
|
||||||
|
{showToggle && (
|
||||||
|
<>
|
||||||
|
{actions && actions.length > 0 && (
|
||||||
|
<Divider orientation="vertical" style={{ height: 20, opacity: 0.4 }} />
|
||||||
|
)}
|
||||||
|
<HeaderToggle isMinimized={isMinimized} onToggle={handleToggle} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Expanded / Full mode
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Helper variable agar kode lebih bersih
|
||||||
|
const hasBreadcrumbs = breadcrumbs && breadcrumbs.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mb={0} mt="xs" pb={18} style={TRANSITION_STYLE} className="module-page-header">
|
||||||
|
{/* 1. Top Navigation Row: Breadcrumbs & Toggle */}
|
||||||
|
{((breadcrumbs && breadcrumbs.length) || showToggle) && (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
mb={{ base: showToggle ? 'xs' : 0, sm: hasBreadcrumbs || showToggle ? 'md' : 0 }}
|
||||||
|
>
|
||||||
|
<Box>{hasBreadcrumbs && <BreadcrumbBar breadcrumbs={breadcrumbs} />}</Box>
|
||||||
|
{showToggle && (
|
||||||
|
<Box ml="auto">
|
||||||
|
<HeaderToggle isMinimized={isMinimized} onToggle={handleToggle} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 2. Main Header Row – 3-column layout */}
|
||||||
|
<Flex
|
||||||
|
direction="row"
|
||||||
|
justify="space-between"
|
||||||
|
align={{ base: 'stretch', sm: 'flex-start' }}
|
||||||
|
gap={{ base: 'md', sm: 'md' }}
|
||||||
|
wrap="nowrap"
|
||||||
|
style={TRANSITION_STYLE}
|
||||||
|
>
|
||||||
|
<Flex gap="md" align={{ base: 'flex-start', sm: 'flex-start' }} style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
{Icon && (
|
||||||
|
<ThemeIcon
|
||||||
|
w={{ base: 40, sm: 48 }}
|
||||||
|
h={{ base: 40, sm: 48 }}
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="brand"
|
||||||
|
visibleFrom="sm"
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
border: '1px solid light-dark(var(--mantine-color-brand-1), var(--mantine-color-brand-6))',
|
||||||
|
boxShadow: 'light-dark(0 4px 12px rgba(0,0,0,0.03), 0 4px 12px rgba(0,0,0,0.2))',
|
||||||
|
...TRANSITION_STYLE,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size={24} strokeWidth={1.5} />
|
||||||
|
</ThemeIcon>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(title || badges || description) && (
|
||||||
|
<Box style={{ flex: 1, minWidth: 0, ...TRANSITION_STYLE }}>
|
||||||
|
<Flex gap="xs" align="center" wrap="wrap">
|
||||||
|
{title && (
|
||||||
|
<Title
|
||||||
|
order={2}
|
||||||
|
fw={600}
|
||||||
|
fz={{ base: 20, sm: 24 }}
|
||||||
|
lh={{ base: 1.3, sm: 1.2 }}
|
||||||
|
style={{
|
||||||
|
color: 'var(--mantine-color-text)',
|
||||||
|
letterSpacing: '-0.3px',
|
||||||
|
...TRANSITION_STYLE,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
)}
|
||||||
|
{badges && <Box>{badges}</Box>}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<Text
|
||||||
|
fz={{ base: 'xs', sm: 'sm' }}
|
||||||
|
c="dimmed"
|
||||||
|
fw={400}
|
||||||
|
lineClamp={2}
|
||||||
|
mt={4}
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
...TRANSITION_STYLE,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
gap="xs"
|
||||||
|
mt={{ base: 'xs', sm: 0 }}
|
||||||
|
ml={{ base: 'lg', sm: 0 }}
|
||||||
|
style={{ flexShrink: 0, alignSelf: 'center' }}
|
||||||
|
>
|
||||||
|
{actions && <PageActions actions={actions} />}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { PrivilegeEntity } from '../entities/entity';
|
||||||
|
|
||||||
|
export const defaultPrivileges: PrivilegeEntity = {
|
||||||
|
ALLOW_VIEW: true,
|
||||||
|
|
||||||
|
ALLOW_CREATE: true,
|
||||||
|
ALLOW_EDIT: true,
|
||||||
|
ALLOW_DELETE: true,
|
||||||
|
ALLOW_DUPLICATE: true,
|
||||||
|
ALLOW_SAVE: true,
|
||||||
|
|
||||||
|
ALLOW_PRINT: true,
|
||||||
|
ALLOW_PRINT_COPY: true,
|
||||||
|
|
||||||
|
ALLOW_APPROVAL: true,
|
||||||
|
ALLOW_ACTIVATE: true,
|
||||||
|
ALLOW_DEACTIVATE: true,
|
||||||
|
|
||||||
|
ALLOW_CONFIRM: true,
|
||||||
|
ALLOW_CANCEL: true,
|
||||||
|
ALLOW_ROLLBACK: true,
|
||||||
|
ALLOW_HOLD: true,
|
||||||
|
|
||||||
|
ALLOW_LOGS: true,
|
||||||
|
ALLOW_NOTES: true,
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './default-privilege';
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import type { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
|
import type { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
|
||||||
|
import type { ModulePageHeaderProps } from '../components/module-page-header';
|
||||||
|
import { PageActionsProps } from '../../../components';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Module Constants & Base Types
|
// Module Constants & Base Types
|
||||||
@@ -87,6 +89,7 @@ export interface DraftConfig {
|
|||||||
* @property singlePageDetailConfig Optional configuration for single-page detail modals or drawers.
|
* @property singlePageDetailConfig Optional configuration for single-page detail modals or drawers.
|
||||||
*/
|
*/
|
||||||
export interface ModuleConfigEntity<E extends BaseEntity = BaseEntity> {
|
export interface ModuleConfigEntity<E extends BaseEntity = BaseEntity> {
|
||||||
|
_data?: E; // Fix unused generic
|
||||||
moduleKey: string;
|
moduleKey: string;
|
||||||
webUrl: string;
|
webUrl: string;
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
@@ -104,6 +107,7 @@ export interface ModuleConfigEntity<E extends BaseEntity = BaseEntity> {
|
|||||||
|
|
||||||
export interface ConfigSlice<E extends BaseEntity = BaseEntity> {
|
export interface ConfigSlice<E extends BaseEntity = BaseEntity> {
|
||||||
config: ModuleConfigEntity<E>;
|
config: ModuleConfigEntity<E>;
|
||||||
|
privileges: PrivilegeEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataServiceSlice<
|
export interface DataServiceSlice<
|
||||||
@@ -184,15 +188,21 @@ export interface EnterpriseFormLifecycleHooks<E extends BaseEntity, TFormData =
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface EnterpriseIndexPageConfig<E extends BaseEntity = BaseEntity> {
|
export interface EnterpriseIndexPageConfig<E extends BaseEntity = BaseEntity> {
|
||||||
|
_data?: E; // Fix unused generic
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
showPageHeader?: boolean;
|
px?: string | number;
|
||||||
useDefaultPadding?: boolean;
|
py?: string | number;
|
||||||
customHiddenActions?: (selected: E[], defaultHidden: string[]) => string[];
|
|
||||||
/** Strongly typed event handler to prevent arbitrary string usage for actions. */
|
|
||||||
onCustomActionClick?: (key: ModuleActionType, data?: unknown) => void;
|
|
||||||
filterDrawerContent?: ReactNode;
|
|
||||||
|
|
||||||
registerRefreshCallback?: (callback: () => void) => void;
|
// customHiddenActions?: (selected: E[], defaultHidden: string[]) => string[];
|
||||||
|
// /** Strongly typed event handler to prevent arbitrary string usage for actions. */
|
||||||
|
// onCustomActionClick?: (key: ModuleActionType, data?: unknown) => void;
|
||||||
|
// filterDrawerContent?: ReactNode;
|
||||||
|
|
||||||
|
// registerRefreshCallback?: (callback: () => void) => void;
|
||||||
|
pageHeaderProps?: Omit<ModulePageHeaderProps, 'actions' | 'moduleKey'>;
|
||||||
|
// actions?: PageActionsProps['actions'];
|
||||||
|
customPageActions?: (actions: PageActionsProps['actions']) => PageActionsProps['actions'];
|
||||||
|
onClickCreate?: (key: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnterpriseFormPageConfig<E extends BaseEntity = BaseEntity> extends EnterpriseFormLifecycleHooks<E> {
|
export interface EnterpriseFormPageConfig<E extends BaseEntity = BaseEntity> extends EnterpriseFormLifecycleHooks<E> {
|
||||||
@@ -223,3 +233,27 @@ export interface EnterpriseDetailPageConfig<E extends BaseEntity = BaseEntity> {
|
|||||||
editMode?: 'FULL' | 'PARTIAL';
|
editMode?: 'FULL' | 'PARTIAL';
|
||||||
afterGetData?: (data: E) => Promise<unknown>;
|
afterGetData?: (data: E) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PrivilegeEntity {
|
||||||
|
ALLOW_VIEW: boolean;
|
||||||
|
ALLOW_CREATE: boolean;
|
||||||
|
ALLOW_EDIT: boolean;
|
||||||
|
ALLOW_DELETE: boolean;
|
||||||
|
ALLOW_DUPLICATE: boolean;
|
||||||
|
ALLOW_SAVE: boolean;
|
||||||
|
|
||||||
|
ALLOW_PRINT: boolean;
|
||||||
|
ALLOW_PRINT_COPY: boolean;
|
||||||
|
|
||||||
|
ALLOW_APPROVAL: boolean;
|
||||||
|
ALLOW_ACTIVATE: boolean;
|
||||||
|
ALLOW_DEACTIVATE: boolean;
|
||||||
|
|
||||||
|
ALLOW_CONFIRM: boolean;
|
||||||
|
ALLOW_CANCEL: boolean;
|
||||||
|
ALLOW_ROLLBACK: boolean;
|
||||||
|
ALLOW_HOLD: boolean;
|
||||||
|
|
||||||
|
ALLOW_LOGS: boolean;
|
||||||
|
ALLOW_NOTES: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import { createContext, useContext } from 'react';
|
|||||||
import type { BaseEntity } from '@repo/core-api/data-services';
|
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||||
|
|
||||||
export interface IndexPageContextValue<E extends BaseEntity = BaseEntity> {
|
export interface IndexPageContextValue<E extends BaseEntity = BaseEntity> {
|
||||||
renderRowActions: (row: E) => React.ReactNode;
|
_data?: E; // Fix unused generic
|
||||||
refreshGrid: () => void;
|
// renderRowActions: (row: E) => React.ReactNode;
|
||||||
// State for batch modals
|
// refreshGrid: () => void;
|
||||||
isConfirmModalOpen: boolean;
|
// // State for batch modals
|
||||||
setIsConfirmModalOpen: (open: boolean) => void;
|
// isConfirmModalOpen: boolean;
|
||||||
isDeleteModalOpen: boolean;
|
// setIsConfirmModalOpen: (open: boolean) => void;
|
||||||
setIsDeleteModalOpen: (open: boolean) => void;
|
// isDeleteModalOpen: boolean;
|
||||||
|
// setIsDeleteModalOpen: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IndexPageContext = createContext<IndexPageContextValue<any> | null>(null);
|
export const IndexPageContext = createContext<IndexPageContextValue<any> | null>(null);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
export * from './constant/';
|
||||||
|
|
||||||
export * from './entities/entity';
|
export * from './entities/entity';
|
||||||
|
|
||||||
export * from './hooks/use-module.context';
|
export * from './hooks/use-module.context';
|
||||||
|
|
||||||
export * from './providers/module.provider';
|
export * from './providers/module.provider';
|
||||||
|
export * from './providers/index-page.provider';
|
||||||
|
export * from './components/module-page-header';
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { BaseEntity } from '@repo/core-api/data-services';
|
||||||
|
import { EnterpriseIndexPageConfig, ModuleAction } from '../entities/entity';
|
||||||
|
import { IndexPageContext } from '../hooks/use-index-page.context';
|
||||||
|
import { CorePageContainer, PageActionProps } from '../../../components';
|
||||||
|
import { ModulePageHeader } from '../components/module-page-header';
|
||||||
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
useEnterpriseModuleConfigContext,
|
||||||
|
useEnterpriseModuleNavigationContext,
|
||||||
|
useEnterpriseModuleTranslationContext,
|
||||||
|
} from '../hooks/use-module.context';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Detect macOS / iOS for displaying platform-specific shortcut labels. */
|
||||||
|
const IS_MAC = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
|
||||||
|
|
||||||
|
/** Platform-aware shortcut label for the Create action. */
|
||||||
|
const CREATE_SHORTCUT_LABEL = IS_MAC ? '⇧⌘N' : 'Ctrl+Shift+N';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Component
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function EnterpriseIndexPageProvider<E extends BaseEntity = BaseEntity>(props: EnterpriseIndexPageConfig<E>) {
|
||||||
|
const { children, pageHeaderProps, px, py, customPageActions, onClickCreate } = props;
|
||||||
|
|
||||||
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
const navigation = useEnterpriseModuleNavigationContext();
|
||||||
|
const { config, privileges } = useEnterpriseModuleConfigContext();
|
||||||
|
const { moduleKey } = config;
|
||||||
|
const { ALLOW_CREATE } = privileges;
|
||||||
|
|
||||||
|
// Stable reference so the useEffect doesn't re-attach on every render.
|
||||||
|
const handleActionClick = useCallback(
|
||||||
|
(key: string) => {
|
||||||
|
if (key === ModuleAction.CREATE && ALLOW_CREATE) {
|
||||||
|
if (onClickCreate) onClickCreate(key);
|
||||||
|
else navigation.navigateToCreate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClickCreate, navigation, ALLOW_CREATE],
|
||||||
|
);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Global keyboard shortcut: Ctrl+Shift+N / ⌘+Shift+N → Create
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
const isShortcut = (e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'n';
|
||||||
|
if (!isShortcut) return;
|
||||||
|
|
||||||
|
// Prevent browser default (e.g., Chrome's "new incognito window").
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
handleActionClick(ModuleAction.CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', onKeyDown, { capture: true });
|
||||||
|
return () => window.removeEventListener('keydown', onKeyDown, { capture: true });
|
||||||
|
}, [handleActionClick]);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Action definitions
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
const pageActions = useMemo(() => {
|
||||||
|
const actions: PageActionProps[] = [];
|
||||||
|
if (ALLOW_CREATE) {
|
||||||
|
actions.push({
|
||||||
|
key: ModuleAction.CREATE,
|
||||||
|
label: t('common:actions.create'),
|
||||||
|
icon: <Plus size={16} />,
|
||||||
|
intent: 'primary',
|
||||||
|
variant: 'filled',
|
||||||
|
shortcutLabel: CREATE_SHORTCUT_LABEL,
|
||||||
|
onClick: (key) => handleActionClick(key),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return customPageActions ? customPageActions(actions) : (actions as any[]);
|
||||||
|
}, [t, customPageActions, handleActionClick, ALLOW_CREATE]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IndexPageContext.Provider value={{}}>
|
||||||
|
<CorePageContainer
|
||||||
|
px={px}
|
||||||
|
py={py}
|
||||||
|
headerSlot={<ModulePageHeader actions={pageActions} {...pageHeaderProps} moduleKey={moduleKey} />}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CorePageContainer>
|
||||||
|
</IndexPageContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useTranslation } from '@repo/core-i18n';
|
import { useTranslation } from '@repo/core-i18n';
|
||||||
import { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
|
import { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
|
||||||
|
|
||||||
import { ModuleConfigEntity, SinglePageFormState, SinglePageModalState } from '../entities/entity';
|
import { ConfigSlice, ModuleConfigEntity, SinglePageFormState, SinglePageModalState } from '../entities/entity';
|
||||||
import {
|
import {
|
||||||
EnterpriseConfigContext,
|
EnterpriseConfigContext,
|
||||||
EnterpriseDataServiceContext,
|
EnterpriseDataServiceContext,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
EnterpriseModalContext,
|
EnterpriseModalContext,
|
||||||
EnterpriseTranslationContext,
|
EnterpriseTranslationContext,
|
||||||
} from '../hooks/use-module.context';
|
} from '../hooks/use-module.context';
|
||||||
|
import { defaultPrivileges } from '../constant/default-privilege';
|
||||||
|
|
||||||
export interface EnterpriseModuleProviderProps<E extends BaseEntity> {
|
export interface EnterpriseModuleProviderProps<E extends BaseEntity> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -26,15 +27,18 @@ export function EnterpriseModuleProvider<E extends BaseEntity>(props: Enterprise
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 1. Config Slice (Static)
|
// 1. Config Slice (Static)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const configSlice = useMemo(() => ({ config }), [config]);
|
const configSlice: ConfigSlice = useMemo(() => {
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
// FIXME => IMPLEMENT PRIVILEGE
|
||||||
|
privileges: defaultPrivileges,
|
||||||
|
};
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 1b. Translation Slice (Dedicated context — decoupled from config)
|
// 1b. Translation Slice (Dedicated context — decoupled from config)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const namespaces = useMemo(
|
const namespaces = useMemo(() => [config.translationNamespace, 'common'], [config.translationNamespace]);
|
||||||
() => [config.translationNamespace, 'common'],
|
|
||||||
[config.translationNamespace],
|
|
||||||
);
|
|
||||||
const { t } = useTranslation(namespaces);
|
const { t } = useTranslation(namespaces);
|
||||||
const translationSlice = useMemo(() => ({ t: t as (key: string, options?: Record<string, unknown>) => string }), [t]);
|
const translationSlice = useMemo(() => ({ t: t as (key: string, options?: Record<string, unknown>) => string }), [t]);
|
||||||
|
|
||||||
@@ -132,6 +136,17 @@ export function EnterpriseModuleProvider<E extends BaseEntity>(props: Enterprise
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Render
|
// Render
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const { ALLOW_VIEW } = configSlice.privileges;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ALLOW_VIEW) navigate('/403', { replace: true });
|
||||||
|
}, [ALLOW_VIEW, navigate]);
|
||||||
|
|
||||||
|
if (!ALLOW_VIEW) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnterpriseConfigContext.Provider value={configSlice}>
|
<EnterpriseConfigContext.Provider value={configSlice}>
|
||||||
<EnterpriseTranslationContext.Provider value={translationSlice}>
|
<EnterpriseTranslationContext.Provider value={translationSlice}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MantineProvider, createTheme, mergeThemeOverrides } from '@mantine/core';
|
import { MantineProvider, createTheme, mergeThemeOverrides } from '@mantine/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { brandColors, errorColors, warningColors, successColors, infoColors } from '../theme/tokens/colors';
|
import { brandColors, errorColors, warningColors, successColors, infoColors, darkColors } from '../theme/tokens/colors';
|
||||||
import { typography } from '../theme/tokens/typography';
|
import { typography } from '../theme/tokens/typography';
|
||||||
import { radius } from '../theme/tokens/radius';
|
import { radius } from '../theme/tokens/radius';
|
||||||
import { compactDensity, standardDensity } from '../theme/tokens/density';
|
import { compactDensity, standardDensity } from '../theme/tokens/density';
|
||||||
@@ -21,12 +21,23 @@ const densityMap = {
|
|||||||
|
|
||||||
export function ThemeProvider({ children, colorScheme = 'light', density = 'compact' }: ThemeProviderProps) {
|
export function ThemeProvider({ children, colorScheme = 'light', density = 'compact' }: ThemeProviderProps) {
|
||||||
const baseTheme = createTheme({
|
const baseTheme = createTheme({
|
||||||
|
/**
|
||||||
|
* Elegant Off-Black for Light Mode text.
|
||||||
|
*
|
||||||
|
* Mantine maps this value to `--mantine-color-black` and uses it as the
|
||||||
|
* default `--mantine-color-text` in Light Mode. Pure #000000 creates
|
||||||
|
* harsh contrast against white backgrounds, causing eye fatigue in
|
||||||
|
* prolonged ERP usage. #1A1B1E is a warm charcoal that maintains
|
||||||
|
* excellent readability (WCAG AAA on white) while feeling softer.
|
||||||
|
*/
|
||||||
|
black: '#1A1B1E',
|
||||||
colors: {
|
colors: {
|
||||||
brand: brandColors,
|
brand: brandColors,
|
||||||
error: errorColors,
|
error: errorColors,
|
||||||
warning: warningColors,
|
warning: warningColors,
|
||||||
success: successColors,
|
success: successColors,
|
||||||
info: infoColors,
|
info: infoColors,
|
||||||
|
dark: darkColors,
|
||||||
},
|
},
|
||||||
primaryColor: 'brand',
|
primaryColor: 'brand',
|
||||||
fontFamily: typography.fontFamily,
|
fontFamily: typography.fontFamily,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
/* Import Mantine core and TipTap extensions */
|
/* Import Mantine core and TipTap extensions */
|
||||||
@import '@mantine/core/styles.css';
|
@import '@mantine/core/styles.css';
|
||||||
@import '@mantine/tiptap/styles.css';
|
@import '@mantine/tiptap/styles.css';
|
||||||
|
@import '@mantine/notifications/styles.css';
|
||||||
|
|
||||||
/* Initialize Tailwind CSS v4 engine */
|
/* Initialize Tailwind CSS v4 engine */
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@@ -260,19 +261,25 @@
|
|||||||
--animate-bounce: bounce 1s infinite;
|
--animate-bounce: bounce 1s infinite;
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes ping {
|
@keyframes ping {
|
||||||
75%, 100% {
|
75%,
|
||||||
|
100% {
|
||||||
transform: scale(2);
|
transform: scale(2);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
50% { opacity: 0.5; }
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes bounce {
|
@keyframes bounce {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translateY(-25%);
|
transform: translateY(-25%);
|
||||||
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,3 +77,31 @@ export const grayColors: MantineColorsTuple = [
|
|||||||
'#1f2937',
|
'#1f2937',
|
||||||
'#111827',
|
'#111827',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elegant Dark Mode surface palette.
|
||||||
|
*
|
||||||
|
* Replaces Mantine's default `dark` scale which uses near-pure-black values
|
||||||
|
* that cause excessive contrast and eye strain. This scale uses a subtle
|
||||||
|
* bluish-charcoal (slate) undertone inspired by Tailwind's Slate palette,
|
||||||
|
* giving dark mode a warmer, more refined feel typical of premium enterprise
|
||||||
|
* applications (Figma, Linear, Notion).
|
||||||
|
*
|
||||||
|
* Index mapping in Mantine v7:
|
||||||
|
* dark[0] → lightest text on dark bg dark[5] → surface borders
|
||||||
|
* dark[6] → card/surface bg dark[7] → main app bg
|
||||||
|
* dark[8] → deeper bg (sidebars) dark[9] → deepest bg
|
||||||
|
*/
|
||||||
|
export const darkColors: MantineColorsTuple = [
|
||||||
|
'#C9CCD1', // 0 – Light text / captions on dark bg
|
||||||
|
'#ADB1B8', // 1 – Secondary text
|
||||||
|
'#8E939B', // 2 – Tertiary / placeholder text
|
||||||
|
'#5E6370', // 3 – Subtle borders, disabled text
|
||||||
|
'#3D4250', // 4 – Elevated borders
|
||||||
|
'#2E3341', // 5 – Surface borders, dividers
|
||||||
|
'#252A37', // 6 – Card / component surface
|
||||||
|
'#1C2030', // 7 – Main app background
|
||||||
|
'#151828', // 8 – Deep background (sidebar, nav)
|
||||||
|
'#0F1120', // 9 – Deepest background
|
||||||
|
];
|
||||||
|
|
||||||
|
|||||||
Generated
+73
@@ -206,6 +206,9 @@ importers:
|
|||||||
zod:
|
zod:
|
||||||
specifier: ^3.25.36
|
specifier: ^3.25.36
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
|
zustand:
|
||||||
|
specifier: ^5.0.14
|
||||||
|
version: 5.0.14(@types/react@19.2.7)(react@19.2.3)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@repo/eslint-config':
|
'@repo/eslint-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -443,6 +446,9 @@ importers:
|
|||||||
'@mantine/hooks':
|
'@mantine/hooks':
|
||||||
specifier: ^8.3.15
|
specifier: ^8.3.15
|
||||||
version: 8.3.15(react@19.2.3)
|
version: 8.3.15(react@19.2.3)
|
||||||
|
'@mantine/notifications':
|
||||||
|
specifier: ^8.3.15
|
||||||
|
version: 8.3.18(@mantine/core@8.3.15)(@mantine/hooks@8.3.15)(react-dom@19.2.3)(react@19.2.3)
|
||||||
'@mantine/tiptap':
|
'@mantine/tiptap':
|
||||||
specifier: ^9.3.2
|
specifier: ^9.3.2
|
||||||
version: 9.3.2(@mantine/core@8.3.15)(@mantine/hooks@8.3.15)(@tiptap/extension-link@3.27.1)(@tiptap/react@3.27.1)(react-dom@19.2.3)(react@19.2.3)
|
version: 9.3.2(@mantine/core@8.3.15)(@mantine/hooks@8.3.15)(@tiptap/extension-link@3.27.1)(@tiptap/react@3.27.1)(react-dom@19.2.3)(react@19.2.3)
|
||||||
@@ -1933,6 +1939,30 @@ packages:
|
|||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@mantine/notifications@8.3.18(@mantine/core@8.3.15)(@mantine/hooks@8.3.15)(react-dom@19.2.3)(react@19.2.3):
|
||||||
|
resolution: {integrity: sha512-IpQ0lmwbigTBbZCR6iSYWqIOKEx1tlcd7PcEJ5M5X1qeVSY/N3mmDQt1eJmObvcyDeL5cTJMbSA9UPqhRqo9jw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@mantine/core': 8.3.18
|
||||||
|
'@mantine/hooks': 8.3.18
|
||||||
|
react: ^18.x || ^19.x
|
||||||
|
react-dom: ^18.x || ^19.x
|
||||||
|
dependencies:
|
||||||
|
'@mantine/core': 8.3.15(@mantine/hooks@8.3.15)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3)
|
||||||
|
'@mantine/hooks': 8.3.15(react@19.2.3)
|
||||||
|
'@mantine/store': 8.3.18(react@19.2.3)
|
||||||
|
react: 19.2.3
|
||||||
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
|
react-transition-group: 4.4.5(react-dom@19.2.3)(react@19.2.3)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@mantine/store@8.3.18(react@19.2.3):
|
||||||
|
resolution: {integrity: sha512-i+QRTLmZzLldea0egtUVnGALd6UMIu8jd44nrNWBSNIXJU/8B6rMlC6gyX+l4szopZSuOaaNJIXkqRdC1gQsVg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.x || ^19.x
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@mantine/tiptap@9.3.2(@mantine/core@8.3.15)(@mantine/hooks@8.3.15)(@tiptap/extension-link@3.27.1)(@tiptap/react@3.27.1)(react-dom@19.2.3)(react@19.2.3):
|
/@mantine/tiptap@9.3.2(@mantine/core@8.3.15)(@mantine/hooks@8.3.15)(@tiptap/extension-link@3.27.1)(@tiptap/react@3.27.1)(react-dom@19.2.3)(react@19.2.3):
|
||||||
resolution: {integrity: sha512-X344wqt3eusMLPANWuNSnKoFjTDlCEOUpYq6hPWU6uBvxEHJGHuJFXjtm/jg/D6aNEvWr+P+m1+ElE7eLT/G0A==}
|
resolution: {integrity: sha512-X344wqt3eusMLPANWuNSnKoFjTDlCEOUpYq6hPWU6uBvxEHJGHuJFXjtm/jg/D6aNEvWr+P+m1+ElE7eLT/G0A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6210,6 +6240,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/dom-helpers@5.2.1:
|
||||||
|
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.4
|
||||||
|
csstype: 3.2.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dompurify@3.4.11:
|
/dompurify@3.4.11:
|
||||||
resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==}
|
resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==}
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -10348,6 +10385,20 @@ packages:
|
|||||||
- '@types/react'
|
- '@types/react'
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-transition-group@4.4.5(react-dom@19.2.3)(react@19.2.3):
|
||||||
|
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.6.0'
|
||||||
|
react-dom: '>=16.6.0'
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.4
|
||||||
|
dom-helpers: 5.2.1
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 19.2.3
|
||||||
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react@19.2.3:
|
/react@19.2.3:
|
||||||
resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
|
resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -12564,5 +12615,27 @@ packages:
|
|||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/zustand@5.0.14(@types/react@19.2.7)(react@19.2.3):
|
||||||
|
resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=18.0.0'
|
||||||
|
immer: '>=9.0.6'
|
||||||
|
react: '>=18.0.0'
|
||||||
|
use-sync-external-store: '>=1.2.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
use-sync-external-store:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 19.2.7
|
||||||
|
react: 19.2.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/zwitch@2.0.4:
|
/zwitch@2.0.4:
|
||||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||||
|
|||||||
Reference in New Issue
Block a user