Скорректировать main к базе 553a1f1 и UI из Pixel

This commit is contained in:
AidarKC 2026-06-10 23:03:01 +04:00
parent e3061b46f9
commit 578b526f96
59 changed files with 2805 additions and 191 deletions

View File

@ -3,14 +3,31 @@
- Краткое описание: минимальный UI-прототип для сабсервера на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста. - Краткое описание: минимальный UI-прототип для сабсервера на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста.
- Что проверять: - Что проверять:
- стартует экран `HOME`; - стартует экран `HOME`;
- на `HOME` видны реальное значение сабсервера или `subserver not set`, реальное значение логина или `login not set`, при отсутствии секрета строка `secret not set`, а также `STATUS`, верхний правый блок с процентом батареи, иконкой батареи и индикатором Wi-Fi, кнопка `SETTINGS` и нижняя подпись `SHiNE subserver (v.0.18)`; - на `HOME` видны реальное значение сабсервера или `subserver not set`, реальное значение логина или `login not set`, при отсутствии секрета строка `secret not set`, а также `STATUS`, верхний правый блок с процентом батареи, иконкой батареи и индикатором Wi-Fi, кнопка баланса, строка `SHiNE: ...`, кнопка `SETTINGS` уменьшенной ширины у правого края и нижняя подпись `SHiNE subserver (v.0.18)`;
- справа от строки логина виден индикатор статуса Solana-аккаунта:
- зелёный, если ключи совпали;
- красный, если mismatch;
- белый контур, если пользователь не найден;
- если статус не зелёный, рядом выводится краткое текстовое пояснение;
- строка Wi-Fi на `HOME` корректно показывает одно из состояний: - строка Wi-Fi на `HOME` корректно показывает одно из состояний:
- `Wi-Fi (not configured) not configured` - `Wi-Fi (not configured) not configured`
- `Wi-Fi (<saved_ssid>) disconnected` - `Wi-Fi (<saved_ssid>) disconnected`
- `Wi-Fi (<current_ssid>) connected` - `Wi-Fi (<current_ssid>) connected`
- строка `SHiNE:` корректно показывает одно из состояний:
- `connected`
- `account not configured`
- `unavailable`
- пока открыт `HOME`, статус сам обновляется без перехода на другие экраны; - пока открыт `HOME`, статус сам обновляется без перехода на другие экраны;
- баланс обновляется кнопкой по нажатию;
- если логин зарегистрирован и секрет/сабсервер заданы, устройство:
- читает `user_pda` через Solana RPC;
- сверяет `root`, `blockchain`, `device` и `subserver` session type `100`;
- поднимает WebSocket-сессию с сервером SHiNE;
- шлёт `Ping` раз в минуту;
- кнопка `SETTINGS` открывает `SETTINGS_MENU`; - кнопка `SETTINGS` открывает `SETTINGS_MENU`;
- свайп влево на `HOME` открывает `SETTINGS_MENU`; - свайп влево на `HOME` открывает `SETTINGS_MENU`;
- если пользователь не найден в Solana PDA, слева снизу появляется `REGISTER ACCOUNT`;
- `REGISTER ACCOUNT` открывает экран-заглушку;
- в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`; - в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`;
- обе видимые карточки меню одного цвета; - обе видимые карточки меню одного цвета;
- свайп вверх показывает `Server` и `Account`; - свайп вверх показывает `Server` и `Account`;
@ -20,6 +37,9 @@
- `SELECT NETWORK` запускает скан; - `SELECT NETWORK` запускает скан;
- после скана показывается список доступных SSID; - после скана показывается список доступных SSID;
- выбор SSID открывает общий экран редактирования текста для пароля; - выбор SSID открывает общий экран редактирования текста для пароля;
- если для этого SSID пароль уже сохранялся раньше, он автоматически подставляется в редактор;
- если затем ввести пароль для другого SSID, пароль первой сети не теряется;
- одновременно хранится до `8` паролей для разных SSID;
- на этом экране видно старое значение, курсор стоит в конце; - на этом экране видно старое значение, курсор стоит в конце;
- две верхние служебные строки над полем ввода отсутствуют; - две верхние служебные строки над полем ввода отсутствуют;
- при вводе пароля Wi-Fi текст показывается открыто, без точек; - при вводе пароля Wi-Fi текст показывается открыто, без точек;
@ -40,6 +60,7 @@
- медленный свайп по экрану не должен превращаться в случайное нажатие кнопки; - медленный свайп по экрану не должен превращаться в случайное нажатие кнопки;
- `ABC/123`, `SHIFT`, `DEL`, `SAVE`, `CANCEL` работают; - `ABC/123`, `SHIFT`, `DEL`, `SAVE`, `CANCEL` работают;
- при успехе SSID и пароль сохраняются, а `HOME` показывает `Wi-Fi connected`; - при успехе SSID и пароль сохраняются, а `HOME` показывает `Wi-Fi connected`;
- если после подключения ко второй сети снова выбрать первую, её старый пароль уже подставлен и достаточно нажать `SAVE`;
- при ошибке показывается `Connection failed`; - при ошибке показывается `Connection failed`;
- `CLEAR SAVED WI-FI` очищает сохранённые настройки; - `CLEAR SAVED WI-FI` очищает сохранённые настройки;
- если сеть была ранее успешно сохранена, после потери связи устройство автоматически пытается переподключиться; - если сеть была ранее успешно сохранена, после потери связи устройство автоматически пытается переподключиться;
@ -60,8 +81,18 @@
- `Subserver` открывает промежуточный экран с `USE SUBSERVER1` и `EDIT MANUALLY`; - `Subserver` открывает промежуточный экран с `USE SUBSERVER1` и `EDIT MANUALLY`;
- `USE SUBSERVER1` возвращает стандартное значение `subserver1`; - `USE SUBSERVER1` возвращает стандартное значение `subserver1`;
- `EDIT MANUALLY` открывает общий экран редактирования и сохраняет значение в NVS; - `EDIT MANUALLY` открывает общий экран редактирования и сохраняет значение в NVS;
- `Secret` открывает экран-заглушку, где сказано, что настройка ещё не реализована;
- `Secret` теперь открывает меню секрета с показом секрета, ручным вводом и генерацией; - `Secret` теперь открывает меню секрета с показом секрета, ручным вводом и генерацией;
- в `SHOW SECRET` показывается прокручиваемый список всех ключей:
- `Secret (base58)`
- `Root key (base58)`
- `Root key priv (base58)`
- `Blockchain key (base58)`
- `Blockchain key priv (base58)`
- `Device key (base58)`
- `Device key priv (base58)`
- `Subserver key (base58)`
- `Subserver key priv (base58)`
- значения ключей показываются полными строками увеличенным шрифтом;
- при смене `login` сохранённый секрет сбрасывается в `not set`; - при смене `login` сохранённый секрет сбрасывается в `not set`;
- во время генерации секрета есть `CANCEL` и подтверждение остановки; - во время генерации секрета есть `CANCEL` и подтверждение остановки;
- при отмене генерации старый секрет, если он был, не должен теряться; - при отмене генерации старый секрет, если он был, не должен теряться;
@ -69,4 +100,5 @@
- свайп вправо из `ACCOUNT_SUBSERVER_SCREEN` и `ACCOUNT_SECRET_SCREEN` возвращает в `ACCOUNT_SCREEN`; - свайп вправо из `ACCOUNT_SUBSERVER_SCREEN` и `ACCOUNT_SECRET_SCREEN` возвращает в `ACCOUNT_SCREEN`;
- если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`. - если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`.
- Ожидаемый результат: новый скетч даёт чистый навигационный каркас и уже умеет настраивать Wi-Fi и серверные адреса на самой ESP32. - Ожидаемый результат: новый скетч даёт чистый навигационный каркас и уже умеет настраивать Wi-Fi и серверные адреса на самой ESP32.
- Дополнительно ожидается: `HOME` уже показывает реальный Solana/WS-статус сабсервера, а отсутствие пользователя в Solana заметно сразу без перехода в настройки.
- Статус: pending - Статус: pending

View File

@ -7,19 +7,17 @@
Этот прототип проверяет базовую механику экранов, крупных кнопок, свайпов, первичную настройку Wi-Fi и настройку серверных адресов через общий экран редактирования текста. Этот прототип проверяет базовую механику экранов, крупных кнопок, свайпов, первичную настройку Wi-Fi и настройку серверных адресов через общий экран редактирования текста.
На этом этапе отсутствуют: На этом этапе отсутствуют:
- логика серверной проверки доступности;
- логин/пароль учётной записи SHiNE; - логин/пароль учётной записи SHiNE;
- PIN; - PIN;
- кошелёк; - кошелёк;
- QR; - QR;
- баланс;
- регистрация; - регистрация;
- PDA и транзакции; - PDA update/create транзакции;
- входящие запросы. - входящие запросы.
## Экраны ## Экраны
Прототип содержит 8 экранов: Прототип содержит 10 экранов:
- `HOME` - `HOME`
- `SETTINGS_MENU` - `SETTINGS_MENU`
- `WIFI_SCREEN` - `WIFI_SCREEN`
@ -27,13 +25,21 @@
- `ACCOUNT_SCREEN` - `ACCOUNT_SCREEN`
- `ACCOUNT_SUBSERVER_SCREEN` - `ACCOUNT_SUBSERVER_SCREEN`
- `ACCOUNT_SECRET_SCREEN` - `ACCOUNT_SECRET_SCREEN`
- `SECRET_SHOW_SCREEN`
- `SECRET_GENERATE_*`
- `TEXT_EDIT_SCREEN` - `TEXT_EDIT_SCREEN`
- `REGISTER_ACCOUNT_PLACEHOLDER`
## HOME ## HOME
Показывает: Показывает:
- сверху слева значение сабсервера или `subserver not set`; - сверху слева значение сабсервера или `subserver not set`;
- ниже значение логина или `login not set`; - ниже значение логина или `login not set`;
- справа от строки логина индикатор статуса Solana-аккаунта:
- зелёный — все ключи совпадают;
- красный — есть mismatch;
- белый контур — пользователь не найден в Solana PDA;
- рядом с индикатором краткий текст ошибки, если статус не зелёный;
- третьей строкой `secret not set`, если секрет ещё не помечен как установленный; - третьей строкой `secret not set`, если секрет ещё не помечен как установленный;
- сверху справа один ряд индикаторов: - сверху справа один ряд индикаторов:
- процент батареи; - процент батареи;
@ -41,7 +47,10 @@
- индикатор Wi-Fi уровня сигнала; - индикатор Wi-Fi уровня сигнала;
- по центру крупный текст `STATUS`; - по центру крупный текст `STATUS`;
- одна строка Wi-Fi вида `Wi-Fi (<ssid>) connected/disconnected`; - одна строка Wi-Fi вида `Wi-Fi (<ssid>) connected/disconnected`;
- снизу большую кнопку `SETTINGS`. - кнопка баланса вида `Balance: <value SOL>` или `Balance: failed to load`, по нажатию выполняет повторный запрос;
- строка `SHiNE: <server> connected/account not configured/unavailable`;
- при отсутствии пользователя в Solana PDA слева снизу появляется кнопка `REGISTER ACCOUNT`;
- снизу кнопку `SETTINGS`, уменьшенную примерно до половины ширины экрана и сдвинутую к правому краю.
- внизу на тёмной полосе подпись `SHiNE subserver (v.0.18)`. - внизу на тёмной полосе подпись `SHiNE subserver (v.0.18)`.
Строка Wi-Fi на `HOME`: Строка Wi-Fi на `HOME`:
@ -50,9 +59,20 @@
- `Wi-Fi (<current_ssid>) connected` - `Wi-Fi (<current_ssid>) connected`
Переходы: Переходы:
- кнопка `REGISTER ACCOUNT` -> `REGISTER_ACCOUNT_PLACEHOLDER`, только если пользователь не найден;
- кнопка `SETTINGS` -> `SETTINGS_MENU`; - кнопка `SETTINGS` -> `SETTINGS_MENU`;
- свайп влево -> `SETTINGS_MENU`. - свайп влево -> `SETTINGS_MENU`.
Фоновая логика:
- пока открыт `HOME`, экран сам обновляется примерно раз в секунду;
- при наличии `login + secret + subserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC;
- сравниваются `root key`, `blockchain key`, `device key` и `subserver` session-запись типа `100`;
- для строки `SHiNE:` устройство держит отдельную WebSocket-сессию с сервером SHiNE:
- авторизация через `AuthChallenge/CreateAuthSession` или `SessionChallenge/SessionLogin`;
- session key = публичный `subserver key`;
- подтверждение создания сессии подписывается `device key`;
- heartbeat выполняется `Ping` раз в минуту.
## SETTINGS_MENU ## SETTINGS_MENU
Показывает вертикальное меню из 3 пунктов: Показывает вертикальное меню из 3 пунктов:
@ -90,7 +110,7 @@
Показывает: Показывает:
- текущий Wi-Fi статус; - текущий Wi-Fi статус;
- сохранённый SSID; - сохранённый SSID и число известных сетей;
- статусное сообщение; - статусное сообщение;
- кнопку `SELECT NETWORK`; - кнопку `SELECT NETWORK`;
- кнопку `CLEAR SAVED WI-FI`; - кнопку `CLEAR SAVED WI-FI`;
@ -109,6 +129,13 @@
Нажатие на SSID открывает `TEXT_EDIT_SCREEN` для ввода пароля. Нажатие на SSID открывает `TEXT_EDIT_SCREEN` для ввода пароля.
Поведение:
- если для выбранного SSID пароль уже был сохранён раньше, он сразу подставляется в поле ввода;
- если пароль меняют для другого SSID, старые сохранённые пароли других сетей не теряются;
- после успешного подключения выбранная сеть становится текущей `wifi_ssid/wifi_pass`;
- одновременно хранится до `8` известных сетей `SSID -> password`;
- `CLEAR SAVED WI-FI` очищает все сохранённые сети и текущую сеть.
Переходы: Переходы:
- свайп вправо из любого режима `WIFI_SCREEN` -> `SETTINGS_MENU` - свайп вправо из любого режима `WIFI_SCREEN` -> `SETTINGS_MENU`
- кнопка `BACK` -> `SETTINGS_MENU` - кнопка `BACK` -> `SETTINGS_MENU`
@ -162,16 +189,34 @@
## ACCOUNT_SECRET_SCREEN ## ACCOUNT_SECRET_SCREEN
Пока это заглушка.
Показывает: Показывает:
- текущий статус секрета `set/not set`; - кнопку `SHOW SECRET` или серую `SECRET NOT SET`, если секрета ещё нет;
- сообщение, что настройка секрета пока не реализована; - кнопку `ENTER SECRET MANUALLY (NOT RECOMMENDED)`;
- кнопку `BACK`. - кнопку `GENERATE SECRET`.
Переходы: Переходы:
- свайп вправо -> `ACCOUNT_SCREEN` - свайп вправо -> `ACCOUNT_SCREEN`
- `BACK` -> `ACCOUNT_SCREEN` - `SHOW SECRET` -> экран просмотра секрета
- `ENTER SECRET MANUALLY (NOT RECOMMENDED)` -> `TEXT_EDIT_SCREEN`
- `GENERATE SECRET` -> экран подтверждения генерации
## SECRET_SHOW_SCREEN
Показывает:
- заголовок `SECRET`;
- вертикально прокручиваемый список ключей;
- `Secret (base58)`;
- `Root key (base58)`;
- `Root key priv (base58)`;
- `Blockchain key (base58)`;
- `Blockchain key priv (base58)`;
- `Device key (base58)`;
- `Device key priv (base58)`;
- `Subserver key (base58)`;
- `Subserver key priv (base58)`;
- для каждого поля показывается формула derivation;
- значения ключей показываются полными строками увеличенным шрифтом;
- кнопку `BACK`.
## TEXT_EDIT_SCREEN ## TEXT_EDIT_SCREEN
@ -191,6 +236,15 @@
- кнопки `SAVE`, `CANCEL`, `DEL`, `CLR`; - кнопки `SAVE`, `CANCEL`, `DEL`, `CLR`;
- большую экранную клавиатуру. - большую экранную клавиатуру.
## REGISTER_ACCOUNT_PLACEHOLDER
Временный экран-заглушка регистрации.
Показывает:
- заголовок `REGISTER ACCOUNT`;
- сообщение, что регистрационный flow пока не реализован;
- кнопку `BACK`.
## Клавиатура ## Клавиатура
Клавиатура единая для всех текстовых вводов. Клавиатура единая для всех текстовых вводов.
@ -211,8 +265,13 @@
- `wifi_ssid` - `wifi_ssid`
- `wifi_pass` - `wifi_pass`
- `wifi_known_good` - `wifi_known_good`
- до `8` сохранённых пар `SSID -> password`
При старте устройства, если сохранён SSID, выполняется попытка подключения к сохранённой сети. При старте устройства, если сохранён SSID, выполняется попытка подключения к текущей сохранённой сети.
Если пользователь уже вводил пароль для сети раньше:
- при повторном выборе этого SSID старый пароль сразу подставляется в editor screen;
- сохранение пароля для другой сети не удаляет уже сохранённые пароли остальных сетей.
Если сеть раньше уже была успешно подключена и помечена как валидная: Если сеть раньше уже была успешно подключена и помечена как валидная:
- после потери связи устройство автоматически пытается переподключиться; - после потери связи устройство автоматически пытается переподключиться;

View File

@ -1,6 +1,10 @@
# Test Device # Test Device
Скрипт заливает официальные Arduino-примеры для быстрой проверки платы. Скрипт заливает официальные Arduino-примеры для быстрой проверки платы.
`burn.sh` теперь:
- сам пытается найти USB-порт ESP32;
- сначала делает быструю инкрементальную сборку;
- если быстрая сборка не удалась, автоматически повторяет полную `clean`-сборку.
Для режимов `widgets`, `audio` и `hello` рядом должен лежать локальный checkout `official-demo/` из официального репозитория Waveshare. В основной git он не добавляется, потому что это большой внешний набор примеров, библиотек, прошивок и артефактов. Для режимов `widgets`, `audio` и `hello` рядом должен лежать локальный checkout `official-demo/` из официального репозитория Waveshare. В основной git он не добавляется, потому что это большой внешний набор примеров, библиотек, прошивок и артефактов.

View File

@ -5,11 +5,29 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BOARD_DIR="$(cd "${ROOT_DIR}/.." && pwd)" BOARD_DIR="$(cd "${ROOT_DIR}/.." && pwd)"
DEMO_BASE="${BOARD_DIR}/official-demo/examples/Arduino-v3.3.5" DEMO_BASE="${BOARD_DIR}/official-demo/examples/Arduino-v3.3.5"
MODE="${1:-widgets}" MODE="${1:-widgets}"
PORT="${PORT:-/dev/ttyACM0}" PORT="${PORT:-}"
FQBN="${FQBN:-esp32:esp32:esp32s3:USBMode=hwcdc,CDCOnBoot=cdc,UploadSpeed=921600,CPUFreq=240,FlashMode=dio,FlashSize=16M,PartitionScheme=app3M_fat9M_16MB,PSRAM=opi}" FQBN="${FQBN:-esp32:esp32:esp32s3:USBMode=hwcdc,CDCOnBoot=cdc,UploadSpeed=921600,CPUFreq=240,FlashMode=dio,FlashSize=16M,PartitionScheme=app3M_fat9M_16MB,PSRAM=opi}"
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/.arduino-build/build-${MODE}}" BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/.arduino-build/build-${MODE}}"
OUT_DIR="${OUT_DIR:-${ROOT_DIR}/.arduino-build/out-${MODE}}" OUT_DIR="${OUT_DIR:-${ROOT_DIR}/.arduino-build/out-${MODE}}"
detect_port() {
local detected
detected="$(arduino-cli board list 2>/dev/null | awk '/\/dev\/tty(ACM|USB)/ {print $1; exit}')"
if [[ -n "${detected}" ]]; then
echo "${detected}"
return 0
fi
for candidate in /dev/ttyACM* /dev/ttyUSB*; do
if [[ -e "${candidate}" ]]; then
echo "${candidate}"
return 0
fi
done
return 1
}
case "${MODE}" in case "${MODE}" in
hello) SKETCH_DIR="${DEMO_BASE}/examples/01_HelloWorld" ;; hello) SKETCH_DIR="${DEMO_BASE}/examples/01_HelloWorld" ;;
widgets) SKETCH_DIR="${DEMO_BASE}/examples/05_LVGL_Widgets" ;; widgets) SKETCH_DIR="${DEMO_BASE}/examples/05_LVGL_Widgets" ;;
@ -34,6 +52,13 @@ case "${MODE}" in
;; ;;
esac esac
if [[ -z "${PORT}" ]]; then
if ! PORT="$(detect_port)"; then
echo "Failed to auto-detect ESP32 port. Set PORT=/dev/ttyACM0 ./burn.sh ${MODE}" >&2
exit 3
fi
fi
echo "== Mode: ${MODE}" echo "== Mode: ${MODE}"
echo "== Sketch: ${SKETCH_DIR}" echo "== Sketch: ${SKETCH_DIR}"
echo "== Port: ${PORT}" echo "== Port: ${PORT}"
@ -41,17 +66,23 @@ echo "== FQBN: ${FQBN}"
mkdir -p "${BUILD_DIR}" "${OUT_DIR}" mkdir -p "${BUILD_DIR}" "${OUT_DIR}"
arduino-cli compile \ compile_args=(
--clean \ --fqbn "${FQBN}"
--fqbn "${FQBN}" \ --build-path "${BUILD_DIR}"
--build-path "${BUILD_DIR}" \ --output-dir "${OUT_DIR}"
--output-dir "${OUT_DIR}" \ --library "${DEMO_BASE}/libraries/GFX_Library_for_Arduino"
--library "${DEMO_BASE}/libraries/GFX_Library_for_Arduino" \ --library "${DEMO_BASE}/libraries/SensorLib"
--library "${DEMO_BASE}/libraries/SensorLib" \ --library "${DEMO_BASE}/libraries/XPowersLib"
--library "${DEMO_BASE}/libraries/XPowersLib" \ --library "${DEMO_BASE}/libraries/lvgl"
--library "${DEMO_BASE}/libraries/lvgl" \ --library "${DEMO_BASE}/libraries/Mylibrary"
--library "${DEMO_BASE}/libraries/Mylibrary" \
"${SKETCH_DIR}" "${SKETCH_DIR}"
)
echo "== Compile: fast incremental build"
if ! arduino-cli compile "${compile_args[@]}"; then
echo "== Compile: fast build failed, retrying clean build"
arduino-cli compile --clean "${compile_args[@]}"
fi
arduino-cli upload \ arduino-cli upload \
-p "${PORT}" \ -p "${PORT}" \

View File

@ -1,2 +1,2 @@
client.version=1.2.151 client.version=1.2.155
server.version=1.2.143 server.version=1.2.147

View File

@ -45,12 +45,10 @@
- **Обычные линии:** SVG `<path> Q` (квадратичные Безье) — тонкие матовые дуги (`stroke-width ~1.01.2`), - **Обычные линии:** SVG `<path> Q` (квадратичные Безье) — тонкие матовые дуги (`stroke-width ~1.01.2`),
градиент с глубоким уходом в прозрачность: неон-центр `0.42` → цвет роли `0.07` у аватарки (растворяются градиент с глубоким уходом в прозрачность: неон-центр `0.42` → цвет роли `0.07` у аватарки (растворяются
в фоне, не спорят с сияющими). Изгиб реагирует на скорость. в фоне, не спорят с сияющими). Изгиб реагирует на скорость.
- **Сияющие связи (плазменный композитинг):** один центральный S-путь (cubic Bézier) и три наложенных - **Сияющие связи (двухслойный «световод», Neon Layering):** два пути на одну связь — широкий размытый
слоя с одинаковым `d`: `.fg-plasma-flare` (16px, `#00bfff`, blur6), `.fg-plasma-tube` (6px, `#00e5ff`, GLOW (`stroke-width 4`, неон, `filter: blur(2px)`, `opacity 0.4`) + тонкий чёткий CORE (`1.5px`,
blur2) и `.fg-plasma-core` (2px, `#dffaff`). Поле и трубка идут в `mix-blend-mode: screen`, поэтому `#e0f7fc`). Линия остаётся изящной, но обретает объёмное OLED-свечение; растут оба синхронно (общий
свечение складывается аддитивно с тёмным фоном и ярче проявляется в пересечениях у центра. dashoffset). Никаких бегущих импульсов.
- **Обычные связи:** теперь это мягкое цветное свечение по типу связи (семья/друзья/бизнес/контакт) —
широкая полупрозрачная подложка плюс тонкое ядро, без SVG-blur.
- **Жесты:** свайп-pan с инерцией (новое касание прерывает); короткий тап — центрирование + нижний - **Жесты:** свайп-pan с инерцией (новое касание прерывает); короткий тап — центрирование + нижний
сниппет; долгий тап — контекстное меню (в `#modal-root`, позиция по `getBoundingClientRect`); тап сниппет; долгий тап — контекстное меню (в `#modal-root`, позиция по `getBoundingClientRect`); тап
по центру — профиль. Нажатия на чипы фильтров гасят `pointerdown` (stopPropagation), чтобы сцена не по центру — профиль. Нажатия на чипы фильтров гасят `pointerdown` (stopPropagation), чтобы сцена не
@ -63,7 +61,7 @@
(`hue-rotate`). На компоновщике (GPU), не будит rAF; подписи имён остаются контрастными. (`hue-rotate`). На компоновщике (GPU), не будит rAF; подписи имён остаются контрастными.
- **Стеклянные чипы фильтров (frosted glass):** `background: rgba(255,255,255,0.03)`, - **Стеклянные чипы фильтров (frosted glass):** `background: rgba(255,255,255,0.03)`,
`backdrop-filter: blur(12px)`, граница `0.5px solid rgba(255,255,255,0.1)`; активный — подсвечен сине-голубым. `backdrop-filter: blur(12px)`, граница `0.5px solid rgba(255,255,255,0.1)`; активный — подсвечен сине-голубым.
- **Поллиш:** «прицел» в центре (+пульс при захвате фокуса); «дыхание» фокуса (бесконечная CSS-анимация - **Поллиш:** «дыхание» фокуса (бесконечная CSS-анимация
размера, GPU, не будит rAF); свечение «сияющих» узлов — мягкая медленная пульсация (3.6с) многослойной размера, GPU, не будит rAF); свечение «сияющих» узлов — мягкая медленная пульсация (3.6с) многослойной
`box-shadow` + размытый ореол через SVG-фильтр `#fg-shine-glow`; тестовые фото-аватарки (`NETWORK_PHOTOS`); `box-shadow` + размытый ореол через SVG-фильтр `#fg-shine-glow`; тестовые фото-аватарки (`NETWORK_PHOTOS`);
хард-лимит ~90 DOM-аватарок (остальное — SVG-точки). хард-лимит ~90 DOM-аватарок (остальное — SVG-точки).
@ -90,6 +88,94 @@
тап по узлам переключает сети. тап по узлам переключает сети.
- Реальный путь (`/network-view`) требует живого `wss://shineup.me/ws` (локально — `ws_open_error`, это норма). - Реальный путь (`/network-view`) требует живого `wss://shineup.me/ws` (локально — `ws_open_error`, это норма).
## Режим «Интерактивная паутина» (ветка `pixel-web`, эксперимент, только лаборатория)
Включается чипом «🌌 Вселенная». Дальние уровни (2-3) по умолчанию скрыты и раскрываются локально:
- **Hover-превью (наведение):** навёл мышь/палец на узел — его ветка временно выплывает; убрал —
втягивается. Реализация: `pointerover/out` (мышь) и `pointerdown/up` (палец) → `onNodeHover`
`graph.setHover(node|null)`; узел получает флаг `hovered`.
- **Фиксация кликом (pin):** тап/клик по узлу → `graph.toggleExpand` ставит флаг `pinned` — ветка
остаётся раскрытой и после ухода курсора. Повторный клик по раскрытому узлу **сворачивает** его
(надёжный toggle: `isOpen = pinned || expandP>0.5`сброс `pinned`+`hovered`).
- Эффективное раскрытие = `pinned || hovered` (см. `expandTargetOf`), прогресс `expandP` (~400мс).
- **Spotlight-затемнение:** пока есть закреплённая ветка, остальные тускнеют до `SPOTLIGHT_DIM=0.25`
(узлы и их линии), фокус и закреплённая/наведённая ветка — 100%. Плавно через `spotCur` (lerp).
- **Узлы 2-го уровня — полноценные аватарки:** фото-лицо (pravatar) + имя, `DEEP2_SCALE=0.62`
(≈radius 16px), `DEEP2_OPACITY=0.85`. Не «пустые кружки», а видимые друзья друзей.
- **Глобальный сброс:** тап по корню (Иван) → `collapseAll()` снимает `pinned`/`hovered` → 100% яркость.
- **Адаптивное расталкивание (collision):** раскрытая ветка усиливает отталкивание соседних узлов
1-го уровня пропорционально `expandP` (`EXPAND_REPULSION=2.4`) — кластеры разъезжаются, не накладываясь.
- **Камера-доводчик:** при фиксации ветки, если её «веер» упирается в край экрана, камера мягко
дотягивается (`glideCameraTo` → `camTargetX/Y`, lerp `CAM_GLIDE_K` в tick). Любой жест отменяет доводчик.
- **Свободный зум:** колесо мыши (`onWheel`) и щипок двумя пальцами (`activePointers`/`pinching`) —
масштаб `zoom` (0.552.6), «к точке» под курсором/центром щипка; мир масштабируется CSS-`scale`,
линии (отдельный SVG) пересчитываются в экранных координатах (× `zoom`).
- **Синхро-пульс линий:** сияющие/трековые «световоды» (`.fg-edge-glow`/`.fg-edge-core`) «дышат»
толщиной/размытием 3.6с — в такт ободку сияющего узла (в покое SVG не перерисовывается → синхронно).
- Мерцающие микрозвёзды 3-го уровня (`fg-star-twinkle`), хаптика (`navigator.vibrate`) на нажатие/раскрытие/натяжение.
### Умный фокус (Smart Zoom / «аквариум») — ветка `pixel-aquarium`
**Наведение** (hover/палец) на узел — лёгкое превью ветки (раскрытие на месте, без камеры).
**Клик/тап по ЛЮБОМУ узлу** — **погружение (dive)** с кинематографичным наездом:
- **Камера-полёт + зум** (`diveTo` → `diveTargetId`/`diveZoom=1.7`, лёт в `tick` с `DIVE_FLY_K` ≈600мс):
узел плавно центрируется (offset ~0) и **вырастает до единого видимого размера** `HERO_VISUAL=1.4`
независимо от уровня (`depthScale = HERO_VISUAL / baseScaleOf`); его прямые дети — до `DIVE_CHILD_VISUAL`.
- **Адаптивный радиус орбиты (фикс слипания):** дети раскладываются на кольце
`ringR = max(baseR + радиус_родителя, числоетей × 13)`НЕ лезут на (увеличенный зумом) родитель
и друг на друга (проверено: мин. дистанция 125px, 0 наложений). Радиус растёт вместе с зумом родителя.
- **Глубина «аквариума»** (`contextTargetOf` → `depthScale`/`depthBlur`/`spotCur`, лерп): Иван и боковые
ветки **уменьшаются** (root ×0.55, фон ×0.55) + уходят в **blur 3px** + тускнеют до 0.25 → задний план.
- **Железный Spotlight (единый активный путь):** `diveTo` сначала гасит ВСЕ прежние pin/hover, затем
раскрывает только путь к новой цели. Открыто → путь Иван→…→узел = 1.0, остальное = 0.25; переключение
веток сбрасывает прежнюю; **выход/`exitDive`/тап по Ивану → ВСЕ узлы гарантированно 1.0 + камера отъезжает**.
- **Нить-крошка**: путь (`divePathSet`/`onPath`) горит ярким «световодом» — виден путь назад к Ивану.
- **Pinch-to-Zoom + LOD**: щипок/колесо меняют `zoom`; при `zoom ≥ LOD_ZOOM (1.55)` видимые точки 3-го
уровня **дорисовываются как аватарки** (`updateLod`/`setNodeLod`), при отдалении — обратно в точки.
- Глубина — фейк-3D через масштаб + CSS-`blur` (GPU), без WebGL.
**Полиш (партия 1):** веер детей раскрывается **полукругом «наружу»** (от пути назад, `DEEP_FAN`,
по `sibIndex`) — не перекрывает нить-крошку; **LOD с гистерезисом** (`LOD_ZOOM_UP=1.6`/`DOWN=1.4` — без
мигания у порога); **двойной тап по фону** и **сильный pinch-out на мин. зуме** = быстрый выход;
**префетч аватарок** детей при наведении/нырке.
**Фишки (партия 2, лаборатория):**
- **Поиск + телепорт** — строка `.fg-search`; Enter → `graph.findNode(имя)` → камера летит к узлу (dive в
«Вселенной», иначе перецентр).
- **Хлебные крошки**`.fg-breadcrumb` «Иван Нина Ада» (движок шлёт `onDiveChange(path)`,
API `getDivePath()`); клик по корню — полный сброс, по предку — навигация на тот уровень.
- **Бейдж числа связей**`.fg-node-badge` (число из `degreeById`, обновляется в `updateBadges`).
- **Цветовые кластеры** — мягкая аура узла по типу связи (CSS `is-family/friend/business/contact`).
**Линии-«жгуты» (партия 4, по референсу — плазменный композитинг):**
- **Сияющие** — ОДИН центральный S-путь (cubic Bézier) + ТРИ наложенных слоя с ОДИНАКОВЫМ `d` (объём
из толщины+размытия, НЕ из геометрии — никаких расходящихся линий):
Настоящий НЕОН (видимый ореол вокруг яркого ядра; поле/трубка в `mix-blend-mode: screen` — свет
складывается аддитивно с тёмным фоном, а в пересечениях у центра ярче — энергохаб):
- `.fg-plasma-flare` — плазменное облако: 16px, `#00bfff`, opacity 0.42, **`feGaussianBlur` stdDev=6**, screen (+ «дыхание» 3.6с);
- `.fg-plasma-tube` — направляющий свет: 6px, `#00e5ff`, opacity 0.85, **`feGaussianBlur` stdDev=2**, screen;
- `.fg-plasma-core` — ядро: 2px, `#dffaff` (светло-голубо-белое), opacity 1, без размытия.
Толщина/насыщенность подогнаны под референс (толстая яркая голубая плазма, гладкие края).
S-волна спокойная/изящная (amp до 13px). Размытие — именно SVG-фильтры (`#fg-plasma-blur6/2`), т.к.
CSS-`filter` на `<path>` в части мобильных WebView не применяется (отсюда был «плоский»/«канатный» вид).
⚠️ Это НЕ Canvas-движок (не библиотека force-graph): связи — реальные SVG `<path>`, фильтры применяются.
Прозрачность слоёв inline (× spotlight/глубину). Тяжёлый blur только у сияющих (их мало) — перф.
- **Не-сияущие** — мягкое свечение **в цвете связи** (семья/друзья/бизнес/контакт): широкая
полупрозрачная подложка + тонкое ядро, без SVG-blur (дёшево). «Похоже, но тише».
**Фишки (партия 3, лаборатория):**
- **Общие связи** — среди друзей человека один помечен как «общий» (он и твой друг тоже): золотой
ободок + ★ (CSS `is-common`; в лаб-генерации `addDeepLevels` подставляет узнаваемого друга Ивана).
- **Доступность** — визуально скрытый (`sr-only`) текстовый список графа `.fg-a11y` (центр + связи
1-го уровня) для скринридеров; обновляется в `updateA11y` при перестроении. Полезно и для реального пути.
**Автопроверки (`?fgtest`):** `js/pages/network/selftest.js` автозапускается в лаборатории при `?fgtest`,
прогоняет 17 ассертов (центровка/collision/полукруг/spotlight/переключение/LOD/поиск/крошки/бейдж/выход) через
детерминированные dev-хелперы движка `graph.debugState()` и `graph.pumpForTest()` (синхронно докручивают кадры
до покоя — не зависят от троттлинга rAF). Результат → консоль и `window.__fgTestResults`. В обычной работе не активны.
> ⚠️ Эксперименты на ветках `pixel-web` (паутина) и `pixel-aquarium` (Smart Zoom) — для отката.
> Реальный путь `/network-view` не затронут: deep-код под `tier ≥ 2` / `hasDeep`, dive — только tier≥2
> (в реальном пути их нет), depthScale/Blur по умолчанию нейтральны, `updateLod` выходит при `!hasDeep`.
## Ограничения / на будущее ## Ограничения / на будущее
- Многоуровневая глубина (друзья друзей мельче, 3-й уровень — точки), кластеры, «общие связи» - Многоуровневая глубина (друзья друзей мельче, 3-й уровень — точки), кластеры, «общие связи»
упираются в API (отдаёт только прямые связи) — требуют доработки сервера. упираются в API (отдаёт только прямые связи) — требуют доработки сервера.

File diff suppressed because it is too large Load Diff

View File

@ -60,34 +60,39 @@
transition: opacity 420ms ease; /* плавное появление линий при перестройке */ transition: opacity 420ms ease; /* плавное появление линий при перестройке */
} }
/* Сияющая связь = плазменный композитинг (3 слоя на одном S-пути, см. renderEdges). */ /* Сияющая связь = плазменный композитинг (3 слоя на ОДНОМ S-пути, см. renderEdges).
.fg-plasma-flare { Настоящий НЕОН: яркое светлое ядро + ВИДИМЫЙ голубой ореол вокруг. Слои поля/трубки идут в режиме
mix-blend-mode: screen свет складывается аддитивно с тёмным фоном (как реальное свечение), а в точках
пересечения нитей у центра ярче (энергетический хаб). Прозрачность слоёв inline (×spotlight/глубину). */
.fg-plasma-flare { /* нижний: широкое насыщенное голубое плазменное свечение (по референсу) */
fill: none; fill: none;
stroke: #00bfff; stroke: #00bfff; /* глубокий голубой */
stroke-width: 16; stroke-width: 16;
stroke-linecap: round; stroke-linecap: round;
filter: url(#fg-plasma-blur6); filter: url(#fg-plasma-blur6); /* мягкое объёмное свечение (гладкие края — как на референсе) */
mix-blend-mode: screen; mix-blend-mode: screen; /* аддитивное свечение поверх тёмного фона */
/* синхро-«дыхание» поля толщиной в такт ободку сияющего узла (3.6с); прозрачность не трогаем (inline) */
animation: fg-plasma-breath 3.6s ease-in-out infinite; animation: fg-plasma-breath 3.6s ease-in-out infinite;
} }
.fg-plasma-tube { .fg-plasma-tube { /* средний: яркая толстая неоновая трубка */
fill: none; fill: none;
stroke: #00e5ff; stroke: #00e5ff; /* яркий циан */
stroke-width: 6; stroke-width: 6;
stroke-linecap: round; stroke-linecap: round;
filter: url(#fg-plasma-blur2); filter: url(#fg-plasma-blur2); /* SVG feGaussianBlur stdDeviation=2 */
mix-blend-mode: screen; mix-blend-mode: screen; /* аддитивное свечение */
} }
.fg-plasma-core { .fg-plasma-core { /* верхний: яркое чёткое ядро (светло-голубо-белое) */
fill: none; fill: none;
stroke: #dffaff; stroke: #dffaff; /* светло-голубо-белое — «жидкое» ядро */
stroke-width: 2; stroke-width: 2;
stroke-linecap: round; stroke-linecap: round;
} }
/* мягкое «дыхание» плазменного облака толщиной, синхронно с пульсом сияющего ободка (3.6с) */
@keyframes fg-plasma-breath { @keyframes fg-plasma-breath {
0%, 100% { stroke-width: 14; } 0%, 100% { stroke-width: 14; }
50% { stroke-width: 19; } 50% { stroke-width: 19; }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
@ -120,7 +125,7 @@
height: 52px; height: 52px;
margin: 0; margin: 0;
font-size: 16px; font-size: 16px;
transition: box-shadow 160ms ease, border-color 160ms ease; transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
} }
.fg-node.is-family .node-dot { .fg-node.is-family .node-dot {
@ -150,10 +155,18 @@
box-shadow: 0 0 0 2px rgba(143, 231, 255, 0.45), 0 10px 22px rgba(4, 8, 15, 0.45); box-shadow: 0 0 0 2px rgba(143, 231, 255, 0.45), 0 10px 22px rgba(4, 8, 15, 0.45);
} }
/* Тактильный отклик «нажатия вглубь»: аватарка слегка вдавливается (scale 0.92), а неоновое кольцо
вспыхивает заметно ярче (~1.5×). Срабатывает при наведении, фокусе и зажатии (.is-pressed). */
.fg-node:focus-visible .node-dot, .fg-node:focus-visible .node-dot,
.fg-node:hover .node-dot { .fg-node:hover .node-dot,
border-color: rgba(166, 218, 255, 0.95); .fg-node.is-pressed .node-dot {
box-shadow: 0 0 0 3px rgba(77, 160, 255, 0.22), 0 8px 16px rgba(4, 8, 15, 0.35); transform: scale(0.92);
border-color: rgba(160, 240, 255, 0.95);
box-shadow: 0 0 0 2px rgba(150, 238, 255, 0.6), 0 0 22px rgba(120, 230, 255, 0.85);
}
@media (prefers-reduced-motion: reduce) {
.fg-node.is-pressed .node-dot { transform: none; }
} }
/* «Сияние» мягкое живое свечение НА УЗЛЕ (аватарке), а не на линии связи. /* «Сияние» мягкое живое свечение НА УЗЛЕ (аватарке), а не на линии связи.
@ -283,28 +296,50 @@
.fg-dot.is-contact { background: #36435c; } .fg-dot.is-contact { background: #36435c; }
.fg-dot.is-shine { box-shadow: 0 0 9px rgba(130, 235, 255, 0.75); } .fg-dot.is-shine { box-shadow: 0 0 9px rgba(130, 235, 255, 0.75); }
/* «Прицел» в центре экрана (зона фокуса) — позади узлов */ /* === Глубина 2-3 уровней (прототип «Вселенная», только лаборатория) ============== */
.fg-reticle { /* 2-й уровень «друзья друзей»: полноценная аватарка (лицо + имя), просто мельче основных.
position: absolute; Масштаб/прозрачность задаёт движок; здесь читаемый ободок и подпись (не «дырка»). */
left: 50%; .fg-node.is-tier2 .node-dot {
top: 50%; border-color: rgba(170, 200, 240, 0.65);
width: 64px; box-shadow: 0 2px 8px rgba(4, 8, 15, 0.4);
height: 64px; }
margin: -32px 0 0 -32px; .fg-node.is-tier2 .fg-node-label {
border-radius: 50%; font-size: 9px;
border: 2px dashed rgba(150, 190, 255, 0.3); opacity: 0.9;
pointer-events: none; top: calc(100% + 1px);
z-index: 0;
opacity: 0.45;
transition: width 200ms ease, height 200ms ease, margin 200ms ease, border-color 200ms ease, opacity 200ms ease;
} }
.fg-reticle.is-locked { /* 3-й уровень — микрозвезда: светящаяся точка без картинки (эффект далёкого созвездия). */
width: 94px; .fg-dot.is-tier3 {
height: 94px; width: 9px;
margin: -47px 0 0 -47px; height: 9px;
border-color: rgba(130, 235, 255, 0.65); border: 0;
opacity: 0.85; background: radial-gradient(circle, #e6f6ff 0%, rgba(150, 215, 255, 0.95) 38%, rgba(120, 200, 255, 0) 75%);
box-shadow: 0 0 6px rgba(150, 220, 255, 0.9), 0 0 13px rgba(115, 200, 255, 0.5);
/* медленное мерцание «звезды» по box-shadow/яркости (НЕ opacity/scale: ими управляет движок при
раскрытии). У каждой звезды своя задержка (inline animation-delay) живое созвездие. */
animation: fg-star-twinkle 3.4s ease-in-out infinite;
}
.fg-dot.is-tier3.is-shine {
background: radial-gradient(circle, #ffffff 0%, rgba(160, 240, 255, 1) 38%, rgba(120, 230, 255, 0) 75%);
box-shadow: 0 0 8px rgba(160, 240, 255, 1), 0 0 16px rgba(115, 220, 255, 0.7);
}
@keyframes fg-star-twinkle {
0%, 100% { box-shadow: 0 0 3px rgba(150, 220, 255, 0.45), 0 0 7px rgba(115, 200, 255, 0.25); filter: brightness(0.78); }
50% { box-shadow: 0 0 7px rgba(165, 235, 255, 0.95), 0 0 15px rgba(120, 210, 255, 0.6); filter: brightness(1.3); }
}
@media (prefers-reduced-motion: reduce) {
.fg-dot.is-tier3 { animation: none; }
}
/* Чип-переключатель «Вселенная» — активное состояние наследует стиль .fg-filter-chip.is-active */
.fg-deep-chip.is-active {
background: rgba(150, 130, 255, 0.18);
border-color: rgba(190, 170, 255, 0.6);
box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.12), 0 0 14px rgba(150, 120, 255, 0.3);
color: #efeaff;
} }
/* «Призрак» старой карты при Z-переходе (эффект погружения) */ /* «Призрак» старой карты при Z-переходе (эффект погружения) */
@ -319,17 +354,6 @@
transition: transform 1000ms cubic-bezier(0.16, 1, 0.3, 1), opacity 1000ms ease; transition: transform 1000ms cubic-bezier(0.16, 1, 0.3, 1), opacity 1000ms ease;
} }
/* Импульс центрального кольца при захвате нового фокуса */
.fg-reticle.is-pulse {
animation: fg-reticle-pulse 0.6s ease;
}
@keyframes fg-reticle-pulse {
0% { transform: scale(1); }
40% { transform: scale(1.22); border-color: rgba(130, 235, 255, 0.9); }
100% { transform: scale(1); }
}
/* Панель фильтров слоёв (оверлей под шапкой) */ /* Панель фильтров слоёв (оверлей под шапкой) */
.fg-filter-bar { .fg-filter-bar {
position: absolute; position: absolute;
@ -514,3 +538,141 @@
.fg-sheet-actions > button { .fg-sheet-actions > button {
flex: 1; flex: 1;
} }
/* === Партия 2: бейдж-счётчик связей, поиск, хлебные крошки, цветовые кластеры ============ */
/* Бейдж числа связей — маленькая пилюля в правом-верхнем углу аватарки */
.fg-node-badge {
position: absolute;
top: -2px;
right: -2px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 999px;
background: rgba(16, 24, 40, 0.92);
border: 1px solid rgba(150, 200, 255, 0.5);
color: #d9ecff;
font-size: 9px;
font-weight: 700;
line-height: 14px;
text-align: center;
pointer-events: none;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
.fg-node.is-focus .fg-node-badge {
background: rgba(61, 196, 223, 0.95);
border-color: rgba(220, 245, 255, 0.8);
color: #06131c;
}
.fg-node.is-tier2 .fg-node-badge { transform: scale(0.85); }
/* Цветовые кластеры: мягкая аура узла по типу связи (визуально группирует «семью/друзей/бизнес») */
.fg-node.is-family .node-dot { box-shadow: 0 0 16px rgba(255, 159, 94, 0.20); }
.fg-node.is-friend .node-dot { box-shadow: 0 0 16px rgba(120, 179, 255, 0.20); }
.fg-node.is-business .node-dot { box-shadow: 0 0 16px rgba(190, 150, 255, 0.20); }
.fg-node.is-contact .node-dot { box-shadow: 0 0 16px rgba(170, 190, 220, 0.16); }
/* сияющие/фокус — свой эффект свечения (см. выше), ауру кластера не навязываем */
.fg-node.is-shine .node-dot, .fg-node.is-focus .node-dot { box-shadow: none; }
/* Строка поиска (оверлей вверху, под панелью фильтров) */
.fg-search {
position: absolute;
top: max(92px, calc(env(safe-area-inset-top) + 88px));
left: 50%;
transform: translateX(-50%);
z-index: 12;
width: min(280px, 70vw);
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.04);
border: 0.5px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.08);
}
.fg-search input {
flex: 1;
border: 0;
background: transparent;
color: #eaf2ff;
font-size: 13px;
outline: none;
}
.fg-search input::placeholder { color: #7d8aa6; }
.fg-search .fg-search-ico { color: #9fc0ff; font-size: 13px; }
/* Хлебные крошки навигации (стек погружений: Иван Нина Ада) */
.fg-breadcrumb {
position: absolute;
top: max(132px, calc(env(safe-area-inset-top) + 128px));
left: 0;
right: 0;
z-index: 12;
display: none;
justify-content: center;
flex-wrap: wrap;
gap: 4px;
padding: 0 12px;
pointer-events: none;
}
.fg-breadcrumb.is-open { display: flex; }
.fg-crumb {
pointer-events: auto;
border: 0;
background: rgba(16, 24, 40, 0.7);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: #cfe0ff;
font-size: 11px;
font-weight: 600;
line-height: 1;
padding: 5px 10px;
border-radius: 999px;
cursor: pointer;
max-width: 110px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fg-crumb.is-last {
background: rgba(125, 215, 255, 0.18);
color: #eaf7ff;
cursor: default;
}
.fg-crumb-sep { color: #5f7196; font-size: 11px; align-self: center; pointer-events: none; }
/* Доступность: визуально скрытый список графа для скринридеров (sr-only, читается ассистивными технологиями) */
.fg-a11y {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
/* «Общая связь» (этот человек — и твой друг тоже): золотой ободок + ★ под аватаркой */
.fg-node.is-common .node-dot {
border-color: rgba(255, 214, 120, 0.95);
box-shadow: 0 0 14px rgba(255, 200, 90, 0.4);
}
.fg-node.is-common .node-dot::after {
content: '★';
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
line-height: 1;
color: #ffd678;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
pointer-events: none;
}

5
test/avatars/SOURCES.md Normal file
View File

@ -0,0 +1,5 @@
# Источники тестовых аватаров
- `avatar_001.png` ... `avatar_050.png`: портреты с `randomuser.me`
- `avatar_051.png` ... `avatar_100.png`: мультяшные аватары с `dicebear.com`
- Все файлы приведены к размеру `512x512`.

BIN
test/avatars/avatar_001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
test/avatars/avatar_002.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

BIN
test/avatars/avatar_003.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

BIN
test/avatars/avatar_004.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

BIN
test/avatars/avatar_005.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
test/avatars/avatar_006.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
test/avatars/avatar_007.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
test/avatars/avatar_008.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

BIN
test/avatars/avatar_009.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
test/avatars/avatar_010.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

BIN
test/avatars/avatar_011.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

BIN
test/avatars/avatar_012.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

BIN
test/avatars/avatar_013.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
test/avatars/avatar_014.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

BIN
test/avatars/avatar_015.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
test/avatars/avatar_016.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

BIN
test/avatars/avatar_017.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
test/avatars/avatar_018.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
test/avatars/avatar_019.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

BIN
test/avatars/avatar_020.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
test/avatars/avatar_021.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
test/avatars/avatar_022.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

BIN
test/avatars/avatar_023.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
test/avatars/avatar_024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

BIN
test/avatars/avatar_025.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

BIN
test/avatars/avatar_026.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

BIN
test/avatars/avatar_027.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

BIN
test/avatars/avatar_028.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

BIN
test/avatars/avatar_029.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
test/avatars/avatar_030.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
test/avatars/avatar_031.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
test/avatars/avatar_032.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

BIN
test/avatars/avatar_033.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
test/avatars/avatar_034.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

BIN
test/avatars/avatar_035.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

BIN
test/avatars/avatar_036.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

BIN
test/avatars/avatar_037.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
test/avatars/avatar_038.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

BIN
test/avatars/avatar_039.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
test/avatars/avatar_040.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
test/avatars/avatar_041.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

BIN
test/avatars/avatar_042.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
test/avatars/avatar_043.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
test/avatars/avatar_044.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

BIN
test/avatars/avatar_045.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
test/avatars/avatar_046.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
test/avatars/avatar_047.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

BIN
test/avatars/avatar_048.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
test/avatars/avatar_049.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB