UI update

This commit is contained in:
Sylvain Schneider
2026-05-22 17:25:23 +02:00
parent f736e6519b
commit a8f66f08b0
8 changed files with 713 additions and 16 deletions

View File

@@ -2,6 +2,92 @@
This template should help get you started developing with Vue 3 in Vite. This template should help get you started developing with Vue 3 in Vite.
## Backend JSON Bridge
The UI exposes a JSON bridge intended for integration inside a native host such as a C++ application.
### Available bridge entry points
- `window.kwaUiBridge.receiveFromBackend(message)` lets the host inject a JSON string or already parsed message object into the UI.
- `window.kwaUiBridge.sendRequest(topic, payload)` sends a `requestMessage` to the host and resolves with the matching `responseMessage.payload`.
- `window.kwaUiBridge.emitEvent(topic, payload)` sends a one-way `eventMessage` to the host.
- Outgoing messages are also mirrored as a browser event named `kwa-ui:outgoing-message` whose `detail` contains the serialized JSON.
- Incoming messages can also be injected by dispatching `kwa-ui:incoming-message` with the raw JSON string in `detail`.
### Message envelopes
Every message is JSON and uses one of the following envelope shapes.
```json
{
"kind": "requestMessage",
"id": "3f4f58aa-a49f-49b1-a43a-3ca3bff8469f",
"timestamp": "2026-05-22T15:28:00.000Z"
"topic": "requirements.list",
"payload": {},
}
```
```json
{
"kind": "responseMessage",
"id": "7e7e0cba-d4b8-4d8f-a0cb-31f4f3110db5",
"requestId": "3f4f58aa-a49f-49b1-a43a-3ca3bff8469f",
"timestamp": "2026-05-22T15:28:00.010Z"
"topic": "requirements.list",
"ok": true,
"payload": {
"requirements": []
},
}
```
```json
{
"kind": "eventMessage",
"id": "94f2b4d4-6f16-4f14-8dc7-869f6d3e1a55",
"timestamp": "2026-05-22T15:28:00.020Z"
"topic": "requirements.updated",
"payload": {
"requirement": {
"id": 101
}
},
}
```
### Native host transport
The bridge will send outgoing JSON to the first available transport below:
- `window.kwaBackendHost.postMessage(string)`
- `window.chrome.webview.postMessage(string)`
- `window.webkit.messageHandlers.kwaUiBridge.postMessage(string)`
If none of these are available, the UI still emits `kwa-ui:outgoing-message`, which is useful for testing or for a wrapper script.
### Registered topics
- `requirements.list`: request the full requirements collection and current selection.
- `requirements.get`: request payload `{ "id": number }`, returns one requirement or `null`.
- `requirements.select`: request or event payload `{ "id": number }`, selects the matching requirement.
- `requirements.update`: request payload `{ "requirement": { ... } }`, updates a requirement and returns the saved value.
- `requirements.replaceAll`: event payload `{ "requirements": [ ... ] }`, replaces the current collection in the UI.
### Events pushed by the backend to the UI
The backend can push an `eventMessage` at any time by calling `window.kwaUiBridge.receiveFromBackend(json)`. The following topics are handled:
- `requirements.replaceAll`: event payload `{ "requirements": [ ... ] }`, replaces the full requirements collection in the UI.
- `requirements.select`: event payload `{ "id": number }`, selects the matching requirement in the UI.
### Events emitted by the UI
- `ui.ready`: emitted once the bridge is installed.
- `requirements.selectionChanged`: emitted whenever the selected requirement changes.
- `requirements.updated`: emitted after a requirement update initiated in the UI.
- `requirements.replaced`: emitted after the full requirements collection is replaced.
## Recommended IDE Setup ## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). [VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).

29
kwa-ui/env.d.ts vendored
View File

@@ -1 +1,30 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
import type { BridgeMessage, UiBridgeApi } from './src/bridge/backendBridge'
declare global {
interface Window {
kwaBackendHost?: {
postMessage: (message: string) => void
}
kwaBridgeConfig?: {
webkitHandlerNames?: string[]
}
kwaUiBridge?: UiBridgeApi
chrome?: {
webview?: {
postMessage: (message: string) => void
}
}
webkit?: {
messageHandlers?: Record<string, { postMessage: (message: string) => void }>
}
}
interface WindowEventMap {
'kwa-ui:incoming-message': CustomEvent<string | BridgeMessage>
'kwa-ui:outgoing-message': CustomEvent<string>
}
}
export {}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">

View File

@@ -0,0 +1,394 @@
const OUTGOING_EVENT_NAME = 'kwa-ui:outgoing-message'
const INCOMING_EVENT_NAME = 'kwa-ui:incoming-message'
const DEFAULT_REQUEST_TIMEOUT_MS = 10_000
export type BridgeMessageKind = 'requestMessage' | 'responseMessage' | 'eventMessage'
export interface BridgeMessageBase<TKind extends BridgeMessageKind, TPayload = unknown> {
kind: TKind
id: string
timestamp: string
topic: string
payload: TPayload
}
export interface RequestMessage<TPayload = unknown>
extends BridgeMessageBase<'requestMessage', TPayload> {}
export interface ResponseMessage<TPayload = unknown>
extends BridgeMessageBase<'responseMessage', TPayload> {
requestId: string
ok: boolean
error?: string
}
export interface EventMessage<TPayload = unknown>
extends BridgeMessageBase<'eventMessage', TPayload> {}
export type BridgeMessage<TPayload = unknown> =
| RequestMessage<TPayload>
| ResponseMessage<TPayload>
| EventMessage<TPayload>
export interface RequestOptions {
timeoutMs?: number
}
export interface UiBridgeApi {
receiveFromBackend: (message: string | BridgeMessage) => boolean
sendRequest: <TResponse = unknown, TPayload = unknown>(
topic: string,
payload: TPayload,
options?: RequestOptions,
) => Promise<TResponse>
emitEvent: <TPayload = unknown>(topic: string, payload: TPayload) => void
}
type RequestHandler<TPayload = unknown, TResponse = unknown> = (
message: RequestMessage<TPayload>,
) => Promise<TResponse> | TResponse
type EventHandler<TPayload = unknown> = (
message: EventMessage<TPayload>,
) => Promise<void> | void
interface PendingRequest {
resolve: (payload: unknown) => void
reject: (reason?: unknown) => void
timeoutHandle: ReturnType<typeof window.setTimeout>
}
interface OutboundHost {
postMessage: (message: string) => void
}
interface WebkitMessageHandler {
postMessage: (message: string) => void
}
const DEFAULT_WEBKIT_HANDLER_NAMES = ['ui_bridge_handler'] as const
const isRecord = (value: unknown): value is Record<string, unknown> => {
return typeof value === 'object' && value !== null
}
const isBridgeMessageKind = (value: unknown): value is BridgeMessageKind => {
return value === 'requestMessage' || value === 'responseMessage' || value === 'eventMessage'
}
const isBridgeMessage = (value: unknown): value is BridgeMessage => {
if (!isRecord(value)) {
return false
}
if (!isBridgeMessageKind(value.kind)) {
return false
}
return (
typeof value.id === 'string' &&
typeof value.topic === 'string' &&
typeof value.timestamp === 'string' &&
'payload' in value
)
}
const parseBridgeMessage = (value: string | BridgeMessage | unknown): BridgeMessage | null => {
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value) as unknown
return isBridgeMessage(parsed) ? parsed : null
} catch {
return null
}
}
return isBridgeMessage(value) ? value : null
}
const createMessageId = () => {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
return `msg-${Date.now()}-${Math.random().toString(16).slice(2)}`
}
const normalizeError = (error: unknown) => {
if (error instanceof Error) {
return error.message
}
return typeof error === 'string' ? error : 'Unknown bridge error'
}
const isWebkitMessageHandler = (value: unknown): value is WebkitMessageHandler => {
return isRecord(value) && typeof value.postMessage === 'function'
}
const resolveWebkitHandlers = (): OutboundHost[] => {
const messageHandlers = window.webkit?.messageHandlers
if (!isRecord(messageHandlers)) {
return []
}
const handlers: OutboundHost[] = []
for (const handlerName of DEFAULT_WEBKIT_HANDLER_NAMES) {
const handler = (messageHandlers as Record<string, unknown>)[handlerName]
if (!isWebkitMessageHandler(handler)) {
continue
}
handlers.push({
postMessage: (message: string) => handler.postMessage(message),
})
}
return handlers
}
const resolveOutboundHosts = (): OutboundHost[] => {
const hosts: OutboundHost[] = []
if (window.kwaBackendHost?.postMessage) {
hosts.push(window.kwaBackendHost)
}
if (window.chrome?.webview?.postMessage) {
hosts.push({
postMessage: (message: string) => window.chrome?.webview?.postMessage(message),
})
}
hosts.push(...resolveWebkitHandlers())
const legacyExternal = (window as Window & { external?: { notify?: (message: string) => void } }).external
if (legacyExternal?.notify) {
hosts.push({
postMessage: (message: string) => legacyExternal.notify?.(message),
})
}
if (window.parent && window.parent !== window) {
hosts.push({
postMessage: (message: string) => window.parent.postMessage(message, '*'),
})
}
return hosts
}
class BackendBridge {
private readonly requestHandlers = new Map<string, RequestHandler>()
private readonly eventHandlers = new Map<string, Set<EventHandler>>()
private readonly pendingRequests = new Map<string, PendingRequest>()
constructor() {
window.addEventListener('message', this.handleWindowMessage)
window.addEventListener(INCOMING_EVENT_NAME, this.handleCustomIncoming as EventListener)
}
sendRequest = <TResponse = unknown, TPayload = unknown>(
topic: string,
payload: TPayload,
options?: RequestOptions,
): Promise<TResponse> => {
const requestMessage: RequestMessage<TPayload> = {
kind: 'requestMessage',
id: createMessageId(),
timestamp: new Date().toISOString(),
topic,
payload,
}
return new Promise<TResponse>((resolve, reject) => {
const timeoutMs = options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
const timeoutHandle = window.setTimeout(() => {
this.pendingRequests.delete(requestMessage.id)
reject(new Error(`Bridge request timed out for topic "${topic}".`))
}, timeoutMs)
this.pendingRequests.set(requestMessage.id, {
resolve: (responsePayload) => resolve(responsePayload as TResponse),
reject,
timeoutHandle,
})
this.postMessage(requestMessage)
})
}
sendResponse = <TPayload = unknown>(
requestId: string,
topic: string,
payload: TPayload,
ok = true,
error?: string,
) => {
const responseMessage: ResponseMessage<TPayload> = {
kind: 'responseMessage',
id: createMessageId(),
requestId,
topic,
payload,
ok,
error,
timestamp: new Date().toISOString(),
}
this.postMessage(responseMessage)
}
emitEvent = <TPayload = unknown>(topic: string, payload: TPayload) => {
const eventMessage: EventMessage<TPayload> = {
kind: 'eventMessage',
id: createMessageId(),
timestamp: new Date().toISOString(),
topic,
payload,
}
this.postMessage(eventMessage)
}
onRequest = <TPayload = unknown, TResponse = unknown>(
topic: string,
handler: RequestHandler<TPayload, TResponse>,
) => {
this.requestHandlers.set(topic, handler as RequestHandler)
return () => {
this.requestHandlers.delete(topic)
}
}
onEvent = <TPayload = unknown>(topic: string, handler: EventHandler<TPayload>) => {
const handlers = this.eventHandlers.get(topic) ?? new Set<EventHandler>()
handlers.add(handler as EventHandler)
this.eventHandlers.set(topic, handlers)
return () => {
handlers.delete(handler as EventHandler)
if (handlers.size === 0) {
this.eventHandlers.delete(topic)
}
}
}
receiveFromBackend = (message: string | BridgeMessage) => {
const parsedMessage = parseBridgeMessage(message)
if (!parsedMessage) {
return false
}
void this.dispatchIncoming(parsedMessage)
return true
}
private readonly handleWindowMessage = (event: MessageEvent) => {
this.receiveFromBackend(event.data)
}
private readonly handleCustomIncoming = (event: Event) => {
const customEvent = event as CustomEvent<string | BridgeMessage>
this.receiveFromBackend(customEvent.detail)
}
private postMessage(message: BridgeMessage) {
const serializedMessage = JSON.stringify(message)
for (const host of resolveOutboundHosts()) {
try {
host.postMessage(serializedMessage)
} catch {
// Best effort transport fan-out.
}
}
window.dispatchEvent(
new CustomEvent<string>(OUTGOING_EVENT_NAME, {
detail: serializedMessage,
}),
)
}
private async dispatchIncoming(message: BridgeMessage) {
if (message.kind === 'responseMessage') {
this.resolvePendingRequest(message)
return
}
if (message.kind === 'requestMessage') {
await this.handleRequestMessage(message)
return
}
await this.handleEventMessage(message)
}
private resolvePendingRequest(message: ResponseMessage) {
const pendingRequest = this.pendingRequests.get(message.requestId)
if (!pendingRequest) {
return
}
window.clearTimeout(pendingRequest.timeoutHandle)
this.pendingRequests.delete(message.requestId)
if (!message.ok) {
pendingRequest.reject(new Error(message.error ?? `Bridge request failed for topic "${message.topic}".`))
return
}
pendingRequest.resolve(message.payload)
}
private async handleRequestMessage(message: RequestMessage) {
const handler = this.requestHandlers.get(message.topic)
if (!handler) {
this.sendResponse(message.id, message.topic, null, false, `No request handler for topic "${message.topic}".`)
return
}
try {
const payload = await handler(message)
this.sendResponse(message.id, message.topic, payload, true)
} catch (error) {
this.sendResponse(message.id, message.topic, null, false, normalizeError(error))
}
}
private async handleEventMessage(message: EventMessage) {
const handlers = this.eventHandlers.get(message.topic)
if (!handlers || handlers.size === 0) {
return
}
for (const handler of handlers) {
await handler(message)
}
}
}
export const backendBridge = new BackendBridge()
export const uiBridgeApi: UiBridgeApi = {
receiveFromBackend: backendBridge.receiveFromBackend,
sendRequest: backendBridge.sendRequest,
emitEvent: backendBridge.emitEvent,
}
window.kwaUiBridge = uiBridgeApi
export const bridgeEventNames = {
incoming: INCOMING_EVENT_NAME,
outgoing: OUTGOING_EVENT_NAME,
}

View File

@@ -0,0 +1,80 @@
import { watch } from 'vue'
import { backendBridge } from './backendBridge'
import { useRequirementsStore, type Requirement } from '../stores/requirements'
interface RequirementSelectionPayload {
id: number
}
interface RequirementUpdatePayload {
requirement: Requirement
}
interface RequirementReplacePayload {
requirements: Requirement[]
}
export const installRequirementsBridge = (store: ReturnType<typeof useRequirementsStore>) => {
backendBridge.onRequest('requirements.list', () => {
return {
requirements: store.requirements,
selectedId: store.selectedId,
}
})
backendBridge.onRequest<RequirementSelectionPayload>('requirements.get', ({ payload }) => {
return {
requirement: store.getRequirementById(payload.id),
}
})
backendBridge.onRequest<RequirementSelectionPayload>('requirements.select', ({ payload }) => {
return {
requirement: store.selectRequirement(payload.id),
}
})
backendBridge.onRequest<RequirementUpdatePayload>('requirements.update', ({ payload }) => {
return {
requirement: store.updateRequirement(payload.requirement),
}
})
backendBridge.onEvent<RequirementReplacePayload>('requirements.replaceAll', ({ payload }) => {
store.setRequirements(payload.requirements)
})
backendBridge.onEvent<RequirementSelectionPayload>('requirements.select', ({ payload }) => {
store.selectRequirement(payload.id)
})
store.$onAction(({ name, after }) => {
after((result) => {
if (name === 'updateRequirement' && result) {
backendBridge.emitEvent('requirements.updated', {
requirement: result,
})
}
if (name === 'setRequirements') {
backendBridge.emitEvent('requirements.replaced', {
requirements: store.requirements,
selectedId: store.selectedId,
})
}
})
})
watch(
() => store.selectedId,
(selectedId) => {
backendBridge.emitEvent('requirements.selectionChanged', {
selectedId,
requirement: store.getRequirementById(selectedId),
})
},
{ immediate: false },
)
backendBridge.emitEvent('ui.ready', {})
}

View File

@@ -1,7 +1,3 @@
import '@fontsource/source-sans-3/latin-400.css'
import '@fontsource/source-sans-3/latin-500.css'
import '@fontsource/source-sans-3/latin-600.css'
import '@fontsource/source-sans-3/latin-700.css'
import './assets/main.css' import './assets/main.css'
import { createApp } from 'vue' import { createApp } from 'vue'
@@ -12,10 +8,22 @@ 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' import router from './router'
import { installRequirementsBridge } from './bridge/requirementsBridge'
import { useRequirementsStore } from './stores/requirements'
const loadEmbeddedFonts = async () => {
await Promise.allSettled([
import('@fontsource/source-sans-3/latin-400.css'),
import('@fontsource/source-sans-3/latin-500.css'),
import('@fontsource/source-sans-3/latin-600.css'),
import('@fontsource/source-sans-3/latin-700.css'),
])
}
const app = createApp(App) const app = createApp(App)
const pinia = createPinia()
app.use(createPinia()) app.use(pinia)
app.use(router) app.use(router)
app.use(PrimeVue, { app.use(PrimeVue, {
@@ -27,4 +35,9 @@ app.use(PrimeVue, {
}, },
}) })
installRequirementsBridge(useRequirementsStore(pinia))
app.mount('#app') app.mount('#app')
// Do not block bridge initialization on font asset loading in embedded hosts.
void loadEmbeddedFonts()

View File

@@ -25,6 +25,16 @@ export interface Requirement {
tolerances: string // Acceptable limits for deviations from the requirement (e.g., "Maximum 5% of requirements can have incomplete artifact links") tolerances: string // Acceptable limits for deviations from the requirement (e.g., "Maximum 5% of requirements can have incomplete artifact links")
} }
const cloneRequirement = (requirement: Requirement): Requirement => {
return {
...requirement,
acceptanceCriteria: [...requirement.acceptanceCriteria],
impactedModules: [...requirement.impactedModules],
blockers: [...requirement.blockers],
stakeholders: [...requirement.stakeholders],
}
}
export const useRequirementsStore = defineStore('requirements', () => { export const useRequirementsStore = defineStore('requirements', () => {
const requirements = ref<Requirement[]>([ const requirements = ref<Requirement[]>([
{ {
@@ -157,22 +167,47 @@ export const useRequirementsStore = defineStore('requirements', () => {
return { total, approvedCount, blockedCount, criticalCount } return { total, approvedCount, blockedCount, criticalCount }
}) })
const getRequirementById = (id: number) => {
return requirements.value.find((requirement) => requirement.id === id) ?? null
}
const selectRequirement = (nextId: number) => {
const nextRequirement = getRequirementById(nextId)
if (!nextRequirement) {
return null
}
selectedId.value = nextId
return nextRequirement
}
const setRequirements = (nextRequirements: Requirement[]) => {
requirements.value = nextRequirements.map(cloneRequirement)
if (requirements.value.length === 0) {
return []
}
if (!getRequirementById(selectedId.value)) {
selectedId.value = requirements.value[0]!.id
}
return requirements.value
}
const updateRequirement = (updatedRequirement: Requirement) => { const updateRequirement = (updatedRequirement: Requirement) => {
const index = requirements.value.findIndex( const index = requirements.value.findIndex(
(requirement) => requirement.id === updatedRequirement.id (requirement) => requirement.id === updatedRequirement.id
) )
if (index === -1) { if (index === -1) {
return return null
} }
requirements.value[index] = { requirements.value[index] = cloneRequirement(updatedRequirement)
...updatedRequirement,
acceptanceCriteria: [...updatedRequirement.acceptanceCriteria], return requirements.value[index]
impactedModules: [...updatedRequirement.impactedModules],
blockers: [...updatedRequirement.blockers],
stakeholders: [...updatedRequirement.stakeholders],
}
} }
return { return {
@@ -181,6 +216,9 @@ export const useRequirementsStore = defineStore('requirements', () => {
searchQuery, searchQuery,
selectedRequirement, selectedRequirement,
stats, stats,
getRequirementById,
selectRequirement,
setRequirements,
updateRequirement, updateRequirement,
} }
}) })

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import Dialog from 'primevue/dialog'
import Splitter from 'primevue/splitter' import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel' import SplitterPanel from 'primevue/splitterpanel'
@@ -8,14 +9,21 @@ import RequirementsTreeList from '../components/RequirementsTreeList.vue'
import RequirementDetail from '../components/RequirementDetail.vue' import RequirementDetail from '../components/RequirementDetail.vue'
import { useRequirementsStore } from '../stores/requirements' import { useRequirementsStore } from '../stores/requirements'
import type { Requirement } from '../stores/requirements' import type { Requirement } from '../stores/requirements'
import { backendBridge } from '../bridge/backendBridge'
import type { TreeNode } from '../components/RequirementsTreeList.vue' import type { TreeNode } from '../components/RequirementsTreeList.vue'
interface RequirementsListResponse {
requirements: Requirement[]
selectedId?: number
}
const requirementsStore = useRequirementsStore() const requirementsStore = useRequirementsStore()
const { requirements, selectedId, searchQuery, selectedRequirement, stats } = const { requirements, selectedId, searchQuery, selectedRequirement, stats } =
storeToRefs(requirementsStore) storeToRefs(requirementsStore)
const hasUnsavedChanges = ref(false) const hasUnsavedChanges = ref(false)
const showTimeoutDialog = ref(false)
const splitterDraggingClass = 'is-splitter-dragging' const splitterDraggingClass = 'is-splitter-dragging'
@@ -31,6 +39,37 @@ onBeforeUnmount(() => {
document.body.classList.remove(splitterDraggingClass) document.body.classList.remove(splitterDraggingClass)
}) })
const loadRequirementsFromBackend = async () => {
try {
const response = await backendBridge.sendRequest<RequirementsListResponse>('requirements.list', {}, {
timeoutMs: 5000,
})
if (!response || !Array.isArray(response.requirements)) {
return
}
requirementsStore.setRequirements(response.requirements)
if (typeof response.selectedId === 'number') {
requirementsStore.selectRequirement(response.selectedId)
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
if (message.toLowerCase().includes('timed out')) {
showTimeoutDialog.value = true
return
}
console.warn('Failed to load requirements from backend:', error)
}
}
onMounted(() => {
void loadRequirementsFromBackend()
})
const buildTreeData = (): TreeNode[] => { const buildTreeData = (): TreeNode[] => {
return [ return [
{ {
@@ -109,12 +148,25 @@ const onSelectRequirement = (nextId: number) => {
} }
hasUnsavedChanges.value = false hasUnsavedChanges.value = false
selectedId.value = nextId requirementsStore.selectRequirement(nextId)
} }
</script> </script>
<template> <template>
<section class="requirements-view"> <section class="requirements-view">
<Dialog
v-model:visible="showTimeoutDialog"
header="Backend timeout"
modal
:closable="true"
:draggable="false"
:style="{ width: '28rem' }"
>
<p class="timeout-message">
The retrieval of requirements has timed out. Please check the connection with the backend and try again.
</p>
</Dialog>
<Splitter <Splitter
class="workspace-splitter" class="workspace-splitter"
:gutter-size="6" :gutter-size="6"
@@ -165,6 +217,11 @@ const onSelectRequirement = (nextId: number) => {
min-height: 0; min-height: 0;
} }
.timeout-message {
margin: 0;
color: #3f4d68;
}
.workspace-splitter :deep(.p-splitter-gutter) { .workspace-splitter :deep(.p-splitter-gutter) {
position: relative; position: relative;
background: transparent; background: transparent;