@glamcor/dna-shared-nav 1.0.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,210 @@
1
+ # DNA Shared Navigation
2
+
3
+ Universal navigation component for all DNA microsites. Provides a consistent two-row navigation header across Dashboard, SPOT, Water, Pulse, Hydrogen, and Oxygen apps.
4
+
5
+ ## Architecture
6
+
7
+ The navigation consists of two rows (80px total height):
8
+
9
+ ```
10
+ Row 1 - Header (44px): [DNA Logo] [Inter-app Nav: Dashboard | SPOT | Water | Pulse | Hydrogen | Oxygen] [Avatar]
11
+ Row 2 - App Subnav (36px): [App Name] [App-specific pages] ......................... [Pinned Jobs →]
12
+ ```
13
+
14
+ - **Header**: Logo links to Dashboard, inter-app navigation for switching between DNA apps, avatar menu with user info and logout
15
+ - **App Subnav**: Current app name, app-specific page links, pinned jobs carousel (right-aligned)
16
+
17
+ ## Installation
18
+
19
+ In a DNA monorepo workspace, add to your app's `package.json`:
20
+
21
+ ```json
22
+ {
23
+ "dependencies": {
24
+ "dna-shared-nav": "*"
25
+ }
26
+ }
27
+ ```
28
+
29
+ Then run `npm install` from the monorepo root.
30
+
31
+ ## Usage
32
+
33
+ ### Basic Setup
34
+
35
+ ```tsx
36
+ import { DNANavigation } from 'dna-shared-nav';
37
+ import 'dna-shared-nav/dist/styles.css';
38
+
39
+ export function Layout({ children }) {
40
+ const user = {
41
+ id: '1',
42
+ email: 'user@example.com',
43
+ name: 'John Doe',
44
+ role: 'admin',
45
+ };
46
+
47
+ return (
48
+ <>
49
+ <DNANavigation
50
+ user={user}
51
+ currentApp="Water"
52
+ appName="Water"
53
+ subnavItems={[
54
+ { name: 'Dashboard', href: '/water', isActive: true },
55
+ { name: 'Customers', href: '/water/customers' },
56
+ { name: 'Jobs', href: '/water/jobs' },
57
+ { name: 'Analytics', href: '/water/analytics' },
58
+ ]}
59
+ onLogout={() => signOut()}
60
+ />
61
+ <main>{children}</main>
62
+ </>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ### With Pinned Jobs
68
+
69
+ Pinned jobs appear on the right side of the app subnav. They show progress rings and support a carousel when there are more than 2 jobs.
70
+
71
+ ```tsx
72
+ <DNANavigation
73
+ user={user}
74
+ currentApp="Water"
75
+ appName="Water"
76
+ subnavItems={subnavItems}
77
+ pinnedJobs={[
78
+ {
79
+ id: '1',
80
+ appName: 'Water',
81
+ appColor: '#38bdf8',
82
+ jobName: 'Q4 Data Enrichment',
83
+ progress: 59,
84
+ },
85
+ {
86
+ id: '2',
87
+ appName: 'SPOT',
88
+ appColor: '#a78bfa',
89
+ jobName: 'Lead Scoring Batch',
90
+ progress: 23,
91
+ },
92
+ ]}
93
+ onUnpinJob={(jobId) => handleUnpin(jobId)}
94
+ onLogout={() => signOut()}
95
+ />
96
+ ```
97
+
98
+ ## Props
99
+
100
+ ### DNANavigationProps
101
+
102
+ | Prop | Type | Required | Description |
103
+ |------|------|----------|-------------|
104
+ | `user` | `DNAUser \| null` | Yes | Current user object, or null if not logged in |
105
+ | `currentApp` | `string` | No | Name of active app in header nav (e.g., "Water", "SPOT") |
106
+ | `appName` | `string` | Yes | Display name shown in app subnav |
107
+ | `headerItems` | `DNANavItem[]` | No | Override default inter-app navigation items |
108
+ | `subnavItems` | `AppSubnavItem[]` | Yes | App-specific page links |
109
+ | `pinnedJobs` | `PinnedJob[]` | No | Jobs to show in pinned carousel |
110
+ | `logoSrc` | `string` | No | Custom logo image URL (defaults to "DNA" text) |
111
+ | `onLogout` | `() => void` | No | Callback when user clicks logout |
112
+ | `onUnpinJob` | `(jobId: string) => void` | No | Callback when user unpins a job |
113
+
114
+ ### DNAUser
115
+
116
+ ```typescript
117
+ interface DNAUser {
118
+ id: string;
119
+ email: string;
120
+ name: string;
121
+ role?: 'superadmin' | 'admin' | 'editor' | 'viewer';
122
+ avatar?: string;
123
+ }
124
+ ```
125
+
126
+ ### AppSubnavItem
127
+
128
+ ```typescript
129
+ interface AppSubnavItem {
130
+ name: string; // Display text
131
+ href: string; // Link URL
132
+ isActive?: boolean; // Highlight as current page
133
+ }
134
+ ```
135
+
136
+ ### PinnedJob
137
+
138
+ ```typescript
139
+ interface PinnedJob {
140
+ id: string;
141
+ appName: string; // Short name (e.g., "Water", "SPOT")
142
+ appColor?: string; // Hex color for progress ring (e.g., '#38bdf8')
143
+ jobName: string; // Job display name
144
+ progress: number; // 0-100 percentage
145
+ meta?: string; // Optional metadata (e.g., "142 / 240 records")
146
+ }
147
+ ```
148
+
149
+ ## Environment Variables
150
+
151
+ The default inter-app navigation uses these environment variables for URLs:
152
+
153
+ ```env
154
+ NEXT_PUBLIC_DNA_DASHBOARD_URL=https://dashboard.dna.example.com
155
+ NEXT_PUBLIC_SPOT_URL=https://spot.dna.example.com
156
+ NEXT_PUBLIC_DNA_WATER_URL=https://water.dna.example.com
157
+ NEXT_PUBLIC_DNA_PULSE_URL=https://pulse.dna.example.com
158
+ NEXT_PUBLIC_DNA_HYDROGEN_URL=https://hydrogen.dna.example.com
159
+ NEXT_PUBLIC_DNA_OXYGEN_URL=https://oxygen.dna.example.com
160
+ ```
161
+
162
+ If not set, links fall back to relative paths (`/`, `/spot`, `/water`, etc.).
163
+
164
+ ## App Colors
165
+
166
+ Recommended colors for each app's pinned jobs:
167
+
168
+ | App | Color | Hex |
169
+ |-----|-------|-----|
170
+ | Water | Sky Blue | `#38bdf8` |
171
+ | SPOT | Purple | `#a78bfa` |
172
+ | Pulse | Rose | `#fb7185` |
173
+ | Hydrogen | Emerald | `#34d399` |
174
+ | Oxygen | Amber | `#fbbf24` |
175
+
176
+ ## Individual Components
177
+
178
+ For advanced use cases, you can import components individually:
179
+
180
+ ```tsx
181
+ import { DNAHeader, AppSubnav, PinnedJobs, AvatarMenu } from 'dna-shared-nav';
182
+ ```
183
+
184
+ ## Styling
185
+
186
+ The navigation uses CSS classes prefixed with `dna-`, `app-subnav-`, and `pinned-`. Styles require these fonts:
187
+
188
+ - **DM Sans** - Primary text
189
+ - **JetBrains Mono** - Logo, progress values
190
+
191
+ Add to your app's `<head>`:
192
+
193
+ ```html
194
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=JetBrains+Mono:wght@500;600&display=swap" rel="stylesheet">
195
+ ```
196
+
197
+ ## Role-Based Navigation
198
+
199
+ Navigation items can be restricted by role. The role hierarchy is:
200
+
201
+ `viewer` < `editor` < `admin` < `superadmin`
202
+
203
+ ```tsx
204
+ const headerItems = [
205
+ { name: 'Dashboard', href: '/' },
206
+ { name: 'Admin', href: '/admin', requiredRole: 'admin' }, // Only admin+ see this
207
+ ];
208
+
209
+ <DNANavigation headerItems={headerItems} ... />
210
+ ```
@@ -0,0 +1,2 @@
1
+ import type { AppSubnavProps } from './types';
2
+ export declare function AppSubnav({ appName, items, pinnedJobs, onUnpinJob, }: AppSubnavProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { PinnedJobs } from './PinnedJobs';
4
+ export function AppSubnav({ appName, items, pinnedJobs = [], onUnpinJob, }) {
5
+ return (_jsxs("nav", { className: "app-subnav", children: [_jsxs("div", { className: "app-subnav-left", children: [_jsx("span", { className: "app-subnav-name", children: appName }), _jsx("div", { className: "app-subnav-links", children: items.map((item) => (_jsx("a", { href: item.href, className: `app-subnav-item ${item.isActive ? 'active' : ''}`, children: item.name }, item.name))) })] }), _jsx(PinnedJobs, { jobs: pinnedJobs, onUnpin: onUnpinJob })] }));
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { AvatarMenuProps } from './types';
2
+ export declare function AvatarMenu({ userName, userEmail, initials, onLogout }: AvatarMenuProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useRef, useEffect } from 'react';
4
+ export function AvatarMenu({ userName, userEmail, initials, onLogout }) {
5
+ const [isOpen, setIsOpen] = useState(false);
6
+ const menuRef = useRef(null);
7
+ useEffect(() => {
8
+ const handleClickOutside = (event) => {
9
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
10
+ setIsOpen(false);
11
+ }
12
+ };
13
+ if (isOpen) {
14
+ document.addEventListener('click', handleClickOutside);
15
+ }
16
+ return () => {
17
+ document.removeEventListener('click', handleClickOutside);
18
+ };
19
+ }, [isOpen]);
20
+ const handleLogout = () => {
21
+ setIsOpen(false);
22
+ if (onLogout) {
23
+ onLogout();
24
+ }
25
+ else {
26
+ window.location.href = '/api/auth/logout';
27
+ }
28
+ };
29
+ return (_jsxs("div", { className: "dna-avatar-menu", ref: menuRef, children: [_jsx("div", { className: "dna-avatar-button", onClick: (e) => {
30
+ e.stopPropagation();
31
+ setIsOpen(!isOpen);
32
+ }, children: initials }), _jsxs("div", { className: `dna-dropdown-menu ${isOpen ? 'active' : ''}`, children: [_jsxs("div", { className: "dna-dropdown-header", children: [_jsx("div", { className: "dna-dropdown-user-name", children: userName }), _jsx("div", { className: "dna-dropdown-user-email", children: userEmail })] }), _jsx("a", { href: "/profile", className: "dna-dropdown-item", children: "Profile" }), _jsx("a", { href: "/settings", className: "dna-dropdown-item", children: "Settings" }), _jsx("a", { href: "/team", className: "dna-dropdown-item", children: "Team" }), _jsx("div", { className: "dna-dropdown-divider" }), _jsx("a", { href: "/docs", className: "dna-dropdown-item", children: "Documentation" }), _jsx("a", { href: "/support", className: "dna-dropdown-item", children: "Support" }), _jsx("div", { className: "dna-dropdown-divider" }), _jsx("button", { onClick: handleLogout, className: "dna-dropdown-item danger", children: "Sign Out" })] })] }));
33
+ }
@@ -0,0 +1,2 @@
1
+ import type { DNAHeaderProps } from './types';
2
+ export declare function DNAHeader({ user, currentApp, items, logoSrc, onLogout, }: DNAHeaderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { AvatarMenu } from './AvatarMenu';
4
+ const DEFAULT_NAV_ITEMS = [
5
+ { name: 'Dashboard', href: process.env.NEXT_PUBLIC_DNA_DASHBOARD_URL || '/' },
6
+ { name: 'SPOT', href: process.env.NEXT_PUBLIC_SPOT_URL || '/spot' },
7
+ { name: 'Water', href: process.env.NEXT_PUBLIC_DNA_WATER_URL || '/water' },
8
+ { name: 'Pulse', href: process.env.NEXT_PUBLIC_DNA_PULSE_URL || '/pulse' },
9
+ { name: 'Hydrogen', href: process.env.NEXT_PUBLIC_DNA_HYDROGEN_URL || '/hydrogen' },
10
+ { name: 'Oxygen', href: process.env.NEXT_PUBLIC_DNA_OXYGEN_URL || '/oxygen' },
11
+ ];
12
+ const ROLE_HIERARCHY = ['viewer', 'editor', 'admin', 'superadmin'];
13
+ function hasPermission(userRole, requiredRole) {
14
+ if (!userRole)
15
+ return false;
16
+ const userRoleIndex = ROLE_HIERARCHY.indexOf(userRole);
17
+ const requiredRoleIndex = ROLE_HIERARCHY.indexOf(requiredRole);
18
+ return userRoleIndex >= requiredRoleIndex;
19
+ }
20
+ function getUserInitials(name) {
21
+ return name
22
+ .split(' ')
23
+ .map((part) => part[0])
24
+ .join('')
25
+ .toUpperCase()
26
+ .slice(0, 2);
27
+ }
28
+ export function DNAHeader({ user, currentApp = 'Dashboard', items = DEFAULT_NAV_ITEMS, logoSrc = '/DNA_LOGO.png', onLogout, }) {
29
+ const visibleItems = items.filter((item) => {
30
+ if (!item.requiredRole)
31
+ return true;
32
+ if (!user)
33
+ return false;
34
+ return hasPermission(user.role, item.requiredRole);
35
+ });
36
+ return (_jsxs("header", { className: "dna-header", children: [_jsx("a", { href: process.env.NEXT_PUBLIC_DNA_DASHBOARD_URL || '/', className: "dna-logo-container", children: _jsx("img", { src: logoSrc, alt: "DNA", className: "dna-logo-img" }) }), _jsx("nav", { className: "dna-nav-links", children: visibleItems.map((item) => {
37
+ const isActive = item.name === currentApp;
38
+ return (_jsxs("a", { href: item.href, className: `dna-nav-item ${isActive ? 'active' : ''}`, children: [item.hasNotification && _jsx("span", { className: "dna-notification-dot" }), item.name] }, item.name));
39
+ }) }), _jsx("div", { className: "dna-header-right", children: user ? (_jsx(AvatarMenu, { userName: user.name, userEmail: user.email, initials: getUserInitials(user.name), onLogout: onLogout })) : (_jsx("a", { href: "/login", className: "dna-login-btn", children: "Sign In" })) })] }));
40
+ }
@@ -0,0 +1,28 @@
1
+ import type { DNANavigationProps } from './types';
2
+ /**
3
+ * DNANavigation - Complete navigation component for DNA microsites
4
+ *
5
+ * Renders:
6
+ * 1. Header bar: Logo | Inter-app navigation | Avatar
7
+ * 2. App subnav: App name | App-specific pages | Pinned jobs (right)
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * <DNANavigation
12
+ * user={user}
13
+ * currentApp="Water"
14
+ * appName="Water"
15
+ * subnavItems={[
16
+ * { name: 'Dashboard', href: '/admin', isActive: true },
17
+ * { name: 'Customers', href: '/admin/customers' },
18
+ * { name: 'Jobs', href: '/admin/jobs' },
19
+ * ]}
20
+ * pinnedJobs={[
21
+ * { id: '1', appName: 'Water', jobName: 'Q4 Enrichment', progress: 59, appColor: '#38bdf8' },
22
+ * ]}
23
+ * onUnpinJob={(id) => console.log('Unpin:', id)}
24
+ * onLogout={() => signOut()}
25
+ * />
26
+ * ```
27
+ */
28
+ export declare function DNANavigation({ user, currentApp, appName, headerItems, subnavItems, pinnedJobs, logoSrc, onLogout, onUnpinJob, }: DNANavigationProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { DNAHeader } from './DNAHeader';
4
+ import { AppSubnav } from './AppSubnav';
5
+ /**
6
+ * DNANavigation - Complete navigation component for DNA microsites
7
+ *
8
+ * Renders:
9
+ * 1. Header bar: Logo | Inter-app navigation | Avatar
10
+ * 2. App subnav: App name | App-specific pages | Pinned jobs (right)
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <DNANavigation
15
+ * user={user}
16
+ * currentApp="Water"
17
+ * appName="Water"
18
+ * subnavItems={[
19
+ * { name: 'Dashboard', href: '/admin', isActive: true },
20
+ * { name: 'Customers', href: '/admin/customers' },
21
+ * { name: 'Jobs', href: '/admin/jobs' },
22
+ * ]}
23
+ * pinnedJobs={[
24
+ * { id: '1', appName: 'Water', jobName: 'Q4 Enrichment', progress: 59, appColor: '#38bdf8' },
25
+ * ]}
26
+ * onUnpinJob={(id) => console.log('Unpin:', id)}
27
+ * onLogout={() => signOut()}
28
+ * />
29
+ * ```
30
+ */
31
+ export function DNANavigation({ user, currentApp, appName, headerItems, subnavItems, pinnedJobs = [], logoSrc, onLogout, onUnpinJob, }) {
32
+ return (_jsxs("div", { className: "dna-navigation", children: [_jsx(DNAHeader, { user: user, currentApp: currentApp, items: headerItems, logoSrc: logoSrc, onLogout: onLogout }), _jsx(AppSubnav, { appName: appName, items: subnavItems, pinnedJobs: pinnedJobs, onUnpinJob: onUnpinJob })] }));
33
+ }
@@ -0,0 +1,2 @@
1
+ import type { PinnedJobsProps } from './types';
2
+ export declare function PinnedJobs({ jobs, onUnpin }: PinnedJobsProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useRef, useEffect } from 'react';
4
+ // Progress ring component
5
+ function ProgressRing({ progress, color = '#facc15' }) {
6
+ const radius = 9;
7
+ const circumference = 2 * Math.PI * radius;
8
+ const offset = circumference - (progress / 100) * circumference;
9
+ return (_jsxs("div", { className: "pinned-progress-ring", children: [_jsxs("svg", { viewBox: "0 0 24 24", children: [_jsx("circle", { className: "pinned-ring-bg", cx: "12", cy: "12", r: radius }), _jsx("circle", { className: "pinned-ring-progress", cx: "12", cy: "12", r: radius, style: {
10
+ strokeDasharray: circumference,
11
+ strokeDashoffset: offset,
12
+ stroke: color,
13
+ } })] }), _jsxs("span", { className: "pinned-progress-value", children: [progress, "%"] })] }));
14
+ }
15
+ // Single pinned job pill
16
+ function PinnedJobPill({ job, onUnpin, }) {
17
+ const handleUnpin = (e) => {
18
+ e.stopPropagation();
19
+ onUnpin?.(job.id);
20
+ };
21
+ return (_jsxs("div", { className: "pinned-job", children: [_jsx(ProgressRing, { progress: job.progress, color: job.appColor }), _jsxs("div", { className: "pinned-job-info", children: [_jsx("span", { className: "pinned-job-app", style: { color: job.appColor }, children: job.appName }), _jsx("span", { className: "pinned-job-name", children: job.jobName })] }), _jsx("button", { className: "pinned-unpin-btn", onClick: handleUnpin, title: "Unpin", "aria-label": `Unpin ${job.jobName}`, children: _jsx("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12" }) }) })] }));
22
+ }
23
+ export function PinnedJobs({ jobs, onUnpin }) {
24
+ const [scrollIndex, setScrollIndex] = useState(0);
25
+ const [showArrows, setShowArrows] = useState(false);
26
+ const containerRef = useRef(null);
27
+ // Check if we need carousel arrows
28
+ useEffect(() => {
29
+ setShowArrows(jobs.length > 2);
30
+ // Reset scroll if jobs are removed
31
+ if (scrollIndex > 0 && scrollIndex >= jobs.length - 1) {
32
+ setScrollIndex(Math.max(0, jobs.length - 2));
33
+ }
34
+ }, [jobs.length, scrollIndex]);
35
+ const maxIndex = Math.max(0, jobs.length - 2);
36
+ const scrollPrev = () => {
37
+ setScrollIndex((prev) => Math.max(0, prev - 1));
38
+ };
39
+ const scrollNext = () => {
40
+ setScrollIndex((prev) => Math.min(maxIndex, prev + 1));
41
+ };
42
+ if (jobs.length === 0) {
43
+ return null;
44
+ }
45
+ return (_jsxs("div", { className: "pinned-jobs", children: [showArrows && scrollIndex > 0 && (_jsx("button", { className: "pinned-nav-btn", onClick: scrollPrev, "aria-label": "Previous pinned jobs", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: _jsx("path", { d: "M15 18l-6-6 6-6" }) }) })), _jsx("div", { className: "pinned-jobs-container", ref: containerRef, style: {
46
+ transform: `translateX(-${scrollIndex * 200}px)`,
47
+ }, children: jobs.map((job) => (_jsx(PinnedJobPill, { job: job, onUnpin: onUnpin }, job.id))) }), showArrows && scrollIndex < maxIndex && (_jsx("button", { className: "pinned-nav-btn", onClick: scrollNext, "aria-label": "Next pinned jobs", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: _jsx("path", { d: "M9 18l6-6-6-6" }) }) }))] }));
48
+ }
@@ -0,0 +1,6 @@
1
+ export { DNANavigation } from './DNANavigation';
2
+ export { DNAHeader } from './DNAHeader';
3
+ export { AppSubnav } from './AppSubnav';
4
+ export { PinnedJobs } from './PinnedJobs';
5
+ export { AvatarMenu } from './AvatarMenu';
6
+ export * from './types';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // Main combined component
2
+ export { DNANavigation } from './DNANavigation';
3
+ // Individual components (for advanced usage)
4
+ export { DNAHeader } from './DNAHeader';
5
+ export { AppSubnav } from './AppSubnav';
6
+ export { PinnedJobs } from './PinnedJobs';
7
+ export { AvatarMenu } from './AvatarMenu';
8
+ // Types
9
+ export * from './types';
@@ -0,0 +1,425 @@
1
+ /* DNA Shared Navigation Styles */
2
+ /* Two-row navigation: Header + App Subnav */
3
+
4
+ /* ============================================
5
+ NAVIGATION WRAPPER
6
+ ============================================ */
7
+ .dna-navigation {
8
+ position: sticky;
9
+ top: 0;
10
+ z-index: 100;
11
+ font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
12
+ }
13
+
14
+ /* ============================================
15
+ HEADER (Logo + Inter-app Nav + Avatar)
16
+ ============================================ */
17
+ .dna-header {
18
+ background: rgba(23, 23, 23, 0.8);
19
+ border-bottom: 1px solid #1a1a1a;
20
+ padding: 0 1.5rem;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: space-between;
24
+ backdrop-filter: blur(12px);
25
+ height: 44px;
26
+ }
27
+
28
+ .dna-logo-container {
29
+ height: 28px;
30
+ display: flex;
31
+ align-items: center;
32
+ text-decoration: none;
33
+ }
34
+
35
+ .dna-logo-img {
36
+ height: 24px;
37
+ width: auto;
38
+ }
39
+
40
+ .dna-logo-text {
41
+ font-family: 'JetBrains Mono', monospace;
42
+ font-size: 16px;
43
+ font-weight: 600;
44
+ letter-spacing: 0.05em;
45
+ color: #facc15;
46
+ text-transform: uppercase;
47
+ text-shadow: 0 0 20px rgba(250, 204, 21, 0.3);
48
+ }
49
+
50
+ /* Inter-app navigation links (centered) */
51
+ .dna-nav-links {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 0.25rem;
55
+ }
56
+
57
+ .dna-nav-item {
58
+ padding: 0.25rem 0.75rem;
59
+ border-radius: 5px;
60
+ background: transparent;
61
+ border: 1px solid transparent;
62
+ color: #737373;
63
+ font-size: 11px;
64
+ font-weight: 500;
65
+ cursor: pointer;
66
+ transition: all 0.15s ease;
67
+ text-decoration: none;
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 0.375rem;
71
+ }
72
+
73
+ .dna-nav-item:hover {
74
+ color: #a3a3a3;
75
+ background: #1f1f1f;
76
+ }
77
+
78
+ .dna-nav-item.active {
79
+ color: #facc15;
80
+ background: rgba(250, 204, 21, 0.08);
81
+ border-color: rgba(250, 204, 21, 0.2);
82
+ }
83
+
84
+ .dna-header-right {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 0.75rem;
88
+ }
89
+
90
+ .dna-login-btn {
91
+ padding: 0.375rem 0.875rem;
92
+ border-radius: 5px;
93
+ background: transparent;
94
+ border: 1px solid #262626;
95
+ color: #a3a3a3;
96
+ font-size: 11px;
97
+ font-weight: 500;
98
+ cursor: pointer;
99
+ transition: all 0.15s ease;
100
+ text-decoration: none;
101
+ }
102
+
103
+ .dna-login-btn:hover {
104
+ color: #fafafa;
105
+ border-color: #404040;
106
+ }
107
+
108
+ .dna-notification-dot {
109
+ width: 6px;
110
+ height: 6px;
111
+ background: #facc15;
112
+ border-radius: 50%;
113
+ animation: dna-pulse 2s infinite;
114
+ box-shadow: 0 0 8px rgba(250, 204, 21, 0.6);
115
+ }
116
+
117
+ @keyframes dna-pulse {
118
+ 0%, 100% {
119
+ opacity: 1;
120
+ transform: scale(1);
121
+ }
122
+ 50% {
123
+ opacity: 0.6;
124
+ transform: scale(1.1);
125
+ }
126
+ }
127
+
128
+ /* ============================================
129
+ APP SUBNAV (App name + pages + pinned jobs)
130
+ ============================================ */
131
+ .app-subnav {
132
+ background: rgba(14, 14, 14, 0.95);
133
+ border-bottom: 1px solid #1a1a1a;
134
+ padding: 0 1.5rem;
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: space-between;
138
+ height: 36px;
139
+ }
140
+
141
+ .app-subnav-left {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 0.5rem;
145
+ }
146
+
147
+ .app-subnav-name {
148
+ font-size: 14px;
149
+ font-weight: 600;
150
+ color: #fafafa;
151
+ margin-right: 1rem;
152
+ padding-right: 1rem;
153
+ border-right: 1px solid #262626;
154
+ }
155
+
156
+ .app-subnav-links {
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 0.25rem;
160
+ }
161
+
162
+ .app-subnav-item {
163
+ padding: 0.375rem 0.75rem;
164
+ color: #525252;
165
+ font-size: 11px;
166
+ font-weight: 500;
167
+ text-decoration: none;
168
+ transition: all 0.15s ease;
169
+ border-radius: 5px;
170
+ }
171
+
172
+ .app-subnav-item:hover {
173
+ color: #a3a3a3;
174
+ background: rgba(255, 255, 255, 0.03);
175
+ }
176
+
177
+ .app-subnav-item.active {
178
+ color: #fafafa;
179
+ background: rgba(255, 255, 255, 0.05);
180
+ }
181
+
182
+ /* ============================================
183
+ PINNED JOBS (right side of app subnav)
184
+ ============================================ */
185
+ .pinned-jobs {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 0.5rem;
189
+ overflow: hidden;
190
+ }
191
+
192
+ .pinned-jobs-container {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 0.5rem;
196
+ transition: transform 0.3s ease;
197
+ }
198
+
199
+ .pinned-job {
200
+ display: flex;
201
+ align-items: center;
202
+ gap: 0.5rem;
203
+ padding: 0.25rem 0.5rem 0.25rem 0.25rem;
204
+ background: rgba(255, 255, 255, 0.03);
205
+ border: 1px solid #1f1f1f;
206
+ border-radius: 6px;
207
+ transition: all 0.15s ease;
208
+ cursor: pointer;
209
+ white-space: nowrap;
210
+ }
211
+
212
+ .pinned-job:hover {
213
+ border-color: #2a2a2a;
214
+ background: rgba(255, 255, 255, 0.05);
215
+ }
216
+
217
+ .pinned-progress-ring {
218
+ width: 24px;
219
+ height: 24px;
220
+ position: relative;
221
+ flex-shrink: 0;
222
+ }
223
+
224
+ .pinned-progress-ring svg {
225
+ transform: rotate(-90deg);
226
+ width: 24px;
227
+ height: 24px;
228
+ }
229
+
230
+ .pinned-ring-bg {
231
+ fill: none;
232
+ stroke: #262626;
233
+ stroke-width: 2.5;
234
+ }
235
+
236
+ .pinned-ring-progress {
237
+ fill: none;
238
+ stroke-width: 2.5;
239
+ stroke-linecap: round;
240
+ filter: drop-shadow(0 0 3px rgba(250, 204, 21, 0.4));
241
+ transition: stroke-dashoffset 0.3s ease;
242
+ }
243
+
244
+ .pinned-progress-value {
245
+ position: absolute;
246
+ top: 50%;
247
+ left: 50%;
248
+ transform: translate(-50%, -50%);
249
+ font-size: 7px;
250
+ font-weight: 600;
251
+ font-family: 'JetBrains Mono', monospace;
252
+ color: #fafafa;
253
+ }
254
+
255
+ .pinned-job-info {
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 0.375rem;
259
+ }
260
+
261
+ .pinned-job-app {
262
+ font-size: 9px;
263
+ font-weight: 600;
264
+ text-transform: uppercase;
265
+ letter-spacing: 0.03em;
266
+ }
267
+
268
+ .pinned-job-name {
269
+ font-size: 11px;
270
+ font-weight: 500;
271
+ color: #a3a3a3;
272
+ max-width: 120px;
273
+ overflow: hidden;
274
+ text-overflow: ellipsis;
275
+ }
276
+
277
+ .pinned-unpin-btn {
278
+ background: none;
279
+ border: none;
280
+ color: #404040;
281
+ cursor: pointer;
282
+ padding: 2px;
283
+ border-radius: 3px;
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: center;
287
+ transition: all 0.15s ease;
288
+ margin-left: 0.25rem;
289
+ }
290
+
291
+ .pinned-unpin-btn:hover {
292
+ color: #f87171;
293
+ background: rgba(248, 113, 113, 0.1);
294
+ }
295
+
296
+ /* Carousel arrows */
297
+ .pinned-nav-btn {
298
+ background: rgba(255, 255, 255, 0.05);
299
+ border: 1px solid #262626;
300
+ color: #525252;
301
+ width: 20px;
302
+ height: 20px;
303
+ border-radius: 4px;
304
+ display: flex;
305
+ align-items: center;
306
+ justify-content: center;
307
+ cursor: pointer;
308
+ transition: all 0.15s ease;
309
+ flex-shrink: 0;
310
+ }
311
+
312
+ .pinned-nav-btn:hover {
313
+ color: #fafafa;
314
+ border-color: #404040;
315
+ background: rgba(255, 255, 255, 0.08);
316
+ }
317
+
318
+ /* ============================================
319
+ AVATAR MENU
320
+ ============================================ */
321
+ .dna-avatar-menu {
322
+ position: relative;
323
+ }
324
+
325
+ .dna-avatar-button {
326
+ width: 28px;
327
+ height: 28px;
328
+ border-radius: 50%;
329
+ background: linear-gradient(135deg, #fde047 0%, #facc15 100%);
330
+ border: 2px solid #0a0a0a;
331
+ box-shadow:
332
+ inset 0 1px 0 rgba(255, 255, 255, 0.2),
333
+ 0 0 0 1px #262626;
334
+ cursor: pointer;
335
+ transition: all 0.15s ease;
336
+ display: flex;
337
+ align-items: center;
338
+ justify-content: center;
339
+ font-weight: 600;
340
+ font-size: 10px;
341
+ color: #0a0a0a;
342
+ text-transform: uppercase;
343
+ font-family: 'JetBrains Mono', monospace;
344
+ }
345
+
346
+ .dna-avatar-button:hover {
347
+ transform: translateY(-1px);
348
+ box-shadow:
349
+ inset 0 1px 0 rgba(255, 255, 255, 0.3),
350
+ 0 0 0 1px rgba(250, 204, 21, 0.3),
351
+ 0 0 12px rgba(250, 204, 21, 0.2);
352
+ }
353
+
354
+ /* Dropdown Menu */
355
+ .dna-dropdown-menu {
356
+ position: absolute;
357
+ top: calc(100% + 0.5rem);
358
+ right: 0;
359
+ min-width: 180px;
360
+ background: linear-gradient(135deg, #171717 0%, #141414 100%);
361
+ border: 1px solid #262626;
362
+ border-radius: 8px;
363
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
364
+ opacity: 0;
365
+ visibility: hidden;
366
+ transform: translateY(-8px);
367
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
368
+ z-index: 1000;
369
+ overflow: hidden;
370
+ }
371
+
372
+ .dna-dropdown-menu.active {
373
+ opacity: 1;
374
+ visibility: visible;
375
+ transform: translateY(0);
376
+ }
377
+
378
+ .dna-dropdown-header {
379
+ padding: 0.625rem 0.875rem;
380
+ border-bottom: 1px solid #1a1a1a;
381
+ }
382
+
383
+ .dna-dropdown-user-name {
384
+ font-size: 12px;
385
+ font-weight: 600;
386
+ color: #fafafa;
387
+ }
388
+
389
+ .dna-dropdown-user-email {
390
+ font-size: 10px;
391
+ color: #737373;
392
+ }
393
+
394
+ .dna-dropdown-divider {
395
+ height: 1px;
396
+ background: #1a1a1a;
397
+ }
398
+
399
+ .dna-dropdown-item {
400
+ padding: 0.5rem 0.875rem;
401
+ color: #a3a3a3;
402
+ font-size: 11px;
403
+ font-weight: 500;
404
+ cursor: pointer;
405
+ transition: all 0.15s ease;
406
+ display: block;
407
+ text-decoration: none;
408
+ background: none;
409
+ border: none;
410
+ width: 100%;
411
+ text-align: left;
412
+ }
413
+
414
+ .dna-dropdown-item:hover {
415
+ background: #1f1f1f;
416
+ color: #fafafa;
417
+ }
418
+
419
+ .dna-dropdown-item.danger {
420
+ color: #f87171;
421
+ }
422
+
423
+ .dna-dropdown-item.danger:hover {
424
+ background: rgba(248, 113, 113, 0.1);
425
+ }
@@ -0,0 +1,60 @@
1
+ export interface DNAUser {
2
+ id: string;
3
+ email: string;
4
+ name: string;
5
+ role?: 'superadmin' | 'admin' | 'editor' | 'viewer';
6
+ avatar?: string;
7
+ }
8
+ export interface DNANavItem {
9
+ name: string;
10
+ href: string;
11
+ requiredRole?: 'viewer' | 'editor' | 'admin' | 'superadmin';
12
+ hasNotification?: boolean;
13
+ }
14
+ export interface AppSubnavItem {
15
+ name: string;
16
+ href: string;
17
+ isActive?: boolean;
18
+ }
19
+ export interface PinnedJob {
20
+ id: string;
21
+ appName: string;
22
+ appColor?: string;
23
+ jobName: string;
24
+ progress: number;
25
+ meta?: string;
26
+ }
27
+ export interface DNAHeaderProps {
28
+ user: DNAUser | null;
29
+ currentApp?: string;
30
+ items?: DNANavItem[];
31
+ logoSrc?: string;
32
+ onLogout?: () => void;
33
+ }
34
+ export interface AppSubnavProps {
35
+ appName: string;
36
+ items: AppSubnavItem[];
37
+ pinnedJobs?: PinnedJob[];
38
+ onUnpinJob?: (jobId: string) => void;
39
+ }
40
+ export interface DNANavigationProps {
41
+ user: DNAUser | null;
42
+ currentApp?: string;
43
+ appName: string;
44
+ headerItems?: DNANavItem[];
45
+ subnavItems: AppSubnavItem[];
46
+ pinnedJobs?: PinnedJob[];
47
+ logoSrc?: string;
48
+ onLogout?: () => void;
49
+ onUnpinJob?: (jobId: string) => void;
50
+ }
51
+ export interface AvatarMenuProps {
52
+ userName: string;
53
+ userEmail: string;
54
+ initials: string;
55
+ onLogout?: () => void;
56
+ }
57
+ export interface PinnedJobsProps {
58
+ jobs: PinnedJob[];
59
+ onUnpin?: (jobId: string) => void;
60
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@glamcor/dna-shared-nav",
3
+ "version": "1.0.0",
4
+ "description": "Shared navigation component for DNA microsites",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.js"
12
+ },
13
+ "./dist/styles.css": "./dist/styles.css"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc && cp src/styles.css dist/styles.css",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/glamcor/dna-shared-nav.git"
25
+ },
26
+ "keywords": [
27
+ "dna",
28
+ "navigation",
29
+ "react",
30
+ "component"
31
+ ],
32
+ "author": "GlamCor",
33
+ "license": "UNLICENSED",
34
+ "peerDependencies": {
35
+ "react": "^18.0.0",
36
+ "react-dom": "^18.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^20.0.0",
40
+ "@types/react": "^18.2.0",
41
+ "@types/react-dom": "^18.2.0",
42
+ "typescript": "^5.0.0"
43
+ },
44
+ "publishConfig": {
45
+ "access": "restricted"
46
+ }
47
+ }