Files
DodoSSH/src/DodoSSH.Client.Android/Views/PhoneShell.axaml
T
jaap-jan 5b32c76c95
ci / build and test (push) Successful in 3m49s
ci / android head (push) Successful in 3m55s
ci / api image (push) Successful in 6s
ci / desktop nightly (push) Successful in 1m22s
Android: the redesign — a Vaults tab with hosts, keychain and snippets, card lists, a keychain that can add, and files that act on what is chosen
The bottom bar is Vaults · Connections · Settings, drawn as Material 3 pills with icons from the embedded
Material font and a count of open sessions on Connections, which replaces the strip of shells above the
bar. Vaults is the open vault: its header carries the name as a switch to the vaults screen, the sync
light and a lock icon, and a Hosts · Keychain · Snippets segmented control sits under it. The keychain
and snippets move there from Settings (IsVaultSurface lights the tab; IsMoreSurface no longer lists
them), and back from either goes to the hosts. Settings becomes an account card and grouped sections:
security, vaults and history, files, this app.

Host rows are cards with a terminal tile and the live dot on its corner. The connect sheet names the
host and its address, labels its box, focuses it as it opens, reveals the password on request, and puts
CANCEL left and CONNECT right; every two-button row on the head now confirms on the right. While a
hosts-screen sheet is up the header and bar are dimmed too and a tap on them lowers it, and the bar
stands down while the software keyboard is up.

The keychain gains a floating + and a sheet of the desktop's five adds (generate a key, paste a key, a
password, a bucket, a tag) over the editors VaultViewModel already had, drawn as a full-screen page with
labels above the boxes; a chosen item gets EDIT and COPY PUBLIC KEY beside MOVE and DELETE. Snippets get
a floating + and cards that say RUNS or INSERTS. Files trade the seven-button block for a selection bar
(save, and a ⋮ sheet with rename, move, permissions and a red delete), folder and file tiles, an upload
button, and header actions for a terminal on the same host, refresh and close; a terminal's + sheet
opens SFTP on that terminal's own host.

The theme takes the audit's visual findings: Fluent's ClipToBounds is cleared on glowing buttons so the
glow is no longer cut to a square (the desktop's Button.accent too), prose is Montserrat with mono kept
for addresses and code, disabled secondary buttons are quieter than enabled ones, Fluent's dropdown,
checkbox, switch, focus and selection brushes come from the palette, touch no longer leaves hover fills
behind, and lists, nav entries and icon buttons carry names for TalkBack.

Builds clean; checked on the emulator end to end: unlock, the three vault views, adding and deleting a
password, connecting through the new sheet, files on the terminal's host with the selection bar and
its sheet. App tests 727 of 733 pass with 5 skipped; the one failure, TheCountsAPhoneShows_
FollowEveryForwardsState, passes three runs of three alone and is untouched here. Layout tests pass.
2026-10-04 15:50:09 +02:00

440 lines
27 KiB
XML

<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
xmlns:views="using:DodoSSH.Client.Android.Views"
x:Class="DodoSSH.Client.Android.Views.PhoneShell"
x:DataType="vm:MainWindowViewModel"
Background="{StaticResource Canvas}">
<!--
The phone's single view, and the counterpart of the desktop head's MainWindow — except that this one has
no window, no nav rail, no titlebar and no status bar.
It switches on ShellState, which is the same six states the desktop has because both heads share one
state machine. What differs is only what each one draws.
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
The desktop's eight rail destinations become four in a bottom bar, and the rest live one tap deeper
behind MORE: snippets, SFTP, S3, logs, preferences — and teams, which v2 did not draw and which is
argued for on the screen itself. That is the v2 design's own arrangement, and it replaces the first
design's four, which had nothing behind them at all. Five characters was a desktop constraint and the
phone uses words.
The order is the design's rather than the rail's. Terminal sits second, beside Hosts, because those two
are the pair a session moves between; on the desktop the terminal is not a rail entry at all.
── a terminal gets the screen ─────────────────────────────────────────────────────────────────────────
The rows around the screen stand down while a shell is showing: the header and the bottom bar. Both go
through flags the control computes, and the bar's is the one that differs — it stays up on Connections
with nothing running, which is the terminal surface drawing a page rather than a shell. See
PhoneShell.RefreshChrome. (A third row, the strip of open shells, was removed by the 2026-10-04 redesign;
the count on Connections took its place — see the remark where it was.)
The arithmetic is why. Header 56, strip 46, bar 64, and the terminal's own two rows on top of that: at
360dp the shell was framed by about a third of the display, all of it about somewhere the user was not.
What takes their place is one 35-pixel bar drawn by the surface itself, carrying back on the left and
the sessions and a + across from it. See TerminalScreen.axaml.
-->
<!--
Named, and the name is load-bearing: everything the phone draws is inside this one element, so its
bottom margin is the single place the software keyboard can be kept off the box being typed into,
whichever of the eleven screens is showing. See PhoneShell.axaml.cs.
-->
<Panel x:Name="Body">
<!-- ============ getting in ============ -->
<views:PendingScreen IsVisible="{Binding IsStarting}"
Heading="OPENING THE KEYCHAIN"
Detail="Reading this phone's local cache to find out whether it is enrolled." />
<views:ServerScreen IsVisible="{Binding IsNeedingServer}" />
<views:EnrollScreen IsVisible="{Binding IsNeedingEnrollment}" />
<views:RecoveryCodeScreen IsVisible="{Binding IsShowingRecoveryCode}" />
<views:LockedScreen IsVisible="{Binding IsLocked}" />
<!-- ============ unlocked ============ -->
<!--
◆ TWO LAYOUTS, AND THE WIDTH DECIDES WHICH.
Below 600dp this is the phone it always was: a header, the screen, the shells strip and a three-entry
bar across the bottom, with everything else one tap deeper behind SETTINGS. That is right at 360dp,
where a fourth bar entry costs the width of the three that are there.
At or above it — a tablet, an unfolded foldable, a landscape phone, a freeform window — the bar stands
down and PhoneRail takes the left edge with every destination on it at once, which is the desktop
head's arrangement. The hub is not one of them: it is a menu of the rail.
The three flags are computed in code rather than assembled here, because none of them is a single
question any more and Avalonia's bindings have no "and" — and the header's is an "or", which not even
a wrapper can express. See PhoneShell.RefreshChrome, which is also where the one behavioural
consequence lives: unfolding while sitting on the hub moves to Hosts, rather than leaving somebody on
a list of things they can now see beside it.
An outer Grid of two columns rather than a DockPanel, so the rail's width is the rail's own business
and the content takes what is left.
-->
<Grid IsVisible="{Binding IsUnlocked}" ColumnDefinitions="Auto,*">
<views:PhoneRail Grid.Column="0"
IsVisible="{Binding $parent[views:PhoneShell].ShowsRail}" />
<Grid Grid.Column="1" RowDefinitions="Auto,*,Auto">
<!--
The header: which vault, and whether it is synced.
Hidden behind SETTINGS on a narrow surface, and that is the design's arrangement rather than a
saving. v2 gives every screen one header carrying that screen's own name and its own actions — a
back arrow, an add, a refresh — so the hub's screens draw their own and this one stands down rather
than stacking a second row of chrome above theirs. What is left is HOSTS, which is the screen the
application opens on and the one where the vault's name and the sync light are the most useful thing
a header could say.
◆ One flag where there were two nested conditions, and it gained a third meaning with the rail. It
still stands down over a terminal, and still stands down behind SETTINGS on a narrow surface — but
on a wide one it stays up everywhere, because there is no hub to be behind and the screens on the
rail draw no header of their own. Losing it on the keychain would be losing the only LOCK button on
the surface. See PhoneShell.ShowsVaultHeader.
-->
<Panel Grid.Row="0" IsVisible="{Binding $parent[views:PhoneShell].ShowsVaultHeader}">
<!--
◆ DeepChrome rather than Chrome, since v5. The desktop's own titlebar and nav rail made the same
move in v5b — one step darker than Chrome, #0B0B14 against #10111E, so the frame reads as what
holds the glass rather than as another pane of it — and this header is the phone's equivalent
furniture: it is what the desktop's titlebar is, on the surface that has no window to carry one.
HostActionBar, which takes this header's own place while hosts are selected, and the editor's
header in HostsScreen, which the header stands down for, both move with it for the same reason;
see the remark on each.
-->
<StackPanel>
<Border Background="{StaticResource DeepChrome}" BorderBrush="{StaticResource Border}"
BorderThickness="0,0,0,1" Padding="10,0,6,0" Height="60">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<!--
◆ The vault's name is a button now, and it opens the vaults.
The 2026-10-04 redesign draws the name with a chevron, as the switch it is on every phone app
that holds more than one of something. What it leads to is VAULTS, which is where the vaults
this phone shows are chosen — so the chevron is a promise the screen behind it keeps, rather
than a dropdown this head has not got.
The mark stays filled, and takes the gradient Button.primary has: it is the one thing on this
header that is not a fact about the vault, and the launcher icon draws it the same way.
-->
<Button Grid.Column="0" Classes="row" MinHeight="48" Padding="4,0,8,0" CornerRadius="12"
HorizontalAlignment="Left"
AutomationProperties.Name="{Binding Vault.VaultName, StringFormat='Vault {0}. Choose vaults'}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.Vaults}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Border Width="32" Height="32" CornerRadius="10" Background="{StaticResource AccentGradient}"
VerticalAlignment="Center">
<TextBlock Text="&gt;_" Foreground="{StaticResource AccentInk}"
FontFamily="{StaticResource MonoFont}" FontSize="12" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw" />
</Border>
<TextBlock Classes="heading" FontSize="18" FontWeight="Bold" MaxWidth="170"
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
<TextBlock Classes="icon" Text="&#xE5CF;" FontSize="20" Foreground="{StaticResource TextFaint}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
<!--
The sync light, and it is green only when it has earned it — see SyncLabel. The design draws
a permanently green "Synced" here, which is the one claim on that mock-up this application
will not make.
-->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center"
Margin="0,0,6,0">
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="8" Height="8"
VerticalAlignment="Center" />
<TextBlock FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource TextDim}"
Text="{Binding SyncLabel}" VerticalAlignment="Center" />
</StackPanel>
<!--
◆ THE PENCIL WAS HERE, AND IT MOVED INTO THE ACTION BAR.
It was in the header because a long press raised a connect card carrying EDIT, and a card over
the bottom of the list was the wrong place for the one action people reach for most. Both
halves of that changed at once: a long press now chooses hosts rather than raising a card, and
the bar it raises takes this header's place — so the pencil is in that bar, beside the count of
what it would edit. See HostActionBar.axaml, which is drawn in the Panel below this one.
What is left in this row is the vault's name, the sync light and the lock: three facts about
the keychain, and none of them about a row. The lock is an icon since the redesign, with its
name on it for a screen reader rather than the word LOCK in nine-pixel capitals.
-->
<Button Grid.Column="3" Classes="icon" Command="{Binding LockCommand}"
AutomationProperties.Name="Lock the keychain" ToolTip.Tip="Lock the keychain">
<TextBlock Classes="icon" Text="&#xE897;" FontSize="21" />
</Button>
</Grid>
</Border>
<!--
◆ HOSTS · KEYCHAIN · SNIPPETS: the vault's three kinds of content, as one switch.
The redesign's answer to the audit's M8, that SETTINGS held the product. The keychain and the
snippets were rows on the hub, a screen away from the hosts they are used with; they are the open
vault's contents as much as the hosts are, so they now sit beside them under the VAULTS tab, and the
hub keeps what is about this phone rather than about a vault.
Narrow surfaces only. A wide one has the rail, which lists all three already — see
PhoneShell.ShowsVaultTabs. The chips are RadioButtons for the reason the keychain's own are: the
three are exclusive and a screen reader should say so, and Mode=OneWay so the switch moves when the
screen does and never ahead of it.
-->
<Border IsVisible="{Binding $parent[views:PhoneShell].ShowsVaultTabs}" Padding="16,12,16,4">
<Border Classes="segmented">
<Grid ColumnDefinitions="*,*,*">
<RadioButton Grid.Column="0" Classes="segment" GroupName="vaulttab" Content="Hosts"
IsChecked="{Binding IsHostsShowing, Mode=OneWay}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.Hosts}" />
<RadioButton Grid.Column="1" Classes="segment" GroupName="vaulttab" Content="Keychain"
IsChecked="{Binding IsKeychainShowing, Mode=OneWay}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.Keychain}" />
<RadioButton Grid.Column="2" Classes="segment" GroupName="vaulttab" Content="Snippets"
IsChecked="{Binding IsSnippetsShowing, Mode=OneWay}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.Snippets}" />
</Grid>
</Border>
</Border>
</StackPanel>
</Panel>
<!-- ============ ◆ the contextual action bar ============ -->
<!--
In the header's row and in its place, never beside it. A bar that stacked under the vault's name
would be two rows of chrome above a list at 360dp, and worse, it would leave the keychain's name and
LOCK on screen over a list that is no longer about the keychain — see PhoneShell.ShowsHostSelectionBar,
which is where the two are made exclusive.
Wrapped so the data context can be the vault, as every other screen in this file is: what the bar
counts and what its four controls do are one vault's business, and the shell's own binding would
resolve none of them.
-->
<Panel Grid.Row="0" IsVisible="{Binding $parent[views:PhoneShell].ShowsHostSelectionBar}">
<views:HostActionBar DataContext="{Binding Vault}" />
</Panel>
<!-- The screens. Only one draws; which one is the shell's business. -->
<Panel Grid.Row="1">
<!--
Wrapped rather than bound directly, and the desktop head's MainWindow carries the same note for
the same reason: this element's visibility is the shell's business and its data context is the
vault, and putting both on one element resolves IsVisible against the vault — where
IsHostsShowing does not exist. It is a compile error here rather than a silent blank screen only
because compiled bindings are on.
-->
<Panel IsVisible="{Binding IsHostsShowing}">
<views:HostsScreen DataContext="{Binding Vault}" />
</Panel>
<Panel IsVisible="{Binding IsKeychainShowing}">
<views:KeychainScreen DataContext="{Binding Vault}" />
</Panel>
<!--
IsTerminalSurface rather than IsTerminalShowing: a tab exists from the moment connecting starts and
its session does not, so this screen has to be on show while there is nothing to render — it is
what says which machine is being connected to. Which of the two things inside it draws is decided
in the screen itself. See TerminalTabViewModel.
-->
<views:TerminalScreen IsVisible="{Binding IsTerminalSurface}" />
<!--
============ under MORE ============
The hub itself takes the shell as its data context, because every row on it is a navigation
command; the destinations behind it each take the view model they are about, so each one is
wrapped. SnippetsScreen and LogsScreen are nullable on the shell — they are rebuilt on every
unlock and nulled on lock — and it is the collapsed wrapper that keeps a template from binding
against nothing.
-->
<views:MoreScreen IsVisible="{Binding IsMoreShowing}" />
<Panel IsVisible="{Binding IsSnippetsShowing}">
<views:SnippetsScreen DataContext="{Binding SnippetsScreen}" />
</Panel>
<Panel IsVisible="{Binding IsLogsShowing}">
<views:LogsScreen DataContext="{Binding LogsScreen}" />
</Panel>
<!--
The sixth destination behind MORE, and the one v2 never drew — see the comment on the screen
itself. Wrapped like its neighbours even though Vaults is not nullable: the reason for the
wrapper is the data context, not the null. IsVaultsShowing is the shell's and Vaults is not the
shell.
Vaults, not Vault: this one is the vaults themselves and the people in them, where the other is
one vault's contents and is what the hosts and keychain screens draw.
-->
<Panel IsVisible="{Binding IsVaultsShowing}">
<views:VaultsScreen DataContext="{Binding Vaults}" />
</Panel>
<!--
One screen for both file destinations. SFTP and S3 differ in which picker they offer and in
nothing else below it — the panes, the queue and the transfers are the same IRemoteFileStore
either way — so drawing them twice would be two copies of a screen that has to stay identical.
Which picker shows is TransfersViewModel.Remote, set on arrival by the shell.
-->
<Panel IsVisible="{Binding IsTransfersShowing}">
<views:FilesScreen DataContext="{Binding Transfers}" />
</Panel>
<Panel IsVisible="{Binding IsBucketsShowing}">
<views:FilesScreen DataContext="{Binding Transfers}" />
</Panel>
<!--
Preferences carries the header its siblings under MORE have, written here rather than inside the
screen: the screen is one of two that hang off this hub with a back arrow, and the arrow belongs
to the shell that knows where back is.
It held a PendingScreen until it had a setting on it. The one it gained is this phone's device
key — the fingerprint the lock screen offers and that nothing here could previously enrol.
-->
<Grid IsVisible="{Binding IsPreferencesShowing}" RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="Auto,*" Height="56" Margin="8,0">
<!-- Gone on a wide surface, with the five on the screens under the hub and for the reason
written there. -->
<Button Grid.Column="0" Classes="icon" Content="←" Command="{Binding ShowScreenCommand}"
IsVisible="{Binding !$parent[views:PhoneShell].IsWide}"
CommandParameter="{x:Static vm:ShellScreen.More}" />
<TextBlock Grid.Column="1" Classes="heading" Text="Preferences" Margin="4,0" />
</Grid>
<views:PreferencesScreen Grid.Row="1" />
</Grid>
</Panel>
<!--
◆ The two host-key decisions, over everything. Outside the screen Panel deliberately: file transfer
makes its own trust decision on the desktop, and the phone now has that screen — so a sheet nested
inside the hosts screen would be one the files screen could not raise.
◆ And that arrangement is what the shell now relies on rather than merely permits. It used to move to
HOSTS before letting a handshake ask the question, because the desktop head drew it as a banner on that
screen; it does not any more, so this sheet is what is over the Connections surface when a machine
typed into the connect box by hand turns out to be a first contact. Which is the case the navigation
was worst for — it took the box away. See MainWindowViewModel.OnVaultConnectionFailed.
-->
<Panel Grid.Row="1">
<views:HostKeySheet DataContext="{Binding Vault}" />
</Panel>
<!--
◆ THE SHELLS STRIP IS GONE, AND THE BADGE ON CONNECTIONS IS WHAT REPLACED IT.
It sat above the bar on every page — the single most desktop-shaped idea in the product, kept so that
an open shell was never invisible from the host list. The 2026-10-04 redesign keeps that promise with
less: Connections carries the number of open sessions in a green badge, and one tap on it is the
sessions themselves, in the terminal's own bar. That is 46 pixels of every page given back to the list,
and one row of the same pills where there were two on the way into a shell.
-->
<!-- ============ navigation ============ -->
<!--
Gone while a shell is showing, which is the whole of that surface's arrangement: the bar's three
destinations are replaced by a back arrow and a + that leads to all of them, both in the terminal's
own bar. See TerminalScreen.axaml.
◆ A shell rather than the surface, which are not the same question — Connections with nothing running
is that surface drawing a page, and the bar stays under it. The screen it stands down for is the one
that pays for the room; a box asking which machine to connect to does not, and taking the nav away
from somebody who has just closed their last tab left them with only the back gesture.
◆ It is no longer a single question — a wide surface takes the rail instead — so it reads one flag
the control computes rather than a condition here. See PhoneShell.ShowsBottomBar.
── VAULTS · CONNECTIONS · SETTINGS ────────────────────────────────────────────────────────────────
Three entries still, and the first is the open vault rather than its host list: hosts, the keychain
and snippets are a switch at the top of it. The keychain had moved behind SETTINGS on the argument that
a bottom bar is for the places a session moves between; the redesign keeps the bar at three and puts the
vault's own contents back one tap from anywhere, which is what the audit's M8 asked for. 72 tall, for
the pill and a 12-pixel label under it — see NavButton.
-->
<!-- DeepChrome rather than Chrome, since v5 — see the remark on the vault header, above, which this
bar is the foot of the same frame the header is the top of. -->
<Border Grid.Row="2" IsVisible="{Binding $parent[views:PhoneShell].ShowsBottomBar}"
Background="{StaticResource DeepChrome}" BorderBrush="{StaticResource Border}"
BorderThickness="0,1,0,0" Height="72">
<Grid ColumnDefinitions="*,*,*">
<!--
IsVaultSurface rather than IsHostsShowing, for the reason SETTINGS reads IsMoreSurface: the tab
stands for three screens, and it stays lit on whichever of them is up. A tap goes to the hosts,
which is where the application opens and what the vault's switch starts on.
-->
<views:NavButton Grid.Column="0" Label="Vaults" Glyph="&#xE1A1;" IsCurrent="{Binding IsVaultSurface}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.Hosts}" />
<!--
The terminal is a surface rather than a page — see ShellSurface — so this one does not go
through ShowScreen. Its own command is on the shell.
"Connections" rather than "Terminal", and the word does more work than a rename usually does:
with nothing open this surface is where a connection is *made* — a machine typed in by hand, and
the ones connected to before — so a tab labelled Terminal would name the thing behind the door
rather than the door. The enum member stays ShellSurface.Terminal, for the reason the keychain's
tab is not called Vault: the surface is a terminal, and the word a user reads is the product's.
◆ It lights again, and IsTerminalSurface is the right question rather than a near miss. This bar
is drawn in exactly two situations — a page, and Connections with nothing running — and that flag
separates them: false for every page, true for the one screen where this is where you are. With a
shell up the bar is gone and nothing here is read at all. It was left unbound while the bar was
collapsed for the whole of the terminal surface, which made a lit state unreachable; a screen the
bar can now sit under and not point at is the entry looking broken instead.
◆ The count is the open tabs, and it is what the shells strip used to say. See the remark above.
-->
<views:NavButton Grid.Column="1" Label="Connections" Glyph="&#xEB8E;"
IsCurrent="{Binding IsTerminalSurface}" Count="{Binding Tabs.Count}"
Command="{Binding ShowTerminalCommand}" />
<!--
IsMoreSurface rather than IsMoreShowing: this tab stands for the hub and everything behind it,
and a bar that went dark the moment you opened one of them would only ever light two of its
three entries.
"Settings" and a gear, where it was "More" and a hamburger. With the vault's contents moved under
VAULTS, what is behind it is about this phone and this account — security, the terminal's
behaviour, which vaults are shown, the logs — which is what a gear has meant on a phone since
before this application existed.
-->
<views:NavButton Grid.Column="2" Label="Settings" Glyph="&#xE8B8;" IsCurrent="{Binding IsMoreSurface}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.More}" />
</Grid>
</Border>
<!--
◆ THE REST OF THE SCRIM, over the two rows of chrome a sheet on the hosts screen cannot reach.
The hosts screen's sheets are drawn inside the screen's own row, so their scrim dimmed the list and
left the header — with the lock on it — and the bottom bar bright and live above and below every
sheet (audit M12). These two finish the job in the same colour, declared last so they draw over
the header and the bar, and a tap on either does what a tap on the sheet's own scrim does: lowers
the sheet. See PhoneShell.ShowsSheetScrim and OnChromeScrimTapped.
-->
<Button Grid.Row="0" Classes="scrim" IsVisible="{Binding $parent[views:PhoneShell].ShowsSheetScrim}"
Click="OnChromeScrimTapped" AutomationProperties.Name="Close the sheet" />
<Button Grid.Row="2" Classes="scrim" IsVisible="{Binding $parent[views:PhoneShell].ShowsSheetScrim}"
Click="OnChromeScrimTapped" AutomationProperties.Name="Close the sheet" />
</Grid>
</Grid>
</Panel>
</UserControl>