Merge branch 'worktree-agent-a1dd614394ee20ff2'

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 15:52:46 -04:00
commit 4a8a35114d
116 files changed
+6484 -9

No files matched your search

+198 -1
View File
@@ -1,3 +1,200 @@
package main
var messagingPages = []page{}
const messagingCategory = "Messaging & Mail"
var messagingPages = []page{
{ID: "401", Name: "ChannelList", ZhName: "频道列表", Category: messagingCategory, Demo: channelListDemo,
Keys: "Tab 进入;上下方向键切换行;Enter / Space 选择或折叠;分组标题上的左、右方向键折叠、展开。", Code: `groups := []mujica.ChannelGroup{{ID: "ch", Title: "Channels", Channels: []mujica.Channel{
{ID: "general", Name: "general", Unread: 3},
{ID: "rehearsal", Name: "rehearsal", Pinned: true},
}}}
selected := "general"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ChannelList(c, &selected, groups, mujica.ChannelListOptions{}).Changed() {
_ = selected // show that channel
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "402", Name: "ChatMessage", ZhName: "聊天消息", Category: messagingCategory, Demo: chatMessageDemo,
Keys: "可选文字支持选择复制;插槽控件按 Tab 访问。", Code: `prev := mujica.ChatEntry{ID: "1", Author: "Uika", Time: time.Now(), Text: "Rehearsal at nine."}
m := mujica.ChatEntry{ID: "2", Author: "Uika", Time: time.Now(), Text: "Bring the masks.", Edited: true}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ChatMessage(c, prev, mujica.ChatMessageOptions{})
mujica.ChatMessage(c, m, mujica.ChatMessageOptions{
Continued: mujica.ChatContinues(prev, m),
Reactions: func() { mujica.Tag(c, "👍 2", mujica.TagOptions{}) },
})
}
ui.Render(view, 400, 300, 1)`},
{ID: "403", Name: "UnreadDivider", ZhName: "未读分隔线", Category: messagingCategory, Demo: unreadDividerDemo,
Keys: "展示组件保持自然焦点序列。", Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.UnreadDivider(c, 3)
}
ui.Render(view, 400, 300, 1)`},
{ID: "404", Name: "ReadReceipt", ZhName: "已读回执", Category: messagingCategory, Demo: readReceiptDemo,
Keys: "展示组件保持自然焦点序列。", Code: `readers := []mujica.AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ReadReceipt(c, mujica.ReceiptRead, mujica.ReadReceiptOptions{Readers: readers})
}
ui.Render(view, 400, 300, 1)`},
{ID: "405", Name: "MemberList", ZhName: "成员列表", Category: messagingCategory, Demo: memberListDemo,
Keys: "Tab 聚焦列表;上下方向键选择;Home / End 首尾;Enter 或双击打开成员。", Code: `var s mujica.MemberListState
members := []mujica.Member{
{ID: "1", Name: "Sakiko", Presence: mujica.PresenceOnline, Role: "Leader"},
{ID: "2", Name: "Umiri", Presence: mujica.PresenceOffline},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if r := mujica.MemberList(c, &s, members, mujica.MemberListOptions{}); r.Opened != "" {
_ = r.Opened // show that member's profile
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "406", Name: "UserProfileCard", ZhName: "用户资料卡", Category: messagingCategory, Demo: userProfileCardDemo,
Keys: "Tab 访问操作插槽;Enter / Space 激活按钮。", Code: `profile := mujica.UserProfile{Name: "Sakiko Togawa", Presence: mujica.PresenceBusy, Role: "Keyboard", Status: "In rehearsal"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.UserProfileCard(c, profile, mujica.UserProfileCardOptions{Actions: func() {
mujica.Button(c, "Message", mujica.ButtonOptions{})
}})
}
ui.Render(view, 400, 300, 1)`},
{ID: "407", Name: "OnlineStatus", ZhName: "在线状态", Category: messagingCategory, Demo: onlineStatusDemo,
Keys: "展示组件保持自然焦点序列。", Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.OnlineStatus(c, mujica.PresenceAway, mujica.OnlineStatusOptions{})
}
ui.Render(view, 400, 300, 1)`},
{ID: "408", Name: "StatusSetter", ZhName: "状态设置", Category: messagingCategory, Demo: statusSetterDemo,
Keys: "Tab 访问表情、文字、期限和清除按钮;选择器用方向键移动、Enter 确认、Escape 关闭。", Code: `status := mujica.CustomStatus{Emoji: "🎻", Text: "Rehearsing"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.StatusSetter(c, &status, mujica.StatusSetterOptions{}).Changed() {
_ = status // publish it; clear it once status.Until passes
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "409", Name: "CallControls", ZhName: "通话控制", Category: messagingCategory, Demo: callControlsDemo,
Keys: "Tab 进入工具栏;左右方向键移动;Enter / Space 激活。Cmd/Ctrl+D 静音,Cmd/Ctrl+E 摄像头,Cmd/Ctrl+Shift+S 共享,Cmd/Ctrl+Shift+H 挂断。", Code: `st := mujica.CallState{}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.CallControls(c, st, mujica.CallControlsOptions{
OnMute: func(on bool) { st.Muted = on },
OnCamera: func(off bool) { st.CameraOff = off },
OnShare: func(on bool) { st.Sharing = on },
OnHangUp: func() {}, // leave the call
})
}
ui.Render(view, 400, 300, 1)`},
{ID: "410", Name: "VideoCallGrid", ZhName: "视频通话宫格", Category: messagingCategory, Demo: videoCallGridDemo,
Keys: "Tab 进入宫格;方向键移动参与者焦点并读取状态。", Code: `people := []mujica.CallParticipant{
{ID: "1", Name: "Sakiko", Speaking: true}, // Frame: the latest decoded frame
{ID: "2", Name: "Uika", Muted: true},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.VideoCallGrid(c, people, mujica.VideoCallGridOptions{})
}
ui.Render(view, 400, 300, 1)`},
{ID: "411", Name: "IncomingCallDialog", ZhName: "来电对话框", Category: messagingCategory, Demo: incomingCallDialogDemo,
Keys: "默认焦点为拒绝;Tab 循环访问操作;Enter / Space 激活;Escape 拒绝;关闭后恢复焦点。", Code: `ringing := true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.IncomingCallDialog(c, &ringing, mujica.IncomingCall{Caller: "Sakiko", Video: true}, mujica.IncomingCallOptions{
OnAccept: func(video bool) {}, // join, with the camera on when video
OnDecline: func() {},
})
}
ui.Render(view, 400, 300, 1)`},
{ID: "412", Name: "EmojiPicker", ZhName: "表情选择器", Category: messagingCategory, Demo: emojiPickerDemo,
Keys: "Tab 访问搜索、肤色、分类、表情;方向键在组内移动;Enter / Space 选择。", Code: `var recent []string
tone := 0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if r := mujica.EmojiPicker(c, mujica.EmojiPickerOptions{Recent: &recent, Tone: &tone}); r.Picked != "" {
_ = r.Picked // insert it
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "413", Name: "PinnedMessages", ZhName: "置顶消息", Category: messagingCategory, Demo: pinnedMessagesDemo,
Keys: "Tab 访问跳转与取消置顶;方向键移动;Enter / Space 激活。", Code: `pins := []mujica.ChatEntry{{ID: "a", Author: "Sakiko", Text: "Rehearsal moves to nine."}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.PinnedMessages(c, pins, mujica.PinnedMessagesOptions{})
if r.Unpinned != "" {
pins = pins[:0] // unpin it
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "414", Name: "ThreadPanel", ZhName: "话题侧栏", Category: messagingCategory, Demo: threadPanelDemo,
Keys: "Tab 访问关闭、回复和发送;Enter 发送回复;Escape 关闭面板。", Code: `open := true
root := mujica.ChatEntry{ID: "r", Author: "Sakiko", Text: "Set list for Friday?"}
var replies []mujica.ChatEntry
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if r := mujica.ThreadPanel(c, &open, root, replies, mujica.ThreadPanelOptions{}); r.Sent != "" {
replies = append(replies, mujica.ChatEntry{ID: r.Sent, Author: "Me", Text: r.Sent, Own: true})
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "415", Name: "MailList", ZhName: "邮件列表", Category: messagingCategory, Demo: mailListDemo,
Keys: "上下方向键、Home / End 移动选择;Enter 或双击打开;S 请求星标;Cmd/Ctrl+A 全选;Cmd/Ctrl 点击切换单项,Shift 点击选择范围。", Code: `var s mujica.ListState[string]
mails := []mujica.MailItem{{ID: "1", From: "Sakiko", Subject: "Second act", Unread: true, Attachments: 1}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if r := mujica.MailList(c, &s, mails, mujica.MailListOptions{}); r.Starred != "" {
mails[0].Starred = !mails[0].Starred
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "416", Name: "MailReader", ZhName: "邮件阅读器", Category: messagingCategory, Demo: mailReaderDemo,
Keys: "Tab 访问链接、引用开关和附件;Enter / Space 激活;正文支持选择复制。", Code: `m := mujica.Mail{From: "Sakiko", Subject: "Second act", Body: "The score is attached.\n> Can you send it?",
Attachments: []mujica.MailAttachment{{Name: "score.pdf", Size: 2048}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if r := mujica.MailReader(c, m); r.Link != "" {
c.OpenURL(r.Link)
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "417", Name: "MailComposer", ZhName: "邮件编辑器", Category: messagingCategory, Demo: mailComposerDemo,
Keys: "Tab 访问字段与操作;Cmd/Ctrl+S 保存草稿;组件内 Cmd/Ctrl+Enter 发送;正文 Enter 换行。", Code: `draft := mujica.MailDraft{To: []string{"uika@example.com"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.MailComposer(c, &draft, mujica.MailComposerOptions{
OnSaveDraft: func(d mujica.MailDraft) {}, // store d
})
if r.Sent {
draft = mujica.MailDraft{}
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "418", Name: "RecipientInput", ZhName: "收件人输入", Category: messagingCategory, Demo: recipientInputDemo,
Keys: "Enter 或分隔符提交标签;空输入 Backspace 删除末项;上下方向键移动联系人建议,Enter 选择,Escape 收起建议。", Code: `var to []string
contacts := []mujica.Contact{{Name: "Uika Misumi", Email: "uika@example.com"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.RecipientInput(c, &to, mujica.RecipientInputOptions{Contacts: contacts, Label: "To"})
}
ui.Render(view, 400, 300, 1)`},
{ID: "419", Name: "QuotedText", ZhName: "引用文字", Category: messagingCategory, Demo: quotedTextDemo,
Keys: "Tab 聚焦开关;Enter / Space 展开或折叠;展开内容支持选择复制。", Code: `open := false
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.QuotedText(c, "> Can you send the score?", &open)
}
ui.Render(view, 400, 300, 1)`},
{ID: "420", Name: "SnoozePicker", ZhName: "稍后提醒", Category: messagingCategory, Demo: snoozePickerDemo,
Keys: "Tab 进入;上下方向键选择快捷项;Enter / Space 激活;自定义时间沿用 DateTimePicker 的输入、确认和取消操作。", Code: `var until time.Time
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.SnoozePicker(c, &until, mujica.SnoozePickerOptions{}).Changed() {
_ = until // hide the mail until then
}
}
ui.Render(view, 400, 300, 1)`},
}
+75
View File
@@ -0,0 +1,75 @@
package main
import (
"fmt"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func callControlsDemo(c *ui.Context, log func(string)) {
type state struct {
call mujica.CallState
disabled bool
ended bool
}
s := ui.Local(c.Root(), "409", func() state { return state{} })
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Reconnect", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.ended = false
log("call joined")
}
mujica.CallControls(c, s.call, mujica.CallControlsOptions{
Disabled: s.disabled || s.ended,
OnMute: func(on bool) { s.call.Muted = on; log(fmt.Sprintf("mute %v", on)) },
OnCamera: func(off bool) { s.call.CameraOff = off; log(fmt.Sprintf("camera off %v", off)) },
OnShare: func(on bool) { s.call.Sharing = on; log(fmt.Sprintf("sharing %v", on)) },
OnHangUp: func() { s.ended = true; log("call left") },
})
ui.Text(c, fmt.Sprintf("Muted: %v; camera off: %v; sharing: %v; ended: %v", s.call.Muted, s.call.CameraOff, s.call.Sharing, s.ended))
}
func videoCallGridDemo(c *ui.Context, log func(string)) {
type state struct {
speaking, muted, cameraOff, frame bool
columns float64
}
s := ui.Local(c.Root(), "410", func() state { return state{speaking: true, frame: true, columns: 2} })
mediaCheck(c, log, &s.speaking, "Speaking")
mediaCheck(c, log, &s.muted, "Muted")
mediaCheck(c, log, &s.cameraOff, "Camera off")
mediaCheck(c, log, &s.frame, "Caller-provided frame")
if mujica.NumberInput(c, &s.columns, mujica.NumberInputOptions{Min: 0, Max: 3, Step: 1, Label: "Columns (0 = auto)"}).Changed() {
log(fmt.Sprintf("columns %.0f", s.columns))
}
people := []mujica.CallParticipant{
{ID: "1", Name: "Sakiko", Speaking: s.speaking, Muted: s.muted, CameraOff: s.cameraOff},
{ID: "2", Name: "Uika", Muted: true}, {ID: "3", Name: "Mutsumi", CameraOff: true}, {ID: "4", Name: "Umiri"},
}
if s.frame {
people[0].Frame = mediaBitmaps[0]
people[3].Frame = mediaBitmaps[1]
}
mujica.VideoCallGrid(c, people, mujica.VideoCallGridOptions{Columns: int(s.columns)})
}
func incomingCallDialogDemo(c *ui.Context, log func(string)) {
type state struct {
open, video, focusAccept, disabled bool
result string
}
s := ui.Local(c.Root(), "411", func() state { return state{video: true} })
mediaCheck(c, log, &s.video, "Video call")
mediaCheck(c, log, &s.focusAccept, "Focus Accept")
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Incoming call", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() {
s.open = true
log("incoming call")
}
mujica.IncomingCallDialog(c, &s.open, mujica.IncomingCall{Caller: "Sakiko Togawa", Video: s.video}, mujica.IncomingCallOptions{
Disabled: s.disabled, FocusAccept: s.focusAccept,
OnAccept: func(video bool) { s.result = fmt.Sprintf("Accepted, video: %v", video); log(s.result) },
OnDecline: func() { s.result = "Declined"; log(s.result) },
})
ui.Text(c, s.result)
}
+141
View File
@@ -0,0 +1,141 @@
package main
import (
"fmt"
"slices"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func chatMessageDemo(c *ui.Context, log func(string)) {
type state struct {
continued, edited, own, reply, reactions bool
reacted bool
}
s := ui.Local(c.Root(), "402", func() state { return state{continued: true, edited: true, reply: true, reactions: true} })
mediaCheck(c, log, &s.continued, "Merge consecutive messages")
mediaCheck(c, log, &s.edited, "Edited")
mediaCheck(c, log, &s.own, "Own message")
mediaCheck(c, log, &s.reply, "Reply slot")
mediaCheck(c, log, &s.reactions, "Reaction slot")
at := time.Date(2026, 10, 5, 21, 7, 0, 0, time.Local)
mujica.ChatMessage(c, mujica.ChatEntry{Author: "Uika", Time: at, Text: "Rehearsal at nine.", Own: s.own}, mujica.ChatMessageOptions{})
opts := mujica.ChatMessageOptions{Continued: s.continued}
if s.reply {
opts.Reply = func() {
if mujica.QuoteReply(c, "Sakiko", "When do we start?").Clicked() {
log("jump to quoted message")
}
}
}
if s.reactions {
opts.Reactions = func() {
if mujica.Toggle(c, &s.reacted, "Like", mujica.ToggleOptions{}).Changed() {
log(fmt.Sprintf("reaction %v", s.reacted))
}
}
}
mujica.ChatMessage(c, mujica.ChatEntry{Author: "Uika", Time: at.Add(time.Minute), Text: "Bring the masks.", Own: s.own, Edited: s.edited}, opts)
}
func unreadDividerDemo(c *ui.Context, log func(string)) {
count := ui.Local(c.Root(), "403", func() float64 { return 3 })
if mujica.NumberInput(c, count, mujica.NumberInputOptions{Min: 0, Max: 999, Step: 1, Label: "Unread count"}).Changed() {
log(fmt.Sprintf("unread %.0f", *count))
}
mujica.UnreadDivider(c, int(*count))
inputSection(c, "Zero count", func() { mujica.UnreadDivider(c, 0) })
}
func readReceiptDemo(c *ui.Context, log func(string)) {
type state struct {
receipt mujica.ReceiptState
readers bool
max float64
}
s := ui.Local(c.Root(), "404", func() state { return state{receipt: mujica.ReceiptRead, readers: true, max: 3} })
items := []mujica.SelectOption[mujica.ReceiptState]{{Value: mujica.ReceiptSent, Label: "Sent"}, {Value: mujica.ReceiptDelivered, Label: "Delivered"}, {Value: mujica.ReceiptRead, Label: "Read"}}
if mujica.Select(c, &s.receipt, items, mujica.SelectOptions{Label: "Receipt"}).Changed() {
log(fmt.Sprintf("receipt %d", s.receipt))
}
mediaCheck(c, log, &s.readers, "Reader avatars")
if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 1, Max: 4, Step: 1, Label: "Max avatars"}).Changed() {
log(fmt.Sprintf("max %.0f", s.max))
}
opts := mujica.ReadReceiptOptions{Max: int(s.max)}
if s.readers {
opts.Readers = []mujica.AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}, {Name: "Umiri"}, {Name: "Nyamu"}}
}
mujica.ReadReceipt(c, s.receipt, opts)
}
func emojiPickerDemo(c *ui.Context, log func(string)) {
type state struct {
recent []string
tone int
picked string
disabled bool
}
s := ui.Local(c.Root(), "412", func() state { return state{} })
mediaCheck(c, log, &s.disabled, "Disabled")
if r := mujica.EmojiPicker(c, mujica.EmojiPickerOptions{Recent: &s.recent, Tone: &s.tone, Disabled: s.disabled}); r.Picked != "" {
s.picked = r.Picked
log("emoji " + r.Picked)
}
ui.Text(c, "Picked: "+s.picked)
}
func pinnedMessagesDemo(c *ui.Context, log func(string)) {
type state struct {
pins []mujica.ChatEntry
disabled bool
jump string
}
newPins := func() []mujica.ChatEntry {
return []mujica.ChatEntry{{ID: "a", Author: "Sakiko", Text: "Rehearsal moves to nine."}, {ID: "b", Author: "Uika", Text: "Masks are in the hall."}}
}
s := ui.Local(c.Root(), "413", func() state { return state{pins: newPins()} })
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Restore pins", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.pins = newPins()
log("pins restored")
}
r := mujica.PinnedMessages(c, s.pins, mujica.PinnedMessagesOptions{Disabled: s.disabled, Height: 180})
if r.Jump != "" {
s.jump = r.Jump
log("jump " + r.Jump)
}
if r.Unpinned != "" {
s.pins = slices.DeleteFunc(slices.Clone(s.pins), func(m mujica.ChatEntry) bool { return m.ID == r.Unpinned })
log("unpin " + r.Unpinned)
}
ui.Text(c, "Jump target: "+s.jump)
}
func threadPanelDemo(c *ui.Context, log func(string)) {
type state struct {
open, disabled bool
replies []mujica.ChatEntry
}
s := ui.Local(c.Root(), "414", func() state {
return state{open: true, replies: []mujica.ChatEntry{{ID: "1", Author: "Uika", Text: "Overture first."}}}
})
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Open thread", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() {
s.open = true
log("thread opened")
}
before := s.open
ui.Row(c).Height(420).Children(func() {
r := mujica.ThreadPanel(c, &s.open, mujica.ChatEntry{ID: "root", Author: "Sakiko", Text: "Set list for Friday?"}, s.replies, mujica.ThreadPanelOptions{Disabled: s.disabled})
if r.Sent != "" {
s.replies = append(s.replies, mujica.ChatEntry{ID: fmt.Sprint(len(s.replies) + 1), Author: "Me", Text: r.Sent, Own: true})
log("reply sent")
}
})
if before && !s.open {
log("thread closed")
}
}
+179
View File
@@ -0,0 +1,179 @@
package main
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func mailListDemo(c *ui.Context, log func(string)) {
type state struct {
list mujica.ListState[string]
mails []mujica.MailItem
disabled, empty bool
opened string
}
s := ui.Local(c.Root(), "415", func() state {
return state{mails: []mujica.MailItem{
{ID: "1", From: "Sakiko", Subject: "Second act", Snippet: "The score is attached.", Unread: true, Attachments: 2},
{ID: "2", From: "Uika", Subject: "Costumes", Snippet: "Fittings on Thursday.", Starred: true},
{ID: "3", From: "Umiri", Subject: "Venue", Snippet: "The hall is booked."},
}}
})
mediaCheck(c, log, &s.disabled, "Disabled")
mediaCheck(c, log, &s.empty, "Empty")
if mujica.Button(c, "Load 100,000 mails", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.mails = make([]mujica.MailItem, 100000)
for i := range s.mails {
s.mails[i] = mujica.MailItem{ID: fmt.Sprint(i), From: "Sender", Subject: fmt.Sprintf("Mail %d", i), Unread: i%2 == 0}
}
log("loaded 100,000 mails")
}
mails := s.mails
if s.empty {
mails = nil
}
r := mujica.MailList(c, &s.list, mails, mujica.MailListOptions{Disabled: s.disabled, Height: 240})
if r.Changed() {
log(fmt.Sprintf("selected %d mails", s.list.Selection.Len()))
}
if r.Opened != "" {
s.opened = r.Opened
for i := range s.mails {
if s.mails[i].ID == r.Opened {
s.mails[i].Unread = false
break
}
}
log("open " + r.Opened)
}
if r.Starred != "" {
for i := range s.mails {
if s.mails[i].ID == r.Starred {
s.mails[i].Starred = !s.mails[i].Starred
break
}
}
log("star " + r.Starred)
}
ui.Text(c, "Opened: "+s.opened)
}
func mailReaderDemo(c *ui.Context, log func(string)) {
type state struct{ markdown, attachments, disabled bool }
s := ui.Local(c.Root(), "416", func() state { return state{attachments: true} })
mediaCheck(c, log, &s.markdown, "Safe Markdown")
mediaCheck(c, log, &s.attachments, "Attachments")
mediaCheck(c, log, &s.disabled, "Disabled actions")
m := mujica.Mail{From: "Sakiko Togawa", To: []string{"Uika Misumi"}, Cc: []string{"Umiri Yahata"}, Subject: "Second act", Date: time.Date(2026, 10, 5, 21, 7, 0, 0, time.Local), Body: "The score is attached.\n> Can you send it tonight?"}
if s.markdown {
m.Format = mujica.MailMarkdown
m.Body = "**Rehearsal** at nine. [Venue](https://example.com)\n\n<script>alert(1)</script>"
}
if s.attachments {
m.Attachments = []mujica.MailAttachment{{Name: "score.pdf", Size: 2048}}
}
ui.Column(c).Disabled(s.disabled).Children(func() {
r := mujica.MailReader(c, m)
if r.Link != "" {
log("link " + r.Link)
}
if r.Opened >= 0 {
log(fmt.Sprintf("open attachment %d", r.Opened))
}
if r.Downloaded >= 0 {
log(fmt.Sprintf("download attachment %d", r.Downloaded))
}
})
}
func mailComposerDemo(c *ui.Context, log func(string)) {
type state struct {
draft mujica.MailDraft
disabled bool
saved string
}
s := ui.Local(c.Root(), "417", func() state { return state{draft: mujica.MailDraft{To: []string{"uika@example.com"}}} })
mediaCheck(c, log, &s.disabled, "Disabled")
r := mujica.MailComposer(c, &s.draft, mujica.MailComposerOptions{
Disabled: s.disabled, Contacts: messagingContacts,
OnSaveDraft: func(d mujica.MailDraft) { s.saved = d.Subject; log("draft saved") },
OnAttach: func() {
s.draft.Attachments = append(s.draft.Attachments, mujica.MailAttachment{Name: fmt.Sprintf("score-%d.pdf", len(s.draft.Attachments)+1), Size: 2048})
log("attachment added")
},
})
if r.Sent {
log("mail sent")
s.draft = mujica.MailDraft{}
}
if r.Discarded {
log("draft discarded")
s.draft = mujica.MailDraft{}
}
ui.Text(c, "Saved subject: "+s.saved)
}
var messagingContacts = []mujica.Contact{{Name: "Uika Misumi", Email: "uika@example.com"}, {Name: "Umiri Yahata", Email: "umiri@example.com"}, {Name: "Sakiko Togawa", Email: "sakiko@example.com"}}
func recipientInputDemo(c *ui.Context, log func(string)) {
type state struct {
to []string
disabled, contacts bool
}
s := ui.Local(c.Root(), "418", func() state { return state{contacts: true} })
mediaCheck(c, log, &s.disabled, "Disabled")
mediaCheck(c, log, &s.contacts, "Contact suggestions")
contacts := messagingContacts
if !s.contacts {
contacts = nil
}
if mujica.RecipientInput(c, &s.to, mujica.RecipientInputOptions{Contacts: contacts, Label: "Recipients", Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("recipients %d", len(s.to)))
}
if mujica.Button(c, "Clear recipients", mujica.ButtonOptions{Variant: mujica.Secondary, Disabled: s.disabled}).Clicked() {
s.to = nil
log("recipients cleared")
}
ui.Text(c, "Type a name or email; Enter or comma commits valid addresses.")
}
func quotedTextDemo(c *ui.Context, log func(string)) {
type state struct {
open, disabled bool
text string
}
s := ui.Local(c.Root(), "419", func() state { return state{text: "> Can you send the score?\n> Thanks."} })
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.TextArea(c, &s.text, mujica.TextAreaOptions{Label: "Quoted source", MinHeight: 80}).Changed() {
log("quote source edited")
}
ui.Column(c).Disabled(s.disabled).Children(func() {
if mujica.QuotedText(c, s.text, &s.open).Changed() {
log(fmt.Sprintf("quote expanded %v", s.open))
}
})
}
func snoozePickerDemo(c *ui.Context, log func(string)) {
type state struct {
until time.Time
disabled bool
morning float64
}
s := ui.Local(c.Root(), "420", func() state { return state{morning: 9} })
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.NumberInput(c, &s.morning, mujica.NumberInputOptions{Min: 1, Max: 23, Step: 1, Label: "Morning hour"}).Changed() {
log(fmt.Sprintf("morning %.0f", s.morning))
}
if mujica.SnoozePicker(c, &s.until, mujica.SnoozePickerOptions{Disabled: s.disabled, MorningHour: int(s.morning)}).Changed() {
log("snooze " + s.until.Format(time.RFC3339))
}
if mujica.Button(c, "Clear snooze", mujica.ButtonOptions{Variant: mujica.Secondary, Disabled: s.disabled}).Clicked() {
s.until = time.Time{}
log("snooze cleared")
}
ui.Text(c, fmt.Sprint("Until: ", s.until))
}
+136
View File
@@ -0,0 +1,136 @@
package main
import (
"fmt"
"slices"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func channelListDemo(c *ui.Context, log func(string)) {
type state struct {
selected string
disabled bool
revision int
groups []mujica.ChannelGroup
}
s := ui.Local(c.Root(), "401", func() state {
return state{groups: []mujica.ChannelGroup{
{ID: "channels", Title: "Channels", Channels: []mujica.Channel{
{ID: "general", Name: "general", Unread: 3}, {ID: "rehearsal", Name: "rehearsal", Pinned: true},
{ID: "random", Name: "random", Unread: 120, Muted: true},
}},
{ID: "direct", Title: "Direct messages", Channels: []mujica.Channel{{ID: "uika", Name: "Uika", Direct: true, Presence: mujica.PresenceOnline}}},
}}
})
mediaCheck(c, log, &s.disabled, "Disabled")
before := s.groups[0].Channels[0]
mediaCheck(c, log, &s.groups[0].Channels[0].Muted, "Mute general")
mediaCheck(c, log, &s.groups[0].Channels[0].Pinned, "Pin general")
if before != s.groups[0].Channels[0] {
s.revision++
}
if mujica.ChannelList(c, &s.selected, s.groups, mujica.ChannelListOptions{Disabled: s.disabled, Revision: s.revision}).Changed() {
log("channel " + s.selected)
}
ui.Text(c, "Selected: "+s.selected)
}
func memberListDemo(c *ui.Context, log func(string)) {
type state struct {
list mujica.MemberListState
disabled, empty bool
members []mujica.Member
}
s := ui.Local(c.Root(), "405", func() state {
return state{members: []mujica.Member{
{ID: "1", Name: "Sakiko", Presence: mujica.PresenceOnline, Role: "Leader"},
{ID: "2", Name: "Uika", Presence: mujica.PresenceBusy, Role: "Vocal"},
{ID: "3", Name: "Mutsumi", Presence: mujica.PresenceAway}, {ID: "4", Name: "Umiri", Presence: mujica.PresenceOffline},
}}
})
mediaCheck(c, log, &s.disabled, "Disabled")
mediaCheck(c, log, &s.empty, "Empty")
if mujica.Button(c, "Toggle Uika presence", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.members = slices.Clone(s.members)
if s.members[1].Presence == mujica.PresenceOffline {
s.members[1].Presence = mujica.PresenceOnline
} else {
s.members[1].Presence = mujica.PresenceOffline
}
log("Uika presence changed")
}
members := s.members
if s.empty {
members = nil
}
r := mujica.MemberList(c, &s.list, members, mujica.MemberListOptions{Disabled: s.disabled, Height: 240})
if r.Changed() {
log("member " + s.list.Selected)
}
if r.Opened != "" {
log("profile " + r.Opened)
}
}
func userProfileCardDemo(c *ui.Context, log func(string)) {
type state struct {
presence mujica.Presence
actions, disabled bool
}
s := ui.Local(c.Root(), "406", func() state { return state{actions: true, presence: mujica.PresenceOnline} })
mediaCheck(c, log, &s.actions, "Actions")
mediaCheck(c, log, &s.disabled, "Disable actions")
messagingPresence(c, log, &s.presence)
opts := mujica.UserProfileCardOptions{}
if s.actions {
opts.Actions = func() {
for _, action := range []string{"Message", "Call"} {
if mujica.Button(c, action, mujica.ButtonOptions{Disabled: s.disabled}).Clicked() {
log(action)
}
}
}
}
mujica.UserProfileCard(c, mujica.UserProfile{Name: "Sakiko Togawa", Presence: s.presence, Role: "Keyboard", Status: "In rehearsal", About: "Founder of the masquerade."}, opts)
}
func onlineStatusDemo(c *ui.Context, log func(string)) {
type state struct {
presence mujica.Presence
hidden bool
}
s := ui.Local(c.Root(), "407", func() state { return state{presence: mujica.PresenceOnline} })
mediaCheck(c, log, &s.hidden, "Hide text")
messagingPresence(c, log, &s.presence)
mujica.OnlineStatus(c, s.presence, mujica.OnlineStatusOptions{HideText: s.hidden})
inputSection(c, "States", func() {
for p := mujica.PresenceOffline; p <= mujica.PresenceBusy; p++ {
mujica.OnlineStatus(c, p, mujica.OnlineStatusOptions{})
}
})
}
func statusSetterDemo(c *ui.Context, log func(string)) {
type state struct {
status mujica.CustomStatus
disabled bool
}
s := ui.Local(c.Root(), "408", func() state { return state{} })
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.StatusSetter(c, &s.status, mujica.StatusSetterOptions{Disabled: s.disabled}).Changed() {
log("status changed")
}
ui.Text(c, fmt.Sprintf("Text: %s; expires: %v", s.status.Text, s.status.Until))
}
func messagingPresence(c *ui.Context, log func(string), value *mujica.Presence) {
items := []mujica.SelectOption[mujica.Presence]{
{Value: mujica.PresenceOnline, Label: "Online"}, {Value: mujica.PresenceAway, Label: "Away"},
{Value: mujica.PresenceBusy, Label: "Busy"}, {Value: mujica.PresenceOffline, Label: "Offline"},
}
if mujica.Select(c, value, items, mujica.SelectOptions{Label: "Presence"}).Changed() {
log(fmt.Sprintf("presence %d", *value))
}
}
+122
View File
@@ -0,0 +1,122 @@
package main
import (
"fmt"
"image/png"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func messagingDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, *[]string) {
t.Helper()
i := slices.IndexFunc(messagingPages, func(p page) bool { return p.ID == id })
if i < 0 {
t.Fatalf("missing page %s", id)
}
events := &[]string{}
tt := ui.NewTester(func(c *ui.Context) {
mujica.Use(c, mujica.Settings{Mode: mode})
ui.Scroll(c).Fill().Padding(24).Gap(12).Children(func() {
messagingPages[i].Demo(c, func(s string) { *events = append(*events, s) })
})
}, 900, 800)
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
tt.Frame()
return tt, events
}
func TestMessagingDemosInteractAndLog(t *testing.T) {
cases := []struct{ id, click, event string }{
{"401", "random, 120 unread", "channel random"},
{"402", "Like", "reaction true"},
{"403", "Unread count", "unread 7"},
{"404", "Read", "receipt 0"},
{"405", "Sakiko", "member 1"},
{"406", "Message", "Message"},
{"407", "Presence", "presence 3"},
{"408", "Status text", "status changed"},
{"409", "Mute", "mute true"},
{"410", "Speaking", "Speaking = false"},
{"411", "Incoming call", "Declined"},
{"412", "grinning face", "emoji "},
{"413", "Unpin Uika's message", "unpin b"},
{"414", "Reply in thread", "reply sent"},
{"415", "Star Second act", "star 1"},
{"416", "Download score.pdf", "download attachment 0"},
{"417", "Attach", "attachment added"},
{"418", "Recipients", "recipients 1"},
{"419", "Show quoted text (2 lines)", "quote expanded true"},
{"420", "Tomorrow", "snooze "},
}
if len(messagingPages) != len(cases) {
t.Fatalf("pages=%d, cases=%d", len(messagingPages), len(cases))
}
for _, tc := range cases {
for _, mode := range []mujica.Mode{mujica.Light, mujica.Dark} {
t.Run(tc.id+mode.String(), func(t *testing.T) {
tt, events := messagingDemoTester(t, tc.id, mode)
if err := tt.Click(tc.click); err != nil {
t.Fatal(err)
}
switch tc.id {
case "403":
tt.Command("selectAll")
tt.Type("7")
tt.Key(0, ui.KeyEnter)
case "404":
if err := tt.Click("Sent"); err != nil {
t.Fatal(err)
}
case "407":
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyEnter)
case "408":
tt.Type("Rehearsing")
case "411":
tt.Key(0, ui.KeyEnter)
case "414":
tt.Type("Second act")
tt.Key(0, ui.KeyEnter)
case "418":
tt.Type("guest@example.com")
tt.Key(0, ui.KeyEnter)
}
if !slices.ContainsFunc(*events, func(s string) bool { return strings.HasPrefix(s, tc.event) }) {
t.Fatalf("want %q, events %v", tc.event, *events)
}
})
}
}
}
func TestMessagingDemoCaptures(t *testing.T) {
dir := os.Getenv("MESSAGING_DEMO_DIR")
if dir == "" {
t.Skip("set MESSAGING_DEMO_DIR to export demos")
}
for _, p := range messagingPages {
if p.Category != "Messaging & Mail" || p.Code == "" || p.Keys == "" {
t.Fatalf("incomplete page %s", p.ID)
}
for _, mode := range []mujica.Mode{mujica.Light, mujica.Dark} {
tt, _ := messagingDemoTester(t, p.ID, mode)
f, err := os.Create(filepath.Join(dir, fmt.Sprintf("%s-%s.png", p.ID, mode)))
if err != nil {
t.Fatal(err)
}
if err := png.Encode(f, tt.Image()); err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
}
}
}