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:
Firman Ramdhani
2026-07-10 22:58:55 +07:00
parent 430b231360
commit 2b8f9a9cbc
55 changed files with 2809 additions and 354 deletions
+2 -1
View File
@@ -31,7 +31,8 @@
"react-i18next": "^15.4.0",
"react-router-dom": "^7.11.0",
"tailwindcss": "^4.1.18",
"zod": "^3.25.36"
"zod": "^3.25.36",
"zustand": "^5.0.14"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
+6 -15
View File
@@ -1,8 +1,9 @@
import { lazy, Suspense, useState } from 'react';
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 { LoadingScreen } from '../core/components/loading-screen';
import { useThemeStore } from '../core/store/theme.store';
const AuthModule = lazy(() => import('./auth'));
const AppModule = lazy(() => import('./modules'));
@@ -10,7 +11,7 @@ const ShowcaseView = lazy(() => import('./showcase/showcase-view'));
const ShellDemo = lazy(() => import('./showcase/shell-demo'));
export default function App() {
const [colorScheme, setColorScheme] = useState<ColorSchemeType>('light');
const colorScheme = useThemeStore((s) => s.colorScheme);
const [density, setDensity] = useState<DensityType>('compact');
return (
@@ -20,20 +21,10 @@ export default function App() {
<Routes>
<Route path="/auth/*" element={<AuthModule />} />
<Route path="/app/*" element={<AppModule />} />
<Route
path="/showcase"
element={
<ShowcaseView
colorScheme={colorScheme}
setColorScheme={setColorScheme}
density={density}
setDensity={setDensity}
/>
}
/>
<Route path="/showcase" element={<ShowcaseView density={density} setDensity={setDensity} />} />
<Route path="/shell-demo" element={<ShellDemo />} />
<Route path="/404" element={<NotFound />} />
<Route path="/403" element={<Forbidden />} />
<Route path="/404" element={<NotFound homeUrl="/app" />} />
<Route path="/403" element={<Forbidden homeUrl="/app" />} />
<Route path="/maintenance" element={<Maintenance />} />
<Route path="/coming-soon" element={<ComingSoon />} />
<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": {
"status": "Status",
"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": {
"status": "Status",
"name": "Nama",
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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 { ChevronRight, CheckCircle2, Trash2, Edit2, Play } from 'lucide-react';
export default function FullPagePageDetail() {
const { t } = useEnterpriseModuleTranslationContext();
return (
<div>
<div>full-page.page.detail</div>
<div>{t('fields.code')}</div>
<div>{t('fields.name')}</div>
<div>{t('fields.status')}</div>
<div>{t('fields.description')}</div>
</div>
<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>
);
}
@@ -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 { ChevronRight, Database } from 'lucide-react';
export default function FullPagePageForm({ formPageType }: { formPageType: 'edit' | 'create' | 'duplicate' }) {
const { t } = useEnterpriseModuleTranslationContext();
return (
<div>
<div>full-page.page.form</div>
<div>Form Type: {formPageType}</div>
<div>{t('fields.code')}</div>
<div>{t('fields.name')}</div>
<div>{t('fields.status')}</div>
<div>{t('fields.description')}</div>
<div>{t('validation:required')}</div>
<div>{t('common:save')}</div>
</div>
<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>
);
}
@@ -1,99 +1,387 @@
import { Table, Box, Title, Paper } 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 } from 'lucide-react';
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',
},
];
import { EnterpriseIndexPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import { Trans } from '@repo/core-i18n';
import { Badge, Text } from '@repo/ui/components';
import { Activity, LayoutDashboard } from 'lucide-react';
import { FullPageModuleConfig } from '../../domain/constants';
export default function FullPagePageIndex() {
// Translation is scoped to ['FULL_PAGE', 'common'] — no prefix needed for module keys
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 (
<Box p="md">
<Title order={2} mb="md">
{t('title')}
</Title>
<Paper withBorder shadow="sm" radius="md">
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<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>
<Table.Th w={200}>{t('common:edit')}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</Paper>
</Box>
<EnterpriseIndexPageProvider
pageHeaderProps={{
title: t('title'),
description: (
<Trans
t={t}
i18nKey="description"
components={{
1: <Text span fw={500} c="var(--mantine-color-text)" />,
}}
/>
),
badges: (
<Badge
variant="light"
color="teal"
size="sm"
radius="sm"
fw={600}
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,
Group,
Select,
Switch,
Text,
useCoreAppShell,
ActionIcon,
@@ -11,8 +12,9 @@ import {
UnstyledButton,
Box,
} 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 { useThemeStore } from '../../../../core/store/theme.store';
import { NotificationDropdown } from './notifications/notification-dropdown';
import { Link } from 'react-router-dom';
@@ -35,16 +37,10 @@ const getInitials = (name: string): string => {
export default function HeaderLayout() {
const { i18n, t } = useTranslation();
const { mobileOpened, toggleMobile } = useCoreAppShell();
const { colorScheme, setColorScheme } = useThemeStore();
return (
<Group
h="100%"
px="md"
justify="space-between"
bg="var(--mantine-color-body)"
style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}
wrap="nowrap"
>
<Group h="100%" px="md" justify="space-between" bg="var(--mantine-color-body)" wrap="nowrap">
{/* 1. LEFT SECTION (Navigation & Context) */}
<Group wrap="nowrap" gap="sm">
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
@@ -57,7 +53,14 @@ export default function HeaderLayout() {
{/* 3. RIGHT SECTION (Actions & Profile) */}
<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} />
</ActionIcon>
@@ -136,6 +139,7 @@ export default function HeaderLayout() {
cursor: 'pointer',
height: '22px',
minHeight: '22px',
fontSize: 'var(--mantine-font-size-sm)',
},
root: {
marginTop: '-4px',
@@ -145,11 +149,30 @@ export default function HeaderLayout() {
/>
</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 />
{/* Application Section */}
<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.Divider />
@@ -78,7 +78,7 @@ export function NotificationDropdown({ children }: NotificationDropdownProps) {
justify="space-between"
px="md"
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">
{t('common:notificationsTitle')}
@@ -129,7 +129,7 @@ export function NotificationDropdown({ children }: NotificationDropdownProps) {
active={!item.isRead}
variant="light"
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 && (
<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')}
</Button>
</Box>
@@ -44,6 +44,7 @@ const MenuItemExpanded = memo(function MenuItemExpanded({
expandVersion = 0,
collapseVersion = 0,
}: MenuItemExpandedProps) {
const { t } = useTranslation();
const Icon = item.icon;
const isActive = activeKeys.has(item.key);
const hasChildren = item.children && item.children.length > 0;
@@ -66,7 +67,7 @@ const MenuItemExpanded = memo(function MenuItemExpanded({
<NavLink
component={hasChildren ? 'button' : (Link as any)}
to={hasChildren ? undefined : item.path}
label={item.label}
label={t(item.label)}
leftSection={<Icon size={18} />}
active={isExactActive}
opened={isOpened}
@@ -75,7 +76,7 @@ const MenuItemExpanded = memo(function MenuItemExpanded({
styles={{
root: {
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: {
overflow: 'hidden',
@@ -110,6 +111,7 @@ interface MenuItemFlyoutProps {
}
const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot = true }: MenuItemFlyoutProps) {
const { t } = useTranslation();
const Icon = item.icon;
const isActive = activeKeys.has(item.key);
const hasChildren = item.children && item.children.length > 0;
@@ -118,14 +120,14 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
if (!hasChildren) {
if (isRoot) {
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
component={Link as any}
to={item.path}
variant={isActive ? 'light' : 'subtle'}
color={isActive ? undefined : 'gray'}
color={isActive ? undefined : 'var(--mantine-color-text)'}
size="lg"
aria-label={item.label}
aria-label={t(item.label)}
>
<Icon size={20} />
</ActionIcon>
@@ -137,7 +139,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
component={Link as any}
to={item.path}
leftSection={<Icon size={14} />}
color={isActive ? 'var(--mantine-primary-color-filled)' : undefined}
color={isActive ? 'brand' : undefined}
styles={{
itemLabel: {
overflow: 'hidden',
@@ -147,7 +149,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
},
}}
>
{item.label}
{t(item.label)}
</Menu.Item>
);
}
@@ -157,9 +159,9 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
const Target = isRoot ? (
<ActionIcon
variant={isActive ? 'light' : 'subtle'}
color={isActive ? undefined : 'gray'}
color={isActive ? undefined : 'var(--mantine-color-text)'}
size="lg"
aria-label={item.label}
aria-label={t(item.label)}
>
<Icon size={20} />
</ActionIcon>
@@ -167,7 +169,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
<Menu.Item
leftSection={<Icon 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
styles={{
itemLabel: {
@@ -178,7 +180,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
},
}}
>
{item.label}
{t(item.label)}
</Menu.Item>
);
@@ -197,7 +199,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
<Menu.Dropdown>
{isRoot && (
<>
<Menu.Label>{item.label}</Menu.Label>
<Menu.Label>{t(item.label)}</Menu.Label>
<Menu.Divider />
</>
)}
@@ -295,7 +297,7 @@ export const SidebarMenu = memo(function SidebarMenu({
const query = searchQuery.toLowerCase();
const filterItem = (item: MenuItemType): MenuItemType | null => {
const isMatch = item.label.toLowerCase().includes(query);
const isMatch = t(item.label).toLowerCase().includes(query);
if (item.children) {
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'));
}, []);
const shortcutText = isMac ? '⌘K' : 'Ctrl+K';
const filterMenuShortcutText = isMac ? '⇧⌘M' : 'Ctrl+Shift+M';
const toggleShortcutText = isMac ? '⇧⌘B' : 'Ctrl+Shift+B';
useEffect(() => {
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();
if (isMini) {
handleExpandAndSearch();
@@ -348,6 +352,12 @@ export const SidebarMenu = memo(function SidebarMenu({
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);
return () => document.removeEventListener('keydown', handleKeyDown);
@@ -359,20 +369,27 @@ export const SidebarMenu = memo(function SidebarMenu({
<Box h="100%" style={{ display: 'flex', flexDirection: 'column' }}>
{withMenuFilter && (
<Box
p="xs"
p="md"
style={{
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: 'color-mix(in srgb, var(--mantine-color-body) 80%, transparent)',
backdropFilter: 'blur(8px)',
borderBottom: '1px solid var(--mantine-color-default-border)',
borderBottom: '1px solid var(--app-shell-border-color)',
display: 'flex',
justifyContent: 'center',
}}
>
<ActionIcon variant="subtle" size="lg" onClick={handleExpandAndSearch} aria-label="Search menu">
<Search size={18} />
<ActionIcon
variant="subtle"
size="md"
onClick={handleExpandAndSearch}
aria-label="Search menu"
color="var(--mantine-color-text)"
radius="md"
>
<Search size={20} />
</ActionIcon>
</Box>
)}
@@ -387,7 +404,7 @@ export const SidebarMenu = memo(function SidebarMenu({
<>
<Divider />
<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">
<ChevronsRight size={18} />
</ActionIcon>
@@ -411,7 +428,7 @@ export const SidebarMenu = memo(function SidebarMenu({
zIndex: 10,
backgroundColor: 'color-mix(in srgb, var(--mantine-color-body) 80%, transparent)',
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' }}>
@@ -439,11 +456,11 @@ export const SidebarMenu = memo(function SidebarMenu({
pointerEvents: 'none',
padding: '2px 4px',
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)',
}}
>
{shortcutText}
{filterMenuShortcutText}
</span>
)
}
@@ -485,7 +502,7 @@ export const SidebarMenu = memo(function SidebarMenu({
<Divider />
<Box p="xs">
<NavLink
label={t('common:collapse')}
label={`${t('common:collapse')} (${toggleShortcutText})`}
leftSection={<ChevronsLeft size={18} />}
onClick={handleToggle}
variant="subtle"
@@ -35,31 +35,31 @@ import type { MenuItemType } from '../types/menu.types';
export const MENU_ITEMS: MenuItemType[] = [
{
key: 'dashboard',
label: 'Dashboard',
label: 'nav:dashboard',
icon: LayoutDashboard,
path: '/app/dashboard',
},
{
key: 'crm',
label: 'CRM',
label: 'nav:crm',
icon: Users,
path: '/app/crm',
children: [
{
key: 'crm-leads',
label: 'Leads',
label: 'nav:crm-leads',
icon: Briefcase,
path: '/app/crm/leads',
},
{
key: 'crm-pipelines',
label: 'Pipelines',
label: 'nav:crm-pipelines',
icon: Activity,
path: '/app/crm/pipelines',
},
{
key: 'crm-contacts',
label: 'Contacts',
label: 'nav:crm-contacts',
icon: Phone,
path: '/app/crm/contacts',
},
@@ -67,25 +67,25 @@ export const MENU_ITEMS: MenuItemType[] = [
},
{
key: 'sales',
label: 'Sales',
label: 'nav:sales',
icon: ShoppingCart,
path: '/app/sales',
children: [
{
key: 'sales-quotations',
label: 'Quotations',
label: 'nav:sales-quotations',
icon: FileText,
path: '/app/sales/quotations',
},
{
key: 'sales-orders',
label: 'Sales Orders',
label: 'nav:sales-orders',
icon: Box,
path: '/app/sales/orders',
},
{
key: 'sales-invoices',
label: 'Invoices',
label: 'nav:sales-invoices',
icon: Receipt,
path: '/app/sales/invoices',
},
@@ -93,31 +93,31 @@ export const MENU_ITEMS: MenuItemType[] = [
},
{
key: 'supply-chain',
label: 'Supply Chain',
label: 'nav:supply-chain',
icon: Truck,
path: '/app/supply-chain',
children: [
{
key: 'sc-inventory',
label: 'Inventory Management',
label: 'nav:sc-inventory',
icon: Box,
path: '/app/supply-chain/inventory',
children: [
{
key: 'sc-inventory-products',
label: 'Products',
label: 'nav:sc-inventory-products',
icon: Layers,
path: '/app/supply-chain/inventory/products',
},
{
key: 'sc-inventory-categories',
label: 'Categories',
label: 'nav:sc-inventory-categories',
icon: Globe,
path: '/app/supply-chain/inventory/categories',
},
{
key: 'sc-inventory-adjustments',
label: 'Stock Adjustments',
label: 'nav:sc-inventory-adjustments',
icon: FileSearch,
path: '/app/supply-chain/inventory/adjustments',
},
@@ -125,13 +125,13 @@ export const MENU_ITEMS: MenuItemType[] = [
},
{
key: 'sc-warehouses',
label: 'Warehouses',
label: 'nav:sc-warehouses',
icon: Warehouse,
path: '/app/supply-chain/warehouses',
},
{
key: 'sc-logistics',
label: 'Logistics',
label: 'nav:sc-logistics',
icon: Globe,
path: '/app/supply-chain/logistics',
},
@@ -139,19 +139,19 @@ export const MENU_ITEMS: MenuItemType[] = [
},
{
key: 'manufacturing',
label: 'Manufacturing',
label: 'nav:manufacturing',
icon: Factory,
path: '/app/manufacturing',
children: [
{
key: 'mfg-bom',
label: 'Bill of Materials',
label: 'nav:mfg-bom',
icon: Layers,
path: '/app/manufacturing/bom',
},
{
key: 'mfg-work-orders',
label: 'Work Orders',
label: 'nav:mfg-work-orders',
icon: HardHat,
path: '/app/manufacturing/work-orders',
},
@@ -159,31 +159,31 @@ export const MENU_ITEMS: MenuItemType[] = [
},
{
key: 'hris',
label: 'HRIS',
label: 'nav:hris',
icon: Briefcase,
path: '/app/hris',
children: [
{
key: 'hris-employees',
label: 'Employees',
label: 'nav:hris-employees',
icon: Users,
path: '/app/hris/employees',
},
{
key: 'hris-attendance',
label: 'Attendance',
label: 'nav:hris-attendance',
icon: Clock,
path: '/app/hris/attendance',
},
{
key: 'hris-payroll',
label: 'Payroll',
label: 'nav:hris-payroll',
icon: CreditCard,
path: '/app/hris/payroll',
},
{
key: 'hris-calendar',
label: 'Company Calendar',
label: 'nav:hris-calendar',
icon: Calendar,
path: '/app/hris/calendar',
},
@@ -191,19 +191,19 @@ export const MENU_ITEMS: MenuItemType[] = [
},
{
key: 'accounting',
label: 'Accounting',
label: 'nav:accounting',
icon: Calculator,
path: '/app/accounting',
children: [
{
key: 'acc-gl',
label: 'General Ledger',
label: 'nav:acc-gl',
icon: Database,
path: '/app/accounting/general-ledger',
},
{
key: 'acc-taxes',
label: 'Taxes',
label: 'nav:acc-taxes',
icon: PiggyBank,
path: '/app/accounting/taxes',
},
@@ -211,25 +211,25 @@ export const MENU_ITEMS: MenuItemType[] = [
},
{
key: 'settings',
label: 'Settings & Configuration',
label: 'nav:settings',
icon: Settings,
path: '/app/settings',
children: [
{
key: 'settings-general',
label: 'General Settings',
label: 'nav:settings-general',
icon: Settings,
path: '/app/settings/general',
},
{
key: 'settings-security',
label: 'Security',
label: 'nav:settings-security',
icon: Shield,
path: '/app/settings/security',
},
{
key: 'long-text-2',
label: 'Extremely Long Menu Name To Test Text Truncation Handling Properly',
label: 'nav:long-text-2',
icon: FileText,
path: '/app/settings/long-menu-test',
},
@@ -237,19 +237,19 @@ export const MENU_ITEMS: MenuItemType[] = [
},
{
key: 'example-module',
label: 'Example Module',
label: 'nav:example-module',
icon: Database,
path: '/app/example-module',
children: [
{
key: 'example-full-page',
label: 'Example Full Page',
label: 'nav:example-full-page',
icon: LayoutDashboard,
path: '/app/example/full-page/index',
},
{
key: 'example-single-page',
label: 'Example Single Page',
label: 'nav:example-single-page',
icon: FileText,
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 { registerModuleNamespace } from '@repo/core-i18n';
import HeaderLayout from './components/header.layout';
import { SidebarMenu } from './components/sidebar';
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 }) {
const configAppShell: CoreAppShellConfig = {
variant: 'header-first',
@@ -6,7 +6,7 @@ import type { LucideIcon } from 'lucide-react';
export interface MenuItemType {
/** Unique identifier for the menu item */
key: string;
/** Display label */
/** i18n translation key (e.g. 'nav:dashboard'). Resolved via t() at render time. */
label: string;
/** Lucide icon component */
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 { useTranslation } from '@repo/core-i18n';
import { Tabs, Card, CorePageContainer } from '@repo/ui/components';
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() {
const { t } = useTranslation();
return (
<Container fluid p="md">
<Paper shadow="sm" p="md" radius="md">
<Title order={2} mb="xs">
{t('common:systemInformation')}
</Title>
<Text c="dimmed">{t('common:systemInformationDesc')}</Text>
</Paper>
</Container>
<CorePageContainer>
<ModulePageHeader
icon={Info}
title={t('information:info.pageTitle')}
description={t('information:info.pageDescription')}
disableMinimize={true}
moduleKey="SYSTEM_INFORMATION"
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 { 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';
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',
label: 'Save Changes',
@@ -65,7 +72,7 @@ export default function ActionToolsShowcase() {
},
];
const rowActions: RowAction[] = [
const rowActions: RowActionProps[] = [
{
key: 'edit',
tooltip: 'Edit Record',
@@ -111,7 +118,7 @@ export default function ActionToolsShowcase() {
<Text c="dimmed" mb="lg">
Used in toolbars and page headers.
</Text>
<PageActions actions={pageActions} onClose={() => {}} />
<PageActions actions={pageActions} />
</Card>
<Card withBorder shadow="sm" radius="md" p="md">
@@ -87,12 +87,7 @@ export function LiveStockGrid() {
Stop Feed
</Button>
)}
<Button
variant="light"
color="info"
onClick={() => setShowAll((s) => !s)}
size="sm"
>
<Button variant="light" color="info" onClick={() => setShowAll((s) => !s)} size="sm">
{showAll ? `Show ${VISIBLE_ROWS} rows` : `Show all ${STOCK_COUNT} rows`}
</Button>
</Group>
@@ -118,7 +113,7 @@ export function LiveStockGrid() {
</Text>
{/* ── 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' }}>
<thead>
<tr
@@ -133,11 +128,51 @@ export function LiveStockGrid() {
zIndex: 1,
}}
>
<th style={{ padding: '6px 8px', textAlign: 'left', borderBottom: '2px solid var(--mantine-color-default-border)' }}>Ticker</th>
<th style={{ padding: '6px 8px', textAlign: 'right', borderBottom: '2px solid var(--mantine-color-default-border)' }}>Price</th>
<th style={{ padding: '6px 8px', textAlign: 'right', borderBottom: '2px solid var(--mantine-color-default-border)' }}>Change</th>
<th style={{ padding: '6px 8px', textAlign: 'right', borderBottom: '2px solid var(--mantine-color-default-border)' }}>Volume</th>
<th style={{ padding: '6px 8px', textAlign: 'right', borderBottom: '2px solid var(--mantine-color-default-border)' }}>Renders</th>
<th
style={{
padding: '6px 8px',
textAlign: 'left',
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>
</thead>
<tbody>
@@ -230,7 +230,7 @@ export default function ShellDemo() {
px="md"
justify="space-between"
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}>
Mock Footer (bg="gray.1")
+4 -6
View File
@@ -1,6 +1,7 @@
import { useState, Suspense } from 'react';
import { useNavigate } from 'react-router-dom';
import { ColorSchemeType, DensityType } from '@repo/ui/provider';
import { useThemeStore } from '../../core/store/theme.store';
import {
Button,
Card,
@@ -34,13 +35,12 @@ import FormDemoView from './example/features/form-demo';
import { COMPONENTS_REGISTRY } from './registry';
interface ShowcaseViewProps {
colorScheme: ColorSchemeType;
setColorScheme: (val: ColorSchemeType) => void;
density: DensityType;
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 { i18n } = useTranslation();
const navigate = useNavigate();
@@ -103,7 +103,7 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
list: {
minWidth: 260,
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)',
},
panel: { flex: 1, display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' },
@@ -407,8 +407,6 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
)
);
})}
</Container>
</Box>
</Tabs.Panel>
+2
View File
@@ -3,6 +3,7 @@ import { createLocalStorage, createIndexedDB } from '@repo/core-storage';
export const AppStorageKey = {
USER_PROFILE: 'user_profile',
LOCALE: 'app_locale',
THEME: 'app_theme',
ACCESS_TOKEN: 'access_token',
REFRESH_TOKEN: 'refresh_token',
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>([
AppStorageKey.LOCALE,
AppStorageKey.THEME,
AppStorageKey.MOCK_DB_COMPANY_A,
AppStorageKey.OFFLINE_DRAFT,
]);
+29
View File
@@ -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),
},
),
);