Skip to content

Commit f6ff1f8

Browse files
committed
Focus mobile support
1 parent 46eb2d0 commit f6ff1f8

6 files changed

Lines changed: 264 additions & 5 deletions

File tree

angular.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
}
3333
],
3434
"styles": [
35-
"src/styles.css"
35+
"src/styles.css",
36+
"src/focus-mode-mobile.css"
3637
],
3738
"scripts": [],
3839
"allowedCommonJsDependencies": [
@@ -49,8 +50,8 @@
4950
},
5051
{
5152
"type": "anyComponentStyle",
52-
"maximumWarning": "12kB",
53-
"maximumError": "14kB"
53+
"maximumWarning": "13kB",
54+
"maximumError": "16kB"
5455
}
5556
],
5657
"outputHashing": "all",
@@ -110,7 +111,8 @@
110111
}
111112
],
112113
"styles": [
113-
"src/styles.css"
114+
"src/styles.css",
115+
"src/focus-mode-mobile.css"
114116
],
115117
"scripts": []
116118
}

src/app/app.component.css

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,38 @@
318318
}
319319
}
320320

321+
/* Portable Lo-fi PIP (~40% of pane outer width); must live here for encapsulation */
322+
@media (max-width: 640px), (orientation: landscape) and (max-height: 520px) {
323+
.focus-lofi-dock {
324+
left: auto;
325+
bottom: auto;
326+
right: max(min(0.2rem, 1vw), env(safe-area-inset-right, 0px));
327+
top: max(min(0.2rem, 1vh), env(safe-area-inset-top, 0px));
328+
width: min(5.8rem, calc((100vw - 0.85rem) * 0.4));
329+
max-width: min(5.8rem, calc((100vw - 0.85rem) * 0.4));
330+
border-radius: 0.26rem;
331+
box-shadow:
332+
0 2px 10px rgba(0, 0, 0, 0.5),
333+
inset 0 1px 0 rgba(255, 255, 255, 0.04);
334+
}
335+
336+
.focus-lofi-dock__gate {
337+
padding: 0.18rem;
338+
gap: 0.2rem;
339+
}
340+
341+
.focus-lofi-dock__sound-btn {
342+
padding: 0.16rem 0.32rem;
343+
font-size: 0.48rem;
344+
letter-spacing: 0.01em;
345+
line-height: 1.1;
346+
}
347+
348+
.focus-lofi-dock__gate-hint {
349+
display: none;
350+
}
351+
}
352+
321353
.site-brand {
322354
position: absolute;
323355
top: 1.25rem;

src/app/app.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ <h1 class="site-title">lucent.earth</h1>
6464
</button>
6565
}
6666

67-
@if (!focusMode) {
67+
@if (focusModeLaunchVisible) {
6868
<button
6969
type="button"
7070
class="focus-mode-launch"

src/app/app.component.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ const FOCUS_LOFI_OPTIONS: readonly {
124124
const FOCUS_MODE_TRANSITION_MS = 1100;
125125
const FOCUS_MODE_TRANSITION_MS_REDUCED = 280;
126126

127+
/** Same expression as `focus-mode-mobile.css` — narrow portrait or phone landscape */
128+
const PORTABLE_UI_MEDIA_QUERY =
129+
'(max-width: 640px), (orientation: landscape) and (max-height: 520px)';
130+
127131
@Component({
128132
selector: 'app-root',
129133
imports: [FocusPomodoroComponent, GlobeViewComponent, StreamSidebarComponent],
@@ -135,6 +139,25 @@ export class AppComponent implements OnInit, OnDestroy {
135139

136140
private focusTransitionClearId: ReturnType<typeof setTimeout> | null = null;
137141

142+
private portableUiMql: MediaQueryList | null = null;
143+
144+
private readonly onPortableUiChange = (): void => {
145+
if (!this.portableUiMql) {
146+
return;
147+
}
148+
this.portableUiActive = this.portableUiMql.matches;
149+
this.cdr.markForCheck();
150+
};
151+
152+
/**
153+
* True when compact portable layout applies (matches CSS portable breakpoint).
154+
* Used to hide the Focus entry control while the stream sidebar is fullscreen.
155+
*/
156+
portableUiActive =
157+
typeof globalThis !== 'undefined' &&
158+
typeof matchMedia !== 'undefined' &&
159+
matchMedia(PORTABLE_UI_MEDIA_QUERY).matches;
160+
138161
/**
139162
* First focus embed after a cold load with `?focus=` in the URL — use muted autoplay so
140163
* playback can start without a user gesture; cleared after the first successful embed URL.
@@ -175,6 +198,8 @@ export class AppComponent implements OnInit, OnDestroy {
175198
clearTimeout(this.focusTransitionClearId);
176199
this.focusTransitionClearId = null;
177200
}
201+
this.portableUiMql?.removeEventListener('change', this.onPortableUiChange);
202+
this.portableUiMql = null;
178203
}
179204

180205
/** Full-screen veil while toggling Focus mode (hides WebGL/layout churn). */
@@ -213,6 +238,11 @@ export class AppComponent implements OnInit, OnDestroy {
213238
);
214239
}
215240

241+
/** Hide on portable while sidebar is open — panel covers the canvas */
242+
get focusModeLaunchVisible(): boolean {
243+
return !this.focusMode && (!this.sidebarOpen || !this.portableUiActive);
244+
}
245+
216246
/** X vs magnifier: updates on close *start*, not when the panel unmounts. */
217247
get searchLaunchShowsClose(): boolean {
218248
return this.sidebarOpen && !this.panelCloseAnimationStarted;
@@ -307,6 +337,11 @@ export class AppComponent implements OnInit, OnDestroy {
307337

308338
ngOnInit(): void {
309339
if (typeof window === 'undefined') return;
340+
if (typeof matchMedia !== 'undefined') {
341+
this.portableUiMql = matchMedia(PORTABLE_UI_MEDIA_QUERY);
342+
this.portableUiActive = this.portableUiMql.matches;
343+
this.portableUiMql.addEventListener('change', this.onPortableUiChange);
344+
}
310345
const u = new URL(window.location.href);
311346
const rawStream = u.searchParams.get('stream');
312347
const rawRadio = u.searchParams.get('radio');

src/app/focus-pomodoro/focus-pomodoro.component.css

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,121 @@
337337
font-family: 'Outfit', system-ui, sans-serif;
338338
}
339339

340+
@media (max-width: 640px), (orientation: landscape) and (max-height: 520px) {
341+
.focus-pomodoro__task {
342+
font-size: 0.82rem;
343+
padding: 0 0.35rem;
344+
margin-bottom: 0.25rem;
345+
}
346+
347+
.focus-pomodoro__phase {
348+
font-size: 0.62rem;
349+
margin-bottom: 0.35rem;
350+
}
351+
352+
.focus-pomodoro__ring-box {
353+
max-width: 12.5rem;
354+
}
355+
356+
.focus-pomodoro__time-wrap {
357+
padding: 0.65rem 0.85rem;
358+
}
359+
360+
.focus-pomodoro__time {
361+
font-size: 1.65rem;
362+
}
363+
364+
.focus-pomodoro__controls {
365+
margin-top: 0.55rem;
366+
gap: 0.35rem;
367+
}
368+
369+
.focus-pomodoro__btn {
370+
width: 2.2rem;
371+
height: 2.2rem;
372+
}
373+
374+
.focus-pomodoro__panel {
375+
margin-top: 0.65rem;
376+
padding: 0.65rem 0.75rem;
377+
}
378+
379+
.focus-pomodoro__field {
380+
font-size: 0.72rem;
381+
margin-bottom: 0.55rem;
382+
}
383+
384+
.focus-pomodoro__field input {
385+
font-size: 0.8rem;
386+
padding: 0.38rem 0.45rem;
387+
}
388+
389+
.focus-pomodoro__apply {
390+
padding: 0.42rem 0.55rem;
391+
font-size: 0.78rem;
392+
}
393+
394+
.focus-pomodoro__task-add input[type='text'] {
395+
font-size: 0.78rem;
396+
}
397+
398+
.focus-pomodoro__cycles-input {
399+
width: 2.85rem;
400+
font-size: 0.78rem;
401+
}
402+
403+
.focus-pomodoro__add-btn {
404+
padding: 0.38rem 0.55rem;
405+
font-size: 0.74rem;
406+
}
407+
408+
.focus-pomodoro__task-list {
409+
max-height: 7rem;
410+
}
411+
412+
.focus-pomodoro__task-row {
413+
font-size: 0.74rem;
414+
padding: 0.3rem 0;
415+
}
416+
}
417+
418+
@media (orientation: landscape) and (max-height: 520px) {
419+
.focus-pomodoro__phase {
420+
margin-bottom: 0.25rem;
421+
}
422+
423+
.focus-pomodoro__ring-box {
424+
max-width: 10.5rem;
425+
}
426+
427+
.focus-pomodoro__time-wrap {
428+
padding: 0.45rem 0.65rem;
429+
}
430+
431+
.focus-pomodoro__time {
432+
font-size: 1.4rem;
433+
}
434+
435+
.focus-pomodoro__controls {
436+
margin-top: 0.4rem;
437+
gap: 0.3rem;
438+
}
439+
440+
.focus-pomodoro__btn {
441+
width: 2rem;
442+
height: 2rem;
443+
}
444+
445+
.focus-pomodoro__panel {
446+
margin-top: 0.5rem;
447+
padding: 0.55rem 0.65rem;
448+
}
449+
450+
.focus-pomodoro__task-list {
451+
max-height: 5.5rem;
452+
}
453+
}
454+
340455
@media (prefers-reduced-motion: reduce) {
341456
.focus-pomodoro__arc {
342457
transition: none;

src/focus-mode-mobile.css

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Portable focus UI: narrow portrait, or phone-style landscape (wide CSS width but short height).
3+
* Landscape phones often exceed 640px width — the second clause covers them.
4+
*/
5+
@media (max-width: 640px), (orientation: landscape) and (max-height: 520px) {
6+
.focus-stream-layer__column {
7+
gap: 0.65rem;
8+
padding: max(min(0.65rem, 3vw), env(safe-area-inset-top, 0px))
9+
max(min(0.65rem, 3vw), env(safe-area-inset-right, 0px))
10+
max(min(0.65rem, 3vw), env(safe-area-inset-bottom, 0px))
11+
max(min(0.65rem, 3vw), env(safe-area-inset-left, 0px));
12+
}
13+
14+
.focus-stream-layer__pane {
15+
width: min(14.5rem, calc(100vw - 0.85rem));
16+
max-width: min(14.5rem, calc(100vw - 0.85rem));
17+
max-height: min(72vh, 26rem);
18+
padding: 0.75rem 0.65rem 0.65rem;
19+
border-radius: 0.75rem;
20+
}
21+
22+
.focus-pane-sound--panel {
23+
margin-top: 0.65rem;
24+
padding: 0.65rem 0.75rem;
25+
gap: 0.55rem;
26+
}
27+
28+
.focus-pane-sound__field {
29+
font-size: 0.72rem;
30+
}
31+
32+
.focus-pane-sound__input {
33+
font-size: 0.8rem;
34+
padding: 0.38rem 0.45rem;
35+
}
36+
37+
.focus-pane-sound__title {
38+
font-size: 0.8rem;
39+
}
40+
}
41+
42+
/* Landscape phones: vertical space is scarce — tighten pane */
43+
@media (orientation: landscape) and (max-height: 520px) {
44+
.focus-stream-layer__column {
45+
gap: 0.4rem;
46+
}
47+
48+
.focus-stream-layer__pane {
49+
max-height: min(92dvh, 18rem);
50+
padding: 0.55rem 0.55rem 0.5rem;
51+
}
52+
53+
.focus-pane-sound--panel {
54+
margin-top: 0.5rem;
55+
padding: 0.5rem 0.65rem;
56+
gap: 0.45rem;
57+
}
58+
59+
/* Bolt-only entry control; keep exit label visible (same class stack, different modifier) */
60+
.focus-mode-launch:not(.focus-mode-launch--exit) {
61+
padding: 0.5rem !important;
62+
}
63+
64+
.focus-mode-launch:not(.focus-mode-launch--exit) .focus-mode-launch__label {
65+
position: absolute !important;
66+
width: 1px !important;
67+
height: 1px !important;
68+
padding: 0 !important;
69+
margin: -1px !important;
70+
overflow: hidden !important;
71+
clip: rect(0, 0, 0, 0) !important;
72+
white-space: nowrap !important;
73+
border: 0 !important;
74+
}
75+
}

0 commit comments

Comments
 (0)