Foundation: tokens, theme, icons, i18n, Button, Spinner, gallery scaffold
No files matched your search
@@ -0,0 +1,3 @@
|
||||
*.got.png
|
||||
.DS_Store
|
||||
/gallery
|
||||
@@ -0,0 +1,60 @@
|
||||
# AGENTS.md
|
||||
|
||||
MujicaUI: court-gothic components for MyGo native UI. Go library, package `mujica`, module `mujicaui`.
|
||||
Progress truth: `TASKS.md`. Requirements: `PRD.md`.
|
||||
|
||||
## Stack
|
||||
|
||||
| Item | Value |
|
||||
| --- | --- |
|
||||
| Go | 1.27.1 (MyGo's minimum) |
|
||||
| MyGo | `github.com/egoist/mygo v0.2.10` (commit 459b512), pinned in go.mod |
|
||||
| Icons | Lucide 1.52.0, ISC, copied into `icons/svg` |
|
||||
| Web mirror | `examples/gallery -web :8080`: Go stdlib server drives the Gallery through `ui.NewTester` and streams frames to a canvas. The browser shows pixels; Go runs the components. |
|
||||
|
||||
Read MyGo source before using an API: `go env GOMODCACHE`/github.com/egoist/mygo@v0.2.10 (`ui/*.go`, `docs/ui/*.md`). Never guess.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
*.go one component per file, root package mujica
|
||||
*_test.go tests beside the component
|
||||
messages_<group>.go built-in copy (en-US, zh-CN) via addMessages
|
||||
theme/ tokens, density, type, motion, contrast
|
||||
icons/ embedded Lucide icons: icons.Must("check")
|
||||
internal/ focus ring, ornaments
|
||||
examples/gallery/ native Gallery; pages_<group>.go per PRD 4.x group
|
||||
docs/components/ NNN-name.md, one page per component
|
||||
testdata/snapshots/ <os>/<name>-<light|dark>.png
|
||||
```
|
||||
|
||||
## Component rules
|
||||
|
||||
- Signature: `func Name(c *ui.Context, value *T, opts NameOptions) *ui.Element`. Composites may return a named struct.
|
||||
- Zero `Options` gives the default look. No `map[string]any` config.
|
||||
- Colors: `k := mujica.Tokens(c)`. Metrics: `theme.*` consts and `Density(c).Unit()` / `.ControlHeight()`.
|
||||
- Build on MyGo Bases (`ui.ButtonBase`, `ui.CheckboxBase`, `ui.SelectBase`, ...) or MyGo widgets restyled through the theme `Use` sets. Never reach into MyGo internals.
|
||||
- Focus: `internal.Ring(e, k.Focus, radius)` for the 2 DIP ring with 2 DIP gap.
|
||||
- State marks (checked, selected, progress) use `OnAccent` on `Accent`, or `AccentText`, never `Accent` alone on dark surfaces (2.44:1).
|
||||
- Ornament color only for dividers, corners, panel titles. No gradients, glow, glass or texture.
|
||||
- Durations: `mujica.Motion(c, theme.StateDuration)`; popups use `theme.PopupDuration`.
|
||||
- Disabled blocks input before handling it (`Disabled(true)`); MyGo dims disabled elements, do not add opacity.
|
||||
- Copy shown by a component comes from `msg(c, key)`; business copy comes from the caller.
|
||||
- No cross-frame `*ui.Element` caching, no goroutines touching elements.
|
||||
- Fail fast: panic on programmer errors (bad options); never swallow errors.
|
||||
- Log state transitions in composites with `slog.Debug("mujica: <component> <event>", ...)`. Never log passwords.
|
||||
- Files under 500 lines. Comments: one line, only when needed.
|
||||
|
||||
## Tests
|
||||
|
||||
- `ui.NewTester` drives click, keys, typing. Assert values, not existence.
|
||||
- Every component: one `snapshot(t, name, w, h, view)` call (light and dark). Refresh with `go test -run <Name> -update .`
|
||||
- Run `go vet ./... && go test ./...` before reporting done.
|
||||
|
||||
## Gallery page
|
||||
|
||||
Each component adds one `page` to its group slice in `examples/gallery/pages_<group>.go`: ID, Name, ZhName, Category, Demo, Code, Keys. Demo state lives in `ui.Local(c.Root(), "<id>", ...)`. Demos call `log` for every event.
|
||||
|
||||
## Review
|
||||
|
||||
Each TASKS.md item is reviewed by `codex exec -m gpt-6.1-sol -c model_reasoning_effort=high` (max 3 rounds). This session runs outside Herdr (`HERDR_ENV` unset), so reviews run as direct `codex exec`.
|
||||
@@ -0,0 +1,462 @@
|
||||
# MujicaUI 产品需求文档
|
||||
|
||||
版本:1.0
|
||||
日期:2026-10-05
|
||||
交付范围:100 个组件、两套主题、三种密度、原生 Gallery、文档与测试。
|
||||
|
||||
## 1. 产品定义
|
||||
|
||||
MujicaUI 是基于 MyGo 原生 UI 的 Go 桌面组件库。采用古典宫廷与哥特风格,覆盖 Windows、macOS、Linux。面向开发桌面工具、设置界面和数据应用的 Go 开发者。
|
||||
|
||||
MyGo 原生 UI 使用 Go 构建并自绘界面,不依赖 HTML、JavaScript 或 WebView。MujicaUI 复用该路线,不开发网页组件库。[^native]
|
||||
|
||||
首版交付本文规定的全部功能。已有组件换肤、已有能力组合和新增组件合计 100 个,不是在 MyGo 之外再增加 100 个。尺寸、颜色、状态、子部件和模式不单独计数。
|
||||
|
||||
不包含浏览器端适配、新渲染引擎、新布局引擎、独立状态管理框架、图表引擎、富文本编辑器、音视频引擎、业务后端和上传服务。
|
||||
|
||||
## 2. MyGo 复用规则
|
||||
|
||||
| 情况 | 实现要求 |
|
||||
| --- | --- |
|
||||
| 只需调整颜色、字体、尺寸 | 封装现有组件,使用主题与公开样式接口。 |
|
||||
| 需要改变外形或内部布局 | 使用已公开的 Base,重做外观,保留输入、焦点和无障碍行为。 |
|
||||
| 需要组合交互 | 组合 MyGo 与 MujicaUI 组件;仅补充缺失的状态和行为。 |
|
||||
| 系统窗口操作、文件选择 | 调用 MyGo 桌面 API;系统控制的外观保持系统样式。 |
|
||||
| 公开接口不足 | 提交上游扩展;合入后固定依赖版本。不访问上游 internal,不另建渲染或输入实现。 |
|
||||
|
||||
已核验的 Base 包括 Button、Checkbox、Switch、Toggle、Radio、Slider、Tabs、Segmented、Collapsible、Select、Combobox、Popover、Dialog、TextInput 和 TextArea 对应的 Base。不得假设存在未公开的 TableBase、TreeBase 等接口。[^bases]
|
||||
|
||||
应用内 Menubar、DropdownMenu、ContextMenu 使用 MujicaUI 自绘样式。系统菜单、系统文件对话框和系统窗口按钮不承诺换肤;不得把系统菜单直接显示为已完成的哥特样式菜单。
|
||||
|
||||
MyGo 的主题可调整颜色、字体、间距与圆角,也可读取高对比度、文本缩放和减少动画设置。MujicaUI 扩展设计变量,不另建 CSS 或样式解释器。[^theme]
|
||||
|
||||
## 3. 视觉规范
|
||||
|
||||
### 3.1 风格与装饰
|
||||
|
||||
视觉参考为用户提供的宫廷场景图片:象牙白墙面、黑红服装、金色细节与衬线标题。只提取配色、线条和字形,不使用人物、海报、Logo 或照片作为组件素材。
|
||||
|
||||
页面以象牙白或炭黑为底。酒红表示主要操作与选中状态。旧金用于分隔线、角饰和面板标题,不用于正文。成功、警告和错误使用独立语义颜色。
|
||||
|
||||
按钮、输入框和数据行不加花纹。Heading、Divider 可使用单个菱形或对称细线。Card、Dialog 的双线边框与角饰通过 `Decorated` 开启,默认关闭。装饰不占焦点、不遮挡内容、不缩小点击区域。
|
||||
|
||||
不使用背景纹理、仿皮革、仿金属、发光、玻璃效果和装饰性渐变。颜色选择器中的色谱不受此限制。
|
||||
|
||||
### 3.2 颜色
|
||||
|
||||
以下为首版默认设计值,不是从参考图精确取色的结果。
|
||||
|
||||
| 变量 | 浅色主题 | 深色主题 | 用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| Background | `#F2EBDD` | `#191618` | 窗口背景 |
|
||||
| Surface | `#FBF7EF` | `#242024` | 卡片与弹层 |
|
||||
| SurfaceHover | `#E8DFD0` | `#342C32` | 悬停背景 |
|
||||
| Text | `#211C1D` | `#F2EBDD` | 正文与标题 |
|
||||
| TextMuted | `#6A5B55` | `#BCAEAA` | 说明与占位文字 |
|
||||
| Border | `#D2C3AF` | `#51424A` | 非交互分隔线 |
|
||||
| ControlBorder | `#877662` | `#968278` | 输入框等交互边界 |
|
||||
| Accent | `#7D2034` | `#9C3C53` | 主按钮填充 |
|
||||
| AccentHover | `#651729` | `#AA425C` | 主按钮悬停 |
|
||||
| AccentPressed | `#511020` | `#813047` | 主按钮按下 |
|
||||
| OnAccent | `#FFF8EE` | `#FFF8EE` | 主按钮文字与图标 |
|
||||
| AccentText | `#7D2034` | `#E4A0AE` | 链接与强调文字 |
|
||||
| Selection | `#EAD7D9` | `#492A36` | 选中项背景 |
|
||||
| Focus | `#7D2034` | `#E4A0AE` | 焦点环 |
|
||||
| Ornament | `#AD9164` | `#BDA172` | 非语义装饰 |
|
||||
| Success | `#326247` | `#94C6A4` | 成功文字与图标 |
|
||||
| Warning | `#805714` | `#DFC080` | 警告文字与图标 |
|
||||
| Danger | `#A12635` | `#F0A1AA` | 错误文字与图标 |
|
||||
| Info | `#355E80` | `#9DBDDA` | 信息文字与图标 |
|
||||
|
||||
语义颜色默认用于文字、图标和边界,不直接拿浅色主题值填满深色主题控件。危险按钮的底色与前景色单独定义并检查对比度。焦点环与控件之间保留背景色间隔。
|
||||
|
||||
### 3.3 字体、尺寸与密度
|
||||
|
||||
标题使用衬线字体,中文标题使用宋体类字形。正文、表单、表格使用系统无衬线字体。代码与快捷键使用系统等宽字体。装饰字形不用于控件标签。
|
||||
|
||||
标题默认 28、22、18 DIP;正文 14 DIP;说明文字 12 DIP;正文行高为字号的 1.5 倍。字体允许由应用注册与覆盖;缺失字体使用系统回退,不显示空白或方框替代正常文字。MyGo 提供字体注册与字体回退接口。[^text]
|
||||
|
||||
| 密度 | 单行控件最小高度 | 数据行最小高度 | 基础间距单位 |
|
||||
| --- | --- | --- | --- |
|
||||
| Compact | 28 DIP | 28 DIP | 3 DIP |
|
||||
| Default | 36 DIP | 36 DIP | 4 DIP |
|
||||
| Comfortable | 44 DIP | 44 DIP | 5 DIP |
|
||||
|
||||
默认间距采用 4、8、12、16、24、32、48 DIP。输入框与按钮圆角 2 DIP;卡片 4 DIP;弹层 6 DIP。边框 1 DIP;焦点环 2 DIP,外留 2 DIP 间隔。图标采用 16、20、24 DIP,独立操作的命中区域不小于 28 × 28 DIP。
|
||||
|
||||
高度是下限,不是固定值。文本放大、换行或字体回退时允许增长。切换密度不缩小字号,也不改变组件语义。细线须按设备像素对齐。
|
||||
|
||||
### 3.4 组件外观与主题行为
|
||||
|
||||
Button 使用酒红填充、象牙白文字与小圆角。TextInput 使用实色背景与细边框。Checkbox 保留方框和勾选、半选含义;Radio 保留单选圆形。Slider 可使用菱形滑块,命中区域仍为矩形。Tabs 与 Sidebar 使用背景、边线和文字共同标记当前项。
|
||||
|
||||
Card 与 Dialog 的装饰只出现在边缘和标题区。DataTable 表头、行分隔和选中行不添加纹样。Button、TextInput、Checkbox、Tabs、Card、Dialog 作为视觉回归基准。
|
||||
|
||||
主题提供 `System`、`Light`、`Dark`,默认跟随系统。每个窗口可独立设置主题与密度。切换主题不得重置表单值、展开状态、选择、焦点或滚动位置。
|
||||
|
||||
样式覆盖顺序为局部参数、窗口主题、库默认值。系统高对比度、文本缩放与减少动画要求仍须生效。主色默认保留酒红,不自动替换为系统强调色;高对比度模式以可读性为先。
|
||||
|
||||
状态过渡默认 120 毫秒,弹层过渡 160 毫秒。不使用弹跳或装饰性循环动画。减少动画模式直接切换状态;加载标记保留静态说明。
|
||||
|
||||
## 4. 组件清单
|
||||
|
||||
以下每行是一个公开组件及其首版最低功能。全部适用组件同时遵守第 5 节的交互要求。
|
||||
|
||||
### 4.1 布局与桌面 · 10 个
|
||||
|
||||
| 编号 | 组件 | 首版功能 |
|
||||
| --- | --- | --- |
|
||||
| 001 | AppShell | 标题栏、侧栏、主内容、状态栏插槽;侧栏折叠;内容区独立滚动。 |
|
||||
| 002 | TitleBar | 应用标题与工具插槽;窗口拖动;为系统窗口按钮留出空间。 |
|
||||
| 003 | StatusBar | 左右分区;状态文字、进度与操作;宽度不足时折叠次要内容。 |
|
||||
| 004 | Container | 内边距、最大宽度、居中、填充;内容裁剪可配置。 |
|
||||
| 005 | Stack | 横向或纵向排列;间距、对齐、换行;隐藏项不占间距。 |
|
||||
| 006 | Grid | 行列尺寸、间距、跨行跨列;固定尺寸与剩余空间分配。 |
|
||||
| 007 | ScrollArea | 横纵滚动;滚动条;滚动到指定位置;保留滚动位置。 |
|
||||
| 008 | SplitPane | 横纵分栏;拖动分隔条;最小尺寸;折叠;键盘调整。 |
|
||||
| 009 | AspectRatio | 保持指定宽高比;在父容器约束下缩放;不拉伸子内容。 |
|
||||
| 010 | Divider | 横纵分隔;文字插槽;可选菱形装饰;不获取焦点。 |
|
||||
|
||||
### 4.2 基础展示 · 10 个
|
||||
|
||||
| 编号 | 组件 | 首版功能 |
|
||||
| --- | --- | --- |
|
||||
| 011 | Text | 换行、省略、选择复制;可编辑模式支持确认、取消和外部保存结果。 |
|
||||
| 012 | Heading | 三级标题;衬线字形;可选副标题和分隔线;支持文本缩放。 |
|
||||
| 013 | Icon | SVG 图标;尺寸与语义颜色;图标名称;加载失败回退。 |
|
||||
| 014 | Image | 位图与 SVG;包含或裁切;占位、加载、错误;替代说明。 |
|
||||
| 015 | Avatar | 图片、姓名首字母与默认图标回退;尺寸可配置。 |
|
||||
| 016 | AvatarGroup | 头像排列;最大显示数量;剩余数量;展开完整名单。 |
|
||||
| 017 | Badge | 文字或计数;上限显示;位置可配置;朗读完整数值。 |
|
||||
| 018 | Tag | 标签文字、图标;可关闭;关闭动作与标签点击分离。 |
|
||||
| 019 | Link | 外部地址或应用内动作;键盘激活;文本下划线;打开失败反馈。 |
|
||||
| 020 | Kbd | 单键与组合键展示;按平台显示 Command 或 Ctrl;仅展示、不注册快捷键。 |
|
||||
|
||||
### 4.3 基础输入 · 10 个
|
||||
|
||||
| 编号 | 组件 | 首版功能 |
|
||||
| --- | --- | --- |
|
||||
| 021 | Button | 主按钮、次按钮、描边、无底色、危险变体;图标;加载与禁用。 |
|
||||
| 022 | IconButton | 仅图标按钮;必填操作名称;提示文字;加载与禁用。 |
|
||||
| 023 | ButtonGroup | 横纵组合;统一边界与尺寸;组内方向键移动;逐项禁用。 |
|
||||
| 024 | TextInput | 单行编辑;占位、前后插槽、清空;选择、撤销、剪贴板与输入法。 |
|
||||
| 025 | TextArea | 多行编辑;最小与最大高度;滚动;字数提示;提交快捷键可配置。 |
|
||||
| 026 | NumberInput | 数值输入与步进;范围、步长、精度、空值;支持仅步进按钮模式。 |
|
||||
| 027 | PasswordInput | 密码遮蔽;显隐切换;粘贴;默认不朗读或记录密码内容。 |
|
||||
| 028 | SearchInput | 搜索与清空;提交;查找栏模式提供匹配数量、上一项和下一项。 |
|
||||
| 029 | Checkbox | 选中、未选、半选;关联标签;组内全选;键盘操作。 |
|
||||
| 030 | RadioGroup | 横纵单选组;稳定选项值;方向键选择;逐项禁用。 |
|
||||
|
||||
### 4.4 选择控件 · 10 个
|
||||
|
||||
| 编号 | 组件 | 首版功能 |
|
||||
| --- | --- | --- |
|
||||
| 031 | Switch | 开关值;标签;禁用;切换后回传结果;不自行调用业务接口。 |
|
||||
| 032 | Toggle | 保持按下状态的按钮;文字或图标;选中状态可辨认。 |
|
||||
| 033 | ToggleGroup | 单选或多选;是否允许空选;键盘导航;逐项禁用。 |
|
||||
| 034 | Slider | 数值范围、步长、刻度、值提示;拖动、方向键、Home 和 End。 |
|
||||
| 035 | RangeSlider | 双端范围;步长;两端不交叉;分别提供焦点和数值说明。 |
|
||||
| 036 | Select | 单选列表;占位、清空、禁用项、空状态;方向键与输入定位。 |
|
||||
| 037 | MultiSelect | 多选、搜索、全选、数量限制;标签折叠;无结果状态。 |
|
||||
| 038 | Combobox | 输入筛选与选项选择;可允许自定义值;异步结果由调用方提供。 |
|
||||
| 039 | Cascader | 多级选择;完整路径;叶节点限制;外部加载子项;加载失败重试。 |
|
||||
| 040 | TreeSelect | 树形单选或多选;搜索、展开、半选;父子选择规则可配置。 |
|
||||
|
||||
### 4.5 表单组合 · 10 个
|
||||
|
||||
| 编号 | 组件 | 首版功能 |
|
||||
| --- | --- | --- |
|
||||
| 041 | Form | 字段注册;校验;提交、重置;提交中防重入;错误字段定位。 |
|
||||
| 042 | FormField | 标签、必填标记、说明、错误;标签关联控件;上下或左右布局。 |
|
||||
| 043 | InputGroup | 输入框与前后文字、按钮组合;统一边框;各操作保留独立焦点。 |
|
||||
| 044 | PinInput | 位数限制;输入与退格;整段粘贴;完成事件;允许前导零。 |
|
||||
| 045 | TagsInput | 标签增删、粘贴分隔、去重、数量限制;提交前校验。 |
|
||||
| 046 | Rating | 评分与只读展示;步长配置;键盘调整;数值说明。 |
|
||||
| 047 | ColorPicker | HEX、RGB、透明度;颜色选择;预设色;输入校验;透明度背景格。 |
|
||||
| 048 | FilePicker | 调用系统文件或目录选择器;单选、多选、扩展名筛选;取消不报错。 |
|
||||
| 049 | FileDropZone | 文件拖入提示;文件列表;类型、数量和大小检查;移除与错误提示。 |
|
||||
| 050 | Transfer | 两侧列表;搜索、选择、移动、批量移动;禁用项;保留稳定条目 ID。 |
|
||||
|
||||
### 4.6 日期与时间 · 8 个
|
||||
|
||||
| 编号 | 组件 | 首版功能 |
|
||||
| --- | --- | --- |
|
||||
| 051 | Calendar | 月视图;日期选择;今日标记;不可选日期;周起始日可配置。 |
|
||||
| 052 | DatePicker | 日期输入与日历弹层;格式、范围、清空;无效输入提示。 |
|
||||
| 053 | DateRangePicker | 起止日期;范围预览;起止限制;确认与清空。 |
|
||||
| 054 | TimePicker | 时分秒;12/24 小时;步长与范围;输入与选择。 |
|
||||
| 055 | TimeRangePicker | 起止时间;范围校验;跨午夜须显式开启。 |
|
||||
| 056 | DateTimePicker | 日期与时间组合;时区由调用方指定;范围限制;确认与取消。 |
|
||||
| 057 | MonthPicker | 年份切换;月份选择;范围、禁用与清空。 |
|
||||
| 058 | YearPicker | 年份区间浏览;键盘选择;范围、禁用与清空。 |
|
||||
|
||||
### 4.7 导航 · 10 个
|
||||
|
||||
| 编号 | 组件 | 首版功能 |
|
||||
| --- | --- | --- |
|
||||
| 059 | Tabs | 标签与面板关联;横纵布局;方向键导航;禁用项;溢出滚动。 |
|
||||
| 060 | SegmentedControl | 分段单选;文字或图标;等宽或自适应;键盘导航。 |
|
||||
| 061 | Breadcrumb | 路径导航;当前项;中间项折叠;完整名称提示。 |
|
||||
| 062 | Pagination | 页码、上一页、下一页、页容量、跳页;总数变化时修正有效页码。 |
|
||||
| 063 | Steps | 当前、完成、等待、错误;横纵布局;是否可点击由调用方控制。 |
|
||||
| 064 | Sidebar | 分组与嵌套;图标、计数、选中项;展开折叠;紧凑模式。 |
|
||||
| 065 | NavigationMenu | 层级导航;当前路径;可选前进、后退按钮;接入已有导航状态。 |
|
||||
| 066 | Menubar | 应用窗口内菜单栏;子菜单;勾选和单选项;快捷键提示;键盘导航。 |
|
||||
| 067 | Toolbar | 操作分组;分隔;窄宽溢出菜单;方向键导航;图标名称。 |
|
||||
| 068 | CommandPalette | 快捷键打开;搜索、分组、无结果;方向键选择;执行后关闭。 |
|
||||
|
||||
### 4.8 内容与数据 · 12 个
|
||||
|
||||
| 编号 | 组件 | 首版功能 |
|
||||
| --- | --- | --- |
|
||||
| 069 | Card | 标题、内容、页脚插槽;描边;可选角饰;不默认增加点击行为。 |
|
||||
| 070 | List | 虚拟化列表与网格模式;稳定条目键;单选、多选;键盘导航与空状态。 |
|
||||
| 071 | DataTable | 虚拟化;排序、筛选、多选;列宽、列顺序、列显隐;自定义单元格。 |
|
||||
| 072 | Tree | 节点展开;选择;键盘导航;外部加载子项;空状态;大量节点虚拟化。 |
|
||||
| 073 | TreeTable | 树形层级与列组合;展开、选择、列宽、排序状态;可见行虚拟化。 |
|
||||
| 074 | Accordion | 单项或多项展开;是否允许全部关闭;键盘操作;展开状态受控。 |
|
||||
| 075 | Collapsible | 触发区与内容区;外部控制展开;隐藏内容退出焦点序列。 |
|
||||
| 076 | Timeline | 时间、标题、内容、状态;纵向排列;长内容换行。 |
|
||||
| 077 | DescriptionList | 名称与值;多列、跨列、复制值;窄宽时改为单列。 |
|
||||
| 078 | Statistic | 数值、单位、前后缀、精度;变化值说明;不计算业务指标。 |
|
||||
| 079 | ImageViewer | 放大、缩小、平移、适应窗口、切换图片;键盘操作与关闭。 |
|
||||
| 080 | Carousel | 上一项、下一项、位置指示;默认不自动播放;开启后支持暂停。 |
|
||||
|
||||
### 4.9 浮层与菜单 · 10 个
|
||||
|
||||
| 编号 | 组件 | 首版功能 |
|
||||
| --- | --- | --- |
|
||||
| 081 | Dialog | 标题、说明、内容、操作区;焦点限制与恢复;关闭规则可配置。 |
|
||||
| 082 | AlertDialog | 危险操作确认;默认聚焦取消;默认不允许点击遮罩关闭。 |
|
||||
| 083 | Drawer | 四侧展开;尺寸限制;模态或非模态;关闭与焦点恢复。 |
|
||||
| 084 | Popover | 锚点定位;自动避让窗口边缘;外部点击与 Escape 关闭。 |
|
||||
| 085 | Tooltip | 悬停或聚焦显示;延迟;边缘避让;不放交互内容。 |
|
||||
| 086 | HoverCard | 悬停或聚焦预览;允许移入内容;键盘可进入;延迟关闭。 |
|
||||
| 087 | DropdownMenu | 按钮触发菜单;分组、子菜单、勾选、禁用;操作后按规则关闭。 |
|
||||
| 088 | ContextMenu | 应用内右键菜单;键盘触发;按指针或焦点定位;边缘避让。 |
|
||||
| 089 | Popconfirm | 锚点旁确认与取消;说明关联;可聚焦操作;不替代高风险确认框。 |
|
||||
| 090 | Overlay | 窗口内覆盖层;定位、遮罩、模态行为;与现有 MyGo 浮层协作。 |
|
||||
|
||||
### 4.10 反馈与状态 · 10 个
|
||||
|
||||
| 编号 | 组件 | 首版功能 |
|
||||
| --- | --- | --- |
|
||||
| 091 | Alert | 信息、成功、警告、错误;标题、内容、操作;可关闭。 |
|
||||
| 092 | Toast | 消息队列;去重 ID;操作、关闭、时长;悬停或聚焦时暂停计时。 |
|
||||
| 093 | Progress | 确定与不确定进度;线形与环形;计量模式支持范围和阈值。 |
|
||||
| 094 | Spinner | 加载标记;尺寸与说明;隐藏或停止加载后停止动画。 |
|
||||
| 095 | Skeleton | 文字、圆形、矩形占位;固定内容尺寸;减少动画模式使用静态填充。 |
|
||||
| 096 | EmptyState | 标题、说明、可选图标与操作;区分无数据和无搜索结果。 |
|
||||
| 097 | Result | 成功、失败、无权限等结果页;说明与下一步操作;不执行业务跳转。 |
|
||||
| 098 | StatusIndicator | 状态标记与文字;成功、警告、错误、离线、处理中;不能只靠颜色。 |
|
||||
| 099 | NotificationCenter | 应用内消息列表;已读、未读、清除、操作;消息与持久化由调用方管理。 |
|
||||
| 100 | LoadingOverlay | 容器范围加载遮罩;阻止该范围重复操作;说明;可选取消动作。 |
|
||||
|
||||
### 4.11 上游组件归并
|
||||
|
||||
MyGo 已有能力按以下方式纳入清单,不增加计数。其余同类组件按第 2 节复用。[^native]
|
||||
|
||||
| MyGo 能力 | MujicaUI 对应项 |
|
||||
| --- | --- |
|
||||
| Primary button、Menu button | Button 的主按钮变体、DropdownMenu |
|
||||
| Stepper、Number input | NumberInput 的步进模式与输入模式 |
|
||||
| Search field、Find bar | SearchInput 的搜索模式与查找栏模式 |
|
||||
| Autocomplete、Combobox | Combobox 的自由输入与选项选择模式 |
|
||||
| Token field、Editable text | TagsInput、Text 的可编辑模式 |
|
||||
| List、Grid view | List 的列表模式与网格模式 |
|
||||
| Table、Outline、Tree | DataTable、TreeTable、Tree |
|
||||
| Date input、Time input | DatePicker、TimePicker |
|
||||
| Back and forward buttons | NavigationMenu 的历史导航操作 |
|
||||
| Meter、Progress bar | Progress 的计量模式与进度模式 |
|
||||
|
||||
## 5. 交互与状态
|
||||
|
||||
### 5.1 通用状态
|
||||
|
||||
| 状态 | 要求 |
|
||||
| --- | --- |
|
||||
| 默认 | 内容、边界与可操作性可辨认。 |
|
||||
| 悬停 | 改变背景或边界,不改变布局尺寸。 |
|
||||
| 按下 | 显示按下反馈;释放在无效区域时不误触发。 |
|
||||
| 聚焦 | 键盘焦点可见;选中或错误状态不能覆盖焦点环。 |
|
||||
| 选中 | 同时使用标记、形状或文字,不只改变颜色。 |
|
||||
| 禁用 | 不修改值,不执行操作,退出常规 Tab 序列;仍向辅助技术报告禁用。 |
|
||||
| 只读 | 不修改值;文字仍可聚焦、选择和复制;不得等同于禁用。 |
|
||||
| 加载 | 保留尺寸与标签;阻止重复提交;完成或失败后恢复操作。 |
|
||||
| 错误 | 保留输入;显示具体字段错误;提供修正或重试入口。 |
|
||||
|
||||
只为适用组件提供对应状态。展示与布局组件不添加无用途的 Loading、Disabled 等参数。
|
||||
|
||||
### 5.2 键盘与无障碍
|
||||
|
||||
所有操作必须能通过键盘完成。Tab 与 Shift+Tab 跨组移动;方向键在 Tabs、菜单、单选组和工具栏内部移动。快捷键遵循平台习惯,不抢占文本编辑和输入法按键。MyGo 已提供焦点组、平台快捷键映射和输入法处理,封装时不得移除。[^input]
|
||||
|
||||
无文字的操作必须提供名称。字段关联标签、说明与错误。复合组件提供对应角色、值、选中、展开和禁用状态。装饰不进入无障碍树。沿用 MyGo 的角色、Label、Description 与 Error 机制。[^accessibility]
|
||||
|
||||
项目对比度下限:正常文字与占位文字 4.5:1;用于识别控件、状态和焦点的图形 3:1。纯装饰边框与禁用状态不按正文处理。采用 WCAG 的对比度计算方法;不以此代替整套无障碍验收。[^text-contrast][^ui-contrast]
|
||||
|
||||
### 5.3 输入与表单
|
||||
|
||||
文本组件必须保留中文输入法、组合输入、选择、剪贴板、撤销和重做。输入法组合期间不自动提交、不跳到下一格、不拆分尚未提交的文本。PasswordInput 不在日志、测试报告或无障碍值中泄露明文。
|
||||
|
||||
Form 支持同步校验与调用方提供的异步校验结果;支持失焦和提交时校验。首次提交前不持续显示未触碰字段错误。提交失败保留所有值并定位第一个错误字段。异步旧结果不得覆盖更新后的字段值;提交中不得重复触发。
|
||||
|
||||
Form 不自带业务模型、远程接口或持久化。跨字段规则由调用方提供。重置恢复调用方给定的初始值,并清除提交与校验状态。
|
||||
|
||||
### 5.4 数据组件
|
||||
|
||||
DataTable 以稳定行 ID 和列 ID 保存状态。排序、筛选、列调整后,选择仍指向同一条数据。提供受控模式与可选本地排序、筛选辅助函数;外部数据查询由调用方负责。
|
||||
|
||||
MyGo Table 提供排序状态和界面交互,但数据仍按调用方提供的顺序显示。MujicaUI 不能只显示排序箭头而不完成排序或向调用方发出排序请求。[^table]
|
||||
|
||||
List、DataTable、Tree、TreeTable 使用虚拟化;不能为全部数据逐项创建界面元素。排序与筛选只在数据或条件变化时执行,不在每帧重复执行。异步加载分别展示首次加载、追加加载、失败和无数据。
|
||||
|
||||
### 5.5 浮层与消息
|
||||
|
||||
模态窗口限制焦点并阻止背景交互;非模态浮层不锁住整个窗口。Escape 优先交给输入法和当前编辑控件,再关闭最上层可关闭浮层。关闭后恢复到触发控件;触发控件消失时回到有效父级。复用 MyGo 的浮层和焦点机制。[^overlays]
|
||||
|
||||
弹层在窗口边缘翻转或限高滚动。嵌套 Select、菜单或 Popover 时,不提前关闭父 Dialog。危险操作默认聚焦取消;不得通过 Enter 在未确认目标时执行。
|
||||
|
||||
Toast 默认显示 5 秒,同一窗口最多同时显示 3 条,其余排队。含操作的消息在悬停或聚焦时暂停计时;错误消息默认保留到用户关闭。消息不得抢走正在输入的焦点。
|
||||
|
||||
### 5.6 日期、文件与本地化
|
||||
|
||||
日期选择保存日期值,不因隐式 UTC 转换改变日期。时间范围默认不跨午夜;日期时间由调用方指定时区。无法表示或无法确定的日期时间须提示,不静默修正。
|
||||
|
||||
FilePicker 只负责选择并返回结果。FileDropZone 只负责接收、校验与展示,不包含上传。文件内容读取、图片解码和网络访问不得阻塞界面线程。取消文件选择不作为错误。
|
||||
|
||||
组件内置文案提供 `zh-CN` 与 `en-US`,应用可覆盖。日期格式、周起始日、12/24 小时制和数字格式可配置。业务文案由调用方传入,不写死在组件内部。
|
||||
|
||||
## 6. API 与实现约束
|
||||
|
||||
### 6.1 API 约定
|
||||
|
||||
对外包名为 `mujica`。函数首参使用 `*ui.Context`;值通过状态指针或专用状态结构传入;配置使用有类型的 Options。基础组件返回 `*ui.Element`;复合组件按需返回命名部件或状态结果。不用 `map[string]any` 作为常规配置接口。
|
||||
|
||||
下列为 MujicaUI 的目标接口,不是 MyGo 现有接口:
|
||||
|
||||
```go
|
||||
func Button(c *ui.Context, label string, opts ButtonOptions) *ui.Element
|
||||
func TextInput(c *ui.Context, value *string, opts TextInputOptions) *ui.Element
|
||||
func Checkbox(c *ui.Context, value *bool, label string, opts CheckboxOptions) *ui.Element
|
||||
```
|
||||
|
||||
事件沿用 `.Clicked()`、`.Changed()`、`.Submitted()` 的调用习惯。一次操作只产生一次业务事件。禁用、只读、范围等会影响行为的参数须在处理输入前生效,不依赖事后换色。
|
||||
|
||||
零值 Options 提供默认样式。必填参数、有效范围和空值规则必须写入 API 文档。子内容使用回调或命名插槽,不为每种组合另建组件。原生 MyGo 组件可以与 MujicaUI 混用。
|
||||
|
||||
### 6.2 状态与生命周期
|
||||
|
||||
业务状态由应用持有;组件只保留输入、展开、焦点、动画等界面状态。临时状态使用 MyGo 已有机制,不建立第二套全局状态仓库。
|
||||
|
||||
MyGo 的视图按帧构建,Element 只属于当前帧。不得跨帧缓存 Element,也不得在 goroutine 中操作它。异步结果通过 `Window.Update` 回到界面线程;窗口关闭后取消任务或忽略结果。动态项目使用稳定键;需即时处理输入的组件通过外层有键容器保持身份。[^views]
|
||||
|
||||
缓存只覆盖字体、图标、装饰路径和必要的布局结果。主题、缩放、字体或数据变化时使相关缓存失效。隐藏组件不持续请求重绘。
|
||||
|
||||
### 6.3 项目结构
|
||||
|
||||
单仓库、单 Go 模块。组件放在根包,测试与实现同目录;不按组件拆成 100 个包。
|
||||
|
||||
```text
|
||||
mujicaui/
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── button.go
|
||||
├── button_test.go
|
||||
├── ... 其余组件及对应测试
|
||||
├── theme/ 主题、设计变量与对比度测试
|
||||
├── icons/ 图标接口与必要资源
|
||||
├── internal/ 少量共享实现与上游适配
|
||||
├── examples/gallery/ 原生演示应用
|
||||
├── docs/components/ 100 个组件文档
|
||||
├── testdata/ 测试数据与视觉基准
|
||||
├── README.md
|
||||
├── LICENSE
|
||||
└── THIRD_PARTY_NOTICES.md
|
||||
```
|
||||
|
||||
固定 MyGo 的已验证 tag 或 commit,并提交 go.mod 与 go.sum。Go 最低版本与系统依赖以该版本为准,写入兼容表。不使用浮动 main,不为 Go 库运行引入 Node.js、WebView 或浏览器依赖。
|
||||
|
||||
## 7. 平台与资源
|
||||
|
||||
Windows、macOS、Linux 均需运行同一套组件 API。允许字体栅格化、系统窗口按钮和系统对话框不同;不允许缺失组件功能。
|
||||
|
||||
TitleBar 使用 MyGo 提供的窗口控制预留区域与拖动接口,不覆盖系统按钮。原生路线在 Windows 不要求 WebView2,在 Linux 仍有 GTK 3 等系统依赖,不能把“不使用 WebView”写成“没有运行时依赖”。[^windows]
|
||||
|
||||
测试 100%、125%、150%、200% 显示缩放,以及跨不同缩放显示器移动窗口。文本缩放到 200% 后,不裁掉标签、字段错误和主要操作。Gallery 默认窗口为 1200 × 800 DIP,最小为 800 × 600 DIP;窄宽时折叠导航,长表格横向滚动。
|
||||
|
||||
发布说明列出已实测的操作系统版本、CPU 架构与 Linux 会话类型。X11、原生 Wayland 和 XWayland 分别记录,不把 XWayland 结果标成原生 Wayland 支持。
|
||||
|
||||
库和 Gallery 默认离线可用,无遥测、账号和强制网络字体。应用未请求时,不读取文件内容、不连接网络、不写入业务配置。字体可由应用提供;自带资源须记录来源与许可,图标和角饰不得依赖远程请求。
|
||||
|
||||
## 8. Gallery 与文档
|
||||
|
||||
Gallery 必须使用 MujicaUI 本身构建,不用网页或截图替代原生组件。提供组件分类、中英文搜索、主题切换、密度切换和示例代码复制。
|
||||
|
||||
每个组件页面包含可交互示例、适用状态展示、参数控制、事件结果和键盘说明。事件展示不得记录密码。提供设置页、文件列表、表单提交三个组合示例,验证跨组件协作,不接业务后端。
|
||||
|
||||
文档包含安装、最小运行示例、主题配置、MyGo 兼容版本和平台依赖。每个组件单独一页,列明参数、默认值、状态、事件、键盘操作、可复制代码和限制。所有 Go 示例必须通过编译检查。
|
||||
|
||||
文档中的组件截图由原生 Gallery 或渲染测试生成。文档可作为静态站发布,但浏览器页面不宣称能运行 Go 原生组件。API 名称与代码使用英文,首版说明文档使用中文。
|
||||
|
||||
## 9. 测试与性能
|
||||
|
||||
### 9.1 功能测试
|
||||
|
||||
使用 MyGo `ui.NewTester` 执行无窗口点击、键盘、输入、滚动与状态测试;使用图像快照检查渲染。MyGo 测试接口还提供缩放、主题、偏好和输入法组合模拟。[^testing]
|
||||
|
||||
每个组件均需覆盖适用的正常、边界、错误与键盘路径。复用上游不免除封装层测试;新增样式不能破坏点击区域、焦点与无障碍名称。
|
||||
|
||||
快照覆盖全部组件的浅色、深色默认状态。六个视觉基准组件额外覆盖三种密度和全部适用交互状态。各平台使用独立字体与快照基准,不要求不同系统逐像素相同。
|
||||
|
||||
真机补测系统文件选择、窗口控制、中文输入法、跨屏缩放与屏幕阅读器。无窗口测试不能替代这些项目。发布测试记录包含 VoiceOver、Windows Narrator 或 NVDA,以及 Linux Orca 的结果。
|
||||
|
||||
### 9.2 性能要求
|
||||
|
||||
以下为验收目标,不是已测量结果。三个平台各固定一台基准设备,记录 CPU、GPU、内存、系统、驱动、字体、显示缩放与 MyGo 版本;使用发布构建,关闭 Inspector。
|
||||
|
||||
| 场景 | 验收要求 |
|
||||
| --- | --- |
|
||||
| 大列表 | 100,000 条固定高度记录可滚动、搜索和选择;每帧创建的元素数量由可见区域与预取范围决定。 |
|
||||
| 大表格 | 100,000 行、10 列纯文本数据;排序与筛选后保留有效选择;不逐帧重算排序。 |
|
||||
| 大树 | 100,000 个节点;折叠子树不创建界面元素;展开、搜索与滚动不阻塞输入。 |
|
||||
| 渲染开销 | 同设备、同尺寸、同内容,对有等价上游实现的控件比较;MujicaUI 的界面构建与绘制 P95 耗时增幅不超过 20%。 |
|
||||
| 空闲 | 关闭加载和过渡动画后,连续观察 60 秒,无组件定时器或自身逻辑持续触发重绘。 |
|
||||
| 生命周期 | 连续打开、关闭同一弹层 100 次;回收后无持续增加的组件实例、后台任务或事件订阅。 |
|
||||
|
||||
持续集成执行格式检查、静态检查、单元测试、示例编译与快照检查。支持的环境运行竞态检测。平台集成与性能结果随发布保存,不用单一平台的结果代替三平台验证。
|
||||
|
||||
## 10. 发布验收
|
||||
|
||||
代码采用 MIT 许可证。仓库保留第三方声明;任何复制的上游代码须保留其声明。发布 Go 模块、源码、三平台 Gallery 构建产物、文档和兼容表。
|
||||
|
||||
| 验收项 | 通过条件 |
|
||||
| --- | --- |
|
||||
| 数量 | 本文 001—100 均有实现、文档、Gallery 示例与测试;变体不重复计数。 |
|
||||
| 功能 | 每项最低功能可操作;不存在占位按钮、空事件或仅静态展示的交互组件。 |
|
||||
| 样式 | 全部组件覆盖浅深主题;无 MyGo 默认外观混入;系统控制的界面按已声明例外处理。 |
|
||||
| 主题与密度 | 切换无需重启;业务状态、焦点与滚动位置不丢失;无文字裁切。 |
|
||||
| 输入与无障碍 | 键盘、中文输入法、焦点恢复、标签关联与屏幕阅读器测试通过。 |
|
||||
| 数据与生命周期 | 大数据测试通过;无状态串行、重复提交、后台任务或订阅泄漏。 |
|
||||
| 三平台 | 兼容表内的平台及架构均完成构建和实机验证;限制写入对应文档。 |
|
||||
| 交付 | 固定依赖、示例可编译、Gallery 可运行、默认离线可用、资源许可齐全。 |
|
||||
|
||||
首次公开发布须满足全部验收项,不以占位组件或后续补齐代替首版范围。
|
||||
|
||||
## 参考资料
|
||||
|
||||
上游能力核验日期:2026-10-05。实现与发布以固定依赖版本的代码为准。
|
||||
|
||||
[^native]: MyGo — [Native UI](https://mygo.egoist.dev/docs/ui).
|
||||
[^bases]: MyGo — [Custom widgets](https://mygo.egoist.dev/docs/ui/custom-widgets).
|
||||
[^theme]: MyGo — [Styling and themes](https://mygo.egoist.dev/docs/ui/styling).
|
||||
[^text]: MyGo — [Text](https://mygo.egoist.dev/docs/ui/text).
|
||||
[^input]: MyGo — [Input](https://mygo.egoist.dev/docs/ui/input).
|
||||
[^accessibility]: MyGo — [Accessibility](https://mygo.egoist.dev/docs/ui/accessibility).
|
||||
[^table]: MyGo — [Table](https://mygo.egoist.dev/docs/ui/table).
|
||||
[^overlays]: MyGo — [Overlays](https://mygo.egoist.dev/docs/ui/overlays).
|
||||
[^views]: MyGo — [Views](https://mygo.egoist.dev/docs/ui/views).
|
||||
[^windows]: MyGo — [Windows with native UI](https://mygo.egoist.dev/docs/ui/windows).
|
||||
[^testing]: MyGo — [Testing](https://mygo.egoist.dev/docs/ui/testing).
|
||||
[^text-contrast]: W3C — [Contrast (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html).
|
||||
[^ui-contrast]: W3C — [Non-text Contrast](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html).
|
||||
@@ -0,0 +1,26 @@
|
||||
# TASKS
|
||||
|
||||
Ordered by PRD sections. Check an item only after its codex review passes. Review rounds noted as `r1`, `r2`, `r3`.
|
||||
|
||||
- [ ] T01 §1–2 Module, MyGo v0.2.10 pin, reuse rules, AGENTS.md
|
||||
- [ ] T02 §3 Tokens, density, type, motion, contrast tests, `Use`, icons, i18n
|
||||
- [ ] T03 §4.1 Layout 001–010
|
||||
- [ ] T04 §4.2 Display 011–020
|
||||
- [ ] T05 §4.3 Inputs 021–030
|
||||
- [ ] T06 §4.4 Choices 031–040
|
||||
- [ ] T07 §4.5 Forms 041–050
|
||||
- [ ] T08 §4.6 Date and time 051–058
|
||||
- [ ] T09 §4.7 Navigation 059–068
|
||||
- [ ] T10 §4.8 Content and data 069–080
|
||||
- [ ] T11 §4.9 Overlays and menus 081–090
|
||||
- [ ] T12 §4.10 Feedback 091–100
|
||||
- [ ] T13 §4.11 Upstream mapping documented
|
||||
- [ ] T14 §5 States, keyboard, a11y, input, overlays: cross-cutting tests
|
||||
- [ ] T15 §6 API and lifecycle audit; README, LICENSE, THIRD_PARTY_NOTICES
|
||||
- [ ] T16 §7 Platform table, offline, resources
|
||||
- [ ] T17 §8 Native Gallery: categories, zh/en search, theme, density, code copy, three composite demos
|
||||
- [ ] T18 §8 Web mirror of the Gallery (user request)
|
||||
- [ ] T19 §8 Docs: install, minimal example, theme, compat; 100 component pages; examples compile
|
||||
- [ ] T20 §9 Snapshots, density baselines, large-data and lifecycle tests
|
||||
- [ ] T21 §9 CI workflow
|
||||
- [ ] T22 §10 Acceptance checklist
|
||||
@@ -0,0 +1,92 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/internal"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// ButtonVariant is a button's emphasis. The zero value is Primary.
|
||||
type ButtonVariant int
|
||||
|
||||
const (
|
||||
Primary ButtonVariant = iota
|
||||
Secondary
|
||||
Outline
|
||||
Ghost
|
||||
Danger
|
||||
)
|
||||
|
||||
// ButtonOptions configure a Button.
|
||||
type ButtonOptions struct {
|
||||
Variant ButtonVariant
|
||||
// Icon goes before the label.
|
||||
Icon *ui.SVG
|
||||
Loading bool
|
||||
Disabled bool
|
||||
// Label names an icon-only button for assistive technology.
|
||||
Label string
|
||||
}
|
||||
|
||||
// Button is a push button. Loading keeps its size and blocks clicks.
|
||||
func Button(c *ui.Context, label string, opts ButtonOptions) *ui.Element {
|
||||
k := Tokens(c)
|
||||
d := Density(c)
|
||||
b := ui.ButtonBase(c).MinHeight(d.ControlHeight()).MinWidth(theme.MinHitSize).
|
||||
Padding(0, d.Unit()*4).Gap(d.Unit()*2).Radius(theme.ControlRadius).
|
||||
Disabled(opts.Disabled || opts.Loading)
|
||||
internal.Ring(b, k.Focus, theme.ControlRadius)
|
||||
if opts.Label != "" {
|
||||
b.Label(opts.Label)
|
||||
}
|
||||
bg, fg, border := buttonColors(k, opts.Variant, b.Hovered() && !opts.Disabled, b.Pressed())
|
||||
b.Background(bg).TextColor(fg)
|
||||
if border.A > 0 {
|
||||
b.Border(theme.BorderWidth, border)
|
||||
}
|
||||
b.Children(func() {
|
||||
if opts.Loading {
|
||||
Spinner(c, SpinnerOptions{Size: 14, Color: fg})
|
||||
} else if opts.Icon != nil {
|
||||
ui.Icon(c, opts.Icon).Size(16, 16)
|
||||
}
|
||||
if label != "" {
|
||||
ui.Text(c, label).SingleLine()
|
||||
}
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
func buttonColors(k theme.Tokens, v ButtonVariant, hover, pressed bool) (bg, fg, border ui.Color) {
|
||||
switch v {
|
||||
case Secondary:
|
||||
bg, fg, border = k.Surface, k.Text, k.ControlBorder
|
||||
case Outline:
|
||||
bg, fg, border = ui.Transparent, k.AccentText, k.AccentText
|
||||
case Ghost:
|
||||
bg, fg = ui.Transparent, k.AccentText
|
||||
case Danger:
|
||||
bg, fg = k.DangerFill, k.OnDanger
|
||||
if pressed {
|
||||
bg = k.DangerFill.Mix(ui.RGB(0, 0, 0), 0.25)
|
||||
} else if hover {
|
||||
bg = k.DangerFill.Mix(ui.RGB(0, 0, 0), 0.12)
|
||||
}
|
||||
return
|
||||
default:
|
||||
bg, fg = k.Accent, k.OnAccent
|
||||
if pressed {
|
||||
bg = k.AccentPressed
|
||||
} else if hover {
|
||||
bg = k.AccentHover
|
||||
}
|
||||
return
|
||||
}
|
||||
if pressed {
|
||||
bg = k.Selection
|
||||
} else if hover {
|
||||
bg = k.SurfaceHover
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
func TestButtonClickOnce(t *testing.T) {
|
||||
n := 0
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{Mode: Light})
|
||||
if Button(c, "Save", ButtonOptions{}).Clicked() {
|
||||
n++
|
||||
}
|
||||
}, 300, 200)
|
||||
if err := tt.Click("Save"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("clicked %d times", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestButtonKeyboard(t *testing.T) {
|
||||
n := 0
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
if Button(c, "Save", ButtonOptions{}).Clicked() {
|
||||
n++
|
||||
}
|
||||
}, 300, 200)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
tt.Key(0, ui.KeySpace)
|
||||
if n != 2 {
|
||||
t.Fatalf("keyboard clicks %d, want 2", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestButtonBlockedStates(t *testing.T) {
|
||||
for _, opts := range []ButtonOptions{{Disabled: true}, {Loading: true}} {
|
||||
n := 0
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
if Button(c, "Pay", opts).Clicked() {
|
||||
n++
|
||||
}
|
||||
}, 300, 200)
|
||||
_ = tt.Click("Pay")
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if n != 0 {
|
||||
t.Fatalf("%+v: clicked %d times", opts, n)
|
||||
}
|
||||
if !tt.HasText("Pay") {
|
||||
t.Fatalf("%+v: label lost", opts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestButtonMinHeight(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
density theme.Density
|
||||
min float32
|
||||
}{{theme.Compact, 28}, {theme.Default, 36}, {theme.Comfortable, 44}} {
|
||||
var h float32
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{Density: tc.density})
|
||||
ui.Column(c).AlignItems(ui.Start).Children(func() {
|
||||
h = Button(c, "Go", ButtonOptions{}).Bounds().H
|
||||
})
|
||||
}, 300, 200)
|
||||
tt.Frame()
|
||||
if h < tc.min {
|
||||
t.Errorf("%v: height %v < %v", tc.density, h, tc.min)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestButtonSnapshot(t *testing.T) {
|
||||
snapshot(t, "button", 640, 140, func(c *ui.Context) {
|
||||
ui.Row(c).Padding(24).Gap(12).Wrap().Children(func() {
|
||||
Button(c, "Primary", ButtonOptions{})
|
||||
Button(c, "Secondary", ButtonOptions{Variant: Secondary})
|
||||
Button(c, "Outline", ButtonOptions{Variant: Outline})
|
||||
Button(c, "Ghost", ButtonOptions{Variant: Ghost})
|
||||
Button(c, "Delete", ButtonOptions{Variant: Danger})
|
||||
Button(c, "Saving", ButtonOptions{Loading: true})
|
||||
Button(c, "Disabled", ButtonOptions{Disabled: true})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
type gallery struct {
|
||||
pages []page
|
||||
selected int
|
||||
events []string
|
||||
}
|
||||
|
||||
func newGallery() *gallery { return &gallery{pages: catalog()} }
|
||||
|
||||
func (g *gallery) log(s string) { g.events = append(g.events, s) }
|
||||
|
||||
func (g *gallery) view(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
ui.Row(c).Fill().Children(func() {
|
||||
ui.Scroll(c).Width(240).FillHeight().Children(func() {
|
||||
for i, p := range g.pages {
|
||||
if mujica.Button(c, p.ID+" "+p.Name, mujica.ButtonOptions{Variant: mujica.Ghost}).Key(p.ID).Clicked() {
|
||||
g.selected = i
|
||||
}
|
||||
}
|
||||
})
|
||||
ui.Scroll(c).Fill().Padding(24).Children(func() {
|
||||
if len(g.pages) > 0 {
|
||||
g.pages[g.selected].Demo(c, g.log)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import "github.com/egoist/mygo/ui"
|
||||
|
||||
// page is one component's gallery entry.
|
||||
type page struct {
|
||||
ID string
|
||||
Name string
|
||||
ZhName string
|
||||
Category string
|
||||
// Demo builds the interactive example; log records events.
|
||||
Demo func(c *ui.Context, log func(string))
|
||||
Code string
|
||||
Keys string
|
||||
}
|
||||
|
||||
func catalog() []page {
|
||||
var all []page
|
||||
for _, g := range [][]page{layoutPages, displayPages, inputPages, choicePages, formPages, datetimePages, navigationPages, dataPages, overlayPages, feedbackPages} {
|
||||
all = append(all, g...)
|
||||
}
|
||||
return all
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Gallery tours every MujicaUI component in a native MyGo window.
|
||||
//
|
||||
// go run ./examples/gallery
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/egoist/mygo"
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := newGallery()
|
||||
mygo.App.WhenReady(func() {
|
||||
mygo.NewWindow(mygo.WindowOptions{
|
||||
Title: "MujicaUI Gallery",
|
||||
Width: 1200,
|
||||
Height: 800,
|
||||
MinWidth: 800,
|
||||
MinHeight: 600,
|
||||
Content: ui.View(app.view),
|
||||
})
|
||||
})
|
||||
if err := mygo.App.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var choicePages = []page{}
|
||||
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var dataPages = []page{}
|
||||
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var datetimePages = []page{}
|
||||
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var displayPages = []page{}
|
||||
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var feedbackPages = []page{}
|
||||
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var formPages = []page{}
|
||||
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var inputPages = []page{}
|
||||
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var layoutPages = []page{}
|
||||
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var navigationPages = []page{}
|
||||
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var overlayPages = []page{}
|
||||
@@ -0,0 +1,11 @@
|
||||
module mujicaui
|
||||
|
||||
go 1.27.1
|
||||
|
||||
require github.com/egoist/mygo v0.2.10
|
||||
|
||||
require (
|
||||
github.com/ebitengine/purego v0.11.1 // indirect
|
||||
github.com/go-text/typesetting v0.3.5 // indirect
|
||||
golang.org/x/image v0.46.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
github.com/ebitengine/purego v0.11.1 h1:2zpWRSQNVKN4eKsKO9eM1ILDgWfYMY9GwqRmK6XeQ/0=
|
||||
github.com/ebitengine/purego v0.11.1/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
|
||||
github.com/egoist/mygo v0.2.10 h1:Mfe6SpQngC9kLjrejpGM7L/k3Q62BdBAyJ8MrHfL6bQ=
|
||||
github.com/egoist/mygo v0.2.10/go.mod h1:1sRzZ6jEhKE7iuEQXjyoXDgxyxQ1uTgMHDcDKZvoDIU=
|
||||
github.com/go-text/typesetting v0.3.5 h1:XZPUooClHY0Vf/rFyUyuPRNEkawARaFzLMQcXLSEyPk=
|
||||
github.com/go-text/typesetting v0.3.5/go.mod h1:XZO1hD+nQVyvVa5IicQk7FsCa4PFQaJ2soWAP1f//68=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc h1:8FGo2It5K75XkavhTiCKExUfVaVDS1feBnLCru5qeoY=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
|
||||
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
@@ -0,0 +1,47 @@
|
||||
package mujica
|
||||
|
||||
import "github.com/egoist/mygo/ui"
|
||||
|
||||
// Locale picks the built-in copy. The zero value is English.
|
||||
type Locale string
|
||||
|
||||
const (
|
||||
EnUS Locale = "en-US"
|
||||
ZhCN Locale = "zh-CN"
|
||||
)
|
||||
|
||||
// messages maps a key to its English and Chinese copy.
|
||||
var messages = map[string][2]string{}
|
||||
|
||||
func addMessages(m map[string][2]string) {
|
||||
for k, v := range m {
|
||||
if _, dup := messages[k]; dup {
|
||||
panic("mujica: duplicate message " + k)
|
||||
}
|
||||
messages[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// msg returns the window's copy for key.
|
||||
func msg(c *ui.Context, key string) string {
|
||||
w := current(c)
|
||||
if s, ok := w.messages[key]; ok {
|
||||
return s
|
||||
}
|
||||
m, ok := messages[key]
|
||||
if !ok {
|
||||
panic("mujica: unknown message " + key)
|
||||
}
|
||||
if w.locale == ZhCN {
|
||||
return m[1]
|
||||
}
|
||||
return m[0]
|
||||
}
|
||||
|
||||
// CurrentLocale returns the window's locale.
|
||||
func CurrentLocale(c *ui.Context) Locale {
|
||||
if l := current(c).locale; l != "" {
|
||||
return l
|
||||
}
|
||||
return EnUS
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Package icons embeds the Lucide icons MujicaUI draws.
|
||||
package icons
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
//go:embed svg/*.svg
|
||||
var files embed.FS
|
||||
|
||||
var set = load()
|
||||
|
||||
func load() map[string]*ui.SVG {
|
||||
entries, err := files.ReadDir("svg")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
m := make(map[string]*ui.SVG, len(entries))
|
||||
for _, e := range entries {
|
||||
data, err := files.ReadFile(path.Join("svg", e.Name()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
m[strings.TrimSuffix(e.Name(), ".svg")] = ui.MustParseSVG(data)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Get returns the icon named name.
|
||||
func Get(name string) (*ui.SVG, bool) {
|
||||
s, ok := set[name]
|
||||
return s, ok
|
||||
}
|
||||
|
||||
// Must returns the icon named name and panics when it is missing.
|
||||
func Must(name string) *ui.SVG {
|
||||
s, ok := set[name]
|
||||
if !ok {
|
||||
panic("icons: no icon " + name)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Names lists every icon name in order.
|
||||
func Names() []string {
|
||||
names := make([]string, 0, len(set))
|
||||
for n := range set {
|
||||
names = append(names, n)
|
||||
}
|
||||
slices.Sort(names)
|
||||
return names
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package icons
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIconsParse(t *testing.T) {
|
||||
if len(Names()) < 60 {
|
||||
t.Fatalf("got %d icons", len(Names()))
|
||||
}
|
||||
if _, ok := Get("check"); !ok {
|
||||
t.Fatal("check missing")
|
||||
}
|
||||
if _, ok := Get("nope"); ok {
|
||||
t.Fatal("unknown name found")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-arrow-down"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 5v14" />
|
||||
<path d="m19 12-7 7-7-7" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 297 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
class="lucide lucide-arrow-up-down"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21 16-4 4-4-4" />
|
||||
<path d="M17 20V4" />
|
||||
<path d="m3 8 4-4 4 4" />
|
||||
<path d="M7 4v16" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-arrow-up"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m5 12 7-7 7 7" />
|
||||
<path d="M12 19V5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-ban"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M4.929 4.929 19.07 19.071" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-bell"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10.268 21a2 2 0 0 0 3.464 0" />
|
||||
<path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 422 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-bold"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 322 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
class="lucide lucide-calendar"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 2v3" />
|
||||
<path d="M16 2v3" />
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||
<path d="M3 9h18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 362 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-check"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 269 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-chevron-down"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 273 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-chevron-left"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-chevron-right"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-chevron-up"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m18 15-6-6-6 6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 273 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-chevrons-left"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m11 17-5-5 5-5" />
|
||||
<path d="m18 17-5-5 5-5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-chevrons-right"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 17 5-5-5-5" />
|
||||
<path d="m13 17 5-5-5-5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-circle-alert"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" x2="12" y1="8" y2="12" />
|
||||
<line x1="12" x2="12.01" y1="16" y2="16" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 369 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-circle-check"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="m16 9-5.5 5.5L8 12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-circle-x"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="m15 9-6 6" />
|
||||
<path d="m9 9 6 6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 326 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-circle"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-clock"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 301 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-command"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 327 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-copy"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 368 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-ellipsis"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="1" />
|
||||
<circle cx="19" cy="12" r="1" />
|
||||
<circle cx="5" cy="12" r="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 345 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-external-link"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M15 3h6v6" />
|
||||
<path d="M10 14 21 3" />
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 370 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
class="lucide lucide-eye-off"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
|
||||
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
|
||||
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
|
||||
<path d="m2 2 20 20" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 536 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-eye"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 388 B |
@@ -0,0 +1,18 @@
|
||||
<svg
|
||||
class="lucide lucide-file-text"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||
<path d="M10 9H8" />
|
||||
<path d="M16 13H8" />
|
||||
<path d="M16 17H8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 478 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-file"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 402 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-folder"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 373 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-funnel"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 401 B |
@@ -0,0 +1,19 @@
|
||||
<svg
|
||||
class="lucide lucide-grip-vertical"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="9" cy="12" r="1" />
|
||||
<circle cx="9" cy="5" r="1" />
|
||||
<circle cx="9" cy="19" r="1" />
|
||||
<circle cx="15" cy="12" r="1" />
|
||||
<circle cx="15" cy="5" r="1" />
|
||||
<circle cx="15" cy="19" r="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 451 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-house"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
|
||||
<path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 413 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-image"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
||||
<circle cx="9" cy="9" r="2" />
|
||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 388 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-inbox"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
||||
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 418 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-info"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 16v-4" />
|
||||
<path d="M12 8h.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-italic"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="19" x2="10" y1="4" y2="4" />
|
||||
<line x1="14" x2="5" y1="20" y2="20" />
|
||||
<line x1="15" x2="9" y1="4" y2="20" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
class="lucide lucide-layout-grid"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="7" height="7" x="3" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="14" rx="1" />
|
||||
<rect width="7" height="7" x="3" y="14" rx="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 452 B |
@@ -0,0 +1,19 @@
|
||||
<svg
|
||||
class="lucide lucide-list"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M3 5h.01" />
|
||||
<path d="M3 12h.01" />
|
||||
<path d="M3 19h.01" />
|
||||
<path d="M8 5h13" />
|
||||
<path d="M8 12h13" />
|
||||
<path d="M8 19h13" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 382 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-loader-circle"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-lock"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 338 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
class="lucide lucide-maximize"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3" />
|
||||
<path d="M21 8V5a2 2 0 0 0-2-2h-3" />
|
||||
<path d="M3 16v3a2 2 0 0 0 2 2h3" />
|
||||
<path d="M16 21h3a2 2 0 0 0 2-2v-3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 399 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-menu"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M4 5h16" />
|
||||
<path d="M4 12h16" />
|
||||
<path d="M4 19h16" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 308 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-minus"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 262 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-moon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
@@ -0,0 +1,18 @@
|
||||
<svg
|
||||
class="lucide lucide-palette"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" />
|
||||
<circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
|
||||
<circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
|
||||
<circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
|
||||
<circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 603 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-panel-left"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M9 3v18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 319 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-plus"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
<path d="M12 5v14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
class="lucide lucide-refresh-cw"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
|
||||
<path d="M21 3v5h-5" />
|
||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
|
||||
<path d="M8 16H3v5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-search"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-settings"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 619 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-shield-off"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m2 2 20 20" />
|
||||
<path d="M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71" />
|
||||
<path d="M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 519 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
class="lucide lucide-star"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 617 B |
@@ -0,0 +1,22 @@
|
||||
<svg
|
||||
class="lucide lucide-sun"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" />
|
||||
<path d="m17.66 17.66 1.41 1.41" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" />
|
||||
<path d="m19.07 4.93-1.41 1.41" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 513 B |
@@ -0,0 +1,18 @@
|
||||
<svg
|
||||
class="lucide lucide-trash-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 11v6" />
|
||||
<path d="M14 11v6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
|
||||
<path d="M3 6h18" />
|
||||
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-triangle-alert"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 384 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-underline"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M6 4v6a6 6 0 0 0 12 0V4" />
|
||||
<line x1="4" x2="20" y1="20" y2="20" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-upload"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 3v12" />
|
||||
<path d="m17 8-5-5-5 5" />
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 349 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-user"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 328 B |
@@ -0,0 +1,20 @@
|
||||
<svg
|
||||
class="lucide lucide-wifi-off"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 20h.01" />
|
||||
<path d="M8.5 16.429a5 5 0 0 1 7 0" />
|
||||
<path d="M5 12.859a10 10 0 0 1 5.17-2.69" />
|
||||
<path d="M19 12.859a10 10 0 0 0-2.007-1.523" />
|
||||
<path d="M2 8.82a15 15 0 0 1 4.177-2.643" />
|
||||
<path d="M22 8.82a15 15 0 0 0-11.288-3.764" />
|
||||
<path d="m2 2 20 20" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 527 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="lucide lucide-x"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
class="lucide lucide-zoom-in"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" x2="16.65" y1="21" y2="16.65" />
|
||||
<line x1="11" x2="11" y1="8" y2="14" />
|
||||
<line x1="8" x2="14" y1="11" y2="11" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
class="lucide lucide-zoom-out"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" x2="16.65" y1="21" y2="16.65" />
|
||||
<line x1="8" x2="14" y1="11" y2="11" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |
@@ -0,0 +1,35 @@
|
||||
// Package internal holds drawing shared by MujicaUI components.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// FocusRing strokes the keyboard ring outside r, leaving the focus gap.
|
||||
func FocusRing(p *ui.Painter, r ui.Rect, radius float32, c ui.Color) {
|
||||
o := theme.FocusGap + theme.FocusWidth
|
||||
p.Stroke(ui.Rect{X: r.X - o, Y: r.Y - o, W: r.W + 2*o, H: r.H + 2*o}, c, radius+o, theme.FocusWidth)
|
||||
}
|
||||
|
||||
// Ring replaces MyGo's ring on e with the MujicaUI ring.
|
||||
func Ring(e *ui.Element, c ui.Color, radius float32) *ui.Element {
|
||||
return e.FocusRing(false).DrawOver(func(p *ui.Painter, r ui.Rect) {
|
||||
if e.FocusVisible() {
|
||||
FocusRing(p, r, radius, c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Diamond returns a rhombus of size s centered at (cx, cy).
|
||||
func Diamond(cx, cy, s float32) *ui.Path {
|
||||
var p ui.Path
|
||||
h := s / 2
|
||||
p.MoveTo(cx, cy-h)
|
||||
p.LineTo(cx+h, cy)
|
||||
p.LineTo(cx, cy+h)
|
||||
p.LineTo(cx-h, cy)
|
||||
p.Close()
|
||||
return &p
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package mujica
|
||||
|
||||
func init() {
|
||||
addMessages(map[string][2]string{
|
||||
"loading": {"Loading", "加载中"},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
var update = flag.Bool("update", false, "rewrite snapshots")
|
||||
|
||||
// snapshot compares view in both themes with testdata/snapshots/<os>/<name>-<mode>.png.
|
||||
func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) {
|
||||
t.Helper()
|
||||
for _, m := range []Mode{Light, Dark} {
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{Mode: m})
|
||||
view(c)
|
||||
}, w, h)
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
got := encode(t, tt.Image())
|
||||
path := filepath.Join("testdata", "snapshots", runtime.GOOS, name+"-"+strings.ToLower(m.String())+".png")
|
||||
if *update {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, got, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
want, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("%v (run go test -update)", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
if err := os.WriteFile(path+".got.png", got, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Errorf("%s differs; see %s.got.png", path, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func encode(t *testing.T, img image.Image) []byte {
|
||||
t.Helper()
|
||||
var b bytes.Buffer
|
||||
if err := png.Encode(&b, img); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/internal"
|
||||
)
|
||||
|
||||
// SpinnerOptions configure a Spinner. Size is 16 DIPs when zero.
|
||||
type SpinnerOptions struct {
|
||||
Size float32
|
||||
// Label is read out and shown beside the mark; empty uses "Loading" unread on screen.
|
||||
Label string
|
||||
ShowLabel bool
|
||||
// Color paints the mark; zero uses AccentText.
|
||||
Color ui.Color
|
||||
}
|
||||
|
||||
// Spinner draws four diamonds turning around a center while it shows.
|
||||
func Spinner(c *ui.Context, opts SpinnerOptions) *ui.Element {
|
||||
k := Tokens(c)
|
||||
size := opts.Size
|
||||
if size <= 0 {
|
||||
size = 16
|
||||
}
|
||||
label := opts.Label
|
||||
if label == "" {
|
||||
label = msg(c, "loading")
|
||||
}
|
||||
color := opts.Color
|
||||
if color.A == 0 {
|
||||
color = k.AccentText
|
||||
}
|
||||
still := c.Preferences().ReduceMotion
|
||||
row := ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).Role(ui.RoleProgress).Label(label)
|
||||
row.Children(func() {
|
||||
ui.Box(c).Size(size, size).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
turn := 0.0
|
||||
if !still {
|
||||
turn = float64(p.Now().UnixMilli()%1200) / 1200
|
||||
p.AnimationFrame()
|
||||
}
|
||||
cx, cy, rad := r.X+r.W/2, r.Y+r.H/2, r.W*0.32
|
||||
for i := range 4 {
|
||||
a := 2 * math.Pi * (turn + float64(i)/4)
|
||||
x := cx + rad*float32(math.Cos(a))
|
||||
y := cy + rad*float32(math.Sin(a))
|
||||
alpha := 1 - float32(i)*0.22
|
||||
p.FillPath(internal.Diamond(x, y, r.W*0.3), color.Alpha(alpha))
|
||||
}
|
||||
})
|
||||
if opts.ShowLabel {
|
||||
ui.Text(c, label).TextColor(k.TextMuted).SingleLine()
|
||||
}
|
||||
})
|
||||
return row
|
||||
}
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
@@ -0,0 +1,125 @@
|
||||
// Package mujica is a court-gothic component library for MyGo native UI.
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// Mode chooses the appearance of a window.
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
System Mode = iota
|
||||
Light
|
||||
Dark
|
||||
)
|
||||
|
||||
func (m Mode) String() string {
|
||||
switch m {
|
||||
case Light:
|
||||
return "Light"
|
||||
case Dark:
|
||||
return "Dark"
|
||||
}
|
||||
return "System"
|
||||
}
|
||||
|
||||
// Settings are a window's theme. Nil token sets use the library defaults.
|
||||
type Settings struct {
|
||||
Mode Mode
|
||||
Density theme.Density
|
||||
Light *theme.Tokens
|
||||
Dark *theme.Tokens
|
||||
Locale Locale
|
||||
// Messages overrides built-in copy by key.
|
||||
Messages map[string]string
|
||||
}
|
||||
|
||||
type windowTheme struct {
|
||||
tokens theme.Tokens
|
||||
density theme.Density
|
||||
locale Locale
|
||||
messages map[string]string
|
||||
set bool
|
||||
}
|
||||
|
||||
type themeKey struct{}
|
||||
|
||||
// Use applies s to the window. Call it first in every frame of the view.
|
||||
func Use(c *ui.Context, s Settings) {
|
||||
dark := c.Theme().Dark
|
||||
switch s.Mode {
|
||||
case Light:
|
||||
dark = false
|
||||
case Dark:
|
||||
dark = true
|
||||
}
|
||||
k := theme.Light()
|
||||
if s.Light != nil {
|
||||
k = *s.Light
|
||||
}
|
||||
if dark {
|
||||
k = theme.Dark()
|
||||
if s.Dark != nil {
|
||||
k = *s.Dark
|
||||
}
|
||||
}
|
||||
p := c.Preferences()
|
||||
if p.HighContrast {
|
||||
k = k.HighContrast()
|
||||
}
|
||||
size := theme.BodySize
|
||||
if p.TextScale > 0 {
|
||||
size *= p.TextScale
|
||||
}
|
||||
c.SetTheme(&ui.Theme{
|
||||
Dark: k.Dark,
|
||||
Background: k.Background,
|
||||
Surface: k.Surface,
|
||||
SurfaceHover: k.SurfaceHover,
|
||||
SurfacePressed: k.Selection,
|
||||
Border: k.ControlBorder,
|
||||
Text: k.Text,
|
||||
TextMuted: k.TextMuted,
|
||||
Accent: k.Accent,
|
||||
AccentHover: k.AccentHover,
|
||||
AccentPressed: k.AccentPressed,
|
||||
AccentText: k.OnAccent,
|
||||
Danger: k.Danger,
|
||||
Warning: k.Warning,
|
||||
Success: k.Success,
|
||||
Selection: k.Selection,
|
||||
Focus: k.Focus,
|
||||
Scrollbar: k.TextMuted.Alpha(0.5),
|
||||
Radius: theme.ControlRadius,
|
||||
Spacing: s.Density.Unit(),
|
||||
FontSize: size,
|
||||
})
|
||||
*ui.Local(c.Root(), themeKey{}, func() windowTheme { return windowTheme{} }) = windowTheme{k, s.Density, s.Locale, s.Messages, true}
|
||||
}
|
||||
|
||||
func current(c *ui.Context) windowTheme {
|
||||
w := *ui.Local(c.Root(), themeKey{}, func() windowTheme { return windowTheme{} })
|
||||
if !w.set {
|
||||
panic("mujica: call mujica.Use at the start of the view")
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// Tokens returns the window's colors.
|
||||
func Tokens(c *ui.Context) theme.Tokens { return current(c).tokens }
|
||||
|
||||
// Density returns the window's density.
|
||||
func Density(c *ui.Context) theme.Density { return current(c).density }
|
||||
|
||||
// Motion returns d, or zero when the desktop asks for reduced motion.
|
||||
func Motion(c *ui.Context, d time.Duration) time.Duration {
|
||||
if c.Preferences().ReduceMotion {
|
||||
return 0
|
||||
}
|
||||
return d
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
// Contrast returns the WCAG contrast ratio of two opaque colors.
|
||||
func Contrast(a, b ui.Color) float64 {
|
||||
la, lb := luminance(a), luminance(b)
|
||||
if la < lb {
|
||||
la, lb = lb, la
|
||||
}
|
||||
return (la + 0.05) / (lb + 0.05)
|
||||
}
|
||||
|
||||
func luminance(c ui.Color) float64 {
|
||||
return 0.2126*channel(c.R) + 0.7152*channel(c.G) + 0.0722*channel(c.B)
|
||||
}
|
||||
|
||||
func channel(v uint8) float64 {
|
||||
s := float64(v) / 255
|
||||
if s <= 0.04045 {
|
||||
return s / 12.92
|
||||
}
|
||||
return math.Pow((s+0.055)/1.055, 2.4)
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Package theme holds MujicaUI's design tokens: colors, type, metrics and motion.
|
||||
package theme
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
// Tokens are the colors of one appearance.
|
||||
type Tokens struct {
|
||||
Dark bool
|
||||
Background ui.Color
|
||||
Surface ui.Color
|
||||
SurfaceHover ui.Color
|
||||
Text ui.Color
|
||||
TextMuted ui.Color
|
||||
Border ui.Color
|
||||
ControlBorder ui.Color
|
||||
Accent ui.Color
|
||||
AccentHover ui.Color
|
||||
AccentPressed ui.Color
|
||||
OnAccent ui.Color
|
||||
AccentText ui.Color
|
||||
Selection ui.Color
|
||||
Focus ui.Color
|
||||
Ornament ui.Color
|
||||
Success ui.Color
|
||||
Warning ui.Color
|
||||
Danger ui.Color
|
||||
Info ui.Color
|
||||
DangerFill ui.Color
|
||||
OnDanger ui.Color
|
||||
}
|
||||
|
||||
// Light returns the ivory appearance.
|
||||
func Light() Tokens {
|
||||
return Tokens{
|
||||
Background: ui.Hex("#F2EBDD"),
|
||||
Surface: ui.Hex("#FBF7EF"),
|
||||
SurfaceHover: ui.Hex("#E8DFD0"),
|
||||
Text: ui.Hex("#211C1D"),
|
||||
TextMuted: ui.Hex("#6A5B55"),
|
||||
Border: ui.Hex("#D2C3AF"),
|
||||
ControlBorder: ui.Hex("#877662"),
|
||||
Accent: ui.Hex("#7D2034"),
|
||||
AccentHover: ui.Hex("#651729"),
|
||||
AccentPressed: ui.Hex("#511020"),
|
||||
OnAccent: ui.Hex("#FFF8EE"),
|
||||
AccentText: ui.Hex("#7D2034"),
|
||||
Selection: ui.Hex("#EAD7D9"),
|
||||
Focus: ui.Hex("#7D2034"),
|
||||
Ornament: ui.Hex("#AD9164"),
|
||||
Success: ui.Hex("#326247"),
|
||||
Warning: ui.Hex("#805714"),
|
||||
Danger: ui.Hex("#A12635"),
|
||||
Info: ui.Hex("#355E80"),
|
||||
DangerFill: ui.Hex("#A12635"),
|
||||
OnDanger: ui.Hex("#FFF8EE"),
|
||||
}
|
||||
}
|
||||
|
||||
// Dark returns the charcoal appearance.
|
||||
func Dark() Tokens {
|
||||
return Tokens{
|
||||
Dark: true,
|
||||
Background: ui.Hex("#191618"),
|
||||
Surface: ui.Hex("#242024"),
|
||||
SurfaceHover: ui.Hex("#342C32"),
|
||||
Text: ui.Hex("#F2EBDD"),
|
||||
TextMuted: ui.Hex("#BCAEAA"),
|
||||
Border: ui.Hex("#51424A"),
|
||||
ControlBorder: ui.Hex("#968278"),
|
||||
Accent: ui.Hex("#9C3C53"),
|
||||
AccentHover: ui.Hex("#AA425C"),
|
||||
AccentPressed: ui.Hex("#813047"),
|
||||
OnAccent: ui.Hex("#FFF8EE"),
|
||||
AccentText: ui.Hex("#E4A0AE"),
|
||||
Selection: ui.Hex("#492A36"),
|
||||
Focus: ui.Hex("#E4A0AE"),
|
||||
Ornament: ui.Hex("#BDA172"),
|
||||
Success: ui.Hex("#94C6A4"),
|
||||
Warning: ui.Hex("#DFC080"),
|
||||
Danger: ui.Hex("#F0A1AA"),
|
||||
Info: ui.Hex("#9DBDDA"),
|
||||
DangerFill: ui.Hex("#A12635"),
|
||||
OnDanger: ui.Hex("#FFF8EE"),
|
||||
}
|
||||
}
|
||||
|
||||
// HighContrast returns t with secondary text and borders raised for readability.
|
||||
func (t Tokens) HighContrast() Tokens {
|
||||
t.TextMuted = t.Text
|
||||
t.Border = t.ControlBorder
|
||||
return t
|
||||
}
|
||||
|
||||
// Density sets control heights and the spacing unit.
|
||||
type Density int
|
||||
|
||||
const (
|
||||
Default Density = iota
|
||||
Compact
|
||||
Comfortable
|
||||
)
|
||||
|
||||
// Unit is the base spacing unit in DIPs.
|
||||
func (d Density) Unit() float32 {
|
||||
switch d {
|
||||
case Compact:
|
||||
return 3
|
||||
case Comfortable:
|
||||
return 5
|
||||
}
|
||||
return 4
|
||||
}
|
||||
|
||||
// ControlHeight is the minimum height of single-line controls and data rows.
|
||||
func (d Density) ControlHeight() float32 {
|
||||
switch d {
|
||||
case Compact:
|
||||
return 28
|
||||
case Comfortable:
|
||||
return 44
|
||||
}
|
||||
return 36
|
||||
}
|
||||
|
||||
func (d Density) String() string {
|
||||
switch d {
|
||||
case Compact:
|
||||
return "Compact"
|
||||
case Comfortable:
|
||||
return "Comfortable"
|
||||
}
|
||||
return "Default"
|
||||
}
|
||||
|
||||
// Type sizes and families.
|
||||
const (
|
||||
H1Size float32 = 28
|
||||
H2Size float32 = 22
|
||||
H3Size float32 = 18
|
||||
BodySize float32 = 14
|
||||
CaptionSize float32 = 12
|
||||
LineHeight float32 = 1.5
|
||||
|
||||
SerifFont = "Georgia, Songti SC, SimSun, Noto Serif CJK SC, serif"
|
||||
MonoFont = "monospace"
|
||||
)
|
||||
|
||||
// Shape metrics in DIPs.
|
||||
const (
|
||||
ControlRadius float32 = 2
|
||||
CardRadius float32 = 4
|
||||
PopupRadius float32 = 6
|
||||
BorderWidth float32 = 1
|
||||
FocusWidth float32 = 2
|
||||
FocusGap float32 = 2
|
||||
MinHitSize float32 = 28
|
||||
)
|
||||
|
||||
// Motion durations.
|
||||
const (
|
||||
StateDuration = 120 * time.Millisecond
|
||||
PopupDuration = 160 * time.Millisecond
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
func TestContrastFloors(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
tokens Tokens
|
||||
}{{"light", Light()}, {"dark", Dark()}} {
|
||||
k := tc.tokens
|
||||
text := map[string][2]ui.Color{
|
||||
"Text/Background": {k.Text, k.Background},
|
||||
"Text/Surface": {k.Text, k.Surface},
|
||||
"Text/SurfaceHover": {k.Text, k.SurfaceHover},
|
||||
"Text/Selection": {k.Text, k.Selection},
|
||||
"TextMuted/Background": {k.TextMuted, k.Background},
|
||||
"TextMuted/Surface": {k.TextMuted, k.Surface},
|
||||
"OnAccent/Accent": {k.OnAccent, k.Accent},
|
||||
"OnAccent/AccentHover": {k.OnAccent, k.AccentHover},
|
||||
"OnAccent/Pressed": {k.OnAccent, k.AccentPressed},
|
||||
"OnDanger/DangerFill": {k.OnDanger, k.DangerFill},
|
||||
"AccentText/Surface": {k.AccentText, k.Surface},
|
||||
"Success/Surface": {k.Success, k.Surface},
|
||||
"Warning/Surface": {k.Warning, k.Surface},
|
||||
"Danger/Surface": {k.Danger, k.Surface},
|
||||
"Info/Surface": {k.Info, k.Surface},
|
||||
}
|
||||
for pair, c := range text {
|
||||
if r := Contrast(c[0], c[1]); r < 4.5 {
|
||||
t.Errorf("%s %s = %.2f, want >= 4.5", tc.name, pair, r)
|
||||
}
|
||||
}
|
||||
graphic := map[string][2]ui.Color{
|
||||
"ControlBorder/Background": {k.ControlBorder, k.Background},
|
||||
"ControlBorder/Surface": {k.ControlBorder, k.Surface},
|
||||
"Focus/Background": {k.Focus, k.Background},
|
||||
}
|
||||
for pair, c := range graphic {
|
||||
if r := Contrast(c[0], c[1]); r < 3 {
|
||||
t.Errorf("%s %s = %.2f, want >= 3", tc.name, pair, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDensity(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
d Density
|
||||
unit, height float32
|
||||
}{{Compact, 3, 28}, {Default, 4, 36}, {Comfortable, 5, 44}} {
|
||||
if tc.d.Unit() != tc.unit || tc.d.ControlHeight() != tc.height {
|
||||
t.Errorf("%v: unit %v height %v", tc.d, tc.d.Unit(), tc.d.ControlHeight())
|
||||
}
|
||||
}
|
||||
}
|
||||