Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd66dbf7d8 | ||
|
|
6bb812f6bd | ||
|
|
d3098a9400 | ||
|
|
5de04af36a | ||
|
|
36cb03961c | ||
|
|
5e34ce7a61 |
@@ -97,3 +97,24 @@ jobs:
|
||||
|
||||
- name: build
|
||||
run: pnpm -C web build
|
||||
|
||||
# The flake pins pnpmDeps.hash against web/pnpm-lock.yaml (#98). A lockfile
|
||||
# change invalidates the pin silently elsewhere, so the packaged build has to
|
||||
# fail here too.
|
||||
flake:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install nix
|
||||
run: |
|
||||
curl -fsSL https://nixos.org/nix/install | sh -s -- --daemon --yes
|
||||
echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: build flake packages
|
||||
run: |
|
||||
nix build \
|
||||
.#webFrontend \
|
||||
.#maestro \
|
||||
.#maestro-web \
|
||||
--no-link
|
||||
|
||||
@@ -1349,9 +1349,7 @@ mod tests {
|
||||
for c in "crimson-otter".chars() {
|
||||
app.handle_draft_input(&InputKey::char(c));
|
||||
}
|
||||
app.handle_draft_input(&InputKey::Tab); // → worktree toggle
|
||||
app.handle_draft_input(&InputKey::Tab); // → branch mode
|
||||
app.handle_draft_input(&InputKey::Tab); // → new-branch name
|
||||
app.handle_draft_input(&InputKey::Tab); // → advanced disclosure (collapsed, #93)
|
||||
app.handle_draft_input(&InputKey::Tab); // → submit
|
||||
|
||||
match app.handle_draft_input(&InputKey::Enter).as_slice() {
|
||||
|
||||
+184
-33
@@ -57,6 +57,9 @@ pub enum DraftField {
|
||||
/// Free-text path, revealed by picking `Other directory…`.
|
||||
CwdRaw,
|
||||
Name,
|
||||
/// Disclosure collapsing the worktree/branch controls below (#93).
|
||||
/// Closed by default; Enter / Space toggles.
|
||||
Advanced,
|
||||
/// Worktree on/off for this session (#65). Enter / Space toggles.
|
||||
WorktreeToggle,
|
||||
/// New-vs-existing branch source; Left / Right switches.
|
||||
@@ -148,6 +151,10 @@ pub struct DraftPane {
|
||||
/// Worktree on for this spawn (#65). Defaults on; a failed gitroot probe
|
||||
/// unticks it, like the web form.
|
||||
pub worktree_enabled: bool,
|
||||
/// Whether the worktree/branch controls are collapsed behind the
|
||||
/// "Advanced options" disclosure (#93). Closed by default — most spawns
|
||||
/// take the default target.
|
||||
pub advanced_collapsed: bool,
|
||||
/// Enclosing repository of the chosen target — from the project scan for
|
||||
/// picked rows, `/api/gitroot` for raw paths. `None` until known.
|
||||
pub base_repo: Option<PathBuf>,
|
||||
@@ -204,6 +211,7 @@ impl Default for DraftPane {
|
||||
name_touched: false,
|
||||
needs_name_refill: false,
|
||||
worktree_enabled: true,
|
||||
advanced_collapsed: true,
|
||||
base_repo: None,
|
||||
probed_cwd: None,
|
||||
probe_seq: 0,
|
||||
@@ -442,6 +450,21 @@ impl DraftPane {
|
||||
self.set_branch_mode(next);
|
||||
}
|
||||
|
||||
/// Enter / Space on the "Advanced options" header (#93).
|
||||
pub fn toggle_advanced(&mut self) {
|
||||
self.advanced_collapsed = !self.advanced_collapsed;
|
||||
}
|
||||
|
||||
/// What the collapsed header says about this spawn's default, so it stays
|
||||
/// visible without expanding (#93).
|
||||
fn advanced_summary(&self) -> &'static str {
|
||||
if self.worktree_enabled {
|
||||
"creates a worktree"
|
||||
} else {
|
||||
"no worktree"
|
||||
}
|
||||
}
|
||||
|
||||
/// Enter / Space on the toggle. Re-enabling reuses a still-valid repo
|
||||
/// answer instead of re-probing.
|
||||
pub fn toggle_worktree(&mut self) {
|
||||
@@ -602,13 +625,16 @@ impl DraftPane {
|
||||
order.push(DraftField::CwdRaw);
|
||||
}
|
||||
order.push(DraftField::Name);
|
||||
order.push(DraftField::WorktreeToggle);
|
||||
if self.worktree_body_visible() {
|
||||
order.push(DraftField::BranchMode);
|
||||
order.push(match self.branch_mode {
|
||||
WorktreeBranchMode::New => DraftField::BranchNew,
|
||||
WorktreeBranchMode::Existing => DraftField::BranchExisting,
|
||||
});
|
||||
order.push(DraftField::Advanced);
|
||||
if !self.advanced_collapsed {
|
||||
order.push(DraftField::WorktreeToggle);
|
||||
if self.worktree_body_visible() {
|
||||
order.push(DraftField::BranchMode);
|
||||
order.push(match self.branch_mode {
|
||||
WorktreeBranchMode::New => DraftField::BranchNew,
|
||||
WorktreeBranchMode::Existing => DraftField::BranchExisting,
|
||||
});
|
||||
}
|
||||
}
|
||||
order.extend([DraftField::Submit]);
|
||||
order
|
||||
@@ -733,6 +759,10 @@ impl DraftPane {
|
||||
self.select_entry();
|
||||
DraftInputResult::Consumed
|
||||
}
|
||||
DraftField::Advanced => {
|
||||
self.toggle_advanced();
|
||||
DraftInputResult::Consumed
|
||||
}
|
||||
DraftField::WorktreeToggle => {
|
||||
self.toggle_worktree();
|
||||
DraftInputResult::Consumed
|
||||
@@ -744,6 +774,14 @@ impl DraftPane {
|
||||
DraftInputResult::Consumed
|
||||
}
|
||||
},
|
||||
InputKey::Char {
|
||||
c: ' ',
|
||||
ctrl: false,
|
||||
..
|
||||
} if self.focused_field == DraftField::Advanced => {
|
||||
self.toggle_advanced();
|
||||
DraftInputResult::Consumed
|
||||
}
|
||||
InputKey::Char {
|
||||
c: ' ',
|
||||
ctrl: false,
|
||||
@@ -832,13 +870,18 @@ impl DraftPane {
|
||||
|
||||
let show_custom = self.is_custom();
|
||||
let show_raw = self.cwd_raw_visible;
|
||||
let show_wt_body = self.worktree_body_visible();
|
||||
// The worktree toggle and its body stay behind the "Advanced options"
|
||||
// disclosure (#93): collapsed hides both, regardless of what they'd
|
||||
// otherwise show.
|
||||
let show_wt_toggle = !self.advanced_collapsed;
|
||||
let show_wt_body = show_wt_toggle && self.worktree_body_visible();
|
||||
// Up to three branch rows stay visible around the selection; held
|
||||
// branches need their reason on screen, so they aren't collapsed.
|
||||
let branch_rows = self.branches.len().clamp(1, 3) as u16;
|
||||
let fixed: u16 = 3 /* tiles + dir + name */ + 1 /* worktree */ + 1 /* spawn */
|
||||
let fixed: u16 = 3 /* tiles + dir + name */ + 1 /* advanced header */ + 1 /* spawn */
|
||||
+ u16::from(show_custom)
|
||||
+ u16::from(show_raw)
|
||||
+ u16::from(show_wt_toggle)
|
||||
+ u16::from(show_wt_body) * (1 + if self.branch_mode == WorktreeBranchMode::New { 1 } else { branch_rows })
|
||||
+ u16::from(self.error.is_some());
|
||||
let list_capacity = (inner.height.saturating_sub(fixed)).max(1) as usize;
|
||||
@@ -853,7 +896,10 @@ impl DraftPane {
|
||||
constraints.push(Constraint::Length(1));
|
||||
}
|
||||
constraints.push(Constraint::Length(1)); // name
|
||||
constraints.push(Constraint::Length(1)); // worktree toggle
|
||||
constraints.push(Constraint::Length(1)); // advanced header
|
||||
if show_wt_toggle {
|
||||
constraints.push(Constraint::Length(1)); // worktree toggle
|
||||
}
|
||||
if show_wt_body {
|
||||
constraints.push(Constraint::Length(1)); // branch mode
|
||||
match self.branch_mode {
|
||||
@@ -1030,33 +1076,55 @@ impl DraftPane {
|
||||
},
|
||||
);
|
||||
|
||||
// -- advanced disclosure (#93) ------------------------------------------
|
||||
let advanced_row = next_row();
|
||||
f.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::raw(format!("{:<GUTTER$}", "adv:")),
|
||||
Span::styled(
|
||||
format!(
|
||||
"[{}] Advanced options",
|
||||
if self.advanced_collapsed { "+" } else { "-" }
|
||||
),
|
||||
field_style(DraftField::Advanced),
|
||||
),
|
||||
Span::styled(
|
||||
format!(" — {}", self.advanced_summary()),
|
||||
Style::default().add_modifier(Modifier::DIM),
|
||||
),
|
||||
])),
|
||||
advanced_row,
|
||||
);
|
||||
|
||||
// -- worktree (#65) -----------------------------------------------------
|
||||
let wt_row = next_row();
|
||||
let hint = if self.cwd().is_none() {
|
||||
Some("choose a directory first")
|
||||
} else if self.repo_missing {
|
||||
Some("not inside a git repository")
|
||||
} else if !self.worktree_enabled {
|
||||
Some("off — spawns in the checkout itself")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut tiles = vec![Span::raw(format!("{:<GUTTER$}", "wt:"))];
|
||||
tiles.push(Span::styled(
|
||||
if self.worktree_enabled {
|
||||
"[x] worktree"
|
||||
if show_wt_toggle {
|
||||
let wt_row = next_row();
|
||||
let hint = if self.cwd().is_none() {
|
||||
Some("choose a directory first")
|
||||
} else if self.repo_missing {
|
||||
Some("not inside a git repository")
|
||||
} else if !self.worktree_enabled {
|
||||
Some("off — spawns in the checkout itself")
|
||||
} else {
|
||||
"[ ] worktree"
|
||||
},
|
||||
field_style(DraftField::WorktreeToggle),
|
||||
));
|
||||
if let Some(hint) = hint {
|
||||
None
|
||||
};
|
||||
let mut tiles = vec![Span::raw(format!("{:<GUTTER$}", "wt:"))];
|
||||
tiles.push(Span::styled(
|
||||
format!(" {hint}"),
|
||||
Style::default().add_modifier(Modifier::DIM),
|
||||
if self.worktree_enabled {
|
||||
"[x] worktree"
|
||||
} else {
|
||||
"[ ] worktree"
|
||||
},
|
||||
field_style(DraftField::WorktreeToggle),
|
||||
));
|
||||
if let Some(hint) = hint {
|
||||
tiles.push(Span::styled(
|
||||
format!(" {hint}"),
|
||||
Style::default().add_modifier(Modifier::DIM),
|
||||
));
|
||||
}
|
||||
f.render_widget(Paragraph::new(Line::from(tiles)), wt_row);
|
||||
}
|
||||
f.render_widget(Paragraph::new(Line::from(tiles)), wt_row);
|
||||
|
||||
if show_wt_body {
|
||||
// branch source: new (created) vs existing (checked out as-is)
|
||||
@@ -1894,7 +1962,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn held_branches_render_unavailable_with_their_holder_as_the_reason() {
|
||||
let mut draft = DraftPane::default();
|
||||
let mut draft = DraftPane {
|
||||
advanced_collapsed: false,
|
||||
..DraftPane::default()
|
||||
};
|
||||
draft.set_projects(
|
||||
vec![repo_project(
|
||||
"maestro",
|
||||
@@ -2069,6 +2140,7 @@ mod tests {
|
||||
fn render_shows_the_worktree_section() {
|
||||
let mut draft = DraftPane {
|
||||
home_dir: "/home/u".into(),
|
||||
advanced_collapsed: false,
|
||||
..DraftPane::default()
|
||||
};
|
||||
draft.set_projects(
|
||||
@@ -2107,6 +2179,85 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_section_is_collapsed_by_default_and_states_the_worktree_default() {
|
||||
let mut draft = DraftPane {
|
||||
home_dir: "/home/u".into(),
|
||||
..DraftPane::default()
|
||||
};
|
||||
draft.set_projects(
|
||||
vec![project("maestro", "/home/u/tea/maestro", 1, Some("claude"))],
|
||||
Utc::now(),
|
||||
);
|
||||
assert!(draft.advanced_collapsed, "closed by default");
|
||||
assert_eq!(
|
||||
draft
|
||||
.field_order()
|
||||
.iter()
|
||||
.filter(|f| matches!(f, DraftField::WorktreeToggle | DraftField::BranchMode))
|
||||
.count(),
|
||||
0,
|
||||
"collapsed fields drop out of tab order"
|
||||
);
|
||||
|
||||
let rendered = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
|
||||
assert!(
|
||||
rendered.contains("Advanced options") && rendered.contains("creates a worktree"),
|
||||
"header states the on-by-default worktree:\n{rendered}"
|
||||
);
|
||||
assert!(
|
||||
!rendered.contains("[x] worktree"),
|
||||
"checkbox stays hidden while collapsed:\n{rendered}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_header_states_no_worktree_for_a_worktree_row_91() {
|
||||
let mut draft = DraftPane::default();
|
||||
let mut wt = repo_project("wt-91", "/repos/wt91", "/repos/main", 1, None);
|
||||
wt.is_worktree = true;
|
||||
draft.set_projects(vec![wt], Utc::now());
|
||||
draft.focused_field = DraftField::DirPicker;
|
||||
draft.on_input(&InputKey::Enter);
|
||||
|
||||
assert!(!draft.worktree_enabled);
|
||||
assert!(
|
||||
draft.advanced_collapsed,
|
||||
"picking a row must not force it open"
|
||||
);
|
||||
let rendered = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
|
||||
assert!(
|
||||
rendered.contains("no worktree"),
|
||||
"header reflects #91's off default:\n{rendered}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_on_the_advanced_header_expands_it_without_moving_fields_above() {
|
||||
let mut draft = DraftPane {
|
||||
home_dir: "/home/u".into(),
|
||||
focused_field: DraftField::Advanced,
|
||||
..DraftPane::default()
|
||||
};
|
||||
draft.set_projects(
|
||||
vec![project("maestro", "/home/u/tea/maestro", 1, Some("claude"))],
|
||||
Utc::now(),
|
||||
);
|
||||
let before = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
|
||||
let before_head: Vec<&str> = before.lines().take(6).collect();
|
||||
|
||||
draft.on_input(&InputKey::Enter);
|
||||
assert!(!draft.advanced_collapsed);
|
||||
|
||||
let after = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
|
||||
let after_head: Vec<&str> = after.lines().take(6).collect();
|
||||
assert_eq!(
|
||||
before_head, after_head,
|
||||
"expanding must not move the fields above the disclosure"
|
||||
);
|
||||
assert!(after.contains("[x] worktree"), "body now visible:\n{after}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abbreviate_path_matches_the_web_helpers() {
|
||||
assert_eq!(
|
||||
|
||||
+45
-20
@@ -152,33 +152,38 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="worktree">
|
||||
<legend>
|
||||
<button type="button" class="tree-folder-header wt-toggle" aria-expanded="false">
|
||||
<span class="chevron">▾</span>
|
||||
<span class="folder-name">Advanced options</span>
|
||||
<span class="wt-summary"></span>
|
||||
</button>
|
||||
<div class="wt-collapsible" hidden>
|
||||
<label class="row-inline">
|
||||
<input type="checkbox" name="worktree_enabled" />
|
||||
Create git worktree
|
||||
</label>
|
||||
</legend>
|
||||
<p class="wt-hint" hidden></p>
|
||||
<div class="wt-body" hidden>
|
||||
<p class="wt-repo"></p>
|
||||
<div class="wt-mode" role="radiogroup" aria-label="Branch source">
|
||||
<label class="row-inline">
|
||||
<input type="radio" name="worktree_branch_mode" value="new" />
|
||||
New branch
|
||||
<p class="wt-hint" hidden></p>
|
||||
<div class="wt-body" hidden>
|
||||
<p class="wt-repo"></p>
|
||||
<div class="wt-mode" role="radiogroup" aria-label="Branch source">
|
||||
<label class="row-inline">
|
||||
<input type="radio" name="worktree_branch_mode" value="new" />
|
||||
New branch
|
||||
</label>
|
||||
<label class="row-inline">
|
||||
<input type="radio" name="worktree_branch_mode" value="existing" />
|
||||
Existing branch
|
||||
</label>
|
||||
</div>
|
||||
<label class="wt-new-wrap">
|
||||
Branch
|
||||
<input name="worktree_branch" type="text" placeholder="feat/foo" autocomplete="off" />
|
||||
</label>
|
||||
<label class="row-inline">
|
||||
<input type="radio" name="worktree_branch_mode" value="existing" />
|
||||
Existing branch
|
||||
<label class="wt-exist-wrap" hidden>
|
||||
Branch
|
||||
<select class="branch-select"></select>
|
||||
</label>
|
||||
</div>
|
||||
<label class="wt-new-wrap">
|
||||
Branch
|
||||
<input name="worktree_branch" type="text" placeholder="feat/foo" autocomplete="off" />
|
||||
</label>
|
||||
<label class="wt-exist-wrap" hidden>
|
||||
Branch
|
||||
<select class="branch-select"></select>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p class="form-error" role="alert" aria-live="polite"></p>
|
||||
@@ -200,6 +205,9 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
||||
const cwdRawWrap = form.querySelector(".cwd-raw-wrap") as HTMLElement;
|
||||
const cwdRaw = form.querySelector(".cwd-raw") as HTMLInputElement;
|
||||
const wtEnabled = form.querySelector("input[name=worktree_enabled]") as HTMLInputElement;
|
||||
const wtToggle = form.querySelector(".wt-toggle") as HTMLButtonElement;
|
||||
const wtCollapsible = form.querySelector(".wt-collapsible") as HTMLElement;
|
||||
const wtSummary = form.querySelector(".wt-summary") as HTMLElement;
|
||||
const wtHint = form.querySelector(".wt-hint") as HTMLElement;
|
||||
const wtBody = form.querySelector(".wt-body") as HTMLElement;
|
||||
const wtRepo = form.querySelector(".wt-repo") as HTMLElement;
|
||||
@@ -526,9 +534,11 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
||||
markActiveChoice();
|
||||
if (choice === "new") {
|
||||
wtEnabled.checked = true;
|
||||
updateWtSummary();
|
||||
applyProjectRepo(group.main.base_repo ?? group.main.path);
|
||||
} else {
|
||||
wtEnabled.checked = false;
|
||||
updateWtSummary();
|
||||
inferredBaseRepo = null;
|
||||
wtBody.hidden = true;
|
||||
wtHint.hidden = true;
|
||||
@@ -661,6 +671,18 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
||||
return p;
|
||||
}
|
||||
|
||||
// ---- worktree: disclosure (#93), reuses the sidebar's folder-header ---
|
||||
let advancedCollapsed = true;
|
||||
function setAdvancedCollapsed(collapsed: boolean): void {
|
||||
advancedCollapsed = collapsed;
|
||||
wtToggle.setAttribute("aria-expanded", String(!collapsed));
|
||||
wtCollapsible.hidden = collapsed;
|
||||
}
|
||||
function updateWtSummary(): void {
|
||||
wtSummary.textContent = wtEnabled.checked ? "creates a worktree" : "no worktree";
|
||||
}
|
||||
wtToggle.addEventListener("click", () => setAdvancedCollapsed(!advancedCollapsed));
|
||||
|
||||
// ---- worktree (branch-only; base_repo inferred via scan or /gitroot) --
|
||||
// The project scan already reports repo membership, so a picked project
|
||||
// resolves its base repo locally (#61); only the free-text escape hatch
|
||||
@@ -741,6 +763,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
||||
// Publish the untick too, or the next render seeds the box checked with
|
||||
// no inferred repo and submitting fails.
|
||||
wtEnabled.checked = false;
|
||||
updateWtSummary();
|
||||
emit();
|
||||
wtBody.hidden = true;
|
||||
wtHint.hidden = false;
|
||||
@@ -814,6 +837,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
||||
branchSelect.addEventListener("change", () => emit());
|
||||
|
||||
wtEnabled.addEventListener("change", () => {
|
||||
updateWtSummary();
|
||||
emit();
|
||||
if (wtEnabled.checked) {
|
||||
resolveBaseRepo();
|
||||
@@ -827,6 +851,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
|
||||
// The toggle defaults ON (#61): the operator almost always wants a
|
||||
// worktree, and every non-repo target unticks it via the gitroot probe.
|
||||
wtEnabled.checked = prefill.worktreeEnabled ?? true;
|
||||
updateWtSummary();
|
||||
if (prefill.worktreeBranch) branchInput.value = prefill.worktreeBranch;
|
||||
setBranchMode(branchMode);
|
||||
|
||||
|
||||
@@ -180,6 +180,59 @@ describe("spawnForm worktree defaults and branch picker (#61)", () => {
|
||||
expect(modes.at(-1)).toBe("existing");
|
||||
});
|
||||
|
||||
it("collapses the advanced (worktree) section by default (#93)", async () => {
|
||||
const form = await mount();
|
||||
const toggle = form.querySelector<HTMLButtonElement>(".wt-toggle")!;
|
||||
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
||||
expect(form.querySelector<HTMLElement>(".wt-collapsible")!.hidden).toBe(true);
|
||||
});
|
||||
|
||||
it("states in the collapsed header that a worktree will be created by default (#93)", async () => {
|
||||
const form = await mount();
|
||||
expect(form.querySelector<HTMLElement>(".wt-summary")!.textContent).toMatch(/creates? a worktree/i);
|
||||
});
|
||||
|
||||
it("states in the collapsed header that no worktree will be created for a worktree row (#91, #93)", async () => {
|
||||
urls = stubFetch({
|
||||
projects: [
|
||||
{
|
||||
path: "/home/user/wt-91",
|
||||
name: "wt-91",
|
||||
is_worktree: true,
|
||||
base_repo: "/home/user/maestro",
|
||||
last_activity: "2026-08-20T10:00:00Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
handle = createSpawnForm({ onSuccess() {} });
|
||||
document.body.appendChild(handle.el);
|
||||
for (let i = 0; i < 40; i++) await Promise.resolve();
|
||||
|
||||
handle.el
|
||||
.querySelectorAll<HTMLButtonElement>(".project-box")
|
||||
.forEach((box) => box.dataset.cwd === "/home/user/wt-91" && box.click());
|
||||
await drain();
|
||||
|
||||
expect(handle.el.querySelector<HTMLElement>(".wt-summary")!.textContent).toMatch(/no worktree/i);
|
||||
// Still collapsed — picking a project must not force it open.
|
||||
expect(handle.el.querySelector<HTMLButtonElement>(".wt-toggle")!.getAttribute("aria-expanded")).toBe(
|
||||
"false",
|
||||
);
|
||||
});
|
||||
|
||||
it("expanding the advanced section does not reorder the fields above it (#93)", async () => {
|
||||
const form = await mount();
|
||||
const before = [...form.querySelectorAll(".field")];
|
||||
|
||||
form.querySelector<HTMLButtonElement>(".wt-toggle")!.click();
|
||||
|
||||
expect(form.querySelector<HTMLButtonElement>(".wt-toggle")!.getAttribute("aria-expanded")).toBe(
|
||||
"true",
|
||||
);
|
||||
expect(form.querySelector<HTMLElement>(".wt-collapsible")!.hidden).toBe(false);
|
||||
expect([...form.querySelectorAll(".field")]).toStrictEqual(before);
|
||||
});
|
||||
|
||||
it("does not repopulate a destroyed form when the branch list lands late", async () => {
|
||||
const gate: { release?: () => void } = {};
|
||||
urls = stubFetch({ gateBranches: (r) => (gate.release = r) });
|
||||
|
||||
+21
-6
@@ -1284,18 +1284,33 @@ textarea {
|
||||
.worktree {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-2);
|
||||
padding: var(--sp-3);
|
||||
padding: 0 var(--sp-3);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-3);
|
||||
background: color-mix(in oklab, var(--surface-2) 50%, transparent);
|
||||
}
|
||||
|
||||
.worktree legend {
|
||||
padding: 0 6px;
|
||||
color: var(--ink);
|
||||
font-size: var(--fs-13);
|
||||
.worktree .wt-toggle {
|
||||
margin: 0 calc(var(--sp-3) * -1);
|
||||
padding-left: var(--sp-3);
|
||||
width: calc(100% + var(--sp-3) * 2);
|
||||
}
|
||||
|
||||
.wt-summary {
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-12);
|
||||
}
|
||||
|
||||
.wt-collapsible {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-3);
|
||||
padding-bottom: var(--sp-3);
|
||||
}
|
||||
|
||||
.wt-collapsible[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
|
||||
Reference in New Issue
Block a user