mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 13:48:57 +00:00
Make colour tiles solid, readable and fully clickable; align Data actions and device icon field with the form kit. Cover narrow Windows layouts and both icon-picker branches with a focused browser smoke. Issue: #605 User-Visible: yes
551 lines
33 KiB
TypeScript
551 lines
33 KiB
TypeScript
/**
|
||
* Общий набор контролов формы (#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);
|
||
}
|
||
`;
|