feat(fe): Add form field navigation - Enter key moves to next field
- Create useFormFieldNavigation composable for Tab-like Enter behavior - KsTextField: Enter -> next field - KsTextArea: Ctrl+Enter for newline, Enter -> next field - KsSelect: Enter -> next field after selection - Implements standard form navigation pattern across input components
This commit is contained in:
@@ -1,3 +1,64 @@
|
||||
<script setup lang="ts">withDefaults(defineProps<{ asideWidth?: string; dense?: boolean }>(), { asideWidth: '24rem', dense: false })</script>
|
||||
<template><section class="ks-crud-workspace" :class="{ dense }" :style="{ '--ks-crud-aside': asideWidth }"><header v-if="$slots.toolbar"><slot name="toolbar" /></header><div class="ks-crud-workspace__body" :class="{ 'has-aside': $slots.aside }"><main><slot /></main><aside v-if="$slots.aside"><slot name="aside" /></aside></div><footer v-if="$slots.actions"><slot name="actions" /></footer></section></template>
|
||||
<style scoped>.ks-crud-workspace{display:grid;gap:var(--ks-space-3)}.ks-crud-workspace__body{display:grid;gap:var(--ks-space-4);min-width:0}.ks-crud-workspace__body.has-aside{grid-template-columns:minmax(0,1fr) var(--ks-crud-aside)}main,aside{min-width:0}.dense{gap:var(--ks-space-2)}@media(max-width:1100px){.ks-crud-workspace__body.has-aside{grid-template-columns:1fr}}</style>
|
||||
<script setup lang="ts">
|
||||
import KsSplitter from '../components/KsSplitter.vue'
|
||||
withDefaults(defineProps<{ asideWidth?: string; dense?: boolean; storageKey?: string; initialRatio?: number }>(), {
|
||||
asideWidth: '28rem',
|
||||
dense: false,
|
||||
storageKey: 'ks_crud_splitter_ratio',
|
||||
initialRatio: 50,
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="ks-crud-workspace" :class="{ dense }">
|
||||
<header v-if="$slots.toolbar"><slot name="toolbar" /></header>
|
||||
<div v-if="$slots.aside" class="ks-crud-workspace__body has-aside">
|
||||
<KsSplitter :storage-key="storageKey" :initial-ratio="initialRatio">
|
||||
<template #left>
|
||||
<main class="ks-crud-main"><slot /></main>
|
||||
</template>
|
||||
<template #right>
|
||||
<aside class="ks-crud-aside"><slot name="aside" /></aside>
|
||||
</template>
|
||||
</KsSplitter>
|
||||
</div>
|
||||
<div v-else class="ks-crud-workspace__body single">
|
||||
<main class="ks-crud-main"><slot /></main>
|
||||
</div>
|
||||
<footer v-if="$slots.actions"><slot name="actions" /></footer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ks-crud-workspace {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ks-space-3);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ks-crud-workspace__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ks-crud-main,
|
||||
.ks-crud-aside {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dense {
|
||||
gap: var(--ks-space-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,2 +1,79 @@
|
||||
<template><div class="ks-dashboard"><section class="ks-dashboard__kpis"><slot name="kpis" /></section><section class="ks-dashboard__primary ks-card"><slot name="primary" /></section><section class="ks-dashboard__secondary ks-card"><slot name="secondary" /></section><section class="ks-dashboard__alerts ks-card"><slot name="alerts" /></section></div></template>
|
||||
<style scoped>.ks-dashboard { display: grid; grid-template-columns: 2fr 1fr; gap: var(--ks-space-4); }.ks-dashboard__kpis { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: var(--ks-space-3); }.ks-dashboard__primary,.ks-dashboard__secondary,.ks-dashboard__alerts { padding: var(--ks-space-4); }.ks-dashboard__alerts { grid-column: 1 / -1; }@media (max-width: 1100px) { .ks-dashboard { grid-template-columns: 1fr; } }</style>
|
||||
<template>
|
||||
<div class="ks-dashboard">
|
||||
<section v-if="$slots.kpis" class="ks-dashboard__kpis">
|
||||
<slot name="kpis" />
|
||||
</section>
|
||||
|
||||
<div class="ks-dashboard__body">
|
||||
<section v-if="$slots.primary" class="ks-dashboard__primary ks-card">
|
||||
<slot name="primary" />
|
||||
</section>
|
||||
|
||||
<div class="ks-dashboard__side">
|
||||
<section v-if="$slots.secondary" class="ks-dashboard__secondary ks-card">
|
||||
<slot name="secondary" />
|
||||
</section>
|
||||
<section v-if="$slots.alerts" class="ks-dashboard__alerts ks-card">
|
||||
<slot name="alerts" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ks-dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
gap: var(--ks-space-3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ks-dashboard__kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
|
||||
gap: var(--ks-space-3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ks-dashboard__body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 360px;
|
||||
gap: var(--ks-space-3);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ks-dashboard__primary {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: var(--ks-space-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ks-dashboard__side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ks-space-3);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ks-dashboard__secondary,
|
||||
.ks-dashboard__alerts {
|
||||
padding: var(--ks-space-3);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.ks-dashboard__body {
|
||||
grid-template-columns: 1fr;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,4 +5,30 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">defineEmits<{ submit: [] }>()</script>
|
||||
<style scoped>.ks-form-layout { display: grid; grid-template-columns: minmax(0, 1fr) var(--ks-preview-width); gap: var(--ks-space-4); align-items: start; flex: 1; min-height: 0; height: 100%; } .ks-form-layout__form,.ks-form-layout__preview { padding: var(--ks-space-4); overflow-y: auto; min-height: 0; } @media (max-width: 1100px) { .ks-form-layout { grid-template-columns: 1fr; } }</style>
|
||||
<style scoped>
|
||||
.ks-form-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) var(--ks-preview-width, 24rem);
|
||||
gap: var(--ks-space-4);
|
||||
align-items: stretch;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ks-form-layout__form,
|
||||
.ks-form-layout__preview {
|
||||
padding: var(--ks-space-4);
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.ks-form-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { provideFieldLayout } from '../contracts/layoutContext'
|
||||
defineProps<{ title: string; subtitle?: string; status?: string; asOf?: string; version?: string; asideWidth?: string }>()
|
||||
const showHelp = ref(false)
|
||||
provideFieldLayout('filter')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="ks-page" :style="{ '--ks-aside-width': asideWidth ?? '22rem' }">
|
||||
<header class="ks-page__header">
|
||||
@@ -35,12 +38,23 @@ const showHelp = ref(false)
|
||||
<div class="ks-page__content"><slot /></div>
|
||||
<aside v-if="$slots.aside" class="ks-page__aside"><slot name="aside" /></aside>
|
||||
</div>
|
||||
<footer v-if="$slots.footer" class="ks-page__footer"><slot name="footer" /></footer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Flex-based page layout with explicit workspace height binding */
|
||||
.ks-page { width: 100%; max-width: var(--ks-content-max); margin: 0 auto; height: 100%; flex: 1; display: flex; flex-direction: column; }
|
||||
.ks-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ks-space-2);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ks-page__header { display: flex; align-items: center; justify-content: space-between; gap: var(--ks-space-4); flex-shrink: 0; min-height: 32px; border-bottom: 1px solid var(--ks-color-neutral-200); padding-bottom: var(--ks-space-2); }
|
||||
.ks-page__title-group { display: flex; flex-direction: column; gap: 2px; }
|
||||
.ks-page__breadcrumb { font-size: 11px; color: var(--ks-color-text-muted); line-height: 14px; }
|
||||
@@ -71,21 +85,22 @@ const showHelp = ref(false)
|
||||
.ks-page__command-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
justify-content: flex-end;
|
||||
padding: 4px 8px;
|
||||
background: var(--ks-color-surface);
|
||||
border: 1px solid var(--ks-color-border);
|
||||
border-radius: var(--ks-radius-sm);
|
||||
flex-shrink: 0;
|
||||
min-height: 34px;
|
||||
box-sizing: border-border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ks-page__command-bar-group { display: flex; align-items: center; gap: var(--ks-space-2); flex-wrap: wrap; }
|
||||
.ks-page__command-bar-group { display: flex; align-items: center; gap: var(--ks-space-2); flex-wrap: wrap; margin-left: auto; }
|
||||
|
||||
.ks-page__actions { display: flex; align-items: center; gap: var(--ks-space-2); flex-shrink: 0; }
|
||||
.ks-page__summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: var(--ks-space-3); flex-shrink: 0; }
|
||||
.ks-page__filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-end;
|
||||
gap: var(--ks-space-3);
|
||||
padding: var(--ks-space-2) var(--ks-space-3);
|
||||
flex-shrink: 0;
|
||||
@@ -93,7 +108,20 @@ const showHelp = ref(false)
|
||||
background: var(--ks-color-surface);
|
||||
border: 1px solid var(--ks-color-border);
|
||||
border-radius: var(--ks-radius-sm);
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.ks-page__filters:empty,
|
||||
.ks-page__command-bar:empty,
|
||||
.ks-page__summary:empty {
|
||||
display: none !important;
|
||||
min-height: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
|
||||
.ks-page__filters input,
|
||||
.ks-page__filters select {
|
||||
height: 34px;
|
||||
|
||||
Reference in New Issue
Block a user