Переиспользуемый 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.