V13-FE-005: restore direct UI components and harden grid provider
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import type { UiButtonType, UiSeverity } from '../adapter/contracts'
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
|
||||
withDefaults(defineProps<{ label?: string; severity?: UiSeverity; type?: UiButtonType; disabled?: boolean; loading?: boolean }>(), {
|
||||
severity: 'primary', type: 'button', disabled: false, loading: false
|
||||
})
|
||||
const emit = defineEmits<{ click: [event: MouseEvent] }>()
|
||||
const adapter = useUiAdapter()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="adapter.components.Button" v-bind="$props" @activate="emit('click', $event)"><slot /></component>
|
||||
<Button v-bind="$props" class="ks-button" @click="emit('click', $event)"><slot /></Button>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useId } from 'vue'
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
const props = defineProps<{ modelValue: boolean; label: string; inputId?: string; disabled?: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
const adapter = useUiAdapter()
|
||||
const generatedId = useId()
|
||||
const resolvedId = computed(() => props.inputId ?? `ks-check-${generatedId}`)
|
||||
</script>
|
||||
<template><label class="ks-check" :for="resolvedId"><component :is="adapter.components.Checkbox" :input-id="resolvedId" :model-value="modelValue" :disabled="disabled" @update:model-value="emit('update:modelValue', $event)" /><span>{{ label }}</span></label></template>
|
||||
<template><label class="ks-check" :for="resolvedId"><Checkbox class="ks-checkbox" :input-id="resolvedId" :model-value="modelValue" binary :disabled="disabled" @update:model-value="emit('update:modelValue', Boolean($event))" /><span>{{ label }}</span></label></template>
|
||||
<style scoped>.ks-check { display: inline-flex; align-items: center; gap: var(--ks-space-2); cursor: pointer; }</style>
|
||||
|
||||
@@ -1,8 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import { ClientSideRowModelModule, ModuleRegistry, RowSelectionModule, TextFilterModule, themeQuartz, type ColDef, type RowClickedEvent } from 'ag-grid-community'
|
||||
import type { UiGridColumn } from '../adapter/contracts'
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
withDefaults(defineProps<{ rows: unknown[]; columns: UiGridColumn[]; loading?: boolean; height?: string; rowSelection?: 'single' | 'multiple' | 'none' }>(), { loading: false, height: '32rem', rowSelection: 'single' })
|
||||
|
||||
ModuleRegistry.registerModules([ClientSideRowModelModule, TextFilterModule, RowSelectionModule])
|
||||
|
||||
const emit = defineEmits<{ rowSelected: [row: unknown] }>()
|
||||
const adapter = useUiAdapter()
|
||||
const props = withDefaults(defineProps<{ rows: unknown[]; columns: UiGridColumn[]; loading?: boolean; height?: string; rowSelection?: 'single' | 'multiple' | 'none' }>(), { loading: false, height: '32rem', rowSelection: 'single' })
|
||||
const columnDefs = computed<ColDef[]>(() => props.columns.map(column => ({
|
||||
field: column.field,
|
||||
headerName: column.header,
|
||||
width: column.width,
|
||||
minWidth: column.minWidth ?? 120,
|
||||
sortable: column.sortable ?? true,
|
||||
filter: column.filterable ?? true,
|
||||
valueFormatter: column.formatter ? params => column.formatter?.(params.value, params.data) ?? '' : undefined
|
||||
})))
|
||||
const rowSelectionOptions = computed(() => props.rowSelection === 'none' ? undefined : ({ mode: props.rowSelection === 'multiple' ? 'multiRow' : 'singleRow' } as const))
|
||||
function onRowClicked(event: RowClickedEvent): void {
|
||||
if (event.data) emit('rowSelected', event.data)
|
||||
}
|
||||
</script>
|
||||
<template><component :is="adapter.components.DataGrid" v-bind="$props" @row-selected="emit('rowSelected', $event)" /></template>
|
||||
<template>
|
||||
<div class="ks-grid" :style="{ height: props.height }" :aria-busy="props.loading">
|
||||
<AgGridVue
|
||||
style="height: 100%; width: 100%"
|
||||
:theme="themeQuartz"
|
||||
:row-data="props.rows"
|
||||
:column-defs="columnDefs"
|
||||
:row-selection="rowSelectionOptions"
|
||||
:loading="props.loading"
|
||||
:suppress-no-rows-overlay="props.rows.length > 0"
|
||||
@row-clicked="onRowClicked"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ks-grid { min-height: 12rem; border: 1px solid var(--ks-color-neutral-200); border-radius: var(--ks-radius-md); overflow: hidden; background: #fff; }
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import DatePicker from 'primevue/datepicker'
|
||||
import { computed } from 'vue'
|
||||
import FieldShell from './FieldShell.vue'
|
||||
const props = defineProps<{ modelValue: string | Date | null; label: string; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; min?: Date; max?: Date }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string | Date | null]; blur: [event: FocusEvent] }>()
|
||||
const adapter = useUiAdapter()
|
||||
const dateValue = computed(() => typeof props.modelValue === 'string' ? new Date(props.modelValue) : props.modelValue)
|
||||
function handleDateChange(value: Date | Date[] | (Date | null)[] | null | undefined): void {
|
||||
if (value instanceof Date) emit('update:modelValue', value)
|
||||
else if (value == null) emit('update:modelValue', null)
|
||||
else emit('update:modelValue', value[0] ?? null)
|
||||
}
|
||||
</script>
|
||||
<template><FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field"><component :is="adapter.components.DateField" :input-id="field.inputId" :model-value="modelValue" :disabled="disabled" :invalid="field.invalid" :min="min" :max="max" :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" /></FieldShell></template>
|
||||
<template><FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field"><DatePicker :input-id="field.inputId" :model-value="dateValue" :disabled="disabled" :invalid="field.invalid" :min-date="min" :max-date="max" date-format="yy-mm-dd" show-icon :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="handleDateChange" @blur="emit('blur', $event as unknown as FocusEvent)" /></FieldShell></template>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import Dialog from 'primevue/dialog'
|
||||
defineProps<{ visible: boolean; title: string; modal?: boolean; closable?: boolean }>()
|
||||
const emit = defineEmits<{ 'update:visible': [value: boolean] }>()
|
||||
const adapter = useUiAdapter()
|
||||
</script>
|
||||
<template><component :is="adapter.components.Dialog" v-bind="$props" @update:visible="emit('update:visible', $event)"><slot /><template #footer><slot name="footer" /></template></component></template>
|
||||
<template><Dialog class="ks-dialog" :visible="visible" :header="title" :modal="modal ?? true" :closable="closable ?? true" @update:visible="emit('update:visible', $event)"><slot /><template #footer><slot name="footer" /></template></Dialog></template>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { UiSeverity } from '../adapter/contracts'
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import Message from 'primevue/message'
|
||||
withDefaults(defineProps<{ severity?: UiSeverity; title?: string; message: string; dismissible?: boolean }>(), { severity: 'info', dismissible: false })
|
||||
const emit = defineEmits<{ dismiss: [] }>()
|
||||
const adapter = useUiAdapter()
|
||||
const map = { primary: 'info', secondary: 'secondary', success: 'success', info: 'info', warning: 'warn', danger: 'error' } as const
|
||||
</script>
|
||||
<template><component :is="adapter.components.InlineMessage" v-bind="$props" @dismiss="emit('dismiss')" /></template>
|
||||
<template><Message :severity="map[severity]" :closable="dismissible" @close="emit('dismiss')"><strong v-if="title">{{ title }} </strong>{{ message }}</Message></template>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { KbxScreenDefinition, KbxAsyncState, KbxSummaryItem, KbxQuickFilterItem, KbxScreenContext } from '@shared/contracts/kbx-types'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
screen: KbxScreenDefinition
|
||||
dataState?: KbxAsyncState
|
||||
loading?: boolean
|
||||
selectionCount?: number
|
||||
summaryItems?: KbxSummaryItem[]
|
||||
quickFilters?: KbxQuickFilterItem[]
|
||||
context?: KbxScreenContext | null
|
||||
allowActions?: boolean
|
||||
}>(), { dataState: 'ready', loading: false, selectionCount: 0, summaryItems: () => [], quickFilters: () => [], context: null, allowActions: true })
|
||||
|
||||
const emit = defineEmits<{ command: [commandId: string]; quickFilter: [filterId: string]; refresh: [] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ks-list-page">
|
||||
<header class="ks-list-page__header">
|
||||
<div><h1>{{ screen.title }}</h1><p v-if="screen.description">{{ screen.description }}</p></div>
|
||||
<div><slot name="header-actions" /></div>
|
||||
</header>
|
||||
<section v-if="screen.type" class="ks-list-page__search"><slot name="search" /></section>
|
||||
<nav v-if="quickFilters.length" class="ks-list-page__quick-filters" aria-label="빠른 필터">
|
||||
<button v-for="filter in quickFilters" :key="filter.id" type="button" :aria-pressed="filter.active" @click="emit('quickFilter', filter.id)">{{ filter.label }}<span v-if="filter.badge"> {{ filter.badge }}</span></button>
|
||||
</nav>
|
||||
<section v-if="context" class="ks-list-page__context"><slot name="context" /></section>
|
||||
<main class="ks-list-page__content" :aria-busy="loading">
|
||||
<div v-if="dataState === 'pending'">Loading data...</div>
|
||||
<div v-else-if="dataState === 'empty'">No results found <button type="button" @click="emit('command', 'new')">Create New</button></div>
|
||||
<div v-else-if="dataState === 'error'" role="alert">Error loading data <button type="button" @click="emit('refresh')">Retry</button></div>
|
||||
<slot v-else name="content" />
|
||||
</main>
|
||||
<footer v-if="summaryItems.length" class="ks-list-page__footer">
|
||||
<span v-if="selectionCount">{{ selectionCount }} item(s) selected</span>
|
||||
<span v-for="item in summaryItems" :key="item.label"><strong>{{ item.label }}:</strong> {{ item.value }}</span>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ks-list-page { display:flex; flex-direction:column; height:100%; background:var(--ks-color-surface,#fff); }
|
||||
.ks-list-page__header,.ks-list-page__search,.ks-list-page__quick-filters,.ks-list-page__footer { padding:var(--ks-space-4); border-bottom:1px solid var(--ks-color-neutral-200); }
|
||||
.ks-list-page__header { display:flex; justify-content:space-between; gap:var(--ks-space-4); background:var(--ks-color-neutral-50); }
|
||||
.ks-list-page__header h1 { margin:0; }
|
||||
.ks-list-page__header p { margin:.5rem 0 0; color:var(--ks-color-text-muted); }
|
||||
.ks-list-page__quick-filters { display:flex; gap:var(--ks-space-2); overflow:auto; }
|
||||
.ks-list-page__quick-filters button { padding:.5rem .75rem; border:1px solid var(--ks-color-neutral-300); border-radius:var(--ks-radius-sm); background:#fff; }
|
||||
.ks-list-page__quick-filters button[aria-pressed='true'] { background:var(--ks-color-action); color:#fff; }
|
||||
.ks-list-page__content { flex:1; overflow:auto; }
|
||||
.ks-list-page__footer { display:flex; gap:var(--ks-space-4); flex-wrap:wrap; background:var(--ks-color-neutral-50); }
|
||||
</style>
|
||||
@@ -1,15 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import FieldShell from './FieldShell.vue'
|
||||
const props = withDefaults(defineProps<{ modelValue: number | null; label: string; currency?: string; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; min?: number; max?: number }>(), { currency: 'KRW' })
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: number | null]; blur: [event: FocusEvent] }>()
|
||||
const adapter = useUiAdapter()
|
||||
</script>
|
||||
<template>
|
||||
<FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field">
|
||||
<span class="ks-money-field">
|
||||
<component
|
||||
:is="adapter.components.NumberField"
|
||||
<InputNumber
|
||||
:input-id="field.inputId"
|
||||
:model-value="modelValue"
|
||||
:disabled="disabled"
|
||||
@@ -21,7 +19,7 @@ const adapter = useUiAdapter()
|
||||
:aria-describedby="field.describedBy"
|
||||
:aria-required="field.required || undefined"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@blur="emit('blur', $event)"
|
||||
@blur="emit('blur', $event as unknown as FocusEvent)"
|
||||
/>
|
||||
<span class="ks-money-field__currency" aria-hidden="true">{{ props.currency }}</span>
|
||||
</span>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { UiSelectOption } from '../adapter/contracts'
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import MultiSelect from 'primevue/multiselect'
|
||||
withDefaults(defineProps<{ modelValue?: Array<string | number | boolean | null>; options: UiSelectOption[]; label?: string; disabled?: boolean; required?: boolean }>(), { modelValue: () => [] })
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: Array<string | number | boolean | null>] }>()
|
||||
const adapter = useUiAdapter()
|
||||
</script>
|
||||
<template><component :is="adapter.components.MultiSelect" v-bind="$props" @update:model-value="emit('update:modelValue', $event)" /></template>
|
||||
<template><label class="ks-field"><span v-if="label">{{ label }}<b v-if="required" aria-hidden="true"> *</b></span><MultiSelect class="ks-multi-select" :model-value="modelValue" :options="options" option-label="label" option-value="value" option-disabled="disabled" :disabled="disabled" @update:model-value="emit('update:modelValue', $event)" /></label></template>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import FieldShell from './FieldShell.vue'
|
||||
const props = defineProps<{ modelValue: number | null; label: string; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; min?: number; max?: number; minFractionDigits?: number; maxFractionDigits?: number }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: number | null]; blur: [event: FocusEvent] }>()
|
||||
const adapter = useUiAdapter()
|
||||
</script>
|
||||
<template><FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field"><component :is="adapter.components.NumberField" :input-id="field.inputId" :model-value="modelValue" :disabled="disabled" :invalid="field.invalid" :min="min" :max="max" :min-fraction-digits="minFractionDigits" :max-fraction-digits="maxFractionDigits" :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" /></FieldShell></template>
|
||||
<template><FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field"><InputNumber :input-id="field.inputId" :model-value="modelValue" :disabled="disabled" :invalid="field.invalid" :min="min" :max="max" :min-fraction-digits="minFractionDigits" :max-fraction-digits="maxFractionDigits" :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event as unknown as FocusEvent)" /></FieldShell></template>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import Paginator from 'primevue/paginator'
|
||||
withDefaults(defineProps<{ page: number; pageSize: number; total: number; pageSizes?: number[]; disabled?: boolean }>(), { pageSizes: () => [20, 50, 100], disabled: false })
|
||||
const emit = defineEmits<{ pageChange: [value: { page: number; pageSize: number }] }>()
|
||||
const adapter = useUiAdapter()
|
||||
</script>
|
||||
<template><component :is="adapter.components.Paginator" v-bind="$props" @page-change="emit('pageChange', $event)" /></template>
|
||||
<template><Paginator :first="(page - 1) * pageSize" :rows="pageSize" :total-records="total" :rows-per-page-options="pageSizes" :disabled="disabled" @page="emit('pageChange', { page: $event.page + 1, pageSize: $event.rows })" /></template>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import FieldShell from './FieldShell.vue'
|
||||
const props = withDefaults(defineProps<{ modelValue: number | null; label: string; unit?: string; availableQuantity?: number; allowNegative?: boolean; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string }>(), { unit: '주', allowNegative: false })
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: number | null]; blur: [event: FocusEvent] }>()
|
||||
const adapter = useUiAdapter()
|
||||
|
||||
const exceedsAvailable = computed(() =>
|
||||
props.modelValue != null && props.availableQuantity != null && props.modelValue > props.availableQuantity
|
||||
@@ -15,8 +14,7 @@ const effectiveHelp = computed(() => props.availableQuantity != null ? `가용 $
|
||||
<template>
|
||||
<FieldShell :label="label" :input-id="inputId" :required="required" :error="effectiveError" :help="effectiveHelp" v-slot="field">
|
||||
<span class="ks-quantity-field">
|
||||
<component
|
||||
:is="adapter.components.NumberField"
|
||||
<InputNumber
|
||||
:input-id="field.inputId"
|
||||
:model-value="modelValue"
|
||||
:disabled="disabled"
|
||||
@@ -27,7 +25,7 @@ const effectiveHelp = computed(() => props.availableQuantity != null ? `가용 $
|
||||
:aria-describedby="field.describedBy"
|
||||
:aria-required="field.required || undefined"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@blur="emit('blur', $event)"
|
||||
@blur="emit('blur', $event as unknown as FocusEvent)"
|
||||
/>
|
||||
<span class="ks-quantity-field__unit" aria-hidden="true">{{ props.unit }}</span>
|
||||
</span>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { UiSelectOption } from '../adapter/contracts'
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import Select from 'primevue/select'
|
||||
import FieldShell from './FieldShell.vue'
|
||||
const props = defineProps<{ modelValue: unknown; label: string; options: UiSelectOption[]; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; placeholder?: string }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: unknown]; blur: [event: FocusEvent] }>()
|
||||
const adapter = useUiAdapter()
|
||||
</script>
|
||||
<template>
|
||||
<FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field">
|
||||
<component :is="adapter.components.Select" :input-id="field.inputId" :model-value="modelValue" :options="options" :disabled="disabled" :invalid="field.invalid" :placeholder="placeholder" :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" />
|
||||
<Select class="ks-select" :input-id="field.inputId" :model-value="modelValue" :options="options" option-label="label" option-value="value" option-disabled="disabled" :disabled="disabled" :invalid="field.invalid" :placeholder="placeholder" :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event as unknown as FocusEvent)" />
|
||||
</FieldShell>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { UiSeverity } from '../adapter/contracts'
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
defineProps<{ value: string; severity?: UiSeverity; iconLabel?: string }>()
|
||||
const adapter = useUiAdapter()
|
||||
import type { UiGridStatusSemantic } from '../gridStatus'
|
||||
import Tag from 'primevue/tag'
|
||||
defineProps<{ value: string; severity?: UiSeverity; iconLabel?: string; semantic?: UiGridStatusSemantic; unknown?: boolean }>()
|
||||
</script>
|
||||
<template><component :is="adapter.components.StatusTag" v-bind="$props" /></template>
|
||||
<template><Tag class="ks-status-tag" :severity="severity ?? 'info'" :data-semantic="semantic" :data-unknown="unknown || undefined" :aria-label="unknown ? `정의되지 않음: ${value}` : value"><span aria-hidden="true">{{ iconLabel ?? (unknown ? '?' : '●') }}</span><span>{{ value }}</span></Tag></template>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { UiTabItem } from '../adapter/contracts'
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
withDefaults(defineProps<{ modelValue: string; items: UiTabItem[]; ariaLabel?: string }>(), { ariaLabel: '탭' })
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
const adapter = useUiAdapter()
|
||||
</script>
|
||||
<template><component :is="adapter.components.Tabs" v-bind="$props" @update:model-value="emit('update:modelValue', $event)"><template #default="slotProps"><slot :active-id="slotProps.activeId" /></template></component></template>
|
||||
<template><div><div class="ks-tabs" role="tablist" :aria-label="ariaLabel"><button v-for="item in items" :key="item.id" type="button" role="tab" :aria-selected="modelValue === item.id" :disabled="item.disabled" @click="emit('update:modelValue', item.id)">{{ item.label }}<small v-if="item.badge"> {{ item.badge }}</small></button></div><div role="tabpanel"><slot :active-id="modelValue" /></div></div></template>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import FieldShell from './FieldShell.vue'
|
||||
const props = defineProps<{ modelValue: string; label: string; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; rows?: number; placeholder?: string }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string]; blur: [event: FocusEvent] }>()
|
||||
const adapter = useUiAdapter()
|
||||
</script>
|
||||
<template>
|
||||
<FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field">
|
||||
<component :is="adapter.components.TextArea" :input-id="field.inputId" :model-value="modelValue" :disabled="disabled" :invalid="field.invalid" :rows="rows" :placeholder="placeholder" :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" />
|
||||
<Textarea class="ks-textarea" :id="field.inputId" :model-value="modelValue" :disabled="disabled" :invalid="field.invalid" :rows="rows ?? 4" :placeholder="placeholder" :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="emit('update:modelValue', String($event ?? ''))" @blur="emit('blur', $event)" />
|
||||
</FieldShell>
|
||||
</template>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import FieldShell from './FieldShell.vue'
|
||||
import type { UiTextFieldType } from '../adapter/contracts'
|
||||
|
||||
const props = defineProps<{ modelValue: string; label: string; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; placeholder?: string }>()
|
||||
const props = defineProps<{ modelValue: string; label: string; inputId?: string; type?: UiTextFieldType; disabled?: boolean; required?: boolean; error?: string; help?: string; placeholder?: string }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string]; blur: [event: FocusEvent] }>()
|
||||
const adapter = useUiAdapter()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field">
|
||||
<component
|
||||
:is="adapter.components.TextField"
|
||||
<InputText
|
||||
:input-id="field.inputId"
|
||||
:model-value="modelValue"
|
||||
:type="type"
|
||||
:disabled="disabled"
|
||||
:invalid="field.invalid"
|
||||
:placeholder="placeholder"
|
||||
:aria-describedby="field.describedBy"
|
||||
:aria-required="field.required || undefined"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@update:model-value="emit('update:modelValue', String($event ?? ''))"
|
||||
@blur="emit('blur', $event)"
|
||||
/>
|
||||
</FieldShell>
|
||||
|
||||
@@ -14,6 +14,7 @@ export { default as KsInlineMessage } from './KsInlineMessage.vue'
|
||||
export { default as KsPaginator } from './KsPaginator.vue'
|
||||
export { default as KsTabs } from './KsTabs.vue'
|
||||
export { default as KsDataGrid } from './KsDataGrid.vue'
|
||||
export { default as KsListPage } from './KsListPage.vue'
|
||||
export { default as FieldShell } from './FieldShell.vue'
|
||||
export { default as KsDataContextHeader } from './KsDataContextHeader.vue'
|
||||
export { default as KsCommandBar } from './KsCommandBar.vue'
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
function componentSources(root: string): string[] {
|
||||
return readdirSync(root, { withFileTypes: true })
|
||||
.filter(entry => entry.isFile() && entry.name.endsWith('.vue'))
|
||||
.map(entry => join(root, entry.name))
|
||||
}
|
||||
|
||||
describe('existing component ownership boundary', () => {
|
||||
it('prevents existing components from regressing to adapter-owned rendering', () => {
|
||||
const root = join(process.cwd(), 'src', 'shared', 'ui', 'components')
|
||||
const forbidden = /useUiAdapter|adapter\.components/
|
||||
const violations = componentSources(root).filter(path => forbidden.test(readFileSync(path, 'utf8')))
|
||||
|
||||
expect(violations).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('grid provider policy', () => {
|
||||
it('uses AG Grid Community as the default direct grid implementation', () => {
|
||||
const packageJson = readFileSync('package.json', 'utf8')
|
||||
const source = readFileSync('src/shared/ui/components/KsDataGrid.vue', 'utf8')
|
||||
|
||||
expect(packageJson).toContain('"ag-grid-community"')
|
||||
expect(packageJson).not.toContain('ag-grid-enterprise')
|
||||
expect(source).toContain("from 'ag-grid-community'")
|
||||
expect(source).toContain('ClientSideRowModelModule')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user