learning_ai_common_plat/packages/dashboard-shell/src/TopBar.tsx
saravanakumardb1 cc0bffea86 feat(packages): close ROADMAP TODOs #1, #2, #3 — density tier, react-auth fix, routePrefix
Three coordinated package changes addressing Wave 1 cross-repo TODOs
from the UI/UX roadmap (learning_ai_uxui_web/docs/ROADMAP_2026.md §10).

═══════════════════════════════════════════════════════════════════════
TODO #2 — @bytelyst/react-auth bump 0.1.8 → 0.2.0
═══════════════════════════════════════════════════════════════════════
  - Changes 'workspace:*' to 'workspace:^' for the @bytelyst/api-client
    dependency. On pnpm publish this resolves to a caret range (e.g.
    ^0.1.6) instead of '*', restoring installability for consumers.
    (The 0.1.6 tarball was published with a literal 'workspace:*'
    string — newer minor bump unblocks the showcase react-auth demo.)
  - 21 tests still passing.

═══════════════════════════════════════════════════════════════════════
TODO #3 — @bytelyst/dashboard-shell bump 0.1.7 → 0.2.0
═══════════════════════════════════════════════════════════════════════
  - Adds 'routePrefix?: string' prop to DashboardShellProps, SidebarProps,
    and TopBarProps. Threads through DashboardShell → Sidebar + TopBar.
  - Built-in /profile, /billing, /settings links now use the prefix:
      routePrefix="/app" → /app/profile, /app/billing, /app/settings
  - Defaults to '' (empty string) — fully back-compat with 0.1.x callers.
  - 2 new vitest cases covering both prefixed and default behavior;
    43 / 43 tests passing (+2 from 41).

═══════════════════════════════════════════════════════════════════════
TODO #1 — @bytelyst/design-tokens bump 0.1.8 → 0.2.0
═══════════════════════════════════════════════════════════════════════
  - Adds a density-aware spacing tier on top of the existing raw
    --ml-space-* tier:
        --bl-space-scale: 1                       (default :root)
        --bl-space-1..16: calc(--ml-space-N × --bl-space-scale)
  - Emits density selectors at the end of tokens.css:
        [data-density="compact"]    { --bl-space-scale: 0.875; }
        [data-density="comfortable"] { --bl-space-scale: 1; }
        [data-density="spacious"]   { --bl-space-scale: 1.125; }
  - Generator (scripts/generate.ts) emits both tiers automatically; the
    auto-generated per-product CSS files (lysnrai, mindlyst, etc.) gain
    a single blank-line diff from regeneration — no semantic change.
  - 11 / 11 token tests passing.

═══════════════════════════════════════════════════════════════════════
Decision doc — docs/ROADMAP_2026_DECISIONS.md
═══════════════════════════════════════════════════════════════════════
  - Records pragmatic defaults for TODO ledger items #9–#13 so
    implementation work doesn't block:
      #9  Storybook hosting → self-hosted on Gitea Pages (free)
      #10 useChat protocol  → adopt Vercel AI SDK shape, abstract transport
      #11 react-auth fold-in → defer to Wave 7
      #12 dashboard-shell merge → defer to Wave 7
      #13 mobile-native UI → out of scope (tokens-only sharing)
  - Each decision is reversible via RFC.

═══════════════════════════════════════════════════════════════════════
Publish flow
═══════════════════════════════════════════════════════════════════════
  These three packages now require a release. The existing publish
  workflow (.gitea/workflows/publish-packages.yml) has PACKAGE_FILTER
  pinned to @bytelyst/errors and won't pick them up automatically — a
  manual workflow_dispatch with a broader filter (or the existing
  publish-all-packages.yml on workflow_dispatch) is needed to ship
  0.2.0 to the Gitea npm registry.

Refs: learning_ai_uxui_web/docs/ROADMAP_2026.md §10 TODOs #1, #2, #3, #9–#13
2026-05-27 11:49:20 -07:00

249 lines
7.6 KiB
TypeScript

import { useState, type ReactNode } from 'react';
import type { TopBarProps } from './types.js';
export function TopBar({
user,
features = {},
onSignOut,
onNavigate,
actions,
onToggleSidebar,
routePrefix = '',
}: TopBarProps): ReactNode {
const profileHref = `${routePrefix}/profile`;
const billingHref = `${routePrefix}/billing`;
const settingsHref = `${routePrefix}/settings`;
const [menuOpen, setMenuOpen] = useState(false);
const handleNav = (href: string) => {
setMenuOpen(false);
if (onNavigate) onNavigate(href);
};
const initials = user
? user.name
.split(' ')
.map(w => w[0])
.join('')
.toUpperCase()
.slice(0, 2)
: '?';
return (
<header
data-testid="bl-shell-topbar"
style={{
height: 56,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 24px',
borderBottom: '1px solid var(--bl-shell-border, var(--color-border, #e5e7eb))',
background: 'var(--bl-shell-topbar-bg, var(--color-surface, #fff))',
flexShrink: 0,
}}
>
{/* Left: mobile hamburger */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{onToggleSidebar && (
<button
data-testid="bl-shell-hamburger"
onClick={onToggleSidebar}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: 20,
padding: 4,
color: 'var(--bl-shell-nav-text, var(--color-muted-foreground, #6b7280))',
}}
aria-label="Toggle sidebar"
>
</button>
)}
</div>
{/* Right: actions + user menu */}
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
{actions}
{features.notifications && (
<button
data-testid="bl-shell-notifications"
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: 18,
padding: 4,
color: 'var(--bl-shell-nav-text, var(--color-muted-foreground, #6b7280))',
}}
aria-label="Notifications"
>
🔔
</button>
)}
{user && (
<div style={{ position: 'relative' }}>
<button
data-testid="bl-shell-user-menu-trigger"
onClick={() => setMenuOpen(!menuOpen)}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '4px 8px',
borderRadius: 8,
}}
>
{user.avatarUrl ? (
<img
src={user.avatarUrl}
alt={user.name}
style={{ width: 32, height: 32, borderRadius: '50%' }}
/>
) : (
<div
data-testid="bl-shell-user-avatar"
style={{
width: 32,
height: 32,
borderRadius: '50%',
background: 'var(--bl-shell-accent, var(--color-primary, #2563eb))',
color: 'var(--bl-accent-foreground, #fff)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 13,
fontWeight: 600,
}}
>
{initials}
</div>
)}
<span
style={{
fontSize: 14,
color: 'var(--color-foreground, #111827)',
}}
>
{user.name}
</span>
<span style={{ fontSize: 10 }}></span>
</button>
{menuOpen && (
<div
data-testid="bl-shell-user-menu"
style={{
position: 'absolute',
right: 0,
top: '100%',
marginTop: 4,
minWidth: 180,
background: 'var(--bl-shell-topbar-bg, var(--color-surface, #fff))',
border: '1px solid var(--bl-shell-border, var(--color-border, #e5e7eb))',
borderRadius: 8,
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
zIndex: 50,
overflow: 'hidden',
}}
>
<div
style={{
padding: '12px 16px',
borderBottom: '1px solid var(--bl-shell-border, var(--color-border, #e5e7eb))',
}}
>
<div style={{ fontSize: 14, fontWeight: 600 }}>{user.name}</div>
<div style={{ fontSize: 12, color: 'var(--color-muted-foreground, #6b7280)' }}>
{user.email}
</div>
</div>
{features.profile !== false && (
<a
data-testid="bl-shell-menu-profile"
href={profileHref}
onClick={e => {
e.preventDefault();
handleNav(profileHref);
}}
style={menuItemStyle}
>
Profile
</a>
)}
{features.billing && (
<a
data-testid="bl-shell-menu-billing"
href={billingHref}
onClick={e => {
e.preventDefault();
handleNav(billingHref);
}}
style={menuItemStyle}
>
Billing
</a>
)}
{features.settings !== false && (
<a
data-testid="bl-shell-menu-settings"
href={settingsHref}
onClick={e => {
e.preventDefault();
handleNav(settingsHref);
}}
style={menuItemStyle}
>
Settings
</a>
)}
{onSignOut && (
<button
data-testid="bl-shell-menu-signout"
onClick={() => {
setMenuOpen(false);
onSignOut();
}}
style={{
...menuItemStyle,
width: '100%',
textAlign: 'left',
borderTop: '1px solid var(--bl-shell-border, var(--color-border, #e5e7eb))',
color: 'var(--color-destructive, #dc2626)',
background: 'none',
border: 'none',
borderTopStyle: 'solid',
borderTopWidth: 1,
borderTopColor: 'var(--bl-shell-border, var(--color-border, #e5e7eb))',
}}
>
Sign Out
</button>
)}
</div>
)}
</div>
)}
</div>
</header>
);
}
const menuItemStyle: React.CSSProperties = {
display: 'block',
padding: '10px 16px',
fontSize: 14,
color: 'var(--color-foreground, #111827)',
textDecoration: 'none',
cursor: 'pointer',
};