diff --git a/docs/components/091-alert.md b/docs/components/091-alert.md new file mode 100644 index 0000000..fef8f3d --- /dev/null +++ b/docs/components/091-alert.md @@ -0,0 +1,51 @@ +# 091 Alert 提示条 + +## 用途 + +在页面内显示一条信息、成功、警告或错误提示,可带标题、一个操作按钮和关闭按钮。严重程度同时由左侧色条、图标和无障碍名称(如“Error”)表达,不只靠颜色。 + +## 最小示例 + +```go +open := true +r := mujica.Alert(c, &open, "The archive could not be read.", mujica.AlertOptions{ + Severity: mujica.SeverityError, + Title: "Import failed", + Action: "Retry", +}) +if r.Action { + // retry the import +} +``` + +签名:`func Alert(c *ui.Context, open *bool, message string, opts AlertOptions) AlertResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `open` | `*bool` | `nil` | 非 nil 时显示关闭按钮,关闭后置为 `false`;为 `false` 时不构建任何元素。`nil` 表示不可关闭。 | +| `message` | `string` | 必填 | 正文;为空时 panic。 | +| `Severity` | `Severity` | `SeverityInfo` | `SeverityInfo`、`SeveritySuccess`、`SeverityWarning`、`SeverityError`。 | +| `Title` | `string` | `""` | 正文上方的粗体标题。 | +| `Action` | `string` | `""` | 操作按钮文字;为空不显示。 | + +返回 `AlertResult{Element, Action, Closed}`;关闭状态下 `Element` 为 nil。 + +## 状态 + +四种严重程度;操作与关闭按钮有悬停、按下和 2 DIP 焦点环。警告与错误在出现或文字变化时通过 `Context.Announce` 播报一次。 + +## 事件 + +- `Action`:操作按钮被点击或经键盘触发。 +- `Closed`:用户关闭;同时 `*open` 变为 `false`。 + +## 键盘操作 + +Tab 依次聚焦操作按钮与关闭按钮;Enter 或 Space 触发。 + +## 限制 + +- 信息与成功提示不主动播报,避免页面加载时打扰读屏用户。 +- 关闭不带退出动画;需要动画时由调用方在外层加 `Transition`。 diff --git a/docs/components/092-toast.md b/docs/components/092-toast.md new file mode 100644 index 0000000..63a6f59 --- /dev/null +++ b/docs/components/092-toast.md @@ -0,0 +1,58 @@ +# 092 Toast 轻提示 + +## 用途 + +在窗口右下角短暂显示操作结果,可带一个操作(如撤销)与关闭按钮。每个窗口一条队列:按 ID 去重,同时最多显示 3 条,其余排队。 + +## 最小示例 + +```go +if mujica.Button(c, "Delete note", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { + mujica.ShowToast(c, mujica.ToastMessage{ID: "note-1", Message: "Note deleted", Action: "Undo"}) +} +// Last in the view: +if r := mujica.Toaster(c); r.Action == "note-1" { + // restore the note +} +``` + +接口: + +- `func ShowToast(c *ui.Context, m ToastMessage)`:加入队列,须在界面线程调用(后台任务经 `Window.Update` 回到界面线程)。 +- `func DismissToast(c *ui.Context, id string)`:关闭显示中或排队中的消息。 +- `func Toaster(c *ui.Context) ToasterResult`:每帧在视图末尾调用一次,绘制消息并报告事件。 + +## 参数 + +`ToastMessage`: + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `ID` | `string` | `Message` | 去重键。相同 ID 再次显示时替换内容并重新计时,不新增一条。 | +| `Severity` | `Severity` | `SeverityInfo` | 图标、色条与颜色。 | +| `Message` | `string` | 必填 | 文字;为空时 panic。 | +| `Action` | `string` | `""` | 操作按钮文字。 | +| `Duration` | `time.Duration` | `ToastDuration`(5 秒) | 显示时长;负数 panic。错误消息在为 0 时一直保留到用户关闭。 | + +常量:`ToastDuration = 5 * time.Second`,`ToastVisible = 3`。 + +## 状态 + +- 排队消息在显示时才开始计时。 +- 指针悬停或焦点位于消息内时暂停计时,离开后从剩余时间继续。 +- 出现时向读屏播报文字;进出场 160 ms 位移与淡入,减少动画时直接切换。 + +## 事件 + +`ToasterResult{Action, Closed}`:本帧被点击操作或被关闭的消息 ID;两者都会移除该消息。到时自动消失不报告。 + +## 键盘操作 + +消息从不抢夺焦点,正在输入的文本框保持焦点。Tab 在页面之后到达消息中的按钮;焦点在消息内时 Escape 关闭该消息。 + +## 复用与限制 + +MyGo 自带 `c.Toast` / `c.ToastAction`(docs/ui/toast.md),但它按文字去重、没有 ID、不限制条数、无关闭按钮、错误不能常驻、外观不可替换,计时只在悬停时暂停而不在聚焦时暂停。这些都是 PRD §5.5 的要求,因此 MujicaUI 自建队列,只复用 MyGo 的 `Overlay`、`Transition`、`Announce` 与 `After` 定时唤醒。 + +- 所有消息(不只是含操作的消息)都在悬停或聚焦时暂停,这是 PRD 要求的超集,便于阅读。 +- 位置固定在右下角;切换到未调用 `Toaster` 的视图时消息仍在队列中,回来后继续显示。 diff --git a/docs/components/093-progress.md b/docs/components/093-progress.md new file mode 100644 index 0000000..8609972 --- /dev/null +++ b/docs/components/093-progress.md @@ -0,0 +1,64 @@ +# 093 Progress 进度 + +## 用途 + +显示任务进度(确定或不确定,线形或环形),以及计量值(如磁盘占用)及其阈值。归并 MyGo 的 Progress bar 与 Meter:`Progress` 为进度模式,`Meter` 为计量模式。两者分别基于 `ui.Progress` 与 `ui.Meter` 元素重绘,保留其无障碍角色与数值。 + +## 最小示例 + +```go +mujica.Progress(c, 0.62, mujica.ProgressOptions{Label: "Uploading", ShowValue: true}) +mujica.Progress(c, 0, mujica.ProgressOptions{Shape: mujica.ProgressRing, Indeterminate: true, Label: "Connecting"}) +mujica.Meter(c, 93, mujica.MeterOptions{ + Max: 100, Label: "Disk", ShowValue: true, + Levels: &ui.MeterLevels{Warning: 70, Critical: 90}, +}) +``` + +签名: + +- `func Progress(c *ui.Context, value float64, opts ProgressOptions) *ui.Element` +- `func Meter(c *ui.Context, value float64, opts MeterOptions) *ui.Element` + +## 参数 + +`ProgressOptions`: + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `value` | `float64` | 必填 | 0 到 1;超出或 NaN 时 panic(不确定模式忽略)。 | +| `Shape` | `ProgressShape` | `ProgressLinear` | `ProgressLinear` 或 `ProgressRing`。 | +| `Indeterminate` | `bool` | `false` | 未知长度的进度。 | +| `Label` | `string` | `""` | 显示在旁边并作为无障碍名称;为空时名称为“Loading”。 | +| `ShowValue` | `bool` | `false` | 显示百分比。 | +| `Size` | `float32` | 环形 48、线形填满 | 环形直径或线形宽度;负数 panic。 | + +`MeterOptions`: + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `value` | `float64` | 必填 | 须在范围内,否则 panic。 | +| `Min`, `Max` | `float64` | 0, 1(两者都为 0 时) | 范围;`Max <= Min` panic。 | +| `Levels` | `*ui.MeterLevels` | `nil` | 阈值:达到 `Warning` 为偏高、达到 `Critical` 为严重;`Critical < Warning` 时低值为坏(如电量)。须在范围内。 | +| `Label` | `string` | `""` | 名称。 | +| `ShowValue` | `bool` | `false` | 显示“值 / 最大值”。 | +| `Width` | `float32` | 填满 | 宽度。 | + +## 状态 + +- 确定进度变化时以 120 ms 过渡,减少动画时直接跳到新值。 +- 不确定进度只在绘制时移动(只重绘、不重建视图);减少动画时显示静态填充和“In progress”文字。 +- 计量条按级别使用 Success / Warning / Danger 颜色,同时显示“Normal / High / Critical / Low”文字与阈值刻度,并把级别写入无障碍名称。 + +## 事件 + +无。 + +## 键盘操作 + +不可聚焦;读屏读取名称与数值。 + +## 限制 + +- 环形直径小于 44 DIP 时百分比显示在旁边,而不是环内。 +- MyGo 不提供自定义无障碍数值的公开接口,因此必须基于 `ui.Progress` / `ui.Meter` 元素绘制。 diff --git a/docs/components/094-spinner.md b/docs/components/094-spinner.md new file mode 100644 index 0000000..06d612f --- /dev/null +++ b/docs/components/094-spinner.md @@ -0,0 +1,39 @@ +# 094 Spinner 加载标记 + +## 用途 + +表示未知长度的加载:四个菱形绕中心旋转。可调尺寸,可显示说明文字。Button 的加载状态也使用它。标记基于 MyGo `ui.Spinner` 元素重绘,读屏视为不确定进度。 + +## 最小示例 + +```go +mujica.Spinner(c, mujica.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true}) +``` + +签名:`func Spinner(c *ui.Context, opts SpinnerOptions) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Size` | `float32` | 16 | 标记边长(DIP)。 | +| `Label` | `string` | 内置“Loading / 加载中” | 无障碍名称;`ShowLabel` 时也显示在旁边。 | +| `ShowLabel` | `bool` | `false` | 在标记旁显示说明。 | +| `Color` | `ui.Color` | `AccentText` | 标记颜色。 | + +## 状态 + +- 只在被绘制时请求重绘:不再构建(隐藏或停止加载)或滚出视口后动画立即停止,不留定时器。 +- 减少动画模式下静止显示,保留名称与说明。 + +## 事件 + +无。 + +## 键盘操作 + +不可聚焦。 + +## 限制 + +- 每帧重绘,而非 MyGo 自带 spinner 的分步重绘;只在显示时产生开销。 diff --git a/docs/components/095-skeleton.md b/docs/components/095-skeleton.md new file mode 100644 index 0000000..4e7e7e1 --- /dev/null +++ b/docs/components/095-skeleton.md @@ -0,0 +1,45 @@ +# 095 Skeleton 骨架屏 + +## 用途 + +内容加载前占位,形状为文字行、圆形或矩形。尺寸固定,加载完成后替换为真实内容时不跳动。 + +## 最小示例 + +```go +ui.Row(c).Gap(12).Children(func() { + mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonCircle}) + ui.Column(c).Grow(1).Children(func() { + mujica.Skeleton(c, mujica.SkeletonOptions{Lines: 2}) + }) +}) +``` + +签名:`func Skeleton(c *ui.Context, opts SkeletonOptions) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Shape` | `SkeletonShape` | `SkeletonText` | `SkeletonText`、`SkeletonCircle`、`SkeletonRect`。 | +| `Width` | `float32` | 填满父级;圆形 40 | 宽度;圆形为直径。 | +| `Height` | `float32` | 当前密度控件高度 | 矩形高度;文字形状由行数决定(每行 21 DIP)。 | +| `Lines` | `int` | 1 | 文字行数,多行时最后一行为 60% 宽。 | + +负值 panic。 + +## 状态 + +明暗缓慢脉动(1.6 秒周期),只重绘不重建视图;减少动画模式使用静态填充。 + +## 事件 + +无。 + +## 键盘操作 + +不可聚焦,不进入无障碍树。 + +## 限制 + +- 骨架本身不向读屏报告加载;请在容器上配合 Spinner 或 LoadingOverlay 的说明文字。 diff --git a/docs/components/096-empty-state.md b/docs/components/096-empty-state.md new file mode 100644 index 0000000..4c4d822 --- /dev/null +++ b/docs/components/096-empty-state.md @@ -0,0 +1,48 @@ +# 096 EmptyState 空状态 + +## 用途 + +说明视图为何为空并给出下一步。区分“无数据”(集合本身为空)与“无搜索结果”(筛选没有匹配),两者使用不同的默认图标与文案。 + +## 最小示例 + +```go +r := mujica.EmptyState(c, mujica.EmptyStateOptions{ + Kind: mujica.EmptyNoResults, + Query: query, + Action: "Clear search", +}) +if r.Action { + query = "" +} +``` + +签名:`func EmptyState(c *ui.Context, opts EmptyStateOptions) EmptyStateResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Kind` | `EmptyKind` | `EmptyNoData` | `EmptyNoData` 或 `EmptyNoResults`。 | +| `Title` | `string` | 按类型的内置文案 | 衬线标题。 | +| `Description` | `string` | 按类型的内置文案 | 说明。 | +| `Query` | `string` | `""` | 仅 `EmptyNoResults`:默认说明写出该关键词。用于 `EmptyNoData` 时 panic。 | +| `Icon` | `*ui.SVG` | inbox / search | 替换图标。 | +| `NoIcon` | `bool` | `false` | 不显示图标;与 `Icon` 同时设置时 panic。 | +| `Action` | `string` | `""` | 次按钮文字。 | + +## 状态 + +静态展示;操作按钮有悬停、按下与焦点状态。 + +## 事件 + +`EmptyStateResult.Action`:操作按钮被触发。 + +## 键盘操作 + +Tab 聚焦操作按钮;Enter 或 Space 触发。 + +## 限制 + +- 只有一个操作;需要多个下一步时使用 Result。 diff --git a/docs/components/097-result.md b/docs/components/097-result.md new file mode 100644 index 0000000..30d0285 --- /dev/null +++ b/docs/components/097-result.md @@ -0,0 +1,46 @@ +# 097 Result 结果页 + +## 用途 + +任务完成后的整页结果:成功、失败或无权限,附说明与下一步操作。组件只报告选择了哪个操作,不执行跳转。标题与操作之间用旧金色细线与菱形分隔。 + +## 最小示例 + +```go +r := mujica.Result(c, mujica.ResultOptions{ + Kind: mujica.ResultForbidden, + Description: "You need the Editor role to change billing.", + Actions: []string{"Request access", "Go back"}, +}) +switch r.Action { +case 0: // send the request +case 1: // navigate back +} +``` + +签名:`func Result(c *ui.Context, opts ResultOptions) ResultResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Kind` | `ResultKind` | `ResultSuccess` | `ResultSuccess`、`ResultFailure`、`ResultForbidden`;未知值 panic。 | +| `Title` | `string` | 按类型的内置文案 | 衬线标题。 | +| `Description` | `string` | `""` | 说明。 | +| `Actions` | `[]string` | 无 | 下一步操作;第一个为主按钮,其余为次按钮。 | + +## 状态 + +图标(对勾、叉、盾牌)与标题文字共同表达结果,不只靠颜色。按钮有悬停、按下与焦点状态。 + +## 事件 + +`ResultResult.Action`:被触发的操作序号,未触发为 -1。 + +## 键盘操作 + +Tab 按顺序聚焦操作按钮;Enter 或 Space 触发。 + +## 限制 + +- 不含路由;导航由调用方完成。 diff --git a/docs/components/098-status-indicator.md b/docs/components/098-status-indicator.md new file mode 100644 index 0000000..63d659d --- /dev/null +++ b/docs/components/098-status-indicator.md @@ -0,0 +1,37 @@ +# 098 StatusIndicator 状态标记 + +## 用途 + +以图形加文字标出状态:成功、警告、错误、离线、处理中。每种状态有独立形状(对勾圆、三角、叉圆、断开的 Wi-Fi、旋转菱形),并总是显示文字,不只靠颜色。 + +## 最小示例 + +```go +mujica.StatusIndicator(c, mujica.StatusSuccess, mujica.StatusIndicatorOptions{Label: "API online"}) +mujica.StatusIndicator(c, mujica.StatusOffline, mujica.StatusIndicatorOptions{}) +``` + +签名:`func StatusIndicator(c *ui.Context, status Status, opts StatusIndicatorOptions) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `status` | `Status` | `StatusSuccess` | `StatusSuccess`、`StatusWarning`、`StatusError`、`StatusOffline`、`StatusBusy`;未知值 panic。 | +| `Label` | `string` | 内置词(Healthy、Degraded、Failing、Offline、Working) | 替换显示的文字。 | + +## 状态 + +处理中使用 Spinner,减少动画时静止。整体为 `RoleStatus`,名称为文字。 + +## 事件 + +无。 + +## 键盘操作 + +不可聚焦。 + +## 限制 + +- 无 Badge 式的纯圆点模式;PRD 要求不能只靠颜色。 diff --git a/docs/components/099-notification-center.md b/docs/components/099-notification-center.md new file mode 100644 index 0000000..95b16fa --- /dev/null +++ b/docs/components/099-notification-center.md @@ -0,0 +1,51 @@ +# 099 NotificationCenter 通知中心 + +## 用途 + +应用内消息列表:未读标记、已读/未读切换、单条清除、全部已读、全部清除和每条消息的操作。消息列表与持久化由调用方持有;组件就地修改调用方的切片,并报告发生了什么,供调用方保存。 + +## 最小示例 + +```go +r := mujica.NotificationCenter(c, &items, mujica.NotificationCenterOptions{}) +if r.Action == "a" { + // open the build log +} +``` + +签名:`func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationCenterOptions) NotificationCenterResult` + +## 参数 + +`Notification`: + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `ID` | `string` | 必填 | 列表内唯一;为空或重复时 panic。 | +| `Title` | `string` | `""` | 标题,未读时加粗。 | +| `Body` | `string` | `""` | 正文。 | +| `Time` | `string` | `""` | 按原样显示的时间,如“5 min ago”。 | +| `Read` | `bool` | `false` | 是否已读。 | +| `Action` | `string` | `""` | 操作按钮文字。 | + +`NotificationCenterOptions.Title`:面板标题,默认“Notifications / 通知”。`items` 为 nil 时 panic。 + +## 状态 + +- 未读:左侧酒红菱形、粗体标题、无障碍名称前缀“Unread”。 +- 行悬停加浅底;按钮有悬停、按下与焦点状态;无未读或无消息时头部按钮禁用。 +- 空列表显示 EmptyState。 +- 面板标题下为旧金细线。列表超出容器高度时滚动。 + +## 事件 + +`NotificationCenterResult`:`Action`、`Toggled`(已读状态切换)、`Opened`(点击正文后标为已读)、`Removed` 为对应 ID;`MarkedAllRead`、`ClearedAll` 为布尔值。 + +## 键盘操作 + +列表是一个 Tab 停靠点;方向键在各条消息及其按钮之间移动,Home/End 到首尾。焦点在消息正文上时 Enter 或 Space 标为已读;Delete 或 Backspace 清除当前消息。 + +## 限制 + +- 不分页,也不虚拟化;面向几十到几百条的应用内消息。 +- 时间文字由调用方格式化。 diff --git a/docs/components/100-loading-overlay.md b/docs/components/100-loading-overlay.md new file mode 100644 index 0000000..e94b819 --- /dev/null +++ b/docs/components/100-loading-overlay.md @@ -0,0 +1,48 @@ +# 100 LoadingOverlay 加载遮罩 + +## 用途 + +在一个容器范围内显示加载遮罩:被覆盖的内容不接收指针、按键和焦点,防止重复操作;窗口其余部分照常可用。显示说明文字与可选取消操作。 + +## 最小示例 + +```go +r := mujica.LoadingOverlay(c, loading, mujica.LoadingOverlayOptions{Cancel: "Cancel"}, func() { + ui.Text(c, "Quarterly report") + mujica.Button(c, "Export", mujica.ButtonOptions{}) +}) +if r.Canceled { + loading = false +} +``` + +签名:`func LoadingOverlay(c *ui.Context, loading bool, opts LoadingOverlayOptions, content func()) LoadingOverlayResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `loading` | `bool` | 必填 | 是否覆盖。 | +| `content` | `func()` | 必填 | 构建被覆盖的内容;nil 时 panic。 | +| `Label` | `string` | 内置“Loading… / 正在加载…” | 说明文字。 | +| `Cancel` | `string` | `""` | 取消按钮文字;为空时不显示。 | + +返回 `LoadingOverlayResult{Element, Canceled}`。`Element` 即遮罩所覆盖的范围,可在其上设置宽高。 + +## 状态 + +- 加载时内容经 MyGo `Disabled` 阻止输入(MyGo 统一减淡),上方为半透明背景与居中面板。 +- 开始加载时播报说明;焦点原在内容内时移到取消按钮(无取消时移到可聚焦的说明面板),焦点不会离开该区域。 +- 遮罩淡入淡出,减少动画时直接切换。 + +## 事件 + +`Canceled`:点击取消,或焦点在面板内时按 Escape。 + +## 键盘操作 + +Tab 到达取消按钮;Enter、Space 或 Escape 取消。被覆盖的控件不进入 Tab 序列。 + +## 限制 + +- 只遮盖容器自身范围;整窗阻塞请使用模态对话框。 diff --git a/example_feedback_test.go b/example_feedback_test.go new file mode 100644 index 0000000..cd49f2e --- /dev/null +++ b/example_feedback_test.go @@ -0,0 +1,145 @@ +package mujica_test + +import ( + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +func ExampleAlert() { + open := true + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.Alert(c, &open, "The archive could not be read.", mujica.AlertOptions{ + Severity: mujica.SeverityError, + Title: "Import failed", + Action: "Retry", + }) + if r.Action { + // retry the import + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleToaster() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.Button(c, "Delete note", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { + mujica.ShowToast(c, mujica.ToastMessage{ID: "note-1", Message: "Note deleted", Action: "Undo"}) + } + if r := mujica.Toaster(c); r.Action == "note-1" { + // restore the note + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleProgress() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Column(c).Padding(16).Gap(16).Children(func() { + mujica.Progress(c, 0.62, mujica.ProgressOptions{Label: "Uploading", ShowValue: true}) + mujica.Progress(c, 0, mujica.ProgressOptions{Shape: mujica.ProgressRing, Indeterminate: true, Label: "Connecting"}) + mujica.Meter(c, 93, mujica.MeterOptions{ + Max: 100, Label: "Disk", ShowValue: true, + Levels: &ui.MeterLevels{Warning: 70, Critical: 90}, + }) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSpinner() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Spinner(c, mujica.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSkeleton() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Row(c).Padding(16).Gap(12).Children(func() { + mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonCircle}) + ui.Column(c).Grow(1).Children(func() { + mujica.Skeleton(c, mujica.SkeletonOptions{Lines: 2}) + }) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleEmptyState() { + query := "quarterly" + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.EmptyState(c, mujica.EmptyStateOptions{ + Kind: mujica.EmptyNoResults, + Query: query, + Action: "Clear search", + }) + if r.Action { + query = "" + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleResult() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.Result(c, mujica.ResultOptions{ + Kind: mujica.ResultForbidden, + Description: "You need the Editor role to change billing.", + Actions: []string{"Request access", "Go back"}, + }) + switch r.Action { + case 0: // send the request + case 1: // navigate back + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleStatusIndicator() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Row(c).Gap(16).Children(func() { + mujica.StatusIndicator(c, mujica.StatusSuccess, mujica.StatusIndicatorOptions{Label: "API online"}) + mujica.StatusIndicator(c, mujica.StatusOffline, mujica.StatusIndicatorOptions{}) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleNotificationCenter() { + items := []mujica.Notification{ + {ID: "a", Title: "Build failed", Body: "3 tests failing", Time: "2 min ago", Action: "View log"}, + {ID: "b", Title: "Backup complete", Time: "Yesterday", Read: true}, + } + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.NotificationCenter(c, &items, mujica.NotificationCenterOptions{}) + if r.Action == "a" { + // open the build log + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleLoadingOverlay() { + loading := true + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.LoadingOverlay(c, loading, mujica.LoadingOverlayOptions{Cancel: "Cancel"}, func() { + ui.Text(c, "Quarterly report") + mujica.Button(c, "Export", mujica.ButtonOptions{}) + }) + if r.Canceled { + loading = false + } + } + ui.Render(view, 400, 300, 1) +} diff --git a/examples/gallery/pages_feedback.go b/examples/gallery/pages_feedback.go index f83272f..ea89ade 100644 --- a/examples/gallery/pages_feedback.go +++ b/examples/gallery/pages_feedback.go @@ -1,3 +1,113 @@ package main -var feedbackPages = []page{} +var feedbackPages = []page{ + { + ID: "091", Name: "Alert", ZhName: "提示条 警告 通知横幅", Category: "Feedback", + Demo: alertDemo, + Code: `open := true +r := mujica.Alert(c, &open, "The archive could not be read.", mujica.AlertOptions{ + Severity: mujica.SeverityError, + Title: "Import failed", + Action: "Retry", +}) +if r.Action { + // retry the import +}`, + Keys: "Tab moves to the action and the dismiss button; Enter or Space presses them.", + }, + { + ID: "092", Name: "Toast", ZhName: "轻提示 消息队列 吐司", Category: "Feedback", + Demo: toastDemo, + Code: `if mujica.Button(c, "Delete note", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { + mujica.ShowToast(c, mujica.ToastMessage{ID: "note-1", Message: "Note deleted", Action: "Undo"}) +} +// Last in the view: +if r := mujica.Toaster(c); r.Action == "note-1" { + // restore the note +}`, + Keys: "Toasts never take the focus. Tab reaches their buttons after the page; Escape closes the focused toast. Hover or focus pauses its timer.", + }, + { + ID: "093", Name: "Progress", ZhName: "进度条 环形进度 计量条", Category: "Feedback", + Demo: progressDemo, + Code: `mujica.Progress(c, 0.62, mujica.ProgressOptions{Label: "Uploading", ShowValue: true}) +mujica.Progress(c, 0, mujica.ProgressOptions{Shape: mujica.ProgressRing, Indeterminate: true, Label: "Connecting"}) +mujica.Meter(c, 93, mujica.MeterOptions{ + Max: 100, Label: "Disk", ShowValue: true, + Levels: &ui.MeterLevels{Warning: 70, Critical: 90}, +})`, + Keys: "Not focusable; assistive technology reads its name and value.", + }, + { + ID: "094", Name: "Spinner", ZhName: "加载标记 加载中 转圈", Category: "Feedback", + Demo: spinnerDemo, + Code: `mujica.Spinner(c, mujica.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true})`, + Keys: "Not focusable; read as an indeterminate progress indicator named by Label.", + }, + { + ID: "095", Name: "Skeleton", ZhName: "骨架屏 占位", Category: "Feedback", + Demo: skeletonDemo, + Code: `ui.Row(c).Gap(12).Children(func() { + mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonCircle}) + ui.Column(c).Grow(1).Children(func() { + mujica.Skeleton(c, mujica.SkeletonOptions{Lines: 2}) + }) +})`, + Keys: "Decorative; not focusable and hidden from assistive technology.", + }, + { + ID: "096", Name: "EmptyState", ZhName: "空状态 无数据 无搜索结果", Category: "Feedback", + Demo: emptyStateDemo, + Code: `r := mujica.EmptyState(c, mujica.EmptyStateOptions{ + Kind: mujica.EmptyNoResults, + Query: query, + Action: "Clear search", +}) +if r.Action { + query = "" +}`, + Keys: "Tab reaches the action button; Enter or Space presses it.", + }, + { + ID: "097", Name: "Result", ZhName: "结果页 成功 失败 无权限", Category: "Feedback", + Demo: resultDemo, + Code: `r := mujica.Result(c, mujica.ResultOptions{ + Kind: mujica.ResultForbidden, + Description: "You need the Editor role to change billing.", + Actions: []string{"Request access", "Go back"}, +}) +switch r.Action { +case 0: // send the request +case 1: // navigate back +}`, + Keys: "Tab moves through the actions, primary first; Enter or Space presses one.", + }, + { + ID: "098", Name: "StatusIndicator", ZhName: "状态标记 在线 离线", Category: "Feedback", + Demo: statusDemo, + Code: `mujica.StatusIndicator(c, mujica.StatusSuccess, mujica.StatusIndicatorOptions{Label: "API online"}) +mujica.StatusIndicator(c, mujica.StatusOffline, mujica.StatusIndicatorOptions{})`, + Keys: "Not focusable; read as a status with its word.", + }, + { + ID: "099", Name: "NotificationCenter", ZhName: "通知中心 消息列表", Category: "Feedback", + Demo: notificationDemo, + Code: `r := mujica.NotificationCenter(c, &items, mujica.NotificationCenterOptions{}) +if r.Action == "a" { + // open the build log +}`, + Keys: "The list is one Tab stop. Arrows move among notifications and their buttons; Enter marks the focused one read; Delete or Backspace removes it.", + }, + { + ID: "100", Name: "LoadingOverlay", ZhName: "加载遮罩 区域加载", Category: "Feedback", + Demo: loadingOverlayDemo, + Code: `r := mujica.LoadingOverlay(c, loading, mujica.LoadingOverlayOptions{Cancel: "Cancel"}, func() { + ui.Text(c, "Quarterly report") + mujica.Button(c, "Export", mujica.ButtonOptions{}) +}) +if r.Canceled { + loading = false +}`, + Keys: "While loading, the covered region takes no input; focus inside moves to Cancel, and Escape there cancels. The rest of the window stays usable.", + }, +} diff --git a/examples/gallery/pages_feedback_demos.go b/examples/gallery/pages_feedback_demos.go new file mode 100644 index 0000000..f7c6cbd --- /dev/null +++ b/examples/gallery/pages_feedback_demos.go @@ -0,0 +1,401 @@ +package main + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" + "mujicaui/theme" +) + +// feedbackSection titles a part of a demo. +func feedbackSection(c *ui.Context, title string, fn func()) { + ui.Column(c).Gap(8).FillWidth().Children(func() { + ui.Text(c, title).FontSize(theme.CaptionSize).TextColor(mujica.Tokens(c).TextMuted) + fn() + }) +} + +var feedbackSeverities = []mujica.Severity{mujica.SeverityInfo, mujica.SeveritySuccess, mujica.SeverityWarning, mujica.SeverityError} + +func alertDemo(c *ui.Context, log func(string)) { + type state struct { + sev int + title, action bool + closable bool + open bool + } + s := ui.Local(c.Root(), "091", func() state { return state{sev: 3, title: true, action: true, closable: true, open: true} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + ui.Segmented(c, &s.sev, "Info", "Success", "Warning", "Error") + ui.Row(c).Gap(16).Children(func() { + ui.Checkbox(c, &s.title, "Title") + ui.Checkbox(c, &s.action, "Action") + ui.Checkbox(c, &s.closable, "Closable") + }) + }) + feedbackSection(c, "Live", func() { + opts := mujica.AlertOptions{Severity: feedbackSeverities[s.sev]} + if s.title { + opts.Title = "Import failed" + } + if s.action { + opts.Action = "Retry" + } + var open *bool + if s.closable { + open = &s.open + } + r := mujica.Alert(c, open, "The archive could not be read.", opts) + if r.Action { + log("alert: action Retry") + } + if r.Closed { + log("alert: closed") + } + if s.closable && !s.open && mujica.Button(c, "Show again", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + s.open = true + log("alert: reopened") + } + }) + feedbackSection(c, "Severities", func() { + for _, sev := range feedbackSeverities { + mujica.Alert(c, nil, "Severity "+sev.String()+" uses its own icon, bar and name.", mujica.AlertOptions{Severity: sev}).Element.Key(sev) + } + }) + }) +} + +func toastDemo(c *ui.Context, log func(string)) { + type state struct { + sev int + action bool + seconds float64 + n int + note string + } + s := ui.Local(c.Root(), "092", func() state { return state{seconds: 5} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + ui.Segmented(c, &s.sev, "Info", "Success", "Warning", "Error") + ui.Checkbox(c, &s.action, "Undo action") + ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { + ui.Text(c, fmt.Sprintf("Duration %.0f s (errors stay until closed)", s.seconds)) + ui.Slider(c, &s.seconds, 1, 10).Width(200) + }) + }) + feedbackSection(c, "Actions", func() { + ui.Row(c).Gap(12).Wrap().Children(func() { + if mujica.Button(c, "Show toast", mujica.ButtonOptions{}).Clicked() { + s.n++ + m := mujica.ToastMessage{ID: fmt.Sprint(s.n), Severity: feedbackSeverities[s.sev], Message: fmt.Sprintf("Message %d", s.n)} + if s.action { + m.Action = "Undo" + } + if m.Severity != mujica.SeverityError { + m.Duration = time.Duration(s.seconds * float64(time.Second)) + } + mujica.ShowToast(c, m) + log("toast: show " + m.ID) + } + if mujica.Button(c, "Show five at once", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + for range 5 { + s.n++ + mujica.ShowToast(c, mujica.ToastMessage{ID: fmt.Sprint(s.n), Message: fmt.Sprintf("Queued %d", s.n)}) + } + log("toast: five queued, three visible") + } + if mujica.Button(c, "Repeat same ID", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + s.n++ + mujica.ShowToast(c, mujica.ToastMessage{ID: "sync", Message: fmt.Sprintf("Synced %d times", s.n)}) + log("toast: dedupe id sync") + } + }) + ui.Text(c, "Type here while toasts appear; the focus stays:") + ui.TextInput(c, &s.note).Width(320) + }) + }) + if r := mujica.Toaster(c); r.Action != "" { + log("toast: action " + r.Action) + } else if r.Closed != "" { + log("toast: closed " + r.Closed) + } +} + +func progressDemo(c *ui.Context, log func(string)) { + type state struct { + value, meter float64 + indet, showValue bool + ring int + } + s := ui.Local(c.Root(), "093", func() state { return state{value: 0.62, meter: 72, showValue: true} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + ui.Segmented(c, &s.ring, "Linear", "Ring") + ui.Row(c).Gap(16).Children(func() { + if ui.Checkbox(c, &s.indet, "Indeterminate").Changed() { + log(fmt.Sprintf("progress: indeterminate %v", s.indet)) + } + ui.Checkbox(c, &s.showValue, "Show value") + }) + ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { + ui.Text(c, "Value") + if ui.Slider(c, &s.value, 0, 1).Width(240).Changed() { + log(fmt.Sprintf("progress: value %.2f", s.value)) + } + }) + }) + feedbackSection(c, "Progress", func() { + mujica.Progress(c, s.value, mujica.ProgressOptions{ + Shape: mujica.ProgressShape(s.ring), Indeterminate: s.indet, Label: "Uploading", ShowValue: s.showValue, + }) + }) + feedbackSection(c, "Meter with thresholds 70 and 90", func() { + ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { + ui.Text(c, "Level") + if ui.Slider(c, &s.meter, 0, 100).Width(240).Changed() { + log(fmt.Sprintf("meter: value %.0f", s.meter)) + } + }) + v := float64(int(s.meter)) + mujica.Meter(c, v, mujica.MeterOptions{Max: 100, Label: "Disk", ShowValue: true, Levels: &ui.MeterLevels{Warning: 70, Critical: 90}}) + mujica.Meter(c, 100-v, mujica.MeterOptions{Max: 100, Label: "Battery (low is bad)", ShowValue: true, Levels: &ui.MeterLevels{Warning: 30, Critical: 10}}) + }) + }) +} + +func spinnerDemo(c *ui.Context, log func(string)) { + type state struct { + size float64 + showLabel, active bool + } + s := ui.Local(c.Root(), "094", func() state { return state{size: 20, showLabel: true, active: true} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { + ui.Text(c, fmt.Sprintf("Size %.0f", s.size)) + ui.Slider(c, &s.size, 12, 48).Width(200) + }) + ui.Row(c).Gap(16).Children(func() { + ui.Checkbox(c, &s.showLabel, "Show label") + if ui.Checkbox(c, &s.active, "Loading (unchecked stops the animation)").Changed() { + log(fmt.Sprintf("spinner: loading %v", s.active)) + } + }) + }) + feedbackSection(c, "Live", func() { + if s.active { + mujica.Spinner(c, mujica.SpinnerOptions{Size: float32(s.size), Label: "Syncing", ShowLabel: s.showLabel}) + } else { + ui.Text(c, "Done") + } + }) + feedbackSection(c, "In a button", func() { + mujica.Button(c, "Saving", mujica.ButtonOptions{Loading: true}) + }) + }) +} + +func skeletonDemo(c *ui.Context, log func(string)) { + type state struct { + lines float64 + loading bool + } + s := ui.Local(c.Root(), "095", func() state { return state{lines: 3, loading: true} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { + ui.Text(c, fmt.Sprintf("Lines %.0f", s.lines)) + ui.Slider(c, &s.lines, 1, 6).Width(200) + }) + if ui.Checkbox(c, &s.loading, "Loading").Changed() { + log(fmt.Sprintf("skeleton: loading %v", s.loading)) + } + }) + feedbackSection(c, "Card (same size loading and loaded)", func() { + ui.Row(c).Gap(12).Padding(16).Width(420).Background(mujica.Tokens(c).Surface).Radius(theme.CardRadius).Children(func() { + if s.loading { + mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonCircle}) + ui.Column(c).Grow(1).Children(func() { + mujica.Skeleton(c, mujica.SkeletonOptions{Lines: int(s.lines)}) + }) + return + } + ui.Box(c).Size(40, 40).Radius(20).Background(mujica.Tokens(c).Selection) + ui.Column(c).Grow(1).Children(func() { + for i := range int(s.lines) { + ui.Text(c, fmt.Sprintf("Loaded line %d", i+1)).LineHeight(theme.LineHeight) + } + }) + }) + }) + feedbackSection(c, "Rect", func() { + mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonRect, Width: 420, Height: 96}) + }) + }) +} + +func emptyStateDemo(c *ui.Context, log func(string)) { + type state struct { + kind int + query string + icon, action bool + } + s := ui.Local(c.Root(), "096", func() state { return state{kind: 1, query: "quarterly", icon: true, action: true} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + ui.Segmented(c, &s.kind, "No data", "No results") + ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() { + ui.Checkbox(c, &s.icon, "Icon") + ui.Checkbox(c, &s.action, "Action") + if s.kind == 1 { + ui.TextInput(c, &s.query).Width(200) + } + }) + }) + feedbackSection(c, "Live", func() { + opts := mujica.EmptyStateOptions{Kind: mujica.EmptyKind(s.kind), NoIcon: !s.icon} + if s.kind == 1 { + opts.Query = s.query + } + if s.action { + opts.Action = []string{"New project", "Clear search"}[s.kind] + } + if mujica.EmptyState(c, opts).Action { + log("empty state: action " + opts.Action) + s.query = "" + } + }) + }) +} + +func resultDemo(c *ui.Context, log func(string)) { + type state struct{ kind int } + s := ui.Local(c.Root(), "097", func() state { return state{kind: 2} }) + descs := []string{"Your order has been placed.", "The payment was declined by the bank.", "You need the Editor role to change billing."} + actions := [][]string{{"View order", "Keep shopping"}, {"Try again", "Back to cart"}, {"Request access", "Go back"}} + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + ui.Segmented(c, &s.kind, "Success", "Failure", "Forbidden") + }) + r := mujica.Result(c, mujica.ResultOptions{Kind: mujica.ResultKind(s.kind), Description: descs[s.kind], Actions: actions[s.kind]}) + if r.Action >= 0 { + log("result: action " + actions[s.kind][r.Action]) + } + }) +} + +func statusDemo(c *ui.Context, log func(string)) { + type state struct { + status int + custom bool + } + s := ui.Local(c.Root(), "098", func() state { return state{} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + if ui.Segmented(c, &s.status, "Success", "Warning", "Error", "Offline", "Busy").Changed() { + log("status: " + mujica.Status(s.status).String()) + } + ui.Checkbox(c, &s.custom, "Custom label") + }) + feedbackSection(c, "Live", func() { + opts := mujica.StatusIndicatorOptions{} + if s.custom { + opts.Label = "Database replica" + } + mujica.StatusIndicator(c, mujica.Status(s.status), opts) + }) + feedbackSection(c, "All states (shape and word, not color alone)", func() { + ui.Row(c).Gap(20).Wrap().Children(func() { + for st := mujica.StatusSuccess; st <= mujica.StatusBusy; st++ { + mujica.StatusIndicator(c, st, mujica.StatusIndicatorOptions{}).Key(st) + } + }) + }) + }) +} + +func notificationDemo(c *ui.Context, log func(string)) { + type state struct { + items []mujica.Notification + n int + } + s := ui.Local(c.Root(), "099", func() state { + return state{items: []mujica.Notification{ + {ID: "a", Title: "Build failed", Body: "main · 3 tests failing", Time: "2 min ago", Action: "View log"}, + {ID: "b", Title: "Invite accepted", Body: "Mira joined the workspace", Time: "1 h ago"}, + {ID: "c", Title: "Backup complete", Time: "Yesterday", Read: true}, + }} + }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Caller-owned list", func() { + if mujica.Button(c, "Add notification", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + s.n++ + id := fmt.Sprintf("new-%d", s.n) + s.items = append([]mujica.Notification{{ID: id, Title: fmt.Sprintf("Comment %d on the design doc", s.n), Time: "now"}}, s.items...) + log("notifications: added " + id) + } + }) + ui.Column(c).Width(520).Height(420).Children(func() { + r := mujica.NotificationCenter(c, &s.items, mujica.NotificationCenterOptions{}) + for _, ev := range [][2]string{{"action", r.Action}, {"toggled", r.Toggled}, {"opened", r.Opened}, {"removed", r.Removed}} { + if ev[1] != "" { + log("notifications: " + ev[0] + " " + ev[1]) + } + } + if r.MarkedAllRead { + log("notifications: marked all read") + } + if r.ClearedAll { + log("notifications: cleared all") + } + }) + }) +} + +func loadingOverlayDemo(c *ui.Context, log func(string)) { + type state struct { + loading, cancel bool + saves int + } + s := ui.Local(c.Root(), "100", func() state { return state{loading: true, cancel: true} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + ui.Row(c).Gap(16).Children(func() { + if ui.Checkbox(c, &s.loading, "Loading").Changed() { + log(fmt.Sprintf("overlay: loading %v", s.loading)) + } + ui.Checkbox(c, &s.cancel, "Cancel action") + }) + }) + feedbackSection(c, "Covered region", func() { + opts := mujica.LoadingOverlayOptions{Label: "Generating report…"} + if s.cancel { + opts.Cancel = "Cancel" + } + r := mujica.LoadingOverlay(c, s.loading, opts, func() { + ui.Column(c).Gap(8).Padding(16).FillWidth().AlignItems(ui.Start).Background(mujica.Tokens(c).Surface).Radius(theme.CardRadius).Children(func() { + ui.Text(c, "Quarterly report").Font(theme.SerifFont).FontSize(theme.H3Size) + ui.Text(c, fmt.Sprintf("Exported %d times.", s.saves)) + if mujica.Button(c, "Export", mujica.ButtonOptions{}).Clicked() { + s.saves++ + log("overlay: export clicked") + } + }) + }) + r.Element.Width(420) + if r.Canceled { + s.loading = false + log("overlay: canceled") + } + }) + feedbackSection(c, "Outside the region (always usable)", func() { + if mujica.Button(c, "Outside button", mujica.ButtonOptions{Variant: mujica.Secondary}).AlignSelf(ui.Start).Clicked() { + log("overlay: outside clicked") + } + }) + }) +} diff --git a/loading_overlay_test.go b/loading_overlay_test.go index d3adc7b..67afefb 100644 --- a/loading_overlay_test.go +++ b/loading_overlay_test.go @@ -108,7 +108,7 @@ func TestLoadingOverlaySnapshot(t *testing.T) { snapshot(t, "loading_overlay", 420, 220, func(c *ui.Context) { ui.Column(c).Padding(16).Children(func() { LoadingOverlay(c, true, LoadingOverlayOptions{Cancel: "Cancel"}, func() { - ui.Column(c).Padding(16).Gap(8).Background(Tokens(c).Surface).Radius(4).Children(func() { + ui.Column(c).Padding(16).Gap(8).AlignItems(ui.Start).Background(Tokens(c).Surface).Radius(4).Children(func() { ui.Text(c, "Quarterly report") ui.Text(c, "Revenue, churn and pipeline for Q3.") Button(c, "Export", ButtonOptions{}) diff --git a/progress_test.go b/progress_test.go index de54799..78c5bdf 100644 --- a/progress_test.go +++ b/progress_test.go @@ -97,7 +97,9 @@ func TestProgressAndMeterPanics(t *testing.T) { "progress NaN": func(c *ui.Context) { Progress(c, math.NaN(), ProgressOptions{}) }, "meter empty range": func(c *ui.Context) { Meter(c, 5, MeterOptions{Min: 10, Max: 10}) }, "meter outside": func(c *ui.Context) { Meter(c, 2, MeterOptions{}) }, - "meter levels": func(c *ui.Context) { Meter(c, 5, MeterOptions{Max: 10, Levels: &ui.MeterLevels{Warning: 8, Critical: 12}}) }, + "meter levels": func(c *ui.Context) { + Meter(c, 5, MeterOptions{Max: 10, Levels: &ui.MeterLevels{Warning: 8, Critical: 12}}) + }, } { func() { defer func() { diff --git a/testdata/snapshots/darwin/loading_overlay-dark.png b/testdata/snapshots/darwin/loading_overlay-dark.png index 2d14543..9f0ba2e 100644 Binary files a/testdata/snapshots/darwin/loading_overlay-dark.png and b/testdata/snapshots/darwin/loading_overlay-dark.png differ diff --git a/testdata/snapshots/darwin/loading_overlay-light.png b/testdata/snapshots/darwin/loading_overlay-light.png index 732747f..fbfc45a 100644 Binary files a/testdata/snapshots/darwin/loading_overlay-light.png and b/testdata/snapshots/darwin/loading_overlay-light.png differ