From 936e53d55b7d693b8b7f71f0b821eb24cf5c6923 Mon Sep 17 00:00:00 2001 From: Aleksandr Neychev Date: Tue, 1 Sep 2026 09:36:37 +0500 Subject: [PATCH] add landing site for the Microsoft Store submission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bilingual (EN/RU) static page in site/, split into index.html, styles.css, boot.js and main.js, with a favicon.svg. Documents placement modes, per-mode look, the Caps Lock shortcut and a privacy policy — the last is meant to double as the Partner Center privacy policy URL. STORE_URL is still a placeholder in both download buttons until the Store listing exists. --- README.RU.md | 43 +++ README.md | 44 +++ site/boot.js | 27 ++ site/favicon.svg | 5 + site/index.html | 676 +++++++++++++++++++++++++++++++++++++++++++++++ site/main.js | 296 +++++++++++++++++++++ site/styles.css | 421 +++++++++++++++++++++++++++++ 7 files changed, 1512 insertions(+) create mode 100644 site/boot.js create mode 100644 site/favicon.svg create mode 100644 site/index.html create mode 100644 site/main.js create mode 100644 site/styles.css diff --git a/README.RU.md b/README.RU.md index e029998..6d6db74 100644 --- a/README.RU.md +++ b/README.RU.md @@ -377,3 +377,46 @@ dotnet build Packaging\Installer\CursorLang.wixproj -p:SuppressValidation=false Три правила остаются подавленными и тогда. MSI исходит из установки на всю машину, а установка в профиль пользователя нарушает правила, которые описывают ровно то, что здесь и задумано. + +## Сайт + +`site/index.html` — страница, на которую ссылается заявка в Store: что делает +приложение и политика конфиденциальности, говорящая, что оно ничего не собирает. +Partner Center не требует политику от приложения, которое не собирает данных, но +страница, сказавшая это вслух, снимает вопрос у проверяющего. + +Всё это — пять файлов в `site`, и ничего больше ниоткуда не подгружается: ни +фреймворка, ни сборки, ни шрифта из сети. + +| Файл | Что внутри | +|---|---| +| `index.html` | Разметка, оба языка сразу | +| `styles.css` | Все правила, включая палитру | +| `boot.js` | Язык и тема прошлого визита — до первого кадра | +| `main.js` | Остальное: переключатели, меню, демонстрация, предпросмотр | +| `favicon.svg` | Та же тёмная плашка с «Aя», что и у иконки приложения | + +`boot.js` вынесен отдельно и мал по одной причине: он должен отработать до +разбора `body`. Он читает выбранное в прошлый раз и ставит это на элемент +`` — без него первый кадр был бы английским и в системной теме, а потом +на глазах исправлялся бы. `main.js` работает по разметке, которая к тому времени +должна существовать, поэтому он отложенный. + +Оба языка живут в разметке одновременно, а переключатель в углу выбирает один. +Каждый переведённый кусок — это `` рядом с ``, и +тот, который не совпадает с языком на элементе ``, скрывается правилом в +две строки. С выключенными скриптами страница всё равно читается — по-английски. + +Выкладка сводится к копированию этих пяти файлов как есть. До неё адреса ждут +две вещи. + +Первая — страница в Store: обе кнопки загрузки сейчас не ссылки, а +``, потому что заглушка в `href` была бы +относительным адресом и всякий, кто по ней пойдёт, получил бы 404. С адресом +каждая возвращается в ``, а примечание рядом уходит. + +Вторая — адрес самого сайта: его ждут три тега, которых в шапке документа пока +нет, — `og:image`, `og:url` и canonical. + +Поддержка — `support@alrakis.kz`: адрес стоит в разделе о данных, под кнопками +загрузки и в подвале. diff --git a/README.md b/README.md index 57adaf4..a5b0072 100644 --- a/README.md +++ b/README.md @@ -370,3 +370,47 @@ dotnet build Packaging\Installer\CursorLang.wixproj -p:SuppressValidation=false Three of its rules stay suppressed even then. MSI assumes an installation for the whole machine, and installing into the user's own profile trips rules that describe exactly what was intended here. + +## The site + +`site/index.html` is the page the Store listing points at: what the application +does, and a privacy policy saying that it collects nothing. Partner Center leaves +the policy optional for an app that gathers no data, but a page saying so out +loud is what takes the question off the reviewer's desk. + +The whole of it is five files in `site`, and nothing is fetched from anywhere +else — no framework, no build step, no font from a network: + +| File | What is in it | +|---|---| +| `index.html` | The markup, both languages at once | +| `styles.css` | Every rule, the palette included | +| `boot.js` | The language and the theme of an earlier visit, before the first frame | +| `main.js` | The rest: the switches, the menu, the demo, the preview | +| `favicon.svg` | The same dark plate with "Aя" as the application icon | + +`boot.js` is small and separate for one reason: it has to run before the body is +parsed. It reads what was chosen last time and puts it on the `` element, +and without it the first frame would be English in the system theme and correct +itself a moment later, in plain sight. `main.js` works on markup that has to +exist by then, so it is deferred instead. + +Both languages live in the markup at once, and a switch in the corner picks one. +Every translated piece is a `` beside a ``, and +whichever does not match the language on the `` element is hidden by a rule +of two lines. With the scripts switched off the page still reads, in English. + +Publishing means copying the five files as they are. Before that, two things are +still waiting for an address. + +The Store listing is the first: the two download buttons are +`` rather than links, because a +placeholder in `href` would be a relative address and anything following it would +land on a 404. The address turns each of them back into +`` and takes the note beside it away. + +The address of the site itself is the second, wanted by three tags the head does +not carry yet: `og:image`, `og:url` and the canonical link. + +Support goes to `support@alrakis.kz`: it stands in the privacy section, under the +download buttons and in the footer. diff --git a/site/boot.js b/site/boot.js new file mode 100644 index 0000000..b233146 --- /dev/null +++ b/site/boot.js @@ -0,0 +1,27 @@ +(function () { + "use strict"; + + var root = document.documentElement; + var lang = null; + var theme = null; + + try { + lang = localStorage.getItem("cursorlang.lang"); + theme = localStorage.getItem("cursorlang.theme"); + } catch (error) { + // Storage refused: what the markup says stands + } + + if (!lang && (navigator.language || "").toLowerCase().indexOf("ru") === 0) { + lang = "ru"; + } + + if (lang === "ru" || lang === "en") { + root.setAttribute("data-lang", lang); + root.setAttribute("lang", lang); + } + + if (theme === "dark" || theme === "light") { + root.setAttribute("data-theme", theme); + } +}()); diff --git a/site/favicon.svg b/site/favicon.svg new file mode 100644 index 0000000..6391c5f --- /dev/null +++ b/site/favicon.svg @@ -0,0 +1,5 @@ + + + + diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..4515c52 --- /dev/null +++ b/site/index.html @@ -0,0 +1,676 @@ + + + + + +CursorLang — the keyboard layout at your cursor + + + + + + + + + + + + + + + + + + +
+
+
+ + CursorLang +
+ + + +
+
+ + +
+ + + +
+
+
+ + + +
+ + +
+
+

+ Your layout — right where you type. + Раскладка — там, где вы печатаете. +

+ +

+ CursorLang is a lightweight Windows utility that lives in your system tray. Whenever you + switch your keyboard layout, a brief popup indicates the new language next to your text caret, + mouse pointer, or a fixed screen position. Half a second later, it fades away. + Simple and unobtrusive. + CursorLang — это легковесная утилита для Windows, которая находится в системном трее. + При смене раскладки клавиатуры рядом с курсором, указателем мыши или фиксированной точкой на экране + появляется короткое всплывающее окно, указывающее на новый язык. Через полсекунды оно исчезает. + Просто и ненавязчиво. +

+ +
+ + + Download from Microsoft Store + Скачать из Microsoft Store + + + Pending Microsoft Store certification — the download link is coming soon. + Ожидается сертификация в Microsoft Store — ссылка для скачивания появится в ближайшее время. + +
+ +
+ FreeБесплатно + Windows 10 1809 and later, 64-bit + Windows 10 1809 и новее, 64 бита + Uses under 9 MB of memoryИспользует менее 9 МБ памяти + No internet connection required, zero telemetryБез доступа в сеть, без телеметрии. +
+ +
+
+ + +
+ + +
+ +
+
+ + +
+
+
+
+ + +
+
+

PositionПоложение

+

+ Three placement modes + Три режима размещения подсказки +

+

+ Choose your preferred placement in the settings. Each mode maintains its own independent + position, offset, and appearance. + Выберите желаемое положение в настройках. Каждый режим сохраняет свое независимое положение, + смещение и внешний вид. +

+ +
+
+
+ + + + + + RU + +
+

Near the mouse pointerРядом с указателем мыши

+

+ Position the indicator at any of the six sides or corners with a custom offset. + The popup dynamically follows your mouse pointer. + Разместите индикатор на любой из шести сторон или углов с заданным смещением. + Всплывающее окно динамически следует за указателем мыши. +

+
+ +
+
+ + + + + + RU + + +
+

Near the text caretРядом с текстовым курсором

+

+ Displays to the left or right of the caret, aligned with your current text line. + Отображается слева или справа от курсора, выравниваясь по текущей текстовой строке. +

+
+ +
+
+ + + + + + + + + + RU + +
+

A fixed point on the screenВ выбранной точке экрана

+

+ Choose from seven preset positions on your active monitor, complete with + customizable margins. The indicator stays pinned to a consistent spot, so you always + know where to look. + Выберите одно из семи предустановленных положений на активном мониторе с + настраиваемыми полями. Индикатор остается зафиксированным в одном и том же месте, + поэтому вы всегда будете знать, куда смотреть. +

+
+
+ +

+ Not every application exposes its text caret. Standard Win32 fields respond + instantly, while Chromium and Electron apps rely on accessibility APIs. However, applications + rendering text onto custom canvases—like certain tool windows in a JetBrains IDE—may not report + caret coordinates at all. + In these fallback scenarios, the indicator automatically defaults to your configured fixed point + placement, using that mode's appearance. It avoids jumping to the mouse pointer, as your eyes are + focused on typing and the mouse might be left anywhere on the screen. + Не всякое приложение сообщает, где его курсор. Стандартные поля Win32 реагируют + мгновенно, в то время как приложения Chromium и Electron используют API специальных возможностей. + Однако приложения, отображающие текст на пользовательских холстах — например, некоторые окна + инструментов в IDE JetBrains — могут вообще не сообщать координаты курсора. + В таких резервных сценариях индикатор автоматически переключается на заданное вами фиксированное + положение, используя внешний вид этого режима. Это позволяет избежать переключения на указатель мыши, + поскольку ваши глаза сосредоточены на наборе текста, а курсор может находиться в любой точке экрана. +

+
+
+ + +
+
+

AppearanceОформление

+

+ Each mode has its own unique look + У каждого режима своё оформление +

+

+ The caret popup lives right inside the text you're reading, keeping it compact and + unobtrusive. The fixed-screen popup, on the other hand, is meant to be larger and more prominent. + Therefore, offset, font size, opacity, and colors are saved independently for each mode — changing + one leaves the rest untouched. + Всплывающее окно с курсором располагается прямо внутри читаемого текста, оставаясь + компактным и ненавязчивым. Всплывающее окно с фиксированным экраном, напротив, предназначено для + большего размера и большей заметности. Поэтому смещение, размер шрифта, прозрачность и цвета + сохраняются независимо для каждого режима — изменение одного параметра не влияет на остальные. +

+ +
+
+

Layout popupПодсказка с раскладкой

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + Side of the caretСторона от курсора + +
+ + +
+
+ +
+ + +
+ + +
+ +
+
+ Sent you a Отправил тебе + pull requestRU +
+
+
+ +

+ The popup only answers a question you explicitly asked. It appears when you switch the + layout yourself. When you switch to another window, Windows may change the layout automatically in + the background, but the popup stays silent. After all, you didn't ask. + Всплывающее окно отвечает только на заданный вами вопрос. Оно появляется, когда вы + сами меняете расположение окон. При переключении на другое окно Windows может автоматически изменить + расположение окон в фоновом режиме, но всплывающее окно остается незамеченным. В конце концов, вы же + не спрашивали. +

+
+
+ + +
+
+

Caps Lock

+

+ A key that could be doing more + Клавиша, которая могла бы работать +

+

+ When enabled in settings, a quick tap of Caps Lock switches your keyboard layout instead + of toggling letter casing. Hold it longer than your custom threshold, and nothing changes: the popup + simply displays your current layout — a quick look, without switching. + Если эта функция включена в настройках, быстрое нажатие клавиши Caps Lock переключает + раскладку клавиатуры вместо изменения регистра букв. Если удерживать клавишу дольше заданного вами + порога, ничего не изменится: во всплывающем окне просто отобразится текущая раскладка — быстрый + взгляд без переключения. +

+ +
    +
  • + + + Disabled by default. This shortcut is disabled by default in the app, so + the Caps Lock key continues to function as usual. + По умолчанию выключено. В приложении Caps Lock отключен по умолчанию, + поэтому он продолжает работать так, как работал всегда. + +
  • +
  • + + + No administrator rights required. Built as a standard user-level + application, it runs under your active user account and simply doesn't need higher + privileges. + Права администратора не запрашиваются. Приложение разработано как + стандартное приложение для пользователей и работает под вашей активной учетной записью, + поэтому более высокие привилегии не требуются. + +
  • +
  • + + + Only one key is monitored. The low-level hook watches strictly for + Caps Lock. No other keystrokes are recorded, stored, or transmitted anywhere — and since + the app is offline, they physically can't be. + Отслеживается одна клавиша. Низкоуровневый перехватчик следит исключительно + за нажатием клавиши Caps Lock. Никакие другие нажатия клавиш не записываются, не сохраняются и + не передаются никуда — а поскольку приложение работает в автономном режиме, это + физически невозможно. + +
  • +
  • + + + Doesn't interfere with elevated apps. Windows blocks ordinary apps from + injecting shortcuts into windows running as administrator (like Task Manager, Registry Editor, + or UAC prompts). + Не работает в окнах, запущенных от администратора. + Windows блокирует возможность передачу нажатия клавиш в окнах, запущенные от имени + администратора, и служебных приложениях (например, диспетчером задач, редактором реестра + или запросами UAC). + +
  • + +
+
+
+ + +
+
+

LightweightЛегковесность

+

+ Small enough to forget about + Настолько маленькое, что о нём легко забыть +

+

+ CursorLang is split into two executables. The one that lives in your tray all day is + built without a UI rendering stack — a tooltip that Win32 draws in eight megabytes has no business + costing a hundred. Anything heavier is strictly kept out of the background process. The second + executable handles the settings: it loads only when you open the window and completely unloads + from memory the moment you close it. + CursorLang разделен на два исполняемых файла. Тот, что находится в системном трее весь + день, создан без стека рендеринга пользовательского интерфейса — всплывающая подсказка, которую + Win32 отрисовывает за восемь мегабайт, не должна стоить сто. Все, что потребляет больше ресурсов, + строго исключено из фонового процесса. Второй исполняемый файл обрабатывает настройки: он загружается + только при открытии окна и полностью выгружается из памяти в момент его закрытия. +

+ +
    +
  • + + + Under 9 MB in the tray. The settings window is a separate + process: it starts when you ask for it and gives its memory back when you + close it. + Меньше 9 МБ в трее. Окно настроек — отдельный процесс: он + запускается, когда его просят, и возвращает память, когда окно + закрывают. + +
  • +
  • + + + Starts with Windows if you ask it to — and Windows always keeps the final + word. Turn off startup in Settings → Apps → Startup, and the application respects your choice + instead of quietly re-enabling itself. + Запускается вместе с Windows, если попросить — и последнее слово всегда + остается за Windows. Отключите автозагрузку в Параметры → Приложения → Автозагрузка, и + приложение будет уважать ваш выбор, вместо того чтобы незаметно включаться снова. + +
  • +
  • + + + Light and dark themes, following Windows or pinned to one + of them. Available in English and Russian. + Светлая и тёмная тема, вслед за Windows или закреплённая. + Интерфейс — русский и английский. + +
  • +
  • + + + Nothing else to install. The application carries its own + copy of .NET, and the Microsoft Store keeps it up to date. + Ничего дополнительно устанавливать не нужно. Приложение несёт с собой + свою копию .NET, a обновляет его Microsoft Store. + +
  • +
+
+
+ + +
+
+

Privacy policyПолитика конфиденциальности

+

+ CursorLang collects nothing + CursorLang ничего не собирает +

+

+ It really is that simple. The points below just provide the technical details. + Всё действительно так просто. Приведённые ниже пункты содержат лишь технические подробности. +

+ +
+
    +
  • + + + No personal data is collected, stored remotely, or shared with anyone. + There are no accounts, no ads, and no analytics. + Персональные данные не собираются, не хранятся удаленно и не + передаются никому. Нет учетных записей, рекламы и аналитики. + +
  • +
  • + + + The application makes no network requests. It contains no networking + code, telemetry, or internal update checks — the Microsoft Store handles updates + automatically. + Приложение не обращается в сеть. Не содержит сетевого кода, телеметрии + или внутренних проверок обновлений — обновления обрабатываются Microsoft Store + автоматически. + +
  • +
  • + + + Settings are stored locally in a single file. The Store version keeps + this in the app's local data folder, which Windows removes entirely upon uninstallation — + leaving no trace behind. + Настройки хранятся локально в одном файле. В версии из магазина + приложений они хранятся в локальной папке данных приложения, которую Windows полностью + удаляет при деинсталляции, не оставляя никаких следов. + +
  • +
  • + + + The optional Caps Lock shortcut uses a low-level keyboard hook. + It monitors strictly the Caps Lock key and nothing else. Your keystrokes are never + recorded or transmitted, and the feature is disabled by default. + Горячая клавиша Caps Lock использует низкоуровневый + перехват клавиатуры. Она отслеживает исключительно нажатие клавиши Caps Lock и + ничего больше. Ваши нажатия клавиш никогда не записываются и не передаются, и эта + функция отсутствует по умолчанию. + +
  • +
  • + + + To position the popup, the app asks Windows for the active layout and + caret location. This data never leaves the app's memory, and your actual typed + text is never read or accessed. + Для позиционирования всплывающего окна приложение запрашивает у Windows + активную компоновку и положение курсора. Эти данные никогда не покидают память + приложения, и фактический введенный вами текст никогда не считывается и не используется. + +
  • +
+
+
+
+ + +
+
+

DownloadЗагрузка

+

+ One click from the Microsoft Store + Устанавливается из Microsoft Store в один клик +

+

+ CursorLang is distributed through the Microsoft Store, which installs it and keeps it + updated on its own — nothing else is asked of you in between. It runs on Windows 10 version 1809 + (build 17763) or later, 64-bit. + CursorLang распространяется через Microsoft Store: он же его устанавливает и сам держит + обновлённым — больше от вас ничего не требуется. Работает на Windows 10 версии 1809 + (сборка 17763) или новее, 64 бита. +

+ +
+ + + Download from Microsoft Store + Скачать из Microsoft Store + + + Pending Microsoft Store certification — the download link is coming soon. + Ожидается сертификация в Microsoft Store — ссылка для скачивания появится в ближайшее время. + +
+ +

+ Something not working, or a question about the application? Write to + support@alrakis.kz and name the version in + the letter — it is in the title of the settings window. + Что-то не работает или есть вопрос о приложении? Напишите на + support@alrakis.kz и назовите в письме + версию — она есть в заголовке окна настроек. +

+
+
+ +
+ + + + + diff --git a/site/main.js b/site/main.js new file mode 100644 index 0000000..374de8f --- /dev/null +++ b/site/main.js @@ -0,0 +1,296 @@ +(function () { + "use strict"; + + /* ------------------------------------------------------ small helpers */ + + var root = document.documentElement; + + function remember(key, value) { + try { localStorage.setItem(key, value); } catch (error) { /* private mode: never mind */ } + } + + /* ------------------------------------------------------- the language */ + + var description = document.querySelector('meta[name="description"]'); + var TITLE_EN = document.title; + var TITLE_RU = "CursorLang — раскладка клавиатуры у курсора"; + var DESCRIPTION_EN = description.getAttribute("content"); + var DESCRIPTION_RU = description.getAttribute("data-content-ru"); + + function applyLanguage(lang) { + if (lang !== "ru") { lang = "en"; } + + root.setAttribute("data-lang", lang); + root.setAttribute("lang", lang); + document.title = lang === "ru" ? TITLE_RU : TITLE_EN; + description.setAttribute("content", lang === "ru" ? DESCRIPTION_RU : DESCRIPTION_EN); + + var labelled = document.querySelectorAll("[data-label-en]"); + for (var i = 0; i < labelled.length; i++) { + labelled[i].setAttribute("aria-label", labelled[i].getAttribute("data-label-" + lang)); + } + + var buttons = document.querySelectorAll("[data-set-lang]"); + for (var j = 0; j < buttons.length; j++) { + buttons[j].setAttribute("aria-pressed", + buttons[j].getAttribute("data-set-lang") === lang ? "true" : "false"); + } + + applyLook(); + } + + document.addEventListener("click", function (event) { + var button = event.target.closest("[data-set-lang]"); + if (!button) { return; } + + var lang = button.getAttribute("data-set-lang"); + applyLanguage(lang); + remember("cursorlang.lang", lang); + }); + + /* ---------------------------------------------------------- the theme */ + + document.getElementById("themeBtn").addEventListener("click", function () { + var pinned = root.getAttribute("data-theme"); + var dark = pinned + ? pinned === "dark" + : window.matchMedia("(prefers-color-scheme: dark)").matches; + + var next = dark ? "light" : "dark"; + root.setAttribute("data-theme", next); + remember("cursorlang.theme", next); + }); + + /* ----------------------------------------------------- the menu button */ + + var header = document.querySelector("header"); + var menuBtn = document.getElementById("menuBtn"); + + function setMenu(open) { + header.setAttribute("data-menu", open ? "open" : "closed"); + menuBtn.setAttribute("aria-expanded", open ? "true" : "false"); + } + + menuBtn.addEventListener("click", function () { + setMenu(header.getAttribute("data-menu") !== "open"); + }); + + document.addEventListener("click", function (event) { + if (header.getAttribute("data-menu") !== "open") { return; } + + if (event.target.closest("nav.links a") || !event.target.closest("header")) { + setMenu(false); + } + }); + + document.addEventListener("keydown", function (event) { + if (event.key !== "Escape" || header.getAttribute("data-menu") !== "open") { return; } + + setMenu(false); + menuBtn.focus(); + }); + + window.matchMedia("(min-width: 901px)").addEventListener("change", function (event) { + if (event.matches) { setMenu(false); } + }); + + // Back to top, without leaving a fragment behind in the address bar + document.addEventListener("click", function (event) { + var back = event.target.closest('a[href="#top"]'); + if (!back) { return; } + + event.preventDefault(); + window.scrollTo({ top: 0 }); + history.replaceState(null, "", location.pathname + location.search); + }); + + /* ------------------------------------------- the border under the bar */ + + var onScroll = function () { + header.classList.toggle("scrolled", window.scrollY > 4); + }; + window.addEventListener("scroll", onScroll, { passive: true }); + onScroll(); + + /* ------------------------------------------------------- the demo above */ + + var demoText = document.getElementById("demoText"); + var demoChip = document.getElementById("demoChip"); + var indicator = document.getElementById("demoIndicator"); + + // A sentence typed in two layouts, the way one is + var SCRIPT = [ + { layout: "EN", indicator: "ENG", text: "Deploy is green, " }, + { layout: "RU", indicator: "РУС", text: "можно мержить" } + ]; + + var stillMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); + var timers = []; + var typing = false; + var onScreen = true; + + function later(fn, delay) { + timers.push(setTimeout(fn, delay)); + } + + function stopTyping() { + for (var i = 0; i < timers.length; i++) { clearTimeout(timers[i]); } + timers = []; + typing = false; + } + + function showStill() { + demoText.textContent = SCRIPT[0].text + SCRIPT[1].text; + demoChip.textContent = SCRIPT[1].layout; + demoChip.classList.remove("out"); + demoChip.hidden = false; + indicator.textContent = SCRIPT[1].indicator; + } + + function showChip(text) { + demoChip.textContent = text; + demoChip.classList.remove("out"); + demoChip.hidden = false; + + later(function () { demoChip.classList.add("out"); }, 1100); + later(function () { demoChip.hidden = true; }, 1260); + } + + function typeSegment(index, done) { + if (index >= SCRIPT.length) { done(); return; } + + var segment = SCRIPT[index]; + indicator.textContent = segment.indicator; + showChip(segment.layout); + + var position = 0; + var step = function () { + if (position >= segment.text.length) { + later(function () { typeSegment(index + 1, done); }, 420); + return; + } + + demoText.textContent += segment.text.charAt(position); + position++; + later(step, 52 + Math.random() * 58); + }; + + later(step, 620); + } + + function run() { + demoText.textContent = ""; + demoChip.hidden = true; + + typeSegment(0, function () { + later(run, 2600); + }); + } + + function renderDemo() { + if (stillMotion.matches) { + stopTyping(); + showStill(); + return; + } + + if (document.hidden || !onScreen) { + stopTyping(); + return; + } + + if (typing) { return; } + + typing = true; + run(); + } + + stillMotion.addEventListener("change", renderDemo); + document.addEventListener("visibilitychange", renderDemo); + + new IntersectionObserver(function (entries) { + onScreen = entries[0].isIntersecting; + renderDemo(); + }).observe(document.querySelector(".stage")); + + renderDemo(); + + /* ------------------------------------------------ the preview below */ + + var chip = document.getElementById("previewChip"); + var offset = document.getElementById("offset"); + var fontSize = document.getElementById("fontSize"); + var opacity = document.getElementById("opacity"); + var bgColor = document.getElementById("bgColor"); + var fgColor = document.getElementById("fgColor"); + var duration = document.getElementById("duration"); + + var offsetVal = document.getElementById("offsetVal"); + var fontSizeVal = document.getElementById("fontSizeVal"); + var opacityVal = document.getElementById("opacityVal"); + var durationVal = document.getElementById("durationVal"); + + function applyLook() { + var onTheLeft = chip.classList.contains("left"); + + chip.style.fontSize = fontSize.value + "px"; + chip.style.opacity = (opacity.value / 100).toFixed(2); + chip.style.background = bgColor.value; + chip.style.color = fgColor.value; + chip.style.marginLeft = onTheLeft ? "0px" : offset.value + "px"; + chip.style.marginRight = onTheLeft ? offset.value + "px" : "0px"; + + offsetVal.textContent = offset.value; + fontSizeVal.textContent = fontSize.value; + opacityVal.textContent = opacity.value + "%"; + durationVal.textContent = duration.value + (root.getAttribute("data-lang") === "ru" ? " мс" : " ms"); + } + + var inputs = [offset, fontSize, opacity, bgColor, fgColor, duration]; + for (var k = 0; k < inputs.length; k++) { + inputs[k].addEventListener("input", applyLook); + } + + document.getElementById("lookForm").addEventListener("submit", function (event) { + event.preventDefault(); + }); + + document.getElementById("side").addEventListener("click", function (event) { + var button = event.target.closest("[data-side]"); + if (!button) { return; } + + var buttons = this.querySelectorAll("[data-side]"); + for (var i = 0; i < buttons.length; i++) { + buttons[i].setAttribute("aria-pressed", buttons[i] === button ? "true" : "false"); + } + + var left = button.getAttribute("data-side") === "left"; + chip.classList.toggle("left", left); + chip.classList.toggle("right", !left); + applyLook(); + }); + + var previewTimers = []; + document.getElementById("previewBtn").addEventListener("click", function () { + for (var i = 0; i < previewTimers.length; i++) { clearTimeout(previewTimers[i]); } + previewTimers = []; + + chip.classList.add("out"); + + previewTimers.push(setTimeout(function () { + chip.classList.remove("out"); + + previewTimers.push(setTimeout(function () { + chip.classList.add("out"); + + previewTimers.push(setTimeout(function () { + chip.classList.remove("out"); + }, 900)); + }, Number(duration.value))); + }, 420)); + }); + + /* ------------------------------------------------------------ the start */ + + applyLanguage(root.getAttribute("data-lang")); +}()); diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..7f65a7b --- /dev/null +++ b/site/styles.css @@ -0,0 +1,421 @@ +/* ---------------------------------------------------------------- palette */ + +:root, +:root[data-theme="light"] { + color-scheme: light dark; + + --bg: #fbfbfd; + --surface: #ffffff; + --surface-2: #f3f3f7; + --line: #e5e5ec; + --text: #15151a; + --muted: #5c5c6a; + --accent: #0060b0; + --accent-bg: rgba(0, 96, 176, .09); + --shadow: 0 1px 2px rgba(18, 18, 28, .05), 0 14px 40px rgba(18, 18, 28, .07); + --shadow-sm: 0 1px 2px rgba(18, 18, 28, .06), 0 4px 12px rgba(18, 18, 28, .05); + + --chip-bg: #202020; + --chip-fg: #ffffff; + + --radius: 14px; + --maxw: 1060px; + + --bar-h: 62px; + /* The air above and below a section. The anchor arithmetic subtracts it, + so the two have to come from one place */ + --section-pad: clamp(56px, 8vw, 96px); + --font: "Segoe UI Variable Display", "Segoe UI", system-ui, -apple-system, + "Helvetica Neue", Arial, sans-serif; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg: #0d0d10; + --surface: #151519; + --surface-2: #1c1c22; + --line: #272730; + --text: #f1f1f4; + --muted: #9d9dab; + --accent: #62c8ff; + --accent-bg: rgba(98, 200, 255, .12); + --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 18px 44px rgba(0, 0, 0, .45); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, .35), 0 6px 16px rgba(0, 0, 0, .35); + } +} + +:root[data-theme="dark"] { + --bg: #0d0d10; + --surface: #151519; + --surface-2: #1c1c22; + --line: #272730; + --text: #f1f1f4; + --muted: #9d9dab; + --accent: #62c8ff; + --accent-bg: rgba(98, 200, 255, .12); + --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 18px 44px rgba(0, 0, 0, .45); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, .35), 0 6px 16px rgba(0, 0, 0, .35); +} + +/* ------------------------------------------------------- the two languages */ + +html[data-lang="en"] [lang="ru"], +html[data-lang="ru"] [lang="en"] { display: none !important; } + +/* ------------------------------------------------------------------- base */ + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font); + font-size: 17px; + line-height: 1.62; + -webkit-font-smoothing: antialiased; + overflow-x: clip; +} + +h1, h2, h3 { line-height: 1.16; letter-spacing: -.021em; margin: 0; font-weight: 700; } +h1 { font-size: clamp(2.1rem, 5.4vw, 3.35rem); } +h2 { font-size: clamp(2rem, 4vw, 2.75rem); } +h3 { font-size: 1.2rem; letter-spacing: -.014em; } +p { margin: 0; } + +a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 3px; } + +:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; } + +.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: 24px; } + +.muted { color: var(--muted); } + +.skip { + position: absolute; left: -9999px; top: 0; + background: var(--surface); color: var(--text); + padding: 10px 16px; border-radius: 8px; z-index: 10; +} +.skip:focus { left: 12px; top: 12px; } + +/* ----------------------------------------------------------------- header */ + +header { + position: sticky; top: 0; z-index: 5; + background: var(--bg); + background: color-mix(in srgb, var(--bg) 86%, transparent); + -webkit-backdrop-filter: saturate(160%) blur(12px); + backdrop-filter: saturate(160%) blur(12px); + border-bottom: 1px solid transparent; + transition: border-color .2s ease; +} +header.scrolled { border-bottom-color: var(--line); } + +.bar { display: flex; align-items: center; gap: 18px; height: var(--bar-h); } + +.brand { display: flex; align-items: center; gap: 11px; font-weight: 650; letter-spacing: -.015em; } +.brand svg { display: block; border-radius: 7px; } + +nav.links { display: flex; gap: 22px; margin-left: auto; font-size: .94rem; } +nav.links a { color: var(--muted); text-decoration: none; } +nav.links a:hover { color: var(--text); } + +.controls { display: flex; align-items: center; gap: 8px; margin-left: auto; } +nav.links + .controls { margin-left: 0; } + +.seg { + display: inline-flex; padding: 2px; gap: 2px; + background: var(--surface-2); border: 1px solid var(--line); border-radius: 9px; +} +.seg button { + font: inherit; font-size: .82rem; font-weight: 600; letter-spacing: .02em; + padding: 4px 10px; border: 0; border-radius: 7px; + background: transparent; color: var(--muted); cursor: pointer; +} +.seg button[aria-pressed="true"] { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); } + +.icon-btn { + display: grid; place-items: center; width: 34px; height: 34px; + border: 1px solid var(--line); border-radius: 9px; + background: var(--surface-2); color: var(--muted); cursor: pointer; +} +.icon-btn:hover { color: var(--text); } +.icon-btn svg { width: 17px; height: 17px; } + +#menuBtn { display: none; } +#menuBtn .cross { display: none; } +header[data-menu="open"] #menuBtn .bars { display: none; } +header[data-menu="open"] #menuBtn .cross { display: block; } + +.icon-btn .sun { display: none; } +.icon-btn .moon { display: block; } +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .icon-btn .sun { display: block; } + :root:not([data-theme="light"]) .icon-btn .moon { display: none; } +} +:root[data-theme="dark"] .icon-btn .sun { display: block; } +:root[data-theme="dark"] .icon-btn .moon { display: none; } + +.scrim { + display: none; + position: fixed; top: var(--bar-h); left: 0; right: 0; bottom: 0; + background: rgba(0, 0, 0, .5); z-index: 4; +} + +/* --------------------------------------------------------------- sections */ + +section { padding-block: var(--section-pad); } +section + section { border-top: 1px solid var(--line); } + +/* An anchor should show as much of the section as the window holds. Landing on + the top of its box would spend the whole top padding on empty space under + the bar and push the tail off the screen, so the padding is taken out of + the offset and the heading arrives a small gap below the bar */ +section { scroll-margin-top: calc(var(--bar-h) + 16px - var(--section-pad)); } + +.lede { color: var(--muted); font-size: 1.06rem; margin-top: 14px; } + +/* The line under the name of a section: what the section says, in one sentence. + Larger than the text it stands over, smaller than the name above it */ +.deck { + margin-top: 12px; + font-size: clamp(1.3rem, 2.3vw, 1.65rem); + font-weight: 600; line-height: 1.28; letter-spacing: -.016em; +} + +/* ------------------------------------------------------------------- hero */ + +.hero { padding-top: clamp(40px, 6vw, 72px); padding-bottom: clamp(48px, 6vw, 80px); } +/* No width cap: both headings are written to stand in one line, and they hold + it down to a window of about 600 px. Narrower than that they wrap, and + `balance` splits them evenly rather than leaving one word below */ +.hero h1 { text-wrap: balance; } +.hero .lede { font-size: 1.14rem; } + +.cta { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; margin-top: 30px; } + +.btn { + display: inline-flex; align-items: center; gap: 10px; + font: inherit; font-size: .97rem; font-weight: 600; + padding: 12px 20px; border-radius: 11px; border: 1px solid transparent; + text-decoration: none; cursor: pointer; + background: var(--text); color: var(--bg); +} +.btn:hover { opacity: .88; } +.btn svg { width: 18px; height: 18px; } +.btn.disabled { + background: var(--surface-2); color: var(--muted); + border-color: var(--line); cursor: default; pointer-events: none; +} +.btn.ghost { background: transparent; color: var(--text); border-color: var(--line); } +.btn.ghost:hover { background: var(--surface-2); opacity: 1; } +.btn.block { width: 100%; justify-content: center; } + +.cta-note { font-size: .9rem; color: var(--muted); } + +.facts { + display: flex; flex-wrap: wrap; gap: 8px 20px; + margin-top: 26px; font-size: .89rem; color: var(--muted); +} +.facts span { display: inline-flex; align-items: center; gap: 8px; } +.facts span::before { + content: ""; width: 4px; height: 4px; border-radius: 50%; + background: currentColor; opacity: .5; +} + +/* ------------------------------------------------------- the typing demo */ + +.stage { margin: clamp(36px, 5vw, 58px) 0 0; } + +.window { + background: var(--surface); border: 1px solid var(--line); + border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; +} +.titlebar { + display: flex; align-items: center; gap: 10px; + padding: 9px 14px; border-bottom: 1px solid var(--line); + background: var(--surface-2); font-size: .8rem; color: var(--muted); +} +.titlebar .dots { display: flex; gap: 6px; } +.titlebar .dots i { width: 9px; height: 9px; border-radius: 50%; background: var(--line); } +.titlebar .name { font-weight: 600; } + +.screen { position: relative; padding: 30px clamp(18px, 4vw, 40px) 0; min-height: 210px; } + +.msg { + max-width: 60ch; margin-bottom: 14px; padding: 10px 14px; + border-radius: 12px; background: var(--surface-2); + font-size: 1rem; color: var(--muted); +} + +.field { + display: flex; align-items: center; min-height: 46px; + margin-bottom: 26px; padding: 8px 14px; + border: 1px solid var(--line); border-radius: 11px; background: var(--surface); + font-size: 1rem; +} +.typed { white-space: pre-wrap; } + +.caret { + position: relative; display: inline-block; + width: 1.5px; height: 1.25em; margin-left: 1px; + background: var(--text); vertical-align: text-bottom; + animation: blink 1.06s steps(1, end) infinite; +} +@keyframes blink { 0%, 55% { opacity: 1; } 56%, 100% { opacity: 0; } } + +.chip { + position: absolute; top: 50%; transform: translateY(-50%); + display: inline-block; padding: .18em .5em; + border-radius: .28em; background: var(--chip-bg); color: var(--chip-fg); + font-family: var(--font); font-size: 20px; font-weight: 600; line-height: 1.25; + letter-spacing: .01em; white-space: nowrap; opacity: .9; + pointer-events: none; user-select: none; + box-shadow: 0 2px 12px rgba(0, 0, 0, .3); +} +.chip[hidden] { display: none; } +.chip.right { left: 100%; margin-left: 10px; } +.chip.left { right: 100%; margin-right: 10px; } +.chip.fade { transition: opacity .12s linear; } +.chip.fade.out { opacity: 0 !important; } + +.taskbar { + display: flex; align-items: center; justify-content: flex-end; gap: 16px; + margin-top: 18px; padding: 7px clamp(18px, 4vw, 40px); + border-top: 1px solid var(--line); background: var(--surface-2); + font-size: .74rem; color: var(--muted); +} +.taskbar .ind { padding: 2px 6px; border-radius: 4px; background: var(--bg); font-weight: 600; } + +/* ------------------------------------------------------------------ cards */ + +.cards { + display: grid; gap: 18px; margin-top: 40px; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} +.card { + padding: 22px; border: 1px solid var(--line); border-radius: var(--radius); + background: var(--surface); box-shadow: var(--shadow-sm); +} +.card h3 { margin-bottom: 8px; } +.card p { font-size: .96rem; color: var(--muted); } +.card figure { margin: 0 0 18px; } +.card svg { display: block; width: 100%; height: auto; } + +.sketch .frame { fill: var(--surface-2); stroke: var(--line); } +.sketch .line { stroke: var(--line); stroke-linecap: round; } +.sketch .ink { fill: var(--muted); } +.sketch .mark { fill: var(--text); } +.sketch .plate { fill: var(--accent); } +.sketch .glyph { fill: var(--bg); font: 700 9px var(--font); } +.sketch .ghost { fill: var(--line); } + +.aside { + margin-top: 20px; padding: 18px 20px; + border: 1px solid var(--line); border-left: 3px solid var(--accent); + border-radius: 10px; background: var(--surface); + font-size: .95rem; color: var(--muted); +} +.aside b { color: var(--text); font-weight: 600; } + +/* ----------------------------------------------------- the live preview */ + +.playground { + display: grid; gap: 24px; margin-top: 26px; + grid-template-columns: minmax(0, 300px) minmax(0, 1fr); + align-items: stretch; +} +@media (max-width: 760px) { .playground { grid-template-columns: 1fr; } } + +.panel { + padding: 20px; border: 1px solid var(--line); border-radius: var(--radius); + background: var(--surface); box-shadow: var(--shadow-sm); +} +.panel h3 { font-size: .82rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); margin-bottom: 16px; } + +.row { display: grid; gap: 6px; margin-bottom: 13px; } +.row:last-child, .row.tight { margin-bottom: 0; } +.row > label, +.row > .lbl { font-size: .89rem; display: flex; justify-content: space-between; gap: 12px; } +.row .val { color: var(--muted); font-variant-numeric: tabular-nums; font-size: .85rem; } + +input[type="range"] { width: 100%; accent-color: var(--accent); } +input[type="color"] { + width: 100%; height: 32px; padding: 2px; + border: 1px solid var(--line); border-radius: 8px; + background: var(--surface-2); cursor: pointer; +} +.pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } + +.preview-stage { + position: relative; display: grid; place-items: center; + min-height: 260px; padding: 28px; + border: 1px solid var(--line); border-radius: var(--radius); + background: + radial-gradient(circle at 1px 1px, var(--line) 1px, transparent 0) 0 0 / 22px 22px, + var(--surface); +} +.preview-line { + display: inline-block; + padding: 12px 16px; border-radius: 11px; + background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow-sm); + font-size: 1rem; max-width: 100%; +} + +/* --------------------------------------------------------------- bullets */ + +.bullets { display: grid; gap: 14px; margin: 32px 0 0; padding: 0; list-style: none; } +.bullets li { display: grid; grid-template-columns: 22px 1fr; gap: 12px; color: var(--muted); font-size: .99rem; } +.bullets li b { color: var(--text); font-weight: 600; } +.bullets svg { width: 18px; height: 18px; margin-top: 4px; color: var(--accent); } + +/* Four short notes side by side rather than one long ladder down half the page */ +.bullets.two-up { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px 44px; } + +/* --------------------------------------------------------------- privacy */ + +#privacy .panel { margin-top: 30px; } +#privacy .bullets { margin-top: 0; } +.stamp { margin-top: 22px; font-size: .86rem; color: var(--muted); } + +/* ---------------------------------------------------------------- footer */ + +footer { border-top: 1px solid var(--line); padding-block: 34px; color: var(--muted); font-size: .9rem; } +.footrow { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 26px; } +.footrow nav { display: flex; flex-wrap: wrap; gap: 20px; margin-left: auto; } +.footrow a { color: var(--muted); text-decoration: none; } +.footrow a:hover { color: var(--text); text-decoration: underline; } + +.support { margin-top: 26px; color: var(--muted); font-size: .95rem; } + +@media (max-width: 900px) { + .bullets.two-up { grid-template-columns: 1fr; } + + + #menuBtn { display: grid; } + nav.links + .controls { margin-left: auto; } + + nav.links { + position: absolute; top: 100%; left: 0; right: 0; + display: none; flex-direction: column; gap: 0; + padding: 6px 24px 12px; + background: var(--surface-2); + border-bottom: 1px solid var(--line); + box-shadow: var(--shadow); + } + header[data-menu="open"] nav.links { display: flex; } + header[data-menu="open"] ~ .scrim { display: block; } + nav.links a { padding: 12px 0; color: var(--text); font-size: 1rem; } + nav.links a + a { border-top: 1px solid var(--line); } + + .field { padding-right: 62px; } + #demoChip { font-size: 17px; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + .caret { animation: none; } + * { transition-duration: .01ms !important; } +}