/** * Общий набор контролов формы (#594, шаг 1 эпика #591). * * Правила ряда-переключателя, карточки-группы, фокуса и выключенного состояния * взяты из редактора боковой панели (`summary-panel-editor-style.ts`): он сделан * по макетам дизайнера и уже живёт в продукте. Чтобы «общий» не оказался общим * на словах, набор параметризован именами — контейнер формы, префикс классов, * карточки, футер и токены цвета, — и обязан воспроизводить текст панели * дословно (проверяется замороженной фикстурой в `test/form-kit.test.mjs`). * * Возвращается строка, а не тег `css`: лист панели вклеивается в ленивый sheet * строкой, а карточка вносит тот же текст через `unsafeCSS`. Один источник — * два потребителя. */ export interface FormKitCssOptions { /** Класс контейнера формы: `.summary-editor` у панели, `.hpf-form` у диалогов. */ form: string; /** Префикс частей: `summary` даёт `.summary-switch`, `hpf` — `.hpf-switch`. */ prefix: string; /** Селекторы карточек-групп: у панели их два, у диалогов один. */ cards: readonly string[]; /** Класс футера — его кнопки получают ту же обводку фокуса. */ footer: string; /** Имена переменных темы: поверхность, линия, акцент, а для диалогов ещё * канва, приглушённый текст, тинт активного состояния и опасность. */ tokens: { surface: string; line: string; accent: string; canvas: string; muted: string; tint: string; tintLine: string; danger: string; }; } /** Параметры панели: с ними генератор обязан вернуть её нынешние правила. */ export const SUMMARY_PANEL_FORM_KIT: FormKitCssOptions = { form: '.summary-editor', prefix: 'summary', cards: ['.summary-general', '.summary-blocks-card'], footer: '.summary-editor-footer', tokens: { surface: '--summary-editor-surface', line: '--summary-editor-line', accent: '--summary-editor-accent', // Панель дополнительных токенов не использует: её лист собирается из общих // фрагментов, а extras она не получает. Имена стоят, чтобы тип был полным. canvas: '--summary-editor-surface', muted: '--secondary-text-color', tint: '--summary-editor-surface', tintLine: '--summary-editor-line', danger: '--error-color', }, }; /** Параметры диалогов карточки. */ export const CARD_DIALOG_FORM_KIT: FormKitCssOptions = { // `hp-dialog .hpf-form`, а не `.hpf-form`: generic-правила диалогов // (`hp-dialog .body { display: flex }`, `hp-dialog .body label { … }`) имеют // ту же специфичность, и без тега набор проигрывал бы им на display и подписях. form: 'hp-dialog .hpf-form', prefix: 'hpf', cards: ['.hpf-card'], footer: '.hpf-footer', tokens: { surface: '--hpf-surface', line: '--hpf-line', accent: '--hpf-accent', canvas: '--hpf-canvas', muted: '--hpf-muted', tint: '--hpf-tint', tintLine: '--hpf-tint-line', danger: '--hpf-danger', }, }; /** * Правила, общие с панелью, — по одному фрагменту на правило (#597). * * Разрезано не ради красоты: в листе панели эти правила НЕ идут подряд, между * ними стоят её собственные (`button { font: inherit }`, `svg { … }`). Пока * генератор отдавал их одним куском, переезд панели на общий источник требовал * бы переставить её объявления — то есть переписать каскад под инструмент. * Отдельные фрагменты позволяют подставить каждый ровно туда, где он стоит * сейчас, и собранный лист совпадает с прежним побайтово. */ export function formKitCardsCss({ cards, tokens }: FormKitCssOptions): string { return ` ${cards.join(',\n ')} { min-width: 0; padding: 14px; border: 1px solid var(${tokens.line}); border-radius: 11px; background: var(${tokens.surface}); }`; } /** Обводка фокуса формы и её футера. */ export function formKitFocusCss({ form, footer, tokens }: FormKitCssOptions): string { return ` ${form} input:focus-visible, ${form} select:focus-visible, ${form} button:focus-visible, ${footer} button:focus-visible { outline: 2px solid var(${tokens.accent}); outline-offset: 2px; }`; } /** Выключенные контролы. */ export function formKitDisabledCss({ form }: FormKitCssOptions): string { return ` ${form} button:disabled, ${form} input:disabled, ${form} select:disabled { opacity: .4; cursor: default; }`; } /** Ряд-переключатель: подпись слева, переключатель справа. */ export function formKitSwitchRowCss({ form, prefix }: FormKitCssOptions): string { return ` ${form} .${prefix}-switch { display: flex; min-width: 0; min-height: 54px; align-items: center; justify-content: space-between; gap: 12px; margin: 0; color: var(--primary-text-color); cursor: pointer; }`; } /** Подпись ряда-переключателя: заголовок и пояснение мелким шрифтом. */ export function formKitSwitchCaptionCss({ prefix }: FormKitCssOptions): string { return ` .${prefix}-switch-caption { display: grid; min-width: 0; gap: 3px; } .${prefix}-switch-caption strong { font-size: .875rem; font-weight: 600; overflow-wrap: anywhere; } .${prefix}-switch-caption small { font-size: .8125rem; color: var(--secondary-text-color); overflow-wrap: anywhere; }`; } /** Все общие правила подряд — так их берут диалоги карточки. */ function sharedCss(options: FormKitCssOptions, withSwitch: boolean): string { const switchRules = !withSwitch ? '' : `\n${formKitSwitchRowCss(options)}\n${formKitSwitchCaptionCss(options)}`; return `${formKitCardsCss(options)}\n${formKitFocusCss(options)}\n${formKitDisabledCss(options)}${switchRules}`; } /** * Контролы, которых у панели нет (#594, расширено в #600 по референсу * `docs/design/600-settings-dialogs/reference/styles.css`). * * Числа — из §3.1 `SPEC.md` референса; цвета — через переменные темы HA, а не * hex прототипа: тёмная тема обязательна. Всё здесь видит только ленивый * редакторский граф — панель эти правила не получает (#597 К9). * * Сегмент (`-seg`): подпись варианта не рвётся посреди слова (ревью #600 r1, * M1 — «Temperatur/e» в пяти колонках). Колонка не сжимается ниже самого * длинного слова (`min-width: fit-content`; браузер без него берёт `0` * строкой выше), слова переносятся только по пробелам (`overflow-wrap: * normal`), а если варианты не помещаются в ряд — сегмент переносится на * вторую строку целыми кнопками (`flex-wrap`), а не ломает текст. */ function extrasCss(options: FormKitCssOptions): string { const { form, prefix, tokens } = options; const p = prefix; return ` ${form} { display: grid; gap: 16px; } ${form} label { margin: 0; font-size: inherit; color: inherit; } ${form} .${p}-card { padding: 0; overflow: hidden; } .${p}-head { display: flex; min-width: 0; align-items: center; gap: 4px; margin: 0; padding: 16px 16px 12px; } .${p}-head h3 { margin: 0; font-size: 1.25rem; line-height: 1.3; font-weight: 600; min-width: 0; overflow-wrap: anywhere; } .${p}-body { display: grid; gap: 15px; padding: 0 16px 16px; } .${p}-body:first-child { padding-top: 16px; } .${p}-sub { display: flex; align-items: center; gap: 4px; min-height: 28px; margin: 7px 0 -3px; } .${p}-sub h4 { margin: 0; font-size: 1rem; line-height: 1.4; font-weight: 600; } .${p}-field { display: grid; gap: 7px; min-width: 0; } .${p}-field > .${p}-label, .${p}-labelrow { display: flex; align-items: center; gap: 4px; min-height: 32px; font-size: .875rem; font-weight: 600; color: var(${tokens.muted}); } .${p}-labelrow > label { cursor: pointer; } .${p}-hint { margin: 0; font-size: .875rem; line-height: 1.5; color: var(${tokens.muted}); } .${p}-error { margin: 0; font-size: .875rem; line-height: 1.5; color: var(${tokens.danger}); } .${p}-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } .${p}-grid.${p}-grid-narrow { grid-template-columns: minmax(0, 1fr) 175px; } ${form} .${p}-input, ${form} .${p}-select { box-sizing: border-box; width: 100%; min-width: 0; min-height: 44px; padding: 8px 10px; border: 1px solid var(${tokens.line}); border-radius: 7px; background: var(${tokens.surface}); color: var(--primary-text-color); font: inherit; font-size: .875rem; line-height: 1.4; } ${form} .${p}-select { appearance: none; padding-right: 32px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7' viewBox='0 0 12 7'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23888' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E"); background-position: right 10px center; background-repeat: no-repeat; background-size: 12px 7px; cursor: pointer; } ${form} .${p}-input:hover, ${form} .${p}-select:hover { border-color: var(${tokens.muted}); } ${form} .${p}-input:focus, ${form} .${p}-select:focus { border-color: var(${tokens.accent}); outline: none; } ${form} [aria-invalid="true"] { border-color: var(${tokens.danger}) !important; } .${p}-unit { display: inline-flex; align-items: center; height: 44px; border: 1px solid var(${tokens.line}); border-radius: 7px; background: var(${tokens.surface}); } .${p}-unit > input { box-sizing: border-box; width: 5.5em; min-width: 5.5em; height: 100%; padding: 8px 9px; border: 0; border-radius: 7px; background: none; color: var(--primary-text-color); font: inherit; font-size: .875rem; font-variant-numeric: tabular-nums; } .${p}-unit > input:focus { outline: 0; } .${p}-unit > span { padding: 0 11px 0 3px; white-space: nowrap; color: var(${tokens.muted}); font-size: .875rem; } .${p}-unit:focus-within { outline: 2px solid var(${tokens.accent}); outline-offset: 2px; } .${p}-unit.${p}-unit-wide, .${p}-unit.${p}-unit-wide > input { width: 100%; min-width: 0; } .${p}-field > .${p}-unit:not(.${p}-unit-wide) { justify-self: start; } .${p}-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; } .${p}-actions > input[type="file"] { display: none; } .${p}-row { display: flex; min-width: 0; min-height: 44px; align-items: center; justify-content: space-between; gap: 12px; } .${p}-row > .${p}-label { min-width: 0; overflow-wrap: anywhere; } .${p}-toggle { display: grid; grid-template-columns: 28px minmax(0, 1fr) 44px; grid-template-rows: auto auto; align-items: center; column-gap: 10px; row-gap: 4px; min-height: 58px; margin: 0; padding: 11px 0; color: var(--primary-text-color); cursor: pointer; } .${p}-toggle.${p}-noicon { grid-template-columns: minmax(0, 1fr) 44px; } .${p}-toggle > .${p}-toggle-icon { grid-column: 1; grid-row: 1; display: flex; align-items: center; justify-content: center; width: 28px; color: var(${tokens.muted}); --mdc-icon-size: 19px; } .${p}-toggle > .${p}-toggle-title { grid-row: 1; display: flex; align-items: center; gap: 4px; min-width: 0; min-height: 28px; font-size: .875rem; font-weight: 600; line-height: 1.4; } .${p}-toggle > .${p}-toggle-title > span { min-width: 0; overflow-wrap: anywhere; } .${p}-toggle > .${p}-toggle-caption { grid-row: 2; margin: 0; font-size: .875rem; line-height: 1.5; color: var(${tokens.muted}); overflow-wrap: anywhere; } .${p}-toggle:not(.${p}-noicon) > .${p}-toggle-title, .${p}-toggle:not(.${p}-noicon) > .${p}-toggle-caption { grid-column: 2; } .${p}-toggle.${p}-noicon > .${p}-toggle-title, .${p}-toggle.${p}-noicon > .${p}-toggle-caption { grid-column: 1; } .${p}-toggle > input { grid-column: -2; grid-row: 1 / 3; align-self: center; } .${p}-toggle.${p}-compact { grid-template-rows: auto; min-height: 40px; padding: 2px 0; } .${p}-toggle.${p}-compact > .${p}-toggle-title { font-weight: 500; min-height: 24px; } .${p}-toggle.${p}-compact > .${p}-toggle-icon { --mdc-icon-size: 18px; } .${p}-toggle:has(input:disabled) { cursor: default; } .${p}-toggle:has(input:disabled) > .${p}-toggle-title, .${p}-toggle:has(input:disabled) > .${p}-toggle-caption { opacity: .65; } .${p}-compact-list { display: grid; gap: 2px; } .${p}-toggle > input { appearance: none; position: relative; width: 44px; height: 44px; flex: 0 0 44px; margin: 0; padding: 0; border: 0; border-radius: 6px; background: transparent; cursor: pointer; } .${p}-toggle > input::before { content: ''; position: absolute; box-sizing: border-box; width: 36px; height: 22px; inset: 11px 4px; border: 1px solid var(${tokens.line}); border-radius: 12px; background: color-mix(in srgb, var(${tokens.surface}) 65%, var(${tokens.muted})); } .${p}-toggle > input::after { content: ''; position: absolute; width: 16px; height: 16px; top: 50%; left: 7px; border-radius: 50%; background: var(--text-primary-color, #fff); box-shadow: 0 1px 2px rgb(0 0 0 / 20%); transform: translateY(-50%); transition: left .15s; } .${p}-toggle > input:checked::before { background: var(${tokens.accent}); border-color: var(${tokens.accent}); } .${p}-toggle > input:checked::after { left: 21px; } .${p}-toggle.${p}-compact > input { grid-row: 1; } .${p}-toggle > input:disabled { cursor: default; } .${p}-toggle > input:disabled::before { opacity: .4; } .${p}-seg { display: flex; flex-wrap: wrap; min-width: 0; gap: 3px; padding: 3px; border: 1px solid var(${tokens.line}); border-radius: 7px; background: var(${tokens.canvas}); } .${p}-seg label { position: relative; display: flex; min-width: 0; min-width: fit-content; min-height: 38px; flex: 1 1 0; align-items: center; justify-content: center; gap: 7px; padding: 4px 5px; border: 1px solid transparent; border-radius: 5px; color: var(${tokens.muted}); font-size: .875rem; font-weight: 500; cursor: pointer; text-align: center; overflow-wrap: normal; } .${p}-seg input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; pointer-events: none; } .${p}-seg label:has(input:checked) { background: var(${tokens.tint}); border-color: var(${tokens.tintLine}); color: var(${tokens.accent}); } .${p}-seg label:has(input:focus-visible) { outline: 2px solid var(${tokens.accent}); outline-offset: 1px; } .${p}-seg[aria-disabled="true"] { opacity: .5; } .${p}-seg .${p}-line { width: 25px; border-top: 2px solid currentColor; } .${p}-seg .${p}-line-dashed { border-top-style: dashed; } .${p}-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } .${p}-tile { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; min-height: 72px; padding: 10px 6px 9px; border: 1px solid var(${tokens.line}); border-radius: 8px; background: var(${tokens.surface}); color: var(${tokens.muted}); cursor: pointer; text-align: center; user-select: none; --mdc-icon-size: 22px; } .${p}-tile input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; } .${p}-tile > span { font-size: .875rem; font-weight: 500; line-height: 1.25; } .${p}-tile:has(input:checked) { background: var(${tokens.tint}); border-color: var(${tokens.tintLine}); color: var(${tokens.accent}); } .${p}-tile:has(input:focus-visible) { outline: 2px solid var(${tokens.accent}); outline-offset: 2px; } .${p}-tile:has(input:disabled) { opacity: .45; cursor: default; } .${p}-tile:has(input:disabled) input { cursor: default; } .${p}-colortiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } .${p}-colortile { display: grid; gap: 6px; min-width: 0; padding: 6px 6px 7px; border: 1px solid var(${tokens.line}); border-radius: 8px; background: var(${tokens.surface}); } .${p}-colortile > .${p}-colortile-swatch { position: relative; min-height: 48px; border: 1px solid var(${tokens.line}); border-radius: 6px; font-size: .875rem; font-weight: 500; line-height: 1.25; } .${p}-colortile > .${p}-colortile-swatch > span { position: absolute; z-index: 1; inset: 0; display: flex; align-items: center; padding: 5px 10px; min-width: 0; overflow-wrap: anywhere; pointer-events: none; } .${p}-colortile > .${p}-colortile-swatch > hp-color-opacity { position: absolute; inset: 0; } .${p}-colortile-meta { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px; } .${p}-colortile-meta .${p}-hex { font-size: .8125rem; } .${p}-colortile-meta .${p}-unit { height: 32px; } .${p}-colortile-meta .${p}-unit > input { width: 3.6em; min-width: 3.6em; padding: 4px 4px 4px 7px; font-size: .8125rem; } .${p}-colortile-meta .${p}-unit > input[type='number'] { appearance: textfield; } .${p}-colortile-meta .${p}-unit > input[type='number']::-webkit-inner-spin-button, .${p}-colortile-meta .${p}-unit > input[type='number']::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } .${p}-colortile-meta .${p}-unit > span { padding: 0 7px 0 0; font-size: .8125rem; } .${p}-colorfield { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; width: fit-content; max-width: 100%; min-height: 44px; padding: 3px 10px 3px 3px; border: 1px solid var(${tokens.line}); border-radius: 9px; background: var(${tokens.canvas}); } .${p}-colorfield > hp-color-opacity { flex: 0 0 auto; } .${p}-hex { font-size: .875rem; color: var(${tokens.muted}); font-variant-numeric: tabular-nums; } .${p}-colorfield > .${p}-opacity { display: flex; align-items: center; gap: 8px; font-size: .875rem; color: var(${tokens.muted}); } .${p}-colorfield > .${p}-opacity > .${p}-unit { height: 38px; } .${p}-colorfield > .${p}-opacity > .${p}-unit > input { width: 4.8em; min-width: 4.8em; } .${p}-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: 8px; min-width: 0; } .${p}-actions .btn { box-sizing: border-box; min-height: 44px; max-width: 100%; white-space: normal; text-align: left; } .${p}-actions .backupupload { max-width: 100%; } .${p}-colorrow { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px 16px; min-height: 56px; } .${p}-colorrow > .${p}-colorfield { justify-self: end; } .${p}-colorrow > .${p}-labelrow { min-height: 0; } .${p}-link { padding: 5px 0; border: 0; background: none; color: var(${tokens.accent}); font: inherit; font-size: .875rem; font-weight: 500; cursor: pointer; } .${p}-link:disabled { opacity: .5; cursor: default; } .${p}-inline { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .${p}-inline > .${p}-labelrow { flex: 1 1 auto; } .${p}-inline > .${p}-select { width: auto; flex: 0 0 auto; min-width: 200px; } .${p}-inline.${p}-wrap { flex-wrap: wrap; row-gap: 6px; } .${p}-radios { display: flex; flex-wrap: wrap; gap: 4px 18px; margin-left: auto; } .${p}-radios label { display: inline-flex; align-items: center; gap: 7px; min-height: 28px; font-size: .875rem; cursor: pointer; white-space: nowrap; } .${p}-radios input { width: 16px; height: 16px; margin: 0; accent-color: var(${tokens.accent}); cursor: pointer; } .${p}-radios label:has(input:checked) { color: var(${tokens.accent}); } .${p}-callout { display: flex; gap: 8px; margin: 0; padding: 11px 12px; border-radius: 8px; background: var(${tokens.tint}); color: var(--primary-text-color); font-size: .875rem; line-height: 1.6; } .${p}-callout > ha-icon { flex: none; margin-top: 2px; --mdc-icon-size: 16px; color: var(${tokens.accent}); } .${p}-callout > p, .${p}-callout > span { margin: 0; min-width: 0; overflow-wrap: anywhere; } .${p}-callout.${p}-warning { background: color-mix(in srgb, var(--warning-color, #ff9800) 14%, var(${tokens.surface})); } .${p}-callout.${p}-warning > ha-icon { color: var(--warning-color, #ff9800); } .${p}-callout .${p}-link { padding: 0; font-weight: 600; } .${p}-range { --hpf-range-value-width: 84px; display: grid; grid-template-columns: minmax(0, 1fr) var(--hpf-range-value-width); align-items: center; gap: 15px; } .${p}-range > input[type="range"], .${p}-range > ha-slider { box-sizing: border-box; width: 100%; min-width: 0; margin-inline: 0; accent-color: var(${tokens.accent}); } .${p}-range > .${p}-unit { box-sizing: border-box; width: var(--hpf-range-value-width); min-width: 0; } .${p}-range > .${p}-unit > input { width: 100%; min-width: 0; padding-right: 3px; } .${p}-range-ends { display: grid; grid-template-columns: minmax(0, 1fr) 84px; gap: 15px; margin-top: -4px; font-size: .875rem; line-height: 1.2; color: var(${tokens.muted}); } .${p}-range-ends-track { display: flex; grid-column: 1; align-items: center; justify-content: space-between; min-width: 0; } .${p}-headline { display: flex; align-items: center; justify-content: space-between; gap: 10px; } .${p}-north { display: grid; grid-template-columns: minmax(0, 1fr) 44px; gap: 7px 16px; align-items: center; } .${p}-north > .${p}-labelrow { grid-area: 1 / 1; min-height: 32px; } .${p}-north > .${p}-select, .${p}-north > .${p}-unit, .${p}-north > .${p}-input { grid-area: 2 / 1; } .${p}-north > .${p}-north-n { grid-area: 1 / 2; text-align: center; font-size: .875rem; line-height: 28px; color: var(${tokens.muted}); } .${p}-compass { grid-area: 2 / 2; box-sizing: border-box; display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; border: 1px solid var(${tokens.line}); border-radius: 50%; background: var(${tokens.canvas}); color: var(${tokens.accent}); } .${p}-compass svg { width: 24px; height: 24px; transform-origin: 50% 50%; transform-box: view-box; } .${p}-compass.${p}-unset { color: var(${tokens.muted}); } .${p}-tint { padding: 14px; border: 1px solid var(${tokens.tintLine}); border-radius: 9px; background: var(${tokens.tint}); } .${p}-tint-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; } .${p}-tint-head h4 { margin: 0; font-size: 1rem; font-weight: 600; } .${p}-tag { padding: 3px 10px; border-radius: 12px; background: color-mix(in srgb, var(${tokens.accent}) 18%, var(${tokens.surface})); color: var(${tokens.muted}); font-size: .8125rem; } .${p}-chips { display: flex; flex-wrap: wrap; gap: 8px; } .${p}-chip { display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 4px 0 10px; border: 1px solid var(${tokens.tintLine}); border-radius: 18px; background: var(${tokens.tint}); color: var(${tokens.accent}); font-size: .875rem; font-weight: 500; } .${p}-chip > ha-icon { --mdc-icon-size: 16px; } .${p}-chip > button { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: inherit; cursor: pointer; --mdc-icon-size: 14px; } .${p}-chip > button:hover { background: color-mix(in srgb, var(${tokens.accent}) 15%, transparent); } .${p}-choices { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } .${p}-choice { position: relative; display: flex; flex-direction: column; justify-content: center; gap: 5px; min-width: 0; min-height: 80px; padding: 13px 35px 13px 12px; border: 1px solid var(${tokens.line}); border-radius: 8px; cursor: pointer; } .${p}-choice input { position: absolute; right: 12px; top: 16px; width: 16px; height: 16px; margin: 0; accent-color: var(${tokens.accent}); } .${p}-choice:has(input:checked) { background: var(${tokens.tint}); border-color: var(${tokens.tintLine}); } .${p}-choice strong { font-size: .875rem; font-weight: 600; } .${p}-choice small { font-size: .875rem; line-height: 1.5; color: var(${tokens.muted}); } .${p}-status { display: flex; align-items: center; gap: 6px; min-width: 0; margin-left: 8px; font-size: .875rem; color: var(${tokens.muted}); } .${p}-status > .${p}-link { color: var(--warning-color, #b87940); } .${p}-divider { border: 0; border-top: 1px solid var(${tokens.line}); margin: 4px 0; } .${p}-picker { display: grid; gap: 6px; } ${form} .${p}-pick { display: flex; align-items: center; gap: 8px; text-align: left; } ${form} .${p}-pick > b { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .${p}-pick-ref { min-width: 0; font-size: .8125rem; color: var(${tokens.muted}); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .${p}-pick-ph { color: var(${tokens.muted}); } .${p}-panel { padding: 8px; border: 1px solid var(${tokens.line}); border-radius: 9px; background: var(${tokens.canvas}); } .${p}-list { display: flex; flex-direction: column; gap: 2px; max-height: 220px; margin-top: 6px; overflow: auto; } .${p}-cand { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; width: 100%; min-height: 44px; padding: 8px 10px; border: 0; border-radius: 7px; background: none; color: var(--primary-text-color); font: inherit; font-size: .875rem; text-align: left; cursor: pointer; } .${p}-cand > small { font-size: .8125rem; color: var(${tokens.muted}); } .${p}-cand:hover { background: var(${tokens.surface}); } .${p}-cand.sel { background: var(${tokens.tint}); color: var(${tokens.accent}); } .${p}-cand:focus-visible { outline: 2px solid var(${tokens.accent}); outline-offset: -2px; } .${p}-templegend { display: flex; justify-content: space-between; margin-top: -6px; font-size: .8125rem; color: var(${tokens.muted}); } .${p}-block { display: grid; gap: 15px; } .${p}-block.${p}-disabled > :not(.${p}-sub):not(.${p}-callout) { opacity: .45; } .${p}-block.${p}-disabled .${p}-seg label, .${p}-block.${p}-disabled .${p}-colorfield, .${p}-block.${p}-disabled .${p}-range { pointer-events: none; } .${p}-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; } .${p}-toolbar > .${p}-input { flex: 1 1 200px; width: auto; } .${p}-check { display: inline-flex; align-items: center; gap: 7px; min-height: 44px; font-size: .875rem; color: var(${tokens.muted}); cursor: pointer; white-space: nowrap; } .${p}-check > input { width: 18px; height: 18px; margin: 0; accent-color: var(${tokens.accent}); cursor: pointer; } .${p}-iconfield { display: flex; align-items: center; gap: 8px; min-width: 0; } .${p}-iconfield-control { box-sizing: border-box; display: flex; align-items: center; flex: 1 1 auto; min-width: 0; min-height: 44px; border: 1px solid var(${tokens.line}); border-radius: 7px; background: var(${tokens.canvas}); } .${p}-iconfield-control:focus-within { outline: 2px solid var(${tokens.accent}); outline-offset: 2px; } .${p}-iconfield-control > .${p}-input, .${p}-iconfield-control > ha-icon-picker { flex: 1 1 auto; min-width: 0; width: 100%; } ${form} .${p}-iconfield-control > .${p}-input { min-height: 42px; height: 42px; border: 0; background: transparent; } .${p}-iconfield-control > .${p}-input:focus { outline: 0; } .${p}-iconfield-control > ha-icon-picker { display: block; min-height: 42px; --mdc-text-field-fill-color: transparent; --mdc-text-field-idle-line-color: transparent; --mdc-text-field-hover-line-color: transparent; --mdc-text-field-height: 42px; --mdc-shape-small: 7px; --ha-color-form-background: transparent; --ha-color-border-neutral-loud: transparent; --md-list-item-one-line-container-height: 42px; --md-list-item-two-line-container-height: 42px; } .${p}-iconpreview { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 42px; width: 42px; height: 42px; color: var(${tokens.muted}); } .${p}-mono { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: .8125rem; } .${p}-hint > .${p}-link { padding: 0; } .${p}-hint ha-icon { --mdc-icon-size: 16px; display: inline-flex; vertical-align: -3px; } .${p}-actionrow { display: flex; align-items: center; justify-content: space-between; gap: 10px; } .${p}-note { display: flex; align-items: flex-start; gap: 6px; } .${p}-note > ha-icon { --mdc-icon-size: 16px; flex: 0 0 auto; margin-top: 2px; } .${p}-note-warning { color: var(--warning-color, #b87940); } .${p}-hint-line { display: block; } .${p}-chip-warning { border-color: var(--warning-color, #b87940); color: var(--warning-color, #b87940); } .${p}-iconclear { flex: 0 0 auto; min-width: 44px; min-height: 44px; padding: 0; } ${form} textarea.${p}-input { min-height: 96px; resize: vertical; line-height: 1.5; } .${p}-preview { display: flex; justify-content: center; } @media (max-width: 480px) { .${p}-body { padding: 0 14px 14px; } .${p}-head { padding: 14px 14px 12px; } .${p}-grid, .${p}-grid.${p}-grid-narrow, .${p}-choices { grid-template-columns: minmax(0, 1fr); } .${p}-tiles, .${p}-colortiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } .${p}-colorrow { grid-template-columns: minmax(0, 1fr); } .${p}-colorrow > .${p}-colorfield { justify-self: start; } .${p}-inline { flex-direction: column; align-items: stretch; gap: 7px; } .${p}-inline > .${p}-select { width: 100%; } .${p}-radios { margin-left: 0; } .${p}-seg { flex-wrap: wrap; } .${p}-seg label { flex-basis: 42%; } } @media (prefers-reduced-motion: reduce) { .${p}-toggle > input::after { transition: none; } } @media (forced-colors: active) { ${form} .${p}-select { appearance: auto; background-image: none; padding-right: 10px; } }`; } /** * Лист набора для заданных имён. * * `withSwitch` выключается там, где рядов-переключателей ещё нет: диалог комнаты * их не содержит, а мёртвые правила в синхронном графе стоят байтов. Панель * получает их всегда — её ряд и есть образец, с которым сверяется фикстура. */ export function formKitCss( options: FormKitCssOptions, { withSwitch = true }: { withSwitch?: boolean } = {}, ): string { return `${sharedCss(options, withSwitch)}\n${extrasCss(options)}\n`; } /** * Токены темы для диалогов карточки: у панели свои, у карточки — эти. * * Канва без темы — на 10 % ближе к тексту, чем карточка: светлая тема даёт * серый, тёмная — чуть светлее карточки. HA всегда задаёт свою переменную. * (Комментарий здесь, а не в CSS: строка листа уезжает в бандл как есть, #526.) */ export const cardDialogFormKitTokens = ` hp-dialog .hpf-form { --hpf-surface: var(--card-background-color, #fff); --hpf-line: var(--hp-line, var(--divider-color, #dce5e7)); --hpf-accent: var(--hp-accent, var(--primary-color)); --hpf-canvas: var(--secondary-background-color, color-mix(in srgb, var(--hpf-surface) 90%, var(--primary-text-color, #000))); --hpf-muted: var(--secondary-text-color, #777); --hpf-tint: color-mix(in srgb, var(--hpf-accent) 10%, var(--hpf-surface)); --hpf-tint-line: color-mix(in srgb, var(--hpf-accent) 38%, var(--hpf-surface)); --hpf-danger: var(--error-color, #db543d); } `;