From 719a0591131fcf77d0420323a4573fc9277362f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 14:39:12 -0400 Subject: [PATCH] WIP essentials 16 --- docs/components/481-split-button.md | 50 ++++ docs/components/482-copy-button.md | 40 +++ docs/components/483-hold-to-confirm.md | 42 +++ docs/components/484-bulk-action-bar.md | 46 +++ docs/components/485-scrub-input.md | 43 +++ docs/components/486-currency-input.md | 45 +++ docs/components/487-masked-input.md | 43 +++ docs/components/488-mention-input.md | 57 ++++ docs/components/489-choice-chips.md | 45 +++ docs/components/490-knob.md | 45 +++ docs/components/491-vertical-slider.md | 46 +++ docs/components/492-color-palette.md | 47 +++ docs/components/493-signature-pad.md | 44 +++ docs/components/494-banner.md | 48 +++ docs/components/495-undo-toast.md | 47 +++ docs/components/496-masonry.md | 47 +++ docs/components/497-resizable-panel-group.md | 48 +++ docs/components/498-dock-panel.md | 47 +++ docs/components/499-qr-code.md | 41 +++ docs/components/500-activity-feed.md | 43 +++ examples/gallery/pages_essentials.go | 261 ++++++++++++++++- examples/gallery/pages_essentials_demos.go | 267 +++++++++++++++++ examples/gallery/pages_essentials_more.go | 292 +++++++++++++++++++ 23 files changed, 1733 insertions(+), 1 deletion(-) create mode 100644 docs/components/481-split-button.md create mode 100644 docs/components/482-copy-button.md create mode 100644 docs/components/483-hold-to-confirm.md create mode 100644 docs/components/484-bulk-action-bar.md create mode 100644 docs/components/485-scrub-input.md create mode 100644 docs/components/486-currency-input.md create mode 100644 docs/components/487-masked-input.md create mode 100644 docs/components/488-mention-input.md create mode 100644 docs/components/489-choice-chips.md create mode 100644 docs/components/490-knob.md create mode 100644 docs/components/491-vertical-slider.md create mode 100644 docs/components/492-color-palette.md create mode 100644 docs/components/493-signature-pad.md create mode 100644 docs/components/494-banner.md create mode 100644 docs/components/495-undo-toast.md create mode 100644 docs/components/496-masonry.md create mode 100644 docs/components/497-resizable-panel-group.md create mode 100644 docs/components/498-dock-panel.md create mode 100644 docs/components/499-qr-code.md create mode 100644 docs/components/500-activity-feed.md create mode 100644 examples/gallery/pages_essentials_demos.go create mode 100644 examples/gallery/pages_essentials_more.go diff --git a/docs/components/481-split-button.md b/docs/components/481-split-button.md new file mode 100644 index 0000000..d2385f2 --- /dev/null +++ b/docs/components/481-split-button.md @@ -0,0 +1,50 @@ +# 481 SplitButton 拆分按钮 + +## 用途 + +主操作与下拉菜单合为一体:左半执行主操作,右半打开 `PopupMenu` 列出相关操作。两半各自获得焦点。 + +## 最小示例 + +```go +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.SplitButton(c, "Save", mujica.SplitButtonOptions{}, func(m *mujica.PopupMenu) { + if m.Item("Save as…", mujica.PopupMenuItemOptions{}) { + // ask for a file name + } + }) + if r.Changed() { + // save + } +} +``` + +签名:`func SplitButton(c *ui.Context, label string, opts SplitButtonOptions, build func(m *PopupMenu)) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `label` | `string` | 必填 | 主操作文字。 | +| `build` | `func(*PopupMenu)` | 必填 | 填充菜单项,`Item` 报告选择。 | +| `Variant` | `ButtonVariant` | `Primary` | 只接受 `Primary` 或 `Secondary`,其余 panic。 | +| `Icon` | `*ui.SVG` | `nil` | 主操作图标。 | +| `MenuLabel` | `string` | “More options” | 菜单半的无障碍名称。 | +| `Disabled` | `bool` | `false` | 两半都不可用,打开的菜单关闭。 | + +## 状态 + +两半分别有悬停、按下、焦点环;菜单打开时右半保持按下色。禁用时 MyGo 统一减淡。 + +## 事件 + +`Changed()`:主操作被触发。菜单项通过 `m.Item` 的返回值报告。 + +## 键盘操作 + +Tab 依次聚焦主操作与菜单按钮;Enter/Space 触发主操作;在菜单按钮上 Enter、Space 或 Down 打开菜单,方向键选择,Escape 关闭。 + +## 限制 + +菜单宽度至少等于整个按钮组;菜单内容由调用方提供。 diff --git a/docs/components/482-copy-button.md b/docs/components/482-copy-button.md new file mode 100644 index 0000000..4c76b83 --- /dev/null +++ b/docs/components/482-copy-button.md @@ -0,0 +1,40 @@ +# 482 CopyButton 复制按钮 + +## 用途 + +把文字放到剪贴板,按钮随即显示“已复制”与对勾两秒,并向屏幕阅读器播报。 + +## 最小示例 + +```go +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.CopyButton(c, "https://example.com/invite/42", mujica.CopyButtonOptions{Label: "Copy link"}) +} +``` + +签名:`func CopyButton(c *ui.Context, text string, opts CopyButtonOptions) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `text` | `string` | 必填 | 要复制的文字;不进日志,只记录长度。 | +| `Label` | `string` | `""` | 可见文字;为空时是方形图标按钮。 | +| `Disabled` | `bool` | `false` | 不复制。 | + +## 状态 + +默认、悬停、按下、焦点;复制后两秒显示“已复制”,宽度按两段文字中较长者预留,不跳动。 + +## 事件 + +`Changed()`:本帧完成复制。 + +## 键盘操作 + +Tab 聚焦;Enter 或 Space 复制。 + +## 限制 + +剪贴板写入由 MyGo 完成,无法得知系统是否拒绝写入。 diff --git a/docs/components/483-hold-to-confirm.md b/docs/components/483-hold-to-confirm.md new file mode 100644 index 0000000..22fa635 --- /dev/null +++ b/docs/components/483-hold-to-confirm.md @@ -0,0 +1,42 @@ +# 483 HoldToConfirm 按住确认 + +## 用途 + +危险操作必须按住指针或空格键一段时间才执行,按住期间填充进度;减少动画时改为显示剩余秒数。 + +## 最小示例 + +```go +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.HoldToConfirm(c, "Delete project", mujica.HoldToConfirmOptions{}).Changed() { + // delete the project + } +} +``` + +签名:`func HoldToConfirm(c *ui.Context, label string, opts HoldToConfirmOptions) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `label` | `string` | 必填 | 按钮文字。 | +| `Duration` | `time.Duration` | 1.5 秒 | 需按住的时长;负数 panic。 | +| `Disabled` | `bool` | `false` | 不响应。 | + +## 状态 + +危险配色;按住时从左向右填充;减少动画时文字变为“继续按住… 1.2 秒”,宽度预留。松开或失去焦点即重置。完成后播报“已确认”。 + +## 事件 + +`Changed()`:按满时长后报告一次;同一次按住不会重复报告。 + +## 键盘操作 + +Tab 聚焦后按住空格;单击、Enter 或短按空格都不会执行。 + +## 限制 + +按住空格依赖系统送来的按键抬起;窗口失去焦点时按住状态即重置。 diff --git a/docs/components/484-bulk-action-bar.md b/docs/components/484-bulk-action-bar.md new file mode 100644 index 0000000..1d0b1c0 --- /dev/null +++ b/docs/components/484-bulk-action-bar.md @@ -0,0 +1,46 @@ +# 484 BulkActionBar 批量操作条 + +## 用途 + +多选后出现的操作条:显示选中数、调用方提供的批量操作和“清除选择”。 + +## 最小示例 + +```go +selected := 3 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.BulkActionBar(c, selected, mujica.BulkActionBarOptions{}, func() { + mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}) + }) + if r.Changed() { + selected = 0 + } +} +``` + +签名:`func BulkActionBar(c *ui.Context, selected int, opts BulkActionBarOptions, actions func()) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `selected` | `int` | 必填 | 选中数;0 时不显示(`Element` 为 nil),负数 panic。 | +| `actions` | `func()` | `nil` | 批量操作按钮插槽。 | +| `Disabled` | `bool` | `false` | 整条不可用。 | + +## 状态 + +浮层样式,160 ms 淡入;选中数变化时播报。 + +## 事件 + +`Changed()`:用户清除选择(按钮或 Escape)。 + +## 键盘操作 + +Tab 进入操作按钮与“清除选择”;焦点在条内时 Escape 清除选择。 + +## 限制 + +位置由调用方放置,不自动悬浮在窗口底部。 diff --git a/docs/components/485-scrub-input.md b/docs/components/485-scrub-input.md new file mode 100644 index 0000000..1a6fc45 --- /dev/null +++ b/docs/components/485-scrub-input.md @@ -0,0 +1,43 @@ +# 485 ScrubInput 拖动数值输入 + +## 用途 + +按住标签左右拖动调整数值,每 4 DIP 一步;旁边的 NumberInput 可直接输入。范围、步长与精度规则与 NumberInput 相同。 + +## 最小示例 + +```go +opacity := 0.6 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ScrubInput(c, "Opacity", &opacity, mujica.ScrubInputOptions{Max: 1, Step: 0.1}) +} +``` + +签名:`func ScrubInput(c *ui.Context, label string, value *float64, opts ScrubInputOptions) InputResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `label` | `string` | 必填 | 可拖动的标签,也是字段名称。 | +| `Min`, `Max` | `float64` | 无界 | 相等表示无界。 | +| `Step` | `float64` | 1 | 每一步的增量。 | +| `Precision` | `int` | 取自 Step | 保留小数位。 | +| `Disabled` | `bool` | `false` | 不可拖动也不可输入。 | + +## 状态 + +标签悬停或拖动时变为强调色;拖到边界即停止。 + +## 事件 + +`Changed()`:拖动或输入改变了值;`Submitted()` 同 NumberInput。 + +## 键盘操作 + +标签不可聚焦;键盘在输入框内用 Up/Down 步进或直接输入。 + +## 限制 + +拖动只对指针可用。 diff --git a/docs/components/486-currency-input.md b/docs/components/486-currency-input.md new file mode 100644 index 0000000..9da7a7a --- /dev/null +++ b/docs/components/486-currency-input.md @@ -0,0 +1,45 @@ +# 486 CurrencyInput 货币输入 + +## 用途 + +以最小单位(分)的整数保存金额,避免浮点误差;显示币种符号,未聚焦时按千分位分组,聚焦后显示纯数字便于编辑。 + +## 最小示例 + +```go +cents := int64(123450) +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.CurrencyInput(c, ¢s, mujica.CurrencyInputOptions{Symbol: "$", Label: "Price"}) +} +``` + +签名:`func CurrencyInput(c *ui.Context, value *int64, opts CurrencyInputOptions) InputResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `value` | `*int64` | 必填 | 金额的最小单位数。 | +| `Symbol` | `string` | `""` | 前缀符号,如 `$`、`¥`。 | +| `Decimals` | `int` | 2 | 小数位,0–6,越界 panic。 | +| `NoFraction` | `bool` | `false` | 无小数(如日元);与 `Decimals` 同设 panic。 | +| `Label` | `string` | `""` | 无障碍名称。 | +| `Disabled` | `bool` | `false` | 不可编辑。 | +| `Error` | `string` | `""` | 调用方错误信息。 | + +## 状态 + +非法输入或小数位过多时保留输入并显示具体错误,值不变。 + +## 事件 + +`Changed()`:合法输入改变了值。 + +## 键盘操作 + +Tab 聚焦;文本编辑、选择、剪贴板与撤销由 MyGo 编辑器提供。 + +## 限制 + +千分位与小数点固定为 `,` 与 `.`。 diff --git a/docs/components/487-masked-input.md b/docs/components/487-masked-input.md new file mode 100644 index 0000000..7b15439 --- /dev/null +++ b/docs/components/487-masked-input.md @@ -0,0 +1,43 @@ +# 487 MaskedInput 掩码输入 + +## 用途 + +按格式掩码输入电话、卡号等:`9` 数字、`a` 字母、`*` 二者皆可,其余字符原样显示。值只保存填入的字符。 + +## 最小示例 + +```go +phone := "" +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.MaskedInput(c, &phone, mujica.MaskedInputOptions{Mask: "(999) 999-9999", Label: "Phone"}) +} +``` + +签名:`func MaskedInput(c *ui.Context, value *string, opts MaskedInputOptions) InputResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Mask` | `string` | 必填 | 掩码;没有可填位 panic。 | +| `value` | `*string` | 必填 | 已填字符;超长或不符掩码 panic。 | +| `Label` | `string` | `""` | 无障碍名称。 | +| `Disabled` | `bool` | `false` | 不可编辑。 | +| `Error` | `string` | `""` | 错误信息。 | + +## 状态 + +未填位置显示下划线;输入法组合期间显示带下划线的组合文字,不提交、不跳格,只有提交的文字进入掩码。 + +## 事件 + +`Changed()`:值变化;`Submitted()`:Enter。另有 `MaskFormat(mask, raw)` 生成带格式文字。 + +## 键盘操作 + +左右方向键、Home/End 移动光标;Backspace/Delete 删除;粘贴只取符合掩码的字符;Enter 提交。 + +## 限制 + +自绘编辑器:没有文字选择与撤销,复制得到未格式化的值。 diff --git a/docs/components/488-mention-input.md b/docs/components/488-mention-input.md new file mode 100644 index 0000000..8f8472b --- /dev/null +++ b/docs/components/488-mention-input.md @@ -0,0 +1,57 @@ +# 488 MentionInput 提及输入 + +## 用途 + +输入 @ 后弹出候选,选中后插入整体提及;提及作为一个单元被光标跳过、被一次删除。文本与提及列表都由调用方持有。 + +## 最小示例 + +```go +text, mentions := "", []mujica.Mention(nil) +people := []mujica.MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Bob"}} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.MentionInput(c, &text, &mentions, mujica.MentionInputOptions{ + Label: "Comment", + Candidates: func(q string) []mujica.MentionCandidate { + var out []mujica.MentionCandidate + for _, p := range people { + if strings.HasPrefix(strings.ToLower(p.Name), strings.ToLower(q)) { + out = append(out, p) + } + } + return out + }, + }) +} +``` + +签名:`func MentionInput(c *ui.Context, value *string, mentions *[]Mention, opts MentionInputOptions) InputResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `value` | `*string` | 必填 | 全文。 | +| `mentions` | `*[]Mention` | 必填 | 提及(ID、Name、Start、End,按 rune 计);与文本不符 panic。 | +| `Candidates` | `func(string) []MentionCandidate` | 必填 | 按 @ 后文字返回候选,仅在该文字变化时调用。 | +| `Placeholder` | `string` | `""` | 占位文字。 | +| `Label` | `string` | `""` | 无障碍名称。 | +| `Disabled` | `bool` | `false` | 不可编辑。 | +| `Error` | `string` | `""` | 错误信息。 | + +## 状态 + +提及以强调色加浅底显示;候选列表用浮层样式,当前项有选中底色与左侧标记。输入法组合期间不提交。 + +## 事件 + +`Changed()`:文本或提及变化;`Submitted()`:候选关闭时按 Enter。 + +## 键盘操作 + +@ 后 Up/Down 选择候选,Enter 或 Tab 插入,Escape 关闭;左右键跨过提及,Backspace/Delete 整体删除;Shift+方向键选择;支持复制、剪切、粘贴、全选、撤销、重做命令。 + +## 限制 + +MyGo 编辑器没有设置光标的公开接口(ui/editor.go:117 只在文本变化时截断光标),无法在插入提及后把光标放到提及之后,因此本组件自绘单行编辑器;不支持多行与双击选词。 diff --git a/docs/components/489-choice-chips.md b/docs/components/489-choice-chips.md new file mode 100644 index 0000000..3d61812 --- /dev/null +++ b/docs/components/489-choice-chips.md @@ -0,0 +1,45 @@ +# 489 ChoiceChips 筛选芯片 + +## 用途 + +单选或多选的筛选芯片,自动换行。选中芯片用选中底色、强调边框和对勾表示。 + +## 最小示例 + +```go +var status []string +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ChoiceChips(c, &status, []mujica.ToggleItem[string]{ + {Value: "open", Label: "Open"}, {Value: "closed", Label: "Closed"}, + }, mujica.ChoiceChipsOptions{Multiple: true, Label: "Status"}) +} +``` + +签名:`func ChoiceChips[T comparable](c *ui.Context, value *[]T, items []ToggleItem[T], opts ChoiceChipsOptions) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `value` | `*[]T` | 必填 | 选中的值;单选时多于一个 panic。 | +| `items` | `[]ToggleItem[T]` | 必填 | 芯片(Value、Label、Icon、Name、Disabled)。 | +| `Multiple` | `bool` | `false` | 允许多选;单选时可再次点击取消。 | +| `Disabled` | `bool` | `false` | 整组禁用。 | +| `Label` | `string` | `""` | 组名称。 | + +## 状态 + +默认、悬停、按下、焦点、选中、禁用;颜色过渡 120 ms。 + +## 事件 + +`Changed()`:选择变化。 + +## 键盘操作 + +整组是一个 Tab 停靠点;左右方向键移动并跳过禁用芯片;Space/Enter 切换。 + +## 限制 + +无。 diff --git a/docs/components/490-knob.md b/docs/components/490-knob.md new file mode 100644 index 0000000..0e77e1e --- /dev/null +++ b/docs/components/490-knob.md @@ -0,0 +1,45 @@ +# 490 Knob 旋钮 + +## 用途 + +圆形旋钮:上下拖动或方向键调整数值,显示刻度、数值弧和菱形指针。 + +## 最小示例 + +```go +gain := 35.0 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Knob(c, &gain, mujica.KnobOptions{Label: "Gain", Ticks: 11}) +} +``` + +签名:`func Knob(c *ui.Context, value *float64, opts KnobOptions) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Min`, `Max` | `float64` | 0–100 | 范围;`Max <= Min` panic。 | +| `Step` | `float64` | 连续 | 步长,按 NumberInput 网格吸附。 | +| `Ticks` | `int` | 0 | 刻度数;负数 panic。 | +| `Size` | `float32` | 56 | 直径。 | +| `Format` | `func(float64) string` | 自动 | 数值文字。 | +| `Disabled` | `bool` | `false` | 不可调。 | +| `Label` | `string` | “Knob” | 名称与标题。 | + +## 状态 + +悬停与拖动时表盘变亮;焦点环为圆形。基于 MyGo SliderBase,读屏视为滑块。 + +## 事件 + +`Changed()`:值变化。 + +## 键盘操作 + +方向键按步调整(连续时每步 1%),Home/End 到两端;上拖增大,160 DIP 走完全程。 + +## 限制 + +无。 diff --git a/docs/components/491-vertical-slider.md b/docs/components/491-vertical-slider.md new file mode 100644 index 0000000..6aee4e4 --- /dev/null +++ b/docs/components/491-vertical-slider.md @@ -0,0 +1,46 @@ +# 491 VerticalSlider 纵向滑块 + +## 用途 + +纵向放置的 Slider:上大下小,规则与 Slider 相同(步长网格、刻度、数值气泡)。 + +## 最小示例 + +```go +level := 40.0 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.VerticalSlider(c, &level, mujica.VerticalSliderOptions{Step: 10, Label: "Level"}) +} +``` + +签名:`func VerticalSlider(c *ui.Context, value *float64, opts VerticalSliderOptions) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Min`, `Max` | `float64` | 0–100 | 范围。 | +| `Step` | `float64` | 连续 | 步长。 | +| `Marks` | `[]SliderMark` | `nil` | 刻度与标签。 | +| `Height` | `float32` | 160 | 轨道长度;负数 panic。 | +| `ShowValue` | `bool` | `false` | 悬停、拖动或聚焦时显示数值。 | +| `Format` | `func(float64) string` | 自动 | 数值文字。 | +| `Disabled` | `bool` | `false` | 不可调。 | +| `Label` | `string` | `""` | 名称。 | + +## 状态 + +菱形滑块,悬停与拖动放大;填充段为 AccentText。 + +## 事件 + +`Changed()`:值变化。 + +## 键盘操作 + +Up/Right 增大,Down/Left 减小,Home/End 到两端;点击或拖动设置。 + +## 限制 + +无。 diff --git a/docs/components/492-color-palette.md b/docs/components/492-color-palette.md new file mode 100644 index 0000000..4ce538f --- /dev/null +++ b/docs/components/492-color-palette.md @@ -0,0 +1,47 @@ +# 492 ColorPalette 预设色板 + +## 用途 + +从预设色块中选择颜色,可附带自定义颜色入口(复用 ColorPicker)。选中色块有内框与对勾,标记颜色按对比度自动取深或浅。 + +## 最小示例 + +```go +color := ui.Hex("#7D2034") +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ColorPalette(c, &color, []mujica.ColorSwatch{ + {Color: ui.Hex("#7D2034"), Name: "Wine"}, + {Color: ui.Hex("#AD9164"), Name: "Old gold"}, + {Color: ui.Hex("#355E80"), Name: "Slate blue"}, + }, mujica.ColorPaletteOptions{Custom: true, Label: "Tag color"}) +} +``` + +签名:`func ColorPalette(c *ui.Context, value *ui.Color, swatches []ColorSwatch, opts ColorPaletteOptions) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `swatches` | `[]ColorSwatch` | 必填 | 颜色与名称;缺名称 panic。 | +| `Columns` | `int` | 8 | 每行色块数;负数 panic。 | +| `Custom` | `bool` | `false` | 显示自定义颜色入口。 | +| `Disabled` | `bool` | `false` | 不可选。 | +| `Label` | `string` | `""` | 组名称。 | + +## 状态 + +悬停加深边框;选中有内框与对勾;每个色块提示其名称。 + +## 事件 + +`Changed()`:颜色变化(含自定义)。 + +## 键盘操作 + +色板是一个 Tab 停靠点;左右键逐个移动,上下键跨行,Enter/Space 选择。 + +## 限制 + +无。 diff --git a/docs/components/493-signature-pad.md b/docs/components/493-signature-pad.md new file mode 100644 index 0000000..bbd9b13 --- /dev/null +++ b/docs/components/493-signature-pad.md @@ -0,0 +1,44 @@ +# 493 SignaturePad 签名板 + +## 用途 + +用指针书写签名,可清除,并可用 `SignaturePath` 导出 SVG 路径数据。 + +## 最小示例 + +```go +var strokes [][]mujica.SignaturePoint +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.SignaturePad(c, &strokes, mujica.SignaturePadOptions{}).Changed() { + _ = mujica.SignaturePath(strokes) // SVG path data to store + } +} +``` + +签名:`func SignaturePad(c *ui.Context, strokes *[][]SignaturePoint, opts SignaturePadOptions) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `strokes` | `*[][]SignaturePoint` | 必填 | 笔画,坐标为相对画板的 DIP。 | +| `Height` | `float32` | 160 | 画板高度;宽度填满。 | +| `Label` | `string` | “Signature” | 名称。 | +| `Disabled` | `bool` | `false` | 不可书写与清除。 | + +## 状态 + +空白时显示“请在此签名”;有签名基线;清除按钮在无笔画时禁用。 + +## 事件 + +`Changed()`:完成一笔或清除。 + +## 键盘操作 + +Tab 聚焦画板,Delete/Backspace 清除;Tab 到“清除”按钮。 + +## 限制 + +书写只能用指针,键盘无法替代手写。 diff --git a/docs/components/494-banner.md b/docs/components/494-banner.md new file mode 100644 index 0000000..688a2ad --- /dev/null +++ b/docs/components/494-banner.md @@ -0,0 +1,48 @@ +# 494 Banner 页面横幅 + +## 用途 + +页面顶部的通栏提示:信息、成功、警告、错误;可带操作和关闭按钮。 + +## 最小示例 + +```go +open := true +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.Banner(c, &open, "Maintenance tonight at 22:00.", mujica.BannerOptions{ + Severity: mujica.SeverityWarning, + Action: "Details", + }) + if r.Action { + // show the details + } +} +``` + +签名:`func Banner(c *ui.Context, open *bool, message string, opts BannerOptions) AlertResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `open` | `*bool` | `nil` | 非 nil 时显示关闭按钮;为 false 时不显示。 | +| `message` | `string` | 必填 | 文字。 | +| `Severity` | `Severity` | `SeverityInfo` | 严重程度;未知值 panic。 | +| `Action` | `string` | `""` | 操作按钮文字。 | + +## 状态 + +图标、底边色条与浅色底同时表达严重程度;警告与错误出现或变化时播报。 + +## 事件 + +`AlertResult.Action`、`AlertResult.Closed`。 + +## 键盘操作 + +Tab 到操作与关闭按钮;Enter/Space 触发。 + +## 限制 + +无。 diff --git a/docs/components/495-undo-toast.md b/docs/components/495-undo-toast.md new file mode 100644 index 0000000..fe9fe22 --- /dev/null +++ b/docs/components/495-undo-toast.md @@ -0,0 +1,47 @@ +# 495 UndoToast 撤销提示 + +## 用途 + +在窗口底部显示可撤销操作的消息与倒计时;撤销时调用回调。悬停或焦点在其中时暂停计时,从不抢夺焦点。 + +## 最小示例 + +```go +open := true +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.UndoToast(c, &open, "Note deleted", mujica.UndoToastOptions{ + OnUndo: func() { + // restore the note + }, + }) +} +``` + +签名:`func UndoToast(c *ui.Context, open *bool, message string, opts UndoToastOptions) UndoToastResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `open` | `*bool` | 必填 | 显示与否;结束时置 false。 | +| `message` | `string` | 必填 | 文字,出现时播报。 | +| `OnUndo` | `func()` | 必填 | 撤销回调。 | +| `Duration` | `time.Duration` | 5 秒 | 倒计时;负数 panic。 | +| `Action` | `string` | “Undo” | 按钮文字。 | + +## 状态 + +按钮显示剩余秒数,下方细条随时间缩短;减少动画时不画细条,只更新秒数。 + +## 事件 + +`Undone`:已撤销(回调已执行);`Expired`:倒计时结束或用户关闭,操作生效。 + +## 键盘操作 + +Tab 到“撤销”与关闭按钮;焦点在其中时 Escape 关闭。 + +## 限制 + +每个窗口同时只显示一条。 diff --git a/docs/components/496-masonry.md b/docs/components/496-masonry.md new file mode 100644 index 0000000..6925378 --- /dev/null +++ b/docs/components/496-masonry.md @@ -0,0 +1,47 @@ +# 496 Masonry 瀑布流 + +## 用途 + +按可用宽度决定列数,把每一项放进当前最短的列;只构建可见范围内的项,以稳定键记住测得的高度。 + +## 最小示例 + +```go +titles := []string{"Sea", "Forest", "Desert", "Glacier"} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Masonry(c, len(titles), mujica.MasonryOptions{ + Key: func(i int) string { return titles[i] }, + }, func(i int) { + ui.Text(c, titles[i]) + }).Size(380, 280) +} +``` + +签名:`func Masonry(c *ui.Context, n int, opts MasonryOptions, item func(i int)) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `n` | `int` | 必填 | 项数;负数 panic。 | +| `Key` | `func(int) string` | 必填 | 稳定且唯一的键;重复 panic。 | +| `ColumnWidth` | `float32` | 220 | 最窄列宽。 | +| `Gap` | `float32` | 3 个间距单位 | 间隔。 | +| `Estimate` | `float32` | 160 | 测量前的估计高度。 | + +## 状态 + +自身是滚动容器,需由调用方给定尺寸。 + +## 事件 + +无。 + +## 键盘操作 + +滚动容器按 MyGo 规则滚动;项内控件按各自规则聚焦。 + +## 限制 + +排布只在项数、列数、宽度、测得高度或可见项的键变化时重算。 diff --git a/docs/components/497-resizable-panel-group.md b/docs/components/497-resizable-panel-group.md new file mode 100644 index 0000000..6bc2bfe --- /dev/null +++ b/docs/components/497-resizable-panel-group.md @@ -0,0 +1,48 @@ +# 497 ResizablePanelGroup 可调面板组 + +## 用途 + +多个面板以分隔条相连,拖动或键盘调整尺寸;可折叠面板拖到最小值一半以下即折叠。尺寸以比例保存在调用方的切片中。 + +## 最小示例 + +```go +var sizes []float32 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.ResizablePanelGroup(c, &sizes, []mujica.ResizablePanel{ + {Label: "Files", Collapsible: true, Build: func() { ui.Text(c, "Files") }}, + {Label: "Editor", Build: func() { ui.Text(c, "Editor") }}, + }, mujica.ResizablePanelGroupOptions{}) + if r.Committed { + // save sizes + } +} +``` + +签名:`func ResizablePanelGroup(c *ui.Context, sizes *[]float32, panels []ResizablePanel, opts ResizablePanelGroupOptions) ResizablePanelGroupResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `sizes` | `*[]float32` | 空则平分 | 各面板比例,和为 1,否则 panic;0 只允许可折叠面板。 | +| `panels` | `[]ResizablePanel` | 必填 | Label、Min(默认 10 个间距单位)、Collapsible、Build;至少两个。 | +| `Vertical` | `bool` | `false` | 上下排列。 | +| `Disabled` | `bool` | `false` | 不可调整。 | + +## 状态 + +分隔条悬停或拖动时变为强调色;折叠后分隔条报告“已折叠”。 + +## 事件 + +`Resized`:拖动中尺寸变化;`Committed`:一次调整完成(松开、按键、折叠),用于保存。 + +## 键盘操作 + +Tab 聚焦分隔条;方向键每次 4 个间距单位;Home/End 到最小与最大;Enter 折叠或恢复相邻的可折叠面板。 + +## 限制 + +无。 diff --git a/docs/components/498-dock-panel.md b/docs/components/498-dock-panel.md new file mode 100644 index 0000000..3f37046 --- /dev/null +++ b/docs/components/498-dock-panel.md @@ -0,0 +1,47 @@ +# 498 DockPanel 停靠面板 + +## 用途 + +左、中、右、下四个停靠区,每区以标签页显示面板;把标签拖到区域或窗口边缘即停靠到那里。布局由调用方保存。 + +## 最小示例 + +```go +var layout mujica.DockLayout +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.DockPanel(c, &layout, []mujica.DockItem{ + {ID: "files", Title: "Files", Build: func() { ui.Text(c, "Files") }}, + {ID: "terminal", Title: "Terminal", Build: func() { ui.Text(c, "$") }}, + }, mujica.DockPanelOptions{}) + if r.Changed() { + // save layout + } +} +``` + +签名:`func DockPanel(c *ui.Context, layout *DockLayout, items []DockItem, opts DockPanelOptions) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `layout` | `*DockLayout` | 全部在中央 | 各区面板 ID 与当前标签;未知或重复 ID panic。 | +| `items` | `[]DockItem` | 必填 | ID、Title、Build;ID 重复 panic。 | +| `Disabled` | `bool` | `false` | 不可切换与停靠。 | + +## 状态 + +拖动时四边出现停靠条,悬停的目标显示选中底色与强调边框;当前标签有下划线与选中底色。 + +## 事件 + +`Changed()`:布局变化(停靠或切换标签),用于保存。 + +## 键盘操作 + +每区标签是一个 Tab 停靠点,左右键切换;在标签上按 Shift+Alt+←/→/↓ 停靠到左、右、下,Shift+Alt+↑ 回到中央,焦点跟随标签。 + +## 限制 + +区域尺寸固定(侧栏 220 DIP,底部 160 DIP);需要调整时与 ResizablePanelGroup 组合。 diff --git a/docs/components/499-qr-code.md b/docs/components/499-qr-code.md new file mode 100644 index 0000000..4ec22d3 --- /dev/null +++ b/docs/components/499-qr-code.md @@ -0,0 +1,41 @@ +# 499 QRCode 二维码 + +## 用途 + +在本地用 Go 编码文字为二维码(字节模式,版本 1–40,纠错 L/M/Q/H),不访问网络。 + +## 最小示例 + +```go +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.QRCode(c, "https://example.com/mujica", mujica.QRCodeOptions{Level: mujica.QRQuartile}) +} +``` + +签名:`func QRCode(c *ui.Context, text string, opts QRCodeOptions) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `text` | `string` | 必填 | 按 UTF-8 字节编码。 | +| `Level` | `QRLevel` | `QRMedium` | `QRLow`、`QRMedium`、`QRQuartile`、`QRHigh`;未知值 panic。 | +| `Size` | `float32` | 160 | 含静区的边长;负数或非有限 panic。 | +| `Label` | `string` | “QR code” | 无障碍名称。 | + +## 状态 + +始终深色模块印在浅色底上,深色主题也一样,以便扫码;模块对齐设备像素。文字过长时显示错误。 + +## 事件 + +无。另有 `EncodeQR(data, level) (QRMatrix, error)` 供调用方导出。 + +## 键盘操作 + +不可聚焦。 + +## 限制 + +只用字节模式;只在文字或纠错级别变化时重新编码。 diff --git a/docs/components/500-activity-feed.md b/docs/components/500-activity-feed.md new file mode 100644 index 0000000..7af1758 --- /dev/null +++ b/docs/components/500-activity-feed.md @@ -0,0 +1,43 @@ +# 500 ActivityFeed 活动流 + +## 用途 + +按日分组显示“参与者 动作 对象 时间”,今天与昨天用文字,其余显示日期;只构建可见行。 + +## 最小示例 + +```go +events := []mujica.Activity{ + {ID: "1", Actor: "Lin", Action: "commented on", Object: "Quarterly report", Time: time.Now()}, +} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ActivityFeed(c, events, mujica.ActivityFeedOptions{}).Size(380, 280) +} +``` + +签名:`func ActivityFeed(c *ui.Context, items []Activity, opts ActivityFeedOptions) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `items` | `[]Activity` | 必填 | ID、Actor、Action、Object、Time;缺 ID、Actor、Action 或 Time panic。 | +| `Location` | `*time.Location` | `time.Local` | 按哪个时区划分日期。 | +| `Today` | `Date` | 时钟 | 固定“今天”。 | + +## 状态 + +日期标题使用衬线字与旧金细线;无数据时显示空状态。 + +## 事件 + +无。 + +## 键盘操作 + +滚动容器按 MyGo 规则滚动。 + +## 限制 + +分组只在条数、首尾 ID 或时区变化时重算;中间条目原地修改需换新切片。 diff --git a/examples/gallery/pages_essentials.go b/examples/gallery/pages_essentials.go index 3dcc596..6562f5f 100644 --- a/examples/gallery/pages_essentials.go +++ b/examples/gallery/pages_essentials.go @@ -1,3 +1,262 @@ package main -var essentialsPages = []page{} +var essentialsPages = []page{ + { + ID: "481", Name: "SplitButton", ZhName: "拆分按钮 主操作 下拉", Category: "Essentials", + Demo: splitButtonDemo, + Code: `view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.SplitButton(c, "Save", mujica.SplitButtonOptions{}, func(m *mujica.PopupMenu) { + if m.Item("Save as…", mujica.PopupMenuItemOptions{}) { + // ask for a file name + } + }) + if r.Changed() { + // save + } +}`, + Keys: "Tab reaches the main half, then the menu half. Enter or Space runs the action; on the menu half, Enter, Space or Down opens the menu.", + }, + { + ID: "482", Name: "CopyButton", ZhName: "复制按钮 剪贴板", Category: "Essentials", + Demo: copyButtonDemo, + Code: `view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.CopyButton(c, "https://example.com/invite/42", mujica.CopyButtonOptions{Label: "Copy link"}) +}`, + Keys: "Tab focuses; Enter or Space copies. The copied state is announced.", + }, + { + ID: "483", Name: "HoldToConfirm", ZhName: "按住确认 危险操作", Category: "Essentials", + Demo: holdToConfirmDemo, + Code: `view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.HoldToConfirm(c, "Delete project", mujica.HoldToConfirmOptions{}).Changed() { + // delete the project + } +}`, + Keys: "Focus it and hold Space for the full duration; a click, Enter or a short Space does nothing.", + }, + { + ID: "484", Name: "BulkActionBar", ZhName: "批量操作条 多选", Category: "Essentials", + Demo: bulkActionBarDemo, + Code: `selected := 3 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.BulkActionBar(c, selected, mujica.BulkActionBarOptions{}, func() { + mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}) + }) + if r.Changed() { + selected = 0 + } +}`, + Keys: "Tab moves through the bar's actions; Escape inside it clears the selection.", + }, + { + ID: "485", Name: "ScrubInput", ZhName: "拖动数值 拖动标签", Category: "Essentials", + Demo: scrubInputDemo, + Code: `opacity := 0.6 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ScrubInput(c, "Opacity", &opacity, mujica.ScrubInputOptions{Max: 1, Step: 0.1}) +}`, + Keys: "The label is pointer-only; in the field, Up and Down step and typing sets the value.", + }, + { + ID: "486", Name: "CurrencyInput", ZhName: "货币输入 金额 千分位", Category: "Essentials", + Demo: currencyInputDemo, + Code: `cents := int64(123450) +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.CurrencyInput(c, ¢s, mujica.CurrencyInputOptions{Symbol: "$", Label: "Price"}) +}`, + Keys: "Tab focuses the field, which shows plain digits while focused and groups thousands when left.", + }, + { + ID: "487", Name: "MaskedInput", ZhName: "掩码输入 电话 卡号", Category: "Essentials", + Demo: maskedInputDemo, + Code: `phone := "" +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.MaskedInput(c, &phone, mujica.MaskedInputOptions{Mask: "(999) 999-9999", Label: "Phone"}) +}`, + Keys: "Arrows, Home and End move the caret; Backspace and Delete remove; paste keeps the characters that fit; Enter submits.", + }, + { + ID: "488", Name: "MentionInput", ZhName: "提及输入 @ 候选", Category: "Essentials", + Demo: mentionInputDemo, + Code: `text, mentions := "", []mujica.Mention(nil) +people := []mujica.MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Bob"}} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.MentionInput(c, &text, &mentions, mujica.MentionInputOptions{ + Label: "Comment", + Candidates: func(q string) []mujica.MentionCandidate { + var out []mujica.MentionCandidate + for _, p := range people { + if strings.HasPrefix(strings.ToLower(p.Name), strings.ToLower(q)) { + out = append(out, p) + } + } + return out + }, + }) +}`, + Keys: "After @, Up and Down pick a candidate, Enter or Tab inserts it, Escape closes the list. Arrows step over a mention; Backspace removes it whole.", + }, + { + ID: "489", Name: "ChoiceChips", ZhName: "筛选芯片 单选 多选", Category: "Essentials", + Demo: choiceChipsDemo, + Code: `var status []string +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ChoiceChips(c, &status, []mujica.ToggleItem[string]{ + {Value: "open", Label: "Open"}, {Value: "closed", Label: "Closed"}, + }, mujica.ChoiceChipsOptions{Multiple: true, Label: "Status"}) +}`, + Keys: "The chips are one Tab stop; Left and Right move, skipping disabled chips; Space or Enter toggles.", + }, + { + ID: "490", Name: "Knob", ZhName: "旋钮 调节", Category: "Essentials", + Demo: knobDemo, + Code: `gain := 35.0 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Knob(c, &gain, mujica.KnobOptions{Label: "Gain", Ticks: 11}) +}`, + Keys: "Arrows step the value; Home and End go to the ends; drag up or down with the pointer.", + }, + { + ID: "491", Name: "VerticalSlider", ZhName: "纵向滑块", Category: "Essentials", + Demo: verticalSliderDemo, + Code: `level := 40.0 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.VerticalSlider(c, &level, mujica.VerticalSliderOptions{Step: 10, Label: "Level"}) +}`, + Keys: "Up and Right raise the value, Down and Left lower it, Home and End go to the ends.", + }, + { + ID: "492", Name: "ColorPalette", ZhName: "预设色板 颜色", Category: "Essentials", + Demo: colorPaletteDemo, + Code: `color := ui.Hex("#7D2034") +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ColorPalette(c, &color, []mujica.ColorSwatch{ + {Color: ui.Hex("#7D2034"), Name: "Wine"}, + {Color: ui.Hex("#AD9164"), Name: "Old gold"}, + {Color: ui.Hex("#355E80"), Name: "Slate blue"}, + }, mujica.ColorPaletteOptions{Custom: true, Label: "Tag color"}) +}`, + Keys: "The palette is one Tab stop; Left and Right move by one swatch, Up and Down by a row; Enter or Space chooses.", + }, + { + ID: "493", Name: "SignaturePad", ZhName: "签名板 手写", Category: "Essentials", + Demo: signaturePadDemo, + Code: `var strokes [][]mujica.SignaturePoint +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.SignaturePad(c, &strokes, mujica.SignaturePadOptions{}).Changed() { + _ = mujica.SignaturePath(strokes) // SVG path data to store + } +}`, + Keys: "Tab focuses the pad; Delete or Backspace clears it. Drawing needs the pointer.", + }, + { + ID: "494", Name: "Banner", ZhName: "横幅 页面提示", Category: "Essentials", + Demo: bannerDemo, + Code: `open := true +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.Banner(c, &open, "Maintenance tonight at 22:00.", mujica.BannerOptions{ + Severity: mujica.SeverityWarning, + Action: "Details", + }) + if r.Action { + // show the details + } +}`, + Keys: "Tab reaches the action and the dismiss button; Enter or Space presses them.", + }, + { + ID: "495", Name: "UndoToast", ZhName: "撤销提示 倒计时", Category: "Essentials", + Demo: undoToastDemo, + Code: `open := true +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.UndoToast(c, &open, "Note deleted", mujica.UndoToastOptions{ + OnUndo: func() { + // restore the note + }, + }) +}`, + Keys: "It never takes the focus. Tab reaches Undo and the close button after the page; Escape inside closes it. Hover or focus pauses the countdown.", + }, + { + ID: "496", Name: "Masonry", ZhName: "瀑布流 卡片", Category: "Essentials", + Demo: masonryDemo, + Code: `titles := []string{"Sea", "Forest", "Desert", "Glacier"} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Masonry(c, len(titles), mujica.MasonryOptions{ + Key: func(i int) string { return titles[i] }, + }, func(i int) { + ui.Text(c, titles[i]) + }).Size(380, 280) +}`, + Keys: "Scroll with the wheel or the scrollbar; only cards in view are built.", + }, + { + ID: "497", Name: "ResizablePanelGroup", ZhName: "可调面板 分隔条", Category: "Essentials", + Demo: resizablePanelGroupDemo, + Code: `var sizes []float32 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.ResizablePanelGroup(c, &sizes, []mujica.ResizablePanel{ + {Label: "Files", Collapsible: true, Build: func() { ui.Text(c, "Files") }}, + {Label: "Editor", Build: func() { ui.Text(c, "Editor") }}, + }, mujica.ResizablePanelGroupOptions{}) + if r.Committed { + // save sizes + } +}`, + Keys: "Tab focuses a divider; arrows move it, Home and End go to the limits, Enter collapses or restores a collapsible neighbor.", + }, + { + ID: "498", Name: "DockPanel", ZhName: "停靠面板 标签", Category: "Essentials", + Demo: dockPanelDemo, + Code: `var layout mujica.DockLayout +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.DockPanel(c, &layout, []mujica.DockItem{ + {ID: "files", Title: "Files", Build: func() { ui.Text(c, "Files") }}, + {ID: "terminal", Title: "Terminal", Build: func() { ui.Text(c, "$") }}, + }, mujica.DockPanelOptions{}) + if r.Changed() { + // save layout + } +}`, + Keys: "Each zone's tabs are one Tab stop; Left and Right switch tabs. Shift+Alt+Left, Right or Down docks the focused tab to that edge; Shift+Alt+Up returns it to the center.", + }, + { + ID: "499", Name: "QRCode", ZhName: "二维码", Category: "Essentials", + Demo: qrCodeDemo, + Code: `view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.QRCode(c, "https://example.com/mujica", mujica.QRCodeOptions{Level: mujica.QRQuartile}) +}`, + Keys: "Not focusable; assistive technology reads its name.", + }, + { + ID: "500", Name: "ActivityFeed", ZhName: "活动流 动态", Category: "Essentials", + Demo: activityFeedDemo, + Code: `events := []mujica.Activity{ + {ID: "1", Actor: "Lin", Action: "commented on", Object: "Quarterly report", Time: time.Now()}, +} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ActivityFeed(c, events, mujica.ActivityFeedOptions{}).Size(380, 280) +}`, + Keys: "Scroll with the wheel or the scrollbar; only rows in view are built.", + }, +} diff --git a/examples/gallery/pages_essentials_demos.go b/examples/gallery/pages_essentials_demos.go new file mode 100644 index 0000000..6e5ab05 --- /dev/null +++ b/examples/gallery/pages_essentials_demos.go @@ -0,0 +1,267 @@ +package main + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +func splitButtonDemo(c *ui.Context, log func(string)) { + type state struct{ variant, disabled bool } + s := ui.Local(c.Root(), "481", func() state { return state{} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + ui.Row(c).Gap(16).Children(func() { + ui.Checkbox(c, &s.variant, "Secondary") + ui.Checkbox(c, &s.disabled, "Disabled") + }) + }) + feedbackSection(c, "Live", func() { + v := mujica.Primary + if s.variant { + v = mujica.Secondary + } + ui.Row(c).Children(func() { + r := mujica.SplitButton(c, "Save", mujica.SplitButtonOptions{Variant: v, Disabled: s.disabled}, func(m *mujica.PopupMenu) { + for _, it := range []string{"Save as…", "Save a copy", "Save all"} { + if m.Item(it, mujica.PopupMenuItemOptions{}) { + log("split button: menu " + it) + } + } + }) + if r.Changed() { + log("split button: main action") + } + }) + }) + }) +} + +func copyButtonDemo(c *ui.Context, log func(string)) { + type state struct { + text string + disabled bool + } + s := ui.Local(c.Root(), "482", func() state { return state{text: "https://example.com/invite/42"} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + ui.TextInput(c, &s.text).Width(320) + ui.Checkbox(c, &s.disabled, "Disabled") + }) + feedbackSection(c, "Live", func() { + ui.Row(c).Gap(12).Children(func() { + if mujica.CopyButton(c, s.text, mujica.CopyButtonOptions{Disabled: s.disabled}).Changed() { + log(fmt.Sprintf("copy button: copied %d characters", len([]rune(s.text)))) + } + if mujica.CopyButton(c, s.text, mujica.CopyButtonOptions{Label: "Copy link", Disabled: s.disabled}).Changed() { + log("copy button: copied with label") + } + }) + }) + }) +} + +func holdToConfirmDemo(c *ui.Context, log func(string)) { + type state struct { + seconds float64 + disabled bool + deleted int + } + s := ui.Local(c.Root(), "483", func() state { return state{seconds: 1.5} }) + 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("Hold %.1f s", s.seconds)) + ui.Slider(c, &s.seconds, 0.5, 4).Width(200) + }) + ui.Checkbox(c, &s.disabled, "Disabled") + }) + feedbackSection(c, "Live (turn on reduced motion to see the countdown text)", func() { + ui.Row(c).Children(func() { + d := time.Duration(s.seconds * float64(time.Second)) + if mujica.HoldToConfirm(c, "Delete project", mujica.HoldToConfirmOptions{Duration: d, Disabled: s.disabled}).Changed() { + s.deleted++ + log("hold to confirm: confirmed") + } + }) + ui.Text(c, fmt.Sprintf("Confirmed %d times", s.deleted)) + }) + }) +} + +func bulkActionBarDemo(c *ui.Context, log func(string)) { + type state struct { + picked [6]bool + disabled bool + } + s := ui.Local(c.Root(), "484", func() state { return state{picked: [6]bool{true, false, true}} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Rows", func() { + for i := range s.picked { + if ui.Checkbox(c, &s.picked[i], fmt.Sprintf("Invoice %d", 1001+i)).Changed() { + log(fmt.Sprintf("bulk bar: row %d %v", i, s.picked[i])) + } + } + ui.Checkbox(c, &s.disabled, "Disable the bar") + }) + n := 0 + for _, p := range s.picked { + if p { + n++ + } + } + r := mujica.BulkActionBar(c, n, mujica.BulkActionBarOptions{Disabled: s.disabled}, func() { + if mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + log(fmt.Sprintf("bulk bar: archive %d", n)) + } + if mujica.Button(c, "Delete", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { + log(fmt.Sprintf("bulk bar: delete %d", n)) + } + }) + if r.Changed() { + s.picked = [6]bool{} + log("bulk bar: cleared") + } + }) +} + +func scrubInputDemo(c *ui.Context, log func(string)) { + type state struct { + opacity, size float64 + disabled bool + } + s := ui.Local(c.Root(), "485", func() state { return state{opacity: 0.6, size: 14} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + ui.Checkbox(c, &s.disabled, "Disabled") + feedbackSection(c, "Drag a label sideways, or type", func() { + if mujica.ScrubInput(c, "Opacity", &s.opacity, mujica.ScrubInputOptions{Max: 1, Step: 0.05, Disabled: s.disabled}).Changed() { + log(fmt.Sprintf("scrub: opacity %.2f", s.opacity)) + } + if mujica.ScrubInput(c, "Size", &s.size, mujica.ScrubInputOptions{Min: 8, Max: 72, Disabled: s.disabled}).Changed() { + log(fmt.Sprintf("scrub: size %.0f", s.size)) + } + }) + }) +} + +func currencyInputDemo(c *ui.Context, log func(string)) { + type state struct { + cents, yen int64 + disabled bool + } + s := ui.Local(c.Root(), "486", func() state { return state{cents: 123450, yen: 98000} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + ui.Checkbox(c, &s.disabled, "Disabled") + feedbackSection(c, "Live", func() { + ui.Row(c).Gap(12).Children(func() { + if mujica.CurrencyInput(c, &s.cents, mujica.CurrencyInputOptions{Symbol: "$", Label: "Price", Disabled: s.disabled}).Changed() { + log(fmt.Sprintf("currency: %d cents", s.cents)) + } + if mujica.CurrencyInput(c, &s.yen, mujica.CurrencyInputOptions{Symbol: "¥", NoFraction: true, Label: "Yen", Disabled: s.disabled}).Changed() { + log(fmt.Sprintf("currency: %d yen", s.yen)) + } + }) + ui.Text(c, "Stored as "+mujica.CurrencyFormat(s.cents, 2, true)+" and "+mujica.CurrencyFormat(s.yen, 0, true)) + }) + }) +} + +func maskedInputDemo(c *ui.Context, log func(string)) { + type state struct { + mask int + value string + disabled bool + } + masks := []string{"(999) 999-9999", "9999 9999 9999 9999", "99/99/9999", "aa-9999"} + s := ui.Local(c.Root(), "487", func() state { return state{} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + if ui.Segmented(c, &s.mask, masks...).Changed() { + s.value = "" + log("masked: mask " + masks[s.mask]) + } + ui.Checkbox(c, &s.disabled, "Disabled") + }) + feedbackSection(c, "Live (try an input method: nothing commits until it does)", func() { + r := mujica.MaskedInput(c, &s.value, mujica.MaskedInputOptions{Mask: masks[s.mask], Label: "Masked field", Disabled: s.disabled}) + r.Element.Width(260) + if r.Changed() { + log(fmt.Sprintf("masked: %d characters filled", len([]rune(s.value)))) + } + if r.Submitted() { + log("masked: submitted") + } + }) + }) +} + +func choiceChipsDemo(c *ui.Context, log func(string)) { + type state struct { + value []string + multiple, disabled bool + } + s := ui.Local(c.Root(), "489", func() state { return state{multiple: true, value: []string{"open"}} }) + items := []mujica.ToggleItem[string]{{Value: "open", Label: "Open"}, {Value: "draft", Label: "Draft"}, {Value: "review", Label: "In review"}, {Value: "closed", Label: "Closed", Disabled: true}, {Value: "merged", Label: "Merged"}} + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + ui.Row(c).Gap(16).Children(func() { + if ui.Checkbox(c, &s.multiple, "Multiple").Changed() && !s.multiple && len(s.value) > 1 { + s.value = s.value[:1] + } + ui.Checkbox(c, &s.disabled, "Disabled") + }) + }) + feedbackSection(c, "Live", func() { + if mujica.ChoiceChips(c, &s.value, items, mujica.ChoiceChipsOptions{Multiple: s.multiple, Disabled: s.disabled, Label: "Status"}).Changed() { + log(fmt.Sprintf("chips: %v", s.value)) + } + }) + }) +} + +func knobDemo(c *ui.Context, log func(string)) { + type state struct { + gain, mix float64 + disabled bool + } + s := ui.Local(c.Root(), "490", func() state { return state{gain: 35, mix: 0.5} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + ui.Checkbox(c, &s.disabled, "Disabled") + ui.Row(c).Gap(32).Children(func() { + if mujica.Knob(c, &s.gain, mujica.KnobOptions{Label: "Gain", Ticks: 11, Disabled: s.disabled}).Changed() { + log(fmt.Sprintf("knob: gain %.1f", s.gain)) + } + if mujica.Knob(c, &s.mix, mujica.KnobOptions{Max: 1, Step: 0.1, Size: 72, Label: "Mix", Disabled: s.disabled}).Changed() { + log(fmt.Sprintf("knob: mix %.1f", s.mix)) + } + }) + }) +} + +func verticalSliderDemo(c *ui.Context, log func(string)) { + type state struct { + levels [4]float64 + showValue, disable bool + } + s := ui.Local(c.Root(), "491", func() state { return state{levels: [4]float64{40, 70, 20, 90}, showValue: true} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + ui.Row(c).Gap(16).Children(func() { + ui.Checkbox(c, &s.showValue, "Show value") + ui.Checkbox(c, &s.disable, "Disabled") + }) + ui.Row(c).Gap(16).Children(func() { + for i := range s.levels { + opts := mujica.VerticalSliderOptions{Step: 10, ShowValue: s.showValue, Disabled: s.disable, Label: fmt.Sprintf("Band %d", i+1)} + if i == 0 { + opts.Marks = []mujica.SliderMark{{Value: 0, Label: "0"}, {Value: 50, Label: "50"}, {Value: 100, Label: "100"}} + } + if mujica.VerticalSlider(c, &s.levels[i], opts).Changed() { + log(fmt.Sprintf("vertical slider: band %d = %.0f", i+1, s.levels[i])) + } + } + }) + }) +} diff --git a/examples/gallery/pages_essentials_more.go b/examples/gallery/pages_essentials_more.go new file mode 100644 index 0000000..0334815 --- /dev/null +++ b/examples/gallery/pages_essentials_more.go @@ -0,0 +1,292 @@ +package main + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +func mentionInputDemo(c *ui.Context, log func(string)) { + type state struct { + text string + mentions []mujica.Mention + disabled bool + } + people := []mujica.MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Albert"}, {ID: "u3", Name: "Bob"}, {ID: "u4", Name: "Sakiko"}, {ID: "u5", Name: "Mutsumi"}} + s := ui.Local(c.Root(), "488", func() state { return state{} }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + ui.Checkbox(c, &s.disabled, "Disabled") + feedbackSection(c, "Type @ to mention someone", func() { + r := mujica.MentionInput(c, &s.text, &s.mentions, mujica.MentionInputOptions{ + Placeholder: "Write a comment…", Label: "Comment", Disabled: s.disabled, + Candidates: func(q string) []mujica.MentionCandidate { + var out []mujica.MentionCandidate + for _, p := range people { + if strings.HasPrefix(strings.ToLower(p.Name), strings.ToLower(q)) { + out = append(out, p) + } + } + return out + }, + }) + r.Element.Width(420) + if r.Changed() { + log(fmt.Sprintf("mention: %d mentions", len(s.mentions))) + } + if r.Submitted() { + log("mention: submitted") + } + for _, m := range s.mentions { + ui.Text(c, fmt.Sprintf("@%s → %s", m.Name, m.ID)).Key(m.ID) + } + }) + }) +} + +func colorPaletteDemo(c *ui.Context, log func(string)) { + type state struct { + color ui.Color + custom, disabled bool + } + s := ui.Local(c.Root(), "492", func() state { return state{color: ui.Hex("#7D2034"), custom: true} }) + swatches := []mujica.ColorSwatch{ + {Color: ui.Hex("#7D2034"), Name: "Wine"}, {Color: ui.Hex("#A12635"), Name: "Crimson"}, {Color: ui.Hex("#AD9164"), Name: "Old gold"}, + {Color: ui.Hex("#805714"), Name: "Bronze"}, {Color: ui.Hex("#326247"), Name: "Ivy"}, {Color: ui.Hex("#355E80"), Name: "Slate blue"}, + {Color: ui.Hex("#4B3A5A"), Name: "Plum"}, {Color: ui.Hex("#211C1D"), Name: "Ink"}, {Color: ui.Hex("#6A5B55"), Name: "Taupe"}, + {Color: ui.Hex("#D2C3AF"), Name: "Parchment"}, {Color: ui.Hex("#F2EBDD"), Name: "Ivory"}, {Color: ui.Hex("#FFFFFF"), Name: "White"}, + } + ui.Column(c).Gap(16).FillWidth().Children(func() { + ui.Row(c).Gap(16).Children(func() { + ui.Checkbox(c, &s.custom, "Custom entry") + ui.Checkbox(c, &s.disabled, "Disabled") + }) + if mujica.ColorPalette(c, &s.color, swatches, mujica.ColorPaletteOptions{Columns: 6, Custom: s.custom, Disabled: s.disabled, Label: "Tag color"}).Changed() { + log(fmt.Sprintf("palette: %v", s.color)) + } + }) +} + +func signaturePadDemo(c *ui.Context, log func(string)) { + type state struct { + strokes [][]mujica.SignaturePoint + disabled bool + } + s := ui.Local(c.Root(), "493", func() state { return state{} }) + ui.Column(c).Gap(16).Width(480).Children(func() { + ui.Checkbox(c, &s.disabled, "Disabled") + if mujica.SignaturePad(c, &s.strokes, mujica.SignaturePadOptions{Disabled: s.disabled}).Changed() { + log(fmt.Sprintf("signature: %d strokes, %d bytes of path", len(s.strokes), len(mujica.SignaturePath(s.strokes)))) + } + }) +} + +func bannerDemo(c *ui.Context, log func(string)) { + type state struct { + sev int + action, closable, op bool + } + s := ui.Local(c.Root(), "494", func() state { return state{sev: 2, action: true, closable: true, op: 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.action, "Action") + ui.Checkbox(c, &s.closable, "Closable") + }) + }) + opts := mujica.BannerOptions{Severity: feedbackSeverities[s.sev]} + if s.action { + opts.Action = "Details" + } + var open *bool + if s.closable { + open = &s.op + } + r := mujica.Banner(c, open, "Scheduled maintenance tonight from 22:00 to 23:00.", opts) + if r.Action { + log("banner: action") + } + if r.Closed { + log("banner: closed") + } + if !s.op && mujica.Button(c, "Show again", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + s.op = true + log("banner: reopened") + } + }) +} + +func undoToastDemo(c *ui.Context, log func(string)) { + type state struct { + notes []string + removed string + open bool + seconds float64 + } + s := ui.Local(c.Root(), "495", func() state { + return state{notes: []string{"Call the florist", "Book the hall", "Order candles"}, seconds: 5} + }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { + ui.Text(c, fmt.Sprintf("Countdown %.0f s", s.seconds)) + ui.Slider(c, &s.seconds, 2, 10).Width(200) + }) + for i, n := range s.notes { + ui.Row(c).Key(n).Gap(12).AlignItems(ui.Center).Children(func() { + ui.Text(c, n).Grow(1) + if mujica.Button(c, "Delete", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + s.removed, s.open = n, true + s.notes = append(s.notes[:i:i], s.notes[i+1:]...) + log("undo toast: deleted " + n) + } + }) + } + }) + if !s.open { + return + } + removed := s.removed + r := mujica.UndoToast(c, &s.open, "Deleted “"+removed+"”", mujica.UndoToastOptions{ + Duration: time.Duration(s.seconds * float64(time.Second)), + OnUndo: func() { s.notes = append(s.notes, removed) }, + }) + if r.Undone { + log("undo toast: undone " + removed) + } + if r.Expired { + log("undo toast: expired") + } +} + +func masonryDemo(c *ui.Context, log func(string)) { + type state struct { + count, width float64 + } + s := ui.Local(c.Root(), "496", func() state { return state{count: 40, width: 180} }) + k := mujica.Tokens(c) + ui.Column(c).Gap(16).FillWidth().Children(func() { + ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { + ui.Text(c, fmt.Sprintf("Items %.0f", s.count)) + if ui.Slider(c, &s.count, 1, 10000).Width(200).Changed() { + log(fmt.Sprintf("masonry: %d items", int(s.count))) + } + ui.Text(c, fmt.Sprintf("Column %.0f", s.width)) + ui.Slider(c, &s.width, 120, 320).Width(160) + }) + mujica.Masonry(c, int(s.count), mujica.MasonryOptions{Key: strconv.Itoa, ColumnWidth: float32(s.width)}, func(i int) { + card := ui.Column(c).Height(float32(60+i*37%90)).Padding(10).Background(k.Surface).Border(1, k.Border).Radius(4).Cursor(ui.CursorPointer) + card.Children(func() { ui.Text(c, "Card "+strconv.Itoa(i+1)) }) + if card.Clicked() { + log("masonry: clicked card " + strconv.Itoa(i+1)) + } + }).FillWidth().Height(420) + }) +} + +func resizablePanelGroupDemo(c *ui.Context, log func(string)) { + type state struct { + sizes []float32 + vertical, disabled bool + } + s := ui.Local(c.Root(), "497", func() state { return state{} }) + k := mujica.Tokens(c) + pane := func(name string) func() { + return func() { + ui.Column(c).Fill().Padding(12).Background(k.Surface).Children(func() { ui.Text(c, name) }) + } + } + ui.Column(c).Gap(16).FillWidth().Children(func() { + ui.Row(c).Gap(16).Children(func() { + if ui.Checkbox(c, &s.vertical, "Vertical").Changed() { + s.sizes = nil + } + ui.Checkbox(c, &s.disabled, "Disabled") + }) + r := mujica.ResizablePanelGroup(c, &s.sizes, []mujica.ResizablePanel{ + {Label: "Files", Collapsible: true, Build: pane("Files")}, + {Label: "Editor", Min: 160, Build: pane("Editor")}, + {Label: "Outline", Collapsible: true, Build: pane("Outline")}, + }, mujica.ResizablePanelGroupOptions{Vertical: s.vertical, Disabled: s.disabled}) + r.Element.FillWidth().Height(320) + if r.Committed { + log(fmt.Sprintf("panels: save %.2f", s.sizes)) + } + }) +} + +func dockPanelDemo(c *ui.Context, log func(string)) { + type state struct { + layout mujica.DockLayout + disabled bool + } + s := ui.Local(c.Root(), "498", func() state { + return state{layout: mujica.DockLayout{Zones: [4][]string{{"editor"}, {"files"}, {"outline"}, {"terminal", "problems"}}}} + }) + var items []mujica.DockItem + for _, id := range []string{"files", "editor", "outline", "terminal", "problems"} { + items = append(items, mujica.DockItem{ID: id, Title: strings.ToUpper(id[:1]) + id[1:], Build: func() { ui.Text(c, "Contents of "+id) }}) + } + ui.Column(c).Gap(16).FillWidth().Children(func() { + ui.Checkbox(c, &s.disabled, "Disabled") + r := mujica.DockPanel(c, &s.layout, items, mujica.DockPanelOptions{Disabled: s.disabled}) + r.Element.FillWidth().Height(440) + if r.Changed() { + log(fmt.Sprintf("dock: save %v", s.layout.Zones)) + } + }) +} + +func qrCodeDemo(c *ui.Context, log func(string)) { + type state struct { + text string + level int + size float64 + } + s := ui.Local(c.Root(), "499", func() state { return state{text: "https://example.com/mujica", level: 1, size: 200} }) + levels := []mujica.QRLevel{mujica.QRLow, mujica.QRMedium, mujica.QRQuartile, mujica.QRHigh} + ui.Column(c).Gap(16).FillWidth().Children(func() { + feedbackSection(c, "Parameters", func() { + if ui.TextInput(c, &s.text).Width(360).Changed() { + log(fmt.Sprintf("qr: %d bytes", len(s.text))) + } + if ui.Segmented(c, &s.level, "L", "M", "Q", "H").Changed() { + log("qr: level " + levels[s.level].String()) + } + ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { + ui.Text(c, fmt.Sprintf("Size %.0f", s.size)) + ui.Slider(c, &s.size, 100, 320).Width(200) + }) + }) + mujica.QRCode(c, s.text, mujica.QRCodeOptions{Level: levels[s.level], Size: float32(s.size)}) + if m, err := mujica.EncodeQR([]byte(s.text), levels[s.level]); err == nil { + ui.Text(c, fmt.Sprintf("Version %d, mask %d, %d × %d modules", m.Version, m.Mask, m.Size(), m.Size())) + } + }) +} + +func activityFeedDemo(c *ui.Context, log func(string)) { + type state struct{ events []mujica.Activity } + s := ui.Local(c.Root(), "500", func() state { + now := time.Now() + var ev []mujica.Activity + actors := []string{"Sakiko", "Mutsumi", "Uika", "Umiri", "Nyamu"} + verbs := []string{"commented on", "edited", "approved", "closed", "opened"} + for i := range 400 { + ev = append(ev, mujica.Activity{ID: strconv.Itoa(i), Actor: actors[i%5], Action: verbs[i*3%5], Object: fmt.Sprintf("Score #%d", 400-i), Time: now.Add(-time.Duration(i) * 97 * time.Minute)}) + } + return state{events: ev} + }) + ui.Column(c).Gap(16).FillWidth().Children(func() { + if mujica.Button(c, "Add event", mujica.ButtonOptions{Variant: mujica.Secondary}).AlignSelf(ui.Start).Clicked() { + e := mujica.Activity{ID: fmt.Sprint("n", len(s.events)), Actor: "You", Action: "added", Object: "a new note", Time: time.Now()} + s.events = append([]mujica.Activity{e}, s.events...) + log("activity: added " + e.ID) + } + mujica.ActivityFeed(c, s.events, mujica.ActivityFeedOptions{}).FillWidth().Height(420) + }) +}