diff --git a/src/app/pages/about-psf/about-psf.page.html b/src/app/pages/about-psf/about-psf.page.html index 305f160b..b9647dfc 100644 --- a/src/app/pages/about-psf/about-psf.page.html +++ b/src/app/pages/about-psf/about-psf.page.html @@ -2,7 +2,9 @@ - 1 + + + Python Software Foundation @@ -146,7 +148,7 @@

python.org

- + PyPI

PyPI

The Python Package Index

diff --git a/src/app/pages/about-psf/about-psf.page.scss b/src/app/pages/about-psf/about-psf.page.scss index ef25f069..fea071b0 100644 --- a/src/app/pages/about-psf/about-psf.page.scss +++ b/src/app/pages/about-psf/about-psf.page.scss @@ -57,3 +57,13 @@ ion-card { color: var(--ion-text-color); } } + +// Sized to match the Ionic icons it sits next to in the resource list. +.resource-icon { + width: 24px; + height: 24px; + margin-inline-end: 32px; + align-self: center; + margin-block: 12px; + object-fit: contain; +} diff --git a/src/app/pages/about-pycon/about-pycon.page.html b/src/app/pages/about-pycon/about-pycon.page.html index e06b9586..e9d9f5f3 100644 --- a/src/app/pages/about-pycon/about-pycon.page.html +++ b/src/app/pages/about-pycon/about-pycon.page.html @@ -2,7 +2,9 @@ - 1 + + + About PyCon US @@ -15,11 +17,6 @@

PyCon US 2026

Long Beach, CA • May 14-18

- - - Fetch latest update - - diff --git a/src/app/pages/about-pycon/about-pycon.page.ts b/src/app/pages/about-pycon/about-pycon.page.ts index af6a7063..c4a120ff 100644 --- a/src/app/pages/about-pycon/about-pycon.page.ts +++ b/src/app/pages/about-pycon/about-pycon.page.ts @@ -90,23 +90,6 @@ export class AboutPyconPage implements OnInit { this.liveUpdateService.checkForUpdate(); } - async performAutomaticUpdate() { - this.loadingCtrl.create({ - message: 'Installing the latest build...', - duration: 60000, - }).then((loader) => { - loader.present(); - if (this.liveUpdateService.needsUpdate) { - this.liveUpdateService.reload(); - setTimeout(() => {loader.dismiss()}, 1000); - } else { - setTimeout(() => {loader.dismiss()}, 1000); - } - }).catch((err) => { - console.log(err); - }); - } - ngOnInit() { this.liveUpdateService.checkForUpdate(); this.reloadContent(); diff --git a/src/app/pages/account/account.html b/src/app/pages/account/account.html index b8666293..69de28e4 100644 --- a/src/app/pages/account/account.html +++ b/src/app/pages/account/account.html @@ -2,7 +2,9 @@ - 1 + + + Account diff --git a/src/app/pages/coc/coc.page.html b/src/app/pages/coc/coc.page.html index 1effcb41..14dd7ec5 100644 --- a/src/app/pages/coc/coc.page.html +++ b/src/app/pages/coc/coc.page.html @@ -2,7 +2,9 @@ - 1 + + + Code of Conduct @@ -37,7 +39,7 @@

Code of Conduct

- +

CoC Hotline

(562) 662-4082

@@ -45,7 +47,7 @@

CoC Hotline

- +

Report via Email

pycon-us-report@python.org

@@ -53,7 +55,7 @@

Report via Email

- +

Reporting Procedures

How to report an incident

@@ -61,7 +63,7 @@

Reporting Procedures

- +

Help & Safety

Info desk, maps, and additional resources

diff --git a/src/app/pages/conference-map/conference-map.page.html b/src/app/pages/conference-map/conference-map.page.html index deefe254..5f4c6c44 100644 --- a/src/app/pages/conference-map/conference-map.page.html +++ b/src/app/pages/conference-map/conference-map.page.html @@ -2,7 +2,9 @@ - 1 + + + Conference Center diff --git a/src/app/pages/help/help.page.html b/src/app/pages/help/help.page.html index 1e2cd732..e160dd1f 100644 --- a/src/app/pages/help/help.page.html +++ b/src/app/pages/help/help.page.html @@ -21,7 +21,7 @@

Help & Safety

- +

Info Desk & Conference Map

Find the registration desk, help desk, and venue layout

@@ -35,7 +35,7 @@

Info Desk & Conference Map

- +

Read the Code of Conduct

PyCon US community guidelines

@@ -43,7 +43,7 @@

Read the Code of Conduct

- +

CoC Hotline

(562) 662-4082

@@ -51,7 +51,7 @@

CoC Hotline

- +

Report via Email

pycon-us-report@python.org

@@ -65,7 +65,7 @@

Report via Email

- +

Report an App Issue

Bug reports and feature requests for the mobile app

@@ -73,7 +73,7 @@

Report an App Issue

- +

Report a Website Issue

Issues with us.pycon.org

diff --git a/src/app/pages/job-listings/job-listings.page.html b/src/app/pages/job-listings/job-listings.page.html index 26f1fc6f..129a0508 100644 --- a/src/app/pages/job-listings/job-listings.page.html +++ b/src/app/pages/job-listings/job-listings.page.html @@ -2,7 +2,9 @@ - 1 + + + Job Listings diff --git a/src/app/pages/keynote-speakers/keynote-speakers.page.html b/src/app/pages/keynote-speakers/keynote-speakers.page.html index e25b09a4..6d12c3e7 100644 --- a/src/app/pages/keynote-speakers/keynote-speakers.page.html +++ b/src/app/pages/keynote-speakers/keynote-speakers.page.html @@ -2,7 +2,9 @@ - 1 + + + Keynote Speakers diff --git a/src/app/pages/lightning-talks/lightning-talks.page.html b/src/app/pages/lightning-talks/lightning-talks.page.html index 4b30c343..b36a23d9 100644 --- a/src/app/pages/lightning-talks/lightning-talks.page.html +++ b/src/app/pages/lightning-talks/lightning-talks.page.html @@ -2,7 +2,9 @@ - 1 + + + Lightning Talks diff --git a/src/app/pages/login/login.html b/src/app/pages/login/login.html index 6f335945..4101e845 100644 --- a/src/app/pages/login/login.html +++ b/src/app/pages/login/login.html @@ -2,7 +2,9 @@ - 1 + + + Login diff --git a/src/app/pages/schedule-list/schedule-list.page.html b/src/app/pages/schedule-list/schedule-list.page.html index a8611fd6..4cbfffbf 100644 --- a/src/app/pages/schedule-list/schedule-list.page.html +++ b/src/app/pages/schedule-list/schedule-list.page.html @@ -2,7 +2,9 @@ - 1 + + + {{trackName | trackName : 'plural'}} diff --git a/src/app/pages/schedule/schedule.html b/src/app/pages/schedule/schedule.html index d952200b..be016996 100644 --- a/src/app/pages/schedule/schedule.html +++ b/src/app/pages/schedule/schedule.html @@ -2,7 +2,9 @@ - 1 + + + diff --git a/src/app/pages/session-detail/session-detail.html b/src/app/pages/session-detail/session-detail.html index 14dbee14..4728be45 100644 --- a/src/app/pages/session-detail/session-detail.html +++ b/src/app/pages/session-detail/session-detail.html @@ -82,6 +82,23 @@

{{ keynoteAbstr [innerHtml]="session?.description" (click)="onDescriptionClick($event)"> + +
+

Posters ({{posters.length}})

+ + + + + +

{{poster.title}}

+

+ {{s.name}}, +

+
+
+
diff --git a/src/app/pages/session-detail/session-detail.scss b/src/app/pages/session-detail/session-detail.scss index b7f3ff2a..03e919f7 100644 --- a/src/app/pages/session-detail/session-detail.scss +++ b/src/app/pages/session-detail/session-detail.scss @@ -190,6 +190,41 @@ ion-toolbar ion-button { } } +.posters-list { + margin: 24px 0 0; + + .posters-list-heading { + font-size: 1.05rem; + font-weight: 700; + margin: 0 0 8px; + color: var(--ion-text-color); + } + + .poster-item { + --padding-start: 0; + --inner-padding-end: 0; + --background: transparent; + + ion-avatar { + width: 40px; + height: 40px; + margin-right: 12px; + } + + h3 { + font-weight: 600; + font-size: 0.95rem; + line-height: 1.35; + } + + p { + font-size: 0.8rem; + color: var(--ion-color-step-500, #808080); + margin-top: 2px; + } + } +} + .session-image-container { margin: 16px 0 20px; text-align: center; diff --git a/src/app/pages/session-detail/session-detail.ts b/src/app/pages/session-detail/session-detail.ts index 6a8f3b47..46696ecf 100644 --- a/src/app/pages/session-detail/session-detail.ts +++ b/src/app/pages/session-detail/session-detail.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnDestroy } from '@angular/core'; import { InAppBrowser, DefaultWebViewOptions } from '@capacitor/inappbrowser'; import { CapacitorCalendar } from '@ebarooni/capacitor-calendar'; import { ConferenceData } from '../../providers/conference-data'; @@ -6,6 +6,7 @@ import { ActivatedRoute } from '@angular/router'; import { UserData } from '../../providers/user-data'; import { LiveUpdateService } from '../../providers/live-update.service'; import { Location } from '@angular/common'; +import { Subscription } from 'rxjs'; import { environment } from '../../../environments/environment'; interface KeynoteAbstract { @@ -20,14 +21,17 @@ interface KeynoteAbstract { styleUrls: ['./session-detail.scss'], templateUrl: 'session-detail.html' }) -export class SessionDetailPage { +export class SessionDetailPage implements OnDestroy { session: any; isFavorite = false; isOpenSpace = false; isKeynote = false; + isPosters = false; + posters: any[] = []; keynoteData: any[] = []; keynoteAbstract: KeynoteAbstract | null = null; defaultHref = ''; + private paramSub?: Subscription; private keynoteAbstracts: KeynoteAbstract[] = [ { @@ -93,32 +97,56 @@ export class SessionDetailPage { ) { } ionViewWillEnter() { + // Subscribe to param changes so navigating between sessions on the same + // route (e.g. tapping an individual poster from the collapsed Posters list) + // re-renders with the new session instead of reusing the stale snapshot. + this.paramSub?.unsubscribe(); + this.paramSub = this.route.paramMap.subscribe((params) => { + const sessionId = params.get('sessionId'); + this.loadSession(sessionId); + }); + } + + ionViewWillLeave() { + this.paramSub?.unsubscribe(); + this.paramSub = undefined; + } + + ngOnDestroy() { + this.paramSub?.unsubscribe(); + } + + private loadSession(sessionId: string | null) { this.dataProvider.load().subscribe((data: any) => { - if (data.sessions) { - const sessionId = this.route.snapshot.paramMap.get('sessionId'); - const foundSession = data.sessions.find( - (s: any) => String(s.id) === String(sessionId) - ) - this.session = foundSession; - this.isOpenSpace = this.session?.tracks?.includes('open-space'); - this.isKeynote = this.session?.tracks?.includes('keynote') || this.session?.track === 'Keynote'; - - // Enrich keynote sessions with speaker photo/bio. Collect every - // matching speaker so co-hosted keynotes (e.g. "Rachell Calhoun & - // Tim Schilling") render all speakers, not just the first match. - if (this.isKeynote) { - const sessionName = (this.session?.name || '').toLowerCase(); - this.keynoteData = Object.entries(this.keynoteSpeakers) - .filter(([name]) => sessionName.includes(name.toLowerCase())) - .map(([name, data]) => ({ name, ...data })); - this.keynoteAbstract = this.keynoteAbstracts.find( - (a) => a.match.some((m) => sessionName.includes(m.toLowerCase())) - ) || null; - } + if (!data?.sessions) return; + const foundSession = data.sessions.find( + (s: any) => String(s.id) === String(sessionId) + ); + this.session = foundSession; + this.isOpenSpace = this.session?.tracks?.includes('open-space'); + this.isKeynote = this.session?.tracks?.includes('keynote') || this.session?.track === 'Keynote'; + // Only the *collapsed* "Posters" schedule slot lists every poster; + // individual poster session-detail pages show their own description. + this.isPosters = this.session?.track === 'Poster' && this.session?.name === 'Posters'; + this.posters = this.isPosters ? (data.posters || []) : []; + this.keynoteData = []; + this.keynoteAbstract = null; + + // Enrich keynote sessions with speaker photo/bio. Collect every + // matching speaker so co-hosted keynotes (e.g. "Rachell Calhoun & + // Tim Schilling") render all speakers, not just the first match. + if (this.isKeynote) { + const sessionName = (this.session?.name || '').toLowerCase(); + this.keynoteData = Object.entries(this.keynoteSpeakers) + .filter(([name]) => sessionName.includes(name.toLowerCase())) + .map(([name, data]) => ({ name, ...data })); + this.keynoteAbstract = this.keynoteAbstracts.find( + (a) => a.match.some((m) => sessionName.includes(m.toLowerCase())) + ) || null; + } - this.isFavorite = this.userProvider.hasFavorite( - String(this.session.id) - ); + if (this.session?.id != null) { + this.isFavorite = this.userProvider.hasFavorite(String(this.session.id)); } }); } diff --git a/src/app/pages/session-types/session-types.page.html b/src/app/pages/session-types/session-types.page.html index 650022d2..cab69be3 100644 --- a/src/app/pages/session-types/session-types.page.html +++ b/src/app/pages/session-types/session-types.page.html @@ -2,7 +2,9 @@ - 1 + + + Session Types diff --git a/src/app/pages/social-media/social-media.page.html b/src/app/pages/social-media/social-media.page.html index d82c1c7a..bbb81e9d 100644 --- a/src/app/pages/social-media/social-media.page.html +++ b/src/app/pages/social-media/social-media.page.html @@ -2,7 +2,9 @@ - 1 + + + Social Media @@ -33,23 +35,23 @@

Social Media

- +

Mastodon

@pycon@fosstodon.org

- - + +

Bluesky

@pycon.org

- - + +

X

@pycon

@@ -57,7 +59,7 @@

X

- +

YouTube

PyCon US

@@ -65,7 +67,7 @@

YouTube

- +

LinkedIn

Python Software Foundation

@@ -79,23 +81,23 @@

LinkedIn

- +

Mastodon

@ThePSF@fosstodon.org

- - + +

Bluesky

@python.org

- - + +

X

@ThePSF

@@ -109,7 +111,7 @@

X

- +

PyCon US Blog

Conference news and announcements

@@ -117,7 +119,7 @@

PyCon US Blog

- +

PSF Blog

News and updates from the Python Software Foundation

@@ -125,15 +127,15 @@

PSF Blog

- +

CPython Blog

Python insider — core development updates

- - + +

PyPI Blog

News from the Python Package Index

diff --git a/src/app/pages/social-media/social-media.page.scss b/src/app/pages/social-media/social-media.page.scss index 62c38272..9d1b45e9 100644 --- a/src/app/pages/social-media/social-media.page.scss +++ b/src/app/pages/social-media/social-media.page.scss @@ -60,23 +60,27 @@ ion-title { .social-icon { width: 24px; height: 24px; + font-size: 24px; margin-inline-end: 16px; - // Ionic's MD mode only applies top/bottom margins to ion-icon[slot="start"], - // not raw — without this the Bluesky/X/PyPI rows collapse shorter than - // the ion-icon-based rows on Android. align-self: center; margin-block: 12px; } -.social-icon.bluesky { - fill: #0085ff; +ion-icon.social-icon { + color: var(--ion-text-color, #000); +} + +ion-icon.social-icon.bluesky { + color: #0085ff; } @media (prefers-color-scheme: dark) { - .social-icon { - filter: invert(1); - } - .social-icon.bluesky { + // PNG/multicolor logos that should NOT be inverted in dark mode + // (ion-icon SVGs and the colored Bluesky icon already adapt via currentColor) + .social-icon.pypi { filter: none; } + img.social-icon:not(.pypi) { + filter: invert(1); + } } diff --git a/src/app/pages/speaker-list/speaker-list.html b/src/app/pages/speaker-list/speaker-list.html index d40ff626..9e6eef3e 100644 --- a/src/app/pages/speaker-list/speaker-list.html +++ b/src/app/pages/speaker-list/speaker-list.html @@ -2,7 +2,9 @@ - 1 + + + Speakers @@ -25,12 +27,12 @@

Speakers

- +

{{speaker.name}}

-

- {{speaker.sessions[0].track | trackName}} - {{speaker.sessions[0].name}} +

+ {{track | trackName}}

+

{{speaker.sessions[0].name}}

diff --git a/src/app/pages/sponsor-detail/sponsor-detail.scss b/src/app/pages/sponsor-detail/sponsor-detail.scss index 27e69ff5..4c11153b 100644 --- a/src/app/pages/sponsor-detail/sponsor-detail.scss +++ b/src/app/pages/sponsor-detail/sponsor-detail.scss @@ -83,6 +83,14 @@ ion-toolbar ion-back-button { line-height: 1.6; margin: 12px 0 8px; color: var(--ion-text-color); + overflow-wrap: anywhere; + word-break: break-word; + + // Long URLs and inline code in sponsor-supplied HTML + a, code { + overflow-wrap: anywhere; + word-break: break-word; + } } .jobs-card { diff --git a/src/app/pages/sprints/sprints.page.html b/src/app/pages/sprints/sprints.page.html index 76ec05b8..d3e25bcd 100644 --- a/src/app/pages/sprints/sprints.page.html +++ b/src/app/pages/sprints/sprints.page.html @@ -2,7 +2,9 @@ - 1 + + + Sprints diff --git a/src/app/pages/venues-hours/venues-hours.page.html b/src/app/pages/venues-hours/venues-hours.page.html index 5e552b1f..53bba505 100644 --- a/src/app/pages/venues-hours/venues-hours.page.html +++ b/src/app/pages/venues-hours/venues-hours.page.html @@ -2,7 +2,9 @@ - 1 + + + Venues & Hours @@ -23,7 +25,7 @@

Venues & Hours

- +

Get Directions

Open in Maps

@@ -31,7 +33,7 @@

Get Directions

- +

Conference Map

Indoor venue layout

diff --git a/src/app/pages/wifi/wifi.page.html b/src/app/pages/wifi/wifi.page.html index 316c9956..8dbfe391 100644 --- a/src/app/pages/wifi/wifi.page.html +++ b/src/app/pages/wifi/wifi.page.html @@ -2,7 +2,9 @@ - 1 + + + Wi-Fi diff --git a/src/app/providers/conference-data.ts b/src/app/providers/conference-data.ts index 085aafbe..adb0bca0 100644 --- a/src/app/providers/conference-data.ts +++ b/src/app/providers/conference-data.ts @@ -140,8 +140,31 @@ export class ConferenceData { "open-spaces": [], "sprints": data.sprints || [], "job-listings": data['job-listings'] || [], + "posters": (data.posters || []).map((poster: any) => ({ + ...poster, + speakers: (poster.speakers || []).map((s: any) => ({ + ...s, + photo: this.resolveSpeakerPhoto(s.photo), + })), + })), }; + // Register poster speakers in the global speakers list so tapping a poster + // author navigates to a populated speaker detail page (the speaker-detail + // route looks up by id from data.speakers). + this.data.posters.forEach((poster: any) => { + poster.speakers.forEach((speaker: any) => { + if (!this.data.speakers.find((s: any) => s.id === speaker.id)) { + this.data.speakers.push({ + id: speaker.id, + name: speaker.name, + profilePic: speaker.photo, + about: speaker.bio || '', + }); + } + }); + }); + data['open-spaces'].forEach((openSpace: any) => { var start = new Date(openSpace.start); var end = new Date(openSpace.end); @@ -168,6 +191,17 @@ export class ConferenceData { this.data.sessions.push(session); }); + // Drop plenary slots that are mislabeled posters. The conference.json + // upstream sometimes returns the daily posters block as kind="plenary" with + // a name like "Posters (Hall AB)" — see PYMOBIL-108. The actual poster + // slots come through correctly as kind="poster" and are collapsed below, + // so this entry is always a duplicate. + data.schedule = data.schedule.filter((slot: any) => { + if (slot.kind !== 'plenary') return true; + const cleanName = markdownToTxt(slot.name).replace(/\s*\([^)]*\)\s*$/, '').trim(); + return !/^posters?$/i.test(cleanName); + }); + // Collapse repeating slots (posters, breaks) into single entries per day/time const collapseKinds = ['poster', 'break']; const collapsedGroups = new Map(); @@ -468,6 +502,69 @@ export class ConferenceData { }); }); + // (Distinct-tracks pass runs after poster wiring below.) + + // Build a synthetic session entry per poster so users can drill from the + // collapsed "Posters" session list into an individual poster's detail + // (title, abstract, speakers). They reuse the schedule's session-detail + // route via id "poster-detail-". + const collapsedPostersSession = this.data.sessions.find((s: any) => s.track === 'Poster' && s.name === 'Posters'); + this.data.posters.forEach((poster: any) => { + const posterSpeakers = poster.speakers.map((s: any) => ({ + id: s.id, + name: s.name, + profilePic: s.photo, + about: s.bio || '', + })); + const posterSession: any = { + name: poster.title, + color: this.slotColors['poster'], + preRegistered: false, + listRender: false, + section: '', + location: collapsedPostersSession?.location || 'Expo Hall AB', + description: poster.description_html, + speakers: posterSpeakers, + speakerNames: posterSpeakers.map((s: any) => s.name), + timeStart: collapsedPostersSession?.timeStart || '', + timeEnd: collapsedPostersSession?.timeEnd || '', + startUtc: collapsedPostersSession?.startUtc, + endUtc: collapsedPostersSession?.endUtc, + track: 'Poster', + tracks: ['Poster'], + id: `poster-detail-${poster.conf_key}`, + day: collapsedPostersSession?.day || 'Sun', + }; + this.data.sessions.push(posterSession); + poster.sessionId = posterSession.id; + + // Wire the individual poster session into each poster author's + // "Presentations" list. We deliberately skip the collapsed "Posters" + // entry here — listing both would show their poster twice on the + // speaker detail page (once as "Posters", once with its actual title). + posterSpeakers.forEach((posterSpeaker: any) => { + const speaker = this.data.speakers.find((s: any) => s.id === posterSpeaker.id); + if (!speaker) return; + speaker.sessions = speaker.sessions || []; + if (!speaker.sessions.find((s: any) => s.id === posterSession.id)) { + speaker.sessions.push(posterSession); + } + }); + }); + + // Compute distinct tracks per speaker so the speaker list can render a + // pill row when someone has more than one type of session (e.g. a Talk + // and a Poster). + this.data.speakers.forEach((speaker: any) => { + const seen = new Set(); + speaker.tracks = []; + (speaker.sessions || []).forEach((session: any) => { + if (!session.track || seen.has(session.track)) return; + seen.add(session.track); + speaker.tracks.push(session.track); + }); + }); + return this.data; } @@ -689,6 +786,14 @@ export class ConferenceData { ); } + getPosters() { + return this.load().pipe( + map((data: any) => { + return data.posters || []; + }) + ); + } + getJobListings() { return this.load().pipe( map((data: any) => { diff --git a/src/app/providers/live-update.service.ts b/src/app/providers/live-update.service.ts index 564adea5..11be3635 100644 --- a/src/app/providers/live-update.service.ts +++ b/src/app/providers/live-update.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { LoadingController } from '@ionic/angular'; import * as LiveUpdates from '@capacitor/live-updates'; import { App } from '@capacitor/app'; @@ -11,7 +12,7 @@ export class LiveUpdateService { build: string = "base"; appVersion: string = ""; - constructor() { + constructor(private loadingCtrl: LoadingController) { App.addListener('appStateChange', ({ isActive }) => { if (isActive) { this.checkForUpdate(); @@ -38,4 +39,16 @@ export class LiveUpdateService { this.needsUpdate = true; } } + + async performAutomaticUpdate() { + const loader = await this.loadingCtrl.create({ + message: 'Installing the latest build...', + duration: 60000, + }); + await loader.present(); + if (this.needsUpdate) { + this.reload(); + } + setTimeout(() => loader.dismiss(), 1000); + } } diff --git a/src/assets/img/icon-bluesky.svg b/src/assets/img/icon-bluesky.svg index ad25107f..68766a4b 100644 --- a/src/assets/img/icon-bluesky.svg +++ b/src/assets/img/icon-bluesky.svg @@ -1 +1 @@ -Bluesky \ No newline at end of file +Bluesky diff --git a/src/assets/img/icon-x.svg b/src/assets/img/icon-x.svg index a7c37cff..1fe5dc8c 100644 --- a/src/assets/img/icon-x.svg +++ b/src/assets/img/icon-x.svg @@ -1 +1 @@ -X \ No newline at end of file +X diff --git a/src/global.scss b/src/global.scss index 257eb0a6..b058c6e1 100644 --- a/src/global.scss +++ b/src/global.scss @@ -48,6 +48,18 @@ ion-content ion-label { -webkit-user-select: text; } +/* Wrap long URLs and unbreakable strings (e.g. inside API-supplied HTML for + sponsor descriptions, speaker bios, session abstracts) instead of letting + them push the layout off-screen. */ +ion-content p, +ion-content li, +ion-content ion-card-content, +ion-content [innerHtml], +ion-content [innerHTML] { + overflow-wrap: anywhere; + word-break: break-word; +} + /* * Track badges — shared across schedule, speaker, and session pages */