chore: remove kbx-foundation-v36 reference (superseded by v4 implementation)

Removed entire kbx-foundation-v36 directory as it's been replaced by
the new KBX Foundation v4 patterns implemented in this session:
- Registry-driven screen definitions
- Density-aware UI adapter components
- Feature module templates (ShadowRun, Models)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 01:39:58 +09:00
parent 831c4b467d
commit c41e5063b7
1079 changed files with 13094 additions and 1134 deletions
@@ -0,0 +1,119 @@
/**
* Composable: useKbxRegistry
* Access screen registry, permissions, and density from components
*/
import { computed, inject, ref } from 'vue'
import type {
KbxScreenDefinition,
KbxPermissionDefinition,
KbxDensity,
} from '@shared/contracts/kbx-types'
// Reactive state
const currentDensity = ref<KbxDensity>('compact')
const userPermissions = ref<Set<string>>(new Set())
export function useKbxRegistry() {
// Get injected registries
const screenRegistry = inject<Map<string, KbxScreenDefinition>>(
'kbx-screens',
new Map(),
)
const permissionRegistry = inject<Map<string, KbxPermissionDefinition>>(
'kbx-permissions',
new Map(),
)
// Screen methods
const getScreen = (screenId: string) => screenRegistry.get(screenId)
const getAllScreens = () => Array.from(screenRegistry.values())
const getScreenByModule = (module: string) =>
getAllScreens().filter(s => s.module === module)
// Permission methods
const hasPermission = (permissionId: string) => {
return userPermissions.value.has(permissionId)
}
const hasAllPermissions = (permissionIds: string[]) => {
return permissionIds.every(id => userPermissions.value.has(id))
}
const hasAnyPermission = (permissionIds: string[]) => {
return permissionIds.some(id => userPermissions.value.has(id))
}
const canAccessScreen = (screenId: string) => {
const screen = getScreen(screenId)
if (!screen) return false
return hasAllPermissions(screen.permissions)
}
// Density methods
const setDensity = (density: KbxDensity) => {
currentDensity.value = density
document.documentElement.style.setProperty('--kbx-density', density)
const tokens = {
compact: {
inputHeight: '34px',
gridRowHeight: '34px',
touchTarget: '44px',
fontSize: '12px',
controlHeight: '34px',
},
comfortable: {
inputHeight: '36px',
gridRowHeight: '36px',
touchTarget: '48px',
fontSize: '14px',
controlHeight: '36px',
},
touch: {
inputHeight: '48px',
gridRowHeight: '48px',
touchTarget: '52px',
fontSize: '16px',
controlHeight: '48px',
},
}
Object.entries(tokens[density]).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--kbx-${key}`, value)
})
}
const getDensity = computed(() => currentDensity.value)
// Update user permissions
const setPermissions = (permissions: string[]) => {
userPermissions.value.clear()
permissions.forEach(p => userPermissions.value.add(p))
}
return {
// Screen access
getScreen,
getAllScreens,
getScreenByModule,
canAccessScreen,
// Permission access
hasPermission,
hasAllPermissions,
hasAnyPermission,
setPermissions,
// Density
setDensity,
getDensity,
// Registries
screenRegistry,
permissionRegistry,
}
}
+147
View File
@@ -0,0 +1,147 @@
/**
* KBX Foundation v4 Core Types
* Single source of truth for screen definitions, permissions, and UI contracts
*/
// Screen Definition (Registry Entry)
export interface KbxScreenDefinition {
screenId: string // e.g., "model-ops.shadow-run.list"
title: string // e.g., "Shadow Run Validation"
module: 'ModelOps' | 'SignalEngine' | 'Admin'
type: 'list' | 'detail' | 'form' | 'dashboard'
path: string // Vue Router path
component: () => Promise<any> // Lazy-loaded component
permissions: string[] // Required permissions (e.g., ['model.read'])
help?: KbxHelpDefinition
grid?: KbxGridDefinition
shortcuts?: KbxShortcut[]
telemetry?: { enabled: boolean }
}
// Grid Column Definition
export interface KbxGridColumn<T = any> {
field: keyof T
header: string
type?: 'text' | 'number' | 'date' | 'status' | 'link' | 'money' | 'quantity'
width?: number | string
pinned?: 'left' | 'right'
sortable?: boolean
filterable?: boolean
formatter?: (value: any, row: T) => string
}
// Grid Configuration
export interface KbxGridDefinition {
columnDefs: KbxGridColumn[]
rowHeight?: number | 'auto'
pageSize?: number
serverSideDatasource?: boolean
theme?: string
}
// Search Field Definition
export interface KbxSearchField {
key: string
label: string
type: 'text' | 'number' | 'date' | 'date-range' | 'select' | 'multi-select'
options?: Array<{ value: string | number; label: string }>
range?: { from: string; to: string } // for date-range
placeholder?: string
width?: 'sm' | 'md' | 'lg'
}
// Help Definition
export interface KbxHelpDefinition {
title: string
sections: KbxHelpSection[]
relatedScreens?: string[]
externalUrl?: string
}
export interface KbxHelpSection {
title: string
content: string
icon?: string
}
// Permission Definition
export interface KbxPermissionDefinition {
permissionId: string // e.g., 'model.create'
label: string
description?: string
screens: string[] // Which screens require this
}
// Command Definition (Actions)
export interface KbxCommand {
id: string
label: string
group?: string // 'query' | 'edit' | 'workflow' | 'output'
permission?: string
requiresSelection?: boolean
minSelection?: number
variant?: 'default' | 'primary' | 'danger'
shortcut?: string
icon?: string
}
// Keyboard Shortcut
export interface KbxShortcut {
key: string // 'F3', 'Ctrl+S', etc.
label: string
action: string
}
// Data State (Loading, Error, Empty)
export type KbxAsyncState = 'idle' | 'pending' | 'ready' | 'error' | 'empty'
// Grid Summary Item
export interface KbxSummaryItem {
label: string
value: string | number
format?: 'number' | 'money' | 'quantity' | 'percentage'
}
// Quick Filter
export interface KbxQuickFilterItem {
id: string
label: string
badge?: string | number
active?: boolean
}
// Screen Context (Breadcrumb, Parent Info)
export interface KbxScreenContext {
parentScreenId?: string
breadcrumb?: string
contextData?: Record<string, any>
}
// Problem/Error Display
export interface KbxProblem {
code: string
message: string
details?: string
recoveryActions?: string[]
retryable?: boolean
}
// Theme Configuration
export interface KbxThemeConfig {
primary: string
secondary: string
danger: string
success: string
warning: string
info: string
}
// Density Token (UI Sizing)
export type KbxDensity = 'compact' | 'comfortable' | 'touch'
export interface KbxDensityTokens {
inputHeight: number
gridRowHeight: number
touchTarget: number
fontSize: number
controlHeight: number
}
@@ -0,0 +1,92 @@
<script setup lang="ts">
import PButton from 'primevue/button'
import { computed } from 'vue'
interface Props {
label?: string
variant?: 'default' | 'primary' | 'danger' | 'success'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
icon?: string
iconPosition?: 'left' | 'right'
fullWidth?: boolean
text?: boolean
}
const props = withDefaults(defineProps<Props>(), {
variant: 'default',
size: 'md',
disabled: false,
loading: false,
iconPosition: 'left',
fullWidth: false,
text: false,
})
const emit = defineEmits<{ click: [] }>()
// Map KBX variants to PrimeVue severity
const severityMap = {
default: 'secondary',
primary: 'primary',
danger: 'danger',
success: 'success',
}
const severity = computed(() => severityMap[props.variant])
// Size classes (density-aware via CSS variables)
const sizeClasses = computed(() => {
const densityVar = 'var(--kbx-density, compact)'
return {
'kbx-button--sm': props.size === 'sm',
'kbx-button--md': props.size === 'md',
'kbx-button--lg': props.size === 'lg',
}
})
</script>
<template>
<PButton
:label="label"
:severity="severity"
:disabled="disabled || loading"
:loading="loading"
:icon="icon"
:icon-pos="iconPosition"
:class="{ ...sizeClasses, 'p-button-text': text, 'w-full': fullWidth }"
@click="emit('click')"
>
<slot />
</PButton>
</template>
<style scoped>
.kbx-button--sm :deep(.p-button) {
min-height: calc(var(--kbx-input-height, 34px) - 4px);
font-size: 0.75rem;
padding: 0.25rem 0.75rem;
}
.kbx-button--md :deep(.p-button) {
min-height: var(--kbx-input-height, 34px);
font-size: 0.875rem;
padding: 0.5rem 1rem;
}
.kbx-button--lg :deep(.p-button) {
min-height: calc(var(--kbx-input-height, 34px) + 6px);
font-size: 1rem;
padding: 0.75rem 1.5rem;
}
/* Density-aware spacing */
:deep(.p-button) {
transition: all 0.2s ease;
}
:deep(.p-button:not(:disabled):hover) {
transform: translateY(-1px);
}
</style>
@@ -0,0 +1,170 @@
<script setup lang="ts">
import { AgGridVue } from 'ag-grid-vue3'
import { computed, ref } from 'vue'
import type { KbxGridColumn, KbxDensity } from '@shared/contracts/kbx-types'
interface Props<T = any> {
columns: KbxGridColumn<T>[]
rows?: T[]
rowKey?: keyof T | string
loading?: boolean
density?: KbxDensity
allowSelection?: boolean
serverSideDatasource?: boolean
pageSize?: number
}
const props = withDefaults(defineProps<Props>(), {
rows: () => [],
loading: false,
density: 'compact',
allowSelection: false,
serverSideDatasource: false,
pageSize: 50,
})
const emit = defineEmits<{
rowClick: [row: any]
selectionChange: [selectedRows: any[]]
loadMore: [{ offset: number; pageSize: number }]
}>()
const selectedRows = ref<any[]>([])
// Density-aware row height
const densityHeights = {
compact: 34,
comfortable: 36,
touch: 48,
}
const gridOptions = computed(() => ({
columnDefs: props.columns.map(col => ({
field: col.field,
headerName: col.header,
width: col.width || 'auto',
pinned: col.pinned,
sortable: col.sortable !== false,
filter: col.filterable !== false,
type: col.type,
})),
rowData: props.rows,
rowSelection: props.allowSelection ? 'multiple' : undefined,
rowHeight: densityHeights[props.density],
pagination: !props.serverSideDatasource,
paginationPageSize: props.pageSize,
suppressMovableColumns: false,
suppressColumnMoveAnimation: false,
headerHeight: 36,
theme: 'ag-theme-quartz',
}))
const onSelectionChanged = (event: any) => {
selectedRows.value = event.api.getSelectedRows()
emit('selectionChange', selectedRows.value)
}
const onRowClicked = (event: any) => {
emit('rowClick', event.data)
}
</script>
<template>
<div
class="kbx-data-grid"
:class="`kbx-data-grid--${density}`"
:style="{
'--kbx-grid-row-height': `${densityHeights[density]}px`,
}"
>
<div v-if="loading" class="kbx-data-grid__loader">
<div class="spinner"></div>
Loading...
</div>
<AgGridVue
:grid-options="gridOptions"
class="ag-theme-quartz"
@selection-changed="onSelectionChanged"
@row-clicked="onRowClicked"
/>
</div>
</template>
<style scoped>
.kbx-data-grid {
position: relative;
width: 100%;
height: 100%;
border: 1px solid var(--kbx-color-border, #e5e7eb);
border-radius: 0.375rem;
overflow: hidden;
}
.kbx-data-grid__loader {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.8);
z-index: 10;
flex-direction: column;
gap: 1rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid var(--kbx-color-border, #e5e7eb);
border-top-color: var(--kbx-color-primary, #3b82f6);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Density variants */
.kbx-data-grid--compact {
font-size: 0.75rem;
}
.kbx-data-grid--comfortable {
font-size: 0.875rem;
}
.kbx-data-grid--touch {
font-size: 1rem;
}
:deep(.ag-theme-quartz) {
--ag-row-height: var(--kbx-grid-row-height, 34px);
--ag-header-height: 36px;
--ag-font-size: 0.875rem;
--ag-border-color: var(--kbx-color-border, #e5e7eb);
}
:deep(.ag-header-cell-text) {
font-weight: 600;
color: var(--kbx-color-text, #000);
}
:deep(.ag-row) {
border-bottom: 1px solid var(--kbx-color-border, #e5e7eb);
}
:deep(.ag-row:hover) {
background-color: var(--kbx-color-hover, #f9fafb);
}
:deep(.ag-row-selected) {
background-color: var(--kbx-color-primary-alpha, rgba(59, 130, 246, 0.1));
}
</style>
+109
View File
@@ -0,0 +1,109 @@
<script setup lang="ts">
import PInputText from 'primevue/inputtext'
import { computed } from 'vue'
interface Props {
modelValue?: string | number
type?: 'text' | 'email' | 'password' | 'number' | 'date'
placeholder?: string
disabled?: boolean
readonly?: boolean
invalid?: boolean
label?: string
help?: string
required?: boolean
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
type: 'text',
placeholder: '',
disabled: false,
readonly: false,
invalid: false,
required: false,
})
const emit = defineEmits<{
'update:modelValue': [value: string | number]
focus: []
blur: []
}>()
const inputClasses = computed(() => ({
'p-invalid': props.invalid,
'kbx-input--disabled': props.disabled,
}))
</script>
<template>
<div class="kbx-input-wrapper">
<label v-if="label" class="kbx-input__label">
{{ label }}
<span v-if="required" class="kbx-input__required">*</span>
</label>
<PInputText
:model-value="modelValue"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:class="inputClasses"
class="kbx-input"
@update:model-value="emit('update:modelValue', $event)"
@focus="emit('focus')"
@blur="emit('blur')"
/>
<small v-if="help" class="kbx-input__help">{{ help }}</small>
</div>
</template>
<style scoped>
.kbx-input-wrapper {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.kbx-input__label {
font-size: 0.875rem;
font-weight: 500;
color: var(--kbx-color-text, #000);
}
.kbx-input__required {
color: var(--kbx-color-danger, #ef4444);
}
.kbx-input {
min-height: var(--kbx-input-height, 34px);
padding: 0.5rem 0.75rem;
border: 1px solid var(--kbx-color-border, #e5e7eb);
border-radius: 0.375rem;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.kbx-input:focus {
border-color: var(--kbx-color-primary, #3b82f6);
box-shadow: 0 0 0 3px var(--kbx-color-primary-alpha, rgba(59, 130, 246, 0.1));
}
.kbx-input:disabled,
.kbx-input--disabled {
background-color: var(--kbx-color-disabled, #f3f4f6);
cursor: not-allowed;
opacity: 0.6;
}
.kbx-input.p-invalid {
border-color: var(--kbx-color-danger, #ef4444);
}
.kbx-input__help {
font-size: 0.75rem;
color: var(--kbx-color-text-muted, #6b7280);
}
</style>
@@ -0,0 +1,281 @@
<script setup lang="ts">
import { computed } from 'vue'
import type {
KbxScreenDefinition,
KbxAsyncState,
KbxSummaryItem,
KbxQuickFilterItem,
KbxScreenContext,
} from '@shared/contracts/kbx-types'
interface Props {
screen: KbxScreenDefinition
dataState?: KbxAsyncState
loading?: boolean
selectionCount?: number
summaryItems?: KbxSummaryItem[]
quickFilters?: KbxQuickFilterItem[]
context?: KbxScreenContext | null
allowActions?: boolean
}
const props = withDefaults(defineProps<Props>(), {
dataState: 'ready',
loading: false,
selectionCount: 0,
summaryItems: () => [],
quickFilters: () => [],
context: null,
allowActions: true,
})
const emit = defineEmits<{
command: [commandId: string]
quickFilter: [filterId: string]
refresh: []
}>()
// Visibility states
const showSearch = computed(() => !!props.screen.type)
const showSummary = computed(() => props.summaryItems.length > 0)
const showQuickFilter = computed(() => props.quickFilters.length > 0)
const stateMessages = {
idle: 'Ready to search',
pending: 'Loading data...',
ready: 'Data loaded',
error: 'Error loading data',
empty: 'No results found',
}
const currentStateMessage = computed(() => stateMessages[props.dataState])
</script>
<template>
<div class="kbx-list-page">
<!-- Page Header -->
<header class="kbx-list-page__header">
<div class="kbx-list-page__header-content">
<div>
<h1 class="kbx-list-page__title">{{ screen.title }}</h1>
<p v-if="screen.description" class="kbx-list-page__description">
{{ screen.description }}
</p>
</div>
<div class="kbx-list-page__header-actions">
<slot name="header-actions" />
</div>
</div>
</header>
<!-- Search Panel (slot) -->
<div v-if="showSearch" class="kbx-list-page__search">
<slot name="search" />
</div>
<!-- Quick Filters -->
<div v-if="showQuickFilter" class="kbx-list-page__quick-filters">
<button
v-for="filter in quickFilters"
:key="filter.id"
class="kbx-list-page__quick-filter-item"
:class="{ active: filter.active }"
@click="emit('quickFilter', filter.id)"
>
{{ filter.label }}
<span v-if="filter.badge" class="badge">{{ filter.badge }}</span>
</button>
</div>
<!-- Context Bar -->
<div v-if="context" class="kbx-list-page__context">
<slot name="context" />
</div>
<!-- Content Area -->
<main class="kbx-list-page__content">
<!-- Loading State -->
<div v-if="dataState === 'pending'" class="kbx-list-page__state">
<div class="state-spinner"></div>
{{ currentStateMessage }}
</div>
<!-- Empty State -->
<div v-else-if="dataState === 'empty'" class="kbx-list-page__state">
<p>{{ currentStateMessage }}</p>
<button class="kbx-button--secondary" @click="emit('command', 'new')">
Create New
</button>
</div>
<!-- Error State -->
<div v-else-if="dataState === 'error'" class="kbx-list-page__state kbx-list-page__state--error">
<p>{{ currentStateMessage }}</p>
<button class="kbx-button--primary" @click="emit('refresh')">
Retry
</button>
</div>
<!-- Content Slot -->
<slot v-else name="content" />
</main>
<!-- Summary Bar (sticky footer) -->
<footer v-if="showSummary" class="kbx-list-page__footer">
<div class="kbx-list-page__summary">
<span v-if="selectionCount > 0" class="summary-item">
{{ selectionCount }} item(s) selected
</span>
<div class="summary-items">
<span v-for="item in summaryItems" :key="item.label" class="summary-item">
<strong>{{ item.label }}:</strong>
{{ item.value }}
</span>
</div>
</div>
</footer>
</div>
</template>
<style scoped>
.kbx-list-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: var(--kbx-color-background, #fff);
}
.kbx-list-page__header {
padding: 1.5rem;
border-bottom: 1px solid var(--kbx-color-border, #e5e7eb);
background-color: var(--kbx-color-shell-chrome, #f9fafb);
}
.kbx-list-page__header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.kbx-list-page__title {
font-size: 1.875rem;
font-weight: 700;
margin: 0 0 0.5rem 0;
color: var(--kbx-color-text, #000);
}
.kbx-list-page__description {
font-size: 0.875rem;
color: var(--kbx-color-text-muted, #6b7280);
margin: 0;
}
.kbx-list-page__header-actions {
display: flex;
gap: 0.5rem;
}
.kbx-list-page__search {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--kbx-color-border, #e5e7eb);
background-color: var(--kbx-color-surface, #fff);
}
.kbx-list-page__quick-filters {
display: flex;
gap: 0.75rem;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--kbx-color-border, #e5e7eb);
background-color: var(--kbx-color-surface, #fff);
overflow-x: auto;
}
.kbx-list-page__quick-filter-item {
padding: 0.5rem 1rem;
border: 1px solid var(--kbx-color-border, #e5e7eb);
border-radius: 0.375rem;
background-color: transparent;
cursor: pointer;
white-space: nowrap;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.kbx-list-page__quick-filter-item:hover {
border-color: var(--kbx-color-primary, #3b82f6);
color: var(--kbx-color-primary, #3b82f6);
}
.kbx-list-page__quick-filter-item.active {
background-color: var(--kbx-color-primary, #3b82f6);
color: white;
border-color: var(--kbx-color-primary, #3b82f6);
}
.badge {
display: inline-block;
margin-left: 0.5rem;
padding: 0.125rem 0.375rem;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 0.25rem;
font-size: 0.75rem;
}
.kbx-list-page__content {
flex: 1;
overflow-y: auto;
background-color: var(--kbx-color-surface, #fff);
display: flex;
align-items: center;
justify-content: center;
}
.kbx-list-page__state {
text-align: center;
color: var(--kbx-color-text-muted, #6b7280);
font-size: 0.875rem;
}
.kbx-list-page__state--error {
color: var(--kbx-color-danger, #ef4444);
}
.state-spinner {
width: 40px;
height: 40px;
margin: 0 auto 1rem;
border: 4px solid var(--kbx-color-border, #e5e7eb);
border-top-color: var(--kbx-color-primary, #3b82f6);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.kbx-list-page__footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--kbx-color-border, #e5e7eb);
background-color: var(--kbx-color-shell-chrome, #f9fafb);
font-size: 0.875rem;
}
.kbx-list-page__summary {
display: flex;
gap: 2rem;
align-items: center;
color: var(--kbx-color-text-muted, #6b7280);
}
.summary-items {
display: flex;
gap: 1rem;
}
.summary-item {
white-space: nowrap;
}
</style>
+45
View File
@@ -0,0 +1,45 @@
/**
* KBX UI Adapter - Export all wrapped components
* Single boundary: PrimeVue/AG Grid usage restricted to this module
*/
// Contracts & Types
export * from '@shared/contracts/kbx-types'
// Adapter Components (PrimeVue wrapped)
export { default as KbxButton } from './KbxButton.vue'
export { default as KbxInput } from './KbxInput.vue'
export { default as KbxDataGrid } from './KbxDataGrid.vue'
export { default as KbxListPage } from './KbxListPage.vue'
// Density tokens
export const densityTokens = {
compact: {
inputHeight: 34,
gridRowHeight: 34,
touchTarget: 44,
fontSize: 12,
},
comfortable: {
inputHeight: 36,
gridRowHeight: 36,
touchTarget: 48,
fontSize: 14,
},
touch: {
inputHeight: 48,
gridRowHeight: 48,
touchTarget: 52,
fontSize: 16,
},
}
// Theme configuration
export const defaultTheme = {
primary: '#3b82f6',
secondary: '#6b7280',
danger: '#ef4444',
success: '#10b981',
warning: '#f59e0b',
info: '#06b6d4',
}