From 38ec45d1a16696bf104f4f6540fc99ea8d05dd09 Mon Sep 17 00:00:00 2001 From: gvreddy04 Date: Wed, 13 May 2026 18:56:21 +0530 Subject: [PATCH 1/4] Add SplitView component with customizable features and documentation - Implemented SplitView component with primary and secondary panes. - Added support for customizable colors, minimum pane sizes, and orientation. - Introduced events for resize actions: OnResizeStarted, OnResized, and OnResizeEnded. - Created multiple demo pages showcasing various SplitView configurations. - Developed comprehensive documentation covering parameters, events, and usage examples. - Included enums for SplitViewColor and SplitViewOrientation to enhance customization. --- .../Layout/DemosMainLayout.razor.cs | 1 + .../Components/Layout/DocsMainLayout.razor.cs | 1 + .../Components/Layout/MainLayout.razor.cs | 1 + .../SplitView/SplitViewDocumentation.razor | 104 +++++++ .../SplitView_Demo_01_Pane_Content.razor | 22 ++ .../SplitView_Demo_02_Orientation.razor | 23 ++ .../SplitView_Demo_03_Primary_Pane_Size.razor | 18 ++ ...ew_Demo_04_Primary_Pane_Size_Changed.razor | 30 ++ .../SplitView_Demo_05_Minimum_Pane_Size.razor | 19 ++ .../SplitView_Demo_06_Is_Disabled.razor | 20 ++ .../SplitView/SplitView_Demo_07_Color.razor | 50 +++ .../SplitView_Demo_08_Custom_Color.razor | 19 ++ .../SplitView_Demo_09_On_Resize_Started.razor | 44 +++ .../SplitView_Demo_10_On_Resized.razor | 41 +++ .../SplitView_Demo_11_On_Resize_Ended.razor | 41 +++ .../SplitView_Demo_12_Nested_Layouts.razor | 43 +++ .../SplitView_Doc_01_Documentation.razor | 45 +++ .../Components/Pages/Home/Index.razor | 5 + .../Constants/DemoRouteConstants.cs | 2 + .../Constants/DemoScreenshotSrcConstants.cs | 1 + .../Components/SplitView/SplitView.razor | 14 + .../Components/SplitView/SplitView.razor.cs | 290 ++++++++++++++++++ blazorbootstrap/Enums/Color/SplitViewColor.cs | 112 +++++++ blazorbootstrap/Enums/SplitViewOrientation.cs | 21 ++ .../SplitViewResizeEventArgs.cs | 37 +++ blazorbootstrap/Extensions/EnumExtensions.cs | 20 ++ blazorbootstrap/wwwroot/blazor.bootstrap.css | 173 +++++++++++ blazorbootstrap/wwwroot/blazor.bootstrap.js | 193 ++++++++++++ docs/docs/05-components/split-view.mdx | 225 ++++++++++++++ 29 files changed, 1615 insertions(+) create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitViewDocumentation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_01_Pane_Content.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_02_Orientation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_03_Primary_Pane_Size.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_04_Primary_Pane_Size_Changed.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_05_Minimum_Pane_Size.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_06_Is_Disabled.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_07_Color.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_08_Custom_Color.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_09_On_Resize_Started.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_10_On_Resized.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_11_On_Resize_Ended.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_12_Nested_Layouts.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Docs/SplitView/SplitView_Doc_01_Documentation.razor create mode 100644 blazorbootstrap/Components/SplitView/SplitView.razor create mode 100644 blazorbootstrap/Components/SplitView/SplitView.razor.cs create mode 100644 blazorbootstrap/Enums/Color/SplitViewColor.cs create mode 100644 blazorbootstrap/Enums/SplitViewOrientation.cs create mode 100644 blazorbootstrap/EventArguments/SplitViewResizeEventArgs.cs create mode 100644 docs/docs/05-components/split-view.mdx diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs index e6d066b2f..081401204 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs @@ -83,6 +83,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "522", Text = "Script Loader", Href = DemoRouteConstants.Demos_URL_ScriptLoader, IconName = IconName.CodeSlash, ParentId = "5" }, new (){ Id = "523", Text = "Sidebar", Href = DemoRouteConstants.Demos_URL_Sidebar, IconName = IconName.LayoutSidebar, ParentId = "5" }, new (){ Id = "524", Text = "Sidebar 2", Href = DemoRouteConstants.Demos_URL_Sidebar2, IconName = IconName.ListNested, ParentId = "5" }, + new (){ Id = "5245", Text = "Split View", Href = DemoRouteConstants.Demos_URL_SplitView, IconName = IconName.LayoutSplit, ParentId = "5" }, new (){ Id = "525", Text = "Sortable List", Href = DemoRouteConstants.Demos_URL_SortableList, IconName = IconName.ArrowsMove, ParentId = "5" }, new (){ Id = "526", Text = "Spinner", Href = DemoRouteConstants.Demos_URL_Spinners, IconName = IconName.ArrowRepeat, ParentId = "5" }, new (){ Id = "527", Text = "Tabs", Href = DemoRouteConstants.Demos_URL_Tabs, IconName = IconName.WindowPlus, ParentId = "5" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs index d05756698..fed5c4c02 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs @@ -59,6 +59,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "522", Text = "Script Loader", Href = DemoRouteConstants.Docs_URL_ScriptLoader, IconName = IconName.CodeSlash, ParentId = "5" }, new (){ Id = "523", Text = "Sidebar", Href = DemoRouteConstants.Docs_URL_Sidebar, IconName = IconName.LayoutSidebar, ParentId = "5" }, new (){ Id = "524", Text = "Sidebar 2", Href = DemoRouteConstants.Docs_URL_Sidebar2, IconName = IconName.ListNested, ParentId = "5" }, + new (){ Id = "5245", Text = "Split View", Href = DemoRouteConstants.Docs_URL_SplitView, IconName = IconName.LayoutSplit, ParentId = "5" }, new (){ Id = "525", Text = "Sortable List", Href = DemoRouteConstants.Docs_URL_SortableList, IconName = IconName.ArrowsMove, ParentId = "5" }, new (){ Id = "526", Text = "Spinner", Href = DemoRouteConstants.Docs_URL_Spinners, IconName = IconName.ArrowRepeat, ParentId = "5" }, new (){ Id = "527", Text = "Tabs", Href = DemoRouteConstants.Docs_URL_Tabs, IconName = IconName.WindowPlus, ParentId = "5" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs index a88f8c744..f20977c51 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs @@ -82,6 +82,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "522", Text = "Script Loader", Href = DemoRouteConstants.Demos_URL_ScriptLoader, IconName = IconName.CodeSlash, ParentId = "5" }, new (){ Id = "523", Text = "Sidebar", Href = DemoRouteConstants.Demos_URL_Sidebar, IconName = IconName.LayoutSidebar, ParentId = "5" }, new (){ Id = "524", Text = "Sidebar 2", Href = DemoRouteConstants.Demos_URL_Sidebar2, IconName = IconName.ListNested, ParentId = "5" }, + new (){ Id = "5245", Text = "Split View", Href = DemoRouteConstants.Demos_URL_SplitView, IconName = IconName.LayoutSplit, ParentId = "5" }, new (){ Id = "525", Text = "Sortable List", Href = DemoRouteConstants.Demos_URL_SortableList, IconName = IconName.ArrowsMove, ParentId = "5" }, new (){ Id = "526", Text = "Spinner", Href = DemoRouteConstants.Demos_URL_Spinners, IconName = IconName.ArrowRepeat, ParentId = "5" }, new (){ Id = "527", Text = "Tabs", Href = DemoRouteConstants.Demos_URL_Tabs, IconName = IconName.WindowPlus, ParentId = "5" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitViewDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitViewDocumentation.razor new file mode 100644 index 000000000..df6b46b19 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitViewDocumentation.razor @@ -0,0 +1,104 @@ +@attribute [Route(pageUrl)] +@layout DemosMainLayout + + + + + +
+
+ Pane1 and Pane2 are the required content regions for SplitView. +
+ +
+ +
+
+ Set Orientation="SplitViewOrientation.Vertical" to stack panes top to bottom with a horizontal divider. +
+ +
+ +
+
+ Use PrimaryPaneSize to set the initial percentage allocated to the primary pane. +
+ +
+ +
+
+ Use @@bind-PrimaryPaneSize or PrimaryPaneSizeChanged to react to live divider movement. +
+ +
+ +
+
+ MinimumPaneSize clamps both panes so one side cannot fully collapse while dragging. +
+ +
+ +
+
+ Set IsDisabled="true" to keep the layout visible while preventing pointer-based resizing. +
+ +
+ +
+
+ The Color parameter uses the SplitViewColor enum and maps to shared Bootstrap-backed CSS variables. +
+ +
+ +
+
+ Use CustomColor when you want to pass a CSS color expression such as var(...) or rgba(...). +
+ +
+ +
+
+ OnResizeStarted fires once when the pointer first captures the divider. +
+ +
+ +
+
+ OnResized streams live primary and secondary pane percentages during drag. +
+ +
+ +
+
+ OnResizeEnded is useful when you only want to persist the final pane sizes after dragging stops. +
+ +
+ +
+
+ SplitView supports nested layouts, enabling IDE-style and dashboard-style workspaces. +
+ +
+ +@code { + private const string pageUrl = DemoRouteConstants.Demos_URL_SplitView; + private const string pageTitle = "Blazor Split View"; + private const string pageDescription = "The Blazor Split View component arranges two resizable panes horizontally or vertically with live drag updates, Bootstrap color theming, and focused parameter demos."; + private const string metaTitle = "Blazor Split View Component"; + private const string metaDescription = "The Blazor Split View component arranges two resizable panes horizontally or vertically with live drag updates, Bootstrap color theming, and focused parameter demos."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_SplitView; +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_01_Pane_Content.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_01_Pane_Content.razor new file mode 100644 index 000000000..bf53a4434 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_01_Pane_Content.razor @@ -0,0 +1,22 @@ + + +
+
Pane1
+
Navigation
+
+
Overview
+
Customers
+
Orders
+
Reports
+
+
+
+ +
+
Pane2
+

Workspace

+

Use the two required pane templates to define the resizable primary and secondary regions.

+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_02_Orientation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_02_Orientation.razor new file mode 100644 index 000000000..1e23e6d0e --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_02_Orientation.razor @@ -0,0 +1,23 @@ + + +
+
Top Pane
+
Summary
+

Vertical orientation stacks panes and changes the divider to horizontal dragging.

+
+
+ +
+
Bottom Pane
+
Details
+
    +
  • Release notes
  • +
  • Deployment checklist
  • +
  • Preview output
  • +
+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_03_Primary_Pane_Size.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_03_Primary_Pane_Size.razor new file mode 100644 index 000000000..77ff83788 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_03_Primary_Pane_Size.razor @@ -0,0 +1,18 @@ + + +
+
Primary Pane
+
30%
+

Set PrimaryPaneSize when you want the initial layout to favor one pane.

+
+
+ +
+
Secondary Pane
+
70%
+

The remaining space is automatically assigned to the secondary pane.

+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_04_Primary_Pane_Size_Changed.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_04_Primary_Pane_Size_Changed.razor new file mode 100644 index 000000000..0a51510ff --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_04_Primary_Pane_Size_Changed.razor @@ -0,0 +1,30 @@ +
+ + + +
Bound primary pane size: @primaryPaneSize.ToString("0.##")%
+
+ + + +
+
Primary Pane
+
Two-way Binding
+

Drag the divider or use the buttons above to update the same bound value.

+
+
+ +
+
Secondary Pane
+
Live Value
+

The parent component stays synchronized through PrimaryPaneSizeChanged.

+
+
+
+ +@code { + private double primaryPaneSize = 40; +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_05_Minimum_Pane_Size.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_05_Minimum_Pane_Size.razor new file mode 100644 index 000000000..b76b20c9a --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_05_Minimum_Pane_Size.razor @@ -0,0 +1,19 @@ + + +
+
MinimumPaneSize
+
25%
+

The divider cannot move past the 25% threshold, so the primary pane always remains usable.

+
+
+ +
+
Secondary Pane
+
Protected Layout
+

The same minimum is enforced from the opposite side, keeping both panes visible.

+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_06_Is_Disabled.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_06_Is_Disabled.razor new file mode 100644 index 000000000..02363f828 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_06_Is_Disabled.razor @@ -0,0 +1,20 @@ + + +
+
Disabled Divider
+
Pinned Pane
+

The layout remains visible, but pointer dragging and hover states are disabled.

+
+
+ +
+
Secondary Pane
+
Static Layout
+

Programmatic updates to PrimaryPaneSize still work even when interaction is disabled.

+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_07_Color.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_07_Color.razor new file mode 100644 index 000000000..b32416992 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_07_Color.razor @@ -0,0 +1,50 @@ +
+
+ + +
+
Primary
+
Bootstrap semantic color
+
+
+ +
Hover and dragging derive from the same base divider color.
+
+
+
+
+ + +
+
Success
+
Shared CSS variable mapping
+
+
+ +
Use enum values when you want Bootstrap-aligned theming.
+
+
+
+
+ + +
+
Dark
+
Theme-aware shared CSS
+
+
+ +
The divider color comes from the shared blazor.bootstrap.css token mapping.
+
+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_08_Custom_Color.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_08_Custom_Color.razor new file mode 100644 index 000000000..bd377a07b --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_08_Custom_Color.razor @@ -0,0 +1,19 @@ + + +
+
CustomColor
+
CSS Expression Override
+

This example uses rgba(var(--bs-info-rgb), 0.85) instead of a semantic enum value.

+
+
+ +
+
Override Precedence
+
Inline Custom Value
+

When both are supplied, CustomColor overrides the shared default and semantic enum class.

+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_09_On_Resize_Started.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_09_On_Resize_Started.razor new file mode 100644 index 000000000..8debf5887 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_09_On_Resize_Started.razor @@ -0,0 +1,44 @@ +
+
+ + +
+
OnResizeStarted
+
Drag To Start
+

Grab the divider to trigger the start event once per interaction.

+
+
+ +
+
Secondary Pane
+
Event Snapshot
+

The event args include both pane sizes as percentages.

+
+
+
+
+
+
+
Start Event
+
Count: @count
+
Primary pane: @primaryPaneSize.ToString("0.##")%
+
Secondary pane: @secondaryPaneSize.ToString("0.##")%
+
+
+
+ +@code { + private int count; + private double primaryPaneSize = 38; + private double secondaryPaneSize = 62; + + private void HandleResizeStarted(SplitViewResizeEventArgs args) + { + count++; + primaryPaneSize = args.PrimaryPaneSize; + secondaryPaneSize = args.SecondaryPaneSize; + } +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_10_On_Resized.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_10_On_Resized.razor new file mode 100644 index 000000000..064c27b2c --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_10_On_Resized.razor @@ -0,0 +1,41 @@ +
+
+ + +
+
OnResized
+
Live Resize Stream
+

Drag the divider and watch the event payload update continuously.

+
+
+ +
+
Live Metrics
+
Current Split
+

Primary pane: @primaryPaneSize.ToString("0.##")%

+
+
+
+
+
+
+
Resized Event
+
Primary pane: @primaryPaneSize.ToString("0.##")%
+
Secondary pane: @secondaryPaneSize.ToString("0.##")%
+
+
+
+ +@code { + private double primaryPaneSize = 42; + private double secondaryPaneSize = 58; + + private void HandleResized(SplitViewResizeEventArgs args) + { + primaryPaneSize = args.PrimaryPaneSize; + secondaryPaneSize = args.SecondaryPaneSize; + } +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_11_On_Resize_Ended.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_11_On_Resize_Ended.razor new file mode 100644 index 000000000..ffab9c544 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_11_On_Resize_Ended.razor @@ -0,0 +1,41 @@ +
+
+ + +
+
OnResizeEnded
+
Persist Final Size
+

This event fires once after dragging finishes, which is useful for saving layout preferences.

+
+
+ +
+
Secondary Pane
+
Release The Divider
+

Only the final percentages are recorded here.

+
+
+
+
+
+
+
Resize Ended
+
Primary pane: @primaryPaneSize.ToString("0.##")%
+
Secondary pane: @secondaryPaneSize.ToString("0.##")%
+
+
+
+ +@code { + private double primaryPaneSize = 36; + private double secondaryPaneSize = 64; + + private void HandleResizeEnded(SplitViewResizeEventArgs args) + { + primaryPaneSize = args.PrimaryPaneSize; + secondaryPaneSize = args.SecondaryPaneSize; + } +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_12_Nested_Layouts.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_12_Nested_Layouts.razor new file mode 100644 index 000000000..9245a0c42 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_12_Nested_Layouts.razor @@ -0,0 +1,43 @@ + + +
+
Explorer
+
Files
+
+
Pages
+
Components
+
Styles
+
Assets
+
+
+
+ + + +
+
Editor
+
SplitView.razor
+
<SplitView @@bind-PrimaryPaneSize="paneSize">
+    <Pane1>...</Pane1>
+    <Pane2>...</Pane2>
+</SplitView>
+
+
+ +
+
Preview
+
Live Preview
+

Nested SplitView layouts are useful for IDEs, dashboards, and administration portals.

+
+
+
+
+
\ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/SplitView/SplitView_Doc_01_Documentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/SplitView/SplitView_Doc_01_Documentation.razor new file mode 100644 index 000000000..332d836d1 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/SplitView/SplitView_Doc_01_Documentation.razor @@ -0,0 +1,45 @@ +@attribute [Route(pageUrl)] +@layout DocsMainLayout + + + + + +
+ @metaTitle +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +@code { + private const string componentName = nameof(SplitView); + private const string pageUrl = DemoRouteConstants.Docs_URL_SplitView; + private const string pageTitle = componentName; + private const string pageDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string metaTitle = $"Blazor {componentName} Component"; + private const string metaDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_SplitView; +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor index b4a94d512..75f780ea2 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor @@ -230,6 +230,11 @@

Sidebar 2 Updated

+

Sortable List

diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs index e4b1a7138..9df346203 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs @@ -90,6 +90,7 @@ public static class DemoRouteConstants public const string Demos_URL_ScriptLoader = Demos_URL_Prefix + "/script-loader"; public const string Demos_URL_Sidebar = Demos_URL_Prefix + "/sidebar"; public const string Demos_URL_Sidebar2 = Demos_URL_Prefix + "/sidebar2"; + public const string Demos_URL_SplitView = Demos_URL_Prefix + "/split-view"; public const string Demos_URL_SortableList = Demos_URL_Prefix + "/sortable-list"; public const string Demos_URL_Spinners = Demos_URL_Prefix + "/spinners"; public const string Demos_URL_Tabs = Demos_URL_Prefix + "/tabs"; @@ -181,6 +182,7 @@ public static class DemoRouteConstants public const string Docs_URL_ScriptLoader = Docs_URL_Prefix + "/script-loader"; public const string Docs_URL_Sidebar = Docs_URL_Prefix + "/sidebar"; public const string Docs_URL_Sidebar2 = Docs_URL_Prefix + "/sidebar2"; + public const string Docs_URL_SplitView = Docs_URL_Prefix + "/split-view"; public const string Docs_URL_SortableList = Docs_URL_Prefix + "/sortable-list"; public const string Docs_URL_Spinners = Docs_URL_Prefix + "/spinners"; public const string Docs_URL_Tabs = Docs_URL_Prefix + "/tabs"; diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs index 3db0516fe..c3b33b2ca 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs @@ -82,6 +82,7 @@ public class DemoScreenshotSrcConstants public const string Demos_URL_ScriptLoader = DemoScreenshotSrcPrefix + "script-loader.png"; public const string Demos_URL_Sidebar = DemoScreenshotSrcPrefix + "sidebar.png"; public const string Demos_URL_Sidebar2 = DemoScreenshotSrcPrefix + "sidebar2.png"; + public const string Demos_URL_SplitView = DemoScreenshotSrcPrefix + "home.png"; public const string Demos_URL_SortableList = DemoScreenshotSrcPrefix + "sortable-list.png"; public const string Demos_URL_Spinners = DemoScreenshotSrcPrefix + "spinners.png"; public const string Demos_URL_Tabs = DemoScreenshotSrcPrefix + "tabs.png"; diff --git a/blazorbootstrap/Components/SplitView/SplitView.razor b/blazorbootstrap/Components/SplitView/SplitView.razor new file mode 100644 index 000000000..d70058741 --- /dev/null +++ b/blazorbootstrap/Components/SplitView/SplitView.razor @@ -0,0 +1,14 @@ +@namespace BlazorBootstrap +@inherits BlazorBootstrapComponentBase + +
+
+ @Pane1 +
+ +
+ @Pane2 +
+
\ No newline at end of file diff --git a/blazorbootstrap/Components/SplitView/SplitView.razor.cs b/blazorbootstrap/Components/SplitView/SplitView.razor.cs new file mode 100644 index 000000000..2e11e591e --- /dev/null +++ b/blazorbootstrap/Components/SplitView/SplitView.razor.cs @@ -0,0 +1,290 @@ +namespace BlazorBootstrap; + +public partial class SplitView : BlazorBootstrapComponentBase +{ + #region Fields and Constants + + private double currentPrimaryPaneSize = 50; + private bool isResizing; + private DotNetObjectReference? objRef; + private SplitViewColor previousColor; + private string? previousCustomColor; + private double previousMinimumPaneSize; + private SplitViewOrientation previousOrientation; + private double previousPrimaryPaneSize; + private bool previousIsDisabled; + + #endregion + + #region Methods + + /// + protected override async ValueTask DisposeAsyncCore(bool disposing) + { + if (disposing) + { + try + { + if (IsRenderComplete && Id is not null) + await SafeInvokeVoidAsync("window.blazorBootstrap.splitView.dispose", Id); + } + catch (JSDisconnectedException) + { + // do nothing + } + + objRef?.Dispose(); + } + + await base.DisposeAsyncCore(disposing); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await SafeInvokeVoidAsync("window.blazorBootstrap.splitView.initialize", Id!, Orientation.ToString(), currentPrimaryPaneSize, EffectiveMinimumPaneSize, + IsDisabled, objRef!); + + CaptureRenderedState(); + } + else if (!isResizing && ParametersChanged()) + { + await SafeInvokeVoidAsync("window.blazorBootstrap.splitView.update", Id!, Orientation.ToString(), currentPrimaryPaneSize, EffectiveMinimumPaneSize, + IsDisabled); + + CaptureRenderedState(); + } + + await base.OnAfterRenderAsync(firstRender); + } + + protected override Task OnInitializedAsync() + { + objRef ??= DotNetObjectReference.Create(this); + + return base.OnInitializedAsync(); + } + + protected override Task OnParametersSetAsync() + { + currentPrimaryPaneSize = NormalizePrimaryPaneSize(PrimaryPaneSize); + + return base.OnParametersSetAsync(); + } + + [JSInvokable] + public async Task OnResizeEndedJS(double primaryPaneSize, double secondaryPaneSize) + { + isResizing = false; + currentPrimaryPaneSize = primaryPaneSize; + CaptureRenderedState(); + + if (OnResizeEnded.HasDelegate) + await OnResizeEnded.InvokeAsync(new SplitViewResizeEventArgs(primaryPaneSize, secondaryPaneSize, Orientation)); + } + + [JSInvokable] + public async Task OnResizeStartedJS(double primaryPaneSize, double secondaryPaneSize) + { + isResizing = true; + currentPrimaryPaneSize = primaryPaneSize; + + if (OnResizeStarted.HasDelegate) + await OnResizeStarted.InvokeAsync(new SplitViewResizeEventArgs(primaryPaneSize, secondaryPaneSize, Orientation)); + } + + [JSInvokable] + public async Task OnResizedJS(double primaryPaneSize, double secondaryPaneSize) + { + if (Math.Abs(currentPrimaryPaneSize - primaryPaneSize) < 0.01d && !PrimaryPaneSizeChanged.HasDelegate && !OnResized.HasDelegate) + return; + + currentPrimaryPaneSize = primaryPaneSize; + + if (PrimaryPaneSizeChanged.HasDelegate) + await PrimaryPaneSizeChanged.InvokeAsync(primaryPaneSize); + + if (OnResized.HasDelegate) + await OnResized.InvokeAsync(new SplitViewResizeEventArgs(primaryPaneSize, secondaryPaneSize, Orientation)); + } + + private void CaptureRenderedState() + { + previousPrimaryPaneSize = currentPrimaryPaneSize; + previousMinimumPaneSize = EffectiveMinimumPaneSize; + previousOrientation = Orientation; + previousIsDisabled = IsDisabled; + previousColor = Color; + previousCustomColor = CustomColor; + } + + private double NormalizePrimaryPaneSize(double primaryPaneSize) + { + var minimumPaneSize = EffectiveMinimumPaneSize; + var maximumPaneSize = 100d - minimumPaneSize; + + return Math.Clamp(primaryPaneSize, minimumPaneSize, maximumPaneSize); + } + + private bool ParametersChanged() => + previousPrimaryPaneSize != currentPrimaryPaneSize || + previousMinimumPaneSize != EffectiveMinimumPaneSize || + previousOrientation != Orientation || + previousIsDisabled != IsDisabled || + previousColor != Color || + previousCustomColor != CustomColor; + + #endregion + + #region Properties, Indexers + + protected override string? ClassNames => + BuildClassNames(Class, + ("bb-split-view", true), + ("bb-split-view-horizontal", Orientation == SplitViewOrientation.Horizontal), + ("bb-split-view-vertical", Orientation == SplitViewOrientation.Vertical), + (Color.ToSplitViewColorClass(), Color != SplitViewColor.None), + ("bb-split-view-disabled", IsDisabled)); + + private double EffectiveMinimumPaneSize => Math.Clamp(MinimumPaneSize, 0d, 50d); + + /// + /// Gets or sets the divider color. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(SplitViewColor.None)] + [Description("Gets or sets the divider color.")] + [Parameter] + public SplitViewColor Color { get; set; } + + /// + /// Gets or sets a custom divider color. + /// + /// Accepts any valid CSS color expression, including CSS variables. + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets a custom divider color.")] + [Parameter] + public string? CustomColor { get; set; } + + /// + /// Gets or sets a value indicating whether user interaction is disabled. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(false)] + [Description("Gets or sets a value indicating whether user interaction is disabled.")] + [Parameter] + public bool IsDisabled { get; set; } + + /// + /// Gets or sets the minimum pane size as a percentage. + /// + /// Default value is 0. + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(0d)] + [Description("Gets or sets the minimum pane size as a percentage.")] + [Parameter] + public double MinimumPaneSize { get; set; } + + /// + /// Fired when resizing ends. + /// + [AddedVersion("4.0.0")] + [Description("Fired when resizing ends.")] + [Parameter] + public EventCallback OnResizeEnded { get; set; } + + /// + /// Fired while the divider is being dragged. + /// + [AddedVersion("4.0.0")] + [Description("Fired while the divider is being dragged.")] + [Parameter] + public EventCallback OnResized { get; set; } + + /// + /// Fired when resizing starts. + /// + [AddedVersion("4.0.0")] + [Description("Fired when resizing starts.")] + [Parameter] + public EventCallback OnResizeStarted { get; set; } + + /// + /// Gets or sets the SplitView orientation. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(SplitViewOrientation.Horizontal)] + [Description("Gets or sets the SplitView orientation.")] + [Parameter] + public SplitViewOrientation Orientation { get; set; } = SplitViewOrientation.Horizontal; + + /// + /// Gets or sets the first pane content. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the first pane content.")] + [EditorRequired] + [Parameter] + public RenderFragment? Pane1 { get; set; } + + /// + /// Gets or sets the second pane content. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the second pane content.")] + [EditorRequired] + [Parameter] + public RenderFragment? Pane2 { get; set; } + + /// + /// Gets or sets the primary pane size as a percentage. + /// + /// Default value is 50. + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(50d)] + [Description("Gets or sets the primary pane size as a percentage.")] + [Parameter] + public double PrimaryPaneSize { get; set; } = 50d; + + /// + /// Fired when the primary pane size changes. + /// + [AddedVersion("4.0.0")] + [Description("Fired when the primary pane size changes.")] + [Parameter] + public EventCallback PrimaryPaneSizeChanged { get; set; } + + private string SeparatorAriaOrientation => Orientation == SplitViewOrientation.Horizontal ? "vertical" : "horizontal"; + + protected override string? StyleNames => + BuildStyleNames(Style, + ($"--bb-split-view-divider-color:{CustomColor}", !string.IsNullOrWhiteSpace(CustomColor))); + + #endregion +} \ No newline at end of file diff --git a/blazorbootstrap/Enums/Color/SplitViewColor.cs b/blazorbootstrap/Enums/Color/SplitViewColor.cs new file mode 100644 index 000000000..ef94f22d0 --- /dev/null +++ b/blazorbootstrap/Enums/Color/SplitViewColor.cs @@ -0,0 +1,112 @@ +namespace BlazorBootstrap; + +/// +/// Defines the supported visual color options for a BlazorBootstrap split view divider. +/// +public enum SplitViewColor +{ + /// + /// Indicates that no explicit color option is selected. + /// + [AddedVersion("4.0.0")] + [Description("Indicates that no explicit color option is selected.")] + None, + + /// + /// Indicates the primary (emphasis) color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the primary (emphasis) color option.")] + Primary, + + /// + /// Indicates the secondary (less emphasized) color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the secondary (less emphasized) color option.")] + Secondary, + + /// + /// Indicates a success-state color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates a success-state color option.")] + Success, + + /// + /// Indicates an error or danger-state color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates an error or danger-state color option.")] + Danger, + + /// + /// Indicates a warning-state color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates a warning-state color option.")] + Warning, + + /// + /// Indicates an informational-state color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates an informational-state color option.")] + Info, + + /// + /// Indicates a light-toned color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates a light-toned color option.")] + Light, + + /// + /// Indicates a dark-toned color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates a dark-toned color option.")] + Dark, + + /// + /// Indicates the body background color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the body background color option.")] + Body, + + /// + /// Indicates the secondary body background color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the secondary body background color option.")] + BodySecondary, + + /// + /// Indicates the tertiary body background color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the tertiary body background color option.")] + BodyTertiary, + + /// + /// Indicates the black color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the black color option.")] + Black, + + /// + /// Indicates the white color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates the white color option.")] + White, + + /// + /// Indicates a transparent color option. + /// + [AddedVersion("4.0.0")] + [Description("Indicates a transparent color option.")] + Transparent +} \ No newline at end of file diff --git a/blazorbootstrap/Enums/SplitViewOrientation.cs b/blazorbootstrap/Enums/SplitViewOrientation.cs new file mode 100644 index 000000000..d4c74da39 --- /dev/null +++ b/blazorbootstrap/Enums/SplitViewOrientation.cs @@ -0,0 +1,21 @@ +namespace BlazorBootstrap; + +/// +/// Defines the available layout orientations for the component. +/// +public enum SplitViewOrientation +{ + /// + /// Arranges panes side by side with a vertical divider. + /// + [AddedVersion("4.0.0")] + [Description("Arranges panes side by side with a vertical divider.")] + Horizontal, + + /// + /// Arranges panes top to bottom with a horizontal divider. + /// + [AddedVersion("4.0.0")] + [Description("Arranges panes top to bottom with a horizontal divider.")] + Vertical, +} \ No newline at end of file diff --git a/blazorbootstrap/EventArguments/SplitViewResizeEventArgs.cs b/blazorbootstrap/EventArguments/SplitViewResizeEventArgs.cs new file mode 100644 index 000000000..a5dd6fdec --- /dev/null +++ b/blazorbootstrap/EventArguments/SplitViewResizeEventArgs.cs @@ -0,0 +1,37 @@ +namespace BlazorBootstrap; + +/// +/// Provides resize data for the component. +/// +public class SplitViewResizeEventArgs : EventArgs +{ + #region Constructors + + public SplitViewResizeEventArgs(double primaryPaneSize, double secondaryPaneSize, SplitViewOrientation orientation) + { + PrimaryPaneSize = primaryPaneSize; + SecondaryPaneSize = secondaryPaneSize; + Orientation = orientation; + } + + #endregion + + #region Properties, Indexers + + /// + /// Gets the current orientation. + /// + public SplitViewOrientation Orientation { get; } + + /// + /// Gets the primary pane size as a percentage. + /// + public double PrimaryPaneSize { get; } + + /// + /// Gets the secondary pane size as a percentage. + /// + public double SecondaryPaneSize { get; } + + #endregion +} \ No newline at end of file diff --git a/blazorbootstrap/Extensions/EnumExtensions.cs b/blazorbootstrap/Extensions/EnumExtensions.cs index a05d02c87..d36cfec3a 100644 --- a/blazorbootstrap/Extensions/EnumExtensions.cs +++ b/blazorbootstrap/Extensions/EnumExtensions.cs @@ -65,6 +65,26 @@ public static string ToBackgroundClass(this BackgroundColor backgroundColor) => _ => "" }; + public static string ToSplitViewColorClass(this SplitViewColor splitViewColor) => + splitViewColor switch + { + SplitViewColor.Primary => "bb-split-view-color-primary", + SplitViewColor.Secondary => "bb-split-view-color-secondary", + SplitViewColor.Success => "bb-split-view-color-success", + SplitViewColor.Danger => "bb-split-view-color-danger", + SplitViewColor.Warning => "bb-split-view-color-warning", + SplitViewColor.Info => "bb-split-view-color-info", + SplitViewColor.Light => "bb-split-view-color-light", + SplitViewColor.Dark => "bb-split-view-color-dark", + SplitViewColor.Body => "bb-split-view-color-body", + SplitViewColor.BodySecondary => "bb-split-view-color-body-secondary", + SplitViewColor.BodyTertiary => "bb-split-view-color-body-tertiary", + SplitViewColor.Black => "bb-split-view-color-black", + SplitViewColor.White => "bb-split-view-color-white", + SplitViewColor.Transparent => "bb-split-view-color-transparent", + _ => "" + }; + public static string ToBadgeColorClass(this BadgeColor badgeColor) => badgeColor switch { diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.css b/blazorbootstrap/wwwroot/blazor.bootstrap.css index 5f2b4d32d..0e43e9aad 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.css +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.css @@ -22,6 +22,29 @@ --bb-tooltip-dark: var(--bs-dark); --bb-tooltip-color-white: var(--bs-white); --bb-tooltip-color-dark: var(--bs-black); + /* split view */ + --bb-split-view-divider-size: 0.75rem; + --bb-split-view-divider-color: var(--bs-border-color); + --bb-split-view-divider-rest-opacity: 0.65; + --bb-split-view-divider-hover-opacity: 0.8; + --bb-split-view-divider-dragging-opacity: 1; + --bb-split-view-divider-disabled-opacity: 0.45; + --bb-split-view-divider-handle-color: var(--bs-emphasis-color); + --bb-split-view-divider-handle-opacity: 0.5; + --bb-split-view-color-primary: var(--bs-primary); + --bb-split-view-color-secondary: var(--bs-secondary); + --bb-split-view-color-success: var(--bs-success); + --bb-split-view-color-danger: var(--bs-danger); + --bb-split-view-color-warning: var(--bs-warning); + --bb-split-view-color-info: var(--bs-info); + --bb-split-view-color-light: var(--bs-light); + --bb-split-view-color-dark: var(--bs-dark); + --bb-split-view-color-body: var(--bs-body-bg); + --bb-split-view-color-body-secondary: var(--bs-secondary-bg); + --bb-split-view-color-body-tertiary: var(--bs-tertiary-bg); + --bb-split-view-color-black: var(--bs-black); + --bb-split-view-color-white: var(--bs-white); + --bb-split-view-color-transparent: transparent; /* border */ --bs-border-radius-xs: 0.125rem; --bs-border-radius-md: 0.375rem; @@ -189,6 +212,156 @@ table button.dropdown-toggle.bb-grid-filter::after { padding: .375rem; } +/* split view */ +.bb-split-view { + display: flex; + min-block-size: 0; + min-inline-size: 0; + overflow: hidden; + position: relative; +} + +.bb-split-view-horizontal { + flex-direction: row; +} + +.bb-split-view-vertical { + flex-direction: column; +} + +.bb-split-view-pane { + min-block-size: 0; + min-inline-size: 0; + overflow: auto; +} + +.bb-split-view-pane-primary { + flex: 0 0 auto; +} + +.bb-split-view-pane-secondary { + flex: 1 1 auto; +} + +.bb-split-view-divider { + align-items: center; + background-color: transparent; + display: flex; + flex: 0 0 var(--bb-split-view-divider-size); + justify-content: center; + position: relative; + touch-action: none; + user-select: none; +} + +.bb-split-view-divider::before { + background-color: var(--bb-split-view-divider-color); + content: ""; + inset: 0; + opacity: var(--bb-split-view-divider-rest-opacity); + position: absolute; + transition: opacity 0.15s ease-in-out; +} + +.bb-split-view-horizontal > .bb-split-view-divider { + cursor: col-resize; +} + +.bb-split-view-vertical > .bb-split-view-divider { + cursor: row-resize; +} + +.bb-split-view:not(.bb-split-view-disabled):not(.bb-split-view-dragging) > .bb-split-view-divider:hover::before { + opacity: var(--bb-split-view-divider-hover-opacity); +} + +.bb-split-view.bb-split-view-dragging > .bb-split-view-divider::before { + opacity: var(--bb-split-view-divider-dragging-opacity); +} + +.bb-split-view-disabled > .bb-split-view-divider { + cursor: default; +} + +.bb-split-view-disabled > .bb-split-view-divider::before { + opacity: var(--bb-split-view-divider-disabled-opacity); +} + +.bb-split-view-divider-handle { + background-color: var(--bb-split-view-divider-handle-color); + border-radius: 999px; + display: block; + opacity: var(--bb-split-view-divider-handle-opacity); + position: relative; + z-index: 1; +} + +.bb-split-view-horizontal > .bb-split-view-divider > .bb-split-view-divider-handle { + height: 2rem; + width: 2px; +} + +.bb-split-view-vertical > .bb-split-view-divider > .bb-split-view-divider-handle { + height: 2px; + width: 2rem; +} + +.bb-split-view.bb-split-view-color-primary { + --bb-split-view-divider-color: var(--bb-split-view-color-primary); +} + +.bb-split-view.bb-split-view-color-secondary { + --bb-split-view-divider-color: var(--bb-split-view-color-secondary); +} + +.bb-split-view.bb-split-view-color-success { + --bb-split-view-divider-color: var(--bb-split-view-color-success); +} + +.bb-split-view.bb-split-view-color-danger { + --bb-split-view-divider-color: var(--bb-split-view-color-danger); +} + +.bb-split-view.bb-split-view-color-warning { + --bb-split-view-divider-color: var(--bb-split-view-color-warning); +} + +.bb-split-view.bb-split-view-color-info { + --bb-split-view-divider-color: var(--bb-split-view-color-info); +} + +.bb-split-view.bb-split-view-color-light { + --bb-split-view-divider-color: var(--bb-split-view-color-light); +} + +.bb-split-view.bb-split-view-color-dark { + --bb-split-view-divider-color: var(--bb-split-view-color-dark); +} + +.bb-split-view.bb-split-view-color-body { + --bb-split-view-divider-color: var(--bb-split-view-color-body); +} + +.bb-split-view.bb-split-view-color-body-secondary { + --bb-split-view-divider-color: var(--bb-split-view-color-body-secondary); +} + +.bb-split-view.bb-split-view-color-body-tertiary { + --bb-split-view-divider-color: var(--bb-split-view-color-body-tertiary); +} + +.bb-split-view.bb-split-view-color-black { + --bb-split-view-divider-color: var(--bb-split-view-color-black); +} + +.bb-split-view.bb-split-view-color-white { + --bb-split-view-divider-color: var(--bb-split-view-color-white); +} + +.bb-split-view.bb-split-view-color-transparent { + --bb-split-view-divider-color: var(--bb-split-view-color-transparent); +} + /* grid - fixed header */ .bb-table { /* NOTE: intentionally overriding the behavior */ diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.js b/blazorbootstrap/wwwroot/blazor.bootstrap.js index 7594cf3a2..9794e33cf 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.js +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.js @@ -814,6 +814,199 @@ window.blazorBootstrap = { }, windowSize: () => window.innerWidth }, + splitView: { + applyPaneSize: (state, primaryPaneSize) => { + let normalizedPaneSize = window.blazorBootstrap.splitView.normalizePaneSize(state, primaryPaneSize); + let availableSize = window.blazorBootstrap.splitView.getAvailableSize(state); + let primaryPanePixels = availableSize * normalizedPaneSize / 100; + + state.primaryPaneSize = normalizedPaneSize; + state.primaryPane.style.flexBasis = `${primaryPanePixels}px`; + }, + clamp: (value, min, max) => Math.min(Math.max(value, min), max), + createResizeObserver: (state) => { + if (typeof ResizeObserver === 'undefined') + return null; + + let resizeObserver = new ResizeObserver(() => { + if (!state.isDragging) + window.blazorBootstrap.splitView.applyPaneSize(state, state.primaryPaneSize); + }); + + resizeObserver.observe(state.root); + + return resizeObserver; + }, + dispose: (elementId) => { + let state = window.blazorBootstrap.splitView.instances[elementId]; + if (!state) + return; + + state.divider.removeEventListener('pointerdown', state.pointerDownHandler); + state.divider.removeEventListener('pointermove', state.pointerMoveHandler); + state.divider.removeEventListener('pointerup', state.pointerUpHandler); + state.divider.removeEventListener('pointercancel', state.pointerUpHandler); + state.divider.removeEventListener('lostpointercapture', state.lostPointerCaptureHandler); + state.resizeObserver?.disconnect(); + state.root.classList.remove('bb-split-view-dragging'); + + delete window.blazorBootstrap.splitView.instances[elementId]; + }, + emitResizeEvent: (state, methodName) => { + if (!state.dotNetHelper) + return; + + let primaryPaneSize = window.blazorBootstrap.splitView.roundPaneSize(state.primaryPaneSize); + let secondaryPaneSize = window.blazorBootstrap.splitView.roundPaneSize(100 - primaryPaneSize); + + state.dotNetHelper.invokeMethodAsync(methodName, primaryPaneSize, secondaryPaneSize) + .catch(() => { + // do nothing + }); + }, + getAvailableSize: (state) => { + let rect = state.root.getBoundingClientRect(); + let dividerRect = state.divider.getBoundingClientRect(); + let dividerSize = state.orientation === 'Horizontal' ? dividerRect.width : dividerRect.height; + let totalSize = state.orientation === 'Horizontal' ? rect.width : rect.height; + + return Math.max(totalSize - dividerSize, 0); + }, + getDirectChild: (root, className) => { + let children = Array.from(root.children); + return children.find((child) => child.classList?.contains(className)) ?? null; + }, + initialize: (elementId, orientation, primaryPaneSize, minimumPaneSize, isDisabled, dotNetHelper) => { + window.blazorBootstrap.splitView.dispose(elementId); + + let root = document.getElementById(elementId); + if (!root) + return; + + let primaryPane = window.blazorBootstrap.splitView.getDirectChild(root, 'bb-split-view-pane-primary'); + let divider = window.blazorBootstrap.splitView.getDirectChild(root, 'bb-split-view-divider'); + let secondaryPane = window.blazorBootstrap.splitView.getDirectChild(root, 'bb-split-view-pane-secondary'); + + if (!primaryPane || !divider || !secondaryPane) + return; + + let state = { + activePointerId: null, + divider: divider, + dotNetHelper: dotNetHelper, + isDisabled: isDisabled, + isDragging: false, + lostPointerCaptureHandler: null, + minimumPaneSize: minimumPaneSize, + orientation: orientation, + pointerDownHandler: null, + pointerMoveHandler: null, + pointerUpHandler: null, + primaryPane: primaryPane, + primaryPaneSize: primaryPaneSize, + resizeObserver: null, + root: root, + secondaryPane: secondaryPane, + startPosition: 0, + startPrimaryPaneSize: primaryPaneSize + }; + + state.pointerDownHandler = (event) => window.blazorBootstrap.splitView.onPointerDown(state, event); + state.pointerMoveHandler = (event) => window.blazorBootstrap.splitView.onPointerMove(state, event); + state.pointerUpHandler = (event) => window.blazorBootstrap.splitView.stopDragging(state, event); + state.lostPointerCaptureHandler = (event) => window.blazorBootstrap.splitView.stopDragging(state, event); + state.resizeObserver = window.blazorBootstrap.splitView.createResizeObserver(state); + + divider.addEventListener('pointerdown', state.pointerDownHandler); + divider.addEventListener('pointermove', state.pointerMoveHandler); + divider.addEventListener('pointerup', state.pointerUpHandler); + divider.addEventListener('pointercancel', state.pointerUpHandler); + divider.addEventListener('lostpointercapture', state.lostPointerCaptureHandler); + + window.blazorBootstrap.splitView.updateState(state, orientation, primaryPaneSize, minimumPaneSize, isDisabled); + + window.blazorBootstrap.splitView.instances[elementId] = state; + }, + instances: {}, + normalizePaneSize: (state, value) => { + let minimumPaneSize = window.blazorBootstrap.splitView.clamp(state.minimumPaneSize, 0, 50); + return window.blazorBootstrap.splitView.clamp(value, minimumPaneSize, 100 - minimumPaneSize); + }, + onPointerDown: (state, event) => { + if (state.isDisabled) + return; + + if (event.pointerType === 'mouse' && event.button !== 0) + return; + + event.preventDefault(); + event.stopPropagation(); + + state.isDragging = true; + state.activePointerId = event.pointerId; + state.startPosition = state.orientation === 'Horizontal' ? event.clientX : event.clientY; + state.startPrimaryPaneSize = state.primaryPaneSize; + + state.root.classList.add('bb-split-view-dragging'); + state.divider.setPointerCapture?.(event.pointerId); + + window.blazorBootstrap.splitView.emitResizeEvent(state, 'OnResizeStartedJS'); + }, + onPointerMove: (state, event) => { + if (!state.isDragging || state.activePointerId !== event.pointerId) + return; + + event.preventDefault(); + event.stopPropagation(); + + let currentPosition = state.orientation === 'Horizontal' ? event.clientX : event.clientY; + let delta = currentPosition - state.startPosition; + let availableSize = window.blazorBootstrap.splitView.getAvailableSize(state); + + if (availableSize <= 0) + return; + + let nextPaneSize = state.startPrimaryPaneSize + (delta / availableSize * 100); + let normalizedPaneSize = window.blazorBootstrap.splitView.normalizePaneSize(state, nextPaneSize); + + if (Math.abs(normalizedPaneSize - state.primaryPaneSize) < 0.01) + return; + + window.blazorBootstrap.splitView.applyPaneSize(state, normalizedPaneSize); + window.blazorBootstrap.splitView.emitResizeEvent(state, 'OnResizedJS'); + }, + roundPaneSize: (value) => Math.round(value * 100) / 100, + stopDragging: (state, event) => { + if (!state.isDragging) + return; + + if (event && state.activePointerId !== event.pointerId) + return; + + state.isDragging = false; + state.activePointerId = null; + state.root.classList.remove('bb-split-view-dragging'); + + window.blazorBootstrap.splitView.emitResizeEvent(state, 'OnResizeEndedJS'); + }, + update: (elementId, orientation, primaryPaneSize, minimumPaneSize, isDisabled) => { + let state = window.blazorBootstrap.splitView.instances[elementId]; + if (!state) + return; + + window.blazorBootstrap.splitView.updateState(state, orientation, primaryPaneSize, minimumPaneSize, isDisabled); + }, + updateState: (state, orientation, primaryPaneSize, minimumPaneSize, isDisabled) => { + state.orientation = orientation; + state.minimumPaneSize = minimumPaneSize; + state.isDisabled = isDisabled; + + if (state.isDragging && isDisabled) + window.blazorBootstrap.splitView.stopDragging(state); + + window.blazorBootstrap.splitView.applyPaneSize(state, primaryPaneSize); + } + }, tabs: { initialize: (elementId, dotNetHelper) => { let navTabsEl = document.getElementById(elementId); diff --git a/docs/docs/05-components/split-view.mdx b/docs/docs/05-components/split-view.mdx new file mode 100644 index 000000000..9bbb2b790 --- /dev/null +++ b/docs/docs/05-components/split-view.mdx @@ -0,0 +1,225 @@ +--- +title: Blazor Split View Component +description: The Blazor Bootstrap Split View component arranges two resizable panes horizontally or vertically with live drag updates. + +sidebar_label: Split View +sidebar_position: 26 +--- + +import CarbonAd from '/carbon-ad.mdx' + +# Blazor Split View + +The Blazor Bootstrap **Split View** component arranges two panes with a draggable divider so users can resize the layout interactively. It supports horizontal and vertical orientations, percentage-based sizing, nested layouts, live resize events, and Bootstrap-aligned divider theming. + + + +## Parameters + +| Name | Type | Default | Required | Description | Added Version | +|:--|:--|:--|:--|:--|:--| +| Color | `SplitViewColor` | `SplitViewColor.None` | | Gets or sets the divider color. | 4.0.0 | +| CustomColor | `string?` | null | | Gets or sets a custom divider color. Accepts any valid CSS color expression, including CSS variables. | 4.0.0 | +| IsDisabled | `bool` | `false` | | Gets or sets a value indicating whether user interaction is disabled. | 4.0.0 | +| MinimumPaneSize | `double` | `0` | | Gets or sets the minimum pane size as a percentage. | 4.0.0 | +| Orientation | `SplitViewOrientation` | `SplitViewOrientation.Horizontal` | | Gets or sets the SplitView orientation. | 4.0.0 | +| Pane1 | `RenderFragment` | null | ✔️ | Gets or sets the first pane content. | 4.0.0 | +| Pane2 | `RenderFragment` | null | ✔️ | Gets or sets the second pane content. | 4.0.0 | +| PrimaryPaneSize | `double` | `50` | | Gets or sets the primary pane size as a percentage. | 4.0.0 | + +## Callback Events + +| Event | Description | Added Version | +|--|--|--| +| OnResizeEnded | Fired when resizing ends. | 4.0.0 | +| OnResized | Fired while the divider is being dragged. | 4.0.0 | +| OnResizeStarted | Fired when resizing starts. | 4.0.0 | +| PrimaryPaneSizeChanged | Fired when the primary pane size changes. | 4.0.0 | + +## Enums + +### SplitViewColor + +| Name | Description | +|--|--| +| None | Uses the shared SplitView CSS variable defaults. | +| Primary | Uses the Bootstrap primary color token. | +| Secondary | Uses the Bootstrap secondary color token. | +| Success | Uses the Bootstrap success color token. | +| Danger | Uses the Bootstrap danger color token. | +| Warning | Uses the Bootstrap warning color token. | +| Info | Uses the Bootstrap info color token. | +| Light | Uses the Bootstrap light color token. | +| Dark | Uses the Bootstrap dark color token. | +| Body | Uses the Bootstrap body background token. | +| BodySecondary | Uses the Bootstrap secondary body background token. | +| BodyTertiary | Uses the Bootstrap tertiary body background token. | +| Black | Uses the Bootstrap black token. | +| White | Uses the Bootstrap white token. | +| Transparent | Uses a transparent divider color. | + +### SplitViewOrientation + +| Name | Description | +|--|--| +| Horizontal | Arranges panes side by side with a vertical divider. | +| Vertical | Arranges panes top to bottom with a horizontal divider. | + +## Examples + +### Pane content + +```cshtml {} showLineNumbers + + +
Pane1 content
+
+ +
Pane2 content
+
+
+``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#pane-content) + +### Orientation + +```cshtml {} showLineNumbers + + +
Summary
+
+ +
Details
+
+
+``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#orientation) + +### PrimaryPaneSizeChanged + +```cshtml {} showLineNumbers + + +
Primary pane
+
+ +
Secondary pane
+
+
+``` + +```cs {} showLineNumbers +@code { + private double primaryPaneSize = 50; +} +``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#primary-pane-size-changed) + +### Semantic Color + +```cshtml {} showLineNumbers + + +
Filters
+
+ +
Results
+
+
+``` + +Hover and dragging states are derived automatically in shared CSS from the resolved base divider color. + +[See demo here.](https://demos.blazorbootstrap.com/split-view#semantic-color) + +### CustomColor + +```cshtml {} showLineNumbers + + +
Pane1
+
+ +
Pane2
+
+
+``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#custom-color) + +### Resize events + +```cshtml {} showLineNumbers + + +
Primary pane
+
+ +
Secondary pane
+
+
+``` + +```cs {} showLineNumbers +@code { + private void OnResizeStarted(SplitViewResizeEventArgs args) + { + } + + private void OnResized(SplitViewResizeEventArgs args) + { + } + + private void OnResizeEnded(SplitViewResizeEventArgs args) + { + } +} +``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#onresizestarted) +[See demo here.](https://demos.blazorbootstrap.com/split-view#onresized) +[See demo here.](https://demos.blazorbootstrap.com/split-view#onresizeended) + +### Nested layouts + +`SplitView` supports nested layouts, which is useful for editors, dashboards, and multi-pane administration tools. + +```cshtml {} showLineNumbers + + +
Explorer
+
+ + + +
Editor
+
+ +
Preview
+
+
+
+
+``` + +[See demo here.](https://demos.blazorbootstrap.com/split-view#nested-layouts) + +### App shell layout + +You can combine multiple SplitView instances to build an app shell with navigation, content, and inspector panes. + +[See demo here.](https://demos.blazorbootstrap.com/split-view#app-shell) \ No newline at end of file From 728eab0d6f3f91aae2dfd48e5181dd571d0d1a75 Mon Sep 17 00:00:00 2001 From: gvreddy04 Date: Wed, 13 May 2026 20:17:19 +0530 Subject: [PATCH 2/4] Enhance SplitView component to handle parameter changes more effectively --- .../Components/SplitView/SplitView.razor.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/blazorbootstrap/Components/SplitView/SplitView.razor.cs b/blazorbootstrap/Components/SplitView/SplitView.razor.cs index 2e11e591e..739d8c093 100644 --- a/blazorbootstrap/Components/SplitView/SplitView.razor.cs +++ b/blazorbootstrap/Components/SplitView/SplitView.razor.cs @@ -5,10 +5,13 @@ public partial class SplitView : BlazorBootstrapComponentBase #region Fields and Constants private double currentPrimaryPaneSize = 50; + private bool hasReceivedParameters; private bool isResizing; private DotNetObjectReference? objRef; private SplitViewColor previousColor; private string? previousCustomColor; + private double previousMinimumPaneSizeParameter; + private double previousPrimaryPaneSizeParameter; private double previousMinimumPaneSize; private SplitViewOrientation previousOrientation; private double previousPrimaryPaneSize; @@ -68,7 +71,18 @@ protected override Task OnInitializedAsync() protected override Task OnParametersSetAsync() { - currentPrimaryPaneSize = NormalizePrimaryPaneSize(PrimaryPaneSize); + var normalizedPrimaryPaneSize = NormalizePrimaryPaneSize(PrimaryPaneSize); + var primaryPaneSizeParameterChanged = !hasReceivedParameters || Math.Abs(previousPrimaryPaneSizeParameter - PrimaryPaneSize) >= 0.01d; + var minimumPaneSizeParameterChanged = !hasReceivedParameters || Math.Abs(previousMinimumPaneSizeParameter - MinimumPaneSize) >= 0.01d; + + if (primaryPaneSizeParameterChanged) + currentPrimaryPaneSize = normalizedPrimaryPaneSize; + else if (minimumPaneSizeParameterChanged) + currentPrimaryPaneSize = NormalizePrimaryPaneSize(currentPrimaryPaneSize); + + hasReceivedParameters = true; + previousPrimaryPaneSizeParameter = PrimaryPaneSize; + previousMinimumPaneSizeParameter = MinimumPaneSize; return base.OnParametersSetAsync(); } From cb42f64a4dd8bd9cb926c1de012710f0fc105bbd Mon Sep 17 00:00:00 2001 From: gvreddy04 Date: Wed, 13 May 2026 20:24:46 +0530 Subject: [PATCH 3/4] Remove unnecessary padding classes from SplitView demo items for cleaner layout --- .../Demos/SplitView/SplitView_Demo_01_Pane_Content.razor | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_01_Pane_Content.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_01_Pane_Content.razor index bf53a4434..5be3fff26 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_01_Pane_Content.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_01_Pane_Content.razor @@ -5,10 +5,10 @@
Pane1
Navigation
-
Overview
-
Customers
-
Orders
-
Reports
+
Overview
+
Customers
+
Orders
+
Reports
From cbb10bfda5d715f398473b2fd9a55b98e11fc8ef Mon Sep 17 00:00:00 2001 From: gvreddy04 Date: Wed, 13 May 2026 22:22:38 +0530 Subject: [PATCH 4/4] Refactor code structure for improved readability and maintainability --- .../SplitView_Demo_02_Orientation.razor | 25 ++++++++++++++++++ .../Constants/DemoScreenshotSrcConstants.cs | 4 +-- .../wwwroot/images/screenshots/split-view.png | Bin 0 -> 35737 bytes 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 BlazorBootstrap.Demo.RCL/wwwroot/images/screenshots/split-view.png diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_02_Orientation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_02_Orientation.razor index 1e23e6d0e..504b646a7 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_02_Orientation.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/SplitView/SplitView_Demo_02_Orientation.razor @@ -1,3 +1,28 @@ +
Horizontal Orientation
+ + +
+
Left Pane
+
Navigation
+

The default horizontal orientation places panes side by side with a vertical divider.

+
+
+ +
+
Right Pane
+
Workspace
+
    +
  • Dashboard widgets
  • +
  • Tables and forms
  • +
  • Contextual details
  • +
+
+
+
+ +
Vertical Orientation
*y|-s~&%XDzoR@R%ncO>f?w$GGnfZRcHS(Fh7R!0= z^9&3OEZR>rUNA76ilZO;=NRc%J~V!KOfODAUuZpIKo0V*&`(Y~s_CgQFjU4eAK09s zpEJFFVg_YkxY%)goalBhdd0vHC8n*RX5?!{q_G4VPrNW_hUcZIToFC7^KD&3~ zXy5(#;dcFZd^Vte@3%0 z9haM$AO3le@2&r~&Cly!byF!#;%@?a(cC7cMW~H{Y}gR3w6!6)nZMcZJ5sy{&p5sPa*6T-1+gdN9dfr?tB2BTD`2A8&m6uEhCDcN{ThAClLp2-nvy{a7 zy>(0|yWa}Bz1IHlrPxb;!TR+Q2L8x9$3n3qM+(5f*T#>K0f|utIHE)n1j_u*DphVx zz!)!{Z0d`sqkLQhO&%brt98~sRngYV@UV9u^ArV-|RgN zwwuVlKLo+p9aGlHM@EA{?#DOj2eusgxZSgNr+sJVs41_%W4A#TYPI5++w&1J30gz4 zbIkc4{;;YOoZg@c1+8!zr28)ZP zt{5FcdafIPw#^i#NQ`#+(A;wO>!E23_5pNmuV!bOkD!9ut%1IzX?;qt3T!6s@zFc- zFs;Vm!?fe~AKC4~{rJG@a?R-Fd_i8_plu>vc@2Nr0?^HJW_oNdHV+=X}js6-2d;u)AdRHK8He$B~ zC)!1v0-t=^);jN$d18unm@%zl2pCnP9f|KtoBoicO-QCqZ&gd$D~#)e$)8*;8met| zpJ|Ow^Sa9&5_;(|D|o`de9EG3$KD|Pdwh}d00A`{VbbP(lM-u+)81P<-YAB-iC1oF zIT>y6`QHQ2HJtijHbl3 z%QXRRsQ{CfI*6NB5g3ZIB-%8E6^{7{;1BRjcc@-&o+8HsN`FCQziD}Oy6%89d0mtg zK+DN3?y6z4zKnk9=)mXOS@Eu9P60-qs_b^JnhW#2u0ynM)R=2DHfzH^=wdk;@A^IE zv+TLWH<#^ISJd9v>d(RCBC-14cGdzVs6GMzEMyRU2rTT3OBmgF-K*!)WbrF%Ez-X* zoK(`ClLN8~0I-@ArepL*wW!~~kFPF+@~eb^?V3wzV|!*|Grvc$l9z1#oALv#D3MIU z?M``b+^&rDn0ov;dn+|9x7Tf|KlAF9B@h(zi;{T5(}fh86z@ws9g=D$dps?Eeig0& zajp>l*`j+&X)gb6)|UMLZFXhE&-e3$7LMLCvZYME3g+L3*X<0#4^5A#ZNY+1j%o9dYXkw5ZP|d$>}0bxgOrbct-ty(z##|? zf6|244_5KkjX0!=!iuz5VZ*%RyuHK|48%27_ZA}IATV#Q!R;db8A1zxw-)@V8LBG? zpTnZrrOeztuMPh9J+5k!Yu^+i8JvZ9t`8|MxxzL>mCt=X?&i#%^+4 z{q5TPIxFm<`_Yz@xy+}&sq~49IqLsb$w4Oz5W$D!%CZxq^a{Y#spAF=mcUk}WX_xH ziiCiF&a>9~6Ih{`e>owW)_=ws7x`YEJ-+R=Ltg?@apZp&w)uaPX&-Z4D{qW`cdP)m z(ykk|_ss@|0twXZjQj0x1?>k^ry{=GO8$P8drA}!!NE1F$;{6 zoq2{P9Ff%bR#yB0pUCrH?l1Ltr~j6J;d(9UNDK&Si1=%~z+mkaTYmTwy)^Ov9>F}aiyPcZy7}Mao zWNU#Y*EHa{3m84Key@k@jcSU_>W z9%&VWNIQT2d`R!==^Gw!m4G=e&thQ0a@XFCBrcq# z4ji1w@S!0nuG`k_wV?h2jE=kBT%xFkLyJDcyNf(GOFzD2II%oN+Z$pLf9>{^_sS`RDA=vViY1r@ngDYFwei?4y^fum(}|!& z$Da9q|AwT$0e$r)#fxiA|S%5 zkNb+=WQ$uxtyr*9Ko5PHI5z9E-2_eiQQVkNZCE+nyPnrFZB3QC$eK1xs>|MDZ~P%+ zNOZ)&{u<+kY*Hx8)!5iL?%*Q$@8XyP*n)a5c)w6F_$%P^$>ndZdJDDf!cl-TEVyBe zPFNullH@AUw;=-OSGcM^ApPPDw5ZPK)~?VmBt)!ZJXPnEfWynh)j zYQ2(o3ZcHXjegQl+;E!V=fDWlqu-AkNv%!W;%*=Jgj^}HM&Aw!!yxYZ+{%L<`Ha1l z?_P97G^r%OJbO2x6DWC|c$3mOIa&(@lZxmqwL80~ZPDNMinzv<)e*^9pm%HATGMbs z$@QrH)#TIi86NK$Wx~sqF+KH}E*XXGU!lEZ7?k|MsI6146!kRn;k})2To}2(pk5O z;#XQRdZ(Mt-hw><;Ze7gFNj+ds^ha8mprzKD439Vl8GoV*Fv5yDx{|@v!@IZL|v*t zdvWKI+#20_g4Y&JJpiSA$hJ1ZO#rPs7Bzx6F>H()2UUbQ@d7mw5m&%rzkd?IkeU3%()k%lJkX>V(o?Si{*0~f&-^qt<9=)Qx zl^QwWIeHf3H^i8WYco;B1-U&C28UnjFg~E}l=K_o*aG!Fq{a5ZG8h!1D%yUsr>50} z51uYBpUq99HAvs|8ru5`&UELLhOM&sWA&bKu<~#99?hdqWI@D~Lex%`Q zopF&P^#sGWd&aN7zb02zdn*JT)W`5C7POqbxRy)%Ya7V05v|(1Q=3L6$IUJ^%F|f} zsu1h5G4ZywWxOT8$7>CvcjnBQ_E#h69LN0Rzqcs%f5=l((DSCKW`}o)ZGRGhZz#Ml zXf!9yCD=%$QI=#@wcOcI~=1SVTfii^N{yk=&+( z8f`=Aw1@2W*N#?RH>GICq~xT5F-15l4UKJoZ~maiH3^2&6kBR?zY(Rg6aUcstjDTi zqejR2KvUXwsZ(9I=o$)hNDBO;0NeTk4KW5EK@B&hHc6T^Q#N5{04YAMYnkA)x41Zw z>JXAo6mc&6HDccwWEJY&9pvL9p21C$YA`7$mW~nglTP;^rB_$)0?mQOmsJXUOA<*Q zrLi*LiRP8m2MU3KBR_Sdt6Upf;mw(vGyG+&R5yO7qLCev#bfwYgfmpC-#c$bdq%ZI zH&-hxRz#Q|F@l{gG^S2pd~SyH`|Vh`*3|ZqcY7w3g3IYMx`y5oJWxkisjSzYVt9Ko zcLCr?^r<;qMD`C1085%2-*MZaJ0ua$yC97h%_H+z7&pBV01zgb9xoOZW7Wdot+0Ho=DYbTDBs;gnXi zbC520U|%m>3c7DH@l^4rfk1*NrC0_b+ihkSgc+Lfqe_BhMd2RNNSSUk2J_u3v)v zBE?uRPsHbft4VXV4wSE}Ko*{w;x{!IfZH>}j+RuX?qU2+^5It8{oH1f({Kq?xW;2l zhKR4!W33NLb@rZEr34p^ognjjP51~fpUSfjnXwJfPgn*XHK-W`?pY~s3q%uO)(vjF zSJvYkh{62NA7_Y6F_Otm_BMvoqB^=x7rdD)Jkd81Qfhu3QT;oicm)V$h+FT%?A+@1 z0@vjYyjIOBa)r`LQ%q|j6tEP7`LQC?B5y$ciL<3FJgeKGVNu&Z!ZnqR97xo5g_+jj z9Z|Xn$oDAXLfoids!DKTcq;4dv+(57)TEi!vMwjW^p@}^lj$Aaa;AmvIA!Y0lX99$ zuCtohRBjiX$Yf&1W1`9vI4sgkTFtr!5sThYJ+I29->kkD&8qs$TzRm~+$VV7w(iQJ z84}~~(-O3~uvwY!GcYzzHWd1hn7^O$GP-nUM0|Dj8drgfvXK6Hbwq;@dh3!p!)?n| z7w(-9rpVn`{fz?)as0!CBKw13AuqoNlvT!-`gX>mQ2%|^4$kxJ>@eXu$oO7jL|YU4 zM#%_(OC!ifJh>!5_2ILkaaY%h~>u8?*t@{I5PNCe1CM=APjVLX`(QA7$X{Q+F>a^#kCT)F5uKL8Eo8>WI7ch_!wc@Amio)GwY3xCt>=AuFmb}chPwv_*Rs2%( z3ASDQ#oCjP=9-I}pMg-yLHPWU#y}KR88Ta((Y}E+lNBcW)NKfLuiAM#3}t?LZf&e& z4)2pU@~r0H@6%Pb3z`o|6dh&#V*Gh<6Sy4>b_Fb{AyTE(I^oG|+iiiOz-lvc=IebS zV(W8dX-^H`fm>QPiBIQn`EXHCUT|ymi!m{H)<~|{2+^$qp9gY3*LLy_K2KK650vT| zT)YNKNp=Wt>8zeTrX${Sn$7t_;})<4g(IQ0jP`%-mZ8mKTbm!+!&7t8HUc!fz_n`# zW--hplPP$6-!0+;|AGn^rWpdt)l{rUmt)?8?s%+B2oF^S%P6Z}*=G*sN8B@=O3&pF zv}f3yeS(n=r`-e{4tce&z7oxq7(R`DM_7vE!Z?G+G$E+=qIwN@-_^vLLY3FGX%Ma> zErAsQ#0An03ncJ(0mq(8*W8to?h!)P(c=5IPZyfw$JLdqP(4nw8LbN= zpo!$}q!mg7EBLW`e9Qspme;;x;i{pTi_EE|JPK^dq{u)oqXdlXm62!^rLR)$h>LLw zYmd3(0Z=e7pH*^b z2kncBhm|iaAalZ?%5&$!?TskUm7TrRg;!|>;kSODr1PX~yND-ikEUXWWVmLKUGOd* zCw}7`FJt&@agXQr#741Z$P}V>cu0!`PeA zR36jKBl#TFFsSy>ePm=HqU(9f?q&vs#N!>5hin)PGVX9`l#Gop35{9n%znHTS@x*Q! z!3>ZBOAV`Vdp@6RMr|RtxRq_bJQWHw&vjgXF;W;hks_9sCz*CLxWOu#U}!SbMhkhW zoEo;fKZQ%39U5qEvea=qKxxKEEOA3qG(ksSKMF=O`8;)jG<=jE_v&>my0|9*1FZ+# zi1#mgHK?RlV=r%1KGkJWHnM=;>Q0l?ODzpoC#=IWOH+BE+heDPWqMP)2w91hOGP>_ zYCa8Cv^~9$V^>h!wfg6E8E;`k*9-uJ-IlFBySBYJq7JBf71EXX$>hz7rMcJe`>HPi zJLO}y=I-IRm36kiEf{lqCwf|U>L$#tg#5OGB1d%w8u~nG_oss2p!&Eb3B?6U*d#{_ z^|@1Csk_)*0L8lWaPnwU*Gwlk6 zoM%#pHBXc9ZDT}pB+wSi8{WsCDpx4To8jHxkt&1#3Xmh5jz0Kthg-2wzX=+cPd6RP zDJwkwFm9%9QDOvh>k;+G(Krx*qL8}3cB63Nd+ZFQI2Dr)2UMA)sa{g?|S4?7mjHaS#J{Nvr zlEzxH)PSV9$|!d)w^ICJOI`iaMW(ELxHc{HF7>8(v+mditum8Fn&AGL4`P2CA6Mjj zA)1zQA4w^8S1Q$V zyf|_8Vrtcyxk$wq!F5hGnNKcG+I?SwsgBnPaz$*o3mP3|n>O(O4F*f!nWCoM8)J{| z--+(MHGyqLoWZ(&U+6w0I2eCz_%_|9%c`6BR`>o6~z5xO8N zt_Ut%=#E{RthB=i{H##Hi<8wEM*+bbWMKqOj3p&MT*&PO&)TK8QnLcH{!{4v{U;mH z8D4$;TsHwF!=dDc(!}3?`JdAHk)|JP&x&PvN@u3Y$F$CE#^~8IHeHjYqR92(izQor zd!2_JQU5CC8>JuR)$N!|`b2wcwgHJVb*Pnh!NV=$Q(-ENq!Qn`f6U6u=I7!N734@W z;idtWS5h|3njng^Ws`JS7_aAyl5zN)!m&kw!IJaFD6p~sotYOWpsb}ud??eVTy$$@ z==pqSyeD=qsH9O*jJZ|eAC-EeBqQd;u+4)Cddr91-@YjvKj5vm+JB~v|9^*>TS4nX zAT*`m;!(59oM(UGIRAux(@R6}UcJ46+BI>L%9IjJqn8kv$O~`DdFy4lPaCBlbJO$Ff`cIe6Vm5?kjJtNn6K7Db7t=tdbYy!xxn z8*^dh11l}(V}!shC4!-W4hf8Zh(FGDzk0@LiQj+58zT`|{Cp4tUc8Ru`>U*(> z499V-#e_rfHManW(%E95Zt{H)etlQ1sUfFpdO--VaI)?o(`B4U@N1!+sXIrvG-<$} zFf6}k9Q1wBl=#&dtanFGn64rVD2Wg2*(v!b?Sn2$csggL1W7SJ;DP0d+xZSyx~UtH z7G$tNR-N&jE@`!;4qs+6i|&0=7BmUHsw6gRDsC`0l!bGD+>%m?O{(G*PeT=a$B`aC zt4?u*YoHx%+pGipqpe@_jo95TSgzU66xy}8O-*E66DoFSLP6T|tbuMi0+#$^=k$9+3fhaAc;F@yUr zQ(u*`L?`7I36uFm+|4f2cthb1)ZgYr@3c_??2GDI2hj{(al4m?D6e*}COi8TyObbh z!y3=Rsn3G6)=BWiKJKR7zb)EUKCfp}{JREJq@<>5$L*?S=z9KHk&L?<_|GQge>4oS zo$RwY~CyZ2e)Bze3 zJKj{KgybD`%?>yB{ig1(X`4QmPY5~ff{|vac@d#@;Y+=p;fMrw_9akR6_MHa8qfdav7d^ouH%pu{R++Gh1PiuLQJ1!-wht;oj7_bI+l zq^K8=V_$zrOv~rI8(=J~1cYuo*~0lsy}DykoYjWIYYSCd@etYgd;L;U3#TTMp~|M5 zZXgNEQ)uphbrjg?E+!-pW2~Uq;oBEN(a`Bb-oi> zEo?YlzhyK?jpoTupJjMEmTx$rktiB-y6_E$ppmQTOsYoor0qn4xIWK#!S8;-X-K)L zdQ7_?w}5#OWUG6Cx#Q_0U<{Z;AD)DPSHH?(dOKAo0#shQ7iU6{D3Q)~ovI#cVr#OB z;&heWlWG}q?p0!!(MtxV>_(2fUl0%24Yp9!%4g174U4S6^^tE2IBtL*C&DBX2BN}O z$VL=-^6!?$QnU@96RUwTb55;{J}EZEcf?R|CzBJcR^RdQz=YJ%qBl} zCE9ROt%mtFdS2!U=OPJjSO-2Fn5}-4gdznY+NJYS-Nhd)41P_}xL$|4@( zaj^WVj;*5mh339v8Z6UK%mDpsP>}uQ*%iB9mDI|mYYcDqUJ9L$+dqrM)z~$ms%}n{ zjp)a_N{w0UIxUq7ChHFfd^QiB{)#HdR5Kjk8ys%;b2E9sHheolE98ySr@nqPpp73Q zXpyI870QD!O=#%KuI4H@xo}V3punkm<^{X?J1K3p3u78-M*73FIP`+B?xvZT`omaq+Q+b5H-S+$OqG9 zSnn%fk4{K|QlwY3KmPq#Z~j&-XBUqm+AG2NsrlsKSH<_5r@LK7Y59@hi-o=r`|vDW z72<@$^*eONH=NIi(U$hoh3f~s$6^Fz*ICUd>v`i(s_ElGmcG${{LC)R$$^sjns%ct zud{dQj*~ufx`U}fud8b^WTvl`YdNtsO2 z5}lr*-(UB=EO>&U&@hQ9?r~@?8bF&seB6UifRPp_r+F$HvzuTvS+rXy<+ zj2Kj*u3J};y)6T#T2L?cN}pDTnpZg>%yD@(6Qy=~M%Cviv$(e@Dt8TlI6`)+1*gm& zuTlD}E1*5RuE3UsRe=wmsyek%&H~DfTbyk2$**~bc9~AFAC1W{pR(>-b-57gfb`Dr z3p_Oc;Tvyt%gHuX3^8Ya{ldPsGav`@Jm;+LPZZ2pYUm7|ixu|2@AZqr8rh$<}%;)rLfg3d50Vs z>GcK2G*`_b`@VpwKuCtg$|JkDa4m|9Ik>b6;28ZTcVspgNMTS06-Uahpug`nxTcl^ zU36#lq=7ZrVlT0IYLDd)eWoI765*KyzYnIi+i7p0X>(7a0 z6~Rv1{EYF;I3hIj*@LXOH8^)*XN2Ej$a8o0C250r7Q{m=`-YaRnn)k-Ap<>c3TCDKEY?WnQ+^D^buT(fk7ejzh*b|Dt<>d#4XEN%nfN6f*2SY z&mH@A3iStBY8#{C-x2E+r2dk6UjMvBue$he)x}Zvm00dTt-_M!rgIF-UzXYFrs^yM zdb5Y!59!#A{(IPT{r{Q^C}J$4r%Rrz)E6%aXj;7JNaW>U{SO=re3p3(cjY<*Gmm7G zoL75WHGQq(I`Jjh!X+;A##@hSJ~Enu5x{ln*&^fppT*{saNyvbdKK#85kHmgha^6( zaq^*7!i9&p!awN6-Owa-h(fG9~MfY#zi5^`RW9|Z5t`Fax7eDR7 zmexGHWt>YHSM1)?Nsi|L{DUO_ zb#It?$|C+Rf4KEhxcbM1YC7p!2vzOsZ`&YwKl7_WwQfrs(&!@2n_2I7`=qkxH?`qCu%9(gr zJ}o8DrPPe}$PaY+6z;n><705JKNPI8)vbEC^#dsz9PrCP>uB$*LAhuH#i=Sejw>)HS!>iz5EX5&Td+b;L;zs30hy(yCOX(ee)m=%4Z4v zWl}waD81Z2kMcM1gkueqj533eKjkX_ge~bPBaN}#mRxNy_YHm1@7wp)A8tcu8h`Kn zKD@T{#(v-86&#y5pzk&m+_tbcGF@#}NoCIn>2_%28X+$Q^G|!={W?7I1a5&Er0TTF z z8DEb!M%eASXmdjR`$yANTfbC|GkizTaJfqAk6WfGP}-P6ooihaSqsy$-*W`PJdUX@ z=)12K6aOZ2dx0^wZFPj*olz&;FL04t7XJ89veDkKYg_`8Q6$}OAG_||A1SF7%T9{- z=EMCcN1xl!PLzC%y!U2qv|QvlW?a6*!ycHV5Cqv~EMHAz{mku*_%VG>t(w2;=1a$3BttmOU)X!EJ`+0JKT36LG}?pn@j zSnJ{@N^+!!rwQUZS{GqGCx)w5q^rNyc4P<+FK7BI*Z%4Bm#i|F-}bZF2R6>RlSaj$5JILN9Q)$P zm^?kMg`E28KC*rTzq?NErLONNB})zoED->j2_tr>zY4hAjPj@IXN8zE8wvNliIZZM zMqe9|Z^$>7VxB4;jr@rnIiQ$vuO^le@SP<(B5e%>@ibU1w)BQO!s9T2t9j6_+sVSX zJiA)t=Av4F@9K)P=Wz@2X;7?%3d>OOHY#VX3wBHxQ_IJEigT9l{On|KrH zSHVl>O}?#0Eq5i)>18_@C2EVThFr4n`3FQlIn@!Z)`mWdd`&j88e) zKvS7|7>uHz4E2?t8d1 z4i=3Yo6((ooK2@D@zgU64;^1q)L*el3l=N=c_%NexwuF;MOj;!7w33VHEI7nmdOtE zQF=*)Uqu^0kP2$N$>Or`P-w&(V=cjIy zESz?g3HV%e#+}_(xWA4rbDIf(J<@>$sG)Cm!=9#Bkl4yUk9R*_}3T^x5^|q=kO>M_9;f*e~1) z)_7RR4iT2eyhmIZVPL;I-p!~342~x%XPcE1A56Ij<^kF+q=kid87F_Mu`Mv$I7+xC zn($__n|#pPT4|_gTyT$gsd@dOVyAuTRRO1^V*#e!Uk4TG+6C)}JbO-3jjmY?I>9hJ z;??rb%T;eW(RBPuS`NS&@+$!tl*e0clw|e-&X5vWmpcW zEXYA%GO??oO5VG8jXF_Gv&@~c&N5)at-pSA8)Ru36Hk?(w#XW=3>Q_cCuXd2&&dNP zHl!|MB@{Lm?D`$JOX#{5^3(Ywbk?EUEQWpjQ^6G$efQP?$(%hg>oj7jgbjQ{d&aimlxxpo?h0hJjAE<5hxyCYs@4@{B7wV+{QsO53 z(zWNWu$eNdxWzXlviF+42hBSvNOq0@hgf4*s>W0U&|5A|M6f)L1?ExQG8 z^utzlUlzG$uYgj?s=1Qn6Ri#=lqZSNP;<90$;c<_5qYxWXr~wZ1WDtr)aMRh?-V3o zIUp&4!}->0#_mDfdr7iF2hH%Z9w4{d6`8@F*p^R)ke(ZNIc13!W0P`j#UV3t#UDSa z7$OhnaDs&Bytms&dVG1ooK1S8CihTt4^&=qo_+h~`ECQr8$Q{>W26jRd8hgG!JgPF z>gQC+GP8mN5ulrSRBiQtLfw4V5$_%gZb<{~;ZBmH&0%+dPz;CE%yLe4jj_~@ZYCjn z0g0jsI2OcScZq>DDR$vYRM+21fIh}OGR*7lNG}g7=dE_VN zBJX7)`%>$n^|X?*1^ZtNj@7Cwr6ExgZE(9AJq*e!LU)OAaw zSj{0YGlJ8vS>Gz!P3lZ+;tlmYr4FHD5_CsC&0vbq;=CweRsLu4n*xsHx3$^HO<7EI zVFA|r#IO{bS3GOF`m7Mzl%pA|?+pUk6i!{a@Fd>m&t>$XUPFo3xzIJ~e9=>DS5vyrm)l^q zv5ADykH;IM&~xhuCol5uno7tD-OpUXdp&uVU#={6cWt3IC%2l$6Z+6!Qcu}Hh??=y zN{wBpb613g(w-S8(7(GIQ8()i4N@7UTFBDjV^t6{PK()I4`MdH?mII|$Vjpqe=XcixSwk06+I24QT)PEKO zfN59%{f|Dm6N+hCjup$_@YMoD*xTj72fJ z-ZD63@huer#Qm{)KNtKTJ3ZAo(Nh`iJT=0y@7}a`n}V@1kYZ=;6mOEthB?B}5Lg?q zV#J|`oN%j~-j`n8HHp}Lgnnt0S-5|P?uq;s`7V{NNgmeqy_Zq0rc+A?=#T0K8*&7p zbf3U)_b?KV^49ELrY9GO8vd2tT!)Q?w0ic^s`tWNk+i^9@W7h)xWP-9@A(MkzU4uV zfuQ+^ek82SZD4Wc;Yhk4wx^L$nF^&kE(Ha zM3K2wr1wcl%uHm-<>{D<_)Oyz=p0z48cd_5(`)5 zDUS%vtv_1_$E6&0Q&K7+B!&1`9g%?kn#L``K1AsX-J5xPnc>E0pd1|a>~R$6`533- zL@DKF-92XN4Vq5*;0yJN@|4vGr{|Abrnsw4RBhw5<6sWXNlfavj!8dZ+B6#^V0Yi> z?n|J+>7wOE0$>L(^XkLLX^$7fN*cNXhv^CD;s- zZwjyth4)$^1N*qNPEKAm9vr+N@2qRKq;;yNUR!Vgf~|E;u+x0G>kqiCPT&V9r3r%< z)S2^f%(q|+Xd2?d9@rk{53!)TYEYR3zniI}RCAanBG*LIRm6$Oci7Q>xAstXYS*Vm zNpSftCH9br`mp=vjRJ2}md+uiT8EMxI|uE4pD&SHP})Hus-Gr7=*#@EXbV}|MNKcB zNzTBf>x^51gj*|%ikH8FNt4&a7w$Wt64PmiM|N~T{+mjUQEv6@_PIGfbh;ZCpYNCn z-MHQUW))@aT+O7F&p&oeyK@;=7jn{>0(i%H%G<(7iF zEGc=Tfi^U(?0Tyr&`+Trd*yD?r89wKzX=FDukZQ?JeUcrp1YSLX95M+LMCJT)GwDF zK9%k~oxR=71^#bEcx&Dp+mItAC-m7YqyYsUw`~j#{Z#g@iQy9($V_RI&(+*#ogv|6xRG z-PtRAk;1PhGxbS#Bu-2GY959#cJRIRlQUY^z%1S%b}b4@yWu|a<@X_d2p~1G4Y~eK zsKwhmx+fVFB4Q*e^uNK3Ls_`D3KE}h3BK1Puf?&xbgub$7*dc}PI{5c#OF~1RvUZL zG|1bgr)LI4V|?+$*uK}cY0Zp#cU&7&O3V}uLdE)KKJC5YU>O{fmR&Y=`F_AIpDGy8 zcb_T->Bk9;_WG$-fme9_%GrB4P(1B?Y}QadF$({p+LkU#gkKyM@<=HO)T-h&&0f7RWh^BknH5P_ zBZN4Wysr63*E+wJ!4CFH$VIcCjt!%Xo@D=Z8Epc&&VqrMj)d)}R&hEnv#~2%sp2ll zOT~`K6^0SYPE27(Ey(Io=++h5pODwhRX1VJZj=Yi{sD04m!BhT*gQ0#Ti#ohJs9u`o99{sx2c|VbSPjQtqySZ`V-iG zT|%T!IzTh~A3<4Xx2_WWM^isv6vjLtf53jhju%Yy=QY(ds}%x z<$M_{G(C(Ea&yma@ke4>wYkjlB=@w2sLK~VWz01EhpE@;u{@%%`i7er*E; z)s{t9We0d2>Y2X^u_$qOY3T#fV-t07ruqY_75a|`sh5_Z7onmCm_k+Y&JHf{auN^+ zVt2Zw$fj$H8g_!W!HU1>HNsqnfhXIw#1*)(yMcROT-QilSUjD5w((B%b9 zbW1f1z`l6+k+YK#<2+9}8J7C|npONv;?)`eQKBwoDHnh?<4~SEjkpU!?*-9UTv!Nu zcv@0M&DhpLx7Yfl1X20*O93Lb8*=8Y;lDAWF9_H`k-uifw4wH)qJOpB^oR!KmezoB z#%*FgX{M`QvQVZur{|f9sVsm^3pl3 ze2XLzeGcQ-Afylk4Ba zH=d#+-;za?1bxGL?*6AMqG!5`Od|fVett_SO{4xp?uSwTpIT!gayIq*7sQpl>sBc< zB@L##Uf|3B+w?hJJ&^O;qd&}!imDEAwJaDtfUMJaD-_@amS(i|0c}ISH7Kef05VQvP^e3x!28*S~qLZEHFlcHuL-SMUFL zq?5jWfi0jYY`k?v099niIPySAEPB^B=s&DXRMGUK1=qrkoPO=YXi z^v>A0#o0-kD^4obdBcpbb3Cz@1OLc6^2tByK&gvEJo*QZ6Q?*We?Xbv1?8$JnzXPI zjjMsXwJUF6Z_hrNY_&nIc1}gaigiGKA=|J>L4)@5i;^|# zOu|vkwzQk`WES3ul29HD%c)3{YP7)zTVMmm!V-+c2laXxa;K7Yc1~5M5k4}>=V|`p zl60?Pah{r}y=q(O3)=h){dn;z+ZsfsJXxw%GP|~%Mh(avb?AB=HnUsL>bu!U;rn*Y zBXj4El2j=(ksveC21#0#$k>aFqo0qd;xwPS^i@Z#W+5@eye)8D$LX!K+CX z2~A(EMvkrHTtm>oKGVIoo%S?FKH4(N1csP=4}^H^rrln{XhiOAf;H||?pEH;6Lt8W z118zd1*YDrS*(P0yKo9M@A+(S_6J}W6yToIjKx*N7m^)ua=#fSB|IaO>aPHcUu!Yd`PyPEGJzH0IH zLf%SJqw(?vmKqbH;sw&FU?)9uTsc9Yg4w!gOW|Igi%U)y;IW|#*6x}YQaFMqnI#zD zRI8KK%!iQYQck59_hE+r>9l^uh*}KlvH|aLcQ$a?} zWM(%FHL*XizeWNf$H|s5ErbqG9eH2~GF`T)()>IHfgEjd(QdI6pj8+9gHIKD)uZba zp;pTzG%$ouqBO>G*iDRb5I`b?(;sSTjBbPvS3t+)JlwJ`E;t{DifYz*XMGvvg~@NK zOp&L);%9%7!8B|2vONGIBBfL-*%g9toNaCIc5L1L#kAgd5HT_BJ2nM5KDlL30MW4)Sr6b{YapkR zc{Z1jU4hd-k7+gCnkG8)XKsSAtAHSbHmM&x$Uk#+_dUf!(ngERs6$gG&61zAFYM&3 zmBDnw5~1~y)!z8${W_6}GTquZo7iJP2Y)!l7(e`cX@0D0#+V1hv=_HA7WD1V2A4O-#BES1^WUr>l%AyqZ+Yex|@ zN|+1G@xmv&o34p{?mE2xThSF*czMuS)PU%}R&?_)=9o=?OYaOy8_()Khn;}RtI9j6 z;;LA*7is&mHHpG|sLs2#6$qK3mmfi0i9CC>Fqwy<-gRiTlZUfnSL4*FhP;Q0K{@|| z)+qv(=;Ebu-VsKvU^uIQ5^V%1VP1%5qWq+`$Jgw{tt8mmZ7QPRWwE@9m332r9k)t0DQ80YA)X~42BOqc=fU) zfR*+YP+!CE7kz-WpZ1yimo>Mpi5`FSL!V}Y*tLH|_P_B$ zWpaut2EvkDz0Wv^41tLd;agtM;fPuuxwJ{^USoL)kb+#Nd6q-3s$#-m1EHVSN{ zX8-HK84r)_{B$Q}eP46eErsV)-SYHiz!R;(JGMmsmex|+7`+cfvtMh;e2=i__W$Am zor1YiR~aQ+KfB)e1N-~nslUR@TF6iA&%hivTo1|B!Dsc-8>eexCL**}S)TbF-pMkR zjt=_~P3ORjnV$AWysK+1&RX*>nxPba6BQOGPv_I*N{TZO{*EIrA5&WBu=@?noD zF){FgW585z2c?e0%xhgsx#JD6XMoZ1er4>M`}Rdih);BPwbWzoN{xtvI*Y)Y-j61> z8>wC%3cC(E`ORyGhJnu7cg-jyW=sKZpgxy$BeO2W`DtP%wap}+k2W~dnPffRPPA?Y z@5C#}q`k=`#(NOR)9W4@cgdJl0bD=2fP*aAG1*Fb$Y!LNqY;w8J<=}h9=D#S5s_8K z&t_l4KgFp z<+VlEebv(Gxy2Stx~8 zmXbOqN99%PA*5iMyLbHgh?F7ampZBOCZBNm0_%s-NR5{CvtHB-m-iMIP_XvnCES;H z^(i+DovmA6lh!^MA`Z6J5Q(&c2J}7Gpq)pU6HAcw=pPLaxl9Mm(3Uy*Hrr1UqtLtJZ0iaIOPmamUMYuRU3XHdqG_)XYK*%EKS9jW7vd+fYqjY{XwwV%-; zZ2`};j1zt{nDitOu1iW524&r(Rp)t=yH)4sThV1p7fP5#;DDqsb74_FXyYBk+uHwL zan^lmse+Tb4H)$7>!4MZYv=GQ^d|!Hnu~&2=34$#* zD*K2RF+gMM;3Zy3-G{PfJmv}-J(kQ|eLQC3I+)mo+eVfL7(GwZbs^acPOqoT3QMA_ zTLYgm6DNxYgB+4YhnMjj#amUAwk%x(q;pYzS^ zZgn%h*JxY$q!2ZnfSW#@j2o>OWm+f2t#%4c4<}bzmLp9(4o)hf-?ZN8km+%f zIe3?3I92>`Fum?DP^%g5kFBy?b)a8!cE!WHGH&Ojmdx)F40!#cW!S9j zDX7YG*<0nQ&XqwHxR8dhP!me8Xh=hRf`auNjrc2}+Dq24GHfty(T8yzfNYh8)S~39ST;V?yAMWxq?h# z5fxtTPdf6R?QMmwDqO0=azvp$__>fEJqAGa3&Au%g^Y0C2GX`vn(s!<9MBEI|WH# z$Y2*Cr0<>NxQIHK?i=lWZ|2&UIj^s9E!}yFSEjqDwV2Ewfk__ln6P`!*}FdY=JvYI znIh4VgQ)nxT!Yp!aF42-^#ey0!F-;8!4uE7YnG-1l?!-JV`xi&lK<+}Yhokw2X{-& zp2L_yyt;(?Dt_ms->0=8cWO2*L4>`h5)lO5>A+dD*#f>{DCAi8DS0sJgC354bVj0dspnUpn8T4PlJ(Bx(FZ*{^AUcRCM!kk`#TnHTwM7qPr0cT+$?@>CrYL8qFX z4^WgYpuWqEU$7i$)>uUkc&aYNT*(_M6FmbhuEp9I;*r}Z9b7)zc1=3VFoWWI$RUw< zv=f~9W90+zh$?xa9uCWsy;D&*uQhj%K0FTn(^f7&l;Ahx4@*nER%*6?#@UDh=P64q zBbUMqUe~J&-DDUW&qFTNfpFrAERHwSr}o0DT4_fPy7c4RS5+%9xuzGFyDAvL5<|qT zEGbMZk^(ax)1XwLRVM#4|}9{8}Z*Q3oVy7x4nS`yZ|doQUY zw*7m4OWC0W?tanspiuK}g4PW)%?cf|UMSIsuyjS*k;PF?hd+p(OG_P~S96VQC<%A7 z@Fd*3#u|~49db)bxw%mK&dHcsdR$YZns+Q2-maOFKbRb2s1GGlzVFO@29<8A~lLd3mIPOOtg@ zmE94k?z|8URN;iYK;N()F+;OKkA7|J7`Regxv3yCmG)q6Vt?x~;MF(f#)>XF>Hko{q5TK*rsZw_Xg0^ zn)b5a?Q7E-M~8y-sB_~?l6@3-do&deNY>?@^;i>on^kPMkt%-zmj+jcoEyJ2ek^?I zS)2t29{G^qx93PYievI|J<`|$!As2lw%Nv>{C{>Z`cGm@%ZaP}#L5Wr9QvYLR+4nO z|0%Lv*hSx4ddaS1T4-B0F$>2xuPM=%DDy3(?7M{K);7FLEa&80>Qx`nl$t5wjF+Fu zI3zLWm;C2~>XstRut5#Gxlnk`hoi@g}j?|Dl%7ME--i z35C(JQsyAaMki4WG#;$IGE(be|9-TYHKS-nCxx!j_sH7#Ukr4`t{ z0+g4L`Ip4OMQ5U80&{sZ z+Gn~asZw&hj-QfQ=VLkF!Mvf7S#5K_XlrSro6=OIv6d{WM%w-WTjbNP`LylJ1b;g9 zz1fRWe1U9}E?q01C*w0pPEk29`n**A1Cy4}GC4612!qee!MdWWYu!YDE2vMwzKw0a z?EX-{QS4(p+%pGV6sTMB$v9~P&F9>9pQ^QbR^{4szoHSdZ;^a!g?MW_We|_@S|7yt zkOCeo%J=fSj0pgij0n1RYti8>k zm+x8(Ok{O>S`*rK-}#9f!bE@6{@B!61lIycl4pHOpTmiOJt|CXeAtsKXGzk{v; zI^M?K$gmx%5-R9j8Ng`Es@JMh+U=CxMZMmoG@#R8oATh>1+mj zC@1(4zwT?JF!6&r-zTsa2YZ=Zil4mOyYuUz)UWT-nKI9>aQh@mhoh{wH%XW!?z~h%w|B;iZQd;Zm7ofoRs&u?^WOB_ z<3*M2@-^OaVe*v%Zt13o02@$c1!llrrW*x$8+fOa+@wCvdU-L_p`m$m`}xb!G5o<$ z*+zhu*n(>m^U0E>ABh6)mL|7dzm$vXpsv@&HE$r_bv7d~uQou&zwa&X2O+K#UdSv# zdMmeb)=NE?PRH7GTXIroGBydeCt4fNyxVd6eUBY$@cAijV%Ka9Bu{z0Rb3k(YQUl` zU$-k$TK1Mw3CIHAyld95__p_if<%{^;UTi#{1vvdqE9GOwm}(}mABRWSqpMD(si_1 zr|Ibxh{zwrF3&dqCp!o;Mb-BqAL7jiPQI{#kT$omKlt~cKqzzh<8=>Kl|{epX7$B@ zpq^bn_f{_BuMcFiFpo&vX@C0P4|6uHGBUZekuV+xj$Rbt;nj@7Kl0@GrKr<_c|0H? zGl@#2Q#^zf^e%laAacTp@iP?HoOcr*u0>Bgw07gfgg<;g_m#8;dDJGwqF0xe%RtGHixK(2u5Q`@3k$K-EIrwm3)pe|k z!tFmCp8l@?{M$C(k45DL6}SH6dB{T06q{491n~b)d-o=kB_?s|0(6rrI&s zEw9Qwh(k%!wQbI*10Fy@@<=YCnIC0wNk)5Z-*o|UJC(yXw)gWBMLUZ>U>i@U0~tg@ zl8=^ZoOPdrLN*In!}uf&>`_7Z+)VPsA=k#IC>2vX1i=o92BOLQXj(Pt#gf_>gy?6!UCxJB8ZZz6rUpmB|AU6-nhd)Gd7I5ceMO!LetPiO%yiH=in!dgmGS zd<4KH=0{RrO-Lbl3JI&mhe1t0oDho zJD!n(282H;o}wdEGCj-ut@=%?Ed4v!qyjm5jT4$D#$PrQYU;CoISAbU?T{er3mZCg zT-$C+W38$&3ywzneF_*4@G&-i5z7S|?-u5~{@@`eC;3BCV-wT91>daXO{lW1yf)WH z9_38HoRMPVfT@r>LhKVrh$wIrG}VH7qK&RY;=L=J*8PJ81KLxErV&TkyG>4N-JCpcq+HDycjc0LFPy5aO9 z;ZQxhml(VT1=*dHY#da)RJ(Y;Xn`7R(417Q7$-9gYMN3%m>TxzB2U0qh$rZfwJaB| zvr*3@TTd}+8P8K++<;DfFN8y*b~yrzVIMxCtO47CZ|PX8YRBfA)FEgSLOHUr@9bh| zJ-4oF&r9}(51$lIg!2MgmktC8*1oF}-N1Ai@g{;I61pJmF^`qbXYOdZE4B?sU#CqTSnu%F(ertnn8A zO3tplj+q>VcOVJ#ah7P2ut11kfZW)oxF**kYW20GoSUTRmP=)|-%2>~Ryp^JjON<0 zgE-lxA~m@rzS&o148L60nGIiT9d(j?d_Jl$YLGMa*Vt_2Z)dq@aV%+<8%~N=t{6Hg z#kI?@D^ruYM-L*`p%&=gC%mU}Uu%Ovl5?m(DU~$~{^PM2&N?scsbf51-L{Gn(1MIE z_i{u1a$oz6rAgs?URU4wd~l-3v!}Jls+-DK0~}K zIG@QNd86V$;^M*UKMx!6ubnsIo=7IVjC;?znxJQ^H#-AfUWA;4#gEZ(rM7YjH!G$t z`Dtr})~VVYDv%t$k@NA>$-9|zg!_?No_GV)-52thVA-7AhRBNe{Rz7u`G#rb&A@l# zCn2xbI|l|ag^iIm>=op^r+4TQhRAXJ@FifXa>dS4ky5Qm_oryD#RZ7s@H7?s+dr!f zkJa@3jaZ`#7AaH-@3i_7vHl`Pv&rH4YG^=y|M%F(!qg?+75XHPKNPlkM-p6aAb)<& z;=b_d+NFaV%jAoduut-t{^fK311+`S`0!m-1sSWkf zfJtg=<096C)2O2}z%2adv|WE0b@*vv`EU`{)` zShn?;(BM27IW*d;W9zGPH@UHM31K%4GlUZHSLj!7ATaVfN&APkHav<4&GP$;)wKqS z6pHv@G*Tbo2U-e)C^NAkpZ~3y5>T2Px95@@%C#I&T{?G`=t{lkUP!>u%YKwzH}o63 zngJUp-3p*0rrC_I$)~S*u+W+JxDt6s{+0g>E~jZ7ewhXJNW~s!%7%nNZkb*hxwV@y zynbui5C`lDfD{D5W=>k_CW@M-WrHk`fMf6{^XX%!#z0FaEa^Xv@{W{ixE?lJ=qG5m zOoR6Pc{gEXU&yWgzb^JLnE8|^16s0S?0L)F`$9e5DN><@?qGc2A~n;53|4cXD2Faq z@Jk?nDEX2l#&EL#@K|8lq}aRTBN+HvE5(evT|%c+b0Vcz{; z%G>lGJzD<_HLUM&fN|1*){kFOXMp&i;(yJIkjsB0Tl#l-M04F5}EUjR=3a>w+Z!J<@X0I0~X_4s~tDy~h5Lh9kn< z8_2KWd1e%d00MR66kDcF^^6JiY^_s_^S*xf^Nxmt`s#UT{nq)n@;DZ@>v+Hy6VS5$eu~sH~8c}L^6wiCsgx?nkJiPs8 z(1NPLf8c_CJ0kBE;*V%eW+8C#;((kQKspe$OEi;4@Bz)gIGzq3>1-Q1q{njHPMxW} z&2zP)MUcE&0WAjm`6Za5)ySFt_d)9~sX|m;%SJN*(T{S80gq`m!@+u<=JwMbAUJs0 zirJ&bf)t18?eTY*o}zd8VcQIMu2M@4zUPGFM2AaXn>^JB1klKI;b~5%;L+9KYeQk} zN0}{h3;V)KRYLWXfWR1%ugrdj<7|_DIC^k{RFf(5F zx`w3y&4X3;W9cBhQfuHGwBeR>qnY8BWo2KzJ)4$-96toYVTdfvpTOrXW3mP%sZMDJ z0Mx>YV@J1FVztJ)0F|asWRv^V|2^T#Nwj$nxCHvm3Mwg;r(^s4Qa#3jNlTx0+f9tL z^3-J3GnOsK3~i)i&@Btz9cp}F%bxS=isNd^=Wsy!i5P=J=fX38ej-Xj&`JpapMOcf>kuzWIi9o1v>?r`7Jp@AG$TN5=KP zBGH%M{(rr&Re0{iFbLOs#2c)82t_0GRrDpsH1%U#fK)wzV_6G0Kes&Pj;Pv`C{?wF zm5fj{xDe!G91-p~N^~t7@utqdfb0t)urL?m6=q)6uchuZr4PL2=kV`nFS!20KC-Nv+A62p}M!zaOHQn zL#Tl$%Ze@{0%9pW=7^98&CU(3W36XiGHt ze9I`!b4HP4rN<^ue~t7k_Q0ti=}=JLx&hF|)&z!ZxSmeVN?jqDX=@We_lI`!H?k}W z2lFOs=LR_;Clt`5GNrC8+Pf#0c?`Pre_P$||i zGOOk1+mSV)Kx>VZ>RRwd);Twe8=sJsuY=DjIx2z~-|INE+WLOGP zVUeHJ4EnkN)_`yPSSY=8MZsyV32i&BsR7eqiS-zid=Ga?@GT$TWJG|mJ6V!YdzGbl zS;}6x0SmLg6@th31?b8d$@$0^4LnsH0S<_ktuEFuGVKndrwqDt z-%*T2l@CdJ{o*dNb1*gn`R40V7ob&(hPgrYu;=t_>>R>t{KK?d z(dAhcj^nA6OQ3Yp776{4q+o7#{mLXJ}z5636_ z_5%TgLl1+zpLvck+wO3H%mmGSPnV$$0KnDb)`qYJXsFx1xrq%~<)V2JW2r`nZSH9O z(9q1%MuE4Z@=qc8@;ul3B{+^(RLKnkp)?tQ;d9g9sTeBrNn zm$X#wA+h1A7P0Nht6vT}a#?zEa^F@RT}gU0-9D#lgDfZ%DZaVp|1dFgFHVk7;`SY? z6eqMfL|*^_u-!Y8AoVhzof9$56PkIVx&UL@kAGc`=M3SlVNE@GwuIAIOgYaLKFv0c zDOVIQYW32NQp0vwDwy!OPL(L%Y+D`mx68ZO_*;`4>1b|g8|!asURAOgqc>@I6osR- zG@Jvv?!PNKR8(0qq#^_i?~f};i3?X%Af9ONOf;KEkEa9G!*j>2#%t4Ibkj3aL6Txo z(ZnZzGPbeIKwK@8ZR8aXP^k1^-i{l6etS=GM-W3#b&V<9In9f4 zgoq9TnMkx+aOKHBri2=9*THu<5mnb>xktZo}!Ng z&nm^mJJ7m^&b%GpNOV-1wpg8NZbX9dg&`p!R@Jho<#O7u!NYO?xv63QvzMW9i~kcU zcC@n-d3(-b%|cfHVQ6G~p<@LYM{6XyYY$#Mc%3-9K2~V%0)XH$T&_K`We?lv0v4Dg z+8@$oPQ}XrEDBWs32Kq&D@eQM26*+HFHw2}Eg-UA2Ji{jS7S2x9~8cq_NRexuO(+^ zy~>Yr($B-qe6Q;O5iN?foon>gwm@aqaVD#AscSXk#W=nW8cf&gk^SgU*WkPwOngGB zw(zkd6!zK6AKoi$=IZAwzrhPs7R*vK9Px;Ylb71YO4!A@GA3lWwqAAkI^?{K0J|61 znw_ise9V2VyZU(oCPHvMJyqb;xRSm}Q5SO*fAwGF)HcT>UHO%Mmt-c}sP)|~8bj^p zyfe!F9rLR?`zV}k;muIbAO{V4vP3FmRpRD;ikNdnN%Z}8hF&IQklWAt4cTv1#>JO~ zY8}kMuS@qg->q=k^J2deuw?I^Sb*>0;X-Sd44oq8cpZ64becw2ZJ%kbt#FE9!DdRf zc57Fh94j9>QN#}J3QU?`#Mq~& z)!_t^(%fpT?Tl*bY95@L2UI+cYI&uO^Y5m*ecvRe&?K`Nrxr;d5I<8q%bZYZWdz zeHDpsz^0;Iqu>pZ|2y<~(~=FZWh0)%yB#*~VmprE)DU?}ByKd*YCp z3~5JjW0}?!qgU6B;ZeooyNUK5qa39ktXL1_68?^~2~JR%_J+SOV_)%EN2`QTvQed( zp?)DL)b`V){feJXtWRGXiC(=1tp zloE}_d~dMEWR!!?*7pB{crt#1TojvN?9Vak$X{!hBn|Pe#kMvP#ROk#$vB=9Cjl8W z$TP2JDcq*RHzZM*vHKmg`;u)0-V5jd3ReB;MAsPxhYwJ=O_6EswOqJ%0@q8F#qfhC zG$hEUmV`08s7jObfaH!@$Qs13GU(%hRlU@Zt7%xGty2_U^B0VEp`c_jY4N^}c6^l{t?!gDhF2IIb`9-oT$5sa0`0F1&FkNgu+mqCF zrOXqc(qU`59P6psKO5L=SV0x*@{gt7?>Zen13a#$RECtQ0zlXx0)&mpE}vfP1&yij#ra` z3#i(*^#=P2qfi5hM2B5~#`86$=?42uWxba7ShZa_2$19%HgcplYR2`6G9_2+DIC*l z*~||a{Bk+b_<^$q4TN{g%e5EWr^9Ln0P!Xwjf{_`YJdQpkG^QvoV<9b4ih~Wq3d6s zI7m+J=aNGVPecMJG54fuTF;qtL#~^`HB6xsH%{HeB7kE>_HcIkd!-fqs!{m<4 z*$<6kUu}2*M2r(oy>=vBT&F?|xWj=&F!r1;;ZrZeI{*j=zuhJ<_(~k|ZZsg{x^l7R z$RS`u8RBNIIHlb1Dq53EdrH!C^Ye?^eFrt)JVaWi*P0W!^@kOF^k-*t*kho$bYI#k zv4r9rcfr>!>s^6Uo3 z^8Hsf#y)|Ks4(1tqzjLG2Ko6ZQgUrAlM_;zS9EkVk-6bp7KU(kXpNo!lwh-Hf#UZ4 z@;fiYEZJ;;db8iJj-ZU4?GFg;mJE80jGb10^wIVJepwB#4Y4zK#34JnHJj@Wd~dP* zaq;obQM<5_?+jm}o=7D`$F^`Xj8!BE4THwk=;Zk8Z%gJpZveb=m0eB;Ak9$&o7fX+ zmSf(YOO<<%N_8tOmwK188j3rN+JKvb}QQKio8xz<-gGay*A$87xRU4Q`}Ddy7{xD1Ht!h)BIYGK_ugHxuicp z%I^O6j@!2^=G3PC%uC3FunE{d++V#7*##IWYu-Vq?44s^?ClGMAD4JaPx9)Fw2)4N z9>XXhSdf9` z;y!U}(7pzDSso^{mN#hI+jdoHqbcAA&5VgG{`Z!NukQgg3*bH?E&DzRyr~Xi2tV45 z>iqq>Q9bN1GAxu|Ry|ry``ee2hi}(jg={{)T{*PYWSMYuX%WM<>Y7swq|2o=Z2t1; zmKmD)_REU$zj1y4$Gtb5!vME3k(H+uXX(}hlu0+xcK&+Iq()I`XIKN=!M(z(ah>*c z@HrsXwf+g-3oxfxnN-M0SX%3VKQvh(4WQfdZsDdI&pE;5lJbX;9BkM=umOKhyGWMd z!7GDs=vIaxGeDgvm7X}-VJj`M#Qzr%+v!S?OZm!RP2%`zDB zC0AT_n!CFKMR->G2LOR^rAdV+(Pka*?~B};kajkA>phV4xs382?!J^MVDatJeSY1f z>?F6s%)2%Hy=UXjuK{@?e_e8x4|JQh9ZhWjP})~QIccw8cHT1l+E7G$N6;^oX38$E z?bQ-a^3f5Pk_I`f29TAREn-f;k?v|a#xGPRG#wu2US5-um%WER>jhB87w>{sjxC38 zE*}<(p;&l$lQ(^6l+p4EAuh9G35()0A!eSF8xgY3!Xi?~?#Xctw2z)0%G=me9-ibD zH^ngSyyFB&HpuQ?i-m5e<3}98kQ6G|yb9jfylz`si_^fO^o@%QiVI;s6o4P%B8^(^_pyXZDYvnkt=*?PvXz-j;G3 zAsKIsl^~eGALR_rJzqzf%Wd;07N74ObwV=?74C3;&uGl0->Wwnk~&RxD2&AI-lti| zf{EHiE`5&w2E>`d(>a7g{#e2bZJpN}W{AHv65TB(zio!dw1@{OWqiPFIVoz?^b1Qq zku*i1%tFChoXsI&&P+7vARjFe9_; zZnpME%+^=_iYH~`9{A^lx8jw3t3UH)Dagufp)Y$uv)LC`D2j}IM)`8@_{jGS3&Cjq zyM;{OwhRnMM-09Gz{lSFVe-MpX$3gqflaopVDkd>u*pc+9<6<4-~D@2<8k3Cz-3N5 zzzz-?)2230slT5U|K3fRN5h%ZqM0GrkLJbokpI2y7)-RM`Sl;vrv69$#Q$46x&NJ; z|G&Y~m?k6fds}QSM{PWS9zSbeV64-TevjUsR5cn_epl#a=R{7NAB-|{qd@?X0ov|+bGW_zOtrI zRwxCq(_PfuQc694y~6%&v`*9{(??qA0}VI7=1@{k#cbW;yjqcdP3-Q?5bHU+mq({j z&%h{XyXuGNLrqOxW0ib%=IE_kf-Ofj+T}5^kQX=Zv=`Nxcb bXNby+0*LU}ryje}DtP=*Teb3mRmlGV!)%AQ literal 0 HcmV?d00001