estreui 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -125,9 +125,11 @@ EstreUI pages have a distinct lifecycle, similar to Android Activities:
125
125
  1. **onBring**: Page is being prepared. Since the Active Struct hasn't been called yet, this is a good place for element creation tasks involving handles.
126
126
  2. **onOpen**: Page is opening (transition start). Called only once.
127
127
  3. **onShow**: Page is fully visible.
128
- 4. **onHide**: Page is hidden (covered by another page or closed).
129
- 5. **onClose**: Page is closed. Called only once.
130
- 6. **onRelease**: Page resources are released.
128
+ 4. **onFocus**: Page takes active focus (after `onShow`, and on window/tab return). Second arg `isFirstFocus` is true only on the first focus of the instance. Return `true` to opt out of the default in-page autoFocus.
129
+ 5. **onBlur**: Page loses active focus (before `onHide`, and on window/tab leave). `handle.isClosing` indicates the final blur along a close path.
130
+ 6. **onHide**: Page is hidden (covered by another page or closed).
131
+ 7. **onClose**: Page is closed. Called only once.
132
+ 8. **onRelease**: Page resources are released.
131
133
 
132
134
  * **onBack**: Called when back navigation is triggered. Return `true` to cancel the default action.
133
135
  * **onReload**: Called when page reload is triggered. Return `true` to cancel the default action (which is closing and reopening the page).
@@ -394,9 +396,11 @@ EstreUI 페이지는 Android Activity와 유사한 뚜렷한 라이프사이클
394
396
  1. **onBring**: 페이지가 준비되는 중입니다. Active struct가 호출되기 이전이므로 각종 handle 등을 포함하는 element 생성 작업을 할 수 있습니다.
395
397
  2. **onOpen**: 페이지가 열리는 중입니다 (전환 시작). 1회만 호출됩니다.
396
398
  3. **onShow**: 페이지가 완전히 보입니다.
397
- 4. **onHide**: 페이지가 숨겨졌습니다 (다른 페이지에 가려지거나 닫힘).
398
- 5. **onClose**: 페이지가 닫혔습니다. 1회만 호출됩니다.
399
- 6. **onRelease**: 페이지 리소스가 해제됩니다.
399
+ 4. **onFocus**: 페이지가 활성 포커스를 받습니다 (`onShow` 이후, 그리고 창/탭 복귀 시). 두 번째 인자 `isFirstFocus` 는 이 인스턴스의 최초 포커스일 때만 `true`. `true` 를 반환하면 기본 autoFocus(페이지 내 DOM 포커스 이동)를 스킵합니다.
400
+ 5. **onBlur**: 페이지가 포커스를 잃습니다 (`onHide` 직전, 그리고 창/탭 이탈 시). 닫힘 경로상 최종 blur 여부는 `handle.isClosing` 으로 식별합니다.
401
+ 6. **onHide**: 페이지가 숨겨졌습니다 (다른 페이지에 가려지거나 닫힘).
402
+ 7. **onClose**: 페이지가 닫혔습니다. 1회만 호출됩니다.
403
+ 8. **onRelease**: 페이지 리소스가 해제됩니다.
400
404
 
401
405
  * **onBack**: Back navigation이 호출될 때 실행되며, true를 반환할 경우 기본 작동이 취소됩니다.
402
406
  * **onReload**: 페이지를 새로고침하려고 할 때 호출됩니다. true를 반환할 경우 기본 작동이 취소됩니다. 기본 작동은 해당 페이지를 닫은 후 다시 열어주는 것입니다.
package/index.html CHANGED
@@ -81,6 +81,7 @@
81
81
  <link rel="preload" as="fetch" type="text/html" href="./staticDoc.html" crossOrigin="anonymous" />
82
82
  <link rel="preload" as="fetch" type="text/html" href="./instantDoc.html" crossOrigin="anonymous" />
83
83
  <link rel="preload" as="fetch" type="text/html" href="./mainMenu.html" crossOrigin="anonymous" />
84
+ <link rel="preload" as="fetch" type="text/html" href="./overwatchPanel.html" crossOrigin="anonymous" />
84
85
  <link rel="preload" as="fetch" type="text/html" href="./stockHandlePrototypes.html" crossOrigin="anonymous" />
85
86
  <link rel="preload" as="fetch" type="text/html" href="./customHandlePrototypes.html" crossOrigin="anonymous" />
86
87
 
@@ -97,6 +98,22 @@
97
98
  </head>
98
99
 
99
100
  <body class="vfv_scroll">
101
+ <!--
102
+ Dark mode pre-paint coupling (opt-in): uncomment to write
103
+ body[data-dark-mode] before the splash paints, avoiding FOLM
104
+ (flash of light mode) on dark-locked sessions. Adjust the
105
+ storage key and auto-mode policy to match your project.
106
+ -->
107
+ <!--
108
+ <script>
109
+ (function () {
110
+ const stored = localStorage.getItem("estreUi.darkMode");
111
+ const dark = stored === "1"
112
+ || (stored == null && window.matchMedia && matchMedia("(prefers-color-scheme: dark)").matches);
113
+ if (dark) document.body.dataset.darkMode = "1";
114
+ })();
115
+ </script>
116
+ -->
100
117
  <main id="splashRoot" style="z-index: 500; ">
101
118
  <section id="splash" data-on-top="0">
102
119
  <div class="container" data-container-id="root">
@@ -113,6 +130,9 @@
113
130
  </header>
114
131
  <nav id="mainMenu" class="right" data-exported="1" data-opened="">
115
132
  </nav>
133
+ <nav id="overwatchPanel" data-exported="1" data-opened="">
134
+ </nav>
135
+ <section id="panelTrigger" data-static="1"></section>
116
136
 
117
137
  <div id="ptr"><div>
118
138
  <!-- Commented out. This is not implemented currently. <dotlottie-player src="./lotties/ptr_indic.json" background="transparent" speed="1" direction="1" mode="normal"></dotlottie-player> -->
@@ -0,0 +1,27 @@
1
+ <header id="panelHeader" data-static="1">
2
+ <!-- Common top header. Clock / date populated by setOverwatchPanelClock in a later commit. -->
3
+ <span id="panelClock"></span>
4
+ <span id="panelDate"></span>
5
+ </header>
6
+ <div class="dynamic_section_host" data-static="1">
7
+ <div class="host_item" data-id="timeline"><span>Timeline</span></div>
8
+ <div class="host_item" data-id="quickPanel"><span>Quick</span></div>
9
+ </div>
10
+ <div class="dynamic_section_block full_screen" data-static="1">
11
+ <section class="block_item" id="timeline" data-id="timeline" data-static="1">
12
+ <!-- Timeline slot: markup reserved in roadmap #008. Implementation scheduled separately. -->
13
+ </section>
14
+ <section class="block_item" id="quickPanel" data-id="quickPanel" data-static="1">
15
+ <div class="container" data-container-id="root" data-static="1">
16
+ <article data-article-id="main" data-static="1">
17
+ <div class="quick_tiles">
18
+ <button id="darkModeToggle" class="quick_tile" type="button" data-dark-mode-state="auto" onclick="estreUi.cycleDarkMode();">
19
+ <span class="tile_icon" aria-hidden="true">&#x1F313;</span>
20
+ <span class="tile_label">Auto</span>
21
+ </button>
22
+ </div>
23
+ </article>
24
+ </div>
25
+ </section>
26
+ </div>
27
+ <section id="panelGrabArea" data-static="1"><div class="handle"></div><div class="pad"></div></section>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "estreui",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "description": "EstreUI Core Library - A comprehensive UI framework for web applications",
5
5
  "main": "scripts/estreUi.js",
6
6
  "files": [
@@ -31,6 +31,11 @@ const estreUi = {
31
31
  menuSectionList: [],
32
32
  get menuArea() { return this.menuSections["menuArea"]; },
33
33
 
34
+ panelSections: {},
35
+ panelSectionList: [],
36
+ get quickPanel() { return this.panelSections["quickPanel"]; },
37
+ get timeline() { return this.panelSections["timeline"]; },
38
+
34
39
  headerSections: {},
35
40
  headerSectionList: [],
36
41
  get appbar() { return this.headerSections["appbar"]; },
@@ -41,6 +46,7 @@ const estreUi = {
41
46
  blindedCurrentOnTop: null,
42
47
  mainCurrentOnTop: null,
43
48
  menuCurrentOnTop: null,
49
+ panelCurrentOnTop: null,
44
50
  headerCurrentOnTop: null,
45
51
 
46
52
  //static getter
@@ -102,6 +108,16 @@ const estreUi = {
102
108
  $menuArea: null,
103
109
  $grabArea: null,
104
110
 
111
+ $overwatchPanel: null,
112
+ get $panelSections() { return this.$panelBlock?.find(c.c + se + uis.blockItem) ?? $(); },
113
+ $panelHeader: null,
114
+ $panelHost: null,
115
+ $panelBlock: null,
116
+ $panelClock: null,
117
+ $panelDate: null,
118
+ $panelGrabArea: null,
119
+ $panelTrigger: null,
120
+
105
121
  $fixedTop: null,
106
122
  get $headerSections() { return this.$fixedTop.find(c.c + se); },
107
123
  $appbar: null,
@@ -121,6 +137,11 @@ const estreUi = {
121
137
 
122
138
  //handles
123
139
  menuSwipeHandler: null,
140
+ panelOpenSwipeHandler: null,
141
+ panelCloseSwipeHandler: null,
142
+ panelClockTimeoutId: null,
143
+ panelClockIntervalId: null,
144
+ darkModeMql: null,
124
145
 
125
146
  //properties
126
147
  euiState: "exit",
@@ -166,6 +187,16 @@ const estreUi = {
166
187
  //getter and setter
167
188
  get isOpenMainMenu() { return this.$mainMenu.attr(eds.opened) == t1; },
168
189
 
190
+ get isOpenOverwatchPanel() { return this.$overwatchPanel.attr(eds.opened) == t1; },
191
+
192
+ get darkMode() {
193
+ const stored = localStorage.getItem("estreUi.darkMode");
194
+ if (stored == "1") return true;
195
+ if (stored == "0") return false;
196
+ return null;
197
+ },
198
+ get isDarkMode() { return document.body.dataset.darkMode == t1; },
199
+
169
200
 
170
201
 
171
202
  //links (object redirection)
@@ -187,6 +218,9 @@ const estreUi = {
187
218
 
188
219
  this.$mainMenu = $("nav#mainMenu");
189
220
 
221
+ this.$overwatchPanel = $("nav#overwatchPanel");
222
+ this.$panelTrigger = $("section#panelTrigger");
223
+
190
224
  this.$fixedTop = $("header#fixedTop");
191
225
 
192
226
  this.$fixedBottom = $("#fixedBottom");
@@ -198,6 +232,7 @@ const estreUi = {
198
232
  this.setReload();
199
233
  this.setBackNavigation();
200
234
  this.setMenuSwipeHandler();
235
+ this.setupDarkMode();
201
236
 
202
237
 
203
238
  const onLoadedFixedBottom = async _ => {
@@ -236,6 +271,22 @@ const estreUi = {
236
271
  return this.initStaticMenus(subTerm);
237
272
  }
238
273
 
274
+ const onLoadedOverwatchPanel = subTerm => {
275
+ this.$panelHeader = this.$overwatchPanel.find("header#panelHeader");
276
+ this.$panelHost = this.$overwatchPanel.find(uis.dynamicSectionHost);
277
+ this.$panelBlock = this.$overwatchPanel.find(uis.dynamicSectionBlock);
278
+ this.$panelClock = this.$panelHeader.find("#panelClock");
279
+ this.$panelDate = this.$panelHeader.find("#panelDate");
280
+ this.$panelGrabArea = this.$overwatchPanel.find("section#panelGrabArea");
281
+
282
+ this.$panelGrabArea.click(this.overwatchPanelGrabAreaOnclick);
283
+ this.setPanelSwipeHandler();
284
+ this.scheduleOverwatchPanelClock();
285
+ this.initOverwatchPanelHandles();
286
+ this.updateDarkModeToggleWidgets();
287
+ return this.initStaticPanels(subTerm);
288
+ }
289
+
239
290
 
240
291
  const loadExported = url => fetch(url).then(response => {
241
292
  if (!response.ok) throw new Error('Network response was not ok');
@@ -315,6 +366,18 @@ const estreUi = {
315
366
  .then(() => loadExportedMainMenu(subTerm, attempt + 1));
316
367
  });
317
368
 
369
+ let loadExportedOverwatchPanel;
370
+ loadExportedOverwatchPanel = (subTerm, attempt = 0) => loadExported("overwatchPanel.html").then(htmlContent => {
371
+ this.$overwatchPanel.prepend(htmlContent);
372
+ return onLoadedOverwatchPanel(subTerm);
373
+ }).catch(error => {
374
+ const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
375
+ console.error("There has been a problem with your fetch operation for overwatchPanel: ", error);
376
+ console.log(`Retrying to load overwatchPanel in ${delay}ms...`);
377
+ return postPromise(resolve => setTimeout(resolve, delay))
378
+ .then(() => loadExportedOverwatchPanel(subTerm, attempt + 1));
379
+ });
380
+
318
381
  let loadExportedStockHandlePrototypes;
319
382
  loadExportedStockHandlePrototypes = (_, attempt = 0) => loadExported("stockHandlePrototypes.html").then(htmlContent => {
320
383
  this.$handlePrototypes.prepend(htmlContent);
@@ -372,6 +435,7 @@ const estreUi = {
372
435
  await Promise.all(mainLoader);
373
436
 
374
437
  await (this.$mainMenu.attr(eds.exported) == t1 ? loadExportedMainMenu(subTerm) : onLoadedMainMenu(subTerm));
438
+ await (this.$overwatchPanel.attr(eds.exported) == t1 ? loadExportedOverwatchPanel(subTerm) : onLoadedOverwatchPanel(subTerm));
375
439
 
376
440
  this.initSessionManager();
377
441
 
@@ -387,6 +451,27 @@ const estreUi = {
387
451
  this.onBlur();
388
452
  });
389
453
 
454
+ // C (roadmap #006) — visibilitychange routes to onFocus/onBlur.
455
+ // More reliable on mobile browsers than window focus/blur, especially
456
+ // on Android WebView where native focus changes may not surface as JS events.
457
+ // Idempotent via the pageHandle.isFocused guard, so duplication with
458
+ // window focus/blur is harmless.
459
+ document.addEventListener("visibilitychange", () => {
460
+ if (window.isDebug) console.log(`[visibilitychange] state=${document.visibilityState} hasFocus=${document.hasFocus()}`);
461
+ if (document.visibilityState === "visible") this.onFocus();
462
+ else this.onBlur();
463
+ });
464
+
465
+ // A (roadmap #006) — track lastFocusedElement on the topmost showing handle.
466
+ // focusin bubbles (unlike blur), so a single document-level capture covers
467
+ // every page. Used by phase B's autoFocus to restore the prior focus point.
468
+ document.addEventListener("focusin", (e) => {
469
+ const topHandle = this.showingTopArticle ?? this.mainCurrentOnTop;
470
+ if (topHandle != null && topHandle.host?.contains(e.target)) {
471
+ topHandle.lastFocusedElement = e.target;
472
+ }
473
+ }, true);
474
+
390
475
  if (setOnReady) this.checkOnReady();
391
476
  });
392
477
  },
@@ -491,6 +576,95 @@ const estreUi = {
491
576
  },
492
577
 
493
578
 
579
+ //dark mode
580
+ setupDarkMode() {
581
+ if (window.matchMedia) {
582
+ this.darkModeMql = window.matchMedia("(prefers-color-scheme: dark)");
583
+ const onChange = _ => { if (this.darkMode == null) this.applyDarkMode(); };
584
+ if (this.darkModeMql.addEventListener) this.darkModeMql.addEventListener("change", onChange);
585
+ else this.darkModeMql.addListener(onChange);
586
+ }
587
+ this.applyDarkMode();
588
+ },
589
+
590
+ setDarkMode(value) {
591
+ let pref;
592
+ if (value == null) pref = null;
593
+ else if (value === false || value === 0 || value === "0") pref = false;
594
+ else pref = true;
595
+
596
+ if (pref == null) localStorage.removeItem("estreUi.darkMode");
597
+ else localStorage.setItem("estreUi.darkMode", pref ? "1" : "0");
598
+
599
+ this.applyDarkMode();
600
+ return this.isDarkMode;
601
+ },
602
+
603
+ applyDarkMode() {
604
+ const pref = this.darkMode;
605
+ const active = (pref == null) ? (this.darkModeMql?.matches ?? false) : pref;
606
+ if (active) document.body.dataset.darkMode = "1";
607
+ else delete document.body.dataset.darkMode;
608
+ this.updateDarkModeToggleWidgets();
609
+ },
610
+
611
+ // Cycle auto -> light -> dark -> auto (single-button 3-state control)
612
+ cycleDarkMode() {
613
+ const pref = this.darkMode;
614
+ if (pref == null) this.setDarkMode(false);
615
+ else if (pref === false) this.setDarkMode(true);
616
+ else this.setDarkMode(null);
617
+ return this.darkMode;
618
+ },
619
+
620
+ /**
621
+ * Register a tile in the quickPanel section of overwatchPanel.
622
+ * Host projects call this after estreUi.init to append custom toggles/shortcuts.
623
+ *
624
+ * @param {Object} config
625
+ * @param {string} config.id unique DOM id for the tile
626
+ * @param {string} [config.icon] short glyph or emoji shown in the .tile_icon span
627
+ * @param {string} [config.label] text label shown in the .tile_label span
628
+ * @param {Function} [config.onClick] click handler; receives the jQuery event
629
+ * @returns {HTMLElement|null} the tile element, or null if quickPanel is not ready / id collides
630
+ */
631
+ registerOverwatchPanelTile(config) {
632
+ if (config == null || config.id == null) return null;
633
+ const $tiles = this.$overwatchPanel?.find("#quickPanel .quick_tiles");
634
+ if ($tiles == null || $tiles.length < 1) return null;
635
+ if ($tiles.find("#" + config.id).length > 0) return null;
636
+ const $tile = $("<button>").addClass("quick_tile").attr("type", "button").attr("id", config.id);
637
+ $tile.append($("<span>").addClass("tile_icon").attr("aria-hidden", "true").text(config.icon ?? ""));
638
+ $tile.append($("<span>").addClass("tile_label").text(config.label ?? ""));
639
+ if (typeof config.onClick == "function") $tile.on("click", config.onClick);
640
+ $tiles.append($tile);
641
+ return $tile[0];
642
+ },
643
+
644
+ unregisterOverwatchPanelTile(id) {
645
+ if (id == null) return false;
646
+ const $tile = this.$overwatchPanel?.find("#quickPanel .quick_tiles #" + id);
647
+ if ($tile == null || $tile.length < 1) return false;
648
+ $tile.off("click").remove();
649
+ return true;
650
+ },
651
+
652
+ updateDarkModeToggleWidgets() {
653
+ const $widgets = $("#darkModeToggle");
654
+ if ($widgets.length < 1) return;
655
+ const pref = this.darkMode;
656
+ const state = (pref == null) ? "auto" : (pref ? "dark" : "light");
657
+ const icon = state == "light" ? "\u2600\uFE0F" : (state == "dark" ? "\u263D" : "\u{1F313}");
658
+ const label = state.charAt(0).toUpperCase() + state.slice(1);
659
+ $widgets.each(function() {
660
+ const $w = $(this);
661
+ $w.attr("data-dark-mode-state", state);
662
+ $w.find(".tile_icon").text(icon);
663
+ $w.find(".tile_label").text(label);
664
+ });
665
+ },
666
+
667
+
494
668
  //mainMenu
495
669
  setMenuSwipeHandler() {
496
670
  if (this.$mainMenu.length > 0) {
@@ -513,6 +687,78 @@ const estreUi = {
513
687
  if (this.menuSwipeHandler != null) this.menuSwipeHandler.release();
514
688
  },
515
689
 
690
+
691
+ //overwatchPanel
692
+ setPanelSwipeHandler() {
693
+ if (this.$overwatchPanel.length < 1) return;
694
+ this.releasePanelSwipeHandler();
695
+ const ui = this;
696
+ if (this.$panelTrigger.length > 0) {
697
+ this.panelOpenSwipeHandler = new EstreSwipeHandler(this.$panelTrigger[0]).unuseX()
698
+ .setResponseBound(this.$overwatchPanel)
699
+ .setOnUp(function(grabX, grabY, handled, canceled, directed) {
700
+ if (handled && grabY > 0 && !ui.isOpenOverwatchPanel) {
701
+ setTimeout(_ => ui.openOverwatchPanel(), 0);
702
+ }
703
+ });
704
+ }
705
+ if (this.$panelGrabArea.length > 0) {
706
+ this.panelCloseSwipeHandler = new EstreSwipeHandler(this.$panelGrabArea[0]).unuseX()
707
+ .setResponseBound(this.$overwatchPanel)
708
+ .setOnUp(function(grabX, grabY, handled, canceled, directed) {
709
+ if (handled && grabY < 0 && ui.isOpenOverwatchPanel) {
710
+ setTimeout(_ => ui.closeOverwatchPanel(), 0);
711
+ }
712
+ });
713
+ }
714
+ },
715
+
716
+ releasePanelSwipeHandler() {
717
+ if (this.panelOpenSwipeHandler != null) { this.panelOpenSwipeHandler.release(); this.panelOpenSwipeHandler = null; }
718
+ if (this.panelCloseSwipeHandler != null) { this.panelCloseSwipeHandler.release(); this.panelCloseSwipeHandler = null; }
719
+ },
720
+
721
+ // The .dynamic_section_block inside overwatchPanel lives outside any <article>,
722
+ // so the standard article-scoped handle init never reaches it. Attach the handle
723
+ // here with the panel itself acting as a minimal host.
724
+ initOverwatchPanelHandles() {
725
+ if (this.$overwatchPanel.length < 1) return;
726
+ const $block = this.$panelBlock;
727
+ if ($block == null || $block.length < 1) return;
728
+ const host = { $host: this.$overwatchPanel };
729
+ new EstreDynamicSectionBlockHandle($block[0], host).init();
730
+ },
731
+
732
+ setOverwatchPanelClock() {
733
+ if (this.$panelClock == null) return;
734
+ const now = new Date();
735
+ if (this.$panelClock.length > 0) {
736
+ const hh = String(now.getHours()).padStart(2, "0");
737
+ const mm = String(now.getMinutes()).padStart(2, "0");
738
+ this.$panelClock.text(hh + ":" + mm);
739
+ }
740
+ if (this.$panelDate.length > 0) {
741
+ const fmt = new Intl.DateTimeFormat(undefined, { weekday: "short", month: "short", day: "numeric" });
742
+ this.$panelDate.text(fmt.format(now));
743
+ }
744
+ },
745
+
746
+ scheduleOverwatchPanelClock() {
747
+ this.releaseOverwatchPanelClock();
748
+ this.setOverwatchPanelClock();
749
+ const now = new Date();
750
+ const msToNext = 60000 - (now.getSeconds() * 1000 + now.getMilliseconds());
751
+ this.panelClockTimeoutId = setTimeout(_ => {
752
+ this.setOverwatchPanelClock();
753
+ this.panelClockIntervalId = setInterval(_ => this.setOverwatchPanelClock(), 60000);
754
+ }, msToNext);
755
+ },
756
+
757
+ releaseOverwatchPanelClock() {
758
+ if (this.panelClockTimeoutId != null) { clearTimeout(this.panelClockTimeoutId); this.panelClockTimeoutId = null; }
759
+ if (this.panelClockIntervalId != null) { clearInterval(this.panelClockIntervalId); this.panelClockIntervalId = null; }
760
+ },
761
+
516
762
  mainMenuBtnOnClick(e) {
517
763
  estreUi.toggleMainMenuButton();
518
764
  },
@@ -521,6 +767,10 @@ const estreUi = {
521
767
  estreUi.closeMainMenu();
522
768
  },
523
769
 
770
+ overwatchPanelGrabAreaOnclick(e) {
771
+ estreUi.closeOverwatchPanel();
772
+ },
773
+
524
774
  toggleMainMenuButton() {
525
775
  if (this.isOpenMainMenu) return this.closeMainMenu();
526
776
  else return this.openMainMenu();
@@ -569,6 +819,53 @@ const estreUi = {
569
819
  },
570
820
 
571
821
 
822
+ //overwatchPanel
823
+ toggleOverwatchPanel(sectionId) {
824
+ if (this.isOpenOverwatchPanel) return this.closeOverwatchPanel();
825
+ else return this.openOverwatchPanel(sectionId);
826
+ },
827
+
828
+ openOverwatchPanel(sectionId) {
829
+ if (!this.isOpenOverwatchPanel) {
830
+ this.$overwatchPanel.attr(eds.opened, t1);
831
+ if (sectionId != null) this.showOverwatchPanelSection(sectionId);
832
+ else {
833
+ const $top = this.$panelSections.filter(asv(eds.onTop, t1));
834
+ const panelCurrentTop = $top[$top.length - 1]?.pageHandle;
835
+ if (panelCurrentTop != null) {
836
+ this.panelCurrentOnTop = panelCurrentTop;
837
+ panelCurrentTop.show(false);
838
+ }
839
+ }
840
+ return true;
841
+ } else if (sectionId != null) {
842
+ this.showOverwatchPanelSection(sectionId);
843
+ return true;
844
+ } else return false;
845
+ },
846
+
847
+ closeOverwatchPanel() {
848
+ if (this.isOpenOverwatchPanel) {
849
+ this.$overwatchPanel.attr(eds.opened, "");
850
+ this.panelCurrentOnTop?.onHide();
851
+ return true;
852
+ } else return false;
853
+ },
854
+
855
+ showOverwatchPanelSection(id) {
856
+ const $target = this.$panelSections.filter(eid + id);
857
+ if ($target.length < 1) return false;
858
+ const targetEl = $target[$target.length - 1];
859
+ targetEl.scrollIntoView({ behavior: "smooth", block: "start", inline: "start" });
860
+ const targetComponent = targetEl.pageHandle;
861
+ if (targetComponent != null) {
862
+ targetComponent.show(false);
863
+ this.panelCurrentOnTop = targetComponent;
864
+ }
865
+ return true;
866
+ },
867
+
868
+
572
869
  //rootbar
573
870
  initRootbar() {
574
871
  this.$rootTabs = this.$tabsbar.find(c.c + btn);
@@ -1372,6 +1669,45 @@ const estreUi = {
1372
1669
  return component;
1373
1670
  },
1374
1671
 
1672
+ async initStaticPanels(term = 0) {
1673
+ const $pss = this.$panelSections;
1674
+
1675
+ const delayer = (delay = term) => postPromise(resolve => setTimeout(resolve, delay));
1676
+ for (var i=0; i<$pss.length; i++) {
1677
+ this.initStaticPanel($pss[i], null, u, true);
1678
+ await delayer();
1679
+ }
1680
+
1681
+ let $top = this.$panelSections.filter(asv(eds.onTop, t1));
1682
+ if ($top.length < 1) $top = this.$panelSections.filter(eid + "quickPanel");
1683
+ if ($top.length < 1) $top = this.$panelSections;
1684
+ if ($top.length > 0) {
1685
+ const targetComponent = $top[$top.length - 1].pageHandle;
1686
+ targetComponent?.show(false);
1687
+ this.panelCurrentOnTop = targetComponent;
1688
+ }
1689
+ },
1690
+
1691
+ releaseStaticPanel(component) {
1692
+ if (component == null) return;
1693
+ const instanceId = component.instanceId;
1694
+ component.release(component.isStatic ? null : true);
1695
+ if (this.panelSections[instanceId] != null) delete this.panelSections[instanceId];
1696
+ const index = this.panelSectionList.indexOf(component);
1697
+ if (index > -1) this.panelSectionList.splice(index, 1);
1698
+ },
1699
+
1700
+ initStaticPanel(bound, intent = null, instanceOrigin, init = false) {
1701
+ this.releaseStaticPanel(bound.pageHandle);
1702
+ const component = new EstrePanelComponent(bound, instanceOrigin);
1703
+ if (!init || component.isStatic) {
1704
+ this.panelSections[component.instanceId] = component;
1705
+ this.panelSectionList.push(component);
1706
+ }
1707
+ component.init(intent);
1708
+ return component;
1709
+ },
1710
+
1375
1711
  async initHeaderBars(term = 0) {
1376
1712
  const $hss = this.$headerSections;
1377
1713
 
@@ -1575,12 +1911,15 @@ const estreUi = {
1575
1911
 
1576
1912
 
1577
1913
  async onFocus() {
1578
- // <= to do implement
1579
- // this.focus();
1914
+ const top = this.showingTopArticle ?? this.mainCurrentOnTop;
1915
+ if (window.isDebug) console.log(`[estreUi.onFocus] visibility=${document.visibilityState} hasFocus=${document.hasFocus()} top=${top?.pid ?? "(none)"}`);
1916
+ top?.focus();
1580
1917
  },
1581
-
1918
+
1582
1919
  async onBlur() {
1583
- // <= to do implement
1920
+ const top = this.showingTopArticle ?? this.mainCurrentOnTop;
1921
+ if (window.isDebug) console.log(`[estreUi.onBlur] visibility=${document.visibilityState} hasFocus=${document.hasFocus()} top=${top?.pid ?? "(none)"}`);
1922
+ await top?.blur();
1584
1923
  },
1585
1924
 
1586
1925
 
@@ -521,6 +521,48 @@ class EstreUiPageManager {
521
521
  });
522
522
  });
523
523
  }
524
+
525
+ /**
526
+ * Default auto-focus policy for a newly-focused page handle.
527
+ * Priority:
528
+ * 1. On repeat focus, restore `handle.lastFocusedElement` if still in the DOM.
529
+ * 2. First `[data-autofocus]` element inside the host.
530
+ * 3. First tab-reachable focusable element inside the host.
531
+ * 4. Otherwise no-op.
532
+ * Invoked from `pageHandle.onFocus()` when `handler.onFocus` does not return true.
533
+ * Projects can override on an `EstreUiCustomPageManager` subclass if a different policy is needed.
534
+ * @param {EstrePageHandle} handle - The page handle receiving focus.
535
+ * @param {boolean} isFirstFocus - True on the first focus after onOpen; false on subsequent focuses.
536
+ * @returns {boolean} Whether a focus() call succeeded.
537
+ */
538
+ autoFocus(handle, isFirstFocus) {
539
+ const host = handle?.host;
540
+ if (host == null) return false;
541
+
542
+ if (!isFirstFocus) {
543
+ const last = handle.lastFocusedElement;
544
+ if (last != null && host.contains(last) && document.body.contains(last)) {
545
+ last.focus();
546
+ if (document.activeElement === last) return true;
547
+ }
548
+ }
549
+
550
+ const markedTarget = host.querySelector("[data-autofocus]");
551
+ if (markedTarget != null && !markedTarget.hasAttribute("disabled") && !markedTarget.hidden) {
552
+ markedTarget.focus();
553
+ if (document.activeElement === markedTarget) return true;
554
+ }
555
+
556
+ const candidates = host.querySelectorAll(
557
+ 'input:not([disabled]),textarea:not([disabled]),select:not([disabled]),button:not([disabled]),[tabindex]:not([tabindex="-1"])'
558
+ );
559
+ for (const el of candidates) {
560
+ if (el.hidden) continue;
561
+ el.focus();
562
+ if (document.activeElement === el) return true;
563
+ }
564
+ return false;
565
+ }
524
566
  }
525
567
 
526
568
  const pageManager = new EstreUiPageManager();
@@ -99,6 +99,8 @@ class EstrePageHandle {
99
99
  get isShowing() { return this.isOpened && this.#isShowing; }
100
100
  #isFocused = false;
101
101
  get isFocused() { return this.isShowing && this.#isFocused; }
102
+ #everFocused = false;
103
+ get everFocused() { return this.#everFocused; }
102
104
 
103
105
  #isHiding = false;
104
106
  get isHiding() { return this.#isHiding; }
@@ -123,6 +125,8 @@ class EstrePageHandle {
123
125
  #isProcessing = f;
124
126
  get isProcessing() { return this.#isProcessing; }
125
127
 
128
+ lastFocusedElement = null;
129
+
126
130
  get mainArticle() { return this; }
127
131
 
128
132
 
@@ -381,7 +385,9 @@ class EstrePageHandle {
381
385
  const task = this.hide();
382
386
  return postAsyncQueue(async _ => {
383
387
  await task;
384
- return await this.onClose(isTermination, isOnRelease);
388
+ const result = await this.onClose(isTermination, isOnRelease);
389
+ this.#isClosing = false;
390
+ return result;
385
391
  });
386
392
  } else return false;
387
393
  }
@@ -416,10 +422,18 @@ class EstrePageHandle {
416
422
 
417
423
  onFocus() {
418
424
  if (!this.isFocused) {
425
+ const isFirstFocus = !this.#everFocused;
419
426
  if (window.isDebug) console.log("[onFocus] " + this.sectionBound + " " + this.hostType + " " + this.pid);//, this.host);
420
427
  this.#isFocused = true;
421
- if (this.handler?.onFocus != null) this.handler.onFocus(this);
428
+ this.#everFocused = true;
429
+ const handled = this.handler?.onFocus?.(this, isFirstFocus);
422
430
  if (this.intent?.onFocus != null) for (var item of this.intent.onFocus) if (item.from == this.hostType && !item.disabled) this.processAction(item);
431
+ if (handled === true) {
432
+ // Snapshot activeElement so a later refocus (e.g. background→foreground)
433
+ // can restore whatever the handler focused, even if focusin didn't record it.
434
+ const ae = document.activeElement;
435
+ if (ae != null && ae !== document.body && this.host?.contains(ae)) this.lastFocusedElement = ae;
436
+ } else pageManager.autoFocus?.(this, isFirstFocus);
423
437
  return true;
424
438
  } else return false;
425
439
  }
@@ -451,7 +465,7 @@ class EstrePageHandle {
451
465
  this.#isFocused = false;
452
466
  if (window.isDebug) console.log("[onBlur] " + this.sectionBound + " " + this.hostType + " " + this.pid);//, this.host);
453
467
  if (this.intent?.onBlur != null) for (var item of this.intent.onBlur) if (item.from == this.hostType && !item.disabled) await this.processAction(item);
454
- if (this.handler?.onBlur != null) await this.handler.onBlur(this);
468
+ await this.handler?.onBlur?.(this, this.isClosing);
455
469
  return true;
456
470
  } else return false;
457
471
  }
@@ -474,6 +488,7 @@ class EstrePageHandle {
474
488
  postQueue(_ => pageManager.bringPage(pid));
475
489
  }
476
490
  }
491
+ this.#isHiding = false;
477
492
  return true;
478
493
  } else return false;
479
494
  }
@@ -481,6 +496,8 @@ class EstrePageHandle {
481
496
  async onClose(isTermination = false, isOnRelease = false) {
482
497
  if (this.isOpened && (isOnRelease || !this.isStatic)) {
483
498
  this.#isOpened = false;
499
+ this.#everFocused = false;
500
+ this.lastFocusedElement = null;
484
501
  if (window.isDebug) console.log("[onClose] " + this.sectionBound + " " + this.hostType + " " + this.pid);//, this.host);
485
502
  if (this.intent?.onClose != null) for (var item of this.intent.onClose) if (item.from == this.hostType && !item.disabled) await this.processAction(item);
486
503
  if (this.handler?.onClose != null) await this.handler.onClose(this);
@@ -1380,6 +1397,7 @@ class EstreComponent extends EstrePageHostHandle {
1380
1397
  case "menu":
1381
1398
  case "overlay":
1382
1399
  case "header":
1400
+ case "panel":
1383
1401
  return false;
1384
1402
  }
1385
1403
  }
@@ -1776,6 +1794,53 @@ class EstreMenuComponent extends EstreComponent {
1776
1794
 
1777
1795
 
1778
1796
 
1797
+ /**
1798
+ * Component page handle for overwatchPanel sections (quickPanel, timeline).
1799
+ * Sections live inside the panel's dynamic_section_block and switch by horizontal scroll-snap;
1800
+ * opening or closing the whole panel is a shell-level operation on estreUi.
1801
+ */
1802
+ class EstrePanelComponent extends EstreComponent {
1803
+ // constants
1804
+ get sectionBound() { return "panel"; };
1805
+
1806
+ // class property
1807
+ static components = {};
1808
+ static componentList = [];
1809
+
1810
+
1811
+ constructor(component, instanceOrigin) {
1812
+ super(component, instanceOrigin);
1813
+ }
1814
+
1815
+ release(remove) {
1816
+ return super.release(remove);
1817
+ }
1818
+
1819
+ init(intent) {
1820
+ super.init(intent);
1821
+ return this;
1822
+ }
1823
+
1824
+ register() {
1825
+ return EstrePanelComponent.register(this);
1826
+ }
1827
+
1828
+ unregister() {
1829
+ EstrePanelComponent.unregister(this);
1830
+ }
1831
+
1832
+ show(isRequest = true, setFocus = true) {
1833
+ if (isRequest) {
1834
+ return estreUi.showOverwatchPanelSection(this.id);
1835
+ } else super.show(false, setFocus);
1836
+ }
1837
+
1838
+ // close() falls through to super — dismissing an individual panel section is not meaningful;
1839
+ // call estreUi.closeOverwatchPanel() to close the shell.
1840
+ }
1841
+
1842
+
1843
+
1779
1844
  /**
1780
1845
  * Component page handle for header sections
1781
1846
  */
@@ -2975,6 +3040,7 @@ class EstreAlertDialogPageHandler extends EstreDialogPageHandler {
2975
3040
 
2976
3041
  onFocus(handle) {
2977
3042
  this.$confirm.focus();
3043
+ return true;
2978
3044
  }
2979
3045
  }
2980
3046
 
@@ -3022,6 +3088,7 @@ class EstreConfirmDialogPageHandler extends EstreDialogPageHandler {
3022
3088
 
3023
3089
  onFocus(handle) {
3024
3090
  this.$negative.focus();
3091
+ return true;
3025
3092
  }
3026
3093
  }
3027
3094
 
@@ -3081,6 +3148,7 @@ class EstrePromptDialogPageHandler extends EstreDialogPageHandler {
3081
3148
 
3082
3149
  onFocus(handle) {
3083
3150
  this.$input.focus();
3151
+ return true;
3084
3152
  }
3085
3153
  }
3086
3154
 
@@ -3110,6 +3178,7 @@ class EstreOptionDialogPageHandler extends EstreDialogPageHandler {
3110
3178
 
3111
3179
  onFocus(handle) {
3112
3180
  this.$optionItems[0]?.focus();
3181
+ return true;
3113
3182
  }
3114
3183
  }
3115
3184
 
@@ -3213,6 +3282,7 @@ class EstreSelectionDialogPageHandler extends EstreDialogPageHandler {
3213
3282
 
3214
3283
  onFocus(handle) {
3215
3284
  this.$confirm.focus();
3285
+ return true;
3216
3286
  }
3217
3287
 
3218
3288
  checkValidSelectAction(handle, handler, index, value, checked) {
@@ -3291,6 +3361,7 @@ class EstreDialsDialogPageHandler extends EstreDialogPageHandler {
3291
3361
 
3292
3362
  onFocus(handle) {
3293
3363
  this.$confirm.focus();
3364
+ return true;
3294
3365
  }
3295
3366
 
3296
3367
  onClose(handle) {
@@ -4185,7 +4256,7 @@ class EstreUiPage {
4185
4256
  if (this.#sectionBound == null) {
4186
4257
  const $componentHost = $component.closest("main, nav, header, footer");
4187
4258
  const hostId = $componentHost.attr("id");
4188
- const sectionBound = hostId == "staticDoc" ? "main" : (hostId == "instantDoc" ? "blind" : (hostId == "managedOverlay" ? "overlay" : (hostId == "mainMenu" ? "menu" : (hostId == "fixedTop" ? "header" : null))));
4259
+ const sectionBound = hostId == "staticDoc" ? "main" : (hostId == "instantDoc" ? "blind" : (hostId == "managedOverlay" ? "overlay" : (hostId == "mainMenu" ? "menu" : (hostId == "fixedTop" ? "header" : (hostId == "overwatchPanel" ? "panel" : null)))));
4189
4260
  this.setSectionBound(sectionBound);
4190
4261
  }
4191
4262
 
package/serviceWorker.js CHANGED
@@ -1,4 +1,4 @@
1
- const INSTALLATION_VERSION_NAME = "1.2.4-r20260418";
1
+ const INSTALLATION_VERSION_NAME = "1.3.0-r20260421";
2
2
  // ^^ Use for check new update "Native application(webview) version(or Android/iOS version combo) - PWA release version"
3
3
  // ex) "1.0.1/1.0.0-r20251101k"
4
4
 
@@ -10,6 +10,7 @@ const INSTALLATION_FILE_LIST = [
10
10
  "./staticDoc.html",
11
11
  "./instantDoc.html",
12
12
  "./managedOverlay.html",
13
+ "./overwatchPanel.html",
13
14
  "./customHandlePrototypes.html",
14
15
 
15
16
 
@@ -21,7 +22,7 @@ const INSTALLATION_FILE_LIST = [
21
22
 
22
23
 
23
24
  // Common files cache - Be changes some time but, well not changed very often
24
- const CACHE_NAME_COMMON_FILES = "common-files-cache-v1-20260418";
25
+ const CACHE_NAME_COMMON_FILES = "common-files-cache-v1-20260421";
25
26
 
26
27
  const COMMON_FILES_TO_CACHE = [
27
28
  "./",
@@ -330,7 +330,7 @@ article .text_capsule.bordered { --border-color: var(--color-boundary); --bg-col
330
330
  article .text_capsule.hide_on_empty:empty { display: none; }
331
331
 
332
332
  article .h_icon_set { display: flex; flex-flow: row nowrap; align-items: stretch; }
333
- article .h_icon_set > .icon_place { width: fit-content; aspect-ratio: 1; flex-grow: 0; flex-shrink: 0; align-self: center; }
333
+ article .h_icon_set > .icon_place { width: fit-content; height: -webkit-fill-available; height: stretch; aspect-ratio: 1; flex-grow: 0; flex-shrink: 0; align-self: center; }
334
334
  article .h_icon_set > .icon_place > img { height: 100%; }
335
335
  article .h_icon_set > .icon_place > img[src=""] { width: 0; height: fit-content; }
336
336
  article .h_icon_set > .content_place { width: 100%; flex-grow: 1; flex-shrink: 1; }
@@ -124,8 +124,55 @@ nav#mainMenu[data-opened="1"] > section#grabArea div.pad { backdrop-filter: unse
124
124
  }
125
125
  }
126
126
 
127
+ /* overwatchPanel (drops from top) */
128
+ nav#overwatchPanel { --grab-y: 0px; --panel-block-height: calc(100vh - var(--top-pad) - 42px - var(--bottom-safe-pad)); position: fixed; z-index: 120; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; pointer-events: none; user-select: none; }
129
+ nav#overwatchPanel > header#panelHeader,
130
+ nav#overwatchPanel > .dynamic_section_host,
131
+ nav#overwatchPanel > .dynamic_section_block,
132
+ nav#overwatchPanel > section#panelGrabArea { pointer-events: auto; }
133
+ nav#overwatchPanel > header#panelHeader { display: flex; flex-direction: row; align-items: baseline; gap: 0.5em; padding: calc(var(--top-pad) + 4px) var(--basic-ui-inset-h) 4px; flex-shrink: 0; background-color: rgba(var(--cabr) / 60%); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); transform: translateY(-100%); transition-duration: 0.3s; }
134
+ nav#overwatchPanel > header#panelHeader > #panelClock { font-size: 1.125rem; font-weight: 600; }
135
+ nav#overwatchPanel > header#panelHeader > #panelDate { font-size: 0.875rem; opacity: 0.75; }
136
+ nav#overwatchPanel > .dynamic_section_host { display: flex; flex-flow: row nowrap; flex-shrink: 0; padding-inline: var(--basic-ui-inset-h); color: var(--color-text-lightest); background-color: rgba(var(--cabr) / 60%); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); transform: translateY(calc(-100% - var(--panel-block-height))); transition-duration: 0.3s; }
137
+ nav#overwatchPanel > .dynamic_section_host > .host_item { flex-grow: 1; padding: 8px 0 7px; border-bottom: solid 1px var(--color-boundary-o10); font-size: 1rem; font-weight: 500; text-align: center; line-height: 1.5em; cursor: pointer; transition-duration: 0.2s; }
138
+ nav#overwatchPanel > .dynamic_section_host > .host_item[data-showing="1"] { padding-bottom: 6px; border-bottom: solid 2px var(--color-indicator-bold); color: var(--color-text-darker); }
139
+ nav#overwatchPanel > .dynamic_section_block { display: flex; flex-flow: row nowrap; flex-shrink: 1; height: var(--panel-block-height); max-height: calc(100vh - var(--top-pad) - 42px - var(--bottom-safe-pad)); overflow-x: overlay; scrollbar-width: none; scroll-behavior: smooth; scroll-snap-type: x mandatory; background-color: rgba(var(--cabr) / 40%); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); transform: translateY(calc(-100vh - var(--panel-block-height))); transition-duration: 0.3s; }
140
+ nav#overwatchPanel > .dynamic_section_block > .block_item { width: 100%; flex-shrink: 0; overflow-y: auto; scroll-snap-align: start; }
141
+ nav#overwatchPanel > section#panelGrabArea { flex-grow: 0; flex-shrink: 0; height: 42px; background-color: transparent; opacity: 0; transition-duration: 0.3s; }
142
+ nav#overwatchPanel > section#panelGrabArea > div.handle { --width: 64px; --height: 4px; display: block; width: var(--width); height: var(--height); margin: 10px auto 0; border-radius: calc(var(--height) / 2); background-color: var(--color-boundary-lightside); }
143
+ nav#overwatchPanel > section#panelGrabArea > div.pad { flex-grow: 1; }
144
+
145
+ nav#overwatchPanel[data-opened="1"] > header#panelHeader,
146
+ nav#overwatchPanel[data-opened="1"] > .dynamic_section_host,
147
+ nav#overwatchPanel[data-opened="1"] > .dynamic_section_block { transform: translateY(0); }
148
+ nav#overwatchPanel[data-opened="1"] > section#panelGrabArea { opacity: 1; background-color: rgb(var(--cadm) / 20%); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); }
149
+
150
+ nav#overwatchPanel[data-on-grab="1"] > header#panelHeader,
151
+ nav#overwatchPanel[data-on-grab="1"] > .dynamic_section_host,
152
+ nav#overwatchPanel[data-on-grab="1"] > .dynamic_section_block { transition-delay: 0s; transition-duration: 0s; }
153
+ nav#overwatchPanel[data-on-grab="1"]:not([data-opened="1"]) > header#panelHeader { transform: translateY(calc(-100% + max(var(--grab-y), 0px))); }
154
+ nav#overwatchPanel[data-on-grab="1"]:not([data-opened="1"]) > .dynamic_section_host { transform: translateY(calc(-100% - var(--panel-block-height) + max(var(--grab-y), 0px))); }
155
+ nav#overwatchPanel[data-on-grab="1"]:not([data-opened="1"]) > .dynamic_section_block { transform: translateY(calc(-100vh - var(--panel-block-height) + max(var(--grab-y), 0px))); }
156
+ nav#overwatchPanel[data-opened="1"][data-on-grab="1"] > header#panelHeader,
157
+ nav#overwatchPanel[data-opened="1"][data-on-grab="1"] > .dynamic_section_host,
158
+ nav#overwatchPanel[data-opened="1"][data-on-grab="1"] > .dynamic_section_block { transform: translateY(min(var(--grab-y), 0px)); }
159
+
160
+ /* Top swipe trigger strip. Sits above fixedTop (z-index 130) so the downward swipe can fire while the panel is closed.
161
+ Height stays inside the safe area (status bar / notch) plus a small 8px bleed into fixedTop so the strip
162
+ does not cover tappable fixedTop controls. */
163
+ section#panelTrigger { position: fixed; z-index: 135; top: 0; left: 0; right: 0; height: calc(var(--top-safe-pad) + 8px); user-select: none; pointer-events: auto; background-color: transparent; }
164
+ nav#overwatchPanel[data-opened="1"] ~ section#panelTrigger { pointer-events: none; }
165
+
166
+ /* Quick panel tiles */
167
+ .quick_tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 8px; padding: 8px var(--basic-ui-inset-h); }
168
+ .quick_tile { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding: 12px 8px; border-radius: 12px; border: 1px solid rgb(var(--cadm) / 20%); background-color: rgba(var(--cabr) / 30%); cursor: pointer; user-select: none; }
169
+ .quick_tile > .tile_icon { font-size: 1.5rem; line-height: 1; }
170
+ .quick_tile > .tile_label { font-size: 0.75rem; }
171
+ #darkModeToggle[data-dark-mode-state="light"] { background-color: rgba(var(--cabr) / 50%); }
172
+ #darkModeToggle[data-dark-mode-state="dark"] { background-color: rgba(var(--cadm) / 50%); }
173
+
127
174
  /* root tabs (bottom) */
128
- footer#fixedBottom { position: fixed; display: flex; flex-direction: row; flex-wrap: nowrap; z-index: 110; bottom: 0; left: 0; right: 0; height: var(--rootbar-height); margin: 0; padding-bottom: var(--bottom-safe-pad); justify-content: center; background-color: rgb(var(--cwht) / 66.666%); backdrop-filter: var(--basic-backdrop-blur); -webkit-backdrop-filter: var(--basic-backdrop-blur); justify-content: center; user-select: none; }
175
+ footer#fixedBottom { position: fixed; display: flex; flex-direction: row; flex-wrap: nowrap; z-index: 110; bottom: 0; left: 0; right: 0; height: var(--rootbar-height); margin: 0; padding-bottom: var(--bottom-safe-pad); justify-content: center; background-color: var(--color-boundary-foggy-o66); backdrop-filter: var(--basic-backdrop-blur); -webkit-backdrop-filter: var(--basic-backdrop-blur); justify-content: center; user-select: none; }
129
176
  footer#fixedBottom nav { position: relative; display: flex; flex-direction: row; flex-wrap: nowrap; height: var(--rootbar-height); flex-grow: 1; flex-shrink: 1; user-select: none; }
130
177
  footer#fixedBottom nav:not(#rootbar) { max-width: 0; }
131
178
  footer#fixedBottom nav:first-child { justify-content: flex-end; }
@@ -55,7 +55,8 @@ body {
55
55
 
56
56
  color: var(--color-text);
57
57
 
58
- --common-bg-color: var(--color-grayscale-lightness);
58
+ /* Concrete hex literal: this stylesheet loads eagerly, before the lazy-loaded estreUiRoot.css defines --color-* tokens, so var(--color-*) here would resolve to the guaranteed-invalid value and background-color would fall back to transparent. Host projects that want a project-owned splash tone override --common-bg-color in their own non-lazy initialize stylesheet. */
59
+ --common-bg-color: #CCC;
59
60
  background-color: var(--common-bg-color);
60
61
 
61
62
  font-size: 16px;
@@ -64,6 +65,9 @@ body[data-dark-mode="1"] {
64
65
 
65
66
  /* adaptive color (dark) */
66
67
 
68
+ /* Dark-mode splash fallback. Only takes effect in sessions that opt into FOLM pre-paint coupling (see index.html); otherwise the dark attribute lands after first paint and the light value above is what the splash shows. */
69
+ --common-bg-color: #222;
70
+
67
71
  }
68
72
 
69
73
 
@@ -60,6 +60,7 @@ html {
60
60
 
61
61
 
62
62
  --color-black: #000; --cblk: 0 0 0;
63
+ --color-barely-black: #070707; --cbblk: 7 7 7;
63
64
  --color-pseudo-black: #111; --cpblk: 17 17 17;
64
65
  --color-almost-black: #171717; --cablk: 23 23 23;
65
66
  --color-grayscale-darkest: #222; --cgdt: 34 34 34;
@@ -79,8 +80,9 @@ html {
79
80
  --color-grayscale-lightly: #D9D9D9; --cgly: 217 217 217;
80
81
  --color-grayscale-lightside: #DDD; --cgld: 221 221 221;
81
82
  --color-grayscale-lightest: #E7E7E7; --cglt: 231 231 231;
82
- --color-alomst-white: #EEE; --cawht: 238 238 238;
83
+ --color-almost-white: #EEE; --cawht: 238 238 238;
83
84
  --color-pseudo-white: #F7F7F7; --cpwht: 247 247 247;
85
+ --color-barely-white: #FAFAFA; --cbwht: 250 250 250;
84
86
  --color-white: #FFF; --cwht: 255 255 255;
85
87
 
86
88
  --color-focused: #F46224; --cf: 244 98 36;
@@ -128,6 +130,7 @@ html {
128
130
  --color-important-day-gray: #F5BF7E; --cidg: 245 191 126;
129
131
 
130
132
  --color-boundary-dim: var(--color-black); --cbdm: var(--cblk);
133
+ --color-boundary-dimly: var(--color-barely-black); --cbdmly: var(--cbblk);
131
134
  --color-boundary-dimmy: var(--color-pseudo-black); --cbdmy: var(--cpblk);
132
135
  --color-boundary-deepdark: var(--color-almost-black); --cbdd: var(--cablk);
133
136
  --color-boundary-darkest: var(--color-grayscale-darkest); --cbdt: var(--cgdt);
@@ -147,8 +150,9 @@ html {
147
150
  --color-boundary-lightly: var(--color-grayscale-lightly); --cbly: var(--cgly);
148
151
  --color-boundary-lightside: var(--color-grayscale-lightside); --cbld: var(--cgld);
149
152
  --color-boundary-lightest: var(--color-grayscale-lightest); --cblt: var(--cglt);
150
- --color-boundary-highlight: var(--color-alomst-white); --cbhl: var(--cawht);
153
+ --color-boundary-highlight: var(--color-almost-white); --cbhl: var(--cawht);
151
154
  --color-boundary-brighty: var(--color-pseudo-white); --cbbry: var(--cpwht);
155
+ --color-boundary-brightly: var(--color-barely-white); --cbbrly: var(--cbwht);
152
156
  --color-boundary-bright: var(--color-white); --cbbr: var(--cwht);
153
157
 
154
158
  --color-boundary-o1: rgba(var(--cbdm) / 1%);
@@ -186,6 +190,7 @@ html {
186
190
  --color-boundary-foggy-o20: rgba(var(--cbbr) / 20%);
187
191
  --color-boundary-foggy-o25: rgba(var(--cbbr) / 25%);
188
192
  --color-boundary-foggy-o30: rgba(var(--cbbr) / 30%);
193
+ --color-boundary-foggy-o33: rgba(var(--cbbr) / 33.333%);
189
194
  --color-boundary-foggy-o35: rgba(var(--cbbr) / 35%);
190
195
  --color-boundary-foggy-o40: rgba(var(--cbbr) / 40%);
191
196
  --color-boundary-foggy-o45: rgba(var(--cbbr) / 45%);
@@ -193,6 +198,7 @@ html {
193
198
  --color-boundary-foggy-o55: rgba(var(--cbbr) / 55%);
194
199
  --color-boundary-foggy-o60: rgba(var(--cbbr) / 60%);
195
200
  --color-boundary-foggy-o65: rgba(var(--cbbr) / 65%);
201
+ --color-boundary-foggy-o66: rgba(var(--cbbr) / 66.666%);
196
202
  --color-boundary-foggy-o70: rgba(var(--cbbr) / 70%);
197
203
  --color-boundary-foggy-o75: rgba(var(--cbbr) / 75%);
198
204
  --color-boundary-foggy-o80: rgba(var(--cbbr) / 80%);
@@ -486,5 +492,136 @@ body {
486
492
  body[data-dark-mode="1"] {
487
493
 
488
494
  /* adaptive color (dark) */
495
+ /*
496
+ Baseline `--color-black` ~ `--color-grayscale-*` ~ `--color-white` palette
497
+ is intentionally inherited from light mode. Only the *semantic* color sets
498
+ below (text / boundary / point / adaptive) flip here.
499
+
500
+ Brand-singleton colors (`--color-focused`, `--color-emphasis*`,
501
+ holiday/sunday/saturday/today/selected-day/important-day) keep their
502
+ identity hue and are not overridden.
503
+
504
+ The boundary opacity ramps (`--color-boundary-o*`, `--color-boundary-foggy-o*`)
505
+ must be re-declared below: CSS custom-property `var()` substitution happens
506
+ eagerly at the declaring scope, so ramps declared only at `:root` bake in
507
+ the light-mode `--cbdm`/`--cbbr` values and do not follow the dark override.
508
+ */
509
+
510
+ --color-text-darker: #FFF; --ctdr: 255 255 255;
511
+ --color-text-darken: #EEE; --ctdn: 238 238 238;
512
+ --color-text-dark: #DDD; --ctd: 221 221 221;
513
+ --color-text: #CCC; --ct: 204 204 204;
514
+ --color-text-light: #BBB; --ctl: 187 187 187;
515
+ --color-text-lighten: #AAA; --ctln: 170 170 170;
516
+ --color-text-lighter: #999; --ctlr: 153 153 153;
517
+ --color-text-lightness: #888; --ctls: 136 136 136;
518
+ --color-text-lightest: #777; --ctlt: 119 119 119;
519
+ --color-text-pale: #666; --ctp: 102 102 102;
520
+ --color-text-palen: #555; --ctpn: 85 85 85;
521
+ --color-text-paler: #444; --ctpr: 68 68 68;
522
+ --color-text-paleness: #333; --ctps: 51 51 51;
523
+ --color-text-palest: #222; --ctpt: 34 34 34;
524
+ --color-text-faint: #111; --ctf: 17 17 17;
525
+ --color-text-inverse: #000; --cai: 0 0 0;
526
+ --color-anti-text: #000; --cat: 0 0 0;
527
+
528
+ --color-boundary-dim: var(--color-white); --cbdm: var(--cwht);
529
+ --color-boundary-dimly: var(--color-barely-white); --cbdmly: var(--cbwht);
530
+ --color-boundary-dimmy: var(--color-pseudo-white); --cbdmy: var(--cpwht);
531
+ --color-boundary-deepdark: var(--color-almost-white); --cbdd: var(--cawht);
532
+ --color-boundary-darkest: var(--color-grayscale-lightest); --cbdt: var(--cglt);
533
+ --color-boundary-darkside: var(--color-grayscale-lightside); --cbdd: var(--cgld);
534
+ --color-boundary-darkly: var(--color-grayscale-lightly); --cbdy: var(--cgly);
535
+ --color-boundary-darkness: var(--color-grayscale-lightness); --cbds: var(--cgls);
536
+ --color-boundary-darker: var(--color-grayscale-lighter); --cbdr: var(--cglr);
537
+ --color-boundary-darken: var(--color-grayscale-lighten); --cbdn: var(--cgln);
538
+ --color-boundary-darkish: var(--color-grayscale-lightish); --cbdh: var(--cglh);
539
+ --color-boundary-dark: var(--color-grayscale-light); --cbd: var(--cgl);
540
+ --color-boundary-light: var(--color-grayscale-dark); --cbl: var(--cgd);
541
+ --color-boundary-lightish: var(--color-grayscale-darkish); --cblh: var(--cgdh);
542
+ --color-boundary-lighten: var(--color-grayscale-darken); --cbln: var(--cgdn);
543
+ --color-boundary-lighter: var(--color-grayscale-darker); --cblr: var(--cgdr);
544
+ --color-boundary-lightness: var(--color-grayscale-darkness); --cbls: var(--cgds);
545
+ --color-boundary-lightly: var(--color-grayscale-darkly); --cbly: var(--cgdy);
546
+ --color-boundary-lightside: var(--color-grayscale-darkside); --cbld: var(--cgdd);
547
+ --color-boundary-lightest: var(--color-grayscale-darkest); --cblt: var(--cgdt);
548
+ --color-boundary-highlight: var(--color-almost-black); --cbhl: var(--cablk);
549
+ --color-boundary-brighty: var(--color-pseudo-black); --cbbry: var(--cpblk);
550
+ --color-boundary-brightly: var(--color-barely-black); --cbbrly: var(--cbblk);
551
+ --color-boundary-bright: var(--color-black); --cbbr: var(--cblk);
552
+
553
+ --color-boundary-o1: rgba(var(--cbdm) / 1%);
554
+ --color-boundary-o3: rgba(var(--cbdm) / 3%);
555
+ --color-boundary-o5: rgba(var(--cbdm) / 5%);
556
+ --color-boundary-o7: rgba(var(--cbdm) / 7%);
557
+ --color-boundary-o10: rgba(var(--cbdm) / 10%);
558
+ --color-boundary-o15: rgba(var(--cbdm) / 15%);
559
+ --color-boundary-o20: rgba(var(--cbdm) / 20%);
560
+ --color-boundary-o25: rgba(var(--cbdm) / 25%);
561
+ --color-boundary-o30: rgba(var(--cbdm) / 30%);
562
+ --color-boundary-o35: rgba(var(--cbdm) / 35%);
563
+ --color-boundary-o40: rgba(var(--cbdm) / 40%);
564
+ --color-boundary-o45: rgba(var(--cbdm) / 45%);
565
+ --color-boundary-o50: rgba(var(--cbdm) / 50%);
566
+ --color-boundary-o55: rgba(var(--cbdm) / 55%);
567
+ --color-boundary-o60: rgba(var(--cbdm) / 60%);
568
+ --color-boundary-o65: rgba(var(--cbdm) / 65%);
569
+ --color-boundary-o70: rgba(var(--cbdm) / 70%);
570
+ --color-boundary-o75: rgba(var(--cbdm) / 75%);
571
+ --color-boundary-o80: rgba(var(--cbdm) / 80%);
572
+ --color-boundary-o85: rgba(var(--cbdm) / 85%);
573
+ --color-boundary-o90: rgba(var(--cbdm) / 90%);
574
+ --color-boundary-o93: rgba(var(--cbdm) / 93%);
575
+ --color-boundary-o95: rgba(var(--cbdm) / 95%);
576
+ --color-boundary-o97: rgba(var(--cbdm) / 97%);
577
+ --color-boundary-o99: rgba(var(--cbdm) / 99%);
578
+
579
+ --color-boundary-foggy-o1: rgba(var(--cbbr) / 1%);
580
+ --color-boundary-foggy-o3: rgba(var(--cbbr) / 3%);
581
+ --color-boundary-foggy-o5: rgba(var(--cbbr) / 5%);
582
+ --color-boundary-foggy-o7: rgba(var(--cbbr) / 7%);
583
+ --color-boundary-foggy-o10: rgba(var(--cbbr) / 10%);
584
+ --color-boundary-foggy-o15: rgba(var(--cbbr) / 15%);
585
+ --color-boundary-foggy-o20: rgba(var(--cbbr) / 20%);
586
+ --color-boundary-foggy-o25: rgba(var(--cbbr) / 25%);
587
+ --color-boundary-foggy-o30: rgba(var(--cbbr) / 30%);
588
+ --color-boundary-foggy-o33: rgba(var(--cbbr) / 33.333%);
589
+ --color-boundary-foggy-o35: rgba(var(--cbbr) / 35%);
590
+ --color-boundary-foggy-o40: rgba(var(--cbbr) / 40%);
591
+ --color-boundary-foggy-o45: rgba(var(--cbbr) / 45%);
592
+ --color-boundary-foggy-o50: rgba(var(--cbbr) / 50%);
593
+ --color-boundary-foggy-o55: rgba(var(--cbbr) / 55%);
594
+ --color-boundary-foggy-o60: rgba(var(--cbbr) / 60%);
595
+ --color-boundary-foggy-o65: rgba(var(--cbbr) / 65%);
596
+ --color-boundary-foggy-o66: rgba(var(--cbbr) / 66.666%);
597
+ --color-boundary-foggy-o70: rgba(var(--cbbr) / 70%);
598
+ --color-boundary-foggy-o75: rgba(var(--cbbr) / 75%);
599
+ --color-boundary-foggy-o80: rgba(var(--cbbr) / 80%);
600
+ --color-boundary-foggy-o85: rgba(var(--cbbr) / 85%);
601
+ --color-boundary-foggy-o90: rgba(var(--cbbr) / 90%);
602
+ --color-boundary-foggy-o93: rgba(var(--cbbr) / 93%);
603
+ --color-boundary-foggy-o95: rgba(var(--cbbr) / 95%);
604
+ --color-boundary-foggy-o97: rgba(var(--cbbr) / 97%);
605
+ --color-boundary-foggy-o99: rgba(var(--cbbr) / 99%);
606
+
607
+ --color-point-dim: var(--color-white); --cpdm: var(--cwht);
608
+ --color-point-dark: var(--color-grayscale-lighter); --cpd: var(--cglr);
609
+ --color-point: var(--color-grayscale-dark); --cp: var(--cgd);
610
+ --color-point-light: var(--color-grayscale-darker); --cpl: var(--cgdr);
611
+ --color-point-bright: var(--color-black); --cpbr: var(--cblk);
612
+
613
+ --color-point-sub-dim: var(--color-white); --cpsdm: var(--cwht);
614
+ --color-point-sub-dark: var(--color-grayscale-lighter); --cpsd: var(--cglr);
615
+ --color-point-sub: var(--color-grayscale-dark); --cps: var(--cgd);
616
+ --color-point-sub-light: var(--color-grayscale-darker); --cpsl: var(--cgdr);
617
+ --color-point-sub-bright: var(--color-black); --cpsbr: var(--cblk);
618
+
619
+ --color-adaptive-dimmest: var(--color-white); --cadmt: var(--cwht);
620
+ --color-adaptive-dim: var(--color-pseudo-white); --cadm: var(--cpwht);
621
+ --color-adaptive-dark: var(--color-grayscale-lighter); --cad: var(--cglr);
622
+ --color-adaptive: var(--color-grayscale-darken); --ca: var(--cgdn);
623
+ --color-adaptive-light: var(--color-grayscale-darkness); --cal: var(--cgds);
624
+ --color-adaptive-bright: var(--color-pseudo-black); --cabr: var(--cpblk);
625
+ --color-adaptive-brightest: var(--color-black); --cabrt: var(--cblk);
489
626
 
490
627
  }