diff --git a/src/App.tsx b/src/App.tsx index ed3c9209..46c75241 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,10 @@ import { CssBaseline } from '@mui/material'; +import { ThemeProvider } from '@mui/material/styles'; import { Provider } from 'react-redux'; import { RouterProvider } from 'react-router-dom'; import { createAppRouter } from './app/router'; import { createAppRuntime } from './app/runtime'; +import { appTheme } from './app/theme'; import { appStore } from './state/store'; const appRuntime = createAppRuntime({ store: appStore }); @@ -27,8 +29,10 @@ if (import.meta.hot) { function App() { return ( - - + + + + ); } diff --git a/src/app/ConnectDialog.tsx b/src/app/ConnectDialog.tsx index 85bf14a8..aa391b4f 100644 --- a/src/app/ConnectDialog.tsx +++ b/src/app/ConnectDialog.tsx @@ -7,14 +7,17 @@ import { DialogActions, DialogContent, DialogTitle, - Divider, - Grid, + IconButton, Stack, TextField, + Tooltip, Typography, } from '@mui/material'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import RefreshIcon from '@mui/icons-material/Refresh'; import { useFetcher } from 'react-router-dom'; import { appConfig, type ConnectionOption } from '../config'; +import { monoValueSx } from './consoleStyles'; import type { RootActionData } from './routeData'; import type { SignifyConnectionState } from './runtime'; @@ -33,29 +36,6 @@ export interface ConnectDialogProps { onClose: () => void; } -/** - * User-facing status label for the typed connection state. - * - * Keep this local until another component truly needs the same presentation - * policy; duplicating a second tiny formatter is preferable to a premature - * shared status abstraction. - */ -const connectionStatusLabel = (connection: SignifyConnectionState): string => { - if (connection.status === 'idle') { - return 'Not Connected'; - } - - if (connection.status === 'connecting') { - return 'Connecting'; - } - - if (connection.status === 'connected') { - return 'Connected'; - } - - return 'Error'; -}; - /** * Modal form for selecting a configured KERIA target and passcode. * @@ -73,7 +53,7 @@ export const ConnectDialog = ({ const [selectedConnection, setSelectedConnection] = useState(appConfig.connectionOptions[0]); const [draftPasscode, setDraftPasscode] = useState(null); - const isConnected = connection.status === 'connected'; + const [copiedPasscode, setCopiedPasscode] = useState(false); const isSubmitting = connection.status === 'connecting' || connectFetcher.state !== 'idle'; const isGenerating = passcodeFetcher.state !== 'idle'; @@ -83,6 +63,7 @@ export const ConnectDialog = ({ ? passcodeFetcher.data.passcode : null; const passcode = draftPasscode ?? generatedPasscode ?? ''; + const passcodeReady = passcode.length >= 21; const actionError = connection.status === 'error' ? connection.error.message @@ -105,9 +86,22 @@ export const ConnectDialog = ({ const formData = new FormData(); formData.set('intent', 'generatePasscode'); setDraftPasscode(null); + setCopiedPasscode(false); passcodeFetcher.submit(formData, { method: 'post', action: '/' }); }; + const handleCopyPasscode = () => { + if (passcode.length === 0) { + return; + } + + void globalThis.navigator.clipboard + ?.writeText(passcode) + .catch(() => undefined); + setCopiedPasscode(true); + globalThis.setTimeout(() => setCopiedPasscode(false), 1500); + }; + return ( - Connect - - - - `${option.label} (${option.adminUrl})` - } - isOptionEqualToValue={(option, value) => - option.adminUrl === value.adminUrl && - option.bootUrl === value.bootUrl - } - renderInput={(params) => ( - - )} - value={selectedConnection} - fullWidth - onChange={(_event, newValue) => { - setSelectedConnection( - newValue ?? appConfig.connectionOptions[0] - ); + Connect Wallet + + + - + > + + + + Passcode generator + + + Generate a fresh Signify passcode or paste + an existing one. + + + + + + + + + + + + + - setDraftPasscode(event.target.value) + onChange={(event) => { + setCopiedPasscode(false); + setDraftPasscode(event.target.value); + }} + helperText={ + passcodeReady + ? copiedPasscode + ? 'Copied to clipboard' + : 'Ready to connect' + : 'Passcode must be at least 21 characters' } - helperText="Passcode must be at least 21 characters" fullWidth - /> - - + /> + @@ -200,40 +238,59 @@ export const ConnectDialog = ({ {actionError} )} - - - - - - - - - - - - - - - + Admin {selectedConnection.adminUrl} | Boot{' '} + {selectedConnection.bootUrl} + + + + + + ); diff --git a/src/app/ConnectionRequired.tsx b/src/app/ConnectionRequired.tsx index 97fe553b..d9059e46 100644 --- a/src/app/ConnectionRequired.tsx +++ b/src/app/ConnectionRequired.tsx @@ -1,4 +1,5 @@ import { Box, Typography } from '@mui/material'; +import { ConsolePanel, StatusPill } from './Console'; /** * Blocked-state view for routes that need a connected Signify client. @@ -7,7 +8,15 @@ import { Box, Typography } from '@mui/material'; * direct URL navigation should be passive until the user chooses to connect. */ export const ConnectionRequired = () => ( - - Connect to KERIA before opening this view. + + } + > + + Connect to KERIA before opening this view. + + ); diff --git a/src/app/Console.tsx b/src/app/Console.tsx new file mode 100644 index 00000000..a818fa52 --- /dev/null +++ b/src/app/Console.tsx @@ -0,0 +1,240 @@ +import type { ReactNode } from 'react'; +import { Box, Button, Stack, Typography } from '@mui/material'; +import type { ButtonProps, SxProps, Theme } from '@mui/material'; +import { monoValueSx } from './consoleStyles'; + +export interface PageHeaderProps { + eyebrow?: string; + title: string; + summary?: string; + actions?: ReactNode; +} + +export const PageHeader = ({ + eyebrow, + title, + summary, + actions, +}: PageHeaderProps) => ( + + + {eyebrow && ( + + {eyebrow} + + )} + + {title} + + {summary && ( + + {summary} + + )} + + {actions && {actions}} + +); + +export interface ConsolePanelProps { + children: ReactNode; + title?: string; + eyebrow?: string; + actions?: ReactNode; + sx?: SxProps; +} + +export const ConsolePanel = ({ + children, + title, + eyebrow, + actions, + sx, +}: ConsolePanelProps) => ( + + {(title || eyebrow || actions) && ( + + + {eyebrow && ( + + {eyebrow} + + )} + {title && ( + + {title} + + )} + + {actions} + + )} + {children} + +); + +export interface EmptyStateProps { + title: string; + message: string; + action?: ReactNode; +} + +export const EmptyState = ({ title, message, action }: EmptyStateProps) => ( + + {title} + + {message} + + {action && {action}} + +); + +export interface StatusPillProps { + label: string; + tone?: 'neutral' | 'success' | 'warning' | 'error' | 'info'; +} + +const statusColors = { + neutral: { borderColor: 'divider', color: 'text.secondary' }, + success: { borderColor: 'success.main', color: 'success.main' }, + warning: { borderColor: 'warning.main', color: 'warning.main' }, + error: { borderColor: 'error.main', color: 'error.main' }, + info: { borderColor: 'primary.main', color: 'primary.main' }, +} as const; + +export const StatusPill = ({ label, tone = 'neutral' }: StatusPillProps) => ( + + {label} + +); + +export interface TelemetryRowProps { + label: string; + value: ReactNode; + mono?: boolean; +} + +export const TelemetryRow = ({ + label, + value, + mono = false, +}: TelemetryRowProps) => ( + + + {label} + + + {value} + + +); + +export const CommandButton = (props: ButtonProps) => ( + @@ -202,7 +216,7 @@ export const TopBar = ({ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} > - + {activeOperations.length === 0 ? ( setOperationsAnchor(null)} + sx={{ + border: 1, + borderColor: 'divider', + borderRadius: 1, + mb: 0.75, + }} > setOperationsAnchor(null)} + sx={{ borderRadius: 1 }} > - + @@ -239,7 +260,7 @@ export const TopBar = ({ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} > - + {recentNotifications.length === 0 ? ( setNotificationsAnchor(null)} + sx={{ borderRadius: 1 }} > diff --git a/src/app/consoleStyles.ts b/src/app/consoleStyles.ts new file mode 100644 index 00000000..5382e395 --- /dev/null +++ b/src/app/consoleStyles.ts @@ -0,0 +1,6 @@ +export const monoValueSx = { + fontFamily: 'var(--app-mono-font)', + letterSpacing: 0, + overflowWrap: 'anywhere', + wordBreak: 'break-word', +} as const; diff --git a/src/app/theme.ts b/src/app/theme.ts new file mode 100644 index 00000000..48dbd62c --- /dev/null +++ b/src/app/theme.ts @@ -0,0 +1,298 @@ +import { createTheme } from '@mui/material/styles'; + +const graphite = { + abyss: '#05090d', + deck: '#091018', + panel: '#0d1722', + panelHigh: '#132334', + line: '#234055', + lineStrong: '#3a6680', + text: '#e7f4ff', + muted: '#8aa1b2', + dim: '#5f7380', + cyan: '#27d7ff', + blue: '#2487ff', + amber: '#ffb02e', + green: '#39d47a', + red: '#ff3d4f', + violet: '#a778ff', +}; + +export const appTheme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: graphite.cyan, + light: '#76e8ff', + dark: '#008bb8', + contrastText: '#041016', + }, + secondary: { + main: graphite.violet, + light: '#c5a8ff', + dark: '#7450c7', + contrastText: '#ffffff', + }, + success: { + main: graphite.green, + dark: '#18a955', + contrastText: '#041016', + }, + warning: { + main: graphite.amber, + dark: '#c67b00', + contrastText: '#05090d', + }, + error: { + main: graphite.red, + dark: '#ba1628', + contrastText: '#ffffff', + }, + info: { + main: graphite.blue, + dark: '#0063cc', + contrastText: '#ffffff', + }, + background: { + default: graphite.abyss, + paper: graphite.panel, + }, + text: { + primary: graphite.text, + secondary: graphite.muted, + disabled: graphite.dim, + }, + divider: graphite.line, + action: { + active: graphite.cyan, + hover: 'rgba(39, 215, 255, 0.09)', + selected: 'rgba(39, 215, 255, 0.16)', + disabled: 'rgba(138, 161, 178, 0.38)', + disabledBackground: 'rgba(95, 115, 128, 0.18)', + }, + }, + shape: { + borderRadius: 4, + }, + typography: { + fontFamily: + 'var(--app-interface-font), Inter, system-ui, Helvetica, Arial, sans-serif', + h1: { fontWeight: 600, letterSpacing: 0 }, + h2: { fontWeight: 600, letterSpacing: 0 }, + h3: { fontWeight: 600, letterSpacing: 0 }, + h4: { fontWeight: 600, letterSpacing: 0 }, + h5: { fontWeight: 600, letterSpacing: 0 }, + h6: { fontWeight: 600, letterSpacing: 0 }, + button: { + fontWeight: 700, + letterSpacing: 0, + textTransform: 'uppercase', + }, + caption: { + letterSpacing: 0, + }, + }, + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + backgroundColor: graphite.abyss, + backgroundImage: + 'linear-gradient(rgba(39, 215, 255, 0.035) 1px, transparent 1px), linear-gradient(90deg, rgba(39, 215, 255, 0.025) 1px, transparent 1px)', + backgroundSize: '48px 48px, 48px 48px', + }, + }, + }, + MuiAppBar: { + styleOverrides: { + root: { + background: 'rgba(6, 13, 20, 0.94)', + borderBottom: `1px solid ${graphite.lineStrong}`, + boxShadow: '0 12px 28px rgba(0, 0, 0, 0.32)', + backdropFilter: 'blur(12px)', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + border: `1px solid ${graphite.line}`, + boxShadow: '0 18px 44px rgba(0, 0, 0, 0.32)', + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + backgroundImage: 'none', + backgroundColor: graphite.panel, + border: `1px solid ${graphite.line}`, + boxShadow: '0 12px 32px rgba(0, 0, 0, 0.24)', + }, + }, + }, + MuiDialog: { + styleOverrides: { + paper: { + backgroundColor: graphite.deck, + border: `1px solid ${graphite.lineStrong}`, + boxShadow: + '0 24px 70px rgba(0, 0, 0, 0.62), inset 0 1px 0 rgba(118, 232, 255, 0.12)', + }, + }, + }, + MuiDialogTitle: { + styleOverrides: { + root: { + borderBottom: `1px solid ${graphite.line}`, + color: graphite.text, + paddingTop: 20, + paddingBottom: 16, + }, + }, + }, + MuiDialogActions: { + styleOverrides: { + root: { + borderTop: `1px solid ${graphite.line}`, + }, + }, + }, + MuiButton: { + defaultProps: { + disableElevation: true, + }, + variants: [ + { + props: { variant: 'contained', color: 'primary' }, + style: { + background: + 'linear-gradient(180deg, #37ddff 0%, #1478c7 100%)', + boxShadow: + 'inset 0 1px 0 rgba(255, 255, 255, 0.24), 0 0 18px rgba(39, 215, 255, 0.18)', + '&:hover': { + background: + 'linear-gradient(180deg, #78eaff 0%, #198eea 100%)', + }, + '&.Mui-disabled': { + background: 'rgba(95, 115, 128, 0.18)', + color: 'rgba(138, 161, 178, 0.38)', + boxShadow: 'none', + }, + }, + }, + ], + styleOverrides: { + root: { + borderRadius: 3, + minHeight: 40, + }, + outlined: { + borderColor: graphite.lineStrong, + color: graphite.text, + '&:hover': { + borderColor: graphite.cyan, + backgroundColor: 'rgba(39, 215, 255, 0.08)', + }, + }, + text: { + color: graphite.cyan, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + borderRadius: 3, + '&:hover': { + backgroundColor: 'rgba(39, 215, 255, 0.1)', + }, + }, + }, + }, + MuiTextField: { + defaultProps: { + variant: 'outlined', + }, + }, + MuiOutlinedInput: { + styleOverrides: { + root: { + backgroundColor: 'rgba(5, 9, 13, 0.68)', + borderRadius: 3, + '& fieldset': { + borderColor: graphite.lineStrong, + }, + '&:hover fieldset': { + borderColor: graphite.cyan, + }, + '&.Mui-focused fieldset': { + borderColor: graphite.cyan, + boxShadow: '0 0 0 1px rgba(39, 215, 255, 0.24)', + }, + }, + }, + }, + MuiInputLabel: { + styleOverrides: { + root: { + color: graphite.muted, + '&.Mui-focused': { + color: graphite.cyan, + }, + }, + }, + }, + MuiTableCell: { + styleOverrides: { + root: { + borderBottom: `1px solid ${graphite.line}`, + }, + head: { + color: graphite.muted, + fontSize: '0.76rem', + fontWeight: 700, + textTransform: 'uppercase', + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + borderRadius: 3, + fontWeight: 700, + }, + }, + }, + MuiAccordion: { + styleOverrides: { + root: { + backgroundColor: graphite.panel, + backgroundImage: 'none', + border: `1px solid ${graphite.line}`, + boxShadow: 'none', + '&:before': { + display: 'none', + }, + }, + }, + }, + MuiDrawer: { + styleOverrides: { + paper: { + backgroundColor: '#070d14', + borderRight: `1px solid ${graphite.lineStrong}`, + }, + }, + }, + MuiBackdrop: { + styleOverrides: { + root: { + backgroundColor: 'rgba(0, 0, 0, 0.68)', + backdropFilter: 'blur(2px)', + }, + }, + }, + }, +}); diff --git a/src/features/client/AidCard.tsx b/src/features/client/AidCard.tsx index 4333a28a..e1a76ec4 100644 --- a/src/features/client/AidCard.tsx +++ b/src/features/client/AidCard.tsx @@ -1,5 +1,6 @@ -import { Card, CardContent, Divider, Grid, Typography } from '@mui/material'; +import { Stack } from '@mui/material'; import type { KeyState } from 'signify-ts'; +import { ConsolePanel, TelemetryRow } from '../../app/Console'; import { keyStateFieldDescriptions } from './keyStateFieldDescriptions'; /** @@ -17,32 +18,18 @@ export interface AidCardProps { * to the raw key. State extraction belongs in `ClientView`. */ export const AidCard = ({ data, text }: AidCardProps) => ( - - - - {text} - - - - {Object.entries(data).map(([key, value]) => - typeof value === 'string' ? ( - - - - {keyStateFieldDescriptions[key]?.title ?? - key} - {' '} - {value} - - - ) : null - )} - - - + + + {Object.entries(data).map(([key, value]) => + typeof value === 'string' ? ( + + ) : null + )} + + ); diff --git a/src/features/client/ClientView.tsx b/src/features/client/ClientView.tsx index 58c20668..ed141b20 100644 --- a/src/features/client/ClientView.tsx +++ b/src/features/client/ClientView.tsx @@ -1,6 +1,8 @@ -import { Box, Grid, Typography } from '@mui/material'; +import { Box, Grid, Stack } from '@mui/material'; import { useLoaderData } from 'react-router-dom'; import { ConnectionRequired } from '../../app/ConnectionRequired'; +import { ConsolePanel, PageHeader, TelemetryRow } from '../../app/Console'; +import { monoValueSx } from '../../app/consoleStyles'; import type { ClientLoaderData } from '../../app/routeData'; import { AidCard } from './AidCard'; @@ -23,21 +25,40 @@ export const ClientView = () => { const controller = summary.state.controller.state; return ( - - - - Controller AID: {summary.controllerPre} - - - Agent AID: {summary.agentPre} - - + + + + + + {summary.controllerPre} + + } + /> + + {summary.agentPre} + + } + /> + + diff --git a/src/features/credentials/CredentialsView.tsx b/src/features/credentials/CredentialsView.tsx index 7af1afde..86b5cb80 100644 --- a/src/features/credentials/CredentialsView.tsx +++ b/src/features/credentials/CredentialsView.tsx @@ -1,8 +1,34 @@ -import { Box, Typography } from '@mui/material'; +import { Box, Grid, Stack, Typography } from '@mui/material'; import { useLoaderData } from 'react-router-dom'; import { ConnectionRequired } from '../../app/ConnectionRequired'; +import { ConsolePanel, PageHeader, StatusPill } from '../../app/Console'; import type { CredentialsLoaderData } from '../../app/routeData'; +const roleCards = [ + { + role: 'Issuer', + state: 'Prepare', + tone: 'info' as const, + summary: + 'Create registry, issue SEDI voter credential, grant to holder.', + steps: ['Registry', 'Credential draft', 'IPEX grant'], + }, + { + role: 'Holder', + state: 'Receive', + tone: 'warning' as const, + summary: 'Resolve issuer, admit credential grant, present to verifier.', + steps: ['Issuer OOBI', 'Grant admit', 'Presentation'], + }, + { + role: 'Verifier', + state: 'Validate', + tone: 'success' as const, + summary: 'Trust issuer/schema, validate presentation, emit result.', + steps: ['Trusted issuer', 'TEL state', 'Webhook event'], + }, +]; + /** * Placeholder credentials route. * @@ -18,13 +44,79 @@ export const CredentialsView = () => { } return ( - - - Credentials - - - Credential workflow placeholder - + + + + {roleCards.map((card) => ( + + + } + sx={{ height: '100%' }} + > + + + {card.summary} + + + {card.steps.map((step, index) => ( + + + {index + 1} + + {step} + + ))} + + + + + ))} + + + + Replace this mission board with live schema resolution, + registry creation, issuer grant, holder admit, holder + presentation, and verifier result panels as those operations + land behind the runtime workflow boundary. + + ); }; diff --git a/src/features/identifiers/IdentifierTable.tsx b/src/features/identifiers/IdentifierTable.tsx index 8b51c68e..776fc5d2 100644 --- a/src/features/identifiers/IdentifierTable.tsx +++ b/src/features/identifiers/IdentifierTable.tsx @@ -122,6 +122,13 @@ export const IdentifierTable = ({ key={identifier.name} variant="outlined" data-testid={`identifier-row-${identifier.name}`} + sx={{ + bgcolor: 'background.paper', + borderColor: 'divider', + '&:hover': { + borderColor: 'primary.main', + }, + }} > PIDX:{' '} {formatIdentifierMetadata( - identifierIdentifierIndex(identifier) + identifierIdentifierIndex( + identifier + ) )} @@ -190,11 +199,20 @@ export const IdentifierTable = ({ - + Name AID Type @@ -209,6 +227,9 @@ export const IdentifierTable = ({ key={identifier.name} sx={{ cursor: 'pointer', + '&:hover': { + bgcolor: 'action.hover', + }, '&:last-child td, &:last-child th': { border: 0, }, diff --git a/src/features/identifiers/IdentifiersView.tsx b/src/features/identifiers/IdentifiersView.tsx index 20b75048..c3d9d9c8 100644 --- a/src/features/identifiers/IdentifiersView.tsx +++ b/src/features/identifiers/IdentifiersView.tsx @@ -1,8 +1,9 @@ import { useState } from 'react'; -import { Box, Fab, Typography } from '@mui/material'; +import { Box, Button, Fab, Typography } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import { useFetcher, useLoaderData } from 'react-router-dom'; import { ConnectionRequired } from '../../app/ConnectionRequired'; +import { EmptyState, PageHeader, StatusPill } from '../../app/Console'; import type { IdentifierActionData, IdentifiersLoaderData, @@ -113,7 +114,9 @@ export const IdentifiersView = () => { fetcher.submit(formData, { method: 'post' }); }; - const handleCreate = async (draft: IdentifierCreateDraft): Promise => { + const handleCreate = async ( + draft: IdentifierCreateDraft + ): Promise => { const requestId = globalThis.crypto.randomUUID(); setActiveCreateRequestId(requestId); setPendingMessage(`Creating identifier ${draft.name}`); @@ -126,12 +129,61 @@ export const IdentifiersView = () => { const isRotateDisabled = (identifierName: string): boolean => activeResourceKeys.has(`identifier:aid:${identifierName}`); + const openCreate = () => { + setActiveCreateRequestId(null); + setCreateOpen(true); + }; return ( - + + } + aria-label="create identifier" + onClick={openCreate} + disabled={actionRunning} + sx={{ display: { xs: 'none', sm: 'inline-flex' } }} + > + Create Identifier + + } + /> {actionState.message && ( - + + {' '} { )} + {identifiers.length === 0 && ( + } + aria-label="add" + onClick={openCreate} + disabled={actionRunning} + sx={{ display: { xs: 'inline-flex', sm: 'none' } }} + > + Create Identifier + + } + /> + )} @@ -154,7 +224,10 @@ export const IdentifiersView = () => { } /> { color="primary" aria-label="add" onClick={() => { - setActiveCreateRequestId(null); - setCreateOpen(true); + openCreate(); }} disabled={actionRunning} sx={{ + display: { + xs: + createDialogOpen || identifiers.length === 0 + ? 'none' + : 'inline-flex', + sm: 'none', + }, position: 'fixed', right: { xs: '16px', sm: '24px' }, bottom: { diff --git a/src/features/notifications/AppNotificationsView.tsx b/src/features/notifications/AppNotificationsView.tsx index be6969ca..cd91e79d 100644 --- a/src/features/notifications/AppNotificationsView.tsx +++ b/src/features/notifications/AppNotificationsView.tsx @@ -1,7 +1,6 @@ import { useEffect } from 'react'; import { Box, - Chip, Link, List, ListItem, @@ -10,6 +9,7 @@ import { Typography, } from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; +import { EmptyState, PageHeader, StatusPill } from '../../app/Console'; import { useAppDispatch, useAppSelector } from '../../state/hooks'; import { allAppNotificationsRead } from '../../state/appNotifications.slice'; import { selectAppNotifications } from '../../state/selectors'; @@ -38,14 +38,17 @@ export const AppNotificationsView = () => { }, [dispatch, unreadCount]); return ( - - - Notifications - + + {notifications.length === 0 ? ( - - No app notifications yet. - + ) : ( {notifications.map((notification) => ( @@ -59,7 +62,7 @@ export const AppNotificationsView = () => { alignItems: 'flex-start', bgcolor: notification.status === 'unread' - ? 'common.white' + ? 'action.selected' : 'action.hover', opacity: notification.status === 'unread' ? 1 : 0.72, @@ -78,16 +81,16 @@ export const AppNotificationsView = () => { {notification.title} - diff --git a/src/features/operations/OperationDetailView.tsx b/src/features/operations/OperationDetailView.tsx index 2de7c2f7..80c23f83 100644 --- a/src/features/operations/OperationDetailView.tsx +++ b/src/features/operations/OperationDetailView.tsx @@ -1,26 +1,37 @@ -import { - Box, - Button, - Chip, - Divider, - Link, - Stack, - Typography, -} from '@mui/material'; +import { Box, Button, Link, Stack } from '@mui/material'; import { Link as RouterLink, useParams } from 'react-router-dom'; +import { + ConsolePanel, + PageHeader, + StatusPill, + TelemetryRow, +} from '../../app/Console'; import { useAppSelector } from '../../state/hooks'; import { selectOperationById } from '../../state/selectors'; -const DetailRow = ({ label, value }: { label: string; value: string | null }) => ( - - - {label} - - - {value ?? 'None'} - - -); +const DetailRow = ({ + label, + value, +}: { + label: string; + value: string | null; +}) => ; + +const operationTone = (status: string) => { + if (status === 'error') { + return 'error' as const; + } + + if (status === 'success') { + return 'success' as const; + } + + if (status === 'running') { + return 'warning' as const; + } + + return 'neutral' as const; +}; export const OperationDetailView = () => { const { requestId = '' } = useParams(); @@ -29,10 +40,16 @@ export const OperationDetailView = () => { if (operation === null) { return ( - - Operation Not Found - - @@ -40,32 +57,33 @@ export const OperationDetailView = () => { } return ( - - - - {operation.title} - - - - {operation.description && ( - - {operation.description} - - )} - - + + + } + /> + - + - + { : null } /> - + - {operation.resultRoute && ( diff --git a/src/features/operations/OperationsView.tsx b/src/features/operations/OperationsView.tsx index d972a646..5410d02b 100644 --- a/src/features/operations/OperationsView.tsx +++ b/src/features/operations/OperationsView.tsx @@ -1,6 +1,5 @@ import { Box, - Chip, List, ListItemButton, ListItemText, @@ -8,21 +7,41 @@ import { Typography, } from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; +import { EmptyState, PageHeader, StatusPill } from '../../app/Console'; import { useAppSelector } from '../../state/hooks'; import { selectOperationRecords } from '../../state/selectors'; +const operationTone = (status: string) => { + if (status === 'error') { + return 'error' as const; + } + + if (status === 'success') { + return 'success' as const; + } + + if (status === 'running') { + return 'warning' as const; + } + + return 'neutral' as const; +}; + export const OperationsView = () => { const operations = [...useAppSelector(selectOperationRecords)].reverse(); return ( - - - Operations - + + {operations.length === 0 ? ( - - No operations have run in this browser session. - + ) : ( {operations.map((operation) => ( @@ -36,6 +55,11 @@ export const OperationsView = () => { borderRadius: 1, mb: 1, alignItems: 'flex-start', + bgcolor: 'background.paper', + '&:hover': { + borderColor: 'primary.main', + bgcolor: 'action.hover', + }, }} > { {operation.title} - } diff --git a/src/index.css b/src/index.css index 9bda864a..f41ae5ba 100644 --- a/src/index.css +++ b/src/index.css @@ -1,9 +1,12 @@ -@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;500;600&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&family=Source+Code+Pro:wght@400;500;600&display=swap'); :root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - --app-mono-font: 'Source Code Pro', 'Roboto Mono', 'SFMono-Regular', - Consolas, 'Liberation Mono', monospace; + font-family: + Rajdhani, Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + --app-interface-font: 'Rajdhani'; + --app-mono-font: + 'Source Code Pro', 'Roboto Mono', 'SFMono-Regular', Consolas, + 'Liberation Mono', monospace; line-height: 1.5; font-weight: 400;