企业产品设计 · 新资源

Duet 设计系统.

通过日期范围工作流,研究 Duet 的 LocalTapiola 色彩和间距比例;其中包括 ISO 日期值、明确的范围验证、组件焦点方法,以及独立的开源选择器边界。

OpenDesign 指南8 个章节39 条参考条目原始应用指南
01

Duet 设计系统 界面预览

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

旅行日期规划器打开完整示例
离线 HTML 解读,使用原生日期输入框和经验证的 Duet 默认主题值;这不是 Duet 日历运行时。· 实现参考
02

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-default4px输入框与容器。
--radius-pill20rem按钮。
--radius-medium8px; In Review不要宣称已可用于生产环境。

间距令牌

来源
名称 / 规则值 / 配置用途
--space-xxx-small2px紧密。
--space-xx-small4px紧密。
--space-x-small8px紧凑。
--space-small12px小。
--space-medium16px默认。
--space-large20px组件之间。
--space-x-large28px较大间距。
--space-xx-large36px按钮水平内边距。
--space-xxx-large48px区块。
--size-layout-max-width1110px文档中规定的布局最大宽度。

字体阶梯

来源
名称 / 规则值 / 配置用途
$font-size-medium16px / 1rem正文与 H5。
$font-size-large20px / 1.25remH4.
$font-size-x-large24px / 1.5remH3.
$font-size-xx-large36px / 2.25remH2.
$font-size-xxx-large48px / 3remH1.
$font-size-xxxx-large72px / 4.5remH0.

日期选择器约定

来源
名称 / 规则值 / 配置用途
value / min / maxYYYY-MM-DD strings存储值使用 ISO 格式,而非显示文本。
duetChangedetail.value and detail.valueAsDate已选日期事件。
validitybadInput / valueMissing / rangeOverflow / rangeUnderflow / customError接受前先检查。
setFocus()Promise<void>聚焦输入框;不是宿主的 focus()。
show() / hide()Promise<void>日历与焦点管理。
languageDeprecated使用 html lang。
disallow-weekendsBoolean组件级排除。

操作属性

来源
名称 / 规则值 / 配置用途
variation="primary"Main action每个视图只设置一个主要操作。
submitBoolean, default false明确选择启用表单提交。
urlAnchor destination导航语义。
expand100% container宽按钮。
themedefault / turva / inherited empty品牌选择。
margin="none"Remove automatic margin应用控制间距。
03

Duet 设计系统 完整指南

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

DESIGN-duet.md

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

下载指南

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

在示例中保留软件包约定

Duet 通过主题化组件和基础规范服务于 LocalTapiola 与 Turva。单独发布的 @duetds/date-picker 项目则是范围更窄的开源发行版。标签名称相同,并不意味着它的属性、本地化设置或 CSS 变量可以与当前主设计系统互换使用。

摘自本 OpenDesign 指南

本页的集成代码面向主系统。离线预览使用原生日期输入框,并根据链接中的 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 日历运行时。

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>

代码参考

  • 此示例通过主系统 CDN 使用 @duetds/components,而非 @duetds/date-picker 1.x。
  • 同一天的日期范围按包含首尾日期的方式计为一个日历日。
  • UTC 运算可避免计数受夏令时偏移影响。
04

Duet 设计系统 Agent 设置

实现起点

完整的主系统 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>

适用于你项目的提示词

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

Duet 设计系统 来源与范围

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