docs: Update frontend routing & serving with KBX Foundation v4 operational navigation
- Replace generic frontend structure with registry-driven architecture - Add KBX Contracts (@kbx/contracts) formal screen definitions - Integrate design token system (compact, comfortable, touch density) - Define UI adapter mandatory boundary (PrimeVue/AG Grid) - Document app initialization lifecycle (installKbx.ts) - Add screen component structure (ScreenHeader, QueryStateBoundary, AgGridShell) - Implement permission enforcement (registry-driven RBAC) - Add help system integration (registry context) - Include contract enforcement CI/CD gate - Update state management rules (registry-linked) - Add route registration flow (registry → router build) - Document serving architecture (component contracts) Reference: docs/Design/kbx-foundation-v52-fe-operational-navigation-screen-anatomy AGENTS.md v16.0: Simplicity (registry single source of truth), Necessity (formal contracts), Traceability (registry linking) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -294,58 +294,238 @@ Features/<SliceName>/
|
||||
- **Evidence & Audit:** Update/delete are blocked; new state appended as new revision.
|
||||
- **Migrations:** `src/KArtSell.DbMigrator` uses DbUp; file naming: `NNNN_description.sql`. Each module has ordered, checksummed migrations.
|
||||
|
||||
### Frontend: Vue 3 + Vite + Modular Feature Structure
|
||||
### Frontend: Vue 3 + Vite + KBX Foundation v4 (Operational Navigation)
|
||||
|
||||
#### Directory Layout
|
||||
#### Directory Layout (Registry-Driven)
|
||||
```
|
||||
frontend/src/
|
||||
app/ # Core app initialization, routing, config
|
||||
features/ # Feature modules (one per business capability)
|
||||
app/
|
||||
router.ts # Vue Router setup (page-level only)
|
||||
installKbx.ts # KBX system initialization (registry, contracts, permissions)
|
||||
features/
|
||||
<feature>/
|
||||
components/ # Scoped to this feature
|
||||
pages/ # Route-level pages
|
||||
stores/ # Pinia stores (state management)
|
||||
composables/ # Reusable logic (Vue 3 hooks)
|
||||
types/ # TS interfaces for this feature
|
||||
routes.ts # Feature route definitions (lazy-loaded)
|
||||
registry.ts # Screen registry entry (@kbx/contracts.ScreenDefinition)
|
||||
pages/
|
||||
<Screen>.vue # Page component (matches registry.screenId)
|
||||
components/ # Feature-scoped components (not shared)
|
||||
stores/ # Pinia stores (feature state)
|
||||
composables/ # Reusable hooks (feature logic)
|
||||
types/ # TS interfaces for this feature
|
||||
shared/
|
||||
ui/
|
||||
adapter/ # PrimeVue/AG Grid wrappers (mandatory boundary)
|
||||
components/ # Common components (QueryStateBoundary, PermissionGuard, CrudForm, etc.)
|
||||
layouts/ # Page layout templates
|
||||
crud/ # Generic CRUD form logic
|
||||
composables/ # Global composables (useFetch, useAuth, etc.)
|
||||
types/ # Global types, contracts
|
||||
stores/ # Global Pinia stores (auth, user, preferences)
|
||||
design-system/ # Design tokens, typography, color scales (PrimeVue theme overrides)
|
||||
adapter/ # MANDATORY boundary: PrimeVue/AG Grid wrappers
|
||||
PrimeVueAdapter.ts # <Button>, <Input>, <Dialog> → framework-agnostic
|
||||
AgGridAdapter.ts # AG Grid config, theming, row models
|
||||
components/ # Cross-feature components (shared contracts)
|
||||
QueryStateBoundary.vue # (loading/error/empty)
|
||||
PermissionGuard.vue # RBAC enforcement via registry
|
||||
KbxHelpPanel.vue # Help system (registry-driven)
|
||||
KbxStatus.vue # Status display (contract-based)
|
||||
layouts/ # Page layout templates (header, sidebar, footer)
|
||||
tokens/ # Design tokens (compact, comfortable, touch density)
|
||||
composables/
|
||||
useKbxValidation.ts # Zod + vee-validate integration
|
||||
useKbxDirtyState.ts # Form unsaved changes detection
|
||||
useKbxPermission.ts # Permission context + registry
|
||||
types/
|
||||
contracts.ts # @kbx/contracts re-exports
|
||||
permission.ts # Permission context, RBAC decision rules
|
||||
stores/
|
||||
authStore.ts # Session, role, user (global Pinia)
|
||||
registryStore.ts # Screen registry cache (UI, help, permissions)
|
||||
registry/ # Central screen definition registry
|
||||
index.ts # Import all feature registries, export merged ScreenRegistry
|
||||
ui-context.ts # UI adapter context provider
|
||||
design-system/ # Design tokens (NOT arbitrary page CSS)
|
||||
tokens.css # CSS custom properties (34px, 44px, 52px, etc.)
|
||||
density/ # compact, comfortable, touch variants
|
||||
```
|
||||
|
||||
#### State Management Rules
|
||||
| State | Owner | Tool |
|
||||
|-------|-------|------|
|
||||
| API responses, cache, stale, retry | TanStack Query | @tanstack/vue-query |
|
||||
| Session, role, UI preferences | Global store | Pinia |
|
||||
| Form values, errors, touched | Form library | vee-validate + Zod |
|
||||
| URL filters, pagination, sorting | Router | vue-router query/params |
|
||||
| Large data tables, virtual scroll | Server-side row model | AG Grid server mode |
|
||||
#### KBX Contracts (@kbx/contracts)
|
||||
All screens implement a formal contract:
|
||||
|
||||
```typescript
|
||||
// ScreenDefinition (required in all feature registries)
|
||||
export interface ScreenDefinition {
|
||||
screenId: string // e.g., "oms.orders.list"
|
||||
title: string // Display name (localized)
|
||||
module: "OMS" | "WMS" | "ERP" // Functional area
|
||||
path: string // Vue Router path
|
||||
component: () => Promise<any> // Lazy-loaded page component
|
||||
permissions: string[] // Required roles (e.g., ["order.view"])
|
||||
help?: HelpDefinition // Contextual help (registry-driven)
|
||||
grid?: GridDefinition // AG Grid config (shared theme)
|
||||
shortcut?: string // Keyboard shortcut (help searchable)
|
||||
}
|
||||
|
||||
// PermissionDefinition (centralized RBAC)
|
||||
export interface PermissionDefinition {
|
||||
permissionId: string // e.g., "order.create"
|
||||
label: string // Human-readable (for audit/help)
|
||||
screens: string[] // Which screens require this permission
|
||||
forms: string[] // Which forms check this permission
|
||||
}
|
||||
|
||||
// HelpDefinition (context-aware, registry-indexed)
|
||||
export interface HelpDefinition {
|
||||
title: string // Panel title (screen context)
|
||||
sections: HelpSection[]
|
||||
relatedScreens: string[] // Cross-screen navigation
|
||||
externalUrl?: string // Knowledge base link
|
||||
}
|
||||
```
|
||||
|
||||
#### App Initialization (@kbx Lifecycle)
|
||||
`frontend/src/app/installKbx.ts`:
|
||||
```typescript
|
||||
// 1. Load screen registry (all feature registries merged)
|
||||
const registry = await loadScreenRegistry()
|
||||
|
||||
// 2. Install permission context (RBAC decision engine)
|
||||
app.use(createPermissionContext(registry))
|
||||
|
||||
// 3. Install router with lazy-loaded pages
|
||||
const router = createRouter({
|
||||
routes: buildRouterFromRegistry(registry) // Page routes only
|
||||
})
|
||||
|
||||
// 4. Install KBX global components (adapter-wrapped UI)
|
||||
app.use(KbxUiPlugin)
|
||||
|
||||
// 5. Populate stores (registry cache for help, permissions, status)
|
||||
useRegistryStore().setRegistry(registry)
|
||||
```
|
||||
|
||||
#### UI Adapter Pattern (Mandatory Boundary)
|
||||
`packages/kbx-ui/src/adapter/` isolates UI framework:
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T: Use PrimeVue directly in screens
|
||||
<PButton label="Save" @click="save" />
|
||||
|
||||
// ✅ DO: Use KBX adapter (framework-agnostic)
|
||||
<KbxButton label="Save" @click="save" />
|
||||
|
||||
// Adapter handles:
|
||||
// - Theme switching (dark/light/system)
|
||||
// - Density token application (compact/comfortable/touch)
|
||||
// - Accessibility (ARIA, focus management)
|
||||
// - Keyboard shortcuts (Ctrl+S, etc.)
|
||||
```
|
||||
|
||||
#### State Management (Registry-Driven, Contract-Based)
|
||||
| State | Owner | Tool | Registry Link |
|
||||
|-------|-------|------|---|
|
||||
| API responses, cache, stale, retry | TanStack Query | @tanstack/vue-query | → API contracts (OpenAPI) |
|
||||
| Session, role, UI preferences | Global Pinia | `authStore`, `registryStore` | → PermissionDefinition |
|
||||
| Form values, errors, touched | Form library | vee-validate + Zod schema | → Screen.forms contract |
|
||||
| URL filters, pagination, sorting | Router | vue-router query/params | → ScreenDefinition.grid |
|
||||
| Large data tables, virtual scroll | Server-side row model | AG Grid server mode (adapter) | → GridDefinition contract |
|
||||
|
||||
**Anti-patterns:**
|
||||
- Do NOT duplicate API responses in Pinia.
|
||||
- Do NOT write 401/409/422/429/503 error handling in every screen.
|
||||
- Do NOT manage query cache manually; let TanStack Query handle it.
|
||||
- ❌ Do NOT duplicate API responses in Pinia (use TanStack Query cache).
|
||||
- ❌ Do NOT write 401/409/422/429/503 error handling in every screen (use ErrorBoundary + QueryStateBoundary).
|
||||
- ❌ Do NOT manage query cache manually.
|
||||
- ❌ Do NOT define routes outside registry (route table is generated from registry).
|
||||
- ❌ Do NOT bypass PermissionGuard for conditional rendering (use registry-driven rendering).
|
||||
|
||||
#### Screen Component Structure (Registry-Aligned)
|
||||
Every screen must implement `ScreenDefinition`:
|
||||
|
||||
```vue
|
||||
<!-- features/orders/pages/OrdersList.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header: registry-driven title, help, export -->
|
||||
<ScreenHeader :screenId="screenId" />
|
||||
|
||||
<!-- Content: data grid with server-side row model -->
|
||||
<QueryStateBoundary :query="ordersQuery">
|
||||
<AgGridShell
|
||||
:gridOptions="gridConfig"
|
||||
:rows="ordersQuery.data"
|
||||
:loading="ordersQuery.isPending"
|
||||
/>
|
||||
</QueryStateBoundary>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// Registry access (read-only, cached)
|
||||
const registry = useRegistry()
|
||||
const screenDef = registry.screens.get('oms.orders.list')
|
||||
const screenId = screenDef.screenId
|
||||
|
||||
// Permission check (registry-driven)
|
||||
const can = usePermission()
|
||||
const canCreate = can('order.create') // Registry permission ID
|
||||
|
||||
// Data fetching (TanStack Query, no Pinia duplication)
|
||||
const ordersQuery = useQuery({
|
||||
queryKey: ['orders', filters],
|
||||
queryFn: () => api.orders.list(filters)
|
||||
})
|
||||
|
||||
// Grid config (adapter-wrapped, density-aware)
|
||||
const gridConfig = computed(() => ({
|
||||
columnDefs: screenDef.grid.columnDefs,
|
||||
rowHeight: tokens.gridRowHeight, // 34px (compact) or 36px (comfortable)
|
||||
...defaultGridOptions
|
||||
}))
|
||||
</script>
|
||||
```
|
||||
|
||||
#### Screen Registry Entry (features/<feature>/registry.ts)
|
||||
```typescript
|
||||
export const ordersListScreen: ScreenDefinition = {
|
||||
screenId: "oms.orders.list",
|
||||
title: "Orders",
|
||||
module: "OMS",
|
||||
path: "/oms/orders",
|
||||
component: () => import("./pages/OrdersList.vue"),
|
||||
permissions: ["order.view"],
|
||||
|
||||
help: {
|
||||
title: "Order Search & Management",
|
||||
sections: [
|
||||
{
|
||||
title: "How to search",
|
||||
content: "Use filters at the top to search by date, customer, or status"
|
||||
}
|
||||
],
|
||||
relatedScreens: ["oms.orders.detail", "oms.orders.register"]
|
||||
},
|
||||
|
||||
grid: {
|
||||
columnDefs: [
|
||||
{ field: "orderId", headerName: "Order ID", width: 120 },
|
||||
{ field: "customerName", headerName: "Customer", width: 200 }
|
||||
],
|
||||
rowHeight: "auto", // adapter applies density token
|
||||
serverSideDatasource: true
|
||||
},
|
||||
|
||||
shortcut: "Ctrl+Shift+O"
|
||||
}
|
||||
|
||||
export default [ordersListScreen]
|
||||
```
|
||||
|
||||
#### Component Elevation Criteria
|
||||
Promote to `shared/ui/components/` only when:
|
||||
1. **Same business meaning & permissions** (not just visual similarity).
|
||||
1. **Same business meaning & permissions** (check registry.screens[].permissions).
|
||||
2. **Repeated state/error handling logic** across 3+ consumers.
|
||||
3. **Accessibility & testing** already fully implemented.
|
||||
3. **Accessibility & testing** fully implemented.
|
||||
4. **Contract-driven** (implements @kbx/contracts interface).
|
||||
|
||||
**Always-shared components:**
|
||||
- `QueryStateBoundary` (loading/error/empty states)
|
||||
- `PermissionGuard` (RBAC enforcement)
|
||||
- `CrudForm` (standard CRUD form)
|
||||
- `VersionConflictDialog` (optimistic concurrency)
|
||||
- `DataFreshnessBadge` (cache/stale indicators)
|
||||
- `DataGridShell` (AG Grid wrapper with sorting, filtering, export)
|
||||
**Always-shared components (KBX system):**
|
||||
- `QueryStateBoundary` (loading/error/empty, registry context-aware)
|
||||
- `PermissionGuard` (RBAC via registry.permissions)
|
||||
- `ScreenHeader` (title, help trigger, export buttons from registry)
|
||||
- `AgGridShell` (AG Grid adapter with density tokens)
|
||||
- `KbxStatus` (status display per StatusDefinition contract)
|
||||
- `KbxHelpPanel` (registry-driven help, contextual)
|
||||
|
||||
### Database & Migrations
|
||||
|
||||
@@ -404,6 +584,218 @@ Jobs do not call other jobs directly; instead, they emit events or check readine
|
||||
|
||||
Used for live notifications (model activation events, approval notifications). Follows Hub/Group pattern with correlation to `CorrelationId` for traceability.
|
||||
|
||||
### Frontend Routing & Serving Architecture (KBX Foundation v4)
|
||||
|
||||
**Key Principle:** Routing is registry-driven; screen definitions are the single source of truth for UI structure, permissions, help, and grid configuration.
|
||||
|
||||
#### Route Registration Flow
|
||||
1. **Feature Registry** (`features/<feature>/registry.ts`): Define ScreenDefinition(s)
|
||||
2. **Central Registry** (`frontend/src/registry/index.ts`): Import and merge all feature registries
|
||||
3. **Router Build** (`app/installKbx.ts`): Generate Vue Router routes from registry
|
||||
4. **Page-Level Routes Only**: No nested routing; each screen is a top-level route
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T: Define routes in app/router.ts
|
||||
const routes = [
|
||||
{ path: '/orders/list', component: OrdersList }, // WRONG: duplicates registry
|
||||
{ path: '/orders/:id', component: OrderDetail }
|
||||
]
|
||||
|
||||
// ✅ DO: Registry-driven routes
|
||||
export const ordersRegistry: ScreenDefinition[] = [
|
||||
{
|
||||
screenId: "oms.orders.list",
|
||||
path: "/oms/orders",
|
||||
component: () => import("./pages/OrdersList.vue"),
|
||||
permissions: ["order.view"]
|
||||
},
|
||||
{
|
||||
screenId: "oms.orders.detail",
|
||||
path: "/oms/orders/:id",
|
||||
component: () => import("./pages/OrderDetail.vue"),
|
||||
permissions: ["order.view"]
|
||||
}
|
||||
]
|
||||
|
||||
// Router is built from registry:
|
||||
const routes = buildRouterFromRegistry(mergedRegistry)
|
||||
```
|
||||
|
||||
#### Screen Serving (Component Contracts)
|
||||
|
||||
Each screen component serves data and UI according to its ScreenDefinition contract:
|
||||
|
||||
```vue
|
||||
<!-- ✅ DO: Implement contract -->
|
||||
<template>
|
||||
<div class="screen-container">
|
||||
<!-- Header (registry-driven: title, help, actions) -->
|
||||
<ScreenHeader :screenId="screenDef.screenId" />
|
||||
|
||||
<!-- Content (state management per contract) -->
|
||||
<QueryStateBoundary :query="dataQuery">
|
||||
<AgGridShell
|
||||
v-if="screenDef.grid"
|
||||
:gridOptions="gridConfig"
|
||||
:rows="dataQuery.data.items"
|
||||
/>
|
||||
</QueryStateBoundary>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRegistry } from '@shared/composables/useRegistry'
|
||||
import { usePermission } from '@shared/composables/usePermission'
|
||||
|
||||
const route = useRoute()
|
||||
const registry = useRegistry()
|
||||
|
||||
// Screen definition (immutable, from registry cache)
|
||||
const screenDef = computed(() =>
|
||||
registry.screens.get('oms.orders.list')
|
||||
)
|
||||
|
||||
// Permission checks (registry-driven)
|
||||
const permissions = usePermission()
|
||||
const canCreate = computed(() => permissions.has('order.create'))
|
||||
const canExport = computed(() => permissions.has('order.export'))
|
||||
|
||||
// Data fetching (TanStack Query, no Pinia cache duplication)
|
||||
const filters = ref({
|
||||
status: route.query.status || 'all',
|
||||
page: parseInt(route.query.page) || 1
|
||||
})
|
||||
|
||||
const dataQuery = useQuery({
|
||||
queryKey: ['orders', filters.value],
|
||||
queryFn: () => api.orders.search(filters.value),
|
||||
staleTime: 60_000
|
||||
})
|
||||
|
||||
// Grid configuration (adapter-wrapped, density-aware)
|
||||
const gridConfig = computed(() => ({
|
||||
...screenDef.value?.grid,
|
||||
rowHeight: useDesignToken('gridRowHeight'), // 34px, 36px, or 52px
|
||||
theme: useTheme().value // 'light', 'dark', 'highContrast'
|
||||
}))
|
||||
|
||||
// Actions (registry-driven help/shortcuts)
|
||||
const openHelp = () => {
|
||||
useHelpPanel().open(screenDef.value.screenId)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### UI Adapter Boundary (PrimeVue + AG Grid)
|
||||
|
||||
All UI framework usage must go through `@kbx/ui/adapter`:
|
||||
|
||||
```typescript
|
||||
// Location: packages/kbx-ui/src/adapter/
|
||||
|
||||
// ✅ Adapter pattern (framework-agnostic)
|
||||
export const KbxButton = defineComponent({
|
||||
props: { label: String, disabled: Boolean, onClick: Function },
|
||||
setup(props, { slots }) {
|
||||
return () => (
|
||||
<PButton
|
||||
label={props.label}
|
||||
disabled={props.disabled}
|
||||
onClick={() => props.onClick?.()}
|
||||
class={['kbx-button', useDesignToken('density')]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// ✅ Grid adapter (AG Grid theme + tokens)
|
||||
export const useGridTheme = () => ({
|
||||
rowHeight: useDesignToken('gridRowHeight'),
|
||||
headerHeight: 36,
|
||||
theme: `ag-theme-${useTheme().value}`,
|
||||
fontSize: useDesignToken('fontSize.grid'),
|
||||
// ... density tokens applied
|
||||
})
|
||||
|
||||
// ❌ DON'T: Use PrimeVue directly in screens
|
||||
// import { Button } from 'primevue/button' // WRONG
|
||||
```
|
||||
|
||||
#### Design Token Density (Registry Config)
|
||||
|
||||
Screen density (compact/comfortable/touch) is applied globally via tokens, NOT per-screen CSS:
|
||||
|
||||
```css
|
||||
/* ✅ DO: Define tokens, let screens inherit */
|
||||
:root {
|
||||
--kbx-density: compact; /* or 'comfortable', 'touch' */
|
||||
|
||||
--kbx-input-height: 34px; /* density: compact */
|
||||
--kbx-grid-row-height: 34px;
|
||||
--kbx-touch-target: 44px;
|
||||
}
|
||||
|
||||
:root[data-density="comfortable"] {
|
||||
--kbx-input-height: 36px;
|
||||
--kbx-grid-row-height: 36px;
|
||||
--kbx-touch-target: 48px;
|
||||
}
|
||||
|
||||
:root[data-density="touch"] {
|
||||
--kbx-input-height: 52px;
|
||||
--kbx-grid-row-height: 48px;
|
||||
--kbx-touch-target: 52px;
|
||||
}
|
||||
```
|
||||
|
||||
#### Permission Enforcement (Registry-Driven RBAC)
|
||||
|
||||
Permissions are registry-based, not hard-coded:
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Registry-driven permission checks
|
||||
const canEdit = computed(() => {
|
||||
const screen = registry.screens.get('oms.orders.detail')
|
||||
return permissions.hasAll(screen.permissions) // ['order.edit', 'order.view']
|
||||
})
|
||||
|
||||
// ❌ DON'T: Hard-coded permission strings in components
|
||||
// const canEdit = permissions.has('order.edit') // WRONG: no registry reference
|
||||
```
|
||||
|
||||
#### Help System Integration (Registry Context)
|
||||
|
||||
Help content is registry-driven, not duplicated in component code:
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Help from registry
|
||||
const { openHelp } = useHelpPanel()
|
||||
|
||||
// In help panel:
|
||||
// const screen = registry.screens.get('oms.orders.list')
|
||||
// const helpDef = screen.help // { title, sections, relatedScreens }
|
||||
|
||||
openHelp('oms.orders.list')
|
||||
|
||||
// ❌ DON'T: Hard-coded help text in component
|
||||
// const helpText = "Use filters to search..." // WRONG: duplicates registry
|
||||
```
|
||||
|
||||
#### Contract Enforcement (CI/CD Gate)
|
||||
|
||||
Build-time validation ensures all screens comply with contracts:
|
||||
|
||||
```bash
|
||||
# .gitea/workflows/quality-gate.yml
|
||||
- name: Validate screen contracts
|
||||
run: |
|
||||
# 1. Check: All files in features/*/pages/*.vue match registry entries
|
||||
# 2. Check: All ScreenDefinition.permissions exist in permissionRegistry
|
||||
# 3. Check: Grid configs use adapter tokens, not inline CSS
|
||||
# 4. Check: No PrimeVue/AG Grid imports outside adapter/
|
||||
# 5. Generate: ScreenManifest.json for help/telemetry indexing
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### xUnit Backend Tests
|
||||
|
||||
Reference in New Issue
Block a user