diff --git a/app/(dashboard)/byra/kpi/page.tsx b/app/(dashboard)/byra/kpi/page.tsx
index f0c903f9..54a6b526 100644
--- a/app/(dashboard)/byra/kpi/page.tsx
+++ b/app/(dashboard)/byra/kpi/page.tsx
@@ -1,8 +1,7 @@
import { redirect } from 'next/navigation'
import { getTranslations } from 'next-intl/server'
-import { TrendingUp } from 'lucide-react'
import { PageHeader } from '@/components/ui/page-header'
-import { EmptyState } from '@/components/ui/empty-state'
+import { EmptyByraClients } from '@/components/ui/empty-state'
import { fetchByraKpiOverview } from '@/lib/byra/kpi-overview'
import ByraKpiView from '@/components/byra/ByraKpiView'
import { getDashboardAuthContext } from '../../request-context'
@@ -46,11 +45,7 @@ export default async function ByraKpiPage({
{overview.allClients.length === 0 ? (
-
+
) : (
{
+ it('is only passed from client components', () => {
+ const files = [
+ ...collectTsxFiles(path.join(repoRoot, 'app')),
+ ...collectTsxFiles(path.join(repoRoot, 'components')),
+ ]
+
+ const offenders = files.filter((file) => {
+ const source = fs.readFileSync(file, 'utf8')
+ if (isClientComponent(source)) return false
+ // `[^>]*` keeps the match inside a single JSX element, newlines included.
+ return /]*\bicon=\{/.test(source)
+ })
+
+ expect(offenders.map((f) => path.relative(repoRoot, f))).toEqual([])
+ })
+})
+
+describe('byrå KPI empty state', () => {
+ const pagePath = path.join(repoRoot, 'app', '(dashboard)', 'byra', 'kpi', 'page.tsx')
+
+ it('renders the preset from the server page instead of passing an icon', () => {
+ const source = fs.readFileSync(pagePath, 'utf8')
+ expect(isClientComponent(source)).toBe(false)
+ expect(source).toMatch(//)
+ expect(source).not.toMatch(/from 'lucide-react'/)
+ })
+
+ it('keeps the icon and the copy in the preset', () => {
+ const source = fs.readFileSync(path.join(repoRoot, 'components', 'ui', 'empty-state.tsx'), 'utf8')
+ expect(source).toMatch(/export function EmptyByraClients\(\)/)
+ expect(source).toMatch(/icon=\{TrendingUp\}/)
+ // Variable-agnostic: the preset reads from the `byra` namespace under its
+ // own translator name, so assert the keys, not the caller's identifier.
+ expect(source).toMatch(/\w+\('kpi_empty_title'\)/)
+ expect(source).toMatch(/\w+\('kpi_empty_description'\)/)
+ })
+})
diff --git a/components/ui/empty-state.tsx b/components/ui/empty-state.tsx
index b7a32f5c..02b62947 100644
--- a/components/ui/empty-state.tsx
+++ b/components/ui/empty-state.tsx
@@ -13,6 +13,7 @@ import {
FileText,
Calendar,
Plus,
+ TrendingUp,
type LucideIcon,
} from 'lucide-react'
import { SupportLink } from '@/components/ui/support-link'
@@ -185,3 +186,25 @@ export function EmptyReports() {
/>
)
}
+
+/**
+ * Byrå cockpit: no client companies yet. A preset, not a bare , because the only caller is a Server Component: a
+ * lucide icon is a forwardRef object that cannot cross the RSC boundary as a
+ * prop, while a reference to this client component can. The copy lives in the
+ * byra namespace, where the byrå surfaces already keep it.
+ */
+export function EmptyByraClients() {
+ // Named tByra, not t: this is the only preset here that reads from a
+ // namespace other than `empty`, and i18n/__tests__/message-keys.test.ts maps
+ // one variable name to one namespace per file. Reusing `t` would silently
+ // re-point every other preset's key in this file at `byra`.
+ const tByra = useTranslations('byra')
+ return (
+
+ )
+}