Merge branch 'main' into worktree-agent-a6a6a7e2b56f82ab2

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 13:13:47 -04:00
commit 20279eff29
157 files changed
+2566 -710

No files matched your search

+7 -6
View File
@@ -105,10 +105,10 @@ view := func(c *ui.Context) {
func choiceDemo(c *ui.Context, controls, samples func()) {
k := mujica.Tokens(c)
ui.Column(c).Gap(20).AlignItems(ui.Start).Children(func() {
ui.Text(c, "Parameters").Font(theme.SerifFont).FontSize(theme.H3Size).TextColor(k.Text)
ui.Text(c, "Parameters").Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size)).TextColor(k.Text)
ui.Row(c).Gap(20).Wrap().AlignItems(ui.Center).Children(controls)
ui.Box(c).Height(1).FillWidth().Background(k.Ornament)
ui.Text(c, "States").Font(theme.SerifFont).FontSize(theme.H3Size).TextColor(k.Text)
ui.Text(c, "States").Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size)).TextColor(k.Text)
ui.Column(c).Gap(16).AlignItems(ui.Start).Children(samples)
})
}
@@ -123,7 +123,7 @@ func choiceFlag(c *ui.Context, on *bool, label string, log func(string)) {
// choiceLabeled shows a caption above a sample.
func choiceLabeled(c *ui.Context, caption string, fn func()) {
ui.Column(c).Gap(6).Children(func() {
ui.Text(c, caption).FontSize(theme.CaptionSize).TextColor(mujica.Tokens(c).TextMuted)
ui.Text(c, caption).FontSize(mujica.FontSize(c, theme.CaptionSize)).TextColor(mujica.Tokens(c).TextMuted)
fn()
})
}
@@ -288,7 +288,7 @@ var choiceFruits = []mujica.SelectOption[string]{
func selectDemo(c *ui.Context, log func(string)) {
type state struct {
value string
value, picked string
clearable, disabled, noData bool
}
s := *ui.Local(c.Root(), "036", func() *state { return &state{value: "banana", clearable: true} })
@@ -306,9 +306,10 @@ func selectDemo(c *ui.Context, log func(string)) {
log(fmt.Sprintf("Fruit: %q", s.value))
}
})
empty := ""
choiceLabeled(c, "Placeholder", func() {
mujica.Select(c, &empty, choiceFruits, mujica.SelectOptions{Placeholder: "Pick a fruit"})
if mujica.Select(c, &s.picked, choiceFruits, mujica.SelectOptions{Placeholder: "Pick a fruit", Clearable: true}).Changed() {
log(fmt.Sprintf("Picked: %q", s.picked))
}
})
})
}
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"slices"
"testing"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
// choiceDemoTester runs the demo of page id at text scale and returns its tester and event log.
func choiceDemoTester(t *testing.T, id string, scale float32) (*ui.Tester, *[]string) {
t.Helper()
i := slices.IndexFunc(choicePages, func(p page) bool { return p.ID == id })
if i < 0 {
t.Fatalf("no page %s", id)
}
events := &[]string{}
tt := ui.NewTester(func(c *ui.Context) {
mujica.Use(c, mujica.Settings{Mode: mujica.Light})
ui.Scroll(c).Fill().Padding(24).Children(func() {
choicePages[i].Demo(c, func(s string) { *events = append(*events, s) })
})
}, 900, 700)
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: scale})
return tt, events
}
func TestChoiceSelectDemoKeepsPlaceholderChoice(t *testing.T) {
tt, events := choiceDemoTester(t, "036", 1)
if err := tt.Click("Pick a fruit"); err != nil {
t.Fatal(err)
}
if err := tt.Click("Fig"); err != nil {
t.Fatal(err)
}
tt.Frame()
if !tt.HasText("Fig") || !slices.Contains(*events, `Picked: "fig"`) {
t.Fatalf("choice lost: texts %q, events %q", tt.Texts(), *events)
}
}
func TestChoiceDemoHeadingsFollowTextScale(t *testing.T) {
height := func(scale float32) float32 {
tt, _ := choiceDemoTester(t, "031", scale)
tt.Frame()
r, ok := tt.Find("Parameters")
if !ok {
t.Fatal("heading missing")
}
return r.H
}
if one, two := height(1), height(2); two < one*1.5 {
t.Fatalf("heading %v high at 2x, %v at 1x", two, one)
}
}
-8
View File
@@ -74,7 +74,6 @@ func textDemo(c *ui.Context, log func(string)) {
log("edit canceled")
}
})
displayCode(c, textCode, log)
}
const textCode = `mujica.Text(c, "Order ID: 4821-AX", mujica.TextOptions{Selectable: true})
@@ -113,7 +112,6 @@ func headingDemo(c *ui.Context, log func(string)) {
mujica.Heading(c, fmt.Sprintf("Level %d · 宫廷", l), mujica.HeadingOptions{Level: l})
}
})
displayCode(c, headingCode, log)
}
const headingCode = `mujica.Heading(c, "The Grand Hall", mujica.HeadingOptions{Subtitle: "Winter ceremonies", Divider: true})
@@ -163,7 +161,6 @@ func iconDemo(c *ui.Context, log func(string)) {
mujica.Icon(c, "missing-name", mujica.IconOptions{Size: 20})
})
})
displayCode(c, iconCode, log)
}
const iconCode = `ui.Row(c).Gap(8).Children(func() {
@@ -203,7 +200,6 @@ func avatarGroupDemo(c *ui.Context, log func(string)) {
}
}
})
displayCode(c, avatarGroupCode, log)
}
const avatarGroupCode = `mujica.AvatarGroup(c, people, mujica.AvatarGroupOptions{Max: 3})`
@@ -250,7 +246,6 @@ func badgeDemo(c *ui.Context, log func(string)) {
mujica.Badge(c, opts, func() { mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{Size: 40}) })
})
})
displayCode(c, badgeCode, log)
}
const badgeCode = `mujica.Badge(c, mujica.BadgeOptions{Text: "New", Tone: mujica.BadgeNeutral}, nil)
@@ -303,7 +298,6 @@ func tagDemo(c *ui.Context, log func(string)) {
}
})
})
displayCode(c, tagCode, log)
}
const tagCode = `for i, t := range tags {
@@ -345,7 +339,6 @@ func linkDemo(c *ui.Context, log func(string)) {
log("in-app action")
}
})
displayCode(c, linkCode, log)
}
const linkCode = `mujica.Link(c, "Read the charter", mujica.LinkOptions{URL: "https://example.com/charter"})
@@ -384,7 +377,6 @@ func kbdDemo(c *ui.Context, log func(string)) {
})
}
})
displayCode(c, kbdCode, log)
}
const kbdCode = `ui.Row(c).Gap(6).Children(func() {
-2
View File
@@ -59,7 +59,6 @@ func imageDemo(c *ui.Context, log func(string)) {
}
})
})
displayCode(c, imageCode, log)
}
const imageCode = `img := mujica.Image(c, photo, mujica.ImageOptions{
@@ -111,7 +110,6 @@ func avatarDemo(c *ui.Context, log func(string)) {
mujica.Avatar(c, "", mujica.AvatarOptions{Size: 40})
})
})
displayCode(c, avatarCode, log)
}
const avatarCode = `ui.Row(c).Gap(8).Children(func() {
-12
View File
@@ -6,7 +6,6 @@ import (
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/theme"
)
// displaySection shows a titled block of a demo.
@@ -22,17 +21,6 @@ func displayControls(c *ui.Context, fn func()) {
ui.Row(c).Gap(16).Wrap().AlignItems(ui.Center).Children(fn)
}
// displayCode shows a page's snippet with a copy button.
func displayCode(c *ui.Context, code string, log func(string)) {
displaySection(c, "Code", func() {
if mujica.Button(c, "Copy code", mujica.ButtonOptions{Variant: mujica.Secondary}).AlignSelf(ui.Start).Clicked() {
c.WriteClipboard(code)
log("code copied")
}
mujica.Text(c, code, mujica.TextOptions{Selectable: true}).Font(theme.MonoFont).FontSize(12)
})
}
// displayLog logs a control's new value when it changed.
func displayLog[T any](log func(string), e *ui.Element, name string, v *T) {
if e.Changed() {
+33 -15
View File
@@ -15,7 +15,7 @@ var overlayPages = []page{
{ID: "081", Name: "Dialog", ZhName: "对话框", Category: overlayCategory, Demo: dialogDemo, Code: dialogCode,
Keys: "Tab / Shift+Tab 在对话框内循环;Escape 关闭(可关闭此规则);关闭后焦点回到触发按钮。"},
{ID: "082", Name: "AlertDialog", ZhName: "警示对话框", Category: overlayCategory, Demo: alertDialogDemo, Code: alertDialogCode,
Keys: "打开时焦点在取消;Enter 只触发聚焦的按钮;Escape 等同取消;点击遮罩无效。"},
Keys: "打开时焦点在取消;Enter 只触发聚焦的按钮,输入框中的 Enter 不确认;Escape 等同取消;点击遮罩无效。"},
{ID: "083", Name: "Drawer", ZhName: "抽屉", Category: overlayCategory, Demo: drawerDemo, Code: drawerCode,
Keys: "模态:焦点限制在抽屉内并在关闭后恢复;Escape 关闭。非模态:窗口仍可操作,Escape 关闭。"},
{ID: "084", Name: "Popover", ZhName: "弹出框", Category: overlayCategory, Demo: popoverDemo, Code: popoverCode,
@@ -25,7 +25,7 @@ var overlayPages = []page{
{ID: "086", Name: "HoverCard", ZhName: "悬停卡片", Category: overlayCategory, Demo: hoverCardDemo, Code: hoverCardCode,
Keys: "键盘聚焦锚点后显示,Tab 进入卡片;Escape 关闭。"},
{ID: "087", Name: "DropdownMenu", ZhName: "下拉菜单", Category: overlayCategory, Demo: dropdownMenuDemo, Code: dropdownMenuCode,
Keys: "Enter / Space / ↓ 打开;↑↓ Home End 移动;→ 打开子菜单,← 关闭;Enter 选择;Escape 或 Tab 关闭。"},
Keys: "Enter / Space / ↓ 打开;↑↓ Home End 移动;→ 打开子菜单,← 关闭;Enter 选择;Escape 关闭最上层;Tab 在任一层关闭整个菜单。"},
{ID: "088", Name: "ContextMenu", ZhName: "右键菜单", Category: overlayCategory, Demo: contextMenuDemo, Code: contextMenuCode,
Keys: "右键在指针处打开;焦点在区域内时按 Menu 键或 Shift+F10 在区域下方打开;其余按键同下拉菜单。"},
{ID: "089", Name: "Popconfirm", ZhName: "气泡确认", Category: overlayCategory, Demo: popconfirmDemo, Code: popconfirmCode,
@@ -171,10 +171,7 @@ func drawerDemo(c *ui.Context, log func(string)) {
log("side = " + s.side)
}
overlayCheck(c, log, &s.nonModal, "NonModal")
ui.Text(c, fmt.Sprintf("Size %.0f", s.size))
if ui.StepSlider(c, &s.size, 120, 900, 20).Width(160).Changed() {
log(fmt.Sprintf("size = %.0f", s.size))
}
overlaySlider(c, log, &s.size, "Size", "DIP", 120, 900, 20)
})
if mujica.Button(c, "Open drawer", mujica.ButtonOptions{}).Clicked() {
s.open = true
@@ -247,10 +244,7 @@ ui.Render(view, 400, 300, 1)`
func tooltipDemo(c *ui.Context, log func(string)) {
delay := ui.Local(c.Root(), "085", func() float64 { return 500 })
overlayControls(c, func() {
ui.Text(c, fmt.Sprintf("Delay %.0f ms", *delay))
if ui.StepSlider(c, delay, 100, 1500, 100).Width(160).Changed() {
log(fmt.Sprintf("delay = %.0f ms", *delay))
}
overlaySlider(c, log, delay, "Delay", "ms", 100, 1500, 100)
})
opts := mujica.TooltipOptions{Delay: time.Duration(*delay) * time.Millisecond}
ui.Row(c).Gap(12).Children(func() {
@@ -276,19 +270,43 @@ const hoverCardCode = `view := func(c *ui.Context) {
}
ui.Render(view, 400, 300, 1)`
type hoverCardDemoState struct {
shown bool
openDelay, closeDelay, width float64
}
func hoverCardDemo(c *ui.Context, log func(string)) {
shown := ui.Local(c.Root(), "086", func() bool { return false })
s := ui.Local(c.Root(), "086", func() hoverCardDemoState {
return hoverCardDemoState{openDelay: 500, closeDelay: 300, width: 280}
})
overlayControls(c, func() {
overlaySlider(c, log, &s.openDelay, "OpenDelay", "ms", 100, 1500, 100)
overlaySlider(c, log, &s.closeDelay, "CloseDelay", "ms", 100, 1500, 100)
overlaySlider(c, log, &s.width, "Width", "DIP", 200, 420, 20)
})
b := mujica.Button(c, "Lady Sakiko", mujica.ButtonOptions{Variant: mujica.Ghost})
card := mujica.HoverCard(c, b, mujica.HoverCardOptions{}, func() {
card := mujica.HoverCard(c, b, mujica.HoverCardOptions{
OpenDelay: time.Duration(s.openDelay) * time.Millisecond,
CloseDelay: time.Duration(s.closeDelay) * time.Millisecond,
Width: float32(s.width),
}, func() {
ui.Text(c, "Lady Sakiko").FontWeight(600)
ui.Text(c, "Keyboardist of the court; writes the masquerade's score.").TextColor(mujica.Tokens(c).TextMuted)
if mujica.Button(c, "Follow", mujica.ButtonOptions{Variant: mujica.Outline}).Clicked() {
log("follow")
}
})
if (card != nil) != *shown {
*shown = card != nil
log(fmt.Sprintf("card shown = %v", *shown))
if (card != nil) != s.shown {
s.shown = card != nil
log(fmt.Sprintf("card shown = %v", s.shown))
}
}
// overlaySlider is a labeled step slider logging its value.
func overlaySlider(c *ui.Context, log func(string), v *float64, name, unit string, lo, hi, step float64) {
ui.Text(c, fmt.Sprintf("%s %.0f %s", name, *v, unit))
if ui.StepSlider(c, v, lo, hi, step).Width(140).Changed() {
log(fmt.Sprintf("%s = %.0f %s", name, *v, unit))
}
}