30 03 25
Добавил сайт с UI прямо сюда
This commit is contained in:
parent
889ce0d921
commit
b33fa4aeaa
27
deploy_shine-ui.sh
Executable file
27
deploy_shine-ui.sh
Executable file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SRC_DIR="/home/player/docker/shine-UI"
|
||||
REMOTE_HOST="root@194.87.0.247"
|
||||
REMOTE_DIR="/home/user/docker/caddyFile/sites/shine-UI"
|
||||
BUILD_VERSION="$(date -u +%Y%m%d%H%M%S)"
|
||||
export BUILD_VERSION
|
||||
|
||||
if [[ ! -d "$SRC_DIR" ]]; then
|
||||
echo "ERROR: source directory not found: $SRC_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Applying build version: $BUILD_VERSION"
|
||||
find "$SRC_DIR" -type f \( -name "*.js" -o -name "index.html" \) -print0 | xargs -0 perl -0pi -e 's/(\.js\?v=)([^"'"'"'\''\s>]*)/$1$ENV{BUILD_VERSION}/g; s/(\.css\?v=)([^"'"'"'\''\s>]*)/$1$ENV{BUILD_VERSION}/g'
|
||||
|
||||
echo "==> Checking SSH connectivity to $REMOTE_HOST"
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_HOST" "echo SSH OK" >/dev/null
|
||||
|
||||
echo "==> Preparing remote directory: $REMOTE_DIR"
|
||||
ssh "$REMOTE_HOST" "mkdir -p '$REMOTE_DIR'"
|
||||
|
||||
echo "==> Syncing files from $SRC_DIR to $REMOTE_DIR"
|
||||
rsync -avz --delete "$SRC_DIR"/ "$REMOTE_HOST":"$REMOTE_DIR"/
|
||||
|
||||
echo "Всё хорошо"
|
||||
1509
shine-UI/.elaira_logs/codex_profile_toggle_20260324_194129.log
Normal file
1509
shine-UI/.elaira_logs/codex_profile_toggle_20260324_194129.log
Normal file
File diff suppressed because it is too large
Load Diff
40
shine-UI/AGENTS.md
Normal file
40
shine-UI/AGENTS.md
Normal file
@ -0,0 +1,40 @@
|
||||
# AGENTS
|
||||
|
||||
## Назначение проекта
|
||||
Это демо-прототип мобильного веб-приложения в формате статического сайта.
|
||||
|
||||
## Технические ограничения
|
||||
- Проект сделан без бэкенда, без базы данных и без реальных API.
|
||||
- Все данные моковые и хранятся в `js/mock-data.js`.
|
||||
- Навигация между экранами идет без полной перезагрузки страницы (SPA-подход на hash-router).
|
||||
|
||||
## Обязательные требования к каждому экрану
|
||||
- У каждого экрана есть явный верхний заголовок на русском языке.
|
||||
- У каждого экрана есть нижняя служебная подпись над toolbar в формате:
|
||||
`[Русское название] ([english-page-id])`.
|
||||
- `page-id` должен совпадать с именем JS-файла страницы или быть максимально близким к нему.
|
||||
|
||||
## Архитектурные правила
|
||||
- Структура проекта должна оставаться понятной и модульной.
|
||||
- Новые доработки нужно вносить аккуратно, не ломая существующую навигацию.
|
||||
- Стиль проекта: темная тема, mobile-first, интерфейс на русском языке.
|
||||
|
||||
## Экраны и файлы
|
||||
- Профиль: `js/pages/profile-view.js`
|
||||
- Кошелёк: `js/pages/wallet-view.js`
|
||||
- Настройки: `js/pages/settings-view.js`
|
||||
- Личные сообщения: `js/pages/messages-list.js`
|
||||
- Чат: `js/pages/chat-view.js`
|
||||
- Каналы: `js/pages/channels-list.js`
|
||||
- Канал: `js/pages/channel-view.js`
|
||||
- Связи: `js/pages/network-view.js`
|
||||
- Уведомления: `js/pages/notifications-view.js`
|
||||
|
||||
## Ключевые файлы приложения
|
||||
- Точка входа: `index.html`
|
||||
- Инициализация приложения: `js/app.js`
|
||||
- Роутинг: `js/router.js`
|
||||
- Состояние клиента: `js/state.js`
|
||||
- Моки: `js/mock-data.js`
|
||||
- Компоненты: `js/components/*`
|
||||
- Стили: `styles/*`
|
||||
83
shine-UI/img/device-qr-64.svg
Normal file
83
shine-UI/img/device-qr-64.svg
Normal file
@ -0,0 +1,83 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" shape-rendering="crispEdges" role="img" aria-label="QR demo 64x64">
|
||||
<rect width="64" height="64" fill="#ffffff"/>
|
||||
<rect x="0" y="0" width="16" height="16" fill="#000000"/>
|
||||
<rect x="2" y="2" width="12" height="12" fill="#ffffff"/>
|
||||
<rect x="4" y="4" width="8" height="8" fill="#000000"/>
|
||||
<rect x="48" y="0" width="16" height="16" fill="#000000"/>
|
||||
<rect x="50" y="2" width="12" height="12" fill="#ffffff"/>
|
||||
<rect x="52" y="4" width="8" height="8" fill="#000000"/>
|
||||
<rect x="0" y="48" width="16" height="16" fill="#000000"/>
|
||||
<rect x="2" y="50" width="12" height="12" fill="#ffffff"/>
|
||||
<rect x="4" y="52" width="8" height="8" fill="#000000"/>
|
||||
|
||||
<rect x="20" y="4" width="2" height="2" fill="#000000"/>
|
||||
<rect x="24" y="4" width="2" height="2" fill="#000000"/>
|
||||
<rect x="28" y="4" width="2" height="2" fill="#000000"/>
|
||||
<rect x="34" y="4" width="2" height="2" fill="#000000"/>
|
||||
<rect x="38" y="4" width="2" height="2" fill="#000000"/>
|
||||
<rect x="42" y="4" width="2" height="2" fill="#000000"/>
|
||||
|
||||
<rect x="18" y="10" width="2" height="2" fill="#000000"/>
|
||||
<rect x="22" y="10" width="2" height="2" fill="#000000"/>
|
||||
<rect x="26" y="10" width="2" height="2" fill="#000000"/>
|
||||
<rect x="30" y="10" width="2" height="2" fill="#000000"/>
|
||||
<rect x="34" y="10" width="2" height="2" fill="#000000"/>
|
||||
<rect x="40" y="10" width="2" height="2" fill="#000000"/>
|
||||
<rect x="44" y="10" width="2" height="2" fill="#000000"/>
|
||||
|
||||
<rect x="18" y="18" width="2" height="2" fill="#000000"/>
|
||||
<rect x="22" y="18" width="2" height="2" fill="#000000"/>
|
||||
<rect x="30" y="18" width="2" height="2" fill="#000000"/>
|
||||
<rect x="34" y="18" width="2" height="2" fill="#000000"/>
|
||||
<rect x="38" y="18" width="2" height="2" fill="#000000"/>
|
||||
<rect x="44" y="18" width="2" height="2" fill="#000000"/>
|
||||
|
||||
<rect x="20" y="24" width="2" height="2" fill="#000000"/>
|
||||
<rect x="24" y="24" width="2" height="2" fill="#000000"/>
|
||||
<rect x="28" y="24" width="2" height="2" fill="#000000"/>
|
||||
<rect x="32" y="24" width="2" height="2" fill="#000000"/>
|
||||
<rect x="40" y="24" width="2" height="2" fill="#000000"/>
|
||||
<rect x="44" y="24" width="2" height="2" fill="#000000"/>
|
||||
|
||||
<rect x="18" y="30" width="2" height="2" fill="#000000"/>
|
||||
<rect x="22" y="30" width="2" height="2" fill="#000000"/>
|
||||
<rect x="26" y="30" width="2" height="2" fill="#000000"/>
|
||||
<rect x="34" y="30" width="2" height="2" fill="#000000"/>
|
||||
<rect x="38" y="30" width="2" height="2" fill="#000000"/>
|
||||
<rect x="44" y="30" width="2" height="2" fill="#000000"/>
|
||||
|
||||
<rect x="18" y="36" width="2" height="2" fill="#000000"/>
|
||||
<rect x="22" y="36" width="2" height="2" fill="#000000"/>
|
||||
<rect x="30" y="36" width="2" height="2" fill="#000000"/>
|
||||
<rect x="34" y="36" width="2" height="2" fill="#000000"/>
|
||||
<rect x="40" y="36" width="2" height="2" fill="#000000"/>
|
||||
<rect x="44" y="36" width="2" height="2" fill="#000000"/>
|
||||
|
||||
<rect x="20" y="42" width="2" height="2" fill="#000000"/>
|
||||
<rect x="24" y="42" width="2" height="2" fill="#000000"/>
|
||||
<rect x="28" y="42" width="2" height="2" fill="#000000"/>
|
||||
<rect x="32" y="42" width="2" height="2" fill="#000000"/>
|
||||
<rect x="36" y="42" width="2" height="2" fill="#000000"/>
|
||||
<rect x="42" y="42" width="2" height="2" fill="#000000"/>
|
||||
|
||||
<rect x="50" y="20" width="2" height="2" fill="#000000"/>
|
||||
<rect x="54" y="20" width="2" height="2" fill="#000000"/>
|
||||
<rect x="58" y="20" width="2" height="2" fill="#000000"/>
|
||||
<rect x="50" y="24" width="2" height="2" fill="#000000"/>
|
||||
<rect x="56" y="24" width="2" height="2" fill="#000000"/>
|
||||
<rect x="60" y="24" width="2" height="2" fill="#000000"/>
|
||||
<rect x="50" y="28" width="2" height="2" fill="#000000"/>
|
||||
<rect x="54" y="28" width="2" height="2" fill="#000000"/>
|
||||
<rect x="58" y="28" width="2" height="2" fill="#000000"/>
|
||||
|
||||
<rect x="20" y="50" width="2" height="2" fill="#000000"/>
|
||||
<rect x="24" y="50" width="2" height="2" fill="#000000"/>
|
||||
<rect x="30" y="50" width="2" height="2" fill="#000000"/>
|
||||
<rect x="36" y="50" width="2" height="2" fill="#000000"/>
|
||||
<rect x="42" y="50" width="2" height="2" fill="#000000"/>
|
||||
<rect x="20" y="54" width="2" height="2" fill="#000000"/>
|
||||
<rect x="28" y="54" width="2" height="2" fill="#000000"/>
|
||||
<rect x="34" y="54" width="2" height="2" fill="#000000"/>
|
||||
<rect x="40" y="54" width="2" height="2" fill="#000000"/>
|
||||
<rect x="44" y="54" width="2" height="2" fill="#000000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
BIN
shine-UI/img/logo.jpg
Normal file
BIN
shine-UI/img/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
20
shine-UI/index.html
Normal file
20
shine-UI/index.html
Normal file
@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<title>Shine UI Demo</title>
|
||||
<link rel="stylesheet" href="./styles/main.css?v=20260327192619" />
|
||||
<link rel="stylesheet" href="./styles/layout.css?v=20260327192619" />
|
||||
<link rel="stylesheet" href="./styles/components.css?v=20260327192619" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
<main id="app-screen" class="screen-content"></main>
|
||||
<div id="page-label-slot" class="page-label-slot"></div>
|
||||
<div id="toolbar-slot" class="toolbar-slot"></div>
|
||||
</div>
|
||||
<div id="modal-root"></div>
|
||||
<script type="module" src="./js/app.js?v=20260327192619"></script>
|
||||
</body>
|
||||
</html>
|
||||
122
shine-UI/js/app.js
Normal file
122
shine-UI/js/app.js
Normal file
@ -0,0 +1,122 @@
|
||||
import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js?v=20260327192619';
|
||||
import { renderToolbar } from './components/toolbar.js?v=20260327192619';
|
||||
import { renderPageLabel } from './components/page-label.js?v=20260327192619';
|
||||
import { state, togglePageLabel } from './state.js?v=20260327192619';
|
||||
|
||||
import * as startView from './pages/start-view.js?v=20260327192619';
|
||||
import * as entrySettingsView from './pages/entry-settings-view.js?v=20260327192619';
|
||||
import * as registerView from './pages/register-view.js?v=20260327192619';
|
||||
import * as registrationPaymentView from './pages/registration-payment-view.js?v=20260327192619';
|
||||
import * as registrationKeysView from './pages/registration-keys-view.js?v=20260327192619';
|
||||
import * as topupView from './pages/topup-view.js?v=20260327192619';
|
||||
import * as loginView from './pages/login-view.js?v=20260327192619';
|
||||
import * as loginCameraView from './pages/login-camera-view.js?v=20260327192619';
|
||||
import * as loginPasswordView from './pages/login-password-view.js?v=20260327192619';
|
||||
import * as keyStorageView from './pages/key-storage-view.js?v=20260327192619';
|
||||
|
||||
import * as profileView from './pages/profile-view.js?v=20260327192619';
|
||||
import * as walletView from './pages/wallet-view.js?v=20260327192619';
|
||||
import * as settingsView from './pages/settings-view.js?v=20260327192619';
|
||||
import * as serverSettingsView from './pages/server-settings-view.js?v=20260327192619';
|
||||
import * as deviceView from './pages/device-view.js?v=20260327192619';
|
||||
import * as connectDeviceView from './pages/connect-device-view.js?v=20260327192619';
|
||||
import * as deviceQrView from './pages/device-qr-view.js?v=20260327192619';
|
||||
import * as deviceCameraView from './pages/device-camera-view.js?v=20260327192619';
|
||||
import * as showKeysView from './pages/show-keys-view.js?v=20260327192619';
|
||||
import * as deviceSessionView from './pages/device-session-view.js?v=20260327192619';
|
||||
import * as languageView from './pages/language-view.js?v=20260327192619';
|
||||
import * as messagesList from './pages/messages-list.js?v=20260327192619';
|
||||
import * as contactSearchView from './pages/contact-search-view.js?v=20260327192619';
|
||||
import * as chatView from './pages/chat-view.js?v=20260327192619';
|
||||
import * as channelsList from './pages/channels-list.js?v=20260327192619';
|
||||
import * as channelView from './pages/channel-view.js?v=20260327192619';
|
||||
import * as networkView from './pages/network-view.js?v=20260327192619';
|
||||
import * as notificationsView from './pages/notifications-view.js?v=20260327192619';
|
||||
|
||||
const routes = {
|
||||
'start-view': startView,
|
||||
'entry-settings-view': entrySettingsView,
|
||||
'register-view': registerView,
|
||||
'registration-payment-view': registrationPaymentView,
|
||||
'registration-keys-view': registrationKeysView,
|
||||
'topup-view': topupView,
|
||||
'login-view': loginView,
|
||||
'login-camera-view': loginCameraView,
|
||||
'login-password-view': loginPasswordView,
|
||||
'key-storage-view': keyStorageView,
|
||||
'profile-view': profileView,
|
||||
'wallet-view': walletView,
|
||||
'settings-view': settingsView,
|
||||
'server-settings-view': serverSettingsView,
|
||||
'device-view': deviceView,
|
||||
'connect-device-view': connectDeviceView,
|
||||
'device-qr-view': deviceQrView,
|
||||
'device-camera-view': deviceCameraView,
|
||||
'show-keys-view': showKeysView,
|
||||
'device-session-view': deviceSessionView,
|
||||
'language-view': languageView,
|
||||
'messages-list': messagesList,
|
||||
'contact-search-view': contactSearchView,
|
||||
'chat-view': chatView,
|
||||
'channels-list': channelsList,
|
||||
'channel-view': channelView,
|
||||
'network-view': networkView,
|
||||
'notifications-view': notificationsView,
|
||||
};
|
||||
|
||||
const screenEl = document.getElementById('app-screen');
|
||||
const labelEl = document.getElementById('page-label-slot');
|
||||
const toolbarEl = document.getElementById('toolbar-slot');
|
||||
|
||||
let currentCleanup = null;
|
||||
|
||||
function renderApp() {
|
||||
const route = getRoute();
|
||||
const pageId = route.pageId || (state.session.isAuthorized ? 'profile-view' : 'start-view');
|
||||
|
||||
if (!state.session.isAuthorized && !PRE_AUTH_PAGES.includes(pageId)) {
|
||||
navigate('start-view');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.session.isAuthorized && PRE_AUTH_PAGES.includes(pageId)) {
|
||||
navigate('profile-view');
|
||||
return;
|
||||
}
|
||||
|
||||
const page = routes[pageId] || routes['start-view'];
|
||||
|
||||
if (typeof currentCleanup === 'function') {
|
||||
currentCleanup();
|
||||
currentCleanup = null;
|
||||
}
|
||||
|
||||
screenEl.innerHTML = '';
|
||||
const screen = page.render({ route, navigate });
|
||||
screenEl.append(screen);
|
||||
currentCleanup = typeof screen.cleanup === 'function' ? screen.cleanup : null;
|
||||
|
||||
const showAppChrome = page.pageMeta?.showAppChrome !== false;
|
||||
screenEl.classList.toggle('no-app-chrome', !showAppChrome);
|
||||
|
||||
labelEl.innerHTML = '';
|
||||
toolbarEl.innerHTML = '';
|
||||
|
||||
if (showAppChrome) {
|
||||
labelEl.append(
|
||||
renderPageLabel(page.pageMeta.title, page.pageMeta.id, state.pageLabelCollapsed, () => {
|
||||
togglePageLabel();
|
||||
renderApp();
|
||||
}),
|
||||
);
|
||||
toolbarEl.append(renderToolbar(page.pageMeta.id, navigate));
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.location.hash) {
|
||||
navigate(state.session.isAuthorized ? 'profile-view' : 'start-view');
|
||||
} else {
|
||||
renderApp();
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', renderApp);
|
||||
31
shine-UI/js/components/header.js
Normal file
31
shine-UI/js/components/header.js
Normal file
@ -0,0 +1,31 @@
|
||||
export function renderHeader({ title, leftAction, rightActions = [] }) {
|
||||
const wrap = document.createElement('header');
|
||||
wrap.className = 'page-header';
|
||||
|
||||
const left = document.createElement('div');
|
||||
left.className = 'header-left';
|
||||
if (leftAction) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'icon-btn';
|
||||
btn.textContent = leftAction.label;
|
||||
btn.addEventListener('click', leftAction.onClick);
|
||||
left.append(btn);
|
||||
}
|
||||
|
||||
const h1 = document.createElement('h1');
|
||||
h1.className = 'page-title';
|
||||
h1.textContent = title;
|
||||
|
||||
const right = document.createElement('div');
|
||||
right.className = 'header-actions';
|
||||
rightActions.forEach((action) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'icon-btn';
|
||||
btn.textContent = action.label;
|
||||
btn.addEventListener('click', action.onClick);
|
||||
right.append(btn);
|
||||
});
|
||||
|
||||
wrap.append(left, h1, right);
|
||||
return wrap;
|
||||
}
|
||||
36
shine-UI/js/components/page-label.js
Normal file
36
shine-UI/js/components/page-label.js
Normal file
@ -0,0 +1,36 @@
|
||||
export function renderPageLabel(titleRu, pageId, collapsed, onToggle) {
|
||||
const label = document.createElement('div');
|
||||
label.className = `page-label${collapsed ? ' is-collapsed' : ''}`;
|
||||
|
||||
const toggle = document.createElement('button');
|
||||
toggle.type = 'button';
|
||||
toggle.className = 'page-label-toggle';
|
||||
toggle.title = collapsed ? 'Показать подпись' : 'Скрыть подпись';
|
||||
toggle.setAttribute(
|
||||
'aria-label',
|
||||
collapsed ? 'Показать подпись страницы для разработки' : 'Скрыть подпись страницы для разработки',
|
||||
);
|
||||
toggle.addEventListener('click', onToggle);
|
||||
|
||||
if (!collapsed) {
|
||||
label.append(toggle);
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'page-label-content';
|
||||
|
||||
const hint = document.createElement('div');
|
||||
hint.className = 'page-label-hint';
|
||||
hint.textContent = 'Для разработки';
|
||||
|
||||
const caption = document.createElement('div');
|
||||
caption.className = 'page-label-caption';
|
||||
caption.textContent = `${titleRu} (${pageId})`;
|
||||
|
||||
content.append(hint, caption);
|
||||
label.append(content);
|
||||
} else {
|
||||
label.append(toggle);
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
25
shine-UI/js/components/toolbar.js
Normal file
25
shine-UI/js/components/toolbar.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { resolveToolbarActive } from '../router.js?v=20260327192619';
|
||||
|
||||
const ITEMS = [
|
||||
{ pageId: 'messages-list', label: 'Личные сообщения', icon: '💬' },
|
||||
{ pageId: 'channels-list', label: 'Каналы', icon: '📢' },
|
||||
{ pageId: 'network-view', label: 'Связи', icon: '🕸' },
|
||||
{ pageId: 'notifications-view', label: 'Уведомления', icon: '🔔' },
|
||||
{ pageId: 'profile-view', label: 'Профиль', icon: '👤' },
|
||||
];
|
||||
|
||||
export function renderToolbar(currentPageId, navigate) {
|
||||
const root = document.createElement('nav');
|
||||
root.className = 'toolbar';
|
||||
const active = resolveToolbarActive(currentPageId);
|
||||
|
||||
ITEMS.forEach((item) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `toolbar-btn${item.pageId === active ? ' active' : ''}`;
|
||||
btn.innerHTML = `<span>${item.icon}</span><span>${item.label}</span>`;
|
||||
btn.addEventListener('click', () => navigate(item.pageId));
|
||||
root.append(btn);
|
||||
});
|
||||
|
||||
return root;
|
||||
}
|
||||
237
shine-UI/js/mock-data.js
Normal file
237
shine-UI/js/mock-data.js
Normal file
@ -0,0 +1,237 @@
|
||||
export const profile = {
|
||||
login: '@shine.alex',
|
||||
name: 'Алексей сияющий',
|
||||
avatarInitials: 'АС',
|
||||
phone: '+7 (916) 221-45-88',
|
||||
address: 'Москва, Пресненская наб., 12',
|
||||
email: 'alex.shine@demo.local',
|
||||
socials: '@alexshine / t.me/alexshine',
|
||||
badges: ['Официальный аккаунт', 'Сияющий'],
|
||||
};
|
||||
|
||||
export const wallet = {
|
||||
balanceSOL: '182.4571',
|
||||
publicAddress: '9sVAXJ2CqP3BrtC6AFeQHhcuWjN1kUyhY7L8pkQJxMZe',
|
||||
updatedAt: 'сегодня, 14:42',
|
||||
};
|
||||
|
||||
export const deviceSessions = [
|
||||
{
|
||||
sessionId: 'sess_7c5e5c4b',
|
||||
clientInfoFromClient: 'Android 15; Pixel 9',
|
||||
clientInfoFromRequest: 'UA=Java-http-client/17.0.18; remote=127.0.0.1',
|
||||
geo: 'RU/Moscow',
|
||||
lastAuthenticatedAtMs: 1774600010500,
|
||||
},
|
||||
{
|
||||
sessionId: 'sess_90ab11de',
|
||||
clientInfoFromClient: 'iOS 19; iPhone 17',
|
||||
clientInfoFromRequest: 'UA=ShineMobile/2.4; remote=10.0.2.12',
|
||||
geo: 'RU/Moscow',
|
||||
lastAuthenticatedAtMs: 1774553310000,
|
||||
},
|
||||
{
|
||||
sessionId: 'sess_3ea4f11c',
|
||||
clientInfoFromClient: 'Windows 11; Chrome 124',
|
||||
clientInfoFromRequest: 'UA=Mozilla/5.0; remote=192.168.1.21',
|
||||
geo: 'RU/Kazan',
|
||||
lastAuthenticatedAtMs: 1774499010000,
|
||||
},
|
||||
];
|
||||
|
||||
export const directMessages = [
|
||||
{
|
||||
id: 'u1',
|
||||
name: 'Марина К.',
|
||||
initials: 'МК',
|
||||
lastMessage: 'Вечером скину обновления по макетам.',
|
||||
time: '15:08',
|
||||
unread: 2,
|
||||
},
|
||||
{
|
||||
id: 'u2',
|
||||
name: 'Илья П.',
|
||||
initials: 'ИП',
|
||||
lastMessage: 'Спасибо, уже проверяю!',
|
||||
time: '14:31',
|
||||
unread: 0,
|
||||
},
|
||||
{
|
||||
id: 'u3',
|
||||
name: 'Елена Д.',
|
||||
initials: 'ЕД',
|
||||
lastMessage: 'Тестовый стенд снова доступен.',
|
||||
time: '13:02',
|
||||
unread: 5,
|
||||
},
|
||||
{
|
||||
id: 'u4',
|
||||
name: 'Никита О.',
|
||||
initials: 'НО',
|
||||
lastMessage: 'Отлично, давай так и сделаем.',
|
||||
time: 'вчера',
|
||||
unread: 0,
|
||||
},
|
||||
];
|
||||
|
||||
export const contactDirectory = [
|
||||
{
|
||||
id: 'u5',
|
||||
name: 'Марк С.',
|
||||
initials: 'МС',
|
||||
about: 'Продуктовый аналитик, любит короткие созвоны и длинные отчёты.',
|
||||
},
|
||||
{
|
||||
id: 'u6',
|
||||
name: 'Мария Л.',
|
||||
initials: 'МЛ',
|
||||
about: 'UI-дизайнер, собирает референсы и следит за визуальным стилем.',
|
||||
},
|
||||
{
|
||||
id: 'u7',
|
||||
name: 'Марина Р.',
|
||||
initials: 'МР',
|
||||
about: 'Контент-менеджер, ведёт каналы и готовит анонсы.',
|
||||
},
|
||||
{
|
||||
id: 'u8',
|
||||
name: 'Максим В.',
|
||||
initials: 'МВ',
|
||||
about: 'Frontend-разработчик, отвечает за анимации и адаптивность.',
|
||||
},
|
||||
{
|
||||
id: 'u9',
|
||||
name: 'Мадина А.',
|
||||
initials: 'МА',
|
||||
about: 'Комьюнити-менеджер, быстро находит нужных людей.',
|
||||
},
|
||||
{
|
||||
id: 'u10',
|
||||
name: 'Ирина П.',
|
||||
initials: 'ИП',
|
||||
about: 'Редактор новостей, помогает с текстами и публикациями.',
|
||||
},
|
||||
{
|
||||
id: 'u11',
|
||||
name: 'Николай Д.',
|
||||
initials: 'НД',
|
||||
about: 'Технический писатель, структурирует знания по продукту.',
|
||||
},
|
||||
{
|
||||
id: 'u12',
|
||||
name: 'Егор Т.',
|
||||
initials: 'ЕТ',
|
||||
about: 'QA-инженер, любит проверять сложные сценарии вручную.',
|
||||
},
|
||||
];
|
||||
|
||||
export const chatMessages = {
|
||||
u1: [
|
||||
{ from: 'in', text: 'Привет! Видел новые карточки?' },
|
||||
{ from: 'out', text: 'Да, смотрятся сильно. Нужен финальный текст.' },
|
||||
{ from: 'in', text: 'Вечером скину обновления по макетам.' },
|
||||
],
|
||||
u2: [
|
||||
{ from: 'out', text: 'Скинул доступы в чат команды.' },
|
||||
{ from: 'in', text: 'Спасибо, уже проверяю!' },
|
||||
],
|
||||
u3: [
|
||||
{ from: 'in', text: 'Тестовый стенд снова доступен.' },
|
||||
{ from: 'out', text: 'Отлично, запускаю прогон сценариев.' },
|
||||
],
|
||||
u4: [
|
||||
{ from: 'in', text: 'Подтверждаю план на завтра.' },
|
||||
{ from: 'out', text: 'Отлично, давай так и сделаем.' },
|
||||
],
|
||||
};
|
||||
|
||||
export const channels = [
|
||||
{
|
||||
id: 'ch1',
|
||||
name: 'Новости продукта',
|
||||
initials: 'НП',
|
||||
description: 'Официальный канал команды Shine с релизами и обновлениями.',
|
||||
lastMessage: 'Опубликовали обзор нового демо-прототипа мобильного интерфейса.',
|
||||
time: '16:05',
|
||||
unread: 14,
|
||||
},
|
||||
{
|
||||
id: 'ch2',
|
||||
name: 'Анекдоты дня',
|
||||
initials: 'АД',
|
||||
description: 'Лёгкий развлекательный канал с короткими шутками и мемами.',
|
||||
lastMessage: 'Новый пост: как дизайнер, разработчик и дедлайн зашли в бар.',
|
||||
time: '15:20',
|
||||
unread: 3,
|
||||
},
|
||||
{
|
||||
id: 'ch3',
|
||||
name: 'Новости рынка',
|
||||
initials: 'НР',
|
||||
description: 'Короткие ежедневные сводки по рынку, технологиям и сообществам.',
|
||||
lastMessage: 'В ленте свежая подборка новостей и главных событий дня.',
|
||||
time: 'вчера',
|
||||
unread: 0,
|
||||
},
|
||||
];
|
||||
|
||||
export const channelPosts = {
|
||||
ch1: [
|
||||
{
|
||||
id: 'p1',
|
||||
title: 'Новый экран профиля',
|
||||
body: 'Добавлены бейджи статуса, переработан верхний блок и улучшены быстрые переходы.',
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
title: 'Навигация без перезагрузки',
|
||||
body: 'Переходы между экранами теперь стабильнее работают в SPA-режиме через hash-router.',
|
||||
},
|
||||
],
|
||||
ch2: [
|
||||
{
|
||||
id: 'p3',
|
||||
title: 'Анекдот утра',
|
||||
body: 'Разработчик говорит: "Я починил один баг". Баги в ответ: "Нас было трое".',
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
title: 'Анекдот про дедлайн',
|
||||
body: 'Дедлайн был настолько близко, что команда начала здороваться с ним по имени.',
|
||||
},
|
||||
],
|
||||
ch3: [
|
||||
{
|
||||
id: 'p5',
|
||||
title: 'Утренний дайджест',
|
||||
body: 'Собрали ключевые новости дня: обновления продуктов, движения рынка и заметные релизы.',
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
title: 'Что обсуждают сегодня',
|
||||
body: 'В фокусе дня: рост интереса к мобильным dApp-интерфейсам и новые анонсы сообществ.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const notifications = {
|
||||
replies: [
|
||||
{ id: 'r1', title: 'Марина К. ответила на ваш комментарий', text: 'Согласна, такую структуру и оставим.', time: '12 минут назад' },
|
||||
{ id: 'r2', title: 'Илья П. ответил в обсуждении', text: 'Добавил примеры экранов для onboarding.', time: '48 минут назад' },
|
||||
],
|
||||
events: [
|
||||
{ id: 'e1', title: 'Елена Д. добавила вас в друзья', text: 'Теперь вы в связях первого уровня.', time: 'сегодня' },
|
||||
{ id: 'e2', title: 'Никита О. удалил из друзей', text: 'Связь перенесена в архив событий.', time: 'вчера' },
|
||||
{ id: 'e3', title: 'Марина К. поставила лайк', text: 'Оценен ваш пост о прототипе.', time: '2 дня назад' },
|
||||
],
|
||||
};
|
||||
|
||||
export const networkGraph = {
|
||||
center: { id: 'me', name: 'Вы', initials: 'ВЫ', x: 50, y: 50 },
|
||||
peers: [
|
||||
{ id: 'p1', name: 'Марина', initials: 'МК', x: 20, y: 24 },
|
||||
{ id: 'p2', name: 'Илья', initials: 'ИП', x: 80, y: 22 },
|
||||
{ id: 'p3', name: 'Елена', initials: 'ЕД', x: 18, y: 78 },
|
||||
{ id: 'p4', name: 'Никита', initials: 'НО', x: 82, y: 76 },
|
||||
],
|
||||
};
|
||||
41
shine-UI/js/pages/channel-view.js
Normal file
41
shine-UI/js/pages/channel-view.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { channelPosts, channels } from '../mock-data.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'channel-view', title: 'Канал' };
|
||||
|
||||
export function render({ navigate, route }) {
|
||||
const channelId = route.params.channelId || 'ch1';
|
||||
const channel = channels.find((c) => c.id === channelId) || channels[0];
|
||||
const posts = channelPosts[channelId] || [];
|
||||
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: `Канал: ${channel.name}`,
|
||||
leftAction: { label: '←', onClick: () => navigate('channels-list') },
|
||||
})
|
||||
);
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.className = 'card';
|
||||
head.innerHTML = `
|
||||
<strong># ${channel.name}</strong>
|
||||
<p class="meta-muted" style="margin-top:4px;">${channel.description}</p>
|
||||
<p class="meta-muted" style="margin-top:8px;">Публичный канал, режим только чтение</p>
|
||||
`;
|
||||
|
||||
const feed = document.createElement('div');
|
||||
feed.className = 'stack';
|
||||
|
||||
posts.forEach((post) => {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `<strong>${post.title}</strong><p class="meta-muted">${post.body}</p>`;
|
||||
feed.append(card);
|
||||
});
|
||||
|
||||
screen.append(head, feed);
|
||||
return screen;
|
||||
}
|
||||
42
shine-UI/js/pages/channels-list.js
Normal file
42
shine-UI/js/pages/channels-list.js
Normal file
@ -0,0 +1,42 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { channels } from '../mock-data.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'channels-list', title: 'Каналы' };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(renderHeader({ title: 'Каналы' }));
|
||||
|
||||
const search = document.createElement('div');
|
||||
search.className = 'card';
|
||||
search.textContent = 'Найти канал';
|
||||
search.style.color = 'var(--text-muted)';
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'stack';
|
||||
|
||||
channels.forEach((channel) => {
|
||||
const row = document.createElement('article');
|
||||
row.className = 'list-item';
|
||||
row.innerHTML = `
|
||||
<div class="avatar">${channel.initials}</div>
|
||||
<div>
|
||||
<strong># ${channel.name}</strong>
|
||||
<p class="meta-muted" style="margin-top:4px;">${channel.description}</p>
|
||||
<p class="meta-muted" style="margin-top:6px; color:#d8e3ff;">${channel.lastMessage}</p>
|
||||
</div>
|
||||
<div style="display:grid; justify-items:end; gap:6px;">
|
||||
<span class="badge alt" style="padding:4px 8px; font-size:10px;">Канал</span>
|
||||
<span class="meta-muted">${channel.time}</span>
|
||||
${channel.unread ? `<span class="unread">${channel.unread}</span>` : '<span></span>'}
|
||||
</div>
|
||||
`;
|
||||
row.addEventListener('click', () => navigate(`channel-view/${channel.id}`));
|
||||
list.append(row);
|
||||
});
|
||||
|
||||
screen.append(search, list);
|
||||
return screen;
|
||||
}
|
||||
58
shine-UI/js/pages/chat-view.js
Normal file
58
shine-UI/js/pages/chat-view.js
Normal file
@ -0,0 +1,58 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { directMessages } from '../mock-data.js?v=20260327192619';
|
||||
import { addChatMessage, getChatMessages } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'chat-view', title: 'Чат' };
|
||||
|
||||
function renderLog(list, chatId) {
|
||||
list.innerHTML = '';
|
||||
const messages = getChatMessages(chatId);
|
||||
messages.forEach((msg) => {
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = `bubble ${msg.from}`;
|
||||
bubble.textContent = msg.text;
|
||||
list.append(bubble);
|
||||
});
|
||||
list.scrollTop = list.scrollHeight;
|
||||
}
|
||||
|
||||
export function render({ navigate, route }) {
|
||||
const chatId = route.params.chatId || 'u1';
|
||||
const contact = directMessages.find((d) => d.id === chatId) || directMessages[0];
|
||||
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: `Чат: ${contact.name}`,
|
||||
leftAction: { label: '←', onClick: () => navigate('messages-list') },
|
||||
})
|
||||
);
|
||||
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'chat-wrap';
|
||||
|
||||
const log = document.createElement('div');
|
||||
log.className = 'messages-log';
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.className = 'chat-input';
|
||||
form.innerHTML = `
|
||||
<input class="input" type="text" name="message" placeholder="Введите сообщение" maxlength="300" />
|
||||
<button class="primary-btn" type="submit">Отправить</button>
|
||||
`;
|
||||
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const input = form.elements.message;
|
||||
addChatMessage(chatId, input.value);
|
||||
input.value = '';
|
||||
renderLog(log, chatId);
|
||||
});
|
||||
|
||||
renderLog(log, chatId);
|
||||
wrap.append(log, form);
|
||||
screen.append(wrap);
|
||||
return screen;
|
||||
}
|
||||
103
shine-UI/js/pages/connect-device-view.js
Normal file
103
shine-UI/js/pages/connect-device-view.js
Normal file
@ -0,0 +1,103 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'connect-device-view', title: 'Подключить устройство' };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Подключить устройство',
|
||||
leftAction: { label: '←', onClick: () => navigate('device-view') },
|
||||
}),
|
||||
);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `
|
||||
<p>Выберите, какие ключи передать на подключаемое устройство</p>
|
||||
<label class="checkbox-row"><input type="checkbox" id="connect-root" ${state.deviceConnect.root ? 'checked' : ''} /> root key</label>
|
||||
<label class="checkbox-row"><input type="checkbox" id="connect-blockchain" ${state.deviceConnect.blockchain ? 'checked' : ''} /> blockchain key</label>
|
||||
<label class="checkbox-row"><input type="checkbox" id="connect-device" checked disabled /> device key</label>
|
||||
<div class="row">
|
||||
<button class="icon-btn small-btn" type="button" id="tech-help">Техсправка</button>
|
||||
</div>
|
||||
<div class="stack">
|
||||
<button class="primary-btn" type="button" id="open-qr">Показать QR-код для подключения</button>
|
||||
<button class="text-btn" type="button" id="open-camera">Подключить через камеру</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const rootToggle = card.querySelector('#connect-root');
|
||||
const blockchainToggle = card.querySelector('#connect-blockchain');
|
||||
const deviceToggle = card.querySelector('#connect-device');
|
||||
deviceToggle.checked = true;
|
||||
|
||||
rootToggle.addEventListener('change', () => {
|
||||
state.deviceConnect.root = rootToggle.checked;
|
||||
});
|
||||
blockchainToggle.addEventListener('change', () => {
|
||||
state.deviceConnect.blockchain = blockchainToggle.checked;
|
||||
});
|
||||
deviceToggle.addEventListener('change', () => {
|
||||
state.deviceConnect.device = true;
|
||||
deviceToggle.checked = true;
|
||||
});
|
||||
|
||||
const helpModal = document.createElement('div');
|
||||
helpModal.className = 'modal-shell';
|
||||
helpModal.hidden = true;
|
||||
helpModal.innerHTML = `
|
||||
<div class="modal-backdrop" data-close="true"></div>
|
||||
<div class="modal-dialog card" role="dialog" aria-modal="true" tabindex="-1">
|
||||
<div class="row" style="align-items:flex-start;">
|
||||
<h3 style="font-size:18px;">Техсправка</h3>
|
||||
<button class="icon-btn" type="button" data-close="true" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div class="stack" style="gap:6px;">
|
||||
<p class="meta-muted">пользователь выбирает ключи для передачи</p>
|
||||
<p class="meta-muted">передать можно только существующие ключи</p>
|
||||
<p class="meta-muted">если ключа нет — он недоступен</p>
|
||||
<p class="meta-muted">blockchain key — можно передать или нет</p>
|
||||
<p class="meta-muted">root key — только если существует</p>
|
||||
<p class="meta-muted">device key передаётся всегда</p>
|
||||
<p class="meta-muted">подключение происходит напрямую через QR</p>
|
||||
<p class="meta-muted">сервер не используется</p>
|
||||
<p class="meta-muted">текущая логика: устройство 1 показывает QR, устройство 2 сканирует</p>
|
||||
<p class="meta-muted">обратный сценарий пока не реализован</p>
|
||||
</div>
|
||||
<button class="primary-btn" type="button" data-close="true">OK</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const openHelp = () => {
|
||||
helpModal.hidden = false;
|
||||
helpModal.querySelector('.modal-dialog').focus();
|
||||
};
|
||||
|
||||
const closeHelp = () => {
|
||||
helpModal.hidden = true;
|
||||
};
|
||||
|
||||
card.querySelector('#tech-help').addEventListener('click', openHelp);
|
||||
card.querySelector('#open-qr').addEventListener('click', () => navigate('device-qr-view'));
|
||||
card.querySelector('#open-camera').addEventListener('click', () => navigate('device-camera-view'));
|
||||
|
||||
helpModal.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLElement && target.dataset.close === 'true') {
|
||||
closeHelp();
|
||||
}
|
||||
});
|
||||
|
||||
helpModal.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeHelp();
|
||||
}
|
||||
});
|
||||
|
||||
screen.append(card, helpModal);
|
||||
return screen;
|
||||
}
|
||||
129
shine-UI/js/pages/contact-search-view.js
Normal file
129
shine-UI/js/pages/contact-search-view.js
Normal file
@ -0,0 +1,129 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { contactDirectory, directMessages } from '../mock-data.js?v=20260327192619';
|
||||
import { ensureChat } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'contact-search-view', title: 'Поиск контактов' };
|
||||
|
||||
function getMatches(query) {
|
||||
const normalized = query.trim().toLowerCase();
|
||||
if (!normalized) return [];
|
||||
|
||||
return contactDirectory
|
||||
.filter((contact) => contact.name.toLowerCase().startsWith(normalized))
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.className = 'input';
|
||||
input.type = 'text';
|
||||
input.name = 'contact';
|
||||
input.placeholder = 'Введите имя контакта';
|
||||
input.autocomplete = 'off';
|
||||
input.maxLength = 80;
|
||||
|
||||
const resultsCard = document.createElement('section');
|
||||
resultsCard.className = 'card stack';
|
||||
resultsCard.hidden = true;
|
||||
|
||||
const status = document.createElement('p');
|
||||
status.className = 'meta-muted';
|
||||
|
||||
const resultsList = document.createElement('div');
|
||||
resultsList.className = 'stack';
|
||||
|
||||
let latestMatches = [];
|
||||
|
||||
const renderResults = (matches, query) => {
|
||||
latestMatches = matches;
|
||||
resultsList.innerHTML = '';
|
||||
resultsCard.hidden = false;
|
||||
|
||||
if (!query.trim()) {
|
||||
status.textContent = 'Введите первые буквы имени, чтобы найти контакт.';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matches.length) {
|
||||
status.textContent = 'Совпадений не найдено.';
|
||||
return;
|
||||
}
|
||||
|
||||
status.textContent = `Найдено пользователей: ${matches.length}`;
|
||||
|
||||
matches.forEach((contact) => {
|
||||
const row = document.createElement('article');
|
||||
row.className = 'list-item';
|
||||
row.innerHTML = `
|
||||
<div class="avatar">${contact.initials}</div>
|
||||
<div>
|
||||
<strong>${contact.name}</strong>
|
||||
<p class="meta-muted" style="margin-top:4px;">${contact.about}</p>
|
||||
</div>
|
||||
<div class="meta-muted">Контакт</div>
|
||||
`;
|
||||
resultsList.append(row);
|
||||
});
|
||||
};
|
||||
|
||||
const searchButton = document.createElement('button');
|
||||
searchButton.className = 'primary-btn';
|
||||
searchButton.type = 'button';
|
||||
searchButton.textContent = 'Найти';
|
||||
searchButton.addEventListener('click', () => {
|
||||
renderResults(getMatches(input.value), input.value);
|
||||
});
|
||||
|
||||
const addButton = document.createElement('button');
|
||||
addButton.className = 'ghost-btn';
|
||||
addButton.type = 'button';
|
||||
addButton.textContent = 'Добавить';
|
||||
addButton.addEventListener('click', () => {
|
||||
if (!latestMatches.length) {
|
||||
status.textContent = 'Сначала выполните поиск, чтобы добавить контакт.';
|
||||
resultsCard.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const contact = latestMatches[0];
|
||||
const exists = directMessages.some((item) => item.id === contact.id);
|
||||
|
||||
if (!exists) {
|
||||
directMessages.unshift({
|
||||
id: contact.id,
|
||||
name: contact.name,
|
||||
initials: contact.initials,
|
||||
lastMessage: 'Новый контакт добавлен. Можно начинать диалог.',
|
||||
time: 'сейчас',
|
||||
unread: 0,
|
||||
});
|
||||
}
|
||||
|
||||
ensureChat(contact.id);
|
||||
navigate(`chat-view/${contact.id}`);
|
||||
});
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'contact-search-actions';
|
||||
controls.append(searchButton, addButton);
|
||||
|
||||
const formCard = document.createElement('section');
|
||||
formCard.className = 'card stack';
|
||||
formCard.append(input, controls);
|
||||
|
||||
resultsCard.append(status, resultsList);
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Поиск контактов',
|
||||
leftAction: { label: '←', onClick: () => navigate('messages-list') },
|
||||
}),
|
||||
formCard,
|
||||
resultsCard,
|
||||
);
|
||||
|
||||
return screen;
|
||||
}
|
||||
26
shine-UI/js/pages/device-camera-view.js
Normal file
26
shine-UI/js/pages/device-camera-view.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'device-camera-view', title: 'Подключить через камеру' };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Подключить через камеру',
|
||||
leftAction: { label: '←', onClick: () => navigate('connect-device-view') },
|
||||
}),
|
||||
);
|
||||
|
||||
const frame = document.createElement('div');
|
||||
frame.className = 'camera-shell';
|
||||
frame.innerHTML = `
|
||||
<div class="camera-placeholder">Область камеры (демо-заглушка)</div>
|
||||
<div class="camera-frame"></div>
|
||||
<div class="camera-hint">Логика сканирования пока не реализована</div>
|
||||
`;
|
||||
|
||||
screen.append(frame);
|
||||
return screen;
|
||||
}
|
||||
36
shine-UI/js/pages/device-qr-view.js
Normal file
36
shine-UI/js/pages/device-qr-view.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { profile } from '../mock-data.js?v=20260327192619';
|
||||
import { state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'device-qr-view', title: 'Показать QR-код' };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const selectedKeys = [];
|
||||
if (state.deviceConnect.root) selectedKeys.push('root key');
|
||||
if (state.deviceConnect.blockchain) selectedKeys.push('blockchain key');
|
||||
if (state.deviceConnect.device) selectedKeys.push('device key');
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Показать QR-код',
|
||||
leftAction: { label: '←', onClick: () => navigate('connect-device-view') },
|
||||
}),
|
||||
);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack qr-card';
|
||||
card.innerHTML = `
|
||||
<img class="qr-image" src="img/device-qr-64.svg" width="64" height="64" alt="QR-код для подключения" />
|
||||
<p class="meta-muted">Логин пользователя: ${profile.login}</p>
|
||||
<p class="meta-muted">Передаваемые ключи: ${selectedKeys.join(', ')}</p>
|
||||
<button class="primary-btn" type="button" id="qr-ok">OK</button>
|
||||
`;
|
||||
|
||||
card.querySelector('#qr-ok').addEventListener('click', () => navigate('connect-device-view'));
|
||||
|
||||
screen.append(card);
|
||||
return screen;
|
||||
}
|
||||
87
shine-UI/js/pages/device-session-view.js
Normal file
87
shine-UI/js/pages/device-session-view.js
Normal file
@ -0,0 +1,87 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { deviceSessions } from '../mock-data.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'device-session-view', title: 'Сеанс устройства' };
|
||||
|
||||
function formatSessionTime(ms) {
|
||||
return new Date(ms).toLocaleString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
export function render({ navigate, route }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const sessionId = route?.params?.sessionId || '';
|
||||
const session = deviceSessions.find((item) => item.sessionId === sessionId) || deviceSessions[0];
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Сеанс устройства',
|
||||
leftAction: { label: '←', onClick: () => navigate('device-view') },
|
||||
}),
|
||||
);
|
||||
|
||||
const details = document.createElement('div');
|
||||
details.className = 'card stack';
|
||||
details.innerHTML = `
|
||||
<div><p class="meta-muted">sessionId</p><p>${session.sessionId}</p></div>
|
||||
<div><p class="meta-muted">clientInfoFromClient</p><p>${session.clientInfoFromClient}</p></div>
|
||||
<div><p class="meta-muted">clientInfoFromRequest</p><p>${session.clientInfoFromRequest}</p></div>
|
||||
<div><p class="meta-muted">geo</p><p>${session.geo}</p></div>
|
||||
<div><p class="meta-muted">дата/время</p><p>${formatSessionTime(session.lastAuthenticatedAtMs)}</p></div>
|
||||
`;
|
||||
|
||||
const actionBtn = document.createElement('button');
|
||||
actionBtn.className = 'text-btn';
|
||||
actionBtn.type = 'button';
|
||||
actionBtn.textContent = 'Завершить сеанс';
|
||||
|
||||
const confirmModal = document.createElement('div');
|
||||
confirmModal.className = 'modal-shell';
|
||||
confirmModal.hidden = true;
|
||||
confirmModal.innerHTML = `
|
||||
<div class="modal-backdrop" data-close="true"></div>
|
||||
<div class="modal-dialog card" role="dialog" aria-modal="true" tabindex="-1">
|
||||
<p>Вы уверены, что хотите завершить этот сеанс?</p>
|
||||
<div class="auth-footer-actions">
|
||||
<button class="primary-btn" type="button" id="confirm-session-ok">ОК</button>
|
||||
<button class="ghost-btn" type="button" data-close="true">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const openModal = () => {
|
||||
confirmModal.hidden = false;
|
||||
confirmModal.querySelector('.modal-dialog').focus();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
confirmModal.hidden = true;
|
||||
};
|
||||
|
||||
actionBtn.addEventListener('click', openModal);
|
||||
|
||||
confirmModal.querySelector('#confirm-session-ok').addEventListener('click', closeModal);
|
||||
|
||||
confirmModal.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLElement && target.dataset.close === 'true') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
confirmModal.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
screen.append(details, actionBtn, confirmModal);
|
||||
return screen;
|
||||
}
|
||||
138
shine-UI/js/pages/device-view.js
Normal file
138
shine-UI/js/pages/device-view.js
Normal file
@ -0,0 +1,138 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { deviceSessions } from '../mock-data.js?v=20260327192619';
|
||||
import { terminateCurrentSession } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'device-view', title: 'Устройства' };
|
||||
|
||||
function formatSessionTime(ms) {
|
||||
return new Date(ms).toLocaleString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Устройства',
|
||||
leftAction: { label: '←', onClick: () => navigate('settings-view') },
|
||||
}),
|
||||
);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'card stack';
|
||||
actions.innerHTML = `
|
||||
<button class="primary-btn" type="button" id="connect-device-btn">Подключить устройство</button>
|
||||
<button class="text-btn" type="button" id="show-keys-btn">Показать ключи</button>
|
||||
`;
|
||||
|
||||
actions.querySelector('#connect-device-btn').addEventListener('click', () => navigate('connect-device-view'));
|
||||
actions.querySelector('#show-keys-btn').addEventListener('click', () => navigate('show-keys-view'));
|
||||
|
||||
const sessionsBlock = document.createElement('div');
|
||||
sessionsBlock.className = 'card stack';
|
||||
|
||||
const currentSession = deviceSessions[0];
|
||||
const otherSessions = deviceSessions.slice(1);
|
||||
|
||||
const createSessionItem = (session, isCurrent) => {
|
||||
const item = document.createElement('button');
|
||||
item.className = 'session-item';
|
||||
item.type = 'button';
|
||||
item.innerHTML = `
|
||||
<div class="row" style="align-items:flex-start;">
|
||||
<div class="stack" style="gap:4px; text-align:left;">
|
||||
<strong>${session.clientInfoFromClient}</strong>
|
||||
<span class="meta-muted">${session.geo}</span>
|
||||
</div>
|
||||
<span class="meta-muted">${formatSessionTime(session.lastAuthenticatedAtMs)}</span>
|
||||
</div>
|
||||
${
|
||||
isCurrent
|
||||
? '<div><span class="session-current-badge">Текущий сеанс</span></div>'
|
||||
: ''
|
||||
}
|
||||
`;
|
||||
item.addEventListener('click', () => navigate(`device-session-view/${session.sessionId}`));
|
||||
return item;
|
||||
};
|
||||
|
||||
const currentMenu = document.createElement('div');
|
||||
currentMenu.className = 'stack';
|
||||
currentMenu.innerHTML = '<p class="meta-muted">Текущий сеанс</p>';
|
||||
currentMenu.append(createSessionItem(currentSession, true));
|
||||
|
||||
const endCurrentSessionBtn = document.createElement('button');
|
||||
endCurrentSessionBtn.className = 'text-btn';
|
||||
endCurrentSessionBtn.type = 'button';
|
||||
endCurrentSessionBtn.textContent = 'Завершить текущую сессию';
|
||||
currentMenu.append(endCurrentSessionBtn);
|
||||
|
||||
const othersMenu = document.createElement('div');
|
||||
othersMenu.className = 'stack';
|
||||
othersMenu.innerHTML = '<p class="meta-muted">Остальные активные сеансы</p>';
|
||||
|
||||
if (otherSessions.length === 0) {
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'meta-muted';
|
||||
empty.textContent = 'Других активных сеансов нет.';
|
||||
othersMenu.append(empty);
|
||||
} else {
|
||||
otherSessions.forEach((session) => {
|
||||
othersMenu.append(createSessionItem(session, false));
|
||||
});
|
||||
}
|
||||
|
||||
const confirmModal = document.createElement('div');
|
||||
confirmModal.className = 'modal-shell';
|
||||
confirmModal.hidden = true;
|
||||
confirmModal.innerHTML = `
|
||||
<div class="modal-backdrop" data-close="true"></div>
|
||||
<div class="modal-dialog card" role="dialog" aria-modal="true" tabindex="-1">
|
||||
<p>Завершить текущую сессию?</p>
|
||||
<div class="auth-footer-actions">
|
||||
<button class="primary-btn" type="button" id="confirm-end-yes">Да</button>
|
||||
<button class="ghost-btn" type="button" data-close="true">Нет</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const openModal = () => {
|
||||
confirmModal.hidden = false;
|
||||
confirmModal.querySelector('.modal-dialog').focus();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
confirmModal.hidden = true;
|
||||
};
|
||||
|
||||
endCurrentSessionBtn.addEventListener('click', openModal);
|
||||
|
||||
confirmModal.querySelector('#confirm-end-yes').addEventListener('click', () => {
|
||||
terminateCurrentSession();
|
||||
navigate('start-view');
|
||||
});
|
||||
|
||||
confirmModal.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLElement && target.dataset.close === 'true') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
confirmModal.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
sessionsBlock.append(currentMenu, othersMenu);
|
||||
screen.append(actions, sessionsBlock, confirmModal);
|
||||
return screen;
|
||||
}
|
||||
161
shine-UI/js/pages/entry-settings-view.js
Normal file
161
shine-UI/js/pages/entry-settings-view.js
Normal file
@ -0,0 +1,161 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'entry-settings-view', title: 'Настройки входа', showAppChrome: false };
|
||||
|
||||
const SERVER_FIELDS = [
|
||||
{ key: 'solanaServer', label: 'Адрес Solana сервера' },
|
||||
{ key: 'shineServer', label: 'Адрес сервера Сияние' },
|
||||
{ key: 'arweaveServer', label: 'Адрес сервера Arweave' },
|
||||
];
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const draft = {
|
||||
language: state.entrySettings.language,
|
||||
solanaServer: state.entrySettings.solanaServer,
|
||||
shineServer: state.entrySettings.shineServer,
|
||||
arweaveServer: state.entrySettings.arweaveServer,
|
||||
statuses: { ...state.entrySettings.statuses },
|
||||
};
|
||||
|
||||
const timers = new Map();
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'card stack';
|
||||
|
||||
const languageLabel = document.createElement('label');
|
||||
languageLabel.className = 'stack';
|
||||
languageLabel.innerHTML = `<span class="field-label">Язык</span>`;
|
||||
|
||||
const languageSelect = document.createElement('select');
|
||||
languageSelect.className = 'select';
|
||||
languageSelect.innerHTML = `
|
||||
<option value="ru">Русский</option>
|
||||
<option value="en">English</option>
|
||||
`;
|
||||
languageSelect.value = draft.language;
|
||||
languageSelect.addEventListener('change', () => {
|
||||
draft.language = languageSelect.value;
|
||||
});
|
||||
languageLabel.append(languageSelect);
|
||||
|
||||
body.append(languageLabel);
|
||||
|
||||
SERVER_FIELDS.forEach((field) => {
|
||||
const block = document.createElement('div');
|
||||
block.className = 'stack';
|
||||
|
||||
const title = document.createElement('label');
|
||||
title.className = 'field-label';
|
||||
title.textContent = field.label;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.className = 'input';
|
||||
input.type = 'text';
|
||||
input.value = draft[field.key];
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'row wrap-row';
|
||||
|
||||
const checkButton = document.createElement('button');
|
||||
checkButton.className = 'ghost-btn server-check-btn';
|
||||
checkButton.type = 'button';
|
||||
checkButton.textContent = 'Проверить';
|
||||
|
||||
const status = document.createElement('span');
|
||||
status.className = 'status-line';
|
||||
|
||||
const applyStatus = (value) => {
|
||||
draft.statuses[field.key] = value;
|
||||
checkButton.classList.remove('is-available', 'is-unavailable');
|
||||
status.classList.remove('is-available', 'is-unavailable');
|
||||
|
||||
if (value === 'available') {
|
||||
status.textContent = 'Доступен';
|
||||
checkButton.classList.add('is-available');
|
||||
status.classList.add('is-available');
|
||||
} else if (value === 'unavailable') {
|
||||
status.textContent = 'Недоступен';
|
||||
checkButton.classList.add('is-unavailable');
|
||||
status.classList.add('is-unavailable');
|
||||
} else {
|
||||
status.textContent = 'Статус не проверен';
|
||||
}
|
||||
};
|
||||
|
||||
const runCheck = () => {
|
||||
draft[field.key] = input.value.trim();
|
||||
applyStatus(checkServerAvailability(input.value));
|
||||
};
|
||||
|
||||
applyStatus(draft.statuses[field.key]);
|
||||
|
||||
checkButton.addEventListener('click', runCheck);
|
||||
input.addEventListener('input', () => {
|
||||
draft[field.key] = input.value;
|
||||
applyStatus('idle');
|
||||
window.clearTimeout(timers.get(field.key));
|
||||
timers.set(field.key, window.setTimeout(runCheck, 3000));
|
||||
});
|
||||
input.addEventListener('blur', runCheck);
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
runCheck();
|
||||
}
|
||||
});
|
||||
|
||||
controls.append(checkButton, status);
|
||||
block.append(title, input, controls);
|
||||
body.append(block);
|
||||
});
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.className = 'ghost-btn';
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.textContent = 'Отмена';
|
||||
cancelButton.addEventListener('click', () => navigate('start-view'));
|
||||
|
||||
const saveButton = document.createElement('button');
|
||||
saveButton.className = 'primary-btn';
|
||||
saveButton.type = 'button';
|
||||
saveButton.textContent = 'Сохранить';
|
||||
saveButton.addEventListener('click', () => {
|
||||
saveEntrySettings(draft);
|
||||
navigate('start-view');
|
||||
});
|
||||
|
||||
actions.append(cancelButton, saveButton);
|
||||
|
||||
const help = document.createElement('button');
|
||||
help.className = 'help-fab';
|
||||
help.type = 'button';
|
||||
help.textContent = '?';
|
||||
help.addEventListener('click', () => {
|
||||
window.alert(
|
||||
'Текст для разработчиков: после ввода адреса любого сервера автопроверка запускается по кнопке "Проверить", после перехода в другое поле или если подождать больше 3 секунд. Зелёный статус означает доступность, красный — недоступность.',
|
||||
);
|
||||
});
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Настройки входа',
|
||||
leftAction: { label: '←', onClick: () => navigate('start-view') },
|
||||
}),
|
||||
body,
|
||||
actions,
|
||||
help,
|
||||
);
|
||||
|
||||
screen.cleanup = () => {
|
||||
timers.forEach((timerId) => window.clearTimeout(timerId));
|
||||
};
|
||||
|
||||
return screen;
|
||||
}
|
||||
99
shine-UI/js/pages/key-storage-view.js
Normal file
99
shine-UI/js/pages/key-storage-view.js
Normal file
@ -0,0 +1,99 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { authorizeSession, state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'key-storage-view', title: 'Какие ключи сохранить', showAppChrome: false };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
|
||||
const rootToggle = document.createElement('input');
|
||||
rootToggle.type = 'checkbox';
|
||||
rootToggle.checked = state.keyStorage.saveRoot;
|
||||
rootToggle.addEventListener('change', () => {
|
||||
state.keyStorage.saveRoot = rootToggle.checked;
|
||||
if (rootToggle.checked) {
|
||||
window.alert('Мы советуем не сохранять главный ключ на устройстве, он используется только для смены паролей и основных настроек.');
|
||||
}
|
||||
});
|
||||
|
||||
const blockchainToggle = document.createElement('input');
|
||||
blockchainToggle.type = 'checkbox';
|
||||
blockchainToggle.checked = state.keyStorage.saveBlockchain;
|
||||
blockchainToggle.addEventListener('change', () => {
|
||||
state.keyStorage.saveBlockchain = blockchainToggle.checked;
|
||||
});
|
||||
|
||||
const deviceToggle = document.createElement('input');
|
||||
deviceToggle.type = 'checkbox';
|
||||
deviceToggle.checked = true;
|
||||
deviceToggle.disabled = true;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="key-card stack">
|
||||
<label class="checkbox-row"><span class="field-label">Root Key</span></label>
|
||||
<input class="input" type="text" value="${state.keyStorage.rootKey}" />
|
||||
</div>
|
||||
<div class="key-card stack">
|
||||
<label class="checkbox-row"><span class="field-label">Blockchain Key</span></label>
|
||||
<input class="input" type="text" value="${state.keyStorage.blockchainKey}" />
|
||||
</div>
|
||||
<div class="key-card stack">
|
||||
<label class="checkbox-row"><span class="field-label">Device Key</span></label>
|
||||
<input class="input" type="text" value="${state.keyStorage.deviceKey}" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
card.children[0].querySelector('label').prepend(rootToggle);
|
||||
card.children[1].querySelector('label').prepend(blockchainToggle);
|
||||
card.children[2].querySelector('label').prepend(deviceToggle);
|
||||
|
||||
const rootInput = card.children[0].querySelector('.input');
|
||||
rootInput.addEventListener('input', () => {
|
||||
state.keyStorage.rootKey = rootInput.value;
|
||||
});
|
||||
|
||||
const blockchainInput = card.children[1].querySelector('.input');
|
||||
blockchainInput.addEventListener('input', () => {
|
||||
state.keyStorage.blockchainKey = blockchainInput.value;
|
||||
});
|
||||
|
||||
const deviceInput = card.children[2].querySelector('.input');
|
||||
deviceInput.addEventListener('input', () => {
|
||||
state.keyStorage.deviceKey = deviceInput.value;
|
||||
});
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.className = 'ghost-btn';
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.textContent = 'Отмена';
|
||||
cancelButton.addEventListener('click', () => navigate('login-password-view'));
|
||||
|
||||
const okButton = document.createElement('button');
|
||||
okButton.className = 'primary-btn';
|
||||
okButton.type = 'button';
|
||||
okButton.textContent = 'OK';
|
||||
okButton.addEventListener('click', () => {
|
||||
authorizeSession();
|
||||
navigate('profile-view');
|
||||
});
|
||||
|
||||
actions.append(cancelButton, okButton);
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Какие ключи сохранить',
|
||||
leftAction: { label: '←', onClick: () => navigate('login-password-view') },
|
||||
}),
|
||||
card,
|
||||
actions,
|
||||
);
|
||||
|
||||
return screen;
|
||||
}
|
||||
43
shine-UI/js/pages/language-view.js
Normal file
43
shine-UI/js/pages/language-view.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'language-view', title: 'Язык' };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Язык',
|
||||
leftAction: { label: '←', onClick: () => navigate('settings-view') },
|
||||
}),
|
||||
);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `
|
||||
<label class="checkbox-row"><input type="radio" name="language" value="ru" ${state.entrySettings.language === 'ru' ? 'checked' : ''} /> Русский</label>
|
||||
<label class="checkbox-row"><input type="radio" name="language" value="en" ${state.entrySettings.language === 'en' ? 'checked' : ''} /> English</label>
|
||||
`;
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
actions.innerHTML = `
|
||||
<button class="primary-btn" type="button" id="language-ok">ОК</button>
|
||||
<button class="ghost-btn" type="button" id="language-cancel">Отмена</button>
|
||||
`;
|
||||
|
||||
actions.querySelector('#language-ok').addEventListener('click', () => {
|
||||
const selected = card.querySelector('input[name="language"]:checked');
|
||||
if (selected) {
|
||||
state.entrySettings.language = selected.value;
|
||||
}
|
||||
navigate('settings-view');
|
||||
});
|
||||
|
||||
actions.querySelector('#language-cancel').addEventListener('click', () => navigate('settings-view'));
|
||||
|
||||
screen.append(card, actions);
|
||||
return screen;
|
||||
}
|
||||
67
shine-UI/js/pages/login-camera-view.js
Normal file
67
shine-UI/js/pages/login-camera-view.js
Normal file
@ -0,0 +1,67 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'login-camera-view', title: 'Войти по камере', showAppChrome: false };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const frame = document.createElement('div');
|
||||
frame.className = 'camera-shell';
|
||||
frame.innerHTML = `
|
||||
<video class="camera-video" autoplay playsinline muted></video>
|
||||
<div class="camera-frame"></div>
|
||||
<div class="camera-hint">Наведите QR-код в рамку</div>
|
||||
`;
|
||||
|
||||
const video = frame.querySelector('video');
|
||||
let stream = null;
|
||||
|
||||
const stopCamera = () => {
|
||||
if (stream) {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
stream = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (navigator.mediaDevices?.getUserMedia) {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ video: { facingMode: 'environment' }, audio: false })
|
||||
.then((nextStream) => {
|
||||
stream = nextStream;
|
||||
video.srcObject = nextStream;
|
||||
})
|
||||
.catch(() => {
|
||||
frame.insertAdjacentHTML('beforeend', '<div class="camera-error">Не удалось открыть камеру. Проверьте разрешения браузера.</div>');
|
||||
});
|
||||
} else {
|
||||
frame.insertAdjacentHTML('beforeend', '<div class="camera-error">Камера не поддерживается в этом браузере.</div>');
|
||||
}
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.className = 'ghost-btn';
|
||||
backButton.type = 'button';
|
||||
backButton.textContent = 'Назад';
|
||||
backButton.addEventListener('click', () => {
|
||||
stopCamera();
|
||||
navigate('login-view');
|
||||
});
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Войти по камере',
|
||||
leftAction: {
|
||||
label: '←',
|
||||
onClick: () => {
|
||||
stopCamera();
|
||||
navigate('login-view');
|
||||
},
|
||||
},
|
||||
}),
|
||||
frame,
|
||||
backButton,
|
||||
);
|
||||
|
||||
screen.cleanup = stopCamera;
|
||||
return screen;
|
||||
}
|
||||
75
shine-UI/js/pages/login-password-view.js
Normal file
75
shine-UI/js/pages/login-password-view.js
Normal file
@ -0,0 +1,75 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'login-password-view', title: 'Войти по логину', showAppChrome: false };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const form = document.createElement('div');
|
||||
form.className = 'card stack';
|
||||
|
||||
const loginInput = document.createElement('input');
|
||||
loginInput.className = 'input';
|
||||
loginInput.type = 'text';
|
||||
loginInput.value = state.loginDraft.login;
|
||||
loginInput.placeholder = 'Введите логин';
|
||||
|
||||
const passwordInput = document.createElement('input');
|
||||
passwordInput.className = 'input';
|
||||
passwordInput.type = 'password';
|
||||
passwordInput.value = state.loginDraft.password;
|
||||
passwordInput.placeholder = 'Введите пароль';
|
||||
|
||||
const advanced = document.createElement('label');
|
||||
advanced.className = 'checkbox-row';
|
||||
advanced.innerHTML = `<input type="checkbox" /> <span>Расширенные настройки</span>`;
|
||||
const advancedInput = advanced.querySelector('input');
|
||||
advancedInput.addEventListener('change', () => {
|
||||
if (advancedInput.checked) {
|
||||
window.alert('Расширенные настройки в стартовой версии приложения пока не используются.');
|
||||
advancedInput.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
form.innerHTML = `
|
||||
<label class="stack"><span class="field-label">Логин</span></label>
|
||||
<label class="stack"><span class="field-label">Пароль</span></label>
|
||||
`;
|
||||
form.children[0].append(loginInput);
|
||||
form.children[1].append(passwordInput);
|
||||
form.append(advanced);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.className = 'ghost-btn';
|
||||
backButton.type = 'button';
|
||||
backButton.textContent = 'Назад';
|
||||
backButton.addEventListener('click', () => navigate('login-view'));
|
||||
|
||||
const enterButton = document.createElement('button');
|
||||
enterButton.className = 'primary-btn';
|
||||
enterButton.type = 'button';
|
||||
enterButton.textContent = 'Войти';
|
||||
enterButton.addEventListener('click', () => {
|
||||
state.loginDraft.login = loginInput.value;
|
||||
state.loginDraft.password = passwordInput.value;
|
||||
navigate('key-storage-view');
|
||||
});
|
||||
|
||||
actions.append(backButton, enterButton);
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Войти по логину',
|
||||
leftAction: { label: '←', onClick: () => navigate('login-view') },
|
||||
}),
|
||||
form,
|
||||
actions,
|
||||
);
|
||||
|
||||
return screen;
|
||||
}
|
||||
72
shine-UI/js/pages/login-view.js
Normal file
72
shine-UI/js/pages/login-view.js
Normal file
@ -0,0 +1,72 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'login-view', title: 'Войти', showAppChrome: false };
|
||||
|
||||
function createQrCode() {
|
||||
const svgNS = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(svgNS, 'svg');
|
||||
svg.setAttribute('viewBox', '0 0 100 100');
|
||||
svg.classList.add('qr-code');
|
||||
|
||||
const cells = [
|
||||
[6, 6, 22, 22], [72, 6, 22, 22], [6, 72, 22, 22], [14, 14, 6, 6], [80, 14, 6, 6], [14, 80, 6, 6],
|
||||
[38, 12, 8, 8], [52, 12, 8, 8], [38, 26, 8, 8], [52, 26, 8, 8], [32, 40, 10, 10], [48, 40, 10, 10],
|
||||
[64, 40, 10, 10], [40, 56, 8, 8], [56, 56, 8, 8], [72, 56, 8, 8], [32, 72, 8, 8], [48, 72, 8, 8],
|
||||
[64, 72, 8, 8], [48, 86, 8, 8],
|
||||
];
|
||||
|
||||
cells.forEach(([x, y, width, height]) => {
|
||||
const rect = document.createElementNS(svgNS, 'rect');
|
||||
rect.setAttribute('x', x);
|
||||
rect.setAttribute('y', y);
|
||||
rect.setAttribute('width', width);
|
||||
rect.setAttribute('height', height);
|
||||
rect.setAttribute('rx', '2');
|
||||
svg.append(rect);
|
||||
});
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const qrCard = document.createElement('div');
|
||||
qrCard.className = 'card stack qr-card';
|
||||
qrCard.append(createQrCode());
|
||||
|
||||
const cameraButton = document.createElement('button');
|
||||
cameraButton.className = 'primary-btn';
|
||||
cameraButton.type = 'button';
|
||||
cameraButton.textContent = 'Войти по камере';
|
||||
cameraButton.addEventListener('click', () => navigate('login-camera-view'));
|
||||
|
||||
const loginButton = document.createElement('button');
|
||||
loginButton.className = 'ghost-btn';
|
||||
loginButton.type = 'button';
|
||||
loginButton.textContent = 'Войти по логину';
|
||||
loginButton.addEventListener('click', () => navigate('login-password-view'));
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-actions';
|
||||
actions.append(cameraButton, loginButton);
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.className = 'ghost-btn';
|
||||
backButton.type = 'button';
|
||||
backButton.textContent = 'Назад';
|
||||
backButton.addEventListener('click', () => navigate('start-view'));
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Войти',
|
||||
leftAction: { label: '←', onClick: () => navigate('start-view') },
|
||||
}),
|
||||
qrCard,
|
||||
actions,
|
||||
backButton,
|
||||
);
|
||||
|
||||
return screen;
|
||||
}
|
||||
42
shine-UI/js/pages/messages-list.js
Normal file
42
shine-UI/js/pages/messages-list.js
Normal file
@ -0,0 +1,42 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { directMessages } from '../mock-data.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Личные сообщения',
|
||||
rightActions: [{ label: '+', onClick: () => navigate('contact-search-view') }],
|
||||
}),
|
||||
);
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'stack';
|
||||
|
||||
directMessages.forEach((item) => {
|
||||
const row = document.createElement('article');
|
||||
row.className = 'list-item';
|
||||
row.innerHTML = `
|
||||
<div class="avatar">${item.initials}</div>
|
||||
<div>
|
||||
<div class="row" style="justify-content:flex-start; gap:8px;">
|
||||
<strong>${item.name}</strong>
|
||||
</div>
|
||||
<p class="meta-muted" style="margin-top:4px;">${item.lastMessage}</p>
|
||||
</div>
|
||||
<div style="display:grid; justify-items:end; gap:6px;">
|
||||
<span class="meta-muted">${item.time}</span>
|
||||
${item.unread ? `<span class="unread">${item.unread}</span>` : '<span></span>'}
|
||||
</div>
|
||||
`;
|
||||
row.addEventListener('click', () => navigate(`chat-view/${item.id}`));
|
||||
list.append(row);
|
||||
});
|
||||
|
||||
screen.append(list);
|
||||
return screen;
|
||||
}
|
||||
77
shine-UI/js/pages/network-view.js
Normal file
77
shine-UI/js/pages/network-view.js
Normal file
@ -0,0 +1,77 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { networkGraph } from '../mock-data.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'network-view', title: 'Связи' };
|
||||
|
||||
function toPoint(v) {
|
||||
return `${v.x}%`;
|
||||
}
|
||||
|
||||
function showHelpModal() {
|
||||
const root = document.getElementById('modal-root');
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="network-help-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 style="font-size:18px;">Справка по схеме связей</h3>
|
||||
<p class="meta-muted">В центре находишься ты.</p>
|
||||
<p class="meta-muted">Рядом показаны друзья первого уровня.</p>
|
||||
<p class="meta-muted">Далее могут существовать друзья второго уровня.</p>
|
||||
<p class="meta-muted">При одном нажатии на узел можно показать его связи.</p>
|
||||
<p class="meta-muted">При двойном нажатии узел может переместиться в центр.</p>
|
||||
<p class="meta-muted">При долгом удержании может открываться меню действий.</p>
|
||||
<p class="meta-muted">Логика схемы строится на одном запросе связей пользователя, дальше дерево достраивается на его основе.</p>
|
||||
<button class="primary-btn" id="close-network-help">Понятно</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
root.querySelector('#close-network-help').addEventListener('click', () => {
|
||||
root.innerHTML = '';
|
||||
});
|
||||
}
|
||||
|
||||
export function render() {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const header = renderHeader({
|
||||
title: 'Связи',
|
||||
rightActions: [{ label: 'Справка', onClick: showHelpModal }],
|
||||
});
|
||||
|
||||
const board = document.createElement('div');
|
||||
board.className = 'network-board';
|
||||
|
||||
const lines = networkGraph.peers
|
||||
.map(
|
||||
(peer) =>
|
||||
`<line x1="${toPoint(networkGraph.center)}" y1="${networkGraph.center.y}%" x2="${peer.x}%" y2="${peer.y}%" stroke="rgba(125,170,255,0.55)" stroke-width="1.5"/>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
board.innerHTML = `<svg class="network-svg" viewBox="0 0 100 100" preserveAspectRatio="none">${lines}</svg>`;
|
||||
|
||||
const centerNode = document.createElement('div');
|
||||
centerNode.className = 'node center';
|
||||
centerNode.style.left = `${networkGraph.center.x}%`;
|
||||
centerNode.style.top = `${networkGraph.center.y}%`;
|
||||
centerNode.innerHTML = `<div class="node-dot">${networkGraph.center.initials}</div><div class="node-label">${networkGraph.center.name}</div>`;
|
||||
|
||||
board.append(centerNode);
|
||||
|
||||
networkGraph.peers.forEach((peer) => {
|
||||
const node = document.createElement('div');
|
||||
node.className = 'node';
|
||||
node.style.left = `${peer.x}%`;
|
||||
node.style.top = `${peer.y}%`;
|
||||
node.innerHTML = `<div class="node-dot">${peer.initials}</div><div class="node-label">${peer.name}</div>`;
|
||||
board.append(node);
|
||||
});
|
||||
|
||||
const note = document.createElement('p');
|
||||
note.className = 'meta-muted';
|
||||
note.textContent = 'Схема статичная для демо, архитектура подготовлена под дальнейшую интерактивность.';
|
||||
|
||||
screen.append(header, board, note);
|
||||
return screen;
|
||||
}
|
||||
48
shine-UI/js/pages/notifications-view.js
Normal file
48
shine-UI/js/pages/notifications-view.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { notifications } from '../mock-data.js?v=20260327192619';
|
||||
import { state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'notifications-view', title: 'Уведомления' };
|
||||
|
||||
function renderList(container) {
|
||||
const active = state.notificationsTab;
|
||||
const items = notifications[active] || [];
|
||||
container.innerHTML = '';
|
||||
|
||||
items.forEach((item) => {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `<strong>${item.title}</strong><p class="meta-muted">${item.text}</p><p class="meta-muted">${item.time}</p>`;
|
||||
container.append(card);
|
||||
});
|
||||
}
|
||||
|
||||
export function render() {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(renderHeader({ title: 'Уведомления' }));
|
||||
|
||||
const tabs = document.createElement('div');
|
||||
tabs.className = 'tabs';
|
||||
tabs.innerHTML = `
|
||||
<button class="tab-btn ${state.notificationsTab === 'replies' ? 'active' : ''}" data-tab="replies">Ответы</button>
|
||||
<button class="tab-btn ${state.notificationsTab === 'events' ? 'active' : ''}" data-tab="events">События</button>
|
||||
`;
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'stack';
|
||||
renderList(list);
|
||||
|
||||
tabs.querySelectorAll('.tab-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
state.notificationsTab = btn.dataset.tab;
|
||||
tabs.querySelectorAll('.tab-btn').forEach((node) => node.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
renderList(list);
|
||||
});
|
||||
});
|
||||
|
||||
screen.append(tabs, list);
|
||||
return screen;
|
||||
}
|
||||
107
shine-UI/js/pages/profile-view.js
Normal file
107
shine-UI/js/pages/profile-view.js
Normal file
@ -0,0 +1,107 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { profile } from '../mock-data.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'profile-view', title: 'Профиль' };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const badgeHelp = {
|
||||
official: {
|
||||
title: 'Официальный аккаунт',
|
||||
text: 'Эта настройка включает или отключает отметку официального аккаунта в профиле. Используйте её, когда нужно показать или скрыть подтверждённый статус.',
|
||||
},
|
||||
shine: {
|
||||
title: 'Сияющий',
|
||||
text: 'Этот переключатель включает или отключает режим «Сияющий». Он управляет отображением дополнительного визуального акцента для профиля.',
|
||||
},
|
||||
};
|
||||
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Профиль',
|
||||
rightActions: [
|
||||
{ label: 'Кошелёк', onClick: () => navigate('wallet-view') },
|
||||
{ label: 'Настройки', onClick: () => navigate('settings-view') },
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `
|
||||
<div class="row">
|
||||
<div class="avatar large">${profile.avatarInitials}</div>
|
||||
<div class="stack" style="justify-items:end; text-align:right;">
|
||||
<button class="badge profile-badge-trigger" type="button" data-badge="official">✔ ${profile.badges[0]}</button>
|
||||
<button class="badge alt profile-badge-trigger" type="button" data-badge="shine">✨ ${profile.badges[1]}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 style="font-size:22px; margin-bottom:2px;">${profile.name}</h2>
|
||||
<p class="meta-muted">${profile.login}</p>
|
||||
</div>
|
||||
<div class="stack" style="gap:8px;">
|
||||
<div class="card" style="padding:10px;"><span class="meta-muted">Телефон:</span> ${profile.phone}</div>
|
||||
<div class="card" style="padding:10px;"><span class="meta-muted">Адрес:</span> ${profile.address}</div>
|
||||
<div class="card" style="padding:10px;"><span class="meta-muted">Email:</span> ${profile.email}</div>
|
||||
<div class="card" style="padding:10px;"><span class="meta-muted">Соцсети:</span> ${profile.socials}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'profile-help-modal';
|
||||
modal.hidden = true;
|
||||
modal.innerHTML = `
|
||||
<div class="profile-help-backdrop" data-close="true"></div>
|
||||
<div class="profile-help-dialog card" role="dialog" aria-modal="true" aria-labelledby="profile-help-title" tabindex="-1">
|
||||
<div class="row" style="align-items:flex-start;">
|
||||
<div>
|
||||
<div class="meta-muted" style="margin-bottom:4px;">Управление функцией</div>
|
||||
<h3 id="profile-help-title" style="font-size:18px;"></h3>
|
||||
</div>
|
||||
<button class="icon-btn profile-help-close" type="button" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<p class="profile-help-text"></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const titleEl = modal.querySelector('#profile-help-title');
|
||||
const textEl = modal.querySelector('.profile-help-text');
|
||||
const dialogEl = modal.querySelector('.profile-help-dialog');
|
||||
|
||||
function closeModal() {
|
||||
modal.hidden = true;
|
||||
}
|
||||
|
||||
function openModal(type) {
|
||||
const content = badgeHelp[type];
|
||||
if (!content) return;
|
||||
|
||||
titleEl.textContent = content.title;
|
||||
textEl.textContent = content.text;
|
||||
modal.hidden = false;
|
||||
dialogEl.focus();
|
||||
}
|
||||
|
||||
card.querySelectorAll('.profile-badge-trigger').forEach((button) => {
|
||||
button.addEventListener('click', () => openModal(button.dataset.badge));
|
||||
});
|
||||
|
||||
modal.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLElement && (target.dataset.close === 'true' || target.classList.contains('profile-help-close'))) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
modal.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
screen.append(card, modal);
|
||||
return screen;
|
||||
}
|
||||
75
shine-UI/js/pages/register-view.js
Normal file
75
shine-UI/js/pages/register-view.js
Normal file
@ -0,0 +1,75 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const form = document.createElement('div');
|
||||
form.className = 'card stack';
|
||||
|
||||
const loginInput = document.createElement('input');
|
||||
loginInput.className = 'input';
|
||||
loginInput.type = 'text';
|
||||
loginInput.value = state.registrationDraft.login;
|
||||
loginInput.placeholder = 'Введите логин';
|
||||
|
||||
const passwordInput = document.createElement('input');
|
||||
passwordInput.className = 'input';
|
||||
passwordInput.type = 'password';
|
||||
passwordInput.value = state.registrationDraft.password;
|
||||
passwordInput.placeholder = 'Введите пароль';
|
||||
|
||||
const advanced = document.createElement('label');
|
||||
advanced.className = 'checkbox-row';
|
||||
advanced.innerHTML = `<input type="checkbox" /> <span>Расширенные настройки</span>`;
|
||||
const advancedInput = advanced.querySelector('input');
|
||||
advancedInput.addEventListener('change', () => {
|
||||
if (advancedInput.checked) {
|
||||
window.alert('Расширенные настройки в стартовой версии не работают и не будут работать.');
|
||||
advancedInput.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
form.innerHTML = `
|
||||
<label class="stack"><span class="field-label">Логин</span></label>
|
||||
<label class="stack"><span class="field-label">Пароль</span></label>
|
||||
`;
|
||||
form.children[0].append(loginInput);
|
||||
form.children[1].append(passwordInput);
|
||||
form.append(advanced);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.className = 'ghost-btn';
|
||||
backButton.type = 'button';
|
||||
backButton.textContent = 'Назад';
|
||||
backButton.addEventListener('click', () => navigate('start-view'));
|
||||
|
||||
const nextButton = document.createElement('button');
|
||||
nextButton.className = 'primary-btn';
|
||||
nextButton.type = 'button';
|
||||
nextButton.textContent = 'Далее';
|
||||
nextButton.addEventListener('click', () => {
|
||||
state.registrationDraft.login = loginInput.value;
|
||||
state.registrationDraft.password = passwordInput.value;
|
||||
navigate('registration-payment-view');
|
||||
});
|
||||
|
||||
actions.append(backButton, nextButton);
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Зарегистрироваться',
|
||||
leftAction: { label: '←', onClick: () => navigate('start-view') },
|
||||
}),
|
||||
form,
|
||||
actions,
|
||||
);
|
||||
|
||||
return screen;
|
||||
}
|
||||
92
shine-UI/js/pages/registration-keys-view.js
Normal file
92
shine-UI/js/pages/registration-keys-view.js
Normal file
@ -0,0 +1,92 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { authorizeSession, state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'registration-keys-view', title: 'Сохранение ключей', showAppChrome: false };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const normalizedLogin = (state.registrationDraft.login || '').trim();
|
||||
const displayLogin = normalizedLogin || '@new.user';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
|
||||
const title = document.createElement('p');
|
||||
title.className = 'auth-copy';
|
||||
title.textContent = `Поздравляю, ваш логин ${displayLogin} зарегистрирован.`;
|
||||
|
||||
const question = document.createElement('p');
|
||||
question.className = 'auth-copy';
|
||||
question.textContent = 'Какие ключи вы хотите сохранить на этом устройстве?';
|
||||
|
||||
const rootToggle = document.createElement('input');
|
||||
rootToggle.type = 'checkbox';
|
||||
rootToggle.checked = state.keyStorage.saveRoot;
|
||||
|
||||
const blockchainToggle = document.createElement('input');
|
||||
blockchainToggle.type = 'checkbox';
|
||||
blockchainToggle.checked = state.keyStorage.saveBlockchain;
|
||||
|
||||
const deviceToggle = document.createElement('input');
|
||||
deviceToggle.type = 'checkbox';
|
||||
deviceToggle.checked = state.keyStorage.saveDevice;
|
||||
|
||||
rootToggle.addEventListener('change', () => {
|
||||
state.keyStorage.saveRoot = rootToggle.checked;
|
||||
});
|
||||
|
||||
blockchainToggle.addEventListener('change', () => {
|
||||
state.keyStorage.saveBlockchain = blockchainToggle.checked;
|
||||
});
|
||||
|
||||
deviceToggle.addEventListener('change', () => {
|
||||
state.keyStorage.saveDevice = deviceToggle.checked;
|
||||
});
|
||||
|
||||
const rootRow = document.createElement('label');
|
||||
rootRow.className = 'checkbox-row';
|
||||
rootRow.append(rootToggle, document.createTextNode('root key'));
|
||||
|
||||
const blockchainRow = document.createElement('label');
|
||||
blockchainRow.className = 'checkbox-row';
|
||||
blockchainRow.append(blockchainToggle, document.createTextNode('blockchain key'));
|
||||
|
||||
const deviceRow = document.createElement('label');
|
||||
deviceRow.className = 'checkbox-row';
|
||||
deviceRow.append(deviceToggle, document.createTextNode('device key'));
|
||||
|
||||
card.append(title, question, rootRow, deviceRow, blockchainRow);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.className = 'ghost-btn';
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.textContent = 'Отмена';
|
||||
cancelButton.addEventListener('click', () => navigate('start-view'));
|
||||
|
||||
const okButton = document.createElement('button');
|
||||
okButton.className = 'primary-btn';
|
||||
okButton.type = 'button';
|
||||
okButton.textContent = 'OK';
|
||||
okButton.addEventListener('click', () => {
|
||||
authorizeSession();
|
||||
navigate('profile-view');
|
||||
});
|
||||
|
||||
actions.append(cancelButton, okButton);
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Сохранение ключей',
|
||||
leftAction: { label: '←', onClick: () => navigate('registration-payment-view') },
|
||||
}),
|
||||
card,
|
||||
actions,
|
||||
);
|
||||
|
||||
return screen;
|
||||
}
|
||||
93
shine-UI/js/pages/registration-payment-view.js
Normal file
93
shine-UI/js/pages/registration-payment-view.js
Normal file
@ -0,0 +1,93 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { refreshRegistrationBalance, state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
|
||||
const walletValue = document.createElement('input');
|
||||
walletValue.className = 'input';
|
||||
walletValue.type = 'text';
|
||||
walletValue.value = state.registrationPayment.walletAddress;
|
||||
walletValue.addEventListener('input', () => {
|
||||
state.registrationPayment.walletAddress = walletValue.value;
|
||||
});
|
||||
|
||||
const walletRow = document.createElement('div');
|
||||
walletRow.className = 'inline-input-row';
|
||||
|
||||
const copyButton = document.createElement('button');
|
||||
copyButton.className = 'ghost-btn';
|
||||
copyButton.type = 'button';
|
||||
copyButton.textContent = 'Скопировать номер';
|
||||
copyButton.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(walletValue.value);
|
||||
copyButton.textContent = 'Скопировано';
|
||||
window.setTimeout(() => {
|
||||
copyButton.textContent = 'Скопировать номер';
|
||||
}, 1500);
|
||||
} catch {
|
||||
window.alert('Не удалось скопировать номер кошелька.');
|
||||
}
|
||||
});
|
||||
|
||||
walletRow.append(walletValue, copyButton);
|
||||
|
||||
const balanceRow = document.createElement('div');
|
||||
balanceRow.className = 'row wrap-row';
|
||||
|
||||
const balanceValue = document.createElement('strong');
|
||||
balanceValue.textContent = `${state.registrationPayment.balanceSOL} SOL`;
|
||||
|
||||
const refreshButton = document.createElement('button');
|
||||
refreshButton.className = 'square-btn';
|
||||
refreshButton.type = 'button';
|
||||
refreshButton.textContent = '↻';
|
||||
refreshButton.title = 'Обновить';
|
||||
refreshButton.addEventListener('click', () => {
|
||||
balanceValue.textContent = `${refreshRegistrationBalance()} SOL`;
|
||||
});
|
||||
|
||||
balanceRow.append(balanceValue, refreshButton);
|
||||
|
||||
const topupButton = document.createElement('button');
|
||||
topupButton.className = 'ghost-btn';
|
||||
topupButton.type = 'button';
|
||||
topupButton.textContent = 'Пополнить счет';
|
||||
topupButton.addEventListener('click', () => navigate('topup-view'));
|
||||
|
||||
const submitButton = document.createElement('button');
|
||||
submitButton.className = 'primary-btn';
|
||||
submitButton.type = 'button';
|
||||
submitButton.textContent = 'Зарегистрироваться';
|
||||
submitButton.addEventListener('click', () => {
|
||||
navigate('registration-keys-view');
|
||||
});
|
||||
|
||||
card.innerHTML = `
|
||||
<p class="auth-copy">Для регистрации в Solana нужно заплатить 0,01 SOL (примерно 1 доллар).</p>
|
||||
<label class="stack"><span class="field-label">Номер кошелька</span></label>
|
||||
<div class="stack">
|
||||
<span class="field-label">Баланс</span>
|
||||
</div>
|
||||
`;
|
||||
card.children[1].append(walletRow);
|
||||
card.children[2].append(balanceRow);
|
||||
card.append(topupButton, submitButton);
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Оплата регистрации',
|
||||
leftAction: { label: '←', onClick: () => navigate('register-view') },
|
||||
}),
|
||||
card,
|
||||
);
|
||||
|
||||
return screen;
|
||||
}
|
||||
143
shine-UI/js/pages/server-settings-view.js
Normal file
143
shine-UI/js/pages/server-settings-view.js
Normal file
@ -0,0 +1,143 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'server-settings-view', title: 'Настройки серверов' };
|
||||
|
||||
const SERVER_FIELDS = [
|
||||
{ key: 'solanaServer', label: 'Адрес Solana сервера' },
|
||||
{ key: 'shineServer', label: 'Адрес сервера Сияние' },
|
||||
{ key: 'arweaveServer', label: 'Адрес сервера Arweave' },
|
||||
];
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const draft = {
|
||||
language: state.entrySettings.language,
|
||||
solanaServer: state.entrySettings.solanaServer,
|
||||
shineServer: state.entrySettings.shineServer,
|
||||
arweaveServer: state.entrySettings.arweaveServer,
|
||||
statuses: { ...state.entrySettings.statuses },
|
||||
};
|
||||
|
||||
const timers = new Map();
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'card stack';
|
||||
|
||||
SERVER_FIELDS.forEach((field) => {
|
||||
const block = document.createElement('div');
|
||||
block.className = 'stack';
|
||||
|
||||
const title = document.createElement('label');
|
||||
title.className = 'field-label';
|
||||
title.textContent = field.label;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.className = 'input';
|
||||
input.type = 'text';
|
||||
input.value = draft[field.key];
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'row wrap-row';
|
||||
|
||||
const checkButton = document.createElement('button');
|
||||
checkButton.className = 'ghost-btn server-check-btn';
|
||||
checkButton.type = 'button';
|
||||
checkButton.textContent = 'Проверить';
|
||||
|
||||
const status = document.createElement('span');
|
||||
status.className = 'status-line';
|
||||
|
||||
const applyStatus = (value) => {
|
||||
draft.statuses[field.key] = value;
|
||||
checkButton.classList.remove('is-available', 'is-unavailable');
|
||||
status.classList.remove('is-available', 'is-unavailable');
|
||||
|
||||
if (value === 'available') {
|
||||
status.textContent = 'Доступен';
|
||||
checkButton.classList.add('is-available');
|
||||
status.classList.add('is-available');
|
||||
} else if (value === 'unavailable') {
|
||||
status.textContent = 'Недоступен';
|
||||
checkButton.classList.add('is-unavailable');
|
||||
status.classList.add('is-unavailable');
|
||||
} else {
|
||||
status.textContent = 'Статус не проверен';
|
||||
}
|
||||
};
|
||||
|
||||
const runCheck = () => {
|
||||
draft[field.key] = input.value.trim();
|
||||
applyStatus(checkServerAvailability(input.value));
|
||||
};
|
||||
|
||||
applyStatus(draft.statuses[field.key]);
|
||||
|
||||
checkButton.addEventListener('click', runCheck);
|
||||
input.addEventListener('input', () => {
|
||||
draft[field.key] = input.value;
|
||||
applyStatus('idle');
|
||||
window.clearTimeout(timers.get(field.key));
|
||||
timers.set(field.key, window.setTimeout(runCheck, 3000));
|
||||
});
|
||||
input.addEventListener('blur', runCheck);
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
runCheck();
|
||||
}
|
||||
});
|
||||
|
||||
controls.append(checkButton, status);
|
||||
block.append(title, input, controls);
|
||||
body.append(block);
|
||||
});
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.className = 'ghost-btn';
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.textContent = 'Отмена';
|
||||
cancelButton.addEventListener('click', () => navigate('settings-view'));
|
||||
|
||||
const saveButton = document.createElement('button');
|
||||
saveButton.className = 'primary-btn';
|
||||
saveButton.type = 'button';
|
||||
saveButton.textContent = 'Сохранить';
|
||||
saveButton.addEventListener('click', () => {
|
||||
saveEntrySettings(draft);
|
||||
navigate('settings-view');
|
||||
});
|
||||
|
||||
actions.append(cancelButton, saveButton);
|
||||
|
||||
const help = document.createElement('button');
|
||||
help.className = 'help-fab';
|
||||
help.type = 'button';
|
||||
help.textContent = '?';
|
||||
help.addEventListener('click', () => {
|
||||
window.alert(
|
||||
'Текст для разработчиков: после ввода адреса любого сервера автопроверка запускается по кнопке "Проверить", после перехода в другое поле или если подождать больше 3 секунд. Зелёный статус означает доступность, красный — недоступность.',
|
||||
);
|
||||
});
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Настройки серверов',
|
||||
leftAction: { label: '←', onClick: () => navigate('settings-view') },
|
||||
}),
|
||||
body,
|
||||
actions,
|
||||
help,
|
||||
);
|
||||
|
||||
screen.cleanup = () => {
|
||||
timers.forEach((timerId) => window.clearTimeout(timerId));
|
||||
};
|
||||
|
||||
return screen;
|
||||
}
|
||||
30
shine-UI/js/pages/settings-view.js
Normal file
30
shine-UI/js/pages/settings-view.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'settings-view', title: 'Настройки' };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Настройки',
|
||||
leftAction: { label: '←', onClick: () => navigate('profile-view') },
|
||||
}),
|
||||
);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `
|
||||
<button class="text-btn" type="button" id="settings-device">Устройства</button>
|
||||
<button class="text-btn" type="button" id="settings-servers">Настройки серверов</button>
|
||||
<button class="text-btn" type="button" id="settings-language">Язык / Language</button>
|
||||
`;
|
||||
|
||||
card.querySelector('#settings-device').addEventListener('click', () => navigate('device-view'));
|
||||
card.querySelector('#settings-servers').addEventListener('click', () => navigate('server-settings-view'));
|
||||
card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view'));
|
||||
|
||||
screen.append(card);
|
||||
return screen;
|
||||
}
|
||||
128
shine-UI/js/pages/show-keys-view.js
Normal file
128
shine-UI/js/pages/show-keys-view.js
Normal file
@ -0,0 +1,128 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'show-keys-view', title: 'Показать ключи' };
|
||||
|
||||
function randomKey(length = 44) {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
result += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const keys = {
|
||||
root: randomKey(),
|
||||
blockchain: randomKey(),
|
||||
device: randomKey(),
|
||||
};
|
||||
|
||||
const visible = {
|
||||
root: false,
|
||||
blockchain: false,
|
||||
device: false,
|
||||
};
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Показать ключи',
|
||||
leftAction: { label: '←', onClick: () => navigate('device-view') },
|
||||
}),
|
||||
);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
|
||||
const renderField = (id, label) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'key-card stack';
|
||||
row.innerHTML = `
|
||||
<div class="row">
|
||||
<span class="field-label">${label}</span>
|
||||
<button class="icon-btn small-btn" type="button" data-toggle="${id}">Показать</button>
|
||||
</div>
|
||||
<div class="key-value" data-value="${id}">*****</div>
|
||||
`;
|
||||
return row;
|
||||
};
|
||||
|
||||
card.append(renderField('root', 'root key'), renderField('blockchain', 'blockchain key'), renderField('device', 'device key'));
|
||||
|
||||
const updateField = (id) => {
|
||||
const valueEl = card.querySelector(`[data-value="${id}"]`);
|
||||
const btnEl = card.querySelector(`[data-toggle="${id}"]`);
|
||||
valueEl.textContent = visible[id] ? keys[id] : '*****';
|
||||
btnEl.textContent = visible[id] ? 'Скрыть' : 'Показать';
|
||||
};
|
||||
|
||||
card.querySelectorAll('[data-toggle]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const { toggle } = button.dataset;
|
||||
visible[toggle] = !visible[toggle];
|
||||
updateField(toggle);
|
||||
});
|
||||
});
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
actions.innerHTML = `
|
||||
<button class="primary-btn" type="button" id="save-keys">Сохранить новые</button>
|
||||
<button class="ghost-btn" type="button" id="cancel-keys">Отмена</button>
|
||||
`;
|
||||
|
||||
const confirmModal = document.createElement('div');
|
||||
confirmModal.className = 'modal-shell';
|
||||
confirmModal.hidden = true;
|
||||
confirmModal.innerHTML = `
|
||||
<div class="modal-backdrop" data-close="true"></div>
|
||||
<div class="modal-dialog card" role="dialog" aria-modal="true" tabindex="-1">
|
||||
<p>Вы уверены, что хотите изменить ключи?</p>
|
||||
<div class="auth-footer-actions">
|
||||
<button class="primary-btn" type="button" id="confirm-keys-ok">ОК</button>
|
||||
<button class="ghost-btn" type="button" data-close="true">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const openModal = () => {
|
||||
confirmModal.hidden = false;
|
||||
confirmModal.querySelector('.modal-dialog').focus();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
confirmModal.hidden = true;
|
||||
};
|
||||
|
||||
actions.querySelector('#save-keys').addEventListener('click', openModal);
|
||||
actions.querySelector('#cancel-keys').addEventListener('click', () => navigate('device-view'));
|
||||
|
||||
confirmModal.querySelector('#confirm-keys-ok').addEventListener('click', () => {
|
||||
keys.root = randomKey();
|
||||
keys.blockchain = randomKey();
|
||||
keys.device = randomKey();
|
||||
updateField('root');
|
||||
updateField('blockchain');
|
||||
updateField('device');
|
||||
closeModal();
|
||||
});
|
||||
|
||||
confirmModal.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLElement && target.dataset.close === 'true') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
confirmModal.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
screen.append(card, actions, confirmModal);
|
||||
return screen;
|
||||
}
|
||||
53
shine-UI/js/pages/start-view.js
Normal file
53
shine-UI/js/pages/start-view.js
Normal file
@ -0,0 +1,53 @@
|
||||
import { clearStartHint, state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'start-view', title: 'Старт', showAppChrome: false };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'auth-screen stack';
|
||||
|
||||
const logo = document.createElement('img');
|
||||
logo.className = 'auth-logo';
|
||||
logo.src = './img/logo.jpg';
|
||||
logo.alt = 'Логотип Сияние';
|
||||
|
||||
const title = document.createElement('h1');
|
||||
title.className = 'auth-brand';
|
||||
title.textContent = 'Сияние';
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-actions';
|
||||
|
||||
const loginButton = document.createElement('button');
|
||||
loginButton.className = 'primary-btn';
|
||||
loginButton.type = 'button';
|
||||
loginButton.textContent = 'Войти';
|
||||
loginButton.addEventListener('click', () => navigate('login-view'));
|
||||
|
||||
const registerButton = document.createElement('button');
|
||||
registerButton.className = 'ghost-btn';
|
||||
registerButton.type = 'button';
|
||||
registerButton.textContent = 'Зарегистрироваться';
|
||||
registerButton.addEventListener('click', () => navigate('register-view'));
|
||||
|
||||
const settingsButton = document.createElement('button');
|
||||
settingsButton.className = 'ghost-btn';
|
||||
settingsButton.type = 'button';
|
||||
settingsButton.textContent = 'Настройки';
|
||||
settingsButton.addEventListener('click', () => navigate('entry-settings-view'));
|
||||
|
||||
actions.append(loginButton, registerButton, settingsButton);
|
||||
|
||||
screen.append(logo, title);
|
||||
|
||||
if (state.startHint) {
|
||||
const notice = document.createElement('div');
|
||||
notice.className = 'card auth-status-card';
|
||||
notice.textContent = state.startHint;
|
||||
screen.append(notice);
|
||||
clearStartHint();
|
||||
}
|
||||
|
||||
screen.append(actions);
|
||||
return screen;
|
||||
}
|
||||
84
shine-UI/js/pages/topup-view.js
Normal file
84
shine-UI/js/pages/topup-view.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { state } from '../state.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'topup-view', title: 'Пополнение счета', showAppChrome: false };
|
||||
|
||||
const BUY_LINK = 'https://www.moonpay.com/buy/sol';
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const walletValue = document.createElement('input');
|
||||
walletValue.className = 'input';
|
||||
walletValue.type = 'text';
|
||||
walletValue.value = state.registrationPayment.walletAddress;
|
||||
walletValue.readOnly = true;
|
||||
walletValue.style.fontSize = '13px';
|
||||
|
||||
const copyButton = document.createElement('button');
|
||||
copyButton.className = 'ghost-btn';
|
||||
copyButton.type = 'button';
|
||||
copyButton.textContent = 'Скопировать';
|
||||
copyButton.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(walletValue.value);
|
||||
copyButton.textContent = 'Скопировано';
|
||||
window.setTimeout(() => {
|
||||
copyButton.textContent = 'Скопировать';
|
||||
}, 1500);
|
||||
} catch {
|
||||
window.alert('Не удалось скопировать номер кошелька.');
|
||||
}
|
||||
});
|
||||
|
||||
const walletRow = document.createElement('div');
|
||||
walletRow.className = 'inline-input-row';
|
||||
walletRow.append(walletValue, copyButton);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `
|
||||
<p class="auth-copy">Для пополнения счета скопируйте номер кошелька.</p>
|
||||
<div class="stack" style="gap:6px;">
|
||||
<p class="meta-muted">1. Пополните через любое свое приложение, используя этот кошелек в сети Solana.</p>
|
||||
<p class="meta-muted">2. Либо откройте страницу для покупки SOL.</p>
|
||||
<p class="meta-muted">3. Либо используйте кнопку «Тестовое пополнение» (работает в тестовой Solana).</p>
|
||||
</div>
|
||||
<a class="link-card" href="${BUY_LINK}" target="_blank" rel="noreferrer">Открыть страницу покупки SOL</a>
|
||||
<div class="card stack" style="padding:12px; max-width:320px;">
|
||||
<div class="field-label" style="margin-bottom:6px;">Кошелёк для пополнения</div>
|
||||
</div>
|
||||
`;
|
||||
card.children[3].append(walletRow);
|
||||
|
||||
const testButton = document.createElement('button');
|
||||
testButton.className = 'ghost-btn';
|
||||
testButton.type = 'button';
|
||||
testButton.textContent = 'Тестовое пополнение';
|
||||
testButton.addEventListener('click', () => {
|
||||
state.registrationPayment.balanceSOL = '0.0250';
|
||||
window.alert('Тестовое пополнение выполнено. Баланс обновлён.');
|
||||
});
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.className = 'primary-btn';
|
||||
backButton.type = 'button';
|
||||
backButton.textContent = 'Назад';
|
||||
backButton.addEventListener('click', () => navigate('registration-payment-view'));
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
actions.append(testButton, backButton);
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Пополнение счета',
|
||||
leftAction: { label: '←', onClick: () => navigate('registration-payment-view') },
|
||||
}),
|
||||
card,
|
||||
actions,
|
||||
);
|
||||
|
||||
return screen;
|
||||
}
|
||||
78
shine-UI/js/pages/wallet-view.js
Normal file
78
shine-UI/js/pages/wallet-view.js
Normal file
@ -0,0 +1,78 @@
|
||||
import { renderHeader } from '../components/header.js?v=20260327192619';
|
||||
import { wallet } from '../mock-data.js?v=20260327192619';
|
||||
|
||||
export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
let statusText = 'Данные демонстрационные';
|
||||
|
||||
const status = document.createElement('p');
|
||||
status.className = 'meta-muted';
|
||||
|
||||
const updateStatus = (text) => {
|
||||
statusText = text;
|
||||
status.textContent = statusText;
|
||||
};
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Кошелёк',
|
||||
leftAction: { label: '←', onClick: () => navigate('profile-view') },
|
||||
})
|
||||
);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `
|
||||
<div>
|
||||
<p class="meta-muted">Баланс</p>
|
||||
<h2 style="font-size:30px;">${wallet.balanceSOL} SOL</h2>
|
||||
<p class="meta-muted">Обновлено: ${wallet.updatedAt}</p>
|
||||
</div>
|
||||
<div class="card" style="padding:10px;">
|
||||
<p class="meta-muted" style="margin-bottom:6px;">Публичный адрес</p>
|
||||
<p style="font-size:13px; line-height:1.4; word-break:break-all;">${wallet.publicAddress}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'stack';
|
||||
actions.innerHTML = `
|
||||
<div class="row">
|
||||
<button class="text-btn" id="copy-address">Копировать адрес</button>
|
||||
<button class="ghost-btn" id="refresh-balance">Обновить баланс</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="primary-btn" id="send-sol" style="width:100%;">Перевести</button>
|
||||
<button class="primary-btn" id="topup-sol" style="width:100%;">Пополнить</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
actions.querySelector('#copy-address').addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(wallet.publicAddress);
|
||||
updateStatus('Адрес скопирован в буфер обмена');
|
||||
} catch {
|
||||
updateStatus('Не удалось скопировать в этом браузере');
|
||||
}
|
||||
});
|
||||
|
||||
actions.querySelector('#refresh-balance').addEventListener('click', () => {
|
||||
updateStatus(`Баланс обновлен: ${wallet.balanceSOL} SOL`);
|
||||
});
|
||||
|
||||
actions.querySelector('#send-sol').addEventListener('click', () => {
|
||||
updateStatus('Демо-функция: перевод будет добавлен позже');
|
||||
});
|
||||
|
||||
actions.querySelector('#topup-sol').addEventListener('click', () => {
|
||||
updateStatus('Демо-функция: пополнение будет добавлено позже');
|
||||
});
|
||||
|
||||
updateStatus(statusText);
|
||||
|
||||
screen.append(card, actions, status);
|
||||
return screen;
|
||||
}
|
||||
62
shine-UI/js/router.js
Normal file
62
shine-UI/js/router.js
Normal file
@ -0,0 +1,62 @@
|
||||
const ROOT_PAGES = ['messages-list', 'channels-list', 'network-view', 'notifications-view', 'profile-view'];
|
||||
|
||||
export const PRE_AUTH_PAGES = [
|
||||
'start-view',
|
||||
'entry-settings-view',
|
||||
'register-view',
|
||||
'registration-payment-view',
|
||||
'registration-keys-view',
|
||||
'topup-view',
|
||||
'login-view',
|
||||
'login-camera-view',
|
||||
'login-password-view',
|
||||
'key-storage-view',
|
||||
];
|
||||
|
||||
export function getRoute() {
|
||||
const raw = window.location.hash.replace(/^#\/?/, '');
|
||||
if (!raw) {
|
||||
return { pageId: '', params: {} };
|
||||
}
|
||||
|
||||
const [pageId, dynamicId] = raw.split('/');
|
||||
|
||||
if (pageId === 'chat-view') {
|
||||
return { pageId, params: { chatId: dynamicId || '' } };
|
||||
}
|
||||
|
||||
if (pageId === 'channel-view') {
|
||||
return { pageId, params: { channelId: dynamicId || '' } };
|
||||
}
|
||||
|
||||
if (pageId === 'device-session-view') {
|
||||
return { pageId, params: { sessionId: dynamicId || '' } };
|
||||
}
|
||||
|
||||
return { pageId, params: {} };
|
||||
}
|
||||
|
||||
export function navigate(path) {
|
||||
window.location.hash = `#/${path}`;
|
||||
}
|
||||
|
||||
export function resolveToolbarActive(pageId) {
|
||||
if (ROOT_PAGES.includes(pageId)) return pageId;
|
||||
if (
|
||||
pageId === 'wallet-view' ||
|
||||
pageId === 'settings-view' ||
|
||||
pageId === 'server-settings-view' ||
|
||||
pageId === 'device-view' ||
|
||||
pageId === 'connect-device-view' ||
|
||||
pageId === 'device-qr-view' ||
|
||||
pageId === 'device-camera-view' ||
|
||||
pageId === 'show-keys-view' ||
|
||||
pageId === 'device-session-view' ||
|
||||
pageId === 'language-view'
|
||||
) {
|
||||
return 'profile-view';
|
||||
}
|
||||
if (pageId === 'chat-view' || pageId === 'contact-search-view') return 'messages-list';
|
||||
if (pageId === 'channel-view') return 'channels-list';
|
||||
return 'profile-view';
|
||||
}
|
||||
111
shine-UI/js/state.js
Normal file
111
shine-UI/js/state.js
Normal file
@ -0,0 +1,111 @@
|
||||
import { chatMessages, wallet } from './mock-data.js?v=20260327192619';
|
||||
|
||||
const clone = (value) => JSON.parse(JSON.stringify(value));
|
||||
|
||||
export const state = {
|
||||
chats: clone(chatMessages),
|
||||
notificationsTab: 'replies',
|
||||
pageLabelCollapsed: false,
|
||||
session: {
|
||||
isAuthorized: false,
|
||||
},
|
||||
startHint: '',
|
||||
entrySettings: {
|
||||
language: 'ru',
|
||||
solanaServer: 'https://api.mainnet-beta.solana.com',
|
||||
shineServer: 'https://demo.shine.local',
|
||||
arweaveServer: 'https://arweave.net',
|
||||
statuses: {
|
||||
solanaServer: 'idle',
|
||||
shineServer: 'idle',
|
||||
arweaveServer: 'idle',
|
||||
},
|
||||
},
|
||||
registrationDraft: {
|
||||
login: '',
|
||||
password: '',
|
||||
},
|
||||
loginDraft: {
|
||||
login: '',
|
||||
password: '',
|
||||
},
|
||||
registrationPayment: {
|
||||
walletAddress: wallet.publicAddress,
|
||||
balanceSOL: '0.0068',
|
||||
},
|
||||
keyStorage: {
|
||||
rootKey: 'RK-4Q8N-1SZP-71LM-AUTH-ROOT',
|
||||
blockchainKey: 'BK-SOL-19F2-CHAIN-ACCESS',
|
||||
deviceKey: 'DK-LOCAL-82XA-DEVICE-SIGN',
|
||||
saveRoot: false,
|
||||
saveBlockchain: true,
|
||||
saveDevice: true,
|
||||
},
|
||||
deviceConnect: {
|
||||
root: true,
|
||||
blockchain: true,
|
||||
device: true,
|
||||
},
|
||||
};
|
||||
|
||||
export function getChatMessages(chatId) {
|
||||
if (!state.chats[chatId]) {
|
||||
state.chats[chatId] = [];
|
||||
}
|
||||
return state.chats[chatId];
|
||||
}
|
||||
|
||||
export function addChatMessage(chatId, text) {
|
||||
const message = text.trim();
|
||||
if (!message) return;
|
||||
getChatMessages(chatId).push({ from: 'out', text: message });
|
||||
}
|
||||
|
||||
export function togglePageLabel() {
|
||||
state.pageLabelCollapsed = !state.pageLabelCollapsed;
|
||||
}
|
||||
|
||||
export function ensureChat(chatId) {
|
||||
return getChatMessages(chatId);
|
||||
}
|
||||
|
||||
export function checkServerAvailability(address) {
|
||||
const normalized = address.trim().toLowerCase();
|
||||
if (!normalized) return 'unavailable';
|
||||
|
||||
const looksLikeUrl = /^https?:\/\/[a-z0-9.-]+/i.test(normalized);
|
||||
const blockedWord = /(offline|down|fail|bad|broken|invalid)/i.test(normalized);
|
||||
return looksLikeUrl && !blockedWord ? 'available' : 'unavailable';
|
||||
}
|
||||
|
||||
export function saveEntrySettings(nextSettings) {
|
||||
state.entrySettings = {
|
||||
...state.entrySettings,
|
||||
...nextSettings,
|
||||
statuses: {
|
||||
...state.entrySettings.statuses,
|
||||
...(nextSettings.statuses || {}),
|
||||
},
|
||||
};
|
||||
state.startHint = 'Настройки входа сохранены, адреса серверов обновлены.';
|
||||
}
|
||||
|
||||
export function clearStartHint() {
|
||||
state.startHint = '';
|
||||
}
|
||||
|
||||
export function authorizeSession() {
|
||||
state.session.isAuthorized = true;
|
||||
state.startHint = '';
|
||||
}
|
||||
|
||||
export function terminateCurrentSession() {
|
||||
state.session.isAuthorized = false;
|
||||
state.startHint = '';
|
||||
}
|
||||
|
||||
export function refreshRegistrationBalance() {
|
||||
const next = (0.005 + Math.random() * 0.03).toFixed(4);
|
||||
state.registrationPayment.balanceSOL = next;
|
||||
return next;
|
||||
}
|
||||
730
shine-UI/styles/components.css
Normal file
730
shine-UI/styles/components.css
Normal file
@ -0,0 +1,730 @@
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.header-actions,
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 74px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.icon-btn,
|
||||
.text-btn,
|
||||
.primary-btn,
|
||||
.ghost-btn {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--card-soft);
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
|
||||
.icon-btn:hover,
|
||||
.text-btn:hover,
|
||||
.primary-btn:hover,
|
||||
.ghost-btn:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background: linear-gradient(120deg, var(--accent-soft), rgba(82, 120, 240, 0.22));
|
||||
}
|
||||
|
||||
.ghost-btn {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: linear-gradient(180deg, rgba(31, 44, 67, 0.62), rgba(21, 30, 48, 0.9));
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(132, 244, 161, 0.35);
|
||||
color: #d7ffe3;
|
||||
background: rgba(132, 244, 161, 0.09);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge.profile-badge-trigger {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.badge.alt {
|
||||
border-color: rgba(83, 216, 251, 0.35);
|
||||
color: #dff8ff;
|
||||
background: rgba(83, 216, 251, 0.11);
|
||||
}
|
||||
|
||||
.profile-help-modal[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-help-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.profile-help-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(5, 9, 16, 0.72);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.profile-help-dialog {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 24px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.profile-help-text {
|
||||
color: #d8e3ff;
|
||||
line-height: 1.45;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-screen {
|
||||
min-height: calc(100dvh - 48px - env(safe-area-inset-bottom));
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
gap: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
width: 126px;
|
||||
height: 126px;
|
||||
border-radius: 28px;
|
||||
object-fit: cover;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.auth-brand {
|
||||
font-size: 32px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.auth-actions,
|
||||
.auth-footer-actions {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
width: min(100%, 320px);
|
||||
}
|
||||
|
||||
.auth-footer-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.auth-copy {
|
||||
line-height: 1.45;
|
||||
color: #d8e3ff;
|
||||
}
|
||||
|
||||
.auth-status-card {
|
||||
width: min(100%, 320px);
|
||||
color: #d8e3ff;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
color: #b2c2e6;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
min-height: 44px;
|
||||
padding: 0 12px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.select:focus {
|
||||
outline: none;
|
||||
border-color: rgba(83, 216, 251, 0.55);
|
||||
box-shadow: 0 0 0 3px rgba(83, 216, 251, 0.12);
|
||||
}
|
||||
|
||||
.wrap-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-line {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-line.is-available {
|
||||
color: #8ef0a8;
|
||||
}
|
||||
|
||||
.status-line.is-unavailable {
|
||||
color: #ff8d97;
|
||||
}
|
||||
|
||||
.server-check-btn.is-available {
|
||||
border-color: rgba(132, 244, 161, 0.42);
|
||||
background: rgba(132, 244, 161, 0.12);
|
||||
color: #d7ffe3;
|
||||
}
|
||||
|
||||
.server-check-btn.is-unavailable {
|
||||
border-color: rgba(255, 113, 143, 0.42);
|
||||
background: rgba(255, 113, 143, 0.12);
|
||||
color: #ffd7df;
|
||||
}
|
||||
|
||||
.help-fab,
|
||||
.square-btn {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--card-soft);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.help-fab {
|
||||
position: fixed;
|
||||
right: max(20px, calc((100vw - min(100vw, 430px)) / 2 + 20px));
|
||||
bottom: calc(20px + env(safe-area-inset-bottom));
|
||||
z-index: 12;
|
||||
}
|
||||
|
||||
.inline-input-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link-card {
|
||||
display: block;
|
||||
padding: 14px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(83, 216, 251, 0.08);
|
||||
border: 1px solid rgba(83, 216, 251, 0.22);
|
||||
color: #d9f8ff;
|
||||
}
|
||||
|
||||
.qr-card {
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: min(220px, 100%);
|
||||
aspect-ratio: 1;
|
||||
fill: #eff5ff;
|
||||
background: #111723;
|
||||
border-radius: 22px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.camera-shell {
|
||||
position: relative;
|
||||
min-height: 380px;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
background: #09101a;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 380px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.camera-frame {
|
||||
position: absolute;
|
||||
inset: 70px 40px 110px;
|
||||
border: 3px solid rgba(83, 216, 251, 0.85);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 0 0 999px rgba(5, 9, 16, 0.38);
|
||||
}
|
||||
|
||||
.camera-hint,
|
||||
.camera-error {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
text-align: center;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(10, 14, 23, 0.78);
|
||||
}
|
||||
|
||||
.camera-hint {
|
||||
bottom: 18px;
|
||||
}
|
||||
|
||||
.camera-error {
|
||||
top: 18px;
|
||||
color: #ffd7df;
|
||||
}
|
||||
|
||||
.camera-placeholder {
|
||||
width: 100%;
|
||||
min-height: 380px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #c8d6f9;
|
||||
background:
|
||||
radial-gradient(circle at 20% 10%, rgba(83, 216, 251, 0.16), transparent 48%),
|
||||
linear-gradient(180deg, #0a1220, #070d17);
|
||||
}
|
||||
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.key-card {
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 11px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(130deg, #3c4f73, #243352);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 700;
|
||||
color: #e5ebff;
|
||||
}
|
||||
|
||||
.avatar.large {
|
||||
width: 82px;
|
||||
height: 82px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.meta-muted {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.unread {
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--accent);
|
||||
color: #08212a;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: rgba(20, 28, 44, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
padding: 8px 4px;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.toolbar-btn.active {
|
||||
background: rgba(83, 216, 251, 0.14);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.page-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
color: #c5d2f4;
|
||||
background: rgba(17, 24, 39, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.page-label.is-collapsed {
|
||||
width: fit-content;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.page-label-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.page-label-hint {
|
||||
margin-bottom: 3px;
|
||||
color: #8ea2cd;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.page-label-caption {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.page-label-toggle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-label-toggle:hover {
|
||||
border-color: rgba(83, 216, 251, 0.5);
|
||||
background: rgba(83, 216, 251, 0.16);
|
||||
}
|
||||
|
||||
.chat-wrap {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
gap: 10px;
|
||||
min-height: calc(100dvh - 210px);
|
||||
}
|
||||
|
||||
.messages-log {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 76%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.bubble.in {
|
||||
justify-self: start;
|
||||
background: #1f2c46;
|
||||
border-top-left-radius: 6px;
|
||||
}
|
||||
|
||||
.bubble.out {
|
||||
justify-self: end;
|
||||
background: #273f63;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contact-search-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
min-height: 40px;
|
||||
padding: 0 12px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: rgba(83, 216, 251, 0.55);
|
||||
box-shadow: 0 0 0 3px rgba(83, 216, 251, 0.12);
|
||||
}
|
||||
|
||||
.small-btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: inherit;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.session-item:hover {
|
||||
border-color: rgba(83, 216, 251, 0.4);
|
||||
}
|
||||
|
||||
.session-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.session-tab {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--text-muted);
|
||||
min-height: 36px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.session-tab.is-active {
|
||||
color: var(--text);
|
||||
border-color: rgba(83, 216, 251, 0.45);
|
||||
background: rgba(83, 216, 251, 0.15);
|
||||
}
|
||||
|
||||
.session-current-badge {
|
||||
display: inline-flex;
|
||||
margin-top: 8px;
|
||||
padding: 4px 9px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
color: #d7ffe3;
|
||||
border: 1px solid rgba(132, 244, 161, 0.36);
|
||||
background: rgba(132, 244, 161, 0.1);
|
||||
}
|
||||
|
||||
.key-value {
|
||||
font-family: "IBM Plex Mono", "Fira Code", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
word-break: break-all;
|
||||
color: #dce7ff;
|
||||
}
|
||||
|
||||
.qr-demo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.85);
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
#f6fbff 0 8px,
|
||||
#0f1524 8px 16px,
|
||||
#f6fbff 16px 24px,
|
||||
#0f1524 24px 32px,
|
||||
#f6fbff 32px 40px,
|
||||
#0f1524 40px 48px,
|
||||
#f6fbff 48px 56px,
|
||||
#0f1524 56px 64px
|
||||
);
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.modal-shell[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-shell {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 24;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(5, 9, 16, 0.74);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 24px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.network-board {
|
||||
position: relative;
|
||||
height: 290px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--radius-lg);
|
||||
background: radial-gradient(circle at center, rgba(83, 216, 251, 0.08), rgba(255, 255, 255, 0.01));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.network-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.node {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 74px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.node-dot {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
margin: 0 auto 4px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 700;
|
||||
background: #2f4265;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.node.center .node-dot {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: linear-gradient(130deg, #3a5f8e, #3dc4df);
|
||||
color: #061119;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
font-size: 11px;
|
||||
color: #d6e2ff;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 9px;
|
||||
padding: 8px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: rgba(83, 216, 251, 0.16);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.62);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
width: min(100%, 390px);
|
||||
background: #172238;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
}
|
||||
55
shine-UI/styles/layout.css
Normal file
55
shine-UI/styles/layout.css
Normal file
@ -0,0 +1,55 @@
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
width: min(100vw, 430px);
|
||||
height: 100dvh;
|
||||
position: relative;
|
||||
background: linear-gradient(165deg, rgba(16, 22, 36, 0.96), rgba(11, 16, 27, 0.99));
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 118px;
|
||||
overflow-y: auto;
|
||||
padding: 14px 14px 24px;
|
||||
}
|
||||
|
||||
.screen-content.no-app-chrome {
|
||||
bottom: 0;
|
||||
padding-bottom: calc(24px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.page-label-slot {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 99px;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.toolbar-slot {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
background: linear-gradient(180deg, rgba(10, 14, 23, 0) 0%, rgba(10, 14, 23, 0.95) 42%);
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.app-shell {
|
||||
margin: 16px 0;
|
||||
height: calc(100dvh - 32px);
|
||||
border-radius: 24px;
|
||||
}
|
||||
}
|
||||
51
shine-UI/styles/main.css
Normal file
51
shine-UI/styles/main.css
Normal file
@ -0,0 +1,51 @@
|
||||
:root {
|
||||
--bg-0: #080b12;
|
||||
--bg-1: #101624;
|
||||
--bg-2: #171f32;
|
||||
--card: #1a2436;
|
||||
--card-soft: #202d45;
|
||||
--line: #2a3854;
|
||||
--text: #ebf1ff;
|
||||
--text-muted: #99a8cb;
|
||||
--accent: #53d8fb;
|
||||
--accent-soft: rgba(83, 216, 251, 0.17);
|
||||
--danger: #ff718f;
|
||||
--ok: #84f4a1;
|
||||
--radius-lg: 18px;
|
||||
--radius-md: 12px;
|
||||
--radius-sm: 9px;
|
||||
--shadow: 0 20px 40px rgba(0, 0, 0, 0.35);
|
||||
--font-main: "Manrope", "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100%;
|
||||
background: radial-gradient(circle at 22% -10%, #1f355e 0%, var(--bg-0) 45%) fixed;
|
||||
color: var(--text);
|
||||
font-family: var(--font-main);
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user