Feature task 3634 tdei theme#54
Conversation
Introduce TDEI branding: add tdei-logo.png, import Lato/Open Sans/Montserrat fonts, update theme primary color to #32006e, and add extensive utility CSS (color variables, button/link styles, layout helpers, accessibility focus styles) in main.scss. Replace many Bootstrap button/variant uses across components/pages with new tdei-* classes (tdei-primary-button, tdei-secondary-button, maroon-bg, tdei-primary-link) and adjust dialog button classes/confirm styles to match the new design system. These changes unify styling and apply the updated TDEI visual identity across the app.
Add new asset (osw-type.svg) and update global styles and pages to improve layout and mobile UX. Main changes: - components/AppNavbar.vue: Rework navbar into a responsive, sticky header with a mobile side menu and updated user dropdown. Adds state and lifecycle hooks to manage mobile menu visibility, accessibility attributes, and dedicated mobile interactions (close button, backdrop, logout flow). Extensive SCSS added to style the navbar, side menu, and responsive behaviors. - pages/index.vue: Make the primary CTA conditional on authentication (redirect to dashboard when signed in), import auth in the script, and update hero styling to use CSS variables and refined colors/hover states. - assets/scss/main.scss: Update font imports (remove Lato, ensure Open Sans and Montserrat are imported). - assets/img/osw-type.svg: Add new SVG asset. These changes are intended to improve navigation on small screens, unify header styling, and make the landing CTA reflect user authentication state.
Revamp dashboard workspace list and toolbar styling: remove duplicate Montserrat font import; rename toolbar button classes to tdei-toolbar-primary-button and add scoped styles for toolbar buttons and grouped button radii. Overhaul WorkspaceItem into a card-style layout (new markup, styles, responsive adjustments), show workspace ID, format type/status labels, and simplify class logic. Update dashboard page to add a page header subtitle, adjust Project Group label styling, wrap workspace list in a template, and add getLastProjectGroupName + selectedProjectGroupName computed to derive the displayed project group name from sessionStorage or project groups.
Add flex and pathways SVG icons and update WorkspaceItem.vue to select the correct icon based on workspace.type. Imports osw, flex, and pathways assets, introduces a computed typeIconSrc that maps types to their icons (falls back to osw), and replaces the hardcoded img src with a bound :src. Enables showing the appropriate icon for different workspace types.
Refactor the homepage template and SCSS to introduce a new hero layout and visual update. Replaced .jumbo-content with .hero-shell and .hero-copy, moved CTAs into a .hero-actions container (primary Nuxt link + external "Learn more"), and kept the auth-based dashboard/signin routing. Reworked background to use layered gradients over the existing bg.webp, added a decorative overlay, improved typography, spacing, and app-logo styling, and refined responsive behavior across breakpoints (lg, md, sm). These changes modernize the landing hero, improve readability and responsiveness, and polish CTA styles and spacing.
SCSS: Remove unnecessary !important from primary button rules, add proper :focus styles (box-shadow) for accessibility, and introduce .tdei-outline-cyan-button (replacing .tdei-button2) with hover/focus/disabled states. Unify radio focus styling and update shadow color to match brand. Minor CSS cleanup and spacing adjustments. Components: remove d-block from the Apply button in FilterDropdown. Pages: simplify selectedProjectGroupName logic in dashboard.vue (drop getLastProjectGroupName fallback) and reduce hero-primary-action min-height in index.vue.
Add an accessible alt attribute for workspace type images and adjust SCSS variables and styles. In WorkspaceItem.vue the img alt is now dynamic (uses formatTypeLabel) to improve accessibility. In assets/scss/main.scss: remove duplicate --primary-color-dark, add --tdei-cyan, change .jsonContent border to use --secondary-color, remove custom radio focus styles, and adjust .column-style flex/margins. Also restore/move text-transform: capitalize for .workspaceStatus to the stronger selector to keep visual consistency.
Add focus management and keyboard support for the mobile side menu: add refs for the toggle button and side menu, focus the close button when opened and restore focus to the toggle when closed, trap Tab/Shift+Tab inside the menu, handle Escape to close, and add aria-modal="true". Attach/detach keydown listener on mount/unmount. Minor style updates: introduce --primary-color-dark and small SCSS cleanup. Remove an unused getLastProjectGroupName() helper from dashboard.vue. Add rel="noopener noreferrer" to an external link for security.
SCSS: Update .tdei-rounded-button to .btn.tdei-rounded-button, remove unnecessary !important rules, nest hover/active state and use --primary-color-dark for hover/border. Also fix stray brace and tighten selector scope. AppNavbar.vue: Use focusable.at(0) and .at(-1) for first/last element and add a null check to avoid runtime errors when focusable is empty. pages/dashboard.vue: Render the "in <project group>" phrase only when a project group name exists and change the computed fallback from a literal 'project group' to null to avoid showing a placeholder when none is selected.
Overhaul the sign-in experience and adjust several layout components. Add a new background image asset and replace the signin page to render the new SigninForm component with a centered card, background, and responsive styles. Revamp SigninForm: new header, show/hide password toggle, improved loading/error states, and extensive SCSS for desktop/mobile. Tidy up AppNavbar templates and mobile menu markup (focus handling/focus trap fixes and markup formatting). Minor tweaks: AppSpinner markup, default layout nuxt-loading-indicator props, and refactor dashboard into a split layout with scrollable workspace list and sticky details panel.
Bind aria-modal to return 'true' or 'false' based on mobileMenuOpen/isMobileView to fix accessibility attribute type. Strengthen SigninForm error handling by adding a fetch-error type guard, parsing 400 responses for validation errors, and providing specific messages for 401 and 404 cases; also normalize other errors to strings. Add missing 'btn' class to the Close button in JoinDialog for consistent styling.
Add role="dialog" to the side menu element in AppNavbar.vue to improve accessibility and complement existing ARIA attributes. Remove an explicit color rule from .team-members-btn in teams/Item.vue to stop overriding the global body color and keep link styling consistent.
Remove external @import URL lines from assets/scss/main.scss and add the @nuxtjs/google-fonts module and configuration in nuxt.config.ts (register Open Sans and Montserrat, set display: 'swap', download: true). Also add @nuxtjs/google-fonts to devDependencies in package.json. Centralizes font loading, enables local font download/optimization, and avoids inline external CSS imports.
Add a long CSS transition to neutralize Chrome's autofill background, preventing the yellow/blue autofill styling from showing. Replace the AppNavbar side menu <aside> with a <div> to match the component structure. Scope SigninForm styles (add scoped) and remove the preset input background so autofill styling is handled consistently. Add an explicit Workspace type import and annotate selectWorkspace(workspace) with the Workspace type.
📝 WalkthroughWalkthroughThis PR implements a comprehensive design system refresh, introducing new TDEI-branded utility classes throughout the application. Global SCSS styling is expanded with CSS variables and typography helpers. Navigation, sign-in, and landing pages are restructured with improved layouts. Components are systematically converted from Bootstrap variants to custom button classes. Dashboard and workspace management UIs are redesigned with split layouts and card-based presentations. ChangesDesign System & UI Styling Refresh
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
components/AppLogo.vueESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. components/AppNavbar.vueESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox. components/AppSpinner.vueESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@assets/scss/main.scss`:
- Around line 9-10: The two CSS `@import` directives for
"maplibre-gl/dist/maplibre-gl.css" and "vue3-toastify/dist/index.css" are placed
after the input:-webkit-autofill rule, causing the
no-invalid-position-at-import-rule lint error; fix this by moving both `@import`
lines to the very top of assets/scss/main.scss so they appear before any style
rules (specifically before the input:-webkit-autofill selector and its block),
preserving their exact strings and order.
In `@components/AppNavbar.vue`:
- Around line 318-324: The CSS currently removes the keyboard focus indicator
for the mobile nav toggler (box-shadow: none) and only defines hover styles for
drawer controls, which hides focus for keyboard users; restore a visible focus
indicator by replacing the "box-shadow: none" rule on the toggler with a focused
style (use :focus and :focus-visible) that applies a visible outline or
box-shadow (e.g. using a --focus-ring-color and matching border-radius) and add
equivalent :focus/:focus-visible rules for the drawer controls so they show the
same focus ring as hover (but distinct for keyboard focus), ensuring you target
the same selectors used for the toggler and drawer controls in AppNavbar.vue so
keyboard users can track focus.
- Around line 65-74: The dropdown toggle currently nests a native <button>
inside the b-dropdown `#button-content` which breaks toggle semantics; replace the
inner interactive element (class "userProfile" button) with a non-interactive
wrapper (e.g., div or span) and move any click/keyboard handling to the
dropdown/toggle itself so the dropdown controls activation; ensure the wrapper
keeps the same classes/visual markup (class "userProfile") and accessible
labeling (use aria attributes on the toggle if needed) to preserve appearance
and accessibility.
In `@components/dashboard/WorkspaceItem.vue`:
- Around line 15-20: The role chip currently only handles 'lead' and 'validator'
and leaves 'contributor' or undefined blank; update the template in
WorkspaceItem.vue to add a v-else fallback that renders a span with class
"workspaceStatus" showing ROLE_LABELS.member (treat undefined the same as
'contributor'). Ensure you reference the existing workspace.role checks and
ROLE_LABELS constant so the new fallback collapses contributor/undefined into
the Member label.
In `@components/SigninForm.vue`:
- Around line 23-27: In SigninForm.vue, add autocomplete attributes to the
credential inputs so browsers/password managers work correctly: set
autocomplete="username" on the input using v-model="username" (id="username")
and set autocomplete="current-password" on the input using v-model="password"
(the password field around lines 43-47); ensure attributes are added to the same
element nodes that currently have class "form-control signin-input" and aria
attributes.
- Around line 107-113: The 400 error branch currently calls await
e.response.json() which can throw if the response body isn't valid JSON; wrap
the JSON parse in a safe guard (e.g., inspect
e.response.headers.get('content-type') for application/json and/or use a
try/catch around await e.response.json()) and fall back to a generic message
when parsing fails so error.value still gets set; update the logic around the
existing body variable and the error.value assignment in the SigninForm.vue
catch block to use the parsed body only when parse succeeds and otherwise set a
sensible fallback error.
In `@pages/signin.vue`:
- Around line 12-14: The redirect currently calls navigateTo('/dashboard')
without awaiting its promise; update the branch that checks tdeiAuth.complete to
await (or return) the navigateTo call so Nuxt handles the navigation
immediately—e.g., inside the script setup where tdeiAuth.complete is checked,
replace navigateTo('/dashboard') with await navigateTo('/dashboard') (or return
await navigateTo('/dashboard')) so the promise is not ignored.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 784db07f-7b18-4614-9663-5037757aebfd
⛔ Files ignored due to path filters (5)
assets/img/bg_login.pngis excluded by!**/*.pngassets/img/flex-type.svgis excluded by!**/*.svgassets/img/osw-type.svgis excluded by!**/*.svgassets/img/pathways-type.svgis excluded by!**/*.svgassets/img/tdei-logo.pngis excluded by!**/*.png
📒 Files selected for processing (35)
assets/scss/main.scssassets/scss/theme.scsscomponents/AppLogo.vuecomponents/AppNavbar.vuecomponents/AppSpinner.vuecomponents/DatasetTypeRadio.vuecomponents/SigninForm.vuecomponents/dashboard/Toolbar.vuecomponents/dashboard/WorkspaceItem.vuecomponents/review/FilterDropdown.vuecomponents/review/Toolbar.vuecomponents/settings/panel/Apps.vuecomponents/settings/panel/Delete.vuecomponents/settings/panel/General.vuecomponents/settings/panel/Imagery.vuecomponents/settings/teams/JoinDialog.vuecomponents/teams/Item.vuecomponents/teams/JoinDialog.vuecomponents/teams/MembersDialog.vuecomponents/teams/SettingsDialog.vuelayouts/default.vuenuxt.config.tspackage.jsonpages/dashboard.vuepages/help.vuepages/index.vuepages/signin.vuepages/workspace/[id]/export/index.vuepages/workspace/[id]/export/tdei.vuepages/workspace/[id]/settings/teams/index.vuepages/workspace/[id]/teams/[teamId].vuepages/workspace/create/blank.vuepages/workspace/create/file.vuepages/workspace/create/index.vuepages/workspace/create/tdei.vue
| @import "maplibre-gl/dist/maplibre-gl.css"; | ||
| @import "vue3-toastify/dist/index.css"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether any `@import` appears after a non-import rule in main.scss
set -euo pipefail
FILE="$(fd '^main\.scss$' -a | head -n1)"
echo "Checking: $FILE"
awk '
BEGIN { seen_non_import=0; bad=0 }
{
line=$0
trimmed=line
sub(/^[[:space:]]+/, "", trimmed)
if (trimmed == "" || trimmed ~ /^\/\// || trimmed ~ /^\/\*/) next
if (trimmed ~ /^`@import`[[:space:]]+/) {
if (seen_non_import) {
printf("Invalid import position at line %d: %s\n", NR, line)
bad=1
}
next
}
# treat other top-level statements as non-import
seen_non_import=1
}
END {
if (bad) exit 1
print "OK: no invalid `@import` positions detected."
}
' "$FILE"Repository: TaskarCenterAtUW/workspaces-frontend
Length of output: 286
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="assets/scss/main.scss"
echo "== $FILE =="
nl -ba "$FILE" | sed -n '1,25p'Repository: TaskarCenterAtUW/workspaces-frontend
Length of output: 169
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="assets/scss/main.scss"
echo "== $FILE =="
cat -n "$FILE" | sed -n '1,25p'Repository: TaskarCenterAtUW/workspaces-frontend
Length of output: 1043
Move CSS @import directives to the top of assets/scss/main.scss (before style rules)
@import statements at lines 9-10 come after the input:-webkit-autofill rule block (lines 4-8), which triggers no-invalid-position-at-import-rule and can fail linting.
💡 Proposed fix
`@import` "theme.scss";
`@import` "bootstrap/scss/bootstrap.scss";
+@import "maplibre-gl/dist/maplibre-gl.css";
+@import "vue3-toastify/dist/index.css";
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
transition: background-color 600000s, color 600000s;
}
-@import "maplibre-gl/dist/maplibre-gl.css";
-@import "vue3-toastify/dist/index.css";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @import "maplibre-gl/dist/maplibre-gl.css"; | |
| @import "vue3-toastify/dist/index.css"; | |
| `@import` "theme.scss"; | |
| `@import` "bootstrap/scss/bootstrap.scss"; | |
| `@import` "maplibre-gl/dist/maplibre-gl.css"; | |
| `@import` "vue3-toastify/dist/index.css"; | |
| input:-webkit-autofill, | |
| input:-webkit-autofill:hover, | |
| input:-webkit-autofill:focus { | |
| transition: background-color 600000s, color 600000s; | |
| } |
🧰 Tools
🪛 Stylelint (17.11.1)
[error] 9-9: Invalid position for @import rule (no-invalid-position-at-import-rule)
(no-invalid-position-at-import-rule)
[error] 10-10: Invalid position for @import rule (no-invalid-position-at-import-rule)
(no-invalid-position-at-import-rule)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@assets/scss/main.scss` around lines 9 - 10, The two CSS `@import` directives
for "maplibre-gl/dist/maplibre-gl.css" and "vue3-toastify/dist/index.css" are
placed after the input:-webkit-autofill rule, causing the
no-invalid-position-at-import-rule lint error; fix this by moving both `@import`
lines to the very top of assets/scss/main.scss so they appear before any style
rules (specifically before the input:-webkit-autofill selector and its block),
preserving their exact strings and order.
| <template #button-content> | ||
| <button class="userProfile"> | ||
| <div>{{ auth.displayName }}</div> | ||
| <app-icon | ||
| variant="account_circle" | ||
| size="32" | ||
| no-margin | ||
| /> | ||
| </button> | ||
| </template> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In bootstrap-vue-next b-dropdown, is it valid to place a native <button> inside the #button-content slot, or should slot content be non-interactive because the component already renders the toggle button?
💡 Result:
Yes, it’s valid to put a native inside b-dropdown’s #button-content slot, because the BootstrapVueNext docs explicitly state that the button-content slot allows “basic HTML and icons in the button content” (i.e., it isn’t limited to non-interactive markup) [1]. However, for correct behavior and accessibility, you generally shouldn’t place another interactive control that the user can click to trigger its own separate action inside the dropdown-toggle button, because the entire header is already the control that toggles the dropdown. In practice: keep the slot content simple (text/icons) or ensure any nested interactive element does not interfere with the dropdown toggle’s click/keyboard handling. Evidence from the component docs: the #button-content slot is provided specifically to customize the dropdown toggle’s content with HTML/icons, and is not documented as “non-interactive only” [1].
Citations:
Avoid nested interactive semantics in b-dropdown #button-content (accessibility/behavior risk).
b-dropdown’s #button-content slot supports arbitrary HTML/icons (so nesting isn’t inherently “invalid markup”), but placing a native nested button inside the dropdown toggle content can interfere with the toggle’s click/focus/keyboard semantics and confuse screen readers. Prefer a non-interactive wrapper.
Suggested fix
<template `#button-content`>
- <button class="userProfile">
+ <span class="userProfile">
<div>{{ auth.displayName }}</div>
<app-icon
variant="account_circle"
size="32"
no-margin
/>
- </button>
+ </span>
</template>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <template #button-content> | |
| <button class="userProfile"> | |
| <div>{{ auth.displayName }}</div> | |
| <app-icon | |
| variant="account_circle" | |
| size="32" | |
| no-margin | |
| /> | |
| </button> | |
| </template> | |
| <template `#button-content`> | |
| <span class="userProfile"> | |
| <div>{{ auth.displayName }}</div> | |
| <app-icon | |
| variant="account_circle" | |
| size="32" | |
| no-margin | |
| /> | |
| </span> | |
| </template> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/AppNavbar.vue` around lines 65 - 74, The dropdown toggle currently
nests a native <button> inside the b-dropdown `#button-content` which breaks
toggle semantics; replace the inner interactive element (class "userProfile"
button) with a non-interactive wrapper (e.g., div or span) and move any
click/keyboard handling to the dropdown/toggle itself so the dropdown controls
activation; ensure the wrapper keeps the same classes/visual markup (class
"userProfile") and accessible labeling (use aria attributes on the toggle if
needed) to preserve appearance and accessibility.
| &:focus { | ||
| box-shadow: none; | ||
| } | ||
|
|
||
| &:hover { | ||
| background-color: var(--primary-color-dark); | ||
| } |
There was a problem hiding this comment.
Restore visible keyboard focus styles for mobile nav controls.
Lines 318-320 remove the toggler focus indicator (box-shadow: none) without a replacement, and drawer controls only define hover styles. Keyboard users can lose track of focus in the menu flow.
Suggested fix
.mobileMenuIcon {
padding: 5px 8px;
border: none;
- &:focus {
- box-shadow: none;
- }
+ &:focus-visible {
+ outline: 2px solid `#ffffff`;
+ outline-offset: 2px;
+ box-shadow: none;
+ }
&:hover {
background-color: var(--primary-color-dark);
}
}
+ .sideMenuClose:focus-visible,
+ .sideMenuLink:focus-visible,
+ .sideMenuButton:focus-visible {
+ outline: 2px solid var(--primary-color);
+ outline-offset: -2px;
+ }Also applies to: 447-452, 460-481
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/AppNavbar.vue` around lines 318 - 324, The CSS currently removes
the keyboard focus indicator for the mobile nav toggler (box-shadow: none) and
only defines hover styles for drawer controls, which hides focus for keyboard
users; restore a visible focus indicator by replacing the "box-shadow: none"
rule on the toggler with a focused style (use :focus and :focus-visible) that
applies a visible outline or box-shadow (e.g. using a --focus-ring-color and
matching border-radius) and add equivalent :focus/:focus-visible rules for the
drawer controls so they show the same focus ring as hover (but distinct for
keyboard focus), ensuring you target the same selectors used for the toggler and
drawer controls in AppNavbar.vue so keyboard users can track focus.
| <span v-if="workspace.role === 'lead'" class="workspaceStatus"> | ||
| {{ ROLE_LABELS.lead }} | ||
| </span> | ||
| <span v-else-if="workspace.role === 'validator'" class="workspaceStatus"> | ||
| {{ ROLE_LABELS.validator }} | ||
| </span> |
There was a problem hiding this comment.
Add a Member fallback for non-lead/non-validator roles.
At Line 15-20, workspace.role === 'contributor' (or undefined) currently renders no role chip. Add a v-else fallback so those users are consistently shown as Member.
Suggested fix
<span v-else-if="workspace.role === 'validator'" class="workspaceStatus">
{{ ROLE_LABELS.validator }}
</span>
+ <span v-else class="workspaceStatus">
+ Member
+ </span>Based on learnings: treat missing workspace.role (undefined) the same as workspace.role === 'contributor', collapsing both into a single 'Member' fallback in dashboard-related role UI.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <span v-if="workspace.role === 'lead'" class="workspaceStatus"> | |
| {{ ROLE_LABELS.lead }} | |
| </span> | |
| <span v-else-if="workspace.role === 'validator'" class="workspaceStatus"> | |
| {{ ROLE_LABELS.validator }} | |
| </span> | |
| <span v-if="workspace.role === 'lead'" class="workspaceStatus"> | |
| {{ ROLE_LABELS.lead }} | |
| </span> | |
| <span v-else-if="workspace.role === 'validator'" class="workspaceStatus"> | |
| {{ ROLE_LABELS.validator }} | |
| </span> | |
| <span v-else class="workspaceStatus"> | |
| Member | |
| </span> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/dashboard/WorkspaceItem.vue` around lines 15 - 20, The role chip
currently only handles 'lead' and 'validator' and leaves 'contributor' or
undefined blank; update the template in WorkspaceItem.vue to add a v-else
fallback that renders a span with class "workspaceStatus" showing
ROLE_LABELS.member (treat undefined the same as 'contributor'). Ensure you
reference the existing workspace.role checks and ROLE_LABELS constant so the new
fallback collapses contributor/undefined into the Member label.
| id="username" | ||
| v-model="username" | ||
| class="form-control signin-input" | ||
| aria-describedby="usernameHelp" | ||
| > |
There was a problem hiding this comment.
Add autocomplete hints for credential fields.
Use autocomplete="username" and autocomplete="current-password" so browsers/password managers behave correctly.
Suggested fix
<input
id="username"
v-model="username"
class="form-control signin-input"
aria-describedby="usernameHelp"
+ autocomplete="username"
>
...
<input
id="password"
v-model="password"
:type="showPassword ? 'text' : 'password'"
class="form-control signin-input password-input"
+ autocomplete="current-password"
>Also applies to: 43-47
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/SigninForm.vue` around lines 23 - 27, In SigninForm.vue, add
autocomplete attributes to the credential inputs so browsers/password managers
work correctly: set autocomplete="username" on the input using
v-model="username" (id="username") and set autocomplete="current-password" on
the input using v-model="password" (the password field around lines 43-47);
ensure attributes are added to the same element nodes that currently have class
"form-control signin-input" and aria attributes.
| if (e.response.status === 400) { | ||
| const body = await e.response.json() as { errors?: string[] } | ||
|
|
||
| if (body.errors && body.errors.length > 0) { | ||
| error.value = 'Error: ' + body.errors[0] | ||
| return | ||
| } |
There was a problem hiding this comment.
Guard JSON parsing in the 400 error branch.
If a 400 response is not JSON, await e.response.json() throws inside the catch block and bypasses your user-facing fallback error handling.
Suggested fix
if (e.response.status === 400) {
- const body = await e.response.json() as { errors?: string[] }
-
- if (body.errors && body.errors.length > 0) {
- error.value = 'Error: ' + body.errors[0]
- return
- }
+ try {
+ const body = await e.response.json() as { errors?: string[] }
+ if (body.errors && body.errors.length > 0) {
+ error.value = 'Error: ' + body.errors[0]
+ return
+ }
+ }
+ catch {
+ // fall through to generic message below
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (e.response.status === 400) { | |
| const body = await e.response.json() as { errors?: string[] } | |
| if (body.errors && body.errors.length > 0) { | |
| error.value = 'Error: ' + body.errors[0] | |
| return | |
| } | |
| if (e.response.status === 400) { | |
| try { | |
| const body = await e.response.json() as { errors?: string[] } | |
| if (body.errors && body.errors.length > 0) { | |
| error.value = 'Error: ' + body.errors[0] | |
| return | |
| } | |
| } | |
| catch { | |
| // fall through to generic message below | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/SigninForm.vue` around lines 107 - 113, The 400 error branch
currently calls await e.response.json() which can throw if the response body
isn't valid JSON; wrap the JSON parse in a safe guard (e.g., inspect
e.response.headers.get('content-type') for application/json and/or use a
try/catch around await e.response.json()) and fall back to a generic message
when parsing fails so error.value still gets set; update the logic around the
existing body variable and the error.value assignment in the SigninForm.vue
catch block to use the parsed body only when parse succeeds and otherwise set a
sensible fallback error.
| if (tdeiAuth.complete) { | ||
| navigateTo('/dashboard'); | ||
| navigateTo('/dashboard') | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Nuxt 4 page <script setup>, what is the recommended pattern for immediate auth redirects: plain navigateTo(...) vs return/await navigateTo(...)?
💡 Result:
Recommended pattern in a Nuxt 4 page <script setup> for immediate auth redirects is to use await (or return) with navigateTo, i.e. await navigateTo('/login') and then prevent further work as needed. What to do 1) In <script setup>, do: - await navigateTo('/login') (or await navigateTo({ path: '/login' })). Nuxt’s navigateTo docs explicitly show await navigateTo(...) usage in <script setup> examples. [1] 2) Avoid “fire-and-forget” usage like navigateTo('/login') without awaiting/returning. Nuxt’s navigateTo docs explicitly state: “Make sure to always use await or return on result of navigateTo when calling it.” [2][1] Plain navigateTo(...) vs return/await navigateTo(...) - navigateTo(...) without awaiting/returning: not the recommended approach; Nuxt advises awaiting/returning the result to ensure the navigation outcome is properly handled. [2][1] - return navigateTo(...): generally discouraged/fragile in <script setup> if you’re trying to “terminate” execution. Nuxt issue discussions note that there’s no reliable way to terminate execution in the <script setup> block and that return navigateTo() was previously possible but has been deprecated; the recommended approach becomes handling redirects with await (or other control-flow patterns). [3] - await navigateTo(...): the documented and recommended way in <script setup> to perform the redirect. [1] Practical guidance for auth redirect in a page - Use middleware for route guarding when possible (so pages don’t render at all). Nuxt’s routing docs show returning navigateTo from route middleware for auth gating. [4] - If you must do it in the page itself, use await navigateTo(...) early in the script. In short: prefer await navigateTo(...) in a page <script setup>; don’t call navigateTo(...) without awaiting/returning; and avoid relying on return navigateTo(...) to stop the rest of <script setup> execution. [2][1][3]
Citations:
- 1: https://nuxt.com/docs/4.x/api/utils/navigate-to
- 2: https://nuxt-nuxt.mintlify.app/api/utils/navigate-to
- 3: transform
navigateToin<script setup>to provide an early return nuxt/nuxt#23698 - 4: https://nuxt.com/docs/4.x/getting-started/routing
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ls -la
# Show the relevant portion of pages/signin.vue
if [ -f "pages/signin.vue" ]; then
echo "---- pages/signin.vue (context) ----"
nl -ba pages/signin.vue | sed -n '1,80p'
else
echo "pages/signin.vue not found"
fd signin.vue -a . || true
fiRepository: TaskarCenterAtUW/workspaces-frontend
Length of output: 2282
🌐 Web query:
Nuxt 4 navigateTo in <script setup> "Make sure to always use await or return"
💡 Result:
In Nuxt 4, navigateTo returns a value/promise (depending on context), and the docs explicitly instruct: “Make sure to always use await or return on result of navigateTo when calling it.” [1] What this means in <script setup> 1) If you call navigateTo directly from your setup code, you should await it: await navigateTo('/search') [1] or await navigateTo({ path: '/search' }) [1] 2) If you are in an async function, you can also return the navigateTo call instead of awaiting it, as long as you “return its result” to keep the calling flow consistent (the docs’ wording covers both await and return) [1]. Route middleware special case If you call navigateTo inside defineNuxtRouteMiddleware, you must return it so the middleware execution flow works correctly. The docs show the correct form: return navigateTo('/search', { redirectCode: 301 }) [1] and the incorrect form (navigateTo executed but not returned): navigateTo('/search', { redirectCode: 301 }) return [1] Why you should do this The official docs warn that not awaiting/returning can lead to “unexpected behavior” in middleware, and they also provide concrete examples of calling navigateTo with await in <script setup> [1]. Note on “return navigateTo” vs “terminate setup execution” Nuxt maintainers have discussed that terminating execution inside <script setup> is not straightforward; using return navigateTo there can be deprecated/insufficient for aborting setup flow. A tracked Nuxt issue discusses the limitation and suggests patterns/workarounds, but the core, source-backed guidance for navigateTo usage is still to await or return its result when you call it [2].
Citations:
- 1: https://nuxt.com/docs/4.x/api/utils/navigate-to
- 2: transform
navigateToin<script setup>to provide an early return nuxt/nuxt#23698
🌐 Web query:
Nuxt 4 navigateTo return vs await guidance in pages
💡 Result:
In Nuxt 4, for programmatic navigation with navigateTo, you should either: 1) return navigateTo(...) from functions that use it (notably route middleware), or 2) await navigateTo(...) in async contexts where you just need to wait for the navigation to complete. Key guidance differences A) In route middleware: you must return Nuxt’s docs explicitly say that when using navigateTo within route middleware, you must return its result to ensure the middleware execution flow works correctly, and they show the non-working pattern where navigateTo(...) is called but not returned. [1] Example (middleware redirect): ts export default defineNuxtRouteMiddleware((to, from) => { if (to.path !== '/search') { return navigateTo('/search', { redirectCode: 301 }) } }) [1] B) In pages/components: prefer await for “do this after navigation” Nuxt’s navigateTo docs also show using await navigateTo('/search') (or awaiting a route object) in pages/components style usage. [1] Example (page/component): ts <script setup lang="ts"> await navigateTo({ path: '/search', query: { page: 1 } }) </script> [1] What “return vs await” really changes - return navigateTo(...) is mainly about control flow: it passes the navigation result back to the caller. This is required in middleware because the router/middleware runner needs the return value. [1] - await navigateTo(...) is about sequencing: it pauses your async code until navigation finishes/settles. The docs instruct to “always use await or return on result of navigateTo when calling it.” [1] C) Why “await but not return” can be problematic in middleware A Nuxt issue discusses middleware behavior and highlights that you need correct handling of navigateTo result in middleware contexts. [2] So: concise recommendation - Route middleware: use return navigateTo(...) (or return await navigateTo(...) if you want, but return is the important part). [1] - Pages/components (async handler): use await navigateTo(...) if you need to wait; otherwise, just ensure you use either await or return as recommended by the docs (don’t call it and ignore the result). [1]
Citations:
- 1: https://nuxt.com/docs/4.x/api/utils/navigate-to
- 2: can't return
navigateToafter await in route middleware (on server) nuxt/nuxt#13340
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant portion of pages/signin.vue
if [ -f "pages/signin.vue" ]; then
echo "---- pages/signin.vue (context) ----"
nl -ba pages/signin.vue | sed -n '1,120p'
else
echo "pages/signin.vue not found"
fd -a -i "signin.vue" . || true
fiRepository: TaskarCenterAtUW/workspaces-frontend
Length of output: 178
🌐 Web query:
Nuxt 4 navigateTo in <script setup> "Make sure to always use await or return"
💡 Result:
In Nuxt 4, when you call navigateTo in <script setup>, you should “always use await or return” because navigateTo returns a value/promise that you need to handle to make the control-flow and navigation behavior correct. The official docs explicitly say: “Make sure to always use await or return on result of navigateTo when calling it.” [1] What to do in <script setup> 1) Use await inside an async function or top-level await in <script setup>: await navigateTo('/search') [1] 2) If your function returns the navigation result, return it: return navigateTo('/search') (Using await or return ensures you don’t ignore the navigation result.) [1] Why the rule matters (route middleware example) Nuxt’s docs also show that inside route middleware you must return navigateTo’s result; otherwise the redirect won’t integrate correctly with the middleware flow: return navigateTo('/search', { redirectCode: 301 }) // ✅ [1] navigateTo('/search', { redirectCode: 301 }); return // ❌ not returned [1] So the underlying principle is: don’t fire navigateTo and drop its return value; use await or return so Nuxt/your code can correctly observe/propagate the navigation result. [1] Notes on return type The docs describe navigateTo’s signature returning a Promise (among other union members), which is consistent with why awaiting/returning is recommended. [1]
Citations:
🌐 Web query:
Nuxt 4 navigateTo return vs await guidance in pages redirect
💡 Result:
In Nuxt 4, use either await or return from navigateTo, but the best guidance depends on where you call it. 1) In route middleware (defineNuxtRouteMiddleware): you must return navigateTo - Nuxt’s docs explicitly say that when using navigateTo within route middleware, you must return its result so the middleware execution flow works correctly [1]. - The middleware docs also list return values such as “return navigateTo('/')” for redirects, and “return abortNavigation” for cancellation [2]. Example (correct): export default defineNuxtRouteMiddleware((to, from) => { if (to.path!== '/') { return navigateTo('/', { redirectCode: 301 }) } }) 2) In pages/components (e.g., a composable, watch, onMounted, etc.): prefer await (or explicitly return) so you stop further logic - Nuxt’s navigateTo API docs state: “Make sure to always use await or return on result of navigateTo when calling it.” [1] - Practically, in a page you often write “await navigateTo(...)” to ensure execution order (and avoid running code meant for the current page). Nuxt also shows auth/recipe examples that do “await navigateTo('/')” after side effects like refreshing session [3]. Example (typical in pages): await navigateTo('/login') Important nuance - If you call navigateTo and do NOT return/await it (e.g., in middleware), Nuxt warns it can lead to unexpected behavior (because the redirect isn’t properly wired into the middleware flow) [1][2]. - If you redirect on the client side during initial load (client-side middleware), you may see hydration issues; Nuxt community guidance suggests redirecting on the server or after hydration for those cases [4]. So, what should you do for “Nuxt 4 navigateTo return vs await guidance in pages redirect”? - In middleware: return navigateTo(...), not just call it. - In pages (scripts/watches/effects): await navigateTo(...) (or return it from the surrounding async function) and then exit/guard further logic.
Citations:
- 1: https://nuxt.com/docs/4.x/api/utils/navigate-to
- 2: https://nuxt.com/docs/4.x/directory-structure/app/middleware
- 3: https://nuxt.com/docs/4.x/guide/recipes/sessions-and-authentication
- 4: navigateTo causes hydration errors from route middleware (client only) nuxt/nuxt#15136
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- pages/signin.vue (lines 1-80) ----"
if [ -f "pages/signin.vue" ]; then
awk 'NR>=1 && NR<=80 {printf "%5d\t%s\n", NR, $0}' pages/signin.vue | sed -n '1,80p'
else
echo "pages/signin.vue not found"
fi
echo "---- pages/signin.vue (lines 8-20) ----"
if [ -f "pages/signin.vue" ]; then
awk 'NR>=8 && NR<=20 {printf "%5d\t%s\n", NR, $0}' pages/signin.vue
fiRepository: TaskarCenterAtUW/workspaces-frontend
Length of output: 1591
Await navigateTo for immediate redirect in <script setup>
In pages/signin.vue (lines 12-14), calling navigateTo('/dashboard') without await/return ignores navigateTo’s returned promise; Nuxt recommends always awaiting/returning navigateTo results.
if (tdeiAuth.complete) {
await navigateTo('/dashboard')
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pages/signin.vue` around lines 12 - 14, The redirect currently calls
navigateTo('/dashboard') without awaiting its promise; update the branch that
checks tdeiAuth.complete to await (or return) the navigateTo call so Nuxt
handles the navigation immediately—e.g., inside the script setup where
tdeiAuth.complete is checked, replace navigateTo('/dashboard') with await
navigateTo('/dashboard') (or return await navigateTo('/dashboard')) so the
promise is not ignored.
There was a problem hiding this comment.
@shweta2101 Initial observation: if we change the logo here, we'll need to update it in the AVIV ScoutRoute apps as well. Those both display the old Workspaces logo.
Looks nice!
DevBoard Task
https://dev.azure.com/TDEI-UW/TDEI/_workitems/edit/3634/
This PR applies TDEI brand theming across the application — updating colours, typography, layout, and accessibility throughout.
Visual changes
#32006e), new responsive mobile side-menu with slide-in animation, user profile dropdown, and sticky positioningtdei-primary-button,tdei-secondary-button,tdei-primary-link,tdei-rounded-button)Screenshots:
Theming & Core Styling
Updated TDEI brand theming throughout the application by changing the primary color from
#9b0092to#32006eintheme.scss. Added comprehensive CSS custom properties for font families (Open Sans, Montserrat) and color variables inmain.scss. Extended global styling with a new "TDEI Utility Classes" section introducing standardized button/link classes (tdei-primary-button,tdei-secondary-button,tdei-rounded-button,tdei-primary-link,tdei-secondary-link), header/title/subtitle typography helpers, and form-related utilities. Added browser autofill styling with smooth transitions and updated dropdown menu active states.Font Management & Assets
Integrated
@nuxtjs/google-fontsmodule vianuxt.config.tsto serve Open Sans (weights 300–800) and Montserrat (weights 100, 300–900) withswapdisplay strategy. UpdatedAppLogo.vueto use the new TDEI logo (tdei-logo.png) instead of the generic icon SVG.Navigation & Header
Completely rebuilt
AppNavbar.vuewith a custom responsive design replacing Bootstrap-style collapsed layout. Added sticky positioning with updated spacing and colors. Implemented interactive mobile side menu with slide-in animation, backdrop, and keyboard management (Escape to close, Tab focus trapping). Added focus management on mount/window resize and mobile view detection. Updated logout behavior to close the mobile menu before navigating. Desktop and mobile typography and menu item states were adjusted.Authentication
Redesigned
SigninForm.vuewith TDEI-branded auth card styling. Implemented toggleable password field with visibility icon button. Refined error handling with stricter fetch-error type guard (isFetchError) that validatesresponse.statusandresponse.json()function availability before interpreting HTTP statuses (400/401/404 show user-facing messages; others fall back to error message). Updated form/card markup with new styling classes and responsive mobile layout with adjusted padding/max-width for small screens.Updated
pages/signin.vueby simplifying the layout to use asignin-shellwrapper containing only the sign-in form, maintaining redirect behavior to/dashboardwhen authentication is complete.Homepage & Landing Page
Updated
pages/index.vuewith a new hero section layout replacing the previous centeredjumbo-content. Introducedhero-shell,hero-copy,hero-divider, andhero-actionsstructure. Made the primary CTA conditionally route to/dashboard(when authenticated) or/signin(when not), with label changing accordingly. Exposedauthstate fromtdeiClientto drive conditional routing. Extensively revised SCSS styling across responsive breakpoints (lg, md, sm) for padding, typography, action layout, and button sizing.Dashboard & Workspace Management
Converted dashboard to split-panel layout in
pages/dashboard.vuewith scrollable left panel for workspace list and sticky right detail panel. Introducedworkspace-split-layoutstructure and new responsive/sticky positioning styles. AddedselectedProjectGroupNamecomputed property from project groups data. Updated workspace list to display scrollable cards with new scrollbar styling.Redesigned
WorkspaceItem.vuecards to display workspace type icons (derived from workspace type), separate type/status labels, and title with inline ID. Replaced badge-based markup with structured card layout using.workspaceCard*CSS classes and introducedtypeIconSrcandformatTypeLabelhelper functions.Updated
components/dashboard/Toolbar.vueto usetdei-toolbar-primary-buttonfor dropdown/action buttons andtdei-secondary-buttonfor export/settings buttons, maintaining existing routing and aria-disabled behavior.Button & Link Standardization
Systematically replaced Bootstrap button variants (
btn-primary,btn-success,btn-danger, etc.) with new TDEI utility classes across the entire application:tdei-primary-buttonin toolbar, settings panels (General, Imagery, Apps), review tools, workspace creation flows, export pages, help page, and team dialogstdei-secondary-buttonin review toolbar, dashboard toolbar, help page, join dialogs, and team-related componentstdei-secondary-linkfor download/QR code linkstdei-primary-linkfor team member linksmaroon-bgclass for delete confirmations in settings and team removal dialogstdei-primary-linkwithbg-transparentfor cancel buttons in various dialogsAffected components:
DatasetTypeRadio.vue,review/FilterDropdown.vue,review/Toolbar.vue,settings/panel/(General, Imagery, Apps, Delete),settings/teams/JoinDialog.vue,teams/Item.vue,teams/JoinDialog.vue,teams/MembersDialog.vue,teams/SettingsDialog.vue,pages/help.vue,pages/workspace/[id]/export/(index and tdei),pages/workspace/[id]/settings/teams/index.vue,pages/workspace/[id]/teams/[teamId].vue,pages/workspace/create/(blank, file, index, tdei).Accessibility & Keyboard Navigation
Enhanced focus management in navbar with modal-like keyboard trapping in mobile side menu. Added global keydown handler for Escape and Tab key support. Applied focus-visible outline overrides for buttons/links with new TDEI utility classes. Updated form styling with custom focus states for inputs and buttons.
Minor Updates
Reformatted
AppSpinner.vuetemplate markup with multi-line attribute structure (no logic changes). Updatedlayouts/default.vueto use taller/throttlednuxt-loading-indicatorwith gradient color via CSS variables. Added TypeScript type annotation toselectWorkspace(workspace: Workspace)in dashboard page.