From 097d79dab32c3a4554c62b3c86aed9c06c28fe6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20L=C3=B6sch?= Date: Tue, 10 Mar 2026 11:17:44 +0100 Subject: [PATCH 1/5] feat: enable auto-hide menu bar in main window and improve scrollbar styling --- electron/main.js | 1 + src/App.vue | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/electron/main.js b/electron/main.js index 4ac3e3a..d22a8f0 100644 --- a/electron/main.js +++ b/electron/main.js @@ -64,6 +64,7 @@ function createWindow() { width: 1200, height: 800, show: false, + autoHideMenuBar: true, webPreferences: { nodeIntegration: false, contextIsolation: true, diff --git a/src/App.vue b/src/App.vue index bc89126..99e559f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -190,6 +190,12 @@ export default { /* small global styles */ body { font-family: Roboto, Arial, sans-serif; } +/* Show scrollbar only when content overflows */ +html, body { overflow-y: auto !important; } +::-webkit-scrollbar { width: 8px; } +::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); border-radius: 4px; } +::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.35); } + .main-container { max-width: 1800px; margin: 0 auto; From 19a9967163624c9aaaec06363f4dfb4b612a522e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20L=C3=B6sch?= Date: Tue, 10 Mar 2026 11:28:03 +0100 Subject: [PATCH 2/5] feat: Workspace management button is not shown in electron anymore because it is no longer needed and works only for the SPA version. --- src/App.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.vue b/src/App.vue index 99e559f..3d6cc86 100644 --- a/src/App.vue +++ b/src/App.vue @@ -17,7 +17,7 @@ - + mdi-database-cog Manage workspace From 1c7b943395367f8baa852f88fb8d557906bb1b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20L=C3=B6sch?= Date: Tue, 10 Mar 2026 12:40:04 +0100 Subject: [PATCH 3/5] chore: update of documentation --- README.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f9276e4..de2937d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Solution Inventory

-Vue 3 + Vuetify application for documenting solution questionnaires across multiple projects. Available as both a Progressive Web App (PWA) and an Electron desktop application. It uses a project tree for navigation, questionnaire tabs for editing, and a configuration editor in a dialog. +Vue 3 + Vuetify application for documenting solution questionnaires across multiple projects. Available as both a Progressive Web App (PWA) and an Electron desktop application for **Windows** and **Linux**. The Electron app stores all data locally on the device (no cloud sync). The web version (hosted via GitHub Pages) stores all data exclusively in the browser's Local Storage (no server-side storage or sync). It uses a project tree for navigation, questionnaire tabs for editing, and a configuration editor in a dialog. ## Features - Project tree with create/rename/delete and drag-and-drop (move and reorder questionnaires) @@ -41,7 +41,8 @@ npm install npm run dev ``` -### Development (Electron) + +### Development (Electron, Windows & Linux) ```bash npm run electron:dev ``` @@ -51,7 +52,8 @@ npm run electron:dev npm run build ``` -### Production Build (Electron) + +### Production Build (Electron, Windows & Linux) ```bash npm run electron:build ``` @@ -61,7 +63,8 @@ npm run electron:build npm run preview ``` -### Preview Build (Electron) + +### Preview Build (Electron, Windows & Linux) ```bash npm run electron:preview ``` @@ -161,12 +164,19 @@ Use the menu (⋮) in the toolbar to: - Export as ThoughtWorks Build-Your-Own-Radar JSON format - Download radar visualization as PNG image + ## Project Import/Export - Export a single project from the project menu. - Import a project JSON using the import dialog in the project tree header. +## Data Storage + +- **Electron App (Windows & Linux):** All data is stored locally on your device. No data is sent to any server or cloud service. +- **Web App (GitHub Pages):** All data is stored exclusively in your browser's Local Storage. There is no server-side storage or synchronization. Clearing your browser data will remove all projects and questionnaires. + + ## GitHub Pages Deployment -GitHub Actions builds and deploys on push to main. Pushes and PRs to dev build but do not deploy. +GitHub Actions builds and deploys on push to main. The web version is hosted at GitHub Pages and stores all data only in the browser's Local Storage. Pushes and PRs to dev build but do not deploy. ## Dependencies - Vue 3 From 9e599d31d13dee1752c9c9c8c07152b391fc253e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20L=C3=B6sch?= Date: Tue, 10 Mar 2026 14:13:08 +0100 Subject: [PATCH 4/5] feat: add search and sort functionality for questionnaire entries --- src/components/questionaire/Questionnaire.vue | 73 ++++++++++++++++--- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/src/components/questionaire/Questionnaire.vue b/src/components/questionaire/Questionnaire.vue index 2fc735a..6d267c6 100644 --- a/src/components/questionaire/Questionnaire.vue +++ b/src/components/questionaire/Questionnaire.vue @@ -166,6 +166,28 @@
+
+ + + {{ entrySort === 'asc' ? 'mdi-sort-alphabetical-ascending' : entrySort === 'desc' ? 'mdi-sort-alphabetical-descending' : 'mdi-sort-variant' }} + {{ entrySort === 'asc' ? 'Sorted A→Z (click for Z→A)' : entrySort === 'desc' ? 'Sorted Z→A (click to reset)' : 'Sort alphabetically' }} + +
@@ -365,6 +387,14 @@ export default { const architecturalRoleValue = computed(() => metadataValue.value?.architecturalRole || '') const activeCategoryId = ref('') const applicabilityFilter = ref('all') + const entrySearch = ref('') + const entrySort = ref('') + + function cycleSort() { + if (entrySort.value === '') entrySort.value = 'asc' + else if (entrySort.value === 'asc') entrySort.value = 'desc' + else entrySort.value = '' + } function toArray(value) { if (!value) return [] @@ -417,16 +447,30 @@ export default { const visibleEntries = computed(() => { const entries = Array.isArray(currentCategory.value.entries) ? currentCategory.value.entries : [] - const filteredByMetadata = entries.filter((entry) => appliesToMatches(entry.appliesTo, metadataValue.value)) - - if (applicabilityFilter.value === 'all') { - return filteredByMetadata + let result = entries.filter((entry) => appliesToMatches(entry.appliesTo, metadataValue.value)) + + if (applicabilityFilter.value !== 'all') { + result = result.filter((entry) => { + const entryApplicability = entry.applicability || 'applicable' + return entryApplicability === applicabilityFilter.value + }) } - - return filteredByMetadata.filter((entry) => { - const entryApplicability = entry.applicability || 'applicable' - return entryApplicability === applicabilityFilter.value - }) + + if (entrySearch.value && entrySearch.value.trim()) { + const q = entrySearch.value.trim().toLowerCase() + result = result.filter((entry) => + (entry.aspect || '').toLowerCase().includes(q) || + (entry.description || '').toLowerCase().includes(q) + ) + } + + if (entrySort.value === 'asc') { + result = [...result].sort((a, b) => (a.aspect || '').localeCompare(b.aspect || '')) + } else if (entrySort.value === 'desc') { + result = [...result].sort((a, b) => (b.aspect || '').localeCompare(a.aspect || '')) + } + + return result }) const applicabilityFilterOptions = computed(() => { @@ -460,6 +504,8 @@ export default { function selectCategory(id) { activeCategoryId.value = id + entrySearch.value = '' + entrySort.value = '' scrollToTop() } @@ -495,6 +541,8 @@ export default { const idx = visibleCategories.value.findIndex((category) => category.id === activeCategoryId.value) if (idx < visibleCategories.value.length - 1) { activeCategoryId.value = visibleCategories.value[idx + 1].id + entrySearch.value = '' + entrySort.value = '' scrollToTop() } } @@ -503,6 +551,8 @@ export default { const idx = visibleCategories.value.findIndex((category) => category.id === activeCategoryId.value) if (idx > 0) { activeCategoryId.value = visibleCategories.value[idx - 1].id + entrySearch.value = '' + entrySort.value = '' scrollToTop() } } @@ -595,7 +645,10 @@ export default { answerTypeOptions, isReference, parentProject, - toggleReference + toggleReference, + entrySearch, + entrySort, + cycleSort } } } From 841f6aa268220821e2951de59ed97e77d1d2798b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20L=C3=B6sch?= Date: Tue, 10 Mar 2026 14:21:42 +0100 Subject: [PATCH 5/5] feat: implement functionality to hide and show questionnaire entries with visual indicators --- src/components/questionaire/Questionnaire.vue | 85 ++++++++++++++++++- src/stores/workspaceStore.js | 23 ++++- 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/src/components/questionaire/Questionnaire.vue b/src/components/questionaire/Questionnaire.vue index 6d267c6..ca5edf4 100644 --- a/src/components/questionaire/Questionnaire.vue +++ b/src/components/questionaire/Questionnaire.vue @@ -33,6 +33,16 @@

{{ currentCategory.title }}

+ + mdi-eye-off-outline + {{ currentCategoryHiddenCount }} hidden +
-
{{ entry.aspect }}
+
+ {{ entry.aspect }} + + + +
Examples: @@ -390,6 +416,21 @@ export default { const entrySearch = ref('') const entrySort = ref('') + const hiddenEntries = computed(() => + props.questionnaireId ? store.getQuestionnaireHiddenEntries(props.questionnaireId) : new Set() + ) + + function hideEntry(entryId) { + if (!props.questionnaireId) return + const updated = new Set([...hiddenEntries.value, entryId]) + store.setQuestionnaireHiddenEntries(props.questionnaireId, updated) + } + + function showAllEntries() { + if (!props.questionnaireId) return + store.setQuestionnaireHiddenEntries(props.questionnaireId, new Set()) + } + function cycleSort() { if (entrySort.value === '') entrySort.value = 'asc' else if (entrySort.value === 'asc') entrySort.value = 'desc' @@ -470,7 +511,16 @@ export default { result = [...result].sort((a, b) => (b.aspect || '').localeCompare(a.aspect || '')) } - return result + return result.filter((entry) => !hiddenEntries.value.has(entry.id)) + }) + + const currentCategoryHiddenCount = computed(() => { + const entries = Array.isArray(currentCategory.value.entries) ? currentCategory.value.entries : [] + let result = entries.filter((entry) => appliesToMatches(entry.appliesTo, metadataValue.value)) + if (applicabilityFilter.value !== 'all') { + result = result.filter((entry) => (entry.applicability || 'applicable') === applicabilityFilter.value) + } + return result.filter((entry) => hiddenEntries.value.has(entry.id)).length }) const applicabilityFilterOptions = computed(() => { @@ -648,7 +698,10 @@ export default { toggleReference, entrySearch, entrySort, - cycleSort + cycleSort, + hideEntry, + showAllEntries, + currentCategoryHiddenCount } } } @@ -665,6 +718,32 @@ export default { resize: vertical; } +.entry-title-row { + flex-wrap: nowrap; +} + +.entry-hide-btn { + opacity: 0; + transition: opacity 0.12s; + flex-shrink: 0; +} +.entry-title-row:hover .entry-hide-btn { + opacity: 0.5; +} +.entry-hide-btn:hover { + opacity: 1 !important; +} + +.hidden-entries-chip { + cursor: pointer; + opacity: 0.4; + transition: opacity 0.15s; + font-size: 11px !important; +} +.hidden-entries-chip:hover { + opacity: 0.85; +} + .entry-highlighted { animation: entry-flash 2s ease-out; border-radius: 4px; diff --git a/src/stores/workspaceStore.js b/src/stores/workspaceStore.js index 7f1a72f..0d6a355 100644 --- a/src/stores/workspaceStore.js +++ b/src/stores/workspaceStore.js @@ -43,6 +43,7 @@ export const useWorkspaceStore = defineStore('workspace', () => { const autoSaveStarted = ref(false) const pendingNavigation = ref(null) // { questionnaireId, categoryId, entryId } | null const workspaceDirNeeded = ref(false) + const questionnaireHiddenEntries = ref({}) // Record const activeQuestionnaire = computed(() => { return workspace.value.questionnaires.find((item) => item.id === activeQuestionnaireId.value) || null @@ -94,6 +95,7 @@ export const useWorkspaceStore = defineStore('workspace', () => { openQuestionnaireIds.value = [] activeWorkspaceTabId.value = '' openProjectSummaryIds.value = [] + questionnaireHiddenEntries.value = data.questionnaireHiddenEntries || {} hydrateLastSaved(data.timestamp) return true } @@ -104,6 +106,7 @@ export const useWorkspaceStore = defineStore('workspace', () => { openQuestionnaireIds.value = [] activeWorkspaceTabId.value = '' openProjectSummaryIds.value = [] + questionnaireHiddenEntries.value = {} hydrateLastSaved(data.timestamp) return true } @@ -170,7 +173,7 @@ export const useWorkspaceStore = defineStore('workspace', () => { if (autoSaveStarted.value) return autoSaveStarted.value = true watch( - () => [workspace.value, activeQuestionnaireId.value, openQuestionnaireIds.value, activeWorkspaceTabId.value, openProjectSummaryIds.value], + () => [workspace.value, activeQuestionnaireId.value, openQuestionnaireIds.value, activeWorkspaceTabId.value, openProjectSummaryIds.value, questionnaireHiddenEntries.value], () => { clearTimeout(persistDebounceTimer) persistDebounceTimer = setTimeout(() => persist(), 500) @@ -187,7 +190,8 @@ export const useWorkspaceStore = defineStore('workspace', () => { activeQuestionnaireId: activeQuestionnaireId.value, openQuestionnaireIds: openQuestionnaireIds.value, activeWorkspaceTabId: activeWorkspaceTabId.value, - openProjectSummaryIds: openProjectSummaryIds.value + openProjectSummaryIds: openProjectSummaryIds.value, + questionnaireHiddenEntries: questionnaireHiddenEntries.value } if (window.electronAPI) { @@ -246,6 +250,17 @@ export const useWorkspaceStore = defineStore('workspace', () => { pendingNavigation.value = null } + function getQuestionnaireHiddenEntries(questionnaireId) { + return new Set(questionnaireHiddenEntries.value[questionnaireId] || []) + } + + function setQuestionnaireHiddenEntries(questionnaireId, entryIdSet) { + questionnaireHiddenEntries.value = { + ...questionnaireHiddenEntries.value, + [questionnaireId]: [...entryIdSet] + } + } + function openProjectSummary(projectId) { const project = workspace.value.projects.find((item) => item.id === projectId) if (!project) return @@ -853,6 +868,8 @@ export const useWorkspaceStore = defineStore('workspace', () => { setReferenceQuestionnaire, pendingNavigation, navigateToEntry, - clearPendingNavigation + clearPendingNavigation, + getQuestionnaireHiddenEntries, + setQuestionnaireHiddenEntries } })