feat: adopt vendor-neutral form components (V13-FE-005)
This commit is contained in:
@@ -42,3 +42,4 @@ PHASE-1-SHADOW-RUN,S0-S5,Cross,252+ Trading Day Shadow Run,BLOCKED,TBD,"docs/CUR
|
||||
V13-FE-001,S0,Cross,UI Vendor import boundary,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-001_KBX_V36_DESIGN_HARNESS_PROPOSAL.md; tools/validate_v16.py",FE Architect/QA,"Dependency AEG-X-003 is COMPLETED. KBX v36 was translated as a non-vendor design-evidence harness: preserve the shared UI adapter boundary, keep feature direct PrimeVue/AG Grid imports at zero, and defer token/recipe implementation to separately approved slices. Actual evidence: python tools/validate_v16.py exited 0 with PASS=1 WARN=2 FAIL=0 on 2026-08-09. Warnings are retained (no full source archive; approved runtime evidence absent); no runtime test/build/migration claim is made."
|
||||
V13-FE-003,S0,Cross,UiAdapter Port 정의,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-003_UI_ADAPTER_PORT_RECONCILIATION.md; frontend/src/shared/ui/adapter/contracts.ts; frontend/src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts",FE Architect/QA,"Dependency V13-FE-001 is COMPLETED. Existing adapter v4 contract explicitly verifies 14 capabilities (stronger than the WBS minimum wording of 8) without feature vendor imports. Actual targeted Vitest evidence: 2 files / 4 tests passed, exit 0, 2026-08-09. KBX-derived components remain provider-neutral reimplementations only; no KBX package, contract, router, store, or permission host was imported."
|
||||
V13-FE-004,S0,Cross,PrimeVue/AG Grid Adapter 구현,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-004_ADAPTER_IMPLEMENTATION_RECONCILIATION.md; frontend/src/shared/ui/adapter/primevue; frontend/src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts",FE Architect/QA,"Dependency V13-FE-003 is COMPLETED. PrimeVue/AG Grid remain confined behind adapter v4. Actual targeted Vitest evidence: 2 files / 4 tests passed, exit 0, 2026-08-09. This is contract/accessibility-attribute evidence only; no visual/AT/runtime claim is made."
|
||||
V13-FE-005,S0,Cross,Ks* vendor-neutral components,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-005_KBX_FORM_COMPONENT_ADOPTION.md; frontend/src/shared/ui/components/KsFormGrid.vue; frontend/src/shared/ui/components/KsFormSection.vue; frontend/src/shared/ui/components/KsFormSpan.vue; frontend/src/shared/ui/components/KsValidationSummary.vue; frontend/src/shared/ui/components/tests/KsFormLayouts.spec.ts",FE Architect/QA,"Dependency V13-FE-004 is COMPLETED. Reimplemented only KBX presentation-only form components against existing K-ArtSell tokens; no KBX package, contracts, provider dependency, routing, permissions, or business policy imported. Actual evidence: targeted Vitest 2 files / 6 tests passed and pnpm typecheck exit 0 on 2026-08-09. Visual/AT/performance baseline remains outside this Slice."
|
||||
|
||||
|
@@ -0,0 +1,30 @@
|
||||
# V13-FE-005 — KBX 폼 컴포넌트 선별 이식
|
||||
|
||||
| 항목 | 값 |
|
||||
| --- | --- |
|
||||
| Requirement / API / DB / Job / UI / Test | REQ-FE-COMP / Cross / Cross / Cross / UI-FOUND-05 / T-FE-COMP-01 |
|
||||
| Dependency / Gate | V13-FE-004 (COMPLETED) / G0 |
|
||||
| Artifact | `frontend/src/shared/ui/components/KsForm*.vue`, `KsValidationSummary.vue` |
|
||||
| 목적 | provider-neutral form layout과 validation presentation 표준화 |
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** KBX v36의 `KbxFormGrid`, `KbxFormSection`, `KbxFormSpan`, `KbxValidationSummary`는 Vue/CSS presentation-only 구성이다.
|
||||
- **Assumption:** K-ArtSell token (`--ks-*`)과 기존 `FieldShell`/Zod 경계를 유지하면 같은 layout pattern을 안전하게 재구현할 수 있다.
|
||||
- **Unknown:** 각 feature의 오류를 어느 field로 focus할지와 visual/AT baseline은 화면별 evidence가 필요하다.
|
||||
- **Decision Required:** 오류 클릭 시 field focus/navigation, density variant, 새로운 token은 실제 3회 이상 반복 및 ADR/QA 기준 승인 후에만 추가한다.
|
||||
|
||||
## 구현 경계
|
||||
|
||||
- `KsFormGrid`, `KsFormSection`, `KsFormSpan`, `KsValidationSummary`만 추가했다.
|
||||
- KBX package, `@kbx/contracts`, provider peer dependency, router, store, permission host, 업무 규칙을 복사하지 않았다.
|
||||
- validation summary는 오류를 숨기기 위한 임의 최대 개수를 두지 않고 전달받은 오류를 모두 표시한다.
|
||||
- 색상/spacing은 기존 semantic token만 사용한다.
|
||||
|
||||
## 실제 증거
|
||||
|
||||
- `pnpm vitest run src/shared/ui/components/tests/KsFormLayouts.spec.ts src/shared/ui/components/tests/accessibility.contract.spec.ts`: 2 files / 6 tests passed, exit 0.
|
||||
- `pnpm typecheck`: exit 0.
|
||||
- 실행일: 2026-08-09.
|
||||
|
||||
위 증거는 component contract/typecheck에 한정된다. browser visual, AT, performance, server/API/DB/migration 통과는 주장하지 않는다.
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{ columns?: 1 | 2; ariaLabel?: string }>(), {
|
||||
columns: 2,
|
||||
ariaLabel: '입력 항목'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ks-form-grid" :data-columns="columns" role="group" :aria-label="ariaLabel"><slot /></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ks-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--ks-space-4); align-items: start; }
|
||||
.ks-form-grid[data-columns='1'] { grid-template-columns: minmax(0, 1fr); }
|
||||
@media (max-width: 56rem) { .ks-form-grid { grid-template-columns: minmax(0, 1fr); } }
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{ title: string; description?: string; labelledBy?: string }>(), { description: '' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="ks-form-section" :aria-labelledby="labelledBy">
|
||||
<header class="ks-form-section__header">
|
||||
<h2 :id="labelledBy">{{ title }}</h2>
|
||||
<p v-if="description">{{ description }}</p>
|
||||
<div v-if="$slots.actions" class="ks-form-section__actions"><slot name="actions" /></div>
|
||||
</header>
|
||||
<div class="ks-form-section__content"><slot /></div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ks-form-section { display: flex; flex-direction: column; gap: var(--ks-space-3); margin-bottom: var(--ks-space-6); }
|
||||
.ks-form-section__header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: var(--ks-space-2); padding-bottom: var(--ks-space-2); border-bottom: 1px solid var(--ks-color-neutral-200); }
|
||||
h2 { margin: 0; font-size: var(--ks-font-section); line-height: var(--ks-line-section); }
|
||||
p { grid-column: 1; margin: 0; color: var(--ks-color-neutral-600); font-size: var(--ks-font-caption); }
|
||||
.ks-form-section__actions { grid-column: 2; grid-row: 1 / -1; display: flex; align-items: center; gap: var(--ks-space-2); }
|
||||
.ks-form-section__content { min-width: 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{ span?: 'cell' | 'full' }>(), { span: 'cell' })
|
||||
</script>
|
||||
|
||||
<template><div class="ks-form-span" :data-span="span"><slot /></div></template>
|
||||
|
||||
<style scoped>
|
||||
.ks-form-span { min-width: 0; }
|
||||
.ks-form-span[data-span='full'] { grid-column: 1 / -1; }
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
export interface ValidationSummaryError { readonly field?: string; readonly message: string }
|
||||
defineProps<{ errors: readonly ValidationSummaryError[]; title?: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="errors.length" class="ks-validation-summary" role="alert" aria-live="assertive">
|
||||
<strong>{{ title ?? `저장할 수 없습니다. ${errors.length}개 항목을 확인하세요.` }}</strong>
|
||||
<ul><li v-for="(error, index) in errors" :key="`${error.field ?? 'form'}-${index}`">{{ error.message }}</li></ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ks-validation-summary { border: 1px solid var(--ks-color-danger); background: color-mix(in srgb, var(--ks-color-danger) 8%, var(--ks-color-surface)); padding: var(--ks-space-2) var(--ks-space-3); border-radius: var(--ks-radius-sm); color: var(--ks-color-neutral-950); }
|
||||
strong { color: var(--ks-color-danger); } ul { margin: var(--ks-space-2) 0 0 var(--ks-space-6); padding: 0; }
|
||||
</style>
|
||||
@@ -15,3 +15,7 @@ export { default as KsDataGrid } from './KsDataGrid.vue'
|
||||
export { default as FieldShell } from './FieldShell.vue'
|
||||
export { default as KsDataContextHeader } from './KsDataContextHeader.vue'
|
||||
export { default as KsCommandBar } from './KsCommandBar.vue'
|
||||
export { default as KsFormGrid } from './KsFormGrid.vue'
|
||||
export { default as KsFormSection } from './KsFormSection.vue'
|
||||
export { default as KsFormSpan } from './KsFormSpan.vue'
|
||||
export { default as KsValidationSummary } from './KsValidationSummary.vue'
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import KsFormGrid from '../KsFormGrid.vue'
|
||||
import KsFormSection from '../KsFormSection.vue'
|
||||
import KsFormSpan from '../KsFormSpan.vue'
|
||||
import KsValidationSummary from '../KsValidationSummary.vue'
|
||||
|
||||
describe('KBX-derived vendor-neutral form components', () => {
|
||||
it('keeps grid semantics and collapses its layout by tokenized CSS', () => {
|
||||
const wrapper = mount(KsFormGrid, { props: { columns: 2, ariaLabel: '고객 정보' }, slots: { default: '<input aria-label="이름">' } })
|
||||
expect(wrapper.attributes('role')).toBe('group')
|
||||
expect(wrapper.attributes('aria-label')).toBe('고객 정보')
|
||||
expect(wrapper.attributes('data-columns')).toBe('2')
|
||||
})
|
||||
|
||||
it('allows a field to span the full form row', () => {
|
||||
expect(mount(KsFormSpan, { props: { span: 'full' } }).attributes('data-span')).toBe('full')
|
||||
})
|
||||
|
||||
it('provides an explicit heading and optional actions without a provider dependency', () => {
|
||||
const wrapper = mount(KsFormSection, { props: { title: '주문 조건', labelledBy: 'order-condition' }, slots: { actions: '<button>초기화</button>' } })
|
||||
expect(wrapper.find('h2').attributes('id')).toBe('order-condition')
|
||||
expect(wrapper.get('button').text()).toBe('초기화')
|
||||
})
|
||||
|
||||
it('announces every supplied validation error', () => {
|
||||
const wrapper = mount(KsValidationSummary, { props: { errors: [{ field: 'amount', message: '금액을 입력하세요.' }, { field: 'date', message: '일자를 확인하세요.' }] } })
|
||||
expect(wrapper.attributes('role')).toBe('alert')
|
||||
expect(wrapper.findAll('li')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user