dsh-desktop-kit 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +87 -2
- package/client.js +3349 -0
- package/cordis.patch.yml +5 -0
- package/index.js +80 -0
- package/package.json +60 -4
- package/plugins/dsh-gkb-ru-ui/cordis.patch.yml +6 -0
- package/plugins/dsh-gkb-ru-ui/lib/client.js +1083 -0
- package/plugins/dsh-gkb-ru-ui/lib/index.js +142 -0
- package/plugins/dsh-gkb-ru-ui/lib/skills.js +376 -0
- package/plugins/dsh-gkb-ru-ui/package.json +28 -0
- package/plugins/dsh-peak-guard/README.md +70 -0
- package/plugins/dsh-peak-guard/client.js +741 -0
- package/plugins/dsh-peak-guard/cordis.patch.yml +5 -0
- package/plugins/dsh-peak-guard/icon.svg +12 -0
- package/plugins/dsh-peak-guard/index.js +2 -0
- package/plugins/dsh-peak-guard/locale/en.json +6 -0
- package/plugins/dsh-peak-guard/locale/ru.json +6 -0
- package/plugins/dsh-peak-guard/locale/zh.json +6 -0
- package/plugins/dsh-peak-guard/package.json +33 -0
- package/plugins/dsh-profile-transfer/README.md +62 -0
- package/plugins/dsh-profile-transfer/bootstrap/01-INSTALL.cmd +7 -0
- package/plugins/dsh-profile-transfer/bootstrap/README-ARCHIVE.md +54 -0
- package/plugins/dsh-profile-transfer/bootstrap/README-/320/243/320/241/320/242/320/220/320/235/320/236/320/222/320/232/320/220.md +42 -0
- package/plugins/dsh-profile-transfer/bootstrap/install-on-clean-machine.ps1 +119 -0
- package/plugins/dsh-profile-transfer/bootstrap//320/243/320/241/320/242/320/220/320/235/320/236/320/222/320/230/320/242/320/254.cmd +5 -0
- package/plugins/dsh-profile-transfer/client.js +806 -0
- package/plugins/dsh-profile-transfer/cordis.patch.yml +3 -0
- package/plugins/dsh-profile-transfer/icon.svg +13 -0
- package/plugins/dsh-profile-transfer/index.js +375 -0
- package/plugins/dsh-profile-transfer/locale/en.json +6 -0
- package/plugins/dsh-profile-transfer/locale/ru.json +6 -0
- package/plugins/dsh-profile-transfer/locale/zh.json +6 -0
- package/plugins/dsh-profile-transfer/package.json +36 -0
- package/plugins/dsh-profile-transfer/transfer.js +1004 -0
- package/plugins/dsh-project-memory/README.md +71 -0
- package/plugins/dsh-project-memory/cordis.patch.yml +4 -0
- package/plugins/dsh-project-memory/icon.svg +12 -0
- package/plugins/dsh-project-memory/index.js +1240 -0
- package/plugins/dsh-project-memory/locale/en.json +6 -0
- package/plugins/dsh-project-memory/locale/ru.json +6 -0
- package/plugins/dsh-project-memory/locale/zh.json +6 -0
- package/plugins/dsh-project-memory/package.json +26 -0
- package/plugins/dsh-session-move/README.md +67 -0
- package/plugins/dsh-session-move/client.js +719 -0
- package/plugins/dsh-session-move/cordis.patch.yml +5 -0
- package/plugins/dsh-session-move/icon.svg +14 -0
- package/plugins/dsh-session-move/index.js +497 -0
- package/plugins/dsh-session-move/locale/en.json +6 -0
- package/plugins/dsh-session-move/locale/ru.json +6 -0
- package/plugins/dsh-session-move/locale/zh.json +6 -0
- package/plugins/dsh-session-move/package.json +31 -0
- package/skills/code-review-checklist/SKILL.md +37 -0
- package/skills/content-writing/README.md +87 -0
- package/skills/content-writing/SKILL.md +348 -0
- package/skills/content-writing/assets/checklists/fact-check.md +53 -0
- package/skills/content-writing/assets/checklists/publish.md +65 -0
- package/skills/content-writing/assets/checklists/social.md +40 -0
- package/skills/content-writing/assets/templates/article.md +47 -0
- package/skills/content-writing/assets/templates/brief.md +44 -0
- package/skills/content-writing/assets/templates/doctor-card.md +41 -0
- package/skills/content-writing/assets/templates/image-prompts.md +128 -0
- package/skills/content-writing/assets/templates/post-telegram.md +36 -0
- package/skills/content-writing/assets/templates/post-vk.md +42 -0
- package/skills/content-writing/assets/templates/press-release.md +56 -0
- package/skills/content-writing/assets/templates/science-pop-note.md +55 -0
- package/skills/content-writing/assets/templates/service-card.md +46 -0
- package/skills/content-writing/references/anti-ai-style.md +248 -0
- package/skills/content-writing/references/editing.md +185 -0
- package/skills/content-writing/references/fact-checking.md +205 -0
- package/skills/content-writing/references/government-medical.md +246 -0
- package/skills/content-writing/references/images.md +212 -0
- package/skills/content-writing/references/journalism.md +252 -0
- package/skills/content-writing/references/science-popular.md +218 -0
- package/skills/content-writing/references/seo-content.md +172 -0
- package/skills/content-writing/references/social-media.md +253 -0
- package/skills/content-writing/references/templates.md +291 -0
- package/skills/database-safety/SKILL.md +31 -0
- package/skills/debugging-playbook/SKILL.md +39 -0
- package/skills/design-critique/SKILL.md +118 -0
- package/skills/dev-workflow/SKILL.md +41 -0
- package/skills/git-and-ci/SKILL.md +40 -0
- package/skills/pg-upgrade-verify/SKILL.md +48 -0
- package/skills/project-deep-dive/SKILL.md +170 -0
- package/skills/project-deep-dive/references/recon-commands.md +78 -0
- package/skills/project-deep-dive/references/section-templates.md +102 -0
- package/skills/project-memory-maintenance/SKILL.md +170 -0
- package/skills/project-memory-maintenance/references/journal-template.md +48 -0
- package/skills/project-memory-maintenance/references/section-rules.md +57 -0
- package/skills/release-and-deploy/SKILL.md +39 -0
- package/skills/requirements-and-planning/SKILL.md +115 -0
- package/skills/security-review/SKILL.md +36 -0
- package/skills/seo/README.md +60 -0
- package/skills/seo/SKILL.md +329 -0
- package/skills/seo/assets/README.md +66 -0
- package/skills/seo/assets/scripts/LICENSE-playwright-core.txt +202 -0
- package/skills/seo/assets/scripts/README.md +179 -0
- package/skills/seo/assets/scripts/lib/args.mjs +105 -0
- package/skills/seo/assets/scripts/lib/browser.mjs +620 -0
- package/skills/seo/assets/scripts/lib/zip.mjs +191 -0
- package/skills/seo/assets/scripts/seo-audit.mjs +1675 -0
- package/skills/seo/assets/snippets/jsonld-breadcrumb.json +30 -0
- package/skills/seo/assets/snippets/jsonld-faq.json +39 -0
- package/skills/seo/assets/snippets/jsonld-medical-clinic.json +49 -0
- package/skills/seo/assets/snippets/jsonld-medical-webpage.json +33 -0
- package/skills/seo/assets/snippets/jsonld-organization.json +74 -0
- package/skills/seo/assets/snippets/jsonld-physician.json +46 -0
- package/skills/seo/assets/snippets/meta.blade.php +61 -0
- package/skills/seo/assets/snippets/robots.txt +61 -0
- package/skills/seo/assets/snippets/sitemap-urlset-example.xml +72 -0
- package/skills/seo/assets/snippets/sitemap.xml +36 -0
- package/skills/seo/references/audit-checklist.md +305 -0
- package/skills/seo/references/content-seo.md +198 -0
- package/skills/seo/references/eeat-medical.md +217 -0
- package/skills/seo/references/measurement.md +223 -0
- package/skills/seo/references/on-page.md +209 -0
- package/skills/seo/references/stack-recipes.md +693 -0
- package/skills/seo/references/structured-data.md +485 -0
- package/skills/seo/references/technical-seo.md +294 -0
- package/skills/service-desk-conventions/SKILL.md +40 -0
- package/skills/service-desk-db-safety/SKILL.md +52 -0
- package/skills/service-desk-release/SKILL.md +43 -0
- package/skills/service-desk-testing/SKILL.md +47 -0
- package/skills/service-desk-ui/SKILL.md +33 -0
- package/skills/skills-router/SKILL.md +91 -0
- package/skills/testing-strategy/SKILL.md +37 -0
- package/skills/ui-design-system/SKILL.md +45 -0
- package/skills/web-research/SKILL.md +40 -0
- package/skills/wow-design/SKILL.md +168 -0
- package/skills/wow-design/assets/kit/README.md +195 -0
- package/skills/wow-design/assets/kit/assets/css/base.css +158 -0
- package/skills/wow-design/assets/kit/assets/css/components.css +1001 -0
- package/skills/wow-design/assets/kit/assets/css/fonts.css +73 -0
- package/skills/wow-design/assets/kit/assets/css/pages.css +737 -0
- package/skills/wow-design/assets/kit/assets/css/tokens.css +218 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-golos-text.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-lora.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/LICENSE-unbounded.txt +93 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-cyrillic-700-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/golos-text-latin-700-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-italic.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/lora-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-cyrillic-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-400-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-500-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/fonts/unbounded-latin-600-normal.woff2 +0 -0
- package/skills/wow-design/assets/kit/assets/js/kit.js +550 -0
- package/skills/wow-design/assets/kit/pages/dashboard.html +539 -0
- package/skills/wow-design/assets/kit/pages/hospital.html +430 -0
- package/skills/wow-design/assets/kit/showcase.html +1164 -0
- package/skills/wow-design/assets/scripts/LICENSE-axe-core.txt +362 -0
- package/skills/wow-design/assets/scripts/LICENSE-playwright-core.txt +202 -0
- package/skills/wow-design/assets/scripts/README.md +312 -0
- package/skills/wow-design/assets/scripts/audit.mjs +1200 -0
- package/skills/wow-design/assets/scripts/capture.mjs +337 -0
- package/skills/wow-design/assets/scripts/package.json +30 -0
- package/skills/wow-design/references/anti-patterns.md +249 -0
- package/skills/wow-design/references/critique-rubric.md +259 -0
- package/skills/wow-design/references/design-brief-template.md +46 -0
- package/skills/wow-design/references/inspiration.md +200 -0
- package/skills/wow-design/references/presets.md +96 -0
- package/skills/wow-design/references/principles.md +368 -0
- package/skills/wow-design/references/stack-recipes.md +93 -0
- package/skills/wow-design/references/visual-qa.md +79 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-peak-guard — browser half.
|
|
3
|
+
*
|
|
4
|
+
* A live, dismissible peak-pricing notice in the chat.
|
|
5
|
+
*
|
|
6
|
+
* • 5 minutes before a peak window starts it warns that the 2× price is coming;
|
|
7
|
+
* • during the window it explains the price, shows the window, a live
|
|
8
|
+
* countdown with a depleting ring, and when to come back;
|
|
9
|
+
* • it only informs — it never stops the chat, closes a session or aborts a
|
|
10
|
+
* turn.
|
|
11
|
+
*
|
|
12
|
+
* Real-time: 1s ticks, exact boundary timers (warning start, peak start, peak
|
|
13
|
+
* end) and wake listeners on focus/visibility/pointer.
|
|
14
|
+
*
|
|
15
|
+
* Schedule (UTC, weekdays): 01:00–04:00 and 06:00–10:00; everything else is
|
|
16
|
+
* off-peak with a 50% discount. Weekends have no peak.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
window.__ModuleLoader__.load({
|
|
20
|
+
id: "dsh-peak-guard",
|
|
21
|
+
factory: (require) => {
|
|
22
|
+
var module = { exports: {} };
|
|
23
|
+
var exports = module.exports;
|
|
24
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
25
|
+
const React = require("react");
|
|
26
|
+
const h = React.createElement;
|
|
27
|
+
const useEffect = React.useEffect;
|
|
28
|
+
const useState = React.useState;
|
|
29
|
+
const useSyncExternalStore = React.useSyncExternalStore;
|
|
30
|
+
|
|
31
|
+
const NS = "peak-guard";
|
|
32
|
+
const WINDOWS_UTC = [
|
|
33
|
+
{ from: 1, to: 4, id: "morning" },
|
|
34
|
+
{ from: 6, to: 10, id: "day" },
|
|
35
|
+
];
|
|
36
|
+
const TICK_MS = 1000;
|
|
37
|
+
const PEAK_LEAD_MS = 5 * 60 * 1000;
|
|
38
|
+
const DISMISS_KEY = "dsh-peak-guard:dismissed-window-end";
|
|
39
|
+
const SOON_DISMISS_KEY = "dsh-peak-guard:dismissed-soon-start";
|
|
40
|
+
const ENABLED_KEY = "dsh-peak-guard:enabled";
|
|
41
|
+
const RING_RADIUS = 26;
|
|
42
|
+
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
|
|
43
|
+
|
|
44
|
+
const EN = {
|
|
45
|
+
badgeLabel: "Peak ×2",
|
|
46
|
+
badgeTitle: "Peak pricing is active now: tokens cost 2×. {wait} left in this window.",
|
|
47
|
+
soonBadgeLabel: "Peak in {wait}",
|
|
48
|
+
nowLabel: "Now",
|
|
49
|
+
kicker: "Peak pricing",
|
|
50
|
+
soonKicker: "Peak coming",
|
|
51
|
+
multiplier: "×2",
|
|
52
|
+
title: "Tokens cost double right now",
|
|
53
|
+
soonTitle: "Peak pricing starts in {wait}",
|
|
54
|
+
countdownLabel: "until this peak window ends",
|
|
55
|
+
soonCountdownLabel: "until peak pricing starts",
|
|
56
|
+
unitHour: "h",
|
|
57
|
+
unitMin: "min",
|
|
58
|
+
unitSec: "s",
|
|
59
|
+
windowLabel: "Window",
|
|
60
|
+
soonWindowLabel: "Upcoming window",
|
|
61
|
+
windowRange: "{start}–{end} {zone}",
|
|
62
|
+
reason: "During peak hours every token type costs twice as much for deepseek-flash and deepseek-v4-pro. Outside these hours the off-peak 50% discount applies.",
|
|
63
|
+
soonReason: "Off-peak pricing is still active: the 50% discount holds until the window starts. After that, tokens are billed at 2× for the whole peak window.",
|
|
64
|
+
recommendationTitle: "Best time to come back",
|
|
65
|
+
soonRecommendationTitle: "Before the price doubles",
|
|
66
|
+
recommendation: "Return after {time} — in {wait} the token price drops by half.",
|
|
67
|
+
soonRecommendation: "Finish the current step before {start}, or come back after {end} — the peak window lasts until then.",
|
|
68
|
+
scheduleTitle: "Peak schedule (weekdays)",
|
|
69
|
+
scheduleWeekend: "weekdays · weekends have no peak",
|
|
70
|
+
scheduleChipLabel: "Peak windows",
|
|
71
|
+
scheduleUtc: "UTC: {windows}",
|
|
72
|
+
scheduleLocal: "Your time ({zone}): {windows}",
|
|
73
|
+
holidayNote: "Chinese public holidays are off-peak too; only regular weekdays are shown.",
|
|
74
|
+
dismiss: "Hide for this peak",
|
|
75
|
+
soonDismiss: "Hide until the peak starts",
|
|
76
|
+
details: "Schedule",
|
|
77
|
+
hideDetails: "Collapse",
|
|
78
|
+
waitHours: "{hours} h {minutes} min",
|
|
79
|
+
waitMinutes: "{minutes} min",
|
|
80
|
+
waitSeconds: "{seconds} s",
|
|
81
|
+
};
|
|
82
|
+
const RU = {
|
|
83
|
+
badgeLabel: "Пик ×2",
|
|
84
|
+
badgeTitle: "Сейчас пиковый тариф: токены ×2. До конца окна осталось {wait}.",
|
|
85
|
+
soonBadgeLabel: "Пик через {wait}",
|
|
86
|
+
nowLabel: "Сейчас",
|
|
87
|
+
kicker: "Пиковый тариф",
|
|
88
|
+
soonKicker: "Скоро пик",
|
|
89
|
+
multiplier: "×2",
|
|
90
|
+
title: "Токены сейчас стоят вдвое дороже",
|
|
91
|
+
soonTitle: "Пиковый тариф начнётся через {wait}",
|
|
92
|
+
countdownLabel: "до конца пикового окна",
|
|
93
|
+
soonCountdownLabel: "до начала пика",
|
|
94
|
+
unitHour: "ч",
|
|
95
|
+
unitMin: "мин",
|
|
96
|
+
unitSec: "с",
|
|
97
|
+
windowLabel: "Окно",
|
|
98
|
+
soonWindowLabel: "Окно начнётся",
|
|
99
|
+
windowRange: "{start}–{end} {zone}",
|
|
100
|
+
reason: "В пиковые часы стоимость всех типов токенов для deepseek-flash и deepseek-v4-pro увеличивается вдвое. Вне этих часов действует скидка 50% (off-peak).",
|
|
101
|
+
soonReason: "Сейчас ещё действует off-peak со скидкой 50%. Когда начнётся окно, все типы токенов будут стоить ×2 до самого конца пика.",
|
|
102
|
+
recommendationTitle: "Лучшее время вернуться",
|
|
103
|
+
soonRecommendationTitle: "До подорожания",
|
|
104
|
+
recommendation: "Вернитесь после {time} — через {wait} цена токенов упадёт вдвое.",
|
|
105
|
+
soonRecommendation: "Завершите текущий шаг до {start} — или вернитесь после {end}, когда пик закончится.",
|
|
106
|
+
scheduleTitle: "Расписание пика (будни)",
|
|
107
|
+
scheduleWeekend: "будни · выходные без пика",
|
|
108
|
+
scheduleChipLabel: "Окна пика",
|
|
109
|
+
scheduleUtc: "UTC: {windows}",
|
|
110
|
+
scheduleLocal: "По вашему времени ({zone}): {windows}",
|
|
111
|
+
holidayNote: "Государственные праздники Китая тоже считаются off-peak — выше только обычные будни.",
|
|
112
|
+
dismiss: "Скрыть на этот пик",
|
|
113
|
+
soonDismiss: "Скрыть до начала пика",
|
|
114
|
+
details: "Расписание",
|
|
115
|
+
hideDetails: "Свернуть",
|
|
116
|
+
waitHours: "{hours} ч {minutes} мин",
|
|
117
|
+
waitMinutes: "{minutes} мин",
|
|
118
|
+
waitSeconds: "{seconds} с",
|
|
119
|
+
};
|
|
120
|
+
const ZH = {
|
|
121
|
+
badgeLabel: "高峰 ×2",
|
|
122
|
+
badgeTitle: "当前为高峰时段:Token 价格 ×2。本时段还剩 {wait}。",
|
|
123
|
+
soonBadgeLabel: "高峰还剩 {wait}",
|
|
124
|
+
nowLabel: "现在",
|
|
125
|
+
kicker: "高峰价格",
|
|
126
|
+
soonKicker: "高峰将至",
|
|
127
|
+
multiplier: "×2",
|
|
128
|
+
title: "当前 Token 价格为 2 倍",
|
|
129
|
+
soonTitle: "高峰价格将在 {wait} 后开始",
|
|
130
|
+
countdownLabel: "距本时段结束",
|
|
131
|
+
soonCountdownLabel: "距高峰开始",
|
|
132
|
+
unitHour: "小时",
|
|
133
|
+
unitMin: "分钟",
|
|
134
|
+
unitSec: "秒",
|
|
135
|
+
windowLabel: "时段",
|
|
136
|
+
soonWindowLabel: "即将开始",
|
|
137
|
+
windowRange: "{start}–{end} {zone}",
|
|
138
|
+
reason: "高峰时段内 deepseek-flash 与 deepseek-v4-pro 的全部 Token 价格翻倍;其余时段享受 50% 的错峰折扣。",
|
|
139
|
+
soonReason: "目前仍为错峰价格(50% 折扣)。时段开始后,全部 Token 价格将翻倍,直至本次高峰结束。",
|
|
140
|
+
recommendationTitle: "建议返回时间",
|
|
141
|
+
soonRecommendationTitle: "涨价之前",
|
|
142
|
+
recommendation: "请在 {time}({wait} 后)再回来 — Token 价格将减半。",
|
|
143
|
+
soonRecommendation: "请在 {start} 前完成当前步骤,或在 {end} 之后再回来(届时高峰结束)。",
|
|
144
|
+
scheduleTitle: "高峰时段(工作日)",
|
|
145
|
+
scheduleWeekend: "工作日 · 周末无高峰",
|
|
146
|
+
scheduleChipLabel: "高峰时段",
|
|
147
|
+
scheduleUtc: "UTC:{windows}",
|
|
148
|
+
scheduleLocal: "本地时间({zone}):{windows}",
|
|
149
|
+
holidayNote: "中国法定节假日同样按错峰计费;上表仅适用于普通工作日。",
|
|
150
|
+
dismiss: "本次高峰不再提示",
|
|
151
|
+
soonDismiss: "高峰开始前不再提示",
|
|
152
|
+
details: "时间表",
|
|
153
|
+
hideDetails: "收起",
|
|
154
|
+
waitHours: "{hours} 小时 {minutes} 分钟",
|
|
155
|
+
waitMinutes: "{minutes} 分钟",
|
|
156
|
+
waitSeconds: "{seconds} 秒",
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const inject = ["slots", "locale"];
|
|
160
|
+
|
|
161
|
+
function pad(value) {
|
|
162
|
+
return String(value).padStart(2, "0");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatTemplate(template, params) {
|
|
166
|
+
return String(template).replace(/\{(\w+)\}/g, (match, name) =>
|
|
167
|
+
params && Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Render "{start}…{end}…" with React nodes inside the translated sentence. */
|
|
172
|
+
function renderTemplate(template, values) {
|
|
173
|
+
const pattern = /\{(\w+)\}/g;
|
|
174
|
+
const parts = [];
|
|
175
|
+
let lastIndex = 0;
|
|
176
|
+
let match;
|
|
177
|
+
let index = 0;
|
|
178
|
+
while ((match = pattern.exec(template)) !== null) {
|
|
179
|
+
if (match.index > lastIndex) parts.push(template.slice(lastIndex, match.index));
|
|
180
|
+
const value = values?.[match[1]];
|
|
181
|
+
parts.push(React.isValidElement(value) ? React.cloneElement(value, { key: `v-${index}` }) : value);
|
|
182
|
+
lastIndex = match.index + match[0].length;
|
|
183
|
+
index += 1;
|
|
184
|
+
}
|
|
185
|
+
if (lastIndex < template.length) parts.push(template.slice(lastIndex));
|
|
186
|
+
return parts;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function timeZoneLabel(t) {
|
|
190
|
+
try {
|
|
191
|
+
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
192
|
+
if (zone === "Europe/Moscow") return t("waitSeconds") === "{seconds} с" ? "МСК" : "MSK";
|
|
193
|
+
return zone;
|
|
194
|
+
} catch {
|
|
195
|
+
return "local";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function formatClock(date) {
|
|
200
|
+
try {
|
|
201
|
+
return new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", hour12: false }).format(date);
|
|
202
|
+
} catch {
|
|
203
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function formatRange(start, end) {
|
|
208
|
+
return `${formatClock(start)}–${formatClock(end)}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Peak window containing `date`, or null. Weekdays are evaluated in UTC. */
|
|
212
|
+
function peakWindowAt(date) {
|
|
213
|
+
const day = date.getUTCDay();
|
|
214
|
+
if (day === 0 || day === 6) return null;
|
|
215
|
+
const minutes = date.getUTCHours() * 60 + date.getUTCMinutes() + date.getUTCSeconds() / 60;
|
|
216
|
+
for (const window of WINDOWS_UTC) {
|
|
217
|
+
if (minutes >= window.from * 60 && minutes < window.to * 60) {
|
|
218
|
+
const year = date.getUTCFullYear();
|
|
219
|
+
const month = date.getUTCMonth();
|
|
220
|
+
const dayOfMonth = date.getUTCDate();
|
|
221
|
+
return {
|
|
222
|
+
id: window.id,
|
|
223
|
+
start: Date.UTC(year, month, dayOfMonth, window.from, 0, 0),
|
|
224
|
+
end: Date.UTC(year, month, dayOfMonth, window.to, 0, 0),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Next peak window start strictly after `date`. */
|
|
232
|
+
function nextPeakWindow(date) {
|
|
233
|
+
const probe = new Date(date.getTime());
|
|
234
|
+
for (let index = 0; index < 8; index += 1) {
|
|
235
|
+
const day = probe.getUTCDay();
|
|
236
|
+
if (day !== 0 && day !== 6) {
|
|
237
|
+
for (const window of WINDOWS_UTC) {
|
|
238
|
+
const start = Date.UTC(probe.getUTCFullYear(), probe.getUTCMonth(), probe.getUTCDate(), window.from, 0, 0);
|
|
239
|
+
if (start > date.getTime()) {
|
|
240
|
+
return { start, end: Date.UTC(probe.getUTCFullYear(), probe.getUTCMonth(), probe.getUTCDate(), window.to, 0, 0) };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
probe.setUTCDate(probe.getUTCDate() + 1);
|
|
245
|
+
probe.setUTCHours(0, 0, 0, 0);
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function waitParts(ms) {
|
|
251
|
+
const totalMinutes = Math.max(1, Math.ceil(ms / 60000));
|
|
252
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
253
|
+
const minutes = totalMinutes % 60;
|
|
254
|
+
return { hours, minutes, totalMinutes, ms };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function waitLabel(parts, t) {
|
|
258
|
+
if (parts.ms !== undefined && parts.ms < 60000) return formatTemplate(t("waitSeconds"), { seconds: Math.max(1, Math.ceil(parts.ms / 1000)) });
|
|
259
|
+
if (parts.hours > 0) return formatTemplate(t("waitHours"), { hours: parts.hours, minutes: parts.minutes });
|
|
260
|
+
return formatTemplate(t("waitMinutes"), { minutes: parts.minutes });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function scheduleStrings() {
|
|
264
|
+
const local = WINDOWS_UTC.map((entry) =>
|
|
265
|
+
formatRange(new Date(Date.UTC(2000, 0, 3, entry.from)), new Date(Date.UTC(2000, 0, 3, entry.to))),
|
|
266
|
+
);
|
|
267
|
+
const utc = WINDOWS_UTC.map((entry) => `${pad(entry.from)}:00–${pad(entry.to)}:00`);
|
|
268
|
+
return { local, utc, localText: local.join(", "), utcText: utc.join(", ") };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function buildState(now, t) {
|
|
272
|
+
const active = peakWindowAt(now);
|
|
273
|
+
const schedule = scheduleStrings();
|
|
274
|
+
const zone = timeZoneLabel(t);
|
|
275
|
+
const base = {
|
|
276
|
+
now,
|
|
277
|
+
zone,
|
|
278
|
+
leadMs: PEAK_LEAD_MS,
|
|
279
|
+
scheduleUtc: schedule.utcText,
|
|
280
|
+
scheduleLocal: schedule.localText,
|
|
281
|
+
scheduleLocalParts: schedule.local,
|
|
282
|
+
scheduleUtcParts: schedule.utc,
|
|
283
|
+
};
|
|
284
|
+
if (active !== null) {
|
|
285
|
+
const total = active.end - active.start;
|
|
286
|
+
const elapsed = now.getTime() - active.start;
|
|
287
|
+
const progress = total > 0 ? Math.min(1, Math.max(0, elapsed / total)) : 1;
|
|
288
|
+
const parts = waitParts(active.end - now.getTime());
|
|
289
|
+
return {
|
|
290
|
+
...base,
|
|
291
|
+
mode: "peak",
|
|
292
|
+
inPeak: true,
|
|
293
|
+
soon: false,
|
|
294
|
+
windowStart: active.start,
|
|
295
|
+
windowEnd: active.end,
|
|
296
|
+
remainingMs: active.end - now.getTime(),
|
|
297
|
+
progress,
|
|
298
|
+
ringOffset: RING_CIRCUMFERENCE * progress,
|
|
299
|
+
wait: waitLabel(parts, t),
|
|
300
|
+
waitParts: parts,
|
|
301
|
+
startLabel: formatClock(new Date(active.start)),
|
|
302
|
+
endLabel: formatClock(new Date(active.end)),
|
|
303
|
+
nowLabel: formatClock(now),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const next = nextPeakWindow(now);
|
|
307
|
+
const startsInMs = next === null ? null : next.start - now.getTime();
|
|
308
|
+
if (next !== null && startsInMs !== null && startsInMs > 0 && startsInMs <= PEAK_LEAD_MS) {
|
|
309
|
+
const progress = Math.min(1, Math.max(0, 1 - startsInMs / PEAK_LEAD_MS));
|
|
310
|
+
const parts = waitParts(startsInMs);
|
|
311
|
+
return {
|
|
312
|
+
...base,
|
|
313
|
+
mode: "soon",
|
|
314
|
+
inPeak: false,
|
|
315
|
+
soon: true,
|
|
316
|
+
windowStart: next.start,
|
|
317
|
+
windowEnd: next.end,
|
|
318
|
+
nextStart: next.start,
|
|
319
|
+
nextEnd: next.end,
|
|
320
|
+
startsInMs,
|
|
321
|
+
progress,
|
|
322
|
+
ringOffset: RING_CIRCUMFERENCE * progress,
|
|
323
|
+
wait: waitLabel(parts, t),
|
|
324
|
+
waitParts: parts,
|
|
325
|
+
startLabel: formatClock(new Date(next.start)),
|
|
326
|
+
endLabel: formatClock(new Date(next.end)),
|
|
327
|
+
nowLabel: formatClock(now),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
...base,
|
|
332
|
+
mode: "off",
|
|
333
|
+
inPeak: false,
|
|
334
|
+
soon: false,
|
|
335
|
+
nextStart: next?.start ?? null,
|
|
336
|
+
nextEnd: next?.end ?? null,
|
|
337
|
+
startsInMs,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function createClockStore() {
|
|
342
|
+
let forcedNow = null;
|
|
343
|
+
let now = new Date();
|
|
344
|
+
const listeners = new Set();
|
|
345
|
+
const emit = () => {
|
|
346
|
+
for (const listener of [...listeners]) {
|
|
347
|
+
try {
|
|
348
|
+
listener();
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error("[peak-guard] listener failed", error);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
return {
|
|
355
|
+
subscribe(listener) {
|
|
356
|
+
listeners.add(listener);
|
|
357
|
+
return () => listeners.delete(listener);
|
|
358
|
+
},
|
|
359
|
+
getNow() {
|
|
360
|
+
return forcedNow ?? now;
|
|
361
|
+
},
|
|
362
|
+
tick() {
|
|
363
|
+
now = new Date();
|
|
364
|
+
emit();
|
|
365
|
+
},
|
|
366
|
+
force(iso) {
|
|
367
|
+
forcedNow = iso === null || iso === undefined ? null : new Date(iso);
|
|
368
|
+
emit();
|
|
369
|
+
},
|
|
370
|
+
getVersion() {
|
|
371
|
+
return now.getTime() + (forcedNow?.getTime?.() ?? 0);
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function usePeakState(store, t) {
|
|
377
|
+
const version = useSyncExternalStore(store.subscribe, store.getVersion, store.getVersion);
|
|
378
|
+
return React.useMemo(() => buildState(store.getNow(), t), [version, t]);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function apply(ctx) {
|
|
382
|
+
const t = ctx.locale.bind(NS);
|
|
383
|
+
const store = createClockStore();
|
|
384
|
+
|
|
385
|
+
ctx.effect(() => ctx.locale.register(NS, { en: EN, zh: ZH }), "peak-guard: dictionaries");
|
|
386
|
+
ctx.effect(() => {
|
|
387
|
+
try {
|
|
388
|
+
return ctx.locale.register(NS, "ru", RU);
|
|
389
|
+
} catch {
|
|
390
|
+
return () => {};
|
|
391
|
+
}
|
|
392
|
+
}, "peak-guard: ru dictionary");
|
|
393
|
+
|
|
394
|
+
// Real-time clock: every second refreshes the countdown; a boundary timer
|
|
395
|
+
// fires exactly at the 5-minute warning start, the peak start and the peak
|
|
396
|
+
// end; focus/visibility/pointer wake it after background throttling.
|
|
397
|
+
ctx.effect(() => {
|
|
398
|
+
let boundaryTimer = null;
|
|
399
|
+
const scheduleBoundary = () => {
|
|
400
|
+
if (boundaryTimer !== null) clearTimeout(boundaryTimer);
|
|
401
|
+
const now = store.getNow();
|
|
402
|
+
const active = peakWindowAt(now);
|
|
403
|
+
const next = nextPeakWindow(now);
|
|
404
|
+
let boundary = null;
|
|
405
|
+
if (active !== null) {
|
|
406
|
+
boundary = active.end;
|
|
407
|
+
} else if (next !== null) {
|
|
408
|
+
const warningAt = next.start - PEAK_LEAD_MS;
|
|
409
|
+
boundary = warningAt > now.getTime() ? warningAt : next.start;
|
|
410
|
+
}
|
|
411
|
+
if (boundary === null) return;
|
|
412
|
+
const delay = Math.max(200, boundary - now.getTime() + 150);
|
|
413
|
+
boundaryTimer = setTimeout(() => {
|
|
414
|
+
boundaryTimer = null;
|
|
415
|
+
store.tick();
|
|
416
|
+
scheduleBoundary();
|
|
417
|
+
}, Math.min(delay, 3600000));
|
|
418
|
+
};
|
|
419
|
+
const tickNow = () => {
|
|
420
|
+
store.tick();
|
|
421
|
+
scheduleBoundary();
|
|
422
|
+
};
|
|
423
|
+
const timer = setInterval(tickNow, TICK_MS);
|
|
424
|
+
const onWake = () => {
|
|
425
|
+
if (!document.hidden) tickNow();
|
|
426
|
+
};
|
|
427
|
+
document.addEventListener("visibilitychange", onWake);
|
|
428
|
+
window.addEventListener("focus", onWake);
|
|
429
|
+
window.addEventListener("pointerdown", onWake);
|
|
430
|
+
scheduleBoundary();
|
|
431
|
+
return () => {
|
|
432
|
+
clearInterval(timer);
|
|
433
|
+
if (boundaryTimer !== null) clearTimeout(boundaryTimer);
|
|
434
|
+
document.removeEventListener("visibilitychange", onWake);
|
|
435
|
+
window.removeEventListener("focus", onWake);
|
|
436
|
+
window.removeEventListener("pointerdown", onWake);
|
|
437
|
+
};
|
|
438
|
+
}, "peak-guard: realtime clock");
|
|
439
|
+
|
|
440
|
+
ctx.effect(() => {
|
|
441
|
+
const style = document.createElement("style");
|
|
442
|
+
style.id = "dsh-peak-guard-style";
|
|
443
|
+
style.textContent = `
|
|
444
|
+
.dpg-card{--dpg-red:var(--dsw-alias-state-error-primary,#e5484d);--dpg-red-deep:#b4232b;--dpg-ink:var(--dsw-alias-label-primary,#111);--dpg-muted:var(--dsw-alias-label-secondary,#5b6472);position:relative;display:flex;flex-direction:column;gap:14px;margin-top:10px;padding:16px 18px 15px;border-radius:16px;border:1px solid color-mix(in srgb,var(--dpg-red) 26%,transparent);background:
|
|
445
|
+
radial-gradient(120% 160% at 100% 0%,color-mix(in srgb,var(--dpg-red) 14%,transparent) 0%,transparent 52%),
|
|
446
|
+
linear-gradient(155deg,color-mix(in srgb,var(--dpg-red) 7%,var(--dsw-alias-bg-layer-1,#fff)) 0%,var(--dsw-alias-bg-layer-1,#fff) 58%),
|
|
447
|
+
var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff 55%,transparent),0 20px 44px -30px color-mix(in srgb,var(--dpg-red) 70%,transparent);overflow:hidden;animation:dpg-in .34s cubic-bezier(.16,1,.3,1)}
|
|
448
|
+
.dpg-card[data-mode="soon"]{--dpg-red:var(--dsw-alias-state-warn-primary,#d97706);--dpg-red-deep:#98540a}
|
|
449
|
+
.dpg-card::before{content:"";position:absolute;left:10px;top:10px;width:13px;height:13px;border-left:1px solid color-mix(in srgb,var(--dpg-red) 55%,transparent);border-top:1px solid color-mix(in srgb,var(--dpg-red) 55%,transparent);pointer-events:none}
|
|
450
|
+
.dpg-card::after{content:"";position:absolute;right:10px;bottom:10px;width:13px;height:13px;border-right:1px solid color-mix(in srgb,var(--dpg-red) 55%,transparent);border-bottom:1px solid color-mix(in srgb,var(--dpg-red) 55%,transparent);pointer-events:none}
|
|
451
|
+
.dpg-top{display:flex;align-items:flex-start;gap:14px}
|
|
452
|
+
.dpg-seal{position:relative;flex:none;width:62px;height:62px;display:flex;align-items:center;justify-content:center}
|
|
453
|
+
.dpg-ring{position:absolute;inset:0;width:62px;height:62px;transform:rotate(-90deg)}
|
|
454
|
+
.dpg-ring-track{fill:none;stroke:color-mix(in srgb,var(--dpg-red) 18%,transparent);stroke-width:2.5}
|
|
455
|
+
.dpg-ring-fill{fill:none;stroke:var(--dpg-red);stroke-width:2.5;stroke-linecap:round;stroke-dasharray:${RING_CIRCUMFERENCE.toFixed(2)};transition:stroke-dashoffset .8s linear,stroke .3s ease}
|
|
456
|
+
.dpg-seal-icon{position:relative;display:flex;align-items:center;justify-content:center;width:38px;height:38px;border-radius:50%;color:#fff;background:linear-gradient(150deg,var(--dpg-red) 0%,color-mix(in srgb,var(--dpg-red) 55%,#4a070c) 100%);box-shadow:0 6px 16px -8px color-mix(in srgb,var(--dpg-red) 90%,transparent),inset 0 1px 0 rgba(255,255,255,.28)}
|
|
457
|
+
.dpg-card[data-mode="soon"] .dpg-seal-icon{background:linear-gradient(150deg,var(--dpg-red) 0%,color-mix(in srgb,var(--dpg-red) 55%,#3d2400) 100%)}
|
|
458
|
+
.dpg-seal-pulse{position:absolute;left:50%;top:50%;width:38px;height:38px;margin:-19px 0 0 -19px;border-radius:50%;border:1px solid color-mix(in srgb,var(--dpg-red) 45%,transparent);animation:dpg-ping 2.6s cubic-bezier(.2,.6,.3,1) infinite;pointer-events:none}
|
|
459
|
+
.dpg-copy{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1}
|
|
460
|
+
.dpg-kicker{display:flex;align-items:center;gap:8px;font-size:10.5px;font-weight:750;letter-spacing:.16em;text-transform:uppercase;color:var(--dpg-red-deep)}
|
|
461
|
+
.dpg-dot{width:6px;height:6px;border-radius:50%;background:var(--dpg-red);box-shadow:0 0 0 3px color-mix(in srgb,var(--dpg-red) 16%,transparent);animation:dpg-pulse 2.4s ease-in-out infinite}
|
|
462
|
+
.dpg-mult{display:inline-flex;align-items:center;justify-content:center;height:17px;padding:0 6px;border-radius:5px;border:1px solid color-mix(in srgb,var(--dpg-red) 45%,transparent);background:color-mix(in srgb,var(--dpg-red) 10%,transparent);color:var(--dpg-red-deep);font-size:10px;font-weight:800;letter-spacing:.04em}
|
|
463
|
+
.dpg-title{font-size:15.5px;line-height:21px;font-weight:680;letter-spacing:-.01em;color:var(--dpg-ink)}
|
|
464
|
+
.dpg-reason{font-size:11.5px;line-height:16px;color:var(--dpg-muted);max-width:62ch}
|
|
465
|
+
.dpg-x{position:absolute;right:11px;top:11px;border:none;background:transparent;color:var(--dpg-muted);cursor:pointer;border-radius:8px;width:26px;height:26px;display:flex;align-items:center;justify-content:center;font-size:16px;line-height:1;z-index:2}
|
|
466
|
+
.dpg-x:hover{background:color-mix(in srgb,var(--dpg-red) 10%,transparent);color:var(--dpg-red-deep)}
|
|
467
|
+
.dpg-x:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f7df6);outline-offset:1px}
|
|
468
|
+
.dpg-mid{display:flex;align-items:flex-end;gap:18px}
|
|
469
|
+
.dpg-count{display:flex;flex-direction:column;gap:1px;flex:none;min-width:118px}
|
|
470
|
+
.dpg-count-num{display:flex;align-items:baseline;gap:4px;color:var(--dpg-ink);font-variant-numeric:tabular-nums;letter-spacing:-.035em;line-height:1}
|
|
471
|
+
.dpg-count-num b{font-size:34px;font-weight:760;color:var(--dpg-red-deep)}
|
|
472
|
+
.dpg-count-num span{font-size:12px;font-weight:650;letter-spacing:.02em;color:var(--dpg-muted)}
|
|
473
|
+
.dpg-count-label{font-size:10px;font-weight:650;letter-spacing:.1em;text-transform:uppercase;color:var(--dpg-muted);margin-top:5px}
|
|
474
|
+
.dpg-meter{display:flex;flex-direction:column;gap:5px;flex:1;min-width:0;padding-bottom:5px}
|
|
475
|
+
.dpg-meter-head{display:flex;justify-content:space-between;align-items:baseline;gap:10px;font-size:10.5px;line-height:14px;color:var(--dpg-muted);font-variant-numeric:tabular-nums}
|
|
476
|
+
.dpg-meter-head b{color:var(--dpg-ink);font-weight:650}
|
|
477
|
+
.dpg-track{position:relative;height:8px;border-radius:3px;background:color-mix(in srgb,var(--dpg-red) 12%,transparent);overflow:hidden}
|
|
478
|
+
.dpg-track::after{content:"";position:absolute;inset:0;background:repeating-linear-gradient(90deg,transparent 0 13px,color-mix(in srgb,var(--dsw-alias-bg-layer-1,#fff) 88%,transparent) 13px 14px);opacity:.9;pointer-events:none}
|
|
479
|
+
.dpg-fill{height:100%;border-radius:3px;background:linear-gradient(90deg,color-mix(in srgb,var(--dpg-red) 72%,#ffb3b6) 0%,var(--dpg-red) 55%,var(--dpg-red-deep) 100%);transition:width .9s linear;box-shadow:0 0 12px -2px color-mix(in srgb,var(--dpg-red) 60%,transparent)}
|
|
480
|
+
.dpg-rec{display:flex;align-items:flex-start;gap:11px;padding:10px 12px;border-radius:11px;border:1px solid color-mix(in srgb,var(--dpg-red) 22%,transparent);background:linear-gradient(120deg,color-mix(in srgb,var(--dpg-red) 9%,transparent),transparent 70%)}
|
|
481
|
+
.dpg-rec-mark{flex:none;width:22px;height:22px;border-radius:7px;display:flex;align-items:center;justify-content:center;color:#fff;background:var(--dpg-red);font-size:12px;font-weight:800;box-shadow:0 4px 10px -5px color-mix(in srgb,var(--dpg-red) 90%,transparent)}
|
|
482
|
+
.dpg-rec-body{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
483
|
+
.dpg-rec-label{font-size:9.5px;font-weight:750;letter-spacing:.14em;text-transform:uppercase;color:var(--dpg-red-deep)}
|
|
484
|
+
.dpg-rec-text{font-size:12.5px;line-height:17px;color:var(--dpg-ink);font-weight:560}
|
|
485
|
+
.dpg-rec-text b{color:var(--dpg-red-deep);font-weight:760;font-variant-numeric:tabular-nums}
|
|
486
|
+
.dpg-tail{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;flex-wrap:wrap}
|
|
487
|
+
.dpg-schedule{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
|
|
488
|
+
.dpg-schedule-label{font-size:9.5px;font-weight:750;letter-spacing:.13em;text-transform:uppercase;color:var(--dpg-muted);margin-right:2px}
|
|
489
|
+
.dpg-chip{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:6px;border:1px solid color-mix(in srgb,var(--dpg-red) 24%,transparent);background:color-mix(in srgb,var(--dpg-red) 7%,transparent);color:var(--dpg-ink);font-size:10.5px;font-weight:650;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
490
|
+
.dpg-schedule-note{font-size:10px;line-height:14px;color:var(--dpg-muted)}
|
|
491
|
+
.dpg-actions{display:flex;gap:6px;flex:none}
|
|
492
|
+
.dpg-btn{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.16));background:transparent;color:var(--dpg-ink);font:inherit;font-size:11px;font-weight:650;line-height:15px;padding:6px 11px;border-radius:8px;cursor:pointer;white-space:nowrap;transition:background .18s ease,border-color .18s ease}
|
|
493
|
+
.dpg-btn:hover{background:color-mix(in srgb,var(--dpg-red) 8%,transparent);border-color:color-mix(in srgb,var(--dpg-red) 38%,transparent)}
|
|
494
|
+
.dpg-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f7df6);outline-offset:1px}
|
|
495
|
+
.dpg-btn[data-emphasis="true"]{border-color:transparent;background:var(--dpg-red-deep);color:#fff}
|
|
496
|
+
.dpg-btn[data-emphasis="true"]:hover{background:color-mix(in srgb,var(--dpg-red-deep) 88%,#000)}
|
|
497
|
+
.dpg-details{display:grid;grid-template-columns:1fr 1fr;gap:6px 18px;padding-top:2px;font-size:10.5px;line-height:15px;color:var(--dpg-muted)}
|
|
498
|
+
.dpg-details b{color:var(--dpg-ink);font-weight:650;font-variant-numeric:tabular-nums}
|
|
499
|
+
.dpg-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 42%,transparent);background:linear-gradient(120deg,color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 12%,transparent),color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 5%,transparent));color:var(--dsw-alias-state-error-primary,#c62828);font-size:11px;font-weight:680;line-height:14px;font-variant-numeric:tabular-nums;white-space:nowrap;cursor:default}
|
|
500
|
+
.dpg-badge[data-mode="soon"]{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#d97706) 45%,transparent);background:linear-gradient(120deg,color-mix(in srgb,var(--dsw-alias-state-warn-primary,#d97706) 12%,transparent),color-mix(in srgb,var(--dsw-alias-state-warn-primary,#d97706) 5%,transparent));color:var(--dsw-alias-state-warn-primary,#b45309)}
|
|
501
|
+
.dpg-badge .dpg-dot{width:5px;height:5px;box-shadow:none}
|
|
502
|
+
.dpg-badge-wait{opacity:.85}
|
|
503
|
+
@keyframes dpg-pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
504
|
+
@keyframes dpg-ping{0%{transform:scale(.92);opacity:.7}70%,100%{transform:scale(1.5);opacity:0}}
|
|
505
|
+
@keyframes dpg-in{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}
|
|
506
|
+
@media (prefers-color-scheme:dark){
|
|
507
|
+
.dpg-card{--dpg-red-deep:#ff9b9f;--dpg-red:var(--dsw-alias-state-error-primary,#ff7a80);box-shadow:inset 0 1px 0 rgba(255,255,255,.06),0 24px 50px -34px rgba(0,0,0,.9)}
|
|
508
|
+
.dpg-card[data-mode="soon"]{--dpg-red-deep:#ffd08a;--dpg-red:var(--dsw-alias-state-warn-primary,#f5b45b)}
|
|
509
|
+
.dpg-seal-icon{color:#2a0709;background:linear-gradient(150deg,var(--dpg-red) 0%,color-mix(in srgb,var(--dpg-red) 55%,#7d1218) 100%)}
|
|
510
|
+
.dpg-card[data-mode="soon"] .dpg-seal-icon{color:#2a1500;background:linear-gradient(150deg,var(--dpg-red) 0%,color-mix(in srgb,var(--dpg-red) 55%,#4a2c00) 100%)}
|
|
511
|
+
.dpg-rec-mark{color:#2a0709}
|
|
512
|
+
.dpg-count-num b{color:var(--dpg-red)}
|
|
513
|
+
.dpg-kicker,.dpg-rec-label,.dpg-mult{color:var(--dpg-red)}
|
|
514
|
+
.dpg-btn[data-emphasis="true"]{background:var(--dpg-red);color:#2a0709}
|
|
515
|
+
}
|
|
516
|
+
@media (max-width:1100px){.dpg-badge-wait{display:none}}
|
|
517
|
+
@media (max-width:900px){.dpg-badge{display:none}.dpg-mid{align-items:stretch;flex-direction:column;gap:8px}.dpg-count{min-width:0;flex-direction:row;align-items:baseline;gap:8px}.dpg-count-label{margin-top:0}.dpg-meter{padding-bottom:0}.dpg-meter-head{flex-wrap:wrap;row-gap:2px}.dpg-tail{align-items:stretch;flex-direction:column}.dpg-actions{display:flex}.dpg-actions .dpg-btn{flex:1;text-align:center}.dpg-seal{width:52px;height:52px}.dpg-ring{width:52px;height:52px}.dpg-title{font-size:14.5px;line-height:20px}}
|
|
518
|
+
@media (max-width:520px){.dpg-card{position:fixed;left:10px;right:10px;bottom:calc(12px + env(safe-area-inset-bottom,0px));z-index:1200;max-height:78vh;overflow:auto;margin:0;padding:14px 15px 13px;gap:12px}.dpg-seal{display:none}.dpg-details{grid-template-columns:1fr}}
|
|
519
|
+
@media (prefers-reduced-motion:reduce){.dpg-card{animation:none}.dpg-dot,.dpg-seal-pulse{animation:none}.dpg-fill,.dpg-ring-fill{transition:none}}
|
|
520
|
+
`;
|
|
521
|
+
document.head.appendChild(style);
|
|
522
|
+
return () => style.remove();
|
|
523
|
+
}, "peak-guard: styles");
|
|
524
|
+
|
|
525
|
+
function IconBolt() {
|
|
526
|
+
return h("svg", { viewBox: "0 0 20 20", width: 19, height: 19, "aria-hidden": "true", focusable: "false" },
|
|
527
|
+
h("path", { d: "M11.2 2.6 5.8 11h4.1l-1.1 6.4 5.4-8.4H10l1.2-6.4Z", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinejoin: "round", strokeLinecap: "round" }),
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function HeaderBadge() {
|
|
532
|
+
const state = usePeakState(store, t);
|
|
533
|
+
if (state.inPeak) {
|
|
534
|
+
return h("span", {
|
|
535
|
+
className: "dpg-badge",
|
|
536
|
+
title: formatTemplate(t("badgeTitle"), { wait: state.wait }),
|
|
537
|
+
"aria-label": formatTemplate(t("badgeTitle"), { wait: state.wait }),
|
|
538
|
+
}, h("span", { className: "dpg-dot", "aria-hidden": "true" }), t("badgeLabel"), h("span", { className: "dpg-badge-wait" }, `· ${state.wait}`));
|
|
539
|
+
}
|
|
540
|
+
if (state.soon) {
|
|
541
|
+
const label = formatTemplate(t("soonBadgeLabel"), { wait: state.wait });
|
|
542
|
+
return h("span", { className: "dpg-badge", "data-mode": "soon", title: label, "aria-label": label },
|
|
543
|
+
h("span", { className: "dpg-dot", "aria-hidden": "true" }), label);
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function Countdown({ state }) {
|
|
549
|
+
const parts = state.waitParts;
|
|
550
|
+
if (parts.hours > 0) {
|
|
551
|
+
return h("div", { className: "dpg-count-num" },
|
|
552
|
+
h("b", null, parts.hours), h("span", null, t("unitHour")),
|
|
553
|
+
h("b", null, parts.minutes), h("span", null, t("unitMin")),
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
if (parts.totalMinutes >= 1) {
|
|
557
|
+
return h("div", { className: "dpg-count-num" },
|
|
558
|
+
h("b", null, parts.minutes), h("span", null, t("unitMin")),
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
return h("div", { className: "dpg-count-num" },
|
|
562
|
+
h("b", null, Math.max(1, Math.ceil(parts.ms / 1000))), h("span", null, t("unitSec")),
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function PeakCard() {
|
|
567
|
+
const state = usePeakState(store, t);
|
|
568
|
+
const [dismissedEnd, setDismissedEnd] = useState(() => {
|
|
569
|
+
try {
|
|
570
|
+
return Number(localStorage.getItem(DISMISS_KEY) || "0");
|
|
571
|
+
} catch {
|
|
572
|
+
return 0;
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
const [dismissedSoon, setDismissedSoon] = useState(() => {
|
|
576
|
+
try {
|
|
577
|
+
return Number(localStorage.getItem(SOON_DISMISS_KEY) || "0");
|
|
578
|
+
} catch {
|
|
579
|
+
return 0;
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
583
|
+
const [enabled, setEnabled] = useState(() => {
|
|
584
|
+
try {
|
|
585
|
+
return localStorage.getItem(ENABLED_KEY) !== "0";
|
|
586
|
+
} catch {
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
useEffect(() => {
|
|
591
|
+
const onStorage = (event) => {
|
|
592
|
+
if (event.key === ENABLED_KEY) setEnabled(event.newValue !== "0");
|
|
593
|
+
};
|
|
594
|
+
window.addEventListener("storage", onStorage);
|
|
595
|
+
return () => window.removeEventListener("storage", onStorage);
|
|
596
|
+
}, []);
|
|
597
|
+
if (!enabled) return null;
|
|
598
|
+
const isSoon = state.mode === "soon";
|
|
599
|
+
if (state.mode !== "peak" && state.mode !== "soon") return null;
|
|
600
|
+
if (state.mode === "peak" && dismissedEnd === state.windowEnd) return null;
|
|
601
|
+
if (isSoon && dismissedSoon === state.windowStart) return null;
|
|
602
|
+
const dismiss = () => {
|
|
603
|
+
if (state.mode === "peak") {
|
|
604
|
+
setDismissedEnd(state.windowEnd);
|
|
605
|
+
try {
|
|
606
|
+
localStorage.setItem(DISMISS_KEY, String(state.windowEnd));
|
|
607
|
+
} catch {
|
|
608
|
+
/* private mode */
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
setDismissedSoon(state.windowStart);
|
|
613
|
+
try {
|
|
614
|
+
localStorage.setItem(SOON_DISMISS_KEY, String(state.windowStart));
|
|
615
|
+
} catch {
|
|
616
|
+
/* private mode */
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
const progressPercent = Math.round((state.progress ?? 0) * 100);
|
|
620
|
+
return h("div", {
|
|
621
|
+
className: "dpg-card",
|
|
622
|
+
"data-mode": isSoon ? "soon" : "peak",
|
|
623
|
+
role: "status",
|
|
624
|
+
"aria-live": "polite",
|
|
625
|
+
"aria-label": isSoon ? `${t("soonKicker")}: ${formatTemplate(t("soonTitle"), { wait: state.wait })}` : `${t("kicker")}: ${t("title")}`,
|
|
626
|
+
},
|
|
627
|
+
h("button", { type: "button", className: "dpg-x", onClick: dismiss, title: isSoon ? t("soonDismiss") : t("dismiss"), "aria-label": isSoon ? t("soonDismiss") : t("dismiss") }, "×"),
|
|
628
|
+
h("div", { className: "dpg-top" },
|
|
629
|
+
h("div", { className: "dpg-seal", "aria-hidden": "true" },
|
|
630
|
+
h("svg", { className: "dpg-ring", viewBox: "0 0 64 64" },
|
|
631
|
+
h("circle", { className: "dpg-ring-track", cx: 32, cy: 32, r: RING_RADIUS }),
|
|
632
|
+
h("circle", { className: "dpg-ring-fill", cx: 32, cy: 32, r: RING_RADIUS, style: { strokeDashoffset: state.ringOffset } }),
|
|
633
|
+
),
|
|
634
|
+
h("span", { className: "dpg-seal-pulse" }),
|
|
635
|
+
h("span", { className: "dpg-seal-icon" }, h(IconBolt)),
|
|
636
|
+
),
|
|
637
|
+
h("div", { className: "dpg-copy" },
|
|
638
|
+
h("div", { className: "dpg-kicker" },
|
|
639
|
+
h("span", { className: "dpg-dot", "aria-hidden": "true" }),
|
|
640
|
+
isSoon ? t("soonKicker") : t("kicker"),
|
|
641
|
+
h("span", { className: "dpg-mult" }, t("multiplier")),
|
|
642
|
+
),
|
|
643
|
+
h("div", { className: "dpg-title" }, isSoon ? formatTemplate(t("soonTitle"), { wait: state.wait }) : t("title")),
|
|
644
|
+
h("div", { className: "dpg-reason" }, isSoon ? t("soonReason") : t("reason")),
|
|
645
|
+
),
|
|
646
|
+
),
|
|
647
|
+
h("div", { className: "dpg-mid" },
|
|
648
|
+
h("div", { className: "dpg-count" },
|
|
649
|
+
h(Countdown, { state }),
|
|
650
|
+
h("div", { className: "dpg-count-label" }, isSoon ? t("soonCountdownLabel") : t("countdownLabel")),
|
|
651
|
+
),
|
|
652
|
+
h("div", { className: "dpg-meter" },
|
|
653
|
+
h("div", { className: "dpg-meter-head" },
|
|
654
|
+
h("span", null, t("windowLabel"), " ", h("b", null, formatTemplate(t("windowRange"), { start: state.startLabel, end: state.endLabel, zone: state.zone }))),
|
|
655
|
+
h("span", null, `${t("nowLabel")} ${state.nowLabel}`, isSoon ? null : h("b", { style: { marginLeft: 6 } }, `${progressPercent}%`)),
|
|
656
|
+
),
|
|
657
|
+
h("div", { className: "dpg-track", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": progressPercent, "aria-label": isSoon ? t("soonCountdownLabel") : t("countdownLabel") },
|
|
658
|
+
h("div", { className: "dpg-fill", style: { width: `${progressPercent}%` } }),
|
|
659
|
+
),
|
|
660
|
+
),
|
|
661
|
+
),
|
|
662
|
+
h("div", { className: "dpg-rec" },
|
|
663
|
+
h("span", { className: "dpg-rec-mark", "aria-hidden": "true" }, "→"),
|
|
664
|
+
h("div", { className: "dpg-rec-body" },
|
|
665
|
+
h("span", { className: "dpg-rec-label" }, isSoon ? t("soonRecommendationTitle") : t("recommendationTitle")),
|
|
666
|
+
h("span", { className: "dpg-rec-text" }, isSoon
|
|
667
|
+
? renderTemplate(t("soonRecommendation"), { start: h("b", null, state.startLabel), end: h("b", null, state.endLabel) })
|
|
668
|
+
: renderTemplate(t("recommendation"), { time: h("b", null, state.endLabel), wait: h("b", null, state.wait) })),
|
|
669
|
+
),
|
|
670
|
+
),
|
|
671
|
+
h("div", { className: "dpg-tail" },
|
|
672
|
+
h("div", { className: "dpg-schedule" },
|
|
673
|
+
h("span", { className: "dpg-schedule-label" }, t("scheduleChipLabel")),
|
|
674
|
+
state.scheduleLocalParts.map((part) => h("span", { className: "dpg-chip", key: part }, part)),
|
|
675
|
+
h("span", { className: "dpg-schedule-note" }, t("scheduleWeekend")),
|
|
676
|
+
),
|
|
677
|
+
h("div", { className: "dpg-actions" },
|
|
678
|
+
h("button", { type: "button", className: "dpg-btn", onClick: () => setShowDetails((value) => !value), "aria-expanded": showDetails ? "true" : "false" }, showDetails ? t("hideDetails") : t("details")),
|
|
679
|
+
h("button", { type: "button", className: "dpg-btn", "data-emphasis": "true", onClick: dismiss }, isSoon ? t("soonDismiss") : t("dismiss")),
|
|
680
|
+
),
|
|
681
|
+
),
|
|
682
|
+
showDetails
|
|
683
|
+
? h("div", { className: "dpg-details" },
|
|
684
|
+
h("span", null, t("scheduleUtc"), " ", h("b", null, state.scheduleUtc)),
|
|
685
|
+
h("span", null, formatTemplate(t("scheduleLocal"), { zone: state.zone }), " ", h("b", null, state.scheduleLocal)),
|
|
686
|
+
h("span", null, t("holidayNote")),
|
|
687
|
+
h("span", null, t("scheduleWeekend")),
|
|
688
|
+
)
|
|
689
|
+
: null,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
ctx.effect(() => ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
694
|
+
name: "conversation.composer.dock",
|
|
695
|
+
id: "peak-guard",
|
|
696
|
+
order: 5,
|
|
697
|
+
locale: NS,
|
|
698
|
+
}, PeakCard)), "peak-guard: composer dock card");
|
|
699
|
+
|
|
700
|
+
ctx.effect(() => ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
|
|
701
|
+
name: "conversation.session.header.utilities",
|
|
702
|
+
id: "peak-guard",
|
|
703
|
+
order: 40,
|
|
704
|
+
label: () => t("badgeLabel"),
|
|
705
|
+
locale: NS,
|
|
706
|
+
}, HeaderBadge)), "peak-guard: header badge");
|
|
707
|
+
|
|
708
|
+
try {
|
|
709
|
+
window.__DSH_PEAK_GUARD__ = {
|
|
710
|
+
state: () => buildState(store.getNow(), t),
|
|
711
|
+
preview: (iso) => store.force(iso),
|
|
712
|
+
clearPreview: () => store.force(null),
|
|
713
|
+
leadMinutes: PEAK_LEAD_MS / 60000,
|
|
714
|
+
setEnabled: (value) => {
|
|
715
|
+
try {
|
|
716
|
+
localStorage.setItem(ENABLED_KEY, value ? "1" : "0");
|
|
717
|
+
} catch {
|
|
718
|
+
/* private mode */
|
|
719
|
+
}
|
|
720
|
+
window.dispatchEvent(new StorageEvent("storage", { key: ENABLED_KEY, newValue: value ? "1" : "0" }));
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
ctx.effect(() => () => {
|
|
724
|
+
try {
|
|
725
|
+
delete window.__DSH_PEAK_GUARD__;
|
|
726
|
+
} catch {
|
|
727
|
+
/* hardened page */
|
|
728
|
+
}
|
|
729
|
+
}, "peak-guard: debug hook");
|
|
730
|
+
} catch {
|
|
731
|
+
/* hardened page */
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
console.info("[peak-guard] ready");
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
exports.apply = apply;
|
|
738
|
+
exports.inject = inject;
|
|
739
|
+
return module.exports;
|
|
740
|
+
},
|
|
741
|
+
});
|