layout: ikuti mock SVG — kolom tengah dashboard, toolbar workspace, kolom Aksi Tab
CI / test (push) Failing after 38s
CI / test (push) Failing after 38s
- Beranda: kolom tengah CTA 'masuk workspace' (klikable), strip Disclaimer dan Info aplikasi (versi/repo/config); geometri satu sumber (homeGeom). - Workspace: baris toolbar — pill Cari (klik atau '/' → cari-lompat daftar) + tombol [+ stack]/[konversi] klikable (sementara keduanya jalur konverter). - Detail kontainer: kolom kanan 'Aksi Tab' (mock TAB ACTION) berisi aksi tab aktif yang bisa diklik; disembunyikan di layar sempit agar tab bar utuh. - Footer: hint aksi diprioritaskan; status diagnostik dibuang bila tak muat. - internal/version: versi build dipakai CLI & strip Info aplikasi. - audio: Play/Status aman untuk Engine nil (Model zero-value di tes). - Tes: 3 skenario E2E baru (CTA, cari toolbar, kolom aksi) + tes layout.
This commit is contained in:
1 parent
8880660ce4
commit
5450a89a80
9 files changed
+638
-112
No files matched your search
+132
-13
@@ -2,31 +2,48 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/audio"
|
||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/config"
|
||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/i18n"
|
||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/ui/components"
|
||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/ui/theme"
|
||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/version"
|
||||
)
|
||||
|
||||
// homeBody — dashboard beranda (F-027). Tata letak terinspirasi mirador:
|
||||
// grid panel dengan judul di border; baris atas Jam · Docker · Host,
|
||||
// baris bawah Kontainer · Aktivitas.
|
||||
func (m Model) homeBody() string {
|
||||
W := max(m.width, 60)
|
||||
// homeGeom — geometri dashboard (satu sumber untuk render & hit-test klik).
|
||||
// Baris: 3 kartu atas · area bawah (Kontainer | CTA | Aktivitas) · strip bawah.
|
||||
func (m Model) homeGeom() (W, row1H, stripH, row2H, cw, k1, k2 int) {
|
||||
W = max(m.width, 60)
|
||||
bodyH := m.bodyHeight()
|
||||
|
||||
row1H := 9
|
||||
row2H := bodyH - row1H
|
||||
row1H, stripH = 9, 5
|
||||
row2H = bodyH - row1H - stripH
|
||||
if row2H < 5 {
|
||||
row1H = 6
|
||||
row2H = max(bodyH-row1H, 3)
|
||||
row1H, stripH = 6, 4
|
||||
row2H = max(bodyH-row1H-stripH, 3)
|
||||
}
|
||||
|
||||
cw = min(max(int(float64(W)*0.20), 18), 26) // kolom tengah: CTA masuk workspace
|
||||
k1 = (W - cw) / 2
|
||||
k2 = W - cw - k1
|
||||
return
|
||||
}
|
||||
|
||||
// homeBody — dashboard beranda (F-027). Tata letak terinspirasi mirador dengan
|
||||
// kolom tengah CTA ala mock pengguna: kartu Jam · Engine · Host di atas,
|
||||
// Kontainer | CTA | Aktivitas di tengah, strip Disclaimer & Info aplikasi di bawah.
|
||||
func (m Model) homeBody() string {
|
||||
W, row1H, stripH, row2H, cw, k1, k2 := m.homeGeom()
|
||||
|
||||
w1 := int(float64(W) * 0.36)
|
||||
w2 := int(float64(W) * 0.30)
|
||||
w3 := W - w1 - w2
|
||||
@@ -37,16 +54,118 @@ func (m Model) homeBody() string {
|
||||
m.hostPanel(w3, row1H),
|
||||
)
|
||||
|
||||
k1 := int(float64(W) * 0.55)
|
||||
k2 := W - k1
|
||||
row2 := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
left := lipgloss.JoinVertical(lipgloss.Left,
|
||||
panel(i18n.T("section.containers"), i18n.Tf("fmt.countTotal", len(m.containers)),
|
||||
m.miniContainerLines(row2H-2), k1, row2H, false),
|
||||
m.disclaimerPanel(k1, stripH),
|
||||
)
|
||||
mid, _, _ := m.centerColumn(cw, row2H+stripH)
|
||||
right := lipgloss.JoinVertical(lipgloss.Left,
|
||||
panel(i18n.T("section.activity"), fmt.Sprintf("(%d)", len(m.activity)),
|
||||
m.activityLines(k2-4, row2H-2), k2, row2H, false),
|
||||
m.appsPanel(k2, stripH),
|
||||
)
|
||||
|
||||
return row1 + "\n" + row2
|
||||
return row1 + "\n" + lipgloss.JoinHorizontal(lipgloss.Top, left, strings.Join(mid, "\n"), right)
|
||||
}
|
||||
|
||||
// centerColumn — kolom tengah: kotak CTA "masuk workspace" (klikable; paritas
|
||||
// dengan tombol yang tampil). Kembalikan juga posisi kotak untuk hit-test.
|
||||
func (m Model) centerColumn(w, h int) (lines []string, ctaTop, ctaH int) {
|
||||
th := theme.Current()
|
||||
border := theme.Style(th.Accent)
|
||||
label := theme.Style(th.Accent).Bold(true)
|
||||
|
||||
inner := max(w-4, 8)
|
||||
rows := wrapText(i18n.T("home.enterWorkspace"), inner)
|
||||
ctaH = len(rows) + 2
|
||||
box := []string{border.Render("╭" + strings.Repeat("─", w-2) + "╮")}
|
||||
for _, r := range rows {
|
||||
pad := inner - lipgloss.Width(r)
|
||||
l := max(pad/2, 0)
|
||||
box = append(box, border.Render("│")+" "+strings.Repeat(" ", l)+
|
||||
label.Render(r)+strings.Repeat(" ", max(pad-l, 0))+" "+border.Render("│"))
|
||||
}
|
||||
box = append(box, border.Render("╰"+strings.Repeat("─", w-2)+"╯"))
|
||||
|
||||
ctaTop = max((h-ctaH)/2, 0)
|
||||
lines = make([]string, 0, h)
|
||||
for i := 0; i < h; i++ {
|
||||
idx := i - ctaTop
|
||||
switch {
|
||||
case idx >= 0 && idx < ctaH:
|
||||
lines = append(lines, box[idx])
|
||||
default:
|
||||
lines = append(lines, strings.Repeat(" ", w))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// disclaimerPanel — strip peringatan aksi destruktif.
|
||||
func (m Model) disclaimerPanel(w, h int) string {
|
||||
rows := wrapText(i18n.T("home.disclaimer"), max(w-4, 8))
|
||||
if len(rows) > h-2 {
|
||||
rows = rows[:h-2]
|
||||
}
|
||||
return panel(i18n.T("section.disclaimer"), "", rows, w, h, false)
|
||||
}
|
||||
|
||||
const repoURL = "gitea.oxpoe.qzz.io/oxpoe/tia-love"
|
||||
|
||||
// appsPanel — strip info aplikasi: versi, repo, lokasi config.
|
||||
func (m Model) appsPanel(w, h int) string {
|
||||
rows := []string{
|
||||
fieldW(i18n.T("field.version"), 7) + dim(truncate(version.String(), w-9)),
|
||||
fieldW(i18n.T("field.repo"), 7) + dim(truncate(repoURL, w-9)),
|
||||
fieldW(i18n.T("field.config"), 7) + dim(truncate(prettyPath(config.Path()), w-9)),
|
||||
}
|
||||
if len(rows) > h-2 {
|
||||
rows = rows[:h-2]
|
||||
}
|
||||
return panel(i18n.T("section.appsInfo"), "", rows, w, h, false)
|
||||
}
|
||||
|
||||
// prettyPath — tampilkan path config dengan "~" untuk home pengguna.
|
||||
func prettyPath(p string) string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil && home != "" && strings.HasPrefix(p, home+"/") {
|
||||
return "~" + strings.TrimPrefix(p, home)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// homeClick — panel beranda: baris kontainer → buka workspace pada item itu;
|
||||
// panel Engine → bagian kontainer; kotak CTA → workspace (Stacks).
|
||||
func (m *Model) homeClick(x, y int) tea.Cmd {
|
||||
W, row1H, stripH, row2H, cw, k1, _ := m.homeGeom()
|
||||
w1 := int(float64(W) * 0.36)
|
||||
w2 := int(float64(W) * 0.30)
|
||||
|
||||
top := m.headerRows() + row1H // baris pertama area bawah
|
||||
first := top + 1 // konten baris pertama (daftar kontainer)
|
||||
|
||||
_, ctaTop, ctaH := m.centerColumn(cw, row2H+stripH)
|
||||
switch {
|
||||
case y >= first && y < first+max(row2H-2, 0) && x < k1:
|
||||
idx := y - first
|
||||
if len(m.containers) == 0 || idx >= len(m.containers) {
|
||||
return nil
|
||||
}
|
||||
m.jumpSection(sectionContainers)
|
||||
m.setCur(idx)
|
||||
m.sound.Play(audio.Click)
|
||||
return m.restartData()
|
||||
case y >= m.headerRows()+1 && y < m.headerRows()+row1H && x >= w1 && x < w1+w2:
|
||||
m.jumpSection(sectionContainers)
|
||||
m.sound.Play(audio.Click)
|
||||
return m.restartData()
|
||||
case y >= top+ctaTop && y < top+ctaTop+ctaH && x >= k1 && x < k1+cw:
|
||||
m.jumpSection(sectionStacks)
|
||||
m.sound.Play(audio.Click)
|
||||
return m.restartData()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) clockPanel(w, h int) string {
|
||||
|
||||
Reference in new issue
Block a user