From a9b85559bb0d75fd9c31c79397a38dca70977c58 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 03:31:34 +0300 Subject: [PATCH 01/53] Copy button page: label column with aligned values, long address wraps on a phone, no dated old names Docs review CB-11 (one value axis, C33), CB-12 (the documentation address breaks anywhere instead of running past the card at 390), S-02 (the appearance alias lives on the names guide only), and a short example tab. Co-Authored-By: Claude Opus 5.5 --- content/ru/smart/actions/copy-button.md | 5 +-- .../copy-button/overview/index.html | 41 ++++++++++++------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/content/ru/smart/actions/copy-button.md b/content/ru/smart/actions/copy-button.md index ae034c03..d338f404 100644 --- a/content/ru/smart/actions/copy-button.md +++ b/content/ru/smart/actions/copy-button.md @@ -21,7 +21,7 @@ description: "Копирует значение или текст элемент на миг станет галочкой. Что скопировано, видно в журнале под примером; вставьте в любое поле, чтобы проверить. -:::example {id="smart-components/copy-button/overview" label="Идентификатор, команда, ссылка" preview=sandbox log_console=true log_open=false} +:::example {id="smart-components/copy-button/overview" label="Подключение проекта" preview=sandbox log_console=true log_open=false} ::: Кнопке нужны источник текста и имя — так устроена строка с командой: @@ -138,9 +138,6 @@ description: "Копирует значение или текст элемент без разрешения на запись в буфер, — кнопка выделяет текст и сообщает «Выделено — нажмите Ctrl+C» (⌘C на Mac). -**Прежние имена.** До 2027-01-01 `appearance="link"` читается как `ghost`; -новая разметка пишет `ghost`. - ## Работа из кода ### События diff --git a/examples/smart-components/copy-button/overview/index.html b/examples/smart-components/copy-button/overview/index.html index ac27987d..62e174d5 100644 --- a/examples/smart-components/copy-button/overview/index.html +++ b/examples/smart-components/copy-button/overview/index.html @@ -1,18 +1,29 @@ +

Подключение проекта

-
- Идентификатор - fhmm3nb668n747qpocad - -
-
- Установка - npm install @simai-studio/ui - -
-
- Документация - https://ui.simai.io/ru/smart/actions/copy-button/ - -
+
+
Идентификатор
+
+ fhmm3nb668n747qpocad + +
+
Установка
+
+ npm install @simai-studio/ui + +
+
Документация
+
+ https://ui.simai.io/ru/smart/actions/copy-button/ + +
+
From 490f07bfc2d7e23cfec186aae6b0391930debec1 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 03:31:34 +0300 Subject: [PATCH 02/53] Download file page: sizes side by side, prepared files carry their size, short tabs, no dated old names Docs review DF-14 (each size beside an icon button of the same step; the ladder reads 28, 32, 40, 48, 56), DF-15 docs side (the prepare examples set file-size with the address, so no HEAD to a blob URL), DF-17 (two- or one-word example tabs fit at 390), S-02 (icon and download aliases stay on the names guide). Co-Authored-By: Claude Opus 5.5 --- content/ru/smart/actions/download-file.md | 31 +++++++++---------- .../download-file/overview/index.js | 8 +++-- .../download-file/prepare/index.js | 10 ++++-- .../download-file/sizes/index.html | 10 +++--- 4 files changed, 33 insertions(+), 26 deletions(-) diff --git a/content/ru/smart/actions/download-file.md b/content/ru/smart/actions/download-file.md index e168d5f9..14f367ee 100644 --- a/content/ru/smart/actions/download-file.md +++ b/content/ru/smart/actions/download-file.md @@ -20,7 +20,7 @@ description: "Показывает файл для скачивания с им сверки.csv» приложение собирает по нажатию и отдаёт браузеру, когда файл готов. Журнал под примером показывает события. -:::example {id="smart-components/download-file/overview" label="Вложения к заказу" preview=sandbox log_console=true log_open=false} +:::example {id="smart-components/download-file/overview" label="Вложения" preview=sandbox log_console=true log_open=false} ::: Готовый файл — ссылка с адресом, именем и размером: @@ -54,10 +54,9 @@ description: "Показывает файл для скачивания с им Ссылка на файл скачивает его в той же вкладке под именем из `file-name`. `target="_blank"` вместо этого открывает файл для просмотра в новой вкладке: у ссылки появляется знак новой вкладки, а экранный диктор слышит «откроется в -новой вкладке». Атрибут `download` оставлен для совместимости и ничего не -меняет. +новой вкладке». -:::example {id="smart-components/download-file/link" label="Скачать и открыть" preview=sandbox} +:::example {id="smart-components/download-file/link" label="Ссылки" preview=sandbox} ::: `file-name` — видимое имя файла; задавайте его всегда. `file-type` — тип @@ -73,10 +72,11 @@ description: "Показывает файл для скачивания с им Без `href` нажатие отправляет событие, и приложение собирает файл: отчёт, выгрузку, архив. На это время оно ставит `state="preparing"`: на месте знака крутится индикатор, рядом — «Готовим файл…», повторное нажатие не -срабатывает. Когда файл готов, приложение задаёт `href` и возвращает -`state="ready"`: файл, за которым пришёл человек, скачивается один раз. +срабатывает. Когда файл готов, приложение задаёт его размер в `file-size`, +адрес в `href` и возвращает `state="ready"`: файл, за которым пришёл человек, +скачивается один раз. -:::example {id="smart-components/download-file/prepare" label="Выгрузка по запросу" preview=sandbox log_console=true log_open=false} +:::example {id="smart-components/download-file/prepare" label="Выгрузка" preview=sandbox log_console=true log_open=false} ::: ```js @@ -85,7 +85,9 @@ file.addEventListener('sf-file-download-download', async (event) => { event.preventDefault(); file.setAttribute('state', 'preparing'); try { - file.setAttribute('href', await buildReport()); + const {url, size} = await buildReport(); + file.setAttribute('file-size', size); + file.setAttribute('href', url); file.setAttribute('state', 'ready'); } catch { file.setAttribute('state', 'error'); @@ -97,7 +99,6 @@ file.addEventListener('sf-file-download-download', async (event) => { |:---|:---| | `href` | адрес файла; у кнопки без адреса — пустая строка | | `fileName` | имя файла | -| `download` | `true`, если задан `download` | ## Внешний вид @@ -106,8 +107,9 @@ file.addEventListener('sf-file-download-download', async (event) => { `size` • по умолчанию `1` • [справочник](#атрибуты) Размер подгоняет элемент к тексту и кнопкам рядом: каждый размер одной -высоты с кнопкой того же размера — 24, 32, 40, 48 и 56 px на широком -экране. В примере рядом с файлом стоит кнопка той же ступени. +высоты с кнопкой того же размера — 28, 32, 40, 48 и 56 px на широком +экране. В примере рядом с каждым файлом стоит кнопка со значком той же +ступени. :::example {id="smart-components/download-file/sizes" label="Размеры" preview=inline} ::: @@ -122,9 +124,6 @@ file.addEventListener('sf-file-download-download', async (event) => { :::example {id="smart-components/download-file/icons" label="Тип файла" preview=inline} ::: -Прежний способ — назвать тип значком в `icon` — до 2027-01-01 ещё рисуется, -но занимает место знака скачивания; пишите тип в `file-type`. - ## Состояния ### Подготовка и ошибка @@ -137,7 +136,7 @@ file.addEventListener('sf-file-download-download', async (event) => { `sf-file-download-retry`, и приложение готовит файл снова. `error-text` добавляет к сообщению, что случилось и что делать. -:::example {id="smart-components/download-file/states" label="Готов, готовится, ошибка" preview=sandbox log_console=true log_open=false} +:::example {id="smart-components/download-file/states" label="Состояния" preview=sandbox log_console=true log_open=false} ::: ### Недоступный @@ -147,7 +146,7 @@ file.addEventListener('sf-file-download-download', async (event) => { Недоступный файл — кнопка без адреса, даже если `href` задан; событий он не отправляет. -:::example {id="smart-components/download-file/state-disabled" label="Доступный и недоступный"} +:::example {id="smart-components/download-file/state-disabled" label="Недоступный"} ::: ## Доступность diff --git a/examples/smart-components/download-file/overview/index.js b/examples/smart-components/download-file/overview/index.js index d55d4abc..1d4f0dec 100644 --- a/examples/smart-components/download-file/overview/index.js +++ b/examples/smart-components/download-file/overview/index.js @@ -1,6 +1,6 @@ // Two files lie at an address: links the browser downloads. The third one // the application builds when it is asked: it sets state="preparing", then -// the href and state="ready", and the element fetches the file once. +// the size, the href and state="ready", and the element fetches the file once. const initialize = async () => { await customElements.whenDefined('sf-file-download'); if (document.body.dataset.exampleReady === 'true') return; @@ -13,7 +13,11 @@ const initialize = async () => { act.setAttribute('state', 'preparing'); setTimeout(() => { const text = 'Дата;Документ;Сумма, ₽\n01.11.2026;Счёт 1042;440000\n05.11.2026;Оплата;440000\n'; - act.setAttribute('href', URL.createObjectURL(new Blob([text], {type: 'text/csv;charset=utf-8'}))); + const file = new Blob([text], {type: 'text/csv;charset=utf-8'}); + // The application knows the size of what it built: with file-size set + // the element does not ask the address for it. + act.setAttribute('file-size', `${file.size} Б`); + act.setAttribute('href', URL.createObjectURL(file)); act.setAttribute('state', 'ready'); }, 1200); }); diff --git a/examples/smart-components/download-file/prepare/index.js b/examples/smart-components/download-file/prepare/index.js index 26e0ebc5..7bd6d453 100644 --- a/examples/smart-components/download-file/prepare/index.js +++ b/examples/smart-components/download-file/prepare/index.js @@ -1,6 +1,6 @@ // Without href the press is the application's: it cancels the request, sets -// state="preparing" while it builds the file, then gives the href and -// state="ready"; the element fetches the file the person pressed for once. +// state="preparing" while it builds the file, then gives the size, the href +// and state="ready"; the element fetches the file the person pressed for once. const buildReport = () => new Promise((done) => setTimeout(() => done( 'Заказ;Дата;Сумма, ₽\n1040;03.11.2026;12500\n1041;04.11.2026;8900\n1042;05.11.2026;440000\n', ), 1200)); @@ -17,7 +17,11 @@ const initialize = async () => { file.setAttribute('state', 'preparing'); docara.example.log('state', 'preparing'); const text = await buildReport(); - file.setAttribute('href', URL.createObjectURL(new Blob([text], {type: 'text/csv;charset=utf-8'}))); + const report = new Blob([text], {type: 'text/csv;charset=utf-8'}); + // The size of the built file is known here: with file-size set the + // element does not ask the address for it. + file.setAttribute('file-size', `${report.size} Б`); + file.setAttribute('href', URL.createObjectURL(report)); file.setAttribute('state', 'ready'); docara.example.log('state', 'ready'); }); diff --git a/examples/smart-components/download-file/sizes/index.html b/examples/smart-components/download-file/sizes/index.html index 851437a7..8b92cb83 100644 --- a/examples/smart-components/download-file/sizes/index.html +++ b/examples/smart-components/download-file/sizes/index.html @@ -1,23 +1,23 @@
-
+
1/3
-
+
1/2
-
+
1
-
+
2
-
+
3
From f072829670a201ad0f1adc2d44c48e4ebb7d8c1f Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 04:07:05 +0300 Subject: [PATCH 03/53] Text field page: fourth standard, a real form, every size, inherited value, fixed error and mask examples Docs review forms part A for sf-input: attribute line under each heading (FA-01), when to take and not to take it (FA-02), specimen grids (FA-04), an application form with the example log (FA-05, IN-02), descriptions for every attribute in the reference (FA-06), no jargon in the class line (FA-10), sizes 1/3 to 3 beside buttons and radius (IN-01), no Ctrl K text in the field (IN-03), no old names (IN-04, IN-08), inherited value (IN-05), number and when to take Quantity (IN-06), an error that clears (IN-07), a mask whose value and hint agree (IN-10), title in the singular (IN-11). Co-Authored-By: Claude Opus 5.5 --- config/reference-pages.json | 2 +- config/smart-reference.json | 27 ++ content/ru/smart/forms/inputs.md | 334 +++++++++++------- .../inputs/appearance/index.html | 18 +- .../inputs/composition/index.html | 26 +- .../inputs/inherited/index.html | 6 + .../inputs/inherited/index.js | 12 + .../smart-components/inputs/label/index.html | 7 + .../smart-components/inputs/mask/index.html | 10 +- .../smart-components/inputs/mask/index.js | 10 + .../inputs/overview/index.html | 31 +- .../smart-components/inputs/overview/index.js | 38 ++ .../smart-components/inputs/radius/index.html | 16 + .../smart-components/inputs/sizes/index.html | 34 +- .../inputs/state-error/index.js | 17 + .../smart-components/inputs/types/index.html | 42 ++- 16 files changed, 438 insertions(+), 192 deletions(-) create mode 100644 examples/smart-components/inputs/inherited/index.html create mode 100644 examples/smart-components/inputs/inherited/index.js create mode 100644 examples/smart-components/inputs/label/index.html create mode 100644 examples/smart-components/inputs/mask/index.js create mode 100644 examples/smart-components/inputs/overview/index.js create mode 100644 examples/smart-components/inputs/radius/index.html create mode 100644 examples/smart-components/inputs/state-error/index.js diff --git a/config/reference-pages.json b/config/reference-pages.json index cd0031ef..3e71d14a 100644 --- a/config/reference-pages.json +++ b/config/reference-pages.json @@ -625,7 +625,7 @@ }, "smart/forms/inputs.md": { "kind": "smart", - "standard": 3 + "standard": 4 }, "smart/forms/radio.md": { "kind": "smart", diff --git a/config/smart-reference.json b/config/smart-reference.json index e2d382aa..56b36504 100644 --- a/config/smart-reference.json +++ b/config/smart-reference.json @@ -71,6 +71,33 @@ "file-download": { "target": "Где открыть ссылку без `download`, как у атрибута `target` ссылки HTML: `_blank` — в новой вкладке, `_self` — на месте", "aria-label": "Имя ссылки или кнопки для экранного диктора вместо имени и размера файла; назовите в нём файл" + }, + "input": { + "label": "Видимая подпись над полем; она же даёт полю доступное имя", + "required": "Обязательное поле: звёздочка у подписи, браузер не отправит форму с пустым полем", + "placeholder": "Образец формата в пустом поле; исчезает после ввода и подпись не заменяет", + "input-type": "Тип поля: `text`, `email`, `password`, `search`, `tel`, `url` или `number`; от него зависят проверка браузера и экранная клавиатура", + "autocomplete": "Подсказка браузеру для автозаполнения, как у `autocomplete` поля HTML: `email`, `tel`, `current-password`", + "inputmode": "Какую экранную клавиатуру показать на телефоне: `numeric` — цифры, `email` — с «@», `tel` — номер", + "minlength": "Наименьшее число символов; браузер проверяет его перед отправкой формы", + "maxlength": "Наибольшее число символов; больше ввести нельзя", + "pattern": "Регулярное выражение, которому должно соответствовать значение; браузер проверяет его перед отправкой формы", + "hint": "Постоянное пояснение под полем; при ошибке его место занимает сообщение", + "default-value": "Начальное значение: дальше поле хранит то, что ввёл человек", + "name": "Имя поля в форме: под ним значение уходит с отправкой", + "icon-start": "Значок в начале поля, имя иконки: `search`, `mail`", + "text-end": "Короткий текст в конце поля: единица или валюта, например «₽»", + "hint-icon": "Значок подсказки в конце рамки, имя иконки, например `info`", + "disabled": "Поле выключено: значение не меняется и не отправляется с формой", + "readonly": "Значение видно, его можно выделить и скопировать, но не изменить; отправляется с формой", + "invalid": "Поле с ошибкой: красная рамка, сообщение из `error-message` под полем, `aria-invalid`", + "error-message": "Текст ошибки под полем, пока задан `invalid`", + "mask": "Включает маску ввода из `mask-pattern` или `mask-options`", + "mask-pattern": "Шаблон маски: `0` — цифра, `a` — буква, `*` — любой символ, `{…}` — постоянная часть значения", + "mask-lazy": "`false` — показать весь шаблон маски сразу, а не по мере ввода", + "mask-placeholder-char": "Символ пустой позиции маски, например `_`", + "mask-options": "Полные настройки маски JSON-строкой, например `{\"mask\": \"Number\"}`", + "value": "Значение, которым управляет страница: при перерисовке поле показывает его" } }, "slots": { diff --git a/content/ru/smart/forms/inputs.md b/content/ru/smart/forms/inputs.md index c9b8e4d3..7a2671c6 100644 --- a/content/ru/smart/forms/inputs.md +++ b/content/ru/smart/forms/inputs.md @@ -1,176 +1,272 @@ --- -title: "Текстовые поля" -description: "Однострочное поле формы с подписью, подсказкой, ошибкой, значком и маской ввода." +title: "Текстовое поле" +description: "Принимает одну строку — имя, почту, пароль, телефон или адрес — с подписью, подсказкой, ошибкой и маской ввода." --- -# Текстовые поля +# Текстовое поле :badge[sf-input]{variant=tonal scheme=primary size=1/2} :badge[1.6.1]{variant=outline scheme=on-surface size=1/2} -Текстовое поле принимает одну строку — текст, почту, пароль, поиск, телефон -или адрес — вместе с подписью, подсказкой и сообщением об ошибке. Для -многострочного текста берите [Многострочное поле](/ru/smart/forms/textarea/), -для даты — [Календарь](/ru/smart/forms/datepicker/). +Текстовое поле принимает одну строку — имя, почту, пароль, телефон, адрес +сайта или поисковый запрос — вместе с подписью, подсказкой и сообщением об +ошибке. Значение уходит с формой, как у обычного поля. -То же на классах — [Текстовые поля](/ru/components/forms/inputs/): -`` собирает разметку поля ввода из атрибутов. +То же на классах — [Текстовые поля](/ru/components/forms/inputs/): для +статической разметки без JavaScript. ## Пример -:::example {id="smart-components/inputs/overview" label="Результат"} -::: +Заполните заявку и нажмите «Записаться». Пустое обязательное поле и неполная +почта покажут ошибку; исправьте значение и выйдите из поля — ошибка уйдёт. +Журнал под примером показывает события и отправленные значения. -## Разметка +:::example {id="smart-components/inputs/overview" label="Заявка" preview=sandbox log_console=true log_open=false} +::: Полю нужны подпись и имя для формы: ```html - + ``` -`label` — видимая подпись и доступное имя поля, `hint` — постоянное -пояснение под ним, `placeholder` — образец формата внутри пустого поля. -Placeholder исчезает после ввода, поэтому подпись он не заменяет. `required` -ставит звёздочку у подписи и делает обязательным нативное поле. В компактной -панели подпись можно скрыть только визуально, оставив её в дереве -доступности. Вместо строк подсказки и ошибки можно передать разметку в слоты -`hint` и `error`. +## Когда брать -Если подпись выводит сама страница — например, панель записи печатает её -рядом с полем, — не задавайте `label`, а передайте `aria-label`. Имя попадёт -на нативное поле, которое читают вспомогательные технологии, и подпись не -напечатается дважды. При видимой подписи `aria-label` не действует: имя поля -не расходится с тем, что написано на странице. +- **Значение в одну строку, которое человек печатает сам**: имя, почта, + пароль, телефон, адрес сайта, поисковый запрос. +- **Число, которое печатают целиком**: сумма, вес, размер — поле с + `input-type="number"`, см. [Тип значения](#тип-значения). -```html - -``` +## Когда не брать -Места для [знака «?»](/ru/smart/feedback/help-mark/) в строке -подписи у `` нет. Поле с пояснением к подписи собирают на классах — -см. [Текстовые поля → Поле с пояснением к подписи](/ru/components/forms/inputs/#поле-с-пояснением-к-подписи). +- **Текст в несколько строк** — [Многострочное поле](/ru/smart/forms/textarea/). +- **Небольшое целое число, которое меняют по шагу**: количество товара, + гости — [Количество](/ru/smart/forms/quantity/). +- **Выбор из известных вариантов** — [Радиокнопки](/ru/smart/forms/radio/) + для нескольких видимых, [Выбор из поиска](/ru/smart/forms/lookup/) для + длинного списка людей или объектов. +- **Дата или время** — [Календарь](/ru/smart/forms/datepicker/). +- **Телефон с выбором страны** — [Код страны](/ru/smart/forms/country-code/). -## Варианты +## Частые задачи -### Вид +### Подпись и подсказка -`appearance` выбирает вид: `bordered` — с рамкой, `filled` — с заливкой. -`appearance="hidden"` выводит только скрытое поле формы без подписи и рамки — -для значения, которое пользователь не видит. +`label`, `hint`, `placeholder`, `required`, `aria-label` • [справочник](#атрибуты) -:::example {id="smart-components/inputs/appearance" label="Поле с рамкой и с заливкой"} -::: +Подпись называет поле и для человека, и для экранного диктора, подсказка под +полем остаётся видна всё время. `placeholder` — образец формата в пустом +поле: он исчезает после ввода, поэтому подпись не заменяет. `required` +ставит звёздочку у подписи и не даёт отправить форму с пустым полем. -### Размер +:::example {id="smart-components/inputs/label" label="Подпись и поиск" preview=inline} +::: -`size` выбирает ступень размерной шкалы: `1/2`, `1` или `2`. +Если подпись выводит сама страница или поле стоит в панели без подписи, как +поиск в примере, задайте `aria-label`: имя попадёт на поле, которое читает +экранный диктор. При видимой подписи `aria-label` не действует. Вместо строк +подсказки и ошибки можно передать разметку в места `hint` и `error`. -:::example {id="smart-components/inputs/sizes" label="Размеры 1/2, 1 и 2"} -::: +Места для [знака «?»](/ru/smart/feedback/help-mark/) в строке подписи нет. +Поле с пояснением к подписи собирают на классах — см. [Текстовые поля → Поле +с пояснением к подписи](/ru/components/forms/inputs/#поле-с-пояснением-к-подписи). ### Тип значения -`input-type` выбирает тип нативного поля: `text`, `email`, `password`, -`search`, `tel` или `url`. Вместе с `autocomplete` и `inputmode` он подсказывает -браузеру автозаполнение и нужную экранную клавиатуру. `minlength`, -`maxlength` и `pattern` переходят на нативное поле, и браузер проверяет их -перед отправкой формы. +`input-type`, `autocomplete`, `inputmode` • по умолчанию `input-type="text"` • [справочник](#атрибуты) -:::example {id="smart-components/inputs/types" label="Почта, пароль, телефон и адрес сайта"} +Тип говорит браузеру, что за значение в поле: он проверяет почту и адрес +сайта, прячет пароль, предлагает автозаполнение и нужную экранную клавиатуру. +`minlength`, `maxlength` и `pattern` браузер тоже проверяет перед отправкой +формы. + +:::example {id="smart-components/inputs/types" label="Типы значения" preview=inline} ::: -### Состав +| Значение | Когда брать | +|:---|:---| +| `text` | имя, название, любой текст в строку | +| `email` | почта: браузер проверит адрес, телефон покажет клавиатуру с «@» | +| `password` | пароль: символы скрыты | +| `tel` | телефон: клавиатура с цифрами; формат задаёт [маска](#маска) | +| `url` | адрес сайта: браузер проверит, что он полный | +| `search` | поиск по странице или списку | +| `number` | число, которое печатают целиком: сумма, вес; `min`, `max` и `step` задают границы и шаг | + +Для небольшого целого числа, которое удобнее менять кнопками «−» и «+», +берите [Количество](/ru/smart/forms/quantity/). + +### Значок и текст в поле + +`icon-start`, `text-end`, `hint-icon` • [справочник](#атрибуты) -`icon-start` ставит значок у логического начала поля, `text-end` — короткий -текст у логического конца, `hint-icon` — значок подсказки в конце рамки. -Логические стороны следуют направлению письма, поэтому та же разметка верна и -справа налево. Своё содержимое передают слоты `start` и `end`; `left-icon` и -`right-text` — другие имена `icon-start` и `text-end`. +Значок в начале подсказывает, что вводить, текст в конце — единицу или +валюту, значок в конце рамки отмечает подсказку. Начало и конец следуют +направлению письма, поэтому та же разметка верна и справа налево. Своё +содержимое передают места `start` и `end`. -:::example {id="smart-components/inputs/composition" label="Значок в начале, текст в конце и значок подсказки"} +:::example {id="smart-components/inputs/composition" label="Значок и текст" preview=inline} ::: +Значки декоративные: смысл, который они несут, повторите в подписи или +подсказке. + ### Маска -`mask` включает маску, `mask-pattern` задаёт шаблон, а `mask-options` — полные -настройки JSON. В шаблоне `0` — цифра, `a` — буква, `*` — любой символ, `{…}` — -постоянная часть, которая входит в значение (`+{7}(000)000-00-00`), `\` -экранирует символ, остальные символы только показываются. `mask-lazy="false"` -показывает весь шаблон сразу, `mask-placeholder-char` задаёт символ пустой -позиции; числовая маска — `{"mask": "Number"}`. Подробно грамматика и -настройки числа — на странице [«Текстовые поля»](/ru/components/forms/inputs/#маска). -Маска помогает вводить значение, но не заменяет нативную и серверную проверку. +`mask`, `mask-pattern`, `mask-options` • [справочник](#атрибуты) + +Маска помогает набрать значение в нужном формате: телефон, номер документа, +код. Наберите в примере десять цифр — скобки, пробел и дефисы встанут сами. + +:::example {id="smart-components/inputs/mask" label="Телефон с маской" preview=sandbox log_console=true log_open=false} +::: + +В шаблоне `0` — цифра, `a` — буква, `*` — любой символ, `{…}` — постоянная +часть: `+{7}` в примере. `\` экранирует символ, остальные символы только +показываются. Значение поля хранит номер вместе с форматом — +`+7 (900) 123-45-67`. `mask-lazy="false"` показывает весь шаблон сразу, +`mask-placeholder-char` задаёт символ пустой позиции, `mask-options` — +полные настройки JSON-строкой, например числовая маска `{"mask": "Number"}`. +Грамматика шаблона и настройки числа — на странице [«Текстовые +поля»](/ru/components/forms/inputs/#маска). + +Учтите: маска помогает вводить, но не проверяет значение — проверка на +сервере нужна всё равно. + +### Наследуемое значение + +`inherited-value`, `inherited-text`, `reset-label` • [справочник](#атрибуты) + +Наследуемое значение действует, пока у поля нет своего: имя из настроек +сайта, значение родительского раздела. Оно видно в пустом поле цветом +подсказки с меткой «наследуется», но не уходит с формой. Напечатайте своё +имя и нажмите кнопку сброса в конце поля — поле вернётся к наследуемому. + +:::example {id="smart-components/inputs/inherited" label="Имя отправителя" preview=sandbox log_console=true log_open=false} +::: + +Перед сбросом приходит отменяемое событие `sf-field-reset`; из кода сброс +делает `resetToInherited()`. + +## Внешний вид + +### Вид + +`appearance` • по умолчанию `bordered` • [справочник](#атрибуты) + +Вид подстраивает поле под фон: рамка — на обычной странице, заливка — в +плотной форме или на цветном фоне. + +:::example {id="smart-components/inputs/appearance" label="Виды" preview=inline} +::: + +### Размер + +`size` • по умолчанию `1` • [справочник](#атрибуты) + +Размер ставит поле в одну строку с кнопками: поле каждого размера одной +высоты с кнопкой того же размера, как в примере. `1/3` — компактный поиск в панели, `3` — +крупный поиск на главной странице. + +:::example {id="smart-components/inputs/sizes" label="Размеры" preview=inline} +::: + +### Скругление + +`radius` • по умолчанию `default` • [справочник](#атрибуты) + +Скругление повторяет кнопки рядом: `rounded` — для поиска рядом с круглыми +кнопками, `square` — для поля, которое стыкуется с другими. -:::example {id="smart-components/inputs/mask" label="Телефон с маской"} +:::example {id="smart-components/inputs/radius" label="Скругление" preview=inline} ::: ## Состояния ### Недоступно -`disabled` выключает поле и исключает его из отправки формы. +`disabled` • [справочник](#атрибуты) + +Недоступное поле нельзя изменить, и оно не уходит с формой. :::example {id="smart-components/inputs/state-disabled" label="Недоступное поле"} ::: ### Только чтение -`readonly` оставляет значение доступным для чтения и копирования. +`readonly` • [справочник](#атрибуты) + +Значение только для чтения можно выделить и скопировать, и оно уходит с +формой. -:::example {id="smart-components/inputs/state-readonly" label="Поле только для чтения"} +:::example {id="smart-components/inputs/state-readonly" label="Только чтение"} ::: ### Ошибка -`invalid` вместе с `error-message` показывает ошибку: сообщение встаёт на -место подсказки и связывается с полем. +`invalid`, `error-message` • [справочник](#атрибуты) -Ошибку снимают так же, как ставят: уберите `invalid` (или `error`) либо -вызовите `setState({invalid: false})`. Поле возвращается к обычному виду -целиком — уходят рамка ошибки, сообщение и `aria-invalid`. +Ошибка показывает, что значение надо исправить: сообщение встаёт на место +подсказки и связывается с полем. Ошибку ставит и снимает страница. Допишите +адрес в примере и выйдите из поля — ошибка уйдёт. -:::example {id="smart-components/inputs/state-error" label="Поле почты с ошибкой"} +:::example {id="smart-components/inputs/state-error" label="Почта с ошибкой" preview=sandbox log_console=true log_open=false} ::: -## Поведение +```js +email.addEventListener('change', () => { + const valid = email.getInputElement().checkValidity(); + email.toggleAttribute('invalid', !valid); +}); +``` + +Поле возвращается к обычному виду целиком — уходят рамка ошибки, сообщение и +`aria-invalid`. -### Значение в форме и события +## Работа из кода + +### Значение и события + +`value`, `default-value` • [справочник](#события) Внутри `` лежит обычный `` с переданным `name`, поэтому -значение попадает в `FormData` и отправку формы. Атрибут `value` задаёт -значение, которым управляет страница: при перерисовке поле показывает его. -`default-value` задаёт только начальное значение, дальше поле хранит то, что -ввёл пользователь. Свойство `value` элемента читает и задаёт текущее значение. - -`input` приходит при каждом изменении во время ввода, `change` — когда -значение зафиксировано. Оба — нативные события внутреннего поля, всплывают -через ``, `detail` у них нет; значение читайте из -`event.target.value`. `focus()` на самом `` переводит фокус во -внутреннее поле; `getInputElement()` возвращает этот ``, если нужны его -собственные методы. +значение попадает в `FormData` и отправку формы. `value` — значение, которым +управляет страница: при перерисовке поле показывает его. `default-value` — +только начальное значение, дальше поле хранит то, что ввёл человек. Свойство +`value` элемента читает и задаёт текущее значение. + +| Событие | Когда приходит | +|:---|:---| +| `input` | при каждом изменении во время ввода | +| `change` | значение принято: человек ушёл из поля | +| `sf-field-reset` | человек нажал сброс к наследуемому значению | + +`input` и `change` — события самого поля, они всплывают через ``; +`detail` у них нет, значение читайте из `event.target.value`. События видны в +журнале под [примером](#пример). ```js const email = document.querySelector('sf-input[name="email"]'); email.addEventListener('change', (event) => { - const valid = event.target.checkValidity(); - email.toggleAttribute('invalid', !valid); - email.setAttribute('error-message', valid ? '' : 'Проверьте адрес почты'); + console.log(event.target.value); }); email.focus(); ``` +`focus()` переводит фокус в поле; `getInputElement()` возвращает сам +``, если нужны его методы, например `checkValidity()`. + ## Доступность -- Задайте видимую подпись в `label`: она связана с полем и даёт ему доступное - имя. Один `placeholder` имени не даёт. Если подпись выводит страница, - передайте `aria-label`. +- Задайте видимую подпись в `label`: она связана с полем и даёт ему имя. + Один `placeholder` имени не даёт. Если подпись выводит страница, передайте + `aria-label`. - Подсказка и сообщение об ошибке связаны с полем через `aria-describedby`; при `invalid` поле получает `aria-invalid` и `aria-errormessage`, а без него `aria-invalid` снимается. - Значки у краёв поля декоративные: смысл, который они несут, повторите в подписи или подсказке. -- Поле — обычная остановка `Tab` с нативным редактированием текста; видимый - фокус сохраняется при клавиатурной навигации. +- Поле — обычная остановка `Tab` с правкой текста как в любом поле; видимый + фокус сохраняется при работе с клавиатуры. ## Справочник @@ -181,34 +277,34 @@ email.focus(); | `size` | строка | `1` | `1/3`, `1/2`, `1`, `2`, `3` | Ступень линейки высот, на которой стоит поле, — высота `sf-button` и `sf-button-icon` того же размера. `sf-input` знает и крайние ступени `1/3` и `3` (компактный поиск в панели, крупный поиск на главной); остальные поля — `1/2`, `1` и `2`. | | `radius` | строка | пусто | пусто, `default`, `square`, `rounded` | Скругление поля, как у `radius` кнопки: пусто и `default` — скруглённый квадрат элементов управления, `square` — без скругления, `rounded` — пилюля (поле поиска рядом с круглыми кнопками). `--sf-input-radius` сильнее. | | `appearance` | строка | `bordered` | `bordered`, `filled`, `hidden` | Вид поля: блок с рамкой, с заливкой или скрытый. | -| `label` | строка | пусто | — | — | -| `required` | да/нет | нет | — | — | -| `placeholder` | строка | пусто | — | — | -| `input-type` | строка | `text` | `text`, `email`, `password`, `search`, `tel`, `url`, `number` | — | -| `autocomplete` | строка | пусто | — | — | -| `inputmode` | строка | пусто | пусто, `none`, `text`, `tel`, `url`, `email`, `numeric`, `decimal`, `search` | — | -| `minlength` | строка | пусто | — | — | -| `maxlength` | строка | пусто | — | — | -| `pattern` | строка | пусто | — | — | +| `label` | строка | пусто | — | Видимая подпись над полем; она же даёт полю доступное имя. | +| `required` | да/нет | нет | — | Обязательное поле: звёздочка у подписи, браузер не отправит форму с пустым полем. | +| `placeholder` | строка | пусто | — | Образец формата в пустом поле; исчезает после ввода и подпись не заменяет. | +| `input-type` | строка | `text` | `text`, `email`, `password`, `search`, `tel`, `url`, `number` | Тип поля: `text`, `email`, `password`, `search`, `tel`, `url` или `number`; от него зависят проверка браузера и экранная клавиатура. | +| `autocomplete` | строка | пусто | — | Подсказка браузеру для автозаполнения, как у `autocomplete` поля HTML: `email`, `tel`, `current-password`. | +| `inputmode` | строка | пусто | пусто, `none`, `text`, `tel`, `url`, `email`, `numeric`, `decimal`, `search` | Какую экранную клавиатуру показать на телефоне: `numeric` — цифры, `email` — с «@», `tel` — номер. | +| `minlength` | строка | пусто | — | Наименьшее число символов; браузер проверяет его перед отправкой формы. | +| `maxlength` | строка | пусто | — | Наибольшее число символов; больше ввести нельзя. | +| `pattern` | строка | пусто | — | Регулярное выражение, которому должно соответствовать значение; браузер проверяет его перед отправкой формы. | | `min` | строка | пусто | — | Наименьшее число в числовом поле (`input-type="number"`); число меньше — ошибка. Для других типов не действует. | | `max` | строка | пусто | — | Наибольшее число в числовом поле (`input-type="number"`); число больше — ошибка. Для других типов не действует. | | `step` | строка | пусто | — | Шаг числового поля: по умолчанию `1`, `any` — любое число; число не по шагу — ошибка. Для других типов не действует. | -| `hint` | строка | пусто | — | — | -| `default-value` | строка | пусто | — | — | -| `name` | строка | пусто | — | — | -| `icon-start` | строка | пусто | — | — | -| `text-end` | строка | пусто | — | — | -| `hint-icon` | строка | пусто | — | — | -| `disabled` | да/нет | нет | — | — | -| `readonly` | да/нет | нет | — | — | -| `invalid` | да/нет | нет | — | — | -| `error-message` | строка | пусто | — | — | -| `mask` | да/нет | нет | — | — | -| `mask-pattern` | строка | пусто | — | — | -| `mask-lazy` | строка | пусто | — | — | -| `mask-placeholder-char` | строка | пусто | — | — | -| `mask-options` | строка | пусто | — | — | -| `value` | строка | пусто | — | — | +| `hint` | строка | пусто | — | Постоянное пояснение под полем; при ошибке его место занимает сообщение. | +| `default-value` | строка | пусто | — | Начальное значение: дальше поле хранит то, что ввёл человек. | +| `name` | строка | пусто | — | Имя поля в форме: под ним значение уходит с отправкой. | +| `icon-start` | строка | пусто | — | Значок в начале поля, имя иконки: `search`, `mail`. | +| `text-end` | строка | пусто | — | Короткий текст в конце поля: единица или валюта, например «₽». | +| `hint-icon` | строка | пусто | — | Значок подсказки в конце рамки, имя иконки, например `info`. | +| `disabled` | да/нет | нет | — | Поле выключено: значение не меняется и не отправляется с формой. | +| `readonly` | да/нет | нет | — | Значение видно, его можно выделить и скопировать, но не изменить; отправляется с формой. | +| `invalid` | да/нет | нет | — | Поле с ошибкой: красная рамка, сообщение из `error-message` под полем, `aria-invalid`. | +| `error-message` | строка | пусто | — | Текст ошибки под полем, пока задан `invalid`. | +| `mask` | да/нет | нет | — | Включает маску ввода из `mask-pattern` или `mask-options`. | +| `mask-pattern` | строка | пусто | — | Шаблон маски: `0` — цифра, `a` — буква, `*` — любой символ, `{…}` — постоянная часть значения. | +| `mask-lazy` | строка | пусто | — | `false` — показать весь шаблон маски сразу, а не по мере ввода. | +| `mask-placeholder-char` | строка | пусто | — | Символ пустой позиции маски, например `_`. | +| `mask-options` | строка | пусто | — | Полные настройки маски JSON-строкой, например `{"mask": "Number"}`. | +| `value` | строка | пусто | — | Значение, которым управляет страница: при перерисовке поле показывает его. | | `aria-label` | строка | пусто | — | Название поля для экранного диктора, если видимой подписи нет. | | `inherited-value` | строка | пусто | — | Значение, к которому поле возвращается, пока у него нет своего, — значение по умолчанию из родительской настройки, бренда, темы. Показано как наследуемое — цветом подсказки и с меткой «наследуется», никогда как значение: значение формы остаётся пустым. Когда у поля появилось своё значение, кнопка сброса возвращает сюда. Числовое поле — это sf-input с inputmode numeric или decimal, оно принимает атрибут так же. | | `inherited-text` | строка | пусто | — | Слова метки, которая видна, пока действует наследуемое значение. Если не задано — на языке страницы («наследуется», «inherited»). | diff --git a/examples/smart-components/inputs/appearance/index.html b/examples/smart-components/inputs/appearance/index.html index b519ae93..8f8768b9 100644 --- a/examples/smart-components/inputs/appearance/index.html +++ b/examples/smart-components/inputs/appearance/index.html @@ -1,10 +1,12 @@ -
-
- - appearance="bordered" -
-
- - appearance="filled" +
+
+
+ + bordered +
+
+ + filled +
diff --git a/examples/smart-components/inputs/composition/index.html b/examples/smart-components/inputs/composition/index.html index bc64a60b..5a39a332 100644 --- a/examples/smart-components/inputs/composition/index.html +++ b/examples/smart-components/inputs/composition/index.html @@ -1,14 +1,16 @@ -
-
- - icon-start="search" -
-
- - text-end="₽" -
-
- - hint hint-icon="info" +
+
+
+ + icon-start +
+
+ + text-end +
+
+ + hint-icon +
diff --git a/examples/smart-components/inputs/inherited/index.html b/examples/smart-components/inputs/inherited/index.html new file mode 100644 index 00000000..04e3a99e --- /dev/null +++ b/examples/smart-components/inputs/inherited/index.html @@ -0,0 +1,6 @@ + + diff --git a/examples/smart-components/inputs/inherited/index.js b/examples/smart-components/inputs/inherited/index.js new file mode 100644 index 00000000..e2431fcf --- /dev/null +++ b/examples/smart-components/inputs/inherited/index.js @@ -0,0 +1,12 @@ +// Until the field has a value of its own it shows the inherited one; the +// reset button returns to it and sends the cancelable sf-field-reset first. +const initialize = async () => { + await customElements.whenDefined('sf-input'); + const field = document.querySelector('sf-input[name="sender"]'); + if (!field || field.dataset.exampleReady === 'true') return; + field.dataset.exampleReady = 'true'; + field.addEventListener('change', () => docara.example.log('change', {value: field.value})); + field.addEventListener('sf-field-reset', (event) => docara.example.log('sf-field-reset', event.detail)); +}; + +initialize(); diff --git a/examples/smart-components/inputs/label/index.html b/examples/smart-components/inputs/label/index.html new file mode 100644 index 00000000..fbc44131 --- /dev/null +++ b/examples/smart-components/inputs/label/index.html @@ -0,0 +1,7 @@ +
+ +
+
+ +
+
diff --git a/examples/smart-components/inputs/mask/index.html b/examples/smart-components/inputs/mask/index.html index 8cc5822c..26429870 100644 --- a/examples/smart-components/inputs/mask/index.html +++ b/examples/smart-components/inputs/mask/index.html @@ -1,11 +1,11 @@ + mask-pattern="+{7} (000) 000-00-00"> diff --git a/examples/smart-components/inputs/mask/index.js b/examples/smart-components/inputs/mask/index.js new file mode 100644 index 00000000..945e899a --- /dev/null +++ b/examples/smart-components/inputs/mask/index.js @@ -0,0 +1,10 @@ +// The value keeps the mask's shape: +7 (900) 123-45-67. +const initialize = async () => { + await customElements.whenDefined('sf-input'); + const phone = document.querySelector('sf-input[name="phone"]'); + if (!phone || phone.dataset.exampleReady === 'true') return; + phone.dataset.exampleReady = 'true'; + phone.addEventListener('change', () => docara.example.log('change', {value: phone.value})); +}; + +initialize(); diff --git a/examples/smart-components/inputs/overview/index.html b/examples/smart-components/inputs/overview/index.html index 5134250e..3670098c 100644 --- a/examples/smart-components/inputs/overview/index.html +++ b/examples/smart-components/inputs/overview/index.html @@ -1,19 +1,12 @@ -
- - - - - -
+
+

Заявка на вебинар

+
+ + + + +
+
+ +
+
diff --git a/examples/smart-components/inputs/overview/index.js b/examples/smart-components/inputs/overview/index.js new file mode 100644 index 00000000..4fd233c5 --- /dev/null +++ b/examples/smart-components/inputs/overview/index.js @@ -0,0 +1,38 @@ +// The page checks the form on submit: a field that does not pass gets +// invalid and its message; once the person corrects it and leaves the field, +// the page takes the error off again. +const MESSAGES = { + name: 'Напишите имя и фамилию', + email: 'Введите адрес целиком, например ivanov@company.ru', +}; + +const check = (field) => { + const input = field.getInputElement(); + const valid = input ? input.checkValidity() : true; + field.toggleAttribute('invalid', !valid); + field.setAttribute('error-message', valid ? '' : (MESSAGES[field.getAttribute('name')] ?? 'Проверьте значение')); + return valid; +}; + +const initialize = async () => { + await customElements.whenDefined('sf-input'); + const form = document.querySelector('#inputs-signup'); + if (!form || form.dataset.exampleReady === 'true') return; + form.dataset.exampleReady = 'true'; + const fields = [...form.querySelectorAll('sf-input')]; + form.addEventListener('change', (event) => { + const field = event.target.closest('sf-input'); + if (!field) return; + docara.example.log('change', {name: field.getAttribute('name'), value: field.value}); + if (field.hasAttribute('invalid')) check(field); + }); + // The example frame may not submit a form, so the button checks the fields + // and logs what the form would send. + form.querySelector('[data-example-send]').addEventListener('click', () => { + const valid = fields.map(check).every(Boolean); + docara.example.log('FormData', valid ? Object.fromEntries(new FormData(form)) : {valid: false}); + if (!valid) fields.find((field) => field.hasAttribute('invalid'))?.focus(); + }); +}; + +initialize(); diff --git a/examples/smart-components/inputs/radius/index.html b/examples/smart-components/inputs/radius/index.html new file mode 100644 index 00000000..d5913a92 --- /dev/null +++ b/examples/smart-components/inputs/radius/index.html @@ -0,0 +1,16 @@ +
+
+
+ + default +
+
+ + square +
+
+ + rounded +
+
+
diff --git a/examples/smart-components/inputs/sizes/index.html b/examples/smart-components/inputs/sizes/index.html index dd81e740..df92c2fb 100644 --- a/examples/smart-components/inputs/sizes/index.html +++ b/examples/smart-components/inputs/sizes/index.html @@ -1,14 +1,24 @@ -
-
- - size="1/2" -
-
- - size="1" -
-
- - size="2" +
+
+
+
+ 1/3 +
+
+
+ 1/2 +
+
+
+ 1 +
+
+
+ 2 +
+
+
+ 3 +
diff --git a/examples/smart-components/inputs/state-error/index.js b/examples/smart-components/inputs/state-error/index.js new file mode 100644 index 00000000..bc6e0a38 --- /dev/null +++ b/examples/smart-components/inputs/state-error/index.js @@ -0,0 +1,17 @@ +// The page sets the error and takes it off: after the person corrects the +// address and leaves the field, the check passes and invalid goes. +const initialize = async () => { + await customElements.whenDefined('sf-input'); + const email = document.querySelector('sf-input[name="email"]'); + if (!email || email.dataset.exampleReady === 'true') return; + email.dataset.exampleReady = 'true'; + email.addEventListener('change', () => { + const valid = email.getInputElement().checkValidity(); + email.toggleAttribute('invalid', !valid); + if (valid) email.removeAttribute('error-message'); + else email.setAttribute('error-message', 'Введите адрес целиком, например ivanov@example.com'); + docara.example.log('change', {value: email.value, invalid: !valid}); + }); +}; + +initialize(); diff --git a/examples/smart-components/inputs/types/index.html b/examples/smart-components/inputs/types/index.html index 71d33976..c24f325a 100644 --- a/examples/smart-components/inputs/types/index.html +++ b/examples/smart-components/inputs/types/index.html @@ -1,18 +1,28 @@ -
-
- - input-type="email" -
-
- - input-type="password" -
-
- - input-type="tel" -
-
- - input-type="url" +
+
+
+ + email +
+
+ + password +
+
+ + tel +
+
+ + url +
+
+ + search +
+
+ + number +
From 58f05cdd768ad562afffe853052e77e872addaf9 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 04:07:05 +0300 Subject: [PATCH 04/53] Quantity page: fourth standard, a cart, steppers as wide as their number, real labels and error Docs review QT-01 (example widths fit the number), QT-02 (the snippet queries sf-input-quantity), QT-05 (no money stepper), QT-06 (more than in stock), QT-07 (real field labels in specimen grids), QT-09 (no error alias), QT-10 (a cart with line totals and the example log), and FA-01, FA-02, FA-04, FA-05, FA-10. Co-Authored-By: Claude Opus 5.5 --- config/reference-pages.json | 2 +- content/ru/smart/forms/quantity.md | 173 +++++++++++------- .../quantity/bounds/index.html | 7 +- .../quantity/decimals/index.html | 7 +- .../quantity/inherited/index.html | 5 +- .../quantity/overview/index.html | 33 +++- .../quantity/overview/index.js | 27 +++ .../quantity/sizes/index.html | 22 ++- .../quantity/state-bound/index.html | 21 ++- .../quantity/state-disabled/index.html | 5 +- .../quantity/state-error/index.html | 5 +- .../quantity/state-readonly/index.html | 5 +- 12 files changed, 224 insertions(+), 88 deletions(-) create mode 100644 examples/smart-components/quantity/overview/index.js diff --git a/config/reference-pages.json b/config/reference-pages.json index 3e71d14a..cc71deb7 100644 --- a/config/reference-pages.json +++ b/config/reference-pages.json @@ -2062,7 +2062,7 @@ }, "smart/forms/quantity.md": { "kind": "smart", - "standard": 3 + "standard": 4 }, "smart/navigation/menu.md": { "kind": "smart", diff --git a/content/ru/smart/forms/quantity.md b/content/ru/smart/forms/quantity.md index 51175e75..d2b5d7da 100644 --- a/content/ru/smart/forms/quantity.md +++ b/content/ru/smart/forms/quantity.md @@ -1,140 +1,181 @@ --- title: "Количество" -description: "Числовое поле с кнопками «−» и «+»: границы, шаг, знаки после запятой и формат числа языка страницы." +description: "Даёт набрать число или поменять его кнопками «−» и «+» в заданных границах и по шагу." --- # Количество :badge[sf-input-quantity]{variant=tonal scheme=primary size=1/2} :badge[1.3.3]{variant=outline scheme=on-surface size=1/2} -Количество — поле для числа с границами, которое удобно менять по шагам: -количество, сумма, целое свойство. Для значения, которое выбирают -перетаскиванием по шкале, берите [Слайдер -диапазона](/ru/smart/forms/range-slider/), для произвольного числа без кнопок -— [Текстовые поля](/ru/smart/forms/inputs/) с `inputmode="decimal"`. +Количество — поле для небольшого числа, которое удобно менять по шагу: +товаров в корзине, гостей, записей на странице. Число можно набрать или +поменять кнопками «−» и «+»; за границы оно не выходит. -То же на классах — [Количество](/ru/components/forms/quantity/): -`` собирает разметку поля количества и добавляет границы, шаг, -клавиатуру и формат числа языка страницы. +То же на классах — [Количество](/ru/components/forms/quantity/): для +статической разметки без JavaScript. ## Пример -:::example {id="smart-components/quantity/overview" label="Количество от 1 до 99"} -::: +Поменяйте количество в корзине: сумма строки и итог пересчитаются. У ручки +верхняя граница — остаток на складе, «+» на ней недоступна. Журнал под +примером показывает события. -## Разметка +:::example {id="smart-components/quantity/overview" label="Корзина" preview=sandbox log_console=true log_open=false} +::: -Минимальная разметка — тег с `label`, `name` и, при необходимости, `value`, -`min` и `max`: +Полю нужны подпись, имя для формы и границы: ```html - + ``` -- `value` пишется с точкой (`2567.5`); в поле число показано по правилам - языка страницы — на русской странице с запятой и разделителем тысяч. -- Форма получает значение из скрытого input с именем `name`: с точкой и без - разделителей (`2567.50`), что бы ни было видно в поле. -- Подпись выводит `label` (или слот `label`), пояснение — `hint`. Если подпись - печатает страница, задайте `aria-label`. +## Когда брать -## Варианты +- **Небольшое целое число, которое меняют по шагу**: количество товара, + гостей, мест, записей на странице. +- **Число с известными границами**: от 1 до остатка на складе, от 5 до 100. -### Размер +## Когда не брать -`size` выбирает ступень полей формы: `1/2`, `1` или `2`. +- **Сумма денег, длина, вес покупателя** — число печатают целиком, кнопки + только мешают: [Текстовое поле](/ru/smart/forms/inputs/#тип-значения) с + `input-type="number"`. +- **Значение на шкале, которое важнее видеть, чем набрать**: громкость, + диапазон цен — [Слайдер диапазона](/ru/smart/forms/range-slider/). +- **Выбор из нескольких известных значений** — + [Радиокнопки](/ru/smart/forms/radio/). -:::example {id="smart-components/quantity/sizes" label="Размеры 1/2, 1 и 2"} -::: +## Частые задачи ### Границы и шаг -`min` и `max` задают границы: введённое число за границей подтягивается к -ней. `step` — на сколько меняют значение «−», «+» и стрелки, `page-step` — -клавиши `PageUp` и `PageDown` (по умолчанию десять шагов). +`min`, `max`, `step`, `page-step` • по умолчанию `step="1"` • [справочник](#атрибуты) -:::example {id="smart-components/quantity/bounds" label="Шаг 5 и год без разделителя"} +Границы не дают выйти за допустимое: введённое число за границей +подтягивается к ней. Шаг — на сколько меняют значение «−», «+» и стрелки, +`page-step` — клавиши `PageUp` и `PageDown` (по умолчанию десять шагов). + +:::example {id="smart-components/quantity/bounds" label="Шаг и год"} ::: -### Формат числа +`grouping="false"` пишет цифры подряд, без разделителя тысяч, — для года +или кода, как во втором поле. + +### Дробное число -Число знаков после запятой берётся из `step` (у `0.25` их два); `precision` +`step`, `precision` • [справочник](#атрибуты) + +Число знаков после запятой берётся из шага: у `0.25` их два. `precision` задаёт его явно. Без знаков после запятой десятичный разделитель ввести -нельзя. `grouping="false"` пишет цифры подряд, без разделителя тысяч, — для -года или кода. +нельзя. -:::example {id="smart-components/quantity/decimals" label="Вес с шагом 0,25 и сумма с копейками"} +:::example {id="smart-components/quantity/decimals" label="Вес и масштаб"} ::: -## Состояния +`value` пишется с точкой (`2.5`); в поле число показано по правилам языка +страницы — на русской странице с запятой и разделителем тысяч. Форма получает +значение с точкой и без разделителей, что бы ни было видно в поле. -### На границе +### Наследуемое значение -Когда значение стоит на `min` или `max`, кнопка в сторону границы -недоступна. +`inherited-value`, `inherited-text`, `reset-label` • [справочник](#атрибуты) -:::example {id="smart-components/quantity/state-bound" label="Значение на нижней и на верхней границе"} +Наследуемое значение действует, пока у поля нет своего, — например, +значение родительского раздела. Оно видно в пустом поле цветом подсказки с +меткой «наследуется», но не уходит с формой. Поставьте своё число и нажмите +кнопку сброса — поле вернётся к наследуемому. + +:::example {id="smart-components/quantity/inherited" label="Записей на странице"} ::: -### Наследуемое значение +## Внешний вид -`inherited-value` — число, которое действует, пока у поля нет своего, — -например, значение родительского раздела. Оно видно в пустом поле цветом -подсказки с меткой «наследуется», но не попадает в форму. Когда у поля -есть своё число, кнопка сброса возвращает к наследуемому. +### Размер + +`size` • по умолчанию `1` • [справочник](#атрибуты) -:::example {id="smart-components/quantity/inherited" label="Пустое поле с наследуемым значением"} +Размер ставит поле в одну строку с кнопками и другими полями: поле каждого +размера одной высоты с кнопкой того же размера. + +:::example {id="smart-components/quantity/sizes" label="Размеры"} +::: + +Ширину поля задаёт страница: в примерах на этой странице она по числу, а не +на всю колонку. + +## Состояния + +### На границе + +`min`, `max` • [справочник](#атрибуты) + +На границе кнопка в её сторону недоступна: дальше идти некуда. + +:::example {id="smart-components/quantity/state-bound" label="На границах"} ::: ### Только чтение -`readonly` — значение видно и отправляется, но кнопки и клавиши его не -меняют. +`readonly` • [справочник](#атрибуты) -:::example {id="smart-components/quantity/state-readonly" label="Поле только для чтения"} +Значение видно и уходит с формой, но кнопки и клавиши его не меняют. + +:::example {id="smart-components/quantity/state-readonly" label="Только чтение"} ::: ### Недоступно -`disabled` — значение видно, но не меняется и не отправляется с формой. +`disabled` • [справочник](#атрибуты) + +Значение видно, но не меняется и не уходит с формой. :::example {id="smart-components/quantity/state-disabled" label="Недоступное поле"} ::: ### Ошибка -`invalid` (или `error`) и `error-message` показывают ошибку вместо пояснения; -поле получает `aria-invalid`. +`invalid`, `error-message` • [справочник](#атрибуты) -:::example {id="smart-components/quantity/state-error" label="Ошибка с сообщением"} +Ошибка показывает, что число не подходит, хотя оно в границах поля: например, +на складе меньше, чем просят. Сообщение встаёт на место пояснения, поле +получает `aria-invalid`. + +:::example {id="smart-components/quantity/state-error" label="Больше, чем на складе"} ::: -## Поведение +## Работа из кода ### Значение и события -Свойство `value` возвращает число или `null`. Событие `input` приходит, пока -значение меняется: при вводе — каждый раз, когда текст читается как другое -число, при нажатии кнопки или клавиши — на каждом шаге. Событие `change` -приходит один раз на изменение: после ввода (по `Enter` или уходу из поля), -после нажатия кнопки (одно на всё удержание) или клавиши. В `detail` обоих — -`value`, `text`, `name` и `source`. +`value` • [справочник](#события) -`setValue()`, `clear()`, `stepUp()` и `stepDown()` меняют число из кода -без событий; `setError()` показывает или снимает ошибку. +Свойство `value` возвращает число или `null`. + +| Событие | Когда приходит | +|:---|:---| +| `input` | значение сдвинулось: при вводе — когда текст читается как другое число, на кнопке или клавише — на каждом шаге | +| `change` | значение сохранено: после ввода (по `Enter` или уходу из поля), один раз на всё удержание кнопки или на нажатие клавиши | +| `sf-field-reset` | человек нажал сброс к наследуемому значению | + +В `detail` событий `input` и `change` — `value`, `text`, `name` и `source`: +откуда пришло изменение. События видны в журнале под [примером](#пример). ```js -const count = document.querySelector('sf-quantity[name="count"]'); -count.addEventListener('change', (event) => { +const guests = document.querySelector('sf-input-quantity[name="guests"]'); +guests.addEventListener('change', (event) => { updateTotal(event.detail.value); }); ``` +`setValue()`, `clear()`, `stepUp()` и `stepDown()` меняют число из кода без +событий; `setError()` показывает или снимает ошибку. + ## Доступность - Поле — `spinbutton` с `aria-valuenow`, `aria-valuemin`, `aria-valuemax` и `aria-valuetext` в формате языка страницы; его называет `label` или - `aria-label`. + `aria-label`. В корзине из примера подписи нет, поэтому каждое поле + называет товар через `aria-label`. - `↑` и `↓` меняют значение на шаг, `PageUp` и `PageDown` — на десять шагов, `Home` и `End` переходят к границам, `Enter` сохраняет ввод, `Escape` отменяет его. @@ -220,5 +261,5 @@ count.addEventListener('change', (event) => { ## См. также - [Слайдер диапазона](/ru/smart/forms/range-slider/) — значение, которое выбирают по шкале. -- [Текстовые поля](/ru/smart/forms/inputs/) — произвольное число без кнопок. +- [Текстовое поле](/ru/smart/forms/inputs/) — произвольное число без кнопок. - [Количество на классах](/ru/components/forms/quantity/) — то же поле в статической разметке. diff --git a/examples/smart-components/quantity/bounds/index.html b/examples/smart-components/quantity/bounds/index.html index c04593e4..dd18316f 100644 --- a/examples/smart-components/quantity/bounds/index.html +++ b/examples/smart-components/quantity/bounds/index.html @@ -1,4 +1,7 @@ +
- - +
+
diff --git a/examples/smart-components/quantity/decimals/index.html b/examples/smart-components/quantity/decimals/index.html index eff7be3c..04d470da 100644 --- a/examples/smart-components/quantity/decimals/index.html +++ b/examples/smart-components/quantity/decimals/index.html @@ -1,4 +1,7 @@ +
- - +
+
diff --git a/examples/smart-components/quantity/inherited/index.html b/examples/smart-components/quantity/inherited/index.html index de0bb176..903c9960 100644 --- a/examples/smart-components/quantity/inherited/index.html +++ b/examples/smart-components/quantity/inherited/index.html @@ -1 +1,4 @@ - + +
diff --git a/examples/smart-components/quantity/overview/index.html b/examples/smart-components/quantity/overview/index.html index d3d590a2..68957b92 100644 --- a/examples/smart-components/quantity/overview/index.html +++ b/examples/smart-components/quantity/overview/index.html @@ -1 +1,32 @@ - + +
+

Корзина

+
+
+ Блокнот в клетку, А5 + 1 290 ₽ за штуку +
+ + +
+
+
+ Ручка гелевая, синяя + 350 ₽ за штуку · на складе 8 +
+ + +
+
+ Итого + +
+
diff --git a/examples/smart-components/quantity/overview/index.js b/examples/smart-components/quantity/overview/index.js new file mode 100644 index 00000000..338ea20d --- /dev/null +++ b/examples/smart-components/quantity/overview/index.js @@ -0,0 +1,27 @@ +// Each line total follows its quantity; change arrives once per change the +// person makes, input on every step while a button is held. +const money = new Intl.NumberFormat('ru-RU', {style: 'currency', currency: 'RUB', maximumFractionDigits: 0}); + +const initialize = async () => { + await customElements.whenDefined('sf-input-quantity'); + const cart = document.querySelector('#quantity-cart'); + if (!cart || cart.dataset.exampleReady === 'true') return; + cart.dataset.exampleReady = 'true'; + const update = () => { + let total = 0; + for (const line of cart.querySelectorAll('.cart-line')) { + const sum = Number(line.dataset.price) * (line.querySelector('sf-input-quantity').value ?? 0); + line.querySelector('.cart-sum').textContent = money.format(sum); + total += sum; + } + cart.querySelector('#quantity-cart-total').textContent = money.format(total); + }; + cart.addEventListener('input', update); + cart.addEventListener('change', (event) => { + update(); + docara.example.log('change', event.detail); + }); + update(); +}; + +initialize(); diff --git a/examples/smart-components/quantity/sizes/index.html b/examples/smart-components/quantity/sizes/index.html index 82cd8761..d0d1a4f4 100644 --- a/examples/smart-components/quantity/sizes/index.html +++ b/examples/smart-components/quantity/sizes/index.html @@ -1,5 +1,19 @@ -
- - - + +
+
+
+
+ 1/2 +
+
+
+ 1 +
+
+
+ 2 +
+
diff --git a/examples/smart-components/quantity/state-bound/index.html b/examples/smart-components/quantity/state-bound/index.html index 2698a54d..760a40cf 100644 --- a/examples/smart-components/quantity/state-bound/index.html +++ b/examples/smart-components/quantity/state-bound/index.html @@ -1,10 +1,15 @@ -
-
- - value="1" min="1" -
-
- - value="8" max="8" + +
+
+
+
+ на нижней границе +
+
+
+ на верхней границе +
diff --git a/examples/smart-components/quantity/state-disabled/index.html b/examples/smart-components/quantity/state-disabled/index.html index 6cd46394..c8349d02 100644 --- a/examples/smart-components/quantity/state-disabled/index.html +++ b/examples/smart-components/quantity/state-disabled/index.html @@ -1 +1,4 @@ - + +
diff --git a/examples/smart-components/quantity/state-error/index.html b/examples/smart-components/quantity/state-error/index.html index 82af54ad..ea95637f 100644 --- a/examples/smart-components/quantity/state-error/index.html +++ b/examples/smart-components/quantity/state-error/index.html @@ -1 +1,4 @@ - + +
diff --git a/examples/smart-components/quantity/state-readonly/index.html b/examples/smart-components/quantity/state-readonly/index.html index 91d5d8f4..7727dafc 100644 --- a/examples/smart-components/quantity/state-readonly/index.html +++ b/examples/smart-components/quantity/state-readonly/index.html @@ -1 +1,4 @@ - + +
From b4c460cfb05db8ac58f3fd336f659aef16ba51ca Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 04:07:05 +0300 Subject: [PATCH 05/53] Textarea page: fourth standard, the message composer fills its row, a support request, inherited text Docs review TA-01 (the composer's field takes the free width), TA-03 (the snippet queries sf-input-textarea), TA-05 (flush is a value of the look table, the composer a task, no selector jargon), TA-06 (focus() folded into working from code), TA-08 (inherited value), TA-09 (a support request form), and FA-01, FA-02, FA-04, FA-05, FA-06, FA-10. Co-Authored-By: Claude Opus 5.5 --- config/reference-pages.json | 2 +- config/smart-reference.json | 23 ++ content/ru/smart/forms/textarea.md | 255 +++++++++++------- .../textarea/appearance/index.html | 18 +- .../textarea/flush/index.html | 24 +- .../textarea/inherited/index.html | 7 + .../textarea/inherited/index.js | 12 + .../textarea/overview/index.html | 25 +- .../textarea/overview/index.js | 37 +++ .../smart-components/textarea/rows/index.html | 18 +- .../textarea/sizes/index.html | 26 +- 11 files changed, 297 insertions(+), 150 deletions(-) create mode 100644 examples/smart-components/textarea/inherited/index.html create mode 100644 examples/smart-components/textarea/inherited/index.js create mode 100644 examples/smart-components/textarea/overview/index.js diff --git a/config/reference-pages.json b/config/reference-pages.json index cc71deb7..43355d5b 100644 --- a/config/reference-pages.json +++ b/config/reference-pages.json @@ -653,7 +653,7 @@ }, "smart/forms/textarea.md": { "kind": "smart", - "standard": 3 + "standard": 4 }, "smart/navigation/breadcrumbs.md": { "kind": "smart", diff --git a/config/smart-reference.json b/config/smart-reference.json index 56b36504..2597cec4 100644 --- a/config/smart-reference.json +++ b/config/smart-reference.json @@ -98,6 +98,29 @@ "mask-placeholder-char": "Символ пустой позиции маски, например `_`", "mask-options": "Полные настройки маски JSON-строкой, например `{\"mask\": \"Number\"}`", "value": "Значение, которым управляет страница: при перерисовке поле показывает его" + }, + "input-textarea": { + "size": "Ступень высоты поля: `1/2`, `1` или `2`, как у текстового поля и кнопки того же размера", + "label": "Видимая подпись над полем; она же даёт полю доступное имя", + "required": "Обязательное поле: звёздочка у подписи, браузер не отправит форму с пустым полем", + "placeholder": "Образец формата в пустом поле; исчезает после ввода и подпись не заменяет", + "autocomplete": "Подсказка браузеру для автозаполнения, как у `autocomplete` поля HTML: `email`, `tel`, `current-password`", + "inputmode": "Какую экранную клавиатуру показать на телефоне: `numeric` — цифры, `email` — с «@», `tel` — номер", + "minlength": "Наименьшее число символов; браузер проверяет его перед отправкой формы", + "maxlength": "Наибольшее число символов; больше ввести нельзя", + "spell-check": "Проверка орфографии браузером; `false` — для кода, адресов, артикулов", + "wrap": "Как браузер отправляет переносы: `soft` — только переносы, которые ввёл человек, `hard` — и переносы по ширине поля", + "hint": "Постоянное пояснение под полем; при ошибке его место занимает сообщение", + "name": "Имя поля в форме: под ним значение уходит с отправкой", + "rows": "Начальная высота в строках; человек может растянуть поле по вертикали. С `auto-grow` не действует", + "disabled": "Поле выключено: значение не меняется и не отправляется с формой", + "readonly": "Значение видно, его можно выделить и скопировать, но не изменить; отправляется с формой", + "invalid": "Поле с ошибкой: красная рамка, сообщение из `error-message` под полем, `aria-invalid`", + "error-message": "Текст ошибки под полем, пока задан `invalid`", + "mask": "Включает маску ввода из `mask-pattern` или `mask-options`", + "mask-pattern": "Шаблон маски: `0` — цифра, `a` — буква, `*` — любой символ, `{…}` — постоянная часть значения", + "mask-options": "Полные настройки маски JSON-строкой, например `{\"mask\": \"Number\"}`", + "value": "Значение, которым управляет страница: при перерисовке поле показывает его" } }, "slots": { diff --git a/content/ru/smart/forms/textarea.md b/content/ru/smart/forms/textarea.md index 428591da..c2cb8470 100644 --- a/content/ru/smart/forms/textarea.md +++ b/content/ru/smart/forms/textarea.md @@ -1,155 +1,206 @@ --- title: "Многострочное поле" -description: "Многострочное поле формы с подписью, подсказкой и сообщением об ошибке." +description: "Принимает текст в несколько строк — комментарий, описание, отзыв — с подписью, подсказкой и ошибкой." --- # Многострочное поле :badge[sf-input-textarea]{variant=tonal scheme=primary size=1/2} :badge[1.7.4]{variant=outline scheme=on-surface size=1/2} -Многострочное поле — для комментариев, описаний и другого свободного текста. -Для короткого значения в одну строку берите [Текстовые -поля](/ru/smart/forms/inputs/). +Многострочное поле принимает свободный текст в несколько строк: комментарий, +описание задачи, отзыв, сообщение. Подпись, подсказка и сообщение об ошибке +устроены как у [текстового поля](/ru/smart/forms/inputs/). -То же на классах — [Многострочное поле](/ru/components/forms/textarea/): -`` собирает его разметку из атрибутов. +То же на классах — [Многострочное поле](/ru/components/forms/textarea/): для +статической разметки без JavaScript. ## Пример -:::example {id="smart-components/textarea/overview" label="Результат"} -::: +Заполните обращение и нажмите «Отправить». Слишком короткое описание покажет +ошибку; допишите текст и выйдите из поля — ошибка уйдёт. Журнал под примером +показывает события и отправленные значения. -## Разметка +:::example {id="smart-components/textarea/overview" label="Обращение" preview=sandbox log_console=true log_open=false} +::: Полю нужны подпись и имя для формы: ```html - + ``` -`label` — видимая подпись поля, `hint` — постоянное пояснение под ним, -`placeholder` — образец ожидаемого текста. Placeholder исчезает, как только -пользователь начинает печатать, поэтому подпись и подсказку он не заменяет. -`required` ставит звёздочку у подписи и делает обязательным нативное поле. -Вместо строк подсказки и ошибки можно передать разметку в слоты `hint` и -`error`. +## Когда брать -`maxlength` и `minlength` ограничивают длину текста средствами браузера; -сообщите ограничение в `hint`, как в примере. +- **Свободный текст длиннее одной строки**: комментарий, описание, отзыв, + сопроводительное письмо. +- **Строка ввода сообщения**, которая растёт по тексту, — вид `flush` внутри + своего блока, см. [Строка ввода сообщения](#строка-ввода-сообщения). -## Варианты +## Когда не брать -### Вид +- **Значение в одну строку**: имя, почта, тема — [Текстовое + поле](/ru/smart/forms/inputs/). +- **Текст с заголовками, списками и ссылками** — [Форматированный + текст](/ru/smart/forms/rich-text/). + +## Частые задачи + +### Длина текста + +`minlength`, `maxlength` • [справочник](#атрибуты) + +Ограничения длины проверяет браузер: больше `maxlength` символов ввести +нельзя, короче `minlength` — форма не отправится. Назовите ограничение в +подсказке, как в [примере](#пример): человек должен знать его до ошибки. + +### Высота по тексту -`appearance` выбирает вид поля: `bordered` — с рамкой, `filled` — с заливкой, -`flush` — без своего блока, внутри составного элемента. +`auto-grow`, `min-rows`, `max-rows` • по умолчанию `min-rows="1"` • [справочник](#атрибуты) -:::example {id="smart-components/textarea/appearance" label="Поле с рамкой и с заливкой"} +Поле по высоте текста не занимает места, пока текста мало, и растёт по +строке. После `max-rows` строк оно остаётся этой высоты и прокручивается +внутри; при удалении текста уменьшается. Напечатайте в первом поле несколько +строк. + +:::example {id="smart-components/textarea/auto-grow" label="Высота по тексту" preview=inline} ::: -### Без рамки +Одна строка равна высоте текстового поля и кнопки того же размера. `rows` с +`auto-grow` не действует, и поле не растягивают вручную. `Enter` переносит +строку: отправка по `Enter` — поведение окна чата, а не поля. + +### Строка ввода сообщения -`appearance="flush"` — поле внутри составного элемента, например строки ввода -сообщения, где рамку, фон, тень и фокус рисует общий блок. У поля нет своей -границы, фона, тени, скругления, боковых отступов, ручки изменения размера и -своего кольца фокуса — ни при наведении, ни при нажатии, ни при фокусе с -клавиатуры. Текст остаётся на размерной шкале: отступ сверху и снизу равен -отступу поля с рамкой вместе с её линией, поэтому одна строка равна высоте -`sf-input` или `sf-button` того же размера, а каждая следующая добавляет -высоту строки этого размера. `auto-grow`, `min-rows` и `max-rows` работают как -у других видов. +`appearance="flush"` • [справочник](#атрибуты) -Фокус показывает составной блок через `:has(:focus-visible)` — поле само его -не рисует, как в примере. Назовите поле: `label` или `aria-label`. В режиме -высокой контрастности поле сохраняет системный контур фокуса. +Поле без своей рамки встраивают в общий блок с кнопкой отправки: рамку, фон +и выделение фокуса рисует блок, а поле занимает всю его ширину и растёт по +тексту. -:::example {id="smart-components/textarea/flush" label="Поле без рамки в строке ввода сообщения"} +:::example {id="smart-components/textarea/flush" label="Строка сообщения"} ::: -### Размер +Блок примера — своя разметка страницы: поле в обёртке, которая забирает +свободную ширину, и кнопка рядом. Фокус блок показывает, когда фокус внутри +него, — поле само рамку фокуса не рисует. Назовите поле через `label` или +`aria-label`, как в примере. + +### Наследуемое значение + +`inherited-value`, `inherited-text`, `reset-label` • [справочник](#атрибуты) + +Наследуемый текст действует, пока у поля нет своего: подпись из настроек +сайта, текст родительского раздела. Он виден в пустом поле цветом подсказки +с меткой «наследуется», но не уходит с формой. Напишите свою подпись и +нажмите кнопку сброса — поле вернётся к наследуемой. + +:::example {id="smart-components/textarea/inherited" label="Подпись в письмах" preview=sandbox log_console=true log_open=false} +::: -`size` выбирает ступень размерной шкалы: `1/2`, `1` или `2`; берите ступень -остальных полей формы. +## Внешний вид -:::example {id="smart-components/textarea/sizes" label="Размеры 1/2, 1 и 2"} +### Вид + +`appearance` • по умолчанию `bordered` • [справочник](#атрибуты) + +Вид подстраивает поле под фон, как у текстового поля. + +:::example {id="smart-components/textarea/appearance" label="Виды" preview=inline} ::: -### Изменение размера +| Значение | Когда брать | +|:---|:---| +| `bordered` | обычная форма на странице | +| `filled` | плотная форма или цветной фон | +| `flush` | поле внутри своего блока, например [строки ввода сообщения](#строка-ввода-сообщения) | -`rows` задаёт начальную высоту в строках, по умолчанию три. Пользователь -может растянуть поле по вертикали. +### Размер + +`size` • по умолчанию `1` • [справочник](#атрибуты) -:::example {id="smart-components/textarea/rows" label="Поля в две и в шесть строк"} +Размер берите тот же, что у остальных полей формы: отступы и текст поля +совпадают с текстовым полем того же размера. + +:::example {id="smart-components/textarea/sizes" label="Размеры" preview=inline} ::: -### Высота по тексту +### Высота -`auto-grow` делает высоту поля равной его тексту: поле начинается с -`min-rows` строк (по умолчанию одна), растёт по строке и после `max-rows` -остаётся этой высоты и прокручивается внутри. Без `max-rows` предела нет; -`max-rows` меньше `min-rows` считается равным `min-rows`. При удалении текста -поле уменьшается. `rows` с `auto-grow` не действует, и поле не растягивают -вручную. Одна строка равна высоте `sf-input` или `sf-button` того же размера. -`Enter` переносит строку: отправка по `Enter` — поведение окна чата, а не поля. +`rows` • по умолчанию `3` • [справочник](#атрибуты) -:::example {id="smart-components/textarea/auto-grow" label="Поле по высоте текста"} +Высота в строках подсказывает, сколько текста ждут: две строки — коротко, +шесть — подробно. Человек может растянуть поле по вертикали. + +:::example {id="smart-components/textarea/rows" label="Две и шесть строк" preview=inline} ::: ## Состояния ### Недоступно -`disabled` выключает поле и исключает его из отправки формы. +`disabled` • [справочник](#атрибуты) + +Недоступное поле нельзя изменить, и оно не уходит с формой. :::example {id="smart-components/textarea/state-disabled" label="Недоступное поле"} ::: ### Только чтение -`readonly` оставляет текст доступным для чтения и копирования. +`readonly` • [справочник](#атрибуты) -:::example {id="smart-components/textarea/state-readonly" label="Поле только для чтения"} +Текст только для чтения можно выделить и скопировать, и он уходит с формой. + +:::example {id="smart-components/textarea/state-readonly" label="Только чтение"} ::: ### Ошибка -`invalid` вместе с `error-message` показывает ошибку: сообщение встаёт на -место подсказки и связывается с полем. +`invalid`, `error-message` • [справочник](#атрибуты) + +Ошибка показывает, что текст надо исправить: сообщение встаёт на место +подсказки и связывается с полем. Ошибку ставит и снимает страница, как в +[примере](#пример). -:::example {id="smart-components/textarea/state-error" label="Обязательное поле с ошибкой"} +:::example {id="smart-components/textarea/state-error" label="Поле с ошибкой"} ::: -## Поведение +## Работа из кода -### Значение в форме и события +### Значение и события -Внутри `` лежит обычный ` - + diff --git a/examples/components/theme/preferences/index.html b/examples/components/theme/preferences/index.html index a97850ae..b28a2925 100644 --- a/examples/components/theme/preferences/index.html +++ b/examples/components/theme/preferences/index.html @@ -1,5 +1,5 @@
- +
diff --git a/examples/components/theme/settings-card/index.html b/examples/components/theme/settings-card/index.html index e56ad15a..e231ffe3 100644 --- a/examples/components/theme/settings-card/index.html +++ b/examples/components/theme/settings-card/index.html @@ -1,7 +1,7 @@
- +
diff --git a/redirects.json b/redirects.json index d11788c1..5e6563ce 100644 --- a/redirects.json +++ b/redirects.json @@ -376,7 +376,7 @@ }, { "from": "ru/components/reference/slider", - "to": "ru/components/forms/slider" + "to": "ru/components/overlays-media/slider" }, { "from": "ru/components/reference/spinner", @@ -452,7 +452,7 @@ }, { "from": "ru/components/slider", - "to": "ru/components/forms/slider" + "to": "ru/components/overlays-media/slider" }, { "from": "ru/components/spinner", @@ -896,7 +896,7 @@ }, { "from": "ru/framework-components/reference/slider", - "to": "ru/components/forms/slider" + "to": "ru/components/overlays-media/slider" }, { "from": "ru/framework-components/reference/spinner", @@ -972,7 +972,7 @@ }, { "from": "ru/framework-components/slider", - "to": "ru/components/forms/slider" + "to": "ru/components/overlays-media/slider" }, { "from": "ru/framework-components/spinner", @@ -2120,7 +2120,7 @@ }, { "from": "ru/smart-components/reference/slider", - "to": "ru/smart/forms/slider" + "to": "ru/smart/content/slider" }, { "from": "ru/smart-components/reference/spinner", @@ -3796,7 +3796,7 @@ }, { "from": "ru/smart-components/forms/slider", - "to": "ru/smart/forms/slider" + "to": "ru/smart/content/slider" }, { "from": "ru/smart-components/forms/switch", @@ -3881,6 +3881,14 @@ { "from": "ru/smart-components/overlays/resizer", "to": "ru/smart/overlays/resizer" + }, + { + "from": "ru/components/forms/slider", + "to": "ru/components/overlays-media/slider" + }, + { + "from": "ru/smart/forms/slider", + "to": "ru/smart/content/slider" } ] } From 01c470f3a946b49f18378f180faa5ab7f9a62142 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 06:16:19 +0300 Subject: [PATCH 14/53] Smart forms pages: current event and selector names only, theme generator statuses (forms-b pre-review) Co-Authored-By: Claude Opus 5.5 --- content/ru/smart/forms/checkbox.md | 2 +- content/ru/smart/forms/code-input.md | 8 +++----- content/ru/smart/forms/color.md | 2 +- content/ru/smart/forms/country-code.md | 2 +- content/ru/smart/forms/datepicker.md | 7 ++----- content/ru/smart/forms/file-upload.md | 2 -- content/ru/smart/forms/image-field.md | 14 ++++++-------- content/ru/smart/forms/radio.md | 3 +-- content/ru/smart/forms/range-slider.md | 2 +- content/ru/smart/forms/theme-generator.md | 13 +++++-------- .../theme-generator/overview/index.html | 8 +++++++- 11 files changed, 28 insertions(+), 35 deletions(-) diff --git a/content/ru/smart/forms/checkbox.md b/content/ru/smart/forms/checkbox.md index ad9b4b06..854703a4 100644 --- a/content/ru/smart/forms/checkbox.md +++ b/content/ru/smart/forms/checkbox.md @@ -104,7 +104,7 @@ description: "Даёт отметить один или несколько не `invalid` • [справочник](#атрибуты) `invalid` показывает недопустимое состояние и ставит `aria-invalid` на -внутренний флажок; `error` — прежнее имя этого атрибута. Текст ошибки +внутренний флажок. Текст ошибки компонент не выводит: разместите его рядом с группой и свяжите с ней через `aria-describedby`. diff --git a/content/ru/smart/forms/code-input.md b/content/ru/smart/forms/code-input.md index c6909ce0..f14e2f73 100644 --- a/content/ru/smart/forms/code-input.md +++ b/content/ru/smart/forms/code-input.md @@ -128,16 +128,14 @@ description: "Принимает одноразовый код из сообще ### Проверка кода и события -Когда заполнены все ячейки, приходит `sf-code-input-completed` с `value` — +Когда заполнены все ячейки, приходит `sf-input-code-completed` с `value` — один раз на каждый целый код. Компонент сам форму не отправляет и код не проверяет: проверяет приложение. Обычные события `input` и `change` всплывают от внутреннего input без `detail`. -Прежнее имя `sf-code-input:complete` приходит следом с тем же `detail` до -2026-12-01. ```js -const code = document.querySelector('sf-code-input[name="code"]'); -code.addEventListener('sf-code-input-completed', async (event) => { +const code = document.querySelector('sf-input-code[name="code"]'); +code.addEventListener('sf-input-code-completed', async (event) => { const ok = await verify(event.detail.value); code.toggleAttribute('invalid', !ok); if (!ok) code.setAttribute('error-message', 'Код не подошёл'); diff --git a/content/ru/smart/forms/color.md b/content/ru/smart/forms/color.md index 67f718a3..520fb017 100644 --- a/content/ru/smart/forms/color.md +++ b/content/ru/smart/forms/color.md @@ -146,7 +146,7 @@ description: "Выбирает цвет системной палитрой ил поэтому форма передаёт «не задано» без дополнительного кода. ```js -const accent = document.querySelector('sf-color[name="accent"]'); +const accent = document.querySelector('sf-input-color[name="accent"]'); accent.addEventListener('change', (event) => { saveTheme({accent: event.detail.value || null}); }); diff --git a/content/ru/smart/forms/country-code.md b/content/ru/smart/forms/country-code.md index 2e5f195c..f1b72a5f 100644 --- a/content/ru/smart/forms/country-code.md +++ b/content/ru/smart/forms/country-code.md @@ -146,7 +146,7 @@ description: "Принимает телефон с выбором страны: или страны из `setState()` тоже отправляет `change`. ```js -const phone = document.querySelector('sf-country-code[name="phone"]'); +const phone = document.querySelector('sf-input-country[name="phone"]'); phone.addEventListener('change', () => { const {value, iso2, dialCode} = phone.state; savePhone({value, iso2, dialCode}); diff --git a/content/ru/smart/forms/datepicker.md b/content/ru/smart/forms/datepicker.md index e59aee04..3793137e 100644 --- a/content/ru/smart/forms/datepicker.md +++ b/content/ru/smart/forms/datepicker.md @@ -272,7 +272,7 @@ ISO принимается тоже: `14.10.2026` и `2026-10-14` дают од @@ -305,9 +305,6 @@ ISO принимается тоже: `14.10.2026` и `2026-10-14` дают од дату ISO показанным текстом. Слушатель на предке, например на форме, слышит и `change` других полей, поэтому проверяет `event.target`. -Устаревшее событие `sf-change` приходит вместе с `change` и с тем же -`detail`. Подписывайтесь на `change`. - ### Значение и окно из кода Свойство `value` возвращает дату или массив границ периода и принимает то же @@ -321,7 +318,7 @@ ISO принимается тоже: `14.10.2026` и `2026-10-14` дают од день, `closePanel({options: {restoreFocus: true}})` возвращает его в поле. ```js -const period = document.querySelector('sf-datepicker[name="period"]'); +const period = document.querySelector('sf-input-date[name="period"]'); period.addEventListener('change', (event) => { const {startValue, endValue} = event.detail; if (startValue && endValue) loadReport(startValue, endValue); diff --git a/content/ru/smart/forms/file-upload.md b/content/ru/smart/forms/file-upload.md index 4f7f5b7b..0edc9718 100644 --- a/content/ru/smart/forms/file-upload.md +++ b/content/ru/smart/forms/file-upload.md @@ -194,8 +194,6 @@ upload.onFileAdd((event) => add(event.detail.item)); события `sf-file-upload-attached`, `sf-file-upload-failed`, `sf-file-upload-completed`, `sf-file-upload-detach`, `sf-file-upload-retry` и `sf-file-upload-finished`. -Прежние имена с двоеточием (`sf-file-upload:select`, `sf-file-upload:file-add` -и другие) приходят следом с тем же `detail` до 2026-12-01. ### Отправка файлов diff --git a/content/ru/smart/forms/image-field.md b/content/ru/smart/forms/image-field.md index 0a22e085..337ceb5d 100644 --- a/content/ru/smart/forms/image-field.md +++ b/content/ru/smart/forms/image-field.md @@ -63,7 +63,7 @@ description: "Даёт выбрать одно изображение: мини `invalid`, `error-message`, `required` • [справочник](#атрибуты) -`invalid` (или `error`) и `error-message` показывают ошибку вместо пояснения. +`invalid` и `error-message` показывают ошибку вместо пояснения. `required` не даёт отправить форму без изображения. :::example {id="smart-components/image-field/state-error" label="Обязательное поле с ошибкой"} @@ -93,18 +93,16 @@ description: "Даёт выбрать одно изображение: мини ### Загрузка и события -Когда человек выбрал или перетащил файл, приходит `sf-image-field-attach` с +Когда человек выбрал или перетащил файл, приходит `sf-input-image-attach` с `file`: приложение загружает его и затем задаёт сохранённое значение через -`setValue()`. «Убрать» отправляет `sf-image-field-detach` с прежним +`setValue()`. «Убрать» отправляет `sf-input-image-detach` с прежним значением; файлы, которых нет в `accept`, отбрасываются с событием -`sf-image-field-rejected`. Каждое изменение сопровождают `input` и `change` с +`sf-input-image-rejected`. Каждое изменение сопровождают `input` и `change` с `value`, `file`, `source` и `name`. -Прежние имена `sf-image-field:select`, `sf-image-field:remove` и -`sf-image-field:reject` приходят следом с тем же `detail` до 2026-12-01. ```js -const logo = document.querySelector('sf-image-field[name="logo"]'); -logo.addEventListener('sf-image-field-attach', async (event) => { +const logo = document.querySelector('sf-input-image[name="logo"]'); +logo.addEventListener('sf-input-image-attach', async (event) => { try { const stored = await upload(event.detail.file); logo.setValue(stored); diff --git a/content/ru/smart/forms/radio.md b/content/ru/smart/forms/radio.md index f404d28d..6ca55a49 100644 --- a/content/ru/smart/forms/radio.md +++ b/content/ru/smart/forms/radio.md @@ -80,8 +80,7 @@ description: "Даёт выбрать ровно один вариант из в `invalid` • [справочник](#атрибуты) `invalid` показывает недопустимое состояние только цветом и ставит -`aria-invalid` на внутреннюю радиокнопку; `error` — прежнее имя этого -атрибута. Текст ошибки разместите у группы. +`aria-invalid` на внутреннюю радиокнопку. Текст ошибки разместите у группы. :::example {id="smart-components/radio/state-error" label="Группа с ошибкой"} ::: diff --git a/content/ru/smart/forms/range-slider.md b/content/ru/smart/forms/range-slider.md index 6ad622be..89b367b2 100644 --- a/content/ru/smart/forms/range-slider.md +++ b/content/ru/smart/forms/range-slider.md @@ -51,7 +51,7 @@ description: "Выбирает одно значение или две гран `label` выбирает, как ручка показывает значение: `none` (по умолчанию) — без подписи, `text` — постоянной подписью, `tooltip` — подсказкой. `prefix-text` -и `suffix` добавляют единицы; `prefix` — прежнее имя `prefix-text`. +и `suffix` добавляют единицы. :::example {id="smart-components/range-slider/labels" label="Подпись значения"} ::: diff --git a/content/ru/smart/forms/theme-generator.md b/content/ru/smart/forms/theme-generator.md index 3e419aea..a416c996 100644 --- a/content/ru/smart/forms/theme-generator.md +++ b/content/ru/smart/forms/theme-generator.md @@ -105,23 +105,20 @@ SIMAI Framework `#005bbe` и его собственная тема. Цвет к | Событие | Когда | |:---|:---| -| `sf-theme-generator-change` | выбор меняется: перетаскивание в палитре, образец цвета, число в поле | -| `sf-theme-generator-apply` | выбор зафиксирован: `change` палитры, образец цвета, `Enter` или уход из поля, «Сбросить» | +| `change` | выбор меняется: перетаскивание в палитре, образец цвета, число в поле | +| `sf-theme-generator-committed` | выбор зафиксирован: `change` палитры, образец цвета, `Enter` или уход из поля, «Сбросить» | В `detail` обоих — `value` (пусто, пока действует цвет SIMAI Framework), `hue`, `chroma`, `theme` (объект темы), `variables` (226 переменных) и `css` — правило под `selector` (по умолчанию `:root`). Сохраните `css` в CSS сайта — и тема действует на каждой странице без компонента. -Прежние имена `sf-theme-change` и `sf-theme-apply` компонент отправляет -рядом с новыми для совместимости до 2026-12-01 (запись журнала -`event-theme-generator-names`); в новом коде слушайте новые имена. -`sf-theme-change` — ещё и событие Loader на `window` о смене светлой и тёмной -темы (см. [События загрузчика](/ru/guide/connection/loader/#события-загрузчика)). +О смене светлой и тёмной темы сообщает другое событие — `sf-theme-change` +загрузчика на `window` (см. [События загрузчика](/ru/guide/connection/loader/#события-загрузчика)). ```js const generator = document.querySelector('sf-theme-generator'); -generator.addEventListener('sf-theme-generator-apply', ({detail}) => { +generator.addEventListener('sf-theme-generator-committed', ({detail}) => { saveSiteSetting('theme', {value: detail.value, hue: detail.hue, chroma: detail.chroma, css: detail.css}); }); ``` diff --git a/examples/smart-components/theme-generator/overview/index.html b/examples/smart-components/theme-generator/overview/index.html index b20a4c48..52250c48 100644 --- a/examples/smart-components/theme-generator/overview/index.html +++ b/examples/smart-components/theme-generator/overview/index.html @@ -16,6 +16,12 @@ Показывать на сайте -

Выберите основной цвет: кнопки, флажок и фон этого блока перекрасятся, а страница вокруг останется в цветах Framework.

+
+ Информация + Готово + Внимание + Ошибка +
+

Выберите основной цвет: кнопки, флажок и фон этого блока перекрасятся, а статусы — информация, успех, внимание и ошибка — останутся своими. Страница вокруг останется в цветах SIMAI Framework.

From 555721e877bd997cdf01ec1e710aa5e55aced684 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 06:16:19 +0300 Subject: [PATCH 15/53] Smart content pages: old names out, kind on list items, accordion and file-preview events, gallery title, icons wording (SC-S04, LI-04/05, AC-01/03, FP-01/04, GA-02/03, IC-01/02, LF-01/02, TM-02, INC-03) Co-Authored-By: Claude Opus 5.5 --- config/smart-reference.ru.json | 4 +-- content/ru/smart/content/accordion.md | 4 +-- content/ru/smart/content/avatar.md | 3 +- content/ru/smart/content/avatars.md | 6 ++-- content/ru/smart/content/file-preview.md | 4 +-- content/ru/smart/content/gallery.md | 9 +++--- content/ru/smart/content/icons.md | 16 +++++------ content/ru/smart/content/include.md | 3 +- content/ru/smart/content/index.md | 2 +- content/ru/smart/content/list-feed.md | 6 ++-- content/ru/smart/content/list-item.md | 28 +++++++++---------- content/ru/smart/content/slider.md | 12 ++++---- content/ru/smart/content/tags.md | 3 +- content/ru/smart/content/tile-metric.md | 2 +- .../accordion/appearance/index.html | 4 +-- .../smart-components/dropdown/grid/index.html | 24 ++++++++-------- .../file-preview/thumbnail/index.html | 8 +++--- .../list-item/overview/index.html | 12 ++++---- 18 files changed, 69 insertions(+), 81 deletions(-) diff --git a/config/smart-reference.ru.json b/config/smart-reference.ru.json index ae246237..80c05eab 100644 --- a/config/smart-reference.ru.json +++ b/config/smart-reference.ru.json @@ -692,7 +692,7 @@ "Selects a batch action by value (an empty value clears the choice) and emits sf-action-change when it differs from the current one.": "Выбирает групповое действие по значению (пустое значение сбрасывает выбор) и отправляет sf-action-change, если оно отличается от текущего.", "Selects a node by id.": "Выбирает узел по id.", "Selects an enabled item by value.": "Выбирает кнопку по значению, если она доступна.", - "Semantic Material Symbol wrapper with exact Loader tracking, accessible decorative or informative modes and token-owned geometry.": "Семантическая обёртка Material Symbol с точным отслеживанием в Loader, доступными декоративным и информативным режимами и геометрией из токенов.", + "Semantic Material Symbol wrapper with exact Loader tracking, accessible decorative or informative modes and token-owned geometry.": "Семантическая обёртка Material Symbol с точным отслеживанием в загрузчике, доступными декоративным и информативным режимами и геометрией из токенов.", "Semantic internal spacing profile.": "Внутренние отступы: `compact` плотнее, `spacious` свободнее.", "Send one failed file again.": "Повторно отправляет один файл, загрузка которого не удалась.", "Set and commit the colour as a person would: normalised, reflected to value, input and then change dispatched when it differs from the committed one. Returns whether the value changed.": "Устанавливает и фиксирует цвет так же, как это делает пользователь: нормализует его, отражает в value и отправляет input, а затем change, если цвет отличается от зафиксированного. Возвращает, изменилось ли значение.", @@ -1599,7 +1599,7 @@ "The edit was put back (Escape): the element shows its HTML from before; sent on the edited element": "Правка отменена (Escape): элемент показывает свой прежний HTML; отправляется на редактируемом элементе", "The editor's layer over a rendered Composition Document: hover and selection frames, a toolbar of the node's actions, insertion places, moves by pointer and keyboard and a structure tree; the canvas is never changed and the host performs every change.": "Слой редактора над отрисованным документом композиции: рамки наведения и выбора, панель действий узла, места вставки, перенос указателем и клавиатурой и дерево структуры; холст никогда не меняется, каждое изменение выполняет приложение.", "The end came into view while has-more: {direction: after}.": "Конец списка показался на экране, пока `has-more`: `{direction: after}`.", - "The exact subset that draws this glyph is in place; synchronized from the Loader, never an authoring switch": "Точное подмножество, рисующее этот символ, на месте; синхронизируется из Loader, это не переключатель для автора", + "The exact subset that draws this glyph is in place; synchronized from the Loader, never an authoring switch": "Точное подмножество, рисующее этот символ, на месте; синхронизируется из загрузчика, это не переключатель для автора", "The fact after a record that could not be read, {reason, message}: the host's code (record-unknown for a stale link) and its sentence.": "Факт после записи, которую не удалось прочитать, `{reason, message}`: код приложения (record-unknown для устаревшей ссылки) и его фраза.", "The fact after an applied delete, vocabulary §4: {ids}.": "Факт после применённого удаления: `{ids}`.", "The fact after an applied save (group.save or record.save), vocabulary §4: {id, revision}.": "Факт после применённого сохранения (`group.save` или `record.save`): `{id, revision}`.", diff --git a/content/ru/smart/content/accordion.md b/content/ru/smart/content/accordion.md index df62718f..97203c99 100644 --- a/content/ru/smart/content/accordion.md +++ b/content/ru/smart/content/accordion.md @@ -132,13 +132,13 @@ description: "Раскрывающиеся секции из данных или `open(id)`, `close(id)` и `toggle(id)` открывают и закрывают секцию и возвращают, изменилось ли что-нибудь; `isOpen(id)` отвечает, открыта ли она. -Каждое открытие и закрытие отправляет `sf-accordion:toggle` с `id`, `open` и +Каждое открытие и закрытие отправляет `sf-accordion-toggle` с `id`, `open` и `source`: `user` — нажал человек, `method` — вызван метод. В режиме `single` закрытая при этом секция отправляет своё событие следом. ```js const faq = document.querySelector('sf-accordion'); -faq.addEventListener('sf-accordion:toggle', (event) => { +faq.addEventListener('sf-accordion-toggle', (event) => { if (event.detail.open) history.replaceState(null, '', `#${event.detail.id}`); }); faq.open(location.hash.slice(1)); diff --git a/content/ru/smart/content/avatar.md b/content/ru/smart/content/avatar.md index ffeb9944..0b5e099b 100644 --- a/content/ru/smart/content/avatar.md +++ b/content/ru/smart/content/avatar.md @@ -42,8 +42,7 @@ JavaScript. `image-url`, `text`, `heading` • [справочник](#атрибуты) Содержимое выбирается по порядку: изображение из `image-url`, затем текст из -`text`, затем инициалы, которые компонент берёт из `heading` (прежнее имя — -`title`) или из явно заданного `aria-label`. Если нет ничего из этого, +`text`, затем инициалы, которые компонент берёт из `heading` или из явно заданного `aria-label`. Если нет ничего из этого, показывается значок человека. Изображение декоративно: имя человека передаёт `aria-label`, а не альтернативный текст картинки. diff --git a/content/ru/smart/content/avatars.md b/content/ru/smart/content/avatars.md index 8b27bbfe..ef4386b4 100644 --- a/content/ru/smart/content/avatars.md +++ b/content/ru/smart/content/avatars.md @@ -48,12 +48,10 @@ description: "Выводит готовые сочетания аватаров: ### Данные одним атрибутом -Значения можно передать по одному атрибуту или целиком: `fields` (прежнее -имя — `data`) принимает JSON с теми же полями в camelCase или snake_case +Значения можно передать по одному атрибуту или целиком: `fields` принимает JSON с теми же полями в camelCase или snake_case (`imageUrl`, `label_text`, `avatars`), `property` — JSON с размерами (`roundSize`, `labelSize`, `profileSize`). Атрибут сильнее поля из JSON. -Размеры принимают ступени шкалы (`1/4`, `1/3`, `1/2`, `2`, `3`) или имена -`tiny`, `small`, `big`, `extra_big`. +Размеры принимают ступени шкалы: `1/4`, `1/3`, `1/2`, `2`, `3`. ```html ``` -`file-name` — видимое имя файла (прежнее имя атрибута — `name`), +`file-name` — видимое имя файла, `file-size` — размер готовым текстом («1,2 МБ»); компонент его не вычисляет. `href` — адрес файла для скачивания. Слева показывается значок из `icon` (по умолчанию `description`) или миниатюра. @@ -99,7 +99,7 @@ JavaScript. ### Скачивание и удаление Нажатие на кнопку отправляет всплывающее событие с возможностью отмены: -`sf-file-preview:download` или `sf-file-preview:remove`. В `event.detail`: +`sf-file-preview-download` или `sf-file-preview-detach`. В `event.detail`: | Поле | Содержимое | |:---|:---| diff --git a/content/ru/smart/content/gallery.md b/content/ru/smart/content/gallery.md index d5038153..c7700a47 100644 --- a/content/ru/smart/content/gallery.md +++ b/content/ru/smart/content/gallery.md @@ -73,10 +73,10 @@ description: "Показывает набор материалов слайде `arrows`, `dots`, `thumbs` и `loop` меняют способ листания, не меняя набор материалов; `space-between` и `speed` — расстояние между слайдами и скорость перехода. Модальное окно настраивается атрибутами галереи с теми же именами, -что у модального окна: `heading` (прежнее имя — `title`), `text`, +что у модального окна: `heading`, `text`, `show-header`, `show-close`, `close-on-esc`, `close-on-overlay`, `position`, `fullscreen`, `width`, `height`. Внешний вид частей задают -`lightbox-class` (прежнее имя — `root-class`), `inline-class`, +`lightbox-class`, `inline-class`, `slide-class` и классы частей окна — утилитами Framework. ### Текущее положение и события @@ -88,8 +88,7 @@ description: "Показывает набор материалов слайде на этом материале. Смена материала отправляет `change`, открытие и закрытие окна — -`sf-gallery-opened` и `sf-gallery-closed` (прежние имена — `sf-change`, -`sf-open` и `sf-button-close` — приходят с тем же `detail`). Все три всплывают; в +`sf-gallery-opened` и `sf-gallery-closed`. Все три всплывают; в `event.detail` приходят `current` и `total`, а у `change` ещё `previous` и `from` — откуда пришла смена: `inline`, `lightbox` или `api`. @@ -108,7 +107,7 @@ API, а вызывайте методы галереи. `getInlineSlider()`, `ge ## Доступность - Задайте `heading`: он становится заголовком модального окна и называет - диалог. По умолчанию заголовок английский — «Gallery». + диалог. По умолчанию заголовок — «Галерея». - Материал встроенного слайдера, который открывает окно, получает роль кнопки и попадает в порядок `Tab`; `Enter` и пробел открывают окно на нём. Его имя — текст материала, поэтому у изображения нужен содержательный `alt`, а diff --git a/content/ru/smart/content/icons.md b/content/ru/smart/content/icons.md index 7aee651e..ce2f5604 100644 --- a/content/ru/smart/content/icons.md +++ b/content/ru/smart/content/icons.md @@ -1,6 +1,6 @@ --- title: "Иконки" -description: "Выводит символ Material Symbols, для которого Loader загружает только нужные знаки шрифта." +description: "Выводит символ Material Symbols, для которого загрузчик Framework загружает только нужные знаки шрифта." --- # Иконки @@ -8,7 +8,7 @@ description: "Выводит символ Material Symbols, для которо :badge[sf-icon]{variant=tonal scheme=primary size=1/2} :badge[1.1.6]{variant=outline scheme=on-surface size=1/2} Иконка выводит один символ Material Symbols по имени, а загружается только -нужная часть шрифта. Размер, начертание, заливка и толщина задаются атрибутами +нужная часть шрифта. Размер, семейство, заливка и толщина задаются атрибутами на общей шкале SIMAI Framework. Для иконки, которую нажимают, берите [Кнопки с иконкой](/ru/smart/actions/icon-buttons/). @@ -39,14 +39,14 @@ JavaScript. `type` • [справочник](#атрибуты) -`type` — начертание: `outlined`, `rounded` или `sharp`. Без него действует -начертание проекта из `SF_BOOT_CONFIG.icons.type`, а если оно не задано — -`outlined`. Держите одно начертание во всём интерфейсе. +`type` — семейство знаков: `outlined`, `rounded` или `sharp`. Без него +действует семейство проекта из `SF_BOOT_CONFIG.icons.type`, а если оно не +задано — `outlined`. Держите одно семейство во всём интерфейсе. :::example {id="smart-components/icons/families" label="outlined, rounded и sharp"} ::: -### Начертание +### Толщина `weight` • по умолчанию `400` • [справочник](#атрибуты) @@ -115,7 +115,7 @@ JavaScript. ### Загрузка знаков -Loader отслеживает каждый `` и загружает знаки шрифта, нужные +Загрузчик Framework отслеживает каждый `` и загружает знаки шрифта, нужные странице, в том числе после смены `icon`. Пока знак не загружен, иконка сжата до нуля, и имя символа не мелькает текстом. Атрибут `loaded` компонент ставит сам, когда знак готов; задавать его в разметке не нужно. @@ -149,7 +149,7 @@ Loader отслеживает каждый `` и загружает з | `badge-dot` | да/нет | нет | — | Точка на иконке — отметка без числа: есть новое. Показывается, когда badge-count не задан; при заданном счётчике он рисуется точкой. Её текстовый эквивалент («есть новое» / «new» по умолчанию, другой задаёт badge-label) входит в доступное имя. | | `badge-cap` | целое число | `99` | от 1 | Наибольшее число на значке; большее показывается с плюсом, например «99+». | | `badge-label` | строка | пусто | — | Слова значка для экранного диктора и подсказки. Для точки — фраза целиком, например «есть новое». Для числа — формы через «\|» с `{count}` на месте числа, например `{count} новое\|{count} новых\|{count} новых`. | -| `loaded` | да/нет | нет | — | Точное подмножество, рисующее этот символ, на месте; синхронизируется из Loader, это не переключатель для автора. | +| `loaded` | да/нет | нет | — | Точное подмножество, рисующее этот символ, на месте; синхронизируется из загрузчика, это не переключатель для автора. | #### Общие для всех компонентов diff --git a/content/ru/smart/content/include.md b/content/ru/smart/content/include.md index e2637f4a..78f36d06 100644 --- a/content/ru/smart/content/include.md +++ b/content/ru/smart/content/include.md @@ -9,8 +9,7 @@ description: "Загружает часть страницы с адреса с Часть страницы по адресу подгружает то, что сервер сайта отдаёт отдельно: блок новостей, список, собранный на сервере, нижнюю часть страницы, которую стоит -загрузить, только когда человек до неё долистает. Она заменяет прежние классы -`ajax`, `ajaxload` и `lazy-load`. +загрузить, только когда человек до неё долистает. ## Пример diff --git a/content/ru/smart/content/index.md b/content/ru/smart/content/index.md index 8e57c8d3..e6f4cf42 100644 --- a/content/ru/smart/content/index.md +++ b/content/ru/smart/content/index.md @@ -8,7 +8,7 @@ profile: reference - [Аватар](/ru/smart/content/avatar/) — Показывает человека или организацию изображением, инициалами или значком, отдельно или с подписью. - [Аватары](/ru/smart/content/avatars/) — Выводит готовые сочетания аватаров: человека со ссылкой на источник, группу, подпись и профиль. -- [Иконки](/ru/smart/content/icons/) — Выводит символ Material Symbols, для которого Loader загружает только нужные знаки шрифта. +- [Иконки](/ru/smart/content/icons/) — Выводит символ Material Symbols, для которого загрузчик Framework загружает только нужные знаки шрифта. - [Теги](/ru/smart/content/tags/) — Показывает короткую метку — категорию, фильтр, выбранное значение или статус — с переключением и удалением. - [Строка списка](/ru/smart/content/list-item/) — Вариант выпадающего списка и общая строка списка: подпись, вторая строка, выбор и перестановка. - [Список](/ru/smart/content/list/) — Список, с которым работают: строки из данных, группы по дню или ключу, открытая строка, действия строки, переименование на месте и следующая страница. diff --git a/content/ru/smart/content/list-feed.md b/content/ru/smart/content/list-feed.md index 58df81b5..ed9fba74 100644 --- a/content/ru/smart/content/list-feed.md +++ b/content/ru/smart/content/list-feed.md @@ -25,9 +25,7 @@ description: "Прокручиваемая колонка записей, кот ## Разметка -В разметке — тег с доступным именем; высоту ленте даёт контейнер. Сам тег — -`display: contents`, а колонка `.sf-list-feed` растягивается во -flex-контейнере (`flex: 1`) и прокручивается внутри: +В разметке — тег с доступным именем; высоту ленте даёт контейнер. Лента занимает всю высоту контейнера и прокручивается внутри него: ```html
@@ -222,7 +220,7 @@ feed.addEventListener('sf-list-feed-load-more', async (event) => { ## См. также - [Список](/ru/smart/content/list/) — список, с которым что-то делают. -- [Лента сообщений](/ru/pro/chat/message-log/) — прежнее имя ленты в пакете +- [Лента сообщений](/ru/pro/chat/message-log/) — лента сообщений чата в пакете Pro. - [JavaScript API](/ru/guide/connection/js-api/) — `SF.readingPosition`, правило положения чтения, на котором построена лента. diff --git a/content/ru/smart/content/list-item.md b/content/ru/smart/content/list-item.md index eda4b1ae..ea38dd31 100644 --- a/content/ru/smart/content/list-item.md +++ b/content/ru/smart/content/list-item.md @@ -27,18 +27,18 @@ description: "Вариант выпадающего списка и общая ```html - - + + ``` -Dropdown читает атрибуты пунктов до первой отрисовки и строит из них +Выпадающий список читает атрибуты пунктов до первой отрисовки и строит из них список. `value` — машинное значение, которое уходит в форму и в событие -`change` Dropdown; без `value` значением становится `text`. `text` — +`change` выпадающего списка; без `value` значением становится `text`. `text` — видимая подпись, она же доступное имя варианта. `group` объединяет идущие подряд пункты под одним заголовком. -Пункт вне Dropdown без `interactive` и `sortable-item` показывается +Пункт вне выпадающего списка без `interactive` и `sortable-item` показывается статичной строкой: без роли, фокуса и реакции на нажатие. Его нельзя использовать как кнопку, ссылку или отдельный флажок. @@ -58,9 +58,7 @@ Dropdown читает атрибуты пунктов до первой отри - `color` — цветовой маркер; цвет задаёт утилита фона в `color-class`, например `bg-error`. -Манифест называет эту ось `kind`, а `type` — прежним именем. Dropdown читает -у дочерних пунктов только `type`, поэтому внутри Dropdown вид строки задавайте -им. +Вид строки задаёт атрибут `kind` — и в выпадающем списке, и вне его. :::example {id="smart-components/list-item/kinds" label="Пять видов строки"} ::: @@ -70,7 +68,7 @@ Dropdown читает атрибуты пунктов до первой отри `size` • по умолчанию `1` • [справочник](#атрибуты) `size` — ступень `1/3`, `1/2`, `1`, `2` или `3`. Без него пункт берёт размер -своего Dropdown. Высота однострочной строки равна высоте элемента управления +своего выпадающего списка. Высота однострочной строки равна высоте элемента управления своего размера. :::example {id="smart-components/list-item/sizes" label="Размеры от 1/3 до 3"} @@ -113,14 +111,14 @@ Dropdown читает атрибуты пунктов до первой отри ### Строка списка -Вне Dropdown `` — общая строка списка: писем, диалогов, +Вне выпадающего списка `` — общая строка списка: писем, диалогов, блоков редактора, записей. Места `leading`, `text` и `trailing` те же, что у -варианта Dropdown. В Dropdown эти возможности выключены, и варианты -Dropdown не меняются. +варианта выпадающего списка. В выпадающем списке эти возможности выключены, и варианты +списка не меняются. | Атрибут | Что даёт | |:---|:---| -| `interactive` | Строка, с которой работают: наведение, фокус с клавиатуры и `selected`, указатель-рука и событие `sf-list-item-open` (`detail: {id, trigger}`, `id` — это `value`; прежнее имя события — `sf-list-item-activate`) по щелчку или `Enter`. Щелчок по элементу управления внутри строки, например по действию в `trailing`, принадлежит этому элементу, а не строке. | +| `interactive` | Строка, с которой работают: наведение, фокус с клавиатуры и `selected`, указатель-рука и событие `sf-list-item-open` (`detail: {id, trigger}`, `id` — это `value`) по щелчку или `Enter`. Щелчок по элементу управления внутри строки, например по действию в `trailing`, принадлежит этому элементу, а не строке. | | `sortable-item=""` | Строка — элемент окружающего `sf-sortable`: получает ручку перетаскивания перед содержимым и `data-sf-sortable-item` на своём блоке. | | `pinned` | Строка списка, которая не двигается: без ручки и не элемент `sf-sortable`. Место ручки остаётся пустым, поэтому текст начинается там же, где у остальных строк. | @@ -149,10 +147,10 @@ Dropdown не меняются. ## Доступность -- В Dropdown каждый пункт получает `role="option"`, `aria-selected` и +- В выпадающем списке каждый пункт получает `role="option"`, `aria-selected` и `aria-disabled`. Имя варианта — `text`; `aria-label` заменяет его, если подпись сокращена или неоднозначна. -- Клавиши принадлежат Dropdown: стрелки перемещают по вариантам, `Home` и +- Клавиши принадлежат выпадающему списку: стрелки перемещают по вариантам, `Home` и `End` — к первому и последнему, `Enter` и пробел выбирают, `Escape` закрывает список. - Иконка, аватар, цветовой маркер и флажок декоративны и не создают diff --git a/content/ru/smart/content/slider.md b/content/ru/smart/content/slider.md index 3e0768cd..e63b1c5d 100644 --- a/content/ru/smart/content/slider.md +++ b/content/ru/smart/content/slider.md @@ -134,16 +134,14 @@ SIMAI Framework: цветовые роли, отступы и радиусы з ### Управление из кода `slideNext()` и `slidePrev()` листают вперёд и назад. Смена слайда -отправляет всплывающее событие `sf-slide-change`; в `event.detail` +отправляет всплывающее событие `change`; в `event.detail` `activeIndex` — номер текущего слайда, `previousIndex` — прошлого, `realIndex` — номер слайда без учёта копий, которые добавляет `loop`. -`refresh()` пересчитывает раскладку после изменения размеров контейнера, -`getSlider()` возвращает экземпляр Swiper для настроек, которых нет в -атрибутах. +`refresh()` пересчитывает раскладку после изменения размеров контейнера. ```js const slider = document.querySelector('sf-slider'); -slider.addEventListener('sf-slide-change', (event) => { +slider.addEventListener('change', (event) => { counter.textContent = `${event.detail.realIndex + 1} из 3`; }); slider.slideNext(); @@ -153,8 +151,8 @@ slider.slideNext(); - Дайте слайдеру имя: поместите его в `
` с заголовком или `aria-labelledby`, чтобы экранный диктор назвал область. -- Стрелки — кнопки в порядке `Tab`. Их доступные имена английские — - «Previous slide» и «Next slide» — и атрибутом не меняются. +- Стрелки — кнопки в порядке `Tab` с доступными именами «Предыдущий слайд» и + «Следующий слайд»; точки называются «Перейти к слайду N». - Слайды вне видимой области недоступны для фокуса и вспомогательных технологий: ссылки и кнопки в них не попадают в порядок `Tab`. - Не прячьте в автопрокрутку единственную копию важных сведений. diff --git a/content/ru/smart/content/tags.md b/content/ru/smart/content/tags.md index 3b25245a..566cbf2a 100644 --- a/content/ru/smart/content/tags.md +++ b/content/ru/smart/content/tags.md @@ -154,8 +154,7 @@ document.querySelector('.filters').addEventListener('change', (event) => { ### Удаление -Нажатие на кнопку удаления отправляет всплывающее событие `sf-tag-close` -(прежнее имя — `close`); `event.detail` пустой, сам тег — `event.target`. Тег +Нажатие на кнопку удаления отправляет всплывающее событие `sf-tag-close`; `event.detail` пустой, сам тег — `event.target`. Тег не исчезает: убирает его приложение, когда удаление принято. ```js diff --git a/content/ru/smart/content/tile-metric.md b/content/ru/smart/content/tile-metric.md index 5629053f..22edabac 100644 --- a/content/ru/smart/content/tile-metric.md +++ b/content/ru/smart/content/tile-metric.md @@ -18,7 +18,7 @@ description: "Одно важное число на панели: подпись ## Пример -:::example {id="smart-components/tile-metric/overview" label="Четыре плитки в ряду" preview=sandbox} +:::example {id="smart-components/tile-metric/overview" label="Плитки показателей" preview=sandbox} ::: ## Разметка diff --git a/examples/smart-components/accordion/appearance/index.html b/examples/smart-components/accordion/appearance/index.html index c140ce43..2822e419 100644 --- a/examples/smart-components/accordion/appearance/index.html +++ b/examples/smart-components/accordion/appearance/index.html @@ -1,5 +1,5 @@
- - + +
diff --git a/examples/smart-components/dropdown/grid/index.html b/examples/smart-components/dropdown/grid/index.html index 4adb3824..17bce264 100644 --- a/examples/smart-components/dropdown/grid/index.html +++ b/examples/smart-components/dropdown/grid/index.html @@ -1,16 +1,16 @@
- - - - - - - - - - - - + + + + + + + + + + + +
diff --git a/examples/smart-components/file-preview/thumbnail/index.html b/examples/smart-components/file-preview/thumbnail/index.html index 96aaf20e..c3a38297 100644 --- a/examples/smart-components/file-preview/thumbnail/index.html +++ b/examples/smart-components/file-preview/thumbnail/index.html @@ -1,18 +1,18 @@
- kind="auto", не изображение — значок + kind="auto", не изображение — значок
- kind="auto", mime-type="image/png" — миниатюра + kind="auto", mime-type="image/png" — миниатюра
- kind="image", адрес без расширения + kind="image", адрес без расширения
- kind="icon" + kind="icon"
diff --git a/examples/smart-components/list-item/overview/index.html b/examples/smart-components/list-item/overview/index.html index 44ff097a..a228c9ad 100644 --- a/examples/smart-components/list-item/overview/index.html +++ b/examples/smart-components/list-item/overview/index.html @@ -1,12 +1,12 @@
- - - + + + - - - + + +
From 9e9c0e2fa8776cd807ee5003388bc0b69936b65c Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 06:16:19 +0300 Subject: [PATCH 16/53] Smart feedback and navigation pages: former-name paragraphs out, phone layouts of contents, spinner and settings examples, pager checkbox, dropdown trigger room, tree selection name (S-08, CT-01/02, SP-02, SE-02, PR-01, DD-03/04, TR-01, TT-01) Co-Authored-By: Claude Opus 5.5 --- content/ru/components/navigation/step.md | 2 +- content/ru/components/navigation/tabs.md | 2 +- content/ru/smart/feedback/empty-state.md | 5 ++--- content/ru/smart/feedback/help-mark.md | 10 ++++------ content/ru/smart/navigation/breadcrumbs.md | 2 +- content/ru/smart/navigation/dropdown.md | 7 ++----- content/ru/smart/navigation/menu.md | 4 ---- content/ru/smart/navigation/pagination.md | 7 ------- content/ru/smart/navigation/steps.md | 2 +- content/ru/smart/navigation/tabs.md | 3 +-- content/ru/smart/navigation/tree-item.md | 13 ++++++------ content/ru/smart/navigation/tree.md | 20 +++++++++---------- .../components/dropdown/trigger/index.css | 5 +++++ .../components/dropdown/trigger/index.html | 2 ++ .../components/inputs/help-mark/index.html | 2 +- .../smart-components/contents/auto/index.css | 10 ++++++++++ .../smart-components/contents/bar/index.css | 10 ++++++++++ .../contents/compact/index.css | 10 ++++++++++ .../contents/overview/index.css | 10 ++++++++++ .../smart-components/contents/rail/index.css | 10 ++++++++++ .../pager/validation/index.html | 5 +---- .../smart-components/settings/api/index.html | 4 ++-- .../smart-components/settings/api/index.js | 2 +- .../smart-components/spinner/looks/index.css | 13 ++++++++++++ .../tree/selection/index.html | 2 +- .../smart-components/tree/selection/index.js | 4 +++- 26 files changed, 107 insertions(+), 59 deletions(-) create mode 100644 examples/components/dropdown/trigger/index.css diff --git a/content/ru/components/navigation/step.md b/content/ru/components/navigation/step.md index bfb04b2d..cec40642 100644 --- a/content/ru/components/navigation/step.md +++ b/content/ru/components/navigation/step.md @@ -101,7 +101,7 @@ profile: reference - Шаги дополняют, а не заменяют заголовок экрана и кнопки «Назад» и «Далее». - Для перехода по шагам делайте их нативными кнопками или ссылками; тогда они получают фокус и срабатывают по Enter. Выбор шага с клавиатуры и - событие `sf-step-change` даёт смарт-компонент `sf-steps`. + событие `sf-steps-change` даёт смарт-компонент `sf-steps`. ## Классы diff --git a/content/ru/components/navigation/tabs.md b/content/ru/components/navigation/tabs.md index 1adb4a87..24a11ab6 100644 --- a/content/ru/components/navigation/tabs.md +++ b/content/ru/components/navigation/tabs.md @@ -126,7 +126,7 @@ profile: reference добавленный позже. Он следит за пунктами и панелями: новые пары можно добавлять без повторной инициализации. Корни, которые отрисовал смарт-компонент ``, он пропускает. Событий переключения скрипт не -отправляет; событие `sf-tab-change` даёт смарт-компонент. +отправляет; событие `sf-tabs-change` даёт смарт-компонент. :::example {id="components/tabs/dynamic" label="Добавление вкладки"} ::: diff --git a/content/ru/smart/feedback/empty-state.md b/content/ru/smart/feedback/empty-state.md index eba12cc4..07b5f1b3 100644 --- a/content/ru/smart/feedback/empty-state.md +++ b/content/ru/smart/feedback/empty-state.md @@ -44,9 +44,8 @@ description: "Показывает, почему в списке, панели Тексты по умолчанию следуют языку страницы. `icon`, `heading` и `text` заменяют их, когда причина конкретнее: «Машин пока нет», «Создайте первую». -`title` — другое имя `heading`, работает до 2026-12-01; это и общий атрибут -HTML, поэтому браузер показывает его всплывающей подсказкой при наведении на -блок. +Заголовок задавайте в `heading`: общий атрибут HTML `title` браузер +показывает всплывающей подсказкой при наведении на блок. :::example {id="smart-components/empty-state/kinds" label="Причины и свои тексты"} ::: diff --git a/content/ru/smart/feedback/help-mark.md b/content/ru/smart/feedback/help-mark.md index 91fa6800..06a6c2de 100644 --- a/content/ru/smart/feedback/help-mark.md +++ b/content/ru/smart/feedback/help-mark.md @@ -8,8 +8,8 @@ description: "Знак «?» рядом с подписью: по нажатию :badge[sf-help-mark]{variant=tonal scheme=primary size=1/2} :badge[1.3.0]{variant=outline scheme=on-surface size=1/2} Подсказка «?» стоит рядом с подписью и открывает короткое пояснение: что -значит поле или настройка. Для короткого имени кнопки-иконки берите -[Всплывающую подсказку](/ru/smart/feedback/tooltip/). +значит поле или настройка. Подпись, которая появляется при наведении на кнопку-иконку, собирайте на +классах [Подсказки](/ru/components/overlays-media/tooltip/). ## Пример @@ -27,8 +27,7 @@ description: "Знак «?» рядом с подписью: по нажатию - `text` — пояснение; вместо него можно передать слот `content` со ссылками или списком. -- `heading` — короткая строка заголовка над пояснением; `title` — другое имя - `heading`, работает до 2026-12-01. +- `heading` — короткая строка заголовка над пояснением. - `label` — доступное имя кнопки, по умолчанию «Подробнее». ```html @@ -79,8 +78,7 @@ description: "Знак «?» рядом с подписью: по нажатию наведение его не открывает. Второе нажатие, `Escape` или щелчок мимо закрывают пояснение. Из скрипта его открывают `open()` и закрывают `close()`. При открытии и закрытии приходят события `sf-help-mark-opened` и -`sf-help-mark-closed`; `sf-help-mark-open` и `sf-help-mark-close` — их -устаревшие имена с тем же `detail`. +`sf-help-mark-closed`. ### Рядом с подписью поля diff --git a/content/ru/smart/navigation/breadcrumbs.md b/content/ru/smart/navigation/breadcrumbs.md index 3bf41689..6e75c851 100644 --- a/content/ru/smart/navigation/breadcrumbs.md +++ b/content/ru/smart/navigation/breadcrumbs.md @@ -78,7 +78,7 @@ description: "Показывает путь от корня сайта до те ### Нажатие на пункт Нажатие на пункт отправляет всплывающее отменяемое событие -`sf-breadcrumbs-press` (прежнее имя — `sf-breadcrumb-click`). Его +`sf-breadcrumbs-press`. Его `event.detail` содержит `item` — данные пункта (`label`, `href`, `value`, `ellipsis` и другие поля), `index` — номер пункта среди показанных, `items` — все показанные пункты. У кнопки «…» в `item` есть `ellipsis: true` и diff --git a/content/ru/smart/navigation/dropdown.md b/content/ru/smart/navigation/dropdown.md index 26d26122..169d93aa 100644 --- a/content/ru/smart/navigation/dropdown.md +++ b/content/ru/smart/navigation/dropdown.md @@ -9,8 +9,8 @@ description: "Выбирает одно или несколько значени Выпадающий список даёт выбрать одно или несколько значений из раскрывающегося списка с поиском. Берите его, когда вариантов больше, чем удобно показать -[радиокнопками](/ru/components/forms/radio/) или -[флажками](/ru/components/forms/checkbox/), или когда значения нужно показать +[радиокнопками](/ru/smart/forms/radio/) или +[флажками](/ru/smart/forms/checkbox/), или когда значения нужно показать тегами. То же на классах — [Выпадающий список](/ru/components/navigation/dropdown/): @@ -176,9 +176,6 @@ description: "Выбирает одно или несколько значени | `root` | элемент `` | | `multiple` | `true`, если разрешён выбор нескольких значений | -Устаревшее имя `sf-dropdown:change` приходит вместе с `change` и с тем же -`detail`; подписывайтесь на `change`. - `root` — живой элемент, а не данные, поэтому `detail` — не JSON: `JSON.stringify(event.detail)` падает на циклической структуре. Берите нужные поля — `value`, `label` и `values` — и записывайте или отправляйте их: diff --git a/content/ru/smart/navigation/menu.md b/content/ru/smart/navigation/menu.md index 0d1db0db..16eece9f 100644 --- a/content/ru/smart/navigation/menu.md +++ b/content/ru/smart/navigation/menu.md @@ -157,10 +157,6 @@ disabled, open, children}`: в свойстве — массивом, в атр клавишей-модификатором или средней кнопкой событие не отправляет. `sf-menu-toggle` сообщает, что ветка открылась или закрылась. -Прежние имена `sf-menu:navigate` и `sf-menu:toggle` приходят с тем же -`detail` до 2026-12-01; отмена перехода действует для обоих имён. Каждое -обращение к прежнему имени записывается в `SF.deprecations`. - ```js const menu = document.querySelector('sf-menu'); menu.addEventListener('sf-menu-navigate', (event) => { diff --git a/content/ru/smart/navigation/pagination.md b/content/ru/smart/navigation/pagination.md index 8318d12c..49e834fd 100644 --- a/content/ru/smart/navigation/pagination.md +++ b/content/ru/smart/navigation/pagination.md @@ -114,13 +114,6 @@ description: "Переключает страницы длинного спис | `sf-pagination-load-more` | нажата «Показать ещё» | `direction` (всегда `after`), `current`, `total`, `pageSize`, `pageCount`, `hasNext` | | `sf-pagination-count` | запрошено общее количество | `reason`, `current`, `pageSize` | -Прежние имена этих событий — `sf-page-change`, `sf-page-size-change`, -`sf-show-more`, `sf-count-request`, `sf-action-change`, -`sf-action-for-all-change`, `sf-action-apply` и `sf-selection-clear` — -приходят до 2026-12-01 с прежним `detail`: в нём ещё `component`, у -`sf-show-more` — `item`, у `sf-action-apply` — `actionForAll`. Подписывайтесь на -новые. - При смене размера `current` остаётся в пределах нового числа страниц. `goToPage(n)`, `setPageSize(n)` и `showMore()` делают то же из кода, `onPageChange(handler)` передаёт обработчику номер новой страницы первым diff --git a/content/ru/smart/navigation/steps.md b/content/ru/smart/navigation/steps.md index 27da6024..e94207a9 100644 --- a/content/ru/smart/navigation/steps.md +++ b/content/ru/smart/navigation/steps.md @@ -89,7 +89,7 @@ description: "Показывает этапы последовательного Каждый доступный этап — кнопка. Нажатие делает этап текущим и отправляет всплывающее событие `sf-steps-change` с `detail.previous`, `detail.current` (номера с нуля), `detail.item` — данными этапа и `detail.items` — всеми -этапами; `sf-step-change` — его устаревшее имя с тем же `detail`. +этапами. Недоступный этап не нажимается. Если переход назад или вперёд нельзя разрешить без проверки — например, пока форма этапа не заполнена, — отметьте следующие этапы `disabled` и снимайте флаг по мере прохождения. diff --git a/content/ru/smart/navigation/tabs.md b/content/ru/smart/navigation/tabs.md index 89213171..172cf5a5 100644 --- a/content/ru/smart/navigation/tabs.md +++ b/content/ru/smart/navigation/tabs.md @@ -251,8 +251,7 @@ tabs.setItems([ `goTo(index)` открывает вкладку по номеру в объявленном списке, считая с нуля, — и после того, как человек переставил вкладки; на недоступную вкладку он не переключает. При переключении элемент отправляет всплывающее событие `sf-tabs-change` с -`detail.previous`, `detail.current`, `detail.item` и `detail.items` -(`sf-tab-change` — его устаревшее имя с тем же `detail`). +`detail.previous`, `detail.current`, `detail.item` и `detail.items`. `previous` и `current` — номера в объявленном списке, `item` — пункт целиком, с `key`, `label` и `count`. `onTabChange(handler)` подписывается на событие и передаёт обработчику номер новой вкладки первым аргументом. diff --git a/content/ru/smart/navigation/tree-item.md b/content/ru/smart/navigation/tree-item.md index ada21789..ad00f12d 100644 --- a/content/ru/smart/navigation/tree-item.md +++ b/content/ru/smart/navigation/tree-item.md @@ -32,7 +32,7 @@ description: "Задаёт один пункт дерева: подпись, с ``` `label` — подпись пункта; без неё подписью служит текст внутри -``. `text` — другое имя `label`, работает до 2026-12-01. +``. `value` — код пункта: его получает строка как `data-value`, и его несут события. @@ -47,8 +47,7 @@ description: "Задаёт один пункт дерева: подпись, с `kind` • по умолчанию `folder` • [справочник](#атрибуты) -`kind` — `folder` (по умолчанию) или `file`; `type` — другое имя `kind`, -работает до 2026-12-01. Папка с вложенными `` получает кнопку +`kind` — `folder` (по умолчанию) или `file`. Папка с вложенными `` получает кнопку раскрытия и иконку открытой или закрытой папки, файл — иконку документа и никогда не раскрывается. Дерево с файлами удобнее передавать данными, как в [примере Дерево](/ru/smart/navigation/tree/#пункты-данными). @@ -137,10 +136,10 @@ description: "Задаёт один пункт дерева: подпись, с ### События -| Событие | Когда | `event.detail` | Прежнее имя (до 2026-12-01) | -|:---|:---|:---|:---| -| `sf-tree-item-open` | нажата подпись или `Enter` на листе без ссылки | `id`, `value` | `sf-tree-item-click` с `{item, value, originalEvent}` | -| `sf-tree-item-toggle` | ветвь раскрыта или свёрнута | `id`, `open`, `value` | имя то же; элемента в `detail` нет — это `event.target` | +| Событие | Когда | `event.detail` | +|:---|:---|:---| +| `sf-tree-item-open` | нажата подпись или `Enter` на листе без ссылки | `id`, `value` | +| `sf-tree-item-toggle` | ветвь раскрыта или свёрнута | `id`, `open`, `value`; сам пункт — `event.target` | `onToggle(handler)` передаёт обработчику новое состояние первым аргументом. Внутри `` слушайте события дерева — `sf-tree-open` и diff --git a/content/ru/smart/navigation/tree.md b/content/ru/smart/navigation/tree.md index 20a8a0c0..9a166d40 100644 --- a/content/ru/smart/navigation/tree.md +++ b/content/ru/smart/navigation/tree.md @@ -88,7 +88,6 @@ JavaScript. | `one` | выбирает один пункт | | `many` | переключает выбор каждого пункта отдельно | -`single` и `multiple` — другие имена `one` и `many`, работают до 2026-12-01. Значение `selected` — код текущего пункта, `selected-ids` — все выбранные при `many`, по порядку записей. Изначальный выбор задают атрибутом `selected` у пункта или полем `selected: true` у записи. @@ -98,18 +97,17 @@ JavaScript. ### События -| Событие | Когда | `event.detail` | Прежнее имя (до 2026-12-01) | -|:---|:---|:---|:---| -| `sf-tree-open` | нажата подпись пункта или `Enter` на листе без ссылки | `id`, `value` | `sf-tree-item-click` | -| `sf-tree-select` | выбор изменился | `ids` — весь выбор, `id` — нажатый пункт, `selected` | `sf-tree-selection-change` | -| `sf-tree-toggle` | ветвь раскрыта или свёрнута | `id`, `open`, `value` | `sf-tree-item-toggle` | -| `sf-tree-move` | на пункт отпустили записи из списка (`accept-records`) | `ids`, `parent`, `before`, `source` | `sf-tree-records-drop` | -| `sf-tree-attach` | на пункт отпустили файлы (`accept-files`) | `files`, `parent` | `sf-tree-files-drop` | -| `sf-tree-window` | изменилось окно длинного дерева | `first`, `last` | — | +| Событие | Когда | `event.detail` | +|:---|:---|:---| +| `sf-tree-open` | нажата подпись пункта или `Enter` на листе без ссылки | `id`, `value` | +| `sf-tree-select` | выбор изменился | `ids` — весь выбор, `id` — нажатый пункт, `selected` | +| `sf-tree-toggle` | ветвь раскрыта или свёрнута | `id`, `open`, `value` | +| `sf-tree-move` | на пункт отпустили записи из списка (`accept-records`) | `ids`, `parent`, `before`, `source` | +| `sf-tree-attach` | на пункт отпустили файлы (`accept-files`) | `files`, `parent` | +| `sf-tree-window` | изменилось окно длинного дерева | `first`, `last` | События приходят на ``. В `detail` — только данные: код записи, а -не элемент. Прежнее имя приходит следом за новым с прежним `detail`, где -`item` — элемент строки; такие события отмечаются в `SF.deprecations`. `onToggle(handler)` слушает `sf-tree-toggle` и +не элемент. `onToggle(handler)` слушает `sf-tree-toggle` и передаёт обработчику элемент строки первым аргументом. ```js diff --git a/examples/components/dropdown/trigger/index.css b/examples/components/dropdown/trigger/index.css new file mode 100644 index 00000000..8e3640ce --- /dev/null +++ b/examples/components/dropdown/trigger/index.css @@ -0,0 +1,5 @@ +/* The open list hangs below the icon button; the stage keeps room for it + inside the example frame. */ +.sf-dropdown-example-stage { + min-block-size: 26rem; +} diff --git a/examples/components/dropdown/trigger/index.html b/examples/components/dropdown/trigger/index.html index bc26810c..7750dc5c 100644 --- a/examples/components/dropdown/trigger/index.html +++ b/examples/components/dropdown/trigger/index.html @@ -1,3 +1,4 @@ +
@@ -11,3 +12,4 @@
code
+
diff --git a/examples/components/inputs/help-mark/index.html b/examples/components/inputs/help-mark/index.html index df84b480..795379ec 100644 --- a/examples/components/inputs/help-mark/index.html +++ b/examples/components/inputs/help-mark/index.html @@ -1,7 +1,7 @@
- diff --git a/examples/smart-components/contents/auto/index.css b/examples/smart-components/contents/auto/index.css index fe557458..54417eff 100644 --- a/examples/smart-components/contents/auto/index.css +++ b/examples/smart-components/contents/auto/index.css @@ -24,3 +24,13 @@ .contents-demo-article p { margin-block: 0 0.5rem; } + +/* On a phone the article needs the whole width: the column moves above it. */ +@media (max-width: 40rem) { + .contents-demo:not(.contents-demo--stack):not(.contents-demo--rail) { + grid-template-columns: minmax(0, 1fr); + } + .contents-demo:not(.contents-demo--stack):not(.contents-demo--rail) > aside { + order: -1; + } +} diff --git a/examples/smart-components/contents/bar/index.css b/examples/smart-components/contents/bar/index.css index fe557458..54417eff 100644 --- a/examples/smart-components/contents/bar/index.css +++ b/examples/smart-components/contents/bar/index.css @@ -24,3 +24,13 @@ .contents-demo-article p { margin-block: 0 0.5rem; } + +/* On a phone the article needs the whole width: the column moves above it. */ +@media (max-width: 40rem) { + .contents-demo:not(.contents-demo--stack):not(.contents-demo--rail) { + grid-template-columns: minmax(0, 1fr); + } + .contents-demo:not(.contents-demo--stack):not(.contents-demo--rail) > aside { + order: -1; + } +} diff --git a/examples/smart-components/contents/compact/index.css b/examples/smart-components/contents/compact/index.css index fe557458..54417eff 100644 --- a/examples/smart-components/contents/compact/index.css +++ b/examples/smart-components/contents/compact/index.css @@ -24,3 +24,13 @@ .contents-demo-article p { margin-block: 0 0.5rem; } + +/* On a phone the article needs the whole width: the column moves above it. */ +@media (max-width: 40rem) { + .contents-demo:not(.contents-demo--stack):not(.contents-demo--rail) { + grid-template-columns: minmax(0, 1fr); + } + .contents-demo:not(.contents-demo--stack):not(.contents-demo--rail) > aside { + order: -1; + } +} diff --git a/examples/smart-components/contents/overview/index.css b/examples/smart-components/contents/overview/index.css index fe557458..54417eff 100644 --- a/examples/smart-components/contents/overview/index.css +++ b/examples/smart-components/contents/overview/index.css @@ -24,3 +24,13 @@ .contents-demo-article p { margin-block: 0 0.5rem; } + +/* On a phone the article needs the whole width: the column moves above it. */ +@media (max-width: 40rem) { + .contents-demo:not(.contents-demo--stack):not(.contents-demo--rail) { + grid-template-columns: minmax(0, 1fr); + } + .contents-demo:not(.contents-demo--stack):not(.contents-demo--rail) > aside { + order: -1; + } +} diff --git a/examples/smart-components/contents/rail/index.css b/examples/smart-components/contents/rail/index.css index fe557458..54417eff 100644 --- a/examples/smart-components/contents/rail/index.css +++ b/examples/smart-components/contents/rail/index.css @@ -24,3 +24,13 @@ .contents-demo-article p { margin-block: 0 0.5rem; } + +/* On a phone the article needs the whole width: the column moves above it. */ +@media (max-width: 40rem) { + .contents-demo:not(.contents-demo--stack):not(.contents-demo--rail) { + grid-template-columns: minmax(0, 1fr); + } + .contents-demo:not(.contents-demo--stack):not(.contents-demo--rail) > aside { + order: -1; + } +} diff --git a/examples/smart-components/pager/validation/index.html b/examples/smart-components/pager/validation/index.html index 414d371d..f2938310 100644 --- a/examples/smart-components/pager/validation/index.html +++ b/examples/smart-components/pager/validation/index.html @@ -1,8 +1,5 @@
- +

Отметьте флажок, чтобы перейти дальше.

diff --git a/examples/smart-components/settings/api/index.html b/examples/smart-components/settings/api/index.html index da32880f..c5e408e6 100644 --- a/examples/smart-components/settings/api/index.html +++ b/examples/smart-components/settings/api/index.html @@ -6,7 +6,7 @@
-
+
- values: + Значения:
diff --git a/examples/smart-components/settings/api/index.js b/examples/smart-components/settings/api/index.js index 5449f3cf..8aa46a84 100644 --- a/examples/smart-components/settings/api/index.js +++ b/examples/smart-components/settings/api/index.js @@ -8,7 +8,7 @@ const initialize = async () => { document.body.dataset.exampleReady = 'true'; const settings = document.querySelector('#settings-api'); const values = document.querySelector('#settings-api-values'); - const print = () => { values.textContent = JSON.stringify(settings.values); }; + const print = () => { values.textContent = JSON.stringify(settings.values).replace(/,"/g, ', "'); }; settings.addEventListener('sf-settings-changed', print); print(); const calls = { diff --git a/examples/smart-components/spinner/looks/index.css b/examples/smart-components/spinner/looks/index.css index 87c0a6bb..e5047684 100644 --- a/examples/smart-components/spinner/looks/index.css +++ b/examples/smart-components/spinner/looks/index.css @@ -9,3 +9,16 @@ max-inline-size: 100%; overflow-x: auto; } + +/* On a phone the five columns do not fit: each step becomes its own row + of four spinners, and the button follows them. */ +@media (max-width: 32rem) { + .spinner-looks { + grid-template-columns: repeat(4, max-content); + justify-content: start; + } + .spinner-looks > sf-button { + grid-column: 1 / -1; + justify-self: start; + } +} diff --git a/examples/smart-components/tree/selection/index.html b/examples/smart-components/tree/selection/index.html index 9dcd4277..5f67b3df 100644 --- a/examples/smart-components/tree/selection/index.html +++ b/examples/smart-components/tree/selection/index.html @@ -7,5 +7,5 @@
-

Выбрана папка: inbox

+

Выбрана папка: Входящие

diff --git a/examples/smart-components/tree/selection/index.js b/examples/smart-components/tree/selection/index.js index ac6200d5..9b548566 100644 --- a/examples/smart-components/tree/selection/index.js +++ b/examples/smart-components/tree/selection/index.js @@ -1,6 +1,8 @@ document.querySelectorAll('[data-tree-selection-demo]').forEach((demo) => { const result = demo.querySelector('[data-tree-selection-result]'); demo.addEventListener('sf-tree-select', (event) => { - result.textContent = `Выбрана папка: ${event.detail.id}`; + const item = demo.querySelector(`sf-tree-item[value="${CSS.escape(String(event.detail.id))}"]`); + const name = item ? item.getAttribute('label') : event.detail.id; + result.textContent = `Выбрана папка: ${name}`; }); }); From f2cd05b6ccd5ccf23944545f5f587379db098225 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 06:19:39 +0300 Subject: [PATCH 17/53] =?UTF-8?q?Palettes=20page:=20=C2=AB=D0=9F=D0=BE?= =?UTF-8?q?=D1=87=D0=B5=D0=BC=D1=83=20=D1=8D=D1=82=D0=B8=20=D1=82=D0=BE?= =?UTF-8?q?=D0=BD=D0=B0=C2=BB,=20role=20to=20tone=20table,=20alpha=20in=20?= =?UTF-8?q?place=20of=20extra=20tones,=20world=20comparison=20(C43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- content/ru/guide/colour/palettes.md | 73 +++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/content/ru/guide/colour/palettes.md b/content/ru/guide/colour/palettes.md index 11c54a6c..e55f31c2 100644 --- a/content/ru/guide/colour/palettes.md +++ b/content/ru/guide/colour/palettes.md @@ -224,6 +224,79 @@ Framework не использует, не проходит контракт `alp окрашенный `--sf-primary-40`, в тёмной теме останется тёмно-синим на почти чёрном фоне. +## Почему эти тона + +Шестнадцати тонов, белого и чёрного, одиннадцати ступеней +[прозрачности](/ru/guide/colour/transparency/) и нынешнего набора оттенков +хватает на все роли. Сверка с Material 3, Tailwind, Radix, IBM Carbon, +Atlassian, Primer, Ant Design, Apple и Fluent не нашла роли, которой не +досталось бы тона. Там, где другие системы держат лишние сплошные тона, здесь +работает прозрачность. + +### Какой тон берёт роль + +| Роль | Светлая тема | Тёмная тема | +|:---|:---|:---| +| Страница `--sf-surface-page` | белый | `neutral-5` | +| Область `--sf-surface-area` | `neutral-98` | `neutral-10` | +| Карточка, поднятое, всплывающее, вложенное | белый, место отделяет тень | `neutral-15`, `neutral-20`, `neutral-25`, `neutral-30` | +| Обратная поверхность `--sf-surface-inverse` | `neutral-20` | `neutral-90` | +| Основной текст `--sf-on-surface` | `neutral-10` | `neutral-90` | +| Второстепенный текст `--sf-on-surface-variant` | `neutral-40` | `neutral-60`; на поднятом и всплывающем — 70, на вложенном — 80 | +| Акцент и статусы: текст и сплошная заливка | тон 40 | тон 80 | +| Наведение и нажатие на акценте | тона 35 и 30 | тона 85 и 90 | +| Текст на акценте `--sf-on-primary` | белый | `primary-20` | +| Подложка акцента `--sf-primary-container` | `primary-50` при 12 % | `primary-50` при 24 % | +| Текст на подложке акцента `--sf-on-primary-container` | `primary-10` | `primary-90` | +| Наведение, выбор, нажатие | `neutral-50` при 4, 8 и 12 % | `neutral-90` при 4, 8 и 12 % | +| Линия `--sf-outline` | `neutral-50` при 80 % | `neutral-90` при 48 % | +| Тонкая линия `--sf-outline-variant` | `neutral-50` при 24 % | `neutral-90` при 24 % | + +Текст на основном фоне читается с запасом: основной — 17,2 : 1 в светлой +теме и 14,7 : 1 в тёмной, акцентный — около 6,5 : 1 и 11 : 1. + +### Прозрачность вместо лишних тонов + +У Material 3 есть десять особых тонов фона (4, 6, 12, 17, 22, 24, 87, 92, 94, +96): они нужны, чтобы подложки были сплошными. Здесь подложки и состояния — +полупрозрачный нейтральный цвет, и попадают они в ту же светлоту. +`neutral-50` на белом при 4, 8, 12, 16 и 20 % даёт светлоту 98, 96, 94, 92 и +91 — ступени Material 98, 96, 94, 92 и 90. В тёмной теме `neutral-90` на +странице при 8, 12, 16 и 20 % даёт 13, 17, 21 и 25 против 12, 17, 22 и 24. + +Полупрозрачный слой подстраивается под место, на котором лежит: на белой +карточке, на тёмной панели и на цветной шапке он остаётся «на ступеньку +темнее места». Поэтому сплошные двойники для каждого места не нужны. + +### Где нужен сплошной цвет + +- **Текст** берёт только сплошные тона. Единственные полупрозрачные чернила — + у недоступных элементов, `--sf-on-surface-disabled`: к ним требования + контраста не относятся. +- **Там, где под элементом движется содержимое** — липкая колонка таблицы, + кольцо вокруг аватара или бейджа, элемент над картинкой, — полупрозрачный + слой кладётся поверх сплошного цвета места `--sf-surface-place`. Новый тон + для этого не нужен: каждое непрозрачное место объявляет свой цвет, и бейджи, + аватары, флажки, переключатели и слайдеры его читают. Подробнее — + [«Место под вырезом»](/ru/guide/colour/surfaces/#место-под-вырезом). + +### Сколько у других + +| Система | Оттенки | Ступеней у оттенка | Прозрачность | +|:---|:---|:---|:---| +| SIMAI Framework | 15 постоянных, серый и 4 палитры темы | 16, белый и чёрный | 11 ступеней в трёх семействах | +| Material 3 | 5 ключевых и ошибка | 13 и 10 особых тонов фона | слои состояний 8, 10 и 16 % | +| Tailwind 4 | 17 и 9 серых | 11 | любая, без шкалы | +| Radix Colors | 25 и 6 серых | 12 | полупрозрачный двойник каждой ступени | +| IBM Carbon | 9 и 3 серых | 10 | в палитре нет | +| Ant Design | 12 и нейтральный | 10 | у нейтральных текста, линий и фона | + +Шестнадцать ступеней — самая частая лестница из всех, кроме серых Fluent. +Системы с меньшим числом ступеней добирают фоны прозрачностью или лишними +серыми; здесь одна лестница и прозрачность закрывают обе задачи. Новых тонов, +полутонов и оттенков Framework не добавляет: нейтральная палитра сама берёт +оттенок темы, `gray` даёт чистый серый, `brown` — тёплый приглушённый. + ## Замена палитры Проще всего сменить фирменный цвет [генератором темы](/ru/smart/forms/theme-generator/): From cb1a46e19e4386071901acf6c18a37edf64f17e8 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 06:24:04 +0300 Subject: [PATCH 18/53] =?UTF-8?q?Surfaces=20page:=20=C2=AB=D0=A4=D0=BE?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=D1=85=C2=BB,=20component=20to=20backgr?= =?UTF-8?q?ound=20table,=20plate=20inside=20a=20card,=20coloured=20places,?= =?UTF-8?q?=20density=20(C41,=20C42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- content/ru/guide/colour/surfaces.md | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/content/ru/guide/colour/surfaces.md b/content/ru/guide/colour/surfaces.md index 1f6550a9..386c9b13 100644 --- a/content/ru/guide/colour/surfaces.md +++ b/content/ru/guide/colour/surfaces.md @@ -167,6 +167,64 @@ description: "Какой фон брать для страницы, област } ``` +## Фоны в компонентах + +Компонент сам выбирает фон по тому же правилу, что и страница: место — +сплошное, всё, что на месте, — полупрозрачное. Поэтому одно и то же поле, +тег или кнопка правильно выглядят на странице, в боковой области, на +карточке и во всплывающем окне, и автору ничего настраивать не нужно. + +| Что | Фон | Почему так | +|:---|:---|:---| +| Поле с рамкой, кнопка списка выбора, многострочное поле | без фона | рамка держит форму, фон — от места | +| Поле с заливкой | подложка `--sf-surface-container`, под указателем `--sf-surface-container-hover` | полупрозрачна, подходит к любому месту | +| Дорожка сегментов, полоса прогресса, скелетон | подложка | то же | +| Бегунок сегментов | светлая тема — поднятое место с тенью; тёмная — `--sf-surface-container-active` поверх дорожки | бегунок всегда светлее дорожки | +| Тег, тональная кнопка, бейдж | подложка; статусный — мягкий фон статуса | — | +| Выбранная вкладка-«таблетка», нажатая кнопка, выбранный пункт списка | `--sf-primary-container` | одна роль для выбранного | +| Текущий пункт меню навигации | плотнее: `--sf-primary-container-active` и полужирная подпись | отличается и от панели, и от наведения | +| Строка меню или списка под указателем | слой `--sf-surface-state-select` | виден на любом месте и тише выбранного | +| Строка таблицы: полоса и наведение | слой `--sf-surface-state-hover`; отмеченная флажком — `--sf-surface-state-select` | — | +| Коробка флажка, переключателя, радиокнопки | цвет места `--sf-surface-place` | «вырез» совпадает с фоном | +| Код, аккордеон, нейтральное сообщение, подкладка внутри карточки | подложка | одна роль для плашки внутри | +| Карточка | фон карточки; внутри карточки и внутри окна — без фона, только край `--sf-outline-variant` | выше карточки фон не наращивают | +| Открытый список, меню, поповер, уведомление, окно | всплывающее место `--sf-surface-popup` | — | +| Список или меню, открытое из окна или меню | вложенное всплывающее `--sf-surface-popup-nested` | в тёмной теме светлее родителя | +| Короткая подсказка | обратное место `--sf-surface-inverse` | — | +| Недоступное | `--sf-surface-disabled` | — | + +> **Что ещё в пути.** Подложка в 8 % вместо 16 %, бегунок сегментов в +> тёмной теме, карточка без фона внутри окна, белые элементы на сильном +> цветном месте и одно наведение на строку для всех меню и списков приходят +> с ближайшими выпусками Framework. Как они выглядят, показывает +> [прототип фонов в компонентах](/proto/surfaces-in-components.html): там +> эти части дорисованы. + +### Подкладка внутри карточки + +Блок «Итого», группа связанных полей, условия — это не новая карточка, а +плашка внутри: подложка `--sf-surface-container` со скруглением +`--sf-radius--ui`. Подложка — нейтральный цвет при 8 %, на ступеньку темнее +места в обеих темах. Поля на ней — с рамкой: поле с заливкой на подложке +почти не видно. Сплошной цвет области для подкладки не берут: в боковой +области он пропадает, на тёмной карточке проваливается ямой. + +### На цветном месте + +Мягкий цветной фон (`bg-primary-container` и другие мягкие роли) +пересчитывает подложку и слои в свой цвет: поле с заливкой, тег, дорожка +сегментов на нём — того же оттенка, что место, только плотнее. Сильный +цветной фон (шапка основного цвета, `bg-primary`) пересчитывает и +нейтральные роли, и акцент: залитая кнопка, отмеченный флажок и включённый +переключатель на нём берут цвет текста места — белая кнопка на синей шапке. +Сплошные плашки внутри цветного места — карточка, меню, уведомление — +возвращают обычный текст темы. + +### Плотность + +Компактная плотность меняет размеры, но не фоны: в обычной и компактной +версии цвета у всех компонентов одинаковые. + ## Как выбрать фон 1. **Это основное содержимое?** — страница `--sf-surface-page`. From 0c1a78a211b7b4c19bd476ba9f9c4e2806dd7636 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 09:52:50 +0300 Subject: [PATCH 19/53] Recipes: new section with the eight-screen index and why they were chosen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds «Рецепты» after the references in the header and lets the two header audits expect it: recipes assemble reference parts into screens. Co-Authored-By: Claude Opus 5.5 --- content/ru/recipes/index.md | 57 +++++++++++++++++++ content/ru/recipes/index.page.json | 1 + content/ru/recipes/section.json | 1 + content/ru/section.json | 5 ++ .../audit-guide-information-architecture.php | 1 + scripts/audit-reference-separation.php | 3 + 6 files changed, 68 insertions(+) create mode 100644 content/ru/recipes/index.md create mode 100644 content/ru/recipes/index.page.json create mode 100644 content/ru/recipes/section.json diff --git a/content/ru/recipes/index.md b/content/ru/recipes/index.md new file mode 100644 index 00000000..ca76fd3e --- /dev/null +++ b/content/ru/recipes/index.md @@ -0,0 +1,57 @@ +--- +title: "Рецепты" +description: "Готовые сборки типовых экранов админки и сайта из частей SIMAI Framework: живой пример, когда брать, из чего собрано, так и не так, копия для ИИ." +--- + +# Рецепты + +Рецепт — один типовой экран, собранный из частей Framework одним способом. +Каждый рецепт показывает живой пример, говорит, когда его брать, перечисляет +части со ссылками на их страницы, разбирает частые ошибки парами «так / не +так» и даёт копию для ИИ: разметку и правила одним блоком, который агент +вставляет в задачу как есть. + +Рецепты нужны, чтобы один и тот же экран выглядел одинаково, кто бы его ни +собирал — человек или агент. Части и их настройки — на страницах +[компонентов](/ru/components/) и [смарт-компонентов](/ru/smart/); здесь — как +их ставить вместе. + +## Экраны + +| Рецепт | Когда брать | Из чего собрано | +|:---|:---|:---| +| [Список записей](/ru/recipes/resource-list/) | все записи одного вида: сайты, заказы, пользователи | заголовок страницы, поиск, сегментный переключатель, список, пустое состояние | +| [Карточка записи](/ru/recipes/record-card/) | одна запись, которую смотрят и иногда правят | заголовок страницы, контекстное меню, карточка, список сведений, хронология | +| [Форма настроек](/ru/recipes/settings-form/) | много настроек, которые сохраняют вместе | карточки, поля, радиокнопки, переключатели, кнопки, уведомление | +| [Удаление](/ru/recipes/delete/) | что-то удаляют: своё, общее или навсегда | список, уведомление с «Вернуть», подтверждение, окно | +| [Пустой раздел и первое действие](/ru/recipes/empty-state/) | в разделе ещё ничего нет или ничего не нашлось | пустое состояние, кнопка, список | +| [Фильтры в шторке](/ru/recipes/filter-drawer/) | отбор по трём признакам и больше | шторка, флажки, радиокнопки, теги, список | +| [Обзор с плитками](/ru/recipes/dashboard/) | первый экран: что сломалось и как идут дела | «Требует внимания», плитки показателей, хронология | +| [Список уведомлений](/ru/recipes/notifications/) | события, которые ждут человека | кнопка с иконкой, поповер, сегментный переключатель, список по дням | + +## Как выбраны рецепты + +Сюда вошли восемь экранов, которые чаще всего собирают в админке и на +сайте и которые чаще всего собирают по-разному: + +- **список, карточка и форма** — три экрана, через которые проходит любая + работа с записями: найти, посмотреть, изменить; +- **удаление и пустой раздел** — места, где ошибка дороже всего: удалить не + то или оставить человека перед пустым экраном без следующего шага; +- **фильтры, обзор и уведомления** — экраны, которые собирают каждый раз + заново и каждый раз из разных частей. + +Каждый рецепт собран только из того, что есть в выпуске Framework, на +котором стоит эта документация. Где решение уже принято, но в выпуск ещё не +вошло, на странице рецепта есть заметка «Что ещё в пути». + +## Как устроен рецепт + +- **Пример** — живая сборка: всё нажимается, журнал под примером показывает + события. +- **Когда брать** — и когда взять другой рецепт или компонент. +- **Из чего собрано** — части со ссылками на их страницы и настройки, которые + у них заданы. +- **Так и не так** — частые ошибки парами. +- **Копия для ИИ** — разметка и правила одним блоком. Скопируйте его в + задачу агенту целиком: правила в начале блока важнее примера под ними. diff --git a/content/ru/recipes/index.page.json b/content/ru/recipes/index.page.json new file mode 100644 index 00000000..070d216f --- /dev/null +++ b/content/ru/recipes/index.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 0}} diff --git a/content/ru/recipes/section.json b/content/ru/recipes/section.json new file mode 100644 index 00000000..01b87927 --- /dev/null +++ b/content/ru/recipes/section.json @@ -0,0 +1 @@ +{"schema": "docara.section.v1", "title": "Рецепты", "navigation": {"order": 50}} diff --git a/content/ru/section.json b/content/ru/section.json index 6fc48f3f..e33666e6 100644 --- a/content/ru/section.json +++ b/content/ru/section.json @@ -32,6 +32,11 @@ "id": "smart-components", "label": "Смарт-компоненты", "href": "/ru/smart/" + }, + { + "id": "recipes", + "label": "Рецепты", + "href": "/ru/recipes/" } ] } diff --git a/scripts/audit-guide-information-architecture.php b/scripts/audit-guide-information-architecture.php index 13944102..02295b57 100755 --- a/scripts/audit-guide-information-architecture.php +++ b/scripts/audit-guide-information-architecture.php @@ -51,6 +51,7 @@ ['Утилиты', '/ru/utilities/'], ['Компоненты', '/ru/components/'], ['Смарт-компоненты', '/ru/smart/'], + ['Рецепты', '/ru/recipes/'], ]; $check($headerActual === $headerExpected, 'header_navigation_mismatch', ['actual' => $headerActual]); diff --git a/scripts/audit-reference-separation.php b/scripts/audit-reference-separation.php index 29ff29dc..eb970619 100644 --- a/scripts/audit-reference-separation.php +++ b/scripts/audit-reference-separation.php @@ -18,6 +18,9 @@ ['Утилиты', '/ru/utilities/'], ['Компоненты', '/ru/components/'], ['Смарт-компоненты', '/ru/smart/'], + // Recipes assemble the reference parts into screens: a guide-side + // section after the references, never a reference of its own. + ['Рецепты', '/ru/recipes/'], ], 'header_is_not_guide_plus_references', ['actual' => $actualHeader]); // content/ru/reference is on this list from 2026-10-09. It held 66 pages of From 5b0ec456561dd6dd5528de8a0d3c6c82c1f77de2 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 09:52:50 +0300 Subject: [PATCH 20/53] Recipes: resource list -- header with count, search, two-way filter, rows, nothing-found Co-Authored-By: Claude Opus 5.5 --- content/ru/recipes/resource-list.md | 120 ++++++++++++++++++ content/ru/recipes/resource-list.page.json | 1 + .../recipes/resource-list/do-dont/index.html | 26 ++++ .../recipes/resource-list/do-dont/index.js | 19 +++ .../recipes/resource-list/overview/index.html | 14 ++ .../recipes/resource-list/overview/index.js | 85 +++++++++++++ 6 files changed, 265 insertions(+) create mode 100644 content/ru/recipes/resource-list.md create mode 100644 content/ru/recipes/resource-list.page.json create mode 100644 examples/recipes/resource-list/do-dont/index.html create mode 100644 examples/recipes/resource-list/do-dont/index.js create mode 100644 examples/recipes/resource-list/overview/index.html create mode 100644 examples/recipes/resource-list/overview/index.js diff --git a/content/ru/recipes/resource-list.md b/content/ru/recipes/resource-list.md new file mode 100644 index 00000000..61019572 --- /dev/null +++ b/content/ru/recipes/resource-list.md @@ -0,0 +1,120 @@ +--- +title: "Список записей" +description: "Страница админки со списком сайтов, заказов или пользователей: заголовок с числом и главным действием, поиск, отбор по состоянию и строки, которые открывают запись." +--- + +# Список записей + +Самый частый экран админки: все записи одного вида на одной странице — +сайты, заказы, пользователи, письма. Сверху заголовок с числом записей и +одним главным действием, под ним поиск и отбор, ниже строки. Нажатие на +строку открывает запись, остальные действия — в меню строки. + +## Пример + +Наберите в поиске «блог» или выберите «С проблемами»; запрос без +результатов показывает пустое состояние с кнопкой «Показать все». Строка +открывается нажатием, меню «⋯» — на каждой строке. + +:::example {id="recipes/resource-list/overview" label="Сайты" preview=sandbox log="sf-list-open sf-list-apply change" log_console=true log_open=false} +::: + +## Когда брать + +- Записей одного вида больше, чем помещается на экран, и человек ищет + нужную, чтобы открыть её. +- У каждой записи есть название и одна короткая вторая строка: домен и тип, + автор и дата, сумма и клиент. + +Не берите список, когда записи сравнивают по многим колонкам — суммы, +даты, ответственные рядом друг с другом: это таблица +[Представления данных](/ru/pro/admin/table/). Список из трёх-пяти записей +без поиска — это [Список](/ru/smart/content/list/) в карточке, без заголовка +страницы. + +## Из чего собрано + +| Часть | Компонент | Как задано | +|:---|:---|:---| +| Заголовок, путь и число | [Заголовок страницы](/ru/smart/composite/page-header/) | `heading`, `count`; путь — [Хлебные крошки](/ru/smart/navigation/breadcrumbs/) в слоте `path` | +| Главное действие | [Кнопка](/ru/smart/actions/buttons/) | в слоте `actions`, вид по умолчанию: залитая основного цвета, одна на экран | +| Второе действие | [Кнопка](/ru/smart/actions/buttons/) | `appearance="outline"` `scheme="on-surface"` | +| Поиск | [Текстовое поле](/ru/smart/forms/inputs/) | `input-type="search"`, `icon-start="search"`, `aria-label` | +| Отбор | [Сегментный переключатель](/ru/smart/actions/segmented/) | дорожка с плашкой, два-три коротких варианта с `count`: на телефоне они должны поместиться в строку | +| Строки | [Список](/ru/smart/content/list/) | строки скриптом: `text`, `supporting`, `icon`, `badge` только у записей с проблемой; меню строки — `actions` | +| Ничего не найдено | [Пустое состояние](/ru/smart/feedback/empty-state/) | в слоте `empty` списка, `kind="nothing-found"`, одно действие «Показать все» | + +Экран админки собирается в компактной плотности: `data-sf-density="compact"` +на корне экрана сдвигает все части на ступень вниз вместе +([Плотность](/ru/guide/size-and-type/density/)). + +## Так и не так + +:::example {id="recipes/resource-list/do-dont" label="Так и не так"} +::: + +- **Одно залитое действие.** «Создать» — единственная залитая кнопка + основного цвета; «Экспорт», «Импорт» — с рамкой. Две залитые кнопки рядом + спорят, и главное действие теряется. +- **Бейдж — только у проблемы.** Запись, у которой всё в порядке, без + бейджа; бейдж стоит у остановленной, у той, где истекает сертификат или + нет копии. Зелёное «Работает» на каждой строке — шум, в котором не видно + проблему. +- **Строка открывается сама.** Нажатие на строку открывает запись; кнопки + «Открыть» в строке нет. Действия — в меню «⋯», в нём же опасные. + +## Копия для ИИ + +Скопируйте блок целиком: в нём разметка экрана и правила, по которым его +собирают. + +```html + +
+ + + + + +
+ + +
+ + + +
+ +``` + +> **Что ещё в пути.** Список ресурсов как вид таблицы представления — факты +> бейджами в ровных колонках, до трёх быстрых действий в строке и +> переключатель «список / таблица», который запоминается, — приходит с +> набором компонентов `view`. До него список собирается из частей на этой +> странице. Как он будет выглядеть, показывает +> [прототип списка ресурсов](/proto/view-resource-list.html). + +## См. также + +- [Карточка записи](/ru/recipes/record-card/) — то, что открывает строка +- [Пустой список](/ru/recipes/empty-state/) — первый экран, когда записей ещё нет +- [Фильтры в шторке](/ru/recipes/filter-drawer/) — когда отбора по одному признаку мало +- [Удаление](/ru/recipes/delete/) diff --git a/content/ru/recipes/resource-list.page.json b/content/ru/recipes/resource-list.page.json new file mode 100644 index 00000000..cc969543 --- /dev/null +++ b/content/ru/recipes/resource-list.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 10}} diff --git a/examples/recipes/resource-list/do-dont/index.html b/examples/recipes/resource-list/do-dont/index.html new file mode 100644 index 00000000..7b585bc3 --- /dev/null +++ b/examples/recipes/resource-list/do-dont/index.html @@ -0,0 +1,26 @@ +
+
+
+
+ + +
+
Так — одна залитая кнопка, второе действие с рамкой
+
+
+
+ + +
+
Не так — две залитые кнопки спорят, главное не видно
+
+
+
+
Так — бейдж только у записи с проблемой
+
+
+
+
Не так — «Работает» на каждой строке прячет проблему
+
+
+
diff --git a/examples/recipes/resource-list/do-dont/index.js b/examples/recipes/resource-list/do-dont/index.js new file mode 100644 index 00000000..8ccecdd1 --- /dev/null +++ b/examples/recipes/resource-list/do-dont/index.js @@ -0,0 +1,19 @@ +// Two lists with the same three sites: the first marks only the site with a +// problem, the second puts a status badge on every row. +const rows = [ + {id: 'a', text: 'Магазин', supporting: 'nord-wind.ru', icon: 'storefront'}, + {id: 'b', text: 'Блог', supporting: 'travel-notes.ru', icon: 'article', badge: {text: 'Сертификат', tone: 'warning'}}, + {id: 'c', text: 'Визитка', supporting: 'simai.example', icon: 'language'}, +]; + +const initialize = async () => { + await customElements.whenDefined('sf-list'); + const good = document.querySelector('#rows-do'); + const bad = document.querySelector('#rows-dont'); + if (!good || good.dataset.exampleReady === 'true') return; + good.dataset.exampleReady = 'true'; + good.setItems({items: rows, options: {hasMore: false}}); + bad.setItems({items: rows.map((row) => ({...row, badge: row.badge ?? {text: 'Работает', tone: 'success'}})), options: {hasMore: false}}); +}; + +initialize(); diff --git a/examples/recipes/resource-list/overview/index.html b/examples/recipes/resource-list/overview/index.html new file mode 100644 index 00000000..bb97d7e1 --- /dev/null +++ b/examples/recipes/resource-list/overview/index.html @@ -0,0 +1,14 @@ +
+ + + + + +
+ + +
+ + + +
diff --git a/examples/recipes/resource-list/overview/index.js b/examples/recipes/resource-list/overview/index.js new file mode 100644 index 00000000..c45349a3 --- /dev/null +++ b/examples/recipes/resource-list/overview/index.js @@ -0,0 +1,85 @@ +// Resource list: the page owns the data, the list owns the rows. Search and +// the «all / with problems» switch filter the same array; an empty result shows the list's +// own empty state with one way back. A row opens the record, the «⋯» menu +// carries the rest; deleting goes through the delete recipe. The example +// log shows what the page receives. +const sites = [ + {id: 's1', text: 'Магазин «Северный ветер»', supporting: 'nord-wind.ru · Магазин', icon: 'storefront', state: 'on'}, + {id: 's2', text: 'Корпоративный сайт', supporting: 'simai.example · Визитка', icon: 'language', state: 'on'}, + {id: 's3', text: 'Блог о путешествиях', supporting: 'travel-notes.ru · Блог', icon: 'article', state: 'on', badge: {text: 'Сертификат', tone: 'warning'}}, + {id: 's4', text: 'Лендинг весенней акции', supporting: 'spring.nord-wind.ru · Лендинг', icon: 'campaign', state: 'off', badge: {text: 'Остановлен', tone: 'neutral'}}, + {id: 's5', text: 'Документация API', supporting: 'docs.simai.example · Документация', icon: 'menu_book', state: 'on'}, + {id: 's6', text: 'Портал партнёров', supporting: 'partners.simai.example · Портал', icon: 'handshake', state: 'on', badge: {text: 'Нет копии', tone: 'error'}}, + {id: 's7', text: 'Архив новостей 2024', supporting: 'news-2024.simai.example · Блог', icon: 'article', state: 'off', badge: {text: 'Остановлен', tone: 'neutral'}}, + {id: 's8', text: 'Школа программирования', supporting: 'code-school.ru · Курсы', icon: 'school', state: 'on'}, + {id: 's9', text: 'Кофейня на Садовой', supporting: 'sadovaya.coffee · Визитка', icon: 'local_cafe', state: 'on'}, +]; + +// The list moves its slotted empty state while it draws its first frame: +// wait a few frames for it rather than reading it mid-move. +const settled = async (selector) => { + for (let frame = 0; frame < 30; frame++) { + const element = document.querySelector(selector); + if (element) return element; + await new Promise((resolve) => requestAnimationFrame(resolve)); + } + return null; +}; + +const initialize = async () => { + await Promise.all(['sf-list', 'sf-input', 'sf-segmented', 'sf-empty-state'].map((tag) => customElements.whenDefined(tag))); + const list = document.querySelector('#sites'); + const search = document.querySelector('#sites-search'); + const state = document.querySelector('#sites-state'); + if (!list || list.dataset.exampleReady === 'true') return; + list.dataset.exampleReady = 'true'; + + let query = ''; + let shown = 'all'; + const render = () => { + const words = query.toLowerCase().split(/\s+/).filter(Boolean); + const items = sites.filter((site) => (shown === 'all' || Boolean(site.badge)) + && words.every((word) => `${site.text} ${site.supporting}`.toLowerCase().includes(word))); + // A stopped site has nothing to stop: its row narrows the menu. + list.setItems({items: items.map((site) => (site.state === 'off' ? {...site, actions: ['visit', 'copy']} : site)), options: {hasMore: false}}); + }; + + list.actions = [ + {id: 'visit', text: 'Открыть сайт', icon: 'open_in_new'}, + {id: 'copy', text: 'Копировать домен', icon: 'content_copy'}, + {id: 'stop', text: 'Остановить', icon: 'pause_circle'}, + ]; + + search.addEventListener('input', () => { + query = String(search.value ?? ''); + render(); + }); + state.addEventListener('change', (event) => { + shown = event.detail.value; + render(); + }); + const empty = await settled('#sites-empty'); + empty.actions = [{id: 'reset', text: 'Показать все', appearance: 'outline'}]; + empty.addEventListener('sf-empty-state-action', () => { + query = ''; + shown = 'all'; + search.value = ''; + state.value = 'all'; + render(); + }); + list.addEventListener('sf-list-open', (event) => { + list.selected = event.detail.id; + }); + list.addEventListener('sf-list-apply', (event) => { + if (event.detail.action !== 'stop') return; + const site = sites.find((entry) => entry.id === event.detail.ids[0]); + if (!site) return; + site.state = 'off'; + site.badge = {text: 'Остановлен', tone: 'neutral'}; + render(); + }); + + render(); +}; + +initialize(); From 90db0bce126957e8e29fa43928578948cfd9e170 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 09:52:50 +0300 Subject: [PATCH 21/53] =?UTF-8?q?Recipes:=20record=20card=20--=20facts=20a?= =?UTF-8?q?s=20text=20with=20=C2=AB=D0=9D=D0=B5=20=D1=83=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D0=B0=D0=BD=C2=BB,=20menu=20for=20the=20rest,=20history=20time?= =?UTF-8?q?line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- content/ru/recipes/record-card.md | 127 ++++++++++++++++++ content/ru/recipes/record-card.page.json | 1 + .../recipes/record-card/do-dont/index.html | 35 +++++ .../recipes/record-card/overview/index.html | 38 ++++++ .../recipes/record-card/overview/index.js | 22 +++ 5 files changed, 223 insertions(+) create mode 100644 content/ru/recipes/record-card.md create mode 100644 content/ru/recipes/record-card.page.json create mode 100644 examples/recipes/record-card/do-dont/index.html create mode 100644 examples/recipes/record-card/overview/index.html create mode 100644 examples/recipes/record-card/overview/index.js diff --git a/content/ru/recipes/record-card.md b/content/ru/recipes/record-card.md new file mode 100644 index 00000000..ad3a1419 --- /dev/null +++ b/content/ru/recipes/record-card.md @@ -0,0 +1,127 @@ +--- +title: "Карточка записи" +description: "Страница одной записи: путь назад к списку, заголовок с действиями, сведения парами «подпись — значение» и история изменений." +--- + +# Карточка записи + +Страница, которую открывает строка списка: всё об одной записи — сайте, +заказе, пользователе. Сверху путь обратно к списку и действия с записью, +ниже сведения парами «подпись — значение» в карточке с кнопкой «Изменить» +и история: что с записью происходило. + +## Пример + +Меню «⋯» в заголовке держит остальные действия, опасное — последним, за +линией. На узком экране история встаёт под сведения. + +:::example {id="recipes/record-card/overview" label="Сайт" preview=sandbox log="sf-menu-context-select" log_console=true log_open=false} +::: + +## Когда брать + +- Человек пришёл посмотреть одну запись и, может быть, поправить её. +- Сведения короткие: адрес, тип, срок, ответственный, сумма. + +Когда запись правят часто и по одному полю, берите +[Свойства записи](/ru/pro/admin/property-view/): там каждое значение +правится на месте. Сводку из двух-четырёх чисел без подписей в колонке +показывайте [Плитками сводки](/ru/recipes/dashboard/). + +## Из чего собрано + +| Часть | Компонент | Как задано | +|:---|:---|:---| +| Путь и название | [Заголовок страницы](/ru/smart/composite/page-header/) | `heading` — название записи; путь — [Хлебные крошки](/ru/smart/navigation/breadcrumbs/) в слоте `path`, последний пункт без ссылки | +| Переход на сайт | [Кнопка](/ru/smart/actions/buttons/) | `appearance="outline"` `scheme="on-surface"`, `href` | +| Остальные действия | [Кнопка с иконкой](/ru/smart/actions/icon-buttons/) и [Контекстное меню](/ru/smart/navigation/context-menu/) | `icon="more_horiz"`, `aria-label`, `aria-haspopup="menu"`; опасный пункт — `danger: true`, последним | +| Раздел | [Карточка](/ru/components/content/card/) | `section.sf-card` с заголовком `h2` и кнопкой «Изменить» `appearance="ghost"` `scheme="on-surface"` | +| Сведения | [Список сведений](/ru/smart/content/list-description/) | `empty-text="Не указан"`; состояние — `status` у строки, путь — `mono` и `copy` | +| История | [Хронология](/ru/smart/content/list-timeline/) | `size="1/2"`, события новые сверху | + +## Так и не так + +:::example {id="recipes/record-card/do-dont" label="Так и не так"} +::: + +- **Пустое — словами.** Пустое значение пишется «Не указан»: прочерк + читается как «не загрузилось». +- **Сведения — текстом.** Карточка записи показывает значения текстом; + править их — по кнопке «Изменить» у раздела. Поля «только для чтения» + выглядят как форма, которую нельзя заполнить. +- **Сведения не в таблицу с рамками.** Пары «подпись — значение» — это + список сведений, а не таблица: рамки между ячейками делят то, что читается + одной строкой. +- **Одна кнопка-ссылка, остальное в «⋯».** В заголовке — переход и меню; + «Удалить» не стоит отдельной красной кнопкой рядом с «Изменить». + +## Копия для ИИ + +```html + +
+
+ + + + + + +
+
+
+

Сведения

+ +
+ + + +
+
+

История

+ + + +
+
+
+
+ +``` + +> **Что ещё в пути.** Карточка записи с разделами в одной колонке подписей +> на всю страницу, значениями-бейджами, знаком справки у подписи, +> действием «Указать» у пустого значения и кнопкой «Копировать», видной +> всегда, приходит с новой версией списка сведений. Пока каждый раздел — +> свой список сведений, и колонка подписей у каждого своя. Как это будет +> выглядеть, показывает [прототип карточки записи](/proto/list-description.html). + +## См. также + +- [Список записей](/ru/recipes/resource-list/) — откуда открывается карточка +- [Форма настроек](/ru/recipes/settings-form/) — то, что открывает «Изменить» +- [Удаление](/ru/recipes/delete/) — пункт «Удалить» в меню «⋯» diff --git a/content/ru/recipes/record-card.page.json b/content/ru/recipes/record-card.page.json new file mode 100644 index 00000000..12cb21b4 --- /dev/null +++ b/content/ru/recipes/record-card.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 20}} diff --git a/examples/recipes/record-card/do-dont/index.html b/examples/recipes/record-card/do-dont/index.html new file mode 100644 index 00000000..b4b76bc8 --- /dev/null +++ b/examples/recipes/record-card/do-dont/index.html @@ -0,0 +1,35 @@ +
+
+
+
+ + + +
+
Так — пустое значение словами: «Не указан»
+
+
+
+ + + +
+
Не так — прочерк: не понять, пусто или не загрузилось
+
+
+
+ + + +
+
Так — сведения текстом, править — по «Изменить»
+
+
+
+ + +
+
Не так — поля только для чтения вместо сведений
+
+
+
diff --git a/examples/recipes/record-card/overview/index.html b/examples/recipes/record-card/overview/index.html new file mode 100644 index 00000000..41b38f58 --- /dev/null +++ b/examples/recipes/record-card/overview/index.html @@ -0,0 +1,38 @@ +
+
+ + + + + + +
+
+
+

Сведения

+ +
+ + + +
+
+

История

+ + + +
+
+
+
diff --git a/examples/recipes/record-card/overview/index.js b/examples/recipes/record-card/overview/index.js new file mode 100644 index 00000000..bf956206 --- /dev/null +++ b/examples/recipes/record-card/overview/index.js @@ -0,0 +1,22 @@ +// The record page: the header carries the path, one link out and «⋯» for the +// rest; the facts card edits as one section; the history sits beside it on a +// wide screen and below it on a narrow one. +const initialize = async () => { + await Promise.all(['sf-menu-context', 'sf-button-icon'].map((tag) => customElements.whenDefined(tag))); + const anchor = document.querySelector('#record-more'); + const menu = document.querySelector('#record-menu'); + if (!menu || menu.dataset.exampleReady === 'true') return; + menu.dataset.exampleReady = 'true'; + menu.entries = [ + {id: 'copy', text: 'Копировать адрес', icon: 'content_copy'}, + {id: 'stop', text: 'Остановить', icon: 'pause_circle'}, + {type: 'separator'}, + {id: 'delete', text: 'Удалить сайт', icon: 'delete', danger: true}, + ]; + anchor.addEventListener('click', () => menu.toggleFor({anchor})); + menu.addEventListener('sf-menu-context-select', (event) => { + docara.example.log('sf-menu-context-select', {id: event.detail.id}); + }); +}; + +initialize(); From 07654258c598dcdb86e0c9a2e8e8a6bf9aa76acd Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 09:52:50 +0300 Subject: [PATCH 22/53] Recipes: settings form -- card sections, quiet cancel, error under the field, toast on save Co-Authored-By: Claude Opus 5.5 --- content/ru/recipes/settings-form.md | 125 ++++++++++++++++++ content/ru/recipes/settings-form.page.json | 1 + .../recipes/settings-form/do-dont/index.html | 43 ++++++ .../recipes/settings-form/overview/index.html | 35 +++++ .../recipes/settings-form/overview/index.js | 38 ++++++ 5 files changed, 242 insertions(+) create mode 100644 content/ru/recipes/settings-form.md create mode 100644 content/ru/recipes/settings-form.page.json create mode 100644 examples/recipes/settings-form/do-dont/index.html create mode 100644 examples/recipes/settings-form/overview/index.html create mode 100644 examples/recipes/settings-form/overview/index.js diff --git a/content/ru/recipes/settings-form.md b/content/ru/recipes/settings-form.md new file mode 100644 index 00000000..7bbc85fd --- /dev/null +++ b/content/ru/recipes/settings-form.md @@ -0,0 +1,125 @@ +--- +title: "Форма настроек" +description: "Страница настроек записи или приложения: разделы карточками, поля с подписями, переключатели, одна кнопка «Сохранить» и уведомление о результате." +--- + +# Форма настроек + +Страница, на которой человек меняет настройки сайта, проекта или своего +профиля. Настройки разложены по разделам — у каждого своя карточка с +заголовком, — ответы стоят под последним разделом: «Сохранить» и «Отмена». +Ошибку видно у самого поля, удачное сохранение подтверждает уведомление в +углу. + +## Пример + +Сотрите название и нажмите «Сохранить»: поле покажет, что не так, и курсор +встанет в него. С названием форма сохранится, а в углу появится +уведомление. «Отмена» возвращает начальные значения. + +:::example {id="recipes/settings-form/overview" label="Настройки сайта" preview=sandbox log="change" log_console=true log_open=false} +::: + +## Когда брать + +- Настроек больше пяти-шести, и они делятся на разделы по смыслу. +- Изменения применяются вместе, одной кнопкой. + +Одну-две настройки, которые применяются сразу, ставьте переключателями +прямо на странице, без кнопки «Сохранить». Настройки вида — плотность, +колонки, сортировку — открывайте [Меню настроек](/ru/smart/navigation/settings/), +а короткую форму из двух-трёх полей — в окне. + +## Из чего собрано + +| Часть | Компонент | Как задано | +|:---|:---|:---| +| Заголовок и путь | [Заголовок страницы](/ru/smart/composite/page-header/) | `heading`; путь через запись, к которой относятся настройки | +| Раздел | [Карточка](/ru/components/content/card/) | `section.sf-card` с заголовком `h2`; колонка формы не шире `max-w-h0` | +| Текстовые поля | [Текстовое поле](/ru/smart/forms/inputs/), [Многострочное поле](/ru/smart/forms/textarea/) | `label` над полем; `hint` — пояснение; `required` и `error-message` — у обязательного | +| Выбор одного | [Радиокнопки](/ru/smart/forms/radio/) | в `fieldset` с `legend`; у вариантов `description` | +| Включить и выключить | [Переключатель](/ru/smart/forms/switch/) | `label` и `description` | +| Ответы | [Кнопка](/ru/smart/actions/buttons/) | «Сохранить» — `native-type="submit"`, вид по умолчанию; «Отмена» — `native-type="reset"` `appearance="ghost"` `scheme="on-surface"` | +| Результат | [Уведомление](/ru/smart/feedback/toast/) | `status="success"`, `duration="5000"`, в контейнере с `role="status"` в углу | + +## Так и не так + +:::example {id="recipes/settings-form/do-dont" label="Так и не так"} +::: + +- **«Отмена» — тихая.** Красный цвет говорит «удалит или не вернётся»; + «Отмена» ничего не ломает, поэтому она без заливки. +- **Ошибка — у поля.** Сообщение стоит под полем и говорит, что сделать. + Общая плашка «Форма заполнена с ошибками» над формой не показывает, где + ошибка, и человек ищет её сам. +- **Подпись — над полем.** Подпись видна всегда; в самом поле — только + образец формата. Подпись внутри поля пропадает, как только человек + начинает печатать. +- **Одна кнопка «Сохранить» на форму.** Не по кнопке в каждой карточке: + человек не понимает, что сохранилось, а что нет. + +## Копия для ИИ + +```html + +
+ + + +
+
+

Основное

+ + +
+
+

Доступ

+
+ Кто видит сайт + + +
+ +
+
+ + +
+
+
+
+ +``` + +> **Что ещё в пути.** Группа связанных полей на подложке внутри карточки — +> на спокойной серой плашке в 8 % вместо нынешних 16 % — приходит с +> ближайшим выпуском Framework; до него разделы не делятся плашками внутри +> карточки. Как это выглядит, показывает +> [прототип фонов в компонентах](/proto/surfaces-in-components.html). + +## См. также + +- [Карточка записи](/ru/recipes/record-card/) — откуда открываются настройки +- [Удаление](/ru/recipes/delete/) — опасные настройки: удалить сайт, сбросить данные diff --git a/content/ru/recipes/settings-form.page.json b/content/ru/recipes/settings-form.page.json new file mode 100644 index 00000000..4e0ddcc5 --- /dev/null +++ b/content/ru/recipes/settings-form.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 30}} diff --git a/examples/recipes/settings-form/do-dont/index.html b/examples/recipes/settings-form/do-dont/index.html new file mode 100644 index 00000000..a3099632 --- /dev/null +++ b/examples/recipes/settings-form/do-dont/index.html @@ -0,0 +1,43 @@ +
+
+
+
+ + +
+
Так — «Отмена» тихая, она ничего не ломает
+
+
+
+ + +
+
Не так — красный значит «удалит», а не «закроет»
+
+
+
+ +
+
Так — ошибка под полем и что сделать
+
+
+
+ + +
+
Не так — общая ошибка сверху, поле не отмечено
+
+
+
+ +
+
Так — подпись над полем, в поле — образец
+
+
+
+ +
+
Не так — подпись в поле пропадает при вводе
+
+
+
diff --git a/examples/recipes/settings-form/overview/index.html b/examples/recipes/settings-form/overview/index.html new file mode 100644 index 00000000..389d5bdd --- /dev/null +++ b/examples/recipes/settings-form/overview/index.html @@ -0,0 +1,35 @@ +
+
+ + + +
+
+

Основное

+ + + +
+
+

Доступ

+
+ Кто видит сайт + + + +
+ +
+
+

Уведомления

+ + +
+
+ + +
+
+
+
+
diff --git a/examples/recipes/settings-form/overview/index.js b/examples/recipes/settings-form/overview/index.js new file mode 100644 index 00000000..fa9b6223 --- /dev/null +++ b/examples/recipes/settings-form/overview/index.js @@ -0,0 +1,38 @@ +// A settings form: sections are cards, the answers sit under the last card. +// Saving checks the one required field first; an empty name keeps the form +// and puts the cursor into the field, a filled one reports the save in a +// toast in the corner. «Отмена» puts the starting values back. +const initialize = async () => { + await Promise.all(['sf-input', 'sf-button'].map((tag) => customElements.whenDefined(tag))); + const form = document.querySelector('#settings-form'); + const name = document.querySelector('#settings-name'); + const toasts = document.querySelector('#settings-toasts'); + if (!form || form.dataset.exampleReady === 'true') return; + form.dataset.exampleReady = 'true'; + + name.addEventListener('input', () => { + if (String(name.value ?? '').trim()) name.removeAttribute('invalid'); + }); + form.addEventListener('submit', (event) => { + event.preventDefault(); + if (!String(name.value ?? '').trim()) { + name.setAttribute('invalid', ''); + name.focus(); + docara.example.log('invalid', {field: 'name'}); + return; + } + const toast = document.createElement('sf-toast'); + toast.setAttribute('status', 'success'); + toast.setAttribute('icon', 'check_circle'); + toast.setAttribute('title', 'Настройки сохранены'); + toast.setAttribute('duration', '5000'); + toasts.append(toast); + docara.example.log('submit', Object.fromEntries(new FormData(form))); + }); + form.addEventListener('reset', () => { + name.removeAttribute('invalid'); + docara.example.log('reset', {}); + }); +}; + +initialize(); From 5a6c9d91f9ac61e06477dca780d783e177a34d2f Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 09:52:50 +0300 Subject: [PATCH 23/53] Recipes: delete -- own with undo, shared asked in the row, final in a window with the name typed Co-Authored-By: Claude Opus 5.5 --- content/ru/recipes/delete.md | 135 +++++++++++++++++++++ content/ru/recipes/delete.page.json | 1 + examples/recipes/delete/do-dont/index.html | 20 +++ examples/recipes/delete/final/index.css | 4 + examples/recipes/delete/final/index.html | 23 ++++ examples/recipes/delete/final/index.js | 27 +++++ examples/recipes/delete/own/index.css | 4 + examples/recipes/delete/own/index.html | 7 ++ examples/recipes/delete/own/index.js | 41 +++++++ examples/recipes/delete/shared/index.html | 24 ++++ examples/recipes/delete/shared/index.js | 48 ++++++++ 11 files changed, 334 insertions(+) create mode 100644 content/ru/recipes/delete.md create mode 100644 content/ru/recipes/delete.page.json create mode 100644 examples/recipes/delete/do-dont/index.html create mode 100644 examples/recipes/delete/final/index.css create mode 100644 examples/recipes/delete/final/index.html create mode 100644 examples/recipes/delete/final/index.js create mode 100644 examples/recipes/delete/own/index.css create mode 100644 examples/recipes/delete/own/index.html create mode 100644 examples/recipes/delete/own/index.js create mode 100644 examples/recipes/delete/shared/index.html create mode 100644 examples/recipes/delete/shared/index.js diff --git a/content/ru/recipes/delete.md b/content/ru/recipes/delete.md new file mode 100644 index 00000000..4fd6801a --- /dev/null +++ b/content/ru/recipes/delete.md @@ -0,0 +1,135 @@ +--- +title: "Удаление" +description: "Как удалять по-разному в зависимости от того, кто потеряет и можно ли вернуть: сразу с «Вернуть», вопросом в строке или окном с вводом названия." +--- + +# Удаление + +Как спросить перед удалением, зависит от двух вопросов: потеряет ли +удалённое кто-то ещё и можно ли его вернуть. Своё и обратимое удаляется +сразу, а уведомление даёт «Вернуть». Общее спрашивают в той же строке. То, +что нельзя вернуть и что задевает многих, спрашивают окном: последствия, +решающая фраза и название, которое нужно ввести. + +## Пример + +### Своё — без вопроса + +Удалите фильтр через «⋯»: он пропадёт сразу, а в углу появится +уведомление с «Вернуть». + +:::example {id="recipes/delete/own" label="Свой фильтр" preview=sandbox log="sf-list-apply sf-toast-action" log_console=true log_open=false} +::: + +### Общее — вопрос в строке + +Нажмите корзину: строка превратится в вопрос. «Отмена» вернёт строку и +поставит фокус на корзину. + +:::example {id="recipes/delete/shared" label="Файл проекта" preview=sandbox log="sf-confirm-answer" log_console=true log_open=false} +::: + +### Нельзя вернуть — окно + +Нажмите «Удалить сайт», затем красную кнопку, не вводя название: курсор +встанет в поле, и под ним появится объяснение. + +:::example {id="recipes/delete/final" label="Удаление сайта" preview=sandbox log="sf-modal-open sf-modal-closed" log_console=true log_open=false} +::: + +## Когда брать + +| Что удаляют | Как спросить | +|:---|:---| +| Своё и обратимое: свой фильтр, черновик, свой вид таблицы | Не спрашивать. Удалить сразу, уведомление с «Вернуть» | +| Общее: файл проекта, комментарий, участник | Вопрос в строке, где нажали «Удалить» | +| Нельзя вернуть или задевает многих: сайт, база, все данные | Окно: последствия, «Вернуть будет нельзя», ввод названия | + +Опасное действие, которое не удаляет, — остановить сайт, сбросить настройки +— спрашивают так же, по тем же двум вопросам. + +## Из чего собрано + +| Часть | Компонент | Как задано | +|:---|:---|:---| +| «Удалить» в меню строки | [Список](/ru/smart/content/list/) | действие `{id: 'delete', destructive: true}` в `actions` | +| «Вернуть» | [Уведомление](/ru/smart/feedback/toast/) | `title` называет удалённое, `action-text="Вернуть"`, `duration="8000"` | +| Корзина в строке | [Кнопка с иконкой](/ru/smart/actions/icon-buttons/) | `icon="delete"` `appearance="ghost"` `scheme="on-surface-variant"`, `aria-label` с названием | +| Вопрос в строке | [Подтверждение](/ru/smart/actions/confirm/) | `question` называет, что удалится и у кого; `status="danger"`, `accept-text="Удалить"` | +| Кнопка, открывающая окно | [Кнопка](/ru/smart/actions/buttons/) | `status="danger"` `appearance="outline"`, `data-sf-modal-open` | +| Окно | [Окно](/ru/smart/overlays/modal/) | `heading` — вопрос с названием; форма в `slot="content"`, `show-footer="false"` | +| Последствия | плашка | `bg-surface-container radius-surface p-1`; решающая фраза — отдельной строкой `weight-5` | +| Ввод названия | [Текстовое поле](/ru/smart/forms/inputs/) | подпись «Чтобы подтвердить, введите …», `error-message` объясняет раннее нажатие | +| Ответы окна | [Кнопка](/ru/smart/actions/buttons/) | красная залитая `status="danger"` первой, затем «Отмена» `appearance="ghost"` `scheme="on-surface"` | + +## Так и не так + +:::example {id="recipes/delete/do-dont" label="Так и не так"} +::: + +- **Вопрос называет, что удалится.** «Удалить «Смета на декабрь.xlsx»?» — + человек видит, о чём его спрашивают. «Вы уверены?» с кнопкой + «Подтвердить» подтверждают не читая. +- **Удаление — красное, ошибка — тоже красная, но это разные слова.** У + кнопок и пунктов меню опасное — `status="danger"`; у поля, сообщения и + уведомления — `error`. +- **Своё не спрашивают.** Вопрос там, где удалённое можно просто вернуть, + приучает нажимать «Удалить» не глядя — и тогда вопрос не спасает там, где + он нужен. +- **Красная кнопка окна всегда залита.** Она не выключается, пока название + не введено: нажатая раньше, она ставит курсор в поле и объясняет, что + нужно. + +## Копия для ИИ + +```html + +
+ + +
+
+

Удалятся сайт, 3 базы данных, 1,2 ГБ файлов и 14 резервных копий.

+

Вернуть будет нельзя.

+
+
+
+ + +
+
+
+
+ +``` + +> **Что ещё в пути.** Окно опасного вопроса в своём размере (28rem, в +> компактной плотности 26rem), с полем окна и промежутками, которые растут к +> ответам, с крестиком на поле окна и выездом снизу на телефоне; вопрос +> `sf-confirm` с плашкой последствий и обязательным вводом названия — всё +> это приходит с ближайшими выпусками Framework. Пока ширину окна задаёт +> `width`, а плашку собирает страница. Как это будет выглядеть, показывает +> [прототип окна опасного вопроса](/proto/modal-look.html). + +## См. также + +- [Список записей](/ru/recipes/resource-list/) +- [Карточка записи](/ru/recipes/record-card/) — «Удалить» в меню «⋯» diff --git a/content/ru/recipes/delete.page.json b/content/ru/recipes/delete.page.json new file mode 100644 index 00000000..23d71cd6 --- /dev/null +++ b/content/ru/recipes/delete.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 40}} diff --git a/examples/recipes/delete/do-dont/index.html b/examples/recipes/delete/do-dont/index.html new file mode 100644 index 00000000..fc7b9106 --- /dev/null +++ b/examples/recipes/delete/do-dont/index.html @@ -0,0 +1,20 @@ +
+
+
+
+
Так — вопрос называет, что удалится, «Удалить» красная
+
+
+
+
Не так — «Вы уверены?» и обычное «Подтвердить»
+
+
+
+
Так — своё и обратимое: удалить сразу, дать «Вернуть»
+
+
+
+
Не так — вопрос там, где можно просто вернуть
+
+
+
diff --git a/examples/recipes/delete/final/index.css b/examples/recipes/delete/final/index.css new file mode 100644 index 00000000..f9579aed --- /dev/null +++ b/examples/recipes/delete/final/index.css @@ -0,0 +1,4 @@ +/* The window opens inside this block (data-sf-overlay-container) and covers only it. */ +#final-root { + min-block-size: var(--sf-g8); +} diff --git a/examples/recipes/delete/final/index.html b/examples/recipes/delete/final/index.html new file mode 100644 index 00000000..1978f03f --- /dev/null +++ b/examples/recipes/delete/final/index.html @@ -0,0 +1,23 @@ +
+
+
+

Удаление сайта

+

Удаляет сайт shop-example со всеми файлами, базами данных и резервными копиями.

+
+
+

+
+ +
+
+

Удалятся сайт, 3 базы данных, 1,2 ГБ файлов и 14 резервных копий. Посетители увидят ошибку 404.

+

Вернуть будет нельзя.

+
+
+
+ + +
+
+
+
diff --git a/examples/recipes/delete/final/index.js b/examples/recipes/delete/final/index.js new file mode 100644 index 00000000..a6edf1f0 --- /dev/null +++ b/examples/recipes/delete/final/index.js @@ -0,0 +1,27 @@ +// What cannot be brought back is asked in a window: the consequences on a +// quiet plate, the deciding phrase on its own line, the name typed to +// confirm. The red answer is always filled; pressed too early it puts the +// cursor into the field and says why. The window shows a copy of its slot +// form, so the listeners sit on document and read the shown copy. +const NAME = 'shop-example'; + +document.addEventListener('submit', (event) => { + const form = event.target; + if (!(form instanceof HTMLFormElement) || !form.matches('[data-final-form]')) return; + event.preventDefault(); + const field = form.querySelector('sf-input[name="confirm"]'); + if (String(field.value ?? '').trim() !== NAME) { + field.setAttribute('invalid', ''); + field.focus(); + docara.example.log('invalid', {typed: String(field.value ?? '')}); + return; + } + document.querySelector('sf-modal[modal-id="final-modal"]').close(); + document.querySelector('#final-result').textContent = `Сайт ${NAME} удалён.`; + docara.example.log('deleted', {site: NAME}); +}); + +document.addEventListener('input', (event) => { + const field = event.target.closest?.('sf-input[name="confirm"]'); + if (field && String(field.value ?? '').trim() === NAME) field.removeAttribute('invalid'); +}); diff --git a/examples/recipes/delete/own/index.css b/examples/recipes/delete/own/index.css new file mode 100644 index 00000000..df645dcf --- /dev/null +++ b/examples/recipes/delete/own/index.css @@ -0,0 +1,4 @@ +/* The toast appears in the corner of this frame: room for it under the list. */ +#own-root { + min-block-size: var(--sf-f8); +} diff --git a/examples/recipes/delete/own/index.html b/examples/recipes/delete/own/index.html new file mode 100644 index 00000000..f6ce0c26 --- /dev/null +++ b/examples/recipes/delete/own/index.html @@ -0,0 +1,7 @@ +
+
+

Мои фильтры

+ +
+
+
diff --git a/examples/recipes/delete/own/index.js b/examples/recipes/delete/own/index.js new file mode 100644 index 00000000..f7fd82b7 --- /dev/null +++ b/examples/recipes/delete/own/index.js @@ -0,0 +1,41 @@ +// A personal filter is deleted at once: nobody else loses it and it can be +// brought back. The toast names what went and offers «Вернуть» for its life. +const views = [ + {id: 'v1', text: 'Мои заказы за неделю', supporting: 'Статус: новый, оплачен', icon: 'filter_alt'}, + {id: 'v2', text: 'Без ответа больше дня', supporting: 'Ответ: нет, создан: больше суток', icon: 'filter_alt'}, + {id: 'v3', text: 'Крупные заказы', supporting: 'Сумма: от 50 000 ₽', icon: 'filter_alt'}, +]; + +const initialize = async () => { + await Promise.all(['sf-list'].map((tag) => customElements.whenDefined(tag))); + const list = document.querySelector('#own-views'); + const toasts = document.querySelector('#own-toasts'); + if (!list || list.dataset.exampleReady === 'true') return; + list.dataset.exampleReady = 'true'; + const render = () => list.setItems({items: views, options: {hasMore: false}}); + list.actions = [{id: 'delete', text: 'Удалить', icon: 'delete', destructive: true}]; + + list.addEventListener('sf-list-apply', (event) => { + if (event.detail.action !== 'delete') return; + const at = views.findIndex((view) => view.id === event.detail.ids[0]); + if (at < 0) return; + const [removed] = views.splice(at, 1); + render(); + const toast = document.createElement('sf-toast'); + toast.setAttribute('icon', 'delete'); + toast.setAttribute('title', `Фильтр «${removed.text}» удалён`); + toast.setAttribute('action-text', 'Вернуть'); + toast.setAttribute('action', 'undo'); + toast.setAttribute('duration', '8000'); + toast.addEventListener('sf-toast-action', () => { + views.splice(Math.min(at, views.length), 0, removed); + render(); + toast.remove(); + }); + toasts.append(toast); + }); + + render(); +}; + +initialize(); diff --git a/examples/recipes/delete/shared/index.html b/examples/recipes/delete/shared/index.html new file mode 100644 index 00000000..ad10b530 --- /dev/null +++ b/examples/recipes/delete/shared/index.html @@ -0,0 +1,24 @@ +
+
+
+ + + + + + + + + + + + + + + + +
Файл проектаРазмерДействия
Смета на декабрь.xlsx86 КБ
Договор с подрядчиком.pdf2,4 МБ
+
+ +
+
diff --git a/examples/recipes/delete/shared/index.js b/examples/recipes/delete/shared/index.js new file mode 100644 index 00000000..ab7d614f --- /dev/null +++ b/examples/recipes/delete/shared/index.js @@ -0,0 +1,48 @@ +// A shared file is asked about in its own row: other people lose it too. +// The bin turns the row into the question; «Удалить» is the red answer, +// «Отмена» puts the row back and returns focus to the bin. «Вернуть файлы» +// only resets the example once a file has gone. +const initialize = async () => { + // sf-confirm is created on demand; the loader defines it when it appears. + const body = document.querySelector('#shared-files'); + const reset = document.querySelector('#shared-reset'); + if (!body || body.dataset.exampleReady === 'true') return; + body.dataset.exampleReady = 'true'; + const rows = body.innerHTML; + + const ask = (row) => { + const saved = row.innerHTML; + const cell = document.createElement('td'); + cell.colSpan = 3; + const confirm = document.createElement('sf-confirm'); + confirm.setAttribute('question', `Удалить «${row.dataset.name}»? Файл пропадёт у всех участников проекта.`); + confirm.setAttribute('confirm-id', row.dataset.file); + confirm.setAttribute('accept-text', 'Удалить'); + confirm.setAttribute('status', 'danger'); + confirm.setAttribute('size', '1/2'); + cell.append(confirm); + row.replaceChildren(cell); + confirm.addEventListener('sf-confirm-answer', (event) => { + if (!event.detail.accepted) { + row.innerHTML = saved; + row.querySelector('sf-button-icon')?.focus(); + return; + } + setTimeout(() => { + confirm.state = 'accepted'; + setTimeout(() => { row.remove(); reset.hidden = false; }, 1200); + }, 600); + }); + }; + + body.addEventListener('click', (event) => { + const row = event.target.closest('sf-button-icon')?.closest('tr[data-file]'); + if (row) ask(row); + }); + reset.addEventListener('click', () => { + body.innerHTML = rows; + reset.hidden = true; + }); +}; + +initialize(); From abaf9fbd7fad8a8c7267da3e881f779f4e7ece10 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 09:52:50 +0300 Subject: [PATCH 24/53] Recipes: empty section -- one first action, header action only after the first record Co-Authored-By: Claude Opus 5.5 --- content/ru/recipes/empty-state.md | 87 +++++++++++++++++++ content/ru/recipes/empty-state.page.json | 1 + .../recipes/empty-state/do-dont/index.html | 38 ++++++++ .../recipes/empty-state/overview/index.html | 15 ++++ .../recipes/empty-state/overview/index.js | 39 +++++++++ 5 files changed, 180 insertions(+) create mode 100644 content/ru/recipes/empty-state.md create mode 100644 content/ru/recipes/empty-state.page.json create mode 100644 examples/recipes/empty-state/do-dont/index.html create mode 100644 examples/recipes/empty-state/overview/index.html create mode 100644 examples/recipes/empty-state/overview/index.js diff --git a/content/ru/recipes/empty-state.md b/content/ru/recipes/empty-state.md new file mode 100644 index 00000000..31f905f1 --- /dev/null +++ b/content/ru/recipes/empty-state.md @@ -0,0 +1,87 @@ +--- +title: "Пустой раздел и первое действие" +description: "Что показать, когда в разделе ещё ничего нет, когда фильтр ничего не нашёл и когда данные не загрузились: причина, пояснение и одно действие." +--- + +# Пустой раздел и первое действие + +Пустой экран — первое, что человек видит в новом разделе. Он говорит, почему +пусто, и даёт одно действие, после которого пусто уже не будет: «Добавить +товар», «Создать сайт», «Пригласить участника». Пусто бывает и по другим +причинам — фильтр ничего не нашёл, данные не загрузились, нет прав, — и у +каждой свои слова и своё действие. + +## Пример + +Нажмите «Добавить товар»: появится первый товар, у заголовка — число и своя +кнопка «Добавить товар». «Начать сначала» вернёт пустой экран. + +:::example {id="recipes/empty-state/overview" label="Первый товар" preview=sandbox log_console=true log_open=false} +::: + +## Когда брать + +| Почему пусто | `kind` | Действие | +|:---|:---|:---| +| Записей ещё нет | `no-data` | одно главное: создать первую запись | +| Фильтр или поиск ничего не нашёл | `nothing-found` | «Сбросить фильтры» или «Показать все» | +| Данные не загрузились | `error` | «Повторить» — компонент ставит его сам | +| Нет прав | `no-access` | без действия или «Запросить доступ» | + +Пустой экран раздела — размер `2` в рамке на месте списка. В панели и +таблице — `1/2`, в выпадающем списке и поиске — одна строка +(`layout="inline"`). + +## Из чего собрано + +| Часть | Компонент | Как задано | +|:---|:---|:---| +| Заголовок раздела | [Заголовок страницы](/ru/smart/composite/page-header/) | пока записей нет — без `count` и без кнопки в `actions`: действие одно, в пустом экране | +| Пустой экран | [Пустое состояние](/ru/smart/feedback/empty-state/) | `kind="no-data"` `size="2"` `heading-level="2"`, свои `heading` и `text` | +| Первое действие | [Кнопка](/ru/smart/actions/buttons/) | в слоте `actions`, вид по умолчанию | +| Место | рамка | `radius-surface border border-outline-variant` на месте будущего списка | +| Первая запись | [Список](/ru/smart/content/list/) | заменяет пустой экран; кнопка «Добавить» переходит в заголовок | + +## Так и не так + +:::example {id="recipes/empty-state/do-dont" label="Так и не так"} +::: + +- **Одно действие.** Пустой экран ведёт к одному шагу. Импорт и справку + ставьте ссылкой в тексте или после первой записи, а не тремя равными + кнопками. +- **Причина — правдивая.** Если пусто из-за фильтра, так и скажите и дайте + сбросить фильтр. «Товаров пока нет» при включённом фильтре читается как + «товары пропали». +- **Кнопка не повторяется.** Пока записей нет, «Добавить» есть только в + пустом экране; в заголовок она переходит вместе с первой записью. +- **Ошибка — не пустота.** Если данные не загрузились, это `kind="error"` + с «Повторить», а не «Ничего не найдено». + +## Копия для ИИ + +```html + +
+
+ + + +
+ + + +
+
+
+``` + +## См. также + +- [Список записей](/ru/recipes/resource-list/) — пусто после поиска +- [Фильтры в шторке](/ru/recipes/filter-drawer/) — пусто после фильтра diff --git a/content/ru/recipes/empty-state.page.json b/content/ru/recipes/empty-state.page.json new file mode 100644 index 00000000..3410c46f --- /dev/null +++ b/content/ru/recipes/empty-state.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 50}} diff --git a/examples/recipes/empty-state/do-dont/index.html b/examples/recipes/empty-state/do-dont/index.html new file mode 100644 index 00000000..ba01f33b --- /dev/null +++ b/examples/recipes/empty-state/do-dont/index.html @@ -0,0 +1,38 @@ +
+
+
+
+ + + +
+
Так — одно действие, которое снимает пустоту
+
+
+
+ + + + + +
+
Не так — три равные кнопки, непонятно, с чего начать
+
+
+
+ + + +
+
Так — пусто из-за фильтра: так и сказать, дать сбросить
+
+
+
+ + + +
+
Не так — при фильтре «товаров нет»: человек решит, что их удалили
+
+
+
diff --git a/examples/recipes/empty-state/overview/index.html b/examples/recipes/empty-state/overview/index.html new file mode 100644 index 00000000..c4805798 --- /dev/null +++ b/examples/recipes/empty-state/overview/index.html @@ -0,0 +1,15 @@ +
+
+ + + + +
+ + + +
+ +
+
+
diff --git a/examples/recipes/empty-state/overview/index.js b/examples/recipes/empty-state/overview/index.js new file mode 100644 index 00000000..4350fd60 --- /dev/null +++ b/examples/recipes/empty-state/overview/index.js @@ -0,0 +1,39 @@ +// The first screen of an empty section: why it is empty and the one thing to +// do. The action does it at once here — the first product appears, the +// header gets its count and its own «Добавить» — and «Начать сначала» goes +// back to the empty screen. +const initialize = async () => { + await Promise.all(['sf-empty-state', 'sf-list', 'sf-header-page', 'sf-button'].map((tag) => customElements.whenDefined(tag))); + const header = document.querySelector('#first-header'); + const empty = document.querySelector('#first-empty'); + const state = document.querySelector('#first-state'); + const list = document.querySelector('#first-list'); + const reset = document.querySelector('#first-reset'); + if (!header || header.dataset.exampleReady === 'true') return; + header.dataset.exampleReady = 'true'; + + // The header takes its slots when it starts, so its «Добавить» is in the + // markup from the start and only shown once there is a first product. + const add = document.querySelector('#first-add'); + + state.addEventListener('click', (event) => { + if (!event.target.closest('sf-button')) return; + list.setItems({items: [{id: 'p1', text: 'Термокружка «Северный ветер»', supporting: '1 290 ₽ · Черновик', icon: 'inventory_2'}], options: {hasMore: false}}); + empty.hidden = true; + list.hidden = false; + reset.hidden = false; + header.setAttribute('count', '1'); + add.hidden = false; + docara.example.log('add', {first: true}); + }); + reset.addEventListener('click', () => { + empty.hidden = false; + list.hidden = true; + reset.hidden = true; + header.removeAttribute('count'); + add.hidden = true; + state.querySelector('sf-button')?.focus(); + }); +}; + +initialize(); From 1d1739f1f052c5f96eeadd5936a969832559adcc Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 09:52:50 +0300 Subject: [PATCH 25/53] =?UTF-8?q?Recipes:=20filters=20in=20a=20drawer=20--?= =?UTF-8?q?=20applied=20filters=20as=20tags,=20live=20=C2=AB=D0=9F=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=B7=D0=B0=D1=82=D1=8C=20N=C2=BB=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- content/ru/recipes/filter-drawer.md | 112 ++++++++++++++++ content/ru/recipes/filter-drawer.page.json | 1 + .../recipes/filter-drawer/do-dont/index.html | 41 ++++++ .../recipes/filter-drawer/overview/index.css | 4 + .../recipes/filter-drawer/overview/index.html | 34 +++++ .../recipes/filter-drawer/overview/index.js | 123 ++++++++++++++++++ 6 files changed, 315 insertions(+) create mode 100644 content/ru/recipes/filter-drawer.md create mode 100644 content/ru/recipes/filter-drawer.page.json create mode 100644 examples/recipes/filter-drawer/do-dont/index.html create mode 100644 examples/recipes/filter-drawer/overview/index.css create mode 100644 examples/recipes/filter-drawer/overview/index.html create mode 100644 examples/recipes/filter-drawer/overview/index.js diff --git a/content/ru/recipes/filter-drawer.md b/content/ru/recipes/filter-drawer.md new file mode 100644 index 00000000..b7d4529e --- /dev/null +++ b/content/ru/recipes/filter-drawer.md @@ -0,0 +1,112 @@ +--- +title: "Фильтры в шторке" +description: "Отбор списка по нескольким признакам: кнопка «Фильтры», боковая панель с флажками и вариантами, кнопка с числом найденного и выбранные фильтры тегами над списком." +--- + +# Фильтры в шторке + +Когда список отбирают по нескольким признакам сразу — статус, доставка, +ответственный, — фильтры убирают в боковую панель. Список остаётся виден +рядом, кнопка панели заранее говорит, сколько записей покажет, а выбранные +фильтры стоят тегами над списком: каждый снимается одним нажатием. + +## Пример + +Нажмите «Фильтры», отметьте «Оплачен» и «Курьером»: кнопка панели +пересчитает заказы. «Показать» применит фильтры и закроет панель; теги над +списком снимают их по одному. + +:::example {id="recipes/filter-drawer/overview" label="Заказы" preview=sandbox log="sf-tag-close sf-drawer-opened sf-drawer-closed" log_console=true log_open=false} +::: + +## Когда брать + +- Признаков для отбора три и больше, и их комбинируют. +- Список длинный, а фильтры меняют не каждую минуту. + +Один признак с двумя-пятью значениями — это +[Сегментный переключатель](/ru/smart/actions/segmented/) над списком, как в +[Списке записей](/ru/recipes/resource-list/). Отбор, который человек +сохраняет и выбирает снова, — сохранённый вид в +[Представлении данных](/ru/pro/admin/table/). + +## Из чего собрано + +| Часть | Компонент | Как задано | +|:---|:---|:---| +| Поиск | [Текстовое поле](/ru/smart/forms/inputs/) | `input-type="search"`, `icon-start="search"`, `aria-label` | +| Кнопка «Фильтры» | [Кнопка](/ru/smart/actions/buttons/) | `appearance="outline"` `scheme="on-surface"` `icon-start="filter_list"`, `data-drawer="toggle"`, `aria-controls` — `id` панели | +| Панель | [Шторка](/ru/smart/overlays/drawer/) | `modal="false"` `placement="inline-end"` `size="1/2"`, `heading="Фильтры"` | +| Признак из нескольких | [Флажок](/ru/smart/forms/checkbox/) | в `fieldset` с `legend` | +| Признак из одного | [Радиокнопки](/ru/smart/forms/radio/) | в `fieldset`, первый вариант — «Любая» | +| Ответы панели | [Кнопка](/ru/smart/actions/buttons/) | «Показать N заказов» — вид по умолчанию, число пересчитывается при каждом выборе; «Сбросить» — `appearance="ghost"` `scheme="on-surface"` | +| Выбранные фильтры | [Тег](/ru/smart/content/tags/) | `kind="default"` `closable`, `close-label` с названием; за ними «Сбросить все» `appearance="ghost"` `size="1/2"` | +| Ничего не подошло | [Пустое состояние](/ru/smart/feedback/empty-state/) | в слоте `empty` списка, `kind="nothing-found"`, действие «Сбросить фильтры» | + +## Так и не так + +:::example {id="recipes/filter-drawer/do-dont" label="Так и не так"} +::: + +- **Выбранное видно.** Фильтры, которые сейчас действуют, стоят тегами над + списком. Без них человек не понимает, почему записей мало. +- **Кнопка считает заранее.** «Показать 3 заказа» говорит, что будет после + нажатия, и «Заказов нет» — что нажимать незачем. «Применить» вслепую + открывает пустой список. +- **Ответы не равны.** «Показать» — главная, «Сбросить» — тихая. +- **Панель не прячет список.** Панель немодальная (`modal="false"`): список + виден рядом, а не под затемнением. + +## Копия для ИИ + +```html + +
+
+ + +
+
+ + +
+ + + + +
+
+ Статус + + +
+
+ Доставка + + +
+
+ + +
+
+
+
+``` + +> **Что ещё в пути.** Сохранённые наборы фильтров — «Мои заказы за +> неделю», «Без ответа больше дня» — выбираются в одном блоке представлений, +> который приходит с набором компонентов `view`. Как он выглядит, показывает +> [прототип представлений](/proto/view-presets.html). + +## См. также + +- [Список записей](/ru/recipes/resource-list/) — отбор по одному признаку +- [Пустой раздел и первое действие](/ru/recipes/empty-state/) diff --git a/content/ru/recipes/filter-drawer.page.json b/content/ru/recipes/filter-drawer.page.json new file mode 100644 index 00000000..79bcbd42 --- /dev/null +++ b/content/ru/recipes/filter-drawer.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 60}} diff --git a/examples/recipes/filter-drawer/do-dont/index.html b/examples/recipes/filter-drawer/do-dont/index.html new file mode 100644 index 00000000..bd3ce580 --- /dev/null +++ b/examples/recipes/filter-drawer/do-dont/index.html @@ -0,0 +1,41 @@ +
+
+
+
+
+ + +
+
+ + + +
+
+
Так — выбранное видно над списком и снимается по одному
+
+
+
+
+ + +
+
+
Не так — фильтры включены, но этого не видно
+
+
+
+ + +
+
Так — кнопка заранее говорит, сколько покажет
+
+
+
+ + +
+
Не так — «Применить» вслепую и две равные кнопки
+
+
+
diff --git a/examples/recipes/filter-drawer/overview/index.css b/examples/recipes/filter-drawer/overview/index.css new file mode 100644 index 00000000..ddae773f --- /dev/null +++ b/examples/recipes/filter-drawer/overview/index.css @@ -0,0 +1,4 @@ +/* The panel opens inside this block (data-sf-overlay-container) on its height. */ +#filters-root { + min-block-size: var(--sf-h0); +} diff --git a/examples/recipes/filter-drawer/overview/index.html b/examples/recipes/filter-drawer/overview/index.html new file mode 100644 index 00000000..c69f2e4c --- /dev/null +++ b/examples/recipes/filter-drawer/overview/index.html @@ -0,0 +1,34 @@ +
+
+ +
+ + +
+
+ + + +
+ +
+
+ Статус + + + + +
+
+ Доставка + + + +
+
+ + +
+
+
+
diff --git a/examples/recipes/filter-drawer/overview/index.js b/examples/recipes/filter-drawer/overview/index.js new file mode 100644 index 00000000..72259a39 --- /dev/null +++ b/examples/recipes/filter-drawer/overview/index.js @@ -0,0 +1,123 @@ +// Filters live in a side panel; the list stays visible beside it. The apply +// button counts what it will show before it is pressed, the chosen filters +// stay above the list as tags that remove one each, and «Сбросить» clears +// them all. Nothing is applied until «Показать». +const orders = [ + {id: 'o1', text: 'Заказ 1042 · Анна Белова', supporting: '3 490 ₽ · Курьером', icon: 'receipt_long', status: 'new', delivery: 'courier'}, + {id: 'o2', text: 'Заказ 1041 · Пётр Ильин', supporting: '12 900 ₽ · Самовывоз', icon: 'receipt_long', status: 'paid', delivery: 'pickup'}, + {id: 'o3', text: 'Заказ 1040 · Мария Котова', supporting: '1 290 ₽ · Курьером', icon: 'receipt_long', status: 'sent', delivery: 'courier'}, + {id: 'o4', text: 'Заказ 1039 · Олег Сафин', supporting: '6 750 ₽ · Курьером', icon: 'receipt_long', status: 'paid', delivery: 'courier'}, + {id: 'o5', text: 'Заказ 1038 · Инна Ершова', supporting: '2 180 ₽ · Самовывоз', icon: 'receipt_long', status: 'cancelled', delivery: 'pickup'}, + {id: 'o6', text: 'Заказ 1037 · Глеб Орлов', supporting: '940 ₽ · Курьером', icon: 'receipt_long', status: 'new', delivery: 'courier'}, + {id: 'o7', text: 'Заказ 1036 · Вера Лаптева', supporting: '18 400 ₽ · Самовывоз', icon: 'receipt_long', status: 'sent', delivery: 'pickup'}, + {id: 'o8', text: 'Заказ 1035 · Денис Уваров', supporting: '4 120 ₽ · Курьером', icon: 'receipt_long', status: 'paid', delivery: 'courier'}, +]; +const LABELS = {new: 'Новый', paid: 'Оплачен', sent: 'Отправлен', cancelled: 'Отменён', courier: 'Курьером', pickup: 'Самовывоз'}; +const plural = (n) => (n % 10 === 1 && n % 100 !== 11 ? 'заказ' : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 'заказа' : 'заказов'); + +let applied = {status: [], delivery: 'any'}; + +const matching = ({status, delivery}) => orders.filter((order) => (!status.length || status.includes(order.status)) + && (delivery === 'any' || order.delivery === delivery)); + +// The panel may move its content into its own surface: look the parts up +// when they are needed rather than holding them from the start. +const part = (selector) => document.querySelector(selector); +const draft = () => ({ + status: [...document.querySelectorAll('#filters-form sf-checkbox[name="status"]')].filter((box) => box.checked).map((box) => box.getAttribute('value')), + delivery: [...document.querySelectorAll('#filters-form sf-radio[name="delivery"]')].find((radio) => radio.checked)?.getAttribute('value') ?? 'any', +}); +const showDraft = (state) => { + document.querySelectorAll('#filters-form sf-checkbox[name="status"]').forEach((box) => { box.checked = state.status.includes(box.getAttribute('value')); }); + document.querySelectorAll('#filters-form sf-radio[name="delivery"]').forEach((radio) => { radio.checked = radio.getAttribute('value') === state.delivery; }); +}; +const countApply = () => { + const n = matching(draft()).length; + part('#filters-apply')?.setAttribute('text', n ? `Показать ${n} ${plural(n)}` : 'Заказов нет'); +}; + +const render = () => { + part('#orders').setItems({items: matching(applied), options: {hasMore: false}}); + const tags = part('#filters-applied'); + tags.replaceChildren(); + const chosen = [...applied.status.map((value) => ['status', value]), ...(applied.delivery === 'any' ? [] : [['delivery', applied.delivery]])]; + chosen.forEach(([key, value]) => { + const tag = document.createElement('sf-tag'); + tag.setAttribute('kind', 'default'); + tag.setAttribute('closable', ''); + tag.setAttribute('text', LABELS[value]); + tag.setAttribute('close-label', `Убрать фильтр «${LABELS[value]}»`); + tag.addEventListener('sf-tag-close', () => { + applied = key === 'status' + ? {...applied, status: applied.status.filter((entry) => entry !== value)} + : {...applied, delivery: 'any'}; + showDraft(applied); + render(); + }); + tags.append(tag); + }); + if (chosen.length) { + const clear = document.createElement('sf-button'); + clear.setAttribute('appearance', 'ghost'); + clear.setAttribute('scheme', 'on-surface'); + clear.setAttribute('size', '1/2'); + clear.setAttribute('text', 'Сбросить все'); + clear.addEventListener('click', () => { + applied = {status: [], delivery: 'any'}; + showDraft(applied); + render(); + }); + tags.append(clear); + } + tags.hidden = !chosen.length; + countApply(); +}; + +// The list moves its slotted empty state while it draws its first frame: +// wait a few frames for it rather than reading it mid-move. +const settled = async (selector) => { + for (let frame = 0; frame < 30; frame++) { + const element = document.querySelector(selector); + if (element) return element; + await new Promise((resolve) => requestAnimationFrame(resolve)); + } + return null; +}; + +const initialize = async () => { + await Promise.all(['sf-list', 'sf-drawer', 'sf-button', 'sf-empty-state'].map((tag) => customElements.whenDefined(tag))); + const root = part('#filters-root'); + if (!root || root.dataset.exampleReady === 'true') return; + root.dataset.exampleReady = 'true'; + + const empty = await settled('#orders-empty'); + empty.actions = [{id: 'reset', text: 'Сбросить фильтры', appearance: 'outline'}]; + empty.addEventListener('sf-empty-state-action', () => { + applied = {status: [], delivery: 'any'}; + showDraft(applied); + render(); + }); + document.addEventListener('change', (event) => { + if (event.target.closest?.('#filters-form')) countApply(); + }); + document.addEventListener('click', (event) => { + const button = event.target.closest?.('sf-button'); + if (!button) return; + if (button.id === 'filters-apply') { + applied = draft(); + render(); + part('#orders-filters').close?.(); + docara.example.log('apply', applied); + } else if (button.id === 'filters-clear') { + showDraft({status: [], delivery: 'any'}); + countApply(); + } + }); + document.addEventListener('submit', (event) => { + if (event.target.id === 'filters-form') event.preventDefault(); + }); + + render(); +}; + +initialize(); From 9d67e79ca16d003c9a45990a275f37bb03fb8e3d Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 09:52:50 +0300 Subject: [PATCH 26/53] Recipes: overview -- attention list first, metric tiles two by two, last events Co-Authored-By: Claude Opus 5.5 --- content/ru/recipes/dashboard.md | 104 ++++++++++++++++++ content/ru/recipes/dashboard.page.json | 1 + examples/recipes/dashboard/do-dont/index.html | 37 +++++++ examples/recipes/dashboard/overview/index.css | 4 + .../recipes/dashboard/overview/index.html | 29 +++++ examples/recipes/dashboard/overview/index.js | 25 +++++ 6 files changed, 200 insertions(+) create mode 100644 content/ru/recipes/dashboard.md create mode 100644 content/ru/recipes/dashboard.page.json create mode 100644 examples/recipes/dashboard/do-dont/index.html create mode 100644 examples/recipes/dashboard/overview/index.css create mode 100644 examples/recipes/dashboard/overview/index.html create mode 100644 examples/recipes/dashboard/overview/index.js diff --git a/content/ru/recipes/dashboard.md b/content/ru/recipes/dashboard.md new file mode 100644 index 00000000..04c0a5ce --- /dev/null +++ b/content/ru/recipes/dashboard.md @@ -0,0 +1,104 @@ +--- +title: "Обзор с плитками" +description: "Первый экран админки: сначала то, что требует внимания, затем три-четыре главных числа плитками со сравнением и последние события." +--- + +# Обзор с плитками + +Первый экран раздела или всей админки. Он отвечает на два вопроса по +порядку: «что-то сломалось?» и «как идут дела?». Сверху — список того, что +требует внимания; когда всё в порядке, его нет совсем. Ниже три-четыре +главных числа плитками, у каждого — сравнение с прошлым периодом; цвет у +плитки появляется, только когда перейдён порог. Внизу — последние события. + +## Пример + +Нажмите «Продлить» — проблема уйдёт из списка. Плитки и «Открыть копии» +ведут к своим отчётам: пример пишет переход в журнал. + +:::example {id="recipes/dashboard/overview" label="Обзор магазина" preview=sandbox log="sf-list-attention-apply" log_console=true log_open=false} +::: + +## Когда брать + +- Раздел, в который заходят каждый день, чтобы проверить, всё ли в порядке. +- Чисел, за которыми следят, немного — до четырёх в ряд. + +Если человеку нужно видеть, что проверка прошла, поставьте списку +`empty="line"`: без проблем он покажет одну тихую строку «Всё в порядке». + +Отчёт, в котором сравнивают много чисел за период, — это таблица или +график, а не плитки. Сведения об одной записи — [Карточка записи](/ru/recipes/record-card/). + +## Из чего собрано + +| Часть | Компонент | Как задано | +|:---|:---|:---| +| Заголовок | [Заголовок страницы](/ru/smart/composite/page-header/) | `heading="Обзор"` и путь к тому, что обозревают | +| Проблемы | [Требует внимания](/ru/smart/feedback/list-attention/) | на странице как есть, не в карточке: пустой список места не занимает; у проблемы `status`, `title`, одно `action` | +| Числа | [Плитка показателя](/ru/smart/content/tile-metric/) | в `div.sf-tile-metric-grid`, ширина колонки — `--sf-tile-metric-min-width` (здесь `var(--sf-f6)`: четыре плитки встают по две, а не три и одна); `previous` и `period-text` — сравнение; `max`, `warning-at`, `error-at` — порог; `href` — отчёт | +| События | [Хронология](/ru/smart/content/list-timeline/) в [Карточке](/ru/components/content/card/) | `size="1/2"` `density="compact"` `group="none"` | + +## Так и не так + +:::example {id="recipes/dashboard/do-dont" label="Так и не так"} +::: + +- **Цвет — сигнал.** Плитка окрашивается сама, когда число переходит порог + (`warning-at`, `error-at`). Зелёная плитка с «Всё хорошо» на каждом числе + приучает не смотреть на цвет. +- **Число со сравнением.** «214 заказов» ничего не говорит, «214, на 9 % + меньше, чем на прошлой неделе» — говорит. Рост, который плох (нагрузка, + ошибки, очередь), — `better="down"`. +- **Проблемы — одним списком.** Две-три проблемы — строки списка «Требует + внимания», серьёзные первыми. Стопка цветных плашек над экраном сдвигает + всё вниз, и каждая новая проблема двигает экран снова. +- **Не больше четырёх плиток в ряд.** Пятое число уже не главное — ему место + в отчёте. + +## Копия для ИИ + +```html + +
+
+ + + + +
+ + + + +
+
+

Последние события

+ + + +
+
+
+``` + +> **Что ещё в пути.** У «Требует внимания» — бейджи серьёзности в шапке, +> строка «что · где · сколько длится», меню «⋯» с «Отложить» и «Не +> напоминать», строка «Проверено в …» и ссылка «Все проблемы (N)» — +> приходят с новой версией компонента. Как это будет выглядеть, показывает +> [прототип «Требует внимания»](/proto/list-attention.html). + +## См. также + +- [Карточка записи](/ru/recipes/record-card/) +- [Список уведомлений](/ru/recipes/notifications/) — события, адресованные человеку diff --git a/content/ru/recipes/dashboard.page.json b/content/ru/recipes/dashboard.page.json new file mode 100644 index 00000000..a4195afd --- /dev/null +++ b/content/ru/recipes/dashboard.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 70}} diff --git a/examples/recipes/dashboard/do-dont/index.html b/examples/recipes/dashboard/do-dont/index.html new file mode 100644 index 00000000..90c0135f --- /dev/null +++ b/examples/recipes/dashboard/do-dont/index.html @@ -0,0 +1,37 @@ +
+
+
+
+
+ + +
+
+
Так — цвет только там, где перейдён порог
+
+
+
+
+ + +
+
+
Не так — зелёное везде и число без сравнения
+
+
+
+ + + +
+
Так — проблемы одним списком, серьёзные первыми
+
+
+
+ + +
+
Не так — стопка цветных плашек сдвигает весь экран
+
+
+
diff --git a/examples/recipes/dashboard/overview/index.css b/examples/recipes/dashboard/overview/index.css new file mode 100644 index 00000000..a053a987 --- /dev/null +++ b/examples/recipes/dashboard/overview/index.css @@ -0,0 +1,4 @@ +/* Four tiles split two by two in a narrow column instead of three and one. */ +#overview-root .sf-tile-metric-grid { + --sf-tile-metric-min-width: var(--sf-f6); +} diff --git a/examples/recipes/dashboard/overview/index.html b/examples/recipes/dashboard/overview/index.html new file mode 100644 index 00000000..2de2c21b --- /dev/null +++ b/examples/recipes/dashboard/overview/index.html @@ -0,0 +1,29 @@ +
+
+ + + + + + +
+ + + + +
+
+

Последние события

+ + + +
+
+
diff --git a/examples/recipes/dashboard/overview/index.js b/examples/recipes/dashboard/overview/index.js new file mode 100644 index 00000000..a0d0d302 --- /dev/null +++ b/examples/recipes/dashboard/overview/index.js @@ -0,0 +1,25 @@ +// The overview answers «is anything wrong?» first, then «how are we doing?». +// Problems come first and leave when fixed; each tile opens its own report. +const initialize = async () => { + await Promise.all(['sf-list-attention', 'sf-tile-metric'].map((tag) => customElements.whenDefined(tag))); + const root = document.querySelector('#overview-root'); + if (!root || root.dataset.exampleReady === 'true') return; + root.dataset.exampleReady = 'true'; + const attention = document.querySelector('#overview-attention'); + attention.addEventListener('sf-list-attention-navigate', (event) => { + event.preventDefault(); + docara.example.log('sf-list-attention-navigate', event.detail); + }); + attention.addEventListener('sf-list-attention-apply', (event) => { + docara.example.log('sf-list-attention-apply', event.detail); + if (event.detail.action === 'renew') attention.items = attention.items.filter((item) => item.id !== event.detail.id); + }); + root.addEventListener('click', (event) => { + const tile = event.target.closest('sf-tile-metric[href]'); + if (!tile) return; + event.preventDefault(); + docara.example.log('open', {href: tile.getAttribute('href')}); + }); +}; + +initialize(); From e2d03c15b34a1d216de8f18545626a28fc30cb51 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 09:52:50 +0300 Subject: [PATCH 27/53] Recipes: notifications -- bell counts unread, popover with day groups and a read filter Co-Authored-By: Claude Opus 5.5 --- content/ru/recipes/notifications.md | 97 +++++++++++++++++++ content/ru/recipes/notifications.page.json | 1 + .../recipes/notifications/do-dont/index.html | 27 ++++++ .../recipes/notifications/do-dont/index.js | 14 +++ .../recipes/notifications/overview/index.css | 5 + .../recipes/notifications/overview/index.html | 21 ++++ .../recipes/notifications/overview/index.js | 62 ++++++++++++ 7 files changed, 227 insertions(+) create mode 100644 content/ru/recipes/notifications.md create mode 100644 content/ru/recipes/notifications.page.json create mode 100644 examples/recipes/notifications/do-dont/index.html create mode 100644 examples/recipes/notifications/do-dont/index.js create mode 100644 examples/recipes/notifications/overview/index.css create mode 100644 examples/recipes/notifications/overview/index.html create mode 100644 examples/recipes/notifications/overview/index.js diff --git a/content/ru/recipes/notifications.md b/content/ru/recipes/notifications.md new file mode 100644 index 00000000..7fd97f8e --- /dev/null +++ b/content/ru/recipes/notifications.md @@ -0,0 +1,97 @@ +--- +title: "Список уведомлений" +description: "Уведомления под колокольчиком в шапке: число непрочитанных, список по дням, отметка «Новое», «Прочитать все» и отбор непрочитанных." +--- + +# Список уведомлений + +То, что случилось без человека и ждёт его: новый заказ, отзыв, неудачная +копия. Колокольчик в шапке считает непрочитанные, по нажатию открывается +панель со списком по дням. Непрочитанное отмечено словом «Новое»; открытая +строка становится прочитанной, «Прочитать все» читает всё сразу. + +## Пример + +Откройте строку — отметка «Новое» уйдёт, число на колокольчике уменьшится. +«Непрочитанные» оставит только непрочитанное; панель закрывается нажатием +снаружи и снова открывается колокольчиком. + +:::example {id="recipes/notifications/overview" label="Уведомления" preview=sandbox log="sf-list-open change" log_console=true log_open=false} +::: + +## Когда брать + +- События приходят без участия человека, и он должен узнать о них, даже если + его не было на экране. +- Их нужно прочитать позже, а не только увидеть в момент прихода. + +Результат того, что человек только что сделал сам, — «Сохранено», +«Удалено», — это [Уведомление](/ru/smart/feedback/toast/) в углу, а не +строка списка. Проблема, которую надо чинить, — строка +[«Требует внимания»](/ru/recipes/dashboard/) на обзоре. + +## Из чего собрано + +| Часть | Компонент | Как задано | +|:---|:---|:---| +| Колокольчик | [Кнопка с иконкой](/ru/smart/actions/icon-buttons/) | `icon="notifications"` `appearance="ghost"` `scheme="on-surface"`; `badge-count` — непрочитанные, `badge-label` — то же словами | +| Панель | [Поповер](/ru/smart/overlays/popover/) | колокольчик в `slot="trigger"`, панель в `slot="content"`, `width="min(var(--sf-g4), calc(100vw - var(--sf-space-4)))"`, `placement="bottom-end"`; колокольчик — последний в строке: поповер не сдвигается к экрану сам, и панель, открытая от края, на телефоне помещается только так | +| Шапка панели | заголовок `h2` и [Кнопка](/ru/smart/actions/buttons/) | «Прочитать все» `appearance="ghost"` `scheme="on-surface"` `size="1/2"`, выключена, когда читать нечего | +| Отбор | [Сегментный переключатель](/ru/smart/actions/segmented/) | `size="1/2"` `full-width`, у «Непрочитанные» — `count` | +| Строки | [Список](/ru/smart/content/list/) | `group-by="day"` `size="1/2"`; у строки `text` — что случилось, `supporting` — с чем, `date`; непрочитанная — `badge: {text: 'Новое', tone: 'info'}` | +| Прокрутка | обёртка | `max-h-g4 overflow-auto`: шапка панели стоит, прокручиваются строки | + +## Так и не так + +:::example {id="recipes/notifications/do-dont" label="Так и не так"} +::: + +- **Число — только непрочитанное.** Колокольчик с «128» за все дни + перестают замечать; число падает до нуля, когда всё прочитано. +- **Строка говорит, что случилось.** Заголовок — событие («Новый заказ + 1042»), вторая строка — с чем оно связано. Не «Уведомление» и не + «Системное сообщение». +- **Новое — словом, не только цветом.** Отметка «Новое» видна и в + чёрно-белом, и экранному диктору. +- **Список, а не уведомления в углу.** Уведомление в углу уходит само, и + то, что человек не успел прочитать, пропадает. + +## Копия для ИИ + +```html + + + +
+
+

Уведомления

+ +
+
+ +
+
+ +
+
+
+ +``` + +## См. также + +- [Обзор с плитками](/ru/recipes/dashboard/) — проблемы, которые надо чинить +- [Удаление](/ru/recipes/delete/) — уведомление с «Вернуть» diff --git a/content/ru/recipes/notifications.page.json b/content/ru/recipes/notifications.page.json new file mode 100644 index 00000000..6eed3712 --- /dev/null +++ b/content/ru/recipes/notifications.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 80}} diff --git a/examples/recipes/notifications/do-dont/index.html b/examples/recipes/notifications/do-dont/index.html new file mode 100644 index 00000000..c0c978f4 --- /dev/null +++ b/examples/recipes/notifications/do-dont/index.html @@ -0,0 +1,27 @@ +
+
+
+
+ +
+
Так — колокольчик считает только непрочитанное
+
+
+
+ +
+
Не так — все уведомления за год: число перестают замечать
+
+
+
+
Так — что случилось и с чем, новое отмечено словом
+
+
+
+ + +
+
Не так — уведомления в углу вместо списка: уходят, не прочитанные
+
+
+
diff --git a/examples/recipes/notifications/do-dont/index.js b/examples/recipes/notifications/do-dont/index.js new file mode 100644 index 00000000..4a03d58f --- /dev/null +++ b/examples/recipes/notifications/do-dont/index.js @@ -0,0 +1,14 @@ +// A notification row: what happened as the title, what it is about as the +// second line, «Новое» on the unread one. +const initialize = async () => { + await customElements.whenDefined('sf-list'); + const list = document.querySelector('#notes-do'); + if (!list || list.dataset.exampleReady === 'true') return; + list.dataset.exampleReady = 'true'; + list.setItems({items: [ + {id: 'a', text: 'Новый заказ 1042', supporting: 'Анна Белова · 3 490 ₽', icon: 'shopping_bag', badge: {text: 'Новое', tone: 'info'}}, + {id: 'b', text: 'Оплачен заказ 1039', supporting: 'Олег Сафин · 6 750 ₽', icon: 'payments'}, + ], options: {hasMore: false}}); +}; + +initialize(); diff --git a/examples/recipes/notifications/overview/index.css b/examples/recipes/notifications/overview/index.css new file mode 100644 index 00000000..c0925702 --- /dev/null +++ b/examples/recipes/notifications/overview/index.css @@ -0,0 +1,5 @@ +/* The panel opens under the bell inside this frame and takes a share of the + frame height: room for it below the bar. */ +#notify-root { + min-block-size: var(--sf-h2); +} diff --git a/examples/recipes/notifications/overview/index.html b/examples/recipes/notifications/overview/index.html new file mode 100644 index 00000000..b8ceca69 --- /dev/null +++ b/examples/recipes/notifications/overview/index.html @@ -0,0 +1,21 @@ +
+
+ Магазин «Северный ветер» + + + +
+
+

Уведомления

+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/examples/recipes/notifications/overview/index.js b/examples/recipes/notifications/overview/index.js new file mode 100644 index 00000000..07172a2f --- /dev/null +++ b/examples/recipes/notifications/overview/index.js @@ -0,0 +1,62 @@ +// Notifications open from the bell in the bar. Unread rows carry a «Новое» +// badge and the bell counts them; opening a row reads it, «Прочитать все» +// reads every row, and «Непрочитанные» shows only what is left. Rows are +// grouped by day in the reader's time zone. +const now = new Date(); +const at = (days, hours, minutes) => { + const date = new Date(now); + date.setDate(date.getDate() - days); + date.setHours(hours, minutes, 0, 0); + return date.toISOString(); +}; +const notes = [ + {id: 'n1', text: 'Новый заказ 1042', supporting: 'Анна Белова · 3 490 ₽', icon: 'shopping_bag', date: at(0, 9, 41), unread: true}, + {id: 'n2', text: 'Резервная копия не создана', supporting: 'Нет места на диске', icon: 'error', date: at(0, 4, 0), unread: true}, + {id: 'n3', text: 'Отзыв о товаре «Термокружка»', supporting: '5 звёзд · «Держит тепло весь день»', icon: 'reviews', date: at(1, 18, 12), unread: true}, + {id: 'n4', text: 'Оплачен заказ 1039', supporting: 'Олег Сафин · 6 750 ₽', icon: 'payments', date: at(1, 11, 30)}, + {id: 'n5', text: 'Сертификат продлён', supporting: 'nord-wind.ru до 14 декабря', icon: 'verified', date: at(3, 10, 0)}, +]; + +const initialize = async () => { + await Promise.all(['sf-list', 'sf-popover', 'sf-segmented', 'sf-button-icon'].map((tag) => customElements.whenDefined(tag))); + const root = document.querySelector('#notify-root'); + if (!root || root.dataset.exampleReady === 'true') return; + root.dataset.exampleReady = 'true'; + const list = document.querySelector('#notify-list'); + const bell = document.querySelector('#notify-bell'); + const filter = document.querySelector('#notify-filter'); + const readAll = document.querySelector('#notify-read-all'); + let shown = 'all'; + + const render = () => { + const unread = notes.filter((note) => note.unread).length; + const rows = notes + .filter((note) => shown === 'all' || note.unread) + .map(({unread: isUnread, ...note}) => ({...note, badge: isUnread ? {text: 'Новое', tone: 'info'} : undefined})); + list.setItems({items: rows, options: {hasMore: false}}); + bell.setAttribute('badge-count', String(unread)); + bell.setAttribute('badge-label', unread ? `${unread} непрочитанных` : ''); + filter.items = [{value: 'all', label: 'Все'}, {value: 'unread', label: 'Непрочитанные', count: unread}]; + filter.value = shown; + readAll.toggleAttribute('disabled', !unread); + }; + + list.addEventListener('sf-list-open', (event) => { + const note = notes.find((entry) => entry.id === event.detail.id); + if (note) note.unread = false; + render(); + }); + readAll.addEventListener('click', () => { + notes.forEach((note) => { note.unread = false; }); + render(); + docara.example.log('read-all', {}); + }); + filter.addEventListener('change', (event) => { + shown = event.detail.value; + render(); + }); + + render(); +}; + +initialize(); From bfb4b93fe56aa9f0f61dd2ffb7a7588aa0464ee8 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 13:35:14 +0300 Subject: [PATCH 28/53] =?UTF-8?q?Recipes:=20=C2=AB=D0=9A=D0=B0=D0=BA=20?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B0=D1=82=D1=8C=C2=BB=20block=20--=20each?= =?UTF-8?q?=20example=20in=20a=20role-coloured=20frame=20with=20its=20own?= =?UTF-8?q?=20=C2=AB=D0=9F=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=C2=BB=20/=20=C2=AB=D0=9D=D0=B5=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=C2=BB=20label=20on=20top=20(R101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The label sat between two examples and «Так / не так» read poorly. Every do/don't figure is now one card: border-success or border-error around it, a tinted first row with check_circle / cancel, the word and the explanation, then the example. Framework utilities only. Co-Authored-By: Claude Opus 5.5 --- content/ru/recipes/dashboard.md | 4 +-- content/ru/recipes/delete.md | 4 +-- content/ru/recipes/empty-state.md | 4 +-- content/ru/recipes/filter-drawer.md | 4 +-- content/ru/recipes/index.md | 4 +-- content/ru/recipes/notifications.md | 4 +-- content/ru/recipes/record-card.md | 4 +-- content/ru/recipes/resource-list.md | 4 +-- content/ru/recipes/settings-form.md | 4 +-- examples/recipes/dashboard/do-dont/index.html | 24 ++++++------- examples/recipes/delete/do-dont/index.html | 24 ++++++------- .../recipes/empty-state/do-dont/index.html | 24 ++++++------- .../recipes/filter-drawer/do-dont/index.html | 24 ++++++------- .../recipes/notifications/do-dont/index.html | 24 ++++++------- .../recipes/record-card/do-dont/index.html | 30 ++++++++-------- .../recipes/resource-list/do-dont/index.html | 24 ++++++------- .../recipes/settings-form/do-dont/index.html | 36 +++++++++---------- 17 files changed, 123 insertions(+), 123 deletions(-) diff --git a/content/ru/recipes/dashboard.md b/content/ru/recipes/dashboard.md index 04c0a5ce..fdff612e 100644 --- a/content/ru/recipes/dashboard.md +++ b/content/ru/recipes/dashboard.md @@ -39,9 +39,9 @@ description: "Первый экран админки: сначала то, чт | Числа | [Плитка показателя](/ru/smart/content/tile-metric/) | в `div.sf-tile-metric-grid`, ширина колонки — `--sf-tile-metric-min-width` (здесь `var(--sf-f6)`: четыре плитки встают по две, а не три и одна); `previous` и `period-text` — сравнение; `max`, `warning-at`, `error-at` — порог; `href` — отчёт | | События | [Хронология](/ru/smart/content/list-timeline/) в [Карточке](/ru/components/content/card/) | `size="1/2"` `density="compact"` `group="none"` | -## Так и не так +## Как делать -:::example {id="recipes/dashboard/do-dont" label="Так и не так"} +:::example {id="recipes/dashboard/do-dont" label="Как делать"} ::: - **Цвет — сигнал.** Плитка окрашивается сама, когда число переходит порог diff --git a/content/ru/recipes/delete.md b/content/ru/recipes/delete.md index 4fd6801a..f8ddf906 100644 --- a/content/ru/recipes/delete.md +++ b/content/ru/recipes/delete.md @@ -62,9 +62,9 @@ description: "Как удалять по-разному в зависимост | Ввод названия | [Текстовое поле](/ru/smart/forms/inputs/) | подпись «Чтобы подтвердить, введите …», `error-message` объясняет раннее нажатие | | Ответы окна | [Кнопка](/ru/smart/actions/buttons/) | красная залитая `status="danger"` первой, затем «Отмена» `appearance="ghost"` `scheme="on-surface"` | -## Так и не так +## Как делать -:::example {id="recipes/delete/do-dont" label="Так и не так"} +:::example {id="recipes/delete/do-dont" label="Как делать"} ::: - **Вопрос называет, что удалится.** «Удалить «Смета на декабрь.xlsx»?» — diff --git a/content/ru/recipes/empty-state.md b/content/ru/recipes/empty-state.md index 31f905f1..5f7a5ab6 100644 --- a/content/ru/recipes/empty-state.md +++ b/content/ru/recipes/empty-state.md @@ -42,9 +42,9 @@ description: "Что показать, когда в разделе ещё ни | Место | рамка | `radius-surface border border-outline-variant` на месте будущего списка | | Первая запись | [Список](/ru/smart/content/list/) | заменяет пустой экран; кнопка «Добавить» переходит в заголовок | -## Так и не так +## Как делать -:::example {id="recipes/empty-state/do-dont" label="Так и не так"} +:::example {id="recipes/empty-state/do-dont" label="Как делать"} ::: - **Одно действие.** Пустой экран ведёт к одному шагу. Импорт и справку diff --git a/content/ru/recipes/filter-drawer.md b/content/ru/recipes/filter-drawer.md index b7d4529e..7ba9be83 100644 --- a/content/ru/recipes/filter-drawer.md +++ b/content/ru/recipes/filter-drawer.md @@ -43,9 +43,9 @@ description: "Отбор списка по нескольким признака | Выбранные фильтры | [Тег](/ru/smart/content/tags/) | `kind="default"` `closable`, `close-label` с названием; за ними «Сбросить все» `appearance="ghost"` `size="1/2"` | | Ничего не подошло | [Пустое состояние](/ru/smart/feedback/empty-state/) | в слоте `empty` списка, `kind="nothing-found"`, действие «Сбросить фильтры» | -## Так и не так +## Как делать -:::example {id="recipes/filter-drawer/do-dont" label="Так и не так"} +:::example {id="recipes/filter-drawer/do-dont" label="Как делать"} ::: - **Выбранное видно.** Фильтры, которые сейчас действуют, стоят тегами над diff --git a/content/ru/recipes/index.md b/content/ru/recipes/index.md index ca76fd3e..8df06382 100644 --- a/content/ru/recipes/index.md +++ b/content/ru/recipes/index.md @@ -1,6 +1,6 @@ --- title: "Рецепты" -description: "Готовые сборки типовых экранов админки и сайта из частей SIMAI Framework: живой пример, когда брать, из чего собрано, так и не так, копия для ИИ." +description: "Готовые сборки типовых экранов админки и сайта из частей SIMAI Framework: живой пример, когда брать, из чего собрано, как делать, копия для ИИ." --- # Рецепты @@ -52,6 +52,6 @@ description: "Готовые сборки типовых экранов адми - **Когда брать** — и когда взять другой рецепт или компонент. - **Из чего собрано** — части со ссылками на их страницы и настройки, которые у них заданы. -- **Так и не так** — частые ошибки парами. +- **Как делать** — частые ошибки парами: правильный пример в зелёной рамке, неправильный в красной. - **Копия для ИИ** — разметка и правила одним блоком. Скопируйте его в задачу агенту целиком: правила в начале блока важнее примера под ними. diff --git a/content/ru/recipes/notifications.md b/content/ru/recipes/notifications.md index 7fd97f8e..ad01243d 100644 --- a/content/ru/recipes/notifications.md +++ b/content/ru/recipes/notifications.md @@ -41,9 +41,9 @@ description: "Уведомления под колокольчиком в шап | Строки | [Список](/ru/smart/content/list/) | `group-by="day"` `size="1/2"`; у строки `text` — что случилось, `supporting` — с чем, `date`; непрочитанная — `badge: {text: 'Новое', tone: 'info'}` | | Прокрутка | обёртка | `max-h-g4 overflow-auto`: шапка панели стоит, прокручиваются строки | -## Так и не так +## Как делать -:::example {id="recipes/notifications/do-dont" label="Так и не так"} +:::example {id="recipes/notifications/do-dont" label="Как делать"} ::: - **Число — только непрочитанное.** Колокольчик с «128» за все дни diff --git a/content/ru/recipes/record-card.md b/content/ru/recipes/record-card.md index ad3a1419..9e51558e 100644 --- a/content/ru/recipes/record-card.md +++ b/content/ru/recipes/record-card.md @@ -39,9 +39,9 @@ description: "Страница одной записи: путь назад к | Сведения | [Список сведений](/ru/smart/content/list-description/) | `empty-text="Не указан"`; состояние — `status` у строки, путь — `mono` и `copy` | | История | [Хронология](/ru/smart/content/list-timeline/) | `size="1/2"`, события новые сверху | -## Так и не так +## Как делать -:::example {id="recipes/record-card/do-dont" label="Так и не так"} +:::example {id="recipes/record-card/do-dont" label="Как делать"} ::: - **Пустое — словами.** Пустое значение пишется «Не указан»: прочерк diff --git a/content/ru/recipes/resource-list.md b/content/ru/recipes/resource-list.md index 61019572..0ce77bf7 100644 --- a/content/ru/recipes/resource-list.md +++ b/content/ru/recipes/resource-list.md @@ -48,9 +48,9 @@ description: "Страница админки со списком сайтов, на корне экрана сдвигает все части на ступень вниз вместе ([Плотность](/ru/guide/size-and-type/density/)). -## Так и не так +## Как делать -:::example {id="recipes/resource-list/do-dont" label="Так и не так"} +:::example {id="recipes/resource-list/do-dont" label="Как делать"} ::: - **Одно залитое действие.** «Создать» — единственная залитая кнопка diff --git a/content/ru/recipes/settings-form.md b/content/ru/recipes/settings-form.md index 7bbc85fd..465b7195 100644 --- a/content/ru/recipes/settings-form.md +++ b/content/ru/recipes/settings-form.md @@ -42,9 +42,9 @@ description: "Страница настроек записи или прилож | Ответы | [Кнопка](/ru/smart/actions/buttons/) | «Сохранить» — `native-type="submit"`, вид по умолчанию; «Отмена» — `native-type="reset"` `appearance="ghost"` `scheme="on-surface"` | | Результат | [Уведомление](/ru/smart/feedback/toast/) | `status="success"`, `duration="5000"`, в контейнере с `role="status"` в углу | -## Так и не так +## Как делать -:::example {id="recipes/settings-form/do-dont" label="Так и не так"} +:::example {id="recipes/settings-form/do-dont" label="Как делать"} ::: - **«Отмена» — тихая.** Красный цвет говорит «удалит или не вернётся»; diff --git a/examples/recipes/dashboard/do-dont/index.html b/examples/recipes/dashboard/do-dont/index.html index 90c0135f..b1fc3449 100644 --- a/examples/recipes/dashboard/do-dont/index.html +++ b/examples/recipes/dashboard/do-dont/index.html @@ -1,37 +1,37 @@
-
-
+
+
Правильно — цвет только там, где перейдён порог
+
-
Так — цвет только там, где перейдён порог
-
-
+
+
Неправильно — зелёное везде и число без сравнения
+
-
Не так — зелёное везде и число без сравнения
-
-
+
+
Правильно — проблемы одним списком, серьёзные первыми
+
-
Так — проблемы одним списком, серьёзные первыми
-
-
+
+
Неправильно — стопка цветных плашек сдвигает весь экран
+
-
Не так — стопка цветных плашек сдвигает весь экран
diff --git a/examples/recipes/delete/do-dont/index.html b/examples/recipes/delete/do-dont/index.html index fc7b9106..f8bcfbe6 100644 --- a/examples/recipes/delete/do-dont/index.html +++ b/examples/recipes/delete/do-dont/index.html @@ -1,20 +1,20 @@
-
-
-
Так — вопрос называет, что удалится, «Удалить» красная
+
+
Правильно — вопрос называет, что удалится, «Удалить» красная
+
-
-
-
Не так — «Вы уверены?» и обычное «Подтвердить»
+
+
Неправильно — «Вы уверены?» и обычное «Подтвердить»
+
-
-
-
Так — своё и обратимое: удалить сразу, дать «Вернуть»
+
+
Правильно — своё и обратимое: удалить сразу, дать «Вернуть»
+
-
-
-
Не так — вопрос там, где можно просто вернуть
+
+
Неправильно — вопрос там, где можно просто вернуть
+
diff --git a/examples/recipes/empty-state/do-dont/index.html b/examples/recipes/empty-state/do-dont/index.html index ba01f33b..a29ed78c 100644 --- a/examples/recipes/empty-state/do-dont/index.html +++ b/examples/recipes/empty-state/do-dont/index.html @@ -1,38 +1,38 @@
-
-
+
+
Правильно — одно действие, которое снимает пустоту
+
-
Так — одно действие, которое снимает пустоту
-
-
+
+
Неправильно — три равные кнопки, непонятно, с чего начать
+
-
Не так — три равные кнопки, непонятно, с чего начать
-
-
+
+
Правильно — пусто из-за фильтра: так и сказать, дать сбросить
+
-
Так — пусто из-за фильтра: так и сказать, дать сбросить
-
-
+
+
Неправильно — при фильтре «товаров нет»: человек решит, что их удалили
+
-
Не так — при фильтре «товаров нет»: человек решит, что их удалили
diff --git a/examples/recipes/filter-drawer/do-dont/index.html b/examples/recipes/filter-drawer/do-dont/index.html index bd3ce580..ed09bb4f 100644 --- a/examples/recipes/filter-drawer/do-dont/index.html +++ b/examples/recipes/filter-drawer/do-dont/index.html @@ -1,7 +1,8 @@
-
-
+
+
Правильно — выбранное видно над списком и снимается по одному
+
@@ -12,30 +13,29 @@
-
Так — выбранное видно над списком и снимается по одному
-
-
+
+
Неправильно — фильтры включены, но этого не видно
+
-
Не так — фильтры включены, но этого не видно
-
-
+
+
Правильно — кнопка заранее говорит, сколько покажет
+
-
Так — кнопка заранее говорит, сколько покажет
-
-
+
+
Неправильно — «Применить» вслепую и две равные кнопки
+
-
Не так — «Применить» вслепую и две равные кнопки
diff --git a/examples/recipes/notifications/do-dont/index.html b/examples/recipes/notifications/do-dont/index.html index c0c978f4..2b7f059b 100644 --- a/examples/recipes/notifications/do-dont/index.html +++ b/examples/recipes/notifications/do-dont/index.html @@ -1,27 +1,27 @@
-
-
+
+
Правильно — колокольчик считает только непрочитанное
+
-
Так — колокольчик считает только непрочитанное
-
-
+
+
Неправильно — все уведомления за год: число перестают замечать
+
-
Не так — все уведомления за год: число перестают замечать
-
-
-
Так — что случилось и с чем, новое отмечено словом
+
+
Правильно — что случилось и с чем, новое отмечено словом
+
-
-
+
+
Неправильно — уведомления в углу вместо списка: уходят, не прочитанные
+
-
Не так — уведомления в углу вместо списка: уходят, не прочитанные
diff --git a/examples/recipes/record-card/do-dont/index.html b/examples/recipes/record-card/do-dont/index.html index b4b76bc8..9815384e 100644 --- a/examples/recipes/record-card/do-dont/index.html +++ b/examples/recipes/record-card/do-dont/index.html @@ -1,35 +1,35 @@
-
-
- +
+
Правильно — пустое значение словами: «Не указан»
+
+
-
Так — пустое значение словами: «Не указан»
-
-
- +
+
Неправильно — прочерк: не понять, пусто или не загрузилось
+
+
-
Не так — прочерк: не понять, пусто или не загрузилось
-
-
- +
+
Правильно — сведения текстом, править — по «Изменить»
+
+
-
Так — сведения текстом, править — по «Изменить»
-
-
+
+
Неправильно — поля только для чтения вместо сведений
+
-
Не так — поля только для чтения вместо сведений
diff --git a/examples/recipes/resource-list/do-dont/index.html b/examples/recipes/resource-list/do-dont/index.html index 7b585bc3..843f0863 100644 --- a/examples/recipes/resource-list/do-dont/index.html +++ b/examples/recipes/resource-list/do-dont/index.html @@ -1,26 +1,26 @@
-
-
+
+
Правильно — одна залитая кнопка, второе действие с рамкой
+
-
Так — одна залитая кнопка, второе действие с рамкой
-
-
+
+
Неправильно — две залитые кнопки спорят, главное не видно
+
-
Не так — две залитые кнопки спорят, главное не видно
-
-
-
Так — бейдж только у записи с проблемой
+
+
Правильно — бейдж только у записи с проблемой
+
-
-
-
Не так — «Работает» на каждой строке прячет проблему
+
+
Неправильно — «Работает» на каждой строке прячет проблему
+
diff --git a/examples/recipes/settings-form/do-dont/index.html b/examples/recipes/settings-form/do-dont/index.html index a3099632..c9544e01 100644 --- a/examples/recipes/settings-form/do-dont/index.html +++ b/examples/recipes/settings-form/do-dont/index.html @@ -1,43 +1,43 @@
-
-
+
+
Правильно — «Отмена» тихая, она ничего не ломает
+
-
Так — «Отмена» тихая, она ничего не ломает
-
-
+
+
Неправильно — красный значит «удалит», а не «закроет»
+
-
Не так — красный значит «удалит», а не «закроет»
-
-
+
+
Правильно — ошибка под полем и что сделать
+
-
Так — ошибка под полем и что сделать
-
-
+
+
Неправильно — общая ошибка сверху, поле не отмечено
+
-
Не так — общая ошибка сверху, поле не отмечено
-
-
+
+
Правильно — подпись над полем, в поле — образец
+
-
Так — подпись над полем, в поле — образец
-
-
+
+
Неправильно — подпись в поле пропадает при вводе
+
-
Не так — подпись в поле пропадает при вводе
From f36a34a23ba2f221ca2f407ecf861e0bf098dbd1 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 13:35:14 +0300 Subject: [PATCH 29/53] Docs: drop comparisons with other systems (D16, R100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Palettes: «Почему эти тона» becomes «Тона, прозрачность и роли» -- the tones, the alpha families and the role-to-tone table, without the cross-system survey, the «Сколько у других» table or Material lightness matches. Colour roles and palettes no longer say they follow Material. Legacy English pages lose their Material, Google theme builder and Bootstrap references; the glossary loses its Bootstrap entry. Co-Authored-By: Claude Opus 5.5 --- .../break-points/break-points-variables.md | 2 +- .../en/fundamentals/color/color-generator.md | 5 +- .../en/fundamentals/color/color-palette.md | 3 +- .../color/new-approach-to-using-color.md | 5 +- .../break-points-break-points-variables.md | 2 +- .../concept/color-color-generator.md | 5 +- .../concept/color-color-palette.md | 3 +- .../color-new-approach-to-using-color.md | 5 +- .../color-primitives/color-palette-neutral.md | 2 +- .../color-palette-transparent-white-black.md | 2 +- .../reference/color-primitives/color-roles.md | 4 +- .../color-primitives/interactive-roles.md | 3 +- content/en/start/glossary/index.md | 3 - content/ru/guide/colour/colour-roles.md | 3 +- content/ru/guide/colour/palettes.md | 56 +++++++------------ 15 files changed, 36 insertions(+), 67 deletions(-) diff --git a/content/en/fundamentals/break-points/break-points-variables.md b/content/en/fundamentals/break-points/break-points-variables.md index c46bd8c9..7f91e126 100644 --- a/content/en/fundamentals/break-points/break-points-variables.md +++ b/content/en/fundamentals/break-points/break-points-variables.md @@ -5,7 +5,7 @@ description: "Breakpoint variables" # Breakpoint variables -Each breakpoint has a variable so you can adjust values centrally. The breakpoints are adapted from Bootstrap with these +Each breakpoint has a variable so you can adjust values centrally. The breakpoints follow these rules: * Breakpoint sizes align with the framework's sizing system. diff --git a/content/en/fundamentals/color/color-generator.md b/content/en/fundamentals/color/color-generator.md index eaf8e2e5..97f47df6 100644 --- a/content/en/fundamentals/color/color-generator.md +++ b/content/en/fundamentals/color/color-generator.md @@ -5,10 +5,7 @@ description: "Color generator" ## Color generator -SIMAI Framework provides its own palette generator, based on Google’s generator. - -* SIMAI generator: [http://sf5.simai.pro/ru/bx/color/](http://sf5.simai.pro/ru/bx/color/) -* Google generator: [https://m3.material.io/theme-builder/](https://m3.material.io/theme-builder/#/) +SIMAI Framework provides its own palette generator: [http://sf5.simai.pro/ru/bx/color/](http://sf5.simai.pro/ru/bx/color/). Generation process: diff --git a/content/en/fundamentals/color/color-palette.md b/content/en/fundamentals/color/color-palette.md index 771b4265..2bfed8d4 100644 --- a/content/en/fundamentals/color/color-palette.md +++ b/content/en/fundamentals/color/color-palette.md @@ -8,7 +8,6 @@ description: "Color palette" The palette contains main groups: **Primary**, **Secondary**, **Tertiary**, **Error**, **Success**, plus three without tones — transparent, white, black. -Palette generation is based on the HCT model (Hue, Chroma, Tone) from Material Design 3. Tone generation follows MD3’s -approach ([Material Design Color System](https://m3.material.io/styles/color/system/overview)). Each color uses these 16 +Palettes are built in the HCT colour model (Hue, Chroma, Tone). Each color uses these 16 tones: 98, 95, 90, 85, 80, 70, 60, 50, 40, 35, 30, 25, 20, 15, 10, 5. Tones 100 and 0 are replaced by white and black respectively. diff --git a/content/en/fundamentals/color/new-approach-to-using-color.md b/content/en/fundamentals/color/new-approach-to-using-color.md index e88efacc..b9f54eec 100644 --- a/content/en/fundamentals/color/new-approach-to-using-color.md +++ b/content/en/fundamentals/color/new-approach-to-using-color.md @@ -5,10 +5,9 @@ description: "New approach to using color" # New approach to using color -SIMAI Framework applies an updated color strategy inspired by Google Material 3, adapted for the web. The changes account -for web specifics, simplifying state handling without adding extra layers like in Material 3. +States are expressed as their own colour roles, not as an extra layer over the element. -Key difference: three accent colors — **primary**, **secondary**, **tertiary** — instead of one. This builds a more +SIMAI Framework uses three accent colors — **primary**, **secondary**, **tertiary** — instead of one. This builds a more flexible visual hierarchy: * **Primary** — saturated color for the most critical elements; use sparingly (ideally one key element per screen). diff --git a/content/en/fundamentals/concept/break-points-break-points-variables.md b/content/en/fundamentals/concept/break-points-break-points-variables.md index bba0d8b2..3dd26f36 100644 --- a/content/en/fundamentals/concept/break-points-break-points-variables.md +++ b/content/en/fundamentals/concept/break-points-break-points-variables.md @@ -5,7 +5,7 @@ description: "Breakpoint variables" # Breakpoint variables -Each breakpoint is defined via a variable, which makes it easy to change values centrally. The breakpoints are adapted from Bootstrap with these conditions in mind: +Each breakpoint is defined via a variable, which makes it easy to change values centrally. The breakpoints follow these conditions: * A breakpoint size should align with the framework’s sizing system. * Breakpoint values should be divisible by 12 (with 1rem = 16px) to support a 12‑column grid. diff --git a/content/en/fundamentals/concept/color-color-generator.md b/content/en/fundamentals/concept/color-color-generator.md index e779401a..e4976750 100644 --- a/content/en/fundamentals/concept/color-color-generator.md +++ b/content/en/fundamentals/concept/color-color-generator.md @@ -5,10 +5,7 @@ description: "Color generator" # Color generator -To build a color palette, SIMAI Framework provides its own generator based on Google’s generator. - -* SIMAI generator: [http://sf5.simai.pro/ru/bx/color/](http://sf5.simai.pro/ru/bx/color/) -* Google generator: [https://m3.material.io/theme-builder/](https://m3.material.io/theme-builder/#/) +To build a color palette, SIMAI Framework provides its own generator: [http://sf5.simai.pro/ru/bx/color/](http://sf5.simai.pro/ru/bx/color/). Generation process: diff --git a/content/en/fundamentals/concept/color-color-palette.md b/content/en/fundamentals/concept/color-color-palette.md index 7dd859b8..3a58ce4f 100644 --- a/content/en/fundamentals/concept/color-color-palette.md +++ b/content/en/fundamentals/concept/color-color-palette.md @@ -7,7 +7,6 @@ description: "Color palette" The color palette includes the following main groups of colors: **Primary**, **Secondary**, **Tertiary**, **Error**, **Success**, as well as three additional tone-less colors — transparent, white, and black. -The palette generation system is based on the HCT (Hue, Chroma, Tone) color scheme used in Material Design 3\. -The approach to generating tones is similar to the Material Design 3 documentation (details: [Material Design Color System](https://m3.material.io/styles/color/system/overview)). +Palettes are built in the HCT (Hue, Chroma, Tone) colour model. For each color, a set of 16 tones is used: 98, 95, 90, 85, 80, 70, 60, 50, 40, 35, 30, 25, 20, 15, 10, 5\. Tones 100 and 0 are replaced with white and black respectively. diff --git a/content/en/fundamentals/concept/color-new-approach-to-using-color.md b/content/en/fundamentals/concept/color-new-approach-to-using-color.md index e93dd5b4..b4a48641 100644 --- a/content/en/fundamentals/concept/color-new-approach-to-using-color.md +++ b/content/en/fundamentals/concept/color-new-approach-to-using-color.md @@ -5,10 +5,9 @@ description: "A new approach to using color" # A new approach to using color -SIMAI Framework uses an updated approach to color management inspired by Google Material 3 concepts, but adapted for web projects. -The changes take web development specifics into account, simplifying the implementation of element states without adding extra layers, as is done in Material 3\. +States are expressed as their own colour roles, not as an extra layer over the element. -A key difference is the use of three main accent colors: **primary**, **secondary**, and **tertiary**, rather than a single one as in traditional schemes. +SIMAI Framework uses three main accent colors: **primary**, **secondary**, and **tertiary**. This makes it possible to build a more flexible visual hierarchy: * **Primary** — a saturated color for the most important, attention‑demanding elements. It’s recommended to use it sparingly — no more than one key element per screen. diff --git a/content/en/reference/color-primitives/color-palette-neutral.md b/content/en/reference/color-primitives/color-palette-neutral.md index ac06dd88..a9971fd3 100644 --- a/content/en/reference/color-primitives/color-palette-neutral.md +++ b/content/en/reference/color-primitives/color-palette-neutral.md @@ -8,7 +8,7 @@ description: Neutral # Neutral -The `neutral` color in SIMAI Framework is intended for surfaces and neutral interface elements. In Google Material this +The `neutral` color in SIMAI Framework is intended for surfaces and neutral interface elements. It is a shade based on the Primary color with reduced chroma (by default 4), which gives a gray color with a tint of the primary color. diff --git a/content/en/reference/color-primitives/color-palette-transparent-white-black.md b/content/en/reference/color-primitives/color-palette-transparent-white-black.md index bf5b5282..42e56222 100644 --- a/content/en/reference/color-primitives/color-palette-transparent-white-black.md +++ b/content/en/reference/color-primitives/color-palette-transparent-white-black.md @@ -11,7 +11,7 @@ In addition to primary and accent colors, the following base primitives are used * **white** — white color. * **black** — black color. -To optimize the number of variables, tones 0 and 100 are not used for all colors (similar to Material Design). Their +To optimize the number of variables, tones 0 and 100 are not used for all colors. Their role is played by white and black: * White and black colors do not change during generation, so their semi-transparent variations are written directly as diff --git a/content/en/reference/color-primitives/color-roles.md b/content/en/reference/color-primitives/color-roles.md index 613b44d9..38a6db72 100644 --- a/content/en/reference/color-primitives/color-roles.md +++ b/content/en/reference/color-primitives/color-roles.md @@ -5,9 +5,7 @@ description: "Color roles" # Color roles -SIMAI Framework uses an extended approach to color roles based on the principles of Material Design (more details: -[Material Design Color Roles](https://m3.material.io/styles/color/roles)). In the current version, the approach is -expanded to simplify usage in web development. +SIMAI Framework colours interfaces with color roles. Roles can be divided into several categories: diff --git a/content/en/reference/color-primitives/interactive-roles.md b/content/en/reference/color-primitives/interactive-roles.md index f9a57c5b..d265c530 100644 --- a/content/en/reference/color-primitives/interactive-roles.md +++ b/content/en/reference/color-primitives/interactive-roles.md @@ -5,8 +5,7 @@ description: "Interactive roles" # Interactive roles -Although the principles are largely inspired by Material Design, SIMAI adapts them for web development. Instead of -creating "elevations" above the surface through a layer, SIMAI uses interactivity roles. +Instead of a layer over the surface, SIMAI uses interactivity roles. * **Hover** — state when the cursor is hovering over an element. * **Active** — state of an active element. diff --git a/content/en/start/glossary/index.md b/content/en/start/glossary/index.md index 157dac74..9a271a55 100644 --- a/content/en/start/glossary/index.md +++ b/content/en/start/glossary/index.md @@ -56,9 +56,6 @@ functionality. Framework frontend is built on UI components and utilities, simplifying creation of responsive and interactive interfaces. -**Bootstrap** — a popular CSS framework for rapid prototyping and building responsive web applications. It is used in -some SIMAI Framework projects to speed up frontend development. - **REST API** — an architectural style for web services based on standard HTTP methods (GET, POST, PUT, DELETE) for data exchange. SIMAI Framework supports building and using REST APIs for integration with external systems and services. diff --git a/content/ru/guide/colour/colour-roles.md b/content/ru/guide/colour/colour-roles.md index d35afc6b..a93c3b53 100644 --- a/content/ru/guide/colour/colour-roles.md +++ b/content/ru/guide/colour/colour-roles.md @@ -18,8 +18,7 @@ description: "Цветовые роли — это токены, которые Компоненты и утилиты красят только ролями. Кнопка запрашивает `--sf-primary`, а конкретный тон определяет тема. -Система ролей основана на подходе Material Design 3 и доработана для веба: -состояния (наведение, выбор, нажатие) выражены не отдельным слоем поверх +Состояния (наведение, выбор, нажатие) выражены не отдельным слоем поверх элемента, а собственными ролями — `-hover`, `-active`, `-state-*`. Их можно применить обычным CSS-свойством и обычной утилитой. diff --git a/content/ru/guide/colour/palettes.md b/content/ru/guide/colour/palettes.md index e55f31c2..c930f512 100644 --- a/content/ru/guide/colour/palettes.md +++ b/content/ru/guide/colour/palettes.md @@ -73,9 +73,8 @@ description: "Цветовые примитивы — это конкретны Шаг неравномерный: у краёв (светлые фоны, тёмный текст) нужны мелкие различия, поэтому там шаг 5; в середине, где цвет используется реже, — шаг 10. -Палитры построены по модели HCT (оттенок, насыщенность, тон), той же, что у -Material Design 3: тон в имени означает одинаковую воспринимаемую светлоту во -всех палитрах. Поэтому `--sf-primary-40` и `--sf-error-40` одинаково светлы, и +Палитры построены по модели HCT (оттенок, насыщенность, тон): тон в имени +означает одинаковую воспринимаемую светлоту во всех палитрах. Поэтому `--sf-primary-40` и `--sf-error-40` одинаково светлы, и пара «тон 40 на белом» даёт сопоставимый контраст в любой палитре. Палитра `info` — своя палитра цвета `#005bbe`, как `error`, `warning` и @@ -224,14 +223,20 @@ Framework не использует, не проходит контракт `alp окрашенный `--sf-primary-40`, в тёмной теме останется тёмно-синим на почти чёрном фоне. -## Почему эти тона +## Тона, прозрачность и роли -Шестнадцати тонов, белого и чёрного, одиннадцати ступеней -[прозрачности](/ru/guide/colour/transparency/) и нынешнего набора оттенков -хватает на все роли. Сверка с Material 3, Tailwind, Radix, IBM Carbon, -Atlassian, Primer, Ant Design, Apple и Fluent не нашла роли, которой не -досталось бы тона. Там, где другие системы держат лишние сплошные тона, здесь -работает прозрачность. +Роли берут цвет из трёх источников: + +- **шестнадцать тонов** каждой палитры, белый и чёрный — для текста, линий и + сплошных заливок; +- **одиннадцать ступеней [прозрачности](/ru/guide/colour/transparency/)** в + трёх семействах: состояние 4, 8, 12; заливка 12–32; штрих 48, 64, 80 — для + наведения, выбора, нажатия, плашек и линий; +- **оттенки [статической палитры](#статическая-палитра)** — для цветов, которые + от темы не зависят. + +Других тонов Framework не добавляет: нейтральная палитра сама берёт оттенок +темы, `gray` даёт чистый серый, `brown` — тёплый приглушённый. ### Какой тон берёт роль @@ -255,18 +260,16 @@ Atlassian, Primer, Ant Design, Apple и Fluent не нашла роли, кот Текст на основном фоне читается с запасом: основной — 17,2 : 1 в светлой теме и 14,7 : 1 в тёмной, акцентный — около 6,5 : 1 и 11 : 1. -### Прозрачность вместо лишних тонов +### Полупрозрачные подложки и состояния -У Material 3 есть десять особых тонов фона (4, 6, 12, 17, 22, 24, 87, 92, 94, -96): они нужны, чтобы подложки были сплошными. Здесь подложки и состояния — -полупрозрачный нейтральный цвет, и попадают они в ту же светлоту. -`neutral-50` на белом при 4, 8, 12, 16 и 20 % даёт светлоту 98, 96, 94, 92 и -91 — ступени Material 98, 96, 94, 92 и 90. В тёмной теме `neutral-90` на -странице при 8, 12, 16 и 20 % даёт 13, 17, 21 и 25 против 12, 17, 22 и 24. +Подложки, наведение, выбор и нажатие — полупрозрачный цвет, а не отдельный +тон. `neutral-50` на белом при 4, 8, 12, 16 и 20 % даёт светлоту 98, 96, 94, +92 и 91; в тёмной теме `neutral-90` на странице при 8, 12, 16 и 20 % даёт 13, +17, 21 и 25. Полупрозрачный слой подстраивается под место, на котором лежит: на белой карточке, на тёмной панели и на цветной шапке он остаётся «на ступеньку -темнее места». Поэтому сплошные двойники для каждого места не нужны. +темнее места». Один и тот же слой работает на любом месте. ### Где нужен сплошной цвет @@ -280,23 +283,6 @@ Atlassian, Primer, Ant Design, Apple и Fluent не нашла роли, кот аватары, флажки, переключатели и слайдеры его читают. Подробнее — [«Место под вырезом»](/ru/guide/colour/surfaces/#место-под-вырезом). -### Сколько у других - -| Система | Оттенки | Ступеней у оттенка | Прозрачность | -|:---|:---|:---|:---| -| SIMAI Framework | 15 постоянных, серый и 4 палитры темы | 16, белый и чёрный | 11 ступеней в трёх семействах | -| Material 3 | 5 ключевых и ошибка | 13 и 10 особых тонов фона | слои состояний 8, 10 и 16 % | -| Tailwind 4 | 17 и 9 серых | 11 | любая, без шкалы | -| Radix Colors | 25 и 6 серых | 12 | полупрозрачный двойник каждой ступени | -| IBM Carbon | 9 и 3 серых | 10 | в палитре нет | -| Ant Design | 12 и нейтральный | 10 | у нейтральных текста, линий и фона | - -Шестнадцать ступеней — самая частая лестница из всех, кроме серых Fluent. -Системы с меньшим числом ступеней добирают фоны прозрачностью или лишними -серыми; здесь одна лестница и прозрачность закрывают обе задачи. Новых тонов, -полутонов и оттенков Framework не добавляет: нейтральная палитра сама берёт -оттенок темы, `gray` даёт чистый серый, `brown` — тёплый приглушённый. - ## Замена палитры Проще всего сменить фирменный цвет [генератором темы](/ru/smart/forms/theme-generator/): From c050d4dec0dad07ebcaae50112077249da2e9f92 Mon Sep 17 00:00:00 2001 From: Rim Zabarov Date: Tue, 10 Nov 2026 14:59:28 +0300 Subject: [PATCH 30/53] JavaScript API: the page becomes the index of window.SF by layer (D17, R103) The generator groups every public name of core/contracts/public-js-api.json by what brings it (core, Loader, Smart base, component script) and links each row to its function page or the page that describes it; a component's script API links to the component page. Short lines and links live in config/js-api-index.json, and --check fails when a public name has no row or a row names no public name. Internal names are no longer listed; the readingPosition and format sections moved to their function pages. Co-Authored-By: Claude Opus 5.5 --- config/js-api-index.json | 349 +++++++++++++++++++ content/ru/guide/connection/js-api.md | 330 +++++++----------- content/ru/guide/connection/js-api.page.json | 3 + scripts/build-smart-reference.mjs | 77 ++-- 4 files changed, 515 insertions(+), 244 deletions(-) create mode 100644 config/js-api-index.json diff --git a/config/js-api-index.json b/config/js-api-index.json new file mode 100644 index 00000000..c45c74cf --- /dev/null +++ b/config/js-api-index.json @@ -0,0 +1,349 @@ +{ + "schema": "ui-doc.js-api-index.v1", + "why": "The JavaScript API page is the index of window.SF (owner D17, 2026-11-10): every public name of core/contracts/public-js-api.json, grouped by what brings it, links to its function page or to the page that describes it; a component's script API links to the component's page. text is the short line the index shows (the contract's summary is the release's record, not an author's line); a component row shows the page title. scripts/build-smart-reference.mjs fails --check when a public name has no row here or a row names no public name.", + "names": { + "SF.ready": { + "text": "Ждёт запуска Loader: обещание или вызов функцией.", + "page": "/ru/guide/functions/loading/ready/", + "label": "Готовность Loader" + }, + "SF.package": { + "text": "Отдаёт API набора компонентов: функции, которые набор зарегистрировал.", + "page": "/ru/guide/functions/loading/package/", + "label": "API набора" + }, + "SF.format": { + "text": "Даты, дни, числа и размеры файлов на языке страницы.", + "page": "/ru/guide/functions/text/format/", + "label": "Даты, числа и размеры" + }, + "SF.language": { + "text": "Язык всей страницы и слова, добавленные для компонентов.", + "page": "/ru/guide/functions/text/language/", + "label": "Язык страницы" + }, + "SF.pageLanguage": { + "text": "Язык, который действует для узла страницы.", + "page": "/ru/guide/functions/text/page-text/", + "label": "Слово компонента" + }, + "SF.pageText": { + "text": "Слово компонента на языке узла.", + "page": "/ru/guide/functions/text/page-text/", + "label": "Слово компонента" + }, + "SF.sanitizeHtml": { + "text": "Очищает HTML из чужого источника перед вставкой на страницу.", + "page": "/ru/guide/functions/text/sanitize-html/", + "label": "Чужой HTML" + }, + "SF.safeHref": { + "text": "Отвечает, можно ли оставить адрес ссылки.", + "page": "/ru/guide/functions/text/sanitize-html/", + "label": "Чужой HTML" + }, + "SF.Mask": { + "text": "Маска ввода у поля и форматирование значения по маске.", + "page": "/ru/guide/functions/text/mask/", + "label": "Маска поля" + }, + "SF.copyText": { + "text": "Кладёт текст в буфер обмена.", + "page": "/ru/guide/functions/actions/copy-text/", + "label": "Копирование текста" + }, + "SF.codeCopy": { + "text": "Код в тексте копируется по нажатию.", + "page": "/ru/guide/functions/actions/code-copy/", + "label": "Копирование кода в тексте" + }, + "SF.notify": { + "text": "Показывает сообщение в области уведомлений страницы.", + "page": "/ru/guide/functions/actions/notify/", + "label": "Уведомления" + }, + "SF.notifications": { + "text": "Настройки области уведомлений и показанные сообщения.", + "page": "/ru/guide/functions/actions/notify/", + "label": "Уведомления" + }, + "SF.breakpoints": { + "text": "Контрольные точки ширины для скрипта.", + "page": "/ru/guide/functions/screen/breakpoints/", + "label": "Контрольные точки" + }, + "SF.readingPosition": { + "text": "Сохраняет место читателя в прокручиваемой колонке.", + "page": "/ru/guide/functions/screen/reading-position/", + "label": "Положение чтения" + }, + "SF.Position": { + "text": "Держит всплывающую панель у её якоря.", + "page": "/ru/guide/functions/screen/position/", + "label": "Привязка к якорю" + }, + "SF.Preferences": { + "text": "Хранит вид, который выбрал человек: порядок, ширину, сортировку.", + "page": "/ru/guide/functions/person/preferences/", + "label": "Настройки вида" + }, + "SF.icons": { + "text": "Список значков, из которых выбирает человек.", + "page": "/ru/guide/functions/person/icons/", + "label": "Значки на выбор" + }, + "SF.RegionNavigation": { + "text": "Переходы между страницами заменой области содержимого.", + "page": "/ru/guide/layouts/building/region-navigation/", + "label": "Навигация по областям страницы" + }, + "SF.workspace": { + "text": "Управляет рабочей областью: список, основная часть, панель.", + "page": "/ru/guide/layouts/building/workspace/", + "label": "Рабочая область" + }, + "SF.Composition": { + "text": "Помощники композиции и редактора блоков.", + "page": "/ru/guide/layouts/reference/api/", + "label": "API композиции" + }, + "SF.Assembly": { + "text": "Среда выполнения сборок.", + "page": "/ru/guide/smart-components/assemblies/", + "label": "Сборки" + }, + "SF.EarlyPort": { + "text": "Порты, объявленные в разметке: первый запрос до кода компонента.", + "page": "/ru/guide/smart-components/data/", + "label": "Данные с сервера" + }, + "SF.Effects": { + "text": "Прежнее имя движка фоновых эффектов; новое — `SF.package('effects')`.", + "page": "/ru/guide/layouts/building/background-effects/", + "label": "Фоновые эффекты" + }, + "SF.RuleLoader": { + "text": "Правила загрузки и регистрация пакета компонентов.", + "page": "/ru/guide/connection/loader/#пакеты-компонентов", + "label": "Пакеты компонентов" + }, + "SF.loadRuntime": { + "text": "Загружает общую среду смарт-компонентов; её вызывает Loader.", + "page": "/ru/guide/smart-components/runtime/", + "label": "Загрузка и общая среда" + }, + "SF.runtimeModule": { + "text": "Модуль общей среды для собранного смарт-компонента.", + "page": "/ru/guide/smart-components/runtime/", + "label": "Загрузка и общая среда" + }, + "SF.deprecations": { + "text": "Прежние имена, которые ещё принимаются, и чем их заменить.", + "page": "/ru/guide/connection/change-lifecycle/", + "label": "Жизненный цикл изменений" + }, + "SF.tags": { + "text": "Прежние теги переименованных смарт-компонентов.", + "page": "/ru/guide/smart-components/names/", + "label": "Имена и общие слова" + }, + "SF.aliases": { + "text": "Прежние имена событий, настроек и атрибутов у элемента.", + "page": "/ru/guide/smart-components/names/", + "label": "Имена и общие слова" + }, + "SF.Loader": { + "text": "Загружает компоненты по требованию и следит за новыми частями страницы.", + "page": "/ru/guide/functions/loading/loader/", + "label": "Загрузка по требованию" + }, + "SF.whenDefined": { + "text": "Ждёт, когда смарт-элемент будет определён.", + "page": "/ru/guide/functions/loading/when-defined/", + "label": "Ожидание элемента" + }, + "SF.whenDefine": { + "text": "Прежнее имя `SF.whenDefined`.", + "page": "/ru/guide/functions/loading/when-defined/", + "label": "Ожидание элемента" + }, + "SF.create": { + "text": "Создаёт элемент компонента по имени.", + "page": "/ru/guide/functions/loading/create/", + "label": "Создание элемента" + }, + "SF.createAsync": { + "text": "Создаёт элемент и ждёт, когда его тег будет определён.", + "page": "/ru/guide/functions/loading/create/", + "label": "Создание элемента" + }, + "SF.loadSmartBase": { + "text": "Загружает основу смарт-компонентов.", + "page": "/ru/guide/functions/loading/smart-base/", + "label": "Загрузка основы смарт-компонентов" + }, + "SF.smartBaseReady": { + "text": "Обещание основы смарт-компонентов, как бы она ни пришла.", + "page": "/ru/guide/functions/loading/smart-base/", + "label": "Загрузка основы смарт-компонентов" + }, + "SF.whenFieldRenderers": { + "text": "Загружает набор отрисовщиков полей.", + "page": "/ru/guide/smart-components/field-renderers/", + "label": "Поля по типу свойства" + }, + "SF.smart": { + "text": "Базовый класс и помощники для своего смарт-компонента.", + "page": "/ru/guide/functions/loading/smart/", + "label": "Свой смарт-компонент" + }, + "SF.SfBaseElement": { + "text": "Базовый класс смарт-элемента.", + "page": "/ru/guide/functions/loading/smart/", + "label": "Свой смарт-компонент" + }, + "SF.html": { + "text": "Тег шаблона, которым рисуют смарт-элементы.", + "page": "/ru/guide/functions/loading/smart/", + "label": "Свой смарт-компонент" + }, + "SF.render": { + "text": "Рисует шаблон в узел.", + "page": "/ru/guide/functions/loading/smart/", + "label": "Свой смарт-компонент" + }, + "SF.nothing": { + "text": "Пустое место в шаблоне.", + "page": "/ru/guide/functions/loading/smart/", + "label": "Свой смарт-компонент" + }, + "SF.AdminMenu": { + "page": "/ru/components/internal/admin-menu/#javascript", + "label": "Основа Admin Menu" + }, + "SF.AnchoredBadge": { + "page": "/ru/components/feedback/badges/#бейдж-на-якоре", + "label": "Бейджи" + }, + "SF.ButtonGroup": { + "page": "/ru/components/actions/button-group/#javascript", + "label": "Группа кнопок" + }, + "SF.Carousel": { + "page": "/ru/components/overlays-media/carousel/#javascript", + "label": "Карусель" + }, + "SF.Checkbox": { + "page": "/ru/components/forms/checkbox/#javascript", + "label": "Флажок" + }, + "SF.Clipboard": { + "page": "/ru/components/actions/clipboard/#javascript", + "label": "Копирование" + }, + "SF.CountryCode": { + "page": "/ru/components/forms/country-code/#javascript", + "label": "Код страны" + }, + "SF.DownloadFile": { + "page": "/ru/components/actions/download-file/#javascript", + "label": "Скачивание файла" + }, + "SF.FileUpload": { + "page": "/ru/components/forms/file-upload/#javascript", + "label": "Загрузка файлов" + }, + "SF.Flag": { + "page": "/ru/components/content/flag/#javascript", + "label": "Флаг" + }, + "SF.Highlight": { + "page": "/ru/components/content/highlight/#javascript", + "label": "Подсветка кода" + }, + "SF.Input": { + "page": "/ru/components/forms/inputs/#javascript", + "label": "Текстовые поля" + }, + "SF.ProgressBar": { + "page": "/ru/components/feedback/progress-bar/#javascript", + "label": "Прогресс" + }, + "SF.ProgressScale": { + "page": "/ru/components/feedback/progress-scale/#javascript", + "label": "Шкала прогресса" + }, + "SF.Radio": { + "page": "/ru/components/forms/radio/#javascript", + "label": "Радиокнопки" + }, + "SF.RangeSlider": { + "page": "/ru/components/forms/range-slider/#javascript", + "label": "Слайдер диапазона" + }, + "SF.Scrollbar": { + "page": "/ru/components/tools/scrollbar/#javascript", + "label": "Полоса прокрутки" + }, + "SF.Slider": { + "page": "/ru/components/overlays-media/slider/#javascript", + "label": "Слайдер контента" + }, + "SF.Spinner": { + "page": "/ru/components/feedback/spinner/#javascript", + "label": "Спиннер" + }, + "SF.Switch": { + "page": "/ru/components/forms/switch/#javascript", + "label": "Переключатель" + }, + "SF.Tab": { + "page": "/ru/components/navigation/tab/#javascript", + "label": "Вкладка (совместимость)" + }, + "SF.Tabs": { + "page": "/ru/components/navigation/tabs/#javascript", + "label": "Вкладки" + }, + "SF.Textarea": { + "page": "/ru/components/forms/textarea/#javascript", + "label": "Многострочное поле" + }, + "SF.Toast": { + "page": "/ru/components/feedback/toast/#javascript", + "label": "Всплывающие сообщения" + }, + "SF.Tree": { + "page": "/ru/components/navigation/tree/#javascript", + "label": "Дерево" + }, + "SF.Verification": { + "page": "/ru/components/forms/verification/#javascript", + "label": "Код подтверждения" + }, + "SF.CompositionOverlay": { + "page": "/ru/smart/editor/composition-overlay/#методы", + "label": "Оверлей редактора" + }, + "SF.InlineEditor": { + "page": "/ru/smart/editor/inline-editor/#методы", + "label": "Inline-редактор" + }, + "SF.Resizer": { + "page": "/ru/smart/overlays/resizer/#методы", + "label": "Изменение ширины" + }, + "SF.Sortable": { + "page": "/ru/smart/editor/sortable/#методы", + "label": "Перетаскивание" + }, + "SF.fieldRenderers": { + "page": "/ru/guide/smart-components/field-renderers/", + "label": "Поля по типу свойства" + }, + "SF.ThemePalette": { + "page": "/ru/smart/forms/theme-generator/", + "label": "Генератор темы" + } + } +} diff --git a/content/ru/guide/connection/js-api.md b/content/ru/guide/connection/js-api.md index 69656cd8..84baa088 100644 --- a/content/ru/guide/connection/js-api.md +++ b/content/ru/guide/connection/js-api.md @@ -1,214 +1,138 @@ --- title: "JavaScript API" -description: "Какие объекты и функции Framework кладёт в window.SF, какие из них публичные и когда каждый появляется на странице." +description: "Оглавление window.SF: публичные функции и объекты Framework по уровням, со ссылкой на страницу каждого." --- # JavaScript API -Framework кладёт свои объекты и функции в `window.SF`. Часть из них — -публичный API, на который страница может опираться; остальное — внутреннее -устройство среды выполнения. Список имён, их статус, вид и то, что их -приносит, выпуск объявляет в контракте `core/contracts/public-js-api.json`; -раздел ниже собран из него. - -Когда имя доступно, зависит от того, что его приносит: - -- **ядро** — сразу после выполнения `core.js`; -- **Loader** — после события `sf-loader-init` (см. - [События загрузчика](/ru/guide/connection/loader/#события-загрузчика)); -- **основа смарт-компонентов** — с первым смарт-элементом на странице или - после `await SF.loadSmartBase()`; -- **компонент** — только когда Loader загрузил этот компонент по разметке - страницы: `SF.Input` есть на странице с полем `sf-input`, `SF.ThemePalette` — - на странице с ``. - -Внутренние имена среды выполнения — шина сообщений, состояние реестра -модулей, объявление основы смарт-компонентов и `litHtml` — лежат в -`SF.__internal`. Прежние имена прямо на `SF` (`SF.send`, `SF.receive`, -`SF.runtimeModules`, `SF.runtimeArrived`, `SF.runtimeLoading`, -`SF.runtimeSettled`, `SF.smartBaseAnnounced`, `SF.litHtml`) читают и пишут -туда же и отмечены устаревшими до 2026-12-01 (запись журнала -`global-sf-internal`, вид `global`). `SF.loadSmartBase`, `SF.smart`, -`SF.smartBaseReady`, `SF.loadRuntime` и `SF.runtimeModule` — публичные. - -`SF.Loader.theme` и `SF.Loader.themePreference` — значения состояния, а не -функции: тема на странице и выбор человека; первое значение появляется, когда -Loader выбрал тему при запуске. - -## Глобальные объекты - -Раздел собран из контракта выпуска `ui-b4a77681f0a7` (`core/contracts/public-js-api.json`, формат `simai.framework.public-js-api`) и обновляется вместе с выпуском. - -Публичные имена (75) сохраняют смысл; переименовать или убрать такое имя можно только через журнал изменений, как атрибут. Когда имя появляется на странице, говорит столбец «Откуда»: имя компонента приходит вместе с его кодом. - -| Имя | Вид | Откуда | Что это | Члены | -|:---|:---|:---|:---|:---| -| `SF.AdminMenu` | объект | компонент `admin-menu` | Скриптовый API компонента AdminMenu; приходит вместе с компонентом (`component/admin-menu`), его методы описаны на странице компонента. | — | -| `SF.aliases` | объект | ядро, сразу после `core.js` | Псевдонимы элемента с датой: `install(element, table)` сохраняет работающими прежние имена событий, настроек, атрибутов, действий и значений компонента до `remove_after` и отмечает каждое использование в `SF.deprecations` с его датами; `strict = true` не принимает истёкшие имена. | `version`, `strict`, `install`, `uninstall`, `dispatch`, `of`, `read`, `expired` | -| `SF.AnchoredBadge` | объект | компонент `badges` | Счётчики и точки компонента component/badges, которые ведутся по разметке:.sf-badge с data-count или data-dot рисуется, ограничивается порогом, прячется при 0 и получает имя, как только оказывается на странице. init(scope) обрабатывает поддерево сразу, update(badge) — один бейдж, state({count, dot, cap, label}, node) отвечает, что бейдж покажет и скажет, не трогая страницу. | `init`, `update`, `state` | -| `SF.Assembly` | объект | ядро, сразу после `core.js` | Среда выполнения сборок стандарта simai.smart-component 1.3.8 (решение владельца 2026-10-27): `define(manifest, {layouts, lang})` определяет тег сборки только по её манифесту; её детали — любые пользовательские элементы с объявленным интерфейсом (`interfaces.register`). Журнал записывает каждый шаг в режиме разработки (`?sf-debug`). Пока нет: отменяемых шагов и слотов в среде выполнения. | `define`, `defined`, `interfaces`, `journal`, `debug`, `activity`, `whenIdle`, `instance`, `instances`, `PART_LINK`, `adaptElement`, `createLink`, `compile`, `AssemblyCore`, `headlessView` | -| `SF.breakpoints` | объект | ядро, сразу после `core.js` | Шкала контрольных точек SIMAI Framework для скриптов, те же шаги, на которых переключаются таблицы стилей (xs 0, sm 32rem, md 44rem, lg 60rem, xl 72rem, xxl 80rem): `ladder`; `query(name, {below})` — медиазапрос; `media(name, {below})` — его MediaQueryList; `matches(name, {below})` — такой ли ширины страница сейчас. docs/developer/format.md. | `ladder`, `query`, `media`, `matches` | -| `SF.ButtonGroup` | объект | компонент `button-group` | Скриптовый API компонента ButtonGroup; приходит вместе с компонентом (`component/button-group`), его методы описаны на странице компонента. | — | -| `SF.Carousel` | объект | компонент `carousel` | Скриптовый API компонента Carousel; приходит вместе с компонентом (`component/carousel`), его методы описаны на странице компонента. | — | -| `SF.Checkbox` | объект | компонент `checkbox` | Скриптовый API компонента Checkbox; приходит вместе с компонентом (`component/checkbox`), его методы описаны на странице компонента. | — | -| `SF.Clipboard` | объект | компонент `clipboard` | Скриптовый API компонента Clipboard; приходит вместе с компонентом (`component/clipboard`), его методы описаны на странице компонента. | — | -| `SF.codeCopy` | объект | ядро, сразу после `core.js` | Код в тексте копируется по щелчку, если у контейнера есть `data-sf-code-copy`. `refresh(root)` заново находит такой код внутри `root`, `copy(code)` копирует текст одного кода, `isCopyable(code)` отвечает, копируется ли он. | `refresh`, `copy`, `isCopyable` | -| `SF.Composition` | объект | ядро, сразу после `core.js` | Помощники композиции: описания фрагментов, выходы страницы, реестры типов компонентов и портов, виды полей и слова встроенных манифестов редактора (`editorFieldMessages`; владелец `sf-composition` в `SF.language`); с `core.runtime` 1.23.0 — контракты единого редактора блоков: каталог и варианты, параметры раскладки как утилиты, собственные модификаторы, блоки сайта, именованные места и порт источника данных (docs/developer/block-author-guide.md); с `core.runtime` 1.26.0 — группы каталога с их значками и словами (`CATALOGUE_GROUP_TABLE`; «Элементы» и «Раскладка» заменяют «Структуру», псевдоним с датой до 2027-01-01), значок каждого встроенного типа и значения параметров, которые тип выводит без настройки (`effectiveProps`); с `core.runtime` 1.27.0 — тип в нескольких разделах каталога (`catalogue.also_in`, `BUILTIN_CATALOGUE_ALSO_IN`, `entryGroups`; `catalogue()` отдаёт разделы), встроенные типы форм и акцентный фон. | `checkFragmentDescriptor`, `publishEffects`, `checkPortValue`, `createPortRegistry`, `defineCompositionScope`, `createRegistry`, `extendRegistry`, `describeRegions`, `resolveFieldKind`, `validateFieldValue`, `applyFieldValue`, `parseFieldSubmission`, `editorFieldMessages`, `catalogue`, `catalogueEntry`, `instantiateVariant`, `slotAccepts`, `slotPlaces`, `utilityClasses`, `checkModifiers`, `defineComposedType`, `composedEditorFields`, `composedInstance`, `expandComposed`, `upgradeComposed`, `validateTypeManifest`, `createSourcePort`, `checkSourceAnswer`, `bindingSettings`, `demoData`, `PARAMETER_SETS`, `expandParameterSets`, `parameterSetFields`, `withParameterSetFields`, `utilityPartClasses`, `PRINTED_ELEMENTS`, `printElement`, `checkPrintedElements`, `elementInputs`, `CATALOGUE_GROUP_TABLE`, `BUILTIN_CATALOGUE_ICONS`, `CATALOGUE_GROUP_ALIASES`, `effectiveProps`, `BUILTIN_CATALOGUE_ALSO_IN`, `entryGroups` | -| `SF.CompositionOverlay` | объект | смарт-компонент `sf-composition-overlay` | Скриптовый API компонента CompositionOverlay; приходит вместе с компонентом (`smart/composition-overlay`), его методы описаны на странице компонента. | — | -| `SF.copyText` | функция | ядро, сразу после `core.js` | Копирует текст: `SF.copyText(text, {document})` завершается значением `'native'` (Clipboard API) или `'legacy'` (execCommand через скрытое поле, где API нет или он отказал) либо отклоняется (`copy_unsupported` или `NotAllowedError` платформы). Так копируют `sf-copy-button` и класс clipboard. docs/developer/format.md. | — | -| `SF.CountryCode` | объект | компонент `country-code` | Скриптовый API компонента CountryCode; приходит вместе с компонентом (`component/country-code`), его методы описаны на странице компонента. | — | -| `SF.create` | функция | Loader, после `sf-loader-init` | Создаёт элемент компонента по имени с атрибутами. | — | -| `SF.createAsync` | функция | Loader, после `sf-loader-init` | `SF.create`, который ждёт определения элемента. | — | -| `SF.deprecations` | объект | ядро, сразу после `core.js` | Прежние имена, которые ещё принимаются, и что их заменило — как их объявляет журнал изменений. | — | -| `SF.DownloadFile` | объект | компонент `download-file` | Скриптовый API компонента DownloadFile; приходит вместе с компонентом (`component/download-file`), его методы описаны на странице компонента. | — | -| `SF.EarlyPort` | объект | ядро, сразу после `core.js` | Порты, объявленные в элементах (JSON-остров ` +``` + +## Особые случаи + +- Набор не объявлен на странице — обещание отклоняется с ошибкой, которая + называет строку, которую нужно добавить: `data-sf-sets="<набор>"` у + скрипта ядра или `SF_BOOT_CONFIG.packages.<набор> = true`. +- Набор объявлен, но не отдал функции за 10 секунд — обещание отклоняется с + ошибкой `gave no exports within 10000 ms`. + +## См. также + +- [Подключение набора](/ru/guide/sets/connect/) — объявление, версии и адреса наборов. +- [Наборы компонентов](/ru/guide/sets/) — какие наборы есть и где их документация. diff --git a/content/ru/guide/functions/loading/package.page.json b/content/ru/guide/functions/loading/package.page.json new file mode 100644 index 00000000..a4195afd --- /dev/null +++ b/content/ru/guide/functions/loading/package.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 70}} diff --git a/content/ru/guide/functions/loading/ready.md b/content/ru/guide/functions/loading/ready.md new file mode 100644 index 00000000..f81611c2 --- /dev/null +++ b/content/ru/guide/functions/loading/ready.md @@ -0,0 +1,62 @@ +--- +title: "Готовность Loader" +description: "SF.ready — обещание запуска Loader и вызов функции, когда Loader и документ готовы." +--- + +# Готовность Loader + +`SF.ready` говорит скрипту страницы, что Loader запустился и всё, что он +приносит, уже на месте: `SF.Loader`, `SF.create`, `SF.whenDefined`, +`SF.loadSmartBase` и остальные имена уровня Loader. Его можно ждать как +обещание или передать ему функцию. Скрипту не нужно знать, запустился ли +Loader до него или после: ответ один и тот же. + +## Вызов + +```js +const SF = await window.SF.ready; // window.SF с запущенным Loader +window.SF.ready((SF) => { /* … */ }); // вызвать функцию, когда всё готово +``` + +| Форма | Что делает | +|:---|:---| +| `await SF.ready`, `SF.ready.then(fn)` | Выполняется со значением `window.SF`, когда Loader отправил `sf-loader-init`. Обещание никогда не отклоняется. | +| `SF.ready(fn)` | Вызывает `fn(window.SF)` один раз, когда Loader запущен и документ разобран; если оба условия уже выполнены — сразу, в том же вызове. | +| `window.SF_ON_READY.push(fn)` | Для скрипта, который стоит выше `core.js`: функция ждёт в очереди и вызывается в тот же момент, что и `SF.ready`. | + +## Когда доступно + +Ядро: `SF.ready` есть в `window.SF` сразу после выполнения `core.js`, до +следующего скрипта страницы. Он один на страницу: второй `core.js` или +сборка компонента его не заменяют. + +## Пример + +Пример спрашивает готовность при загрузке и ещё раз по кнопке. Второй ответ +приходит сразу: Loader уже запущен. + +:::example {id="guide/functions/ready" label="Готовность Loader" preview=sandbox log_console=true} +::: + +Скрипт, который подключается выше `core.js`, ставит функцию в очередь: + +```html + + +``` + +## Особые случаи + +- Обработчик события `sf-loader-init`, добавленный после события, не + сработает никогда. `SF.ready` такой ошибки не допускает: ждите его, а не + событие. +- `SF.ready` не ждёт компоненты и основу смарт-компонентов. Элемент + дожидаются через [Ожидание элемента](/ru/guide/functions/loading/when-defined/), + основу — через [`SF.loadSmartBase()`](/ru/guide/functions/loading/smart-base/). + +## См. также + +- [Как работает Loader](/ru/guide/connection/loader/#события-загрузчика) — события загрузчика и порядок, в котором появляются имена. +- [Загрузка по требованию](/ru/guide/functions/loading/loader/) — что умеет `SF.Loader`. diff --git a/content/ru/guide/functions/loading/ready.page.json b/content/ru/guide/functions/loading/ready.page.json new file mode 100644 index 00000000..cc969543 --- /dev/null +++ b/content/ru/guide/functions/loading/ready.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 10}} diff --git a/content/ru/guide/functions/loading/section.json b/content/ru/guide/functions/loading/section.json new file mode 100644 index 00000000..462c3819 --- /dev/null +++ b/content/ru/guide/functions/loading/section.json @@ -0,0 +1 @@ +{"schema": "docara.section.v1", "title": "Загрузка и готовность", "navigation": {"order": 10}} diff --git a/content/ru/guide/functions/loading/smart-base.md b/content/ru/guide/functions/loading/smart-base.md new file mode 100644 index 00000000..e91135bc --- /dev/null +++ b/content/ru/guide/functions/loading/smart-base.md @@ -0,0 +1,53 @@ +--- +title: "Загрузка основы смарт-компонентов" +description: "SF.loadSmartBase и SF.smartBaseReady — получить основу смарт-компонентов на странице, где ещё нет ни одного смарт-элемента." +--- + +# Загрузка основы смарт-компонентов + +Все смарт-компоненты страницы работают на одной общей основе: в ней базовый +класс, шаблоны и язык страницы. Обычно её приносит первый смарт-элемент. +Скрипту, которому основа нужна раньше — для своего смарт-компонента +проекта, — `SF.loadSmartBase()` загружает её сама, а `SF.smartBaseReady` +отвечает, когда она пришла, каким бы путём это ни случилось. + +## Вызов + +```js +const smart = await SF.loadSmartBase(); // то же, что SF.smart +const same = await SF.smartBaseReady; // основа, как бы она ни пришла +``` + +| Имя | Что делает | +|:---|:---| +| `SF.loadSmartBase()` | Загружает основу, если её ещё нет, и выполняется с `SF.smart`. Повторный вызов ничего не загружает заново. | +| `SF.smartBaseReady` | Обещание основы: выполняется с `SF.smart`, когда основу принёс смарт-элемент или `SF.loadSmartBase()`. Само ничего не загружает. | + +## Когда доступно + +Loader: обе функции появляются после события `sf-loader-init`, сама основа — +после вызова. Дождитесь Loader через [`await SF.ready`](/ru/guide/functions/loading/ready/). + +## Пример + +Кнопка загружает основу и показывает, что в ней есть. Оба вызова +выполняются с одним и тем же объектом `SF.smart`. + +:::example {id="guide/functions/smart-base" label="Загрузка основы" preview=sandbox log_console=true} +::: + +## Особые случаи + +- На странице без смарт-элементов основа сама не загружается: без вызова + `SF.loadSmartBase()` обещание `SF.smartBaseReady` ждёт. +- Если основа не загрузилась, `SF.loadSmartBase()` не отклоняется: оно + выполняется с `null`, а в консоли появляется предупреждение + `smart base load failed`. Проверяйте результат перед `extends`. +- Когда основа пришла, на `window` приходит событие `sf-smart-base-ready` — + один раз на страницу. Обещание надёжнее: оно выполняется и для скрипта, + который спросил позже. + +## См. также + +- [Свой смарт-компонент](/ru/guide/functions/loading/smart/) — что лежит в `SF.smart` и как написать компонент. +- [Загрузка и общая среда](/ru/guide/smart-components/runtime/) — как основа делится между компонентами страницы. diff --git a/content/ru/guide/functions/loading/smart-base.page.json b/content/ru/guide/functions/loading/smart-base.page.json new file mode 100644 index 00000000..3410c46f --- /dev/null +++ b/content/ru/guide/functions/loading/smart-base.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 50}} diff --git a/content/ru/guide/functions/loading/smart.md b/content/ru/guide/functions/loading/smart.md new file mode 100644 index 00000000..0694a731 --- /dev/null +++ b/content/ru/guide/functions/loading/smart.md @@ -0,0 +1,60 @@ +--- +title: "Свой смарт-компонент" +description: "SF.smart, SF.SfBaseElement, SF.html, SF.render и SF.nothing — основа, на которой проект пишет свой смарт-компонент." +--- + +# Свой смарт-компонент + +Проект может написать свой смарт-компонент на той же основе, что и +компоненты Framework: он получит те же настройки из атрибутов, язык страницы +и отрисовку шаблоном. `SF.smart` отдаёт базовый класс и помощники; тот же +класс и функции шаблона лежат и прямо на `SF`. + +## Вызов + +```js +await SF.loadSmartBase(); +const {SfBaseElement, html} = SF.smart; + +class ProjectGreeting extends SfBaseElement { + static get props() { return {name: {type: String, default: 'гость'}}; } + template() { return html`

Здравствуйте, ${this.getProp('name')}!

`; } +} +ProjectGreeting.define('project-greeting'); +``` + +| Имя | Что это | +|:---|:---| +| `SF.smart.SfBaseElement`, `SF.SfBaseElement` | Базовый класс смарт-элемента. Наследник объявляет настройки в `static get props()`, рисует в `template()` и регистрирует тег через `define(tag)`. | +| `SF.smart.html`, `SF.html` | Тег шаблона, которым рисуют смарт-элементы (Lit). | +| `SF.smart.render`, `SF.render` | Рисует шаблон в узел: `render(шаблон, узел)`. | +| `SF.smart.nothing`, `SF.nothing` | Пустое место в шаблоне: ничего не рисует. | +| `toBoolean`, `toNumber`, `normalizeEnum`, `parseJsonAttribute`, `toAttributeName` | Чтение значений атрибутов: да или нет, число, одно из допустимых значений, JSON, имя атрибута из имени свойства. | + +## Когда доступно + +Основа смарт-компонентов: после первого смарт-элемента на странице или после +[`await SF.loadSmartBase()`](/ru/guide/functions/loading/smart-base/). На +странице без смарт-элементов и без этого вызова `SF.smart` нет. + +## Пример + +Компонент проекта `project-greeting` берёт имя из атрибута и рисует +приветствие. Кнопка меняет атрибут — компонент рисуется заново, журнал +показывает новое имя. + +:::example {id="guide/functions/smart" label="Свой смарт-компонент" preview=sandbox log_console=true} +::: + +## Особые случаи + +- Тег проекта начинается со своего префикса, а не с `sf-`: этот префикс + носят компоненты Framework. +- Loader не загружает файлы проекта: скрипт компонента проект подключает + сам. Чтобы Loader загружал компонент по тегу, проект оформляет его пакетом + (см. [Пакеты компонентов](/ru/guide/connection/loader/#пакеты-компонентов)). + +## См. также + +- [Модель компонента](/ru/guide/smart-components/model/) — из чего состоит смарт-компонент. +- [Подключение к проекту](/ru/guide/connection/project-setup/) — где граница между Framework и компонентами проекта. diff --git a/content/ru/guide/functions/loading/smart.page.json b/content/ru/guide/functions/loading/smart.page.json new file mode 100644 index 00000000..79bcbd42 --- /dev/null +++ b/content/ru/guide/functions/loading/smart.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 60}} diff --git a/content/ru/guide/functions/loading/when-defined.md b/content/ru/guide/functions/loading/when-defined.md new file mode 100644 index 00000000..1353eeb4 --- /dev/null +++ b/content/ru/guide/functions/loading/when-defined.md @@ -0,0 +1,60 @@ +--- +title: "Ожидание элемента" +description: "SF.whenDefined — обещание, что смарт-элемент на странице определён и с ним можно работать из скрипта." +--- + +# Ожидание элемента + +Смарт-элемент в разметке становится рабочим не сразу: Loader сначала +загружает его код. `SF.whenDefined` ждёт этого момента, после него у +элемента есть свойства и методы. Функция принимает короткие имена +Framework и полные имена элементов пакетов и ждёт сразу несколько тегов. + +## Вызов + +```js +const [tag] = await SF.whenDefined('datepicker'); // ['sf-datepicker'] +const tags = await SF.whenDefined(['button', 'dropdown']); // ['sf-button', 'sf-dropdown'] +``` + +| Аргумент | Что принимает | +|:---|:---| +| теги | Строка, массив строк или несколько строк подряд. Короткое имя (`datepicker`) означает элемент Framework (`sf-datepicker`); полное имя элемента пакета (`shop-catalog`) остаётся как есть, если его называет правило пакета. | + +Обещание выполняется с массивом полных имён тегов в том же порядке — даже +для одного тега. Без аргументов — сразу с пустым массивом. + +## Когда доступно + +Loader: после события `sf-loader-init`, вместе с `SF.Loader`. Дождитесь его +через [`await SF.ready`](/ru/guide/functions/loading/ready/). + +## Пример + +Карточка показателя стоит в разметке: обещание выполняется, как только Loader +её загрузил, и пример читает значение элемента. Вторая кнопка ждёт тег, +которого на странице нет: через три секунды журнал показывает, что ответа +всё ещё нет. + +:::example {id="guide/functions/when-defined" label="Ожидание элемента" preview=sandbox log_console=true} +::: + +## Особые случаи + +- `SF.whenDefined` ничего не загружает. Обещание для тега, которого нет в + разметке и который никто не загрузил, не выполнится никогда. Для такого + элемента берите [`SF.Loader.ensure()`](/ru/guide/functions/loading/loader/) + или [`SF.createAsync()`](/ru/guide/functions/loading/create/). +- Определён — ещё не нарисован. Чтобы читать размеры или содержимое, после + определения дождитесь `await element.whenRendered()` (см. [Ожидание + готовности](/ru/guide/smart-components/page/#ожидание-готовности)). +- Тег пакета, чьи правила ещё не пришли, ждётся под обоими именами + (`shop-catalog` и `sf-shop-catalog`): выполняется первое определённое. +- Свой тег проекта без правила пакета (`project-greeting`) ждут через + `customElements.whenDefined('project-greeting')`: `SF.whenDefined` + прочтёт имя без правила как имя Framework. + +## См. также + +- [Как работает Loader](/ru/guide/connection/loader/#события-загрузчика) — когда какое имя появляется. +- [На странице](/ru/guide/smart-components/page/) — свойства, методы и события смарт-элемента. diff --git a/content/ru/guide/functions/loading/when-defined.page.json b/content/ru/guide/functions/loading/when-defined.page.json new file mode 100644 index 00000000..4e0ddcc5 --- /dev/null +++ b/content/ru/guide/functions/loading/when-defined.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 30}} diff --git a/content/ru/guide/functions/person/icons.md b/content/ru/guide/functions/person/icons.md new file mode 100644 index 00000000..fcf590ba --- /dev/null +++ b/content/ru/guide/functions/person/icons.md @@ -0,0 +1,47 @@ +--- +title: "Значки на выбор" +description: "SF.icons.choices — общий список из 120 значков, из которых человек выбирает значок раздела, пункта меню или плитки." +--- + +# Значки на выбор + +Когда человек сам отмечает значком раздел записи, пункт меню или плитку +панели, ему нужен понятный выбор, а не весь шрифт иконок. `SF.icons.choices` +— общий для всех наборов список из ста двадцати значков, сгруппированных по +смыслу: что это, чьё, когда, сколько стоит, из чего состоит. Выбор рисуют +выпадающим списком в виде сетки значков. + +## Вызов + +```js +SF.icons.choices; // ['folder', 'folder_open', 'topic', 'home', 'dashboard', …] +SF.icons.choices.length; // 120 +``` + +| Член | Что это | +|:---|:---| +| `choices` | Неизменяемый массив имён значков Material Symbols. | + +## Когда доступно + +Ядро: сразу после выполнения `core.js`, ждать не нужно. + +## Пример + +Выпадающий список в виде сетки собран из `SF.icons.choices`: у каждого +пункта вид `icon`. В журнале — выбранный значок. + +:::example {id="guide/functions/icons" label="Значки на выбор" preview=sandbox log_console=true} +::: + +## Особые случаи + +- Список заморожен: его нельзя изменить. Своему выбору возьмите копию — + `[...SF.icons.choices, 'rocket']`. +- В списке только имена; подписи пунктов для экранного диктора даёт сама + страница. + +## См. также + +- [Выпадающий список](/ru/smart/navigation/dropdown/) — вид сетки и пункты со значком. +- [Иконки](/ru/components/content/icons/) — шрифт значков и как его загружает Loader. diff --git a/content/ru/guide/functions/person/icons.page.json b/content/ru/guide/functions/person/icons.page.json new file mode 100644 index 00000000..12cb21b4 --- /dev/null +++ b/content/ru/guide/functions/person/icons.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 20}} diff --git a/content/ru/guide/functions/person/index.md b/content/ru/guide/functions/person/index.md new file mode 100644 index 00000000..558858d6 --- /dev/null +++ b/content/ru/guide/functions/person/index.md @@ -0,0 +1,12 @@ +--- +title: "Выбор человека" +description: "Как запомнить вид, который выбрал человек, и дать ему выбрать значок." +--- + +# Выбор человека + +Функции для того, что человек настраивает под себя: вид таблицы или списка, +который нужно запомнить, и значок, который он выбирает для раздела. + +- [Настройки вида](/ru/guide/functions/person/preferences/) — запомнить порядок, ширину и сортировку в браузере или у хоста. +- [Значки на выбор](/ru/guide/functions/person/icons/) — общий список значков для выбора. diff --git a/content/ru/guide/functions/person/index.page.json b/content/ru/guide/functions/person/index.page.json new file mode 100644 index 00000000..3410c46f --- /dev/null +++ b/content/ru/guide/functions/person/index.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 50}} diff --git a/content/ru/guide/functions/person/preferences.md b/content/ru/guide/functions/person/preferences.md new file mode 100644 index 00000000..3ad7458e --- /dev/null +++ b/content/ru/guide/functions/person/preferences.md @@ -0,0 +1,71 @@ +--- +title: "Настройки вида" +description: "SF.Preferences — запомнить вид, который выбрал человек (порядок и ширину колонок, сортировку, плотность), в браузере и у хоста." +--- + +# Настройки вида + +Человек меняет под себя порядок колонок, их ширину, сортировку, плотность +списка — и ждёт увидеть то же при следующем открытии. `SF.Preferences` +хранит такой выбор одинаково для всех компонентов: значение под ключом +читается сразу, до первой отрисовки, лежит в браузере и, если хост (например, +Larena) хранит настройки пользователя, уходит и к нему. + +## Вызов + +```js +const key = 'sf-table/orders'; +const view = SF.Preferences.read(key); // значение или null +SF.Preferences.write(key, {density: 'compact'}); +const stop = SF.Preferences.subscribe(key, (value) => apply(value)); +SF.Preferences.remove(key); // «Сбросить вид» +``` + +| Член | Что делает | +|:---|:---| +| `read(key)` | Значение ключа: данные хоста, если он их дал, иначе копия браузера; `null`, если ничего нет. Читается сразу, без ожидания. | +| `readMerged(...keys)` | Несколько ключей от общего к частному, слитые по полям: `readMerged('sf-table/*', 'sf-table/orders')`. | +| `write(key, value)` | Сохраняет значение — простой JSON — в браузере и, если есть хост, отправляет ему через 400 мс после последней записи. То же значение второй раз не пишется. Возвращает `false` для неверного ключа. | +| `remove(key)` | Убирает значение у браузера и у хоста и сообщает подписчикам `null`. | +| `subscribe(key, listener)` | Вызывает `listener(value, key)`, когда значение пришло от хоста, изменилось в другой вкладке или убрано. Возвращает функцию отписки. | +| `preload(values)` | Значения хоста для этого человека: `{ключ: значение}`. То, что отличается от копии браузера, заменяет её и сообщается подписчикам. | +| `useHost(adapter)` | Подключает хост: `{save(key, value), remove(key), load?(keys)}`. | +| `load(keys)` | Просит у хоста ключи, которых он ещё не дал. | +| `hosted` | Подключён ли хост. | +| `flush()` | Отправляет хосту ожидающие записи сразу; при уходе со страницы это делается само. | + +Ключ — `<компонент>/<экземпляр>`, например `sf-table/orders`: буквы, цифры, +`_ . * : -`, до 200 знаков. + +## Когда доступно + +Ядро: сразу после выполнения `core.js`, ждать не нужно. + +## Пример + +Хост примера хранит настройки в памяти, отдаёт их через `preload` и пишет в +журнал всё, что получил. Кнопки плотности записывают выбор — через 400 мс хост получает значение; +«Сбросить» убирает его. + +:::example {id="guide/functions/preferences" label="Настройки вида" preview=sandbox log_console=true} +::: + +Значения хоста можно отдать прямо в разметке страницы, до первого чтения: + +```html + +``` + +## Особые случаи + +- Ключ не задан или неверный — ничего не хранится: `read` отвечает `null`, + `write` — `false`. +- Своя запись подписчиков не вызывает. +- Где браузерное хранилище недоступно (изолированный фрейм, запрет + настроек), `read` отвечает только тем, что дал хост: значение ключа, + которого хост не давал, после `write` не читается. + +## См. также + +- [Персональный вид](/ru/guide/smart-components/view-preferences/) — какие компоненты что запоминают. +- [Правила взаимодействия](/ru/guide/standards/interaction-rules/) — где вид человека хранится и как сбрасывается. diff --git a/content/ru/guide/functions/person/preferences.page.json b/content/ru/guide/functions/person/preferences.page.json new file mode 100644 index 00000000..cc969543 --- /dev/null +++ b/content/ru/guide/functions/person/preferences.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 10}} diff --git a/content/ru/guide/functions/person/section.json b/content/ru/guide/functions/person/section.json new file mode 100644 index 00000000..845d6da7 --- /dev/null +++ b/content/ru/guide/functions/person/section.json @@ -0,0 +1 @@ +{"schema": "docara.section.v1", "title": "Выбор человека", "navigation": {"order": 50}} diff --git a/content/ru/guide/functions/screen/breakpoints.md b/content/ru/guide/functions/screen/breakpoints.md new file mode 100644 index 00000000..82aa4af5 --- /dev/null +++ b/content/ru/guide/functions/screen/breakpoints.md @@ -0,0 +1,52 @@ +--- +title: "Контрольные точки" +description: "SF.breakpoints — шкала ширины SIMAI Framework в скрипте: медиазапрос, его MediaQueryList и проверка ширины страницы." +--- + +# Контрольные точки + +Стили Framework переключаются на шагах ширины `sm`, `md`, `lg`, `xl` и +`xxl`. `SF.breakpoints` даёт те же шаги скрипту: компонент, которому нужно +знать, узкая ли страница, проверяет её по той же шкале, что и стили, и не +расходится с ними на границе. + +## Вызов + +```js +SF.breakpoints.ladder; // {xs: '0rem', sm: '32rem', md: '44rem', lg: '60rem', xl: '72rem', xxl: '80rem'} +SF.breakpoints.query('lg'); // '(min-width: 60rem)' +SF.breakpoints.query('md', {below: true}); // '(max-width: 43.9375rem)' +SF.breakpoints.matches('lg'); // широка ли страница сейчас +SF.breakpoints.media('lg')?.addEventListener('change', redraw); +``` + +| Член | Что делает | +|:---|:---| +| `ladder` | Шаги шкалы в rem, как их пишут стили. | +| `query(name, {below})` | Медиазапрос шага: от этой ширины и шире, а с `below: true` — уже этой ширины, как `breakpoint-max` в стилях. | +| `media(name, {below, view})` | `MediaQueryList` того же запроса: на него подписываются, чтобы узнать о смене ширины. `view` — окно, по которому мерить (по умолчанию текущее). | +| `matches(name, {below})` | `true`, если страница сейчас такой ширины. | + +## Когда доступно + +Ядро: сразу после выполнения `core.js`, ждать не нужно. + +## Пример + +Пример показывает, какие шаги выполняются сейчас, и обновляется при смене +ширины. Откройте пример на весь экран и переключите ширину телефона, +планшета и компьютера: журнал покажет каждую смену. + +:::example {id="guide/functions/breakpoints" label="Контрольные точки" preview=sandbox log_console=true} +::: + +## Особые случаи + +- Неизвестное имя шага даёт `null` у `query` и `media` и `false` у + `matches`: запроса, который выполняется всегда, нет. +- Ширина считается по окну документа: в примере — по ширине его рамки. + +## См. также + +- [Адаптивные размеры](/ru/guide/size-and-type/adaptive-sizing/) — как стили меняются по ширине. +- [Утилиты и раскладка](/ru/guide/utilities/) — префиксы шагов у классов. diff --git a/content/ru/guide/functions/screen/breakpoints.page.json b/content/ru/guide/functions/screen/breakpoints.page.json new file mode 100644 index 00000000..cc969543 --- /dev/null +++ b/content/ru/guide/functions/screen/breakpoints.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 10}} diff --git a/content/ru/guide/functions/screen/index.md b/content/ru/guide/functions/screen/index.md new file mode 100644 index 00000000..33d966e5 --- /dev/null +++ b/content/ru/guide/functions/screen/index.md @@ -0,0 +1,13 @@ +--- +title: "Экран и прокрутка" +description: "Контрольные точки ширины в скрипте, место читателя в прокручиваемой колонке и панель у якоря." +--- + +# Экран и прокрутка + +Функции для того, что зависит от размера окна и прокрутки: ширина страницы, +место читателя в длинной колонке и панель, которая держится у своей кнопки. + +- [Контрольные точки](/ru/guide/functions/screen/breakpoints/) — те же шаги ширины, что у стилей, в скрипте. +- [Положение чтения](/ru/guide/functions/screen/reading-position/) — лента и журнал не сдвигают то, что человек читает. +- [Привязка к якорю](/ru/guide/functions/screen/position/) — своя всплывающая панель у кнопки или поля. diff --git a/content/ru/guide/functions/screen/index.page.json b/content/ru/guide/functions/screen/index.page.json new file mode 100644 index 00000000..23d71cd6 --- /dev/null +++ b/content/ru/guide/functions/screen/index.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 40}} diff --git a/content/ru/guide/functions/screen/position.md b/content/ru/guide/functions/screen/position.md new file mode 100644 index 00000000..51056fe6 --- /dev/null +++ b/content/ru/guide/functions/screen/position.md @@ -0,0 +1,72 @@ +--- +title: "Привязка к якорю" +description: "SF.Position — своя всплывающая панель у кнопки или поля: сторона и выравнивание, переворот у края окна и сдвиг внутрь." +--- + +# Привязка к якорю + +Выпадающие списки, меню, панели дат и подсказки Framework открываются у +своей кнопки одинаково: с нужной стороны, переворачиваются у края окна, +сдвигаются внутрь и не вылезают за экран. `SF.Position` даёт то же правило +своей панели проекта: панель встаёт у якоря и следует за ним при прокрутке и +изменении размера. + +## Вызов + +```js +const anchored = SF.Position.anchor(button, panel, {side: 'block-end', align: 'start'}); +await anchored.update(); // пересчитать сейчас +anchored.side; // сторона, с которой панель встала +anchored.stop(); // отпустить панель +``` + +| Настройка | Что делает | +|:---|:---| +| `side` | Сторона якоря: `block-end` (снизу, по умолчанию), `block-start`, `inline-start`, `inline-end`. В письме справа налево стороны отражаются. | +| `align` | Выравнивание вдоль якоря: `start` (по умолчанию), `center`, `end`. | +| `offset` | Расстояние до якоря: число пикселей или длина CSS, в том числе переменная (по умолчанию `var(--sf-space-1\/4)`). | +| `alignmentOffset` | Сдвиг вдоль края якоря. | +| `padding` | Отступ от краёв окна (по умолчанию `var(--sf-space-1\/2)`). | +| `flip`, `shift` | Перевернуть на другую сторону, если не помещается, и сдвинуть внутрь окна. Оба включены. | +| `matchWidth` | Ширина панели по ширине якоря. | +| `fitHeight` | Ограничить высоту местом, которое осталось в окне (включено). | +| `autoUpdate` | Следить за прокруткой и размерами (включено). | +| `topLayer` | Поднять панель в верхний слой страницы, пока она привязана (включено). | +| `onPosition({side, placement, x, y})` | Вызывается после каждого расчёта. | + +`anchor` возвращает `{update(), stop(), side}`. Пока панель привязана, у неё +стоит `data-sf-side` с текущей стороной. `stop()` возвращает стили, которые +написала привязка, и опускает панель из верхнего слоя. + +Другие члены: `resolveLength(value, context)` — длина CSS в пикселях; +`logicalSide` и `physicalPlacement` — перевод между логической стороной и +стороной на экране; `anchorReference`, `referenceRect` и `liftToTopLayer` — +части, из которых собран `anchor`. + +## Когда доступно + +Ядро: сразу после выполнения `core.js`, ждать не нужно. + +## Пример + +Кнопка открывает свою панель у себя. Кнопка «Сверху» привязывает её с +верхней стороны; у края рамки панель переворачивается. В журнале — сторона +после каждого расчёта. + +:::example {id="guide/functions/position" label="Привязка к якорю" preview=sandbox log_console=true} +::: + +## Особые случаи + +- Якорь без своего прямоугольника — смарт-элемент с `display: contents`, + например `sf-button`, — измеряется по тому, что он рисует. +- `anchor` бросает `TypeError`, если якорь — не элемент или панель — не + HTML-элемент. Неизвестная сторона или выравнивание отклоняют расчёт + (`update()`) с `TypeError`. +- Ограничения высоты и ширины из стилей панели сохраняются: привязка может + только уменьшить их под окно. + +## См. также + +- [Окна и панели](/ru/guide/smart-components/layers/) — верхний слой и порядок панелей. +- [Всплывающая панель](/ru/smart/overlays/popover/) — готовая панель у якоря. diff --git a/content/ru/guide/functions/screen/position.page.json b/content/ru/guide/functions/screen/position.page.json new file mode 100644 index 00000000..4e0ddcc5 --- /dev/null +++ b/content/ru/guide/functions/screen/position.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 30}} diff --git a/content/ru/guide/functions/screen/reading-position.md b/content/ru/guide/functions/screen/reading-position.md new file mode 100644 index 00000000..1a1ba55b --- /dev/null +++ b/content/ru/guide/functions/screen/reading-position.md @@ -0,0 +1,71 @@ +--- +title: "Положение чтения" +description: "SF.readingPosition — лента, журнал или переписка сохраняет место читателя, пока содержимое меняется, и говорит, когда край близко." +--- + +# Положение чтения + +В ленте, журнале или переписке содержимое меняется под читателем: сверху +подгружается история, снизу приходят новые записи. `SF.readingPosition` +держит место человека: то, что он читает, не сдвигается, за новой записью +колонка идёт, только если он был внизу, а близкий край вызывает загрузку +ещё одной порции. На этом правиле построены [Лента](/ru/smart/content/list-feed/) +и [Список](/ru/smart/content/list/). + +## Вызов + +```js +const {create, watchEdge, scrollerOf} = SF.readingPosition; + +const position = create(scroller, {onStart: () => loadEarlier()}); +scroller.addEventListener('scroll', () => position.check()); + +position.keep(() => column.prepend(earlier)); // история сверху, читаемое на месте +const follow = position.atEnd(); +column.append(entry); +if (follow) position.toEnd(); // за новой записью — только если был внизу +``` + +| Член | Что делает | +|:---|:---| +| `create(scroller, {nearEnd, nearStart, onStart})` | Контроллер одной прокручиваемой области. `nearEnd` — сколько пикселей до конца ещё считается «внизу» (96), `nearStart` — до начала (240), `onStart` — что вызвать, когда начало близко. | +| `watchEdge(sentinel, callback, {root, margin})` | Следит за меткой у края и вызывает `callback`, когда она близко (`margin`, по умолчанию `200px`). Возвращает функцию, которая останавливает слежение. | +| `scrollerOf(node)` | Ближайший предок узла с прокруткой по вертикали; `null` — прокручивается страница. Его передают в `root`. | +| `NEAR_END`, `NEAR_START`, `EDGE_MARGIN` | Значения по умолчанию: 96, 240 и `'200px'`. | + +У контроллера: `atEnd()` и `atStart()` — близко ли конец и начало; `keep(mutate)` +— выполнить изменение, не сдвинув читаемое; `toEnd({instant, settle, then})` — +перейти в конец в следующем кадре, плавно, если не задано `instant`; +`check()` — вызывать при прокрутке; `hold()` и `release()` — придержать и +отпустить сообщения о начале; `element` — сама область, её можно заменить. + +## Когда доступно + +Ядро: сразу после выполнения `core.js`, ждать не нужно. + +## Пример + +Колонка сообщений. «Новое внизу» добавляет запись в конец: если вы +прокрутили вверх, колонка остаётся на месте. «История сверху» добавляет +записи в начало, не сдвигая то, что видно. Прокрутите к началу — журнал +покажет вызов `onStart`. + +:::example {id="guide/functions/reading-position" label="Положение чтения" preview=sandbox log_console=true} +::: + +## Особые случаи + +- Колонка, которую ещё не нарисовали, считается стоящей в конце. +- `toEnd` идёт плавно, если у человека не включено уменьшение движения + (`prefers-reduced-motion: reduce`); тогда переход мгновенный. +- `toEnd({settle: true})` после замены колонки придерживает `onStart` на + кадр: колонка, на миг оказавшаяся в начале, не просит раннюю историю. +- `check()` спрашивает `onStart` при каждом вызове у начала. Ушёл ли уже + запрос, решает вызывающий код. +- Без `IntersectionObserver` `watchEdge` ничего не делает: оставьте кнопку + «Показать ещё». + +## См. также + +- [Лента](/ru/smart/content/list-feed/) — готовая лента с подгрузкой. +- [Список](/ru/smart/content/list/) — список, который держит место при обновлении. diff --git a/content/ru/guide/functions/screen/reading-position.page.json b/content/ru/guide/functions/screen/reading-position.page.json new file mode 100644 index 00000000..12cb21b4 --- /dev/null +++ b/content/ru/guide/functions/screen/reading-position.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 20}} diff --git a/content/ru/guide/functions/screen/section.json b/content/ru/guide/functions/screen/section.json new file mode 100644 index 00000000..f7f3ac11 --- /dev/null +++ b/content/ru/guide/functions/screen/section.json @@ -0,0 +1 @@ +{"schema": "docara.section.v1", "title": "Экран и прокрутка", "navigation": {"order": 40}} diff --git a/content/ru/guide/functions/section.json b/content/ru/guide/functions/section.json new file mode 100644 index 00000000..72bf2b24 --- /dev/null +++ b/content/ru/guide/functions/section.json @@ -0,0 +1 @@ +{"schema": "docara.section.v1", "title": "Функции", "navigation": {"order": 25}} diff --git a/content/ru/guide/functions/text/format.md b/content/ru/guide/functions/text/format.md new file mode 100644 index 00000000..10862793 --- /dev/null +++ b/content/ru/guide/functions/text/format.md @@ -0,0 +1,70 @@ +--- +title: "Даты, числа и размеры" +description: "SF.format — даты, «сегодня» и «вчера», числа, суммы и размеры файлов на языке страницы через Intl." +--- + +# Даты, числа и размеры + +`SF.format` выводит дату, «вчера» или «5 минут назад», число, сумму и размер +файла так, как принято на языке страницы: на русской странице — «1 234,50 ₽» +и «3 нояб. 2026 г.», на английской — «1,234.50» и «Nov 3, 2026». Язык берётся +у узла, рядом с которым стоит значение, поэтому английский блок на русской +странице получает английские даты. Те же правила используют компоненты +Framework, и подписи на странице не расходятся. + +## Вызов + +```js +const {format} = window.SF; +format.date(at, {node}); // 3 нояб. 2026 г. +format.date(at, {node, date: 'long', time: 'short'}); // 3 ноября 2026 г. в 14:05 +format.relativeDay(at, {node}); // сегодня, вчера, пятница, 27 октября +format.relativeTime(at, {node}); // 5 минут назад +format.number(1234.5, 'currency-RUB', {node}); // 1 234,50 ₽ +format.fileSize(1468006, {node}); // 1,4 МБ +``` + +Каждая функция принимает `{locale, node}`: язык — `locale`, если он задан, +иначе язык страницы для `node` (ближайший `[lang]` выше узла). + +| Член | Что делает | +|:---|:---| +| `date(value, {date, time, intl})` | Дата и время. `date` и `time` — `'short'`, `'medium'` (дата по умолчанию), `'long'` или `null`, чтобы не показывать часть; `intl` — параметры Intl напрямую. | +| `relativeDay(value, {now, today, yesterday, tomorrow, weekday, capital})` | «Сегодня», «вчера», «завтра», день недели в пределах недели или дата. Дни считаются по календарю в часовом поясе читателя. `weekday: false` не называет день недели, `capital: true` пишет с заглавной, свои слова передают в `today`, `yesterday`, `tomorrow`. | +| `msUntilNextDay(now)` | Сколько миллисекунд до полуночи читателя: когда «сегодня» станет «вчера». | +| `relativeTime(value, {now})` | «5 минут назад», «через 2 часа». | +| `dayOffset(value, {now})` | Разница в календарных днях: `0` — сегодня, `-1` — вчера. | +| `number(value, format, {step, grouping, gap})` | Число по слову формата: `number`, `integer`, `decimal-`, `percent`, `compact`, `currency-<КОД>`, `unit-<единица>`. `grouping: false` убирает разделитель разрядов. | +| `numberOptions(format, step)` | Параметры Intl для слова формата. | +| `fileSize(size, {units})` | Размер файла в Б, КБ, МБ и ГБ; свои слова единиц — массивом `units`. | +| `locale({locale, node})` | Язык с регионом, которым будет выведено значение. | + +## Когда доступно + +Ядро: сразу после выполнения `core.js`, ждать не нужно. + +## Пример + +Одни и те же значения выведены в русском и в английском блоке: у английского +блока свой `lang="en"`. Значения подставляет скрипт примера. + +:::example {id="guide/functions/format" label="Даты, числа и размеры" preview=sandbox log_console=true} +::: + +## Особые случаи + +- Не значение — нет даты, `NaN`, `null` — выводится пустой строкой. Свой + знак передают в `gap`: `format.number(NaN, 'number', {gap: '—'})`. +- Неизвестное слово формата читается как `number`, неизвестная валюта или + единица — как обычное число. +- «Сегодня» меняется в полночь. Компонент, который показывает дни, ставит + таймер на `msUntilNextDay()` с запасом в секунду и рисует подписи заново. +- Компонент, который собирается вместе с Framework, импортирует только + нужные функции из модулей `core/js/format-number.js`, + `core/js/format-date.js`, `core/js/format-relative.js` и + `core/js/format-file-size.js`; импорт ничего не меняет на странице. + +## См. также + +- [Язык интерфейса](/ru/guide/size-and-type/language/) — как определяется язык части страницы. +- [Слово компонента](/ru/guide/functions/text/page-text/) — язык узла и слова на нём. diff --git a/content/ru/guide/functions/text/format.page.json b/content/ru/guide/functions/text/format.page.json new file mode 100644 index 00000000..cc969543 --- /dev/null +++ b/content/ru/guide/functions/text/format.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 10}} diff --git a/content/ru/guide/functions/text/index.md b/content/ru/guide/functions/text/index.md new file mode 100644 index 00000000..2cecbdd4 --- /dev/null +++ b/content/ru/guide/functions/text/index.md @@ -0,0 +1,15 @@ +--- +title: "Текст, язык и значения" +description: "Даты и числа на языке страницы, язык и слова компонентов, очистка чужого HTML и маска поля." +--- + +# Текст, язык и значения + +Функции для того, что видит и вводит человек: значения на языке страницы, +слова компонентов, HTML из чужого источника и поле с маской. + +- [Даты, числа и размеры](/ru/guide/functions/text/format/) — дата, «вчера», сумма и размер файла на языке страницы. +- [Язык страницы](/ru/guide/functions/text/language/) — выбрать язык страницы и добавить слова компонентам. +- [Слово компонента](/ru/guide/functions/text/page-text/) — язык части страницы и слово на этом языке. +- [Чужой HTML](/ru/guide/functions/text/sanitize-html/) — вставить ответ сервера или ассистента без опасных частей. +- [Маска поля](/ru/guide/functions/text/mask/) — телефон, код или число в поле по шаблону. diff --git a/content/ru/guide/functions/text/index.page.json b/content/ru/guide/functions/text/index.page.json new file mode 100644 index 00000000..12cb21b4 --- /dev/null +++ b/content/ru/guide/functions/text/index.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 20}} diff --git a/content/ru/guide/functions/text/language.md b/content/ru/guide/functions/text/language.md new file mode 100644 index 00000000..79e90f0e --- /dev/null +++ b/content/ru/guide/functions/text/language.md @@ -0,0 +1,56 @@ +--- +title: "Язык страницы" +description: "SF.language — выбрать язык всей страницы из скрипта и добавить или поправить слова компонентов на любом языке." +--- + +# Язык страницы + +Компоненты Framework говорят на языке страницы. `SF.language` меняет этот +язык из скрипта — например, по переключателю языка — и добавляет компонентам +слова на языке, которого Framework не знает, или правит одно слово, не +переписывая остальные. Компоненты на странице рисуются заново сами. + +## Вызов + +```js +SF.language.use('en'); // язык всей страницы +SF.language.register('sf-pager', 'ru', {next: 'Вперёд'}); // поправить одно слово +SF.language.registerAll('kk', {'sf-pager': {next: 'Келесі'}}); +const stop = SF.language.onChange(() => redraw()); +``` + +| Член | Что делает | +|:---|:---| +| `current(node)` | Язык с регионом, который действует для узла (по умолчанию — для ``). | +| `use(language)` | Выбирает язык всей страницы и пишет его в ``. `use(null)` возвращает язык разметки. Возвращает выбранный язык. | +| `register(component, language, words)` | Добавляет или заменяет слова одного компонента на одном языке. Слова сливаются с прежними; `null` вместо слов убирает добавленное. | +| `registerAll(language, {компонент: слова})` | То же для многих компонентов сразу. | +| `known(component)` | Языки, на которых компоненту добавлены слова. | +| `components()` | Компоненты, которым что-то добавлено. | +| `onChange(listener)` | Вызывает `listener` при смене языка или слов; возвращает функцию, которая отписывает. | +| `forget()` | Забывает всё добавленное и выбор языка. | + +## Когда доступно + +Ядро: сразу после выполнения `core.js`, ждать не нужно. + +## Пример + +Кнопки меняют язык страницы, третья добавляет листалке своё слово «Вперёд». +Листалка рисуется заново, журнал показывает язык после каждого изменения. + +:::example {id="guide/functions/language" label="Язык страницы" preview=sandbox log_console=true} +::: + +## Особые случаи + +- Язык части страницы сильнее выбора: блок со своим `lang` говорит на своём + языке и после `use()`. +- Слово из атрибута самого элемента (например, `next-label` у листалки) не меняется ни + от языка, ни от `register`. +- `register` без компонента или языка бросает `TypeError`. + +## См. также + +- [Язык интерфейса](/ru/guide/size-and-type/language/) — откуда компонент берёт язык и слова. +- [Слово компонента](/ru/guide/functions/text/page-text/) — слово на языке узла для своего кода. diff --git a/content/ru/guide/functions/text/language.page.json b/content/ru/guide/functions/text/language.page.json new file mode 100644 index 00000000..12cb21b4 --- /dev/null +++ b/content/ru/guide/functions/text/language.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 20}} diff --git a/content/ru/guide/functions/text/mask.md b/content/ru/guide/functions/text/mask.md new file mode 100644 index 00000000..1e2c49b7 --- /dev/null +++ b/content/ru/guide/functions/text/mask.md @@ -0,0 +1,70 @@ +--- +title: "Маска поля" +description: "SF.Mask — надеть маску на поле ввода (телефон, код, число) и вывести значение по маске без поля." +--- + +# Маска поля + +Маска ведёт человека при вводе: телефон сам получает скобки и дефисы, код — +разделители, сумма — пробелы между разрядами. Поля ввода Framework надевают +маску по атрибуту `data-mask-pattern`. `SF.Mask` нужен, когда маску надевает +скрипт — на своё поле или поле, созданное позже, — и когда значение надо +вывести по маске без поля. + +## Вызов + +```js +const phone = await SF.Mask.create(input, {mask: '+{7} (000) 000-00-00', lazy: false}); +phone.unmaskedValue; // '79161234567' +phone.destroy(); + +await SF.Mask.pipe('9161234567', '+{7} (000) 000-00-00'); // '+7 (916) 123-45-67' +await SF.Mask.pipe(1234.5, {mask: Number, thousandsSeparator: ' ', scale: 2, padFractionalZeros: true}); // '1 234,50' +``` + +| Член | Что делает | +|:---|:---| +| `create(target, options)` | Надевает маску на поле — элемент или селектор. Обещание выполняется с маской поля, или с `null`, если поля или настроек нет. | +| `pipe(value, mask)` | Обещание строки: значение по маске без поля. `mask` — шаблон строкой или объект настроек. | +| `destroy(instance)` | Снимает маску; то же, что `instance.destroy()`. | +| `load()` | Обещание функции, которая создаёт маску; `create` вызывает её сам. | + +Шаблон: `0` — цифра, `a` — буква, `*` — любой знак, `{…}` — постоянная часть, +которая входит в значение (`+{7}`), `\` — следующий знак как есть, остальные +знаки показываются, но в значение не входят. + +| Настройка | Что делает | +|:---|:---| +| `mask` | Шаблон строкой, `Number` для числа, регулярное выражение или функция. | +| `lazy` | `false` показывает весь шаблон с пустыми местами сразу; по умолчанию места появляются по мере ввода. | +| `placeholderChar` | Знак пустого места, по умолчанию `_`. | +| `radix`, `thousandsSeparator`, `scale`, `padFractionalZeros`, `min`, `max`, `signed` | Для числа: десятичный знак (по умолчанию запятая), разделитель разрядов, знаки после запятой (по умолчанию 2), дописывать нули, границы, разрешить минус. | + +У маски поля: `value` — то, что в поле; `unmaskedValue` — только введённое и +постоянные части; `typedValue` — число для числовой маски; `updateValue()` — +перечитать поле; `destroy()` — снять маску. Все три значения можно и +записать. + +## Когда доступно + +Ядро: сразу после выполнения `core.js`, ждать не нужно. + +## Пример + +Скрипт надевает маску телефона и маску суммы на два поля. Журнал показывает +введённое без маски при каждом вводе. + +:::example {id="guide/functions/mask" label="Маска поля" preview=sandbox log_console=true} +::: + +## Особые случаи + +- Число проверяется, когда человек уходит из поля: тогда применяются `min`, + `max` и `scale`. Пока он печатает, `12,` и `-` остаются как есть. +- Знак, которого шаблон в этом месте не ждёт, переходит к следующему месту, + которое его принимает. + +## См. также + +- [Текстовые поля](/ru/components/forms/inputs/) — маска по атрибуту `data-mask-pattern`. +- [Даты, числа и размеры](/ru/guide/functions/text/format/) — вывести число по языку страницы. diff --git a/content/ru/guide/functions/text/mask.page.json b/content/ru/guide/functions/text/mask.page.json new file mode 100644 index 00000000..3410c46f --- /dev/null +++ b/content/ru/guide/functions/text/mask.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 50}} diff --git a/content/ru/guide/functions/text/page-text.md b/content/ru/guide/functions/text/page-text.md new file mode 100644 index 00000000..73213f65 --- /dev/null +++ b/content/ru/guide/functions/text/page-text.md @@ -0,0 +1,51 @@ +--- +title: "Слово компонента" +description: "SF.pageLanguage и SF.pageText — язык, который действует для узла страницы, и слово своего компонента на этом языке." +--- + +# Слово компонента + +Свой код на странице — подпись, сообщение, компонент проекта — должен +говорить на том же языке, что и компоненты Framework рядом с ним. +`SF.pageLanguage(node)` отвечает, какой язык действует для узла, а +`SF.pageText` берёт слово из словаря компонента на этом языке, с учётом +слов, которые страница добавила через `SF.language.register`. + +## Вызов + +```js +const WORDS = {ru: {empty: 'Пока пусто'}, en: {empty: 'Nothing yet'}}; + +SF.pageLanguage(node); // 'ru' +SF.pageText(WORDS, 'empty', node); // 'Пока пусто' +SF.pageText(WORDS, 'empty', node, 'shop-cart'); // со словами, добавленными для shop-cart +``` + +| Функция | Что возвращает | +|:---|:---| +| `SF.pageLanguage(node)` | Язык без региона (`ru`, `pt`) для узла: ближайший `[lang]` ниже ``, иначе язык, выбранный через `SF.language.use()`, иначе ``, иначе `en`. | +| `SF.pageText(dictionary, key, node, component)` | Слово `key` на языке узла. Ищет сначала слова, добавленные для `component` (по умолчанию — тег самого узла), потом слова из `dictionary`; затем то же для языка без региона и для английского. | + +## Когда доступно + +Ядро: сразу после выполнения `core.js`, ждать не нужно. + +## Пример + +Два блока с разным `lang` берут одно и то же слово из одного словаря. Кнопка +добавляет русскому слову свою замену через `SF.language.register`, и блок +показывает её. + +:::example {id="guide/functions/page-text" label="Слово компонента" preview=sandbox log_console=true} +::: + +## Особые случаи + +- Слова нет ни на одном языке — возвращается сам ключ. +- `lang` ищется и через теневое дерево: узел внутри смарт-элемента получает + язык блока, в котором стоит элемент. + +## См. также + +- [Язык страницы](/ru/guide/functions/text/language/) — выбрать язык и добавить слова. +- [Язык интерфейса](/ru/guide/size-and-type/language/) — порядок, в котором компонент выбирает слово. diff --git a/content/ru/guide/functions/text/page-text.page.json b/content/ru/guide/functions/text/page-text.page.json new file mode 100644 index 00000000..4e0ddcc5 --- /dev/null +++ b/content/ru/guide/functions/text/page-text.page.json @@ -0,0 +1 @@ +{"schema": "docara.page.v1", "navigation": {"order": 30}} diff --git a/content/ru/guide/functions/text/sanitize-html.md b/content/ru/guide/functions/text/sanitize-html.md new file mode 100644 index 00000000..fd59559b --- /dev/null +++ b/content/ru/guide/functions/text/sanitize-html.md @@ -0,0 +1,74 @@ +--- +title: "Чужой HTML" +description: "SF.sanitizeHtml и SF.safeHref — вставить HTML из ответа сервера, сообщения или ассистента без скриптов, обработчиков и опасных ссылок." +--- + +# Чужой HTML + +HTML, который пришёл не от самого сайта — ответ сервера, сообщение +пользователя, ответ ИИ-ассистента, — вставляют на страницу только через +`SF.sanitizeHtml`. Функция оставляет абзацы, списки, выделение, код, таблицы и +безопасные ссылки, а скрипты, обработчики событий, стили и всё, что может +загрузить или выполнить чужой код, убирает. `SF.safeHref` отвечает, можно ли +оставить адрес ссылки. + +## Вызов + +```js +element.replaceChildren(SF.sanitizeHtml(html)); // DocumentFragment +SF.sanitizeHtml(html, {elements: ['p', 'br', 'a']}); // список ещё уже +SF.sanitizeHtml(html, {codeLanguage: true}); // оставить language-* у +SF.safeHref('https://example.com/a'); // адрес или null +``` + +| Параметр | Что делает | +|:---|:---| +| `html` | Строка HTML. Не строка и пустая строка дают пустой фрагмент. | +| `elements` | Свой, более узкий список разрешённых элементов. Вернуть запрещённый элемент он не может. | +| `codeLanguage` | `true` оставляет у `` один класс `language-*` для подсветки. По умолчанию выключено. | +| `document` | Документ, в котором создаётся фрагмент; по умолчанию документ страницы. | + +`SF.sanitizeHtml` возвращает `DocumentFragment`, его вставляют через +`append` или `replaceChildren`. `SF.safeHref(url)` возвращает адрес, если он +`http:`, `https:`, `mailto:` или путь от корня сайта (`/docs/a`), иначе `null`. + +## Что остаётся + +- **Элементы:** `p br strong em code pre ul ol li blockquote a table thead + tbody tr th td h2 h3 h4 del`. +- **Убираются вместе с содержимым:** `script`, `style`, `iframe`, `svg`, + `img`, `video`, `form`, `input`, `button` и другие элементы, чьё содержимое + не текст для читателя. +- **Раскрываются:** остальные элементы (`div`, `span`, `h1`, `b`) заменяются + своим текстом и детьми. +- **Атрибуты** убираются все, кроме `href` у ссылки (если его оставил + `SF.safeHref`), `colspan`, `rowspan` и `scope` у ячеек таблицы. Каждая + оставленная ссылка открывается в новой вкладке с + `rel="noopener noreferrer nofollow"`. + +## Когда доступно + +Ядро: сразу после выполнения `core.js`, ждать не нужно. + +## Пример + +В поле — HTML со скриптом, обработчиком, картинкой и ссылкой +`javascript:`. Кнопка очищает его и вставляет ниже; в журнале — что +получилось. + +:::example {id="guide/functions/sanitize-html" label="Чужой HTML" preview=sandbox log_console=true} +::: + +## Особые случаи + +- Разбор не выполняет ничего: строка читается в `