Merge branch 'worktree-agent-a9950b6fffa6f4a57'

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 15:52:46 -04:00
commit f467660a2e
96 files changed
+6468 -16

No files matched your search

+21 -1
View File
@@ -1,3 +1,23 @@
package main
var filesPages = []page{}
const filesCategory = "Files & Collaboration"
var filesPages = []page{
{ID: "381", Name: "FileExplorer", ZhName: "文件浏览器", Category: filesCategory, Demo: fileExplorerDemo, Code: filesFileExplorerCode, Keys: "Tab 切换;方向键选择;Enter 打开;macOS Command+[ / ] / ↑,其他平台 Alt+← / → / ↑。"},
{ID: "382", Name: "FileGrid", ZhName: "文件网格", Category: filesCategory, Demo: fileGridDemo, Code: filesFileGridCode, Keys: "方向键选择;Command/Ctrl+点击多选;Shift 扩展;Enter 打开。"},
{ID: "383", Name: "FileIcon", ZhName: "文件图标", Category: filesCategory, Demo: fileIconDemo, Code: filesFileIconCode, Keys: "展示组件,沿用父级焦点。"},
{ID: "384", Name: "FilePreview", ZhName: "文件预览", Category: filesCategory, Demo: filePreviewDemo, Code: filesFilePreviewCode, Keys: "Escape 关闭;Tab 到操作;文本支持选择与复制。"},
{ID: "385", Name: "PathBar", ZhName: "路径栏", Category: filesCategory, Demo: pathBarDemo, Code: filesPathBarCode, Keys: "Tab 进入;左右移动;Enter 激活;Escape 关闭折叠菜单。"},
{ID: "386", Name: "RenameInline", ZhName: "行内重命名", Category: filesCategory, Demo: renameInlineDemo, Code: filesRenameInlineCode, Keys: "Enter 确认;Escape 取消;双击开始编辑;失焦确认有效名称。"},
{ID: "387", Name: "FileOperationProgress", ZhName: "文件操作进度", Category: filesCategory, Demo: fileOperationProgressDemo, Code: filesFileOperationProgressCode, Keys: "Tab 到取消、全选规则和冲突选项;Enter / Space 激活。"},
{ID: "388", Name: "TransferQueue", ZhName: "传输队列", Category: filesCategory, Demo: transferQueueDemo, Code: filesTransferQueueCode, Keys: "Tab 在可见行操作间移动;Enter / Space 激活。"},
{ID: "389", Name: "StorageUsage", ZhName: "存储占用", Category: filesCategory, Demo: storageUsageDemo, Code: filesStorageUsageCode, Keys: "Tab 聚焦分段条;左右循环读取分类与剩余空间。"},
{ID: "390", Name: "RecentFiles", ZhName: "最近文件", Category: filesCategory, Demo: recentFilesDemo, Code: filesRecentFilesCode, Keys: "上下选择文件;Enter 打开;Tab 到清除和移除按钮。"},
{ID: "391", Name: "ArchiveViewer", ZhName: "压缩包浏览", Category: filesCategory, Demo: archiveViewerDemo, Code: filesArchiveViewerCode, Keys: "上下选择;左右展开折叠;Enter 打开;Tab 到列操作。"},
{ID: "392", Name: "PresenceAvatars", ZhName: "协作头像", Category: filesCategory, Demo: presenceAvatarsDemo, Code: filesPresenceAvatarsCode, Keys: "Tab 进入;左右移动;Enter 激活;Escape 关闭溢出名单。"},
{ID: "394", Name: "Reactions", ZhName: "表情回应", Category: filesCategory, Demo: reactionsDemo, Code: filesReactionsCode, Keys: "Tab 到回应与添加;Enter / Space 激活;选择器左右导航;Escape 关闭。"},
{ID: "395", Name: "ShareDialog", ZhName: "分享对话框", Category: filesCategory, Demo: shareDialogDemo, Code: filesShareDialogCode, Keys: "Tab 在对话框内移动;Enter 邀请或激活操作;Escape 关闭并恢复焦点。"},
{ID: "396", Name: "PermissionSelect", ZhName: "权限选择", Category: filesCategory, Demo: permissionSelectDemo, Code: filesPermissionSelectCode, Keys: "Enter 打开;上下移动;Enter 确认;Escape 关闭。"},
{ID: "399", Name: "LiveIndicator", ZhName: "同步状态", Category: filesCategory, Demo: liveIndicatorDemo, Code: filesLiveIndicatorCode, Keys: "Tab 到重试;Enter / Space 激活。"},
{ID: "400", Name: "RemoteCursor", ZhName: "远程光标", Category: filesCategory, Demo: remoteCursorDemo, Code: filesRemoteCursorCode, Keys: "输入穿透至下方编辑器。"},
}
+362
View File
@@ -0,0 +1,362 @@
package main
import (
"fmt"
"path"
"strings"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
var filesFolders = map[string][]string{
"/court": {"chronicles/", "decrees/", "seal.go", "notes.md"},
"/court/chronicles": {"first.md", "second.md"},
"/court/decrees": {"42.pdf", "crest.png"},
}
func filesEntries(dir string) []mujica.FileEntry {
var entries []mujica.FileEntry
for _, n := range filesFolders[dir] {
name := strings.TrimSuffix(n, "/")
entries = append(entries, mujica.FileEntry{Path: path.Join(dir, name), Name: name, Dir: name != n, Size: 2048})
}
return entries
}
func fileExplorerDemo(c *ui.Context, log func(string)) {
type state struct {
explorer mujica.FileExplorerState
disabled, empty bool
}
s := ui.Local(c.Root(), "381", func() state { return state{} })
mediaCheck(c, log, &s.disabled, "Disabled")
mediaCheck(c, log, &s.empty, "Empty folder")
r := mujica.FileExplorer(c, &s.explorer, mujica.FileExplorerOptions{
Roots: []string{"/court"}, Disabled: s.disabled,
Folders: func(dir string) []string {
var dirs []string
for _, e := range filesEntries(dir) {
if e.Dir {
dirs = append(dirs, e.Path)
}
}
return dirs
},
Entries: func(dir string) []mujica.FileEntry {
if s.empty {
return nil
}
return filesEntries(dir)
},
})
r.Element.Height(320).FillWidth()
if r.Navigated {
log("Navigate: " + s.explorer.Current)
}
if r.Changed {
log(fmt.Sprintf("Selected: %v", s.explorer.Selected()))
}
if r.Opened != nil {
log("Open: " + r.Opened.Path)
}
}
func fileGridDemo(c *ui.Context, log func(string)) {
type state struct {
grid mujica.FileGridState
entries []mujica.FileEntry
disabled, thumbs, empty bool
}
s := ui.Local(c.Root(), "382", func() state {
return state{thumbs: true, entries: append(filesEntries("/court"), filesEntries("/court/decrees")...)}
})
mediaCheck(c, log, &s.disabled, "Disabled")
mediaCheck(c, log, &s.thumbs, "Thumbnails")
mediaCheck(c, log, &s.empty, "Empty")
entries := s.entries
if s.empty {
entries = nil
}
r := mujica.FileGrid(c, entries, &s.grid, mujica.FileGridOptions{Disabled: s.disabled, Thumbnail: func(e mujica.FileEntry) bool {
if !s.thumbs || mujica.FileKindOf(e.Name) != mujica.FileImage {
return false
}
mujica.Image(c, mediaBitmaps[0], mujica.ImageOptions{Alt: e.Name}).Element.Fill()
return true
}})
r.Element.Height(320).FillWidth()
if r.Changed {
log(fmt.Sprintf("Selected: %v", s.grid.Selected()))
}
if r.Opened != nil {
log("Open: " + r.Opened.Path)
}
}
func fileIconDemo(c *ui.Context, log func(string)) {
type state struct {
name string
folder, open bool
size float64
}
s := ui.Local(c.Root(), "383", func() state { return state{name: "seal.go", size: 32} })
if mujica.TextInput(c, &s.name, mujica.TextInputOptions{Label: "File name"}).Changed() {
log("File name changed")
}
mediaCheck(c, log, &s.folder, "Folder")
if s.folder {
mediaCheck(c, log, &s.open, "Open folder")
}
if mujica.NumberInput(c, &s.size, mujica.NumberInputOptions{Min: 20, Max: 64, Step: 4, Label: "Icon size"}).Changed() {
log(fmt.Sprintf("Size: %.0f", s.size))
}
mujica.FileIcon(c, s.name, mujica.FileIconOptions{Folder: s.folder, Open: s.folder && s.open, Size: float32(s.size)})
ui.Row(c).Gap(12).Children(func() {
for _, name := range []string{"seal.go", "notes.md", "crest.png", "deed.pdf", "ledger.zip", "hymn.mp3", "rite.mov", "blob"} {
mujica.FileIcon(c, name, mujica.FileIconOptions{Size: 32})
}
})
}
func filePreviewDemo(c *ui.Context, log func(string)) {
type state struct {
kind int
status mujica.DataStatus
closed bool
}
s := ui.Local(c.Root(), "384", func() state { return state{} })
if mujica.Select(c, &s.kind, []mujica.SelectOption[int]{{Value: 0, Label: "Code"}, {Value: 1, Label: "Text"}, {Value: 2, Label: "Image"}, {Value: 3, Label: "Unsupported"}}, mujica.SelectOptions{Label: "Preview type"}).Changed() {
log("Preview type changed")
s.closed = false
}
filesStatus(c, log, &s.status)
if s.closed {
if mujica.Button(c, "Show preview", mujica.ButtonOptions{}).Clicked() {
s.closed = false
log("Preview reopened")
}
return
}
f := []mujica.PreviewFile{{Name: "seal.go", Text: "package court\n\nfunc Seal() {}"}, {Name: "notes.md", Text: "The seal of the court."}, {Name: "crest.png", Image: mediaBitmaps[0]}, {Name: "deed.pdf", Size: 4096}}[s.kind]
r := mujica.FilePreview(c, f, mujica.FilePreviewOptions{Status: s.status, Error: "Reading failed"})
r.Element.Height(280).FillWidth()
if r.Closed {
s.closed = true
log("Preview closed")
}
if r.Opened {
log("Open: " + f.Name)
}
if r.Retried {
s.status = mujica.DataReady
log("Read retried")
}
}
func filesStatus(c *ui.Context, log func(string), value *mujica.DataStatus) {
if mujica.Select(c, value, []mujica.SelectOption[mujica.DataStatus]{{Value: mujica.DataReady, Label: "Ready"}, {Value: mujica.DataLoading, Label: "Loading"}, {Value: mujica.DataFailed, Label: "Failed"}}, mujica.SelectOptions{Label: "Load status"}).Changed() {
log(fmt.Sprintf("Status: %d", *value))
}
}
func pathBarDemo(c *ui.Context, log func(string)) {
type state struct {
path string
disabled bool
}
s := ui.Local(c.Root(), "385", func() state { return state{path: "/court/chronicles/royal/archives/seals"} })
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.TextInput(c, &s.path, mujica.TextInputOptions{Label: "Path"}).Changed() {
log("Path changed")
}
if strings.Trim(s.path, "/") == "" {
ui.Text(c, "Enter a path")
return
}
if r := mujica.PathBar(c, s.path, mujica.PathBarOptions{Disabled: s.disabled}); r.Navigated != "" {
s.path = r.Navigated
log("Navigate: " + r.Navigated)
}
}
func renameInlineDemo(c *ui.Context, log func(string)) {
type state struct {
name string
editing, disabled, folder bool
}
s := ui.Local(c.Root(), "386", func() state { return state{name: "seal.txt"} })
mediaCheck(c, log, &s.disabled, "Disabled")
mediaCheck(c, log, &s.folder, "Folder")
if mujica.Button(c, "Rename", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() {
s.editing = true
log("Rename started")
}
r := mujica.RenameInline(c, &s.name, &s.editing, mujica.RenameInlineOptions{Disabled: s.disabled, Folder: s.folder, Validate: func(name string) string {
if strings.ContainsAny(name, "/\\") {
return "Name contains a path separator"
}
return ""
}})
if r.Renamed {
log("Renamed: " + s.name)
}
if r.Canceled {
log("Rename canceled")
}
}
func fileOperationProgressDemo(c *ui.Context, log func(string)) {
type state struct {
op mujica.FileOperation
disabled, conflict bool
canceled bool
}
s := ui.Local(c.Root(), "387", func() state {
return state{op: mujica.FileOperation{Kind: mujica.FileCopy, Total: 10, Current: "seal.go"}}
})
mediaCheck(c, log, &s.disabled, "Disabled")
mediaCheck(c, log, &s.conflict, "Conflict")
if mujica.Select(c, &s.op.Kind, []mujica.SelectOption[mujica.FileOperationKind]{{Value: mujica.FileCopy, Label: "Copy"}, {Value: mujica.FileMove, Label: "Move"}, {Value: mujica.FileDelete, Label: "Delete"}}, mujica.SelectOptions{Label: "Operation"}).Changed() {
log("Operation changed")
}
if mujica.Button(c, "Advance", mujica.ButtonOptions{Disabled: s.op.Done == s.op.Total}).Clicked() {
s.op.Done++
log(fmt.Sprintf("Done: %d", s.op.Done))
}
if mujica.Button(c, "Reset", mujica.ButtonOptions{}).Clicked() {
s.op.Done = 0
s.canceled = false
log("Operation reset")
}
op := s.op
if s.conflict {
op.Conflict = "seal.go"
}
r := mujica.FileOperationProgress(c, op, mujica.FileOperationOptions{Disabled: s.disabled || s.canceled})
if r.Canceled {
s.canceled = true
log("Operation canceled")
}
if r.Resolved != mujica.ConflictNone {
s.conflict = false
log(fmt.Sprintf("Conflict: %d; apply all: %v", r.Resolved, r.ApplyAll))
}
}
func filesTransfers() []mujica.QueuedTransfer {
return []mujica.QueuedTransfer{
{ID: "a", Name: "ledger.zip", Upload: true, Status: mujica.TransferActive, Done: 30, Total: 120},
{ID: "b", Name: "crest.png", Status: mujica.TransferPaused, Done: 1, Total: 4},
{ID: "c", Name: "deed.pdf", Upload: true, Status: mujica.TransferFailed, Total: 2, Error: "Network lost"},
}
}
func transferQueueDemo(c *ui.Context, log func(string)) {
type state struct {
items []mujica.QueuedTransfer
queue mujica.TransferQueueState
disabled bool
revision uint64
}
s := ui.Local(c.Root(), "388", func() state { return state{items: filesTransfers()} })
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Reset", mujica.ButtonOptions{}).Clicked() {
s.items = filesTransfers()
log("Queue reset")
}
r := mujica.TransferQueue(c, s.items, &s.queue, mujica.TransferQueueOptions{Disabled: s.disabled, Revision: s.revision})
r.Element.Height(260).FillWidth()
if r.Action == mujica.TransferNone {
return
}
for i := range s.items {
if s.items[i].ID == r.ID {
switch r.Action {
case mujica.TransferPause:
s.items[i].Status = mujica.TransferPaused
case mujica.TransferResume, mujica.TransferRetry:
s.items[i].Status = mujica.TransferActive
case mujica.TransferRemove:
s.items = append(s.items[:i:i], s.items[i+1:]...)
}
break
}
}
s.revision++
log(fmt.Sprintf("Transfer %s: %d", r.ID, r.Action))
}
func storageUsageDemo(c *ui.Context, log func(string)) {
type state struct{ photos float64 }
s := ui.Local(c.Root(), "389", func() state { return state{photos: 40} })
if mujica.NumberInput(c, &s.photos, mujica.NumberInputOptions{Min: 0, Max: 92, Step: 1, Label: "Photos (GB)"}).Changed() {
log(fmt.Sprintf("Photos: %.0f GB", s.photos))
}
mujica.StorageUsage(c, []mujica.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: int64(s.photos) * (1 << 30)}, {Label: "Apps", Bytes: 12 << 30}}, mujica.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"}).FillWidth()
}
func filesRecent() []mujica.RecentFile {
now := time.Date(2026, 10, 5, 15, 0, 0, 0, time.UTC)
return []mujica.RecentFile{{Path: "/court/seal.go", Opened: now}, {Path: "/court/notes.md", Opened: now.AddDate(0, 0, -1)}, {Path: "/court/crest.png", Opened: now.AddDate(0, 0, -4)}, {Path: "/court/charter.pdf", Opened: now.AddDate(0, -1, 0)}}
}
func recentFilesDemo(c *ui.Context, log func(string)) {
type state struct {
files []mujica.RecentFile
recent mujica.RecentFilesState
disabled bool
}
s := ui.Local(c.Root(), "390", func() state { return state{files: filesRecent()} })
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Reset", mujica.ButtonOptions{}).Clicked() {
s.files = filesRecent()
log("Recent files reset")
}
r := mujica.RecentFiles(c, s.files, &s.recent, mujica.RecentFilesOptions{Disabled: s.disabled, Now: time.Date(2026, 10, 5, 15, 0, 0, 0, time.UTC)})
r.Element.Height(320).FillWidth()
if r.Opened != nil {
log("Open: " + r.Opened.Path)
}
if r.Cleared {
s.files = nil
log("Recent files cleared")
}
if r.Removed != "" {
for i, f := range s.files {
if f.Path == r.Removed {
s.files = append(s.files[:i:i], s.files[i+1:]...)
break
}
}
log("Removed: " + r.Removed)
}
}
func archiveViewerDemo(c *ui.Context, log func(string)) {
type state struct {
archive mujica.ArchiveViewerState
entries []mujica.ArchiveEntry
status mujica.DataStatus
empty bool
}
s := ui.Local(c.Root(), "391", func() state {
return state{entries: []mujica.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}, {Path: "court/decrees/42.pdf", Size: 6000, Packed: 5000}, {Path: "README.md", Size: 1000, Packed: 400}}}
})
filesStatus(c, log, &s.status)
mediaCheck(c, log, &s.empty, "Empty")
entries := s.entries
if s.empty {
entries = nil
}
r := mujica.ArchiveViewer(c, entries, &s.archive, mujica.ArchiveViewerOptions{Status: s.status, Error: "Invalid archive"})
r.Element.Height(280).FillWidth()
if r.Opened != nil {
log("Open archive entry: " + r.Opened.Path)
}
if r.Retried {
s.status = mujica.DataReady
log("Archive read retried")
}
}
+137
View File
@@ -0,0 +1,137 @@
package main
const filesFileExplorerCode = `var state mujica.FileExplorerState
opts := mujica.FileExplorerOptions{
Roots: []string{"/court"},
Folders: func(string) []string { return nil },
Entries: func(string) []mujica.FileEntry {
return []mujica.FileEntry{{Path: "/court/seal.go", Name: "seal.go", Size: 1024}}
},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.FileExplorer(c, &state, opts).Element.Height(240)
}
ui.Render(view, 600, 300, 1)`
const filesFileGridCode = `entries := []mujica.FileEntry{{Path: "/court/seal.go", Name: "seal.go"}}
var state mujica.FileGridState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.FileGrid(c, entries, &state, mujica.FileGridOptions{}).Element.Height(240)
}
ui.Render(view, 400, 300, 1)`
const filesFileIconCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.FileIcon(c, "seal.go", mujica.FileIconOptions{Size: 32})
}
ui.Render(view, 100, 80, 1)`
const filesFilePreviewCode = `file := mujica.PreviewFile{Name: "seal.go", Text: "package court\nfunc Seal() {}"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.FilePreview(c, file, mujica.FilePreviewOptions{}).Element.Height(240)
}
ui.Render(view, 500, 300, 1)`
const filesPathBarCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.PathBar(c, "/court/chronicles/seals", mujica.PathBarOptions{})
}
ui.Render(view, 400, 100, 1)`
const filesRenameInlineCode = `name, editing := "seal.txt", true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.RenameInline(c, &name, &editing, mujica.RenameInlineOptions{})
}
ui.Render(view, 400, 100, 1)`
const filesFileOperationProgressCode = `op := mujica.FileOperation{Kind: mujica.FileCopy, Done: 2, Total: 10, Current: "seal.go"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.FileOperationProgress(c, op, mujica.FileOperationOptions{})
}
ui.Render(view, 500, 240, 1)`
const filesTransferQueueCode = `items := []mujica.QueuedTransfer{{ID: "a", Name: "ledger.zip", Upload: true, Status: mujica.TransferActive, Done: 25, Total: 100}}
var state mujica.TransferQueueState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.TransferQueue(c, items, &state, mujica.TransferQueueOptions{}).Element.Height(240)
}
ui.Render(view, 500, 300, 1)`
const filesStorageUsageCode = `segments := []mujica.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: 40 << 30}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.StorageUsage(c, segments, mujica.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"})
}
ui.Render(view, 500, 240, 1)`
const filesRecentFilesCode = `now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
files := []mujica.RecentFile{{Path: "/court/seal.go", Opened: now}}
var state mujica.RecentFilesState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.RecentFiles(c, files, &state, mujica.RecentFilesOptions{Now: now}).Element.Height(240)
}
ui.Render(view, 500, 300, 1)`
const filesArchiveViewerCode = `entries := []mujica.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}}
var state mujica.ArchiveViewerState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ArchiveViewer(c, entries, &state, mujica.ArchiveViewerOptions{}).Element.Height(240)
}
ui.Render(view, 600, 300, 1)`
const filesPresenceAvatarsCode = `people := []mujica.Collaborator{{ID: "ada", Name: "Ada Lovelace"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.PresenceAvatars(c, people, mujica.PresenceAvatarsOptions{})
}
ui.Render(view, 300, 100, 1)`
const filesReactionsCode = `items := []mujica.Reaction{{Emoji: "\U0001f44d", Count: 3, Mine: true}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Reactions(c, items, mujica.ReactionsOptions{Choices: []string{"\U0001f44d", "\U0001f389"}})
}
ui.Render(view, 400, 100, 1)`
const filesShareDialogCode = `open := false
members := []mujica.ShareMember{{ID: "ada", Name: "Ada", Permission: mujica.PermissionEdit, Owner: true}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.Button(c, "Share", mujica.ButtonOptions{}).Clicked() {
open = true
}
mujica.ShareDialog(c, &open, &members, mujica.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger"})
}
ui.Render(view, 600, 400, 1)`
const filesPermissionSelectCode = `value := mujica.PermissionView
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.PermissionSelect(c, &value, mujica.PermissionSelectOptions{})
}
ui.Render(view, 300, 100, 1)`
const filesLiveIndicatorCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.LiveIndicator(c, mujica.LiveOffline, mujica.LiveIndicatorOptions{})
}
ui.Render(view, 500, 100, 1)`
const filesRemoteCursorCode = `carets := []mujica.RemoteCaret{{ID: "ada", Name: "Ada", Offset: 4, Anchor: 4}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
ui.Box(c).Size(300, 100).Children(func() {
mujica.RemoteCursor(c, carets, mujica.RemoteCursorOptions{
Caret: func(offset int) ui.Rect { return ui.Rect{X: float32(offset) * 8, Y: 20, H: 20} },
})
})
}
ui.Render(view, 320, 120, 1)`
@@ -0,0 +1,195 @@
package main
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/theme"
)
var filesPeople = []mujica.Collaborator{
{ID: "a", Name: "Ada Lovelace"}, {ID: "b", Name: "Blaise Pascal", Away: true},
{ID: "c", Name: "Carl Gauss"}, {ID: "d", Name: "Dora Maar"}, {ID: "e", Name: "Emmy Noether"},
}
func presenceAvatarsDemo(c *ui.Context, log func(string)) {
type state struct {
following string
disabled, empty bool
max float64
}
s := ui.Local(c.Root(), "392", func() state { return state{max: 3} })
mediaCheck(c, log, &s.disabled, "Disabled")
mediaCheck(c, log, &s.empty, "Empty")
if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 1, Max: 5, Step: 1, Label: "Visible avatars"}).Changed() {
log(fmt.Sprintf("Visible: %.0f", s.max))
}
people := filesPeople
if s.empty {
people = nil
}
r := mujica.PresenceAvatars(c, people, mujica.PresenceAvatarsOptions{Following: s.following, Max: int(s.max), Disabled: s.disabled})
if r.Follow != "" {
s.following = r.Follow
log("Follow: " + r.Follow)
}
if r.Unfollow {
s.following = ""
log("Following stopped")
}
}
var filesReactionChoices = []string{"\U0001f44d", "\u2764\ufe0f", "\U0001f389", "\U0001f440", "\U0001f64f"}
func reactionsDemo(c *ui.Context, log func(string)) {
type state struct {
items []mujica.Reaction
disabled bool
}
s := ui.Local(c.Root(), "394", func() state {
return state{items: []mujica.Reaction{{Emoji: filesReactionChoices[0], Count: 3, Mine: true}, {Emoji: filesReactionChoices[2], Count: 1}}}
})
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Clear reactions", mujica.ButtonOptions{}).Clicked() {
s.items = nil
log("Reactions cleared")
}
r := mujica.Reactions(c, s.items, mujica.ReactionsOptions{Choices: filesReactionChoices, Disabled: s.disabled})
emoji := r.Toggled
if emoji == "" {
emoji = r.Added
}
if emoji == "" {
return
}
for i := range s.items {
if s.items[i].Emoji == emoji {
if s.items[i].Mine {
s.items[i].Mine = false
s.items[i].Count--
} else {
s.items[i].Mine = true
s.items[i].Count++
}
if s.items[i].Count == 0 {
s.items = append(s.items[:i:i], s.items[i+1:]...)
}
log("Reaction toggled")
return
}
}
s.items = append(s.items, mujica.Reaction{Emoji: emoji, Count: 1, Mine: true})
log("Reaction added")
}
func shareDialogDemo(c *ui.Context, log func(string)) {
type state struct {
members []mujica.ShareMember
open, disabled bool
next int
}
s := ui.Local(c.Root(), "395", func() state {
return state{members: []mujica.ShareMember{
{ID: "owner", Name: "Ada Lovelace", Email: "ada@court.org", Owner: true, Permission: mujica.PermissionEdit},
{ID: "b", Name: "Blaise Pascal", Email: "blaise@court.org", Permission: mujica.PermissionView},
}}
})
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Share Ledger", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() {
s.open = true
log("Share dialog opened")
}
was := s.open
r := mujica.ShareDialog(c, &s.open, &s.members, mujica.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger", Disabled: s.disabled, OnCopy: func(link string) { c.WriteClipboard(link); log("Copy callback") }})
if r.Invited != nil {
s.next++
s.members = append(s.members, mujica.ShareMember{ID: fmt.Sprintf("invite-%d", s.next), Name: r.Invited.Email, Email: r.Invited.Email, Permission: r.Invited.Permission})
log("Member invited")
}
if r.Changed != "" {
log("Permission changed: " + r.Changed)
}
if r.Removed != "" {
log("Member removed: " + r.Removed)
}
if r.Copied {
log("Link copied")
}
if was && !s.open {
log("Share dialog closed")
}
}
func permissionSelectDemo(c *ui.Context, log func(string)) {
type state struct {
value mujica.Permission
disabled bool
}
s := ui.Local(c.Root(), "396", func() state { return state{} })
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.PermissionSelect(c, &s.value, mujica.PermissionSelectOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("Permission: %d", s.value))
}
}
func liveIndicatorDemo(c *ui.Context, log func(string)) {
type state struct {
status mujica.LiveStatus
showTime bool
}
s := ui.Local(c.Root(), "399", func() state { return state{showTime: true} })
if mujica.Select(c, &s.status, []mujica.SelectOption[mujica.LiveStatus]{{Value: mujica.LiveSynced, Label: "Synced"}, {Value: mujica.LiveSyncing, Label: "Syncing"}, {Value: mujica.LiveReconnecting, Label: "Reconnecting"}, {Value: mujica.LiveOffline, Label: "Offline"}, {Value: mujica.LiveError, Label: "Error"}}, mujica.SelectOptions{Label: "Sync status"}).Changed() {
log(fmt.Sprintf("Sync status: %d", s.status))
}
mediaCheck(c, log, &s.showTime, "Last sync time")
now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
opts := mujica.LiveIndicatorOptions{Error: "Server unavailable", Now: now}
if s.showTime {
opts.LastSynced = now.Add(-3 * time.Minute)
}
if mujica.LiveIndicator(c, s.status, opts).Changed() {
s.status = mujica.LiveReconnecting
log("Sync retried")
}
}
func remoteCursorDemo(c *ui.Context, log func(string)) {
type state struct {
offset float64
selection, second bool
}
s := ui.Local(c.Root(), "400", func() state { return state{offset: 48, selection: true, second: true} })
if mujica.NumberInput(c, &s.offset, mujica.NumberInputOptions{Min: 0, Max: 79, Step: 1, Label: "Remote text offset"}).Changed() {
log(fmt.Sprintf("Offset: %.0f", s.offset))
}
mediaCheck(c, log, &s.selection, "Selection")
mediaCheck(c, log, &s.second, "Second collaborator")
carets := []mujica.RemoteCaret{{ID: "ada", Name: "Ada", Offset: int(s.offset), Anchor: int(s.offset)}}
if s.selection {
carets[0].Anchor = max(0, carets[0].Offset-5)
}
if s.second {
carets = append(carets, mujica.RemoteCaret{ID: "blaise", Name: "Blaise", Offset: 8, Anchor: 8})
}
ui.Box(c).Height(140).FillWidth().Background(mujica.Tokens(c).Surface).Children(func() {
ui.Column(c).Children(func() {
for _, line := range []string{"The seal of the court is kept", "by the chancellor, who answers"} {
ui.Text(c, line).Font(theme.MonoFont).FontSize(mujica.FontSize(c, 13)).Height(20)
}
})
at := func(o int) (float32, float32) { return float32(o%40) * 8, float32(o/40) * 20 }
mujica.RemoteCursor(c, carets, mujica.RemoteCursorOptions{
Caret: func(o int) ui.Rect { x, y := at(o); return ui.Rect{X: x, Y: y, H: 20} },
Selection: func(from, to int) []ui.Rect {
var out []ui.Rect
for o := from; o < to; o++ {
x, y := at(o)
out = append(out, ui.Rect{X: x, Y: y, W: 8, H: 20})
}
return out
},
})
})
}
+116
View File
@@ -0,0 +1,116 @@
package main
import (
"fmt"
"strings"
"testing"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func filesDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, *[]string) {
t.Helper()
for _, p := range filesPages {
if p.ID == 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() { p.Demo(c, func(s string) { *events = append(*events, s) }) })
}, 900, 800)
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
tt.Frame()
return tt, events
}
}
t.Fatalf("missing files page %s", id)
return nil, nil
}
func TestFilesGalleryExistingSet(t *testing.T) {
want := []string{"381", "382", "383", "384", "385", "386", "387", "388", "389", "390", "391", "392", "394", "395", "396", "399", "400"}
if len(filesPages) != len(want) {
t.Fatalf("files pages %d, want %d", len(filesPages), len(want))
}
for i, p := range filesPages {
if p.ID != want[i] || p.Category != "Files & Collaboration" || p.Code == "" || p.Keys == "" {
t.Fatalf("page %s has incorrect metadata", p.ID)
}
}
}
func TestFilesGalleryActions(t *testing.T) {
cases := []struct{ id, action, event string }{
{"381", "Disabled", "Disabled = true"},
{"382", "Empty", "Empty = true"},
{"383", "Folder", "Folder = true"},
{"384", "Close preview", "Preview closed"},
{"385", "court", "Navigate: /court"},
{"386", "Rename", "Rename started"},
{"387", "Cancel", "Operation canceled"},
{"388", "Pause ledger.zip", "Transfer a: 1"},
{"390", "Clear", "Recent files cleared"},
{"391", "Empty", "Empty = true"},
{"392", "Follow Ada Lovelace", "Follow: a"},
{"394", "Clear reactions", "Reactions cleared"},
{"395", "Share Ledger", "Share dialog opened"},
{"396", "Disabled", "Disabled = true"},
{"399", "Last sync time", "Last sync time = false"},
{"400", "Selection", "Selection = false"},
}
for _, tc := range cases {
for _, mode := range []mujica.Mode{mujica.Light, mujica.Dark} {
t.Run(fmt.Sprintf("%s-%s", tc.id, mode), func(t *testing.T) {
tt, events := filesDemoTester(t, tc.id, mode)
if err := tt.Click(tc.action); err != nil {
t.Fatal(err)
}
found := false
for _, event := range *events {
if event == tc.event {
found = true
}
}
if !found {
t.Fatalf("events %v, want %s", *events, tc.event)
}
if tc.id == "395" {
if err := tt.Click("Copy link"); err != nil {
t.Fatal(err)
}
if tt.Clipboard() != "https://court.org/ledger" {
t.Fatal("share callback did not copy link")
}
}
})
}
}
}
func TestFilesGalleryStorageParameter(t *testing.T) {
tt, events := filesDemoTester(t, "389", mujica.Light)
if err := tt.Click("Photos (GB)"); err != nil {
t.Fatal(err)
}
tt.Key(ui.Cmd, ui.KeyA)
tt.Type("41")
tt.Key(0, ui.KeyEnter)
if !tt.HasText("77.0 GB of 128.0 GB used") || len(*events) == 0 {
t.Fatalf("storage parameter left %q; events %v", tt.Texts(), *events)
}
}
func TestFilesGalleryGridRange(t *testing.T) {
tt, events := filesDemoTester(t, "382", mujica.Light)
if err := tt.Click("notes.md"); err != nil {
t.Fatal(err)
}
tt.Frame()
if err := tt.ClickWith(ui.Shift, "crest.png"); err != nil {
t.Fatal(err)
}
last := (*events)[len(*events)-1]
if strings.Contains(last, "/court/chronicles") || !strings.Contains(last, "/court/notes.md") || !strings.Contains(last, "/court/decrees/crest.png") {
t.Fatalf("range selection event %q", last)
}
}