feat: add RowActions component for enhanced row-level actions in data grids
- Introduced RowActions component to manage row-level actions with tooltips and dropdown menus. - Created types for row actions and page actions to standardize action properties. - Implemented utility function to map action intents to Mantine theme colors. - Updated CoreAppShell component to support optional slots for better flexibility. - Added enterprise module structure with context hooks for managing module state and actions. - Implemented draft management for forms to enhance user experience during data entry. - Established context providers for detail, form, and index pages to streamline data handling. - Updated dependencies to ensure compatibility with the latest versions.
This commit is contained in:
@@ -1,25 +1,23 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { withMermaid } from 'vitepress-plugin-mermaid'
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { withMermaid } from 'vitepress-plugin-mermaid';
|
||||
|
||||
const config = withMermaid(
|
||||
defineConfig({
|
||||
// title: "Frontend Monorepo",
|
||||
title: 'Frontend Arch',
|
||||
description: "Centralized documentation for the Enterprise Frontend Monorepo",
|
||||
description: 'Centralized documentation for the Enterprise Frontend Monorepo',
|
||||
head: [
|
||||
['link', { rel: 'icon', href: '/favicon.svg' }] // Jika Anda menggunakan favicon.svg
|
||||
],
|
||||
['link', { rel: 'icon', href: '/favicon.svg' }], // Jika Anda menggunakan favicon.svg
|
||||
],
|
||||
themeConfig: {
|
||||
search: {
|
||||
provider: 'local',
|
||||
options: {
|
||||
detailedView: true
|
||||
}
|
||||
detailedView: true,
|
||||
},
|
||||
},
|
||||
logo: '/logo.svg',
|
||||
nav: [
|
||||
{ text: 'Docs', link: '/overview' },
|
||||
],
|
||||
nav: [{ text: 'Docs', link: '/overview' }],
|
||||
|
||||
sidebar: [
|
||||
{
|
||||
@@ -33,13 +31,22 @@ const config = withMermaid(
|
||||
text: 'Core Architecture',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'API & Domain Logic', link: '/packages/core-api/' },
|
||||
// { text: 'API & Domain Logic', link: '/packages/core-api/' },
|
||||
{
|
||||
text: 'API & Domain Logic',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'API Engine', link: '/packages/core-api' },
|
||||
{ text: 'Data Transformers', link: '/packages/core-api/transformers' },
|
||||
],
|
||||
},
|
||||
|
||||
{ text: 'Event Bus System', link: '/packages/core-events/' },
|
||||
{ text: 'Storage & Persistence', link: '/packages/core-storage/' },
|
||||
{ text: 'I18n & Localization', link: '/packages/core-i18n/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
{
|
||||
text: 'UI System',
|
||||
collapsed: false,
|
||||
items: [
|
||||
@@ -52,23 +59,23 @@ const config = withMermaid(
|
||||
text: 'Desktop Ecosystem',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Overview', link: '/apps/desktop/' },
|
||||
{ text: 'Lifecycle & Configuration', link: '/apps/desktop/CONFIGURATION' },
|
||||
{ text: 'IPC & Bridge Architecture', link: '/apps/desktop/IPC_ARCHITECTURE' },
|
||||
{ text: 'Distribution & Auto-Update', link: '/apps/desktop/AUTO_UPDATER' },
|
||||
{ text: 'Overview', link: '/apps/desktop/' },
|
||||
{ text: 'Lifecycle & Configuration', link: '/apps/desktop/CONFIGURATION' },
|
||||
{ text: 'IPC & Bridge Architecture', link: '/apps/desktop/IPC_ARCHITECTURE' },
|
||||
{ text: 'Distribution & Auto-Update', link: '/apps/desktop/AUTO_UPDATER' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
outline: { level: [2, 3] },
|
||||
socialLinks: [
|
||||
{
|
||||
icon: {
|
||||
svg: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" height="24" width="24"><title>Gitea</title><path d="M4.209 4.603c-0.247 0 -0.525 0.02 -0.84 0.088 -0.333 0.07 -1.28 0.283 -2.054 1.027C-0.403 7.25 0.035 9.685 0.089 10.052c0.065 0.446 0.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s0.462 1.103 1.168 2.119c0.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212 -0.004 7.212 -0.004s0.458 0.004 1.08 -0.394c0.535 -0.324 1.013 -0.893 1.013 -0.893s0.492 -0.527 1.18 -1.73c0.21 -0.37 0.385 -0.729 0.538 -1.068 0 0 2.107 -4.471 2.107 -8.823 -0.042 -1.318 -0.367 -1.55 -0.443 -1.627 -0.156 -0.156 -0.366 -0.153 -0.366 -0.153s-4.475 0.252 -6.792 0.306c-0.508 0.011 -1.012 0.023 -1.512 0.027v4.474l-0.634 -0.301c0 -1.39 -0.004 -4.17 -0.004 -4.17 -1.107 0.016 -3.405 -0.084 -3.405 -0.084s-5.399 -0.27 -5.987 -0.324c-0.187 -0.011 -0.401 -0.032 -0.648 -0.032zm0.354 1.832h0.111s0.271 2.269 0.6 3.597C5.549 11.147 6.22 13 6.22 13s-0.996 -0.119 -1.641 -0.348c-0.99 -0.324 -1.409 -0.714 -1.409 -0.714s-0.73 -0.511 -1.096 -1.52C1.444 8.73 2.021 7.7 2.021 7.7s0.32 -0.859 1.47 -1.145c0.395 -0.106 0.863 -0.12 1.072 -0.12zm8.33 2.554c0.26 0.003 0.509 0.127 0.509 0.127l0.868 0.422 -0.529 1.075a0.686 0.686 0 0 0 -0.614 0.359 0.685 0.685 0 0 0 0.072 0.756l-0.939 1.924a0.69 0.69 0 0 0 -0.66 0.527 0.687 0.687 0 0 0 0.347 0.763 0.686 0.686 0 0 0 0.867 -0.206 0.688 0.688 0 0 0 -0.069 -0.882l0.916 -1.874a0.667 0.667 0 0 0 0.237 -0.02 0.657 0.657 0 0 0 0.271 -0.137 8.826 8.826 0 0 1 1.016 0.512 0.761 0.761 0 0 1 0.286 0.282c0.073 0.21 -0.073 0.569 -0.073 0.569 -0.087 0.29 -0.702 1.55 -0.702 1.55a0.692 0.692 0 0 0 -0.676 0.477 0.681 0.681 0 1 0 1.157 -0.252c0.073 -0.141 0.141 -0.282 0.214 -0.431 0.19 -0.397 0.515 -1.16 0.515 -1.16 0.035 -0.066 0.218 -0.394 0.103 -0.814 -0.095 -0.435 -0.48 -0.638 -0.48 -0.638 -0.467 -0.301 -1.116 -0.58 -1.116 -0.58s0 -0.156 -0.042 -0.27a0.688 0.688 0 0 0 -0.148 -0.241l0.516 -1.062 2.89 1.401s0.48 0.218 0.583 0.619c0.073 0.282 -0.019 0.534 -0.069 0.657 -0.24 0.587 -2.1 4.317 -2.1 4.317s-0.232 0.554 -0.748 0.588a1.065 1.065 0 0 1 -0.393 -0.045l-0.202 -0.08 -4.31 -2.1s-0.417 -0.218 -0.49 -0.596c-0.083 -0.31 0.104 -0.691 0.104 -0.691l2.073 -4.272s0.183 -0.37 0.466 -0.497a0.855 0.855 0 0 1 0.35 -0.077z" fill="currentColor" stroke-width="1"></path></svg>'
|
||||
{
|
||||
icon: {
|
||||
svg: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" height="24" width="24"><title>Gitea</title><path d="M4.209 4.603c-0.247 0 -0.525 0.02 -0.84 0.088 -0.333 0.07 -1.28 0.283 -2.054 1.027C-0.403 7.25 0.035 9.685 0.089 10.052c0.065 0.446 0.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s0.462 1.103 1.168 2.119c0.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212 -0.004 7.212 -0.004s0.458 0.004 1.08 -0.394c0.535 -0.324 1.013 -0.893 1.013 -0.893s0.492 -0.527 1.18 -1.73c0.21 -0.37 0.385 -0.729 0.538 -1.068 0 0 2.107 -4.471 2.107 -8.823 -0.042 -1.318 -0.367 -1.55 -0.443 -1.627 -0.156 -0.156 -0.366 -0.153 -0.366 -0.153s-4.475 0.252 -6.792 0.306c-0.508 0.011 -1.012 0.023 -1.512 0.027v4.474l-0.634 -0.301c0 -1.39 -0.004 -4.17 -0.004 -4.17 -1.107 0.016 -3.405 -0.084 -3.405 -0.084s-5.399 -0.27 -5.987 -0.324c-0.187 -0.011 -0.401 -0.032 -0.648 -0.032zm0.354 1.832h0.111s0.271 2.269 0.6 3.597C5.549 11.147 6.22 13 6.22 13s-0.996 -0.119 -1.641 -0.348c-0.99 -0.324 -1.409 -0.714 -1.409 -0.714s-0.73 -0.511 -1.096 -1.52C1.444 8.73 2.021 7.7 2.021 7.7s0.32 -0.859 1.47 -1.145c0.395 -0.106 0.863 -0.12 1.072 -0.12zm8.33 2.554c0.26 0.003 0.509 0.127 0.509 0.127l0.868 0.422 -0.529 1.075a0.686 0.686 0 0 0 -0.614 0.359 0.685 0.685 0 0 0 0.072 0.756l-0.939 1.924a0.69 0.69 0 0 0 -0.66 0.527 0.687 0.687 0 0 0 0.347 0.763 0.686 0.686 0 0 0 0.867 -0.206 0.688 0.688 0 0 0 -0.069 -0.882l0.916 -1.874a0.667 0.667 0 0 0 0.237 -0.02 0.657 0.657 0 0 0 0.271 -0.137 8.826 8.826 0 0 1 1.016 0.512 0.761 0.761 0 0 1 0.286 0.282c0.073 0.21 -0.073 0.569 -0.073 0.569 -0.087 0.29 -0.702 1.55 -0.702 1.55a0.692 0.692 0 0 0 -0.676 0.477 0.681 0.681 0 1 0 1.157 -0.252c0.073 -0.141 0.141 -0.282 0.214 -0.431 0.19 -0.397 0.515 -1.16 0.515 -1.16 0.035 -0.066 0.218 -0.394 0.103 -0.814 -0.095 -0.435 -0.48 -0.638 -0.48 -0.638 -0.467 -0.301 -1.116 -0.58 -1.116 -0.58s0 -0.156 -0.042 -0.27a0.688 0.688 0 0 0 -0.148 -0.241l0.516 -1.062 2.89 1.401s0.48 0.218 0.583 0.619c0.073 0.282 -0.019 0.534 -0.069 0.657 -0.24 0.587 -2.1 4.317 -2.1 4.317s-0.232 0.554 -0.748 0.588a1.065 1.065 0 0 1 -0.393 -0.045l-0.202 -0.08 -4.31 -2.1s-0.417 -0.218 -0.49 -0.596c-0.083 -0.31 0.104 -0.691 0.104 -0.691l2.073 -4.272s0.183 -0.37 0.466 -0.497a0.855 0.855 0 0 1 0.35 -0.077z" fill="currentColor" stroke-width="1"></path></svg>',
|
||||
},
|
||||
link: 'https://git.eigen.co.id/eigen/fe-monorepo-template',
|
||||
},
|
||||
link: 'https://git.eigen.co.id/eigen/fe-monorepo-template'
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
|
||||
// Mermaid configuration
|
||||
@@ -79,22 +86,20 @@ const config = withMermaid(
|
||||
// Fix cascading CJS/ESM SyntaxErrors caused by Vite dynamically discovering mermaid
|
||||
vite: {
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'mermaid'
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
include: ['mermaid'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Pnpm strict workspace workaround:
|
||||
// Pnpm strict workspace workaround:
|
||||
// vitepress-plugin-mermaid aggressively injects sub-dependencies into optimizeDeps.include.
|
||||
// Because pnpm uses strict symlinks, Vite fails to resolve these sub-dependencies from the project root,
|
||||
// causing pre-bundling to fail and cascading CJS/ESM SyntaxErrors in the browser.
|
||||
// We strip them out so esbuild can naturally inline them into the 'mermaid' chunk instead.
|
||||
if (config.vite?.optimizeDeps?.include) {
|
||||
config.vite.optimizeDeps.include = config.vite.optimizeDeps.include.filter(
|
||||
(dep) => !['@braintree/sanitize-url', 'debug', 'cytoscape-cose-bilkent', 'cytoscape'].includes(dep)
|
||||
(dep) => !['@braintree/sanitize-url', 'debug', 'cytoscape-cose-bilkent', 'cytoscape'].includes(dep),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"dayjs": "^1.11.19",
|
||||
"events": "^3.3.0",
|
||||
"i18next": "^24.2.2",
|
||||
"lucide-react": "^1.17.0",
|
||||
"lucide-react": "^1.22.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hook-form": "^7.56.4",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { lazy, Suspense, useState } from 'react';
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { ThemeProvider, ColorSchemeType, DensityType } from '@repo/ui/provider';
|
||||
import { NotFound, Forbidden, Maintenance, ComingSoon } from '@repo/ui/components';
|
||||
import { LoadingScreen } from '../core/components/loading-screen';
|
||||
|
||||
const AuthModule = lazy(() => import('./auth'));
|
||||
const AppModule = lazy(() => import('./modules'));
|
||||
@@ -15,7 +16,7 @@ export default function App() {
|
||||
return (
|
||||
<ThemeProvider colorScheme={colorScheme} density={density}>
|
||||
<BrowserRouter>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<Routes>
|
||||
<Route path="/auth/*" element={<AuthModule />} />
|
||||
<Route path="/app/*" element={<AppModule />} />
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function ExamplePage() {
|
||||
return <div className="bg-amber-200">example</div>;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { BaseRemoteDataServices } from '@repo/core-api/data-services';
|
||||
import { FullPageDTO, FullPageEntity } from '../domain/entities';
|
||||
|
||||
/**
|
||||
* Full Page Remote Data Services
|
||||
*
|
||||
* Provides core data services for the full-page module by extending the base remote data services.
|
||||
* While this class automatically handles standard CRUD operations and data transformations out-of-the-box,
|
||||
* implementers can freely extend it by adding custom methods to support domain-specific API endpoints
|
||||
* or complex business logic as needed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* export class FullPageRemoteDataServices extends BaseRemoteDataServices<FullPageEntity, FullPageDTO> {
|
||||
* // Example of adding a custom method tailored to specific module needs
|
||||
* public async getDashboardMetrics(status: string): Promise<MetricsPayload> {
|
||||
* const response = await this.httpClient.get(`${this.apiUrl}/metrics`, {
|
||||
* params: { status }
|
||||
* });
|
||||
* return response.data;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class FullPageRemoteDataServices extends BaseRemoteDataServices<FullPageEntity, FullPageDTO> {}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ModuleConfigEntity } from '@repo/ui/foundations';
|
||||
|
||||
/**
|
||||
* Core configuration and constants for the Full Page module.
|
||||
* Used across Domain, Data, and Presentation layers.
|
||||
*/
|
||||
export const FullPageModuleConfig: ModuleConfigEntity = {
|
||||
/** Unique identifier for permissions, caching, and i18n */
|
||||
moduleKey: 'EXAMPLE_FULL_PAGE',
|
||||
|
||||
/** Translation namespace — must match the namespace used in registerModuleNamespace() */
|
||||
translationNamespace: 'EXAMPLE_FULL_PAGE',
|
||||
|
||||
/** Base API endpoint for remote data services */
|
||||
apiUrl: '/full-page',
|
||||
|
||||
/** Base Web Router URL for UI navigation */
|
||||
webUrl: '/app/full-page',
|
||||
|
||||
/** Architectural category of the module, used for rendering and routing logic */
|
||||
moduleCategory: 'FULL_PAGE',
|
||||
} as const;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './full-page.constants';
|
||||
@@ -0,0 +1,26 @@
|
||||
import { BaseEntity } from '@repo/core-api/data-services';
|
||||
|
||||
/**
|
||||
* Represents a full-page entity in the frontend domain model.
|
||||
*
|
||||
* This entity is derived from the API's `FullPageDTO` via the
|
||||
* {@link FullPageTransformer}, which handles field name mapping
|
||||
* and computed field derivation.
|
||||
*
|
||||
*/
|
||||
export interface FullPageEntity extends BaseEntity {
|
||||
status?: string;
|
||||
name?: string;
|
||||
code?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the raw data structure returned by the API for a full-page resource.
|
||||
*
|
||||
* This DTO uses snake_case field names matching the backend's JSON serialization.
|
||||
* It is transformed into a {@link FullPageEntity} by the {@link FullPageTransformer}.
|
||||
*/
|
||||
export interface FullPageDTO extends FullPageEntity {
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './full-page.entity';
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Full Page Factory
|
||||
*
|
||||
* This module acts as the dependency injection and configuration center for the full-page feature.
|
||||
* It pre-configures and exports singleton instances of the data transformer and data service.
|
||||
* By centralizing the instantiation here, it ensures that all UI components and hooks
|
||||
* within the module share the same API client, configuration, and transformation logic.
|
||||
*/
|
||||
|
||||
import { apiClient } from '../../../../../../core/lib/api-client';
|
||||
import { FullPageRemoteDataServices } from '../../data/full-page.remote.service';
|
||||
import { FullPageModuleConfig } from '../constants/full-page.constants';
|
||||
import { FullPageRemoteDataTransformer } from '../transformers/full-page.remote.transformer';
|
||||
|
||||
/**
|
||||
* Singleton instance of the FullPageRemoteDataTransformer.
|
||||
* Exported for potential direct usage if manual data mapping is required outside the standard API flow.
|
||||
*/
|
||||
export const fullPageDataTransformer = new FullPageRemoteDataTransformer();
|
||||
|
||||
/**
|
||||
* Pre-configured singleton instance of the FullPageRemoteDataServices.
|
||||
* Ready to be consumed by UI components, state managers, or module providers.
|
||||
* It is fully wired with the HTTP client and automatically handles data mapping via the injected transformer.
|
||||
*/
|
||||
export const fullPageDataService = new FullPageRemoteDataServices(apiClient, {
|
||||
apiUrl: FullPageModuleConfig.apiUrl,
|
||||
moduleKey: FullPageModuleConfig.moduleKey,
|
||||
transformer: fullPageDataTransformer,
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { BaseDataTransformer } from '@repo/core-api/data-services';
|
||||
import { FullPageEntity, FullPageDTO } from '../entities';
|
||||
|
||||
/**
|
||||
* Full Page Remote Data Transformer
|
||||
*
|
||||
* Responsible for transforming data between the raw API data transfer objects (DTOs)
|
||||
* and the frontend domain entities for the full-page module. By extending the base transformer,
|
||||
* it ensures strict type safety and decouples data parsing logic from the API service layer.
|
||||
*
|
||||
* Implementers must define the core mapping rules (`transformToEntity`, `transformToDTO`)
|
||||
* and can freely add custom transformation methods for specific API responses.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* export class FullPageRemoteDataTransformer extends BaseDataTransformer<FullPageEntity, FullPageDTO> {
|
||||
* // Map snake_case API payload to camelCase frontend entity
|
||||
* public transformToEntity(dto: FullPageDTO): FullPageEntity {
|
||||
* return {
|
||||
* id: dto.id,
|
||||
* documentNumber: dto.document_number,
|
||||
* createdAt: new Date(dto.created_at),
|
||||
* // ... other property mappings
|
||||
* };
|
||||
* }
|
||||
*
|
||||
* // Map camelCase frontend entity back to snake_case API payload
|
||||
* public transformToDTO(entity: FullPageEntity): FullPageDTO {
|
||||
* return {
|
||||
* id: entity.id,
|
||||
* document_number: entity.documentNumber,
|
||||
* // ... other property mappings
|
||||
* };
|
||||
* }
|
||||
*
|
||||
* // Example of adding a custom transformation method for a specific feature
|
||||
* public transformMetrics(rawData: any): MetricsEntity {
|
||||
* return {
|
||||
* totalActive: rawData.total_active_count ?? 0,
|
||||
* };
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class FullPageRemoteDataTransformer extends BaseDataTransformer<FullPageEntity, FullPageDTO> {}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
import { compose, required, rangeLength } from '@repo/ui/validators';
|
||||
|
||||
/**
|
||||
* Factory function to generate the Zod validation schema for the Full Page module.
|
||||
* It accepts a translation object `t` to maintain domain purity while supporting
|
||||
* dynamic, localized error messages (i18n).
|
||||
*
|
||||
* @param t - The translation object (typically derived from `useTranslation()` in the UI layer).
|
||||
* @returns The configured Zod object schema.
|
||||
*/
|
||||
export const createFullPageSchema = (t: any) => {
|
||||
return z.object({
|
||||
// Code: Required, length between 3 and 10 characters.
|
||||
code: compose(z.string(), required(t.fields.code), rangeLength(3, 10, t.fields.code)),
|
||||
|
||||
// Name: Required, length between 3 and 50 characters.
|
||||
name: compose(z.string(), required(t.fields.name), rangeLength(3, 50, t.fields.name)),
|
||||
|
||||
// Status: Required selection (typically from a dropdown/select).
|
||||
status: compose(z.string(), required(t.fields.status)),
|
||||
|
||||
// Description: Optional text field.
|
||||
description: z.string().optional(),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Data Transfer Object (DTO) for the Full Page form.
|
||||
* This type is automatically inferred from the Zod schema factory.
|
||||
* Use this type as a generic for form initialization, e.g., `useForm<FullPageFormDTO>()`.
|
||||
*/
|
||||
export type FullPageFormDTO = z.infer<ReturnType<typeof createFullPageSchema>>;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { lazy } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { EnterpriseModuleProvider } from '@repo/ui/foundations';
|
||||
import { registerModuleNamespace } from '@repo/core-i18n';
|
||||
import { FullPageModuleConfig } from '../../domain/constants';
|
||||
import { fullPageDataService } from '../../domain/factories';
|
||||
import { FullPageEntity } from '../../domain/entities';
|
||||
|
||||
import fullPageId from '../locales/id/full-page.json';
|
||||
import fullPageEn from '../locales/en/full-page.json';
|
||||
|
||||
const IndexPage = lazy(() => import('../pages/full-page.page.index'));
|
||||
const FormPage = lazy(() => import('../pages/full-page.page.form'));
|
||||
const DetailPage = lazy(() => import('../pages/full-page.page.detail'));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Namespace Registration (Module Scope)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Called once at import time — safe, idempotent, outside React render cycle.
|
||||
// The namespace 'full-page' must match config.translationNamespace.
|
||||
registerModuleNamespace(FullPageModuleConfig.translationNamespace, {
|
||||
id: fullPageId,
|
||||
en: fullPageEn,
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module Factory
|
||||
// ---------------------------------------------------------------------------
|
||||
export default function FullPageModule() {
|
||||
return (
|
||||
<EnterpriseModuleProvider<FullPageEntity> config={FullPageModuleConfig} dataServices={fullPageDataService}>
|
||||
<Routes>
|
||||
<Route path="/index" element={<IndexPage />} />
|
||||
<Route path="/detail/:dataId" element={<DetailPage />} />
|
||||
<Route path="/edit/:dataId" element={<FormPage formPageType="edit" />} />
|
||||
<Route path="/duplicate/:dataId" element={<FormPage formPageType="duplicate" />} />
|
||||
<Route path="/create" element={<FormPage formPageType="create" />} />
|
||||
<Route path="/" element={<Navigate to={`${FullPageModuleConfig.webUrl}/index`} replace={true} />} />
|
||||
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||
</Routes>
|
||||
</EnterpriseModuleProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"title": "Full Page Management",
|
||||
"fields": {
|
||||
"status": "Status",
|
||||
"name": "Name",
|
||||
"code": "Code",
|
||||
"description": "Description"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"title": "Manajemen Halaman Penuh",
|
||||
"fields": {
|
||||
"status": "Status",
|
||||
"name": "Nama",
|
||||
"code": "Kode",
|
||||
"description": "Deskripsi"
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||
|
||||
export default function FullPagePageDetail() {
|
||||
const { t } = useEnterpriseModuleTranslationContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>full-page.page.detail</div>
|
||||
<div>{t('fields.code')}</div>
|
||||
<div>{t('fields.name')}</div>
|
||||
<div>{t('fields.status')}</div>
|
||||
<div>{t('fields.description')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||
|
||||
export default function FullPagePageForm({ formPageType }: { formPageType: 'edit' | 'create' | 'duplicate' }) {
|
||||
const { t } = useEnterpriseModuleTranslationContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>full-page.page.form</div>
|
||||
<div>Form Type: {formPageType}</div>
|
||||
<div>{t('fields.code')}</div>
|
||||
<div>{t('fields.name')}</div>
|
||||
<div>{t('fields.status')}</div>
|
||||
<div>{t('fields.description')}</div>
|
||||
<div>{t('validation:required')}</div>
|
||||
<div>{t('common:save')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { Table, Box, Title, Paper } from '@repo/ui/components';
|
||||
import { PageActions } from '@repo/ui/components';
|
||||
import {
|
||||
useEnterpriseModuleTranslationContext,
|
||||
useEnterpriseModuleNavigationContext,
|
||||
} from '@repo/ui/foundations';
|
||||
import { FullPageEntity } from '../../domain/entities';
|
||||
import { Edit2, Eye, Copy, Trash2 } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
// TODO: Replace this mock data with real API data loaded from DataService via useEnterpriseModuleDataServiceContext
|
||||
const MOCK_DATA: FullPageEntity[] = [
|
||||
{ id: '1', name: 'Dashboard Widget', code: 'WID-001', status: 'ACTIVE', description: 'Main dashboard widget' },
|
||||
{ id: '2', name: 'Report Generator', code: 'REP-002', status: 'INACTIVE', description: 'Generates monthly reports' },
|
||||
{
|
||||
id: '3',
|
||||
name: 'User Management',
|
||||
code: 'USR-003',
|
||||
status: 'ACTIVE',
|
||||
description: 'Manages user roles and permissions',
|
||||
},
|
||||
];
|
||||
|
||||
export default function FullPagePageIndex() {
|
||||
// Translation is scoped to ['full-page', 'common'] — no prefix needed for module keys
|
||||
const { t } = useEnterpriseModuleTranslationContext();
|
||||
|
||||
const { navigateToDetail, navigateToEdit, navigateToDuplicate } = useEnterpriseModuleNavigationContext();
|
||||
|
||||
const getRowActions = useCallback(
|
||||
(row: FullPageEntity) => [
|
||||
{
|
||||
key: 'view',
|
||||
label: t('common:view'),
|
||||
icon: <Eye size={14} />,
|
||||
onClick: () => navigateToDetail(row.id as string),
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
label: t('common:edit'),
|
||||
icon: <Edit2 size={14} />,
|
||||
onClick: () => navigateToEdit(row.id as string),
|
||||
},
|
||||
{
|
||||
key: 'duplicate',
|
||||
label: t('common:duplicate'),
|
||||
icon: <Copy size={14} />,
|
||||
onClick: () => navigateToDuplicate(row.id as string),
|
||||
},
|
||||
{
|
||||
type: 'divider' as const,
|
||||
key: 'div-1',
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: t('common:delete'),
|
||||
icon: <Trash2 size={14} />,
|
||||
intent: 'destructive' as const,
|
||||
onClick: () => {
|
||||
// Mock delete action
|
||||
alert(`Delete ${row.name}`);
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, navigateToDetail, navigateToEdit, navigateToDuplicate],
|
||||
);
|
||||
|
||||
const rows = MOCK_DATA.map((item) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td>{item.code}</Table.Td>
|
||||
<Table.Td>{item.name}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td>{item.description}</Table.Td>
|
||||
<Table.Td>
|
||||
<PageActions actions={getRowActions(item)} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<Box p="md">
|
||||
<Title order={2} mb="md">
|
||||
{t('title')}
|
||||
</Title>
|
||||
|
||||
<Paper withBorder shadow="sm" radius="md">
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t('fields.code')}</Table.Th>
|
||||
<Table.Th>{t('fields.name')}</Table.Th>
|
||||
<Table.Th>{t('fields.status')}</Table.Th>
|
||||
<Table.Th>{t('fields.description')}</Table.Th>
|
||||
<Table.Th w={200}>{t('common:edit')}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import { lazy } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import ModuleLayout from './layouts/module.layout';
|
||||
|
||||
const ExamplePage = lazy(() => import('./example/example.page'));
|
||||
const FullPageModule = lazy(() => import('./example/full-page/presentation/factory'));
|
||||
|
||||
export default function AppModule() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<ExamplePage />} />
|
||||
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||
</Routes>
|
||||
<ModuleLayout>
|
||||
<Routes>
|
||||
<Route path="/full-page/*" element={<FullPageModule />} />
|
||||
<Route path="/" element={<Navigate to="/app/full-page" replace={true} />} />
|
||||
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||
</Routes>
|
||||
</ModuleLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useTranslation } from '@repo/core-i18n';
|
||||
import { Burger, Group, Select, Text, useCoreAppShell } from '@repo/ui/components';
|
||||
import { Globe } from 'lucide-react';
|
||||
import { AppStorageKey, secureStorage } from '../../../../core/storage/local';
|
||||
|
||||
export default function HeaderLayout() {
|
||||
const { i18n } = useTranslation();
|
||||
const { mobileOpened, toggleMobile } = useCoreAppShell();
|
||||
return (
|
||||
<Group h="100%" px="md" justify="space-between">
|
||||
<Group>
|
||||
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
||||
<Text fw={700} size="lg">
|
||||
Mock Header (bg="green.1")
|
||||
</Text>
|
||||
<Select
|
||||
w={180}
|
||||
size="sm"
|
||||
variant="filled"
|
||||
leftSection={<Globe size={16} />}
|
||||
data={[
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'id', label: 'Bahasa Indonesia' },
|
||||
]}
|
||||
value={i18n.resolvedLanguage || i18n.language}
|
||||
onChange={async (val) => {
|
||||
if (!val) return;
|
||||
await i18n.changeLanguage(val);
|
||||
await secureStorage.setItem(AppStorageKey.LOCALE, val);
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { CoreAppShell, CoreAppShellConfig } from '@repo/ui/components';
|
||||
import HeaderLayout from './components/header.layout';
|
||||
|
||||
export default function ModuleLayout({ children }: { children: React.ReactNode }) {
|
||||
const configAppShell: CoreAppShellConfig = {
|
||||
variant: 'header-first',
|
||||
features: {
|
||||
desktopCollapseVariant: 'mini',
|
||||
withUtilityBar: false,
|
||||
withAside: false,
|
||||
withFooter: false,
|
||||
withDoubleSidebar: false,
|
||||
persistState: false,
|
||||
disabled: false,
|
||||
zIndex: 100,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<CoreAppShell config={configAppShell} slots={{ header: <HeaderLayout /> }}>
|
||||
{children}
|
||||
</CoreAppShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Card, Title, Text, Table, Stack, Badge } from '@repo/ui/components';
|
||||
import { PageActions, RowActions, PageAction, RowAction } from '@repo/ui/components';
|
||||
import { Save, Printer, Trash, MoreVertical, Edit, FileText, CheckCircle, Check } from 'lucide-react';
|
||||
|
||||
export default function ActionToolsShowcase() {
|
||||
const pageActions: PageAction[] = [
|
||||
{
|
||||
key: 'save',
|
||||
label: 'Save Changes',
|
||||
icon: <Save size={16} />,
|
||||
onClick: (key) => console.log('Clicked', key),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: 'print',
|
||||
label: 'Print',
|
||||
icon: <Printer size={16} />,
|
||||
children: [
|
||||
{
|
||||
key: 'print-original',
|
||||
label: 'Print Original',
|
||||
icon: <FileText size={16} />,
|
||||
onClick: (k) => console.log(k),
|
||||
},
|
||||
// { type: 'divider' },
|
||||
{ key: 'print-copy', label: 'Print Copy', icon: <FileText size={16} />, onClick: (k) => console.log(k) },
|
||||
{ key: 'print-copy', label: 'Print Copy', icon: <FileText size={16} />, onClick: (k) => console.log(k) },
|
||||
{ key: 'print-copy', label: 'Print Copy', icon: <FileText size={16} />, onClick: (k) => console.log(k) },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'confirm',
|
||||
label: 'Confirm',
|
||||
icon: <CheckCircle size={16} />,
|
||||
onClick: (key) => console.log('Clicked', key),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
icon: <Trash size={16} />,
|
||||
intent: 'destructive',
|
||||
onClick: (key) => console.log('Clicked', key),
|
||||
},
|
||||
|
||||
{
|
||||
key: 'success',
|
||||
label: 'Success',
|
||||
icon: <Check size={16} />,
|
||||
intent: 'success',
|
||||
onClick: (key) => console.log('Clicked', key),
|
||||
},
|
||||
{
|
||||
key: 'warning',
|
||||
label: 'Warning',
|
||||
icon: <Check size={16} />,
|
||||
intent: 'warning',
|
||||
onClick: (key) => console.log('Clicked', key),
|
||||
},
|
||||
{
|
||||
key: 'Primary',
|
||||
label: 'Primary',
|
||||
icon: <Check size={16} />,
|
||||
intent: 'primary',
|
||||
onClick: (key) => console.log('Clicked', key),
|
||||
},
|
||||
];
|
||||
|
||||
const rowActions: RowAction[] = [
|
||||
{
|
||||
key: 'edit',
|
||||
tooltip: 'Edit Record',
|
||||
label: 'Edit Record',
|
||||
icon: <Edit size={16} />,
|
||||
onClick: (key) => console.log('Clicked', key),
|
||||
},
|
||||
{
|
||||
key: 'approve',
|
||||
tooltip: 'Approve',
|
||||
label: 'Approve',
|
||||
icon: <CheckCircle size={16} />,
|
||||
intent: 'success',
|
||||
onClick: (key) => console.log('Clicked', key),
|
||||
},
|
||||
{
|
||||
key: 'more',
|
||||
label: 'More Options',
|
||||
icon: <MoreVertical size={16} />,
|
||||
children: [
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete Record',
|
||||
icon: <Trash size={16} />,
|
||||
intent: 'destructive',
|
||||
onClick: (k) => console.log(k),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const tableData = [
|
||||
{ id: '1', name: 'Invoice #001', status: 'Pending' },
|
||||
{ id: '2', name: 'Invoice #002', status: 'Approved' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Page Actions
|
||||
</Title>
|
||||
<Text c="dimmed" mb="lg">
|
||||
Used in toolbars and page headers.
|
||||
</Text>
|
||||
<PageActions actions={pageActions} onClose={() => {}} />
|
||||
</Card>
|
||||
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Row Actions
|
||||
</Title>
|
||||
<Text c="dimmed" mb="lg">
|
||||
Used inside data grids or list items.
|
||||
</Text>
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>ID</Table.Th>
|
||||
<Table.Th>Name</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th style={{ width: 120 }}>Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{tableData.map((row) => (
|
||||
<Table.Tr key={row.id}>
|
||||
<Table.Td>{row.id}</Table.Td>
|
||||
<Table.Td>{row.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={row.status === 'Approved' ? 'success' : 'warning'}>{row.status}</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<RowActions actions={rowActions} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<RowActions showLabels actions={rowActions} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { lazy } from 'react';
|
||||
import { Layers } from 'lucide-react';
|
||||
|
||||
export const COMPONENTS_REGISTRY = {
|
||||
actionTools: {
|
||||
id: 'action-tools',
|
||||
name: 'Action Tools',
|
||||
description: 'Showcase for PageActions and RowActions components',
|
||||
icon: Layers,
|
||||
component: lazy(() => import('./components/ActionToolsShowcase')),
|
||||
},
|
||||
};
|
||||
@@ -1,38 +1,32 @@
|
||||
import { useState, useMemo, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
Text,
|
||||
Paper,
|
||||
Switch,
|
||||
Select,
|
||||
Burger
|
||||
} from '@repo/ui/components';
|
||||
import {
|
||||
CoreAppShell,
|
||||
CorePageContainer,
|
||||
CoreAppShellConfig,
|
||||
LayoutVariant,
|
||||
import { Box, Button, Group, SegmentedControl, Stack, Text, Paper, Switch, Select, Burger } from '@repo/ui/components';
|
||||
import {
|
||||
CoreAppShell,
|
||||
CorePageContainer,
|
||||
CoreAppShellConfig,
|
||||
LayoutVariant,
|
||||
DesktopCollapseVariant,
|
||||
useCoreAppShell
|
||||
useCoreAppShell,
|
||||
} from '@repo/ui/components';
|
||||
import { Home, BarChart2, Settings as SettingsIcon } from 'lucide-react';
|
||||
|
||||
// Sub-component to test hook methods
|
||||
function LayoutControls() {
|
||||
const { toggleDesktop, toggleMobile, sidebarVariant, setSidebarVariant, toggleAside, toggleNavbarPanel } = useCoreAppShell();
|
||||
const { toggleDesktop, toggleMobile, sidebarVariant, setSidebarVariant, toggleAside, toggleNavbarPanel } =
|
||||
useCoreAppShell();
|
||||
|
||||
return (
|
||||
<Group mb="md">
|
||||
<Button onClick={toggleDesktop} variant="default" size="xs">Toggle Desktop Sidebar</Button>
|
||||
<Button onClick={toggleMobile} variant="default" size="xs" hiddenFrom="sm">Toggle Mobile Sidebar</Button>
|
||||
<Button
|
||||
onClick={() => setSidebarVariant(sidebarVariant === 'expanded' ? 'mini' : 'expanded')}
|
||||
<Button onClick={toggleDesktop} variant="default" size="xs">
|
||||
Toggle Desktop Sidebar
|
||||
</Button>
|
||||
<Button onClick={toggleMobile} variant="default" size="xs" hiddenFrom="sm">
|
||||
Toggle Mobile Sidebar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setSidebarVariant(sidebarVariant === 'expanded' ? 'mini' : 'expanded')}
|
||||
variant="default"
|
||||
size="xs"
|
||||
>
|
||||
@@ -51,10 +45,19 @@ function LayoutControls() {
|
||||
function MockHeader() {
|
||||
const { mobileOpened, toggleMobile } = useCoreAppShell();
|
||||
return (
|
||||
<Group h="100%" px="md" justify="space-between" bg="green.1" c="green.9" style={{ borderBottom: '1px solid var(--mantine-color-green-3)' }}>
|
||||
<Group
|
||||
h="100%"
|
||||
px="md"
|
||||
justify="space-between"
|
||||
bg="green.1"
|
||||
c="green.9"
|
||||
style={{ borderBottom: '1px solid var(--mantine-color-green-3)' }}
|
||||
>
|
||||
<Group>
|
||||
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
||||
<Text fw={700} size="lg">Mock Header (bg="green.1")</Text>
|
||||
<Text fw={700} size="lg">
|
||||
Mock Header (bg="green.1")
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
@@ -63,10 +66,16 @@ function MockHeader() {
|
||||
function MockMobileDrawer() {
|
||||
return (
|
||||
<Box p="md" h="100%" bg="yellow.1" c="yellow.9">
|
||||
<Text fw={700} mb="sm">Mock Mobile Drawer (bg="yellow.1")</Text>
|
||||
<Text fw={700} mb="sm">
|
||||
Mock Mobile Drawer (bg="yellow.1")
|
||||
</Text>
|
||||
<Stack gap="xs">
|
||||
<Button variant="light" color="yellow" justify="flex-start" fullWidth>Mobile Dashboard</Button>
|
||||
<Button variant="light" color="yellow" justify="flex-start" fullWidth>Mobile Settings</Button>
|
||||
<Button variant="light" color="yellow" justify="flex-start" fullWidth>
|
||||
Mobile Dashboard
|
||||
</Button>
|
||||
<Button variant="light" color="yellow" justify="flex-start" fullWidth>
|
||||
Mobile Settings
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
@@ -78,7 +87,9 @@ function SettingRow({ title, description, control }: { title: string; descriptio
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Stack gap={0}>
|
||||
<Text fw={500}>{title}</Text>
|
||||
<Text c="dimmed" size="sm">{description}</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
{description}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Box>{control}</Box>
|
||||
</Group>
|
||||
@@ -104,7 +115,7 @@ export default function ShellDemo() {
|
||||
withAside: withAside ? undefined : false,
|
||||
withFooter: withFooter ? undefined : false,
|
||||
withDoubleSidebar,
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [layoutVariant, collapseVariant, withUtilityBar, withAside, withFooter, withDoubleSidebar]);
|
||||
|
||||
@@ -123,184 +134,213 @@ export default function ShellDemo() {
|
||||
withFooter: withFooter ? undefined : false,
|
||||
withDoubleSidebar,
|
||||
persistState: false, // Don't persist for the demo to avoid confusing other showcases
|
||||
disabled: false, // Allow nested render overrides if needed
|
||||
disabled: false, // Allow nested render overrides if needed
|
||||
zIndex: 100,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<CoreAppShell
|
||||
config={config}
|
||||
|
||||
slots={{
|
||||
utilityBar: (
|
||||
<Group h="100%" px="md" justify="flex-end" bg="blue.1" c="blue.9">
|
||||
<Text size="xs" fw={600}>Mock Utility Bar (bg="blue.1")</Text>
|
||||
</Group>
|
||||
),
|
||||
header: <MockHeader />,
|
||||
sidebarMobile: <MockMobileDrawer />,
|
||||
sidebar: !withDoubleSidebar ? (
|
||||
<Box p="md" h="100%" bg="grape.1" c="grape.9" style={{ borderRight: '1px solid var(--mantine-color-grape-3)' }}>
|
||||
<Text fw={700} mb="sm">Mock Standard Navbar (bg="grape.1")</Text>
|
||||
<Stack gap="xs">
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>Dashboard</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>Users</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>Reports</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>Settings</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
) : undefined,
|
||||
sidebarRail: withDoubleSidebar ? (
|
||||
<Stack align="center" gap="lg" pt="md" h="100%" bg="orange.1" c="orange.9">
|
||||
<Text size="xs" fw={700} style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}>
|
||||
Mock Rail (bg="orange.1")
|
||||
</Text>
|
||||
<Home size={24} />
|
||||
<BarChart2 size={24} />
|
||||
<SettingsIcon size={24} />
|
||||
</Stack>
|
||||
) : undefined,
|
||||
sidebarPanel: withDoubleSidebar ? (
|
||||
<Box p="md" h="100%" bg="grape.1" c="grape.9" style={{ borderRight: '1px solid var(--mantine-color-grape-3)' }}>
|
||||
<Text fw={700} mb="sm">Mock Panel (bg="grape.1")</Text>
|
||||
<Stack gap="xs">
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>Dashboard</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>Users</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>Reports</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>Settings</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
) : undefined,
|
||||
aside: (
|
||||
<Box p="md" h="100%" bg="cyan.1" c="cyan.9">
|
||||
<Text fw={700} mb="md">Mock Aside</Text>
|
||||
<Text size="sm">This area could be used for notifications, help text, or contextual settings.</Text>
|
||||
</Box>
|
||||
),
|
||||
footer: (
|
||||
<Group h="100%" px="md" justify="space-between" bg="gray.1" style={{ borderTop: '1px solid var(--mantine-color-default-border)' }}>
|
||||
<Text size="sm" fw={600}>Mock Footer (bg="gray.1")</Text>
|
||||
</Group>
|
||||
),
|
||||
|
||||
|
||||
}}
|
||||
>
|
||||
<CorePageContainer
|
||||
headerSlot={
|
||||
<Group justify="space-between" align="center">
|
||||
<Text component="h1" size="xl" fw={700} m={0}>
|
||||
Layout Engine Interactive Demo
|
||||
</Text>
|
||||
<Group>
|
||||
<Button component={Link} to="/showcase" leftSection={<ArrowLeft size={16} />} variant="default">
|
||||
Back to Showcase
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
}
|
||||
stickyHeader
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Paper withBorder p="md" bg="var(--mantine-color-gray-0)">
|
||||
<LayoutControls />
|
||||
</Paper>
|
||||
|
||||
<SettingRow
|
||||
title="Layout Variant"
|
||||
description="Switch between standard cloud or SaaS layout styles."
|
||||
control={
|
||||
<Select
|
||||
value={layoutVariant}
|
||||
onChange={(value) => setLayoutVariant((value as LayoutVariant) || 'sidebar-first')}
|
||||
data={[
|
||||
{ label: 'Sidebar First (Alt)', value: 'sidebar-first' },
|
||||
{ label: 'Header First (Default)', value: 'header-first' },
|
||||
{ label: 'Top Nav (Hidden Sidebar)', value: 'top-nav' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
title="Desktop Collapse Strategy"
|
||||
description="Determine if the sidebar shrinks to icons or slides out completely."
|
||||
control={
|
||||
<SegmentedControl
|
||||
value={collapseVariant}
|
||||
onChange={(value) => setCollapseVariant(value as DesktopCollapseVariant)}
|
||||
data={[
|
||||
{ label: 'Hide (Slide Out)', value: 'hide' },
|
||||
{ label: 'Mini (Shrink)', value: 'mini' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
title="Enable Double Sidebar"
|
||||
description="Activate the Google-style rail and contextual panel navigation."
|
||||
control={
|
||||
<Switch
|
||||
checked={withDoubleSidebar}
|
||||
onChange={(event) => setWithDoubleSidebar(event.currentTarget.checked)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
title="Render Utility Bar"
|
||||
description="Show a system-level announcement bar above the main header."
|
||||
control={
|
||||
<Switch
|
||||
checked={withUtilityBar}
|
||||
onChange={(event) => setWithUtilityBar(event.currentTarget.checked)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
title="Render Aside"
|
||||
description="Toggle the right-hand properties or filter panel."
|
||||
control={
|
||||
<Switch
|
||||
checked={withAside}
|
||||
onChange={(event) => setWithAside(event.currentTarget.checked)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
title="Render Footer"
|
||||
description="Toggle the bottom application footer."
|
||||
control={
|
||||
<Switch
|
||||
checked={withFooter}
|
||||
onChange={(event) => setWithFooter(event.currentTarget.checked)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Box mt="xl">
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Text fw={700} size="lg">Configuration Preview</Text>
|
||||
<Button
|
||||
variant={copied ? 'filled' : 'light'}
|
||||
color={copied ? 'teal' : 'blue'}
|
||||
size="xs"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{copied ? 'Copied to Clipboard!' : 'Copy JSON'}
|
||||
</Button>
|
||||
</Group>
|
||||
<Paper withBorder p="md" bg="dark.8" c="gray.0" style={{ fontFamily: 'monospace', overflowX: 'auto' }}>
|
||||
<pre style={{ margin: 0 }}>{JSON.stringify(liveConfig, null, 2)}</pre>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
slots={{
|
||||
utilityBar: (
|
||||
<Group h="100%" px="md" justify="flex-end" bg="blue.1" c="blue.9">
|
||||
<Text size="xs" fw={600}>
|
||||
Mock Utility Bar (bg="blue.1")
|
||||
</Text>
|
||||
</Group>
|
||||
),
|
||||
header: <MockHeader />,
|
||||
sidebarMobile: <MockMobileDrawer />,
|
||||
sidebar: !withDoubleSidebar ? (
|
||||
<Box
|
||||
p="md"
|
||||
h="100%"
|
||||
bg="grape.1"
|
||||
c="grape.9"
|
||||
style={{ borderRight: '1px solid var(--mantine-color-grape-3)' }}
|
||||
>
|
||||
<Text fw={700} mb="sm">
|
||||
Mock Standard Navbar (bg="grape.1")
|
||||
</Text>
|
||||
<Stack gap="xs">
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>
|
||||
Dashboard
|
||||
</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>
|
||||
Users
|
||||
</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>
|
||||
Reports
|
||||
</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>
|
||||
Settings
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
) : undefined,
|
||||
sidebarRail: withDoubleSidebar ? (
|
||||
<Stack align="center" gap="lg" pt="md" h="100%" bg="orange.1" c="orange.9">
|
||||
<Text size="xs" fw={700} style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}>
|
||||
Mock Rail (bg="orange.1")
|
||||
</Text>
|
||||
<Home size={24} />
|
||||
<BarChart2 size={24} />
|
||||
<SettingsIcon size={24} />
|
||||
</Stack>
|
||||
</CorePageContainer>
|
||||
</CoreAppShell>
|
||||
) : undefined,
|
||||
sidebarPanel: withDoubleSidebar ? (
|
||||
<Box
|
||||
p="md"
|
||||
h="100%"
|
||||
bg="grape.1"
|
||||
c="grape.9"
|
||||
style={{ borderRight: '1px solid var(--mantine-color-grape-3)' }}
|
||||
>
|
||||
<Text fw={700} mb="sm">
|
||||
Mock Panel (bg="grape.1")
|
||||
</Text>
|
||||
<Stack gap="xs">
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>
|
||||
Dashboard
|
||||
</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>
|
||||
Users
|
||||
</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>
|
||||
Reports
|
||||
</Button>
|
||||
<Button variant="light" color="grape" justify="flex-start" fullWidth>
|
||||
Settings
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
) : undefined,
|
||||
aside: (
|
||||
<Box p="md" h="100%" bg="cyan.1" c="cyan.9">
|
||||
<Text fw={700} mb="md">
|
||||
Mock Aside
|
||||
</Text>
|
||||
<Text size="sm">This area could be used for notifications, help text, or contextual settings.</Text>
|
||||
</Box>
|
||||
),
|
||||
footer: (
|
||||
<Group
|
||||
h="100%"
|
||||
px="md"
|
||||
justify="space-between"
|
||||
bg="gray.1"
|
||||
style={{ borderTop: '1px solid var(--mantine-color-default-border)' }}
|
||||
>
|
||||
<Text size="sm" fw={600}>
|
||||
Mock Footer (bg="gray.1")
|
||||
</Text>
|
||||
</Group>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<CorePageContainer
|
||||
headerSlot={
|
||||
<Group justify="space-between" align="center">
|
||||
<Text component="h1" size="xl" fw={700} m={0}>
|
||||
Layout Engine Interactive Demo
|
||||
</Text>
|
||||
<Group>
|
||||
<Button component={Link} to="/showcase" leftSection={<ArrowLeft size={16} />} variant="default">
|
||||
Back to Showcase
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
}
|
||||
stickyHeader
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Paper withBorder p="md" bg="var(--mantine-color-gray-0)">
|
||||
<LayoutControls />
|
||||
</Paper>
|
||||
|
||||
<SettingRow
|
||||
title="Layout Variant"
|
||||
description="Switch between standard cloud or SaaS layout styles."
|
||||
control={
|
||||
<Select
|
||||
value={layoutVariant}
|
||||
onChange={(value) => setLayoutVariant((value as LayoutVariant) || 'sidebar-first')}
|
||||
data={[
|
||||
{ label: 'Sidebar First (Alt)', value: 'sidebar-first' },
|
||||
{ label: 'Header First (Default)', value: 'header-first' },
|
||||
{ label: 'Top Nav (Hidden Sidebar)', value: 'top-nav' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
title="Desktop Collapse Strategy"
|
||||
description="Determine if the sidebar shrinks to icons or slides out completely."
|
||||
control={
|
||||
<SegmentedControl
|
||||
value={collapseVariant}
|
||||
onChange={(value) => setCollapseVariant(value as DesktopCollapseVariant)}
|
||||
data={[
|
||||
{ label: 'Hide (Slide Out)', value: 'hide' },
|
||||
{ label: 'Mini (Shrink)', value: 'mini' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
title="Enable Double Sidebar"
|
||||
description="Activate the Google-style rail and contextual panel navigation."
|
||||
control={
|
||||
<Switch
|
||||
checked={withDoubleSidebar}
|
||||
onChange={(event) => setWithDoubleSidebar(event.currentTarget.checked)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
title="Render Utility Bar"
|
||||
description="Show a system-level announcement bar above the main header."
|
||||
control={
|
||||
<Switch checked={withUtilityBar} onChange={(event) => setWithUtilityBar(event.currentTarget.checked)} />
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
title="Render Aside"
|
||||
description="Toggle the right-hand properties or filter panel."
|
||||
control={<Switch checked={withAside} onChange={(event) => setWithAside(event.currentTarget.checked)} />}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
title="Render Footer"
|
||||
description="Toggle the bottom application footer."
|
||||
control={<Switch checked={withFooter} onChange={(event) => setWithFooter(event.currentTarget.checked)} />}
|
||||
/>
|
||||
|
||||
<Box mt="xl">
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Text fw={700} size="lg">
|
||||
Configuration Preview
|
||||
</Text>
|
||||
<Button
|
||||
variant={copied ? 'filled' : 'light'}
|
||||
color={copied ? 'teal' : 'blue'}
|
||||
size="xs"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{copied ? 'Copied to Clipboard!' : 'Copy JSON'}
|
||||
</Button>
|
||||
</Group>
|
||||
<Paper withBorder p="md" bg="dark.8" c="gray.0" style={{ fontFamily: 'monospace', overflowX: 'auto' }}>
|
||||
<pre style={{ margin: 0 }}>{JSON.stringify(liveConfig, null, 2)}</pre>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
</CorePageContainer>
|
||||
</CoreAppShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, Suspense } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ColorSchemeType, DensityType } from '@repo/ui/provider';
|
||||
import {
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
Box,
|
||||
Paper,
|
||||
} from '@repo/ui/components';
|
||||
import { ShieldCheck, Database, Lock, Layout, Activity, Printer, FileText } from 'lucide-react';
|
||||
import { ShieldCheck, Database, Lock, Layout, Activity, Printer, FileText, LayoutDashboard } from 'lucide-react';
|
||||
import { Globe } from 'lucide-react';
|
||||
import { useTranslation } from '@repo/core-i18n';
|
||||
import PrinterList from './printer-list';
|
||||
@@ -31,6 +31,7 @@ import ExamplePage from './example/example.page';
|
||||
import EventsDemoPage from './events-demo';
|
||||
import PouchSample from './pouch-sample';
|
||||
import FormDemoView from './example/features/form-demo';
|
||||
import { COMPONENTS_REGISTRY } from './registry';
|
||||
|
||||
interface ShowcaseViewProps {
|
||||
colorScheme: ColorSchemeType;
|
||||
@@ -52,6 +53,11 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
|
||||
];
|
||||
|
||||
const getSubtitle = () => {
|
||||
const registryItem = Object.values(COMPONENTS_REGISTRY).find((item) => item.id === activeTab);
|
||||
if (registryItem) {
|
||||
return registryItem.description;
|
||||
}
|
||||
|
||||
switch (activeTab) {
|
||||
case 'rbac':
|
||||
return 'Role-Based Access Control and Permissions';
|
||||
@@ -83,6 +89,8 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
|
||||
onChange={(val) => {
|
||||
if (val === 'layout-engine') {
|
||||
navigate('/shell-demo');
|
||||
} else if (val === 'apps') {
|
||||
navigate('/app');
|
||||
} else {
|
||||
setActiveTab(val);
|
||||
}
|
||||
@@ -137,6 +145,18 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
|
||||
<Tabs.Tab value="hardware" leftSection={<Printer size={18} />}>
|
||||
Hardware
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="apps" leftSection={<LayoutDashboard size={18} />}>
|
||||
Apps
|
||||
</Tabs.Tab>
|
||||
|
||||
{Object.values(COMPONENTS_REGISTRY).map((registryItem) => {
|
||||
const Icon = registryItem.icon;
|
||||
return (
|
||||
<Tabs.Tab key={registryItem.id} value={registryItem.id} leftSection={<Icon size={18} />}>
|
||||
{registryItem.name}
|
||||
</Tabs.Tab>
|
||||
);
|
||||
})}
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value={activeTab as string}>
|
||||
@@ -374,6 +394,20 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- DYNAMIC COMPONENTS FROM REGISTRY --- */}
|
||||
{Object.values(COMPONENTS_REGISTRY).map((registryItem) => {
|
||||
const Component = registryItem.component;
|
||||
return (
|
||||
activeTab === registryItem.id && (
|
||||
<Stack gap="xl" key={registryItem.id}>
|
||||
<Suspense fallback={<Text>Loading {registryItem.name}...</Text>}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Blok Utama (Brand Color) -->
|
||||
<rect x="6" y="6" width="16" height="16" rx="4" fill="#652ed9"/>
|
||||
<!-- Blok Sekunder (Memberikan kesan struktur & kedalaman) -->
|
||||
<rect x="18" y="18" width="16" height="16" rx="4" fill="#652ed9" fill-opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
@@ -0,0 +1,11 @@
|
||||
import { Loader, Stack } from '@repo/ui/components';
|
||||
|
||||
export const LoadingScreen = () => {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex h-screen w-screen items-center justify-center bg-white">
|
||||
<Stack align="center" gap="sm">
|
||||
<Loader color="blue" size="lg" type="dots" />
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -24,8 +24,12 @@ async function bootstrap() {
|
||||
// Initialize i18next and load language from secureStorage
|
||||
await setupI18n({
|
||||
storageAdapter: {
|
||||
getLanguage: async () => await secureStorage.getItem<string>(AppStorageKey.LOCALE),
|
||||
setLanguage: async (lng: string) => await secureStorage.setItem(AppStorageKey.LOCALE, lng),
|
||||
getLanguage: async () => {
|
||||
return secureStorage.getItem<string>(AppStorageKey.LOCALE);
|
||||
},
|
||||
setLanguage: async (lng: string) => {
|
||||
await secureStorage.setItem(AppStorageKey.LOCALE, lng);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
import 'react-i18next';
|
||||
// Import the core types so we don't break the common namespace
|
||||
import type { resources as coreResources } from '@repo/core-i18n/src/setup';
|
||||
import bookingEn from '../apps/modules/example/features/i18n/locales/en/booking.json';
|
||||
|
||||
// Combine core resources with app-specific decentralized resources
|
||||
declare module 'react-i18next' {
|
||||
interface CustomTypeOptions {
|
||||
defaultNS: 'common';
|
||||
resources: typeof coreResources['en'] & {
|
||||
booking: typeof bookingEn;
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user