|
54 | 54 | :aria-expanded="opened ? 'true' : 'false'" |
55 | 55 | @click="onTriggerClick('currentApp')"> |
56 | 56 | <template #icon> |
57 | | - <!-- Settings sub-sections share one generic cog. An inline MDI icon |
58 | | - inherits the button's currentColor (--color-background-plain-text), |
| 57 | + <!-- Sections of the settings app share one generic cog. An inline MDI |
| 58 | + icon inherits the button's currentColor (--color-background-plain-text), |
59 | 59 | so it stays legible on both bright and dark headers without a filter. --> |
60 | 60 | <IconCog |
61 | | - v-if="currentApp.type === 'settings'" |
| 61 | + v-if="isSettingsSection" |
62 | 62 | class="app-menu__current-app-cog" |
63 | 63 | :size="20" /> |
64 | | - <img |
65 | | - v-else |
66 | | - class="app-menu__current-app-icon" |
67 | | - :src="currentApp.icon" |
68 | | - alt="" |
69 | | - aria-hidden="true"> |
| 64 | + <!-- Outer element carries the header fade, inner one the icon shape. --> |
| 65 | + <span v-else class="app-menu__current-app-icon"> |
| 66 | + <span |
| 67 | + class="app-menu__current-app-glyph" |
| 68 | + :style="currentAppIconStyle" /> |
| 69 | + </span> |
70 | 70 | </template> |
71 | 71 | <span class="app-menu__current-app-name"> |
72 | 72 | {{ displayName }} |
@@ -94,6 +94,8 @@ import logger from '../logger.js' |
94 | 94 | // Settings IDs that represent actions, not navigable pages. |
95 | 95 | const SETTINGS_ACTION_IDS = new Set(['logout']) |
96 | 96 |
|
| 97 | +const SETTINGS_SECTION_IDS = new Set(['settings_personal', 'settings_administration', 'accessibility_settings']) |
| 98 | +
|
97 | 99 | export default defineComponent({ |
98 | 100 | name: 'AppMenu', |
99 | 101 |
|
@@ -169,18 +171,28 @@ export default defineComponent({ |
169 | 171 | ?? Object.values(this.settingsList).find((entry) => entry.active && !SETTINGS_ACTION_IDS.has(entry.id)) |
170 | 172 | }, |
171 | 173 |
|
172 | | - // Trigger label. Settings sub-section names ("Personal info", |
173 | | - // "Appearance and accessibility", ...) are too long and varied to |
174 | | - // surface in the header; collapse them all to a single "Settings". |
| 174 | + isSettingsSection(): boolean { |
| 175 | + return this.currentApp !== undefined && SETTINGS_SECTION_IDS.has(this.currentApp.id) |
| 176 | + }, |
| 177 | +
|
175 | 178 | displayName(): string { |
176 | 179 | if (!this.currentApp) { |
177 | 180 | return '' |
178 | 181 | } |
179 | | - return this.currentApp.type === 'settings' |
| 182 | + return this.isSettingsSection |
180 | 183 | ? t('core', 'Settings') |
181 | 184 | : this.currentApp.name |
182 | 185 | }, |
183 | 186 |
|
| 187 | + // The icon is painted through a mask, so entries with a dark icon |
| 188 | + // (the app management page ships one for the settings list) are legible |
| 189 | + // on the header as well. Escaped so a crafted path cannot break out of |
| 190 | + // the url() token, same as AppIcon.vue. |
| 191 | + currentAppIconStyle(): Record<string, string> { |
| 192 | + const icon = this.currentApp?.icon ?? '' |
| 193 | + return { '--app-icon-url': `url("${icon.replace(/["\\]/g, '\\$&')}")` } |
| 194 | + }, |
| 195 | +
|
184 | 196 | // aria-label overrides the inner span text, so the displayed name |
185 | 197 | // has to be duplicated here for screen readers. |
186 | 198 | currentAppLabel(): string { |
@@ -459,13 +471,29 @@ export default defineComponent({ |
459 | 471 | } |
460 | 472 |
|
461 | 473 | &__current-app-icon { |
| 474 | + display: flex; |
462 | 475 | width: calc(var(--default-grid-baseline) * 5); |
463 | 476 | height: calc(var(--default-grid-baseline) * 5); |
464 | | - // Theme-aware inversion + vertical alpha fade via --header-menu-icon-mask. |
465 | | - filter: var(--background-image-invert-if-bright); |
| 477 | + // Vertical alpha fade, like the cog and the other header icons. |
466 | 478 | mask: var(--header-menu-icon-mask); |
467 | 479 | } |
468 | 480 |
|
| 481 | + &__current-app-glyph { |
| 482 | + width: 100%; |
| 483 | + height: 100%; |
| 484 | + // Masked rather than shown: app icons ship a hardcoded fill, so the |
| 485 | + // color has to come from the background. Matches AppIcon.vue. |
| 486 | + background-color: var(--color-background-plain-text); |
| 487 | + mask: var(--app-icon-url) center / contain no-repeat; |
| 488 | + } |
| 489 | +
|
| 490 | + // Masked backgrounds are not force-adjusted the way <img> is. |
| 491 | + @media (forced-colors: active) { |
| 492 | + &__current-app-glyph { |
| 493 | + background-color: CanvasText; |
| 494 | + } |
| 495 | + } |
| 496 | +
|
469 | 497 | &__current-app-cog { |
470 | 498 | mask: var(--header-menu-icon-mask); |
471 | 499 | } |
|
0 commit comments