185790fb14d45273282753a630f4a4cf131a1681
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d02d103569 |
Take the group's EDIT and DELETE off the heading row
A pair of buttons sat at the end of the GROUPS heading, and the card's own right-click menu arrived later offering the same two things. Two controls for one job, and the buttons were the harder of the two to read: a button beside a heading has no card under a pointer to mean, so it had to work its subject out — the selected card, or failing that the group the trail ends with, which once a group is open is not a card on screen at all. The menu never has that problem, because opening it is what aims it. The menu is the whole of Edit and Delete on the desktop now. ShowsGroupActions went with the buttons, since hiding them was all it did. GroupTarget stays: the menu's two entries read it after the code-behind has selected whatever was right-clicked, and its fallback to the open group is what makes + NEW HOST open on the group somebody is standing in rather than on none. One case changes shape. Opening a group with nothing inside it folds the card grid away, so from in there nothing can be right-clicked — renaming that group means pressing the trail back one level, to where it has a card of its own. The buttons used to cover it through the fallback. The trail was already the way out of an empty group, and 3.2a says so now. The test that pressed EDIT through its binding is replaced by one holding that no button on the screen commands either of them, which is the failure worth catching: a button coming back is not a compile error, and it would draw itself in place, aimed at the group the trail ends with. What that test covered — a command that has to accept an empty parameter, and act on the card the pointer was on — the two menu tests beside it already do. |
||
|
|
742f65c204 | Merge branch 'claude/card-selection-state-sharing-f9348c' | ||
|
|
3f419cb19b |
Let one card be selected at a time
The hosts screen draws two grids, one above the other, and each is a ListBox with a selection of its own. Nothing joined them, so a group card and a host card could be lit at the same moment — two chosen things, under two pairs of buttons of which only one would act on whichever the eye had settled on. Both grids mark a selection the same way, so there was nothing on screen to say which of the two the next press belonged to. They share one mark now. Selecting a host clears the group and selecting a group clears the host, and that second one takes the detail pane down with it: a pane about one machine cannot go on standing beside a marked group, because nothing on it would be about what is selected. Losing a selection deliberately clears nothing. A null arrives whenever either list is rebuilt — every keystroke in the filter box and every background sync — and treating that as somebody deselecting would take the mark off a group card because a search emptied the grid beneath it. The reload needed the same guard for the same reason. It falls back to the first host when nothing is selected, which is what puts a target under CONNECT on a fresh unlock; with one mark between the two grids that fallback would have unselected a group nobody had touched, once a minute. It is skipped while a group holds the selection, and it moved into a method of its own because the comment saying why pushed ReloadHostsAsync past sixty lines. One consequence needed handling rather than accepting. The phone's only route into the group editor is a button on a heading in the host list, and it worked by selecting the group first — which under this rule takes the highlight off the machine somebody was about to connect to, on a screen that draws no group cards to say where it has gone. EditGroup takes the group as an argument now: the heading passes its own row, and the desktop's button beside the cards passes nothing and still means "the card that is selected". What the tests hold is the half that lives in the controls. Clearing the property has to reach the list that is drawing the card, and a selection nulled in the view model while the card stays highlighted is the exact failure this is about — so the rule is driven on the real screen, in both directions, against the ListBoxes' own SelectedItem. The desktop's EDIT is pressed through its binding for the same kind of reason: a command refusing the button's empty parameter would be a button that never fires, and nothing about the markup would say so. |
||
|
|
be012585b3 |
Let a group be right-clicked, as a host already can
The host cards have had a menu since the grid replaced the sidebar; the group cards above them had a double-click and two buttons beside the heading, and nothing that named the card under the pointer. Open, Edit and Delete are on them now, drawn and aimed the same way. On the list rather than in the item template, for the reason the host grid's is: the three commands are the vault's, and a ContextMenu inside a DataTemplate has the row for its data context, so every binding in it would silently resolve to nothing. The code-behind selects whatever was right-clicked before the menu opens, and that is what makes one menu act on the card under the pointer rather than on whichever was selected before. **Cancelled over the space around the cards, and here that guard is doing more than the host grid's.** GroupTarget falls back to the group whose contents are on screen when no card is selected — the right answer for a pair of buttons beside the heading, which would otherwise have no subject the moment a group with nothing inside it is opened, and the wrong one for a menu that opened on a card. Without the guard, right-clicking the gap beside the cards would offer to delete the group the trail ends with: a question about something the user is not pointing at, in the one menu where the answer is a deletion. Only Open takes a parameter, and it has to. OpenGroupCommand's null is a real argument rather than a missing one — it is the trail's first crumb, ALL HOSTS — so an entry with no parameter would not open the card, it would leave the group the user right-clicked and go back to the top level. Two tests beside the two the host menu already had. What they hold that a build cannot is the CommandParameter binding: a path that resolves to nothing compiles and draws, and the entry would then quietly do the opposite of what it says. The popup itself is still the platform's, so manual-checks 7.9 gained the group half of the same check. |
||
|
|
2a56ae3efb |
Put a host inside the group it is filed under, rather than beside it
The group cards were headings with a navigation gesture bolted on. Opening one narrowed the grid to its hosts, but the level above already held every host in the keychain — so a card could only ever subtract, filing something changed nothing but a chip, and a keychain with forty machines was forty cards however carefully anybody had arranged them. The trail said ALL HOSTS and meant it. So the grid holds one level of the tree, the way a directory pane holds one directory. A host filed under a group is inside that group and is not also on the screen the group's card sits on; the outermost level is what nothing has been filed into. A group is a place now, and the cards, the trail and the drop target were already the vocabulary for saying so. The find box is the exception and had to be one. Typed into, it searches the open group and everything under it, which from the outermost level is every machine in the keychain. A box scoped to the level it was typed on would answer "no host matches that" about a host this keychain has got, and finding a machine without first remembering where it was filed is most of what the box is for. The accent chip on a card is what tells a searched-up host from one that lives at this level. Two things came free with the change and are handled rather than left. The phone would have broken. Its list binds SidebarRows, which was a projection over VisibleHosts, and it has no group cards and nowhere to open one into — so level-scoping would have left it drawing only the hosts nobody had filed. RebuildSidebarRows takes its own pass over the hosts now, narrowed by the vault switches and the box and by nothing else, which is the whole tree flattened under headings: exactly what it drew before. And anything created inside a group disappeared the moment it was saved. The host editor opens on the group the screen is about rather than only on a selected card, and + NEW GROUP defaults its parent to the group that is open. Deliberately not the selected card there: a highlighted card is what EDIT and DELETE are aimed at, and reading it as "and the next group goes inside it" would nest one because somebody had clicked something. The host editor takes both because it always took the selection, and its picker shows the answer before anything is written. Dropping a host on a group card now takes the card off the grid, and the selection goes with it — Connect, Edit and Delete all read that property and none of them should be aimed at a card that has left the screen. The status line is what says where it went, which is why the manual check now asks for it to be read. Coming back out through the editor lands the host on this level again, and there the selection survives. An empty grid has two more things it can say: that every host is filed away, which the level-at-a-time grid made reachable and which is not the same sentence as "there are none", and that nothing under this group matches what was typed — with ALL HOSTS named as the way to widen it. Not changed, and next door: a group card counts the hosts filed directly under it, so a group holding only subgroups reads "0 hosts". That was already true and is more visible now that its subgroups' hosts are not spilled onto the level above. 1522 tests pass. Four are new — the level rule and the phone's flat list asserted together, the box reaching two levels down, the all-filed sentence, and where a host and a group made inside a group end up. MovingAHostToAGroup was asserting the old outcome and is rewritten rather than adjusted: it held that the host stayed selected, and what it holds now is that the host leaves the level it came from. |
||
|
|
fd6c4f5155 | Merge branch 'claude/host-detail-pane-design-e98621' | ||
|
|
e3dfe5c371 |
Make the host pane something you ask for, and draw it as cards
THE DRAWER USED TO ARRIVE WITH THE SELECTION. IsDrawerOpen read "a host is selected", so touching any card took 304 pixels off the grid — including every card arrowed past on the way to the one somebody wanted. Choosing among forty machines was charged the price of the pane for one of them. A pencil now appears on the card under the pointer and on the selected card, and that is what opens it; IsHostPaneOpen is the flag, and the grid's context menu gained Details… so the pane is reachable without a pointer, which a hover-only control is not. Once open it follows the selection rather than pinning the host it was opened on: a pane about one host beside a grid marking a different one is two answers to the same question. Losing the selection closes it and clears the flag, or a filter matching nothing would leave the pane armed to spring open again on the next card merely selected — which is the behaviour the pencil exists to remove. The pencil is drawn over the card rather than in a column of its own. A column would have cost the name 30 pixels of a 232-pixel tile, permanently, for a control that is only there while the pointer is; the dot and the pencil stack in the two corners of that edge instead. IsVisible and not opacity, because a button at zero opacity still takes the click and the card underneath does not. A HEADER, A BODY THAT SCROLLS, AND A FOOTER, which is the one structural change in the pane. The header names what the drawer is about and carries the two things true of every panel; the footer carries the one thing each panel is for — CONNECT, or SAVE, or the question about deleting. Only the middle scrolls, so the button somebody came here to press can no longer be below the fold, which CONNECT could be on a host with fifteen tags. That also widens what the layout harness certifies: it skips anything inside a ScrollViewer, and the control each panel exists to offer is now outside one. THE SAME THREE CARDS TWICE. Address, General, Connection — first as rows stating what the host is, then as boxes for changing it. The detail pane's rows are buttons that open the editor: the design draws every fact as a filled box, and rather than draw an input that refuses the pointer, pressing one leads to the same card with a real box in it. Nothing here saves as you type, and that is not timidity — saving validates the key-or-credential exclusion and writes one encrypted payload, so a box committing per keystroke would be a save per character and a half-typed hostname on the wire. Every value the pane prints is the resolved one, and says "inherited" beside it where a group supplied it. The number is the same either way and the edit is not: clearing a group's default moves every host that never overrode it. A HOST CARD IS TWO LINES AND NO CHIPS. The subtitle is now "ssh, root, pci, eu-west-1" — the transport, the resolved account, then every tag — replacing both the user@host:port line and the wrapped row of tag chips under it. The address went to the card's tooltip rather than nowhere: a card is read while scanning forty machines, where the name and the kind of machine are what is being looked for, and an address is what you read once you have found it. "ssh" is a constant today and is printed anyway, which is the one thing here that argues with this codebase's own rule about constants dressed as readings. It is the first item of a list whose other items vary, and a list beginning with the account on one card and a tag on the next has no shape to scan. The remark on HostRowViewModel.Summary says so rather than leaving it to be discovered. WHAT THE DESIGN DRAWS AND THIS PANE HAS NOT GOT: Share this host, Add Telnet, "SSH ID, Certificate, FIDO2", the backspace-key mapping row, the vault picker's chevron and Show more. Sharing is per vault and not per item, every session here is an SSH channel, there are no identity or certificate item types, nothing carries a terminal setting to the renderer, and an item cannot be moved between vaults at all. Six controls with nothing behind them, listed in docs/design-import-gaps.md with what ships instead, and none drawn disabled. The credentials row is marked with ◆ rather than the ⚿ the nav rail uses for the keychain. U+26BF is outside both faces this application substitutes for the design's fonts, so it lands on whatever the platform's fallback has; every other glyph in the pane is from Geometric Shapes, which both carry. |
||
|
|
0c0ac94312 |
Open a group by double-clicking it, and say where you are
ONE PRESS WAS DOING TWO JOBS. A group card was the only place a group could be selected — it is what EDIT and DELETE aim at — and it was also the control that narrowed the grid to that group. So there was no way to name a group in order to rename it without every host outside it leaving the screen at the same moment, and no way back except a SHOW ALL button that appeared beside the heading. Two gestures instead. A click selects and does nothing else; a double-click opens, which is what the host cards below already do to get a shell and what the transfers screen's directories already do to go inside one. The grid now has one vocabulary rather than one per list. The gesture is wired in the code-behind beside the host one, and guarded the same way: a double-click on the space around the cards must not open whichever group happened to be selected. THE CARDS ARE ONE LEVEL NOW, not every group in the keychain. Groups nest, and drawing all of them flat was the only honest thing to do while a card was a filter — a filter nobody can see is a filter nobody can turn off. Once opening one became navigation the cards became its contents, and VisibleGroups is that level beside Groups the way VisibleHosts sits beside Hosts. Groups itself is untouched: it is what every lookup reads and what the phone's headings are built from, and the phone binds none of the new members. Which is what the trail is for. A level with no name and no way out is a grid that has quietly hidden things, so a breadcrumb sits above the cards — drawn exactly as the transfers screen draws a directory path, same flat crumbs and same separator, because it is the same control answering the same question and a window with two breadcrumbs that look different has two ideas of what a path is. The first crumb is always there and always goes back to every host, which is what SHOW ALL was; that button went with it, because a control that only says "stop" beside a trail that says where you are is a second control for one job, and this one also gets you back one level rather than all the way. EDIT AND DELETE AIM AT GroupTarget: the selected card, or the open group when no card is selected. Without the fallback a group with nothing inside it could be opened and then never edited, because opening a group is exactly what takes its own card off the screen. It is also what a file manager does — act on the selection, and on the current folder when there is none — and the pair is now hidden with nothing to act on rather than shown doing nothing. A DANGLING PARENT AND A CYCLE BOTH HAD TO END UP SOMEWHERE REACHABLE. Neither is prevented: a parent id may point at a group deleted on another machine, and two clients can each re-parent A under B and B under A while offline, which no merge can see because the pointer is inside the payload. EffectiveParents promotes both to the outermost level, which is the same degradation the resolver's visited set produces for inheritance. The repair for either is the group's own editor and the editor is opened from its card, so a group drawn nowhere would be a broken state with the fix locked inside it. Three tests in HostGridTests: the split rule through the properties the cards bind, the pointer gesture itself in two windows so that "one press still only selects" is asserted separately from the pair, and a nested group opened, emptied of cards and walked back out of one level. The last presses the trail as it is actually rendered rather than calling the command, because a crumb reaches the vault through a $parent binding — a string that compiles whether or not it resolves, and would otherwise leave a row of buttons that do nothing. 85 layout tests and 234 shell-flow tests pass. Manual-checks 3.2, 7.6 and 7.7 follow the new gestures, and 3.2a and 3.4a are new: nesting, and the two states above, both of which need two machines and neither of which headless Avalonia can reach. |
||
|
|
f9d08b738c |
Take the group headings out of the host grid, and drop onto a group card
A first group turned the wrap of host cards into an accordion: SidebarRows interleaves a full-width fold-away heading — chevron, name, count — between the cards, and in a grid that reads as a dropdown somebody left open. The desktop grid binds VisibleHosts now. The headings and the fold stay for the phone, whose list has no room for the row of group cards the desktop draws above the grid. What a heading said, the card says: HostRowViewModel.GroupLabel, resolved once per reload like the tag names, drawn as an accent chip and absent from a host in no group — or in one that has been deleted, which is the same thing to look at. What a heading also was is the drop target, and that moves to the group cards. Two things go with it. A host dropped onto another host card used to be filed beside it, which was legible while a heading named the band of cards it landed in and is guesswork now; it is refused. And UNGROUPED was how a host was dragged back out of a group; the way out is the picker in its own editor, which is the one place "no group" can be said in words. A drag held at either edge of the grid scrolls it. Without that the gesture only works for whoever can see both ends of it: the group cards are the first thing in the scroller, the host may be the fortieth card down, and a drag cannot use the wheel. A step per drag event rather than a timer, so it follows the pointer and stops when it stops. The two heading-shaped tests are replaced. TheHostsGridHoldsCardsAndNoGroupHeadings asserts the grid's contents rather than only measuring them, because a heading that came back would lay out perfectly cleanly. TheGroupCardsAreWhatAcceptsADroppedHost raises a real DragOver over both kinds of card and checks the effects and the mark — the nearest a headless test gets to a gesture no headless test can synthesise. manual-checks 3.1-3.2 and 7.6-7.9 follow. |
||
|
|
208443b932 |
Move SFTP and S3 into the tab strip, and the host list into a card grid
Four asks in one pass over the desktop head, and two of them are furniture moving rather than anything new. THE STRIP IS THE WINDOW'S NOW, not the terminal's. Vaults, SFTP and S3 sit at its head and cannot be closed, and open terminals follow them. SFTP and S3 left the nav rail to get there, which is the one semantic change: they are the two destinations you stay in while something runs, and a rail entry is for somewhere you go and come back from. So the rail belongs to the Vaults tab and collapses with it, which also means SFTP, S3 and a terminal each get the full 1016 rather than the 826 a page gets. The tab is expressed as "a page, and not one of the two the strip took" rather than as a fourth ShellSurface. Both are still ShellScreen members and have to be — that is what they are on the phone, where they are two rows in a hub — so a surface each would have been a second way to say a thing Screen already says. IsTransfersShowing and IsBucketsShowing light the other two tabs unchanged. What is new is one field: the page Vaults comes back to, because it is the one tab with sub-navigation and therefore the one that can return to the wrong place. That is not the hidden field ShellSurface argues against — that one would be a second copy of "which page"; this is a tab remembering its own. THE HOSTS SCREEN IS A GRID, and the 268-pixel sidebar is gone. That column was choosing among forty machines and editing one of them at two-thirds width, and it was narrow so the editor beneath it could be a column at all. Cards took the first job at full width; a 304-pixel drawer took the second and collapses when nothing is selected. Pressing a group card narrows the grid; SHOW ALL is the way back. The group editor moved into the drawer as well, which finally makes IsEditingGroup mean the same thing on both heads — it was the phone's alone, because the desktop's editor was a bar that was always on screen. AreHostsExpanded and ToggleHosts went with the control that used them. They folded the whole list away under one heading, an affordance that existed because the column was narrow. Folding a single group is a different thing and is still here. THE TYPE SCALE IS A POINT LARGER and the text ramp is white. The base size was never stated anywhere — a bare TextBlock took TextElement's default of 12 — so raising the scale meant naming it, on Window and on UserControl. The second selector is not redundancy: the layout harness hosts a UserControl in a window it builds itself, and without it the suite would measure every screen a point smaller than it ships, silently. A selector on TextBlock would have been the obvious way and is wrong, because a style setter beats an inherited value and would collapse every deliberate step back to one number. #E3E7F4 is a blue-tinted white on blue-black surfaces, which costs contrast twice — once for being darker than white and once for sharing a hue with what it is drawn on. Pure white is 18.3:1 against the canvas where that was 15.5:1. Every step below moved with the top, so the intervals the design chose are kept and TextDim clears 9:1 against 6.4:1. The palette is shared, so the phone has both changes too. TWO DEFECTS THE HARNESS STRUCTURALLY CANNOT SEE, found by rendering the screen rather than by measuring it, and both now covered. The tile was 232 and was first written as 248, from arithmetic that left out the scrolling stack's own margins. Every layout test passed — the harness asks whether a control is inside the window, never how many fit on a line — so the grid quietly became one column wide at exactly the minimum this application guarantees, which is the shape cards exist to avoid. TheHostsGridKeepsTwoColumnsAtTheMinimumWithTheDrawerOpen counts columns instead, and fails at 248. And a card's text ran past its own border, because a horizontal StackPanel measures children with infinite width: a TextBlock inside one never learns it is short of room, so TextTrimming never fires. Both card rows are grids with a star column that gives way and an Auto column that does not — a hostname with its tail cut is still the machine you were looking for, where a badge or the word naming an auth method is not. The keychain header changed shape for the same class of reason. It was Auto,Auto,*,Auto with the buttons last, so the slack column was the only thing absorbing a change of width and five buttons fell off the right edge the moment the type grew. That is how GENERATE lost the word KEY once already. The summary sits in the star column and trims now, so the buttons always get their width. HostSidebarTests became HostGridTests and moved to the grid with the gestures it drives. docs/design-import-gaps.md gains a v3 section naming the five toolbar controls in the design with nothing behind them — a view-mode switch, a tag filter, a calendar, a share control and Serial — and manual-checks.md and the README follow the controls that moved. |