gui changes

This commit is contained in:
Sylvain Schneider
2026-05-11 23:59:20 +02:00
parent d4248775b9
commit 8aa3128edf
10 changed files with 374 additions and 222 deletions

View File

@@ -1,16 +1,40 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import AppMenubar from './components/AppMenubar.vue' import AppMenubar from './components/AppMenubar.vue'
import AppToolbar from './components/AppToolbar.vue' import AppToolbar from './components/AppToolbar.vue'
import AppStatusbar from './components/AppStatusbar.vue' import AppStatusbar from './components/AppStatusbar.vue'
import RequirementsView from './views/RequirementsView.vue'
const route = useRoute()
const router = useRouter()
const activeView = computed<'home' | 'requirements'>(() => {
return route.name === 'home' ? 'home' : 'requirements'
})
const goHome = () => {
router.push({ name: 'home' })
}
const goRequirements = () => {
router.push({ name: 'requirements' })
}
</script> </script>
<template> <template>
<div id="app"> <div id="app">
<AppMenubar /> <AppMenubar
<AppToolbar /> :active-view="activeView"
@home="goHome"
@requirements="goRequirements"
/>
<AppToolbar
:active-view="activeView"
@home="goHome"
@requirements="goRequirements"
/>
<main class="app-main"> <main class="app-main">
<RequirementsView /> <RouterView />
</main> </main>
<AppStatusbar /> <AppStatusbar />
</div> </div>

View File

@@ -1,9 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { computed } from 'vue'
import Menubar from 'primevue/menubar' import Menubar from 'primevue/menubar'
import type { MenuItem } from 'primevue/menuitem' import type { MenuItem } from 'primevue/menuitem'
const menuItems = ref<MenuItem[]>([ interface Props {
activeView: 'home' | 'requirements'
}
const props = defineProps<Props>()
const emit = defineEmits<{
home: []
requirements: []
}>()
const menuItems = computed<MenuItem[]>(() => [
{ {
label: 'File', label: 'File',
items: [ items: [
@@ -91,6 +102,24 @@ const menuItems = ref<MenuItem[]>([
}, },
], ],
}, },
{
label: 'Windows',
items: [
{
label: 'Home',
icon: props.activeView === 'home' ? 'pi pi-fw pi-check' : 'pi pi-fw pi-home',
command: () => emit('home'),
},
{
label: 'Requirements',
icon:
props.activeView === 'requirements'
? 'pi pi-fw pi-check'
: 'pi pi-fw pi-list-check',
command: () => emit('requirements'),
},
],
},
{ {
label: 'Tools', label: 'Tools',
items: [ items: [

View File

@@ -1,22 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import Toolbar from 'primevue/toolbar' import Toolbar from 'primevue/toolbar'
import Button from 'primevue/button' import Button from 'primevue/button'
import Divider from 'primevue/divider' import Divider from 'primevue/divider'
interface Props {
activeView: 'home' | 'requirements'
}
defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
home: []
requirements: []
new: [] new: []
open: [] open: []
save: [] save: []
undo: []
redo: []
}>() }>()
const handleHome = () => emit('home')
const handleRequirements = () => emit('requirements')
const handleNew = () => emit('new') const handleNew = () => emit('new')
const handleOpen = () => emit('open') const handleOpen = () => emit('open')
const handleSave = () => emit('save') const handleSave = () => emit('save')
const handleUndo = () => emit('undo')
const handleRedo = () => emit('redo')
</script> </script>
<template> <template>
@@ -51,20 +56,22 @@ const handleRedo = () => emit('redo')
<Divider layout="vertical" /> <Divider layout="vertical" />
<Button <Button
icon="pi pi-undo" class="nav-button"
icon="pi pi-home"
rounded rounded
text text
severity="secondary" :severity="activeView === 'home' ? 'contrast' : 'secondary'"
@click="handleUndo" @click="handleHome"
v-tooltip="'Undo'" v-tooltip="'Home'"
/> />
<Button <Button
icon="pi pi-redo" class="nav-button"
icon="pi pi-list-check"
rounded rounded
text text
severity="secondary" :severity="activeView === 'requirements' ? 'contrast' : 'secondary'"
@click="handleRedo" @click="handleRequirements"
v-tooltip="'Redo'" v-tooltip="'Requirements'"
/> />
</div> </div>
</template> </template>
@@ -119,6 +126,15 @@ const handleRedo = () => emit('redo')
gap: 0.25rem; gap: 0.25rem;
} }
:deep(.nav-button.p-button) {
transition: background-color 0.2s ease, color 0.2s ease;
}
:deep(.nav-button.p-button-contrast) {
background: rgba(78, 107, 255, 0.15);
color: #1e3a8a;
}
:deep(.p-button.p-button-rounded) { :deep(.p-button.p-button-rounded) {
width: 2.5rem; width: 2.5rem;
height: 2.5rem; height: 2.5rem;

View File

@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from 'primevue/button' import Button from 'primevue/button'
import Card from 'primevue/card'
import Chip from 'primevue/chip' import Chip from 'primevue/chip'
import Divider from 'primevue/divider' import Divider from 'primevue/divider'
import TabView from 'primevue/tabview' import TabView from 'primevue/tabview'
@@ -33,7 +32,6 @@ interface Props {
defineProps<Props>() defineProps<Props>()
// Helper function to get severity color for status
const statusSeverity = (status: Requirement['status']) => { const statusSeverity = (status: Requirement['status']) => {
const map: Record<Requirement['status'], 'secondary' | 'info' | 'success' | 'danger' | 'contrast'> = { const map: Record<Requirement['status'], 'secondary' | 'info' | 'success' | 'danger' | 'contrast'> = {
Draft: 'secondary', Draft: 'secondary',
@@ -45,7 +43,6 @@ const statusSeverity = (status: Requirement['status']) => {
return map[status] return map[status]
} }
// Helper function to get severity color for priority
const prioritySeverity = (priority: Requirement['priority']) => { const prioritySeverity = (priority: Requirement['priority']) => {
const map: Record<Requirement['priority'], 'success' | 'info' | 'warn' | 'danger'> = { const map: Record<Requirement['priority'], 'success' | 'info' | 'warn' | 'danger'> = {
Low: 'success', Low: 'success',
@@ -59,132 +56,123 @@ const prioritySeverity = (priority: Requirement['priority']) => {
<template> <template>
<section class="detail-zone"> <section class="detail-zone">
<Card class="detail-card"> <section class="detail-surface">
<template #title> <header class="detail-card__header">
<div class="detail-card__header"> <div>
<div> <p class="eyebrow">Selected requirement</p>
<p class="eyebrow">Selected requirement</p> <h2>{{ requirement.title }}</h2>
<h2>{{ requirement.title }}</h2> <p class="detail-card__subtitle">
<p class="detail-card__subtitle"> {{ requirement.reference }} - Owner: {{ requirement.owner }}
{{ requirement.reference }} · Owner: {{ requirement.owner }} </p>
</p>
</div>
<div class="detail-actions">
<Button label="Edit" icon="pi pi-pencil" severity="secondary" outlined />
<Button label="Mark as delivered" icon="pi pi-check" />
</div>
</div> </div>
</template>
<template #content> <div class="detail-actions">
<TabView> <Button label="Edit" icon="pi pi-pencil" severity="secondary" outlined />
<TabPanel value="details" header="Details"> </div>
<!-- Status and priority tags --> </header>
<div class="detail-badges">
<Tag :value="requirement.status" :severity="statusSeverity(requirement.status)" />
<Tag :value="requirement.priority" :severity="prioritySeverity(requirement.priority)" rounded />
</div>
<!-- Description --> <TabView class="detail-tabs">
<p class="detail-description">{{ requirement.description }}</p> <TabPanel value="details" header="Details">
<div class="detail-badges">
<Tag :value="requirement.status" :severity="statusSeverity(requirement.status)" />
<Tag :value="requirement.priority" :severity="prioritySeverity(requirement.priority)" rounded />
</div>
<!-- Blockers section (shown only if blockers exist) --> <p class="detail-description">{{ requirement.description }}</p>
<article v-if="requirement.blockers.length" class="info-panel info-panel--warning">
<h3>Identified blockers</h3> <article v-if="requirement.blockers.length" class="info-panel info-panel--warning">
<ul class="blockers-list"> <h3>Identified blockers</h3>
<li v-for="blocker in requirement.blockers" :key="blocker"> <ul class="blockers-list">
<i class="pi pi-exclamation-triangle" /> <li v-for="blocker in requirement.blockers" :key="blocker">
<span>{{ blocker }}</span> <i class="pi pi-exclamation-triangle" />
<span>{{ blocker }}</span>
</li>
</ul>
</article>
<Divider />
<div class="detail-grid">
<article class="info-panel">
<h3>Author</h3>
<p>{{ requirement.owner }}</p>
</article>
<article class="info-panel">
<h3>Stakeholders</h3>
<div class="chip-row">
<Chip v-for="stakeholder in requirement.stakeholders" :key="stakeholder" :label="stakeholder" />
</div>
</article>
<article class="info-panel">
<h3>Flexibility</h3>
<p>{{ requirement.flexibility }}</p>
</article>
<article class="info-panel">
<h3>Tolerances</h3>
<p>{{ requirement.tolerances }}</p>
</article>
</div>
<Divider />
<div class="detail-columns">
<article class="info-panel">
<h3>Acceptance criteria</h3>
<ul class="checklist">
<li v-for="item in requirement.acceptanceCriteria" :key="item">
<i class="pi pi-check-circle" />
<span>{{ item }}</span>
</li> </li>
</ul> </ul>
</article> </article>
<Divider /> <article class="info-panel">
<h3>Impacted modules</h3>
<div class="chip-row">
<Chip v-for="module in requirement.impactedModules" :key="module" :label="module" />
</div>
<!-- Key attributes: Author, Stakeholders, Priority, Flexibility --> <Divider />
<div class="detail-grid">
<article class="info-panel">
<h3>Author</h3>
<p>{{ requirement.owner }}</p>
</article>
<article class="info-panel"> <h3>Remarks</h3>
<h3>Stakeholders</h3> <p>{{ requirement.notes }}</p>
<div class="chip-row"> </article>
<Chip v-for="stakeholder in requirement.stakeholders" :key="stakeholder" :label="stakeholder" /> </div>
</div> </TabPanel>
</article>
<article class="info-panel"> <TabPanel value="tests" header="Tests to perform">
<h3>Flexibility</h3> <div class="tests-placeholder">
<p>{{ requirement.flexibility }}</p> <i class="pi pi-inbox" />
</article> <p>No tests defined yet</p>
</div>
<article class="info-panel"> </TabPanel>
<h3>Tolerances</h3> </TabView>
<p>{{ requirement.tolerances }}</p> </section>
</article>
</div>
<Divider />
<!-- Acceptance criteria and impacted modules -->
<div class="detail-columns">
<article class="info-panel">
<h3>Acceptance criteria</h3>
<ul class="checklist">
<li v-for="item in requirement.acceptanceCriteria" :key="item">
<i class="pi pi-check-circle" />
<span>{{ item }}</span>
</li>
</ul>
</article>
<article class="info-panel">
<h3>Impacted modules</h3>
<div class="chip-row">
<Chip v-for="module in requirement.impactedModules" :key="module" :label="module" />
</div>
<Divider />
<h3>Remarks</h3>
<p>{{ requirement.notes }}</p>
</article>
</div>
</TabPanel>
<TabPanel value="tests" header="Tests to perform">
<div class="tests-placeholder">
<i class="pi pi-inbox" />
<p>No tests defined yet</p>
</div>
</TabPanel>
</TabView>
</template>
</Card>
</section> </section>
</template> </template>
<style scoped> <style scoped>
.detail-zone { .detail-zone {
min-width: 0; min-width: 0;
min-height: 0;
height: 100%;
} }
.detail-card { .detail-surface {
padding: 0.5rem; display: flex;
border: 1px solid rgba(96, 117, 156, 0.16); flex-direction: column;
border-radius: 0.5rem; height: 100%;
padding: 1rem;
border: none;
border-radius: 0;
background: rgba(255, 255, 255, 0.86); background: rgba(255, 255, 255, 0.86);
box-shadow: 0 24px 60px rgba(34, 49, 77, 0.12); box-shadow: 0 24px 60px rgba(34, 49, 77, 0.12);
backdrop-filter: blur(18px); backdrop-filter: blur(18px);
} }
.detail-card :is(.p-card-body) {
padding: 1rem;
}
.detail-card__header { .detail-card__header {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -201,7 +189,7 @@ const prioritySeverity = (priority: Requirement['priority']) => {
color: #5c6b88; color: #5c6b88;
} }
.detail-card h2 { .detail-surface h2 {
margin: 0; margin: 0;
color: #12213a; color: #12213a;
} }
@@ -219,6 +207,25 @@ const prioritySeverity = (priority: Requirement['priority']) => {
gap: 0.5rem; gap: 0.5rem;
} }
.detail-tabs {
min-height: 0;
flex: 1;
}
:deep(.detail-tabs.p-tabview) {
display: flex;
flex-direction: column;
min-height: 0;
}
:deep(.detail-tabs .p-tabview-panels) {
flex: 1;
min-height: 0;
overflow: auto;
padding-left: 0;
padding-right: 0;
}
.detail-badges { .detail-badges {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -265,36 +272,12 @@ const prioritySeverity = (priority: Requirement['priority']) => {
color: #4c5d77; color: #4c5d77;
} }
.info-panel--accent {
background: linear-gradient(180deg, rgba(237, 243, 255, 0.92), rgba(248, 250, 255, 0.95));
}
.info-panel--warning { .info-panel--warning {
margin-top: 0.75rem; margin-top: 0.75rem;
background: linear-gradient(180deg, rgba(255, 244, 236, 0.95), rgba(255, 249, 244, 0.95)); background: linear-gradient(180deg, rgba(255, 244, 236, 0.95), rgba(255, 249, 244, 0.95));
border-color: rgba(223, 134, 57, 0.24); border-color: rgba(223, 134, 57, 0.24);
} }
.info-panel dl {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
margin: 0.75rem 0 1rem;
}
.info-panel dt {
color: #73829d;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.info-panel dd {
margin: 0.2rem 0 0;
color: #12213a;
font-weight: 700;
}
.checklist, .checklist,
.blockers-list { .blockers-list {
display: grid; display: grid;
@@ -343,11 +326,6 @@ const prioritySeverity = (priority: Requirement['priority']) => {
font-size: 2.5rem; font-size: 2.5rem;
} }
:deep(.p-tabview-panels) {
padding-left: 0;
padding-right: 0;
}
@media (max-width: 1120px) { @media (max-width: 1120px) {
.detail-grid, .detail-grid,
.detail-columns { .detail-columns {
@@ -366,9 +344,11 @@ const prioritySeverity = (priority: Requirement['priority']) => {
} }
.detail-grid, .detail-grid,
.detail-columns, .detail-columns {
.info-panel dl {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
</style> </style>

View File

@@ -184,8 +184,8 @@ const onNodeSelect = (node: TreeNode) => {
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
padding: 0.75rem; padding: 0.75rem;
border: 1px solid rgba(96, 117, 156, 0.16); border: none;
border-radius: 0.5rem; border-radius: 0;
background: rgba(255, 255, 255, 0.86); background: rgba(255, 255, 255, 0.86);
box-shadow: 0 24px 60px rgba(34, 49, 77, 0.12); box-shadow: 0 24px 60px rgba(34, 49, 77, 0.12);
backdrop-filter: blur(18px); backdrop-filter: blur(18px);
@@ -330,3 +330,5 @@ const onNodeSelect = (node: TreeNode) => {
} }
} }
</style> </style>

View File

@@ -7,10 +7,12 @@ import PrimeVue from 'primevue/config'
import Aura from '@primeuix/themes/aura' import Aura from '@primeuix/themes/aura'
import 'primeicons/primeicons.css' import 'primeicons/primeicons.css'
import App from './App.vue' import App from './App.vue'
import router from './router'
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router)
app.use(PrimeVue, { app.use(PrimeVue, {
theme: { theme: {

View File

@@ -0,0 +1,9 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import { routes } from './routes'
const router = createRouter({
history: createWebHashHistory(),
routes,
})
export default router

View File

@@ -0,0 +1,20 @@
import type { RouteRecordRaw } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import RequirementsView from '../views/RequirementsView.vue'
export const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/requirements',
name: 'requirements',
component: RequirementsView,
},
{
path: '/:pathMatch(.*)*',
redirect: { name: 'home' },
},
]

View File

@@ -0,0 +1,58 @@
<template>
<section class="home-view">
<section class="home-panel">
<p class="eyebrow">Home</p>
<h1>Requirements Workspace</h1>
<p class="subtitle">
Use the toolbar icons to switch between this home page and the requirements dashboard.
</p>
</section>
</section>
</template>
<style scoped>
.home-view {
display: flex;
height: 100%;
padding: 0;
background:
radial-gradient(circle at top left, rgba(78, 107, 255, 0.18), transparent 34%),
radial-gradient(circle at top right, rgba(21, 184, 164, 0.14), transparent 28%),
linear-gradient(180deg, rgba(245, 248, 255, 1) 0%, rgba(235, 241, 250, 1) 100%);
}
.home-panel {
flex: 1;
min-height: 0;
border: none;
background: rgba(255, 255, 255, 0.86);
box-shadow: 0 24px 60px rgba(34, 49, 77, 0.12);
backdrop-filter: blur(18px);
padding: 1.5rem;
}
.eyebrow {
margin: 0;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 0.72rem;
font-weight: 700;
color: #5c6b88;
}
h1 {
margin: 0.5rem 0 0;
color: #12213a;
}
.subtitle {
margin-top: 0.75rem;
color: #4c5d77;
}
@media (max-width: 760px) {
.home-panel {
padding: 1rem;
}
}
</style>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, onBeforeUnmount } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel'
import RequirementsTreeList from '../components/RequirementsTreeList.vue' import RequirementsTreeList from '../components/RequirementsTreeList.vue'
import RequirementDetail from '../components/RequirementDetail.vue' import RequirementDetail from '../components/RequirementDetail.vue'
@@ -13,7 +15,20 @@ const requirementsStore = useRequirementsStore()
const { requirements, selectedId, searchQuery, selectedRequirement, stats } = const { requirements, selectedId, searchQuery, selectedRequirement, stats } =
storeToRefs(requirementsStore) storeToRefs(requirementsStore)
// Helper function to create tree nodes from requirements const splitterDraggingClass = 'is-splitter-dragging'
const onSplitterResizeStart = () => {
document.body.classList.add(splitterDraggingClass)
}
const onSplitterResizeEnd = () => {
document.body.classList.remove(splitterDraggingClass)
}
onBeforeUnmount(() => {
document.body.classList.remove(splitterDraggingClass)
})
const buildTreeData = (): TreeNode[] => { const buildTreeData = (): TreeNode[] => {
return [ return [
{ {
@@ -70,24 +85,35 @@ const treeData = computed(() => buildTreeData())
<template> <template>
<section class="requirements-view"> <section class="requirements-view">
<section class="workspace"> <Splitter
<RequirementsTreeList class="workspace-splitter"
:tree-data="treeData" :gutter-size="6"
:searchQuery="searchQuery" @resizestart="onSplitterResizeStart"
:selectedId="selectedId" @resizeend="onSplitterResizeEnd"
:stats="stats" >
@update:searchQuery="searchQuery = $event" <SplitterPanel :size="28" :min-size="20">
@update:selectedId="selectedId = $event" <RequirementsTreeList
/> :tree-data="treeData"
:searchQuery="searchQuery"
:selectedId="selectedId"
:stats="stats"
@update:searchQuery="searchQuery = $event"
@update:selectedId="selectedId = $event"
/>
</SplitterPanel>
<RequirementDetail :requirement="selectedRequirement" /> <SplitterPanel :size="72" :min-size="35">
</section> <RequirementDetail :requirement="selectedRequirement" />
</SplitterPanel>
</Splitter>
</section> </section>
</template> </template>
<style scoped> <style scoped>
.requirements-view { .requirements-view {
padding: 2rem; display: flex;
flex-direction: column;
padding: 0;
min-height: 100%; min-height: 100%;
background: background:
radial-gradient(circle at top left, rgba(78, 107, 255, 0.18), transparent 34%), radial-gradient(circle at top left, rgba(78, 107, 255, 0.18), transparent 34%),
@@ -95,74 +121,60 @@ const treeData = computed(() => buildTreeData())
linear-gradient(180deg, rgba(245, 248, 255, 1) 0%, rgba(235, 241, 250, 1) 100%); linear-gradient(180deg, rgba(245, 248, 255, 1) 0%, rgba(235, 241, 250, 1) 100%);
} }
.hero-card { .workspace-splitter {
display: flex; flex: 1;
align-items: flex-end; height: 100%;
justify-content: space-between; min-height: 0;
gap: 1rem;
margin-bottom: 1rem;
padding: 1rem 1.25rem;
border: 1px solid rgba(96, 117, 156, 0.16);
border-radius: 0.5rem;
background: rgba(255, 255, 255, 0.86);
box-shadow: 0 24px 60px rgba(34, 49, 77, 0.12);
backdrop-filter: blur(18px);
} }
.hero-card h1 { .workspace-splitter :deep(.p-splitter-panel) {
margin: 0.35rem 0 0.65rem; min-height: 0;
font-size: clamp(1.8rem, 3vw, 2.7rem);
font-weight: 700;
line-height: 1.05;
color: #12213a;
} }
.hero-card__text { .workspace-splitter :deep(.p-splitter-gutter) {
max-width: 60ch; position: relative;
color: #51627f; background: transparent;
cursor: col-resize;
} }
.hero-actions { .workspace-splitter :deep(.p-splitter-gutter::before) {
display: flex; content: '';
flex-wrap: wrap; position: absolute;
justify-content: flex-end; top: 8px;
gap: 0.75rem; bottom: 8px;
left: 50%;
width: 2px;
transform: translateX(-50%);
border-radius: 999px;
background: rgba(96, 117, 156, 0.32);
transition: background-color 0.2s ease;
} }
.eyebrow { .workspace-splitter :deep(.p-splitter-gutter:hover::before),
margin: 0; .workspace-splitter :deep(.p-splitter-gutter:active::before) {
text-transform: uppercase; background: rgba(78, 107, 255, 0.6);
letter-spacing: 0.14em;
font-size: 0.72rem;
font-weight: 700;
color: #5c6b88;
} }
.workspace { .workspace-splitter :deep(.p-splitter-gutter-handle) {
display: grid; width: 0;
grid-template-columns: minmax(18rem, 22rem) minmax(0, 1fr); height: 0;
gap: 1rem; }
align-items: start;
:global(body.is-splitter-dragging),
:global(body.is-splitter-dragging *) {
user-select: none;
-webkit-user-select: none;
} }
@media (max-width: 1120px) { @media (max-width: 1120px) {
.workspace { .workspace-splitter {
grid-template-columns: 1fr; height: 100%;
} }
} }
@media (max-width: 760px) { @media (max-width: 760px) {
.requirements-view { .requirements-view {
padding: 1rem; padding: 0;
}
.hero-card {
flex-direction: column;
align-items: stretch;
}
.hero-actions {
justify-content: flex-start;
} }
} }
</style> </style>