Public Access
Give the desktop the second design too, and the window the size it now needs
The desktop v2 design is the other half of the one the phone took last commit, and this is its chrome: a 190-pixel labelled sidebar where the 54-pixel icon rail was, a titlebar with the search box centred in it, and session tabs drawn as pills. The palette was already here — it is shared, and moved when the phone's did — so what this changes is shape rather than colour. **The window's minimum grew, and by exactly what v2 added.** The sidebar is 136 wider and the chrome 14 taller, so 880x560 became 1016x574. That is not a round number somebody liked: it leaves every screen the same 826x464 it was designed against, which is the arithmetic the layout suite is built on. Four of the tables stop fitting at 690 wide, so widening the sidebar and leaving the window alone would have broken them somewhere no test was looking. LayoutHarness carries the new constants and the suite still passes at the minimum, which is the whole reason it exists. The rail's five-character abbreviations are gone with the width that caused them — PINS and SNIPS are Pins and Snippets again — and each row gains a glyph and a count. A count is drawn only where one is real, so SFTP, Logs and Preferences show nothing rather than a zero: a transfer queue's depth is not how many files a screen holds, and a log has no total until it is read. The count beside Pins is the vault's own, not the Pins screen's VisiblePins, which is the filtered list and would have made the sidebar count whatever somebody had just typed into a filter box on another screen. Teams has no count for a related reason: they are read from the server when that screen is opened rather than on unlock, so a number there would read 0 until somebody had already been to look. One colour moved with it, finishing what the repalette started: the live-session summaries on the unlock and sign-out cards were Info, so the two heads disagreed about a fact the phone paints green. They match again. **Buckets became a destination rather than a mode**, which is what the design draws and what the phone already does. The HOST / BUCKET pair inside the files screen is gone; ShellScreen.Buckets draws the same TransfersScreen with the other picker, and the sidebar entry is what sets it. That also settles an old disagreement rather than merely moving it: TotalItemCount is keys plus passwords and excludes buckets, so the number beside the keychain used to disagree with the list under it, and now counts what that screen shows. There is one session behind both file destinations, so asking for the other kind while something is open is refused rather than obeyed — and refusing means staying put. An earlier turn of this had it move anyway and only decline to switch the picker, which put the S3 entry in the sidebar over a screen still listing an SFTP host: two pieces of chrome disagreeing about where you are, which is worse than the navigation simply not happening. The message that says so goes to Transfers.Status, which turned out to be drawn in the same grid cell as the connected chip — survivable while it was mostly read before connecting, and not once a refusal reports itself there. It has its own column now. The design has nine entries' worth of screens and draws five. Pins, Teams, Import and Preferences are built, working screens, so they keep their entries — the sidebar is labelled now and has the room, and dropping an entry would have stranded a screen rather than simplified anything. The Team vault card the design pins to the foot is not drawn: it is a second route to a screen already in the list, carrying a seat count nothing here produces. **The status bar survives the design that deletes it**, cut down to one thing. Two of the three facts it carried moved into the titlebar with v2 — the sync word is beside its dot and the shortcut hint is inside the box that uses it — so those are gone from it rather than printed twice. The third is Vault.Status, the only channel this application has for saying a save failed or a merge picked a winner. The design is a mock-up of an afternoon that goes well and has nowhere to put a sentence like that; dropping the bar would have meant dropping the sentence or repeating it on nine screens. What v2 draws and this does not is in docs/design-import-gaps.md, and it is the same list as the phone's for the same reasons: the forwarding screen and both its chips, the host detail's fingerprint, tags and last-session cards, the keychain's rotate button, the logs' FOLLOW pill and severity filters, and the session footer's latency. The terminal is not inset behind a rounded frame either — it is a native child window that composites above everything Avalonia paints, so the frame would clip nothing, which is the same answer the phone gave. **The light theme is not built.** Its accent is #6D5AE6, a different hue rather than a tint of the dark one, so it needs every colour doubled, a variant to switch on, the renderer's own page switching with it, and contrast checked twice. That is a piece of work rather than a setting, and it is separable from the layout — which is why this commit is the layout. The screens themselves are restyled through the shared vocabulary rather than rebuilt: corner radii, chips, cards and the accent's ink, all in App.axaml, so every screen moves at once. Their layouts are left alone deliberately. The design draws read-only detail panes and these screens carry the editors and forms it has no equivalent of, so replacing a layout with the mock-up's would have lost the half that is actually used. Verified by the whole suite: 1309 tests over nineteen projects, none failing, including the 68 layout cases that stand up real Avalonia and measure every screen at the new minimum. Both heads build. Not run on a machine with a display — see docs/manual-checks.md for what wants looking at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZE3u99BNt6LzgTC5jhbz2
This commit is contained in:
@@ -268,9 +268,14 @@ the reasoning behind it. Sign in, unlock, browse hosts, open a shell, and read t
|
|||||||
host-key decisions and the counted delete confirmations are there too, and none of them were softened to
|
host-key decisions and the counted delete confirmations are there too, and none of them were softened to
|
||||||
fit 360dp.
|
fit 360dp.
|
||||||
|
|
||||||
Its interface is the **v2 design**: four destinations in a bottom bar — Hosts, Terminal, Vault and More —
|
Its interface is the **v2 design**: four destinations in a bottom bar — Hosts, Terminal, Keychain and More
|
||||||
with snippets, SFTP, S3 buckets, logs and preferences one tap deeper behind the last. That is also where
|
— with snippets, SFTP, S3 buckets, logs and preferences one tap deeper behind the last. Both heads are on
|
||||||
the product's palette now comes from, and both heads share it, so the desktop is blue too.
|
that design now; the desktop's own v2 is a 190-pixel labelled sidebar in place of the icon rail, a centred
|
||||||
|
search box in the titlebar, and session tabs as pills. Its light theme is not built — see
|
||||||
|
[`docs/design-import-gaps.md`](docs/design-import-gaps.md) — so the application is dark on both.
|
||||||
|
|
||||||
|
Widening the sidebar moved the desktop window's minimum from `880x560` to `1016x574`, which leaves every
|
||||||
|
screen exactly the width it was designed against.
|
||||||
|
|
||||||
File transfer **is** here now, in the shape scoped storage allows: one remote pane and the queue, over
|
File transfer **is** here now, in the shape scoped storage allows: one remote pane and the queue, over
|
||||||
either an SFTP host or a bucket. There is no local pane, because there is no browsable local filesystem to
|
either an SFTP host or a bucket. There is no local pane, because there is no browsable local filesystem to
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ desktop head and does not port; an Android head would be a sibling project shari
|
|||||||
|
|
||||||
880×560 minimum, a 54-pixel nav rail, a 268-pixel host sidebar, a two-pane file browser with six columns per
|
880×560 minimum, a 54-pixel nav rail, a 268-pixel host sidebar, a two-pane file browser with six columns per
|
||||||
pane, a tab strip, and a layout suite (`DodoSSH.Client.App.Layout.Tests`, 64 tests) whose entire premise is
|
pane, a tab strip, and a layout suite (`DodoSSH.Client.App.Layout.Tests`, 64 tests) whose entire premise is
|
||||||
that everything fits at that minimum.
|
that everything fits at that minimum. (Those are the numbers this audit was taken against. The desktop's own
|
||||||
|
v2 has since widened the rail to 190 and the minimum to 1016×574, and the suite is 69 cases — which makes
|
||||||
|
the point below larger rather than smaller.)
|
||||||
|
|
||||||
None of it survives a phone. What an Android client would be is a different product with the same core:
|
None of it survives a phone. What an Android client would be is a different product with the same core:
|
||||||
probably a host list, a terminal, and a single-pane file browser, with the keychain, snippets, logs and pins
|
probably a host list, a terminal, and a single-pane file browser, with the keychain, snippets, logs and pins
|
||||||
|
|||||||
@@ -37,6 +37,38 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
|
|||||||
> | A 14px rounded inset around the terminal | Not drawn. The renderer is a native child view composited above everything Avalonia draws, so a rounded frame behind it clips nothing. What the design was after comes from the page's own background, which is the design's `#171A26`. |
|
> | A 14px rounded inset around the terminal | Not drawn. The renderer is a native child view composited above everything Avalonia draws, so a rounded frame behind it clips nothing. What the design was after comes from the page's own background, which is the design's `#171A26`. |
|
||||||
> | Instrument Sans / Fira Code | Inter and the system monospace stack, as before and for the same reason — see the note on `MonoFont` in `Palette.axaml`. |
|
> | Instrument Sans / Fira Code | Inter and the system monospace stack, as before and for the same reason — see the note on `MonoFont` in `Palette.axaml`. |
|
||||||
> | A theme toggle (`☀`) in the header, and the desktop v2's light theme | Omitted. There is one theme by decision, and a switch with one position is a control that does nothing. |
|
> | A theme toggle (`☀`) in the header, and the desktop v2's light theme | Omitted. There is one theme by decision, and a switch with one position is a control that does nothing. |
|
||||||
|
>
|
||||||
|
> ## The desktop's v2
|
||||||
|
>
|
||||||
|
> **DodoSSH v2** — the desktop design in the same project — has since been imported too, dark only. Its
|
||||||
|
> light theme is not built: the light accent is `#6D5AE6`, a different hue rather than a tint, so it needs
|
||||||
|
> every colour doubled, a theme variant to switch on, the renderer's page switching with it, and contrast
|
||||||
|
> checked twice. That is a piece of work, not a setting, and it is separable from the layout.
|
||||||
|
>
|
||||||
|
> **The window's minimum grew, and by exactly what v2 added.** The 54-pixel icon rail became a 190-pixel
|
||||||
|
> labelled sidebar and the chrome went from 72 tall to 86, so `880x560` became `1016x574` — leaving every
|
||||||
|
> screen the same `826x464` it was designed against. Four of the tables stop fitting at 690 wide, so
|
||||||
|
> widening the sidebar without widening the window would have broken them where the layout suite was not
|
||||||
|
> looking.
|
||||||
|
>
|
||||||
|
> **Buckets became a destination** rather than a toggle inside the files screen, matching the phone: the
|
||||||
|
> `HOST` / `BUCKET` pair is gone and `ShellScreen.Buckets` draws the same `TransfersScreen` with the other
|
||||||
|
> picker. That also settles an old disagreement — `TotalItemCount` excludes buckets and used to disagree
|
||||||
|
> with the list under it, and now counts exactly what the keychain screen shows.
|
||||||
|
>
|
||||||
|
> | v2 element | What ships instead |
|
||||||
|
> | --- | --- |
|
||||||
|
> | The **FORWARDING** screen, the `⇄ N forwards` chip on the session strip, and the host detail's FORWARDING card | Nothing, as on the phone and for the same reason. Three surfaces for a feature the SSH layer does not have. |
|
||||||
|
> | The host detail's 2×2 card grid — IDENTITY fingerprint, TAGS, LAST SESSION | Not drawn. There is no fingerprint on a stored key, no tag item type, and no last-used timestamp at any layer. The pane keeps the host editor the design has no equivalent of, which is the thing that is actually used. |
|
||||||
|
> | The keychain detail's public-key block, USED BY hosts, and **Rotate** | The detail pane as it stands. Copy public key is real and already there; rotation is not a thing this client can do. |
|
||||||
|
> | Logs: **FOLLOW**, the severity chips, and `tailing via ssh · journald + files` | The two logs it really has. Same as the phone — these are synced audit records, not a tail, and the footer sentence describes a feature rather than the screen. |
|
||||||
|
> | Snippets: **RUNS ON** host tags and a last-run line | The command and the two buttons that name the terminal they type into. |
|
||||||
|
> | The session footer: latency, forwards, `utf-8 · xterm-256color` | Omitted. The first two have no source; the third is a constant, and a constant printed as though it were a reading is decoration. |
|
||||||
|
> | The terminal as an inset rounded panel | Not drawn, for the reason the phone does not draw it: it is a native child window that composites above everything Avalonia paints, so a rounded frame behind it clips nothing. |
|
||||||
|
> | **Split ⌘D** | Still omitted — the renderer stacks panes and shows one; tiling needs a pane geometry it has not got. |
|
||||||
|
> | macOS traffic lights, and `⌘K` | The window's own minimise/maximise/close, and `CTRL K`. Development is Windows-first and the chrome is `BorderOnly` for a documented reason. |
|
||||||
|
> | No status bar | Kept, and cut down to the one thing the titlebar does not now carry: `Vault.Status`, which is the only channel this application has for saying a save failed or a merge picked a winner. The design is a mock-up of a working afternoon and has nowhere to put a sentence like that. |
|
||||||
|
> | The sidebar's five destinations, and a **Team vault** card at its foot | Nine destinations, because Pins, Teams, Import and Preferences are built screens and dropping their entry would strand them. The card is not drawn: it is a second route to a screen already in the list, carrying a seat count nothing here produces. |
|
||||||
|
|
||||||
Most of it landed. This file is the rest: every element of that design with nothing behind it, which
|
Most of it landed. This file is the rest: every element of that design with nothing behind it, which
|
||||||
project each piece would have to land in, and **what the shipped interface does instead**. That last
|
project each piece would have to land in, and **what the shipped interface does instead**. That last
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ question that cannot be reached.
|
|||||||
|
|
||||||
### 2.1 The fingerprint column is readable end to end
|
### 2.1 The fingerprint column is readable end to end
|
||||||
|
|
||||||
Connect to two or three hosts, approving each fingerprint. Go to PINS and widen the window to its minimum
|
Connect to two or three hosts, approving each fingerprint. Go to Pins and narrow the window to its minimum
|
||||||
(880px), then to something ordinary.
|
(1016px since the v2 sidebar; it was 880 while the rail was 54 wide), then widen it to something ordinary.
|
||||||
|
|
||||||
**Pass:** the full `SHA256:…` is on screen at both sizes, never cut off and never ellipsised.
|
**Pass:** the full `SHA256:…` is on screen at both sizes, never cut off and never ellipsised.
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<Setter Property="Background" Value="{StaticResource Chrome}" />
|
<Setter Property="Background" Value="{StaticResource Chrome}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="8" />
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
<Setter Property="Padding" Value="24" />
|
<Setter Property="Padding" Value="24" />
|
||||||
<Setter Property="MaxWidth" Value="520" />
|
<Setter Property="MaxWidth" Value="520" />
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
<Style Selector="Border.chip">
|
<Style Selector="Border.chip">
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="3" />
|
<Setter Property="CornerRadius" Value="4" />
|
||||||
<Setter Property="Padding" Value="6,2" />
|
<Setter Property="Padding" Value="6,2" />
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
</Style>
|
</Style>
|
||||||
@@ -101,8 +101,12 @@
|
|||||||
<Style Selector="Border.chip.accent">
|
<Style Selector="Border.chip.accent">
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource AccentSoft}" />
|
<Setter Property="BorderBrush" Value="{StaticResource AccentSoft}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<!--
|
||||||
|
AccentText rather than Accent, which is the v2 split: the fill colour is too saturated to read as
|
||||||
|
small text on a dark surface, and a chip is small text. See the remark on the pair in Palette.axaml.
|
||||||
|
-->
|
||||||
<Style Selector="Border.chip.accent > TextBlock">
|
<Style Selector="Border.chip.accent > TextBlock">
|
||||||
<Setter Property="Foreground" Value="{StaticResource Accent}" />
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Border.chip.warn">
|
<Style Selector="Border.chip.warn">
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource WarnSoft}" />
|
<Setter Property="BorderBrush" Value="{StaticResource WarnSoft}" />
|
||||||
@@ -124,7 +128,7 @@
|
|||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="3" />
|
<Setter Property="CornerRadius" Value="7" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="Button.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
@@ -141,13 +145,13 @@
|
|||||||
<Style Selector="Button.accent /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="Button.accent /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
<Setter Property="Background" Value="{StaticResource Accent}" />
|
<Setter Property="Background" Value="{StaticResource Accent}" />
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="3" />
|
<Setter Property="CornerRadius" Value="7" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource Canvas}" />
|
<Setter Property="Foreground" Value="{StaticResource AccentInk}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="Button.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
<Setter Property="Background" Value="{StaticResource Accent}" />
|
<Setter Property="Background" Value="{StaticResource Accent}" />
|
||||||
<Setter Property="Opacity" Value="0.85" />
|
<Setter Property="Opacity" Value="0.85" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource Canvas}" />
|
<Setter Property="Foreground" Value="{StaticResource AccentInk}" />
|
||||||
</Style>
|
</Style>
|
||||||
<!--
|
<!--
|
||||||
A disabled accent button keeps its shape and loses its fill. Fluent's disabled state greys the
|
A disabled accent button keeps its shape and loses its fill. Fluent's disabled state greys the
|
||||||
@@ -164,7 +168,7 @@
|
|||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource DangerSoft}" />
|
<Setter Property="BorderBrush" Value="{StaticResource DangerSoft}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="3" />
|
<Setter Property="CornerRadius" Value="7" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource Danger}" />
|
<Setter Property="Foreground" Value="{StaticResource Danger}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.danger:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="Button.danger:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
@@ -242,71 +246,108 @@
|
|||||||
behind it, which is the design's whole idiom for "you are here" — the same two marks a selected list
|
behind it, which is the design's whole idiom for "you are here" — the same two marks a selected list
|
||||||
row carries, so the window has one vocabulary for selection rather than one per control.
|
row carries, so the window has one vocabulary for selection rather than one per control.
|
||||||
-->
|
-->
|
||||||
|
<!--
|
||||||
|
A destination in the sidebar. v2 turns these from 54-pixel stacked labels into 32-pixel rows with a
|
||||||
|
glyph, a word and a count, and the active one becomes a filled rounded row rather than a label with an
|
||||||
|
accent bar down its left edge. The bar is gone because a filled row at this width already reads as
|
||||||
|
chosen, and the bar was carrying that on its own when there was no room for a fill.
|
||||||
|
-->
|
||||||
<Style Selector="Button.nav">
|
<Style Selector="Button.nav">
|
||||||
<Setter Property="Padding" Value="0,15" />
|
<Setter Property="Height" Value="32" />
|
||||||
|
<Setter Property="Padding" Value="10,0" />
|
||||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
<Setter Property="FontSize" Value="9" />
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<Setter Property="FontSize" Value="12" />
|
||||||
<Setter Property="LetterSpacing" Value="1" />
|
<Setter Property="FontWeight" Value="Medium" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.nav /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="Button.nav /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||||
<Setter Property="BorderBrush" Value="Transparent" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="BorderThickness" Value="2,0,0,0" />
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.nav:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="Button.nav:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="Background" Value="{StaticResource Hover}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.nav.active /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="Button.nav.active /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||||
<Setter Property="Background" Value="{StaticResource AccentWash}" />
|
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
A terminal tab. The active one is marked along its top edge rather than its left, because the strip
|
The three parts of a sidebar row. Separate classes rather than inline setters because nine rows draw
|
||||||
runs horizontally and a left bar on a row of tabs reads as a divider between them.
|
them and the day one of the three moves is the day eight of them would not have.
|
||||||
|
|
||||||
|
The glyph and the label inherit the row's own foreground, so they light with it; the count does not,
|
||||||
|
because a number that lit with its row would compete with the word beside it for the same emphasis.
|
||||||
|
-->
|
||||||
|
<Style Selector="TextBlock.navicon">
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="Width" Value="20" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.navlabel">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.navcount">
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
|
<Setter Property="FontSize" Value="10" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A terminal tab, and since v2 a pill rather than a filing-cabinet tab: 30 tall inside a 42 strip, its
|
||||||
|
own rounded outline, and the active one filled instead of marked along an edge. The edge mark is gone
|
||||||
|
because a pill has no edge to share with its neighbour — the gap between two of them is the divider
|
||||||
|
the old top-and-right border was standing in for.
|
||||||
-->
|
-->
|
||||||
<Style Selector="Button.tab">
|
<Style Selector="Button.tab">
|
||||||
<!-- Less on the right than the left: the close box lives inside the tab and brings its own margin. -->
|
<!-- Less on the right than the left: the close box lives inside the tab and brings its own margin. -->
|
||||||
<Setter Property="Padding" Value="12,0,7,0" />
|
<Setter Property="Padding" Value="12,0,8,0" />
|
||||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
<Setter Property="Height" Value="30" />
|
||||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
<Setter Property="Margin" Value="0,0,6,0" />
|
||||||
<Setter Property="FontSize" Value="10.5" />
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
|
<Setter Property="FontSize" Value="12" />
|
||||||
<Setter Property="FontWeight" Value="Medium" />
|
<Setter Property="FontWeight" Value="Medium" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The button that opens a connection. A tab in every respect but the marks a tab carries: no active
|
The button that opens a connection. A tab in every respect but the marks a tab carries: no active
|
||||||
state, because it is never the thing showing, and no right border, because it is not separating
|
state, because it is never the thing showing, and no outline, because it is not one of the things
|
||||||
itself from anything.
|
being chosen between.
|
||||||
-->
|
-->
|
||||||
<Style Selector="Button.tab.plus">
|
<Style Selector="Button.tab.plus">
|
||||||
<Setter Property="Padding" Value="0" />
|
<Setter Property="Padding" Value="0" />
|
||||||
|
<Setter Property="Width" Value="28" />
|
||||||
|
<Setter Property="Height" Value="28" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.tab.plus /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="Button.tab.plus /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
<Setter Property="BorderThickness" Value="0,2,0,0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="BorderBrush" Value="Transparent" />
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.tab.plus:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="Button.tab.plus:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
<Setter Property="Background" Value="{StaticResource Raised}" />
|
<Setter Property="Background" Value="{StaticResource Hover}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.tab /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="Button.tab /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtle}" />
|
<Setter Property="Background" Value="{StaticResource Panel}" />
|
||||||
<Setter Property="BorderThickness" Value="0,2,1,0" />
|
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.tab.active /template/ ContentPresenter#PART_ContentPresenter">
|
<Style Selector="Button.tab.active /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
<Setter Property="Background" Value="{StaticResource Canvas}" />
|
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
<Setter Property="BorderThickness" Value="0,2,0,0" />
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -8,16 +8,23 @@
|
|||||||
Title="DodoSSH"
|
Title="DodoSSH"
|
||||||
Width="1180"
|
Width="1180"
|
||||||
Height="760"
|
Height="760"
|
||||||
MinWidth="880"
|
MinWidth="1016"
|
||||||
MinHeight="560"
|
MinHeight="574"
|
||||||
Background="{StaticResource Canvas}"
|
Background="{StaticResource Canvas}"
|
||||||
SystemDecorations="BorderOnly"
|
SystemDecorations="BorderOnly"
|
||||||
Focusable="True">
|
Focusable="True">
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The shell window: a titlebar it draws itself, a nav rail, a tab strip, one surface at a time, and a
|
The shell window: a titlebar it draws itself, a sidebar, a tab strip, one surface at a time, and a
|
||||||
status bar.
|
status bar.
|
||||||
|
|
||||||
|
THE MINIMUM GREW, and it grew by exactly what v2 added rather than by a round number somebody liked.
|
||||||
|
The sidebar went from 54 pixels to 190 and the chrome from 72 tall to 86, so 880x560 became 1016x574 —
|
||||||
|
which leaves every screen the same 826x464 it was designed against. That is the whole point of moving
|
||||||
|
it: the layout suite's premise is that everything fits at the minimum, and four of the tables stop
|
||||||
|
fitting at 690 wide. Widening the sidebar without widening the window would have quietly broken them
|
||||||
|
somewhere nobody was looking.
|
||||||
|
|
||||||
Windows is asked for a resize border and nothing else, so TitleBar does the dragging, the maximising and
|
Windows is asked for a resize border and nothing else, so TitleBar does the dragging, the maximising and
|
||||||
the closing. That is a real cost, and the reason it is paid is that a stock grey system bar above a
|
the closing. That is a real cost, and the reason it is paid is that a stock grey system bar above a
|
||||||
near-black application is the one part of the window that would look borrowed.
|
near-black application is the one part of the window that would look borrowed.
|
||||||
@@ -105,6 +112,17 @@
|
|||||||
<views:TransfersScreen DataContext="{Binding Transfers}" />
|
<views:TransfersScreen DataContext="{Binding Transfers}" />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
<!-- ============ S3 ============ -->
|
||||||
|
<!--
|
||||||
|
The same screen as FILES above, over the same view model, because an object store and an
|
||||||
|
SFTP host are both an IRemoteFileStore and everything below the picker was written once.
|
||||||
|
What differs is which picker is offered, and that is decided by the destination rather than
|
||||||
|
by a toggle inside the screen — see ShowFiles, and the sidebar entry that calls it.
|
||||||
|
-->
|
||||||
|
<Panel IsVisible="{Binding IsBucketsScreen}">
|
||||||
|
<views:TransfersScreen DataContext="{Binding Transfers}" />
|
||||||
|
</Panel>
|
||||||
|
|
||||||
<!-- ============ KEYCHAIN ============ -->
|
<!-- ============ KEYCHAIN ============ -->
|
||||||
<!--
|
<!--
|
||||||
Wrapped rather than bound directly, for the reason the vault column always was: this
|
Wrapped rather than bound directly, for the reason the vault column always was: this
|
||||||
|
|||||||
@@ -5,13 +5,28 @@
|
|||||||
x:DataType="vm:MainWindowViewModel">
|
x:DataType="vm:MainWindowViewModel">
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Six destinations down the left edge.
|
The window's destinations, down the left edge.
|
||||||
|
|
||||||
One of them — TEAM — reaches a screen that says it is not built. It is in the rail anyway rather than
|
Still called NavRail although v2 makes it 190 pixels wide and gives every entry a word: the type is
|
||||||
dropped, and the reasoning is in ShellScreen: the milestones are public, the screen behind it says
|
named in MainWindow and in the layout suite, and "the list of places this window goes" is what it was
|
||||||
plainly what is missing, and a rail that quietly had four entries would make sharing look like a change
|
called when it was 54 pixels and is what it still is. The width is the thing that changed, not the job.
|
||||||
of product rather than the next milestone. It is recorded in docs/design-import-gaps.md. FILES was the
|
|
||||||
other one until M2 built it.
|
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
Three things the extra 136 pixels buy, and they are why the design widened it rather than a matter of
|
||||||
|
taste. The five-character abbreviations are gone — PINS and SNIPS were a width constraint and are now
|
||||||
|
Pins and Snippets. Each entry carries a glyph, so the list can be scanned by shape as well as read. And
|
||||||
|
each carries a count, which is the one genuinely new fact: how many hosts, how many keys, how many pins
|
||||||
|
is a question you would otherwise have to open the screen to answer.
|
||||||
|
|
||||||
|
A count is drawn only where one is real. SFTP, Logs and Preferences have none — a transfer queue's depth
|
||||||
|
is not "how many files this screen holds", a log has no total until it is read, and preferences are not
|
||||||
|
counted — so those three show nothing rather than a zero. The design draws a number on every row; a zero
|
||||||
|
beside Logs would be a fact this application never computed.
|
||||||
|
|
||||||
|
One of the destinations — TEAM — reaches a screen that says it is not built. It is in the list anyway
|
||||||
|
rather than dropped, and the reasoning is in ShellScreen: the milestones are public, the screen behind it
|
||||||
|
says plainly what is missing, and a list that quietly had fewer entries would make sharing look like a
|
||||||
|
change of product rather than the next milestone. FILES was the other one until M2 built it.
|
||||||
|
|
||||||
Buttons rather than a TabStrip or a ListBox, for the same reason the vault's category rail is: all three
|
Buttons rather than a TabStrip or a ListBox, for the same reason the vault's category rail is: all three
|
||||||
of those hold the selection themselves, so a click moves the highlight before the shell can decide
|
of those hold the selection themselves, so a click moves the highlight before the shell can decide
|
||||||
@@ -19,57 +34,140 @@
|
|||||||
|
|
||||||
Lit from IsXShowing and not from IsXScreen, which are different questions now that the tab strip spans
|
Lit from IsXShowing and not from IsXScreen, which are different questions now that the tab strip spans
|
||||||
every screen. A terminal opened from here leaves Screen on Hosts — deliberately, so closing the tab comes
|
every screen. A terminal opened from here leaves Screen on Hosts — deliberately, so closing the tab comes
|
||||||
back — and a rail entry lit while a terminal filled the window would be pointing at a screen that is not
|
back — and an entry lit while a terminal filled the window would be pointing at a screen that is not
|
||||||
showing. So nothing here is lit at all while a terminal is up: the selected tab already carries that
|
showing. So nothing here is lit at all while a terminal is up: the selected tab already carries that
|
||||||
mark, in the strip, and two "you are here" marks is one too many.
|
mark, in the strip, and two "you are here" marks is one too many.
|
||||||
|
|
||||||
|
The design pins a "Team vault" card to the foot of this list, beside a Team entry in the list itself.
|
||||||
|
Two routes to one screen, one of them carrying a seat count nothing here can produce, so what sits at
|
||||||
|
the foot is Preferences — which is where it already was, and which is the one entry that is about the
|
||||||
|
machine rather than about the keychain.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Border Width="54" Background="{StaticResource Chrome}"
|
<Border Width="190" Background="{StaticResource Sidebar}"
|
||||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,1,0">
|
BorderBrush="{StaticResource Border}" BorderThickness="0,0,1,0">
|
||||||
<DockPanel LastChildFill="False">
|
<DockPanel LastChildFill="False">
|
||||||
|
|
||||||
<StackPanel DockPanel.Dock="Top" Margin="0,8,0,0">
|
<StackPanel DockPanel.Dock="Top" Margin="8,10,8,0" Spacing="2">
|
||||||
<Button Classes="flat nav" Content="HOSTS" Classes.active="{Binding IsHostsShowing}"
|
|
||||||
|
<Button Classes="flat nav" Classes.active="{Binding IsHostsShowing}"
|
||||||
Command="{Binding ShowScreenCommand}"
|
Command="{Binding ShowScreenCommand}"
|
||||||
CommandParameter="{x:Static vm:ShellScreen.Hosts}"
|
CommandParameter="{x:Static vm:ShellScreen.Hosts}"
|
||||||
ToolTip.Tip="Your hosts, and what is known about the one you have selected" />
|
ToolTip.Tip="Your hosts, and what is known about the one you have selected">
|
||||||
<Button Classes="flat nav" Content="FILES" Classes.active="{Binding IsTransfersShowing}"
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
Command="{Binding ShowScreenCommand}"
|
<TextBlock Grid.Column="0" Classes="navicon" Text="▤" />
|
||||||
CommandParameter="{x:Static vm:ShellScreen.Transfers}"
|
<TextBlock Grid.Column="1" Classes="navlabel" Text="Hosts" />
|
||||||
ToolTip.Tip="Move files to and from a host over SFTP" />
|
<TextBlock Grid.Column="2" Classes="navcount" Text="{Binding Vault.Hosts.Count}" />
|
||||||
<Button Classes="flat nav" Content="KEYS" Classes.active="{Binding IsVaultShowing}"
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="flat nav" Classes.active="{Binding IsTransfersShowing}"
|
||||||
|
Command="{Binding ShowFilesCommand}"
|
||||||
|
CommandParameter="{x:Static vm:RemoteKind.Host}"
|
||||||
|
ToolTip.Tip="Move files to and from a host over SFTP">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Classes="navicon" Text="⇅" />
|
||||||
|
<TextBlock Grid.Column="1" Classes="navlabel" Text="SFTP" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Buckets are their own destination now, as they are on the phone, rather than a toggle inside the
|
||||||
|
files screen. Same screen behind both — an object store and an SFTP host are both an
|
||||||
|
IRemoteFileStore — and the entry chosen is what decides which picker is offered. See ShowFiles.
|
||||||
|
-->
|
||||||
|
<Button Classes="flat nav" Classes.active="{Binding IsBucketsShowing}"
|
||||||
|
Command="{Binding ShowFilesCommand}"
|
||||||
|
CommandParameter="{x:Static vm:RemoteKind.Bucket}"
|
||||||
|
ToolTip.Tip="Objects in an S3-compatible bucket from your keychain">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Classes="navicon" Text="◳" />
|
||||||
|
<TextBlock Grid.Column="1" Classes="navlabel" Text="S3" />
|
||||||
|
<TextBlock Grid.Column="2" Classes="navcount" Text="{Binding Vault.ObjectStores.Count}" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
TotalItemCount is keys plus passwords and deliberately excludes buckets, which used to disagree
|
||||||
|
with the list under it. It no longer does: buckets have their own entry above, so this number and
|
||||||
|
this screen now count the same things.
|
||||||
|
-->
|
||||||
|
<Button Classes="flat nav" Classes.active="{Binding IsVaultShowing}"
|
||||||
Command="{Binding ShowScreenCommand}"
|
Command="{Binding ShowScreenCommand}"
|
||||||
CommandParameter="{x:Static vm:ShellScreen.Vault}"
|
CommandParameter="{x:Static vm:ShellScreen.Vault}"
|
||||||
ToolTip.Tip="Your keychain: SSH keys, stored passwords, and the host keys you have approved" />
|
ToolTip.Tip="Your keychain: SSH keys and stored passwords">
|
||||||
<!--
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
PINS, not HOST KEYS. The rail is 54 pixels wide at mono FontSize 9, which is five characters —
|
<TextBlock Grid.Column="0" Classes="navicon" Text="⚿" />
|
||||||
and "pins" is what this codebase calls them everywhere else anyway.
|
<TextBlock Grid.Column="1" Classes="navlabel" Text="Keychain" />
|
||||||
-->
|
<TextBlock Grid.Column="2" Classes="navcount" Text="{Binding Vault.TotalItemCount}" />
|
||||||
<Button Classes="flat nav" Content="PINS" Classes.active="{Binding IsKnownHostsShowing}"
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="flat nav" Classes.active="{Binding IsKnownHostsShowing}"
|
||||||
Command="{Binding ShowScreenCommand}"
|
Command="{Binding ShowScreenCommand}"
|
||||||
CommandParameter="{x:Static vm:ShellScreen.KnownHosts}"
|
CommandParameter="{x:Static vm:ShellScreen.KnownHosts}"
|
||||||
ToolTip.Tip="Host keys you have approved, and how to withdraw one" />
|
ToolTip.Tip="Host keys you have approved, and how to withdraw one">
|
||||||
<!-- SNIPS, for the same five-character reason as PINS above. -->
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
<Button Classes="flat nav" Content="SNIPS" Classes.active="{Binding IsSnippetsShowing}"
|
<TextBlock Grid.Column="0" Classes="navicon" Text="◈" />
|
||||||
|
<TextBlock Grid.Column="1" Classes="navlabel" Text="Pins" />
|
||||||
|
<!--
|
||||||
|
The vault's pins, not the screen's VisiblePins — that one is the filtered list, so a sidebar
|
||||||
|
bound to it would count what the Pins screen's own filter box happens to match and would
|
||||||
|
change as somebody typed in it. Every other count here is a total; this one has to be too.
|
||||||
|
-->
|
||||||
|
<TextBlock Grid.Column="2" Classes="navcount"
|
||||||
|
Text="{Binding Vault.KnownHostPins.Count}" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="flat nav" Classes.active="{Binding IsSnippetsShowing}"
|
||||||
Command="{Binding ShowScreenCommand}"
|
Command="{Binding ShowScreenCommand}"
|
||||||
CommandParameter="{x:Static vm:ShellScreen.Snippets}"
|
CommandParameter="{x:Static vm:ShellScreen.Snippets}"
|
||||||
ToolTip.Tip="Commands you have saved, and how to put one into a terminal" />
|
ToolTip.Tip="Commands you have saved, and how to put one into a terminal">
|
||||||
<!-- LOGS, four characters, so it needs no abbreviating at all. -->
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
<Button Classes="flat nav" Content="LOGS" Classes.active="{Binding IsLogsShowing}"
|
<TextBlock Grid.Column="0" Classes="navicon" Text="❯" />
|
||||||
|
<TextBlock Grid.Column="1" Classes="navlabel" Text="Snippets" />
|
||||||
|
<TextBlock Grid.Column="2" Classes="navcount" Text="{Binding Vault.Snippets.Count}" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="flat nav" Classes.active="{Binding IsLogsShowing}"
|
||||||
Command="{Binding ShowScreenCommand}"
|
Command="{Binding ShowScreenCommand}"
|
||||||
CommandParameter="{x:Static vm:ShellScreen.Logs}"
|
CommandParameter="{x:Static vm:ShellScreen.Logs}"
|
||||||
ToolTip.Tip="What has been connected to, and what has been changed in this keychain" />
|
ToolTip.Tip="What has been connected to, and what has been changed in this keychain">
|
||||||
<Button Classes="flat nav" Content="TEAM" Classes.active="{Binding IsTeamShowing}"
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Classes="navicon" Text="≡" />
|
||||||
|
<TextBlock Grid.Column="1" Classes="navlabel" Text="Logs" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="flat nav" Classes.active="{Binding IsTeamShowing}"
|
||||||
Command="{Binding ShowScreenCommand}"
|
Command="{Binding ShowScreenCommand}"
|
||||||
CommandParameter="{x:Static vm:ShellScreen.Team}"
|
CommandParameter="{x:Static vm:ShellScreen.Team}"
|
||||||
ToolTip.Tip="Shared keychains and the people in them. Not built yet — see the screen for what is missing." />
|
ToolTip.Tip="Shared keychains and the people in them">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Classes="navicon" Text="◎" />
|
||||||
|
<!--
|
||||||
|
No count. Teams are read from the server when the screen is opened, not on unlock, so this
|
||||||
|
would read 0 until somebody had already been there — which is the one number on this list
|
||||||
|
that would be a statement rather than a blank.
|
||||||
|
-->
|
||||||
|
<TextBlock Grid.Column="1" Classes="navlabel" Text="Teams" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button DockPanel.Dock="Bottom" Classes="flat nav" Content="PREFS"
|
<Button DockPanel.Dock="Bottom" Classes="flat nav" Margin="8,0,8,10"
|
||||||
Classes.active="{Binding IsPreferencesShowing}"
|
Classes.active="{Binding IsPreferencesShowing}"
|
||||||
Command="{Binding ShowScreenCommand}"
|
Command="{Binding ShowScreenCommand}"
|
||||||
CommandParameter="{x:Static vm:ShellScreen.Preferences}"
|
CommandParameter="{x:Static vm:ShellScreen.Preferences}"
|
||||||
ToolTip.Tip="Preferences, and this machine's device key" />
|
ToolTip.Tip="Preferences, and this machine's device key">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Classes="navicon" Text="⚙" />
|
||||||
|
<TextBlock Grid.Column="1" Classes="navlabel" Text="Preferences" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -34,7 +34,9 @@
|
|||||||
BorderThickness="1" CornerRadius="4" Padding="10,8"
|
BorderThickness="1" CornerRadius="4" Padding="10,8"
|
||||||
IsVisible="{Binding HasLiveSessions, FallbackValue=False}">
|
IsVisible="{Binding HasLiveSessions, FallbackValue=False}">
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="{Binding LiveSessionSummary}" Foreground="{StaticResource Info}"
|
<!-- Live, not Info: this counts shells that are still running, which is the fact the host list,
|
||||||
|
the tab strip and the phone's own lock screen all paint green. -->
|
||||||
|
<TextBlock Text="{Binding LiveSessionSummary}" Foreground="{StaticResource Live}"
|
||||||
FontWeight="SemiBold" TextWrapping="Wrap" />
|
FontWeight="SemiBold" TextWrapping="Wrap" />
|
||||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||||
Text="Signing out does not close them, exactly as locking does not. Quit DodoSSH to end them." />
|
Text="Signing out does not close them, exactly as locking does not. Quit DodoSSH to end them." />
|
||||||
|
|||||||
@@ -15,11 +15,21 @@
|
|||||||
The last message is the load-bearing one, and it is why this bar is worth having at all: the vault's
|
The last message is the load-bearing one, and it is why this bar is worth having at all: the vault's
|
||||||
status line used to be a cramped label in the account bar, and everything the application has to say
|
status line used to be a cramped label in the account bar, and everything the application has to say
|
||||||
about a save, a sync, a refusal or a conflict goes through it.
|
about a save, a sync, a refusal or a conflict goes through it.
|
||||||
|
|
||||||
|
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
The design has no status bar. Two of the three things this one carried moved into the titlebar with it —
|
||||||
|
the sync word is up there beside its dot, and the shortcut hint is inside the search box that uses it —
|
||||||
|
so those two are gone from here rather than printed twice.
|
||||||
|
|
||||||
|
The third did not move, and is why this bar survives the design that deletes it. Vault.Status is the one
|
||||||
|
channel this application has for saying that a save failed, that a sync was refused, that a merge picked
|
||||||
|
a winner. The design is a mock-up of a working afternoon and has nowhere for a sentence like that to go;
|
||||||
|
dropping the bar would have meant dropping the sentence, or repeating it on six screens.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Border Height="24" Background="{StaticResource Chrome}"
|
<Border Height="24" Background="{StaticResource Chrome}"
|
||||||
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0">
|
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" Margin="12,0">
|
<Grid ColumnDefinitions="Auto,*" Margin="14,0">
|
||||||
|
|
||||||
<!-- What is connected, and where. Absent rather than empty when nothing is. -->
|
<!-- What is connected, and where. Absent rather than empty when nothing is. -->
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="5" VerticalAlignment="Center"
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="5" VerticalAlignment="Center"
|
||||||
@@ -37,14 +47,6 @@
|
|||||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
|
||||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding SyncLabel}" FontSize="9.5"
|
|
||||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" Margin="14,0,0,0"
|
|
||||||
IsVisible="{Binding IsUnlocked}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Column="3" Classes="mono" Text="CTRL K SEARCH" FontSize="9.5"
|
|
||||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" Margin="14,0,0,0"
|
|
||||||
IsVisible="{Binding IsUnlocked}" />
|
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,17 @@
|
|||||||
An ItemsControl of buttons rather than a TabStrip, because the selection lives on the shell — a tab
|
An ItemsControl of buttons rather than a TabStrip, because the selection lives on the shell — a tab
|
||||||
outlives the vault that opened it — and a strip that owned its own selection would be a second copy of
|
outlives the vault that opened it — and a strip that owned its own selection would be a second copy of
|
||||||
that state.
|
that state.
|
||||||
|
|
||||||
|
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
Tabs became pills: taller, rounded, each with its own outline, on the sidebar's surface rather than the
|
||||||
|
chrome's. The design also puts a "Hosts" pill at the head of this strip and hides the sidebar while a
|
||||||
|
session is showing, so that pill is the only way back. This window keeps the sidebar up instead — it is
|
||||||
|
beside both surfaces rather than inside one — so the way back from a terminal is every destination at
|
||||||
|
once rather than a single pill leading to one of them. That makes the design's pill redundant, and a
|
||||||
|
redundant control on a strip this narrow is one the user has to rule out before finding the tabs.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Border Height="34" Background="{StaticResource Chrome}"
|
<Border Height="42" Background="{StaticResource Sidebar}"
|
||||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -17,20 +17,28 @@
|
|||||||
overlapping it would be painted underneath and its close button would not be clickable.
|
overlapping it would be painted underneath and its close button would not be clickable.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Border Height="38" Background="{StaticResource Chrome}"
|
<Border Height="44" Background="{StaticResource Chrome}"
|
||||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1"
|
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1"
|
||||||
PointerPressed="OnDrag" DoubleTapped="OnToggleMaximised">
|
PointerPressed="OnDrag" DoubleTapped="OnToggleMaximised">
|
||||||
|
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="12,0,10,0">
|
<Grid ColumnDefinitions="Auto,*,Auto" Margin="14,0,10,0">
|
||||||
|
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
<Border Width="20" Height="20" BorderBrush="{StaticResource Accent}" BorderThickness="1">
|
<!--
|
||||||
<TextBlock Classes="mono" Text=">_" FontSize="9" FontWeight="SemiBold"
|
Filled rather than outlined since v2, and rounded. The same mark the phone's header draws and the
|
||||||
Foreground="{StaticResource Accent}"
|
same one the launcher icon carries, so the three cannot drift.
|
||||||
|
-->
|
||||||
|
<Border Width="20" Height="20" CornerRadius="6" Background="{StaticResource Accent}">
|
||||||
|
<TextBlock Classes="mono" Text=">_" FontSize="9" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource AccentInk}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</Border>
|
</Border>
|
||||||
<TextBlock Classes="mono" Text="DodoSSH" FontSize="12" FontWeight="SemiBold"
|
<!--
|
||||||
LetterSpacing="0.5" Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
The product's name is the one string in this bar that is not machine-shaped, so v2 sets it in the
|
||||||
|
sans face while the address, the account and the fingerprint beside it stay monospaced.
|
||||||
|
-->
|
||||||
|
<TextBlock Text="DodoSSH" FontSize="13" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
||||||
<!--
|
<!--
|
||||||
The design puts an organisation here — "dodotech / platform". There are no organisations: the
|
The design puts an organisation here — "dodotech / platform". There are no organisations: the
|
||||||
server has team tables and no endpoint that reads them, so the only name this application can
|
server has team tables and no endpoint that reads them, so the only name this application can
|
||||||
@@ -50,18 +58,25 @@
|
|||||||
command", and there is no snippet or saved-command item type for it to run. Clicking it is the same
|
command", and there is no snippet or saved-command item type for it to run. Clicking it is the same
|
||||||
as Ctrl+K, which is what the window binds; the design says ⌘K, and this is a Windows build.
|
as Ctrl+K, which is what the window binds; the design says ⌘K, and this is a Windows build.
|
||||||
-->
|
-->
|
||||||
<Button Grid.Column="1" Classes="flat" MaxWidth="420" Height="24" Margin="16,0"
|
<Button Grid.Column="1" Classes="flat" MaxWidth="380" Height="28" Margin="16,0"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
HorizontalAlignment="Center" HorizontalContentAlignment="Stretch"
|
||||||
Command="{Binding ToggleSearchCommand}" IsEnabled="{Binding IsUnlocked}">
|
Command="{Binding ToggleSearchCommand}" IsEnabled="{Binding IsUnlocked}">
|
||||||
<Border Background="{StaticResource Field}" BorderBrush="{StaticResource Border}"
|
<Border Background="{StaticResource Field}" BorderBrush="{StaticResource BorderMid}"
|
||||||
BorderThickness="1" CornerRadius="4" Padding="8,0">
|
BorderThickness="1" CornerRadius="8" Padding="10,0" Width="380">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
<TextBlock Grid.Column="0" Classes="mono" Text=">" FontSize="10"
|
<TextBlock Grid.Column="0" Text="⌕" FontSize="12"
|
||||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||||
<TextBlock Grid.Column="1" Classes="mono" Text="search hosts" FontSize="10.5" Margin="8,0"
|
<!--
|
||||||
|
"Search or connect…", which is what it does: the palette connects on Enter. Not the design's
|
||||||
|
wider promise of running a command — a snippet is inserted from its own screen, and a box that
|
||||||
|
offered to run one would be offering something this palette does not do.
|
||||||
|
-->
|
||||||
|
<TextBlock Grid.Column="1" Text="Search or connect…" FontSize="12" Margin="8,0"
|
||||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||||
<Border Grid.Column="2" Classes="chip" Padding="5,1">
|
<Border Grid.Column="2" BorderBrush="{StaticResource BorderMid}" BorderThickness="1"
|
||||||
<TextBlock Text="CTRL K" FontSize="9" Foreground="{StaticResource TextFaint}" />
|
CornerRadius="4" Padding="5,1" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="mono" Text="CTRL K" FontSize="9"
|
||||||
|
Foreground="{StaticResource TextFaint}" />
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -77,25 +77,24 @@
|
|||||||
<Border Grid.Row="0" Padding="14,0" BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
<Border Grid.Row="0" Padding="14,0" BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||||
<Grid ColumnDefinitions="Auto,Auto,180,Auto,Auto,Auto,*" VerticalAlignment="Center">
|
<Grid ColumnDefinitions="Auto,Auto,180,Auto,Auto,Auto,*" VerticalAlignment="Center">
|
||||||
|
|
||||||
<TextBlock Grid.Column="0" Classes="mono" Text="FILES" FontSize="11" FontWeight="SemiBold"
|
<!--
|
||||||
|
The screen names whichever remote it is offering, because since v2 it is reached as two
|
||||||
|
destinations rather than one: SFTP and S3 are separate entries in the sidebar and this control
|
||||||
|
draws both. A single "FILES" heading over a bucket picker would name neither of them.
|
||||||
|
-->
|
||||||
|
<TextBlock Grid.Column="0" Classes="mono" Text="SFTP" FontSize="11" FontWeight="SemiBold"
|
||||||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||||||
Margin="0,0,12,0" />
|
Margin="0,0,12,0" IsVisible="{Binding ShowsHostPicker}" />
|
||||||
|
<TextBlock Grid.Column="0" Classes="mono" Text="S3" FontSize="11" FontWeight="SemiBold"
|
||||||
|
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||||||
|
Margin="0,0,12,0" IsVisible="{Binding ShowsBucketPicker}" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Which sort of remote. Two buttons rather than one picker holding hosts and buckets together, and
|
The HOST / BUCKET pair that used to sit here is gone: which sort of remote this screen offers is
|
||||||
the reason is that the two are not interchangeable: a host brings a password box, a host key
|
now the destination you chose in the sidebar, and a toggle that silently moved you to the other
|
||||||
prompt and a mismatch refusal with it, and a bucket has no equivalent of any of them. One picker
|
one would leave the lit sidebar entry naming a screen you are no longer on. What sets it is
|
||||||
would mean half this bar appearing and disappearing with the selection.
|
ShowFiles on the shell, which is what the sidebar calls.
|
||||||
-->
|
-->
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="2" Margin="0,0,8,0"
|
|
||||||
IsVisible="{Binding !IsConnected}">
|
|
||||||
<Button Classes="flat cat" Content="HOST" Classes.active="{Binding ShowsHostPicker}"
|
|
||||||
Command="{Binding ShowRemoteCommand}"
|
|
||||||
CommandParameter="{x:Static vm:RemoteKind.Host}" />
|
|
||||||
<Button Classes="flat cat" Content="BUCKET" Classes.active="{Binding ShowsBucketPicker}"
|
|
||||||
Command="{Binding ShowRemoteCommand}"
|
|
||||||
CommandParameter="{x:Static vm:RemoteKind.Bucket}" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<ComboBox Grid.Column="2" ItemsSource="{Binding Hosts}"
|
<ComboBox Grid.Column="2" ItemsSource="{Binding Hosts}"
|
||||||
SelectedItem="{Binding SelectedHost}"
|
SelectedItem="{Binding SelectedHost}"
|
||||||
@@ -155,7 +154,13 @@
|
|||||||
<TextBlock Text="{Binding ConnectedTo}" />
|
<TextBlock Text="{Binding ConnectedTo}" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<TextBlock Grid.Column="5" Classes="hint" Text="{Binding Status}" FontSize="10.5"
|
<!--
|
||||||
|
Column 6, not 5, which is where it used to be — sharing a cell with the connected chip, so the two
|
||||||
|
drew over each other for as long as anything was open. That was survivable while the status was
|
||||||
|
mostly read before connecting; it is not now, because refusing to switch between SFTP and S3 while
|
||||||
|
a session is live reports itself here, which is precisely when the chip is on screen.
|
||||||
|
-->
|
||||||
|
<TextBlock Grid.Column="6" Classes="hint" Text="{Binding Status}" FontSize="10.5"
|
||||||
Margin="12,0,0,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
Margin="12,0,0,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
||||||
TextWrapping="NoWrap" />
|
TextWrapping="NoWrap" />
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,9 @@
|
|||||||
BorderThickness="1" CornerRadius="4" Padding="10,8"
|
BorderThickness="1" CornerRadius="4" Padding="10,8"
|
||||||
IsVisible="{Binding HasLiveSessions, FallbackValue=False}">
|
IsVisible="{Binding HasLiveSessions, FallbackValue=False}">
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="{Binding LiveSessionSummary}" Foreground="{StaticResource Info}"
|
<!-- Live, not Info: this counts shells that are still running, which is the fact the host list,
|
||||||
|
the tab strip and the phone's own lock screen all paint green. -->
|
||||||
|
<TextBlock Text="{Binding LiveSessionSummary}" Foreground="{StaticResource Live}"
|
||||||
FontWeight="SemiBold" TextWrapping="Wrap" />
|
FontWeight="SemiBold" TextWrapping="Wrap" />
|
||||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||||
Text="Locking closes the keychain, not your terminals: a job you started keeps running, and its output is waiting behind this screen. It also means this machine still holds an open, authenticated channel to those hosts — locked describes the keychain, not the connections. Quit DodoSSH to end them." />
|
Text="Locking closes the keychain, not your terminals: a job you started keeps running, and its output is waiting behind this screen. It also means this machine still holds an open, authenticated channel to those hosts — locked describes the keychain, not the connections. Quit DodoSSH to end them." />
|
||||||
|
|||||||
@@ -2515,17 +2515,23 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ShowFiles(RemoteKind kind)
|
private void ShowFiles(RemoteKind kind)
|
||||||
{
|
{
|
||||||
|
// Refusing means staying put, not arriving somewhere and saying no. Moving Screen anyway would put
|
||||||
|
// the S3 entry in the sidebar over a screen still listing an SFTP host — two pieces of chrome
|
||||||
|
// disagreeing about where you are, which is worse than the navigation simply not happening.
|
||||||
if (Transfers.IsConnected && Transfers.Remote != kind)
|
if (Transfers.IsConnected && Transfers.Remote != kind)
|
||||||
{
|
{
|
||||||
Transfers.Status = kind is RemoteKind.Bucket
|
Transfers.Status = kind is RemoteKind.Bucket
|
||||||
? "An SFTP session is open. Close it before opening a bucket."
|
? "An SFTP session is open. Close it before opening a bucket."
|
||||||
: "A bucket is open. Close it before connecting to a host.";
|
: "A bucket is open. Close it before connecting to a host.";
|
||||||
}
|
|
||||||
else
|
// Still show the screen the open session belongs to, so the message is somewhere it can be
|
||||||
{
|
// read — the button that was pressed is in the sidebar, which is on screen either way.
|
||||||
Transfers.Remote = kind;
|
Screen = Transfers.Remote is RemoteKind.Bucket ? ShellScreen.Buckets : ShellScreen.Transfers;
|
||||||
|
Surface = ShellSurface.Page;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Transfers.Remote = kind;
|
||||||
Screen = kind is RemoteKind.Bucket ? ShellScreen.Buckets : ShellScreen.Transfers;
|
Screen = kind is RemoteKind.Bucket ? ShellScreen.Buckets : ShellScreen.Transfers;
|
||||||
Surface = ShellSurface.Page;
|
Surface = ShellSurface.Page;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -557,9 +557,10 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
|||||||
TypedPassword = string.Empty;
|
TypedPassword = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Shows one of the two kinds of remote in the picker.</summary>
|
// ShowRemoteCommand was here, and it went with the toggle that invoked it. Which kind of remote this
|
||||||
[RelayCommand]
|
// screen offers is a destination now rather than a control on the screen — both heads reach it through
|
||||||
private void ShowRemote(RemoteKind kind) => Remote = kind;
|
// MainWindowViewModel.ShowFiles, which sets Remote directly because it has a refusal to make first.
|
||||||
|
// Keeping the command would have left one nothing could invoke.
|
||||||
|
|
||||||
/// <summary>Opens the chosen remote, whichever kind it is.</summary>
|
/// <summary>Opens the chosen remote, whichever kind it is.</summary>
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
@@ -31,28 +31,28 @@ internal static class LayoutHarness
|
|||||||
/// two constants still match the XAML, so the harness cannot quietly start measuring a window larger
|
/// two constants still match the XAML, so the harness cannot quietly start measuring a window larger
|
||||||
/// than the one a user is allowed to drag to.
|
/// than the one a user is allowed to drag to.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal const double MinimumWidth = 880;
|
internal const double MinimumWidth = 1016;
|
||||||
|
|
||||||
/// <inheritdoc cref="MinimumWidth" />
|
/// <inheritdoc cref="MinimumWidth" />
|
||||||
internal const double MinimumHeight = 560;
|
internal const double MinimumHeight = 574;
|
||||||
|
|
||||||
/// <summary>The host sidebar's fixed width, from the hosts screen's <c>ColumnDefinitions</c>.</summary>
|
/// <summary>The host sidebar's fixed width, from the hosts screen's <c>ColumnDefinitions</c>.</summary>
|
||||||
internal const double HostSidebarWidth = 268;
|
internal const double HostSidebarWidth = 268;
|
||||||
|
|
||||||
/// <summary>The nav rail's fixed width, from <c>NavRail.axaml</c>.</summary>
|
/// <summary>The nav rail's fixed width, from <c>NavRail.axaml</c>.</summary>
|
||||||
internal const double NavRailWidth = 54;
|
internal const double NavRailWidth = 190;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What the titlebar, the tab strip and the status bar take off the window before any screen gets a
|
/// What the titlebar, the tab strip and the status bar take off the window before any screen gets a
|
||||||
/// pixel.
|
/// pixel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// All three are fixed heights declared in their own markup — 38, 34 and 24 — rather than shapes that
|
/// All three are fixed heights declared in their own markup — 44, 42 and 24 — rather than shapes that
|
||||||
/// grow with their contents, which is what makes stating them here honest. Three tests hold the three
|
/// grow with their contents, which is what makes stating them here honest. Three tests hold the three
|
||||||
/// controls to those numbers, so the budget below cannot drift away from what the window actually
|
/// controls to those numbers, so the budget below cannot drift away from what the window actually
|
||||||
/// leaves.
|
/// leaves.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal const double TitleBarHeight = 38;
|
internal const double TitleBarHeight = 44;
|
||||||
|
|
||||||
/// <inheritdoc cref="TitleBarHeight" />
|
/// <inheritdoc cref="TitleBarHeight" />
|
||||||
internal const double StatusBarHeight = 24;
|
internal const double StatusBarHeight = 24;
|
||||||
@@ -63,10 +63,10 @@ internal static class LayoutHarness
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// It comes off every screen, not just the hosts screen, which is the layout consequence of the strip
|
/// It comes off every screen, not just the hosts screen, which is the layout consequence of the strip
|
||||||
/// spanning the window. The strip does not collapse when there are no tabs — a row of chrome that came
|
/// spanning the window. The strip does not collapse when there are no tabs — a row of chrome that came
|
||||||
/// and went would move every screen up and down by 34 pixels each time the last tab closed — so this is
|
/// and went would move every screen up and down by 42 pixels each time the last tab closed — so this is
|
||||||
/// a fixed cost rather than a conditional one, and the budget can be a constant.
|
/// a fixed cost rather than a conditional one, and the budget can be a constant.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal const double TerminalTabsHeight = 34;
|
internal const double TerminalTabsHeight = 42;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What a setup card leaves its contents: its maximum width, less the padding on both sides.
|
/// What a setup card leaves its contents: its maximum width, less the padding on both sides.
|
||||||
|
|||||||
@@ -905,7 +905,7 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
|||||||
/// silently dropping off the bottom would still pass every other assertion here.
|
/// silently dropping off the bottom would still pass every other assertion here.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TheNavRailHoldsEightDestinationsAtTheWindowsMinimum()
|
public async Task TheNavRailHoldsNineDestinationsAtTheWindowsMinimum()
|
||||||
{
|
{
|
||||||
await LayoutHarness.OnTheUiThreadAsync(
|
await LayoutHarness.OnTheUiThreadAsync(
|
||||||
() =>
|
() =>
|
||||||
@@ -918,17 +918,18 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
|||||||
{
|
{
|
||||||
var buttons = rail.GetVisualDescendants().OfType<Button>().ToList();
|
var buttons = rail.GetVisualDescendants().OfType<Button>().ToList();
|
||||||
|
|
||||||
buttons.Count.ShouldBe(8, "one per screen the rail reaches");
|
buttons.Count.ShouldBe(9, "one per screen the rail reaches");
|
||||||
|
|
||||||
foreach (var button in buttons)
|
foreach (var button in buttons)
|
||||||
{
|
{
|
||||||
button.Bounds.Height.ShouldBeGreaterThan(20);
|
button.Bounds.Height.ShouldBeGreaterThan(20);
|
||||||
|
|
||||||
// One pixel narrower than the rail, because the rail draws its own divider down its
|
// 190 wide, less the divider down the rail's right edge, less the 8 of inset on
|
||||||
// right edge and that comes out of the content. Stated exactly rather than as a
|
// each side that v2 gives the rows so a filled one reads as a rounded row rather
|
||||||
// lower bound: a button that stopped filling the rail would leave a dead strip
|
// than as a full-width band. Stated exactly rather than as a lower bound: a button
|
||||||
// beside every destination, which is precisely the kind of near-miss a bound hides.
|
// that stopped filling the row would leave a dead strip beside a destination, which
|
||||||
button.Bounds.Width.ShouldBe(LayoutHarness.NavRailWidth - 1);
|
// is precisely the kind of near-miss a bound hides.
|
||||||
|
button.Bounds.Width.ShouldBe(LayoutHarness.NavRailWidth - 1 - 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
LayoutHarness.Unreachable(window).ShouldBeEmpty();
|
LayoutHarness.Unreachable(window).ShouldBeEmpty();
|
||||||
|
|||||||
Reference in New Issue
Block a user