Изучите цветовую и интервальную шкалы Duet от LocalTapiola на примере выбора диапазона дат: с датами в формате ISO, явной проверкой диапазона, методами управления фокусом компонентов и отдельной границей ответственности внешнего пикера с открытым исходным кодом.
Автономная интерпретация HTML с нативными полями даты и проверенными значениями темы Duet по умолчанию; это не среда выполнения календаря Duet. · Справочник по реализации
02
Справочник по дизайну дизайн-система Duet
Снимок общедоступной документации Duet: компоненты основной системы, токены темы LocalTapiola по умолчанию и описанная интеграция версии 10.7.0/CDN. Это не отдельный пакет @duetds/date-picker версии 1.x. Некоторые токены скругления по-прежнему отмечены статусом In Review.
Дизайн-система Duet Основная система Duet и picker с открытым исходным кодом — разные продукты
Сохраняйте контракт пакета вместе с примером
Duet предоставляет LocalTapiola и Turva тематические компоненты и базовые стили. Отдельно опубликованный проект @duetds/date-picker — более узкий дистрибутив с открытым исходным кодом. Совпадение имени тега не означает, что его свойства, настройка локализации или переменные CSS взаимозаменяемы с текущей основной дизайн-системой.
Код интеграции на этой странице предназначен для основной системы. В автономном предварительном просмотре используются нативные поля даты и оригинальное оформление на основе связанных токенов Duet. Пример показывает правила диапазона, но не утверждает, что в нём работает всплывающий календарь Duet или его модель клавиатурного управления.
Дизайн-система Duet Фирменный синий и сигнальный красный выполняют разные задачи
Используйте тему, а не заменяйте один акцентный цвет
В теме LocalTapiola по умолчанию синий цвет действия сочетается с тёмно-синим основным текстом. В Turva используются собственный красный цвет действия и отдельный стиль вторичного текста. Замена только цвета действия не учитывает остальные связи между элементами темы. В текущей таблице старые оттенки Turva также отмечены как устаревшие; копирование всех исторических цветов палитры закрепило бы устаревшие рекомендации.
В нашем оригинальном примере выбора дат для поездки синий обозначает следующее действие, а красный используется только тогда, когда выбранный период нужно исправить. Это демонстрация планирования, а не рекомендация по страхованию, расчёт стоимости или покупка.
Дизайн-система Duet Иерархия строится вокруг основного текста размером 16px
Для масштаба не требуется огромный заголовок повсюду
В крупной части шкалы размеров Duet используются 16, 20, 24, 36, 48 и 72px. Во встроенной панели планирования задан размер 24px, чтобы кнопка действия и подписи дат оставались заметными. Маркетинговый размер 72px не является значением по умолчанию для компактной формы.
Пакеты шрифтов LocalTapiola и Turva — отдельные ресурсы. В автономной интерпретации используется системный шрифт-замена; фирменные файлы шрифтов в неё не входят. В авторизованном приложении следует загрузить выбранное семейство шрифтов по документированному пути и затем использовать соответствующие ему токены начертания.
Дизайн-система Duet Используйте фактические интервалы 20px и 28px
Шкала Duet не сводится к универсальным интервалам в восемь пикселей
Помимо привычных степеней двойки, шкала включает 12, 20, 28 и 36px. Эти промежуточные интервалы важны для точного воспроизведения ритма формы в системе. В широком макете используйте интервал 20px между двумя полями; при расположении столбцом сохраняйте каждую подпись рядом с соответствующим полем. Точку перехода между макетами приложение выбирает самостоятельно.
В элементах управления по умолчанию используется скругление углов 4px, а для кнопок — форма капсулы. Некоторые новые значения радиуса контейнеров в справочнике токенов явно помечены статусом In Review; в этом руководстве этот статус сохранён, а не заменён утверждением, будто одобрены все перечисленные токены.
Дизайн-система Duet Храните значения в формате ISO и отдельно указывайте видимый формат
Не обрабатывайте локализованные строки отображения как данные
Значения и границы компонента задаются в формате YYYY-MM-DD. Событие выбранной даты содержит строку и объект Date. Для сравнения и хранения используйте строку; локализованное отображение в поле относится к представлению данных. Текущая точка входа для локализации — язык документа, а устаревшее свойство language объявлено устаревшим.
В примере разрешены даты только в июне 2027 года. Этот фиксированный интервал делает тестирование предсказуемым и явно обозначен как ограничение примера. В рабочей форме допустимый интервал следует определять правилами продукта и пояснять рядом с полем.
Дизайн-система Duet Допустимая дата не обязательно означает допустимый диапазон
Проверяйте взаимосвязь дат после проверки каждого поля
Объект validity компонента описывает ошибки отдельных полей. Дата возвращения, которая предшествует дате отъезда, всё ещё может быть допустимой календарной датой, поэтому приложение должно отдельно проверить их взаимное расположение. В нативном примере назначается понятная ошибка и вызывается setFocus() — документированный метод, который переводит фокус на внутреннее поле ввода.
Количество дней рассчитывается включительно: если отъезд и возвращение приходятся на один день, результатом будет один календарный день. Преобразование дат ISO в полночь по UTC не позволяет переходу на летнее время превратить календарный интервал в дробное значение длительности. Это оригинальная логика приложения, а не правило ценообразования Duet.
Отклоняйте отсутствующие, неправильно сформированные значения и даты за пределами допустимого интервала.
Дизайн-система Duet Основное действие не подразумевает отправку формы
В Duet для отправки формы предусмотрено отдельное свойство submit
Кнопка основной системы включает отправку формы с помощью submit; по умолчанию это свойство равно false. В предоставленной композиции действие click подготавливает локальный результат, поэтому это свойство не требуется. Для перехода используется url, который задаёт семантику ссылки.
Предварительный просмотр очищает подготовленное сообщение при изменении любого из полей. Иначе сообщение могло бы подтверждать значения, которые посетитель уже заменил. Индикатор загрузки не отображается, поскольку асинхронных операций нет.
Используйте одно основное действие подготовки.
Укажите, что результат создаётся локально.
Не используйте формулировку об успехе, чтобы создать впечатление, будто полис оформлен.
Дизайн-система Duet Зафиксируйте ресурсы основной системы как совместимый набор
Пример — документированный снимок, а не обещание использовать последнюю версию
На проверенной странице с примерами использования указаны компоненты версии 10.7.0 и CSS версии 5.1.4; в снимках поискового индекса уже упоминаются более поздние выпуски. В примере намеренно сохранена пара, непосредственно проверенная на странице. Перед внедрением в production выберите один выпуск и проверьте совместимость с CSS, шрифтами и поведением компонентов.
Поля даты — пользовательские элементы. Дождитесь их определения, прежде чем вызывать методы; назначайте обработчики для duetChange и читайте документированные данные события, не предполагая, что оно будет нативным событием change. Не используйте на одной странице модуль CDN основной системы вместе с автономным средством выбора дат: оба определяют один и тот же тег.
Сохраните весь документ как index.html и откройте его в браузере. Для автономной работы HTML используются нативные поля даты и проверенные значения темы Duet по умолчанию; это не среда выполнения календаря Duet.
html
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Duet travel period interpretation</title><style>*{box-sizing:border-box}body{margin:0;background:#f0f7fa;color:#082a4d;font:16px/1.5 system-ui,sans-serif}main{max-width:1000px;margin:auto;padding:36px 28px}.eyebrow{font-size:12px;letter-spacing:2px;text-transform:uppercase;margin-bottom:12px}h1{font-size:36px;line-height:1.2;font-weight:600;margin:0 0 20px}p{margin:0 0 20px}.hero{display:grid;grid-template-columns:1fr 300px;gap:28px;align-items:center;margin-bottom:28px}.art{height:170px;position:relative;overflow:hidden;border-radius:50%;background:#c6e1ee}.sun{position:absolute;top:25px;right:60px;width:36px;height:36px;border-radius:50%;background:white}.hill{position:absolute;background:#0077b3;width:210px;height:170px;bottom:-95px;left:-10px;transform:rotate(25deg);border-radius:24px}.hill.two{left:135px;background:#00619b;bottom:-110px;transform:rotate(-25deg)}.road{position:absolute;width:150px;height:200px;border:12px solid white;border-radius:50%;top:65px;left:95px;transform:rotate(25deg)}.card{background:white;border-radius:4px;padding:28px;box-shadow:0 2px 8px #082a4d0d;display:grid;grid-template-columns:1fr 230px;gap:28px}h2{font-size:24px;line-height:1.3;margin:0 0 20px}.fields{display:grid;grid-template-columns:1fr 1fr;gap:20px}label{display:block;font-weight:600;margin-bottom:8px}input{width:100%;max-width:100%;min-width:0;border:1px solid #909599;border-radius:4px;font:inherit;color:inherit;padding:12px;background:white}input[aria-invalid=true]{border-color:#d61c52}button{background:#0077b3;color:white;border:0;border-radius:20rem;padding:12px 36px;font:600 16px/1.5 system-ui;cursor:pointer}button:hover{background:#004b81}button:focus-visible,input:focus-visible{outline:3px solid #082a4d;outline-offset:3px}.error{font-size:14px;color:#d61c52;min-height:24px;margin:8px 0 20px}.calendar{background:#f0f7fa;border-radius:4px;padding:20px}.calendar strong{display:block;margin-bottom:12px}.grid{display:grid;grid-template-columns:repeat(7,1fr);gap:4px;font-size:12px;text-align:center}.grid span{padding:3px 0}.grid .in-range{background:#c6e1ee}.grid .endpoint{background:#0077b3;color:white;border-radius:50%}.caption{font-size:12px;margin:12px 0 0}.result{margin-top:20px;background:#e4f1f7;padding:20px;border-radius:4px}.result:empty{display:none}.boundary{font-size:12px;margin-top:28px}.hint{font-size:14px}@media(max-width:650px){main{padding:24px 16px}.hero{grid-template-columns:1fr}.art{display:none}h1{font-size:28px}.card{grid-template-columns:1fr;padding:20px}.fields{grid-template-columns:1fr;gap:16px}.calendar{max-width:300px;width:100%}button{width:100%;padding:12px 20px}}
</style><main><div class="hero"><div><div class="eyebrow">Travel period planner</div><h1>Make time for a change of scenery.</h1><p>Choose a departure and return date to explore an inclusive calendar-day count.</p></div><div class="art" aria-hidden="true"><div class="sun"></div><div class="hill"></div><div class="hill two"></div><div class="road"></div></div></div><div class="card"><section><h2>When will you travel?</h2><p class="hint" id="hint">Use dates from 1–30 June 2027. This fixed month is for demonstration.</p><form novalidate><div class="fields"><div><label for="start">Departure</label><input id="start" type="date" min="2027-06-01" max="2027-06-30" value="2027-06-10" required aria-describedby="hint error"></div><div><label for="end">Return</label><input id="end" type="date" min="2027-06-01" max="2027-06-30" value="2027-06-17" required aria-describedby="hint error"></div></div><div class="error" id="error" role="alert"></div><button type="submit">Prepare date range</button></form></section><aside class="calendar"><strong>June 2027</strong><div class="grid" id="calendar" aria-hidden="true"></div><p class="caption" id="range-caption">10–17 June selected</p></aside></div><div class="result" id="result" role="status"></div><p class="boundary">Independent HTML interpretation of Duet tokens with native date fields. The calendar image is a non-interactive range overview. No insurance quote, booking or policy is created.</p></main><script>const start=document.querySelector('#start'),end=document.querySelector('#end'),result=document.querySelector('#result'),error=document.querySelector('#error');function valid(){return start.validity.valid&&end.validity.valid&&start.value&&end.value&&end.value>=start.value}function draw(){let days=['M','T','W','T','F','S','S'].map(d=>'<span>'+d+'</span>').join('')+'<span></span>';for(let i=1;i<=30;i++){let iso='2027-06-'+String(i).padStart(2,'0'),range=valid()&&iso>=start.value&&iso<=end.value,edge=range&&(iso===start.value||iso===end.value);days+='<span class="'+(edge?'endpoint':range?'in-range':'')+'">'+i+'</span>'}document.querySelector('#calendar').innerHTML=days;document.querySelector('#range-caption').textContent=valid()?start.value+' to '+end.value:'Choose a valid range to highlight it.'}function changed(){result.textContent='';error.textContent='';start.removeAttribute('aria-invalid');end.removeAttribute('aria-invalid');draw()}[start,end].forEach(el=>el.oninput=changed);document.querySelector('form').onsubmit=e=>{e.preventDefault();let bad;if(!start.value||!start.validity.valid){error.textContent='Choose a departure date in June 2027.';bad=start}else if(!end.value||!end.validity.valid){error.textContent='Choose a return date in June 2027.';bad=end}else if(end.value<start.value){error.textContent='Return must be on or after departure.';bad=end}if(bad){bad.setAttribute('aria-invalid','true');bad.focus();result.textContent='';return}const days=(Date.parse(end.value+'T00:00:00Z')-Date.parse(start.value+'T00:00:00Z'))/86400000+1;result.textContent=days+' calendar '+(days===1?'day':'days')+', including departure and return. Local draft: '+start.value+' to '+end.value+'. No policy created.'};draw();</script></html>
В этом документе используются нативные элементы управления датой браузера. В отдельном примере Duet применяется средство выбора дат основной системы; не путайте его с автономным пакетом выбора дат с открытым исходным кодом.
Проверьте пару средств выбора дат основной системы
Два поля с метками, даты июня в заданных пределах, проверка порядка дат, фокус на поле с ошибкой и подсчёт календарных дней включительно.
html
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Date range draft</title>
<link rel="stylesheet" href="https://cdn.duetds.com/api/css/5.1.4/lib/duet.min.css">
<script type="module" src="https://cdn.duetds.com/api/components/10.7.0/lib/duet/duet.esm.js"></script>
<style>main{max-width:680px;margin:auto;padding:28px}.range{display:grid;grid-template-columns:1fr 1fr;gap:20px}@media(max-width:576px){.range{grid-template-columns:1fr}}</style>
<main><h1>Choose a travel period</h1><p>Demonstration only. No insurance is quoted or purchased.</p><div class="range">
<duet-date-picker id="start" label="Departure" required expand value="2027-06-10" min="2027-06-01" max="2027-06-30"></duet-date-picker>
<duet-date-picker id="end" label="Return" required expand value="2027-06-17" min="2027-06-01" max="2027-06-30"></duet-date-picker></div>
<duet-button id="prepare" variation="primary">Prepare date range</duet-button>
<p id="result" role="status"></p></main><script type="module">
await customElements.whenDefined('duet-date-picker');
const start=document.querySelector('#start'),end=document.querySelector('#end'),result=document.querySelector('#result');
function error(picker){const v=picker.validity;if(!picker.value)return 'Choose a date.';if(v&&!v.valid)return 'Choose a valid date in June 2027.';return '';}
function validate(){start.error=error(start);end.error=error(end);if(!start.error&&!end.error&&end.value<start.value)end.error='Return must be on or after departure.';return !start.error&&!end.error;}
[start,end].forEach(p=>p.addEventListener('duetChange',()=>{result.textContent='';validate()}));
document.querySelector('#prepare').addEventListener('click',async()=>{if(!validate()){await(start.error?start:end).setFocus();return;}const a=Date.parse(start.value+'T00:00:00Z'),b=Date.parse(end.value+'T00:00:00Z');result.textContent=`Local range: ${start.value} to ${end.value}, ${(b-a)/86400000+1} calendar days inclusive. No policy created.`;});
</script></html>
Здесь используется @duetds/components через CDN основной системы, а не @duetds/date-picker 1.x.
Диапазон с одинаковыми начальной и конечной датами составляет один календарный день включительно.
При подсчёте используется арифметика UTC, чтобы избежать смещения из-за перехода на летнее время.
04
Дизайн-система Duet Настройка агента
Отправная точка для реализации
Полная композиция через CDN основной системы с версиями, явно указанными на проверенной странице интеграции. В общедоступных снимках поиска есть и более новые версии; зафиксируйте совместимый выпуск, не предполагая, что здесь приведена последняя версия. Шрифты здесь не распространяются.
html
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Date range draft</title>
<link rel="stylesheet" href="https://cdn.duetds.com/api/css/5.1.4/lib/duet.min.css">
<script type="module" src="https://cdn.duetds.com/api/components/10.7.0/lib/duet/duet.esm.js"></script>
<style>main{max-width:680px;margin:auto;padding:28px}.range{display:grid;grid-template-columns:1fr 1fr;gap:20px}@media(max-width:576px){.range{grid-template-columns:1fr}}</style>
<main><h1>Choose a travel period</h1><p>Demonstration only. No insurance is quoted or purchased.</p><div class="range">
<duet-date-picker id="start" label="Departure" required expand value="2027-06-10" min="2027-06-01" max="2027-06-30"></duet-date-picker>
<duet-date-picker id="end" label="Return" required expand value="2027-06-17" min="2027-06-01" max="2027-06-30"></duet-date-picker></div>
<duet-button id="prepare" variation="primary">Prepare date range</duet-button>
<p id="result" role="status"></p></main><script type="module">
await customElements.whenDefined('duet-date-picker');
const start=document.querySelector('#start'),end=document.querySelector('#end'),result=document.querySelector('#result');
function error(picker){const v=picker.validity;if(!picker.value)return 'Choose a date.';if(v&&!v.valid)return 'Choose a valid date in June 2027.';return '';}
function validate(){start.error=error(start);end.error=error(end);if(!start.error&&!end.error&&end.value<start.value)end.error='Return must be on or after departure.';return !start.error&&!end.error;}
[start,end].forEach(p=>p.addEventListener('duetChange',()=>{result.textContent='';validate()}));
document.querySelector('#prepare').addEventListener('click',async()=>{if(!validate()){await(start.error?start:end).setFocus();return;}const a=Date.parse(start.value+'T00:00:00Z'),b=Date.parse(end.value+'T00:00:00Z');result.textContent=`Local range: ${start.value} to ${end.value}, ${(b-a)/86400000+1} calendar days inclusive. No policy created.`;});
</script></html>
Запрос для вашего проекта
Промпт для AI-агента
Создайте диапазон дат Duet основной системы с помощью двух полей выбора даты с метками, значений ISO и проверок корректности. Отдельно проверьте порядок дат, при ошибках вызывайте setFocus и используйте явно обозначенное основное действие для локальной подготовки. Не подключайте к той же странице автономный пакет выбора дат.
05
Дизайн-система Duet Источники и область применения
Независимая интерпретация OpenDesign. Самостоятельный планировщик; полис, бронирование или предложение не создаются. У основной системы Duet и её отдельного средства выбора дат с открытым исходным кодом разные контракты.