docs: design handoff, manual events and archive in plan, spec and functional doc

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
This commit is contained in:
2026-09-04 01:09:54 -03:00
parent d80e70ae22
commit c26a2cd7e4
11 changed files with 3758 additions and 145 deletions

View File

@@ -0,0 +1,80 @@
# Hado 波動 — design handoff
Сервис напоминаний экосистемы Sekai (世界). Планета № 05.
Дизайн-система: Sekai — 和風幻想 (dark only, токены из tokens/colors.css).
## Файлы пакета
- `hado-inbox.dc.html` + `support.js` — рабочий прототип веб-инбокса (открыть в браузере, файлы держать рядом)
- `hado-planet.dc.html` — карточка планеты Hado отдельно
- `sekai-planets.html` — все 5 планет экосистемы, автономный HTML (формат исходного planets.html)
- `sekai-template.html` — исходный шаблон страницы сервиса (каркас, туманность, сёстры-планеты)
- `sekai-nebula.html` — исходный компонент туманности
- `functional-spec.md` — исходное функциональное описание
## Планета 05 · HADO 波動
Hado = волна / пульс. Напоминание — сигнал, который уходит и возвращается.
- Градиент тела: `radial-gradient(circle at 32% 28%, var(--ruri), #24337F 55%, var(--ink-0) 92%)`
(瑠璃 ruri — «magic / pulse» → тёмный ruri #24337F; глубокий ультрамарин, чтобы не путать с ледяным Fumi)
- Свечение: `0 0 46px rgba(53,92,214,.4)`, пульс 2.6s
- Эхо-кольца: 2 расходящихся кольца (byakugun .45 / ruri .5), skEcho 4.2s со сдвигом 2.1s
- Кольца орбит: rotateX(69deg), внешнее пунктирное rotateZ(-8deg)
- Спутники: СИГНАЛ 10px `--byakugun` (skSpin 13s) + ЭХО 7px `--ruri` (skSpinBack 30s)
- Мини-версия для навигации: круг 11px, тот же градиент, box-shadow 0 0 8px rgba(53,92,214,.45)
## Страница инбокса (по sekai-template, без отступлений)
Слои фона: звёзды (90) → туман → ядро полосы → пылевые разломы → звёзды полосы (170, дуга 14°).
Сёстры-планеты (hover — подпись): YUME 17%/7%, MYAKU 26%/right 8%, KIOKU 60%/4.5%, FUMI 64%/right 6%. Своей планеты на странице нет.
Шапка: 波動 (Shippori Mincho 54px --washi) + HADO (Space Mono 13px .5em --nibi) +
бейдж-счётчик (мини-планета 11px с пульсом + число --byakugun, пилюля border ruri .55 / bg ruri .14).
Счётчик = «Сегодня» + «Просрочено»; при 0 скрыт. Заголовок вкладки: «(N) Hado».
Справа: «+ Добавить» (primary) и номер 05. Футер: 波動 HADO · 世界 SEKAI.
## Типографика
- UI: Space Grotesk; кандзи/заголовки групп: Shippori Mincho; мета/служебные: Space Mono
- Группы: кандзи 20px + русское имя Space Mono 11px .35em --kasumi + hairline
今日 СЕГОДНЯ · 遅延 ПРОСРОЧЕНО · 近日 ГРЯДЁТ · 予定 ЗАПЛАНИРОВАНО (порядок фиксирован; пустая — «ПУСТО»)
## Карточка события
bg rgba(27,33,72,.72), border --hair, radius 12, padding 16/20.
Слева плашка срока: --ink-1, border --hair-2, radius 10 — месяц (Mono 9px --nibi),
день (Shippori Mincho 24px --washi), время (Mono 10px --byakugun).
Заголовок 16px --washi (если есть ссылка в источник — dotted underline, hover --byakugun);
подзаголовок 13px --kasumi; служебная строка Mono 11px --nibi: «источник · срок YYYY-MM-DD · относительно» («сегодня», «через 6 дней», «3 дня назад» --kasumi).
Кнопки (принципиально разные):
- «Помню» — ghost: border --hair-2, текст --kasumi, hover --byakugun. Нет в «Запланировано».
- «Сделано» — primary: bg rgba(53,92,214,.22), border --ruri, текст --byakugun, hover bg .4.
Подпись задаёт источник («Поздравил», «Оплатил», «Продлил»).
Нажатие: кнопка блокируется (opacity .45) до ответа; «Сделано» уносит карточку в архив и уменьшает счётчик.
## Архив 書庫
Сворачиваемый блок над настройками, в заголовке счётчик «N закрыто».
Строки opacity .75: заголовок --kasumi, источник · срок Mono --nibi, ярлык закрытия — пилюля --fuji
(fuji = «quiet magic / decay»: закрытое событие угасает по лайфциклу системы).
## Ручное создание («+ Добавить»)
Форма под шапкой (border dashed --hair-2): заголовок*, подзаголовок, срок*, время.
Группа определяется датой: прошла → Просрочено, сегодня → Сегодня, ≤30 дней → Грядёт, дальше → Запланировано. Источник: «вручную».
## Настройки 設定 (свёрнуты, в заголовке логин)
- Часовой пояс: полный IANA-список (Intl.supportedValuesOf)
- Тихие часы: обязаны переходить через полночь (22:00→09:00 верно, 08:00→22:00 — ошибка --sakura)
- Каналы: Веб (без удаления) / Telegram / Webhook (несколько), «Удалить» — ghost, hover --nadeshiko
- Добавить webhook: адрес доставки* + адрес присутствия*, оба URL http(s)
- Подключить Telegram: одноразовый код «/start XXXXXX», живёт 15 минут
## Цветовые правила (60/30/10)
Сумрак ≥60% (фон, карточки, бордеры). Акцент сервиса — холодный полюс (hanada/ruri/byakugun),
только кнопки-действия, бейдж, время — ~10%. Ошибки — --sakura (светлый лепесток, читается).
Насыщенный --ruri никогда не используется как текст; текст — --washi/--kasumi/--nibi/--byakugun.

View File

@@ -0,0 +1,130 @@
# Hado · веб-инбокс — функциональное описание для дизайна
Что есть на экранах и что делает каждый элемент. Без визуальных требований:
размеры, цвета, сетка, стиль — на стороне дизайна.
## Контекст
Hado — сервис напоминаний. Другие сервисы (документы, люди, оплаты) присылают ему
события со сроком, Hado сам решает, когда и куда напомнить: сюда в инбокс, в Telegram,
в умный дом. Веб-инбокс — единственный экран Hado. Пользователь попадает в него уже
залогиненным через общий вход экосистемы, своей формы логина здесь нет.
Два действия над событием, они принципиально разные и должны читаться по-разному:
- **«Помню»** — «я в курсе, отстань до следующей контрольной точки». Событие остаётся,
счётчик не меняется. Напоминание вернётся само: в день события через час, до события
на следующей контрольной точке, после события через день.
- **«Сделано»** — «закрыл вопрос». Событие уходит с экрана навсегда, счётчик падает.
Подпись кнопки может задавать сервис-источник: «Поздравил», «Оплатил», «Продлил».
По умолчанию «Сделано».
## Понятия
| термин | что это |
|---|---|
| Событие | одна штука со сроком: ДР Васи, седула протухает 30 сен, вылет 10 сен 08:15 |
| Срок | день события, иногда с конкретным временем |
| Источник | сервис, который прислал событие (`docs`, `people`). Показывается как подпись |
| Счётчик | число событий в группах «Сегодня» и «Просрочено». Это же число потом будет бейджем на иконке iOS |
| Ссылка в источник | у события может быть ссылка на объект в сервисе-источнике (карточка человека, документ) |
## Экран 1. Инбокс (главная и единственная страница)
### Шапка
- Название «Hado».
- Счётчик: число событий «Сегодня» + «Просрочено». Если ноль — не показывается.
- Заголовок вкладки браузера дублирует счётчик: «(3) Hado», чтобы видеть его в списке вкладок.
### Группы событий
Четыре группы, всегда в этом порядке. Пустая группа показывает подпись «Пусто»
(или скрывается — решение дизайна, но порядок остальных не меняется).
1. **Сегодня** — срок сегодня, время ещё не наступило. Самое важное, наверху.
2. **Просрочено** — срок прошёл, но событие всё ещё надо сделать (седула истекла,
аренда не оплачена). Висит, пока не нажмёшь «Сделано».
3. **Грядёт** — идёт подготовка, срок через дни или недели. В счётчик не входит.
4. **Запланировано** — Hado про событие знает, но ещё молчит. В счётчик не входит.
Внутри группы события отсортированы по сроку, ближайший первым.
### Карточка события
- **Заголовок** — текст от источника («Сегодня ДР — Вася»). Если у события есть ссылка
в источник, заголовок — ссылка, открывает объект в сервисе-источнике (в той же вкладке
или новой — решение дизайна).
- **Подзаголовок** — вторая строка от источника, опциональная («исполняется 34»,
«осталось 30 дней»).
- **Служебная строка** — имя источника и срок: «docs · срок 2026-09-30». Если срок с
временем — «2026-09-10 08:15».
- **Кнопка «Помню»** — есть во всех группах, кроме «Запланировано» (там напоминаний
ещё нет, «помнить» нечего). После нажатия карточка остаётся на месте, ничего
визуально не должно намекать, что событие закрыто.
- **Кнопка «Сделано»** — есть везде, включая «Запланировано» (закрыть можно заранее,
хоть за два месяца). Подпись берётся у источника, если он её задал. После нажатия
карточка исчезает, счётчик уменьшается, если событие было в «Сегодня» или «Просрочено».
Нажатие любой кнопки: кнопка блокируется до ответа сервера, затем список обновляется.
При ошибке кнопка разблокируется, карточка остаётся.
### Блок «Настройки»
Свёрнут по умолчанию, раскрывается на той же странице. В заголовке блока — логин
пользователя.
**Пояс и тихие часы**
- Выбор часового пояса из полного списка (IANA: `America/Montevideo`, `Asia/Shanghai`).
Пользователь много переезжает и меняет пояс руками; все напоминания сдвигаются под него.
- Тихие часы: «с» и «до». По умолчанию с 22:00 до 09:00. В тихие часы Hado молчит во
всех каналах. Ограничение: интервал обязан переходить через полночь («с 22:00 до
09:00» верно, «с 08:00 до 22:00» — ошибка, показать сообщение).
- Кнопка «Сохранить». После сохранения страница обновляется.
**Каналы** — куда Hado доставляет напоминания. Список:
- **Веб** — этот инбокс. Есть всегда, удалить нельзя, кнопки удаления нет. Подпись
«этот инбокс».
- **Telegram** — подпись с идентификатором чата. Появляется после привязки (ниже).
Кнопка «Удалить».
- **Webhook** — адрес, куда Hado шлёт напоминание (в жизни это умный дом). Подпись —
сам адрес. Кнопка «Удалить». Таких может быть несколько.
**Добавить webhook** — форма из двух полей и кнопки:
- «Адрес доставки» — куда Hado отправит напоминание.
- «Адрес присутствия» — откуда Hado спросит «пользователь сейчас дома/рядом?».
- Кнопка «Добавить webhook». Оба поля обязательны, должны быть URL. Ошибки валидации
показываются у формы.
**Подключить Telegram** — кнопка. По нажатию появляется одноразовый код и ссылка на
бота: «Отправь боту `/start ABC123` или открой». Код живёт 15 минут. После того как
пользователь отправит код боту, канал Telegram появится в списке (при следующем
открытии страницы).
### Поведение страницы, невидимое пользователю
- Пока вкладка открыта, видима и пользователь что-то делал за последние 3 минуты,
страница сообщает серверу «я тут». Hado тогда не дёргает Telegram и умный дом:
напоминание считается доставленным в инбокс. Забытая фоновая вкладка «я тут» не
сообщает.
- Никаких всплывающих уведомлений и звуков в самом инбоксе нет: он пассивный экран.
Активные каналы — Telegram и умный дом, потом iOS.
## Экран 0. Нет доступа
Если пользователь пришёл не через общий вход экосистемы, страница не открывается
(ответ 401). Своей заглушки у Hado нет: общий вход сам перебросит на форму логина.
Дизайнить здесь нечего, упомянуто для полноты.
## Вне веба, но для консистентности
**Сообщение в Telegram** — текст: заголовок и подзаголовок события. Под ним две кнопки:
«Помню» и «Сделано» (или подпись источника). После «Сделано» кнопки под сообщением
убираются. Каждое новое напоминание — новое сообщение, старые не редактируются.
Ответы бота на нажатия: «Ок, напомню позже», «Закрыто», «Уже неактуально».
**Умный дом (webhook)** получает те же поля: заголовок, подзаголовок, источник, срок,
подпись кнопки «Сделано» и две ссылки-действия. Как это озвучивается или показывается —
на стороне умного дома.
## Чего здесь нет намеренно
- Истории закрытых событий: закрыл — исчезло, архива нет.
- Ручного создания событий: их присылают только сервисы-источники.
- Откладывания «на дату»: этим занимается правило «Помню».
- Настройки частоты напоминаний: она общая и не настраивается.
- Мобильного приложения: iOS с бейджем, виджетом «Грядёт» и Live Activity — отдельный
проект, его описание будет отдельно.

View File

@@ -0,0 +1,456 @@
<!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>

View File

@@ -0,0 +1,61 @@
<!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+Mono&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:var(--bg)}
a{color:var(--byakugun)}a:hover{color:var(--washi)}
@keyframes skFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-18px)}}
@keyframes skSpin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
@keyframes skSpinBack{from{transform:rotate(360deg)}to{transform:rotate(0deg)}}
@keyframes skPulse{0%,100%{opacity:.35;transform:scale(1)}50%{opacity:.85;transform:scale(1.06)}}
@keyframes skEcho{0%{transform:scale(.55);opacity:.7}100%{transform:scale(1.5);opacity:0}}
</style>
</helmet>
<div style="min-height:100vh;background:var(--bg);display:flex;align-items:center;justify-content:center;padding:60px 24px;font-family:'Space Mono',monospace">
<div style="border:1px solid var(--hair);border-radius:16px;background:var(--ink-1);padding:48px 32px 32px 32px;display:flex;flex-direction:column;align-items:center;gap:40px;max-width:460px;width:100%">
<div style="width:260px;height:260px;position:relative">
<div style="position:absolute;inset:-15%;border:1px solid var(--hair);border-radius:50%;transform:rotateX(69deg)"></div>
<div style="position:absolute;inset:-27%;border:1px dashed var(--hair);border-radius:50%;transform:rotateX(69deg) rotateZ(-8deg);animation:skSpinBack 48s linear infinite"></div>
<!-- эхо: волна напоминания расходится и возвращается -->
<div style="position:absolute;inset:-6%;border:1px solid rgba(143,214,220,.45);border-radius:50%;animation:skEcho 4.2s ease-out infinite"></div>
<div style="position:absolute;inset:-6%;border:1px solid rgba(53,92,214,.5);border-radius:50%;animation:skEcho 4.2s ease-out 2.1s infinite"></div>
<div style="width:100%;height:100%;border-radius:50%;background:radial-gradient(circle at 32% 28%, var(--ruri), #24337F 55%, var(--ink-0) 92%);box-shadow:0 0 70px rgba(53,92,214,.35), inset -22px -26px 60px rgba(9,11,22,.85);animation:skFloat 7.5s ease-in-out infinite"></div>
<div style="position:absolute;inset:0;border-radius:50%;pointer-events:none;box-shadow:0 0 46px rgba(53,92,214,.4);animation:skPulse 2.6s ease-in-out infinite"></div>
<!-- спутник 1: сигнал -->
<div style="position:absolute;inset:-15%;transform:rotateX(69deg);pointer-events:none">
<div style="width:100%;height:100%;animation:skSpin 13s linear infinite">
<div style="position:absolute;top:0;left:50%;width:10px;height:10px;border-radius:50%;background:var(--byakugun);box-shadow:0 0 14px rgba(143,214,220,.75);transform:translate(-50%,-50%) rotateX(-69deg)"></div>
</div>
</div>
<!-- спутник 2: эхо возвращается по внешней орбите -->
<div style="position:absolute;inset:-27%;transform:rotateX(69deg) rotateZ(-8deg);pointer-events:none">
<div style="width:100%;height:100%;animation:skSpinBack 30s linear infinite">
<div style="position:absolute;top:0;left:50%;width:7px;height:7px;border-radius:50%;background:var(--ruri);box-shadow:0 0 10px rgba(53,92,214,.8);transform:translate(-50%,-50%) rotateX(-69deg)"></div>
</div>
</div>
</div>
<div style="text-align:center">
<div style="font-family:'Shippori Mincho',serif;font-size:34px;color:var(--washi)">波動</div>
<div style="font-size:12px;letter-spacing:.4em;color:var(--kasumi);margin-top:6px">HADO · НАПОМИНАНИЯ</div>
<div style="font-size:11px;letter-spacing:.2em;color:var(--nibi);margin-top:10px">05 · 波動共鳴 · СПУТНИКИ: СИГНАЛ --byakugun + ЭХО --ruri</div>
<div style="display:flex;align-items:center;justify-content:center;gap:10px;margin-top:16px">
<span style="width:11px;height:11px;border-radius:50%;background:radial-gradient(circle at 32% 28%, var(--ruri), #24337F 55%, var(--ink-0) 92%);box-shadow:0 0 8px rgba(53,92,214,.45)"></span>
<span style="font-size:10px;letter-spacing:.25em;color:var(--nibi)">МИНИ-ВЕРСИЯ ДЛЯ НАВИГАЦИИ</span>
</div>
</div>
</div>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:560,&quot;height&quot;:640}}"></script>
</body>
</html>

View File

@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sekai — 星雲 Nebula</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500&family=Space+Mono&display=swap" rel="stylesheet">
<style>
/* ═══ Sekai Nebula — самодостаточный компонент ═══
Вставляется целиком: <div class="sk-nebula"> + скрипт звёзд.
Слои: фоновые звёзды → туман → ядро (дуга) → пылевые разломы → звёзды полосы. */
*{box-sizing:border-box;margin:0;padding:0}
body{background:#070912;min-height:100vh}
.sk-nebula{position:relative;min-height:100vh;background:#070912;overflow:hidden}
.sk-nebula__layer{position:absolute;pointer-events:none}
/* базовый туман */
.sk-nebula__mist{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%)}
/* яркое ядро — сгустки по дуге */
.sk-nebula__core{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%)}
/* тёмные пылевые разломы поверх ядра */
.sk-nebula__dust{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%)}
/* контейнеры звёзд (наполняются скриптом) */
.sk-nebula__stars{inset:0;overflow:hidden}
.sk-nebula__band{top:44%;left:-15%;width:135%;height:14%;transform:rotate(-14deg)}
.sk-nebula__caption{position:absolute;left:0;right:0;bottom:36px;display:flex;justify-content:center;padding:0 32px}
.sk-nebula__caption>div{max-width:640px;text-align:center}
.sk-nebula__caption .t{font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.35em;color:#8FD6DC;margin-bottom:10px}
.sk-nebula__caption p{font-family:'Space Grotesk',sans-serif;font-size:13px;line-height:1.7;color:#6B7099}
@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%)}}
</style>
</head>
<body>
<div class="sk-nebula">
<div class="sk-nebula__layer sk-nebula__stars" data-sk-stars></div>
<div class="sk-nebula__layer sk-nebula__mist"></div>
<div class="sk-nebula__layer sk-nebula__core"></div>
<div class="sk-nebula__layer sk-nebula__dust"></div>
<div class="sk-nebula__layer sk-nebula__band" data-sk-band></div>
<div class="sk-nebula__caption">
<div>
<div class="t">星雲 · SEKAI NEBULA</div>
<p>Млечная полоса мира Sekai — диск галактики, видимый с планеты Fumi с ребра, поэтому он ложится дугой через всё небо. Розовое свечение — цветение (сакура), редкие голубые вспышки — лёд и вода, а тёмные разрывы в ядре — пылевые рукава, скрывающие свет.</p>
</div>
</div>
</div>
<script>
(function () {
function star(color, size, bright, x, y, dur, delay) {
var 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;
}
// фоновые звёзды
var sky = document.querySelector('[data-sk-stars]');
for (var 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)
));
}
// россыпь внутри полосы: гауссово распределение вокруг дуги,
// дуга = парабола + дополнительный увод левого края вниз
var band = document.querySelector('[data-sk-band]');
for (var j = 0; j < 170; j++) {
var bright = Math.random() < 0.08;
var r = Math.random();
var color = r < 0.68 ? '#F2ECE0' : (r < 0.88 ? '#F7CEDC' : '#8FD6DC');
var g = (Math.random() + Math.random() + Math.random()) / 3;
var x = Math.random() * 100;
var 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)
));
}
})();
</script>
<template id="__bundler_thumbnail">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" fill="#070912"/><ellipse cx="50" cy="52" rx="34" ry="10" fill="#EA8FAE" opacity=".35" transform="rotate(-14 50 52)"/><ellipse cx="50" cy="50" rx="24" ry="4" fill="#F7CEDC" opacity=".6" transform="rotate(-14 50 50)"/><circle cx="30" cy="38" r="1.2" fill="#F2ECE0"/><circle cx="68" cy="60" r="1.2" fill="#F2ECE0"/><circle cx="60" cy="34" r="1" fill="#8FD6DC"/></svg>
</template>
</body>
</html>

View File

@@ -0,0 +1,181 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sekai — планеты (компоненты)</title>
<link href="https://fonts.googleapis.com/css2?family=Shippori+Mincho:wght@500;700&family=Space+Mono&display=swap" rel="stylesheet">
<style>
/* Sekai tokens (standalone) */
: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;margin:0;padding:0}
body{background:var(--bg);color:var(--washi);font-family:'Space Mono',monospace;min-height:100vh;padding:60px 40px}
@keyframes skFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-18px)}}
@keyframes skFloatSlow{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}}
@keyframes skSpin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
@keyframes skSpinBack{from{transform:rotate(360deg)}to{transform:rotate(0deg)}}
@keyframes skPulse{0%,100%{opacity:.35;transform:scale(1)}50%{opacity:.8;transform:scale(1.06)}}
@keyframes skEcho{0%{transform:scale(.55);opacity:.7}100%{transform:scale(1.5);opacity:0}}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(380px,1fr));gap:40px;max-width:1240px;margin:0 auto}
.cell{border:1px solid var(--hair);border-radius:16px;background:var(--ink-1);padding:48px 24px 32px;display:flex;flex-direction:column;align-items:center;gap:40px}
.stage{width:260px;height:260px;position:relative}
.label{text-align:center}
.label .jp{font-family:'Shippori Mincho',serif;font-size:34px;color:var(--washi)}
.label .en{font-size:12px;letter-spacing:.4em;color:var(--kasumi);margin-top:6px}
.label .meta{font-size:11px;letter-spacing:.2em;color:var(--nibi);margin-top:10px}
/* общие части планеты */
.planet{width:100%;height:100%;border-radius:50%}
.ring{position:absolute;border:1px solid var(--hair);border-radius:50%}
.sat-orbit{position:absolute;pointer-events:none}
.sat-spin{width:100%;height:100%}
.sat{position:absolute;top:0;left:50%}
.glow{position:absolute;inset:0;border-radius:50%;pointer-events:none}
</style>
</head>
<body>
<div style="max-width:1240px;margin:0 auto 40px">
<div style="font-family:'Shippori Mincho',serif;font-size:28px">世界 · Планеты</div>
<div style="font-size:12px;letter-spacing:.2em;color:var(--nibi);margin-top:8px">5 ПЛАНЕТ ЭКОСИСТЕМЫ · КАЖДЫЙ БЛОК — АВТОНОМНЫЙ, КОПИРУЕТСЯ ЦЕЛИКОМ</div>
</div>
<div class="grid">
<!-- ═══════ 01 · MYAKU 脈 — финансы (tsutsuji/kobai + золотая монета) ═══════ -->
<div class="cell">
<div class="stage">
<div class="ring" style="inset:-14%;transform:rotateX(68deg)"></div>
<div class="ring" style="inset:-26%;border-style:dashed;transform:rotateX(68deg);animation:skSpinBack 44s linear infinite"></div>
<div class="planet" style="background:radial-gradient(circle at 32% 28%, var(--tsutsuji), var(--kobai) 52%, var(--ink-0) 92%);box-shadow:0 0 70px rgba(232,78,138,.30), inset -22px -26px 60px rgba(9,11,22,.85);animation:skFloat 7s ease-in-out infinite"></div>
<div class="glow" style="box-shadow:0 0 46px rgba(232,78,138,.35);animation:skPulse 3.2s ease-in-out infinite"></div>
<!-- спутник: золотая монета -->
<div class="sat-orbit" style="inset:-14%;transform:rotateX(68deg)">
<div class="sat-spin" style="animation:skSpin 12s linear infinite">
<div class="sat" style="width:12px;height:12px;border-radius:50%;background:var(--yamabuki);box-shadow:0 0 14px rgba(242,178,51,.7);transform:translate(-50%,-50%) rotateX(-68deg)"></div>
</div>
</div>
</div>
<div class="label">
<div class="jp"></div>
<div class="en">MYAKU · ФИНАНСЫ</div>
<div class="meta">01 · 脈拍安定 · СПУТНИК: МОНЕТА --yamabuki</div>
</div>
</div>
<!-- ═══════ 02 · KIOKU 記憶 — документы (fuji + кристалл) ═══════ -->
<div class="cell">
<div class="stage">
<div class="ring" style="inset:-18%;transform:rotateX(72deg) rotateZ(-12deg)"></div>
<div class="planet" style="background:radial-gradient(circle at 34% 30%, var(--fuji), #4A4177 55%, var(--ink-0) 92%);box-shadow:0 0 60px rgba(159,143,203,.25), inset -22px -26px 60px rgba(9,11,22,.85);animation:skFloatSlow 9s ease-in-out infinite"></div>
<!-- осколки памяти -->
<div style="position:absolute;top:6%;right:-6%;width:14px;height:14px;background:var(--fuji);opacity:.7;transform:rotate(45deg);animation:skFloat 6s ease-in-out infinite"></div>
<div style="position:absolute;bottom:14%;left:-9%;width:9px;height:9px;background:var(--fuji);opacity:.45;transform:rotate(45deg);animation:skFloatSlow 8s ease-in-out infinite"></div>
<!-- спутник: кристалл памяти -->
<div class="sat-orbit" style="inset:-18%;transform:rotateX(72deg) rotateZ(-12deg)">
<div class="sat-spin" style="animation:skSpinBack 20s linear infinite">
<div class="sat" style="width:9px;height:9px;background:var(--fuji);box-shadow:0 0 12px rgba(159,143,203,.7);transform:translate(-50%,-50%) rotateX(-72deg) rotate(45deg)"></div>
</div>
</div>
</div>
<div class="label">
<div class="jp">記憶</div>
<div class="en">KIOKU · ПАМЯТЬ</div>
<div class="meta">02 · 記憶保存 · СПУТНИК: КРИСТАЛЛ --fuji</div>
</div>
</div>
<!-- ═══════ 03 · YUME 夢 — манга (sakura/nadeshiko + лепесток) ═══════ -->
<div class="cell">
<div class="stage">
<div class="ring" style="inset:-16%;transform:rotateX(66deg) rotateZ(10deg)"></div>
<div class="planet" style="background:radial-gradient(circle at 30% 26%, var(--sakura), var(--nadeshiko) 48%, var(--ink-0) 94%);box-shadow:0 0 60px rgba(234,143,174,.28), inset -22px -26px 60px rgba(9,11,22,.8);animation:skFloat 8s ease-in-out infinite"></div>
<!-- лепестки -->
<div style="position:absolute;top:-4%;left:12%;width:10px;height:10px;border-radius:50% 0 50% 50%;background:var(--sakura);opacity:.8;animation:skFloatSlow 5s ease-in-out infinite"></div>
<div style="position:absolute;bottom:4%;right:-4%;width:8px;height:8px;border-radius:50% 0 50% 50%;background:var(--nadeshiko);opacity:.6;animation:skFloat 7s ease-in-out infinite"></div>
<!-- спутник: лепесток -->
<div class="sat-orbit" style="inset:-16%;transform:rotateX(66deg) rotateZ(10deg)">
<div class="sat-spin" style="animation:skSpin 16s linear infinite">
<div class="sat" style="width:11px;height:11px;border-radius:50% 0 50% 50%;background:var(--sakura);box-shadow:0 0 12px rgba(247,206,220,.6);transform:translate(-50%,-50%) rotateX(-66deg)"></div>
</div>
</div>
</div>
<div class="label">
<div class="jp"></div>
<div class="en">YUME · СОН</div>
<div class="meta">03 · 夢降下中 · СПУТНИК: ЛЕПЕСТОК --sakura</div>
</div>
</div>
<!-- ═══════ 04 · FUMI 文 — транскрибация (byakugun/hanada + лёд и капля) ═══════ -->
<div class="cell">
<div class="stage">
<div class="ring" style="inset:-15%;transform:rotateX(70deg)"></div>
<div class="ring" style="inset:-28%;border-style:dashed;transform:rotateX(70deg) rotateZ(16deg);animation:skSpinBack 52s linear infinite"></div>
<div class="planet" style="background:radial-gradient(circle at 32% 28%, var(--byakugun), var(--hanada) 50%, var(--ink-0) 93%);box-shadow:0 0 60px rgba(31,127,196,.30), inset -22px -26px 60px rgba(9,11,22,.85);animation:skFloatSlow 8.5s ease-in-out infinite"></div>
<div class="glow" style="box-shadow:0 0 40px rgba(143,214,220,.28);animation:skPulse 4.4s ease-in-out infinite"></div>
<!-- спутник 1: льдинка -->
<div class="sat-orbit" style="inset:-15%;transform:rotateX(70deg)">
<div class="sat-spin" style="animation:skSpin 14s linear infinite">
<div class="sat" style="width:10px;height:10px;border-radius:50%;background:var(--byakugun);box-shadow:0 0 14px rgba(143,214,220,.75);transform:translate(-50%,-50%) rotateX(-70deg)"></div>
</div>
</div>
<!-- спутник 2: капля на внешней орбите -->
<div class="sat-orbit" style="inset:-28%;transform:rotateX(70deg) rotateZ(16deg)">
<div class="sat-spin" style="animation:skSpinBack 26s linear infinite">
<div class="sat" style="width:7px;height:7px;border-radius:50%;background:var(--hanada);box-shadow:0 0 10px rgba(31,127,196,.7);transform:translate(-50%,-50%) rotateX(-70deg)"></div>
</div>
</div>
</div>
<div class="label">
<div class="jp"></div>
<div class="en">FUMI · ПИСЬМО</div>
<div class="meta">04 · 文字変換 · СПУТНИКИ: ЛЁД --byakugun + КАПЛЯ --hanada</div>
</div>
</div>
<!-- ═══════ 05 · HADO 波動 — напоминания (hanada/ruri + сигнал и эхо) ═══════ -->
<div class="cell">
<div class="stage">
<div class="ring" style="inset:-15%;transform:rotateX(69deg)"></div>
<div class="ring" style="inset:-27%;border-style:dashed;transform:rotateX(69deg) rotateZ(-8deg);animation:skSpinBack 48s linear infinite"></div>
<!-- эхо: волна напоминания расходится и возвращается -->
<div style="position:absolute;inset:-6%;border:1px solid rgba(143,214,220,.45);border-radius:50%;animation:skEcho 4.2s ease-out infinite"></div>
<div style="position:absolute;inset:-6%;border:1px solid rgba(53,92,214,.5);border-radius:50%;animation:skEcho 4.2s ease-out 2.1s infinite"></div>
<div class="planet" style="background:radial-gradient(circle at 32% 28%, var(--ruri), #24337F 55%, var(--ink-0) 92%);box-shadow:0 0 70px rgba(53,92,214,.35), inset -22px -26px 60px rgba(9,11,22,.85);animation:skFloat 7.5s ease-in-out infinite"></div>
<div class="glow" style="box-shadow:0 0 46px rgba(53,92,214,.4);animation:skPulse 2.6s ease-in-out infinite"></div>
<!-- спутник 1: сигнал -->
<div class="sat-orbit" style="inset:-15%;transform:rotateX(69deg)">
<div class="sat-spin" style="animation:skSpin 13s linear infinite">
<div class="sat" style="width:10px;height:10px;border-radius:50%;background:var(--byakugun);box-shadow:0 0 14px rgba(143,214,220,.75);transform:translate(-50%,-50%) rotateX(-69deg)"></div>
</div>
</div>
<!-- спутник 2: эхо возвращается по внешней орбите -->
<div class="sat-orbit" style="inset:-27%;transform:rotateX(69deg) rotateZ(-8deg)">
<div class="sat-spin" style="animation:skSpinBack 30s linear infinite">
<div class="sat" style="width:7px;height:7px;border-radius:50%;background:var(--ruri);box-shadow:0 0 10px rgba(53,92,214,.8);transform:translate(-50%,-50%) rotateX(-69deg)"></div>
</div>
</div>
</div>
<div class="label">
<div class="jp">波動</div>
<div class="en">HADO · НАПОМИНАНИЯ</div>
<div class="meta">05 · 波動共鳴 · СПУТНИКИ: СИГНАЛ --byakugun + ЭХО --ruri</div>
</div>
</div>
</div>
<div style="max-width:1240px;margin:48px auto 0;font-size:11px;letter-spacing:.15em;color:var(--nibi);line-height:2">
МИНИ-ВЕРСИЯ (ДЛЯ НАВИГАЦИИ): КРУГ 11PX С ТЕМ ЖЕ RADIAL-GRADIENT + BOX-SHADOW 0 0 8PX АКЦЕНТА .45<br>
НОВАЯ ПЛАНЕТА = СВОЙ АКЦЕНТ ИЗ ПАЛИТРЫ → ГРАДИЕНТ «АКЦЕНТ → ТЁМНЫЙ АКЦЕНТ → --ink-0 НА ~92%» + КОЛЬЦО rotateX(6672°) + 12 СПУТНИКА-МЕТАФОРЫ
</div>
</body>
</html>

View File

@@ -0,0 +1,188 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sekai — service page template</title>
<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>
/* ═══ Sekai — шаблон страницы сервиса ═══
Заглушки для дизайнеров помечены классом .sk-ph (пунктир).
Заменить: кандзи + имя сервиса в шапке, номер сервиса справа,
кандзи + имя в футере. Контент кладётся в <main>. */
*{box-sizing:border-box;margin:0;padding:0}
html,body{background:#070912}
::selection{background:rgba(143,214,220,.25)}
.sk-page{min-height:100vh;background:#070912;color:#F2ECE0;font-family:'Space Grotesk',sans-serif;
position:relative;overflow:hidden;display:flex;flex-direction:column}
.sk-layer{position:absolute;pointer-events:none;z-index:0}
/* — туманность — */
.sk-mist{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%)}
.sk-core{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%)}
.sk-dust{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%)}
.sk-stars{inset:0;overflow:hidden}
.sk-band{top:44%;left:-15%;width:135%;height:14%;transform:rotate(-14deg)}
/* — сестринские планеты (другие сервисы) — */
.sk-planets{inset:0}
.sk-planet{position:absolute}
.sk-planet .orb-x,.sk-planet .orb-y{display:flex;flex-direction:column;align-items:center;gap:9px}
.sk-planet .dot{pointer-events:auto;border-radius:50%}
.sk-planet .lbl{font-family:'Space Mono',monospace;font-size:9px;letter-spacing:.3em;color:#6B7099;
white-space:nowrap;opacity:0;transition:opacity .4s}
.sk-planet .dot:hover + .lbl{opacity:1}
.sk-planet--a{top:17%;left:7%}
.sk-planet--a .orb-x{animation:skOrbXa 19s ease-in-out infinite}
.sk-planet--a .orb-y{animation:skOrbYa 19s ease-in-out -4.75s infinite}
.sk-planet--a .dot{width:15px;height:15px;background:radial-gradient(circle at 32% 28%,#F7CEDC,#EA8FAE 50%,#090B16 92%);box-shadow:0 0 10px rgba(234,143,174,.4)}
.sk-planet--b{top:26%;right:8%}
.sk-planet--b .orb-x{animation:skOrbXb 27s ease-in-out infinite}
.sk-planet--b .orb-y{animation:skOrbYb 27s ease-in-out -20.25s infinite}
.sk-planet--b .dot{width:12px;height:12px;background:radial-gradient(circle at 32% 28%,#E84E8A,#C13E6A 52%,#090B16 92%);box-shadow:0 0 9px rgba(232,78,138,.4)}
.sk-planet--c{top:60%;left:4.5%}
.sk-planet--c .orb-x{animation:skOrbXc 14s ease-in-out infinite}
.sk-planet--c .orb-y{animation:skOrbYc 14s ease-in-out -3.5s infinite;gap:8px}
.sk-planet--c .dot{width:9px;height:9px;background:radial-gradient(circle at 34% 30%,#9F8FCB,#4A4177 55%,#090B16 92%);box-shadow:0 0 8px rgba(159,143,203,.4)}
/* — каркас страницы — */
.sk-header{position:relative;z-index:1;display:flex;justify-content:space-between;align-items:flex-start;padding:30px 44px 0 44px}
.sk-logo{display:flex;align-items:baseline;gap:14px}
.sk-main{position:relative;z-index:1;max-width:880px;width:100%;margin:0 auto;padding:72px 32px 100px 32px;flex:1}
.sk-footer{position:relative;z-index:1;max-width:880px;width:100%;margin:0 auto;padding:22px 32px 36px 32px;
border-top:1px solid rgba(174,180,214,.16);display:flex;justify-content:center;gap:8px;
font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.3em;color:#6B7099}
/* — заглушки: заменить содержимым сервиса —
Кандзи в шапке: Shippori Mincho 54px #F2ECE0, имя: Space Mono 13px .5em #6B7099 */
.sk-ph{border-bottom:1px dashed rgba(174,180,214,.3);padding-bottom:2px}
.sk-ph--kanji{display:flex;align-items:center;justify-content:center;width:54px;height:54px;
border:1px dashed rgba(174,180,214,.3);border-radius:8px;
font-family:'Space Mono',monospace;font-size:9px;letter-spacing:.2em;color:#6B7099}
.sk-ph--name{font-family:'Space Mono',monospace;font-size:13px;letter-spacing:.5em;color:#6B7099}
.sk-ph--no{font-family:'Space Mono',monospace;font-size:11px;letter-spacing:.3em;color:#6B7099;margin-top:14px}
@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)}}
</style>
</head>
<body>
<div class="sk-page">
<!-- атмосфера -->
<div class="sk-layer sk-stars" data-sk-stars></div>
<div class="sk-layer sk-mist"></div>
<div class="sk-layer sk-core"></div>
<div class="sk-layer sk-dust"></div>
<div class="sk-layer sk-band" data-sk-band></div>
<!-- сестринские планеты: подписи видны при hover -->
<div class="sk-layer sk-planets">
<div class="sk-planet sk-planet--a"><div class="orb-x"><div class="orb-y"><div class="dot"></div><span class="lbl">YUME</span></div></div></div>
<div class="sk-planet sk-planet--b"><div class="orb-x"><div class="orb-y"><div class="dot"></div><span class="lbl">MYAKU</span></div></div></div>
<div class="sk-planet sk-planet--c"><div class="orb-x"><div class="orb-y"><div class="dot"></div><span class="lbl">KIOKU</span></div></div></div>
</div>
<header class="sk-header">
<div class="sk-logo">
<!-- ЗАГЛУШКА: кандзи сервиса (Shippori Mincho 54px #F2ECE0) -->
<span class="sk-ph--kanji">漢字</span>
<!-- ЗАГЛУШКА: латинское имя сервиса -->
<span class="sk-ph sk-ph--name">SERVICE NAME</span>
</div>
<!-- ЗАГЛУШКА: номер сервиса, напр. 04 -->
<div class="sk-ph sk-ph--no">NO</div>
</header>
<!-- контент сервиса сюда -->
<main class="sk-main"></main>
<footer class="sk-footer">
<!-- ЗАГЛУШКА: кандзи + имя сервиса -->
<span class="sk-ph">漢字 SERVICE</span>
<span>· 世界 SEKAI</span>
</footer>
</div>
<script>
(function () {
function star(color, size, bright, x, y, dur, delay) {
var 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;
}
// фоновые звёзды
var sky = document.querySelector('[data-sk-stars]');
for (var 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)
));
}
// звёзды млечной полосы: гаусс вокруг дуги, левый край уходит ниже
var band = document.querySelector('[data-sk-band]');
for (var j = 0; j < 170; j++) {
var bright = Math.random() < 0.08;
var r = Math.random();
var color = r < 0.68 ? '#F2ECE0' : (r < 0.88 ? '#F7CEDC' : '#8FD6DC');
var g = (Math.random() + Math.random() + Math.random()) / 3;
var x = Math.random() * 100;
var 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)
));
}
})();
</script>
<template id="__bundler_thumbnail">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" fill="#070912"/><ellipse cx="50" cy="55" rx="34" ry="9" fill="#EA8FAE" opacity=".3" transform="rotate(-14 50 55)"/><rect x="30" y="32" width="16" height="16" rx="3" fill="none" stroke="#6B7099" stroke-dasharray="3 3"/><rect x="52" y="38" width="20" height="4" rx="2" fill="none" stroke="#6B7099" stroke-dasharray="3 3"/><circle cx="72" cy="28" r="2" fill="#8FD6DC"/></svg>
</template>
</body>
</html>

File diff suppressed because it is too large Load Diff