Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
"lint": "eslint src tests scripts eslint.config.mjs vite.config.ts vitest.config.ts --max-warnings 0",
"preview": "vite preview",
"keria:smoke": "tsx scripts/keria-smoke.ts",
"contact:ui-smoke": "tsx tests/contact-oobi-smoke.ts",
"contact:challenge-smoke": "tsx tests/contact-challenge-smoke.ts",
"browser:smoke": "node tests/browser-smoke.mjs",
"responsive:smoke": "node tests/responsive-smoke.mjs",
"unit:test": "vitest run tests/unit",
"scenario:test": "vitest run tests/scenarios/salty.test.ts tests/scenarios/randy.test.ts tests/scenarios/witnessed.test.ts tests/scenarios/challenge.test.ts tests/scenarios/controller-rotation.test.ts",
"scenario:test": "vitest run tests/scenarios/salty.test.ts tests/scenarios/randy.test.ts tests/scenarios/witnessed.test.ts tests/scenarios/challenge.test.ts tests/scenarios/controller-rotation.test.ts tests/scenarios/oobi-contacts.test.ts",
"scenario:test:all": "vitest run tests/scenarios",
"test:ci": "pnpm lint && pnpm build && pnpm unit:test && pnpm responsive:smoke && pnpm keria:smoke -- --mode connect && pnpm keria:smoke && pnpm scenario:test && pnpm browser:smoke"
"test:ci": "pnpm lint && pnpm build && pnpm unit:test && pnpm responsive:smoke && pnpm keria:smoke -- --mode connect && pnpm keria:smoke && pnpm scenario:test && pnpm contact:ui-smoke && pnpm browser:smoke"
},
"engines": {
"node": ">=20.19.0"
Expand Down
42 changes: 41 additions & 1 deletion src/app/ConnectDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import {
DialogContent,
DialogTitle,
IconButton,
InputAdornment,
Stack,
TextField,
Tooltip,
Typography,
} from '@mui/material';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import RefreshIcon from '@mui/icons-material/Refresh';
import VisibilityIcon from '@mui/icons-material/Visibility';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import { useFetcher } from 'react-router-dom';
import { appConfig, type ConnectionOption } from '../config';
import { monoValueSx } from './consoleStyles';
Expand Down Expand Up @@ -54,6 +57,7 @@ export const ConnectDialog = ({
useState<ConnectionOption>(appConfig.connectionOptions[0]);
const [draftPasscode, setDraftPasscode] = useState<string | null>(null);
const [copiedPasscode, setCopiedPasscode] = useState(false);
const [passcodeVisible, setPasscodeVisible] = useState(false);
const isSubmitting =
connection.status === 'connecting' || connectFetcher.state !== 'idle';
const isGenerating = passcodeFetcher.state !== 'idle';
Expand Down Expand Up @@ -187,7 +191,7 @@ export const ConnectDialog = ({
<TextField
id="outlined-password-input"
label="Passcode"
type="text"
type={passcodeVisible ? 'text' : 'password'}
autoComplete="current-password"
variant="outlined"
value={passcode}
Expand All @@ -209,6 +213,42 @@ export const ConnectDialog = ({
...monoValueSx,
fontSize: { xs: '1rem', sm: '1.12rem' },
},
endAdornment: (
<InputAdornment position="end">
<Tooltip
title={
passcodeVisible
? 'Hide passcode'
: 'Show passcode'
}
>
<IconButton
aria-label={
passcodeVisible
? 'Hide passcode'
: 'Show passcode'
}
edge="end"
onClick={() => {
setPasscodeVisible(
(visible) =>
!visible
);
}}
onMouseDown={(event) => {
event.preventDefault();
}}
data-testid="toggle-passcode-visibility"
>
{passcodeVisible ? (
<VisibilityOffIcon />
) : (
<VisibilityIcon />
)}
</IconButton>
</Tooltip>
</InputAdornment>
),
},
inputLabel: {
shrink: true,
Expand Down
11 changes: 10 additions & 1 deletion src/app/Console.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
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';
import { Link as RouterLink } from 'react-router-dom';
import { clickablePanelSx, monoValueSx } from './consoleStyles';

export interface PageHeaderProps {
eyebrow?: string;
Expand Down Expand Up @@ -61,6 +62,8 @@ export interface ConsolePanelProps {
eyebrow?: string;
actions?: ReactNode;
sx?: SxProps<Theme>;
to?: string;
testId?: string;
}

export const ConsolePanel = ({
Expand All @@ -69,8 +72,13 @@ export const ConsolePanel = ({
eyebrow,
actions,
sx,
to,
testId,
}: ConsolePanelProps) => (
<Box
component={to === undefined ? 'section' : RouterLink}
to={to}
data-testid={testId}
sx={[
{
position: 'relative',
Expand All @@ -88,6 +96,7 @@ export const ConsolePanel = ({
borderTop: '1px solid rgba(118, 232, 255, 0.16)',
},
},
...(to === undefined ? [] : [clickablePanelSx]),
...(Array.isArray(sx) ? sx : sx ? [sx] : []),
]}
>
Expand Down
123 changes: 119 additions & 4 deletions src/app/NavigationDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { KeyboardEvent } from 'react';
import {
Box,
Divider,
Drawer,
List,
ListItemButton,
Expand All @@ -11,9 +12,11 @@ import {
} from '@mui/material';
import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined';
import CreditCardIcon from '@mui/icons-material/CreditCard';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import ListAltIcon from '@mui/icons-material/ListAlt';
import NotificationsIcon from '@mui/icons-material/Notifications';
import TerminalIcon from '@mui/icons-material/Terminal';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import { useLocation, useNavigate } from 'react-router-dom';
import { APP_NAV_ITEMS } from './router';
import type { AppRouteId } from './router';
Expand All @@ -26,6 +29,13 @@ export interface NavigationDrawerProps {
open: boolean;
/** Close the drawer after backdrop, keyboard, or item selection events. */
onClose: () => void;
/** Clear all persisted local app state for every controller bucket. */
onClearLocalState: () => void;
}

export interface DesktopNavigationRailProps {
/** Clear all persisted local app state for every controller bucket. */
onClearLocalState: () => void;
}

const routeIcon = (routeId: AppRouteId) => {
Expand Down Expand Up @@ -67,14 +77,69 @@ const navButtonSx = (active: boolean) => ({
},
});

const LOCAL_STATE_CLEAR_CONFIRMATION =
'Clear all saved local app state for every identifier? This removes operation history, app notifications, dismissed exchange records, and saved challenge words stored in this browser.';

const ClearLocalStateIcon = () => (
<ListItemIcon
sx={{
minWidth: 38,
color: 'error.main',
position: 'relative',
}}
>
<DeleteForeverIcon />
<WarningAmberIcon
sx={{
position: 'absolute',
left: 18,
top: 14,
fontSize: 15,
color: 'warning.main',
}}
/>
</ListItemIcon>
);

const clearLocalStateButtonSx = {
...navButtonSx(false),
color: 'error.main',
borderColor: 'transparent',
'&:hover': {
borderColor: 'error.main',
bgcolor: 'rgba(255, 75, 90, 0.08)',
color: 'error.main',
},
'.MuiListItemIcon-root': {
minWidth: 38,
},
};

const confirmAndClearLocalState = (
onClearLocalState: () => void,
onClose?: () => void
) => {
const confirmed = globalThis.confirm(LOCAL_STATE_CLEAR_CONFIRMATION);
if (!confirmed) {
return;
}

onClearLocalState();
onClose?.();
};

/**
* Drawer generated from data-router route handles.
*
* This component is intentionally route-aware and feature-unaware: adding a new
* drawer item should mean updating the route descriptor, not hardcoding a
* second navigation list here.
*/
export const NavigationDrawer = ({ open, onClose }: NavigationDrawerProps) => {
export const NavigationDrawer = ({
open,
onClose,
onClearLocalState,
}: NavigationDrawerProps) => {
const navigate = useNavigate();
const location = useLocation();

Expand All @@ -99,7 +164,15 @@ export const NavigationDrawer = ({ open, onClose }: NavigationDrawerProps) => {
},
}}
>
<div role="presentation" onKeyDown={handleKeyDown}>
<Box
role="presentation"
onKeyDown={handleKeyDown}
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<List>
{APP_NAV_ITEMS.map((view) => (
<ListItemButton
Expand All @@ -120,12 +193,29 @@ export const NavigationDrawer = ({ open, onClose }: NavigationDrawerProps) => {
</ListItemButton>
))}
</List>
</div>
<Box sx={{ flexGrow: 1 }} />
<Divider sx={{ mx: 1, my: 0.75 }} />
<ListItemButton
onClick={() =>
confirmAndClearLocalState(onClearLocalState, onClose)
}
data-testid="clear-local-state"
sx={clearLocalStateButtonSx}
>
<ClearLocalStateIcon />
<ListItemText
primary="Clear local state"
secondary="All identifier buckets"
/>
</ListItemButton>
</Box>
</Drawer>
);
};

export const DesktopNavigationRail = () => {
export const DesktopNavigationRail = ({
onClearLocalState,
}: DesktopNavigationRailProps) => {
const navigate = useNavigate();
const location = useLocation();

Expand Down Expand Up @@ -187,6 +277,31 @@ export const DesktopNavigationRail = () => {
</Tooltip>
);
})}
<Box sx={{ flexGrow: 1 }} />
<Divider sx={{ my: 0.75 }} />
<Tooltip title="Clear all saved local app state" placement="right">
<ListItemButton
onClick={() => confirmAndClearLocalState(onClearLocalState)}
data-testid="rail-clear-local-state"
sx={{
...clearLocalStateButtonSx,
mx: 0,
minHeight: 48,
}}
>
<ClearLocalStateIcon />
<ListItemText
primary={
<Typography
component="span"
sx={{ fontWeight: 600 }}
>
Clear local state
</Typography>
}
/>
</ListItemButton>
</Tooltip>
</Box>
);
};
Loading
Loading