Files
hado/docs/design/handoff/hado-inbox.dc.html

457 lines
36 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Shippori+Mincho:wght@500;700&family=Space+Grotesk:wght@400;500;600&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#070912;--ink-0:#090B16;--ink-1:#11152E;--ink-2:#1B2148;--ink-3:#2B3566;--sakura:#F7CEDC;--nadeshiko:#EA8FAE;--tsutsuji:#E84E8A;--kobai:#C13E6A;--yamabuki:#F2B233;--hanada:#1F7FC4;--ruri:#355CD6;--byakugun:#8FD6DC;--fuji:#9F8FCB;--washi:#F2ECE0;--kasumi:#AEB4D6;--nibi:#6B7099;--hair:rgba(174,180,214,0.16);--hair-2:rgba(174,180,214,0.30)}
*{box-sizing:border-box}html,body{margin:0;padding:0;background:#070912}
::selection{background:rgba(143,214,220,.25)}
a{color:var(--byakugun);text-decoration-color:rgba(143,214,220,.4)}a:hover{color:var(--washi)}
input,select,button{font:inherit}
button[disabled]{opacity:.45;pointer-events:none}
.skdot:hover + .sklbl{opacity:1}
@keyframes skTwinkle{0%,100%{opacity:.15}50%{opacity:.75}}
@keyframes skNebula{0%,100%{transform:rotate(-14deg) translateX(-2.5%)}50%{transform:rotate(-14deg) translateX(2.5%)}}
@keyframes skNebulaCore{0%,100%{transform:rotate(-14deg) translateX(3%)}50%{transform:rotate(-14deg) translateX(-3%)}}
@keyframes skOrbXa{0%,100%{transform:translateX(36px)}50%{transform:translateX(-36px)}}
@keyframes skOrbYa{0%,100%{transform:translateY(13px)}50%{transform:translateY(-13px)}}
@keyframes skOrbXb{0%,100%{transform:translateX(24px)}50%{transform:translateX(-24px)}}
@keyframes skOrbYb{0%,100%{transform:translateY(22px)}50%{transform:translateY(-22px)}}
@keyframes skOrbXc{0%,100%{transform:translateX(14px)}50%{transform:translateX(-14px)}}
@keyframes skOrbYc{0%,100%{transform:translateY(28px)}50%{transform:translateY(-28px)}}
@keyframes skOrbXd{0%,100%{transform:translateX(-20px)}50%{transform:translateX(20px)}}
@keyframes skOrbYd{0%,100%{transform:translateY(16px)}50%{transform:translateY(-16px)}}
@keyframes skPulseDot{0%,100%{box-shadow:0 0 6px rgba(53,92,214,.5)}50%{box-shadow:0 0 14px rgba(53,92,214,.95)}}
</style>
</helmet>
<div style="min-height:100vh;background:var(--bg);color:var(--washi);font-family:'Space Grotesk',sans-serif;position:relative;overflow:hidden;display:flex;flex-direction:column">
<!-- атмосфера -->
<div ref="{{ starsRef }}" style="position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:0"></div>
<div style="position:absolute;pointer-events:none;z-index:0;top:26%;left:-22%;width:150%;height:52%;filter:blur(34px);opacity:.72;animation:skNebula 46s ease-in-out infinite;background:radial-gradient(ellipse 42% 58% at 20% 60%, rgba(234,143,174,.20), transparent 70%),radial-gradient(ellipse 58% 46% at 48% 44%, rgba(232,78,138,.14), transparent 72%),radial-gradient(ellipse 38% 54% at 70% 57%, rgba(159,143,203,.17), transparent 70%),radial-gradient(ellipse 28% 40% at 88% 51%, rgba(143,214,220,.11), transparent 70%),radial-gradient(ellipse 52% 34% at 36% 62%, rgba(53,92,214,.11), transparent 72%)"></div>
<div style="position:absolute;pointer-events:none;z-index:0;top:44%;left:-15%;width:135%;height:14%;filter:blur(14px);opacity:.7;animation:skNebulaCore 58s ease-in-out infinite;background:radial-gradient(ellipse 20% 70% at 8% 71%, rgba(247,206,220,.22), transparent 70%),radial-gradient(ellipse 14% 90% at 22% 52%, rgba(242,236,224,.18), transparent 70%),radial-gradient(ellipse 18% 80% at 36% 48%, rgba(234,143,174,.22), transparent 72%),radial-gradient(ellipse 12% 100% at 48% 38%, rgba(247,206,220,.24), transparent 70%),radial-gradient(ellipse 16% 85% at 60% 43%, rgba(242,236,224,.17), transparent 72%),radial-gradient(ellipse 13% 75% at 72% 43%, rgba(234,143,174,.20), transparent 70%),radial-gradient(ellipse 18% 90% at 86% 55%, rgba(247,206,220,.18), transparent 72%),radial-gradient(ellipse 10% 60% at 94% 61%, rgba(143,214,220,.12), transparent 70%)"></div>
<div style="position:absolute;pointer-events:none;z-index:0;top:44%;left:-15%;width:135%;height:14%;filter:blur(10px);opacity:.8;animation:skNebula 46s ease-in-out infinite;background:radial-gradient(ellipse 20% 38% at 16% 72%, rgba(7,9,18,.6), transparent 75%),radial-gradient(ellipse 28% 34% at 44% 55%, rgba(9,11,22,.55), transparent 75%),radial-gradient(ellipse 18% 38% at 70% 50%, rgba(7,9,18,.55), transparent 75%),radial-gradient(ellipse 14% 30% at 90% 68%, rgba(9,11,22,.5), transparent 75%)"></div>
<div ref="{{ bandRef }}" style="position:absolute;pointer-events:none;z-index:0;top:44%;left:-15%;width:135%;height:14%;transform:rotate(-14deg)"></div>
<!-- сестринские планеты: все, кроме Hado -->
<div style="position:absolute;inset:0;pointer-events:none;z-index:0">
<div style="position:absolute;top:17%;left:7%">
<div style="animation:skOrbXa 19s ease-in-out infinite">
<div style="display:flex;flex-direction:column;align-items:center;gap:9px;animation:skOrbYa 19s ease-in-out -4.75s infinite">
<div class="skdot" style="pointer-events:auto;width:15px;height:15px;border-radius:50%;background:radial-gradient(circle at 32% 28%, var(--sakura), var(--nadeshiko) 50%, var(--ink-0) 92%);box-shadow:0 0 10px rgba(234,143,174,.4)"></div>
<span class="sklbl" style="font-family:'Space Mono',monospace;font-size:9px;letter-spacing:.3em;color:var(--nibi);white-space:nowrap;opacity:0;transition:opacity .4s">YUME</span>
</div>
</div>
</div>
<div style="position:absolute;top:26%;right:8%">
<div style="animation:skOrbXb 27s ease-in-out infinite">
<div style="display:flex;flex-direction:column;align-items:center;gap:9px;animation:skOrbYb 27s ease-in-out -20.25s infinite">
<div class="skdot" style="pointer-events:auto;width:12px;height:12px;border-radius:50%;background:radial-gradient(circle at 32% 28%, var(--tsutsuji), var(--kobai) 52%, var(--ink-0) 92%);box-shadow:0 0 9px rgba(232,78,138,.4)"></div>
<span class="sklbl" style="font-family:'Space Mono',monospace;font-size:9px;letter-spacing:.3em;color:var(--nibi);white-space:nowrap;opacity:0;transition:opacity .4s">MYAKU</span>
</div>
</div>
</div>
<div style="position:absolute;top:60%;left:4.5%">
<div style="animation:skOrbXc 14s ease-in-out infinite">
<div style="display:flex;flex-direction:column;align-items:center;gap:8px;animation:skOrbYc 14s ease-in-out -3.5s infinite">
<div class="skdot" style="pointer-events:auto;width:9px;height:9px;border-radius:50%;background:radial-gradient(circle at 34% 30%, var(--fuji), #4A4177 55%, var(--ink-0) 92%);box-shadow:0 0 8px rgba(159,143,203,.4)"></div>
<span class="sklbl" style="font-family:'Space Mono',monospace;font-size:9px;letter-spacing:.3em;color:var(--nibi);white-space:nowrap;opacity:0;transition:opacity .4s">KIOKU</span>
</div>
</div>
</div>
<div style="position:absolute;top:64%;right:6%">
<div style="animation:skOrbXd 22s ease-in-out infinite">
<div style="display:flex;flex-direction:column;align-items:center;gap:8px;animation:skOrbYd 22s ease-in-out -5.5s infinite">
<div class="skdot" style="pointer-events:auto;width:10px;height:10px;border-radius:50%;background:radial-gradient(circle at 32% 28%, var(--byakugun), var(--hanada) 50%, var(--ink-0) 93%);box-shadow:0 0 9px rgba(143,214,220,.4)"></div>
<span class="sklbl" style="font-family:'Space Mono',monospace;font-size:9px;letter-spacing:.3em;color:var(--nibi);white-space:nowrap;opacity:0;transition:opacity .4s">FUMI</span>
</div>
</div>
</div>
</div>
<header style="position:relative;z-index:1;display:flex;justify-content:space-between;align-items:flex-start;padding:30px 44px 0 44px">
<div style="display:flex;align-items:baseline;gap:14px">
<span style="font-family:'Shippori Mincho',serif;font-size:54px;line-height:1;color:var(--washi)">波動</span>
<span style="font-family:'Space Mono',monospace;font-size:13px;letter-spacing:.5em;color:var(--nibi)">HADO</span>
<sc-if value="{{ hasCounter }}" hint-placeholder-val="{{ true }}">
<span style="display:inline-flex;align-items:center;gap:8px;padding:5px 12px 5px 10px;border:1px solid rgba(53,92,214,.55);border-radius:999px;background:rgba(53,92,214,.14)">
<span style="width:11px;height:11px;border-radius:50%;background:radial-gradient(circle at 32% 28%, var(--ruri), #24337F 55%, var(--ink-0) 92%);animation:skPulseDot 2.6s ease-in-out infinite"></span>
<span style="font-family:'Space Mono',monospace;font-size:12px;color:var(--byakugun)">{{ counter }}</span>
</span>
</sc-if>
</div>
<div style="display:flex;align-items:center;gap:16px;margin-top:14px">
<button onClick="{{ toggleAdd }}" style="background:rgba(53,92,214,.22);border:1px solid var(--ruri);color:var(--byakugun);border-radius:8px;padding:8px 18px;font-size:13px;cursor:pointer;pointer-events:auto" style-hover="background:rgba(53,92,214,.4)">+ Добавить</button>
<span style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.3em;color:var(--nibi)">05</span>
</div>
</header>
<main style="position:relative;z-index:1;max-width:880px;width:100%;margin:0 auto;padding:72px 32px 100px 32px;flex:1">
<sc-if value="{{ addOpen }}">
<section style="margin-bottom:52px;border:1px dashed var(--hair-2);border-radius:14px;background:rgba(17,21,46,.7);padding:22px">
<div style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.3em;color:var(--kasumi);margin-bottom:16px">НОВОЕ СОБЫТИЕ</div>
<div style="display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end">
<label style="display:flex;flex-direction:column;gap:6px;flex:2;min-width:220px">
<span style="font-size:12px;color:var(--nibi)">Заголовок</span>
<input value="{{ nTitle }}" onChange="{{ onNTitle }}" placeholder="Продлить страховку" style="background:var(--ink-1);border:1px solid var(--hair-2);border-radius:8px;color:var(--washi);padding:9px 12px;font-size:14px">
</label>
<label style="display:flex;flex-direction:column;gap:6px;flex:1;min-width:160px">
<span style="font-size:12px;color:var(--nibi)">Подзаголовок (опц.)</span>
<input value="{{ nSub }}" onChange="{{ onNSub }}" placeholder="раз в год" style="background:var(--ink-1);border:1px solid var(--hair-2);border-radius:8px;color:var(--washi);padding:9px 12px;font-size:14px">
</label>
<label style="display:flex;flex-direction:column;gap:6px">
<span style="font-size:12px;color:var(--nibi)">Срок</span>
<input type="date" value="{{ nDate }}" onChange="{{ onNDate }}" style="background:var(--ink-1);border:1px solid var(--hair-2);border-radius:8px;color:var(--washi);padding:8px 12px;font-size:14px">
</label>
<label style="display:flex;flex-direction:column;gap:6px">
<span style="font-size:12px;color:var(--nibi)">Время (опц.)</span>
<input type="time" value="{{ nTime }}" onChange="{{ onNTime }}" style="background:var(--ink-1);border:1px solid var(--hair-2);border-radius:8px;color:var(--washi);padding:8px 12px;font-size:14px">
</label>
<button onClick="{{ addEvent }}" style="background:rgba(53,92,214,.22);border:1px solid var(--ruri);color:var(--byakugun);border-radius:8px;padding:9px 18px;font-size:13px;cursor:pointer" style-hover="background:rgba(53,92,214,.4)">Создать</button>
<button onClick="{{ toggleAdd }}" style="background:transparent;border:1px solid var(--hair-2);color:var(--kasumi);border-radius:8px;padding:9px 14px;font-size:13px;cursor:pointer" style-hover="border-color:var(--byakugun);color:var(--byakugun)">Отмена</button>
</div>
<sc-if value="{{ nErr }}">
<div style="color:var(--sakura);font-size:12px;margin-top:12px">{{ nErr }}</div>
</sc-if>
</section>
</sc-if>
<sc-for list="{{ groups }}" as="g" hint-placeholder-count="4">
<section style="margin-bottom:52px">
<div style="display:flex;align-items:baseline;gap:12px;margin-bottom:18px">
<span style="font-family:'Shippori Mincho',serif;font-size:20px;color:var(--washi)">{{ g.jp }}</span>
<span style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.35em;color:var(--kasumi)">{{ g.ru }}</span>
<span style="flex:1;border-bottom:1px solid var(--hair)"></span>
</div>
<sc-if value="{{ g.empty }}">
<div style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.3em;color:var(--nibi);padding:12px 2px">ПУСТО</div>
</sc-if>
<div style="display:flex;flex-direction:column;gap:12px">
<sc-for list="{{ g.events }}" as="ev" hint-placeholder-count="1">
<article style="background:rgba(27,33,72,.72);border:1px solid var(--hair);border-radius:12px;padding:16px 20px;display:flex;align-items:center;gap:18px;flex-wrap:wrap">
<div style="display:flex;flex-direction:column;align-items:center;gap:2px;min-width:64px;padding:8px 10px;border:1px solid var(--hair-2);border-radius:10px;background:var(--ink-1)">
<span style="font-family:'Space Mono',monospace;font-size:9px;letter-spacing:.2em;color:var(--nibi)">{{ ev.dueMonth }}</span>
<span style="font-family:'Shippori Mincho',serif;font-size:24px;line-height:1;color:var(--washi)">{{ ev.dueDay }}</span>
<sc-if value="{{ ev.dueTime }}">
<span style="font-family:'Space Mono',monospace;font-size:10px;color:var(--byakugun)">{{ ev.dueTime }}</span>
</sc-if>
</div>
<div style="flex:1;min-width:220px;display:flex;flex-direction:column;gap:5px">
<sc-if value="{{ ev.link }}" hint-placeholder-val="{{ true }}">
<a href="{{ ev.link }}" style="align-self:flex-start;font-size:16px;font-weight:500;color:var(--washi);text-decoration:underline;text-decoration-style:dotted;text-decoration-color:var(--hair-2);text-underline-offset:4px" style-hover="color:var(--byakugun)">{{ ev.title }}</a>
</sc-if>
<sc-if value="{{ ev.noLink }}">
<span style="font-size:16px;font-weight:500;color:var(--washi)">{{ ev.title }}</span>
</sc-if>
<sc-if value="{{ ev.subtitle }}">
<span style="font-size:13px;color:var(--kasumi)">{{ ev.subtitle }}</span>
</sc-if>
<span style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.12em;color:var(--nibi);margin-top:3px">{{ ev.source }} · срок {{ ev.due }}<sc-if value="{{ ev.rel }}"> · <span style="color:var(--kasumi)">{{ ev.rel }}</span></sc-if></span>
</div>
<div style="display:flex;gap:10px;align-items:center">
<sc-if value="{{ ev.canRemind }}" hint-placeholder-val="{{ true }}">
<button disabled="{{ ev.busy }}" onClick="{{ ev.onRemind }}" style="background:transparent;border:1px solid var(--hair-2);color:var(--kasumi);border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer;transition:border-color .2s,color .2s" style-hover="border-color:var(--byakugun);color:var(--byakugun)">Помню</button>
</sc-if>
<button disabled="{{ ev.busy }}" onClick="{{ ev.onDone }}" style="background:rgba(53,92,214,.22);border:1px solid var(--ruri);color:var(--byakugun);border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer;transition:background .2s" style-hover="background:rgba(53,92,214,.4)">{{ ev.doneLabel }}</button>
</div>
</article>
</sc-for>
</div>
</section>
</sc-for>
<!-- архив -->
<section style="margin-top:64px;border:1px solid var(--hair);border-radius:14px;background:rgba(17,21,46,.7);overflow:hidden">
<button onClick="{{ toggleArchive }}" style="width:100%;display:flex;align-items:center;gap:14px;background:none;border:none;padding:18px 22px;cursor:pointer;text-align:left;color:var(--washi)">
<span style="font-family:'Shippori Mincho',serif;font-size:16px">書庫</span>
<span style="font-size:14px">Архив</span>
<span style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.15em;color:var(--nibi)">{{ archiveCount }} закрыто</span>
<span style="margin-left:auto;font-family:'Space Mono',monospace;font-size:11px;color:var(--nibi)">{{ archiveChevron }}</span>
</button>
<sc-if value="{{ archiveOpen }}">
<div style="border-top:1px solid var(--hair);padding:18px 22px;display:flex;flex-direction:column;gap:10px">
<sc-if value="{{ archiveEmpty }}">
<div style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.3em;color:var(--nibi)">ПУСТО</div>
</sc-if>
<sc-for list="{{ archive }}" as="ar" hint-placeholder-count="2">
<div style="display:flex;align-items:center;gap:14px;border:1px solid var(--hair);border-radius:10px;padding:11px 16px;background:var(--ink-1);opacity:.75">
<span style="font-size:14px;color:var(--kasumi);flex:1;min-width:180px">{{ ar.title }}</span>
<span style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.12em;color:var(--nibi)">{{ ar.source }} · {{ ar.due }}</span>
<span style="font-family:'Space Mono',monospace;font-size:10px;letter-spacing:.15em;color:var(--fuji);border:1px solid var(--hair);border-radius:999px;padding:3px 10px">{{ ar.closedAs }}</span>
</div>
</sc-for>
</div>
</sc-if>
</section>
<!-- настройки -->
<section style="margin-top:64px;border:1px solid var(--hair);border-radius:14px;background:rgba(17,21,46,.7);overflow:hidden">
<button onClick="{{ toggleSettings }}" style="width:100%;display:flex;align-items:center;gap:14px;background:none;border:none;padding:18px 22px;cursor:pointer;text-align:left;color:var(--washi)">
<span style="font-family:'Shippori Mincho',serif;font-size:16px">設定</span>
<span style="font-size:14px">Настройки</span>
<span style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.15em;color:var(--nibi)">m.orlov</span>
<span style="margin-left:auto;font-family:'Space Mono',monospace;font-size:11px;color:var(--nibi)">{{ settingsChevron }}</span>
</button>
<sc-if value="{{ settingsOpen }}">
<div style="border-top:1px solid var(--hair);padding:26px 22px 30px 22px;display:flex;flex-direction:column;gap:34px">
<div>
<div style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.3em;color:var(--kasumi);margin-bottom:16px">ПОЯС И ТИХИЕ ЧАСЫ</div>
<div style="display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end">
<label style="display:flex;flex-direction:column;gap:6px;min-width:240px">
<span style="font-size:12px;color:var(--nibi)">Часовой пояс</span>
<select value="{{ tz }}" onChange="{{ onTz }}" style="background:var(--ink-1);border:1px solid var(--hair-2);border-radius:8px;color:var(--washi);padding:9px 12px;font-size:14px">
<sc-for list="{{ tzList }}" as="z" hint-placeholder-count="3">
<option value="{{ z }}">{{ z }}</option>
</sc-for>
</select>
</label>
<label style="display:flex;flex-direction:column;gap:6px">
<span style="font-size:12px;color:var(--nibi)">Тихие часы, с</span>
<input type="time" value="{{ qFrom }}" onChange="{{ onQFrom }}" style="background:var(--ink-1);border:1px solid var(--hair-2);border-radius:8px;color:var(--washi);padding:8px 12px;font-size:14px">
</label>
<label style="display:flex;flex-direction:column;gap:6px">
<span style="font-size:12px;color:var(--nibi)">до</span>
<input type="time" value="{{ qTo }}" onChange="{{ onQTo }}" style="background:var(--ink-1);border:1px solid var(--hair-2);border-radius:8px;color:var(--washi);padding:8px 12px;font-size:14px">
</label>
<button onClick="{{ saveQuiet }}" style="background:rgba(53,92,214,.22);border:1px solid var(--ruri);color:var(--byakugun);border-radius:8px;padding:9px 18px;font-size:13px;cursor:pointer" style-hover="background:rgba(53,92,214,.4)">Сохранить</button>
</div>
<sc-if value="{{ qErr }}">
<div style="color:var(--sakura);font-size:12px;margin-top:12px">{{ qErr }}</div>
</sc-if>
<sc-if value="{{ qSaved }}">
<div style="color:var(--byakugun);font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.15em;margin-top:12px">Сохранено — напоминания сдвинуты под пояс</div>
</sc-if>
</div>
<div>
<div style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.3em;color:var(--kasumi);margin-bottom:16px">КАНАЛЫ</div>
<div style="display:flex;flex-direction:column;gap:10px">
<sc-for list="{{ channels }}" as="ch" hint-placeholder-count="3">
<div style="display:flex;align-items:center;gap:14px;border:1px solid var(--hair);border-radius:10px;padding:11px 16px;background:var(--ink-1)">
<span style="font-size:14px;color:var(--washi);min-width:76px">{{ ch.name }}</span>
<span style="font-family:'Space Mono',monospace;font-size:11px;color:var(--nibi);flex:1;overflow-wrap:anywhere">{{ ch.note }}</span>
<sc-if value="{{ ch.removable }}">
<button onClick="{{ ch.onDelete }}" style="background:transparent;border:1px solid var(--hair-2);color:var(--kasumi);border-radius:8px;padding:6px 12px;font-size:12px;cursor:pointer" style-hover="border-color:var(--nadeshiko);color:var(--sakura)">Удалить</button>
</sc-if>
</div>
</sc-for>
</div>
</div>
<div>
<div style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.3em;color:var(--kasumi);margin-bottom:16px">ДОБАВИТЬ WEBHOOK</div>
<div style="display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end">
<label style="display:flex;flex-direction:column;gap:6px;flex:1;min-width:220px">
<span style="font-size:12px;color:var(--nibi)">Адрес доставки</span>
<input type="url" value="{{ whUrl }}" onChange="{{ onWhUrl }}" placeholder="https://home.local/hado/notify" style="background:var(--ink-1);border:1px solid var(--hair-2);border-radius:8px;color:var(--washi);padding:9px 12px;font-size:14px">
</label>
<label style="display:flex;flex-direction:column;gap:6px;flex:1;min-width:220px">
<span style="font-size:12px;color:var(--nibi)">Адрес присутствия</span>
<input type="url" value="{{ whPresence }}" onChange="{{ onWhPresence }}" placeholder="https://home.local/presence" style="background:var(--ink-1);border:1px solid var(--hair-2);border-radius:8px;color:var(--washi);padding:9px 12px;font-size:14px">
</label>
<button onClick="{{ addWebhook }}" style="background:transparent;border:1px solid var(--hair-2);color:var(--kasumi);border-radius:8px;padding:9px 18px;font-size:13px;cursor:pointer" style-hover="border-color:var(--byakugun);color:var(--byakugun)">Добавить webhook</button>
</div>
<sc-if value="{{ whErr }}">
<div style="color:var(--sakura);font-size:12px;margin-top:12px">{{ whErr }}</div>
</sc-if>
</div>
<div>
<div style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.3em;color:var(--kasumi);margin-bottom:16px">TELEGRAM</div>
<sc-if value="{{ noTgCode }}" hint-placeholder-val="{{ true }}">
<button onClick="{{ connectTg }}" style="background:transparent;border:1px solid var(--hair-2);color:var(--kasumi);border-radius:8px;padding:9px 18px;font-size:13px;cursor:pointer" style-hover="border-color:var(--byakugun);color:var(--byakugun)">Подключить Telegram</button>
</sc-if>
<sc-if value="{{ tgCode }}">
<div style="border:1px dashed var(--hair-2);border-radius:10px;padding:14px 18px;display:flex;flex-direction:column;gap:8px">
<span style="font-size:14px;color:var(--washi)">Отправь боту <span style="font-family:'Space Mono',monospace;color:var(--byakugun)">/start {{ tgCode }}</span> или <a href="#tg">открой</a></span>
<span style="font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.15em;color:var(--nibi)">Код живёт 15 минут. Канал появится в списке при следующем открытии страницы.</span>
</div>
</sc-if>
</div>
</div>
</sc-if>
</section>
</main>
<footer style="position:relative;z-index:1;max-width:880px;width:100%;margin:0 auto;padding:22px 32px 36px 32px;border-top:1px solid var(--hair);display:flex;justify-content:center;gap:8px;font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.3em;color:var(--nibi)">
<span>波動 HADO</span>
<span>· 世界 SEKAI</span>
</footer>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;showEmpty&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:true,&quot;tsType&quot;:&quot;boolean&quot;,&quot;section&quot;:&quot;Инбокс&quot;},&quot;telegramLinked&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:true,&quot;tsType&quot;:&quot;boolean&quot;,&quot;section&quot;:&quot;Каналы&quot;}}">
class Component extends DCLogic {
constructor(props) {
super(props);
this.starsRef = React.createRef();
this.bandRef = React.createRef();
const mk = (id, group, title, subtitle, source, due, link, doneLabel) => ({ id, group, title, subtitle, source, due, link, doneLabel: doneLabel || 'Сделано' });
this.state = {
events: [
mk('e1', 'today', 'Сегодня ДР — Вася', 'исполняется 34', 'people', '2026-09-04', '#people/vasya', 'Поздравил'),
mk('e2', 'today', 'Вылет GOL 1483 — Монтевидео → Сан-Пауло', null, 'docs', '2026-09-04 19:40', '#docs/gol1483', null),
mk('e3', 'overdue', 'Аренда за сентябрь не оплачена', 'срок прошёл 3 дня назад', 'payments', '2026-09-01', null, 'Оплатил'),
mk('e4', 'soon', 'Седула истекает', 'осталось 26 дней', 'docs', '2026-09-30', '#docs/cedula', 'Продлил'),
mk('e5', 'soon', 'Вылет SU 401 — Стамбул', null, 'docs', '2026-09-10 08:15', '#docs/su401', null),
mk('e6', 'planned', 'ДР — Мама', null, 'people', '2026-11-02', '#people/mama', 'Поздравил'),
mk('e7', 'planned', 'Продление домена hado.uy', null, 'docs', '2026-12-15', null, 'Продлил')
],
busy: {}, settingsOpen: false,
tz: 'America/Montevideo', qFrom: '22:00', qTo: '09:00', qErr: '', qSaved: false,
channels: [
{ id: 'web', name: 'Веб', note: 'этот инбокс', removable: false },
{ id: 'tg', name: 'Telegram', note: 'чат 88 214 302', removable: true },
{ id: 'wh1', name: 'Webhook', note: 'https://home.orlov.uy/hado/notify', removable: true }
],
whUrl: '', whPresence: '', whErr: '', tgCode: null,
archive: [
{ id: 'a1', title: 'ДР — Лена', source: 'people', due: '2026-08-28', closedAs: 'Поздравил' },
{ id: 'a2', title: 'Аренда за август', source: 'payments', due: '2026-08-01', closedAs: 'Оплатил' },
{ id: 'a3', title: 'Виза — подача документов', source: 'docs', due: '2026-07-14', closedAs: 'Сделано' }
],
archiveOpen: false,
addOpen: false, nTitle: '', nSub: '', nDate: '', nTime: '', nErr: ''
};
try { this.tzList = Intl.supportedValuesOf('timeZone'); }
catch (e) { this.tzList = ['America/Montevideo', 'Asia/Shanghai', 'Europe/Belgrade', 'UTC']; }
}
counter() { return this.state.events.filter(e => e.group === 'today' || e.group === 'overdue').length; }
syncTitle() { const c = this.counter(); document.title = c ? '(' + c + ') Hado' : 'Hado'; }
componentDidMount() { this.syncTitle(); this.spawnStars(); }
componentDidUpdate() { this.syncTitle(); }
spawnStars() {
const star = (color, size, bright, x, y, dur, delay) => {
const s = document.createElement('div');
s.style.cssText = 'position:absolute;border-radius:50%;left:' + x + '%;top:' + y + '%;width:' + size + 'px;height:' + size + 'px;background:' + color + ';' + (bright ? 'box-shadow:0 0 6px ' + color + ';' : '') + 'animation:skTwinkle ' + dur + 's ease-in-out ' + delay + 's infinite';
return s;
};
const sky = this.starsRef.current, band = this.bandRef.current;
if (!sky || !band || sky.childElementCount > 0) return;
for (let i = 0; i < 90; i++) {
sky.appendChild(star(Math.random() < 0.2 ? '#8FD6DC' : '#F2ECE0', Math.random() < 0.85 ? 1.5 : 2.5, false,
(Math.random() * 100).toFixed(2), (Math.random() * 100).toFixed(2), (3 + Math.random() * 5).toFixed(1), (-Math.random() * 8).toFixed(1)));
}
for (let j = 0; j < 170; j++) {
const bright = Math.random() < 0.08, r = Math.random();
const color = r < 0.68 ? '#F2ECE0' : (r < 0.88 ? '#F7CEDC' : '#8FD6DC');
const g = (Math.random() + Math.random() + Math.random()) / 3;
const x = Math.random() * 100;
const arc = 28 * Math.pow((x - 50) / 50, 2) + Math.max(0, 35 - x) * 0.35;
band.appendChild(star(color, bright ? 2.2 : (0.8 + Math.random() * 1.1).toFixed(1), bright,
x.toFixed(2), (6 + g * 55 + arc).toFixed(2), (2.5 + Math.random() * 5).toFixed(1), (-Math.random() * 8).toFixed(1)));
}
}
act(id, remove) {
this.setState(s => ({ busy: { ...s.busy, [id]: true } }));
setTimeout(() => {
this.setState(s => {
const ev = s.events.find(e => e.id === id);
return {
busy: { ...s.busy, [id]: false },
events: remove ? s.events.filter(e => e.id !== id) : s.events,
archive: remove && ev ? [{ id: 'a' + id, title: ev.title, source: ev.source, due: ev.due, closedAs: ev.doneLabel }, ...s.archive] : s.archive
};
});
}, 700);
}
dueParts(due) {
const m = due.match(/^(\d{4})-(\d{2})-(\d{2})(?:\s+(\d{2}:\d{2}))?$/);
if (!m) return { dueMonth: '', dueDay: due, dueTime: null, rel: null };
const months = ['ЯНВ', 'ФЕВ', 'МАР', 'АПР', 'МАЙ', 'ИЮН', 'ИЮЛ', 'АВГ', 'СЕН', 'ОКТ', 'НОЯ', 'ДЕК'];
const today = new Date(); today.setHours(0, 0, 0, 0);
const d = new Date(+m[1], +m[2] - 1, +m[3]);
const diff = Math.round((d - today) / 86400000);
const px = n => n % 10 === 1 && n % 100 !== 11 ? 'день' : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 'дня' : 'дней');
let rel = null;
if (diff === 0) rel = 'сегодня';
else if (diff === 1) rel = 'завтра';
else if (diff > 1) rel = 'через ' + diff + ' ' + px(diff);
else if (diff === -1) rel = 'вчера';
else rel = Math.abs(diff) + ' ' + px(Math.abs(diff)) + ' назад';
return { dueMonth: months[+m[2] - 1], dueDay: m[3], dueTime: m[4] || null, rel };
}
renderVals() {
const st = this.state;
const showEmpty = this.props.showEmpty ?? true;
const tgLinked = this.props.telegramLinked ?? true;
const deco = list => list.slice().sort((a, b) => (a.due < b.due ? -1 : 1)).map(e => ({
...e, ...this.dueParts(e.due), noLink: !e.link, busy: !!st.busy[e.id], canRemind: e.group !== 'planned',
onRemind: () => this.act(e.id, false), onDone: () => this.act(e.id, true)
}));
const defs = [['today', '今日', 'СЕГОДНЯ'], ['overdue', '遅延', 'ПРОСРОЧЕНО'], ['soon', '近日', 'ГРЯДЁТ'], ['planned', '予定', 'ЗАПЛАНИРОВАНО']];
const groups = defs.map(([key, jp, ru]) => {
const events = deco(st.events.filter(e => e.group === key));
return { key, jp, ru, events, empty: events.length === 0 };
}).filter(g => !g.empty || showEmpty);
const c = this.counter();
return {
groups, counter: c, hasCounter: c > 0,
starsRef: this.starsRef, bandRef: this.bandRef,
settingsOpen: st.settingsOpen, settingsChevron: st.settingsOpen ? '▲' : '▼',
toggleSettings: () => this.setState(s => ({ settingsOpen: !s.settingsOpen })),
tz: st.tz, tzList: this.tzList, onTz: e => this.setState({ tz: e.target.value }),
qFrom: st.qFrom, qTo: st.qTo, qErr: st.qErr, qSaved: st.qSaved,
onQFrom: e => this.setState({ qFrom: e.target.value, qSaved: false }),
onQTo: e => this.setState({ qTo: e.target.value, qSaved: false }),
saveQuiet: () => {
if (this.state.qFrom <= this.state.qTo) {
this.setState({ qErr: 'Интервал обязан переходить через полночь: «с 22:00 до 09:00» — верно, «с 08:00 до 22:00» — ошибка.', qSaved: false });
} else {
this.setState({ qErr: '', qSaved: true });
clearTimeout(this._qt);
this._qt = setTimeout(() => this.setState({ qSaved: false }), 3000);
}
},
channels: st.channels.filter(ch => ch.id !== 'tg' || tgLinked).map(ch => ({
...ch, onDelete: () => this.setState(s => ({ channels: s.channels.filter(c2 => c2.id !== ch.id) }))
})),
whUrl: st.whUrl, whPresence: st.whPresence, whErr: st.whErr,
onWhUrl: e => this.setState({ whUrl: e.target.value }),
onWhPresence: e => this.setState({ whPresence: e.target.value }),
addWebhook: () => {
const ok = u => { try { const x = new URL(u); return x.protocol === 'http:' || x.protocol === 'https:'; } catch (e) { return false; } };
const { whUrl, whPresence } = this.state;
if (!ok(whUrl) || !ok(whPresence)) { this.setState({ whErr: 'Оба поля обязательны и должны быть URL (http/https).' }); return; }
this.setState(s => ({ channels: [...s.channels, { id: 'wh' + Date.now(), name: 'Webhook', note: whUrl, removable: true }], whUrl: '', whPresence: '', whErr: '' }));
},
archive: st.archive, archiveOpen: st.archiveOpen, archiveEmpty: st.archive.length === 0,
archiveCount: st.archive.length, archiveChevron: st.archiveOpen ? '▲' : '▼',
toggleArchive: () => this.setState(s => ({ archiveOpen: !s.archiveOpen })),
addOpen: st.addOpen, nTitle: st.nTitle, nSub: st.nSub, nDate: st.nDate, nTime: st.nTime, nErr: st.nErr,
toggleAdd: () => this.setState(s => ({ addOpen: !s.addOpen, nErr: '' })),
onNTitle: e => this.setState({ nTitle: e.target.value }),
onNSub: e => this.setState({ nSub: e.target.value }),
onNDate: e => this.setState({ nDate: e.target.value }),
onNTime: e => this.setState({ nTime: e.target.value }),
addEvent: () => {
const { nTitle, nSub, nDate, nTime } = this.state;
if (!nTitle.trim() || !nDate) { this.setState({ nErr: 'Заголовок и срок обязательны.' }); return; }
const today = new Date(); today.setHours(0, 0, 0, 0);
const [y, mo, da] = nDate.split('-').map(Number);
const diff = Math.round((new Date(y, mo - 1, da) - today) / 86400000);
const group = diff < 0 ? 'overdue' : (diff === 0 ? 'today' : (diff <= 30 ? 'soon' : 'planned'));
this.setState(s => ({
events: [...s.events, { id: 'u' + Date.now(), group, title: nTitle.trim(), subtitle: nSub.trim() || null, source: 'вручную', due: nDate + (nTime ? ' ' + nTime : ''), link: null, doneLabel: 'Сделано' }],
addOpen: false, nTitle: '', nSub: '', nDate: '', nTime: '', nErr: ''
}));
},
tgCode: st.tgCode, noTgCode: !st.tgCode,
connectTg: () => {
const abc = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
this.setState({ tgCode: Array.from({ length: 6 }, () => abc[Math.floor(Math.random() * abc.length)]).join('') });
}
};
}
}
</script>
</body>
</html>