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
27 changes: 21 additions & 6 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<ion-app [class.dark-theme]="dark">
<ion-app
[class.dark-theme]="theme === 'dark'"
[class.high-contrast-theme]="theme === 'high-contrast-light' || theme === 'high-contrast-dark'"
[class.high-contrast-light-theme]="theme === 'high-contrast-light'"
[class.high-contrast-dark-theme]="theme === 'high-contrast-dark'">
<a href="#main-content" class="skip-link">Skip to main content</a>
<ion-split-pane contentId="main-content">

<ion-menu contentId="main-content">
Expand Down Expand Up @@ -189,17 +194,27 @@ <h3>Install Latest Update</h3>
</ion-item>
</ion-menu-toggle>

<ion-item>
<ion-icon slot="start" name="moon-outline"></ion-icon>
<ion-label>Dark Mode</ion-label>
<ion-toggle [(ngModel)]="dark" (ngModelChange)="toggleDarkTheme()"></ion-toggle>
<ion-item lines="none" class="theme-picker-item">
<ion-icon slot="start" name="contrast-outline" aria-hidden="true"></ion-icon>
<ion-select
label="Theme"
[value]="theme"
(ionChange)="setTheme($any($event.detail.value))"
interface="action-sheet"
[interfaceOptions]="{ header: 'Choose theme' }"
aria-label="App theme">
<ion-select-option value="light">Light</ion-select-option>
<ion-select-option value="dark">Dark</ion-select-option>
<ion-select-option value="high-contrast-light">High Contrast (Light)</ion-select-option>
<ion-select-option value="high-contrast-dark">High Contrast (Dark)</ion-select-option>
</ion-select>
</ion-item>
</ion-list>

</ion-content>
</ion-menu>

<ion-router-outlet id="main-content"></ion-router-outlet>
<ion-router-outlet id="main-content" tabindex="-1"></ion-router-outlet>

</ion-split-pane>

Expand Down
35 changes: 35 additions & 0 deletions src/app/app.component.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
/*
* Skip-to-content link for keyboard and screen-reader users. Hidden off-canvas
* until focused, then anchors to top-left as the first interactive control on
* every page. Targets the router outlet's id so focus actually transfers.
*/
.skip-link {
position: absolute;
top: -100px;
left: 0;
z-index: 10000;
padding: 12px 20px;
background: var(--ion-color-primary);
color: var(--ion-color-primary-contrast);
font-weight: 600;
font-size: 0.9rem;
text-decoration: none;
border-radius: 0 0 8px 0;
transition: top 180ms ease-out;
}

.skip-link:focus,
.skip-link:focus-visible {
top: 0;
outline: 3px solid var(--ion-color-primary);
outline-offset: 2px;
}

ion-menu ion-content {
--padding-top: 20px;
--padding-bottom: 20px;
Expand Down Expand Up @@ -90,6 +117,14 @@ ion-item.indent {
padding-left: 1.25em;
}

/*
* Theme picker — ion-select popover keeps the four theme options compact
* in the side menu and leaves room for descriptive labels.
*/
ion-menu ion-item.theme-picker-item ion-select {
font-weight: 500;
}

ion-accordion div[slot="content"] ion-item {
--padding-start: 40px;
}
Expand Down
13 changes: 7 additions & 6 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { PushNotifications, PushNotificationSchema } from '@capacitor/push-notif

import { Storage } from '@ionic/storage-angular';

import { UserData } from './providers/user-data';
import { UserData, ThemeMode } from './providers/user-data';
import { ConferenceData } from './providers/conference-data';
import { LiveUpdateService } from './providers/live-update.service';
import { environment } from '../environments/environment';
Expand Down Expand Up @@ -61,7 +61,7 @@ export class AppComponent implements OnInit {
]
nickname = null;
loggedIn = false;
dark = false;
theme: ThemeMode = 'light';

updateAvailable: any = null;

Expand Down Expand Up @@ -207,13 +207,14 @@ export class AppComponent implements OnInit {
}

loadTheme() {
this.userData.getDarkTheme().then(dark => {
this.dark = dark;
this.userData.getTheme().then(theme => {
this.theme = theme;
});
}

toggleDarkTheme() {
this.userData.toggleDarkTheme();
setTheme(theme: ThemeMode) {
this.theme = theme;
this.userData.setTheme(theme);
}

openUrl(url: string) {
Expand Down
5 changes: 5 additions & 0 deletions src/app/pages/about-pycon/about-pycon.page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ ion-title {
--background: #1e1e1e;
}

:host-context(.high-contrast-theme) .about-card {
--background: var(--ion-card-background);
border: 1px solid var(--ion-border-color);
}

.dev-accordion {
margin: 0 16px 16px;
}
Expand Down
16 changes: 16 additions & 0 deletions src/app/pages/room-detail/room-detail.page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ ion-title {
box-shadow: 0 2px 6px rgba(221, 4, 210, 0.30);
}

:host-context(.high-contrast-theme) .day-section .day-header {
--pycon-accent: var(--ion-color-primary);
background: var(--ion-card-background);
color: var(--ion-color-primary);
border: 1px solid var(--ion-border-color);
box-shadow: none;
}

.session-item {
--padding-start: 16px;
--padding-end: 8px;
Expand Down Expand Up @@ -153,3 +161,11 @@ ion-title {
--background: rgba(221, 4, 210, 0.10);
box-shadow: inset 3px 0 0 #DD04D2;
}

:host-context(.high-contrast-theme) .session-item.session-item-highlight {
--background: var(--ion-item-background);
box-shadow: inset 4px 0 0 var(--ion-color-primary);
outline: 2px solid var(--ion-color-primary);
outline-offset: -2px;
animation: none;
}
24 changes: 3 additions & 21 deletions src/app/pages/schedule-list/schedule-list.page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -86,27 +86,9 @@ ion-title {
margin-right: 12px;
}

$tracks: (
talk: #5833E9,
tutorial: #DD04D2,
keynote: #680579,
plenary: #630675,
break: #3A3A3A,
lightning-talks: #C05CA0,
security: #F19C0B,
ai: #10B57F,
charla: #527CB2,
poster: #D47454,
sponsor\ presentation: #FFD779,
open\ space: #6FCF97,
);

@each $track, $color in $tracks {
ion-item[track='#{$track}'] ion-label {
border-left: 3px solid $color;
padding-left: 10px;
}
}
// Track membership is communicated by the .track-badge text inside each
// row (see src/global.scss). The side-stripe accent was removed for the
// same reasons noted in src/app/pages/schedule/schedule.scss.

.session-list-item {
--padding-top: 10px;
Expand Down
2 changes: 1 addition & 1 deletion src/app/pages/schedule/schedule.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ <h3 class="notice-title">
</a>

<ion-item-sliding *ngIf="session.track !== 'Informational'" #slidingItem [attr.track]="session.track | lowercase" [hidden]="session.hide">
<ion-item [color]="session?.color? session.color : ''" [routerLink]="session.listRender? '/app/tabs/tracks/' + session.section: '/app/tabs/schedule/session/'+session.id">
<ion-item [routerLink]="session.listRender? '/app/tabs/tracks/' + session.section: '/app/tabs/schedule/session/'+session.id">
<ion-label>
<h3>
<ion-icon *ngIf="session.favorite" slot="icon-only" name="star"></ion-icon>
Expand Down
35 changes: 14 additions & 21 deletions src/app/pages/schedule/schedule.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,12 @@ ion-fab-button {
--background-activated: var(--ion-color-step-250, #d9d9d9);
}

$tracks: (
talk: #5833E9,
tutorial: #DD04D2,
keynote: #680579,
plenary: #630675,
break: #3A3A3A,
lightning-talks: #C05CA0,
security: #F19C0B,
ai: #10B57F,
charla: #527CB2,
poster: #D47454,
sponsor\ presentation: #FFD779,
open\ space: #6FCF97,
);

@each $track, $color in $tracks {
ion-item-sliding[track='#{$track}'] ion-label {
border-left: 3px solid $color;
padding-left: 10px;
}
}
// Track membership is communicated by the .track-badge text inside each
// row (see src/global.scss). The previous side-stripe accent was removed
// because (a) it violated the side-stripe ban for >1px colored borders,
// (b) several of its colors fell below 3:1 against the cream HC-Light
// background, and (c) duplicating track info via a colored border on top
// of a colored badge was redundant.

:host {
--pycon-accent: #680579;
Expand All @@ -44,6 +29,14 @@ $tracks: (
--pycon-accent: #DD04D2;
}

:host-context(.high-contrast-light-theme) {
--pycon-accent: #4a0072;
}

:host-context(.high-contrast-dark-theme) {
--pycon-accent: #ffd60a;
}

.schedule-toolbar {
--padding-start: 0;
--padding-end: 0;
Expand Down
15 changes: 15 additions & 0 deletions src/app/pages/session-detail/session-detail.scss
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ ion-toolbar ion-button {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}

:host-context(.high-contrast-theme) .session-meta-card {
background: var(--ion-card-background);
border: 1px solid var(--ion-border-color);
box-shadow: none;
}

.meta-row {
display: flex;
align-items: center;
Expand Down Expand Up @@ -175,6 +181,15 @@ ion-toolbar ion-button {
color: #8b8fd4;
}

:host-context(.high-contrast-theme) .meta-row .room-link {
color: var(--ion-color-primary);
text-decoration-thickness: 2px;
}

:host-context(.high-contrast-theme) .meta-row ion-icon {
color: var(--ion-color-primary);
}

/*
* Body content below the card
*/
Expand Down
9 changes: 6 additions & 3 deletions src/app/pages/social-media/social-media.page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,12 @@ ion-icon.social-icon.bluesky {
color: #0085ff;
}

@media (prefers-color-scheme: dark) {
// PNG/multicolor logos that should NOT be inverted in dark mode
// (ion-icon SVGs and the colored Bluesky icon already adapt via currentColor)
// PNG/multicolor logos that should be inverted on dark surfaces. Driven off
// the in-app theme classes rather than `prefers-color-scheme` so HC Light over
// an OS-dark preference still shows un-inverted logos on the cream background.
// (ion-icon SVGs and the colored Bluesky icon already adapt via currentColor.)
:host-context(.dark-theme),
:host-context(.high-contrast-dark-theme) {
.social-icon.pypi {
filter: none;
}
Expand Down
47 changes: 37 additions & 10 deletions src/app/providers/user-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ export function isCustomScheduleFilter(excluded: ReadonlyArray<string>): boolean
return !excluded.every(track => defaults.has(track));
}

export type ThemeMode =
| 'light'
| 'dark'
| 'high-contrast-light'
| 'high-contrast-dark';
export const THEME_MODES: ThemeMode[] = [
'light',
'dark',
'high-contrast-light',
'high-contrast-dark',
];

@Injectable({
providedIn: 'root'
Expand Down Expand Up @@ -77,16 +88,32 @@ export class UserData {
})
}

// Get current theme from storage
getDarkTheme() {
return this.storage.get('darkTheme');
}

// Toggle Dark Theme. Sets inverted value to storage
toggleDarkTheme() {
this.getDarkTheme().then((darkTheme) => {
this.storage.set('darkTheme', !darkTheme);
});
// Resolve the active theme. Migrates legacy storage shapes:
// darkTheme: true -> 'dark' (pre-tri-state picker)
// theme: 'high-contrast' -> 'high-contrast-dark' (pre-HC-light split)
async getTheme(): Promise<ThemeMode> {
const stored = await this.storage.get('theme');
if (stored === 'high-contrast') {
await this.storage.set('theme', 'high-contrast-dark');
return 'high-contrast-dark';
}
if (stored && THEME_MODES.indexOf(stored) !== -1) {
return stored;
}
const legacyDark = await this.storage.get('darkTheme');
if (legacyDark === true) {
await this.storage.set('theme', 'dark');
await this.storage.remove('darkTheme');
return 'dark';
}
if (legacyDark === false) {
await this.storage.remove('darkTheme');
}
return 'light';
}

setTheme(theme: ThemeMode): Promise<any> {
return this.storage.set('theme', theme);
}

hasFavorite(sessionId: string): boolean {
Expand Down
Loading
Loading