feat(branding): add hiddenNavHrefs to hide sidebar items via env var (#382)

Extends the branding service with a hiddenNavHrefs: string[] field so
forks can hide sidebar entries (e.g. /salary, /customers) without
patching DashboardNav.tsx. Configurable via NEXT_PUBLIC_BRANDING_HIDDEN_NAV
as a comma-separated list. Default is [] — vanilla gnubok unchanged.

Routes remain reachable; this is a nav-visibility switch only. Settings
sidebar is intentionally out of scope.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-04-29 22:53:34 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 064fb7f7a9
commit c86dbdc60d
5 changed files with 41 additions and 0 deletions
+1
View File
@@ -47,6 +47,7 @@ All branding can be set via env vars. Public ones use `NEXT_PUBLIC_BRANDING_*` (
| `NEXT_PUBLIC_BRANDING_THEME_COLOR` | `themeColor` | `#304D83` |
| `NEXT_PUBLIC_BRANDING_MANIFEST_THEME_COLOR` | `manifestThemeColor` | `#1a1a1a` |
| `NEXT_PUBLIC_BRANDING_MANIFEST_BG_COLOR` | `manifestBackgroundColor` | `#ffffff` |
| `NEXT_PUBLIC_BRANDING_HIDDEN_NAV` | `hiddenNavHrefs` (comma-separated, e.g. `/salary,/customers`) | `` (none hidden) |
Resolution order (last wins): **defaults → env vars → extension override**.
+4
View File
@@ -29,6 +29,7 @@ import {
HandCoins,
Sparkles,
} from 'lucide-react'
import { getBranding } from '@/lib/branding/service'
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
import { isAgentInboxEnabled } from '@/lib/ai/feature-flag'
import { resolveIcon } from '@/lib/extensions/icon-resolver'
@@ -157,9 +158,12 @@ export default function DashboardNav({ companyName: _companyName, entityType, un
}, 200)
}
const hiddenNavHrefs = new Set(getBranding().hiddenNavHrefs)
// Filter nav items by entity type, hidden flag, and conditional visibility
const filteredItems = navItems.filter(item => {
if (item.hidden) return false
if (hiddenNavHrefs.has(item.href)) return false
if (item.modes && !item.modes.includes(entityType)) return false
// Only show Granskning when there are pending operations
if (item.href === '/pending' && pendingOperationsCount === 0) return false
@@ -19,6 +19,7 @@ registerBrandingService({
// themeColor: '#000000',
// manifestThemeColor: '#000000',
// manifestBackgroundColor: '#ffffff',
// hiddenNavHrefs: ['/salary', '/salary/employees', '/customers'],
})
export const exampleBrandingExtension: Extension = {
+27
View File
@@ -14,6 +14,7 @@ const ENV_KEYS = [
'NEXT_PUBLIC_BRANDING_THEME_COLOR',
'NEXT_PUBLIC_BRANDING_MANIFEST_THEME_COLOR',
'NEXT_PUBLIC_BRANDING_MANIFEST_BG_COLOR',
'NEXT_PUBLIC_BRANDING_HIDDEN_NAV',
] as const
describe('branding service', () => {
@@ -51,6 +52,32 @@ describe('branding service', () => {
expect(b.themeColor).toBe('#304D83')
expect(b.manifestThemeColor).toBe('#1a1a1a')
expect(b.manifestBackgroundColor).toBe('#ffffff')
expect(b.hiddenNavHrefs).toEqual([])
})
it('parses NEXT_PUBLIC_BRANDING_HIDDEN_NAV as comma-separated hrefs', async () => {
process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = '/salary,/salary/employees,/customers'
const { getBranding } = await import('../service')
expect(getBranding().hiddenNavHrefs).toEqual(['/salary', '/salary/employees', '/customers'])
})
it('trims whitespace and drops empty entries in hidden nav list', async () => {
process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = ' /salary , ,/customers, '
const { getBranding } = await import('../service')
expect(getBranding().hiddenNavHrefs).toEqual(['/salary', '/customers'])
})
it('empty NEXT_PUBLIC_BRANDING_HIDDEN_NAV keeps default empty list', async () => {
process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = ''
const { getBranding } = await import('../service')
expect(getBranding().hiddenNavHrefs).toEqual([])
})
it('extension override replaces hiddenNavHrefs', async () => {
process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = '/salary'
const { getBranding, registerBrandingService } = await import('../service')
registerBrandingService({ hiddenNavHrefs: ['/customers', '/suppliers'] })
expect(getBranding().hiddenNavHrefs).toEqual(['/customers', '/suppliers'])
})
it('env vars override defaults', async () => {
+8
View File
@@ -38,6 +38,9 @@ export interface BrandingConfig {
themeColor: string
manifestThemeColor: string
manifestBackgroundColor: string
// Navigation
hiddenNavHrefs: string[]
}
const DEFAULT_BRANDING: BrandingConfig = {
@@ -55,6 +58,7 @@ const DEFAULT_BRANDING: BrandingConfig = {
themeColor: '#304D83',
manifestThemeColor: '#1a1a1a',
manifestBackgroundColor: '#ffffff',
hiddenNavHrefs: [],
}
let _override: Partial<BrandingConfig> = {}
@@ -88,5 +92,9 @@ function readEnvOverrides(): Partial<BrandingConfig> {
if (env.NEXT_PUBLIC_BRANDING_THEME_COLOR) o.themeColor = env.NEXT_PUBLIC_BRANDING_THEME_COLOR
if (env.NEXT_PUBLIC_BRANDING_MANIFEST_THEME_COLOR) o.manifestThemeColor = env.NEXT_PUBLIC_BRANDING_MANIFEST_THEME_COLOR
if (env.NEXT_PUBLIC_BRANDING_MANIFEST_BG_COLOR) o.manifestBackgroundColor = env.NEXT_PUBLIC_BRANDING_MANIFEST_BG_COLOR
if (env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV) {
const hrefs = env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV.split(',').map(s => s.trim()).filter(Boolean)
if (hrefs.length > 0) o.hiddenNavHrefs = hrefs
}
return o
}