@forjio/portal-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # @forjio/portal-ui
2
+
3
+ Shared portal chrome for the Forjio family of SaaS products. Sister
4
+ package to [`@forjio/website-ui`](https://github.com/hachimi-cat/forjio-website-ui)
5
+ — that handles the marketing site; this handles the authenticated
6
+ dashboard.
7
+
8
+ Single source of truth for:
9
+
10
+ - **`<Sidebar />`** — workspace switcher + nav sections + profile dropdown
11
+ - Workspace persistence helpers (cookie / localStorage / API)
12
+
13
+ Extracted from `saas-plugipay` on 2026-05-19 as the canonical reference
14
+ build (per [TEMPLATE.md](https://github.com/hachimi-cat/forjio-service-template/blob/master/TEMPLATE.md)).
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm i @forjio/portal-ui lucide-react
20
+ ```
21
+
22
+ Peer deps: `react`, `react-dom`, `next` (App Router), `lucide-react`.
23
+
24
+ ## Minimal usage
25
+
26
+ ```tsx
27
+ 'use client';
28
+ import { Sidebar } from '@forjio/portal-ui';
29
+ import { LayoutDashboard, CreditCard, Settings } from 'lucide-react';
30
+ import { useState, useEffect } from 'react';
31
+
32
+ export function PortalLayout({ children }: { children: React.ReactNode }) {
33
+ const [open, setOpen] = useState(false);
34
+ const [workspaces, setWorkspaces] = useState([]);
35
+ const [activeId, setActiveId] = useState<string | null>(null);
36
+
37
+ useEffect(() => {
38
+ fetch('/api/v1/workspaces', { credentials: 'include' })
39
+ .then((r) => r.json())
40
+ .then((b) => setWorkspaces(b?.data ?? []));
41
+ }, []);
42
+
43
+ return (
44
+ <div style={{ display: 'flex' }}>
45
+ <Sidebar
46
+ brandSlug="kalium"
47
+ brandName="Kalium"
48
+ brandColor="#7C3AED"
49
+ workspacePersist="cookie"
50
+ workspaces={workspaces}
51
+ activeWorkspaceId={activeId}
52
+ sections={[
53
+ {
54
+ label: 'Overview',
55
+ items: [
56
+ { href: '/dashboard', label: 'Dashboard', icon: LayoutDashboard },
57
+ ],
58
+ },
59
+ {
60
+ label: 'Money',
61
+ items: [
62
+ { href: '/dashboard/billing', label: 'Billing', icon: CreditCard },
63
+ ],
64
+ },
65
+ {
66
+ label: 'Settings',
67
+ items: [
68
+ { href: '/dashboard/settings', label: 'Settings', icon: Settings },
69
+ ],
70
+ },
71
+ ]}
72
+ user={{ name: 'Gojo Sensei', email: 'gojo@forjio.com' }}
73
+ onLogout={async () => {
74
+ await fetch('/api/v1/auth/logout', { method: 'POST', credentials: 'include' });
75
+ window.location.href = '/';
76
+ }}
77
+ open={open}
78
+ onClose={() => setOpen(false)}
79
+ />
80
+ <main style={{ flex: 1 }}>{children}</main>
81
+ </div>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ## Workspace persistence
87
+
88
+ | Mode | Persistence | Backend reads from |
89
+ |-----------|---------------------------------------------------|--------------------------------------------------|
90
+ | `cookie` | `<brand>_active_workspace` cookie (recommended) | Cookie header on every request |
91
+ | `local` | `<brand>_active_workspace` localStorage | `X-Account-Id` header you thread by hand |
92
+ | `api` | POST to `apiSwitchPath` mutating server session | Backend session state |
93
+
94
+ **Use `cookie` for new products** — it's the only mode where the
95
+ backend's auth middleware can resolve the active workspace without
96
+ either a header round-trip or a separate localStorage→header thread.
97
+ saas-plugipay's pattern; the storlaunch/linksnap localStorage variant
98
+ causes a known frontend/backend desync that bit us during the
99
+ seeded-data screenshot session.
100
+
101
+ ## Why not Tailwind?
102
+
103
+ Inline styles + CSS custom properties only. Consumers can keep their
104
+ own Tailwind config without theme collisions, and the component renders
105
+ identically regardless of the host app's CSS framework. brandColor +
106
+ brandSlug props drive everything.
107
+
108
+ ## Anti-patterns from previous patterns
109
+
110
+ - ❌ Don't hardcode brand strings in the sidebar — pass via props
111
+ - ❌ Don't use localStorage workspace persistence on new products
112
+ - ❌ Don't ship `position: fixed` overlay without the lg:hidden gate
113
+
114
+ ## License
115
+
116
+ UNLICENSED — private Forjio family package.