@bsuite/nav-core 0.1.0 → 0.2.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/dist/AppSwitcher.d.ts +17 -0
- package/dist/AppSwitcher.d.ts.map +1 -0
- package/dist/AppSwitcher.js +45 -0
- package/dist/cookieStorage.d.ts +26 -0
- package/dist/cookieStorage.d.ts.map +1 -0
- package/dist/cookieStorage.js +96 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +17 -3
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ElementType } from 'react';
|
|
2
|
+
export interface AppEntry {
|
|
3
|
+
key: string;
|
|
4
|
+
name: string;
|
|
5
|
+
shortName: string;
|
|
6
|
+
icon: ElementType;
|
|
7
|
+
url: string;
|
|
8
|
+
description: string;
|
|
9
|
+
current?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface AppSwitcherProps {
|
|
12
|
+
apps: AppEntry[];
|
|
13
|
+
currentApp?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function AppSwitcher({ apps, currentApp, className }: AppSwitcherProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
//# sourceMappingURL=AppSwitcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AppSwitcher.d.ts","sourceRoot":"","sources":["../src/AppSwitcher.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAAE,KAAK,WAAW,EAA+B,MAAM,OAAO,CAAA;AAErE,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,WAAW,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,EAAE,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,SAAc,EAAE,EAAE,gBAAgB,2CAsEjF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* AppSwitcher — Cross-app navigation dropdown, shared by all BSuite apps.
|
|
5
|
+
*
|
|
6
|
+
* Each consuming project passes its app list as props (resolving env vars locally).
|
|
7
|
+
* This component has no dependency on import.meta.env or process.env.
|
|
8
|
+
*
|
|
9
|
+
* Usage (Vite project):
|
|
10
|
+
* const apps = [
|
|
11
|
+
* { key: 'bsu', name: 'Business Suite', shortName: 'BSU', icon: Grid3X3,
|
|
12
|
+
* url: import.meta.env.VITE_BSU_URL || 'https://suite.crm7.app',
|
|
13
|
+
* description: 'Portal & dashboard' },
|
|
14
|
+
* ]
|
|
15
|
+
* <AppSwitcher apps={apps} currentApp="r8" />
|
|
16
|
+
*/
|
|
17
|
+
import { ChevronDown } from 'lucide-react';
|
|
18
|
+
import { useEffect, useRef, useState } from 'react';
|
|
19
|
+
export function AppSwitcher({ apps, currentApp, className = '' }) {
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
const ref = useRef(null);
|
|
22
|
+
const current = apps.find((a) => a.key === currentApp) ?? apps[0];
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!open)
|
|
25
|
+
return;
|
|
26
|
+
function handleClick(e) {
|
|
27
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
28
|
+
setOpen(false);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
document.addEventListener('mousedown', handleClick);
|
|
32
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
33
|
+
}, [open]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!open)
|
|
36
|
+
return;
|
|
37
|
+
function handleKey(e) {
|
|
38
|
+
if (e.key === 'Escape')
|
|
39
|
+
setOpen(false);
|
|
40
|
+
}
|
|
41
|
+
document.addEventListener('keydown', handleKey);
|
|
42
|
+
return () => document.removeEventListener('keydown', handleKey);
|
|
43
|
+
}, [open]);
|
|
44
|
+
return (_jsxs("div", { ref: ref, className: `relative ${className}`, children: [_jsxs("button", { type: "button", onClick: () => setOpen((o) => !o), "aria-haspopup": "true", "aria-expanded": open, className: "flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium hover:bg-accent transition-colors", children: [current && _jsx(current.icon, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }), _jsx("span", { className: "hidden sm:inline", children: current?.shortName ?? 'Apps' }), _jsx(ChevronDown, { className: `h-3.5 w-3.5 transition-transform ${open ? 'rotate-180' : ''}`, "aria-hidden": "true" })] }), open && (_jsx("div", { role: "menu", className: "absolute left-0 top-full z-50 mt-1 w-56 rounded-lg border border-border bg-popover shadow-lg", children: _jsx("div", { className: "p-1", children: apps.map((app) => (_jsxs("a", { href: app.url, role: "menuitem", className: `flex items-center gap-3 rounded-md px-3 py-2.5 text-sm transition-colors hover:bg-accent ${app.key === currentApp ? 'bg-accent/50 font-medium' : ''}`, "aria-current": app.key === currentApp ? 'page' : undefined, children: [_jsx(app.icon, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "font-medium truncate", children: app.name }), _jsx("div", { className: "text-xs text-muted-foreground truncate", children: app.description })] })] }, app.key))) }) }))] }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared chunked cookie storage for cross-subdomain Supabase sessions.
|
|
3
|
+
*
|
|
4
|
+
* Supabase session JSON can exceed the 4,096-byte browser cookie limit (RFC 6265 §6.1)
|
|
5
|
+
* once URI-encoded. We split values across numbered chunks (`key.0`, `key.1`, …)
|
|
6
|
+
* and reassemble on read — same approach as @supabase/ssr.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const storage = createCookieStorage({ domain: '.crm7.app' })
|
|
10
|
+
* createClient(url, key, { auth: { storage } })
|
|
11
|
+
*/
|
|
12
|
+
export interface CookieStorageOptions {
|
|
13
|
+
/** Cross-subdomain cookie domain, e.g. '.crm7.app'. Only set on matching hostnames. */
|
|
14
|
+
domain?: string;
|
|
15
|
+
/** Max cookie age in seconds. Default: 2,592,000 (30 days). */
|
|
16
|
+
maxAge?: number;
|
|
17
|
+
/** Chunk size in bytes. Default: 3,500 (leaves headroom under 4,096 limit). */
|
|
18
|
+
chunkSize?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface CookieStorageLike {
|
|
21
|
+
getItem(key: string): string | null;
|
|
22
|
+
setItem(key: string, value: string): void;
|
|
23
|
+
removeItem(key: string): void;
|
|
24
|
+
}
|
|
25
|
+
export declare function createCookieStorage(options?: CookieStorageOptions): CookieStorageLike;
|
|
26
|
+
//# sourceMappingURL=cookieStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookieStorage.d.ts","sourceRoot":"","sources":["../src/cookieStorage.ts"],"names":[],"mappings":"AACA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,oBAAoB;IACnC,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACnC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACzC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,GAAG,iBAAiB,CAsFzF"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// packages/nav-core/src/cookieStorage.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared chunked cookie storage for cross-subdomain Supabase sessions.
|
|
4
|
+
*
|
|
5
|
+
* Supabase session JSON can exceed the 4,096-byte browser cookie limit (RFC 6265 §6.1)
|
|
6
|
+
* once URI-encoded. We split values across numbered chunks (`key.0`, `key.1`, …)
|
|
7
|
+
* and reassemble on read — same approach as @supabase/ssr.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const storage = createCookieStorage({ domain: '.crm7.app' })
|
|
11
|
+
* createClient(url, key, { auth: { storage } })
|
|
12
|
+
*/
|
|
13
|
+
export function createCookieStorage(options = {}) {
|
|
14
|
+
const { domain, maxAge = 2592000, chunkSize = 3500 } = options;
|
|
15
|
+
function cookieAttrs() {
|
|
16
|
+
const parts = [];
|
|
17
|
+
if (domain && typeof window !== 'undefined' && window.location.hostname.endsWith(domain.replace(/^\./, ''))) {
|
|
18
|
+
parts.push(`domain=${domain}`);
|
|
19
|
+
}
|
|
20
|
+
parts.push('path=/');
|
|
21
|
+
parts.push(`max-age=${maxAge}`);
|
|
22
|
+
parts.push('SameSite=Lax');
|
|
23
|
+
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
|
|
24
|
+
parts.push('Secure');
|
|
25
|
+
}
|
|
26
|
+
return parts.join('; ');
|
|
27
|
+
}
|
|
28
|
+
function deleteAttrs() {
|
|
29
|
+
const parts = [];
|
|
30
|
+
if (domain && typeof window !== 'undefined' && window.location.hostname.endsWith(domain.replace(/^\./, ''))) {
|
|
31
|
+
parts.push(`domain=${domain}`);
|
|
32
|
+
}
|
|
33
|
+
parts.push('path=/');
|
|
34
|
+
parts.push('max-age=0');
|
|
35
|
+
parts.push('SameSite=Lax');
|
|
36
|
+
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
|
|
37
|
+
parts.push('Secure');
|
|
38
|
+
}
|
|
39
|
+
return parts.join('; ');
|
|
40
|
+
}
|
|
41
|
+
/** Read raw (URI-encoded) cookie value without decoding. */
|
|
42
|
+
function readCookieRaw(name) {
|
|
43
|
+
if (typeof document === 'undefined')
|
|
44
|
+
return null;
|
|
45
|
+
const prefix = `${name}=`;
|
|
46
|
+
const entry = document.cookie.split('; ').find((c) => c.startsWith(prefix));
|
|
47
|
+
return entry ? entry.slice(prefix.length) : null;
|
|
48
|
+
}
|
|
49
|
+
function removeItem(key) {
|
|
50
|
+
if (typeof document === 'undefined')
|
|
51
|
+
return;
|
|
52
|
+
const attrs = deleteAttrs();
|
|
53
|
+
document.cookie = `${key}=; ${attrs}`;
|
|
54
|
+
for (let i = 0; i < 20; i++) {
|
|
55
|
+
const chunk = readCookieRaw(`${key}.${i}`);
|
|
56
|
+
if (chunk === null)
|
|
57
|
+
break;
|
|
58
|
+
document.cookie = `${key}.${i}=; ${attrs}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
getItem(key) {
|
|
63
|
+
// Try un-chunked first (fast path for small values / legacy cookies)
|
|
64
|
+
const single = readCookieRaw(key);
|
|
65
|
+
if (single)
|
|
66
|
+
return decodeURIComponent(single);
|
|
67
|
+
// Reassemble chunks — read raw to avoid splitting encoded sequences
|
|
68
|
+
const chunks = [];
|
|
69
|
+
for (let i = 0;; i++) {
|
|
70
|
+
const chunk = readCookieRaw(`${key}.${i}`);
|
|
71
|
+
if (chunk === null)
|
|
72
|
+
break;
|
|
73
|
+
chunks.push(chunk);
|
|
74
|
+
}
|
|
75
|
+
// Decode once after joining all chunks
|
|
76
|
+
return chunks.length > 0 ? decodeURIComponent(chunks.join('')) : null;
|
|
77
|
+
},
|
|
78
|
+
setItem(key, value) {
|
|
79
|
+
if (typeof document === 'undefined')
|
|
80
|
+
return;
|
|
81
|
+
const attrs = cookieAttrs();
|
|
82
|
+
const encoded = encodeURIComponent(value);
|
|
83
|
+
removeItem(key); // clear any previous chunks first
|
|
84
|
+
if (encoded.length <= chunkSize) {
|
|
85
|
+
document.cookie = `${key}=${encoded}; ${attrs}`;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
for (let i = 0; i * chunkSize < encoded.length; i++) {
|
|
89
|
+
const slice = encoded.substring(i * chunkSize, (i + 1) * chunkSize);
|
|
90
|
+
document.cookie = `${key}.${i}=${slice}; ${attrs}`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
removeItem,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export type { IconComponent, NavItem, NavItemGroup, NavSection, NavConfig, } from './types';
|
|
2
2
|
export { isActivePath, isSectionActive } from './utils';
|
|
3
|
+
export { createCookieStorage } from './cookieStorage';
|
|
4
|
+
export type { CookieStorageOptions, CookieStorageLike } from './cookieStorage';
|
|
5
|
+
export { AppSwitcher } from './AppSwitcher';
|
|
6
|
+
export type { AppEntry, AppSwitcherProps } from './AppSwitcher';
|
|
3
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,OAAO,EACP,YAAY,EACZ,UAAU,EACV,SAAS,GACV,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,OAAO,EACP,YAAY,EACZ,UAAU,EACV,SAAS,GACV,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAExD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsuite/nav-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
"main": "dist/index.js",
|
|
16
16
|
"types": "dist/index.d.ts",
|
|
17
17
|
"exports": {
|
|
18
|
-
".": {
|
|
19
|
-
|
|
18
|
+
".": {
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts"
|
|
21
|
+
},
|
|
22
|
+
"./types": {
|
|
23
|
+
"import": "./dist/types.js",
|
|
24
|
+
"types": "./dist/types.d.ts"
|
|
25
|
+
},
|
|
20
26
|
"./tokens/d2c-sidebar.css": "./src/tokens/d2c-sidebar.css",
|
|
21
27
|
"./tokens/corporate-sidebar.css": "./src/tokens/corporate-sidebar.css"
|
|
22
28
|
},
|
|
@@ -26,6 +32,14 @@
|
|
|
26
32
|
"test": "vitest run"
|
|
27
33
|
},
|
|
28
34
|
"devDependencies": {
|
|
35
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
36
|
+
"@testing-library/react": "^16.3.2",
|
|
37
|
+
"@types/react": "^18.3.28",
|
|
38
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
39
|
+
"jsdom": "^28.1.0",
|
|
40
|
+
"lucide-react": "^0.576.0",
|
|
41
|
+
"react": "^18.3.1",
|
|
42
|
+
"react-dom": "^18.3.1",
|
|
29
43
|
"typescript": "~5.7.3",
|
|
30
44
|
"vitest": "^3.2.4"
|
|
31
45
|
}
|