Skip to content
Open
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
3 changes: 1 addition & 2 deletions VueApp/src/CMS/pages/ManageLinkCollections.vue
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,6 @@ loadCollections()

.link-url {
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
}

Expand Down Expand Up @@ -854,7 +853,7 @@ loadCollections()
}

.link-field {
word-break: break-word;
overflow-wrap: break-word;
min-width: 0;
}

Expand Down
14 changes: 9 additions & 5 deletions VueApp/src/CTS/components/LevelSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -160,23 +160,27 @@ div.levelSelection button.selectedLevel {
}
div.levelSelection button.selectedLevel.selectedLevel--1 {
background-color: rgba(62, 127, 238, 0.3);
background-color: rgb(62 127 238 / 30%);
color: #212529;
}
div.levelSelection button.selectedLevel.selectedLevel--2 {
background-color: rgba(62, 127, 238, 0.7);
background-color: rgb(62 127 238 / 70%);
color: #212529;
}
div.levelSelection button.selectedLevel.selectedLevel--3 {
background-color: rgba(62, 127, 238, 1);
background-color: rgb(62 127 238 / 100%);
color: #000;
}
div.levelSelection button.selectedLevel.selectedLevel--4 {
background-color: rgba(0, 44, 175, 0.8);
background-color: rgb(0 44 175 / 80%);
color: #fff;
}
div.levelSelection button.selectedLevel.selectedLevel--5 {
background-color: rgba(11, 3, 139, 1);
background-color: rgb(11 3 139 / 100%);
color: #fff;
}
Expand Down
8 changes: 4 additions & 4 deletions VueApp/src/CTS/pages/CompetenciesBundleReport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -381,21 +381,21 @@ onMounted(() => {
.col-competency-name {
display: block;
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
line-height: 1.4;
max-width: 100%;
}

/* Apply wrapping to competency name column cells */
.competency-name-column {
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
}

/* Ensure name column respects its width and allows wrapping */
:deep(.q-table td:nth-child(2)) {
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
vertical-align: top;
max-width: 0; /* This forces the cell to respect table-layout: fixed */
}
Expand All @@ -404,7 +404,7 @@ onMounted(() => {
:deep(.q-table td:nth-child(3)),
:deep(.q-table td:nth-child(4)) {
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
}

/* Bundle chips wrapper */
Expand Down
2 changes: 1 addition & 1 deletion VueApp/src/ClinicalScheduler/components/ScheduleView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ defineExpose({
}
/* Mobile single column: q-gutter-md adds only a left margin, which left-pins the card */
@media (max-width: 599.98px) {
@media (width <= 599.98px) {
.schedule-week-grid,
.schedule-week-grid > * {
margin-left: 0;
Expand Down
4 changes: 2 additions & 2 deletions VueApp/src/ClinicalScheduler/components/WeekCell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -437,10 +437,10 @@ const cardClasses = computed(() => {
background-color: var(--ucdavis-gold-20);
border-radius: 4px;
padding: 2px 4px;
animation: fadeToBackground var(--highlight-duration) ease-out forwards; /* Duration from ANIMATIONS.HIGHLIGHT_DURATION_MS */
animation: fade-to-background var(--highlight-duration) ease-out forwards; /* Duration from ANIMATIONS.HIGHLIGHT_DURATION_MS */
}

@keyframes fadeToBackground {
@keyframes fade-to-background {
0% {
background-color: var(--ucdavis-gold-30);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ watch(
max-height: 55vh;

/* Height comes from the measured content (script); animate size changes between weeks */
/* stylelint-disable-next-line declaration-property-value-no-unknown -- Vue SFC v-bind() is resolved at compile time */
height: v-bind("bodyHeight");
transition: height 0.24s cubic-bezier(0.22, 1, 0.36, 1);
will-change: height;
Expand Down
2 changes: 1 addition & 1 deletion VueApp/src/Effort/pages/AuditList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,6 @@ onMounted(() => initPage())
<style scoped>
.changes-cell {
white-space: normal !important;
word-break: break-word;
overflow-wrap: break-word;
}
</style>
22 changes: 20 additions & 2 deletions VueApp/src/Effort/pages/StaffDashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1258,7 +1258,17 @@ watch(
.dept-row--clickable:hover,
.dept-row--clickable:focus {
background-color: #e0e0e0;
outline: none;
}

/* A background tint alone is too weak a focus indicator for keyboard users
(WCAG 2.4.7), so carry the system focus ring. The transparent outline is
invisible normally but becomes the indicator under Windows forced-colors,
where box-shadow is dropped. */
.dept-row--clickable:focus-visible {
outline: 2px solid transparent;
box-shadow:
0 0 0 0.1rem white,
0 0 0 0.25rem var(--focus-ring-color);
Comment thread
rlorenzo marked this conversation as resolved.
}

.dept-row--no-dept {
Expand Down Expand Up @@ -1318,7 +1328,15 @@ watch(
.clickable-badge:hover,
.clickable-badge:focus {
filter: brightness(0.95);
outline: none;
}

/* A 5% brightness shift is not a visible focus indicator (WCAG 2.4.7), so carry
the system focus ring. See the note on .dept-row--clickable above. */
.clickable-badge:focus-visible {
outline: 2px solid transparent;
box-shadow:
0 0 0 0.1rem white,
0 0 0 0.25rem var(--focus-ring-color);
}

#no-instructors-alerts {
Expand Down
22 changes: 0 additions & 22 deletions VueApp/src/assets/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,13 @@
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;

--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;

--vt-c-indigo: #2c3e50;

--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);

--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}

/* semantic color variables for this project */
Expand All @@ -36,20 +28,6 @@
--section-gap: 160px;
}

@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);

--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);

--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}

*,
*::before,
*::after {
Expand Down
11 changes: 9 additions & 2 deletions VueApp/src/components/SortableList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,8 @@ function onMoveDown(index: number) {
}

/* "Just moved" cue: a brand-blue tint and ring that fade out. It is a colour/shadow
fade (no movement), so it also plays under reduced motion — where the slide is
skipped — keeping a clear signal that something changed. */
fade with no movement, but reduced motion still turns it off (see the media query
below); the row's new position remains the signal that something changed. */
@keyframes sortable-row-flash {
0% {
background-color: var(--ucdavis-blue-10);
Expand All @@ -315,6 +315,13 @@ function onMoveDown(index: number) {
animation: sortable-row-flash 1s ease-out;
}

/* Must follow the rule above: equal specificity, so the later declaration wins. */
@media screen and (prefers-reduced-motion: reduce) {
.sortable-row--moved {
animation: none;
}
}
Comment thread
rlorenzo marked this conversation as resolved.

/* On phones the row becomes a stacked card: handle + controls share a top bar,
the body drops to its own full-width line below. */
@media (width <= 599px) {
Expand Down
6 changes: 4 additions & 2 deletions scripts/lib/lint-staged-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,10 @@ function parseArguments() {
function parseJsonOutput(stdout, stderr, toolName, textFallbackParser) {
const logger = createLogger(toolName)

// Parse JSON output (should be in stdout with --format json)
const jsonOutput = stdout.trim()
// ESLint writes its JSON report to stdout, but Stylelint writes to stderr, so
// fall back to stderr when stdout is empty. Without this the report is dropped
// and the tool silently reports no issues.
const jsonOutput = stdout.trim() || stderr.trim()

try {
if (jsonOutput) {
Expand Down
94 changes: 60 additions & 34 deletions scripts/lint-staged-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,48 +90,74 @@ function parseTextOutput(output) {
return issues
}

try {
// Run Stylelint using shared command runner
const stylelintArgs = [...(fixFlag ? ["--fix"] : []), "--formatter", "json", "--allow-empty-input", ...files]
/**
* Block the commit, dumping whatever Stylelint emitted so the failure is diagnosable
* @param {string} reason - What went wrong
* @param {string} blockedMessage - Short summary for the COMMIT BLOCKED line
* @param {string} stdout - Stylelint stdout
* @param {string} stderr - Stylelint stderr, deprecation warnings already filtered
* @returns {never}
*/
function blockCommit(reason, blockedMessage, stdout, stderr) {
logger.error(reason)
if (stdout) {
logger.error(stdout)
}
if (stderr) {
logger.error(stderr)
}
logger.error(`🛑 COMMIT BLOCKED - ${blockedMessage}`)
process.exit(1)
}

// Stylelint receives each path as an argument and Windows caps a command line at
// ~8191 chars, so a whole-tree run (200+ files) has to be split into batches.
const MAX_BATCH_SIZE = 50

try {
logger.info(`Running Stylelint accessibility and style checks on ${files.length} CSS/Vue files...`)
const stylelintResult = runCommand("stylelint", stylelintArgs, "Stylelint", projectRoot)

// Check for fatal errors
if (stylelintResult.status !== 0 && stylelintResult.status !== 2) {
logger.error("Stylelint command failed:")
if (stylelintResult.stdout) {
logger.error(stylelintResult.stdout)
}
if (stylelintResult.stderr) {
logger.error(stylelintResult.stderr)
const issues = []

for (let index = 0; index < files.length; index += MAX_BATCH_SIZE) {
const batch = files.slice(index, index + MAX_BATCH_SIZE)
const stylelintArgs = [...(fixFlag ? ["--fix"] : []), "--formatter", "json", "--allow-empty-input", ...batch]
Comment thread
rlorenzo marked this conversation as resolved.

const stylelintResult = runCommand("stylelint", stylelintArgs, "Stylelint", projectRoot)

// Filter out deprecation warnings before parsing: stylelint writes its JSON
// report to stderr, so a leading DeprecationWarning line would make the whole
// report unparseable.
const cleanStderr = stylelintResult.stderr
? stylelintResult.stderr
.split("\n")
.filter((line) => !line.includes("DeprecationWarning"))
.join("\n")
.trim()
: ""

// Check for fatal errors
if (stylelintResult.status !== 0 && stylelintResult.status !== 2) {
blockCommit("Stylelint command failed:", "Stylelint execution failed", stylelintResult.stdout, cleanStderr)
}
logger.error("🛑 COMMIT BLOCKED - Stylelint execution failed")
process.exit(1)
}

// Status 2 means "violations found" - only warn if no violations were parsed
if (stylelintResult.status === 2) {
const jsonToCheck = stylelintResult.stdout.trim() || stylelintResult.stderr.trim()
const hasValidJson = jsonToCheck && jsonToCheck.startsWith("[")
if (!hasValidJson) {
logger.warning("STYLELINT CONFIGURATION WARNING: Status 2 with no parseable violations")
logger.warning("📋 Consider reviewing stylelint.config.mjs if unexpected behavior occurs")
// Parse and accumulate this batch's Stylelint output
const batchIssues = parseStylelintOutput(stylelintResult.stdout, cleanStderr)
issues.push(...batchIssues)

// Status 2 means "violations found", so an empty batch means the report was lost in
// parsing. Fail closed: passing silently here is the blind-stylelint bug this script
// exists to prevent.
if (stylelintResult.status === 2 && batchIssues.length === 0) {
blockCommit(
"Stylelint reported violations but none could be parsed:",
"Stylelint output could not be read",
stylelintResult.stdout,
cleanStderr,
)
}
}

// Filter out deprecation warnings
const cleanStderr = stylelintResult.stderr
? stylelintResult.stderr
.split("\n")
.filter((line) => !line.includes("DeprecationWarning"))
.join("\n")
.trim()
: ""

// Parse and categorize Stylelint output
const issues = parseStylelintOutput(stylelintResult.stdout, cleanStderr)

// For CSS, we need special handling of accessibility categories
const criticalAccessibilityIssues = []
const accessibilityWarnings = []
Expand Down
9 changes: 8 additions & 1 deletion stylelint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
// oxlint-disable-next-line import/no-default-export, import/no-anonymous-default-export -- Stylelint config requires default export
export default {
extends: ["stylelint-config-standard", "@double-great/stylelint-a11y/strict"],
// The a11y plugin's own "recommended" tier is what we enforce. Its "strict" tier
// turns on nine further rules which between them produced 132 findings here and
// no real defects: dark-theme demands (we ship a single light theme whose
// contrast pairings are verified against WCAG AA, see DESIGN.md), baseline-grid
// line heights, and display:none inside print and responsive blocks. Every
// genuine WCAG failure found so far came from a recommended rule. Revisit strict,
// media-prefers-color-scheme in particular, if we implement dark mode.
extends: ["stylelint-config-standard", "@double-great/stylelint-a11y/recommended"],
customSyntax: "postcss-html",
ignoreFiles: [
"**/bin/**", // .NET build output directories
Expand Down
Loading