# Duet 设计系统

## Duet 设计系统 界面预览

设置出发和返程日期，测试无效日期范围，并计算包含首尾日期的天数。

## Duet 设计系统 设计参考

Duet 公开文档快照：主系统组件、LocalTapiola 默认令牌，以及文档中说明的 10.7.0/CDN 集成方式。这不是独立的 @duetds/date-picker 1.x 软件包。部分圆角令牌仍标记为审核中。

### 默认主题与 Turva 主题角色

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| --color-primary | #0077b3 | LocalTapiola 操作色。 |
| --color-primary-dark | #004b81 | 较深的交互状态。 |
| --color-primary-darker | #00619b | 链接角色。 |
| --color-primary-light | #c6e1ee | 范围色调。 |
| --color-secondary | #082a4d | 正文文本。 |
| --color-primary-turva | #e61740 | Turva 操作色，属于独立主题。 |
| --color-danger | #d61c52 | 验证。 |
| --radius-default | 4px | 输入框与容器。 |
| --radius-pill | 20rem | 按钮。 |
| --radius-medium | 8px; In Review | 不要宣称已可用于生产环境。 |
### 间距令牌

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| --space-xxx-small | 2px | 紧密。 |
| --space-xx-small | 4px | 紧密。 |
| --space-x-small | 8px | 紧凑。 |
| --space-small | 12px | 小。 |
| --space-medium | 16px | 默认。 |
| --space-large | 20px | 组件之间。 |
| --space-x-large | 28px | 较大间距。 |
| --space-xx-large | 36px | 按钮水平内边距。 |
| --space-xxx-large | 48px | 区块。 |
| --size-layout-max-width | 1110px | 文档中规定的布局最大宽度。 |
### 字体阶梯

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| $font-size-medium | 16px / 1rem | 正文与 H5。 |
| $font-size-large | 20px / 1.25rem | H4. |
| $font-size-x-large | 24px / 1.5rem | H3. |
| $font-size-xx-large | 36px / 2.25rem | H2. |
| $font-size-xxx-large | 48px / 3rem | H1. |
| $font-size-xxxx-large | 72px / 4.5rem | H0. |
### 日期选择器约定

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| value / min / max | YYYY-MM-DD strings | 存储值使用 ISO 格式，而非显示文本。 |
| duetChange | detail.value and detail.valueAsDate | 已选日期事件。 |
| validity | badInput / valueMissing / rangeOverflow / rangeUnderflow / customError | 接受前先检查。 |
| setFocus() | Promise<void> | 聚焦输入框；不是宿主的 focus()。 |
| show() / hide() | Promise<void> | 日历与焦点管理。 |
| language | Deprecated | 使用 html lang。 |
| disallow-weekends | Boolean | 组件级排除。 |
### 操作属性

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| variation="primary" | Main action | 每个视图只设置一个主要操作。 |
| submit | Boolean, default false | 明确选择启用表单提交。 |
| url | Anchor destination | 导航语义。 |
| expand | 100% container | 宽按钮。 |
| theme | default / turva / inherited empty | 品牌选择。 |
| margin="none" | Remove automatic margin | 应用控制间距。 |
## Duet 设计系统 完整指南

实用指南，介绍如何为真实界面选择、应用和评审此系统。

完整指导、实现起点和源代码链接。

## Duet 设计系统 Duet 主系统与开源选择器相互独立

### 在示例中保留软件包约定

> 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 并非通用的 8px 间距体系

除了常见的 2 的幂次值，间距体系还包含 12、20、28 和 36px。这些中间间距对于匹配系统的表单节奏很重要。宽屏布局中，两个字段之间使用 20px 间距；改为纵向排列时，标签应始终与对应字段保持关联。示例中的断点由应用自行决定。

默认控件使用 4px 圆角，按钮则使用药丸形圆角。令牌参考中的一些较新容器圆角值明确标记为审核中；本指南保留这一状态，不会把所有列出的令牌都说成已获批准。

阅读配套规范

## Duet 设计系统 存储 ISO 值，并说明可见格式

### 不要将本地化的显示字符串解析为数据

组件的值和边界使用 YYYY-MM-DD 格式。其已选日期事件同时包含字符串和 Date 对象。比较和存储时应保留字符串；本地化的字段显示属于呈现层面。文档语言是当前的本地化入口，而旧的语言属性已弃用。

示例仅允许选择 2027 年 6 月内的日期。这个固定区间便于稳定地进行测试，并明确属于示例约束。生产表单应根据产品规则确定允许的日期范围，并在字段旁说明该规则。

阅读配套规范

## Duet 设计系统 日期有效，不代表日期范围也有效

### 每个字段有效后，还要检查日期之间的关系

组件的有效性对象描述各个字段的错误。晚于返程日期的出发日期仍可能是有效的日历日期，因此应用必须单独检查两个日期的关系。原生示例会设置明确的错误，并使用 setFocus()——这是文档说明的、可聚焦内部输入框的方法。

计数采用包含首尾日期的方式：出发和返回在同一天时，计为一个日历日。将 ISO 日期转换为 UTC 午夜时间戳，可以避免夏令时变化导致一个日历日间隔变成小数时长。此计算属于应用原有逻辑，并非 Duet 的定价规则。

1. 如果端点缺失、格式错误或超出范围，则拒绝处理。

2. 如果返回日期早于出发日期，则拒绝处理。

3. 将焦点移至需要更正的字段。

4. 只有两项检查都通过后，才计算包含首尾日期的日期范围。

阅读配套规范

## Duet 设计系统 主要操作不代表提交表单

### Duet 使用显式的 submit 属性

主系统中的按钮通过 submit 属性选择提交表单；该属性默认为 false。示例组合使用点击操作来准备本地结果，因此无需设置此属性。目标地址应设置在 url 中，这样会采用链接语义。

任一输入发生变化时，预览都会清除已准备的提示。否则，提示可能会让人误以为确认的是访问者已经替换的值。这里没有显示加载动画，因为没有异步操作。

- 仅使用一个主要准备操作。

- 说明结果仅在本地生成。

- 不要重复使用表示成功的措辞，以免让人误以为已创建保单。

阅读配套规范

## Duet 设计系统 将主系统资源锁定为一组兼容版本

### 此示例是有文档记录的版本快照，并不承诺使用最新版本

查阅过的用法页面提供了 10.7.0 版组件和 CSS 5.1.4；搜索索引快照中则已提及更新的版本。此示例有意将直接查阅到的这一对版本搭配使用。用于生产环境时，应选定一个版本，并确认与之匹配的 CSS、字体和组件行为。

日期字段是自定义元素。调用方法前，请等待其定义完成；将监听器附加到 duetChange，并读取文档说明的负载，不要假定它会触发原生 change 事件。不要在同一页面上混用主系统 CDN 模块和独立日期选择器：二者定义了相同的标签。

阅读配套规范

## Duet 设计系统组件示例

### 完整的独立交互示例

将完整文档保存为 index.html，然后在浏览器中打开。离线 HTML 示例使用原生日期输入框和经核实的 Duet 默认主题值；它不是 Duet 日历运行时。

```
<!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 示例使用主系统的日期选择器；不要将其与独立的开源日期选择器包混淆。

### 验证一对主系统日期选择器

两个带标签的字段、六月日期范围限制、端点顺序校验、将焦点移至无效的日期选择器，以及包含首尾日期的日期计数。

```
<!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>
```

代码参考

- 此示例通过主系统 CDN 使用 @duetds/components，而非 @duetds/date-picker 1.x。

- 同一天的日期范围按包含首尾日期的方式计为一个日历日。

- UTC 运算可避免计数受夏令时偏移影响。

## Duet 设计系统 Agent 设置

### 实现起点

完整的主系统 CDN 组合使用了查阅过的集成页面中明确列出的版本。公开搜索快照也显示了更新的版本；请锁定兼容版本，不要默认这里列出的就是最新版本。此处不再分发字体。

```
<!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>
```

### 适用于你项目的提示词

```
使用两个带标签的日期选择器、ISO 值和有效性检查，构建 Duet 主系统日期范围。分别校验端点顺序，在出错时调用 setFocus，并使用明确的主要本地准备操作。不要在同一页面中加入独立日期选择器包。
```

## Duet 设计系统 来源与范围

独立的 OpenDesign 参考示例。原始行程规划器；不会创建保单、预订或报价。Duet 主系统与其独立的开源日期选择器采用不同的使用约定。

- 设计令牌 主题颜色、间距和令牌状态。

- 排版 字号比例及品牌字体区分。

- 日期选择器 ISO 值、校验和方法。

- Button 提交选项与样式变体。

- 组件用法 主系统 CDN 和包。

- 独立日期选择器 单独的开源发行版本；本示例并非以其代码为目标。

## Duet 设计系统 相关资源

### Elastic UI

构建一个 Elastic UI 日志浏览器，采用经过验证的 Borealis 尺度、原生表格和搜索控件、严重程度筛选、本地保存的事件，以及完整的 React 源代码。

### Emplifi Soul

阅读 Soul 的数据表规范，了解其 6 像素间距、分层配色角色和紧凑的 Inter 字体。比较批量选择、缺失数据表示和汇总行，然后查看经过核实的私有 React 包入口。

### Esri Calcite

Calcite 的实用参考，聚焦于空间工具：语义化界面表面和蓝色操作、紧凑的检查面板、真实的输入 API，以及完整的本地站点审查示例。


- [实现参考](https://www.duetds.com/components/date-picker/)
- [来源](https://www.duetds.com/tokens/)
- [来源](https://www.duetds.com/typography/)
- [来源](https://www.duetds.com/components/date-picker/)
- [来源](https://www.duetds.com/components/button/)
- [阅读配套规范](https://www.duetds.com/using-components/)
- [阅读配套规范](https://www.duetds.com/tokens/)
- [阅读配套规范](https://www.duetds.com/typography/)
- [阅读配套规范](https://www.duetds.com/components/date-picker/)
- [阅读配套规范](https://www.duetds.com/components/button/)
- [代码参考](https://www.duetds.com/components/date-picker/)
- [设计令牌 主题颜色、间距和令牌状态。](https://www.duetds.com/tokens/)
- [排版 字号比例及品牌字体区分。](https://www.duetds.com/typography/)
- [日期选择器 ISO 值、校验和方法。](https://www.duetds.com/components/date-picker/)
- [Button 提交选项与样式变体。](https://www.duetds.com/components/button/)
- [组件用法 主系统 CDN 和包。](https://www.duetds.com/using-components/)
- [独立日期选择器 单独的开源发行版本；本示例并非以其代码为目标。](https://github.com/duetds/date-picker)
