Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions docs/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ Same names, properties and variants as MAUI, except:
| Carousel | Swipe, arrows, dots | Drag with the mouse or touch; the arrows are tab stops and Left / Right move while one has focus; with `AspectRatio="0"` set `Height` |
| Stepper | Shows the active step's view | Every step stays in the tree and only the active one is visible; reached steps are tab stops (Enter / Space) |
| Pagination | Labels hidden on phones | `ShowLabels` defaults to true; every page and arrow is a tab stop (Enter / Space) |
| Resizable | Drag a divider | Dividers are also tab stops: the arrow keys move them 16px; the pointer shows a resize cursor |
| Navbar | Links and the menu button react to taps | They are also tab stops (Enter / Space); children are collected in `Items` |
| Sidebar | Width animates on collapse | Same, with items as tab stops; children are collected in `Groups` and `Items` |
| DateRangePicker | Tap the trigger | Also Enter / Space / Down on the focused trigger; Escape closes the panel |
| FileUpload | `Files` are `FileResult`; `FileTypes` is a `FilePickerFileType`; drop works on Windows only | `Files` are `IStorageFile`; `FileTypes` is a list of `FilePickerFileType` (e.g. `FilePickerFileTypes.ImageAll`); drop works wherever Avalonia supports it; Enter / Space open the picker |
| ToggleGroup | Items toggle on tap | Items are also tab stops (Enter / Space); children are collected in `Items` |
| NumberInput | Numeric keyboard | Up / Down step the value while the field has focus |
| TagInput | Enter, comma or semicolon adds a tag | Backspace in the empty field also removes the last tag |
Expand Down Expand Up @@ -488,6 +493,61 @@ shellui-native add date-picker # also installs calendar
<ui:DatePicker Date="{Binding DueDate}" Format="MMM d, yyyy" />
```

### DateRangePicker
A DatePicker-style trigger that opens one calendar for a start and an end date. The first pick
is the start, the second the end; a pick before the start, or one made when the range is
already complete, starts over. The panel closes when the range is complete, and the × clears
it.

```bash
shellui-native add date-range-picker
```

**Properties:** `StartDate`, `EndDate` (two-way, nullable), `Placeholder` ("Pick a date
range"), `Format` ("MMM d, yyyy"), `AllowClear` (true), `MinimumDate`, `MaximumDate` ·
**Events:** `RangeChanged` · **Methods:** `SetOpen(bool)`, `Close()`, `Clear()`

```xml
<ui:DateRangePicker StartDate="{Binding From}" EndDate="{Binding To}" />
```

It uses `Calendar`'s `RangeStart` and `RangeEnd`, which tint a span of days; you can set them
on a `Calendar` of your own too.

---

### FileUpload
A dashed drop zone with an upload icon and a prompt. Tapping it opens the platform file picker;
the picked files are listed underneath, each with a × to remove it. On Windows, files can also
be dropped on it.

```bash
shellui-native add file-upload
```

**Properties:** `Text`, `Multiple` (false: a new pick replaces the file), `MaxFiles` (0 = no
limit; applies when `Multiple` is on), `FileTypes` (`FilePickerFileType`, e.g.
`FilePickerFileType.Images`; null offers every file), `ShowFiles` (true), `Files` (read-only) ·
**Events:** `FilesChanged` · **Methods:** `PickAsync()`, `Add(files)`, `Remove(file)`,
`Clear()`

```xml
<ui:FileUpload Multiple="True" MaxFiles="5" FilesChanged="OnFiles" />
```

```csharp
private async void OnFiles(object? sender, IReadOnlyList<FileResult> files)
{
foreach (var file in files)
{
await using var stream = await file.OpenReadAsync();
// upload or read the stream
}
}
```

---

### TimePicker
Custom-drawn: a 40px trigger (clock icon + formatted time) that opens scrollable hour and minute
columns — plus AM/PM on a 12-hour clock — floating over the page. Picking a value updates `Time`
Expand Down Expand Up @@ -569,6 +629,30 @@ shellui-native add wrap-layout

Prefer it to `FlexLayout` for simple wrapping: it measures each child once per layout pass.

### Resizable
Panels side by side (or stacked) with a hairline between them. Dragging the hairline moves
space from one neighbor to the other; neither goes under its `MinSize`. Groups nest, so a panel
can hold another `Resizable` with the other orientation. Give the group a size.

```bash
shellui-native add resizable
```

**Properties (Resizable):** `Orientation` (Horizontal), `ShowHandle` (true: a short bar on each
divider) · **Events:** `Resized` (a drag ended)

**Properties (ResizablePanel):** `Size` (two-way; its share of the space, 2 is twice as much as
1), `MinSize` (48) · its child view is the panel's content

```xml
<ui:Resizable HeightRequest="200">
<ui:ResizablePanel Size="1" MinSize="120"> ... </ui:ResizablePanel>
<ui:ResizablePanel Size="2"> ... </ui:ResizablePanel>
</ui:Resizable>
```

---

### Card
`rounded-xl border bg-card shadow-sm` container.

Expand Down Expand Up @@ -662,6 +746,68 @@ shellui-native add accordion

## Navigation Components

### Navbar
Top bar (56px, border below): a brand, links and actions at the end. Narrower than
`CollapseBelow`, the links move into a menu that a button opens under the bar; picking a link
closes it.

```bash
shellui-native add navbar
```

**Properties (Navbar):** `Title`, `Icon`, `Brand` (a view that replaces the icon and title),
`Actions` (a view at the end of the bar), `SelectedValue` (two-way), `CollapseBelow` (640; 0
never collapses), `IsMenuOpen` (two-way) · **Events:** `SelectedValueChanged`

**Properties (NavbarItem):** `Text`, `Value`, `Icon`, `IsEnabled` · **Events:** `Clicked`

```xml
<ui:Navbar Title="ShellUI" Icon="Sparkles" SelectedValue="{Binding Page}">
<ui:NavbarItem Value="docs" Text="Docs" />
<ui:NavbarItem Value="components" Text="Components" />
<ui:Navbar.Actions>
<ui:Button Icon="Search" Size="Icon" Variant="Ghost" />
</ui:Navbar.Actions>
</ui:Navbar>
```

---

### Sidebar
App sidebar: a 256px column with a header, scrolling groups of items and a footer. Collapsed,
it is a 56px rail that shows the icons only. Put it in a column next to your content (or
inside a `Sheet` on phones).

```bash
shellui-native add sidebar
```

**Properties (Sidebar):** `IsCollapsed` (two-way), `SelectedValue` (two-way), `ExpandedWidth`
(256), `CollapsedWidth` (56), `Side` (Left, Right: the border is drawn on the inner edge),
`Header`, `Footer` · **Events:** `IsCollapsedChanged`, `SelectedValueChanged` · **Methods:**
`Toggle()`

**SidebarGroup:** `Label` · **SidebarItem:** `Text`, `Value`, `Icon`, `Badge`, `IsEnabled` ·
**Events:** `Clicked`

The header and footer are your own views: hide what doesn't fit the rail in
`IsCollapsedChanged`.

```xml
<Grid ColumnDefinitions="Auto,*">
<ui:Sidebar x:Name="AppSidebar" SelectedValue="{Binding Page}">
<ui:Sidebar.Header> ... </ui:Sidebar.Header>
<ui:SidebarGroup Label="Workspace">
<ui:SidebarItem Value="home" Text="Home" Icon="House" />
<ui:SidebarItem Value="inbox" Text="Inbox" Icon="Inbox" Badge="12" />
</ui:SidebarGroup>
</ui:Sidebar>
<ContentView Grid.Column="1"> ... </ContentView>
</Grid>
```

---

### Tabs
A muted pill-shaped list; the active tab is raised on the background color. Panels fade in on switch.

Expand Down
17 changes: 8 additions & 9 deletions docs/COMPONENTS_ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Prioritized list of components to create for ShellUI Native, aligned with [Shell

Last revised: **2026-10-07** (first prerelease, `0.1.0-alpha.1`).

**Status at a glance:** P0–P5 ✅ done · P6 partly done (resizable, navbar, sidebar open) · P7 partly done · shipped as `0.1.0-alpha.1` (58 families, 89 CLI targets) · **Avalonia (Phase 2): every component ported, not released yet**.
**Status at a glance:** P0–P6 ✅ done · P7 partly done · shipped as `0.1.0-alpha.1` (58 families, 89 CLI targets) · **Avalonia (Phase 2): every component ported, not released yet**.

---

Expand Down Expand Up @@ -187,15 +187,15 @@ Icons: `python scripts/generate-icons.py` regenerates `Icon.cs` from the ShellIc

---

### P6 — Lower (Advanced) — partly done
### P6 — Lower (Advanced) ✅
| Component | Priority | Dependencies | ShellUI Ref | Notes |
|-----------|----------|--------------|-------------|-------|
| **context-menu** ✅ | P6.1 | shell, icon, element-extensions | ContextMenu | Done 2026-10-05 — one template; opens at the pointer on right-click, under the trigger on long-press |
| **carousel** ✅ | P6.2 | shell, icon | Carousel | Done 2026-10-05 — one template; swipe, arrows, dots, loop, auto-play |
| **stepper** ✅ | P6.3 | shell, icon, button | Stepper | Done 2026-10-05 — one template (Stepper + StepperStep) with built-in navigation |
| **resizable** | P6.4 | — | Resizable | Resizable panels |
| **navbar** | P6.5 | nav-trigger, nav-content, nav-list, nav-item | Navbar | Collapsible nav bar |
| **sidebar** | P6.6 | — | Sidebar | App sidebar layout |
| **resizable** ✅ | P6.4 | shell | Resizable | Done 2026-10-11 — one template (Resizable + ResizablePanel); drag the divider, nests for both directions |
| **navbar** ✅ | P6.5 | shell, icon, element-extensions | Navbar | Done 2026-10-11 — one template (Navbar + NavbarItem); links collapse into a menu under the bar |
| **sidebar** ✅ | P6.6 | shell, icon, element-extensions | Sidebar | Done 2026-10-11 — one template (Sidebar, SidebarGroup, SidebarItem); collapses to an icon rail |

---

Expand All @@ -204,9 +204,9 @@ Icons: `python scripts/generate-icons.py` regenerates `Icon.cs` from the ShellIc
|-----------|----------|-------|
| chart (line, bar, pie, area) | P7 | Consider external chart lib integration |
| combobox ✅ | P7 | Done 2026-10-04 — Select with a filter field, Enter picks the first match |
| file-upload | P7 | File picker |
| **file-upload** ✅ | P7 | Done 2026-10-11, MAUI and Avalonia — drop zone, platform file picker, list of picked files |
| input-otp ✅ | P7 | Done 2026-10-04 — slots over one hidden field (paste / autofill / numeric keyboard) |
| date-range-picker | P7 | Date range selection |
| **date-range-picker** ✅ | P7 | Done 2026-10-11, MAUI and Avalonia — one calendar, start then end; `Calendar` gained `RangeStart` / `RangeEnd` |
| toggle-group ✅ | — | Done 2026-10-05 — single or multiple selection, Default / Outline |
| number-input ✅ | — | Done 2026-10-05 — − / + buttons, min / max clamp, decimal value |
| tag-input ✅ | — | Done 2026-10-05 — chips with remove, Enter / comma / semicolon to commit |
Expand Down Expand Up @@ -293,6 +293,5 @@ P7 items marked ✅ above — 58 families, 89 CLI targets, MAUI only.
component APIs, tokens and sizing contract. MAUI is far ahead, so new MAUI components wait
until Avalonia has P0 + P1.

**MAUI backlog after that:** resizable, navbar, sidebar (P6); chart, file-upload,
date-range-picker (P7); and the ShellUI Blazor targets not yet ported (button-group,
**Backlog after that:** chart (P7); and the ShellUI Blazor targets not yet ported (button-group,
input-group, command / command-palette, menubar, navigation-menu, data-table, …).
8 changes: 6 additions & 2 deletions docs/DEVELOPMENT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ main ← Phase 1a (2026-07-05), 1b (PR #2), 1c + 1d (PR #3), 1e (PR #4), brand

| Branch | Base | Status | Purpose |
|--------|------|--------|---------|
| `main` | — | Phase 1a–1e, release `0.1.0-alpha.1`, Phase 2 foundation and P0–P6 merged | 92 registry entries (89 CLI targets, 58 families); Avalonia templates for the foundation and P0–P6 |
| `feat/avalonia-p7-remaining` | `main` | **Active** | Phase 2: the 14 components that were still MAUI-only |
| `main` | — | Phase 1a–1e, release `0.1.0-alpha.1`, Phase 2 merged: every component has an Avalonia version | 92 registry entries (89 CLI targets, 58 families), each with MAUI and Avalonia content |
| `feat/p6-layout-navigation` | `main` | **Active** | resizable, navbar, sidebar (finishes P6), date-range-picker and file-upload (P7), each for MAUI and Avalonia |

---

Expand Down Expand Up @@ -496,6 +496,10 @@ Cross-desktop (Windows + macOS + Linux) from one XAML codebase.
wrap-layout is a `Panel` with the MAUI layout's `LastChildFill`. Each installs alone into a
fresh Avalonia app and builds with 0 warnings; checked in the demo in light and dark with
pointer and key input
- [x] From here on a component lands on both platforms in one branch. First
(`feat/p6-layout-navigation`): resizable, navbar and sidebar, which finish P6, then
date-range-picker (Calendar gains `RangeStart` / `RangeEnd`) and file-upload from P7; 97
registry entries. A release branch follows
- [ ] Replace the generated `Icon.cs` with the `ShellIcons.Maui` / `ShellIcons.Avalonia` packages
once they are on NuGet. Their names already match (`IconName`, `Icon`, `Kind` on Avalonia), so
components change little: token tinting binds the package's `Color` (MAUI) or `Foreground`
Expand Down
36 changes: 31 additions & 5 deletions examples/Avalonia.Demo/Components/UI/Calendar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ namespace AvaloniaDemo.Components.UI;

// Month calendar — prev/next header, weekday row, 6x7 day grid of 32px cells. Selected day is
// bg-primary, today is bg-accent, days outside the month are muted. Month names, weekday names
// and the first day of the week follow the current culture.
// and the first day of the week follow the current culture. RangeStart and RangeEnd tint a span
// of days (DateRangePicker uses them).
// Usage: <ui:Calendar SelectedDate="{Binding Day}" DateSelected="OnDay" />
public class Calendar : Border
{
Expand All @@ -26,6 +27,13 @@ public class Calendar : Border
public static readonly StyledProperty<DateTime?> MaximumDateProperty =
AvaloniaProperty.Register<Calendar, DateTime?>(nameof(MaximumDate));

// The ends of a highlighted span: both ends are bg-primary, the days between bg-accent.
public static readonly StyledProperty<DateTime?> RangeStartProperty =
AvaloniaProperty.Register<Calendar, DateTime?>(nameof(RangeStart));

public static readonly StyledProperty<DateTime?> RangeEndProperty =
AvaloniaProperty.Register<Calendar, DateTime?>(nameof(RangeEnd));

private const double Cell = 32;

private DateTime _month = new(DateTime.Today.Year, DateTime.Today.Month, 1);
Expand All @@ -39,6 +47,8 @@ static Calendar()
SelectedDateProperty.Changed.AddClassHandler<Calendar>((c, _) => c.OnSelectedDateChanged());
MinimumDateProperty.Changed.AddClassHandler<Calendar>((c, _) => c.Refresh());
MaximumDateProperty.Changed.AddClassHandler<Calendar>((c, _) => c.Refresh());
RangeStartProperty.Changed.AddClassHandler<Calendar>((c, _) => c.Refresh());
RangeEndProperty.Changed.AddClassHandler<Calendar>((c, _) => c.Refresh());
}

public DateTime? SelectedDate
Expand All @@ -47,6 +57,18 @@ public DateTime? SelectedDate
set => SetValue(SelectedDateProperty, value);
}

public DateTime? RangeStart
{
get => GetValue(RangeStartProperty);
set => SetValue(RangeStartProperty, value);
}

public DateTime? RangeEnd
{
get => GetValue(RangeEndProperty);
set => SetValue(RangeEndProperty, value);
}

public DateTime? MinimumDate
{
get => GetValue(MinimumDateProperty);
Expand Down Expand Up @@ -194,21 +216,25 @@ private void StyleCell(int index)
var date = _dates[index];
var inMonth = date.Month == _month.Month;
var selectable = IsSelectable(date);
var selected = SelectedDate?.Date == date;
var selected = SelectedDate?.Date == date || RangeStart?.Date == date || RangeEnd?.Date == date;
var inRange = !selected && RangeStart is { } from && RangeEnd is { } to && date > from.Date && date < to.Date;
var today = date == DateTime.Today;

if (selected)
box.Token(BackgroundProperty, ShellToken.Primary);
else if (today || (index == _hovered && selectable))
else if (inRange || today || (index == _hovered && selectable))
box.Token(BackgroundProperty, ShellToken.Accent);
else
{
box.ClearToken(BackgroundProperty);
box.Background = Brushes.Transparent;
}

// Days inside a span lose their corners, so the span reads as one band.
box.CornerRadius = new CornerRadius(inRange ? 0 : ShellTheme.RadiusMd);

text.Token(TextBlock.ForegroundProperty,
selected ? ShellToken.PrimaryForeground : inMonth ? ShellToken.Foreground : ShellToken.MutedForeground);
box.Opacity = !selectable ? 0.3 : inMonth || selected ? 1 : 0.5;
selected ? ShellToken.PrimaryForeground : inMonth || inRange ? ShellToken.Foreground : ShellToken.MutedForeground);
box.Opacity = !selectable ? 0.3 : inMonth || selected || inRange ? 1 : 0.5;
}
}
Loading
Loading