layout: subjudul bar workspace jadi tautan seksi (klik = pindah seksi)
CI / test (push) Successful in 28s
CI / test (push) Successful in 28s
Nama seksi di subjudul (stack, kontainer, image, volume, jaringan) kini bergaya aksen dan bisa diklik → pindah seksi, paritas tombol 1-5. Rentang kolom tiap nama dihitung dari satu sumber (workspaceSubsections) sehingga render & hit-test tidak bisa melenceng; layar sempit memotong nama yang tidak muat. Tes: unit (span → seksi, batas sempit) + e2e (klik 'image').
This commit is contained in:
1 parent
75ead4be8a
commit
7af6d48a80
3 files changed
+104
-18
No files matched your search
@@ -731,6 +731,12 @@ func TestWorkspaceBar(t *testing.T) {
|
||||
u.waitContains("Konversi docker run", 3*time.Second) // modal terbuka
|
||||
u.send("\x03") // Ctrl+C = tutup modal (bukan keluar)
|
||||
u.waitContains(sub, 3*time.Second)
|
||||
|
||||
// Tautan seksi di subjudul: klik "image" → pindah ke seksi Image.
|
||||
if !u.clickText("image") {
|
||||
t.Fatal("tautan seksi 'image' tidak ditemukan di subjudul")
|
||||
}
|
||||
u.waitContains("Image", 3*time.Second) // judul panel sidebar = seksi Image
|
||||
u.send("q")
|
||||
if code := u.waitExit(5 * time.Second); code != 0 {
|
||||
t.Fatalf("exit code %d, mau 0", code)
|
||||
|
||||
+67
-17
@@ -1056,23 +1056,42 @@ func (m *Model) mouseUI(x, y int) (tea.Cmd, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// workspaceSubsections — subjudul bar = daftar seksi workspace, plus rentang
|
||||
// kolom (rune) tiap nama untuk klik → pindah seksi.
|
||||
func (m Model) workspaceSubsections() (sub string, spans [][2]int) {
|
||||
keys := []string{"stacks", "containers", "images", "volumes", "networks"}
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
sub += ", "
|
||||
}
|
||||
name := strings.ToLower(i18n.T("section." + k))
|
||||
start := len([]rune(sub))
|
||||
sub += name
|
||||
spans = append(spans, [2]int{start, start + len([]rune(name))})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// workspaceBarParts — potongan bar atas workspace (mock draf-tia): subjudul
|
||||
// seksi, tombol kanan, pill cari. Satu sumber untuk render & hit-test klik.
|
||||
func (m Model) workspaceBarParts() (sub, right, search string, gap int) {
|
||||
func (m Model) workspaceBarParts() (sub string, spans [][2]int, right, search string, gap int) {
|
||||
w := max(m.width-4, 10)
|
||||
names := []string{
|
||||
i18n.T("section.stacks"), i18n.T("section.containers"), i18n.T("section.images"),
|
||||
i18n.T("section.volumes"), i18n.T("section.networks"),
|
||||
}
|
||||
for i, n := range names {
|
||||
names[i] = strings.ToLower(n)
|
||||
}
|
||||
sub = strings.Join(names, ", ")
|
||||
sub, spans = m.workspaceSubsections()
|
||||
right = "[ " + i18n.T("toolbar.addStack") + " ] [ " + i18n.T("toolbar.convert") + " ]"
|
||||
gap = w - lipgloss.Width(sub) - lipgloss.Width(right)
|
||||
if gap < 2 {
|
||||
gap = 2
|
||||
sub = truncate(sub, max(w-lipgloss.Width(right)-2, 4))
|
||||
limit := max(w-lipgloss.Width(right)-2, 4)
|
||||
if limit < len([]rune(sub)) {
|
||||
sub = truncate(sub, limit)
|
||||
cut, kept := len([]rune(sub)), spans[:0]
|
||||
for _, sp := range spans {
|
||||
if sp[0] < cut { // nama yang terpotong tetap bisa diklik sebatas yang tampak
|
||||
kept = append(kept, [2]int{sp[0], min(sp[1], cut)})
|
||||
}
|
||||
}
|
||||
spans = kept
|
||||
}
|
||||
}
|
||||
search = "/ " + i18n.T("toolbar.search")
|
||||
if m.searching {
|
||||
@@ -1083,30 +1102,61 @@ func (m Model) workspaceBarParts() (sub, right, search string, gap int) {
|
||||
|
||||
// workspaceBarPlain — bar tanpa gaya warna (sumber koordinat hit-test).
|
||||
func (m Model) workspaceBarPlain() (subLine, searchLine string) {
|
||||
sub, right, search, gap := m.workspaceBarParts()
|
||||
sub, _, right, search, gap := m.workspaceBarParts()
|
||||
return sub + strings.Repeat(" ", gap) + right, "[ " + search + " ]"
|
||||
}
|
||||
|
||||
// workspaceBar — bar atas workspace: judul di border, subjudul seksi (kiri)
|
||||
// + tombol stack/konversi (kanan), pill cari di baris sendiri.
|
||||
// workspaceSectionAt — kolom x (relatif isi bar) → seksi di subjudul.
|
||||
func (m Model) workspaceSectionAt(x int) (section, bool) {
|
||||
_, spans, _, _, _ := m.workspaceBarParts()
|
||||
for i, sp := range spans {
|
||||
if x >= sp[0] && x < sp[1] {
|
||||
return section(i), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// workspaceBar — bar atas workspace: judul di border, subjudul seksi (nama
|
||||
// seksi = tautan, klik = pindah seksi) + tombol stack/konversi, pill cari.
|
||||
func (m Model) workspaceBar() string {
|
||||
th := theme.Current()
|
||||
sub, right, search, gap := m.workspaceBarParts()
|
||||
sub, spans, right, search, gap := m.workspaceBarParts()
|
||||
|
||||
runes := []rune(sub)
|
||||
var b strings.Builder
|
||||
prev := 0
|
||||
for _, sp := range spans {
|
||||
b.WriteString(dim(string(runes[prev:sp[0]])))
|
||||
b.WriteString(theme.Style(th.Accent).Render(string(runes[sp[0]:sp[1]])))
|
||||
prev = sp[1]
|
||||
}
|
||||
b.WriteString(dim(string(runes[prev:])))
|
||||
|
||||
rows := []string{
|
||||
dim(sub) + strings.Repeat(" ", gap) + theme.Style(th.Accent).Render(right),
|
||||
b.String() + strings.Repeat(" ", gap) + theme.Style(th.Accent).Render(right),
|
||||
theme.Style(th.Accent).Render("[ " + search + " ]"),
|
||||
}
|
||||
return panel(i18n.T("workspace.title"), "", rows, m.width, 4, false)
|
||||
}
|
||||
|
||||
// workspaceBarKeyAt — klik di bar workspace: pill Cari / tombol stack.
|
||||
// workspaceBarKeyAt — klik di bar workspace: nama seksi, pill Cari, tombol stack.
|
||||
func (m *Model) workspaceBarKeyAt(x, y int) tea.Cmd {
|
||||
x -= 2 // bar dalam panel: konten mulai kolom 2
|
||||
sub, search := m.workspaceBarPlain()
|
||||
if y == m.headerRows()-3 {
|
||||
if s, ok := m.workspaceSectionAt(x); ok {
|
||||
m.jumpSection(s)
|
||||
m.searching, m.query = false, ""
|
||||
m.sound.Play(audio.Click)
|
||||
return m.restartData()
|
||||
}
|
||||
}
|
||||
line := sub
|
||||
if y == m.headerRows()-2 {
|
||||
line = search
|
||||
}
|
||||
label, ok := labelZoneAt(line, x-2) // bar dalam panel: konten mulai kolom 2
|
||||
label, ok := labelZoneAt(line, x)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestWorkspaceBar(t *testing.T) {
|
||||
t.Fatalf("w=%d: query tidak tampil di pill: %q", width, searchLine)
|
||||
}
|
||||
}
|
||||
// Subjudul = daftar seksi (mock): "stack, kontainer, image, volume, network".
|
||||
// Subjudul = daftar seksi (mock): "stack, kontainer, image, volume, jaringan".
|
||||
m := Model{width: 120, view: viewList}
|
||||
subLine, _ := m.workspaceBarPlain()
|
||||
want := strings.ToLower(i18n.T("section.stacks")) + ", " +
|
||||
@@ -62,6 +62,36 @@ func TestWorkspaceBar(t *testing.T) {
|
||||
if !strings.HasPrefix(subLine, want) {
|
||||
t.Fatalf("subjudul bar salah: %q (mau %q)", subLine, want)
|
||||
}
|
||||
|
||||
// Tautan seksi: tiap span menunjuk nama seksi yang benar & bisa diklik.
|
||||
keys := []string{"stacks", "containers", "images", "volumes", "networks"}
|
||||
sub, spans := m.workspaceSubsections()
|
||||
runes := []rune(sub)
|
||||
if len(spans) != len(keys) {
|
||||
t.Fatalf("jumlah span = %d, mau %d", len(spans), len(keys))
|
||||
}
|
||||
for i, sp := range spans {
|
||||
got, wantName := string(runes[sp[0]:sp[1]]), strings.ToLower(i18n.T("section."+keys[i]))
|
||||
if got != wantName {
|
||||
t.Fatalf("span %d = %q, mau %q", i, got, wantName)
|
||||
}
|
||||
if s, ok := m.workspaceSectionAt(sp[0]); !ok || int(s) != i {
|
||||
t.Fatalf("klik kolom %d harus → seksi %d (ok=%v s=%d)", sp[0], i, ok, s)
|
||||
}
|
||||
}
|
||||
if _, ok := m.workspaceSectionAt(len(runes) + 5); ok {
|
||||
t.Fatal("kolom di luar subjudul tidak boleh memetakan seksi")
|
||||
}
|
||||
|
||||
// Layar sempit: span tetap di dalam subjudul yang tampak.
|
||||
narrow := Model{width: 40, view: viewList}
|
||||
subNarrow, spansNarrow, _, _, _ := narrow.workspaceBarParts()
|
||||
limit := len([]rune(subNarrow))
|
||||
for _, sp := range spansNarrow {
|
||||
if sp[0] < 0 || sp[1] > limit {
|
||||
t.Fatalf("span %v di luar subjudul tampak (%d kolom)", sp, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPanelTitleFits — judul/meta panjang tidak boleh melebihi lebar panel:
|
||||
|
||||
Reference in new issue
Block a user