Files
aue/docs/前端优化方案与设计规范.md
2026-06-03 13:16:30 +08:00

700 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端优化方案与设计规范文档
**基于**: `前端项目分析报告.md`
**日期**: 2026-05-13
**原则**: 简洁、高可读性、高可操作性、统一白色体系
---
## 第一部分:色彩规范优化方案(白色体系)
### 1.1 设计理念
采用**白色及白色不同色调作为唯一设计色彩体系**,遵循以下原则:
- 以白色为基础,通过不同的灰度层级区分信息重要程度和视觉层次
- 去除所有彩色渐变背景,回归简洁干净的商业软件风格
- 仅保留少量必要的功能色用于状态标识(成功/警告/错误)
- 确保所有文字与背景对比度满足 WCAG 2.1 AA 级标准(至少 4.5:1
### 1.2 完整色值定义
#### 基础色板(白 → 黑灰度层级)
```css
:root {
/* ===== 白色体系 - 背景色 ===== */
--bg-page: #FFFFFF; /* 页面背景 - 纯白 */
--bg-container: #F8F9FA; /* 容器背景 - 极浅灰 */
--bg-card: #FFFFFF; /* 卡片背景 - 纯白 */
--bg-hover: #F1F3F5; /* 悬停背景 - 浅灰 */
--bg-active: #E9ECEF; /* 激活背景 - 中浅灰 */
--bg-disabled: #F8F9FA; /* 禁用背景 */
--bg-header: #FFFFFF; /* 头部背景 - 纯白 */
--bg-sidebar: #F8F9FA; /* 侧边栏背景 */
/* ===== 白色体系 - 边框色 ===== */
--border-light: #E9ECEF; /* 浅边框 */
--border-default: #DEE2E6; /* 默认边框 */
--border-medium: #CED4DA; /* 中等边框 */
--border-dark: #ADB5BD; /* 深边框 */
/* ===== 白色体系 - 文字色 ===== */
--text-primary: #212529; /* 主要文字 (AA级: 16.9:1) */
--text-secondary: #495057; /* 次要文字 (AA级: 8.6:1) */
--text-tertiary: #6C757D; /* 辅助文字 (AA级: 5.2:1) */
--text-disabled: #ADB5BD; /* 禁用文字 */
--text-inverse: #FFFFFF; /* 反色文字(深色底上) */
/* ===== 功能色(仅状态标识)===== */
--color-success: #2B8A3E; /* 成功状态 (AA级: 5.0:1) */
--color-warning: #E67700; /* 警告状态 */
--color-error: #C92A2A; /* 错误状态 (AA级: 6.3:1) */
--color-info: #1C7ED6; /* 信息状态 */
/* ===== 功能色 - 浅色背景 ===== */
--bg-success: #EBFBEE; /* 成功浅底 */
--bg-warning: #FFF9DB; /* 警告浅底 */
--bg-error: #FFF5F5; /* 错误浅底 */
--bg-info: #E7F5FF; /* 信息浅底 */
/* ===== 主色调(品牌色 - 克制使用)===== */
--color-primary: #212529; /* 主操作色 - 深灰黑 */
--color-primary-hover: #495057; /* 悬停 */
--color-primary-active: #000000; /* 按下 */
--color-link: #1C7ED6; /* 链接色 - 仅用于超链接 */
/* ===== 阴影层级(替代彩色阴影)===== */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
--shadow-xl: 0 8px 32px rgba(0, 0, 0, 0.10);
}
```
### 1.3 组件色彩映射表
| 组件/区域 | 旧色值 | 新色值 | 说明 |
|----------|--------|--------|------|
| Header | `linear-gradient(#1890ff, #096dd9)` | `#FFFFFF` + `border-bottom: 1px solid --border-light` | 纯白头部 |
| 主按钮 | `linear-gradient(#1890ff, #096dd9)` | `#212529` (深灰黑) → 悬停 `#000` | 简洁深色按钮 |
| 次按钮 | `transparent` | `#FFFFFF` + 边框 | 白色边框按钮 |
| 菜单选中 | `linear-gradient(#1890ff, #40a9ff)` | `#F1F3F5` 背景 + `#212529` 文字 | 灰底黑字 |
| 卡片 | 渐变彩色背景 | `#FFFFFF` + `shadow-sm` 边框 | 纯白卡片 |
| 统计卡片 | 彩色图标背景 | `#F8F9FA` 图标底 + 深色文字 | 灰底卡片 |
| 标签 | 6色渐变 | 白底 + 深色边框 + 深色文字 | 统一标签样式 |
| 进度条 | 4色渐变 | 灰色系(`#212529 → #495057 → #ADB5BD` | 灰度进度 |
| 模态弹窗 | 彩色渐变更改 | `#FFFFFF` + `shadow-xl` | 纯白模态 |
| 表格头部 | `linear-gradient(#fafafa, #f5f5f5)` | `#F8F9FA` 纯色 | 统一表头 |
### 1.4 设计前后对比
```
【修改前】 【修改后】
┌──────────────────────────┐ ┌──────────────────────────┐
│ 🔵 蓝色渐变Header │ │ ⬜ 纯白Header + 底部灰色边线 │
├──────────────────────────┤ ├──────────────────────────┤
│ 🔵 蓝色菜单 │ │ ⬜ 灰色激活态 + 黑色文字 │
├──────────────────────────┤ ├──────────────────────────┤
│ 🟣 紫色渐变标题 │ │ ⬜ 灰色标题 │
├──────────────────────────┤ ├──────────────────────────┤
│ 🔴🟢🔵🟡 彩色统计卡片 │ │ ⬜ 统一灰色统计卡片 │
├──────────────────────────┤ ├──────────────────────────┤
│ 🔵 蓝色主按钮 │ │ ⬛ 深灰色主按钮 │
├──────────────────────────┤ ├──────────────────────────┤
│ 🟢🔵🟠🔴 彩色标签 │ │ ⬜ 统一白色标签 │
└──────────────────────────┘ └──────────────────────────┘
碎片化、多彩 统一、简洁、专业
```
---
## 第二部分:页面大小与信息密度优化
### 2.1 组件尺寸标准
| 尺寸级别 | 值 | 用途 | 示例 |
|:-------:|:--:|------|------|
| **XS** | 4px | 极小间距 | 图标与文字间距 |
| **SM** | 8px | 小间距 | 按钮内边距、标签间距 |
| **MD** | 12px | 中间距 | 卡片内边距、列表项间距 |
| **LG** | 16px | 大间距 | 区块间距 |
| **XL** | 20px | 特大间距 | 页面边距 |
| **2XL** | 24px | 区块分隔 | 主要内容区边距 |
### 2.2 关键组件尺寸优化
| 组件 | 当前尺寸 | 优化后 | 节省空间 |
|------|---------|--------|:------:|
| Header | 64px | **48px** | -25% |
| `main-tabs-bar` 按钮 | padding: 14px 20px | **padding: 8px 16px** | -35% |
| `exam-header` | ~80px | **56px** | -30% |
| `stat-card` | ~100px | **72px** | -28% |
| `exam-card` 内边距 | 24px | **16px** | -33% |
| 表格行高 | ~57px | **44px** | -23% |
| 表单垂直间距 | 24px | **16px** | -33% |
| 文件卡片 `file-card` | ~200×160 | **160×128** | -36% |
| 统计行 `stats-row` 间距 | 16px | **12px** | -25% |
### 2.3 信息密度提升策略
```css
/* 全局紧凑模式 */
:root {
--density-compact: 1; /* 紧凑系数0.75 = 紧凑1 = 标准 */
}
/* 应用示例 */
.ant-table {
font-size: 13px; /* 原14px */
}
.ant-table-thead > tr > th {
padding: 8px 12px; /* 原16px 16px */
}
.ant-table-tbody > tr > td {
padding: 8px 12px; /* 原16px 16px */
}
.ant-card-body {
padding: 16px; /* 原24px */
}
.ant-form-item {
margin-bottom: 16px; /* 原24px */
}
.ant-tabs-tab {
padding: 8px 16px; /* 原14px 20px */
}
```
### 2.4 信息布局优化
#### 优化前(低密度)
```
┌────────────────────────────────────────┐
│ exam-header (~80px 含大量留白) │
├────────────────────────────────────────┤
│ stats-row (~120px 含彩色图标) │
├────────────────────────────────────────┤
│ exam-card 标题 (~50px) │
│ ┌──────────────────────────────────┐ │
│ │ 表单 (24px 间距,大输入框) │ │
│ │ 字段1 │ │
│ │ ──24px── │ │
│ │ 字段2 │ │
│ │ ──24px── │ │
│ │ 字段3 │ │
│ └──────────────────────────────────┘ │
│ ──24px── │
│ 底部操作区 │
└────────────────────────────────────────┘
总高度: ~450px, 有效信息区域: ~35%
```
#### 优化后(高密度)
```
┌────────────────────────────────────────┐
│ 标题 + 操作 (56px紧凑单行) │
├────────────────────────────────────────┤
│ stats-row (80px精简统计卡片) │
├────────────────────────────────────────┤
│ card: 表单 (12px 间距) │
│ 字段1 字段2 │
│ 字段3 字段4 (两列布局) │
│ ──12px── │
│ 底部操作区 │
└────────────────────────────────────────┘
总高度: ~260px, 有效信息区域: ~55%
```
---
## 第三部分:页面组件统一性优化
### 3.1 组件体系设计
#### 通用布局组件(消除碎片化)
```
BasePage.vue ← 统一页面容器(替换各模块重复的 exam-page
├─ PageHeader.vue ← 统一页面标题栏(替换 exam-header
├─ TabsBar.vue ← 统一标签栏(替换 main-tabs-bar
├─ StatsRow.vue ← 统一统计卡片行(替换 stats-row + stat-card
├─ ContentCard.vue ← 统一内容卡片(替换 exam-card
└─ DataTable.vue ← 统一数据表格(包装 antd Table
```
### 3.2 组件规范定义
#### 3.2.1 PageHeader 组件规范
```vue
<!-- 统一页面标题栏 -->
<template>
<div class="page-header">
<div class="header-left">
<h1 class="page-title">{{ title }}</h1>
<p v-if="description" class="page-desc">{{ description }}</p>
</div>
<div class="header-right">
<slot name="actions" />
</div>
</div>
</template>
<style scoped>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px; /* 原 大padding → 紧凑 */
background: #FFFFFF;
border-bottom: 1px solid #E9ECEF;
min-height: 56px; /* 原 ~80px → 56px */
}
.page-title {
font-size: 16px; /* 原 20px+ → 16px */
font-weight: 600;
color: #212529;
margin: 0;
}
.page-desc {
font-size: 12px;
color: #6C757D;
margin: 4px 0 0 0;
}
</style>
```
#### 3.2.2 TabsBar 组件规范
```vue
<template>
<div class="tabs-bar">
<button
v-for="tab in tabs"
:key="tab.key"
:class="['tab-btn', { active: modelValue === tab.key }]"
@click="$emit('update:modelValue', tab.key)"
>
<span v-if="tab.icon" class="tab-icon">{{ tab.icon }}</span>
<span class="tab-text">{{ tab.label }}</span>
</button>
</div>
</template>
<style scoped>
.tabs-bar {
display: flex;
gap: 4px;
padding: 8px 16px;
background: #FFFFFF;
border-bottom: 1px solid #E9ECEF;
}
.tab-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px; /* 原 14px 20px → 紧凑 */
border: none;
border-radius: 6px;
background: transparent;
color: #495057;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.tab-btn:hover {
background: #F1F3F5;
color: #212529;
}
.tab-btn.active {
background: #F1F3F5;
color: #212529;
font-weight: 600;
}
</style>
```
#### 3.2.3 ContentCard 组件规范
```vue
<template>
<div class="content-card" :class="{ 'has-header': $slots.header }">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div class="card-body" :style="{ padding: padding }">
<slot />
</div>
</div>
</template>
<script setup>
defineProps({
padding: { type: String, default: '16px' }
})
</script>
<style scoped>
.content-card {
background: #FFFFFF;
border: 1px solid #E9ECEF;
border-radius: 8px; /* 原 12px → 8px */
box-shadow: 0 1px 2px rgba(0,0,0,0.04); /* 原大阴影 → 轻阴影 */
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #E9ECEF;
}
</style>
```
#### 3.2.4 StatsRow 组件规范
```vue
<template>
<div class="stats-row">
<div v-for="stat in stats" :key="stat.label" class="stat-card">
<span class="stat-value">{{ stat.value }}</span>
<span class="stat-label">{{ stat.label }}</span>
</div>
</div>
</template>
<style scoped>
.stats-row {
display: flex;
gap: 12px;
padding: 12px 20px;
background: #FFFFFF;
border-bottom: 1px solid #E9ECEF;
}
.stat-card {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px 12px;
background: #F8F9FA;
border-radius: 6px;
border: 1px solid #E9ECEF;
min-height: 72px; /* 原 ~100px → 72px */
}
.stat-value {
font-size: 22px; /* 原 28px+ → 22px */
font-weight: 700;
color: #212529;
}
.stat-label {
font-size: 12px;
color: #6C757D;
font-weight: 500;
}
</style>
```
#### 3.2.5 DataTable 组件规范
基于 Ant Design Vue Table 的封装,统一表头样式、行高、分页、空状态:
```vue
<!-- 统一表格组件 -->
<template>
<a-table
:dataSource="dataSource"
:columns="columns"
:loading="loading"
:pagination="paginationConfig"
:rowKey="rowKey"
size="small" /* 统一小尺寸 */
:locale="emptyLocale"
class="data-table"
@change="handleTableChange"
>
<template v-for="slot in Object.keys($slots)" #[slot]="scope">
<slot :name="slot" v-bind="scope" />
</template>
</a-table>
</template>
```
### 3.3 消除碎片化的实施路径
| 阶段 | 内容 | 涉及文件 |
|:----:|------|---------|
| 1 | 创建通用组件库 `BasePage`, `PageHeader`, `TabsBar`, `ContentCard`, `StatsRow`, `DataTable` | 新建6个组件 |
| 2 | 重构 `ReadModule.vue` 使用通用组件 | 修改1个文件 |
| 3 | 重构 `ManageModule.vue` 使用通用组件 | 修改1个文件 |
| 4 | 重构 `ExamModule.vue` 使用通用组件 | 修改1个文件 |
| 5 | 重构 `QAModule.vue` 使用通用组件 | 修改1个文件 |
| 6 | 重构 `MindModule.vue`, `DashboardModule.vue`, `PermissionModule.vue` | 修改3个文件 |
| 7 | 移除各模块中的重复样式统一到全局CSS变量 | 全局样式修改 |
---
## 第四部分:功能流程与操作流程优化
### 4.1 导航体验优化
**当前问题**:使用 `<a-select>` 下拉框选择功能模块,不符合主流习惯。
**优化方案**:改为顶部固定标签栏导航
```vue
<!-- 优化前下拉选择 -->
<a-select v-model:value="currentModule" style="width: 220px;">
<a-select-option value="read">📄 制度文件查看和学习</a-select-option>
...
</a-select>
<!-- 优化后顶部标签栏 -->
<div class="top-nav">
<button
v-for="mod in visibleModules"
:class="['nav-item', { active: currentModule === mod.key }]"
@click="currentModule = mod.key"
>
{{ mod.icon }} {{ mod.label }}
</button>
</div>
```
### 4.2 文件选择器流程简化
**当前流程3步**
```
选择知识库 → 搜索/浏览文件 → 选择版本 → 确认
```
**优化流程2步**
```
搜索/浏览文件(自动关联知识库)→ 点击选择(默认最新版本)→ 确认
```
### 4.3 出题流程优化
**当前流程6步**
```
选择知识库 → 选择文件 → 配置题型数量 → 设置难度 → 提交 → 等待轮询 → 查看结果
```
**优化流程4步**
```
选择文件 → 配置题型(带智能推荐)→ 提交 → 实时进度 → 查看结果
```
---
## 第五部分:设计规范文档(开发维护参考)
### 5.1 通用设计原则
1. **白色优先**:所有背景使用白色体系,彩色仅用于状态标识
2. **减法设计**:去除不必要的装饰元素(渐变、阴影、动画)
3. **信息优先**:内容区域最大化,装饰和留白最小化
4. **一致性**:所有页面使用统一的布局组件和间距体系
5. **可访问性**:所有文字对比度 ≥ 4.5:1WCAG AA级
### 5.2 CSS变量完整清单
```css
:root {
/* 间距系统 */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 12px;
--space-lg: 16px;
--space-xl: 20px;
--space-2xl: 24px;
--space-3xl: 32px;
/* 圆角系统 */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
/* 字体系统 */
--font-xs: 11px;
--font-sm: 12px;
--font-md: 13px;
--font-lg: 14px;
--font-xl: 16px;
--font-2xl: 18px;
--font-3xl: 22px;
/* 字体粗细 */
--weight-normal: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
/* 阴影系统 */
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
--shadow-md: 0 2px 8px rgba(0,0,0,0.06);
--shadow-lg: 0 4px 16px rgba(0,0,0,0.08);
/* 过渡 */
--transition-fast: 0.15s ease;
--transition-normal: 0.2s ease;
/* 层级 */
--z-dropdown: 1000;
--z-modal: 1050;
--z-tooltip: 1100;
}
```
### 5.3 按钮规范
| 类型 | 背景 | 边框 | 文字色 | 悬停 | 使用场景 |
|------|------|------|--------|------|---------|
| **主按钮** | `#212529` | 无 | `#FFFFFF` | `#000` | 主要操作(提交、确认) |
| **次按钮** | `#FFFFFF` | `#DEE2E6` | `#212529` | `#F1F3F5` | 次要操作(取消、返回) |
| **文字按钮** | 透明 | 无 | `#495057` | `#F1F3F5` | 低优先级操作 |
| **危险按钮** | `#C92A2A` | 无 | `#FFFFFF` | `#B02525` | 删除、清空 |
| **成功按钮** | `#2B8A3E` | 无 | `#FFFFFF` | `#246E33` | 审核通过 |
```css
.btn { /* 通用按钮基础 */
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
line-height: 1.5;
}
.btn-primary { background: #212529; color: #fff; }
.btn-secondary { background: #fff; color: #212529; border: 1px solid #DEE2E6; }
.btn-text { background: transparent; color: #495057; }
.btn-danger { background: #C92A2A; color: #fff; }
.btn-success { background: #2B8A3E; color: #fff; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn-lg { padding: 8px 18px; font-size: 14px; }
```
### 5.4 表单规范
```css
.form-item {
margin-bottom: 16px; /* 原24px */
}
.form-label {
font-size: 13px;
font-weight: 500;
color: #212529;
margin-bottom: 6px;
}
.form-input {
height: 36px; /* 原40px */
padding: 6px 12px;
border: 1px solid #DEE2E6;
border-radius: 6px;
font-size: 13px;
color: #212529;
transition: border-color 0.15s ease;
}
.form-input:focus {
border-color: #212529;
outline: none;
box-shadow: 0 0 0 3px rgba(0,0,0,0.05);
}
```
### 5.5 标签规范
```css
/* 统一标签替代6种彩色标签 */
.tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.tag-default { background: #F1F3F5; color: #495057; border: 1px solid #DEE2E6; }
.tag-success { background: #EBFBEE; color: #2B8A3E; border: 1px solid #B2F2BB; }
.tag-warning { background: #FFF9DB; color: #E67700; border: 1px solid #FFE066; }
.tag-error { background: #FFF5F5; color: #C92A2A; border: 1px solid #FFC9C9; }
.tag-info { background: #E7F5FF; color: #1C7ED6; border: 1px solid #A5D8FF; }
```
### 5.6 表格规范
| 属性 | 当前 | 优化后 | 说明 |
|------|------|--------|------|
| 表头背景 | `linear-gradient(#fafafa, #f5f5f5)` | `#F8F9FA` | 纯色统一 |
| 表头字体 | 14px / 600 | 13px / 600 | - |
| 表头内边距 | 16px 16px | 8px 12px | 紧凑 |
| 行高 | ~57px | 44px | -22% |
| 单元格内边距 | 16px 16px | 8px 12px | 紧凑 |
| 悬停背景 | `#f0f7ff` (蓝色) | `#F1F3F5` (灰色) | 统一灰色系 |
| 圆角 | 12px | 6px | 弱化圆角 |
| 阴影 | `0 2px 8px rgba(...)` | `0 1px 2px rgba(0,0,0,0.04)` | 轻阴影 |
---
## 第六部分:实施建议
### 6.1 优先级排序
| 优先级 | 优化项 | 预计工时 | 影响范围 |
|:------:|--------|:------:|:------:|
| P0 | CSS变量体系建立 + 全局样式替换 | 2天 | 全局 |
| P0 | 色彩体系迁移到白色方案 | 3天 | 全局 |
| P1 | 通用组件库创建 (`BasePage`, `PageHeader`, `TabsBar`, `ContentCard`) | 2天 | 新建 |
| P1 | 组件尺寸和信息密度调整 | 2天 | 全局 |
| P2 | 各模块使用通用组件重构 | 5天 | 8个组件 |
| P2 | 导航优化(下拉→标签栏) | 1天 | Home.vue |
| P3 | 文件选择器流程简化 | 2天 | FileSelector.vue |
| P3 | 出题流程优化 | 1天 | ExamModule.vue |
### 6.2 回滚策略
1. 所有样式修改通过CSS变量进行方便快速回滚
2. 通用组件与原有组件并行开发,逐模块切换
3. 每个模块切换后进行功能回归测试
4. 保留原有组件的备份分支
### 6.3 验收标准
- [ ] 全站使用统一的白色色板,无彩色渐变背景
- [ ] 所有文字对比度 ≥ 4.5:1
- [ ] 组件尺寸减少 ≥ 20%
- [ ] 同等屏幕尺寸下信息展示量提升 ≥ 30%
- [ ] 所有模块使用统一的通用布局组件
- [ ] 用户操作步骤减少导航1步文件选择减少1步
- [ ] 1920×1080 和 1366×768 分辨率下均无横向滚动条
---
## 附录
### A. 需要删除的CSS类清单
以下全局CSS类将在白色体系下移除或重写
| 文件 | 类名 | 操作 |
|------|------|------|
| `style.css` | `.btn-gradient-primary` ~ `.btn-gradient-purple` | 删除6个渐变按钮类 |
| `style.css` | `.card-gradient-blue` ~ `.card-gradient-purple` | 删除6个渐变卡片类 |
| `style.css` | `.tag-blue` ~ `.tag-cyan` | 删除6个彩色标签类 |
| `style.css` | `.progress-blue` ~ `.progress-cyan` | 删除6个彩色进度条类 |
| `style.css` | `.animate-float`, `.animate-pulse-glow`, `.animate-shimmer` | 删除或简化 |
| `index.css` | `.header` 蓝色背景 | 改为白色 |
| 各组件 | 内联 `style="background: linear-gradient(...)"` | 替换为CSS变量 |
### B. 参考标准
- WCAG 2.1 AA 对比度标准: https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
- Ant Design 5.0 设计语言: https://ant.design/
- Tailwind CSS 间距/色彩系统: https://tailwindcss.com/