deckhearth/components/PermissionIndicator.js
Randall Stillwell d269bd29c1 feat(design-system): sweep authenticated body-content panels to glass
PR #95/#96 shipped the Liquid Glass foundation (tokens, primitives, gates)
plus Layout shell, modals, landing, auth pages, and form CTAs — but body-
content panels on authenticated pages (admin Card Editor, admin Card
Import, admin Submissions, dashboard, my-cards, settings, scanner panels,
card detail price cards, popovers) were still rendering as flat
var(--bg-secondary) cards. Result: the admin Tools screen and several
core pages looked unchanged after the redesign.

This sweep adds a `.glass-panel` / `.glass-panel-strong` utility
(<GlassSurface tint=mid/high rim=subtle elevation=ambient/pronounced
blur=mid/high> in class form) and applies it across 18 surfaces:

  * Admin Card Editor view, search panel, form (5 sections), preview
  * Admin Card Import navigation + 3 body cards + sync panel
  * Admin Card Submissions list items
  * Dashboard stat cards + empty-state + grid items (5 surfaces)
  * My-cards empty-state CTA card
  * Settings panels (3)
  * Scanner page settings + grid + queue + bulk toolbar + dialog
  * Scanner destination picker + camera status banner + disambiguation
  * Card detail price cards (Current / TCGPlayer / CardKingdom)
  * Permission indicator tooltips
  * Collections page header card
  * Card detail view price cards

Also migrates the lingering admin Card Editor "Card Editor / Card Import"
nav buttons and the "Save Changes" / "Import Cards" / "Run catalog sync"
CTAs to the <Button> primitive (consistent loading + disabled states).

Page header bands (full-bleed strips with border-bottom on dashboard,
my-cards, cards, collections, community/collections, collection/[id])
are intentionally left solid — they're not card-shaped surfaces and
stacking glass-on-glass directly below the already-glass topbar would
muddy the hierarchy.

Tests: lint clean, vitest 104/104, build green. The visual diff
baseline will need refresh because the homepage spec is unaffected
(it targets the unauthenticated landing page) but the dashboard/
admin/scanner surfaces will diff if/when we add baselines for them.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 20:44:40 -05:00

153 lines
4.9 KiB
JavaScript

import { useState } from 'react';
export default function PermissionIndicator({ userRole, isPublic, showTooltip = true }) {
const [showDetails, setShowDetails] = useState(false);
const getRoleInfo = (role) => {
switch (role) {
case 'owner':
return {
icon: '👑',
label: 'Owner',
color: '#f59e0b',
permissions: ['View', 'Edit', 'Delete', 'Manage Users', 'Change Visibility']
};
case 'editor':
return {
icon: '✏️',
label: 'Editor',
color: '#10b981',
permissions: ['View', 'Edit', 'Add/Remove Cards']
};
case 'viewer':
return {
icon: '👁️',
label: 'Viewer',
color: '#6b7280',
permissions: ['View Only']
};
default:
return {
icon: '🔒',
label: 'No Access',
color: '#ef4444',
permissions: []
};
}
};
const getVisibilityInfo = (isPublic) => {
if (isPublic) {
return {
icon: '🌍',
label: 'Public',
color: '#10b981',
description: 'Visible in community, invite-only editing'
};
} else {
return {
icon: '🔒',
label: 'Private',
color: '#6b7280',
description: 'Hidden from community, invite-only editing'
};
}
};
const roleInfo = getRoleInfo(userRole);
const visibilityInfo = getVisibilityInfo(isPublic);
return (
<div className="flex items-center space-x-2">
{/* Role Badge */}
{userRole && (
<div className="relative">
<div
className="flex items-center space-x-1 px-2 py-1 rounded-full text-xs font-medium cursor-pointer"
style={{
backgroundColor: `${roleInfo.color}20`,
color: roleInfo.color,
border: `1px solid ${roleInfo.color}40`
}}
onMouseEnter={() => showTooltip && setShowDetails(true)}
onMouseLeave={() => setShowDetails(false)}
>
<span>{roleInfo.icon}</span>
<span>{roleInfo.label}</span>
</div>
{/* Role Tooltip */}
{showDetails && showTooltip && (
<div
className="glass-panel-strong absolute bottom-full left-0 mb-2 p-3 rounded-lg z-10 min-w-48"
>
<div className="text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
{roleInfo.icon} {roleInfo.label} Permissions:
</div>
<ul className="text-xs space-y-1" style={{ color: 'var(--text-secondary)' }}>
{roleInfo.permissions.map((permission, index) => (
<li key={index} className="flex items-center space-x-1">
<span className="text-green-500"></span>
<span>{permission}</span>
</li>
))}
</ul>
</div>
)}
</div>
)}
{/* Visibility Badge */}
{isPublic !== undefined && (
<div className="relative">
<div
className="flex items-center space-x-1 px-2 py-1 rounded-full text-xs font-medium cursor-pointer"
style={{
backgroundColor: `${visibilityInfo.color}20`,
color: visibilityInfo.color,
border: `1px solid ${visibilityInfo.color}40`
}}
onMouseEnter={() => showTooltip && setShowDetails(true)}
onMouseLeave={() => setShowDetails(false)}
>
<span>{visibilityInfo.icon}</span>
<span>{visibilityInfo.label}</span>
</div>
{/* Visibility Tooltip */}
{showDetails && showTooltip && (
<div
className="glass-panel-strong absolute bottom-full right-0 mb-2 p-3 rounded-lg z-10 min-w-48"
>
<div className="text-sm font-medium mb-1" style={{ color: 'var(--text-primary)' }}>
{visibilityInfo.icon} {visibilityInfo.label}
</div>
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{visibilityInfo.description}
</p>
</div>
)}
</div>
)}
</div>
);
}
// Utility component for inline permission checks
export function CanEdit({ userRole, children }) {
const canEdit = ['owner', 'editor'].includes(userRole);
return canEdit ? children : null;
}
export function CanManage({ userRole, children }) {
const canManage = userRole === 'owner';
return canManage ? children : null;
}
export function PermissionGate({ userRole, requiredRole, children, fallback = null }) {
const roleHierarchy = { viewer: 1, editor: 2, owner: 3 };
const userLevel = roleHierarchy[userRole] || 0;
const requiredLevel = roleHierarchy[requiredRole] || 0;
return userLevel >= requiredLevel ? children : fallback;
}