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:
2026-08-12 01:19:29 +09:00
parent 3fbbca223e
commit 831c4b467d
+429 -37
View File
@@ -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