estreui 1.3.0 → 1.4.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/index.html +1 -0
- package/managedOverlay.html +3 -2
- package/package.json +1 -1
- package/scripts/estreUi-dialog.js +1 -4
- package/scripts/estreUi-main.js +11 -0
- package/scripts/estreUi-notification.js +539 -0
- package/scripts/estreUi-pageModel.js +141 -1
- package/serviceWorker.js +3 -2
- package/styles/estreUiCore.css +46 -4
- package/styles/estreUiCore2.css +25 -1
package/index.html
CHANGED
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
<script defer type="text/javascript" src="./scripts/estreUi-core.js"></script>
|
|
65
65
|
<script defer type="text/javascript" src="./scripts/estreUi-dialog.js"></script>
|
|
66
66
|
<script defer type="text/javascript" src="./scripts/estreUi-notation.js"></script>
|
|
67
|
+
<script defer type="text/javascript" src="./scripts/estreUi-notification.js"></script>
|
|
67
68
|
<script defer type="text/javascript" src="./scripts/estreUi-pageModel.js"></script>
|
|
68
69
|
<script defer type="text/javascript" src="./scripts/estreUi-pageManager.js"></script>
|
|
69
70
|
<script defer type="text/javascript" src="./scripts/estreUi-handles.js"></script>
|
package/managedOverlay.html
CHANGED
|
@@ -199,12 +199,13 @@
|
|
|
199
199
|
<div class="container top" data-container-id="noti" data-static="1" data-on-top="1">
|
|
200
200
|
<article class="" data-article-id="noti" data-static="" data-multi-instance="1" data-instance-origin="">
|
|
201
201
|
<div class="h_icon_set post_block" data-bind-attr="interactive@data-interactive" data-bind-style="bgColor@--bg-color">
|
|
202
|
-
<div class="icon_place"><img data-bind-attr="
|
|
202
|
+
<div class="icon_place"><img data-bind-attr="largeIconSrc@src" /></div>
|
|
203
203
|
<div class="content_place">
|
|
204
204
|
<div class="title_line"><span data-bind="contentTitle"></span></div>
|
|
205
|
+
<div class="subtitle_line"><span data-bind="subtitle"></span></div>
|
|
205
206
|
<div class="h_icon_Set content_area">
|
|
206
207
|
<div class="content_place" data-bind="content" data-bind-style="textColor@--color textSize@--size textWeight@--weight"></div>
|
|
207
|
-
<div class="icon_place"><img data-bind-attr="
|
|
208
|
+
<div class="icon_place"><img data-bind-attr="iconSrc@src" /></div>
|
|
208
209
|
</div>
|
|
209
210
|
</div>
|
|
210
211
|
</div>
|
package/package.json
CHANGED
|
@@ -502,10 +502,7 @@ const arrived = function (instanceOrigin) {
|
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
|
|
505
|
-
|
|
506
|
-
const noti = function (title, htmlContent, onTakeInteraction = (intent) => {}, mainIconSrc, subIconSrc) {
|
|
507
|
-
//<= To do implement
|
|
508
|
-
}
|
|
505
|
+
// noti() moved to estreUi-notification.js (roadmap #009).
|
|
509
506
|
|
|
510
507
|
|
|
511
508
|
// ======================================================================
|
package/scripts/estreUi-main.js
CHANGED
|
@@ -283,6 +283,7 @@ const estreUi = {
|
|
|
283
283
|
this.setPanelSwipeHandler();
|
|
284
284
|
this.scheduleOverwatchPanelClock();
|
|
285
285
|
this.initOverwatchPanelHandles();
|
|
286
|
+
this.initOverwatchPanelTimeline();
|
|
286
287
|
this.updateDarkModeToggleWidgets();
|
|
287
288
|
return this.initStaticPanels(subTerm);
|
|
288
289
|
}
|
|
@@ -729,6 +730,16 @@ const estreUi = {
|
|
|
729
730
|
new EstreDynamicSectionBlockHandle($block[0], host).init();
|
|
730
731
|
},
|
|
731
732
|
|
|
733
|
+
// Mounts EstreTimelineView onto overwatchPanel #timeline slot (roadmap #010).
|
|
734
|
+
// Timeline persists entries left by checkOut()ed noti banners.
|
|
735
|
+
initOverwatchPanelTimeline() {
|
|
736
|
+
if (typeof EstreTimelineView === "undefined") return;
|
|
737
|
+
if (this.$overwatchPanel == null || this.$overwatchPanel.length < 1) return;
|
|
738
|
+
const $timeline = this.$overwatchPanel.find("#timeline");
|
|
739
|
+
if ($timeline.length < 1) return;
|
|
740
|
+
this.timelineView = new EstreTimelineView($timeline);
|
|
741
|
+
},
|
|
742
|
+
|
|
732
743
|
setOverwatchPanelClock() {
|
|
733
744
|
if (this.$panelClock == null) return;
|
|
734
745
|
const now = new Date();
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/*
|
|
2
|
+
EstreUI rimwork — Notification Banner
|
|
3
|
+
Part of the split from estreUi.js (roadmap #002 phase 2; roadmap #009).
|
|
4
|
+
|
|
5
|
+
This file is loaded as a plain <script> tag and shares the global scope
|
|
6
|
+
with the other estreUi-*.js files. Load order matters: see index.html.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// MODULE: Notification -- EstreNotificationManager, noti(), push adapters
|
|
10
|
+
// ======================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* iOS-style notification banner queue manager.
|
|
14
|
+
* Mirrors EstreNotationManager (note toast) — one-at-a-time, per-item showTime.
|
|
15
|
+
*/
|
|
16
|
+
class EstreNotificationManager {
|
|
17
|
+
|
|
18
|
+
// static
|
|
19
|
+
static #page = "popNoti";
|
|
20
|
+
|
|
21
|
+
static #queue = [];
|
|
22
|
+
static current = null;
|
|
23
|
+
|
|
24
|
+
static postHandle = null;
|
|
25
|
+
|
|
26
|
+
/** Default auto-dismiss ms for banners (longer than note toast). */
|
|
27
|
+
static defaultShowTime = 4500;
|
|
28
|
+
|
|
29
|
+
static get noInteraction() { return (intent) => {}; }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Enqueue a banner.
|
|
33
|
+
* @param {object} options - Normalized banner options (see EstreNotificationManager#data).
|
|
34
|
+
* @returns {Promise<EstreNotificationManager>|undefined}
|
|
35
|
+
*/
|
|
36
|
+
static post(options) {
|
|
37
|
+
if (options != null && typeof options === "object") {
|
|
38
|
+
if (this.#isTimelineVisible()) {
|
|
39
|
+
this.#appendToTimelineDirect(options);
|
|
40
|
+
return Promise.resolve(null);
|
|
41
|
+
}
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const it = new EstreNotificationManager(options, resolve);
|
|
44
|
+
this.#queue.push(it);
|
|
45
|
+
if (window.isDebug) console.log(this.#page + " posted: ", it);
|
|
46
|
+
postQueue(_ => this.postHandler());
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* True when the overwatchPanel is open and the timeline section is currently showing.
|
|
53
|
+
* In that case banner posts are diverted into the timeline list directly (iOS behavior).
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
static #isTimelineVisible() {
|
|
57
|
+
if (typeof estreUi === "undefined" || !estreUi.isOpenOverwatchPanel) return false;
|
|
58
|
+
if (typeof EstreTimelineStore === "undefined") return false;
|
|
59
|
+
return estreUi.$overwatchPanel.find('.host_item[data-showing="1"][data-id="timeline"]').length > 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build a timeline entry directly from noti options and append to the store.
|
|
64
|
+
* Mirrors the shape used in checkOut().
|
|
65
|
+
*/
|
|
66
|
+
static #appendToTimelineDirect(options) {
|
|
67
|
+
const ui = options.ui ?? {};
|
|
68
|
+
EstreTimelineStore.append({
|
|
69
|
+
postedAt: Date.now(),
|
|
70
|
+
title: options.title,
|
|
71
|
+
body: options.body,
|
|
72
|
+
subtitle: options.subtitle,
|
|
73
|
+
iconSrc: options.icon,
|
|
74
|
+
largeIconSrc: options.largeIcon,
|
|
75
|
+
url: options.url,
|
|
76
|
+
payload: options.data,
|
|
77
|
+
bgColor: ui.bgColor,
|
|
78
|
+
textColor: ui.textColor,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static postHandler() {
|
|
83
|
+
if (window.isDebug) console.log("queue: ", this.#queue);
|
|
84
|
+
if (this.postHandle == null && this.current == null && this.#queue.length > 0) {
|
|
85
|
+
const handle = Date.now();
|
|
86
|
+
this.postHandle = handle;
|
|
87
|
+
const current = this.#queue.splice(0, 1)[0];
|
|
88
|
+
current.data.posted = handle;
|
|
89
|
+
if (window.isDebug) console.log(this.#page + " bring: ", current);
|
|
90
|
+
return pageManager.bringPage("!" + this.#page, current, handle);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** True when at least one banner is waiting to be brought. */
|
|
95
|
+
static get hasQueued() { return this.#queue.length > 0; }
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Release the queue lock at close-start so the next banner can begin its
|
|
99
|
+
* enter animation in parallel with the outgoing banner's exit animation.
|
|
100
|
+
* Does the timeline-append + resolver work; checkOut() becomes a noop
|
|
101
|
+
* afterwards via the `_earlyCheckedOut` flag.
|
|
102
|
+
*/
|
|
103
|
+
static beginCheckOut(intent) {
|
|
104
|
+
if (intent == null || intent._earlyCheckedOut) return;
|
|
105
|
+
if (intent.data.posted != null && this.postHandle == intent.data.posted) {
|
|
106
|
+
if (this.current == intent) this.current = null;
|
|
107
|
+
this.postHandle = null;
|
|
108
|
+
if (typeof EstreTimelineStore !== "undefined") {
|
|
109
|
+
const d = intent.data;
|
|
110
|
+
EstreTimelineStore.append({
|
|
111
|
+
postedAt: d.posted,
|
|
112
|
+
title: d.contentTitle,
|
|
113
|
+
body: d.content,
|
|
114
|
+
subtitle: d.subtitle,
|
|
115
|
+
iconSrc: d.iconSrc,
|
|
116
|
+
largeIconSrc: d.largeIconSrc,
|
|
117
|
+
url: d.url,
|
|
118
|
+
payload: d.payload,
|
|
119
|
+
bgColor: d.bgColor,
|
|
120
|
+
textColor: d.textColor,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
intent._earlyCheckedOut = true;
|
|
124
|
+
intent.resolver?.(intent);
|
|
125
|
+
if (window.isDebug) console.log(this.#page + " early checked out: ", intent);
|
|
126
|
+
postQueue(_ => this.postHandler());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static checkOut(intent) {
|
|
131
|
+
if (intent?._earlyCheckedOut) return;
|
|
132
|
+
if (intent.data.posted != null && this.postHandle == intent.data.posted) {
|
|
133
|
+
if (this.current == intent) {
|
|
134
|
+
this.current = null;
|
|
135
|
+
}
|
|
136
|
+
this.postHandle = null;
|
|
137
|
+
if (window.isDebug) console.log(this.#page + " checked out: ", intent);
|
|
138
|
+
postQueue(_ => this.postHandler());
|
|
139
|
+
}
|
|
140
|
+
if (typeof EstreTimelineStore !== "undefined") {
|
|
141
|
+
const d = intent.data;
|
|
142
|
+
EstreTimelineStore.append({
|
|
143
|
+
postedAt: d.posted,
|
|
144
|
+
title: d.contentTitle,
|
|
145
|
+
body: d.content,
|
|
146
|
+
subtitle: d.subtitle,
|
|
147
|
+
iconSrc: d.iconSrc,
|
|
148
|
+
largeIconSrc: d.largeIconSrc,
|
|
149
|
+
url: d.url,
|
|
150
|
+
payload: d.payload,
|
|
151
|
+
bgColor: d.bgColor,
|
|
152
|
+
textColor: d.textColor,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
intent.resolver?.(intent);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
// instance property
|
|
160
|
+
data = {
|
|
161
|
+
posted: undefined,
|
|
162
|
+
contentTitle: undefined,
|
|
163
|
+
subtitle: undefined,
|
|
164
|
+
content: undefined,
|
|
165
|
+
showTime: undefined,
|
|
166
|
+
interactive: undefined,
|
|
167
|
+
|
|
168
|
+
// icons
|
|
169
|
+
iconSrc: undefined, // small / sub icon
|
|
170
|
+
largeIconSrc: undefined, // large / main icon (subIconSrc naming kept for template)
|
|
171
|
+
|
|
172
|
+
// payload / routing
|
|
173
|
+
buttons: undefined,
|
|
174
|
+
url: undefined,
|
|
175
|
+
payload: undefined,
|
|
176
|
+
|
|
177
|
+
// visual tokens
|
|
178
|
+
textSize: undefined,
|
|
179
|
+
textWeight: undefined,
|
|
180
|
+
textColor: undefined,
|
|
181
|
+
bgColor: undefined,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
onTakeInteraction = undefined;
|
|
185
|
+
resolver = undefined;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {object} options - Normalized banner options.
|
|
189
|
+
* @param {Function} resolver - Promise resolver injected by post().
|
|
190
|
+
*/
|
|
191
|
+
constructor(options, resolver) {
|
|
192
|
+
const d = this.data;
|
|
193
|
+
d.contentTitle = options.title;
|
|
194
|
+
d.content = options.body;
|
|
195
|
+
d.subtitle = options.subtitle;
|
|
196
|
+
d.iconSrc = options.icon;
|
|
197
|
+
d.largeIconSrc = options.largeIcon;
|
|
198
|
+
d.buttons = options.buttons;
|
|
199
|
+
d.url = options.url;
|
|
200
|
+
d.payload = options.data;
|
|
201
|
+
|
|
202
|
+
const ui = options.ui ?? {};
|
|
203
|
+
d.showTime = ui.showTime ?? EstreNotificationManager.defaultShowTime;
|
|
204
|
+
d.textSize = ui.textSize;
|
|
205
|
+
d.textWeight = ui.textWeight;
|
|
206
|
+
d.textColor = ui.textColor;
|
|
207
|
+
d.bgColor = ui.bgColor;
|
|
208
|
+
|
|
209
|
+
this.onTakeInteraction = options.onTakeInteraction ?? EstreNotificationManager.noInteraction;
|
|
210
|
+
const hasInteraction = this.onTakeInteraction !== EstreNotificationManager.noInteraction
|
|
211
|
+
|| options.url != null
|
|
212
|
+
|| (Array.isArray(options.buttons) && options.buttons.length > 0);
|
|
213
|
+
d.interactive = (ui.interactive ?? hasInteraction) ? "" : undefined;
|
|
214
|
+
|
|
215
|
+
this.resolver = resolver;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Post an iOS-style notification banner.
|
|
221
|
+
*
|
|
222
|
+
* Positional form (frequency-first, matches the original stub):
|
|
223
|
+
* noti(title, body, onTakeInteraction, mainIconSrc, subIconSrc)
|
|
224
|
+
*
|
|
225
|
+
* Object overload — passes through to EstreNotificationManager.post():
|
|
226
|
+
* noti({ title, body, subtitle, icon, largeIcon, data, url, buttons, onTakeInteraction, ui })
|
|
227
|
+
*
|
|
228
|
+
* @param {string|object} title - Title text, or the full options object (overload).
|
|
229
|
+
* @param {string} [body] - Body text (HTML-capable).
|
|
230
|
+
* @param {Function} [onTakeInteraction] - Tap callback. intent is passed.
|
|
231
|
+
* @param {string} [mainIconSrc] - Large/leading icon src (maps to largeIcon).
|
|
232
|
+
* @param {string} [subIconSrc] - Small/trailing icon src (maps to icon).
|
|
233
|
+
* @returns {Promise<EstreNotificationManager>|undefined}
|
|
234
|
+
*/
|
|
235
|
+
const noti = function (title, body, onTakeInteraction, mainIconSrc, subIconSrc) {
|
|
236
|
+
if (title != null && typeof title === "object") {
|
|
237
|
+
return EstreNotificationManager.post(title);
|
|
238
|
+
}
|
|
239
|
+
return EstreNotificationManager.post({
|
|
240
|
+
title,
|
|
241
|
+
body,
|
|
242
|
+
onTakeInteraction,
|
|
243
|
+
largeIcon: mainIconSrc,
|
|
244
|
+
icon: subIconSrc,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* FCM payload adapter.
|
|
250
|
+
* Accepts either the full message (`{ notification, data }`) or the `notification` object directly.
|
|
251
|
+
* @param {object} payload
|
|
252
|
+
*/
|
|
253
|
+
noti.fromFcm = function (payload) {
|
|
254
|
+
if (payload == null) return;
|
|
255
|
+
const n = payload.notification ?? payload;
|
|
256
|
+
const data = payload.data ?? (payload !== n ? undefined : undefined);
|
|
257
|
+
return noti({
|
|
258
|
+
title: n.title,
|
|
259
|
+
body: n.body,
|
|
260
|
+
icon: n.icon,
|
|
261
|
+
largeIcon: n.image,
|
|
262
|
+
url: n.click_action ?? payload.fcm_options?.link,
|
|
263
|
+
data,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* APNs payload adapter.
|
|
269
|
+
* Accepts the outer aps-bearing object (`{ aps: { alert: ... }, ...custom }`).
|
|
270
|
+
* `alert` may be a string or object.
|
|
271
|
+
* @param {object} payload
|
|
272
|
+
*/
|
|
273
|
+
noti.fromApns = function (payload) {
|
|
274
|
+
if (payload == null) return;
|
|
275
|
+
const aps = payload.aps ?? {};
|
|
276
|
+
const alert = typeof aps.alert === "string" ? { body: aps.alert } : (aps.alert ?? {});
|
|
277
|
+
return noti({
|
|
278
|
+
title: alert.title,
|
|
279
|
+
body: alert.body,
|
|
280
|
+
subtitle: alert.subtitle,
|
|
281
|
+
data: payload,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* OneSignal payload adapter.
|
|
287
|
+
* `headings` / `contents` may be locale maps; first value is picked.
|
|
288
|
+
* @param {object} payload
|
|
289
|
+
*/
|
|
290
|
+
noti.fromOneSignal = function (payload) {
|
|
291
|
+
if (payload == null) return;
|
|
292
|
+
const pick = (v) => (v != null && typeof v === "object") ? Object.values(v)[0] : v;
|
|
293
|
+
return noti({
|
|
294
|
+
title: pick(payload.headings),
|
|
295
|
+
body: pick(payload.contents),
|
|
296
|
+
subtitle: pick(payload.subtitle),
|
|
297
|
+
largeIcon: payload.big_picture ?? payload.chrome_big_picture,
|
|
298
|
+
icon: payload.small_icon ?? payload.chrome_icon,
|
|
299
|
+
url: payload.url,
|
|
300
|
+
data: payload.data ?? payload.additionalData,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Persistent history for dismissed notification banners (roadmap #010).
|
|
307
|
+
* Backed by ECLS (localStorage, JSON). Retains up to maxEntries for ttlMs.
|
|
308
|
+
*/
|
|
309
|
+
class EstreTimelineStore {
|
|
310
|
+
|
|
311
|
+
static #key = "timelineEntries";
|
|
312
|
+
static maxEntries = 100;
|
|
313
|
+
static ttlMs = 7 * 24 * 60 * 60 * 1000;
|
|
314
|
+
|
|
315
|
+
static #listeners = new Set();
|
|
316
|
+
|
|
317
|
+
/** @returns {Array<object>} entries newest-first, TTL-pruned. */
|
|
318
|
+
static load() {
|
|
319
|
+
const raw = ECLS.get(this.#key, []);
|
|
320
|
+
if (!Array.isArray(raw)) return [];
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
return raw.filter(it => it != null && (now - (it.postedAt ?? 0)) < this.ttlMs);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
static save(entries) {
|
|
326
|
+
ECLS.set(this.#key, entries);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Append an entry (newest-first). Assigns id/postedAt if missing. Enforces cap + TTL.
|
|
331
|
+
* @param {object} entry
|
|
332
|
+
*/
|
|
333
|
+
static append(entry) {
|
|
334
|
+
if (entry == null) return;
|
|
335
|
+
const now = Date.now();
|
|
336
|
+
const normalized = { ...entry,
|
|
337
|
+
id: entry.id ?? String(entry.postedAt ?? now),
|
|
338
|
+
postedAt: entry.postedAt ?? now,
|
|
339
|
+
};
|
|
340
|
+
const entries = this.load().filter(it => it.id !== normalized.id);
|
|
341
|
+
entries.unshift(normalized);
|
|
342
|
+
if (entries.length > this.maxEntries) entries.length = this.maxEntries;
|
|
343
|
+
this.save(entries);
|
|
344
|
+
this.#emit();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
static remove(id) {
|
|
348
|
+
const entries = this.load().filter(it => it.id !== id);
|
|
349
|
+
this.save(entries);
|
|
350
|
+
this.#emit();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
static clear() {
|
|
354
|
+
this.save([]);
|
|
355
|
+
this.#emit();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Subscribe to store changes. Callback receives the current entries array.
|
|
360
|
+
* @param {Function} cb
|
|
361
|
+
* @returns {Function} unsubscribe
|
|
362
|
+
*/
|
|
363
|
+
static subscribe(cb) {
|
|
364
|
+
this.#listeners.add(cb);
|
|
365
|
+
return () => this.#listeners.delete(cb);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
static #emit() {
|
|
369
|
+
const entries = this.load();
|
|
370
|
+
for (const cb of this.#listeners) {
|
|
371
|
+
try { cb(entries); } catch (ex) { if (window.isLogging) console.error(ex); }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Renders EstreTimelineStore entries into a host element (e.g. overwatchPanel #timeline).
|
|
379
|
+
* Groups by date bucket (Today / Yesterday / Older), item visuals share banner styles.
|
|
380
|
+
*/
|
|
381
|
+
class EstreTimelineView {
|
|
382
|
+
|
|
383
|
+
#$host;
|
|
384
|
+
#unsubscribe;
|
|
385
|
+
#lastIds = new Set();
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @param {Element|JQuery} host - Container element for the list.
|
|
389
|
+
*/
|
|
390
|
+
constructor(host) {
|
|
391
|
+
this.#$host = host instanceof jQuery ? host : $(host);
|
|
392
|
+
this.#$host.addClass("timeline_host");
|
|
393
|
+
this.render(EstreTimelineStore.load());
|
|
394
|
+
this.#unsubscribe = EstreTimelineStore.subscribe((entries) => this.render(entries));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
destroy() {
|
|
398
|
+
this.#unsubscribe?.();
|
|
399
|
+
this.#$host.empty();
|
|
400
|
+
this.#$host.removeClass("timeline_host");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
render(entries) {
|
|
404
|
+
const $host = this.#$host;
|
|
405
|
+
$host.empty();
|
|
406
|
+
|
|
407
|
+
if (!entries || entries.length === 0) {
|
|
408
|
+
this.#lastIds = new Set();
|
|
409
|
+
$host.append('<div class="timeline_empty">No notifications</div>');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const currentIds = new Set(entries.map(e => e.id));
|
|
414
|
+
const lastIds = this.#lastIds;
|
|
415
|
+
const isFirstRender = lastIds.size === 0;
|
|
416
|
+
|
|
417
|
+
const groups = this.#groupByDate(entries);
|
|
418
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
419
|
+
const group = groups[gi];
|
|
420
|
+
const $group = $('<div class="timeline_group"></div>');
|
|
421
|
+
const $header = $('<div class="timeline_group_header"></div>');
|
|
422
|
+
$header.append($('<span class="timeline_group_label"></span>').text(group.label));
|
|
423
|
+
if (gi === 0) {
|
|
424
|
+
const $btn = $('<button type="button" class="timeline_clear_all">Clear All</button>');
|
|
425
|
+
$btn.on("click", (e) => {
|
|
426
|
+
e.preventDefault();
|
|
427
|
+
e.stopPropagation();
|
|
428
|
+
this.#clearAllWithCascade();
|
|
429
|
+
});
|
|
430
|
+
$header.append($btn);
|
|
431
|
+
}
|
|
432
|
+
$group.append($header);
|
|
433
|
+
for (const entry of group.entries) {
|
|
434
|
+
const isNew = !isFirstRender && !lastIds.has(entry.id);
|
|
435
|
+
$group.append(this.#buildItem(entry, isNew));
|
|
436
|
+
}
|
|
437
|
+
$host.append($group);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
this.#lastIds = currentIds;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
#clearAllWithCascade() {
|
|
444
|
+
const $items = this.#$host.find(".timeline_item");
|
|
445
|
+
if ($items.length === 0) {
|
|
446
|
+
EstreTimelineStore.clear();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const stagger = 50;
|
|
450
|
+
const maxDelay = 400;
|
|
451
|
+
$items.each(function (i) {
|
|
452
|
+
const delay = Math.min(i * stagger, maxDelay);
|
|
453
|
+
$(this).css("--exit-delay", delay + "ms").addClass("timeline_item_exit");
|
|
454
|
+
});
|
|
455
|
+
setTimeout(() => EstreTimelineStore.clear(), maxDelay + 300);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#groupByDate(entries) {
|
|
459
|
+
const now = new Date();
|
|
460
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
461
|
+
const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000;
|
|
462
|
+
|
|
463
|
+
const today = [], yesterday = [], older = [];
|
|
464
|
+
for (const entry of entries) {
|
|
465
|
+
const t = entry.postedAt ?? 0;
|
|
466
|
+
if (t >= startOfToday) today.push(entry);
|
|
467
|
+
else if (t >= startOfYesterday) yesterday.push(entry);
|
|
468
|
+
else older.push(entry);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const groups = [];
|
|
472
|
+
if (today.length) groups.push({ label: "Today", entries: today });
|
|
473
|
+
if (yesterday.length) groups.push({ label: "Yesterday", entries: yesterday });
|
|
474
|
+
if (older.length) groups.push({ label: "Older", entries: older });
|
|
475
|
+
return groups;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
#buildItem(entry, isNew = false) {
|
|
479
|
+
const $item = $('<div class="h_icon_set post_block timeline_item"></div>');
|
|
480
|
+
$item.attr("data-id", entry.id);
|
|
481
|
+
if (entry.bgColor) $item.css("--bg-color", entry.bgColor);
|
|
482
|
+
if (isNew) $item.addClass("timeline_item_enter");
|
|
483
|
+
|
|
484
|
+
if (entry.largeIconSrc) {
|
|
485
|
+
const $mainIcon = $('<div class="icon_place"></div>');
|
|
486
|
+
$mainIcon.append($('<img />').attr("src", entry.largeIconSrc));
|
|
487
|
+
$item.append($mainIcon);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const $content = $('<div class="content_place"></div>');
|
|
491
|
+
if (entry.title) $content.append($('<div class="title_line"></div>').append($('<span></span>').text(entry.title)));
|
|
492
|
+
if (entry.subtitle) $content.append($('<div class="subtitle_line"></div>').append($('<span></span>').text(entry.subtitle)));
|
|
493
|
+
if (entry.body || entry.iconSrc) {
|
|
494
|
+
const $area = $('<div class="content_area"></div>');
|
|
495
|
+
if (entry.body) {
|
|
496
|
+
const $body = $('<div class="content_place"></div>').html(entry.body);
|
|
497
|
+
if (entry.textColor) $body.css("--color", entry.textColor);
|
|
498
|
+
$area.append($body);
|
|
499
|
+
}
|
|
500
|
+
if (entry.iconSrc) {
|
|
501
|
+
const $subIcon = $('<div class="icon_place"></div>');
|
|
502
|
+
$subIcon.append($('<img />').attr("src", entry.iconSrc));
|
|
503
|
+
$area.append($subIcon);
|
|
504
|
+
}
|
|
505
|
+
$content.append($area);
|
|
506
|
+
}
|
|
507
|
+
$item.append($content);
|
|
508
|
+
|
|
509
|
+
$item.on("click", (e) => {
|
|
510
|
+
e.preventDefault();
|
|
511
|
+
if (entry.url) {
|
|
512
|
+
window.open(entry.url, "_blank", "noopener");
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Left→right swipe to delete. Opposite direction to the parent's horizontal
|
|
517
|
+
// scroll-snap (quick panel switch), so the two gestures don't collide.
|
|
518
|
+
if (typeof EstreSwipeHandler !== "undefined") {
|
|
519
|
+
new EstreSwipeHandler($item)
|
|
520
|
+
.setStopPropagation()
|
|
521
|
+
.unuseY()
|
|
522
|
+
.setThresholdX(1)
|
|
523
|
+
.setDropStrayed(false)
|
|
524
|
+
.setResponseBound($item)
|
|
525
|
+
.setOnUp(function (grabX, grabY, handled, canceled, directed) {
|
|
526
|
+
if (!handled) return;
|
|
527
|
+
if (this.handledDirection === "right" && grabX > 80) {
|
|
528
|
+
$item.css("--exit-delay", "0ms").addClass("timeline_item_exit");
|
|
529
|
+
setTimeout(() => EstreTimelineStore.remove(entry.id), 300);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return $item;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
// ======================================================================
|
|
@@ -3723,7 +3723,147 @@ class EstreUiPage {
|
|
|
3723
3723
|
|
|
3724
3724
|
},
|
|
3725
3725
|
|
|
3726
|
-
"$i&o=notification#noti@noti^": class extends EstrePageHandler {
|
|
3726
|
+
"$i&o=notification#noti@noti^": class extends EstrePageHandler {
|
|
3727
|
+
$postBlock;
|
|
3728
|
+
$mainIconPlace;
|
|
3729
|
+
$subIconPlace;
|
|
3730
|
+
$titleLine;
|
|
3731
|
+
$subtitleLine;
|
|
3732
|
+
$contentLine;
|
|
3733
|
+
swipeHandler;
|
|
3734
|
+
#closeTimer;
|
|
3735
|
+
#closing = false;
|
|
3736
|
+
|
|
3737
|
+
#applyIntentToBlock(intent) {
|
|
3738
|
+
const data = intent?.data ?? {};
|
|
3739
|
+
this.$mainIconPlace.toggle(data.largeIconSrc != null && data.largeIconSrc !== "");
|
|
3740
|
+
this.$subIconPlace.toggle(data.iconSrc != null && data.iconSrc !== "");
|
|
3741
|
+
this.$titleLine.toggle(data.contentTitle != null && data.contentTitle !== "");
|
|
3742
|
+
this.$subtitleLine.toggle(data.subtitle != null && data.subtitle !== "");
|
|
3743
|
+
this.$contentLine.toggle(data.content != null && data.content !== "");
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
#resetCloseTimer(handle) {
|
|
3747
|
+
if (this.#closeTimer != null) {
|
|
3748
|
+
clearTimeout(this.#closeTimer);
|
|
3749
|
+
this.#closeTimer = null;
|
|
3750
|
+
}
|
|
3751
|
+
const showTime = handle.intent?.data?.showTime ?? EstreNotificationManager.defaultShowTime;
|
|
3752
|
+
this.#closeTimer = setTimeout(_ => this.#beginClose(handle), showTime);
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
#beginClose(handle) {
|
|
3756
|
+
if (this.#closing) return;
|
|
3757
|
+
this.#closing = true;
|
|
3758
|
+
if (this.#closeTimer != null) {
|
|
3759
|
+
clearTimeout(this.#closeTimer);
|
|
3760
|
+
this.#closeTimer = null;
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
if (!EstreNotificationManager.hasQueued) {
|
|
3764
|
+
handle.close();
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
const $block = this.$postBlock;
|
|
3769
|
+
const blockEl = $block[0];
|
|
3770
|
+
if (blockEl == null) { handle.close(); return; }
|
|
3771
|
+
const $article = $block.parent();
|
|
3772
|
+
const articleEl = $article[0];
|
|
3773
|
+
const rect = blockEl.getBoundingClientRect();
|
|
3774
|
+
const articleRect = articleEl.getBoundingClientRect();
|
|
3775
|
+
const $ghost = $block.clone(false).removeClass("banner_incoming");
|
|
3776
|
+
$ghost.addClass("banner_ghost_exit");
|
|
3777
|
+
$ghost.css({
|
|
3778
|
+
position: "absolute",
|
|
3779
|
+
left: (rect.left - articleRect.left) + "px",
|
|
3780
|
+
top: (rect.top - articleRect.top) + "px",
|
|
3781
|
+
width: rect.width + "px",
|
|
3782
|
+
margin: 0,
|
|
3783
|
+
pointerEvents: "none",
|
|
3784
|
+
});
|
|
3785
|
+
$article.append($ghost);
|
|
3786
|
+
setTimeout(() => $ghost.remove(), 550);
|
|
3787
|
+
|
|
3788
|
+
$block.css({ visibility: "hidden", pointerEvents: "none" });
|
|
3789
|
+
|
|
3790
|
+
EstreNotificationManager.beginCheckOut(handle.intent);
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
onBring(handle) {
|
|
3794
|
+
const $host = handle.$host;
|
|
3795
|
+
this.$postBlock = $host.find(".post_block");
|
|
3796
|
+
this.$mainIconPlace = this.$postBlock.children(".icon_place");
|
|
3797
|
+
this.$titleLine = this.$postBlock.find("> .content_place > .title_line");
|
|
3798
|
+
this.$subtitleLine = this.$postBlock.find("> .content_place > .subtitle_line");
|
|
3799
|
+
this.$contentLine = this.$postBlock.find("> .content_place > .content_area > .content_place");
|
|
3800
|
+
this.$subIconPlace = this.$postBlock.find("> .content_place > .content_area > .icon_place");
|
|
3801
|
+
|
|
3802
|
+
this.#applyIntentToBlock(handle.intent);
|
|
3803
|
+
|
|
3804
|
+
EstreNotificationManager.current = handle.intent;
|
|
3805
|
+
if (window.isVerbosely) console.log("pushed", handle.intent);
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
onOpen(handle) {
|
|
3809
|
+
this.#closing = false;
|
|
3810
|
+
this.$postBlock.click((e) => {
|
|
3811
|
+
e.preventDefault();
|
|
3812
|
+
|
|
3813
|
+
if (window.isVerbosely) console.log("clicked: ", handle.intent);
|
|
3814
|
+
handle.intent?.onTakeInteraction?.(handle.intent);
|
|
3815
|
+
this.#beginClose(handle);
|
|
3816
|
+
|
|
3817
|
+
return false;
|
|
3818
|
+
});
|
|
3819
|
+
|
|
3820
|
+
const self = this;
|
|
3821
|
+
this.swipeHandler = new EstreSwipeHandler(this.$postBlock)
|
|
3822
|
+
.setStopPropagation()
|
|
3823
|
+
.setPreventDefault()
|
|
3824
|
+
.unuseX()
|
|
3825
|
+
.setThresholdY(1)
|
|
3826
|
+
.setDropStrayed(false)
|
|
3827
|
+
.setResponseBound(this.$postBlock)
|
|
3828
|
+
.setOnUp(function (grabX, grabY, handled, canceled, directed) {
|
|
3829
|
+
if (!handled) return;
|
|
3830
|
+
if (this.handledDirection === "up" && Math.abs(grabY) > 20) {
|
|
3831
|
+
self.#beginClose(handle);
|
|
3832
|
+
} else if (this.handledDirection === "down" && Math.abs(grabY) > 40) {
|
|
3833
|
+
handle.intent?.onTakeInteraction?.(handle.intent);
|
|
3834
|
+
self.#beginClose(handle);
|
|
3835
|
+
}
|
|
3836
|
+
});
|
|
3837
|
+
|
|
3838
|
+
this.#resetCloseTimer(handle);
|
|
3839
|
+
if (window.isVerbosely) console.log("showing: ", handle.intent);
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
onIntentUpdated(handle, intent) {
|
|
3843
|
+
// Queue-chain handover: previous banner's exit is playing on a
|
|
3844
|
+
// detached ghost clone; this article reuses the same DOM with
|
|
3845
|
+
// fresh content + a restart-triggered enter animation.
|
|
3846
|
+
this.#closing = false;
|
|
3847
|
+
if (intent?.data != null) this.#applyIntentToBlock(intent);
|
|
3848
|
+
EstreNotificationManager.current = handle.intent;
|
|
3849
|
+
|
|
3850
|
+
const $block = this.$postBlock;
|
|
3851
|
+
$block.css({ visibility: "", pointerEvents: "" });
|
|
3852
|
+
$block.removeClass("banner_incoming");
|
|
3853
|
+
void $block[0]?.offsetWidth;
|
|
3854
|
+
$block.addClass("banner_incoming");
|
|
3855
|
+
|
|
3856
|
+
this.#resetCloseTimer(handle);
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
onClose(handle) {
|
|
3860
|
+
if (this.#closeTimer != null) {
|
|
3861
|
+
clearTimeout(this.#closeTimer);
|
|
3862
|
+
this.#closeTimer = null;
|
|
3863
|
+
}
|
|
3864
|
+
EstreNotificationManager.checkOut(handle.intent);
|
|
3865
|
+
}
|
|
3866
|
+
},
|
|
3727
3867
|
"$i&o=notification#note@note^": class extends EstrePageHandler {
|
|
3728
3868
|
$postBlock;
|
|
3729
3869
|
|
package/serviceWorker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const INSTALLATION_VERSION_NAME = "1.
|
|
1
|
+
const INSTALLATION_VERSION_NAME = "1.4.0-r20260424";
|
|
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
|
|
|
@@ -22,7 +22,7 @@ const INSTALLATION_FILE_LIST = [
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
// Common files cache - Be changes some time but, well not changed very often
|
|
25
|
-
const CACHE_NAME_COMMON_FILES = "common-files-cache-v1-
|
|
25
|
+
const CACHE_NAME_COMMON_FILES = "common-files-cache-v1-20260424";
|
|
26
26
|
|
|
27
27
|
const COMMON_FILES_TO_CACHE = [
|
|
28
28
|
"./",
|
|
@@ -51,6 +51,7 @@ const COMMON_FILES_TO_CACHE = [
|
|
|
51
51
|
"./scripts/estreUi-core.js",
|
|
52
52
|
"./scripts/estreUi-dialog.js",
|
|
53
53
|
"./scripts/estreUi-notation.js",
|
|
54
|
+
"./scripts/estreUi-notification.js",
|
|
54
55
|
"./scripts/estreUi-pageModel.js",
|
|
55
56
|
"./scripts/estreUi-pageManager.js",
|
|
56
57
|
"./scripts/estreUi-handles.js",
|
package/styles/estreUiCore.css
CHANGED
|
@@ -129,7 +129,11 @@ nav#overwatchPanel { --grab-y: 0px; --panel-block-height: calc(100vh - var(--top
|
|
|
129
129
|
nav#overwatchPanel > header#panelHeader,
|
|
130
130
|
nav#overwatchPanel > .dynamic_section_host,
|
|
131
131
|
nav#overwatchPanel > .dynamic_section_block,
|
|
132
|
-
nav#overwatchPanel > section#panelGrabArea { pointer-events: auto; }
|
|
132
|
+
nav#overwatchPanel > section#panelGrabArea { position: relative; pointer-events: auto; }
|
|
133
|
+
nav#overwatchPanel > header#panelHeader { z-index: 4; }
|
|
134
|
+
nav#overwatchPanel > .dynamic_section_host { z-index: 3; }
|
|
135
|
+
nav#overwatchPanel > .dynamic_section_block { z-index: 2; }
|
|
136
|
+
nav#overwatchPanel > section#panelGrabArea { z-index: 1; }
|
|
133
137
|
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
138
|
nav#overwatchPanel > header#panelHeader > #panelClock { font-size: 1.125rem; font-weight: 600; }
|
|
135
139
|
nav#overwatchPanel > header#panelHeader > #panelDate { font-size: 0.875rem; opacity: 0.75; }
|
|
@@ -150,13 +154,20 @@ nav#overwatchPanel[data-opened="1"] > section#panelGrabArea { opacity: 1; backgr
|
|
|
150
154
|
nav#overwatchPanel[data-on-grab="1"] > header#panelHeader,
|
|
151
155
|
nav#overwatchPanel[data-on-grab="1"] > .dynamic_section_host,
|
|
152
156
|
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))); }
|
|
157
|
+
nav#overwatchPanel[data-on-grab="1"]:not([data-opened="1"]) > header#panelHeader { transform: translateY(calc(-100% + min(max(var(--grab-y), 0px), 100%))); }
|
|
158
|
+
nav#overwatchPanel[data-on-grab="1"]:not([data-opened="1"]) > .dynamic_section_host { transform: translateY(calc(-100% - var(--panel-block-height) + min(max(var(--grab-y), 0px), calc(100% + var(--panel-block-height))))); }
|
|
159
|
+
nav#overwatchPanel[data-on-grab="1"]:not([data-opened="1"]) > .dynamic_section_block { transform: translateY(calc(-100vh - var(--panel-block-height) + min(max(var(--grab-y), 0px), calc(100vh + var(--panel-block-height))))); }
|
|
156
160
|
nav#overwatchPanel[data-opened="1"][data-on-grab="1"] > header#panelHeader,
|
|
157
161
|
nav#overwatchPanel[data-opened="1"][data-on-grab="1"] > .dynamic_section_host,
|
|
158
162
|
nav#overwatchPanel[data-opened="1"][data-on-grab="1"] > .dynamic_section_block { transform: translateY(min(var(--grab-y), 0px)); }
|
|
159
163
|
|
|
164
|
+
/* Tablet+ (≥740 wide) — surface both block_items side-by-side instead of tab swipe. */
|
|
165
|
+
@media all and (min-width: 740px) {
|
|
166
|
+
nav#overwatchPanel > .dynamic_section_host { display: none; }
|
|
167
|
+
nav#overwatchPanel > .dynamic_section_block { scroll-snap-type: none; overflow-x: hidden; }
|
|
168
|
+
nav#overwatchPanel > .dynamic_section_block > .block_item { width: auto; flex: 1 1 0; min-width: 0; }
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
/* Top swipe trigger strip. Sits above fixedTop (z-index 130) so the downward swipe can fire while the panel is closed.
|
|
161
172
|
Height stays inside the safe area (status bar / notch) plus a small 8px bleed into fixedTop so the strip
|
|
162
173
|
does not cover tappable fixedTop controls. */
|
|
@@ -171,6 +182,37 @@ nav#overwatchPanel[data-opened="1"] ~ section#panelTrigger { pointer-events: non
|
|
|
171
182
|
#darkModeToggle[data-dark-mode-state="light"] { background-color: rgba(var(--cabr) / 50%); }
|
|
172
183
|
#darkModeToggle[data-dark-mode-state="dark"] { background-color: rgba(var(--cadm) / 50%); }
|
|
173
184
|
|
|
185
|
+
/* Timeline (overwatchPanel #timeline) — roadmap #010. Reuses .post_block visuals from estreUiCore2.css. */
|
|
186
|
+
nav#overwatchPanel #timeline { --bg-color: rgb(var(--cabr) / 70%); box-sizing: border-box; padding: 8px var(--basic-ui-inset-h) calc(8px + var(--bottom-safe-pad)); }
|
|
187
|
+
nav#overwatchPanel #timeline > .timeline_host { display: flex; flex-flow: column nowrap; gap: 10px; }
|
|
188
|
+
nav#overwatchPanel #timeline .timeline_group { display: flex; flex-flow: column nowrap; gap: 6px; }
|
|
189
|
+
nav#overwatchPanel #timeline .timeline_group_header { display: flex; align-items: center; justify-content: space-between; padding: 4px 4px 2px; font-size: 0.75rem; font-weight: 600; color: rgb(var(--ca) / 70%); letter-spacing: 0.02em; text-transform: uppercase; }
|
|
190
|
+
nav#overwatchPanel #timeline .timeline_group_header > .timeline_group_label { flex: 1 1 auto; min-width: 0; }
|
|
191
|
+
nav#overwatchPanel #timeline .timeline_clear_all { flex: 0 0 auto; margin-left: 8px; padding: 3px 10px; border: none; border-radius: 10px; background-color: rgb(var(--ca) / 10%); color: rgb(var(--ca) / 80%); font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; }
|
|
192
|
+
nav#overwatchPanel #timeline .timeline_clear_all:active { background-color: rgb(var(--ca) / 25%); }
|
|
193
|
+
nav#overwatchPanel #timeline .timeline_item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; box-sizing: border-box; border-radius: 14px; background-color: var(--bg-color); backdrop-filter: var(--basic-backdrop-blur); -webkit-backdrop-filter: var(--basic-backdrop-blur); box-shadow: 1px 2px 4px 1px rgb(var(--ca) / 18%); cursor: pointer; user-select: none; }
|
|
194
|
+
nav#overwatchPanel #timeline .timeline_item > .icon_place { flex: 0 0 auto; width: 38px; height: 38px; border-radius: 8px; overflow: hidden; display: flex; align-items: center; justify-content: center; }
|
|
195
|
+
nav#overwatchPanel #timeline .timeline_item > .icon_place > img { width: 100%; height: 100%; object-fit: cover; }
|
|
196
|
+
nav#overwatchPanel #timeline .timeline_item > .content_place { flex: 1 1 auto; min-width: 0; --color: var(--color-text); --size: 0.9375rem; --weight: 400; }
|
|
197
|
+
nav#overwatchPanel #timeline .timeline_item > .content_place > .title_line { font-size: 0.9375rem; font-weight: 600; color: var(--color-text); line-height: 1.25em; }
|
|
198
|
+
nav#overwatchPanel #timeline .timeline_item > .content_place > .subtitle_line { font-size: 0.8125rem; font-weight: 400; color: rgb(var(--ca) / 70%); line-height: 1.25em; margin-top: 1px; }
|
|
199
|
+
nav#overwatchPanel #timeline .timeline_item > .content_place > .content_area { display: flex; gap: 8px; align-items: flex-start; margin-top: 3px; }
|
|
200
|
+
nav#overwatchPanel #timeline .timeline_item > .content_place > .content_area > .content_place { flex: 1 1 auto; min-width: 0; color: var(--color); font-size: var(--size); font-weight: var(--weight); line-height: 1.3em; word-break: break-word; }
|
|
201
|
+
nav#overwatchPanel #timeline .timeline_item > .content_place > .content_area > .icon_place { flex: 0 0 auto; width: 20px; height: 20px; border-radius: 4px; overflow: hidden; }
|
|
202
|
+
nav#overwatchPanel #timeline .timeline_item > .content_place > .content_area > .icon_place > img { width: 100%; height: 100%; object-fit: cover; }
|
|
203
|
+
nav#overwatchPanel #timeline .timeline_empty { padding: 32px 16px; text-align: center; font-size: 0.875rem; color: rgb(var(--ca) / 60%); }
|
|
204
|
+
nav#overwatchPanel #timeline .timeline_item.timeline_item_enter { animation: timeline-item-enter 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) both; transform-origin: top center; }
|
|
205
|
+
@keyframes timeline-item-enter {
|
|
206
|
+
0% { opacity: 0; transform: translateY(-14px) scale(0.96); }
|
|
207
|
+
60% { opacity: 1; }
|
|
208
|
+
100% { opacity: 1; transform: translateY(0) scale(1); }
|
|
209
|
+
}
|
|
210
|
+
nav#overwatchPanel #timeline .timeline_item.timeline_item_exit { animation: timeline-item-exit 0.3s ease-in both; animation-delay: var(--exit-delay, 0ms); pointer-events: none; }
|
|
211
|
+
@keyframes timeline-item-exit {
|
|
212
|
+
0% { opacity: 1; transform: translateX(0); }
|
|
213
|
+
100% { opacity: 0; transform: translateX(60px); }
|
|
214
|
+
}
|
|
215
|
+
|
|
174
216
|
/* root tabs (bottom) */
|
|
175
217
|
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; }
|
|
176
218
|
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; }
|
package/styles/estreUiCore2.css
CHANGED
|
@@ -290,7 +290,31 @@ nav#managedOverlay > section#notification > div.container > article { position:
|
|
|
290
290
|
nav#managedOverlay > section#notification > div.container > article:not(:is([data-on-top^="1"], [data-on-top^="0"])) { display: none; }
|
|
291
291
|
nav#managedOverlay > section#notification > div.container > article:not([data-on-top="1"]) { opacity: 0; }
|
|
292
292
|
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] { top: var(--top-pad); }
|
|
293
|
-
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article { top:
|
|
293
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article { top: 0; transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94); transition-duration: 0.45s; will-change: transform, opacity; }
|
|
294
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article[data-on-top="1"] { top: 12px; transform: translateY(0); }
|
|
295
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article[data-on-top="0"] { top: 12px; transform: translateY(calc(-100% - var(--top-pad) - 12px)); }
|
|
296
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block { display: flex; align-items: center; gap: 10px; padding: 10px 14px; max-width: calc(100vw - var(--left-pad) - var(--right-pad) - 16px); min-width: min(320px, calc(100vw - var(--left-pad) - var(--right-pad) - 16px)); box-sizing: border-box; border-radius: 18px; background-color: var(--bg-color); backdrop-filter: var(--basic-backdrop-blur); -webkit-backdrop-filter: var(--basic-backdrop-blur); box-shadow: 1px 4px 8px 2px rgb(var(--ca) / 25%); cursor: pointer; }
|
|
297
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block:not([data-interactive]) { cursor: default; }
|
|
298
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block > .icon_place { flex: 0 0 auto; width: 38px; height: 38px; border-radius: 8px; overflow: hidden; display: flex; align-items: center; justify-content: center; }
|
|
299
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block > .icon_place > img { width: 100%; height: 100%; object-fit: cover; }
|
|
300
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block > .content_place { flex: 1 1 auto; min-width: 0; --color: var(--color-text); --size: 0.9375rem; --weight: 400; }
|
|
301
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block > .content_place > .title_line { font-size: 0.9375rem; font-weight: 600; color: var(--color-text); line-height: 1.25em; }
|
|
302
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block > .content_place > .subtitle_line { font-size: 0.8125rem; font-weight: 400; color: rgb(var(--ca) / 70%); line-height: 1.25em; margin-top: 1px; }
|
|
303
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block > .content_place > .content_area { display: flex; gap: 8px; align-items: flex-start; margin-top: 3px; }
|
|
304
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block > .content_place > .content_area > .content_place { flex: 1 1 auto; min-width: 0; color: var(--color); font-size: var(--size); font-weight: var(--weight); line-height: 1.3em; word-break: break-word; }
|
|
305
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block > .content_place > .content_area > .icon_place { flex: 0 0 auto; width: 20px; height: 20px; border-radius: 4px; overflow: hidden; }
|
|
306
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block > .content_place > .content_area > .icon_place > img { width: 100%; height: 100%; object-fit: cover; }
|
|
307
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block { position: relative; z-index: 1; }
|
|
308
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block.banner_incoming { animation: banner-incoming 0.45s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; }
|
|
309
|
+
nav#managedOverlay > section#notification > div.container[data-container-id="noti"] > article > .post_block.banner_ghost_exit { z-index: 0; animation: banner-ghost-exit 0.45s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; pointer-events: none; }
|
|
310
|
+
@keyframes banner-incoming {
|
|
311
|
+
0% { opacity: 0; transform: translateY(calc(-100% - var(--top-pad) - 12px)); }
|
|
312
|
+
100% { opacity: 1; transform: translateY(0); }
|
|
313
|
+
}
|
|
314
|
+
@keyframes banner-ghost-exit {
|
|
315
|
+
0% { opacity: 1; transform: translateY(0); }
|
|
316
|
+
100% { opacity: 0; transform: translateY(calc(-100% - var(--top-pad) - 12px)); }
|
|
317
|
+
}
|
|
294
318
|
nav#managedOverlay > section#notification > div.container[data-container-id="note"] { bottom: var(--bottom-pad); }
|
|
295
319
|
nav#managedOverlay > section#notification > div.container[data-container-id="note"] > article { bottom: 0; }
|
|
296
320
|
nav#managedOverlay > section#notification > div.container[data-container-id="note"] > article:is([data-on-top^="1"], [data-on-top="0"]) { }
|