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