WIP components (as built)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 15:51:57 -04:00
1 parent c804f2ab94
commit e9d5dbe6ad
102 files changed
+6538 -1

No files matched your search

+80 -1
View File
@@ -1,3 +1,82 @@
package main
var projectsPages = []page{}
const projectsCategory = "Projects"
var projectsPages = append(append(projectsTaskPages, projectsFieldPages...), projectsTimePages...)
var projectsTaskPages = []page{
{ID: "361", Name: "KanbanBoard", ZhName: "任务看板", Category: projectsCategory, Demo: projectsDemoKanbanBoard, Keys: "Tab 聚焦卡片;Enter/Space 打开;Alt+左右跨列;Alt+上下排序;可拖放到卡片之前或列末。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.KanbanBoard(c, &tasks, mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses})
if move, ok := r.Moved(); ok {
log.Println("move", move.ID, move.To, move.Index)
}
if id, ok := r.Opened(); ok {
log.Println("open", id)
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "362", Name: "TaskList", ZhName: "任务列表", Category: projectsCategory, Demo: projectsDemoTaskList, Keys: "Tab 在筛选器和任务控件间移动;空格切换完成;Enter 打开;F2 编辑标题。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}}
filter := mujica.TaskShowAll
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.TaskList(c, tasks, &filter, mujica.TaskListOptions{GroupBy: mujica.TaskGroupStatus, Statuses: statuses})
if id, ok := r.Changed(); ok {
log.Println("edit", id)
}
if id, ok := r.Opened(); ok {
log.Println("open", id)
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "363", Name: "TaskItem", ZhName: "任务行", Category: projectsCategory, Demo: projectsDemoTaskItem, Keys: "Tab 在复选框和标题间移动;空格切换完成;Enter 打开;F2 或双击开始编辑;Enter 保存;Escape 取消。", Code: `task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.TaskItem(c, &task, mujica.TaskItemOptions{})
if id, ok := r.Changed(); ok {
log.Println("edit", id)
}
if id, ok := r.Opened(); ok {
log.Println("open", id)
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "364", Name: "TaskDetailPanel", ZhName: "任务详情侧栏", Category: projectsCategory, Demo: projectsDemoTaskDetailPanel, Keys: "Tab 在字段间移动;选择框使用方向键和 Enter;Escape 返回关闭事件;子任务 Enter 新增。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.TaskDetailPanel(c, &task, mujica.TaskDetailPanelOptions{Statuses: statuses, Activity: func() { ui.Text(c, "Release planned") }})
if r.Changed() {
log.Println("task edited", task.ID)
}
if r.Closed() {
log.Println("close", task.ID)
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "365", Name: "SubtaskList", ZhName: "子任务列表", Category: projectsCategory, Demo: projectsDemoSubtaskList, Keys: "Tab 移动;Space 切换完成;新增输入框 Enter 提交;Alt+上下排序;拖动握柄排序。", Code: `subtasks := []mujica.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.SubtaskList(c, &subtasks, mujica.SubtaskListOptions{}).Changed() {
log.Println("subtasks changed", len(subtasks))
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "368", Name: "SprintBoard", ZhName: "迭代看板", Category: projectsCategory, Demo: projectsDemoSprintBoard, Keys: "看板卡片支持 Tab、Enter/Space、Alt+方向键与拖放。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}}
sprint := mujica.Sprint{Name: "Release sprint", Goal: "Ship", Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.SprintBoard(c, sprint, &tasks, mujica.SprintBoardOptions{Board: mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}})
if move, ok := r.Moved(); ok {
log.Println("move", move.ID, move.To)
}
if id, ok := r.Opened(); ok {
log.Println("open", id)
}
}
ui.Render(view, 1000, 700, 1)`},
}
+172
View File
@@ -0,0 +1,172 @@
package main
import (
"fmt"
"slices"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
var projectsStatuses = []mujica.WorkflowStatus{
{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted},
{ID: "doing", Name: "In progress", Group: mujica.StatusStarted},
{ID: "done", Name: "Done", Group: mujica.StatusCompleted},
}
var projectsMembers = []mujica.Member{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}, {ID: "cy", Name: "Cy"}}
var projectsLabels = []mujica.TaskLabel{{ID: "ui", Name: "UI", Color: mujica.EventInfo}, {ID: "release", Name: "Release", Color: mujica.EventSuccess}}
func projectsCheck(c *ui.Context, log func(string), value *bool, label string) {
if mujica.Checkbox(c, value, label, mujica.CheckboxOptions{}).Changed() {
log(fmt.Sprintf("%s = %v", label, *value))
}
}
func projectsChoice[T comparable](c *ui.Context, log func(string), value *T, options []mujica.SelectOption[T], label string) {
if mujica.Select(c, value, options, mujica.SelectOptions{Label: label}).Changed() {
log(fmt.Sprintf("%s = %v", label, *value))
}
}
type projectsTaskState struct {
tasks []mujica.Task
disabled bool
version int
group mujica.TaskGrouping
filter mujica.TaskFilter
wip float64
closed bool
}
func projectsTasks(c *ui.Context, key string) *projectsTaskState {
return *ui.Local(c.Root(), key, func() *projectsTaskState {
return &projectsTaskState{wip: 2, tasks: []mujica.Task{
{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh, Due: mujica.Date{Year: 2026, Month: 10, Day: 3}, Assignees: []string{"ann"}, Labels: []string{"ui"}, Subtasks: []mujica.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}}},
{ID: "ENG-2", Title: "Check keyboard paths", Status: "doing", Priority: mujica.PriorityUrgent, Assignees: []string{"ben"}, Labels: []string{"release"}},
{ID: "ENG-3", Title: "Publish docs", Status: "done", Done: true, Priority: mujica.PriorityLow},
}}
})
}
func projectsBoardOptions(s *projectsTaskState) mujica.KanbanBoardOptions {
return mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "doing", WIP: int(s.wip)}, {Status: "done"}}, Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Version: s.version, Disabled: s.disabled}
}
func projectsLogBoard(s *projectsTaskState, r mujica.KanbanResult, log func(string)) {
if move, ok := r.Moved(); ok {
s.version++
log(fmt.Sprintf("move %s: %s -> %s at %d", move.ID, move.From, move.To, move.Index))
}
if id, ok := r.Opened(); ok {
log("open " + id)
}
}
func projectsDemoKanbanBoard(c *ui.Context, log func(string)) {
s := projectsTasks(c, "361")
projectsCheck(c, log, &s.disabled, "Disabled")
if mujica.NumberInput(c, &s.wip, mujica.NumberInputOptions{Min: 0, Max: 10, Step: 1, Label: "WIP limit"}).Changed() {
log(fmt.Sprintf("WIP = %g", s.wip))
}
ui.ScrollHorizontal(c).Height(420).FillWidth().Children(func() {
ui.Box(c).Width(880).Height(420).Children(func() { projectsLogBoard(s, mujica.KanbanBoard(c, &s.tasks, projectsBoardOptions(s)), log) })
})
}
func projectsDemoTaskList(c *ui.Context, log func(string)) {
s := projectsTasks(c, "362")
projectsCheck(c, log, &s.disabled, "Disabled")
projectsChoice(c, log, &s.group, []mujica.SelectOption[mujica.TaskGrouping]{{Value: mujica.TaskGroupNone, Label: "Ungrouped"}, {Value: mujica.TaskGroupStatus, Label: "By status"}, {Value: mujica.TaskGroupPriority, Label: "By priority"}}, "Grouping")
ui.Box(c).Height(360).FillWidth().Children(func() {
before := s.filter
r := mujica.TaskList(c, s.tasks, &s.filter, mujica.TaskListOptions{Statuses: projectsStatuses, Members: projectsMembers, GroupBy: s.group, Version: s.version, Disabled: s.disabled})
if s.filter != before {
log(fmt.Sprintf("filter = %d", s.filter))
}
if id, ok := r.Changed(); ok {
s.version++
log("edit " + id)
}
if id, ok := r.Opened(); ok {
log("open " + id)
}
})
}
func projectsDemoTaskItem(c *ui.Context, log func(string)) {
s := projectsTasks(c, "363")
projectsCheck(c, log, &s.disabled, "Disabled")
projectsChoice(c, log, &s.tasks[0].Priority, []mujica.SelectOption[mujica.Priority]{{Value: mujica.PriorityLow, Label: "Low"}, {Value: mujica.PriorityHigh, Label: "High"}, {Value: mujica.PriorityUrgent, Label: "Urgent"}}, "Priority")
r := mujica.TaskItem(c, &s.tasks[0], mujica.TaskItemOptions{Members: projectsMembers, Disabled: s.disabled})
if id, ok := r.Changed(); ok {
log("edit " + id)
}
if id, ok := r.Opened(); ok {
log("open " + id)
}
}
func projectsDemoTaskDetailPanel(c *ui.Context, log func(string)) {
s := projectsTasks(c, "364")
projectsCheck(c, log, &s.disabled, "Disabled")
if s.closed {
if mujica.Button(c, "Open details", mujica.ButtonOptions{}).Clicked() {
s.closed = false
log("open details")
}
return
}
ui.Scroll(c).Height(620).FillWidth().Children(func() {
ui.Box(c).MaxWidth(500).FillWidth().Children(func() {
r := mujica.TaskDetailPanel(c, &s.tasks[0], mujica.TaskDetailPanelOptions{Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Disabled: s.disabled, Activity: func() { ui.Text(c, "Ann created this task") }})
if r.Changed() {
log("task fields changed")
}
if r.Closed() {
s.closed = true
log("close details")
}
})
})
}
func projectsDemoSubtaskList(c *ui.Context, log func(string)) {
s := projectsTasks(c, "365")
projectsCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Clear subtasks", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() {
s.tasks[0].Subtasks = nil
log("clear subtasks")
}
if mujica.SubtaskList(c, &s.tasks[0].Subtasks, mujica.SubtaskListOptions{Disabled: s.disabled}).Changed() {
log("subtasks changed")
}
}
func projectsDemoSprintBoard(c *ui.Context, log func(string)) {
s := projectsTasks(c, "368")
projectsCheck(c, log, &s.disabled, "Disabled")
sprint := mujica.Sprint{Name: "Release sprint", Goal: "Ship the first release", Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}}
ui.ScrollHorizontal(c).Height(540).FillWidth().Children(func() {
ui.Box(c).Width(880).Height(540).Children(func() {
projectsLogBoard(s, mujica.SprintBoard(c, sprint, &s.tasks, mujica.SprintBoardOptions{Board: projectsBoardOptions(s)}), log)
})
})
}
func projectsDemoLabelManager(c *ui.Context, log func(string)) {
s := *ui.Local(c.Root(), "374", func() *struct {
labels []mujica.TaskLabel
selected []string
disabled bool
} {
return &struct {
labels []mujica.TaskLabel
selected []string
disabled bool
}{labels: slices.Clone(projectsLabels)}
})
projectsCheck(c, log, &s.disabled, "Disabled")
if mujica.LabelManager(c, &s.labels, &s.selected, mujica.LabelManagerOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("labels changed: %d, selected %v", len(s.labels), s.selected))
}
}
+61
View File
@@ -0,0 +1,61 @@
package main
var projectsFieldPages = []page{
{ID: "369", Name: "BurndownChart", ZhName: "燃尽图", Category: projectsCategory, Demo: projectsDemoBurndownChart, Keys: "Tab 聚焦图例与图表;Space 切换图例;方向键逐日读取;Home/End 首尾;Escape 清除读数。", Code: `remaining := []float64{40, 32, 25, 18}
opts := mujica.BurndownChartOptions{Frame: mujica.ChartFrame{Label: "Release burndown"}, Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}, Total: 40}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.BurndownChart(c, remaining, opts)
if r.Changed() {
log.Println("point", r.Point)
}
if name, ok := r.Toggled(); ok {
log.Println("series", name)
}
if r.Retried() {
log.Println("retry")
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "370", Name: "PriorityIndicator", ZhName: "优先级指示", Category: projectsCategory, Demo: projectsDemoPriorityIndicator, Keys: "静态展示,通过无障碍名称读取完整等级。", Code: `priority := mujica.PriorityHigh
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.PriorityIndicator(c, priority, mujica.PriorityIndicatorOptions{})
}
ui.Render(view, 1000, 700, 1)`},
{ID: "371", Name: "StatusSelect", ZhName: "状态选择", Category: projectsCategory, Demo: projectsDemoStatusSelect, Keys: "Enter/Space 或上下键打开;上下、Home/End 定位;Enter 确认;Escape 关闭。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
value := "todo"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.StatusSelect(c, &value, statuses, mujica.StatusSelectOptions{}).Changed() {
log.Println("status", value)
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "372", Name: "AssigneePicker", ZhName: "负责人选择", Category: projectsCategory, Demo: projectsDemoAssigneePicker, Keys: "Enter/Space 打开;输入搜索;下键进入选项;Space/Enter 勾选;Backspace 移除最后一项;Escape 关闭。", Code: `members := []mujica.Member{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}}
ids := []string{"ann"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.AssigneePicker(c, &ids, members, mujica.AssigneePickerOptions{Max: 2}).Changed() {
log.Println("assignees", ids)
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "373", Name: "DueDatePicker", ZhName: "截止日期选择", Category: projectsCategory, Demo: projectsDemoDueDatePicker, Keys: "Tab 到快捷项、日期输入与日历按钮;Enter 选择;日历方向键移动;Escape 关闭。", Code: `var due mujica.Date
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.DueDatePicker(c, &due, mujica.DueDatePickerOptions{}).Changed() {
log.Println("due", due)
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "374", Name: "LabelManager", ZhName: "标签管理", Category: projectsCategory, Demo: projectsDemoLabelManager, Keys: "Tab 移动;Space 勾选;新增输入 Enter 创建;编辑颜色按钮打开编辑;Enter 保存名称;Escape 结束编辑。", Code: `labels := []mujica.TaskLabel{{ID: "ui", Name: "UI", Color: mujica.EventInfo}}
selected := []string{}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.LabelManager(c, &labels, &selected, mujica.LabelManagerOptions{}).Changed() {
log.Println("labels changed", len(labels), selected)
}
}
ui.Render(view, 1000, 700, 1)`},
}
@@ -0,0 +1,142 @@
package main
import (
"fmt"
"github.com/egoist/mygo/ui"
"math"
mujica "mujicaui"
)
func projectsDemoBurndownChart(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "369", func() struct {
disabled bool
status mujica.DataStatus
total float64
values []float64
} {
return struct {
disabled bool
status mujica.DataStatus
total float64
values []float64
}{total: 40, values: []float64{40, 38, 32, 34, 26, math.NaN(), 20, 18}}
})
projectsCheck(c, log, &s.disabled, "Disabled")
projectsChoice(c, log, &s.status, []mujica.SelectOption[mujica.DataStatus]{{Value: mujica.DataReady, Label: "Ready"}, {Value: mujica.DataLoading, Label: "Loading"}, {Value: mujica.DataFailed, Label: "Error"}}, "Data state")
if mujica.NumberInput(c, &s.total, mujica.NumberInputOptions{Min: 1, Max: 100, Step: 1, Label: "Total work"}).Changed() {
log(fmt.Sprintf("total = %g", s.total))
}
ui.Box(c).Disabled(s.disabled).FillWidth().Children(func() {
r := mujica.BurndownChart(c, s.values, mujica.BurndownChartOptions{Frame: mujica.ChartFrame{Label: "Release burndown", Status: s.status, Error: "Data unavailable"}, Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}, Total: s.total})
if r.Changed() {
log(fmt.Sprintf("point = %d", r.Point))
}
if name, ok := r.Toggled(); ok {
log("toggle " + name)
}
if r.Retried() {
s.status = mujica.DataReady
log("retry")
}
})
}
func projectsDemoPriorityIndicator(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "370", func() struct {
priority mujica.Priority
iconOnly bool
} {
return struct {
priority mujica.Priority
iconOnly bool
}{priority: mujica.PriorityHigh}
})
projectsCheck(c, log, &s.iconOnly, "Icon only")
projectsChoice(c, log, &s.priority, []mujica.SelectOption[mujica.Priority]{{Value: mujica.PriorityNone, Label: "None"}, {Value: mujica.PriorityLow, Label: "Low"}, {Value: mujica.PriorityMedium, Label: "Medium"}, {Value: mujica.PriorityHigh, Label: "High"}, {Value: mujica.PriorityUrgent, Label: "Urgent"}}, "Priority")
mujica.PriorityIndicator(c, s.priority, mujica.PriorityIndicatorOptions{IconOnly: s.iconOnly})
}
func projectsDemoStatusSelect(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "371", func() struct {
value string
disabled bool
} {
return struct {
value string
disabled bool
}{value: "todo"}
})
projectsCheck(c, log, &s.disabled, "Disabled")
if mujica.StatusSelect(c, &s.value, projectsStatuses, mujica.StatusSelectOptions{Disabled: s.disabled}).Changed() {
log("status " + s.value)
}
}
func projectsDemoAssigneePicker(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "372", func() struct {
ids []string
disabled bool
max float64
} {
return struct {
ids []string
disabled bool
max float64
}{ids: []string{"ann"}, max: 2}
})
projectsCheck(c, log, &s.disabled, "Disabled")
if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 0, Max: 3, Step: 1, Label: "Selection limit"}).Changed() {
log(fmt.Sprintf("limit = %g", s.max))
}
if mujica.AssigneePicker(c, &s.ids, projectsMembers, mujica.AssigneePickerOptions{Max: int(s.max), Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("assignees %v", s.ids))
}
}
func projectsDemoDueDatePicker(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "373", func() struct {
due mujica.Date
disabled, done bool
} {
return struct {
due mujica.Date
disabled, done bool
}{due: mujica.Date{Year: 2026, Month: 10, Day: 3}}
})
projectsCheck(c, log, &s.disabled, "Disabled")
projectsCheck(c, log, &s.done, "Completed")
if mujica.DueDatePicker(c, &s.due, mujica.DueDatePickerOptions{Done: s.done, Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("due %v", s.due))
}
}
func projectsDemoIssueCard(c *ui.Context, log func(string)) {
s := projectsTasks(c, "375")
selected := ui.Local(c.Root(), "375-selected", func() bool { return false })
projectsCheck(c, log, &s.disabled, "Disabled")
projectsCheck(c, log, selected, "Selected")
ui.Box(c).Width(300).Children(func() {
if mujica.IssueCard(c, s.tasks[0], mujica.IssueCardOptions{Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Selected: *selected, Disabled: s.disabled}).Clicked() {
*selected = !*selected
log("open " + s.tasks[0].ID)
}
})
}
func projectsDemoIssueIdBadge(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "376", func() struct {
disabled, copyable bool
id string
} {
return struct {
disabled, copyable bool
id string
}{copyable: true, id: "ENG-123"}
})
projectsCheck(c, log, &s.disabled, "Disabled")
projectsCheck(c, log, &s.copyable, "Copyable")
projectsChoice(c, log, &s.id, []mujica.SelectOption[string]{{Value: "ENG-123", Label: "ENG-123"}, {Value: "OPS-7", Label: "OPS-7"}}, "Issue key")
if mujica.IssueIdBadge(c, s.id, mujica.IssueIdBadgeOptions{Copyable: s.copyable, Disabled: s.disabled}).Changed() {
log("copied " + s.id)
}
}
+97
View File
@@ -0,0 +1,97 @@
package main
import (
"strings"
"testing"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func projectsGalleryTester(t *testing.T, id string) (*ui.Tester, *[]string) {
t.Helper()
var events []string
for _, p := range projectsPages {
if p.ID == id {
tt := ui.NewTester(func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
ui.Column(c).Fill().Gap(12).Padding(16).Children(func() { p.Demo(c, func(s string) { events = append(events, s) }) })
}, 1200, 1000)
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
return tt, &events
}
}
t.Fatal("unknown project page " + id)
return nil, nil
}
func TestProjectsGalleryCatalog(t *testing.T) {
if len(projectsPages) != 18 {
t.Fatalf("pages = %d", len(projectsPages))
}
seen := map[string]bool{}
for _, p := range projectsPages {
if p.Category != "Projects" || p.Keys == "" || p.Code == "" || seen[p.ID] {
t.Fatalf("invalid page %s", p.ID)
}
seen[p.ID] = true
projectsGalleryTester(t, p.ID)
}
if seen["366"] || seen["367"] {
t.Fatal("dropped components have gallery pages")
}
}
func TestProjectsGalleryEvents(t *testing.T) {
for _, tc := range []struct{ id, action, event string }{
{"361", "ENG-1 Plan the release", "open ENG-1"},
{"362", "Done 1", "filter = 2"},
{"363", "Plan the release", "open ENG-1"},
{"364", "Close details", "close details"},
{"365", "Clear subtasks", "clear subtasks"},
{"368", "ENG-1 Plan the release", "open ENG-1"},
{"369", "Ideal", "toggle Ideal"},
{"370", "Icon only", "Icon only = true"},
{"371", "Status", ""},
{"372", "Disabled", "Disabled = true"},
{"373", "Today", "due "},
{"374", "UI", "labels changed:"},
{"375", "ENG-1 Plan the release", "open ENG-1"},
{"376", "Copy ENG-123", "copied ENG-123"},
{"377", "Completed tasks", ""},
{"378", "Ben assigned hours", ""},
{"379", "Start", "tracked "},
{"380", "Start", "timer phase = 0, running = true"},
} {
t.Run(tc.id, func(t *testing.T) {
tt, events := projectsGalleryTester(t, tc.id)
if err := tt.Click(tc.action); err != nil {
t.Fatal(err)
}
switch tc.id {
case "371":
tt.Key(0, ui.KeyEnd)
tt.Key(0, ui.KeyEnter)
tc.event = "status done"
case "377", "378":
tt.Command("selectAll")
tt.Type("5")
tt.Key(0, ui.KeyEnter)
if tc.id == "377" {
tc.event = "completed = 5"
} else {
tc.event = "assigned = 5"
}
}
found := false
for _, e := range *events {
if strings.HasPrefix(e, tc.event) {
found = true
}
}
if !found {
t.Fatalf("event %q missing in %v", tc.event, *events)
}
})
}
}
+53
View File
@@ -0,0 +1,53 @@
package main
var projectsTimePages = []page{
{ID: "375", Name: "IssueCard", ZhName: "任务卡片", Category: projectsCategory, Demo: projectsDemoIssueCard, Keys: "Tab 聚焦;Enter/Space 激活。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.IssueCard(c, task, mujica.IssueCardOptions{Statuses: statuses}).Clicked() {
log.Println("open", task.ID)
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "376", Name: "IssueIdBadge", ZhName: "任务编号", Category: projectsCategory, Demo: projectsDemoIssueIdBadge, Keys: "Copyable 开启时 Tab 聚焦,Enter/Space 复制。", Code: `id := "ENG-123"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.IssueIdBadge(c, id, mujica.IssueIdBadgeOptions{Copyable: true}).Changed() {
log.Println("copied", id)
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "377", Name: "MilestoneProgress", ZhName: "里程碑进度", Category: projectsCategory, Demo: projectsDemoMilestoneProgress, Keys: "静态展示,进度条通过无障碍名称与百分比读取。", Code: `milestone := mujica.Milestone{Title: "Release", Due: mujica.Date{Year: 2026, Month: 10, Day: 14}, Done: 3, Total: 8}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.MilestoneProgress(c, milestone, mujica.MilestoneProgressOptions{})
}
ui.Render(view, 1000, 700, 1)`},
{ID: "378", Name: "WorkloadView", ZhName: "成员负载", Category: projectsCategory, Demo: projectsDemoWorkloadView, Keys: "静态行通过无障碍名称读取负载与超出量;滚动查看更多成员。", Code: `loads := []mujica.Workload{{Member: mujica.Member{ID: "ann", Name: "Ann"}, Assigned: 46, Capacity: 40}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.WorkloadView(c, loads, mujica.WorkloadViewOptions{})
}
ui.Render(view, 1000, 700, 1)`},
{ID: "379", Name: "TimeTracker", ZhName: "工时计时", Category: projectsCategory, Demo: projectsDemoTimeTracker, Keys: "Tab 移动;Enter/Space 开始停止;停止后打开修正,编辑时长,Apply 提交。", Code: `var state mujica.TimeTrackerState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.TimeTracker(c, &state, mujica.TimeTrackerOptions{}).Changed() {
log.Println("tracked", state.Total(c.Now()))
}
}
ui.Render(view, 1000, 700, 1)`},
{ID: "380", Name: "PomodoroTimer", ZhName: "番茄钟", Category: projectsCategory, Demo: projectsDemoPomodoroTimer, Keys: "Tab 移动;Enter/Space 开始、暂停、跳过与重置。", Code: `var state mujica.PomodoroState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.PomodoroTimer(c, &state, mujica.PomodoroTimerOptions{Focus: 25 * time.Minute})
if r.Changed() {
log.Println("timer", state.Phase(), state.Running())
}
if phase, ok := r.Finished(); ok {
log.Println("finished", phase)
}
}
ui.Render(view, 1000, 700, 1)`},
}
@@ -0,0 +1,83 @@
package main
import (
"fmt"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"time"
)
func projectsDemoMilestoneProgress(c *ui.Context, log func(string)) {
done := ui.Local(c.Root(), "377", func() float64 { return 3 })
if mujica.NumberInput(c, done, mujica.NumberInputOptions{Min: 0, Max: 8, Step: 1, Label: "Completed tasks"}).Changed() {
log(fmt.Sprintf("completed = %g", *done))
}
mujica.MilestoneProgress(c, mujica.Milestone{Title: "Release", Due: mujica.Date{Year: 2026, Month: 10, Day: 14}, Done: int(*done), Total: 8}, mujica.MilestoneProgressOptions{})
}
func projectsDemoWorkloadView(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "378", func() struct {
assigned float64
loads []mujica.Workload
version int
} {
return struct {
assigned float64
loads []mujica.Workload
version int
}{assigned: 46, loads: []mujica.Workload{{Member: projectsMembers[0], Assigned: 32, Capacity: 40}, {Member: projectsMembers[1], Assigned: 46, Capacity: 40}, {Member: projectsMembers[2], Assigned: 12, Capacity: 20}}}
})
if mujica.NumberInput(c, &s.assigned, mujica.NumberInputOptions{Min: 0, Max: 80, Step: 1, Label: "Ben assigned hours"}).Changed() {
s.loads[1].Assigned = s.assigned
s.version++
log(fmt.Sprintf("assigned = %g", s.assigned))
}
ui.Box(c).Height(240).FillWidth().Children(func() { mujica.WorkloadView(c, s.loads, mujica.WorkloadViewOptions{Version: s.version}) })
}
func projectsDemoTimeTracker(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "379", func() struct {
timer mujica.TimeTrackerState
disabled bool
} {
return struct {
timer mujica.TimeTrackerState
disabled bool
}{}
})
projectsCheck(c, log, &s.disabled, "Disabled")
ui.ScrollHorizontal(c).Height(120).FillWidth().Children(func() {
if mujica.TimeTracker(c, &s.timer, mujica.TimeTrackerOptions{Disabled: s.disabled}).Changed() {
log("tracked " + s.timer.Total(c.Now()).String())
}
})
}
func projectsDemoPomodoroTimer(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "380", func() struct {
timer mujica.PomodoroState
disabled, hidden bool
seconds float64
} {
return struct {
timer mujica.PomodoroState
disabled, hidden bool
seconds float64
}{seconds: 10}
})
projectsCheck(c, log, &s.disabled, "Disabled")
projectsCheck(c, log, &s.hidden, "Hidden")
if mujica.NumberInput(c, &s.seconds, mujica.NumberInputOptions{Min: 1, Max: 1500, Step: 1, Label: "Focus seconds", Disabled: s.timer.Running()}).Changed() {
log(fmt.Sprintf("focus = %g seconds", s.seconds))
}
if s.hidden {
return
}
r := mujica.PomodoroTimer(c, &s.timer, mujica.PomodoroTimerOptions{Focus: time.Duration(s.seconds) * time.Second, Break: 5 * time.Second, LongBreak: 10 * time.Second, LongEvery: 2, Disabled: s.disabled})
if r.Changed() {
log(fmt.Sprintf("timer phase = %d, running = %v", s.timer.Phase(), s.timer.Running()))
}
if phase, ok := r.Finished(); ok {
log(fmt.Sprintf("finished phase %d", phase))
}
}