Commit 00489f41 authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: enhance UI accessibility and theme handling with new attributes and improved structure

parent 0ba7355e
Loading
Loading
Loading
Loading
+1 −0
Changes for portal-gui/index.html: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="theme-color" content="#ffffff" />
    <title>OOP Portal</title>
  </head>
  <body>
+24 −2
Changes for portal-gui/src/app/theme-provider.tsx: 24 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -31,6 +31,14 @@ function isTheme(value: string | null): value is Theme {
  return THEME_VALUES.includes(value as Theme);
}

function saveTheme(storageKey: string, theme: Theme) {
  try {
    localStorage.setItem(storageKey, theme);
  } catch {
    // Keep the in-memory theme when browser storage is unavailable.
  }
}

function getSystemTheme(): ResolvedTheme {
  if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {
    return 'dark';
@@ -85,17 +93,21 @@ export function ThemeProvider({
  ...props
}: ThemeProviderProps) {
  const [theme, setThemeState] = React.useState<Theme>(() => {
    try {
      const storedTheme = localStorage.getItem(storageKey);
      if (isTheme(storedTheme)) {
        return storedTheme;
      }
    } catch {
      // Use the default theme when browser storage is unavailable.
    }

    return defaultTheme;
  });

  const setTheme = React.useCallback(
    (nextTheme: Theme) => {
      localStorage.setItem(storageKey, nextTheme);
      saveTheme(storageKey, nextTheme);
      setThemeState(nextTheme);
    },
    [storageKey],
@@ -112,6 +124,12 @@ export function ThemeProvider({

      root.classList.remove('light', 'dark');
      root.classList.add(resolvedTheme);
      document
        .querySelector('meta[name="theme-color"]')
        ?.setAttribute(
          'content',
          resolvedTheme === 'dark' ? '#090b0c' : '#ffffff',
        );

      if (restoreTransitions) {
        restoreTransitions();
@@ -167,7 +185,7 @@ export function ThemeProvider({
                ? 'light'
                : 'dark';

        localStorage.setItem(storageKey, nextTheme);
        saveTheme(storageKey, nextTheme);
        return nextTheme;
      });
    };
@@ -181,9 +199,13 @@ export function ThemeProvider({

  React.useEffect(() => {
    const handleStorageChange = (event: StorageEvent) => {
      try {
        if (event.storageArea !== localStorage) {
          return;
        }
      } catch {
        return;
      }

      if (event.key !== storageKey) {
        return;
+44 −15
Changes for portal-gui/src/components/applications/create-app-dialog.tsx: 44 added lines, 15 removed lines.
Original line number Diff line number Diff line
@@ -295,14 +295,14 @@ export const CreateApplicationDialog = ({
              </div>

              <div className="space-y-2">
                <Label>Package type *</Label>
                <Label htmlFor="app-package-type">Package type *</Label>
                <Select
                  value={form.packageType}
                  onValueChange={(v) =>
                    set('packageType', v as AppFormState['packageType'])
                  }
                >
                  <SelectTrigger>
                  <SelectTrigger id="app-package-type">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
@@ -329,14 +329,14 @@ export const CreateApplicationDialog = ({

            <div className="grid gap-4 md:grid-cols-[150px_1fr]">
              <div className="space-y-2">
                <Label>Type *</Label>
                <Label htmlFor="app-repo-type">Type *</Label>
                <Select
                  value={form.repoType}
                  onValueChange={(v) =>
                    set('repoType', v as AppFormState['repoType'])
                  }
                >
                  <SelectTrigger>
                  <SelectTrigger id="app-repo-type">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
@@ -361,29 +361,33 @@ export const CreateApplicationDialog = ({
              <div className="rounded-lg border bg-muted/30 p-4">
                <div className="grid gap-4 md:grid-cols-3">
                  <div className="space-y-2">
                    <Label>Username</Label>
                    <Label htmlFor="app-repo-username">Username</Label>
                    <Input
                      id="app-repo-username"
                      value={form.userName}
                      onChange={(v) => set('userName', v.target.value)}
                    />
                  </div>
                  <div className="space-y-2">
                    <Label>Credentials / token</Label>
                    <Label htmlFor="app-repo-credentials">
                      Credentials / token
                    </Label>
                    <Input
                      id="app-repo-credentials"
                      value={form.credentials}
                      onChange={(v) => set('credentials', v.target.value)}
                      type="password"
                    />
                  </div>
                  <div className="space-y-2">
                    <Label>Auth type</Label>
                    <Label htmlFor="app-auth-type">Auth type</Label>
                    <Select
                      value={form.authType}
                      onValueChange={(v) =>
                        set('authType', v as AppFormState['authType'])
                      }
                    >
                      <SelectTrigger>
                      <SelectTrigger id="app-auth-type">
                        <SelectValue />
                      </SelectTrigger>
                      <SelectContent>
@@ -421,8 +425,11 @@ export const CreateApplicationDialog = ({
              >
                <div className="flex items-end gap-3">
                  <div className="flex-1 space-y-2">
                    <Label>Component name *</Label>
                    <Label htmlFor={`app-component-${ci}`}>
                      Component name *
                    </Label>
                    <Input
                      id={`app-component-${ci}`}
                      value={comp.componentName}
                      onChange={(v) =>
                        setComponent(ci, 'componentName', v.target.value)
@@ -435,9 +442,10 @@ export const CreateApplicationDialog = ({
                      type="button"
                      variant="ghost"
                      size="icon"
                      aria-label={`Remove component ${ci + 1}`}
                      onClick={() => removeComponent(ci)}
                    >
                      <Trash2 className="h-4 w-4" />
                      <Trash2 aria-hidden className="h-4 w-4" />
                    </Button>
                  )}
                </div>
@@ -450,6 +458,7 @@ export const CreateApplicationDialog = ({
                      className="grid gap-2 md:grid-cols-[1fr_100px_80px_150px_auto]"
                    >
                      <Input
                        aria-label={`Component ${ci + 1}, interface ${ii + 1} ID`}
                        value={iface.interfaceId}
                        onChange={(v) =>
                          setInterface(ci, ii, 'interfaceId', v.target.value)
@@ -462,7 +471,9 @@ export const CreateApplicationDialog = ({
                          setInterface(ci, ii, 'protocol', v)
                        }
                      >
                        <SelectTrigger>
                        <SelectTrigger
                          aria-label={`Component ${ci + 1}, interface ${ii + 1} protocol`}
                        >
                          <SelectValue />
                        </SelectTrigger>
                        <SelectContent>
@@ -472,6 +483,7 @@ export const CreateApplicationDialog = ({
                        </SelectContent>
                      </Select>
                      <Input
                        aria-label={`Component ${ci + 1}, interface ${ii + 1} port`}
                        type="number"
                        min={1}
                        max={65535}
@@ -488,7 +500,9 @@ export const CreateApplicationDialog = ({
                          setInterface(ci, ii, 'visibilityType', value)
                        }
                      >
                        <SelectTrigger>
                        <SelectTrigger
                          aria-label={`Component ${ci + 1}, interface ${ii + 1} visibility`}
                        >
                          <SelectValue />
                        </SelectTrigger>
                        <SelectContent>
@@ -506,9 +520,10 @@ export const CreateApplicationDialog = ({
                          type="button"
                          variant="ghost"
                          size="icon"
                          aria-label={`Remove interface ${ii + 1} from component ${ci + 1}`}
                          onClick={() => removeInterface(ci, ii)}
                        >
                          <Trash2 className="h-4 w-4" />
                          <Trash2 aria-hidden className="h-4 w-4" />
                        </Button>
                      ) : (
                        <div />
@@ -535,7 +550,9 @@ export const CreateApplicationDialog = ({
          <section className="space-y-3">
            <div className="flex items-center justify-between">
              <div>
                <h3 className="text-sm font-semibold">Required Resources *</h3>
                <h3 id="app-resources-label" className="text-sm font-semibold">
                  Required Resources *
                </h3>
                <p className="text-xs text-muted-foreground">
                  Kubernetes resources are required. CPU values are cores;
                  memory is MB.
@@ -546,6 +563,8 @@ export const CreateApplicationDialog = ({
                type="button"
                variant="ghost"
                size="sm"
                aria-expanded={resourcesExpanded}
                aria-controls="app-resources"
                onClick={() => setResourcesExpanded((v) => !v)}
              >
                {resourcesExpanded ? '▲ Collapse' : '▼ Expand'}
@@ -553,13 +572,23 @@ export const CreateApplicationDialog = ({
            </div>
            {resourcesExpanded && (
              <Textarea
                id="app-resources"
                aria-labelledby="app-resources-label"
                aria-invalid={Boolean(jsonError) || undefined}
                aria-describedby={jsonError ? 'app-resources-error' : undefined}
                value={form.requiredResourcesJson}
                onChange={(e) => handleResourcesChange(e.target.value)}
                className="min-h-[300px] font-mono text-xs"
              />
            )}
            {jsonError && (
              <p className="text-sm text-destructive">{jsonError}</p>
              <p
                id="app-resources-error"
                role="alert"
                className="text-sm text-destructive"
              >
                {jsonError}
              </p>
            )}
          </section>

+1 −1
Changes for portal-gui/src/features/apis/api-card.tsx: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
export function ApiCard({ api }: ApiCardProps) {
  return (
    <Link to={`${ROUTES.APIS}/${api.id}`}>
      <Card className="group h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg">
      <Card className="group h-full cursor-pointer transition-[transform,box-shadow] hover:-translate-y-1 hover:shadow-lg motion-reduce:hover:translate-y-0">
        <CardHeader>
          <CardTitle className="text-2xl">{api.title}</CardTitle>
          <div className="flex flex-wrap items-center gap-2">
+5 −1
Changes for portal-gui/src/features/apis/apis.tsx: 5 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -32,8 +32,12 @@ export function ApisPage() {
            <PageHero title="Browse and discover OpenOP APIs">
              <div className="mt-5 max-w-2xl">
                <div className="relative">
                  <Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
                  <Search
                    aria-hidden
                    className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground"
                  />
                  <Input
                    aria-label="Search APIs by title or category"
                    placeholder="Search APIs by title or category..."
                    className="h-11 pl-10"
                    value={search}
Loading