Merge Settings & Account

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 15:46:45 -04:00
commit 0e51784330
108 files changed
+6501 -2

No files matched your search

+153 -1
View File
@@ -1,3 +1,155 @@
package main
var accountPages = []page{}
import (
"fmt"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
const accountCategory = "Settings & Account"
var accountPages = []page{
{ID: "461", Name: "SettingsLayout", ZhName: "设置布局", Category: accountCategory, Code: accountCode["SettingsLayout"], Demo: accountSettingsDemo,
Keys: "Arrows move in the navigation; Tab enters the page; clicking a row's text focuses its control."},
{ID: "462", Name: "SettingsSearch", ZhName: "设置搜索", Category: accountCategory, Code: accountCode["SettingsSearch"], Demo: accountSearchDemo,
Keys: "Type to search; Up and Down move; Enter jumps to the setting and focuses it."},
{ID: "463", Name: "ThemeSelector", ZhName: "主题选择", Category: accountCategory, Code: accountCode["ThemeSelector"], Demo: accountThemeDemo,
Keys: "Left and Right move between cards; Space picks."},
{ID: "464", Name: "AccentColorPicker", ZhName: "主色选择", Category: accountCategory, Code: accountCode["AccentColorPicker"], Demo: accountAccentDemo,
Keys: "Left and Right move between swatches; Space picks."},
{ID: "465", Name: "ShortcutRecorder", ZhName: "快捷键录制", Category: accountCategory, Code: accountCode["ShortcutRecorder"], Demo: accountRecorderDemo,
Keys: "Enter or Space records; press the combination; Escape cancels."},
{ID: "466", Name: "KeyboardShortcutsList", ZhName: "快捷键列表", Category: accountCategory, Code: accountCode["KeyboardShortcutsList"], Demo: accountShortcutsDemo,
Keys: "Type in the search field to filter by action or key."},
{ID: "467", Name: "LoginForm", ZhName: "登录表单", Category: accountCategory, Code: accountCode["LoginForm"], Demo: accountLoginDemo,
Keys: "Enter in any field signs in; an empty field takes the focus."},
{ID: "468", Name: "OAuthButtons", ZhName: "第三方登录按钮", Category: accountCategory, Code: accountCode["OAuthButtons"], Demo: accountOAuthDemo,
Keys: "Tab between providers; Enter or Space continues."},
{ID: "469", Name: "TwoFactorInput", ZhName: "两步验证码", Category: accountCategory, Code: accountCode["TwoFactorInput"], Demo: accountTwoFactorDemo,
Keys: "Type or paste digits; Backspace clears; Tab reaches Resend."},
{ID: "470", Name: "ProfileEditor", ZhName: "个人资料编辑", Category: accountCategory, Code: accountCode["ProfileEditor"], Demo: accountProfileDemo,
Keys: "Tab through picture, name, bio, Discard and Save."},
{ID: "471", Name: "AccountSwitcher", ZhName: "账户切换", Category: accountCategory, Code: accountCode["AccountSwitcher"], Demo: accountSwitcherDemo,
Keys: "Enter, Space or Down opens; arrows move; Enter chooses; Escape closes."},
{ID: "472", Name: "WorkspaceSwitcher", ZhName: "工作区切换", Category: accountCategory, Code: accountCode["WorkspaceSwitcher"], Demo: accountWorkspaceDemo,
Keys: "Open, type to search, Up and Down move, Enter switches, Escape closes."},
{ID: "473", Name: "UserMenu", ZhName: "用户菜单", Category: accountCategory, Code: accountCode["UserMenu"], Demo: accountUserMenuDemo,
Keys: "Enter, Space or Down opens; arrows move; Enter chooses."},
{ID: "474", Name: "SessionList", ZhName: "登录会话", Category: accountCategory, Code: accountCode["SessionList"], Demo: accountSessionsDemo,
Keys: "Tab to each Sign out; the confirmation focuses Cancel."},
{ID: "475", Name: "ApiKeyManager", ZhName: "API 密钥管理", Category: accountCategory, Code: accountCode["ApiKeyManager"], Demo: accountKeysDemo,
Keys: "Enter in the name field creates; Copy key copies; Revoke asks first."},
{ID: "476", Name: "UsageQuota", ZhName: "用量配额", Category: accountCategory, Code: accountCode["UsageQuota"], Demo: accountQuotaDemo,
Keys: "Display only; each bar reads as a meter."},
{ID: "477", Name: "OnboardingWizard", ZhName: "引导向导", Category: accountCategory, Code: accountCode["OnboardingWizard"], Demo: accountWizardDemo,
Keys: "Next has the focus; Tab reaches Back and Skip."},
{ID: "478", Name: "Coachmark", ZhName: "功能提示", Category: accountCategory, Code: accountCode["Coachmark"], Demo: accountCoachDemo,
Keys: "Next has the focus; Escape closes the tour."},
{ID: "479", Name: "WhatsNewDialog", ZhName: "更新内容", Category: accountCategory, Code: accountCode["WhatsNewDialog"], Demo: accountWhatsNewDemo,
Keys: "Got it has the focus; Escape closes."},
{ID: "480", Name: "FeedbackWidget", ZhName: "反馈组件", Category: accountCategory, Code: accountCode["FeedbackWidget"], Demo: accountFeedbackDemo,
Keys: "Arrows switch the type; Tab reaches the message and buttons."},
}
type accountSettingsState struct {
page, query string
sound, digest, report bool
}
var accountSettingPages = []mujica.SettingsPage{{ID: "general", Label: "General"}, {ID: "privacy", Label: "Privacy"}}
var accountSettingEntries = []mujica.SettingEntry{
{ID: "sound", Page: "general", Label: "Sound", Description: "Chime on new messages.", Keywords: []string{"audio", "notification"}},
{ID: "digest", Page: "general", Label: "Weekly digest", Description: "A summary every Monday.", Keywords: []string{"email"}},
{ID: "report", Page: "privacy", Label: "Usage reports", Description: "Send anonymous usage."},
}
// accountSettingsPage builds the demo pages shared by 461 and 462.
func accountSettingsPage(c *ui.Context, d *accountSettingsState, log func(string)) {
ui.Box(c).Width(900).Height(320).Children(func() {
if mujica.SettingsLayout(c, &d.page, accountSettingPages, mujica.SettingsLayoutOptions{}, func(id string) {
switch id {
case "general":
mujica.SettingsSection(c, "Notifications", func() {
accountSwitchRow(c, "sound", "Sound", "Chime on new messages.", &d.sound, log)
accountSwitchRow(c, "digest", "Weekly digest", "A summary every Monday.", &d.digest, log)
})
case "privacy":
mujica.SettingsSection(c, "Data", func() {
accountSwitchRow(c, "report", "Usage reports", "Send anonymous usage.", &d.report, log)
})
}
}).Changed() {
log("page " + d.page)
}
})
}
// accountSwitchRow is a settings row with a switch.
func accountSwitchRow(c *ui.Context, id, label, desc string, v *bool, log func(string)) {
mujica.SettingsRow(c, label, mujica.SettingsRowOptions{ID: id, Description: desc}, func() *ui.Element {
sw := mujica.Switch(c, v, "", mujica.SwitchOptions{})
if sw.Changed() {
log(fmt.Sprintf("%s %v", label, *v))
}
return sw
})
}
func accountSettingsDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "461", func() accountSettingsState { return accountSettingsState{sound: true} })
accountSettingsPage(c, d, log)
}
func accountSearchDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "462", func() accountSettingsState { return accountSettingsState{sound: true} })
ui.Box(c).Width(320).Children(func() {
if e, ok := mujica.SettingsSearch(c, &d.query, accountSettingEntries, mujica.SettingsSearchOptions{}).Chosen(); ok {
d.page = e.Page
log("jump to " + e.ID)
}
})
accountSettingsPage(c, d, log)
}
type accountThemeState struct {
mode mujica.Mode
disabled bool
}
func accountThemeDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "463", func() accountThemeState { return accountThemeState{} })
accountToggles(c, log, []string{"Disabled"}, &d.disabled)
if mujica.ThemeSelector(c, &d.mode, mujica.ThemeSelectorOptions{Disabled: d.disabled}).Changed() {
log("theme " + d.mode.String())
}
}
type accountAccentState struct {
color ui.Color
disabled bool
}
func accountAccentDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "464", func() accountAccentState { return accountAccentState{color: ui.Hex("#7D2034")} })
accountToggles(c, log, []string{"Disabled"}, &d.disabled)
if mujica.AccentColorPicker(c, &d.color, mujica.AccentColorPickerOptions{Disabled: d.disabled}).Changed() {
log(fmt.Sprintf("accent %v", d.color))
}
mujica.Button(c, "Preview button", mujica.ButtonOptions{}).Background(d.color)
}
func accountToggles(c *ui.Context, log func(string), labels []string, values ...*bool) {
before := make([]bool, len(values))
for i, v := range values {
before[i] = *v
}
formToggles(c, labels, values...)
for i, v := range values {
if before[i] != *v {
log(fmt.Sprintf("%s %v", labels[i], *v))
}
}
}
+159
View File
@@ -0,0 +1,159 @@
package main
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
type accountRecorderState struct {
palette, zen mujica.Shortcut
disabled bool
}
var accountTaken = []mujica.ShortcutBinding{{Action: "Save", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}}
func accountRecorderDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "465", func() accountRecorderState {
return accountRecorderState{palette: mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}}
})
accountToggles(c, log, []string{"Disabled"}, &d.disabled)
ui.Text(c, "Cmd/Ctrl+S is taken by Save.")
for _, r := range []struct {
name string
v *mujica.Shortcut
}{{"Command palette", &d.palette}, {"Zen mode", &d.zen}} {
if mujica.ShortcutRecorder(c, r.v, mujica.ShortcutRecorderOptions{Label: r.name, Taken: accountTaken, Disabled: d.disabled}).Changed() {
log(fmt.Sprintf("%s = %s", r.name, r.v.String()))
}
}
}
type accountShortcutsState struct{ query string }
func accountShortcutsDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "466", func() accountShortcutsState { return accountShortcutsState{} })
groups := []mujica.ShortcutGroup{
{Title: "General", Bindings: []mujica.ShortcutBinding{
{Action: "Command palette", Shortcut: mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}},
{Action: "Settings", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeyComma}},
{Action: "Save", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}},
}},
{Title: "Editing", Bindings: []mujica.ShortcutBinding{
{Action: "Duplicate line", Shortcut: mujica.Shortcut{Mods: ui.Alt | ui.Shift, Key: ui.KeyDown}},
{Action: "Toggle comment", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeySlash}},
{Action: "Zen mode"},
}},
}
before := d.query
ui.Box(c).Width(480).Children(func() { mujica.KeyboardShortcutsList(c, &d.query, groups, mujica.KeyboardShortcutsListOptions{}) })
if d.query != before {
log(fmt.Sprintf("search %q", d.query))
}
}
type accountLoginState struct {
form mujica.LoginFormState
disabled, reject bool
attempts int
}
func accountLoginDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "467", func() accountLoginState { return accountLoginState{reject: true} })
done := *ui.Local(c.Root(), "467.done", func() chan string { return make(chan string, 1) })
select {
case e := <-done:
d.form.EndSubmit(e)
log(fmt.Sprintf("sign-in finished, error %q", e))
default:
}
accountToggles(c, log, []string{"Disabled", "Server rejects"}, &d.disabled, &d.reject)
mujica.LoginForm(c, &d.form, mujica.LoginFormOptions{Disabled: d.disabled, OnSubmit: func(account, password string, remember bool) {
// Credentials stay in the caller's sign-in request.
d.attempts++
log(fmt.Sprintf("submit %q, remember %v", account, remember))
fail := ""
switch {
case d.attempts >= 3:
fail = "Too many attempts. Try again in 5 minutes."
case d.reject:
fail = "Wrong account or password."
}
go func() {
time.Sleep(700 * time.Millisecond)
done <- fail
c.Invalidate()
}()
}})
}
type accountOAuthState struct {
pending string
disabled bool
}
func accountOAuthDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "468", func() accountOAuthState { return accountOAuthState{} })
accountToggles(c, log, []string{"Disabled"}, &d.disabled)
providers := []mujica.OAuthProvider{{ID: "git", Name: "Git host"}, {ID: "sso", Name: "Company SSO"}, {ID: "mail", Name: "Email link"}}
if id, ok := mujica.OAuthButtons(c, providers, mujica.OAuthButtonsOptions{Pending: d.pending, Disabled: d.disabled}).Chosen(); ok {
d.pending = id
log("continue with " + id)
}
if d.pending != "" && tradingAction(c, "Finish sign-in") {
log("sign-in with " + d.pending + " finished")
d.pending = ""
}
}
type accountTwoFactorState struct {
code string
resendAt time.Time
disabled bool
err string
}
func accountTwoFactorDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "469", func() accountTwoFactorState { return accountTwoFactorState{} })
accountToggles(c, log, []string{"Disabled"}, &d.disabled)
r := mujica.TwoFactorInput(c, &d.code, mujica.TwoFactorInputOptions{ResendAt: d.resendAt, Error: d.err, Disabled: d.disabled})
if r.Completed() {
// The code is checked here and never logged.
d.err = ""
if d.code != "123456" {
d.err = "That code is not right."
}
log(fmt.Sprintf("code complete, accepted %v", d.err == ""))
}
if r.Resend() {
d.resendAt, d.code, d.err = c.Now().Add(30*time.Second), "", ""
log("resend requested")
}
}
type accountProfileState struct {
saved, edit mujica.Profile
saving, disabled bool
}
func accountProfileDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "470", func() accountProfileState {
p := mujica.Profile{Name: "Sakiko Togawa", Bio: "Keyboard, Ave Mujica."}
return accountProfileState{saved: p, edit: p}
})
accountToggles(c, log, []string{"Saving", "Disabled"}, &d.saving, &d.disabled)
r := mujica.ProfileEditor(c, &d.edit, mujica.ProfileEditorOptions{Saved: d.saved, Saving: d.saving, Disabled: d.disabled})
if r.Save() {
d.saved = d.edit
log("saved")
}
if r.Discarded() {
log("discarded")
}
if r.ChangeAvatar() {
log("change picture")
}
}
+175
View File
@@ -0,0 +1,175 @@
package main
// accountCode contains the compiled Example bodies.
var accountCode = map[string]string{
"SettingsLayout": `page := ""
sound := true
pages := []mujica.SettingsPage{{ID: "general", Label: "General"}, {ID: "privacy", Label: "Privacy"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.SettingsLayout(c, &page, pages, mujica.SettingsLayoutOptions{}, func(id string) {
mujica.SettingsSection(c, "Notifications", func() {
mujica.SettingsRow(c, "Sound", mujica.SettingsRowOptions{ID: "sound"}, func() *ui.Element {
return mujica.Switch(c, &sound, "", mujica.SwitchOptions{})
})
})
})
}
ui.Render(view, 400, 300, 1)`,
"SettingsSearch": `query, page := "", "general"
entries := []mujica.SettingEntry{{ID: "sound", Page: "general", Label: "Sound", Keywords: []string{"audio"}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if e, ok := mujica.SettingsSearch(c, &query, entries, mujica.SettingsSearchOptions{}).Chosen(); ok {
page = e.Page
}
fmt.Println("page", page)
}
ui.Render(view, 400, 300, 1)`,
"ThemeSelector": `mode := mujica.System
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{Mode: mode})
mujica.ThemeSelector(c, &mode, mujica.ThemeSelectorOptions{})
}
ui.Render(view, 400, 300, 1)`,
"AccentColorPicker": `accent := ui.Hex("#7D2034")
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AccentColorPicker(c, &accent, mujica.AccentColorPickerOptions{})
}
ui.Render(view, 400, 300, 1)`,
"ShortcutRecorder": `palette := mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ShortcutRecorder(c, &palette, mujica.ShortcutRecorderOptions{
Label: "Command palette",
Taken: []mujica.ShortcutBinding{{Action: "Save", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}},
})
}
ui.Render(view, 400, 300, 1)`,
"KeyboardShortcutsList": `query := ""
groups := []mujica.ShortcutGroup{{Title: "General", Bindings: []mujica.ShortcutBinding{
{Action: "Command palette", Shortcut: mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}},
}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.KeyboardShortcutsList(c, &query, groups, mujica.KeyboardShortcutsListOptions{})
}
ui.Render(view, 400, 300, 1)`,
"LoginForm": `var state mujica.LoginFormState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.LoginForm(c, &state, mujica.LoginFormOptions{OnSubmit: func(account, password string, remember bool) {
// Submit credentials through the caller-provided auth service.
state.EndSubmit("")
}})
}
ui.Render(view, 400, 300, 1)`,
"OAuthButtons": `providers := []mujica.OAuthProvider{{ID: "sso", Name: "Company SSO"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if id, ok := mujica.OAuthButtons(c, providers, mujica.OAuthButtonsOptions{}).Chosen(); ok {
fmt.Println("start sign-in with", id)
}
}
ui.Render(view, 400, 300, 1)`,
"TwoFactorInput": `code := ""
resendAt := time.Now().Add(30 * time.Second)
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.TwoFactorInput(c, &code, mujica.TwoFactorInputOptions{ResendAt: resendAt})
if r.Completed() {
fmt.Println("verify the code")
}
}
ui.Render(view, 400, 300, 1)`,
"ProfileEditor": `saved := mujica.Profile{Name: "Sakiko", Bio: "Keyboard."}
profile := saved
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ProfileEditor(c, &profile, mujica.ProfileEditorOptions{Saved: saved}).Save() {
saved = profile
}
}
ui.Render(view, 400, 300, 1)`,
"AccountSwitcher": `current := "a"
accounts := []mujica.Account{{ID: "a", Name: "Sakiko Togawa", Email: "sakiko@court"}, {ID: "b", Name: "Oblivionis"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.AccountSwitcher(c, &current, accounts, mujica.AccountSwitcherOptions{}).Add() {
fmt.Println("sign in another account")
}
}
ui.Render(view, 400, 300, 1)`,
"WorkspaceSwitcher": `current := "ave"
spaces := []mujica.Workspace{{ID: "ave", Name: "Ave Mujica"}, {ID: "mygo", Name: "MyGO!!!!!"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.WorkspaceSwitcher(c, &current, spaces, mujica.WorkspaceSwitcherOptions{})
}
ui.Render(view, 400, 300, 1)`,
"UserMenu": `status := mujica.PresenceOnline
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.UserMenu(c, &status, mujica.UserMenuOptions{Name: "Sakiko Togawa"}).SignOut() {
fmt.Println("sign out")
}
}
ui.Render(view, 400, 300, 1)`,
"SessionList": `sessions := []mujica.Session{{ID: "s1", Device: "MacBook Pro", Current: true}, {ID: "s2", Device: "iPhone", Location: "Osaka"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if id, ok := mujica.SessionList(c, sessions, mujica.SessionListOptions{}).Revoked(); ok {
fmt.Println("sign out", id)
}
}
ui.Render(view, 400, 300, 1)`,
"ApiKeyManager": `var state mujica.ApiKeyManagerState
keys := []mujica.APIKey{{ID: "k1", Name: "Deploy bot", Prefix: "mk_live_9f2c"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if name, ok := mujica.ApiKeyManager(c, &state, keys, mujica.ApiKeyManagerOptions{}).Create(); ok {
fmt.Println("create", name)
state.Reveal("secret from your key service")
}
}
ui.Render(view, 400, 300, 1)`,
"UsageQuota": `items := []mujica.QuotaItem{{Label: "API calls", Used: 4200, Limit: 10000}, {Label: "Storage", Used: 9, Limit: 10, Unit: "GB"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.UsageQuota(c, items, mujica.UsageQuotaOptions{ResetAt: time.Now().Add(72 * time.Hour)})
}
ui.Render(view, 400, 300, 1)`,
"OnboardingWizard": `step := 0
steps := []mujica.WizardStep{{Title: "Welcome"}, {Title: "Pick a theme"}, {Title: "Done"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.OnboardingWizard(c, &step, steps, mujica.OnboardingWizardOptions{}).Finished() {
fmt.Println("onboarded")
}
}
ui.Render(view, 400, 300, 1)`,
"Coachmark": `open, step := true, 0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
compose := mujica.Button(c, "Compose", mujica.ButtonOptions{})
mujica.Coachmark(c, &open, &step, []mujica.CoachmarkStep{{Anchor: compose, Title: "Write here", Body: "Start a new letter."}})
}
ui.Render(view, 400, 300, 1)`,
"WhatsNewDialog": `open, hide := true, false
releases := []mujica.Release{{Version: "1.1", Changes: []mujica.ReleaseChange{{Kind: mujica.ChangeNew, Text: "500 components."}}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.WhatsNewDialog(c, &open, &hide, releases, mujica.WhatsNewDialogOptions{})
}
ui.Render(view, 400, 300, 1)`,
"FeedbackWidget": `var state mujica.FeedbackWidgetState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if f, ok := mujica.FeedbackWidget(c, &state, mujica.FeedbackWidgetOptions{}).Submitted(); ok {
fmt.Println("send", f.Kind)
state.EndSubmit("")
}
}
ui.Render(view, 400, 300, 1)`,
}
+276
View File
@@ -0,0 +1,276 @@
package main
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
type accountSwitchState struct {
account, space string
accounts []mujica.Account
disabled bool
}
func accountSwitcherDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "471", func() accountSwitchState {
return accountSwitchState{account: "a", accounts: []mujica.Account{
{ID: "a", Name: "Sakiko Togawa", Email: "sakiko@court"}, {ID: "b", Name: "Oblivionis", Email: "ob@mask"}}}
})
accountToggles(c, log, []string{"Disabled"}, &d.disabled)
r := mujica.AccountSwitcher(c, &d.account, d.accounts, mujica.AccountSwitcherOptions{Disabled: d.disabled})
if r.Changed() {
log("switched to " + d.account)
}
if r.Add() {
n := len(d.accounts) + 1
d.accounts = append(d.accounts, mujica.Account{ID: fmt.Sprint(n), Name: fmt.Sprintf("Guest %d", n)})
log("add account")
}
}
var accountSpaces = []mujica.Workspace{
{ID: "ave", Name: "Ave Mujica", Detail: "5 members"}, {ID: "mygo", Name: "MyGO!!!!!", Detail: "5 members"},
{ID: "crychic", Name: "CRYCHIC", Detail: "Archived"}, {ID: "sumimi", Name: "sumimi", Detail: "2 members"},
}
func accountWorkspaceDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "472", func() accountSwitchState { return accountSwitchState{space: "ave"} })
accountToggles(c, log, []string{"Disabled"}, &d.disabled)
if mujica.WorkspaceSwitcher(c, &d.space, accountSpaces, mujica.WorkspaceSwitcherOptions{Disabled: d.disabled}).Changed() {
log("workspace " + d.space)
}
}
type accountUserState struct {
status mujica.UserStatus
disabled bool
}
func accountUserMenuDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "473", func() accountUserState { return accountUserState{} })
accountToggles(c, log, []string{"Disabled"}, &d.disabled)
r := mujica.UserMenu(c, &d.status, mujica.UserMenuOptions{Name: "Sakiko Togawa", Email: "sakiko@court", Disabled: d.disabled})
if r.StatusChanged() {
log(fmt.Sprintf("status %d", d.status))
}
if r.Settings() {
log("open settings")
}
if r.SignOut() {
log("sign out")
}
}
type accountSessionsState struct {
sessions []mujica.Session
disabled bool
}
func accountSessionsDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "474", func() accountSessionsState {
return accountSessionsState{sessions: []mujica.Session{
{ID: "s1", Device: "MacBook Pro · Safari", Location: "Tokyo", LastActive: time.Now(), Current: true},
{ID: "s2", Device: "iPhone · App", Location: "Osaka", LastActive: time.Now().Add(-26 * time.Hour)},
{ID: "s3", Device: "Windows · Edge", Location: "Kyoto"},
}}
})
accountToggles(c, log, []string{"Disabled"}, &d.disabled)
r := mujica.SessionList(c, d.sessions, mujica.SessionListOptions{Disabled: d.disabled})
if id, ok := r.Revoked(); ok {
for i, s := range d.sessions {
if s.ID == id {
d.sessions = append(d.sessions[:i:i], d.sessions[i+1:]...)
break
}
}
log("signed out " + id)
}
if r.OthersRevoked() {
d.sessions = d.sessions[:1]
log("signed out every other session")
}
}
type accountKeysState struct {
state mujica.ApiKeyManagerState
keys []mujica.APIKey
disabled, reject bool
}
func accountKeysDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "475", func() accountKeysState {
return accountKeysState{keys: []mujica.APIKey{{ID: "k1", Name: "Deploy bot", Prefix: "mk_live_9f2c", Created: time.Now().AddDate(0, -1, 0)}}}
})
accountToggles(c, log, []string{"Disabled", "Create fails"}, &d.disabled, &d.reject)
r := mujica.ApiKeyManager(c, &d.state, d.keys, mujica.ApiKeyManagerOptions{Disabled: d.disabled})
if name, ok := r.Create(); ok {
if d.reject {
d.state.Fail("Key limit reached.")
log("create failed")
return
}
// The caller supplies the generated key.
n := len(d.keys) + 1
prefix := fmt.Sprintf("mk_live_%04x", n*7919)
d.keys = append(d.keys, mujica.APIKey{ID: fmt.Sprint("k", n), Name: name, Prefix: prefix, Created: time.Now()})
d.state.Reveal(fmt.Sprintf("%s_%x", prefix, time.Now().UnixNano()))
log("created key " + name)
}
if id, ok := r.Revoked(); ok {
for i, k := range d.keys {
if k.ID == id {
d.keys = append(d.keys[:i:i], d.keys[i+1:]...)
break
}
}
log("revoked " + id)
}
}
type accountQuotaState struct{ calls float64 }
func accountQuotaDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "476", func() accountQuotaState { return accountQuotaState{calls: 4200} })
if mujica.Slider(c, &d.calls, mujica.SliderOptions{Min: 0, Max: 12000, Step: 100, Label: "API calls used", ShowValue: true}).Changed() {
log(fmt.Sprintf("calls %.0f", d.calls))
}
ui.Box(c).Width(440).Children(func() {
mujica.UsageQuota(c, []mujica.QuotaItem{{Label: "API calls", Used: d.calls, Limit: 10000}, {Label: "Storage", Used: 9, Limit: 10, Unit: "GB"}},
mujica.UsageQuotaOptions{ResetAt: time.Now().Add(52 * time.Hour)})
})
}
type accountWizardState struct {
step int
theme mujica.Mode
name string
noSkip bool
}
func accountWizardDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "477", func() accountWizardState { return accountWizardState{} })
accountToggles(c, log, []string{"Hide Skip"}, &d.noSkip)
steps := []mujica.WizardStep{
{Title: "Welcome", Description: "Three short steps to set up your court."},
{Title: "Your name", Ready: func() bool { return d.name != "" }, Content: func() {
if mujica.TextInput(c, &d.name, mujica.TextInputOptions{Label: "Name", Placeholder: "Name"}).Changed() {
log("name changed")
}
}},
{Title: "Pick a theme", Content: func() {
if mujica.ThemeSelector(c, &d.theme, mujica.ThemeSelectorOptions{}).Changed() {
log("theme " + d.theme.String())
}
}},
}
from := d.step
r := mujica.OnboardingWizard(c, &d.step, steps, mujica.OnboardingWizardOptions{NoSkip: d.noSkip})
if d.step != from {
log(fmt.Sprintf("step %d", d.step+1))
}
if r.Finished() || r.Skipped() {
log(fmt.Sprintf("finished %v skipped %v", r.Finished(), r.Skipped()))
d.step = 0
}
}
type accountCoachState struct {
open, disabled bool
step int
}
func accountCoachDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "478", func() accountCoachState { return accountCoachState{} })
accountToggles(c, log, []string{"Disabled anchor"}, &d.disabled)
if tradingAction(c, "Start tour") {
d.open, d.step = true, 0
log("tour started")
}
var compose, archive *ui.Element
ui.Row(c).Gap(48).Padding(8).Children(func() {
compose = mujica.Button(c, "Compose", mujica.ButtonOptions{Disabled: d.disabled})
archive = mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary})
})
if compose.Clicked() {
log("compose")
}
if archive.Clicked() {
log("archive")
}
from := d.step
r := mujica.Coachmark(c, &d.open, &d.step, []mujica.CoachmarkStep{
{Anchor: compose, Title: "Write here", Body: "Start a new letter."},
{Anchor: archive, Title: "Keep it tidy", Body: "Archive threads you have finished."},
})
if d.step != from {
log(fmt.Sprintf("tip %d", d.step+1))
}
if r.Finished() {
log("tour finished")
}
if r.Dismissed() {
log("tour dismissed")
}
}
type accountWhatsNewState struct{ open, hide bool }
func accountWhatsNewDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "479", func() accountWhatsNewState { return accountWhatsNewState{} })
if tradingAction(c, "Show what's new") {
d.open = true
log("open")
}
releases := []mujica.Release{
{Version: "1.1", Date: "2026-10-05", Changes: []mujica.ReleaseChange{{Kind: mujica.ChangeNew, Text: "400 new components."}, {Kind: mujica.ChangeFixed, Text: "Snapshots no longer drift."}}},
{Version: "1.0", Date: "2026-09-01", Changes: []mujica.ReleaseChange{{Kind: mujica.ChangeImproved, Text: "Court-gothic theme."}}},
}
was := d.open
if mujica.WhatsNewDialog(c, &d.open, &d.hide, releases, mujica.WhatsNewDialogOptions{}).Changed() {
log(fmt.Sprintf("don't show again %v", d.hide))
}
if was && !d.open {
log("closed")
}
}
type accountFeedbackState struct {
state mujica.FeedbackWidgetState
disabled bool
}
func accountFeedbackDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "480", func() accountFeedbackState { return accountFeedbackState{} })
done := *ui.Local(c.Root(), "480.done", func() chan string { return make(chan string, 1) })
select {
case e := <-done:
d.state.EndSubmit(e)
log("feedback sent")
default:
}
accountToggles(c, log, []string{"Disabled"}, &d.disabled)
before := d.state.Draft
r := mujica.FeedbackWidget(c, &d.state, mujica.FeedbackWidgetOptions{Disabled: d.disabled, Attachments: func() {
if tradingAction(c, "Attach screenshot") {
log("attach screenshot")
}
}})
if before.Kind != d.state.Draft.Kind {
log("type changed")
}
if before.Text != d.state.Draft.Text {
log("message changed")
}
if f, ok := r.Submitted(); ok {
log(fmt.Sprintf("send kind %d, %d characters", f.Kind, len([]rune(f.Text))))
go func() {
time.Sleep(600 * time.Millisecond)
done <- ""
c.Invalidate()
}()
}
}
+102
View File
@@ -0,0 +1,102 @@
package main
import (
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"strings"
"testing"
)
func accountClick(t *testing.T, tt *ui.Tester, label string) {
t.Helper()
if err := tt.Click(label); err != nil {
t.Fatal(err)
}
}
func TestAccountPagesInteractiveEvents(t *testing.T) {
actions := map[string]func(*testing.T, *ui.Tester){
"461": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "Privacy") },
"462": func(t *testing.T, tt *ui.Tester) {
accountClick(t, tt, "Search settings")
tt.Type("audio")
tt.Key(0, ui.KeyEnter)
},
"463": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "Dark") },
"464": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "#355E80") },
"465": func(t *testing.T, tt *ui.Tester) {
value := mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}
accountClick(t, tt, "Command palette: "+value.String())
tt.Key(ui.Cmd, ui.KeyK)
},
"466": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "Search shortcuts"); tt.Type("Save") },
"467": func(t *testing.T, tt *ui.Tester) {
accountClick(t, tt, "Email or username")
tt.Type("test@court")
accountClick(t, tt, "Password")
tt.Type("test-secret-467")
accountClick(t, tt, "Sign in")
},
"468": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "Continue with Git host") },
"469": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "Verification code"); tt.Type("381942") },
"470": func(t *testing.T, tt *ui.Tester) {
accountClick(t, tt, "Name")
tt.Type(" edited")
accountClick(t, tt, "Save")
},
"471": func(t *testing.T, tt *ui.Tester) {
accountClick(t, tt, "Sakiko Togawa")
accountClick(t, tt, "Oblivionis · ob@mask")
},
"472": func(t *testing.T, tt *ui.Tester) {
accountClick(t, tt, "Ave Mujica")
tt.Type("arch")
tt.Key(0, ui.KeyEnter)
},
"473": func(t *testing.T, tt *ui.Tester) {
accountClick(t, tt, "Sakiko Togawa, Online")
accountClick(t, tt, "Away")
},
"474": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "Sign out of iPhone · App") },
"475": func(t *testing.T, tt *ui.Tester) {
accountClick(t, tt, "Create key")
tt.Type("CI")
tt.Key(0, ui.KeyEnter)
},
"476": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "API calls used"); tt.Key(0, ui.KeyRight) },
"477": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "Next") },
"478": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "Start tour"); accountClick(t, tt, "Next") },
"479": func(t *testing.T, tt *ui.Tester) {
accountClick(t, tt, "Show what's new")
accountClick(t, tt, "Don't show again")
},
"480": func(t *testing.T, tt *ui.Tester) {
accountClick(t, tt, "Message")
tt.Type("This is a feedback test.")
accountClick(t, tt, "Send feedback")
},
}
if len(accountPages) != 20 {
t.Fatalf("%d account pages", len(accountPages))
}
for _, p := range accountPages {
t.Run(p.ID, func(t *testing.T) {
if p.Category != "Settings & Account" || p.Code == "" || p.Keys == "" {
t.Fatal("incomplete gallery metadata")
}
var events []string
tt := ui.NewTester(func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
ui.Column(c).Padding(20).Gap(16).Children(func() { p.Demo(c, func(s string) { events = append(events, s) }) })
}, 1100, 800)
actions[p.ID](t, tt)
if len(events) == 0 {
t.Fatal("interaction has no gallery event")
}
logs := strings.Join(events, "\n")
if strings.Contains(logs, "test-secret-467") || strings.Contains(logs, "381942") {
t.Fatal("gallery event exposes a secret")
}
})
}
}