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">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import AppMenubar from './components/AppMenubar.vue'
import AppToolbar from './components/AppToolbar.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>
<template>
<div id="app">
<AppMenubar />
<AppToolbar />
<AppMenubar
:active-view="activeView"
@home="goHome"
@requirements="goRequirements"
/>
<AppToolbar
:active-view="activeView"
@home="goHome"
@requirements="goRequirements"
/>
<main class="app-main">
<RequirementsView />
<RouterView />
</main>
<AppStatusbar />
</div>

View File

@@ -1,9 +1,20 @@
<script setup lang="ts">
import { ref } from 'vue'
import { computed } from 'vue'
import Menubar from 'primevue/menubar'
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',
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',
items: [

View File

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

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Card from 'primevue/card'
import Chip from 'primevue/chip'
import Divider from 'primevue/divider'
import TabView from 'primevue/tabview'
@@ -33,7 +32,6 @@ interface Props {
defineProps<Props>()
// Helper function to get severity color for status
const statusSeverity = (status: Requirement['status']) => {
const map: Record<Requirement['status'], 'secondary' | 'info' | 'success' | 'danger' | 'contrast'> = {
Draft: 'secondary',
@@ -45,7 +43,6 @@ const statusSeverity = (status: Requirement['status']) => {
return map[status]
}
// Helper function to get severity color for priority
const prioritySeverity = (priority: Requirement['priority']) => {
const map: Record<Requirement['priority'], 'success' | 'info' | 'warn' | 'danger'> = {
Low: 'success',
@@ -59,37 +56,30 @@ const prioritySeverity = (priority: Requirement['priority']) => {
<template>
<section class="detail-zone">
<Card class="detail-card">
<template #title>
<div class="detail-card__header">
<section class="detail-surface">
<header class="detail-card__header">
<div>
<p class="eyebrow">Selected requirement</p>
<h2>{{ requirement.title }}</h2>
<p class="detail-card__subtitle">
{{ requirement.reference }} · Owner: {{ requirement.owner }}
{{ requirement.reference }} - Owner: {{ requirement.owner }}
</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>
</template>
</header>
<template #content>
<TabView>
<TabView class="detail-tabs">
<TabPanel value="details" header="Details">
<!-- Status and priority tags -->
<div class="detail-badges">
<Tag :value="requirement.status" :severity="statusSeverity(requirement.status)" />
<Tag :value="requirement.priority" :severity="prioritySeverity(requirement.priority)" rounded />
</div>
<!-- Description -->
<p class="detail-description">{{ requirement.description }}</p>
<!-- Blockers section (shown only if blockers exist) -->
<article v-if="requirement.blockers.length" class="info-panel info-panel--warning">
<h3>Identified blockers</h3>
<ul class="blockers-list">
@@ -102,7 +92,6 @@ const prioritySeverity = (priority: Requirement['priority']) => {
<Divider />
<!-- Key attributes: Author, Stakeholders, Priority, Flexibility -->
<div class="detail-grid">
<article class="info-panel">
<h3>Author</h3>
@@ -129,7 +118,6 @@ const prioritySeverity = (priority: Requirement['priority']) => {
<Divider />
<!-- Acceptance criteria and impacted modules -->
<div class="detail-columns">
<article class="info-panel">
<h3>Acceptance criteria</h3>
@@ -162,29 +150,29 @@ const prioritySeverity = (priority: Requirement['priority']) => {
</div>
</TabPanel>
</TabView>
</template>
</Card>
</section>
</section>
</template>
<style scoped>
.detail-zone {
min-width: 0;
min-height: 0;
height: 100%;
}
.detail-card {
padding: 0.5rem;
border: 1px solid rgba(96, 117, 156, 0.16);
border-radius: 0.5rem;
.detail-surface {
display: flex;
flex-direction: column;
height: 100%;
padding: 1rem;
border: none;
border-radius: 0;
background: rgba(255, 255, 255, 0.86);
box-shadow: 0 24px 60px rgba(34, 49, 77, 0.12);
backdrop-filter: blur(18px);
}
.detail-card :is(.p-card-body) {
padding: 1rem;
}
.detail-card__header {
display: flex;
align-items: flex-start;
@@ -201,7 +189,7 @@ const prioritySeverity = (priority: Requirement['priority']) => {
color: #5c6b88;
}
.detail-card h2 {
.detail-surface h2 {
margin: 0;
color: #12213a;
}
@@ -219,6 +207,25 @@ const prioritySeverity = (priority: Requirement['priority']) => {
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 {
display: flex;
flex-wrap: wrap;
@@ -265,36 +272,12 @@ const prioritySeverity = (priority: Requirement['priority']) => {
color: #4c5d77;
}
.info-panel--accent {
background: linear-gradient(180deg, rgba(237, 243, 255, 0.92), rgba(248, 250, 255, 0.95));
}
.info-panel--warning {
margin-top: 0.75rem;
background: linear-gradient(180deg, rgba(255, 244, 236, 0.95), rgba(255, 249, 244, 0.95));
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,
.blockers-list {
display: grid;
@@ -343,11 +326,6 @@ const prioritySeverity = (priority: Requirement['priority']) => {
font-size: 2.5rem;
}
:deep(.p-tabview-panels) {
padding-left: 0;
padding-right: 0;
}
@media (max-width: 1120px) {
.detail-grid,
.detail-columns {
@@ -366,9 +344,11 @@ const prioritySeverity = (priority: Requirement['priority']) => {
}
.detail-grid,
.detail-columns,
.info-panel dl {
.detail-columns {
grid-template-columns: 1fr;
}
}
</style>

View File

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

View File

@@ -7,10 +7,12 @@ import PrimeVue from 'primevue/config'
import Aura from '@primeuix/themes/aura'
import 'primeicons/primeicons.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(PrimeVue, {
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">
import { computed } from 'vue'
import { computed, onBeforeUnmount } from 'vue'
import { storeToRefs } from 'pinia'
import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel'
import RequirementsTreeList from '../components/RequirementsTreeList.vue'
import RequirementDetail from '../components/RequirementDetail.vue'
@@ -13,7 +15,20 @@ const requirementsStore = useRequirementsStore()
const { requirements, selectedId, searchQuery, selectedRequirement, stats } =
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[] => {
return [
{
@@ -70,7 +85,13 @@ const treeData = computed(() => buildTreeData())
<template>
<section class="requirements-view">
<section class="workspace">
<Splitter
class="workspace-splitter"
:gutter-size="6"
@resizestart="onSplitterResizeStart"
@resizeend="onSplitterResizeEnd"
>
<SplitterPanel :size="28" :min-size="20">
<RequirementsTreeList
:tree-data="treeData"
:searchQuery="searchQuery"
@@ -79,15 +100,20 @@ const treeData = computed(() => buildTreeData())
@update:searchQuery="searchQuery = $event"
@update:selectedId="selectedId = $event"
/>
</SplitterPanel>
<SplitterPanel :size="72" :min-size="35">
<RequirementDetail :requirement="selectedRequirement" />
</section>
</SplitterPanel>
</Splitter>
</section>
</template>
<style scoped>
.requirements-view {
padding: 2rem;
display: flex;
flex-direction: column;
padding: 0;
min-height: 100%;
background:
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%);
}
.hero-card {
display: flex;
align-items: flex-end;
justify-content: space-between;
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);
.workspace-splitter {
flex: 1;
height: 100%;
min-height: 0;
}
.hero-card h1 {
margin: 0.35rem 0 0.65rem;
font-size: clamp(1.8rem, 3vw, 2.7rem);
font-weight: 700;
line-height: 1.05;
color: #12213a;
.workspace-splitter :deep(.p-splitter-panel) {
min-height: 0;
}
.hero-card__text {
max-width: 60ch;
color: #51627f;
.workspace-splitter :deep(.p-splitter-gutter) {
position: relative;
background: transparent;
cursor: col-resize;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.75rem;
.workspace-splitter :deep(.p-splitter-gutter::before) {
content: '';
position: absolute;
top: 8px;
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 {
margin: 0;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 0.72rem;
font-weight: 700;
color: #5c6b88;
.workspace-splitter :deep(.p-splitter-gutter:hover::before),
.workspace-splitter :deep(.p-splitter-gutter:active::before) {
background: rgba(78, 107, 255, 0.6);
}
.workspace {
display: grid;
grid-template-columns: minmax(18rem, 22rem) minmax(0, 1fr);
gap: 1rem;
align-items: start;
.workspace-splitter :deep(.p-splitter-gutter-handle) {
width: 0;
height: 0;
}
:global(body.is-splitter-dragging),
:global(body.is-splitter-dragging *) {
user-select: none;
-webkit-user-select: none;
}
@media (max-width: 1120px) {
.workspace {
grid-template-columns: 1fr;
.workspace-splitter {
height: 100%;
}
}
@media (max-width: 760px) {
.requirements-view {
padding: 1rem;
}
.hero-card {
flex-direction: column;
align-items: stretch;
}
.hero-actions {
justify-content: flex-start;
padding: 0;
}
}
</style>