# Suomi.fi 设计系统

## Suomi.fi 设计系统 界面预览

试用 PDF、拒绝不受支持的文件，并移除所选文档。文件会保留在浏览器中。

## Suomi.fi 设计系统 设计参考

官方文档、经检查的设计令牌仓库，以及 suomifi-ui-components 18.0.0 元数据。此预览为原创解读，并非原生 React 运行时呈现。

### 排版

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| Family | Source Sans 3 | 18 版；17 版及更早版本使用 Source Sans Pro。需单独加载字体。 |
| Body text | 18px | 移动端和桌面端使用相同默认值。 |
| Small body | 16px | 文本的 smallScreen 样式处理，并非自动应用的视口规则。 |
| Lead | 22px / 20px smallScreen | 每个视图只使用一段引言；不要将 lead 用于每个段落。 |
| Weights | 300 / 400 / 600 | 组件包所需的字体资源。 |
### 间距与界面表面

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| Space scale | 5, 10, 15, 20, 25, 30, 40, 60, 80px | 经检查的仓库 JSON 中，从 xxs 到 xxxxl。 |
| Inset scale | 2, 4, 6, 8, 10, 16, 20px | insetXxs 至 insetXxl；这不是外部间距刻度。 |
| Basic radius | 2px | 仓库中的 radius.basic；不要据此推断卡片应使用较大的圆角。 |
| Highlight base | hsl(212, 63%, 45%) | 默认交互蓝色。 |
| Highlight dark 1 | hsl(212, 63%, 37%) | 更深的交互色角色。 |
| Highlight light 2 | hsl(212, 63%, 90%) | 浅色界面表面角色；请针对所选文本检查对比度。 |
### FileInput 布局与语言

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| Default maximum width | 600px | 组件专用宽度，不是整个网站的网格宽度。 |
| fullWidth | Boolean layout option | 表单需要时，让组件在父容器内拉伸。 |
| smallScreen | Explicit compact presentation option | 不要仅凭此属性臆造全局自动断点。 |
| Single selection | Chosen file replaces picker and drop area | 多选是另一种模式，所选文件会列在下方。 |
| Required copy roles | labelText, hintText, dragAreaText, removeFileText, addedFileAriaText | 按角色进行本地化；仅显示文件名并不能构成完整的无障碍播报。 |
## Suomi.fi 设计系统 完整指南

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

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

## Suomi.fi 设计系统 实现边界

### React 组件系统

> Suomi.fi 提供 React 个通过 styled-components 设置样式的组件，以及一个独立的设计令牌包。18 版将预期字体切换为 Source Sans 3。现有的 17 版页面可能仍使用 Source Sans Pro；如果不先确认包版本就比较这些截图，可能会误以为需要调整字体。

嵌入式预览仍是对文档所述单文件模式的 HTML/CSS 解读，并已明确标注。下方新增了一个独立的原生 React FileInput 示例，其中包含已核实的回调、受控值、依赖项和字体导入。这是完成同一本地文件选择任务的两种实现；原生示例不会追溯性地将嵌入式 HTML 预览变成 React 组件。

阅读配套规范

## Suomi.fi 设计系统 字体排印与字体加载

### 确保正文易于阅读

移动端和桌面端均使用文档所述的 18px 正文字号。视口较窄并不意味着可以缩小所有表单说明文字。小号文本样式为 16px，适用于次要信息。引导文字为 22px，使用紧凑样式时缩小至 20px；仅用于页面引言。

比较换行效果前，请先加载所需的字体字重。组件包预期使用 Source Sans 3 字体资源，而此独立预览特意使用 Arial。预览说明中已标明这一替换，不应将其描述为原生组件的精确呈现。

阅读配套规范

## Suomi.fi 设计系统 间距与宽度

### 两种不同的间距刻度

设计令牌来源将普通间距与内边距间距分开。经检查的 JSON 中，普通间距 l 为 25px；不要照搬将 l 描述为 32px 的旧 README 示例。内边距间距从 2px 起步，按另一组不同的数值递增。生成样式时，请使用已安装版本实际导出的值。

FileInput 的文档规定最大宽度为 600px，并提供 fullWidth 选项。这是组件约束，并不能证明页面容器普遍适用 600px 宽度。文档接收表单可以将字段放入更宽的布局中，同时让说明文字靠近其标签。

阅读配套规范

## Suomi.fi 设计系统 文件选择器结构

### 文案是组件契约的一部分

可见标签应说明所需的文档。提示信息应在用户打开文件选择器之前，说明支持的格式、文件大小和数量。拖放区域提供了指针操作的快捷方式，但不能取代文件输入框。官方指南将选择控件描述为隐藏的原生文件输入框的标签。

单文件模式下，所选文档会替代选择器和拖放区域。在文档旁保留清晰的移除操作。多文件模式使用不同的列表布局；不要在单文件请求中悄悄追加其他文件。

- labelText：说明所需的证据或文档，而不只是写“上传”。

- removeFileText：让用户清楚了解在文件场景下移除操作的含义。

- addedFileAriaText：在读出文件名之前，提供所选文件的上下文。

阅读配套规范

## Suomi.fi 设计系统 选择与恢复

### 本地附件状态机

此示例接受一个不超过 5,000,000 字节的 PDF。这些是为本次工作坊场景设定的应用规则，不是软件包默认值，也不是 Suomi.fi 的官方上传政策。浏览器的 accept 筛选仅是文件选择器的提示；此示例会在选择或拖放文件后独立检查文件名和大小。

元数据无效时，文件选择器仍可使用，并会说明如何恢复。选择有效文件后，示例会使用 textContent 显示其名称、报告字节数，并将焦点移至移除控件。移除文件时会清空底层输入框的值、恢复文件选择器并将焦点移回，从而允许再次选择同一个文件。

1. 仅选择或拖放一个文件；如选择多个文件，则显示明确的错误消息。

2. 检查本地示例规则，并播报错误信息或所选文件名。

3. 移除附件并恢复原始的文件选择操作。

阅读配套规范

## Suomi.fi 设计系统 原生集成

### 安装匹配的依赖项

已发布的 18.0.0 版本 FileInput 实现会通过 onChange 传入 FileList，而不是 React change 事件。受控 value 是文件项对象数组；此示例会将每个已接受的 File 映射为 { file }。内置移除操作更改列表时也会调用同一个回调，因此空 FileList 会清空受控数组。这些契约已在已发布的 dist/esm/core/Form/FileInput/FileInput.js 模块中核实。

从首次渲染开始就将 value 设为受控状态，初始值为空数组。将传入的 FileList 映射到数组之前，先进行验证。此示例会拒绝文件名不以 .pdf 结尾或大小超过 5,000,000 字节的文件，并在拒绝时清空所选文件。这些是本地示例规则，不是安全检查，也不是 Suomi.fi 政策。在接受实际上传之前，仍需在服务器端验证文件内容。

下面的原生示例会加载 400 和 600 字重的 Source Sans 3，将 React 挂载到明确指定的根元素，并通过该库使用 styled-components。请显式包含 tslib：18.0.0 的 ESM 模块会导入其辅助函数，而缺少此依赖时，干净构建会失败。下面固定版本的配置包含了所需的运行时和字体资源。此默认主题示例不需要额外的主题提供器。

阅读配套规范

### 使用已核实的令牌导出项

官方样式指南展示了 suomifiDesignTokens.spacing 和 suomifiDesignTokens.colors。下面的示例使用这些导出路径构建自定义包装器；它不是 FileInput 内部实现的替代品。

```
import { suomifiDesignTokens } from 'suomifi-design-tokens';
// Real exported token paths. Use values instead of copying README example numbers.
const attachmentStyle = {
  maxWidth: 600,
  padding: suomifiDesignTokens.spacing.m,
  borderColor: suomifiDesignTokens.colors.highlightBase
};
```

代码参考

阅读配套规范

## Suomi.fi 设计系统 无障碍与响应式使用

### 无需拖放即可操作字段

即使拖放区域十分醒目，也要保留可通过键盘操作的原生文件输入框。使用 aria-describedby 关联提示信息和错误信息。播报文件选择和移除状态的变化，而不必重新朗读整个页面。如果表单中可能出现多个移除按钮，应让按钮标明对应的所选文件。

官方的 smallScreen prop 提供紧凑布局，但不会定义全局断点。此独立预览会将内边距调整为应用专用的 480px，同时保持正文文本为 18px。请测试长文件名、缩放和翻译后的标签；文件名应能换行，且不能导致整个页面变宽。

阅读配套规范

## Suomi.fi 设计系统 生产环境职责

### 已选择的文件不等于已上传的文件

此演示不会读取文档内容，也不会发送请求。所选文件的名称和字节数不能证明文档有效、已存储或已获接受。实际服务需要具备服务器端验证、上传进度、可恢复的失败处理、保留规则，以及与真实响应相关联的确认信息。

该组件代码采用 MIT 许可证。代码许可证并不授权独立项目将自身描述为芬兰政府服务，或使用受限的机构标识。进行实验时请使用中性的项目名称；如需官方实现，请先获取适当的品牌使用指导。

阅读配套规范

## Suomi.fi 设计系统组件示例

### 使用官方设计令牌导出项

已核实自定义封装组件的间距和颜色导出路径。

```
import { suomifiDesignTokens } from 'suomifi-design-tokens';
// Real exported token paths. Use values instead of copying README example numbers.
const attachmentStyle = {
  maxWidth: 600,
  padding: suomifiDesignTokens.spacing.m,
  borderColor: suomifiDesignTokens.colors.highlightBase
};
```

代码参考

### 选择并移除单个文件

完整的本地示例。PDF 格式和大小限制是场景设定；不会上传文件。

```
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Attachment example</title><style>*{box-sizing:border-box}body{margin:0;padding:30px 20px;font:18px/1.5 Arial,sans-serif;color:#202020;background:#fff}main{max-width:600px;margin:auto}h1{font-size:32px;line-height:1.2;font-weight:400;margin:0 0 20px}p{margin:0 0 20px}label{font-weight:600;display:block}small{display:block;font-size:16px}.drop{margin-top:15px;border:2px dashed hsl(212,63%,45%);padding:30px;text-align:center;background:hsl(212,63%,90%)}button,input::file-selector-button{font:600 18px Arial;padding:12px 20px;border:1px solid hsl(212,63%,37%);border-radius:2px;background:hsl(212,63%,45%);color:white;cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid #202020;outline-offset:3px}input{width:100%;font-size:16px;margin:15px 0}input::file-selector-button{margin-right:10px}.file{border:1px solid #777;padding:20px;margin:20px 0;overflow-wrap:anywhere}.file button{margin-top:15px;background:white;color:hsl(212,63%,37%)}#error{color:#a32020;font-weight:bold}footer{font-size:14px;margin-top:30px;border-top:1px solid #ccc;padding-top:15px}[hidden]{display:none!important}@media(max-width:480px){body{padding:20px 15px}.drop{padding:20px}h1{font-size:28px}}</style><main><small>COMMUNITY WORKSHOP · ATTACHMENT</small><h1>Add your project outline</h1><p>Choose one PDF up to 5 MB. This local example checks file metadata; it does not read or send the document.</p><div id="picker"><label for="file">Project outline</label><small id="hint">One PDF file. Maximum 5 MB (5,000,000 bytes).</small><input id="file" type="file" accept=".pdf,application/pdf" aria-describedby="hint error"><div class="drop" id="drop">Or drop one PDF here</div></div><p id="error" role="alert" hidden></p><section id="selected" class="file" hidden aria-label="Selected attachment"><strong id="name"></strong><small id="size"></small><button id="remove" type="button">Remove file</button></section><p id="status" role="status">No file selected.</p><footer>OpenDesign interpretation of the Suomi.fi attachment pattern. Arial fallback; no official identity or React component runtime. PDF and 5 MB are example rules.</footer></main><script>const input=document.querySelector('#file'),picker=document.querySelector('#picker'),selected=document.querySelector('#selected'),error=document.querySelector('#error'),status=document.querySelector('#status');function choose(files){error.hidden=true;if(files.length!==1){error.textContent='Choose exactly one file.';error.hidden=false;input.value='';return}const file=files[0];if(!/\.pdf$/i.test(file.name)||file.size>5000000){error.textContent='Choose a PDF no larger than 5 MB.';error.hidden=false;input.setAttribute('aria-invalid','true');input.value='';return}input.removeAttribute('aria-invalid');document.querySelector('#name').textContent=file.name;document.querySelector('#size').textContent=file.size.toLocaleString('en')+' bytes · selected locally';document.querySelector('#remove').setAttribute('aria-label','Remove '+file.name);picker.hidden=true;selected.hidden=false;status.textContent='Selected file: '+file.name;document.querySelector('#remove').focus()}input.addEventListener('change',()=>{if(input.files.length)choose(input.files)});document.querySelector('#drop').addEventListener('dragover',e=>e.preventDefault());document.querySelector('#drop').addEventListener('drop',e=>{e.preventDefault();choose(e.dataTransfer.files)});document.querySelector('#remove').onclick=()=>{input.value='';picker.hidden=false;selected.hidden=true;error.hidden=true;input.removeAttribute('aria-invalid');status.textContent='File removed. Choose another file.';input.focus()};</script></html>
```

代码参考

- 不是原生 React 组件。

- 用于生产环境的服务应在服务器端验证文件内容。

### 使用受控验证的原生 FileInput

保存为 main.jsx。使用这些设置命令和这三个文件，即可复现原生 React 示例。

```
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
import { FileInput } from 'suomifi-ui-components';
import '@fontsource/source-sans-3/400.css';
import '@fontsource/source-sans-3/600.css';
import './app.css';
function App() {
  const [value, setValue] = useState([]);
  const [error, setError] = useState('');
  function change(files) {
    const next = Array.from(files);
    const invalid = next.length > 1 || next.some(file => !/\.pdf$/i.test(file.name) || file.size > 5000000);
    if (invalid) {
      setValue([]);
      setError('Choose one PDF no larger than 5 MB.');
      return;
    }
    setValue(next.map(file => ({ file })));
    setError('');
  }
  return <main><h1>Select a workshop document</h1><p>This local example does not upload or read the document.</p>
    <FileInput id="document" labelText="Workshop document" hintText="One PDF, maximum 5 MB (5,000,000 bytes)."
      inputButtonText="Choose PDF" dragAreaText="or drop a PDF here" removeFileText="Remove file"
      addedFileAriaText="Selected file" accept=".pdf" value={value} onChange={change}
      status={error ? 'error' : 'default'} statusText={error} />
    <p role="status">{value.length ? `${value[0].file.name} selected locally.` : 'No file selected.'}</p>
  </main>;
}
createRoot(document.getElementById('root')).render(<App />);
```

代码参考

- 验证由应用处理；原生组件提供文件选择、移除功能及可访问的文件控件。

### 原生示例页面布局

保存为 app.css。使用这些设置命令和这三个文件，即可复现原生 React 示例。

```
body{margin:0;font:18px/1.5 'Source Sans 3',sans-serif;color:#292929;background:#fff}main{max-width:600px;margin:0 auto;padding:24px}h1{font-size:32px;line-height:1.2}*{box-sizing:border-box}
```

代码参考

- 验证由应用处理；原生组件提供文件选择、移除功能及可访问的文件控件。

### 原生示例挂载入口及生成的资源

保存为 index.html。使用这些设置命令和这三个文件，即可复现原生 React 示例。

```
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Native Suomi.fi FileInput</title><link rel="stylesheet" href="./app.css"><div id="root"></div><script type="module" src="./app.js"></script></html>
```

代码参考

- 验证由应用处理；原生组件提供文件选择、移除功能及可访问的文件控件。

## Suomi.fi 设计系统 AI Agent 配置

### 实现起点

以下示例会构建原生 React 示例页面，其中包含本地 Source Sans 3 字体文件。已在 390px 和 1440px 宽度下测试无效格式、有效文件选择、内置移除功能，以及超出大小限制的 PDF。现有可下载预览 ZIP 是单独的 HTML/CSS 演绎版本；其 README 仍然准确。

```
npm install --save-exact suomifi-ui-components@18.0.0 react@18.3.1 react-dom@18.3.1 styled-components@6.5.3 tslib@2.8.1 @fontsource/source-sans-3@5.3.0
npm install --save-dev --save-exact esbuild@0.28.2
# Save main.jsx, app.css and index.html from the complete examples below.
npx esbuild main.jsx --bundle --outdir=dist --entry-names=app --loader:.woff=copy --loader:.woff2=copy
cp index.html dist/index.html
python3 -m http.server 8080 --directory dist
# Open http://localhost:8080
```

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

```
使用提供的原生 Suomi.fi FileInput 18.0.0 React 示例，并搭配 Source Sans 3 和锁定版本的运行时依赖，包括 tslib。将 value 设为由 { file } 对象组成的数组，并在 onChange 中直接接收 FileList；不要从该回调读取 event.target。接受本地文件前先验证格式和大小，处理移除文件时产生的空列表，并确保提示和错误文本始终可见。将嵌入式 HTML 预览视为单独标注的演绎版本。两种实现都不会上传文件；服务器端验证、上传进度和确认存储都需要实际的后端约定。
```

## Suomi.fi 设计系统 来源与范围

嵌入式预览和可下载 ZIP 是 OpenDesign HTML/CSS 演绎版本。单独提供的完整 React 示例使用 suomifi-ui-components 18.0.0，其 FileInput 的 value 和 onChange 约定已通过核实。两个示例都支持本地文件选择，但都不会将文档上传或存储到服务器。

- FileInput 使用指南 组件结构、文案用途、宽度以及单选/多选行为。

- 排版 Source Sans 3 与版本迁移。

- 文本组件 正文、小号和引导文本字号。

- 设计令牌来源 实际的间距、颜色和圆角值。

- 组件代码库 React 实现、依赖项和 MIT 许可证。

- 下载完整可运行示例 包含完整 HTML、本地 CSS/JavaScript 依赖、随附许可证以及确切启动说明的 ZIP 包。无需构建步骤。

- 已核实的 FileInput 18.0.0 实现 已发布的软件包源码：dist/esm/core/Form/FileInput/FileInput.js。已根据此版本核对受控 value 和 FileList 回调，并在单独的原生构建中进行了测试。

## Suomi.fi 设计系统 相关资源

### UNDP 设计系统

探索 UNDP 已发布的颜色与间距令牌、支持原生键盘操作的标签页，以及完整的工作坊阅读示例，其中包含本地记录的阅读进度。

### USWDS

将 USWDS 间距令牌、主题角色、语义表格和原生折叠面板结合起来，打造可搜索的机会对比体验。

### VA 设计系统

运用 VA 的语义令牌和原生 Process List，打造清晰的服务流程：展示步骤说明和含义明确的状态标签，并提供补充详情，同时保持主要路径清晰易读。


- [实现参考](https://designsystem.suomi.fi/components/fileinput/)
- [来源](https://designsystem.suomi.fi/components/text/)
- [来源](https://github.com/vrk-kpa/suomifi-design-tokens/blob/master/src/tokens.json)
- [来源](https://designsystem.suomi.fi/components/fileinput/)
- [阅读配套规范](https://github.com/vrk-kpa/suomifi-ui-components)
- [阅读配套规范](https://designsystem.suomi.fi/components/text/)
- [阅读配套规范](https://github.com/vrk-kpa/suomifi-design-tokens/blob/master/src/tokens.json)
- [阅读配套规范](https://designsystem.suomi.fi/components/fileinput/)
- [阅读配套规范](https://www.npmjs.com/package/suomifi-ui-components/v/18.0.0)
- [代码参考](https://designsystem.suomi.fi/styles)
- [阅读配套规范](https://designsystem.suomi.fi/styles)
- [代码参考](https://designsystem.suomi.fi/components/fileinput/)
- [代码参考](https://www.npmjs.com/package/suomifi-ui-components/v/18.0.0)
- [FileInput 使用指南 组件结构、文案用途、宽度以及单选/多选行为。](https://designsystem.suomi.fi/components/fileinput/)
- [排版 Source Sans 3 与版本迁移。](https://designsystem.suomi.fi/styles/typography/)
- [文本组件 正文、小号和引导文本字号。](https://designsystem.suomi.fi/components/text/)
- [设计令牌来源 实际的间距、颜色和圆角值。](https://github.com/vrk-kpa/suomifi-design-tokens/blob/master/src/tokens.json)
- [组件代码库 React 实现、依赖项和 MIT 许可证。](https://github.com/vrk-kpa/suomifi-ui-components)
- [已核实的 FileInput 18.0.0 实现 已发布的软件包源码：dist/esm/core/Form/FileInput/FileInput.js。已根据此版本核对受控 value 和 FileList 回调，并在单独的原生构建中进行了测试。](https://www.npmjs.com/package/suomifi-ui-components/v/18.0.0)
