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
249 lines
7.6 KiB
TypeScript
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',
|
|
};
|