Переиспользуемый UI-кит на чистом OneScript: кнопки, таблицы, плашки, поля, каркас форм, токены темы, глифы, значки и свой базовый лист стилей. Классы кита — желуди ОСени, но зависимости на неё у библиотеки нет: аннотации это синтаксис, читает их контейнер снаружи.
Кит самодостаточен: поставленный в чужой проект, он выглядит прилично без единой строки CSS от потребителя — и правила, и файлы проект при этом волен переопределить.
О предметной области приложения кит не знает ничего. Компонент принимает МОДЕЛЬ (структуру полей) и отдаёт HTML; кто собрал модель, ему неизвестно и знать не положено.
opm install oscript-ui
В packagedef приложения:
.ЗависитОт("oscript-ui", "0.1.0")В точке входа — до сборки контейнера ОСени:
#Использовать oscript-uiДиректива обязана отработать раньше Новый Поделка(...): ОСень регистрирует желуди
по ИЗВЕСТНЫМ типам, а тип, появившийся после конструктора, желудём уже не станет.
Ни путей, ни сканирования каталогов не требуется.
src/Классы/
├── основа/ ЭкранированиеHTML · ЧтениеМодели · ЧтениеГлифа · НаборИконок —
│ то, на чём стоят компоненты и что само разметки не печатает
├── компоненты/ ровно то, что отдаёт фасад НаборКомпонентов
├── холст/ Кит · ЭлементЭкрана · РазметкаЭлементов — экран деревом узлов
├── каркас/ Формы · ОбъявлениеФормы · ПечатьФормы · ПроверкаПоля · ЗначенияФормы
│ РендерерНастроек — «объект → дерево разделов → секции → панель»
│ КаркасСтраницы · ОбрамлениеИнсталляцииПоУмолчанию — корень страницы
├── оформление/ ТокеныТемы · ВыборОформления · РаскладкаСтатики
│ ОтпечаткиСтатики · АктивыСтатики
│ ОформлениеИнсталляцииПоУмолчанию · СтатикаИнсталляцииПоУмолчанию
├── тексты/ ТекстыИнтерфейса (резолвер ключей) · СловарьИнтерфейсаПоУмолчанию
├── витрина/ СборкаОбразцов — записи раздела и образца
│ ОбразцыСтраницы · ОбразцыВвода · ОбразцыПоказа · ОбразцыХолста —
│ каталоги ИСХОДНИКОВ образцов, по каталогу на слой словаря
├── НаборКомпонентов.os фасад: по методу на компонент
└── ВитринаКита.os витрина: исполняет исходники и печатает страницу
src/статика/
├── oscript-ui.css базовый лист: правила примитивов кита, ни одного цвета
├── иконки/ глифы, по .svg на иконку, и текст разрешения на их использование
└── провайдеры/ значки чужих сервисов — отдаются файлами, а не вставляются в разметку
Имя файла глифа и есть имя иконки: замена иконки — замена файла, и рисунок, и viewBox
берутся из него самого. Как проект перебивает правило базового листа и как подменяет
любой из этих файлов — раздел «Переопределение».
Компонент отдаёт готовую разметку по модели:
Кнопки = Поделка.НайтиЖелудь("Кнопка");
HTML = Кнопки.Отрисовать(Новый Структура("Подпись, Вариант, Иконка",
"Сохранить", "основная", "сохранить"));Фасад НаборКомпонентов даёт по методу на компонент — экрану достаточно одного желудя:
Набор = Поделка.НайтиЖелудь("НаборКомпонентов");
HTML = Набор.Карточка().Отрисовать(Новый Структура("Заголовок, Тело",
"Пул main", Набор.Тег().Отрисовать("stable")));Экран можно собирать не строками, а деревом узлов — тогда теги и CSS-классы печатает
РазметкаЭлементов, единственное место, где узел становится тегом:
Холст = Кит.Холст();
Шапка = Холст.Добавить(Кит.Ряд("выбор"));
Шапка.Добавить(Кит.Надпись("название")).Добавить(Кит.Текст("Витрина"));
HTML = Холст.Рендер();Род — что узел такое; вид — как он выглядит, значением из закрытого набора, а не именем CSS-класса. Неизвестный вид останавливает сборку, а не даёт молча неоформленный экран.
| Род | Фабрика | Что это |
|---|---|---|
| холст | Кит.Холст() |
прозрачная группировка: своего тега нет, пустой печатается пустой строкой |
| область | Кит.Область(Вид) |
блок со своим тегом — на него вешают признак и роль |
| ряд | Кит.Ряд(Вид) |
элементы в строку с переносом |
| раскрывашка | Кит.Раскрывашка(Вид) |
<details>; свёрнутая строка наполняется через Шапка(), Подпись() называет её скринридеру |
| статья | Кит.Статья(Вид) |
<article>: самостоятельный кусок, который можно вынуть из списка |
| список | Кит.Список(Вид) |
<ul>; в него кладут пункты |
| пункт | Кит.Пункт() |
<li>, холст |
| свойства | Кит.Свойства() |
<dl>: перечисление пар «имя — значение» |
| свойство | Кит.Свойство(Имя) |
пара <dt>/<dd>: имя подписью, значение детьми |
| группа | Кит.Группа(Подпись, Вид) |
<fieldset> с легендой |
| форма | Кит.Форма(Действие, Токен, Вид) |
POST вошедшего; без CSRF-токена не собирается |
| форма-без-сессии | Кит.ФормаБезСессии(Действие, Вид) |
POST посетителя, у которого сессии ещё нет |
| форма-отбора | Кит.ФормаОтбора(Действие, Вид) |
GET: на сервере ничего не меняет |
| форма-без-токена | Кит.ФормаБезТокена(Действие, Вид) |
POST, у которого сессия есть, а токена сервер не спрашивает: цена подделки — неудобство (сменить тему, выйти) |
| таблица | Кит.Таблица(Вид) |
список; Пусто() — что показать вместо неё |
| колонка | Кит.Колонка(Подпись) |
заголовок столбца, холст |
| запись | Кит.Запись() |
строка таблицы; Разворот() — подробность под ней |
| ячейка | Кит.Ячейка(Вид) |
холст внутри записи; «столбиком» — двухэтажная |
| заголовок | Кит.Заголовок(Уровень, Вид) |
заголовок 1..6 |
| надпись | Кит.Надпись(Вид) |
кусок строки со своей ролью в ней |
| примечание | Кит.Примечание(Текст, Вид) |
абзац мелким шрифтом; холст |
| ссылка | Кит.Ссылка(Адрес, Вид) |
холст; адрес чужой схемы ссылкой не станет |
| текст | Кит.Текст(Значение) |
экранируется по своей природе |
| код | Кит.Код(Значение, Вид) |
машинное значение моноширинным; «блочный» — образец в <pre>, «приглушённый» — не спорит с соседним текстом |
| шаблон | Кит.Шаблон(Образец) |
текст с местами %1…%N, которые занимают дети |
| картинка | Кит.Картинка(Адрес, Вид) |
<img>; пустой адрес не даёт битой картинки, Подпись() становится alt |
| компонент | Кит.Кнопка(…), Кит.Карточка(…) |
обёртка над компонентом; со слотом принимает детей |
| документ | Кит.Документ(Язык, Тема) |
корень страницы: объявление типа и <html>; внутрь кладут только голову и тело |
| голова | Кит.Голова() |
<head>; внутрь — заглавие, объявления, ресурсы, палитра |
| титул | Кит.Титул(Текст) |
<title> — как страница названа во вкладке |
| мета | Кит.Мета(Имя, Содержание), Кит.Кодировка(Имя) |
объявление в голове документа |
| ресурс | Кит.Ресурс(Отношение, Адрес, Тип, Размеры) |
<link>: лист стилей, иконка сайта; без адреса не печатается |
| стиль | Кит.Стиль(Имя, Правила) |
<style> инлайном; правила — ДОВЕРЕННЫЙ CSS, экранирование там не защищает |
| скрипт | Кит.Скрипт(Адрес) |
<script defer>: разбор разметки не останавливает |
| тело | Кит.Тело() |
<body> |
| шапка | Кит.Шапка(Вид) |
<header> — верхняя полоса страницы |
| основное | Кит.Основное(Вид) |
<main>: то, ради чего страницу открыли |
| подвал | Кит.Подвал(Вид) |
<footer> |
| навигация | Кит.Навигация(Вид) |
<nav> — ориентир для скринридера |
| готовое | Кит.Готовое(Разметка) |
⚠ костыль перехода: сырой HTML |
Таблица собирается тем же деревом, а ячейка принимает элементы, а не готовую строку:
Таблица = Кит.Таблица();
Таблица.Добавить(Кит.Колонка("Имя"));
Таблица.Пусто().Добавить(Кит.Примечание("Ничего не найдено"));
Запись = Таблица.Добавить(Кит.Запись());
Запись.Добавить(Кит.Ячейка()).Добавить(Кит.Код("main"));
Запись.Разворот().Добавить(Кит.Примечание("подробности"));Контейнерный компонент берёт содержимое детьми, а не полем модели:
Карточка = Кит.Карточка(Новый Структура("Заголовок", "Пулы"));
Карточка.Добавить(Кит.Примечание("две штуки"));Сетка раскладывает детей сама, поэтому получает их по одному — каждый ребёнок становится своей ячейкой:
Сетка = Кит.Сетка(Новый Структура("Колонки", 2));
Сетка.Добавить(Кит.Примечание("слева"));
Сетка.Добавить(Кит.Примечание("справа"));Плашка тоже берёт содержимое детьми, когда его надо разметить своими узлами — например
подписать отдельным <span>, за который возьмётся скрипт страницы:
Плашка = Кит.Плашка(Новый Структура("Вид", "ok"));
Плашка.Добавить(Кит.Надпись().Признак("chip-caption", "")).Добавить(Кит.Текст("lts"));Помимо вида у каждого узла есть свойства, которые печатаются атрибутами: Признак(Имя, Значение) (data-*, печатается даже пустым), Роль(Значение), Подпись(Текст),
Подсказка(Текст) (title), Отключить(), Скрыть(). Четыре свойства спрашиваются
только своим родом и на чужом отбиваются: Высота(Пикселей) и Ширина(Пикселей) —
у картинки, СФайлами() — у формы, Открыть() — у раскрывашки.
КаркасСтраницы собирает документ целиком — от объявления типа до отложенного скрипта —
вокруг готового содержимого:
Каркас = Поделка.НайтиЖелудь("КаркасСтраницы");
Ответ.ТелоТекст = Каркас.Страница("Каталог", Тело,
Каркас.КонтекстСтраницы(Пользователь, КукиЗапроса));Тело — либо элемент экрана, либо строка непереведённого экрана: строку каркас
заворачивает в готовый фрагмент, и два стиля сборки сосуществуют. Документ(…) отдаёт
тот же каркас элементом — Дерево() на нём печатает, из чего собрана страница.
Кто такая инсталляция, каркас не знает: знак, подпись, адреса ходов, иконки и разделы
навигации приходят контрактом ОбрамлениеИнсталляции (ниже).
Компонент печатает себя целиком, а в дереве экрана виден по имени — компонент[кнопка],
а не безымянный кусок HTML. Обёрнуты все компоненты, кроме Таблица: её заменил
раскладочный род.
| Фабрика | Что это | Вид узла |
|---|---|---|
Кит.Кнопка(Модель) |
кнопка формы или ссылка-кнопка | кнопка |
Кит.Плашка(Модель) |
короткий признак: состояние, тип, источник | плашка |
Кит.РядПлашек(Список) |
ряд плашек одним элементом | плашки |
Кит.Поле(Модель) |
поле ввода со своей меткой и отказом | поле |
Кит.ПолеИзКонфига(Модель) |
поле, значение которого задано конфигурацией | поле-конфига |
Кит.СкрытоеПоле(Имя, Значение) |
скрытое поле формы | скрытое |
Кит.БлокКоманды(Модель) |
копируемая команда | команда |
Кит.Карточка(Модель) |
рамка с заголовком; тело — дети | карточка |
Кит.ОпаснаяЗона(Модель) |
необратимые действия; тело — дети | зона |
Кит.Сетка(Модель) |
раскладка равнозначных блоков; ячейки — дети | сетка |
Кит.ШапкаОбъекта(Модель) |
первый экран объекта; действия — дети | шапка |
Кит.ЗаголовокСтраницы(Модель) |
заголовок страницы с пояснением | заглавие |
Кит.ПустоеСостояние(Модель) |
значок, объяснение и призыв к действию | пустое |
Кит.Табы(Модель) |
полоса вкладок, каждая своим адресом | вкладки |
Кит.СтрокаПоиска(Модель) |
готовая строка поиска по списку | поиск |
Кит.Пагинация(Модель) |
постраничная навигация | страницы |
Кит.СтраницаОшибки(Модель) |
тело страницы отказа | отказ |
Кит.Проводник(Модель) |
список шагов многошаговой задачи | шаги |
Кит.Иконка(Модель) |
значок из набора иконок | иконка |
Кит.РядМеты(Элементы, Вид) |
пары «значок + значение» в строку | мета |
Кит.ПолосаЗаполнения(Модель) |
доля занятого от предела | полоса |
Кит.Тумблер(Модель) |
поле выбора из двух состояний | тумблер |
Кит.Меню(Модель) |
кнопка-открывашка и панель пунктов | меню |
Кит.Плитка(Модель) |
сводный показатель одним числом | плитка |
Кит.Уведомление(Модель) |
сообщение о результате действия (компонент Тост) |
уведомление |
Кит.ДеревоРазделов(Модель) |
навигация по дому настроек | разделы |
Кит.ПанельСохранения(Модель) |
липкая полоса с кнопкой «Сохранить» | панель |
Два имени рядом, которые легко перепутать: Кит.Иконка(Имя) рисует глиф из набора
иконок, а Кит.Картинка(Адрес, "значок") показывает внешнюю картинку размером
со значок. Кит.СтрокаПоиска — готовая форма поиска по списку, Кит.ФормаОтбора — пустая
GET-форма, которую наполняют сами.
Объявленная форма знает свои поля и умеет разобрать пришедшее, поэтому она входит в дерево тем же узлом-компонентом, а не переписывается родами:
Холст.Добавить(Кит.ОбъявленнаяФорма(Объявление, Значения, "/pools", Токен, "столбик"));
Холст.Добавить(Кит.Окно(Объявление, Значения, "/pools", Токен, СвойстваОкна));Кит объявляет каждый контракт ПРОЗВИЩЕМ и кладёт под ним пустышку. Приложение подставляет
свою реализацию тем же прозвищем плюс &Верховный:
| Прозвище | Пустышка кита | Что даёт инсталляция |
|---|---|---|
СловарьИнтерфейса |
пустой словарь | тексты по ключам |
ОформлениеИнсталляции |
пустое оформление | тему, акцент по умолчанию и имя cookie выбора |
СтатикаИнсталляции |
ни одного файла | каталог статики, её файлы и их типы |
ОбрамлениеИнсталляции |
пустое обрамление | знак, подпись подвала, язык, иконки, адреса ходов (корень, вход, кабинет, выход, переключение оформления), разделы верхней и приватной навигации и имя вошедшего |
&Верховный
&Прозвище("СловарьИнтерфейса")
&Желудь
Процедура ПриСозданииОбъекта() ЭкспортИмя класса контрактом быть НЕ МОЖЕТ: ОСень ищет желудь сперва в определениях по имени
и только потом по прозвищу, поэтому класс, названный СловарьИнтерфейса, не перебивается
ничем — до сравнения &Верховных дело не доходит.
Своих строк у кита нет, но семнадцать ключей он запрашивает сам. Ключа нет в словаре —
на экране появится видимый маркер [?ключ]:
| Ключ | Где нужен |
|---|---|
form.error.required · form.error.number · form.error.min · form.error.max · form.error.choice |
ПроверкаПоля — отказы формы по полю |
settings.frame.save · settings.frame.nav.title · settings.frame.crumbs.label · settings.frame.empty |
РендерерНастроек — каркас дома настроек |
chrome.top.nav.title · chrome.top.login · chrome.top.logout · chrome.top.office · chrome.top.theme.toggle · chrome.top.theme.to-dark · chrome.top.theme.to-light · chrome.office.nav.title |
КаркасСтраницы — шапка и навигация приватной части |
РаскладкаСтатики знает два каталога разной природы. Собственный каталог кита она находит
сама — от файла своего класса вверх по имени статика, и это работает и после установки
в oscript_modules. Каталог инсталляции вместе со списком её файлов приходит контрактом
СтатикаИнсталляции.
Что везёт сама библиотека:
| Файл | Где лежит | Как отдаётся |
|---|---|---|
oscript-ui.css |
src/статика/ |
базовый лист стилей — правила примитивов кита |
github.svg · gitlab.svg · google.svg · key.svg |
src/статика/провайдеры/ |
значки чужих сервисов; ключи закрытым списком отдаёт КлючиЗначков() |
phosphor-LICENSE.txt |
src/статика/иконки/ |
текст разрешения на набор рисунков |
*.svg |
src/статика/иконки/ |
глифы; в разметку они попадают телом, а не адресом |
Что даёт инсталляция — лицо конкретного приложения: знак сайта, иконка вкладки, иконка домашнего экрана, свой лист стилей.
&Верховный
&Прозвище("СтатикаИнсталляции")
&Желудь
Процедура ПриСозданииОбъекта() Экспорт
КонецПроцедуры
Функция Каталог() Экспорт
Возврат "/путь/к/приложению/src/статика";
КонецФункции
Функция Файлы() Экспорт
Результат = Новый Соответствие();
Результат.Вставить("app.css", Новый Структура("Подкаталог, ТипКонтента",
"", "text/css; charset=utf-8"));
Результат.Вставить("mark.svg", Новый Структура("Подкаталог, ТипКонтента",
"бренд", "image/svg+xml"));
Возврат Результат;
КонецФункцииАктивыСтатики отдаёт по этой раскладке односегментный адрес с отпечатком содержимого:
/static/app-1f2e3d4c5b6a7988.css. Правка файла без смены адреса невозможна как класс —
адрес есть функция содержимого. У каждого файла свой отпечаток, и базовый лист кита —
такой же файл, как остальные: АктивыСтатики.АдресБазовогоЛиста().
Кит самодостаточен: поставленный в чужой проект, он выглядит прилично сам по себе, без единой строки CSS от потребителя. Всё, что проект хочет изменить, он меняет одним из трёх способов — новых механизмов заводить не нужно.
КаркасСтраницы печатает в <head> два тега <link rel="stylesheet"> в этом порядке:
- базовый лист библиотеки —
АктивыСтатики.АдресБазовогоЛиста(); - лист инсталляции — поле
АдресСтилейобрамления.
Правило из листа инсталляции побеждает китовое той же специфичностью: ни !important,
ни лишний селектор не нужны. Пустой АдресСтилей не печатается вовсе — инсталляции,
которой хватает базового листа, второй тег не достаётся.
/* лист инсталляции: кнопка приложения — прямоугольная */
.button { border-radius: 0; }Порядок каскада стережёт КаркасСтраницы_Тесты.БазовыйЛистСтоитПередЛистомИнсталляции.
Раскладка кладёт свои файлы первыми, файлы инсталляции — поверх. Имя, объявленное инсталляцией, вытесняет китовое целиком: файл поедет из её каталога, с её типом и с её отпечатком.
// свой значок вместо китового, своя иконка вместо своей же — механизм один
Функция Файлы() Экспорт
Результат = Новый Соответствие();
Результат.Вставить("github.svg", Новый Структура("Подкаталог, ТипКонтента",
"значки", "image/svg+xml"));
Возврат Результат;
КонецФункцииТак же подменяется и сам базовый лист — объявлением файла oscript-ui.css, — но это
крайняя мера: перебить правило вторым листом дешевле и не рвёт связь с обновлениями
библиотеки.
Цвета, интервалы и радиусы кит печатает инлайновым <style id="токены-темы"> до обоих
листов. Менять их полагается не правилом, а контрактом ОформлениеИнсталляции: он отдаёт
тему (тёмная/светлая) и акцент #rrggbb, из которого ТокеныТемы выводит ступень 500
акцентной рампы и контрастный цвет текста на заливке.
&Верховный
&Прозвище("ОформлениеИнсталляции")
&Желудь
Процедура ПриСозданииОбъекта() Экспорт
КонецПроцедуры
Функция Оформление() Экспорт
Возврат Новый Структура("Тема, Акцент", "светлая", "#2f7d4c");
КонецФункцииТокен, которого контракт не покрывает, переопределяется листом инсталляции — он идёт после инлайнового блока:
:root { --layout-width: 1280px; }Витрина — для того, кто пишет экран на этой библиотеке. Под каждым образцом стоит не только картинка, но и исходник, которым эта картинка нарисована: копируете кнопкой справа от кода, вставляете к себе — получаете ровно то, что видели. Поднимается из репозитория одной командой, сервера не требует:
oscript витрина.os
Команда собирает кит и кладёт рядом витрина.html — путь печатается в консоль, файл
открывается браузером. Внутри — весь словарь библиотеки: по карточке на компонент
(кнопка во всех вариантах, плашки, карточки, таблицы, формы, пустые состояния, пагинация,
тосты и остальные), карточка на каждую группу родов узлов холста — от корня документа
до готового куска чужой разметки, — и таблица родов с тегом, признаком вложения и списком
известных видов. Ни одного рода и ни одной обёртки над компонентом мимо витрины не проходит:
это стережёт ВитринаКита_Тесты.ВитринаПоказываетВесьСловарьКита.
В исходниках доступны три желудя — те же, что экран приложения достаёт из ОСени:
| Имя | Что это |
|---|---|
Набор |
фасад компонентов: Набор.Кнопка().Отрисовать(Модель) |
Кит |
холст: экран собирается деревом узлов |
Формы |
фабрика объявлений формы |
Каждый образец заканчивается присваиванием переменной HTML — это и есть разметка,
которую он отдаёт. Больше в исходнике нет ничего: он самодостаточен ровно настолько,
чтобы работать у вас без правок.
Показанный код — источник образца, а не его копия. Витрина не хранит рядом с картинкой
её описание: она берёт исходник и ИСПОЛНЯЕТ его, а нарисованным показывает результат.
Разойтись им негде — расходиться нечему. Что это правда, проверяет
ВитринаКита_Тесты.ПоказанныйКодРисуетТотЖеОбразец: он исполняет каждый исходник заново,
в чужом модуле, где нет ничего кроме трёх желудей, и сверяет с показанным.
Кнопка «Копировать» — обычный Блок команды кита, тот же, что на страницах приложения:
код лежит в разметке, а кнопку оживляет короткий скрипт страницы по атрибуту data-copy.
Своего скрипта библиотека не везёт и в этом случае: без JavaScript кнопка молчит, а сам
код остаётся видимым и выделяемым — его печатает разметка, а не скрипт.
Образцы намеренно безымянные по предмету: проекты, документы, метки — данные, которые
читаются в любом приложении. Витрина есть лицо библиотеки, и подставлять в неё словарь
одного конкретного продукта значило бы стереть ту самую границу, ради которой кит и живёт
отдельно. Кладёте свой компонент — приводите образец на таких же нейтральных данных
и назовите в разделе, что из словаря он показывает: род:<имя> либо компонент:<вид>.
Оформление у витрины своё, из коробки: базовый лист библиотеки вшивается в страницу без единого ключа — ровно тот вид, который получит чужой проект сразу после установки.
Управлять можно двумя ключами:
| Ключ | Что делает |
|---|---|
--стили <путь.css> |
докладывает лист инсталляции ВТОРЫМ — как в каскаде живой страницы |
--вывод <путь.html> |
кладёт страницу по указанному пути |
С листом хаба поверх базового это выглядит так — видно ровно то, что проект перебивает:
oscript витрина.os --стили ../oscript_openhub/src/статика/openhub.css
oneunit execute
MIT. Глифы — набор Phosphor, текст разрешения лежит рядом
с ними в src/статика/иконки/phosphor-LICENSE.txt.