diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.scss
index bc4237dd4b..3212d75a40 100644
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.scss
+++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.scss
@@ -1,3 +1,5 @@
+@import "../../../Bit.BlazorUI/Styles/functions.scss";
+
/* ============================================================
BitDataGrid - styles
Theming is driven entirely by the Bit BlazorUI theme system
@@ -9,12 +11,12 @@
.bit-dtg {
--bit-dtg-cell-pad: 8px 10px;
- font-family: var(--bit-tpg-font-family);
+ font-family: $tg-font-family;
font-size: 14px;
line-height: 1.4;
- color: var(--bit-clr-fg-pri);
- background: var(--bit-clr-bg-pri);
- border-radius: var(--bit-shp-brd-radius);
+ color: $clr-fg-pri;
+ background: $clr-bg-pri;
+ border-radius: $shp-border-radius;
display: flex;
flex-direction: column;
position: relative;
@@ -28,7 +30,7 @@
}
.bit-dtg.bit-dtg-bordered {
- border: 1px solid var(--bit-clr-brd-ter);
+ border: 1px solid $clr-brd-ter;
}
/* ---------------------------------------------------------- Toolbar */
@@ -38,7 +40,7 @@
align-items: center;
gap: 8px;
padding: 8px;
- border-bottom: 1px solid var(--bit-clr-brd-ter);
+ border-bottom: 1px solid $clr-brd-ter;
flex-wrap: wrap;
}
@@ -55,8 +57,8 @@
flex-wrap: wrap;
gap: 12px;
padding: 10px;
- border-bottom: 1px solid var(--bit-clr-brd-ter);
- background: var(--bit-clr-bg-sec);
+ border-bottom: 1px solid $clr-brd-ter;
+ background: $clr-bg-sec;
}
.bit-dtg-chooser-item {
@@ -87,7 +89,7 @@
display: grid;
grid-template-columns: var(--bit-dtg-template);
align-items: stretch;
- border-bottom: 1px solid var(--bit-clr-brd-ter);
+ border-bottom: 1px solid $clr-brd-ter;
}
.bit-dtg-header {
@@ -98,11 +100,11 @@
.bit-dtg-header-row,
.bit-dtg-filter-row {
- background: var(--bit-clr-bg-sec);
+ background: $clr-bg-sec;
}
.bit-dtg-filter-row {
- border-bottom: 1px solid var(--bit-clr-brd-ter);
+ border-bottom: 1px solid $clr-brd-ter;
}
.bit-dtg-hcell {
@@ -112,7 +114,7 @@
padding: var(--bit-dtg-cell-pad);
font-weight: 600;
position: relative;
- background: var(--bit-clr-bg-sec);
+ background: $clr-bg-sec;
border-inline-end: 1px solid transparent;
user-select: none;
overflow: hidden;
@@ -125,12 +127,12 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- background: var(--bit-clr-bg-pri);
+ background: $clr-bg-pri;
}
.bit-dtg-bordered .bit-dtg-cell,
.bit-dtg-bordered .bit-dtg-hcell {
- border-inline-end: 1px solid var(--bit-clr-brd-ter);
+ border-inline-end: 1px solid $clr-brd-ter;
}
.bit-dtg-center {
@@ -145,26 +147,26 @@
/* Striping & hover */
.bit-dtg-striped .bit-dtg-body .bit-dtg-row:nth-child(even) .bit-dtg-cell {
- background: var(--bit-clr-bg-sec);
+ background: $clr-bg-sec;
}
.bit-dtg-hoverable .bit-dtg-body .bit-dtg-row:hover .bit-dtg-cell {
- background: var(--bit-clr-bg-pri-hover);
+ background: $clr-bg-pri-hover;
}
/* Selection & editing must win over both striping and hover (in every row position) */
.bit-dtg-row.bit-dtg-selected .bit-dtg-cell,
.bit-dtg-striped .bit-dtg-body .bit-dtg-row.bit-dtg-selected:nth-child(even) .bit-dtg-cell,
.bit-dtg-hoverable .bit-dtg-body .bit-dtg-row.bit-dtg-selected:hover .bit-dtg-cell {
- color: var(--bit-clr-pri-text);
- background: var(--bit-clr-pri);
+ color: $clr-pri-text;
+ background: $clr-pri;
}
.bit-dtg-row.bit-dtg-editing .bit-dtg-cell,
.bit-dtg-striped .bit-dtg-body .bit-dtg-row.bit-dtg-editing:nth-child(even) .bit-dtg-cell,
.bit-dtg-hoverable .bit-dtg-body .bit-dtg-row.bit-dtg-editing:hover .bit-dtg-cell {
- color: var(--bit-clr-fg-pri);
- background: var(--bit-clr-wrn-light);
+ color: $clr-fg-pri;
+ background: $clr-wrn-light;
}
/* Sticky / frozen columns */
@@ -219,13 +221,13 @@ button.bit-dtg-htext {
.bit-dtg-sort-icon {
font-size: 10px;
- color: var(--bit-clr-pri);
+ color: $clr-pri;
}
.bit-dtg-sort-priority {
font-size: 9px;
- background: var(--bit-clr-pri);
- color: var(--bit-clr-pri-text);
+ background: $clr-pri;
+ color: $clr-pri-text;
border-radius: 8px;
padding: 0 4px;
line-height: 14px;
@@ -248,7 +250,7 @@ button.bit-dtg-htext {
}
.bit-dtg-resizer:hover {
- background: var(--bit-clr-pri);
+ background: $clr-pri;
opacity: .5;
}
@@ -262,7 +264,7 @@ button.bit-dtg-htext {
/* ---------------------------------------------------------- Group rows */
.bit-dtg-group-row {
- border-bottom: 1px solid var(--bit-clr-brd-ter);
+ border-bottom: 1px solid $clr-brd-ter;
}
.bit-dtg-group-cell {
@@ -271,34 +273,34 @@ button.bit-dtg-htext {
align-items: center;
gap: 10px;
padding: var(--bit-dtg-cell-pad);
- background: var(--bit-clr-bg-ter);
+ background: $clr-bg-ter;
}
.bit-dtg-group-count {
- color: var(--bit-clr-fg-sec);
+ color: $clr-fg-sec;
}
.bit-dtg-group-agg {
- color: var(--bit-clr-fg-sec);
+ color: $clr-fg-sec;
font-size: 12px;
}
/* Nested groups: subtle indentation shading applied at every level so deeper groups
keep the hierarchy cue (the level value drives indentation elsewhere). */
.bit-dtg-group-row .bit-dtg-group-cell {
- border-inline-start: 3px solid var(--bit-clr-pri);
+ border-inline-start: 3px solid $clr-pri;
}
/* ----------------------------------------------- Grouped column headers */
.bit-dtg-group-header-row {
- background: var(--bit-clr-bg-sec);
- border-bottom: 1px solid var(--bit-clr-brd-ter);
+ background: $clr-bg-sec;
+ border-bottom: 1px solid $clr-brd-ter;
}
.bit-dtg-group-header {
font-weight: 700;
- background: var(--bit-clr-bg-sec);
- border-inline-end: 1px solid var(--bit-clr-brd-ter);
+ background: $clr-bg-sec;
+ border-inline-end: 1px solid $clr-brd-ter;
display: flex;
align-items: center;
justify-content: center;
@@ -322,7 +324,7 @@ button.bit-dtg-htext {
border: none;
padding: 0;
cursor: grab;
- color: var(--bit-clr-fg-sec);
+ color: $clr-fg-sec;
user-select: none;
}
@@ -336,12 +338,12 @@ button.bit-dtg-htext {
/* ---------------------------------------------------------- Detail rows */
.bit-dtg-detail-row {
- border-bottom: 1px solid var(--bit-clr-brd-ter);
+ border-bottom: 1px solid $clr-brd-ter;
}
.bit-dtg-detail-content {
padding: 12px 16px;
- background: var(--bit-clr-bg-sec);
+ background: $clr-bg-sec;
}
/* ---------------------------------------------------------- Tree view */
@@ -366,13 +368,13 @@ button.bit-dtg-htext {
/* The focus ring is driven solely by real DOM :focus so exactly one cell can
ever be outlined (state and DOM focus are kept in sync via @key + FocusAsync). */
.bit-dtg-cell[tabindex]:focus {
- outline: 2px solid var(--bit-clr-pri);
+ outline: 2px solid $clr-pri;
outline-offset: -2px;
z-index: 1;
}
.bit-dtg-cell[tabindex]:focus-visible {
- outline: 2px solid var(--bit-clr-pri);
+ outline: 2px solid $clr-pri;
outline-offset: -2px;
z-index: 1;
}
@@ -383,7 +385,7 @@ button.bit-dtg-htext {
cell stays clearly visible even while selected. */
.bit-dtg-row.bit-dtg-selected .bit-dtg-cell[tabindex]:focus,
.bit-dtg-row.bit-dtg-selected .bit-dtg-cell[tabindex]:focus-visible {
- outline-color: var(--bit-clr-pri-text);
+ outline-color: $clr-pri-text;
}
/* ---------------------------------------------------------- Footer */
@@ -394,9 +396,9 @@ button.bit-dtg-htext {
}
.bit-dtg-footer-row .bit-dtg-cell {
- background: var(--bit-clr-bg-sec);
+ background: $clr-bg-sec;
font-weight: 600;
- border-top: 2px solid var(--bit-clr-brd-ter);
+ border-top: 2px solid $clr-brd-ter;
}
/* ---------------------------------------------------------- Empty / loading */
@@ -404,14 +406,14 @@ button.bit-dtg-htext {
.bit-dtg-loading {
padding: 32px;
text-align: center;
- color: var(--bit-clr-fg-sec);
+ color: $clr-fg-sec;
}
.bit-dtg-spinner {
display: inline-block;
width: 14px;
height: 14px;
- border: 2px solid var(--bit-clr-fg-sec);
+ border: 2px solid $clr-fg-sec;
border-top-color: transparent;
border-radius: 50%;
vertical-align: middle;
@@ -441,9 +443,9 @@ button.bit-dtg-htext {
height: 10px;
border-radius: 6px;
background: linear-gradient(90deg,
- var(--bit-clr-brd-ter) 25%,
- var(--bit-clr-bg-sec) 37%,
- var(--bit-clr-brd-ter) 63%);
+ $clr-brd-ter 25%,
+ $clr-bg-sec 37%,
+ $clr-brd-ter 63%);
background-size: 400% 100%;
}
@@ -466,7 +468,7 @@ button.bit-dtg-htext {
.bit-dtg-infinite-end {
padding: 14px;
text-align: center;
- color: var(--bit-clr-fg-sec);
+ color: $clr-fg-sec;
font-size: 13px;
}
@@ -475,10 +477,10 @@ button.bit-dtg-htext {
.bit-dtg-filter-input {
width: 100%;
padding: 4px 6px;
- border: 1px solid var(--bit-clr-brd-ter);
+ border: 1px solid $clr-brd-ter;
border-radius: 4px;
- background: var(--bit-clr-bg-pri);
- color: var(--bit-clr-fg-pri);
+ background: $clr-bg-pri;
+ color: $clr-fg-pri;
font: inherit;
}
@@ -494,7 +496,7 @@ button.bit-dtg-htext {
background: none;
appearance: auto;
-webkit-appearance: checkbox;
- accent-color: var(--bit-clr-pri);
+ accent-color: $clr-pri;
cursor: pointer;
}
@@ -512,7 +514,7 @@ button.bit-dtg-htext {
/* Highlights the row/header cell a touch drag would drop onto. */
.bit-dtg-drop-target {
- outline: 2px dashed var(--bit-clr-pri);
+ outline: 2px dashed $clr-pri;
outline-offset: -2px;
}
@@ -531,8 +533,8 @@ button.bit-dtg-htext {
/* Inline-edit validation: flag the invalid editor and show its message underneath. */
.bit-dtg-editor[aria-invalid="true"] {
- border-color: var(--bit-clr-err);
- outline-color: var(--bit-clr-err);
+ border-color: $clr-err;
+ outline-color: $clr-err;
}
.bit-dtg-editor-error {
@@ -540,7 +542,7 @@ button.bit-dtg-htext {
margin-top: 2px;
font-size: 11px;
line-height: 1.2;
- color: var(--bit-clr-err);
+ color: $clr-err;
}
/* Lays out the operator dropdown and the filter input side by side. */
@@ -556,10 +558,10 @@ button.bit-dtg-htext {
max-width: 40%;
margin-inline-end: 4px;
padding: 4px 2px;
- border: 1px solid var(--bit-clr-brd-ter);
+ border: 1px solid $clr-brd-ter;
border-radius: 4px;
- background: var(--bit-clr-bg-pri);
- color: var(--bit-clr-fg-pri);
+ background: $clr-bg-pri;
+ color: $clr-fg-pri;
font-size: 11px;
font-weight: 400;
}
@@ -570,10 +572,10 @@ button.bit-dtg-htext {
align-items: center;
gap: 4px;
padding: 5px 10px;
- border: 1px solid var(--bit-clr-brd-ter);
+ border: 1px solid $clr-brd-ter;
border-radius: 6px;
- background: var(--bit-clr-bg-pri);
- color: var(--bit-clr-fg-pri);
+ background: $clr-bg-pri;
+ color: $clr-fg-pri;
font: inherit;
cursor: pointer;
text-decoration: none;
@@ -581,7 +583,7 @@ button.bit-dtg-htext {
}
.bit-dtg-btn:hover:not(:disabled) {
- background: var(--bit-clr-bg-pri-hover);
+ background: $clr-bg-pri-hover;
}
.bit-dtg-btn:disabled {
@@ -590,21 +592,21 @@ button.bit-dtg-htext {
}
.bit-dtg-btn-primary {
- background: var(--bit-clr-pri);
- border-color: var(--bit-clr-pri);
- color: var(--bit-clr-pri-text);
+ background: $clr-pri;
+ border-color: $clr-pri;
+ color: $clr-pri-text;
}
/* Win over the generic .bit-dtg-btn:hover rule so primary buttons keep their primary treatment on hover */
.bit-dtg-btn-primary:hover:not(:disabled) {
- background: var(--bit-clr-pri-hover);
- border-color: var(--bit-clr-pri-hover);
- color: var(--bit-clr-pri-text);
+ background: $clr-pri-hover;
+ border-color: $clr-pri-hover;
+ color: $clr-pri-text;
}
.bit-dtg-btn-danger {
- color: var(--bit-clr-err);
- border-color: var(--bit-clr-err);
+ color: $clr-err;
+ border-color: $clr-err;
}
.bit-dtg-cell-command {
@@ -614,7 +616,7 @@ button.bit-dtg-htext {
.bit-dtg-icon-btn {
background: none;
border: none;
- color: var(--bit-clr-fg-pri);
+ color: $clr-fg-pri;
cursor: pointer;
font-size: 13px;
padding: 2px 4px;
@@ -622,11 +624,11 @@ button.bit-dtg-htext {
}
.bit-dtg-icon-btn:hover {
- background: var(--bit-clr-bg-pri-hover);
+ background: $clr-bg-pri-hover;
}
.bit-dtg-group-btn.bit-dtg-active {
- color: var(--bit-clr-pri);
+ color: $clr-pri;
}
/* ---------------------------------------------------------- Pager */
@@ -636,7 +638,7 @@ button.bit-dtg-htext {
align-items: center;
gap: 8px;
padding: 8px;
- border-top: 1px solid var(--bit-clr-brd-ter);
+ border-top: 1px solid $clr-brd-ter;
flex-wrap: wrap;
}
@@ -644,7 +646,7 @@ button.bit-dtg-htext {
display: flex;
align-items: center;
gap: 10px;
- color: var(--bit-clr-fg-sec);
+ color: $clr-fg-sec;
}
.bit-dtg-pager-buttons {
@@ -659,9 +661,9 @@ button.bit-dtg-htext {
.bit-dtg-page-size {
padding: 4px 6px;
- border: 1px solid var(--bit-clr-brd-ter);
+ border: 1px solid $clr-brd-ter;
border-radius: 6px;
- background: var(--bit-clr-bg-pri);
- color: var(--bit-clr-fg-pri);
+ background: $clr-bg-pri;
+ color: $clr-fg-pri;
font: inherit;
}
\ No newline at end of file
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.razor
deleted file mode 100644
index 3252ee7b13..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.razor
+++ /dev/null
@@ -1,152 +0,0 @@
-@namespace Bit.BlazorUI
-@typeparam TGridItem
-
-
- @{
- StartCollectingColumns();
- }
- @(Columns ?? ChildContent)
-
- @{
- FinishCollectingColumns();
- }
-
-
-
-
-
- @_renderColumnHeaders
-
-
-
- @if (Virtualize)
- {
- if (IsLoading && LoadingTemplate is not null)
- {
-
-
- @LoadingTemplate
-
-
- }
- else
- {
-
- }
- }
- else
- {
- if (IsLoading && LoadingTemplate is not null)
- {
-
-
- @LoadingTemplate
-
-
- }
- else
- {
- @_renderNonVirtualizedRows
- }
- }
-
-
-
-
-
-@code {
- private void RenderNonVirtualizedRows(RenderTreeBuilder __builder)
- {
- var initialRowIndex = 2; // aria-rowindex is 1-based, plus the first row is the header
- var rowIndex = initialRowIndex;
- foreach (var item in _currentNonVirtualizedViewItems)
- {
- RenderRow(__builder, rowIndex++, item);
- }
-
- // When pagination is enabled, by default ensure we render the exact number of expected rows per page,
- // even if there aren't enough data items. This avoids the layout jumping on the last page.
- // Consider making this optional.
- if (Pagination is not null)
- {
- while (rowIndex++ < initialRowIndex + Pagination.ItemsPerPage)
- {
-
-
-
- }
- }
- }
-
- private void RenderRow(RenderTreeBuilder __builder, int rowIndex, TGridItem item)
- {
- if (RowTemplate is null)
- {
- RenderOriginalRow(__builder, rowIndex, item);
- }
- else
- {
- var args = new BitQuickGridRowTemplateArgs
- {
- RowIndex = rowIndex,
- RowItem = item,
- OriginalRow = (builder) => RenderOriginalRow(builder, rowIndex, item)
- };
- __builder.AddContent(0, RowTemplate(args));
- }
- }
-
- private void RenderOriginalRow(RenderTreeBuilder __builder, int rowIndex, TGridItem item)
- {
-
- @foreach (var col in _columns)
- {
-
- @{
- col.CellContent(__builder, item);
- }
-
- }
-
- }
-
- private void RenderPlaceholderRow(RenderTreeBuilder __builder, PlaceholderContext placeholderContext)
- {
-
- @foreach (var col in _columns)
- {
-
- @{
- col.RenderPlaceholderContent(__builder, placeholderContext);
- }
-
- }
-
- }
-
- private void RenderColumnHeaders(RenderTreeBuilder __builder)
- {
- foreach (var col in _columns)
- {
-
- @col.HeaderContent
-
- @if (col == _displayOptionsForColumn)
- {
- @col.ColumnOptions
- }
-
- @if (ResizableColumns)
- {
-
- }
-
- }
- }
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.razor.cs
deleted file mode 100644
index faef31da68..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.razor.cs
+++ /dev/null
@@ -1,751 +0,0 @@
-// a fork from the Blazor QuickGrid at https://github.com/dotnet/aspnetcore/tree/main/src/Components/QuickGrid
-
-namespace Bit.BlazorUI;
-
-///
-/// BitQuickGrid is a robust way to display an information-rich collection of items, and allow people to sort, and filter the content.
-///
-/// The type of data represented by each row in the grid.
-[CascadingTypeParameter(nameof(TGridItem))]
-public partial class BitQuickGrid : IAsyncDisposable
-{
- private bool _disposed;
- private int _ariaBodyRowCount;
- private ElementReference _tableReference;
- private Virtualize<(int, TGridItem)>? _virtualizeComponent;
- private ICollection _currentNonVirtualizedViewItems = Array.Empty();
-
- // IQueryable only exposes synchronous query APIs. IAsyncQueryExecutor is an adapter that lets us invoke any
- // async query APIs that might be available. We have built-in support for using EF Core's async query APIs.
- private IAsyncQueryExecutor? _asyncQueryExecutor;
-
- // We cascade the InternalGridContext to descendants, which in turn call it to add themselves to _columns
- // This happens on every render so that the column list can be updated dynamically
- private InternalGridContext _internalGridContext;
- private List> _columns;
- private bool _collectingColumns; // Columns might re-render themselves arbitrarily. We only want to capture them at a defined time.
-
- // Tracking state for options and sorting
- private BitQuickGridColumnBase? _displayOptionsForColumn;
- private BitQuickGridColumnBase? _sortByColumn;
- private bool _sortByAscending;
- private bool _checkColumnOptionsPosition;
- // Set when column recollection drops the active sort column; triggers a data refresh after render
- // so the grid query stays in sync with the (now changed) header sort state.
- private bool _queueSortReconciliationRefresh;
- // Tracks whether columns have been collected at least once, and the sort column captured at the
- // start of a collection pass, so a *new* default sort applied during a later recollection can also
- // queue a refresh (the very first collection already loads data via ColumnsFirstCollected).
- private bool _columnsCollectedOnce;
- private BitQuickGridColumnBase? _sortByColumnBeforeCollect;
- // Captures the first default-sort column (and its direction) discovered during the current
- // collection pass, so FinishCollectingColumns can adopt it when the previously active sort column
- // is no longer present after recollection instead of clearing sorting outright.
- private BitQuickGridColumnBase? _defaultSortColumnDuringCollect;
- private BitQuickGridSortDirection? _defaultSortDirectionDuringCollect;
-
- // The associated ES6 module, which uses document-level event listeners
- //private IJSObjectReference? _jsModule;
- private IJSObjectReference? _jsEventDisposable;
-
- // Caches of method->delegate conversions
- private readonly RenderFragment _renderColumnHeaders;
- private readonly RenderFragment _renderNonVirtualizedRows;
-
- // We try to minimize the number of times we query the items provider, since queries may be expensive
- // We only re-query when the developer calls RefreshDataAsync, or if we know something's changed, such
- // as sort order, the pagination state, or the data source itself. These fields help us detect when
- // things have changed, and to discard earlier load attempts that were superseded.
- private int? _lastRefreshedPaginationStateHash;
- private object? _lastAssignedItemsOrProvider;
- // Tracks the Virtualize value the data was last refreshed under, so a flip between virtualized and
- // non-virtualized rendering forces a re-query (otherwise the stale non-virtualized view would linger).
- private bool? _lastRefreshedVirtualize;
- private CancellationTokenSource? _pendingDataLoadCancellationTokenSource;
- // Hash of the collected column set the resize handles were last bound against, so we only rebind
- // when the columns actually change rather than on every render.
- private int? _lastInitColumnsHash;
- // Tracks the ResizableColumns value the resize handles were last bound against, so toggling the
- // feature on/off (without otherwise changing the columns) still rebinds the new/removed handles.
- private bool _lastResizableColumns;
-
- // If the PaginationState mutates, it raises this event. We use it to trigger a re-render.
- private readonly EventCallbackSubscriber _currentPageItemsChanged;
-
-
-
- [Inject] private IJSRuntime _js { get; set; } = default!;
- [Inject] private IServiceProvider _services { get; set; } = default!;
-
-
-
- ///
- /// Constructs an instance of .
- ///
- public BitQuickGrid()
- {
- _columns = new();
- _internalGridContext = new(this);
- _currentPageItemsChanged = new(EventCallback.Factory.Create(this, RefreshDataCoreAsync));
- _renderColumnHeaders = RenderColumnHeaders;
- _renderNonVirtualizedRows = RenderNonVirtualizedRows;
-
- // As a special case, we don't issue the first data load request until we've collected the initial set of columns
- // This is so we can apply default sort order (or any future per-column options) before loading data
- // We use EventCallbackSubscriber to safely hook this async operation into the synchronous rendering flow
- var columnsFirstCollectedSubscriber = new EventCallbackSubscriber(
- EventCallback.Factory.Create(this, RefreshDataCoreAsync));
- columnsFirstCollectedSubscriber.SubscribeOrMove(_internalGridContext.ColumnsFirstCollected);
- }
-
- private bool IsLoading => _pendingDataLoadCancellationTokenSource is not null;
-
-
-
- ///
- /// Defines the child components of this instance. For example, you may define columns by adding
- /// components derived from the base class.
- ///
- [Parameter] public RenderFragment? ChildContent { get; set; }
-
- ///
- /// An optional CSS class name. If given, this will be included in the class attribute of the rendered table.
- ///
- [Parameter] public string? Class { get; set; }
-
- ///
- /// Alias of the ChildContent parameter.
- ///
- [Parameter] public RenderFragment? Columns { get; set; }
-
- ///
- /// Optionally defines a value for @key on each rendered row. Typically this should be used to specify a
- /// unique identifier, such as a primary key value, for each data item.
- ///
- /// This allows the grid to preserve the association between row elements and data items based on their
- /// unique identifiers, even when the TGridItem instances are replaced by new copies (for
- /// example, after a new query against the underlying data store).
- ///
- /// If not set, the @key will be the TGridItem instance itself.
- ///
- [Parameter] public Func ItemKey { get; set; } = x => x!;
-
- ///
- /// A queryable source of data for the grid.
- ///
- /// This could be in-memory data converted to queryable using the
- /// extension method,
- /// or an EntityFramework DataSet or an derived from it.
- ///
- /// You should supply either or , but not both.
- ///
- [Parameter] public IQueryable? Items { get; set; }
-
- ///
- /// This is applicable only when using . It defines an expected height in pixels for
- /// each row, allowing the virtualization mechanism to fetch the correct number of items to match the display
- /// size and to ensure accurate scrolling.
- ///
- [Parameter] public float ItemSize { get; set; } = 50;
-
- ///
- /// A callback that supplies data for the grid.
- ///
- /// You should supply either or , but not both.
- ///
- [Parameter] public BitQuickGridItemsProvider? ItemsProvider { get; set; }
-
- ///
- /// The custom template to render while loading the new items.
- /// It is honored in both the non-virtualized and the virtualized ( ) paths:
- /// while a data refresh is in flight the template is shown instead of the rows, and the
- /// placeholder flow still covers rows streamed in during scrolling.
- ///
- [Parameter] public RenderFragment? LoadingTemplate { get; set; }
-
- ///
- /// Optionally links this instance with a model,
- /// causing the grid to fetch and render only the current page of data.
- ///
- /// This is normally used in conjunction with a component or some other UI logic
- /// that displays and updates the supplied instance.
- ///
- [Parameter] public BitQuickGridPaginationState? Pagination { get; set; }
-
- ///
- /// If true, renders draggable handles around the column headers, allowing the user to resize the columns
- /// manually. Size changes are not persisted.
- ///
- [Parameter] public bool ResizableColumns { get; set; }
-
- ///
- /// The CSS class of all rows of the data grid.
- ///
- [Parameter] public string? RowClass { get; set; }
-
- ///
- /// The function to generate the CSS class of each row of the data grid.
- ///
- [Parameter] public Func? RowClassSelector { get; set; }
-
- ///
- /// The CSS style of all row of the data grid.
- ///
- [Parameter] public string? RowStyle { get; set; }
-
- ///
- /// The function to generate the CSS style of each row of the data grid.
- ///
- [Parameter] public Func? RowStyleSelector { get; set; }
-
- ///
- /// Optional template to customize row rendering. Receives with
- /// set to the default row content; call it to render the original cells or replace with custom content.
- ///
- [Parameter] public RenderFragment>? RowTemplate { get; set; }
-
- ///
- /// A theme name, with default value "default". This affects which styling rules match the table.
- ///
- [Parameter] public string? Theme { get; set; } = "default";
-
- ///
- /// If true, the grid will be rendered with virtualization. This is normally used in conjunction with
- /// scrolling and causes the grid to fetch and render only the data around the current scroll viewport.
- /// This can greatly improve the performance when scrolling through large data sets.
- ///
- /// If you use , you should supply a value for and must
- /// ensure that every row renders with the same constant height.
- ///
- /// Generally it's preferable not to use if the amount of data being rendered
- /// is small or if you are using pagination.
- ///
- [Parameter] public bool Virtualize { get; set; }
-
-
-
- ///
- /// Sets the grid's current sort column to the specified .
- ///
- /// The column that defines the new sort order.
- /// The direction of sorting. If the value is , then it will toggle the direction on each call.
- /// A representing the completion of the operation.
- public Task SortByColumnAsync(BitQuickGridColumnBase column, BitQuickGridSortDirection direction = BitQuickGridSortDirection.Auto)
- {
- _sortByAscending = direction switch
- {
- BitQuickGridSortDirection.Ascending => true,
- BitQuickGridSortDirection.Descending => false,
- BitQuickGridSortDirection.Auto => _sortByColumn == column ? !_sortByAscending : true,
- _ => throw new NotSupportedException($"Unknown sort direction {direction}"),
- };
-
- _sortByColumn = column;
-
- StateHasChanged(); // We want to see the updated sort order in the header, even before the data query is completed
- return RefreshDataAsync();
- }
-
- ///
- /// Displays the UI for the specified column, closing any other column
- /// options UI that was previously displayed.
- ///
- /// The column whose options are to be displayed, if any are available.
- public void ShowColumnOptions(BitQuickGridColumnBase column)
- {
- _displayOptionsForColumn = column;
- _checkColumnOptionsPosition = true; // Triggers a call to JS to position the options element, apply autofocus, and any other setup
- StateHasChanged();
- }
-
- ///
- /// Instructs the grid to re-fetch and render the current data from the supplied data source
- /// (either or ).
- ///
- /// A that represents the completion of the operation.
- public async Task RefreshDataAsync()
- {
- try
- {
- await RefreshDataCoreAsync();
- }
- finally
- {
- // Always rerender after the core refresh settles, even when it throws, so the grid
- // doesn't get stuck showing the loading state if the caller handles the exception.
- StateHasChanged();
- }
- }
-
-
-
- // Invoked by descendant columns at a special time during rendering
- internal void AddColumn(BitQuickGridColumnBase column, BitQuickGridSortDirection? isDefaultSortDirection)
- {
- if (_collectingColumns)
- {
- _columns.Add(column);
-
- if (_sortByColumn is null && isDefaultSortDirection.HasValue)
- {
- _sortByColumn = column;
- _sortByAscending = isDefaultSortDirection.Value != BitQuickGridSortDirection.Descending;
- }
-
- // Remember the first default-sort column collected in this pass even when a (possibly stale)
- // _sortByColumn is still set. If that prior column turns out to have been dropped, this lets
- // FinishCollectingColumns switch to the newly declared default instead of clearing sorting.
- if (isDefaultSortDirection.HasValue && _defaultSortColumnDuringCollect is null)
- {
- _defaultSortColumnDuringCollect = column;
- _defaultSortDirectionDuringCollect = isDefaultSortDirection.Value;
- }
- }
- }
-
-
-
- ///
- protected override Task OnParametersSetAsync()
- {
- // The associated pagination state may have been added/removed/replaced
- _currentPageItemsChanged.SubscribeOrMove(Pagination?.CurrentPageItemsChanged);
-
- if (Items is not null && ItemsProvider is not null)
- {
- throw new InvalidOperationException($"BitQuickGrid requires one of {nameof(Items)} or {nameof(ItemsProvider)}, but both were specified.");
- }
-
- // Perform a re-query only if the data source or something else has changed
- var _newItemsOrItemsProvider = Items ?? (object?)ItemsProvider;
- var dataSourceHasChanged = _newItemsOrItemsProvider != _lastAssignedItemsOrProvider;
- if (dataSourceHasChanged)
- {
- _lastAssignedItemsOrProvider = _newItemsOrItemsProvider;
- _asyncQueryExecutor = AsyncQueryExecutorSupplier.GetAsyncQueryExecutor(_services, Items);
- }
-
- var mustRefreshData = dataSourceHasChanged
- || (_lastRefreshedVirtualize != Virtualize)
- || (ComputePaginationStateHash() != _lastRefreshedPaginationStateHash);
-
- // We don't want to trigger the first data load until we've collected the initial set of columns,
- // because they might perform some action like setting the default sort order, so it would be wasteful
- // to have to re-query immediately
- if (_columns.Count > 0 && mustRefreshData)
- {
- return RefreshDataCoreAsync();
- }
-
- return Task.CompletedTask;
- }
-
- protected override async Task OnAfterRenderAsync(bool firstRender)
- {
- if (firstRender)
- {
- _jsEventDisposable = await _js.BitQuickGridInit(_tableReference);
- _lastInitColumnsHash = ComputeColumnsHash();
- _lastResizableColumns = ResizableColumns;
- }
- else if (ResizableColumns)
- {
- // The resize handles (.bit-qkg-drg) are bound per-element by init. When the column set
- // changes, the header re-renders with fresh handles that have no listeners, so rebind them.
- // The handles also appear/disappear when ResizableColumns itself is toggled, so rebind on
- // that transition too. Re-running init re-adds the document-level listeners, so stop the
- // previous registration first to avoid leaking duplicate handlers. Unchanged renders are skipped.
- var hash = ComputeColumnsHash();
- if (hash != _lastInitColumnsHash || !_lastResizableColumns)
- {
- _lastInitColumnsHash = hash;
- _lastResizableColumns = true;
- await StopJsEventsAsync();
- _jsEventDisposable = await _js.BitQuickGridInit(_tableReference);
- }
- }
- else if (_lastResizableColumns)
- {
- // ResizableColumns was just turned off; the drag handles are gone. Rebind so the
- // document-level listeners are refreshed and the stale handle registration is dropped.
- _lastResizableColumns = false;
- await StopJsEventsAsync();
- _jsEventDisposable = await _js.BitQuickGridInit(_tableReference);
- }
-
- if (_checkColumnOptionsPosition && _displayOptionsForColumn is not null)
- {
- _checkColumnOptionsPosition = false;
- await _js.BitQuickGridCheckColumnOptionsPosition(_tableReference);
- }
-
- if (_queueSortReconciliationRefresh)
- {
- // Column recollection dropped the active sort column; re-query so the grid data matches
- // the header state that no longer shows that sort.
- _queueSortReconciliationRefresh = false;
- await RefreshDataAsync();
- }
- }
-
- private int ComputeColumnsHash()
- {
- var hash = new HashCode();
- foreach (var col in _columns) hash.Add(col);
- return hash.ToHashCode();
- }
-
- // Only the requested slice inputs (page index + page size) should trigger a re-query. The pagination
- // state's own GetHashCode also folds in TotalItemCount, which the grid mutates after every successful
- // load, so using it here would make a completed fetch look like a fresh pagination change and kick off
- // an immediate redundant second query.
- private int? ComputePaginationStateHash()
- => Pagination is null ? null : HashCode.Combine(Pagination.CurrentPageIndex, Pagination.ItemsPerPage);
-
- private async Task StopJsEventsAsync()
- {
- try
- {
- if (_jsEventDisposable is not null)
- {
- await _jsEventDisposable.InvokeVoidAsync("stop");
- await _jsEventDisposable.DisposeAsync();
- _jsEventDisposable = null;
- }
- }
- catch (JSDisconnectedException) { }
- catch (JSException) { }
- }
-
-
-
- private void StartCollectingColumns()
- {
- _sortByColumnBeforeCollect = _sortByColumn;
- _defaultSortColumnDuringCollect = null;
- _defaultSortDirectionDuringCollect = null;
- _columns.Clear();
- _collectingColumns = true;
- }
-
- private void FinishCollectingColumns()
- {
- _collectingColumns = false;
-
- // The column that drove the last data load may no longer be among the freshly collected
- // columns (it was removed or replaced). Leaving _sortByColumn pointing at a dropped column
- // desyncs the data query from the header, so reconcile it and queue a refresh so the grid
- // re-queries. The refresh is run from OnAfterRenderAsync because this runs mid-render.
- if (_sortByColumn is not null && _columns.Contains(_sortByColumn) is false)
- {
- if (_defaultSortColumnDuringCollect is not null)
- {
- // A newly collected column declares a default sort, so adopt it instead of clearing
- // sorting; otherwise a dynamic column swap would drop the intended default order.
- _sortByColumn = _defaultSortColumnDuringCollect;
- _sortByAscending = _defaultSortDirectionDuringCollect!.Value != BitQuickGridSortDirection.Descending;
- }
- else
- {
- _sortByColumn = null;
- _sortByAscending = false;
- }
- _queueSortReconciliationRefresh = true;
- }
- else if (_columnsCollectedOnce && _sortByColumnBeforeCollect is null && _sortByColumn is not null)
- {
- // A recollection assigned a brand-new default sort (none was active before). The initial
- // collection already loads data via ColumnsFirstCollected, but later recollections do not,
- // so queue a refresh to re-query in the newly defaulted sort order and keep header/data in sync.
- _queueSortReconciliationRefresh = true;
- }
-
- _columnsCollectedOnce = true;
- }
-
- // Same as RefreshDataAsync, except without forcing a re-render. We use this from OnParametersSetAsync
- // because in that case there's going to be a re-render anyway.
- private async Task RefreshDataCoreAsync()
- {
- // Record the Virtualize mode this refresh runs under so every refresh path keeps the marker
- // current: the initial column-driven load (via ColumnsFirstCollected), RefreshDataAsync, and the
- // parameter-change trigger in OnParametersSetAsync all funnel through here. Updating it only in
- // OnParametersSetAsync would leave it stale after those other paths, so a later parameter set
- // could wrongly (or never) detect a virtualized/non-virtualized flip.
- _lastRefreshedVirtualize = Virtualize;
-
- // Snapshot the requested pagination slice up front so both the virtualized and non-virtualized
- // paths record it. Doing this only in the non-virtualized branch (and relying on
- // ProvideVirtualizedItems otherwise) leaves the marker stale when Virtualize is on but its child
- // hasn't requested items yet (e.g. first render / right after toggling virtualization). The next
- // OnParametersSetAsync would then see ComputePaginationStateHash() != _lastRefreshedPaginationStateHash
- // and fire a duplicate initial query for the same slice.
- _lastRefreshedPaginationStateHash = ComputePaginationStateHash();
-
- // Move into a "loading" state, cancelling any earlier-but-still-pending load. Do NOT dispose
- // the previous source here: the load that owns it may still be in flight and holding its token
- // (e.g. registered on it), so disposing now could surface an ObjectDisposedException instead of
- // the expected OperationCanceledException. Each load disposes its own source in its finally block
- // once it has finished using it (whether or not it is still the current one), so a superseded
- // source is disposed by its owning load rather than leaked to the GC.
- _pendingDataLoadCancellationTokenSource?.Cancel();
- var thisLoadCts = _pendingDataLoadCancellationTokenSource = new CancellationTokenSource();
-
- // Render now so the loading state (IsLoading / LoadingTemplate) becomes visible as soon as the
- // refresh starts, instead of only after the async load below completes.
- StateHasChanged();
-
- if (Virtualize)
- {
- // If we're using Virtualize, we have to go through its RefreshDataAsync API otherwise:
- // (1) It won't know to update its own internal state if the provider output has changed
- // (2) We won't know what slice of data to query for
- // The reference can still be null before it's captured (first render) or right after toggling
- // virtualization on; in that case Virtualize will request its own items once it renders, so we
- // just reconcile the load-state here. The non-virtualized provider request must never run for a
- // virtualized grid.
- try
- {
- if (_virtualizeComponent is not null)
- {
- await _virtualizeComponent.RefreshDataAsync();
- }
- }
- finally
- {
- // Always reconcile the load-state, even if RefreshDataAsync threw, so we don't leak the
- // CTS or leave _pendingDataLoadCancellationTokenSource pointing at a disposed instance.
- // This load is done with its own source, so dispose it unconditionally; only clear the
- // field when it still points at this source (a newer load may already own it).
- thisLoadCts.Dispose();
- if (ReferenceEquals(_pendingDataLoadCancellationTokenSource, thisLoadCts))
- {
- _pendingDataLoadCancellationTokenSource = null;
- }
- }
- }
- else
- {
- // If we're not using Virtualize, we build and execute a request against the items provider directly
- var startIndex = Pagination is null ? 0 : (Pagination.CurrentPageIndex * Pagination.ItemsPerPage);
- var request = new BitQuickGridItemsProviderRequest(
- startIndex, Pagination?.ItemsPerPage, _sortByColumn, _sortByAscending, thisLoadCts.Token);
- try
- {
- var result = await ResolveItemsRequestAsync(request);
- if (!thisLoadCts.IsCancellationRequested)
- {
- _currentNonVirtualizedViewItems = result.Items;
- _ariaBodyRowCount = _currentNonVirtualizedViewItems.Count;
- await (Pagination?.SetTotalItemCountAsync(result.TotalItemCount) ?? Task.CompletedTask);
- }
- }
- catch (OperationCanceledException) when (thisLoadCts.IsCancellationRequested)
- {
- // This load was superseded by a newer request (our own cancellation token fired); swallow
- // the cancellation and fall through to the cleanup below so the load-state remains
- // consistent. Cancellations from any other source (e.g. a provider-side timeout) propagate.
- }
- finally
- {
- // This load is done with its own source, so dispose it unconditionally to avoid leaking
- // a superseded source; only clear the field when it still points at this source.
- thisLoadCts.Dispose();
- if (ReferenceEquals(_pendingDataLoadCancellationTokenSource, thisLoadCts))
- {
- _pendingDataLoadCancellationTokenSource = null;
- }
- }
- }
- }
-
- // Gets called both by RefreshDataCoreAsync and directly by the Virtualize child component during scrolling
- private async ValueTask> ProvideVirtualizedItems(ItemsProviderRequest request)
- {
- _lastRefreshedPaginationStateHash = ComputePaginationStateHash();
-
- // Debounce the requests. This eliminates a lot of redundant queries at the cost of slight lag after interactions.
- // TODO: Consider making this configurable, or smarter (e.g., doesn't delay on first call in a batch, then the amount
- // of delay increases if you rapidly issue repeated requests, such as when scrolling a long way)
- try
- {
- await Task.Delay(100, request.CancellationToken);
- }
- catch (OperationCanceledException)
- {
- // The request was superseded/cancelled during the debounce window; abandon it early.
- return default;
- }
- if (request.CancellationToken.IsCancellationRequested)
- {
- return default;
- }
-
- // Combine the query parameters from Virtualize with the ones from PaginationState
- var startIndex = request.StartIndex;
- var count = request.Count;
- if (Pagination is not null)
- {
- startIndex += Pagination.CurrentPageIndex * Pagination.ItemsPerPage;
- count = Math.Max(0, Math.Min(request.Count, Pagination.ItemsPerPage - request.StartIndex));
- }
-
- var providerRequest = new BitQuickGridItemsProviderRequest(
- startIndex, count, _sortByColumn, _sortByAscending, request.CancellationToken);
- BitQuickGridItemsProviderResult providerResult;
- try
- {
- providerResult = await ResolveItemsRequestAsync(providerRequest);
- }
- catch (OperationCanceledException) when (request.CancellationToken.IsCancellationRequested)
- {
- // The request was superseded by a newer one after the debounce window (our own cancellation
- // token fired); the items provider observed the cancellation token and bailed out. Return an
- // empty result the virtualization system can handle rather than letting the cancellation
- // propagate out of here. Cancellations from any other source propagate as real errors.
- return default;
- }
-
- if (!request.CancellationToken.IsCancellationRequested)
- {
- // ARIA's rowcount is part of the UI, so it should reflect what the human user regards as the number of rows in the table,
- // not the number of physical elements. For virtualization this means what's in the entire scrollable range, not just
- // the current viewport. In the case where you're also paginating then it means what's conceptually on the current page.
- // The last page can hold fewer than ItemsPerPage rows, so clamp the paginated count to the items remaining on the current
- // page; otherwise assistive tech would announce non-existent trailing rows on a short final page.
- _ariaBodyRowCount = Pagination is null
- ? providerResult.TotalItemCount
- : Math.Clamp(providerResult.TotalItemCount - Pagination.CurrentPageIndex * Pagination.ItemsPerPage, 0, Pagination.ItemsPerPage);
-
- await (Pagination?.SetTotalItemCountAsync(providerResult.TotalItemCount) ?? Task.CompletedTask);
-
- // We're supplying the row index along with each row's data because we need it for aria-rowindex, and we have to account for
- // the virtualized start index. It might be more performant just to have some _latestQueryRowStartIndex field, but we'd have
- // to make sure it doesn't get out of sync with the rows being rendered.
- return new ItemsProviderResult<(int, TGridItem)>(
- items: providerResult.Items.Select((x, i) => ValueTuple.Create(i + request.StartIndex + 2, x)),
- totalItemCount: _ariaBodyRowCount);
- }
-
- return default;
- }
-
- // Normalizes all the different ways of configuring a data source so they have common GridItemsProvider-shaped API
- private async ValueTask> ResolveItemsRequestAsync(BitQuickGridItemsProviderRequest request)
- {
- if (ItemsProvider is not null)
- {
- return await ItemsProvider(request);
- }
- else if (Items is not null)
- {
- var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items);
- var result = request.ApplySorting(Items).Skip(request.StartIndex);
- if (request.Count.HasValue)
- {
- result = result.Take(request.Count.Value);
- }
- var resultArray = _asyncQueryExecutor is null ? result.ToArray() : await _asyncQueryExecutor.ToArrayAsync(result);
- return BitQuickGridItemsProviderResult.From(resultArray, totalItemCount);
- }
- else
- {
- return BitQuickGridItemsProviderResult.From(Array.Empty(), 0);
- }
- }
-
- private string AriaSortValue(BitQuickGridColumnBase column)
- => _sortByColumn == column
- ? (_sortByAscending ? "ascending" : "descending")
- : "none";
-
- private string? ColumnHeaderClass(BitQuickGridColumnBase column)
- => _sortByColumn == column
- ? $"{ColumnClass(column)} {(_sortByAscending ? "bit-qkg-csa" : "bit-qkg-csd")}"
- : ColumnClass(column);
-
- private string GridClass()
- => $"bit-qkg {Class} {((IsLoading && LoadingTemplate is null) ? "loading" : null)}".Trim();
-
- private void CloseColumnOptions()
- {
- _displayOptionsForColumn = null;
- }
-
- private string? GetRowClass(TGridItem item)
- {
- var selected = RowClassSelector?.Invoke(item);
-
- if (string.IsNullOrEmpty(RowClass)) return string.IsNullOrEmpty(selected) ? null : selected;
- if (string.IsNullOrEmpty(selected)) return RowClass;
- return $"{RowClass} {selected}";
- }
-
- private string? GetRowStyle(TGridItem item)
- {
- var selected = RowStyleSelector?.Invoke(item);
-
- if (string.IsNullOrEmpty(RowStyle)) return string.IsNullOrEmpty(selected) ? null : selected;
- if (string.IsNullOrEmpty(selected)) return RowStyle;
- return $"{RowStyle};{selected}";
- }
-
-
-
- private static string? ColumnClass(BitQuickGridColumnBase column) => column.Align switch
- {
- BitQuickGridAlign.Center => $"bit-qkg-cjc {column.Class}",
- BitQuickGridAlign.Right => $"bit-qkg-cje {column.Class}",
- _ => column.Class,
- };
-
-
-
-
- ///
- public async ValueTask DisposeAsync()
- {
- await DisposeAsync(true);
- GC.SuppressFinalize(this);
- }
-
- protected virtual async ValueTask DisposeAsync(bool disposing)
- {
- if (_disposed || disposing is false) return;
-
- // Cancel (but don't dispose) any in-flight load: the load that owns this source may still be
- // holding its token, so disposing here could race into an ObjectDisposedException. The owning
- // load disposes it in its finally block (it's still the current source during disposal), so we
- // only signal cancellation here.
- _pendingDataLoadCancellationTokenSource?.Cancel();
-
- _currentPageItemsChanged.Dispose();
-
- try
- {
- if (_jsEventDisposable is not null)
- {
- await _jsEventDisposable.InvokeVoidAsync("stop");
- await _jsEventDisposable.DisposeAsync();
- }
-
- //if (_jsModule is not null)
- //{
- // await _jsModule.DisposeAsync();
- //}
- }
- catch (JSDisconnectedException)
- {
- // The JS side may routinely be gone already if the reason we're disposing is that
- // the client disconnected. This is not an error.
- }
- catch (JSException ex)
- {
- // it seems it's safe to just ignore this exception here.
- // otherwise it will blow up the MAUI app in a page refresh for example.
- Console.WriteLine(ex.Message);
- }
-
- _disposed = true;
- }
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.scss
deleted file mode 100644
index 1109021ff7..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.scss
+++ /dev/null
@@ -1,138 +0,0 @@
-.bit-qkg {
- width: 100%;
- --bit-qkg-col-gap: 1rem;
-
- th {
- position: relative;
- }
-
- > thead > tr > th {
- font-weight: normal;
- }
-
- &.loading > tbody {
- opacity: 0.25;
- transition: opacity linear 100ms 25ms;
- }
-
- > tbody > tr > td {
- padding: 0.1rem calc(0.4rem + var(--bit-qkg-col-gap)) 0.1rem 0.4rem;
- }
-}
-
-
-.bit-qkg-hct {
- display: flex;
- position: relative;
- align-items: center;
- padding-inline-end: var(--bit-qkg-col-gap);
-}
-
-.bit-qkg-cop {
- z-index: 1;
- padding: 1rem;
- border: 1px solid;
- position: absolute;
- inset-inline-start: 0;
- border-color: var(--bit-clr-brd-pri);
- background-color: var(--bit-clr-bg-sec);
-}
-
-.bit-qkg-cob {
- width: 1.5rem;
- background: unset;
-
- &::before {
- content: "\E712";
- font-style: normal;
- font-weight: normal;
- display: inline-block;
- font-family: 'Fabric MDL2 bit BlazorUI Extras';
- }
-}
-
-.bit-qkg-drg {
- width: 1rem;
- cursor: ew-resize;
- position: absolute;
- inset-block-end: 0;
- inset-block-start: 0;
- inset-inline-end: calc(var(--bit-qkg-col-gap)/2 - 0.5rem);
-
- &::after {
- content: ' ';
- position: absolute;
- inset-block-end: 5px;
- inset-block-start: 5px;
- inset-inline-start: 0.5rem;
- border-color: var(--bit-clr-brd-pri);
- border-inline-start: 1px solid var(--bit-clr-brd-pri);
- }
-}
-
-.bit-qkg-srt {
- width: 1rem;
- height: 1rem;
- opacity: 0.5;
- align-self: center;
- text-align: center;
-}
-
-.bit-qkg-csa .bit-qkg-srt::before,
-.bit-qkg-csd .bit-qkg-srt::before {
- content: "\E96F";
- font-style: normal;
- font-weight: normal;
- display: inline-block;
- transform: rotate(90deg);
- font-family: 'Fabric MDL2 bit BlazorUI Extras';
-}
-
-.bit-qkg-csd .bit-qkg-srt {
- transform: scaleY(-1) translateY(-2px);
-}
-
-.bit-qkg-ctl {
- gap: 0.4rem;
- flex-grow: 1;
- display: flex;
- min-width: 0px;
- font-size: 1rem;
- font-weight: bold;
- padding: 0.1rem 0.4rem;
-}
-
-button.bit-qkg-ctl {
- border: none;
- color: inherit;
- cursor: pointer;
- background: none;
- position: relative;
-}
-
-.bit-qkg-ctt {
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
-}
-
-.bit-qkg-cjc {
- text-align: center;
-
- .bit-qkg-ctl {
- justify-content: center;
- }
-}
-
-.bit-qkg-cje {
- text-align: end;
-
- .bit-qkg-ctl {
- flex-direction: row-reverse;
- }
-}
-
-.bit-qkg-plh::after {
- opacity: 0.75;
- content: '\2026';
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.ts
deleted file mode 100644
index 03a2585c3a..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGrid.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-namespace BitBlazorUI {
- export class QuickGrid {
- public static init(tableElement: any) {
- // Tracks the drag handles this init() bound so stop() can remove their listeners too,
- // preventing handlers from accumulating across repeated init()/stop() cycles.
- const boundDragHandles: { handle: any, listener: any }[] = [];
- // Holds the teardown for an in-progress column resize drag (the document-level move/up
- // listeners installed by handleMouseDown) so stop() can detach them even if disposal
- // happens mid-drag, before the pointer is released.
- const dragState: { cleanup: (() => void) | null } = { cleanup: null };
- QuickGrid.enableColumnResizing(tableElement, boundDragHandles, dragState);
-
- const bodyClickHandler = (event: any) => {
- const columnOptionsElement = tableElement.tHead.querySelector('.bit-qkg-cop');
- if (columnOptionsElement && event.composedPath().indexOf(columnOptionsElement) < 0) {
- tableElement.dispatchEvent(new CustomEvent('closecolumnoptions', { bubbles: true }));
- }
- };
- const keyDownHandler = (event: any) => {
- const columnOptionsElement = tableElement.tHead.querySelector('.bit-qkg-cop');
- if (columnOptionsElement && event.key === "Escape") {
- tableElement.dispatchEvent(new CustomEvent('closecolumnoptions', { bubbles: true }));
- }
- };
-
- document.body.addEventListener('click', bodyClickHandler);
- document.body.addEventListener('mousedown', bodyClickHandler); // Otherwise it seems strange that it doesn't go away until you release the mouse button
- document.body.addEventListener('keydown', keyDownHandler);
-
- return {
- stop: () => {
- document.body.removeEventListener('click', bodyClickHandler);
- document.body.removeEventListener('mousedown', bodyClickHandler);
- document.body.removeEventListener('keydown', keyDownHandler);
-
- // Detach any document-level listeners left by an in-progress resize drag and clear
- // the active drag state, so disposal/re-init mid-drag can't keep mutating a stale th.
- if (dragState.cleanup) {
- dragState.cleanup();
- dragState.cleanup = null;
- }
-
- // Remove the per-handle drag listeners and clear the bound marker so a later
- // init() can rebind the same surviving elements without duplicating handlers.
- boundDragHandles.forEach(({ handle, listener }) => {
- handle.removeEventListener('mousedown', listener);
- handle.removeEventListener('touchstart', listener);
- delete handle.__bitQkgResizeBound;
- });
- boundDragHandles.length = 0;
- }
- };
- }
-
- public static checkColumnOptionsPosition(tableElement: any) {
- const colOptions = tableElement.tHead && tableElement.tHead.querySelector('.bit-qkg-cop'); // Only match within *our* thead, not nested tables
- if (colOptions) {
- // We want the options popup to be positioned over the grid, not overflowing on either side, because it's possible that
- // beyond either side is off-screen or outside the scroll range of an ancestor
- const gridRect = tableElement.getBoundingClientRect();
- const optionsRect = colOptions.getBoundingClientRect();
- const leftOverhang = Math.max(0, gridRect.left - optionsRect.left);
- const rightOverhang = Math.max(0, optionsRect.right - gridRect.right);
- if (leftOverhang || rightOverhang) {
- // In the unlikely event that it overhangs both sides, we'll center it
- const applyOffset = leftOverhang && rightOverhang ? (leftOverhang - rightOverhang) / 2 : (leftOverhang - rightOverhang);
- colOptions.style.transform = `translateX(${applyOffset}px)`;
- } else {
- // Clear any offset left over from a previous opening so the popup isn't misaligned.
- colOptions.style.transform = '';
- }
-
- if (typeof colOptions.scrollIntoViewIfNeeded === 'function') {
- colOptions.scrollIntoViewIfNeeded();
- } else {
- // Fall back to a nearest-edge scroll so browsers without scrollIntoViewIfNeeded
- // don't scroll more aggressively than needed (the default scrollIntoView() can
- // jump the popup fully into view and shift the grid).
- colOptions.scrollIntoView({ block: 'nearest', inline: 'nearest' });
- }
-
- const autoFocusElem = colOptions.querySelector('[autofocus]');
- if (autoFocusElem) {
- autoFocusElem.focus();
- }
- }
- }
-
- private static enableColumnResizing(tableElement: any, boundDragHandles: { handle: any, listener: any }[], dragState: { cleanup: (() => void) | null }) {
- tableElement.tHead.querySelectorAll('.bit-qkg-drg').forEach((handle: any) => {
- // Bind each handle only once. A surviving handle (reused by Blazor's diffing across
- // re-renders) would otherwise accumulate a fresh listener on every init() call.
- if (handle.__bitQkgResizeBound) return;
- handle.__bitQkgResizeBound = true;
-
- handle.addEventListener('mousedown', handleMouseDown);
- if ('ontouchstart' in window) {
- handle.addEventListener('touchstart', handleMouseDown);
- }
- boundDragHandles.push({ handle, listener: handleMouseDown });
-
- function handleMouseDown(evt: any) {
- evt.preventDefault();
- evt.stopPropagation();
-
- const th = handle.parentElement;
- const startPageX = evt.touches ? evt.touches[0].pageX : evt.pageX;
- const originalColumnWidth = th.offsetWidth;
- const rtlMultiplier = window.getComputedStyle(th, null).getPropertyValue('direction') === 'rtl' ? -1 : 1;
- let updatedColumnWidth = 0;
-
- function handleMouseMove(evt: any) {
- evt.stopPropagation();
- const newPageX = evt.touches ? evt.touches[0].pageX : evt.pageX;
- // Clamp to a minimum width so a column can't collapse to (or below) zero while dragging.
- const minColumnWidth = 20;
- const nextWidth = Math.max(minColumnWidth, originalColumnWidth + (newPageX - startPageX) * rtlMultiplier);
- if (Math.abs(nextWidth - updatedColumnWidth) > 0) {
- updatedColumnWidth = nextWidth;
- th.style.width = `${updatedColumnWidth}px`;
- }
- }
-
- function handleMouseUp() {
- document.body.removeEventListener('mousemove', handleMouseMove);
- document.body.removeEventListener('mouseup', handleMouseUp);
- document.body.removeEventListener('touchmove', handleMouseMove);
- document.body.removeEventListener('touchend', handleMouseUp);
- document.body.removeEventListener('touchcancel', handleMouseUp);
- dragState.cleanup = null;
- }
-
- if (window.TouchEvent && evt instanceof TouchEvent) {
- document.body.addEventListener('touchmove', handleMouseMove, { passive: true });
- document.body.addEventListener('touchend', handleMouseUp, { passive: true });
- // A touch gesture can be interrupted (e.g. by the system) without firing touchend,
- // which would leave the move/end listeners attached. Tear down on touchcancel too.
- document.body.addEventListener('touchcancel', handleMouseUp, { passive: true });
- } else {
- document.body.addEventListener('mousemove', handleMouseMove, { passive: true });
- document.body.addEventListener('mouseup', handleMouseUp, { passive: true });
- }
-
- // Expose this drag's teardown so stop() can detach the document-level listeners if
- // the grid is disposed/re-initialized before the pointer is released.
- dragState.cleanup = handleMouseUp;
- }
- });
- }
- }
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGridJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGridJsRuntimeExtensions.cs
deleted file mode 100644
index 74e83b985a..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGridJsRuntimeExtensions.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Bit.BlazorUI;
-
-internal static class BitQuickGridJsRuntimeExtensions
-{
- public static async ValueTask BitQuickGridInit(this IJSRuntime jsRuntime, ElementReference tableElement)
- {
- return await jsRuntime.InvokeAsync("BitBlazorUI.QuickGrid.init", tableElement);
- }
-
- public static async ValueTask BitQuickGridCheckColumnOptionsPosition(this IJSRuntime jsRuntime, ElementReference tableElement)
- {
- await jsRuntime.InvokeVoidAsync("BitBlazorUI.QuickGrid.checkColumnOptionsPosition", tableElement);
- }
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGridRowTemplateArgs.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGridRowTemplateArgs.cs
deleted file mode 100644
index e79ed3fb46..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGridRowTemplateArgs.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// Arguments passed to the render fragment.
-///
-/// The type of data represented by each row in the grid.
-public class BitQuickGridRowTemplateArgs
-{
- ///
- /// A render fragment that produces the original row markup (the default <tr> with all column cells).
- /// Render this in your custom template to include the default row, or omit it to replace entirely.
- ///
- public required RenderFragment OriginalRow { get; set; }
-
- ///
- /// The 1-based row index used for accessibility (e.g. aria-rowindex).
- ///
- public int RowIndex { get; set; }
-
- ///
- /// The data item for this row.
- ///
- public T RowItem { get; set; } = default!;
-}
-
-///
-/// Backward-compatible alias for , kept so existing code that
-/// stored, returned or accepted the old BitDataGridRowTemplateArgs<T> type keeps compiling. It
-/// inherits the renamed type (so instances are accepted anywhere a
-/// is expected) and preserves the same public API surface.
-///
-/// The type of data represented by each row in the grid.
-[Obsolete("BitDataGridRowTemplateArgs has been renamed to BitQuickGridRowTemplateArgs. Use BitQuickGridRowTemplateArgs instead.")]
-public class BitDataGridRowTemplateArgs : BitQuickGridRowTemplateArgs
-{
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGridSortDirection.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGridSortDirection.cs
deleted file mode 100644
index 0136b25a3b..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/BitQuickGridSortDirection.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// Describes the direction in which a column is sorted.
-///
-public enum BitQuickGridSortDirection
-{
- ///
- /// Ascending order.
- ///
- Ascending,
-
- ///
- /// Descending order.
- ///
- Descending,
-
- ///
- /// Automatic sort order. When used with ,
- /// the sort order will automatically toggle between and on successive calls, and
- /// resets to whenever the specified column is changed.
- ///
- Auto,
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridAlign.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridAlign.cs
deleted file mode 100644
index f71d0eb676..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridAlign.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// Describes alignment for a column.
-///
-public enum BitQuickGridAlign
-{
- ///
- /// Justifies the content against the start of the container.
- ///
- Left,
-
- ///
- /// Justifies the content at the center of the container.
- ///
- Center,
-
- ///
- /// Justifies the content at the end of the container.
- ///
- Right,
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridColumnBase.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridColumnBase.razor
deleted file mode 100644
index cc030ea8e5..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridColumnBase.razor
+++ /dev/null
@@ -1,148 +0,0 @@
-@namespace Bit.BlazorUI
-@typeparam TGridItem
-
-@{
- InternalGridContext.Grid.AddColumn(this, IsDefaultSort);
-}
-
-@code
-{
- [CascadingParameter] internal InternalGridContext InternalGridContext { get; set; } = default!;
-
- ///
- /// Title text for the column. This is rendered automatically if is not used.
- ///
- [Parameter] public string? Title { get; set; }
-
- ///
- /// An optional CSS class name. If specified, this is included in the class attribute of table header and body cells
- /// for this column.
- ///
- [Parameter] public string? Class { get; set; }
-
- ///
- /// If specified, controls the justification of table header and body cells for this column.
- ///
- [Parameter] public BitQuickGridAlign Align { get; set; }
-
- ///
- /// An optional template for this column's header cell. If not specified, the default header template
- /// includes the along with any applicable sort indicators and options buttons.
- ///
- [Parameter] public RenderFragment>? HeaderTemplate { get; set; }
-
- ///
- /// If specified, indicates that this column has this associated options UI. A button to display this
- /// UI will be included in the header cell by default.
- ///
- /// If is used, it is left up to that template to render any relevant
- /// "show options" UI and invoke the grid's ).
- ///
- [Parameter] public RenderFragment? ColumnOptions { get; set; }
-
- ///
- /// Indicates whether the data should be sortable by this column.
- ///
- /// The default value may vary according to the column type (for example, a
- /// is sortable by default if any parameter is specified).
- ///
- [Parameter] public bool? Sortable { get; set; }
-
- ///
- /// If specified and not null, indicates that this column represents the initial sort order
- /// for the grid. The supplied value controls the default sort direction.
- ///
- [Parameter] public BitQuickGridSortDirection? IsDefaultSort { get; set; }
-
- ///
- /// If specified, virtualized grids will use this template to render cells whose data has not yet been loaded.
- ///
- [Parameter] public RenderFragment? PlaceholderTemplate { get; set; }
-
- ///
- /// Gets a reference to the enclosing .
- ///
- public BitQuickGrid Grid => InternalGridContext.Grid;
-
- ///
- /// Overridden by derived components to provide rendering logic for the column's cells.
- ///
- /// The current .
- /// The data for the row being rendered.
- protected internal abstract void CellContent(RenderTreeBuilder builder, TGridItem item);
-
- ///
- /// Gets or sets a that will be rendered for this column's header cell.
- /// This allows derived components to change the header output. However, derived components are then
- /// responsible for using within that new output if they want to continue
- /// respecting that option.
- ///
- protected internal RenderFragment HeaderContent { get; protected set; }
-
- ///
- /// Get a value indicating whether this column should act as sortable if no value was set for the
- /// parameter. The default behavior is not to be
- /// sortable unless is true.
- ///
- /// Derived components may override this to implement alternative default sortability rules.
- ///
- /// True if the column should be sortable by default, otherwise false.
- protected virtual bool IsSortableByDefault() => false;
-
- ///
- /// Constructs an instance of .
- ///
- public BitQuickGridColumnBase()
- {
- HeaderContent = RenderDefaultHeaderContent;
- }
-
- private string SortButtonLabel()
- => string.IsNullOrEmpty(Title) ? "Sort" : $"Sort by {Title}";
-
- private string ColumnOptionsLabel()
- => string.IsNullOrEmpty(Title) ? "Column options" : $"Column options for {Title}";
-
- private void RenderDefaultHeaderContent(RenderTreeBuilder __builder)
- {
- @if (HeaderTemplate is not null)
- {
- @HeaderTemplate(this)
- }
- else
- {
- @if (ColumnOptions is not null && Align != BitQuickGridAlign.Right)
- {
- Grid.ShowColumnOptions(this))">
- }
-
- if (Sortable.HasValue ? Sortable.Value : IsSortableByDefault())
- {
- Grid.SortByColumnAsync(this))">
- @Title
-
-
- }
- else
- {
-
- }
-
- @if (ColumnOptions is not null && Align == BitQuickGridAlign.Right)
- {
- Grid.ShowColumnOptions(this))">
- }
- }
- }
-
- internal void RenderPlaceholderContent(RenderTreeBuilder __builder, PlaceholderContext placeholderContext)
- {
- // Blank if no placeholder template was supplied, as it's enough to style with CSS by default
- if (PlaceholderTemplate is not null)
- {
- @PlaceholderTemplate(placeholderContext)
- }
- }
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridColumnBase.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridColumnBase.razor.cs
deleted file mode 100644
index d0dc409a5f..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridColumnBase.razor.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// An abstract base class for columns in a .
-///
-/// The type of data represented by each row in the grid.
-public abstract partial class BitQuickGridColumnBase
-{
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridPropertyColumn.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridPropertyColumn.cs
deleted file mode 100644
index 7687d475da..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridPropertyColumn.cs
+++ /dev/null
@@ -1,129 +0,0 @@
-using System.Linq.Expressions;
-
-namespace Bit.BlazorUI;
-
-///
-/// Represents a column whose cells display a single value.
-///
-/// The type of data represented by each row in the grid.
-/// The type of the value being displayed in the column's cells.
-public class BitQuickGridPropertyColumn : BitQuickGridColumnBase, IBitQuickGridSortBuilderColumn
-{
- private Expression>? _lastAssignedProperty;
- private string? _lastAssignedFormat;
- private bool _titleWasExplicitlySet;
- private Func? _cellTextFunc;
- private BitQuickGridSort? _sortBuilder;
-
- ///
- /// Defines the value to be displayed in this column's cells.
- ///
- [Parameter, EditorRequired] public Expression> Property { get; set; } = default!;
-
- ///
- /// Optionally specifies a format string for the value.
- ///
- /// Using this requires the type to implement .
- ///
- [Parameter] public string? Format { get; set; }
-
- BitQuickGridSort? IBitQuickGridSortBuilderColumn.SortBuilder => _sortBuilder;
-
-
- ///
- public override Task SetParametersAsync(ParameterView parameters)
- {
- // Track whether Title was supplied explicitly by the consumer in *this* render's ParameterView
- // rather than inferring intent from value equality (which can't tell an auto-derived header apart
- // from an explicit one matching the member name). Recompute it every render instead of latching:
- // if a consumer later removes Title, explicitness must drop back to false so the auto-generated
- // header can return.
- _titleWasExplicitlySet = parameters.TryGetValue(nameof(Title), out _);
-
- // When the consumer stops passing Title, the incoming ParameterView no longer contains it, so
- // base.SetParametersAsync leaves the previously assigned (explicit) value in place. Clear it up
- // front so a removed Title can't linger as a stale header; OnParametersSet then re-derives the
- // auto-generated title from the Property when possible.
- if (!_titleWasExplicitlySet)
- {
- Title = null;
- }
-
- return base.SetParametersAsync(parameters);
- }
-
-
- ///
- protected override void OnParametersSet()
- {
- // We have to do a bit of pre-processing on the lambda expression. Only do that if the Property
- // or the Format has changed, so a Format-only change still rebuilds the cell formatter.
- if (_lastAssignedProperty != Property || _lastAssignedFormat != Format)
- {
- var compiledPropertyExpression = Property.Compile();
- Func cellTextFunc;
-
- if (Format.HasValue())
- {
- // For a nullable value type (e.g. int?, DateTime?) Nullable itself does not implement
- // IFormattable, but its underlying type does and a boxed non-null value formats correctly.
- // Check the underlying type so Format is allowed on nullable columns too.
- var formattableType = Nullable.GetUnderlyingType(typeof(TProp)) ?? typeof(TProp);
- if (typeof(IFormattable).IsAssignableFrom(formattableType))
- {
- cellTextFunc = item => ((IFormattable?)compiledPropertyExpression!(item))?.ToString(Format, null);
- }
- else
- {
- throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
- }
- }
- else
- {
- cellTextFunc = item => compiledPropertyExpression!(item)?.ToString();
- }
-
- _cellTextFunc = cellTextFunc;
- _sortBuilder = BitQuickGridSort.ByAscending(Property);
-
- // Only record the assignments after the formatter has been built and validated, so a failed
- // Format/TProp validation above doesn't suppress a retry on the next parameters set (which
- // would leave _cellTextFunc in a stale or null state).
- _lastAssignedProperty = Property;
- _lastAssignedFormat = Format;
- }
-
- if (_titleWasExplicitlySet)
- {
- // The consumer supplied Title this render; base.SetParametersAsync already applied it, so
- // there is nothing more to do.
- }
- else if (Property.Body is MemberExpression memberExpression)
- {
- // No explicit Title this render, so derive the header from the member name. Recomputed every
- // render, this also lets the auto-generated header follow Property changes and reappear after
- // a previously explicit Title is removed.
- Title = memberExpression.Member.Name;
- }
- // else: Property is a method/cast expression (no member name to derive a header from) and no
- // explicit Title was supplied. SetParametersAsync already cleared any stale value, so the column
- // simply has no header.
- }
-
- ///
- protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item)
- => builder.AddContent(0, _cellTextFunc!(item));
-}
-
-///
-/// Backward-compatible alias for , kept so existing
-/// Razor markup that referenced the old BitDataGridPropertyColumn<TGridItem, TProp> component keeps
-/// compiling. It inherits the renamed column, so it behaves identically while remaining discoverable
-/// alongside during the migration window.
-///
-/// The type of data represented by each row in the grid.
-/// The type of the value being displayed in the column's cells.
-[Obsolete("BitDataGridPropertyColumn has been renamed to BitQuickGridPropertyColumn. Use BitQuickGridPropertyColumn instead.")]
-public class BitDataGridPropertyColumn : BitQuickGridPropertyColumn
-{
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridSort.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridSort.cs
deleted file mode 100644
index 2bd193bf60..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridSort.cs
+++ /dev/null
@@ -1,220 +0,0 @@
-using System.Linq.Expressions;
-
-namespace Bit.BlazorUI;
-
-///
-/// Represents a sort order specification used within .
-///
-/// The type of data represented by each row in the grid.
-public class BitQuickGridSort
-{
- private const string ExpressionNotRepresentableMessage = "The supplied expression can't be represented as a property name for sorting. Only simple member expressions, such as @(x => x.SomeProperty), can be converted to property names.";
-
- private Func, bool, IOrderedQueryable> _first;
- private List, bool, IOrderedQueryable>>? _then;
-
- private (LambdaExpression, bool) _firstExpression;
- private List<(LambdaExpression, bool)>? _thenExpressions;
-
- private IReadOnlyCollection<(string PropertyName, BitQuickGridSortDirection Direction)>? _cachedPropertyListAscending;
- private IReadOnlyCollection<(string PropertyName, BitQuickGridSortDirection Direction)>? _cachedPropertyListDescending;
-
- internal BitQuickGridSort(Func, bool, IOrderedQueryable> first, (LambdaExpression, bool) firstExpression)
- {
- _first = first;
- _firstExpression = firstExpression;
- _then = default;
- _thenExpressions = default;
- }
-
- ///
- /// Produces a instance that sorts according to the specified , ascending.
- ///
- /// The type of the expression's value.
- /// An expression defining how a set of instances are to be sorted.
- /// A instance representing the specified sorting rule.
- public static BitQuickGridSort ByAscending(Expression> expression)
- => new BitQuickGridSort((queryable, asc) => asc ? queryable.OrderBy(expression) : queryable.OrderByDescending(expression),
- (expression, true));
-
- ///
- /// Produces a instance that sorts according to the specified , descending.
- ///
- /// The type of the expression's value.
- /// An expression defining how a set of instances are to be sorted.
- /// A instance representing the specified sorting rule.
- public static BitQuickGridSort ByDescending(Expression> expression)
- => new BitQuickGridSort((queryable, asc) => asc ? queryable.OrderByDescending(expression) : queryable.OrderBy(expression),
- (expression, false));
-
- ///
- /// Updates a instance by appending a further sorting rule.
- ///
- /// The type of the expression's value.
- /// An expression defining how a set of instances are to be sorted.
- /// A instance representing the specified sorting rule.
- public BitQuickGridSort ThenAscending(Expression> expression)
- {
- _then ??= new();
- _thenExpressions ??= new();
- _then.Add((queryable, asc) => asc ? queryable.ThenBy(expression) : queryable.ThenByDescending(expression));
- _thenExpressions.Add((expression, true));
- _cachedPropertyListAscending = null;
- _cachedPropertyListDescending = null;
- return this;
- }
-
- ///
- /// Updates a instance by appending a further sorting rule.
- ///
- /// The type of the expression's value.
- /// An expression defining how a set of instances are to be sorted.
- /// A instance representing the specified sorting rule.
- public BitQuickGridSort ThenDescending(Expression> expression)
- {
- _then ??= new();
- _thenExpressions ??= new();
- _then.Add((queryable, asc) => asc ? queryable.ThenByDescending(expression) : queryable.ThenBy(expression));
- _thenExpressions.Add((expression, false));
- _cachedPropertyListAscending = null;
- _cachedPropertyListDescending = null;
- return this;
- }
-
- internal IOrderedQueryable Apply(IQueryable queryable, bool ascending)
- {
- var orderedQueryable = _first(queryable, ascending);
-
- if (_then is not null)
- {
- foreach (var clause in _then)
- {
- orderedQueryable = clause(orderedQueryable, ascending);
- }
- }
-
- return orderedQueryable;
- }
-
- internal IReadOnlyCollection<(string PropertyName, BitQuickGridSortDirection Direction)> ToPropertyList(bool ascending)
- {
- if (ascending)
- {
- _cachedPropertyListAscending ??= BuildPropertyList(ascending: true);
- return _cachedPropertyListAscending;
- }
- else
- {
- _cachedPropertyListDescending ??= BuildPropertyList(ascending: false);
- return _cachedPropertyListDescending;
- }
- }
-
- private IReadOnlyCollection<(string PropertyName, BitQuickGridSortDirection Direction)> BuildPropertyList(bool ascending)
- {
- var result = new List<(string, BitQuickGridSortDirection)>();
- result.Add((ToPropertyName(_firstExpression.Item1), (_firstExpression.Item2 ^ ascending) ? BitQuickGridSortDirection.Descending : BitQuickGridSortDirection.Ascending));
-
- if (_thenExpressions is not null)
- {
- foreach (var (thenLambda, thenAscending) in _thenExpressions)
- {
- result.Add((ToPropertyName(thenLambda), (thenAscending ^ ascending) ? BitQuickGridSortDirection.Descending : BitQuickGridSortDirection.Ascending));
- }
- }
-
- return result;
- }
-
- // Not sure we really want this level of complexity, but it converts expressions like @(c => c.Medals.Gold) to "Medals.Gold"
- private static string ToPropertyName(LambdaExpression expression)
- {
- var body = expression.Body as MemberExpression;
- if (body is null)
- {
- throw new ArgumentException(ExpressionNotRepresentableMessage);
- }
-
- // Handles cases like @(x => x.Name)
- if (body.Expression is ParameterExpression)
- {
- return body.Member.Name;
- }
-
- // First work out the length of the string we'll need, so that we can use string.Create
- var length = body.Member.Name.Length;
- var node = body;
- while (node.Expression is not null)
- {
- if (node.Expression is MemberExpression parentMember)
- {
- length += parentMember.Member.Name.Length + 1;
- node = parentMember;
- }
- else if (node.Expression is ParameterExpression)
- {
- break;
- }
- else
- {
- throw new ArgumentException(ExpressionNotRepresentableMessage);
- }
- }
-
- // Now construct the string
- return string.Create(length, body, (chars, body) =>
- {
- var nextPos = chars.Length;
- while (body is not null)
- {
- nextPos -= body.Member.Name.Length;
- body.Member.Name.CopyTo(chars.Slice(nextPos));
- if (nextPos > 0)
- {
- chars[--nextPos] = '.';
- }
- body = (body.Expression as MemberExpression)!;
- }
- });
- }
-}
-
-///
-/// Backward-compatible alias for , kept so existing code that
-/// stored, returned or accepted the old BitDataGridSort<TGridItem> type keeps compiling. It
-/// inherits the renamed type (so instances are accepted anywhere a
-/// is expected) and its factory/chaining helpers return the old type to preserve the original API shape.
-///
-/// The type of data represented by each row in the grid.
-[Obsolete("BitDataGridSort has been renamed to BitQuickGridSort. Use BitQuickGridSort instead.")]
-public class BitDataGridSort : BitQuickGridSort
-{
- internal BitDataGridSort(Func, bool, IOrderedQueryable> first, (LambdaExpression, bool) firstExpression)
- : base(first, firstExpression)
- {
- }
-
- ///
- public static new BitDataGridSort ByAscending(Expression> expression)
- => new BitDataGridSort((queryable, asc) => asc ? queryable.OrderBy(expression) : queryable.OrderByDescending(expression),
- (expression, true));
-
- ///
- public static new BitDataGridSort ByDescending(Expression> expression)
- => new BitDataGridSort((queryable, asc) => asc ? queryable.OrderByDescending(expression) : queryable.OrderBy(expression),
- (expression, false));
-
- ///
- public new BitDataGridSort ThenAscending(Expression> expression)
- {
- base.ThenAscending(expression);
- return this;
- }
-
- ///
- public new BitDataGridSort ThenDescending(Expression> expression)
- {
- base.ThenDescending(expression);
- return this;
- }
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridTemplateColumn.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridTemplateColumn.cs
deleted file mode 100644
index 1801d70687..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/BitQuickGridTemplateColumn.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// Represents a column whose cells render a supplied template.
-///
-/// The type of data represented by each row in the grid.
-public class BitQuickGridTemplateColumn : BitQuickGridColumnBase, IBitQuickGridSortBuilderColumn
-{
- private readonly static RenderFragment EmptyChildContent = _ => builder => { };
-
- ///
- /// Specifies the content to be rendered for each row in the table.
- ///
- [Parameter] public RenderFragment ChildContent { get; set; } = EmptyChildContent;
-
- ///
- /// Optionally specifies sorting rules for this column.
- ///
- [Parameter] public BitQuickGridSort? SortBy { get; set; }
-
- BitQuickGridSort? IBitQuickGridSortBuilderColumn.SortBuilder => SortBy;
-
- ///
- protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item)
- => builder.AddContent(0, ChildContent(item));
-
- ///
- protected override bool IsSortableByDefault()
- => SortBy is not null;
-}
-
-///
-/// Backward-compatible alias for , kept so existing
-/// Razor markup that referenced the old BitDataGridTemplateColumn<TGridItem> component keeps
-/// compiling. It inherits the renamed column, so it behaves identically while remaining discoverable
-/// alongside during the migration window.
-///
-/// The type of data represented by each row in the grid.
-[Obsolete("BitDataGridTemplateColumn has been renamed to BitQuickGridTemplateColumn. Use BitQuickGridTemplateColumn instead.")]
-public class BitDataGridTemplateColumn : BitQuickGridTemplateColumn
-{
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/IBitQuickGridSortBuilderColumn.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/IBitQuickGridSortBuilderColumn.cs
deleted file mode 100644
index fee3035a48..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Columns/IBitQuickGridSortBuilderColumn.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// An interface that, if implemented by a subclass, allows a
-/// to understand the sorting rules associated with that column.
-///
-/// If a subclass does not implement this, that column can still be marked as sortable and can
-/// be the current sort column, but its sorting logic cannot be applied to the data queries automatically. The developer would be
-/// responsible for implementing that sorting logic separately inside their .
-///
-/// The type of data represented by each row in the grid.
-public interface IBitQuickGridSortBuilderColumn
-{
- ///
- /// Gets the sorting rules associated with the column.
- ///
- public BitQuickGridSort? SortBuilder { get; }
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/AsyncQueryExecutorSupplier.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/AsyncQueryExecutorSupplier.cs
deleted file mode 100644
index 46afc5dfd1..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/AsyncQueryExecutorSupplier.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using System.Collections.Concurrent;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace Bit.BlazorUI;
-
-internal static class AsyncQueryExecutorSupplier
-{
- // The primary goal with this is to ensure that:
- // - If you're using EF Core, then we resolve queries efficiently using its ToXyzAsync async extensions and don't
- // just fall back on the synchronous IQueryable ToXyz calls
- // - ... but without BitQuickGrid referencing Microsoft.EntityFramework directly. That's because it would bring in
- // heavy dependencies you may not be using (and relying on trimming isn't enough, as it's still desirable to have
- // heavy unused dependencies for Blazor Server).
- //
- // As a side-effect, we have an abstraction IAsyncQueryExecutor that developers could use to plug in their own
- // mechanism for resolving async queries from other data sources than EF. It's not really a major goal to make this
- // adapter generally useful beyond EF, but fine if people do have their own uses for it.
-
- private static readonly ConcurrentDictionary IsEntityFrameworkProviderTypeCache = new();
-
- [SuppressMessage("Trimming", "IL2111:Method with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method.", Justification = "")]
- public static IAsyncQueryExecutor? GetAsyncQueryExecutor(IServiceProvider services, IQueryable? queryable)
- {
- if (queryable is not null)
- {
- // Inspect every registered executor, not just the first one resolved: a registered executor
- // that does not support this queryable must not shadow another that does, nor suppress the
- // EF misconfiguration warning below. Keep scanning and return the *last* supported executor
- // so later, more specific registrations override earlier generic ones - mirroring how DI
- // resolves a single service (last registration wins).
- IAsyncQueryExecutor? selected = null;
- foreach (var executor in services.GetServices())
- {
- if (executor.IsSupported(queryable))
- {
- selected = executor;
- }
- }
-
- if (selected is not null)
- {
- return selected;
- }
-
- // No registered executor supports this queryable. It's useful to detect if the developer is
- // unaware that they should register an IAsyncQueryExecutor, otherwise they will likely never
- // notice and simply deploy an inefficient app that blocks threads on each query.
- var providerType = queryable.Provider?.GetType();
- if (providerType is not null && IsEntityFrameworkProviderTypeCache.GetOrAdd(providerType, IsEntityFrameworkProviderType))
- {
- throw new InvalidOperationException(
- $"The supplied {nameof(IQueryable)} is provided by Entity Framework. To query it efficiently without blocking threads, " +
- $"register an implementation of {nameof(IAsyncQueryExecutor)} in your service collection that wraps EF Core's async query APIs " +
- $"(for example ToArrayAsync/CountAsync) and reports IsSupported(queryable) == true for EF queryables. " +
- $"Alternatively, supply non-EF data via the Items or ItemsProvider parameters.");
- }
- }
-
- return null;
- }
-
- // We have to do this via reflection because the whole point is to avoid any static dependency on EF unless you
- // reference the adapter. Trimming won't cause us any problems because this is only a way of detecting misconfiguration
- // so it's sufficient if it can detect the misconfiguration in development.
- private static bool IsEntityFrameworkProviderType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type queryableProviderType)
- => queryableProviderType.GetInterfaces().Any(x => string.Equals(x.FullName, "Microsoft.EntityFrameworkCore.Query.IAsyncQueryProvider")) == true;
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/BitQuickGridColumnsCollectedNotifier.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/BitQuickGridColumnsCollectedNotifier.cs
deleted file mode 100644
index 8f31d94e1c..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/BitQuickGridColumnsCollectedNotifier.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-namespace Bit.BlazorUI;
-
-// One awkwardness of the way BitQuickGrid collects its list of child columns is that, during OnParametersSetAsync,
-// it only knows about the set of columns that were present on the *previous* render. If it's going to trigger a
-// data load during OnParametersSetAsync, that operation can't depend on the current set of columns as it might
-// have changed, or might even still be empty (i.e., on the first render).
-//
-// Ways this could be resolved:
-//
-// - In the future, we could implement the long-wanted feature of being able to query the contents of a RenderFragment
-// separately from rendering. Then the whole trick of collection-during-rendering would not be needed.
-// - Or, we could factor out most of BitQuickGrid's internals into some new component BitQuickGridCore. The parent component,
-// BitQuickGrid, would then only be responsible for collecting columns followed by rendering BitQuickGridCore. So each time
-// BitQuickGridCore renders, we'd already have the latest set of columns
-// - Drawback: since BitQuickGrid has public API, it's much messier to have to forward all of that to some new child type.
-// - However, this is arguably the most correct solution in general (at least until option 1 above is implemented)
-// - Or, we could decide it's enough to fix this on the first render (since that's the only time we're going to guarantee
-// to apply a default sort order), and then as a special case put in some extra component in the render flow that raises
-// an event once the columns are first collected.
-// - This is relatively simple and non-disruptive, though it doesn't cover cases where queries need to be delayed until
-// after a dynamically-added column is added
-//
-// The final option is what's implemented here. We send the notification via EventCallbackSubscribable so that the async
-// operation and re-rendering follows normal semantics without us having to call StateHasChanged or think about exceptions.
-
-///
-/// For internal use only. Do not use.
-///
-/// For internal use only. Do not use.
-public class BitQuickGridColumnsCollectedNotifier : IComponent
-{
- private bool _isFirstRender = true;
-
- [CascadingParameter] internal InternalGridContext InternalGridContext { get; set; } = default!;
-
- public void Attach(RenderHandle renderHandle)
- {
- // This component never renders, so we can ignore the renderHandle
- }
-
- public Task SetParametersAsync(ParameterView parameters)
- {
- if (_isFirstRender)
- {
- _isFirstRender = false;
- parameters.SetParameterProperties(this);
- return InternalGridContext.ColumnsFirstCollected.InvokeCallbacksAsync(null);
- }
- else
- {
- return Task.CompletedTask;
- }
- }
-}
-
-///
-/// For internal use only. Do not use.
-/// Backward-compatible alias for , kept so
-/// existing code referencing the old BitDataGridColumnsCollectedNotifier<TGridItem> type keeps
-/// compiling. It inherits the renamed notifier, preserving the same behavior.
-///
-/// For internal use only. Do not use.
-[Obsolete("BitDataGridColumnsCollectedNotifier has been renamed to BitQuickGridColumnsCollectedNotifier. Use BitQuickGridColumnsCollectedNotifier instead.")]
-public class BitDataGridColumnsCollectedNotifier : BitQuickGridColumnsCollectedNotifier
-{
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/BitQuickGridDefer.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/BitQuickGridDefer.cs
deleted file mode 100644
index db5deafc63..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/BitQuickGridDefer.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-namespace Bit.BlazorUI;
-
-// This is used by BitQuickGrid to move its body rendering to the end of the render queue so we can collect
-// the list of child columns first. It has to be public only because it's used from .razor logic.
-
-///
-/// For internal use only. Do not use.
-///
-public class BitQuickGridDefer : ComponentBase
-{
- ///
- /// For internal use only. Do not use.
- ///
- [Parameter] public RenderFragment? ChildContent { get; set; }
-
- ///
- /// For internal use only. Do not use.
- ///
- protected override void BuildRenderTree(RenderTreeBuilder builder)
- {
- builder.AddContent(0, ChildContent);
- }
-}
-
-///
-/// Backward-compatible alias for , kept so existing code that referenced
-/// the old BitDataGridDefer type keeps compiling. It inherits the renamed type, so it behaves
-/// identically during the migration window.
-///
-[Obsolete("BitDataGridDefer has been renamed to BitQuickGridDefer. Use BitQuickGridDefer instead.")]
-public class BitDataGridDefer : BitQuickGridDefer
-{
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/EventCallbackSubscribable.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/EventCallbackSubscribable.cs
deleted file mode 100644
index 3e54e76c25..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/EventCallbackSubscribable.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// Represents an event that you may subscribe to. This differs from normal C# events in that the handlers
-/// are EventCallback , and so may have async behaviors and cause component re-rendering
-/// while retaining error flow.
-///
-/// A type for the eventargs.
-internal class EventCallbackSubscribable
-{
- private readonly Dictionary, EventCallback> _callbacks = new();
-
- ///
- /// Invokes all the registered callbacks sequentially, in an undefined order.
- ///
- public async Task InvokeCallbacksAsync(T eventArg)
- {
- foreach (var callback in _callbacks.Values)
- {
- await callback.InvokeAsync(eventArg);
- }
- }
-
- // Don't call this directly - it gets called by EventCallbackSubscription
- public void Subscribe(EventCallbackSubscriber owner, EventCallback callback)
- => _callbacks.Add(owner, callback);
-
- // Don't call this directly - it gets called by EventCallbackSubscription
- public void Unsubscribe(EventCallbackSubscriber owner)
- => _callbacks.Remove(owner);
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/EventCallbackSubscriber.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/EventCallbackSubscriber.cs
deleted file mode 100644
index 7dfe8827d1..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/EventCallbackSubscriber.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// Represents a subscriber that may be subscribe to an .
-/// The subscription can move between instances over time,
-/// and automatically unsubscribes from earlier instances
-/// whenever it moves to a new one.
-///
-internal class EventCallbackSubscriber : IDisposable
-{
- private readonly EventCallback _handler;
- private EventCallbackSubscribable? _existingSubscription;
-
- public EventCallbackSubscriber(EventCallback handler)
- {
- _handler = handler;
- }
-
- ///
- /// Creates a subscription on the , or moves any existing subscription to it
- /// by first unsubscribing from the previous .
- ///
- /// If the supplied is null, no new subscription will be created, but any
- /// existing one will still be unsubscribed.
- ///
- ///
- public void SubscribeOrMove(EventCallbackSubscribable? subscribable)
- {
- if (subscribable != _existingSubscription)
- {
- _existingSubscription?.Unsubscribe(this);
- subscribable?.Subscribe(this, _handler);
- _existingSubscription = subscribable;
- }
- }
-
- public void Dispose()
- {
- _existingSubscription?.Unsubscribe(this);
- }
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/EventHandlers.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/EventHandlers.cs
deleted file mode 100644
index 22482feff7..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/EventHandlers.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// Configures event handlers for .
-///
-[EventHandler("onclosecolumnoptions", typeof(EventArgs), enableStopPropagation: true, enablePreventDefault: true)]
-public static class EventHandlers
-{
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/IAsyncQueryExecutor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/IAsyncQueryExecutor.cs
deleted file mode 100644
index 62cc08de3e..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/IAsyncQueryExecutor.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// Provides methods for asynchronous evaluation of queries against an .
-///
-public interface IAsyncQueryExecutor
-{
- ///
- /// Determines whether the is supported by this type.
- ///
- /// The data type.
- /// An instance.
- /// True if this instance can perform asynchronous queries for the supplied , otherwise false.
- bool IsSupported(IQueryable queryable);
-
- ///
- /// Asynchronously counts the items in the , if supported.
- ///
- /// The data type.
- /// An instance.
- /// The number of items in . .
- Task CountAsync(IQueryable queryable);
-
- ///
- /// Asynchronously materializes the as an array, if supported.
- ///
- /// The data type.
- /// An instance.
- /// The items in the . .
- Task ToArrayAsync(IQueryable queryable);
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/InternalGridContext.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/InternalGridContext.cs
deleted file mode 100644
index 1c66112664..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Infrastructure/InternalGridContext.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Bit.BlazorUI;
-
-// The grid cascades this so that descendant columns can talk back to it. It's an internal type
-// so that it doesn't show up by mistake in unrelated components.
-internal class InternalGridContext
-{
- public BitQuickGrid Grid { get; }
- public EventCallbackSubscribable ColumnsFirstCollected { get; } = new();
-
- public InternalGridContext(BitQuickGrid grid)
- {
- Grid = grid;
- }
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/ItemsProvider/BitQuickGridItemsProvider.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/ItemsProvider/BitQuickGridItemsProvider.cs
deleted file mode 100644
index 6985a54e8a..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/ItemsProvider/BitQuickGridItemsProvider.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// A callback that provides data for a .
-///
-/// The type of data represented by each row in the grid.
-/// Parameters describing the data being requested.
-/// A (specifically ValueTask<BitQuickGridItemsProviderResult<TGridItem>> ) whose result is a that gives the data to be displayed.
-public delegate ValueTask> BitQuickGridItemsProvider(BitQuickGridItemsProviderRequest request);
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/ItemsProvider/BitQuickGridItemsProviderRequest.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/ItemsProvider/BitQuickGridItemsProviderRequest.cs
deleted file mode 100644
index 471b66baf5..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/ItemsProvider/BitQuickGridItemsProviderRequest.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// Parameters for data to be supplied by a 's .
-///
-/// The type of data represented by each row in the grid.
-public struct BitQuickGridItemsProviderRequest
-{
- ///
- /// The zero-based index of the first item to be supplied.
- ///
- public int StartIndex { get; }
-
- ///
- /// If set, the maximum number of items to be supplied. If not set, the maximum number is unlimited.
- ///
- public int? Count { get; }
-
- ///
- /// Specifies which column represents the sort order.
- ///
- /// Rather than inferring the sort rules manually, you should normally call either
- /// or , since they also account for and automatically.
- ///
- public BitQuickGridColumnBase? SortByColumn { get; }
-
- ///
- /// Specifies the current sort direction.
- ///
- /// Rather than inferring the sort rules manually, you should normally call either
- /// or , since they also account for and automatically.
- ///
- public bool SortByAscending { get; }
-
- ///
- /// A token that indicates if the request should be cancelled.
- ///
- public CancellationToken CancellationToken { get; }
-
- internal BitQuickGridItemsProviderRequest(
- int startIndex, int? count, BitQuickGridColumnBase? sortByColumn, bool sortByAscending,
- CancellationToken cancellationToken)
- {
- StartIndex = startIndex;
- Count = count;
- SortByColumn = sortByColumn;
- SortByAscending = sortByAscending;
- CancellationToken = cancellationToken;
- }
-
- ///
- /// Applies the request's sorting rules to the supplied .
- ///
- /// Note that this only works if the current implements
- /// and exposes a non-null sort builder. If the column does not implement that interface, or implements it
- /// but its sort builder is null (as with ), it will throw.
- ///
- /// An .
- /// A new representing the with sorting rules applied.
- public IQueryable ApplySorting(IQueryable source) => SortByColumn switch
- {
- // A sort-builder column with a null SortBuilder cannot apply its sort; treat it as unsupported
- // (like a non-sort-builder column) rather than silently returning the unsorted source, which
- // would hide an active sort on e.g. BitQuickGridTemplateColumn.
- IBitQuickGridSortBuilderColumn { SortBuilder: { } sortBuilder } => sortBuilder.Apply(source, SortByAscending),
- null => source,
- _ => throw new NotSupportedException(ColumnNotSortableMessage(SortByColumn)),
- };
-
- ///
- /// Produces a collection of (property name, direction) pairs representing the sorting rules.
- ///
- /// Note that this only works if the current implements
- /// and exposes a non-null sort builder. If the column does not implement that interface, or implements it
- /// but its sort builder is null (as with ), it will throw.
- ///
- /// A collection of (property name, direction) pairs representing the sorting rules
- public IReadOnlyCollection<(string PropertyName, BitQuickGridSortDirection Direction)> GetSortByProperties() => SortByColumn switch
- {
- // Mirror ApplySorting: a null SortBuilder on a sort-builder column is unsupported rather than
- // an empty (no-op) sort, so the caller isn't misled into thinking there is no active sort.
- IBitQuickGridSortBuilderColumn { SortBuilder: { } sortBuilder } => sortBuilder.ToPropertyList(SortByAscending),
- null => Array.Empty<(string, BitQuickGridSortDirection)>(),
- _ => throw new NotSupportedException(ColumnNotSortableMessage(SortByColumn)),
- };
-
- private static string ColumnNotSortableMessage(BitQuickGridColumnBase col)
- => col is IBitQuickGridSortBuilderColumn
- ? $"The current sort column '{col.GetType().FullName}' implements {nameof(IBitQuickGridSortBuilderColumn)} but its {nameof(IBitQuickGridSortBuilderColumn.SortBuilder)} is null, so its sorting rules cannot be applied automatically."
- : $"The current sort column is of type '{col.GetType().FullName}', which does not implement {nameof(IBitQuickGridSortBuilderColumn)}, so its sorting rules cannot be applied automatically.";
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/ItemsProvider/BitQuickGridItemsProviderResult.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/ItemsProvider/BitQuickGridItemsProviderResult.cs
deleted file mode 100644
index 1a778dce57..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/ItemsProvider/BitQuickGridItemsProviderResult.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// Holds data being supplied to a 's .
-///
-/// The type of data represented by each row in the grid.
-public struct BitQuickGridItemsProviderResult
-{
- ///
- /// The items being supplied.
- ///
- public ICollection Items { get; set; }
-
- ///
- /// The total number of items that may be displayed in the grid. This normally means the total number of items in the
- /// underlying data source after applying any filtering that is in effect.
- ///
- /// If the grid is paginated, this should include all pages. If the grid is virtualized, this should include the entire scroll range.
- ///
- public int TotalItemCount { get; set; }
-
- ///
- /// Constructs an instance of .
- ///
- /// The items being supplied.
- /// The total number of items that exist. See for details.
- public BitQuickGridItemsProviderResult(ICollection items, int totalItemCount)
- {
- Items = items;
- TotalItemCount = totalItemCount;
- }
-}
-
-///
-/// Provides convenience methods for constructing instances.
-///
-public static class BitQuickGridItemsProviderResult
-{
- // This is just to provide generic type inference, so you don't have to specify TGridItem yet again.
-
- ///
- /// Supplies an instance of .
- ///
- /// The type of data represented by each row in the grid.
- /// The items being supplied.
- /// The total number of items that exist. See for details.
- /// An instance of .
- public static BitQuickGridItemsProviderResult From(ICollection items, int totalItemCount) => new(items, totalItemCount);
-}
-
-///
-/// Backward-compatible alias for , kept so code
-/// that constructed the old BitDataGridItemsProviderResult<TGridItem> keeps compiling. An
-/// implicit conversion lets it be returned where a
-/// is expected.
-///
-/// The type of data represented by each row in the grid.
-[Obsolete("BitDataGridItemsProviderResult has been renamed to BitQuickGridItemsProviderResult. Use BitQuickGridItemsProviderResult instead.")]
-public struct BitDataGridItemsProviderResult
-{
- /// The items being supplied.
- public ICollection Items { get; set; }
-
- /// The total number of items that may be displayed in the grid.
- public int TotalItemCount { get; set; }
-
- /// Constructs an instance of .
- /// The items being supplied.
- /// The total number of items that exist.
- public BitDataGridItemsProviderResult(ICollection items, int totalItemCount)
- {
- Items = items;
- TotalItemCount = totalItemCount;
- }
-
- /// Implicitly converts to the current type.
- public static implicit operator BitQuickGridItemsProviderResult(BitDataGridItemsProviderResult result)
- => new(result.Items, result.TotalItemCount);
-}
-
-///
-/// Backward-compatible alias for , kept so existing calls to
-/// the old BitDataGridItemsProviderResult.From(...) helper keep compiling. The helper forwards to
-/// , which the grid's ItemsProvider now expects.
-///
-[Obsolete("BitDataGridItemsProviderResult has been renamed to BitQuickGridItemsProviderResult. Use BitQuickGridItemsProviderResult instead.")]
-public static class BitDataGridItemsProviderResult
-{
- ///
- // Returns the old alias type so callers still assigning to BitDataGridItemsProviderResult
- // keep compiling; the implicit conversion above bridges to BitQuickGridItemsProviderResult
- // wherever the grid's ItemsProvider expects the renamed type.
- public static BitDataGridItemsProviderResult From(ICollection items, int totalItemCount) => new(items, totalItemCount);
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginationState.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginationState.cs
deleted file mode 100644
index 122ac7bb51..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginationState.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// Holds state to represent pagination in a .
-///
-public class BitQuickGridPaginationState
-{
- ///
- /// Gets the current zero-based page index. To set it, call .
- ///
- public int CurrentPageIndex { get; private set; }
-
- ///
- /// Gets or sets the number of items on each page.
- ///
- public int ItemsPerPage { get; set; } = 10;
-
- ///
- /// Gets the zero-based index of the last page, if known. The value will be null until is known.
- ///
- public int? LastPageIndex => (TotalItemCount - 1) / ItemsPerPage;
-
- ///
- /// Gets the total number of items across all pages, if known. The value will be null until an
- /// associated assigns a value after loading data.
- ///
- public int? TotalItemCount { get; private set; }
-
- ///
- /// An event that is raised when the total item count has changed.
- ///
- public event EventHandler? TotalItemCountChanged;
-
- internal EventCallbackSubscribable CurrentPageItemsChanged { get; } = new();
- internal EventCallbackSubscribable TotalItemCountChangedSubscribable { get; } = new();
-
- ///
- public override int GetHashCode()
- => HashCode.Combine(ItemsPerPage, CurrentPageIndex, TotalItemCount);
-
- ///
- /// Sets the current page index, and notifies any associated
- /// to fetch and render updated data.
- ///
- /// The new, zero-based page index.
- /// A representing the completion of the operation.
- public Task SetCurrentPageIndexAsync(int pageIndex)
- {
- CurrentPageIndex = pageIndex;
- return CurrentPageItemsChanged.InvokeCallbacksAsync(this);
- }
-
- // Can be internal because this only needs to be called by BitQuickGrid itself, not any custom pagination UI components.
- internal Task SetTotalItemCountAsync(int totalItemCount)
- {
- if (totalItemCount == TotalItemCount)
- {
- return Task.CompletedTask;
- }
-
- TotalItemCount = totalItemCount;
-
- if (CurrentPageIndex > 0 && CurrentPageIndex > LastPageIndex)
- {
- // If the number of items has reduced such that the current page index is no longer valid, move
- // automatically to the final valid page index and trigger a further data load.
- return SetCurrentPageIndexAsync(LastPageIndex.Value);
- }
- else
- {
- // Under normal circumstances, we just want any associated pagination UI to update
- TotalItemCountChanged?.Invoke(this, TotalItemCount);
- return TotalItemCountChangedSubscribable.InvokeCallbacksAsync(this);
- }
- }
-}
-
-///
-/// Backward-compatible alias for , kept so existing code that
-/// constructed the old BitDataGridPaginationState type keeps compiling. It inherits the renamed
-/// type, so it behaves identically and can still be assigned to a grid's pagination during the
-/// migration window.
-///
-[Obsolete("BitDataGridPaginationState has been renamed to BitQuickGridPaginationState. Use BitQuickGridPaginationState instead.")]
-public class BitDataGridPaginationState : BitQuickGridPaginationState
-{
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginator.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginator.razor
deleted file mode 100644
index 788a8e9f29..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginator.razor
+++ /dev/null
@@ -1,61 +0,0 @@
-@namespace Bit.BlazorUI
-
-
- @if (Value.TotalItemCount.HasValue)
- {
-
- @if (SummaryTemplate is not null)
- {
- @SummaryTemplate(Value)
- }
- else if (SummaryFormat is not null)
- {
- @SummaryFormat(Value)
- }
- else
- {
- @Value.TotalItemCount items
- }
-
-
-
-
-
- @if (TextTemplate is not null)
- {
- @TextTemplate(Value)
- }
- else if (TextFormat is not null)
- {
- @TextFormat(Value)
- }
- else
- {
- Page @(Value.CurrentPageIndex + 1) of @(Value.LastPageIndex + 1)
- }
-
-
-
-
- }
-
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginator.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginator.razor.cs
deleted file mode 100644
index ef8d07cb2c..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginator.razor.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-namespace Bit.BlazorUI;
-
-///
-/// A component that provides a user interface for .
-///
-public partial class BitQuickGridPaginator : IDisposable
-{
- private readonly EventCallbackSubscriber _totalItemCountChanged;
-
- ///
- /// The title of the go to first page button.
- ///
- [Parameter] public string GoToFirstButtonTitle { get; set; } = "Go to first page";
-
- ///
- /// The title of the go to previous page button.
- ///
- [Parameter] public string GoToPrevButtonTitle { get; set; } = "Go to previous page";
-
- ///
- /// The title of the go to next page button.
- ///
- [Parameter] public string GoToNextButtonTitle { get; set; } = "Go to next page";
-
- ///
- /// The title of the go to last page button.
- ///
- [Parameter] public string GoToLastButtonTitle { get; set; } = "Go to last page";
-
- ///
- /// Optionally supplies a format for rendering the page count summary.
- ///
- [Parameter] public Func? SummaryFormat { get; set; }
-
- ///
- /// Optionally supplies a template for rendering the page count summary.
- ///
- [Parameter] public RenderFragment? SummaryTemplate { get; set; }
-
- ///
- /// The optional custom format for the main text of the paginator in the middle of it.
- ///
- [Parameter] public Func? TextFormat { get; set; }
-
- ///
- /// The optional custom template for the main text of the paginator in the middle of it.
- ///
- [Parameter] public RenderFragment? TextTemplate { get; set; }
-
- ///
- /// Specifies the associated . This parameter is required.
- ///
- [Parameter, EditorRequired] public BitQuickGridPaginationState Value { get; set; } = default!;
-
- ///
- /// Constructs an instance of .
- ///
- public BitQuickGridPaginator()
- {
- // The "total item count" handler doesn't need to do anything except cause this component to
- // re-render. Invoking this EventCallback already routes through the paginator's
- // IHandleEvent.HandleEventAsync (the receiver is `this`), which re-renders the component on its
- // own, so the callback body is intentionally empty - calling StateHasChanged() here as well
- // would queue a second, redundant render.
- _totalItemCountChanged = new(EventCallback.Factory.Create(this, () => { }));
- }
-
- private Task GoFirstAsync() => GoToPageAsync(0);
- private Task GoPreviousAsync() => GoToPageAsync(Value.CurrentPageIndex - 1);
- private Task GoNextAsync() => GoToPageAsync(Value.CurrentPageIndex + 1);
- private Task GoLastAsync() => GoToPageAsync(Value.LastPageIndex.GetValueOrDefault(0));
-
- private bool CanGoBack => Value.CurrentPageIndex > 0;
- private bool CanGoForwards => Value.CurrentPageIndex < Value.LastPageIndex;
-
- private Task GoToPageAsync(int pageIndex)
- => Value.SetCurrentPageIndexAsync(pageIndex);
-
- ///
- protected override void OnParametersSet()
- => _totalItemCountChanged.SubscribeOrMove(Value.TotalItemCountChangedSubscribable);
-
- ///
- public void Dispose()
- => _totalItemCountChanged.Dispose();
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginator.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginator.scss
deleted file mode 100644
index 5d72b85b01..0000000000
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/QuickGrid/Pagination/BitQuickGridPaginator.scss
+++ /dev/null
@@ -1,59 +0,0 @@
-.bit-dgp {
- display: flex;
- flex-wrap: wrap;
- padding: 0.25rem;
- align-items: center;
-}
-
-.bit-dgp-txt {
- margin: 0 0.5rem;
- text-align: center;
-}
-
-.bit-dgp-nav {
- gap: 0.5rem;
- display: flex;
- align-items: center;
- margin-inline-start: auto;
-
- button {
- border: 0;
- width: 2rem;
- height: 2rem;
- background: none center center / 1rem no-repeat;
-
- &[disabled] {
- opacity: 0.4;
- }
-
- &:not([disabled]):hover {
- background-color: var(--bit-clr-bg-pri-hover);
- }
-
- &:not([disabled]):active {
- background-color: var(--bit-clr-bg-pri-active);
- }
- }
-}
-
-.bit-dgp-fst::before,
-.bit-dgp-lst::before {
- content: "\F371";
- font-style: normal;
- font-weight: normal;
- display: inline-block;
- font-family: 'Fabric MDL2 bit BlazorUI Extras';
-}
-
-.bit-dgp-prv::before,
-.bit-dgp-nex::before {
- content: "\E96F";
- font-style: normal;
- font-weight: normal;
- display: inline-block;
- font-family: 'Fabric MDL2 bit BlazorUI Extras';
-}
-
-.bit-dgp-nex, .bit-dgp-lst {
- transform: scaleX(-1);
-}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/extra-components.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/extra-components.scss
index da4dc51496..564e1a6a38 100644
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/extra-components.scss
+++ b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/extra-components.scss
@@ -5,8 +5,6 @@
@import "../Components/Flag/BitFlag.scss";
@import "../Components/FullCalendar/BitFullCalendar.scss";
@import "../Components/InfiniteScrolling/BitInfiniteScrolling.scss";
-@import "../Components/QuickGrid/BitQuickGrid.scss";
-@import "../Components/QuickGrid/Pagination/BitQuickGridPaginator.scss";
@import "../Components/Map/BitMap.scss";
@import "../Components/MarkdownEditor/BitMarkdownEditor.scss";
@import "../Components/MarkdownViewer/BitMarkdownViewer.scss";
diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridLegacy.scss b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridLegacy.scss
index 1109021ff7..b1e5bdcc3b 100644
--- a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridLegacy.scss
+++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/BitDataGridLegacy.scss
@@ -1,3 +1,5 @@
+@import "../../../Bit.BlazorUI/Styles/functions.scss";
+
.bit-qkg {
width: 100%;
--bit-qkg-col-gap: 1rem;
@@ -34,8 +36,8 @@
border: 1px solid;
position: absolute;
inset-inline-start: 0;
- border-color: var(--bit-clr-brd-pri);
- background-color: var(--bit-clr-bg-sec);
+ border-color: $clr-brd-pri;
+ background-color: $clr-bg-sec;
}
.bit-qkg-cob {
@@ -65,8 +67,8 @@
inset-block-end: 5px;
inset-block-start: 5px;
inset-inline-start: 0.5rem;
- border-color: var(--bit-clr-brd-pri);
- border-inline-start: 1px solid var(--bit-clr-brd-pri);
+ border-color: $clr-brd-pri;
+ border-inline-start: 1px solid $clr-brd-pri;
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridLegacyPaginator.scss b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridLegacyPaginator.scss
index 5d72b85b01..9e9731ad8d 100644
--- a/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridLegacyPaginator.scss
+++ b/src/BlazorUI/Bit.BlazorUI.Legacy/Components/DataGrid/Pagination/BitDataGridLegacyPaginator.scss
@@ -1,3 +1,5 @@
+@import "../../../../Bit.BlazorUI/Styles/functions.scss";
+
.bit-dgp {
display: flex;
flex-wrap: wrap;
@@ -27,11 +29,11 @@
}
&:not([disabled]):hover {
- background-color: var(--bit-clr-bg-pri-hover);
+ background-color: $clr-bg-pri-hover;
}
&:not([disabled]):active {
- background-color: var(--bit-clr-bg-pri-active);
+ background-color: $clr-bg-pri-active;
}
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ActionButton/BitActionButton.scss b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ActionButton/BitActionButton.scss
index 6230d430e6..a69720e736 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ActionButton/BitActionButton.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ActionButton/BitActionButton.scss
@@ -27,16 +27,16 @@
}
&:focus-visible {
- outline: none;
+ @include focus-ring(var(--bit-acb-clr-focus));
+
border-radius: $shp-border-radius;
- box-shadow: 0 0 0 spacing(0.25) var(--bit-acb-clr-hover);
}
&.bit-dis {
cursor: default;
- color: $clr-fg-dis;
pointer-events: none;
- --bit-acb-clr-ico: #{$clr-fg-dis};
+ color: var(--bit-acb-clr-dis-text);
+ --bit-acb-clr-ico: var(--bit-acb-clr-dis-text);
}
}
@@ -137,48 +137,64 @@
--bit-acb-clr-ico: #{$clr-pri};
--bit-acb-clr-hover: #{$clr-pri-hover};
--bit-acb-clr-active: #{$clr-pri-active};
+ --bit-acb-clr-focus: #{$clr-pri-focus};
+ --bit-acb-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-acb-sec {
--bit-acb-clr-ico: #{$clr-sec};
--bit-acb-clr-hover: #{$clr-sec-hover};
--bit-acb-clr-active: #{$clr-sec-active};
+ --bit-acb-clr-focus: #{$clr-sec-focus};
+ --bit-acb-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-acb-ter {
--bit-acb-clr-ico: #{$clr-ter};
--bit-acb-clr-hover: #{$clr-ter-hover};
--bit-acb-clr-active: #{$clr-ter-active};
+ --bit-acb-clr-focus: #{$clr-ter-focus};
+ --bit-acb-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-acb-inf {
--bit-acb-clr-ico: #{$clr-inf};
--bit-acb-clr-hover: #{$clr-inf-hover};
--bit-acb-clr-active: #{$clr-inf-active};
+ --bit-acb-clr-focus: #{$clr-inf-focus};
+ --bit-acb-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-acb-suc {
--bit-acb-clr-ico: #{$clr-suc};
--bit-acb-clr-hover: #{$clr-suc-hover};
--bit-acb-clr-active: #{$clr-suc-active};
+ --bit-acb-clr-focus: #{$clr-suc-focus};
+ --bit-acb-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-acb-wrn {
--bit-acb-clr-ico: #{$clr-wrn};
--bit-acb-clr-hover: #{$clr-wrn-hover};
--bit-acb-clr-active: #{$clr-wrn-active};
+ --bit-acb-clr-focus: #{$clr-wrn-focus};
+ --bit-acb-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-acb-swr {
--bit-acb-clr-ico: #{$clr-swr};
--bit-acb-clr-hover: #{$clr-swr-hover};
--bit-acb-clr-active: #{$clr-swr-active};
+ --bit-acb-clr-focus: #{$clr-swr-focus};
+ --bit-acb-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-acb-err {
--bit-acb-clr-ico: #{$clr-err};
--bit-acb-clr-hover: #{$clr-err-hover};
--bit-acb-clr-active: #{$clr-err-active};
+ --bit-acb-clr-focus: #{$clr-err-focus};
+ --bit-acb-clr-dis-text: #{$clr-err-dis-text};
}
@@ -186,54 +202,72 @@
--bit-acb-clr-ico: #{$clr-bg-pri};
--bit-acb-clr-hover: #{$clr-bg-pri-hover};
--bit-acb-clr-active: #{$clr-bg-pri-active};
+ --bit-acb-clr-focus: #{$clr-bg-pri-focus};
+ --bit-acb-clr-dis-text: #{$clr-bg-pri-dis-text};
}
.bit-acb-sbg {
--bit-acb-clr-ico: #{$clr-bg-sec};
--bit-acb-clr-hover: #{$clr-bg-sec-hover};
--bit-acb-clr-active: #{$clr-bg-sec-active};
+ --bit-acb-clr-focus: #{$clr-bg-sec-focus};
+ --bit-acb-clr-dis-text: #{$clr-bg-sec-dis-text};
}
.bit-acb-tbg {
--bit-acb-clr-ico: #{$clr-bg-ter};
--bit-acb-clr-hover: #{$clr-bg-ter-hover};
--bit-acb-clr-active: #{$clr-bg-ter-active};
+ --bit-acb-clr-focus: #{$clr-bg-ter-focus};
+ --bit-acb-clr-dis-text: #{$clr-bg-ter-dis-text};
}
.bit-acb-pfg {
--bit-acb-clr-ico: #{$clr-fg-pri};
--bit-acb-clr-hover: #{$clr-fg-pri-hover};
--bit-acb-clr-active: #{$clr-fg-pri-active};
+ --bit-acb-clr-focus: #{$clr-fg-pri-focus};
+ --bit-acb-clr-dis-text: #{$clr-fg-pri-dis-text};
}
.bit-acb-sfg {
--bit-acb-clr-ico: #{$clr-fg-sec};
--bit-acb-clr-hover: #{$clr-fg-sec-hover};
--bit-acb-clr-active: #{$clr-fg-sec-active};
+ --bit-acb-clr-focus: #{$clr-fg-sec-focus};
+ --bit-acb-clr-dis-text: #{$clr-fg-sec-dis-text};
}
.bit-acb-tfg {
--bit-acb-clr-ico: #{$clr-fg-ter};
--bit-acb-clr-hover: #{$clr-fg-ter-hover};
--bit-acb-clr-active: #{$clr-fg-ter-active};
+ --bit-acb-clr-focus: #{$clr-fg-ter-focus};
+ --bit-acb-clr-dis-text: #{$clr-fg-ter-dis-text};
}
.bit-acb-pbr {
--bit-acb-clr-ico: #{$clr-brd-pri};
--bit-acb-clr-hover: #{$clr-brd-pri-hover};
--bit-acb-clr-active: #{$clr-brd-pri-active};
+ --bit-acb-clr-focus: #{$clr-brd-pri-focus};
+ --bit-acb-clr-dis-text: #{$clr-brd-pri-dis-text};
}
.bit-acb-sbr {
--bit-acb-clr-ico: #{$clr-brd-sec};
--bit-acb-clr-hover: #{$clr-brd-sec-hover};
--bit-acb-clr-active: #{$clr-brd-sec-active};
+ --bit-acb-clr-focus: #{$clr-brd-sec-focus};
+ --bit-acb-clr-dis-text: #{$clr-brd-sec-dis-text};
}
.bit-acb-tbr {
--bit-acb-clr-ico: #{$clr-brd-ter};
--bit-acb-clr-hover: #{$clr-brd-ter-hover};
--bit-acb-clr-active: #{$clr-brd-ter-active};
+ --bit-acb-clr-focus: #{$clr-brd-ter-focus};
+ --bit-acb-clr-dis-text: #{$clr-brd-ter-dis-text};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/Button/BitButton.scss b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/Button/BitButton.scss
index dec33a8317..ab0cdc1b65 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/Button/BitButton.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/Button/BitButton.scss
@@ -17,18 +17,14 @@
border-width: $shp-border-width;
border-style: $shp-border-style;
border-radius: $shp-border-radius;
- --bit-btn-clr-bg-dis: #{$clr-bg-dis};
- --bit-btn-clr-brd-dis: #{$clr-brd-dis};
--bit-btn-float-offset: #{spacing(2)};
&:focus-visible {
- outline-offset: spacing(0.25);
- outline: spacing(0.25) solid var(--bit-btn-clr);
+ @include focus-ring(var(--bit-btn-clr-focus));
}
&.bit-dis {
cursor: default;
- color: $clr-fg-dis;
pointer-events: none;
}
@@ -170,8 +166,9 @@
}
&.bit-dis {
- border-color: var(--bit-btn-clr-brd-dis);
- background-color: var(--bit-btn-clr-bg-dis);
+ color: var(--bit-btn-clr-dis-text);
+ border-color: var(--bit-btn-clr-dis);
+ background-color: var(--bit-btn-clr-dis);
}
}
@@ -193,7 +190,9 @@
}
&.bit-dis {
- border-color: var(--bit-btn-clr-brd-dis);
+ color: var(--bit-btn-clr-dis-text);
+ border-color: var(--bit-btn-clr-dis);
+ background-color: transparent;
}
}
@@ -213,6 +212,12 @@
color: var(--bit-btn-clr-txt);
background-color: var(--bit-btn-clr-active);
}
+
+ &.bit-dis {
+ color: var(--bit-btn-clr-dis-text);
+ border-color: transparent;
+ background-color: transparent;
+ }
}
@@ -221,7 +226,10 @@
--bit-btn-clr-txt: #{$clr-pri-text};
--bit-btn-clr-hover: #{$clr-pri-hover};
--bit-btn-clr-active: #{$clr-pri-active};
+ --bit-btn-clr-focus: #{$clr-pri-focus};
--bit-btn-clr-spn: #{$clr-pri-dark};
+ --bit-btn-clr-dis: #{$clr-pri-dis};
+ --bit-btn-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-btn-sec {
@@ -229,7 +237,10 @@
--bit-btn-clr-txt: #{$clr-sec-text};
--bit-btn-clr-hover: #{$clr-sec-hover};
--bit-btn-clr-active: #{$clr-sec-active};
+ --bit-btn-clr-focus: #{$clr-sec-focus};
--bit-btn-clr-spn: #{$clr-sec-dark};
+ --bit-btn-clr-dis: #{$clr-sec-dis};
+ --bit-btn-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-btn-ter {
@@ -237,7 +248,10 @@
--bit-btn-clr-txt: #{$clr-ter-text};
--bit-btn-clr-hover: #{$clr-ter-hover};
--bit-btn-clr-active: #{$clr-ter-active};
+ --bit-btn-clr-focus: #{$clr-ter-focus};
--bit-btn-clr-spn: #{$clr-ter-dark};
+ --bit-btn-clr-dis: #{$clr-ter-dis};
+ --bit-btn-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-btn-inf {
@@ -245,7 +259,10 @@
--bit-btn-clr-txt: #{$clr-inf-text};
--bit-btn-clr-hover: #{$clr-inf-hover};
--bit-btn-clr-active: #{$clr-inf-active};
+ --bit-btn-clr-focus: #{$clr-inf-focus};
--bit-btn-clr-spn: #{$clr-inf-dark};
+ --bit-btn-clr-dis: #{$clr-inf-dis};
+ --bit-btn-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-btn-suc {
@@ -253,7 +270,10 @@
--bit-btn-clr-txt: #{$clr-suc-text};
--bit-btn-clr-hover: #{$clr-suc-hover};
--bit-btn-clr-active: #{$clr-suc-active};
+ --bit-btn-clr-focus: #{$clr-suc-focus};
--bit-btn-clr-spn: #{$clr-suc-dark};
+ --bit-btn-clr-dis: #{$clr-suc-dis};
+ --bit-btn-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-btn-wrn {
@@ -261,7 +281,10 @@
--bit-btn-clr-txt: #{$clr-wrn-text};
--bit-btn-clr-hover: #{$clr-wrn-hover};
--bit-btn-clr-active: #{$clr-wrn-active};
+ --bit-btn-clr-focus: #{$clr-wrn-focus};
--bit-btn-clr-spn: #{$clr-wrn-dark};
+ --bit-btn-clr-dis: #{$clr-wrn-dis};
+ --bit-btn-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-btn-swr {
@@ -269,7 +292,10 @@
--bit-btn-clr-txt: #{$clr-swr-text};
--bit-btn-clr-hover: #{$clr-swr-hover};
--bit-btn-clr-active: #{$clr-swr-active};
+ --bit-btn-clr-focus: #{$clr-swr-focus};
--bit-btn-clr-spn: #{$clr-swr-dark};
+ --bit-btn-clr-dis: #{$clr-swr-dis};
+ --bit-btn-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-btn-err {
@@ -277,7 +303,10 @@
--bit-btn-clr-txt: #{$clr-err-text};
--bit-btn-clr-hover: #{$clr-err-hover};
--bit-btn-clr-active: #{$clr-err-active};
+ --bit-btn-clr-focus: #{$clr-err-focus};
--bit-btn-clr-spn: #{$clr-err-dark};
+ --bit-btn-clr-dis: #{$clr-err-dis};
+ --bit-btn-clr-dis-text: #{$clr-err-dis-text};
}
.bit-btn-pbg {
@@ -285,7 +314,10 @@
--bit-btn-clr-txt: #{$clr-fg-pri};
--bit-btn-clr-hover: #{$clr-bg-pri-hover};
--bit-btn-clr-active: #{$clr-bg-pri-active};
+ --bit-btn-clr-focus: #{$clr-bg-pri-focus};
--bit-btn-clr-spn: #{$clr-fg-pri};
+ --bit-btn-clr-dis: #{$clr-bg-pri-dis};
+ --bit-btn-clr-dis-text: #{$clr-bg-pri-dis-text};
}
.bit-btn-sbg {
@@ -293,7 +325,10 @@
--bit-btn-clr-txt: #{$clr-fg-pri};
--bit-btn-clr-hover: #{$clr-bg-sec-hover};
--bit-btn-clr-active: #{$clr-bg-sec-active};
+ --bit-btn-clr-focus: #{$clr-bg-sec-focus};
--bit-btn-clr-spn: #{$clr-fg-pri};
+ --bit-btn-clr-dis: #{$clr-bg-sec-dis};
+ --bit-btn-clr-dis-text: #{$clr-bg-sec-dis-text};
}
.bit-btn-tbg {
@@ -301,7 +336,10 @@
--bit-btn-clr-txt: #{$clr-fg-pri};
--bit-btn-clr-hover: #{$clr-bg-ter-hover};
--bit-btn-clr-active: #{$clr-bg-ter-active};
+ --bit-btn-clr-focus: #{$clr-bg-ter-focus};
--bit-btn-clr-spn: #{$clr-fg-pri};
+ --bit-btn-clr-dis: #{$clr-bg-ter-dis};
+ --bit-btn-clr-dis-text: #{$clr-bg-ter-dis-text};
}
.bit-btn-pfg {
@@ -309,7 +347,10 @@
--bit-btn-clr-txt: #{$clr-bg-pri};
--bit-btn-clr-hover: #{$clr-fg-pri-hover};
--bit-btn-clr-active: #{$clr-fg-pri-active};
+ --bit-btn-clr-focus: #{$clr-fg-pri-focus};
--bit-btn-clr-spn: #{$clr-bg-pri};
+ --bit-btn-clr-dis: #{$clr-fg-pri-dis};
+ --bit-btn-clr-dis-text: #{$clr-fg-pri-dis-text};
}
.bit-btn-sfg {
@@ -317,7 +358,10 @@
--bit-btn-clr-txt: #{$clr-bg-pri};
--bit-btn-clr-hover: #{$clr-fg-sec-hover};
--bit-btn-clr-active: #{$clr-fg-sec-active};
+ --bit-btn-clr-focus: #{$clr-fg-sec-focus};
--bit-btn-clr-spn: #{$clr-bg-pri};
+ --bit-btn-clr-dis: #{$clr-fg-sec-dis};
+ --bit-btn-clr-dis-text: #{$clr-fg-sec-dis-text};
}
.bit-btn-tfg {
@@ -325,7 +369,10 @@
--bit-btn-clr-txt: #{$clr-bg-pri};
--bit-btn-clr-hover: #{$clr-fg-ter-hover};
--bit-btn-clr-active: #{$clr-fg-ter-active};
+ --bit-btn-clr-focus: #{$clr-fg-ter-focus};
--bit-btn-clr-spn: #{$clr-bg-pri};
+ --bit-btn-clr-dis: #{$clr-fg-ter-dis};
+ --bit-btn-clr-dis-text: #{$clr-fg-ter-dis-text};
}
.bit-btn-pbr {
@@ -333,7 +380,10 @@
--bit-btn-clr-txt: #{$clr-fg-pri};
--bit-btn-clr-hover: #{$clr-brd-pri-hover};
--bit-btn-clr-active: #{$clr-brd-pri-active};
+ --bit-btn-clr-focus: #{$clr-brd-pri-focus};
--bit-btn-clr-spn: #{$clr-fg-pri};
+ --bit-btn-clr-dis: #{$clr-brd-pri-dis};
+ --bit-btn-clr-dis-text: #{$clr-brd-pri-dis-text};
}
.bit-btn-sbr {
@@ -341,7 +391,10 @@
--bit-btn-clr-txt: #{$clr-fg-pri};
--bit-btn-clr-hover: #{$clr-brd-sec-hover};
--bit-btn-clr-active: #{$clr-brd-sec-active};
+ --bit-btn-clr-focus: #{$clr-brd-sec-focus};
--bit-btn-clr-spn: #{$clr-fg-pri};
+ --bit-btn-clr-dis: #{$clr-brd-sec-dis};
+ --bit-btn-clr-dis-text: #{$clr-brd-sec-dis-text};
}
.bit-btn-tbr {
@@ -349,7 +402,10 @@
--bit-btn-clr-txt: #{$clr-fg-pri};
--bit-btn-clr-hover: #{$clr-brd-ter-hover};
--bit-btn-clr-active: #{$clr-brd-ter-active};
+ --bit-btn-clr-focus: #{$clr-brd-ter-focus};
--bit-btn-clr-spn: #{$clr-fg-pri};
+ --bit-btn-clr-dis: #{$clr-brd-ter-dis};
+ --bit-btn-clr-dis-text: #{$clr-brd-ter-dis-text};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ButtonGroup/BitButtonGroup.scss b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ButtonGroup/BitButtonGroup.scss
index d8123b6295..9d14ac2254 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ButtonGroup/BitButtonGroup.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ButtonGroup/BitButtonGroup.scss
@@ -12,16 +12,26 @@
--bit-btg-itm-brd-wid: #{$shp-border-width};
--bit-btg-itm-brd-wid-ver: 0;
+ // The ring is rendered on the group container (like the MenuButton split-button) because the
+ // root's overflow:hidden would clip a box-shadow ring drawn on the focused item itself.
+ &:focus-visible,
+ &:has(:focus-visible) {
+ @include focus-ring(var(--bit-btg-clr-focus));
+ }
+
&.bit-dis, &.bit-dis * {
cursor: default;
- color: $clr-fg-dis;
pointer-events: none;
}
&.bit-dis {
- border-color: $clr-brd-dis;
- --bit-btg-itm-clr-bg: #{$clr-bg-dis};
- --bit-btg-itm-clr-brd: #{$clr-brd-dis};
+ color: var(--bit-btg-clr-dis-text);
+ border-color: var(--bit-btg-clr-dis);
+
+ // Resolve through the per-variant `*-dis` tokens so outline/text variants keep transparent
+ // backgrounds and only filled variants render the colored disabled tint.
+ --bit-btg-itm-clr-bg: var(--bit-btg-itm-clr-bg-dis);
+ --bit-btg-itm-clr-brd: var(--bit-btg-itm-clr-brd-dis, var(--bit-btg-clr-dis));
}
}
@@ -118,7 +128,8 @@
&[disabled],
&[aria-disabled="true"] {
cursor: default;
- color: $clr-fg-dis;
+ color: var(--bit-btg-clr-dis-text);
+ border-color: var(--bit-btg-itm-clr-brd-dis, var(--bit-btg-clr-dis));
background-color: var(--bit-btg-itm-clr-bg-dis);
}
@@ -279,7 +290,7 @@
--bit-btg-itm-clr-hover: var(--bit-btg-clr-txt);
--bit-btg-itm-clr-bg-hover: var(--bit-btg-clr-hover);
--bit-btg-itm-clr-bg-active: var(--bit-btg-clr-active);
- --bit-btg-itm-clr-bg-dis: #{$clr-bg-dis};
+ --bit-btg-itm-clr-bg-dis: var(--bit-btg-clr-dis);
--bit-btg-itm-clr-fcs: var(--bit-btg-clr-txt);
@media (hover: hover) {
@@ -313,6 +324,10 @@
&:active {
border-color: var(--bit-btg-clr-active);
}
+
+ &.bit-dis {
+ background-color: transparent;
+ }
}
.bit-btg-txt {
@@ -356,9 +371,12 @@
--bit-btg-clr-brd: #{$clr-pri-dark};
--bit-btg-clr-hover: #{$clr-pri-hover};
--bit-btg-clr-active: #{$clr-pri-active};
+ --bit-btg-clr-focus: #{$clr-pri-focus};
--bit-btg-clr-dark: #{$clr-pri-dark};
--bit-btg-clr-dark-hover: #{$clr-pri-dark-hover};
--bit-btg-clr-dark-active: #{$clr-pri-dark-active};
+ --bit-btg-clr-dis: #{$clr-pri-dis};
+ --bit-btg-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-btg-sec {
@@ -367,9 +385,12 @@
--bit-btg-clr-brd: #{$clr-sec-dark};
--bit-btg-clr-hover: #{$clr-sec-hover};
--bit-btg-clr-active: #{$clr-sec-active};
+ --bit-btg-clr-focus: #{$clr-sec-focus};
--bit-btg-clr-dark: #{$clr-sec-dark};
--bit-btg-clr-dark-hover: #{$clr-sec-dark-hover};
--bit-btg-clr-dark-active: #{$clr-sec-dark-active};
+ --bit-btg-clr-dis: #{$clr-sec-dis};
+ --bit-btg-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-btg-ter {
@@ -378,9 +399,12 @@
--bit-btg-clr-brd: #{$clr-ter-dark};
--bit-btg-clr-hover: #{$clr-ter-hover};
--bit-btg-clr-active: #{$clr-ter-active};
+ --bit-btg-clr-focus: #{$clr-ter-focus};
--bit-btg-clr-dark: #{$clr-ter-dark};
--bit-btg-clr-dark-hover: #{$clr-ter-dark-hover};
--bit-btg-clr-dark-active: #{$clr-ter-dark-active};
+ --bit-btg-clr-dis: #{$clr-ter-dis};
+ --bit-btg-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-btg-inf {
@@ -389,9 +413,12 @@
--bit-btg-clr-brd: #{$clr-inf-dark};
--bit-btg-clr-hover: #{$clr-inf-hover};
--bit-btg-clr-active: #{$clr-inf-active};
+ --bit-btg-clr-focus: #{$clr-inf-focus};
--bit-btg-clr-dark: #{$clr-inf-dark};
--bit-btg-clr-dark-hover: #{$clr-inf-dark-hover};
--bit-btg-clr-dark-active: #{$clr-inf-dark-active};
+ --bit-btg-clr-dis: #{$clr-inf-dis};
+ --bit-btg-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-btg-suc {
@@ -400,9 +427,12 @@
--bit-btg-clr-brd: #{$clr-suc-dark};
--bit-btg-clr-hover: #{$clr-suc-hover};
--bit-btg-clr-active: #{$clr-suc-active};
+ --bit-btg-clr-focus: #{$clr-suc-focus};
--bit-btg-clr-dark: #{$clr-suc-dark};
--bit-btg-clr-dark-hover: #{$clr-suc-dark-hover};
--bit-btg-clr-dark-active: #{$clr-suc-dark-active};
+ --bit-btg-clr-dis: #{$clr-suc-dis};
+ --bit-btg-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-btg-wrn {
@@ -411,9 +441,12 @@
--bit-btg-clr-brd: #{$clr-wrn-dark};
--bit-btg-clr-hover: #{$clr-wrn-hover};
--bit-btg-clr-active: #{$clr-wrn-active};
+ --bit-btg-clr-focus: #{$clr-wrn-focus};
--bit-btg-clr-dark: #{$clr-wrn-dark};
--bit-btg-clr-dark-hover: #{$clr-wrn-dark-hover};
--bit-btg-clr-dark-active: #{$clr-wrn-dark-active};
+ --bit-btg-clr-dis: #{$clr-wrn-dis};
+ --bit-btg-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-btg-swr {
@@ -422,9 +455,12 @@
--bit-btg-clr-brd: #{$clr-swr-dark};
--bit-btg-clr-hover: #{$clr-swr-hover};
--bit-btg-clr-active: #{$clr-swr-active};
+ --bit-btg-clr-focus: #{$clr-swr-focus};
--bit-btg-clr-dark: #{$clr-swr-dark};
--bit-btg-clr-dark-hover: #{$clr-swr-dark-hover};
--bit-btg-clr-dark-active: #{$clr-swr-dark-active};
+ --bit-btg-clr-dis: #{$clr-swr-dis};
+ --bit-btg-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-btg-err {
@@ -433,9 +469,12 @@
--bit-btg-clr-brd: #{$clr-err-dark};
--bit-btg-clr-hover: #{$clr-err-hover};
--bit-btg-clr-active: #{$clr-err-active};
+ --bit-btg-clr-focus: #{$clr-err-focus};
--bit-btg-clr-dark: #{$clr-err-dark};
--bit-btg-clr-dark-hover: #{$clr-err-dark-hover};
--bit-btg-clr-dark-active: #{$clr-err-dark-active};
+ --bit-btg-clr-dis: #{$clr-err-dis};
+ --bit-btg-clr-dis-text: #{$clr-err-dis-text};
}
@@ -445,9 +484,12 @@
--bit-btg-clr-brd: #{$clr-bg-pri};
--bit-btg-clr-hover: #{$clr-bg-pri-hover};
--bit-btg-clr-active: #{$clr-bg-pri-active};
+ --bit-btg-clr-focus: #{$clr-bg-pri-focus};
--bit-btg-clr-dark: #{$clr-bg-pri};
--bit-btg-clr-dark-hover: #{$clr-bg-pri-hover};
--bit-btg-clr-dark-active: #{$clr-bg-pri-active};
+ --bit-btg-clr-dis: #{$clr-bg-pri-dis};
+ --bit-btg-clr-dis-text: #{$clr-bg-pri-dis-text};
}
.bit-btg-sbg {
@@ -456,9 +498,12 @@
--bit-btg-clr-brd: #{$clr-bg-sec};
--bit-btg-clr-hover: #{$clr-bg-sec-hover};
--bit-btg-clr-active: #{$clr-bg-sec-active};
+ --bit-btg-clr-focus: #{$clr-bg-sec-focus};
--bit-btg-clr-dark: #{$clr-bg-sec};
--bit-btg-clr-dark-hover: #{$clr-bg-sec-hover};
--bit-btg-clr-dark-active: #{$clr-bg-sec-active};
+ --bit-btg-clr-dis: #{$clr-bg-sec-dis};
+ --bit-btg-clr-dis-text: #{$clr-bg-sec-dis-text};
}
.bit-btg-tbg {
@@ -467,9 +512,12 @@
--bit-btg-clr-brd: #{$clr-bg-ter};
--bit-btg-clr-hover: #{$clr-bg-ter-hover};
--bit-btg-clr-active: #{$clr-bg-ter-active};
+ --bit-btg-clr-focus: #{$clr-bg-ter-focus};
--bit-btg-clr-dark: #{$clr-bg-ter};
--bit-btg-clr-dark-hover: #{$clr-bg-ter-hover};
--bit-btg-clr-dark-active: #{$clr-bg-ter-active};
+ --bit-btg-clr-dis: #{$clr-bg-ter-dis};
+ --bit-btg-clr-dis-text: #{$clr-bg-ter-dis-text};
}
.bit-btg-pfg {
@@ -478,9 +526,12 @@
--bit-btg-clr-brd: #{$clr-fg-pri};
--bit-btg-clr-hover: #{$clr-fg-pri-hover};
--bit-btg-clr-active: #{$clr-fg-pri-active};
+ --bit-btg-clr-focus: #{$clr-fg-pri-focus};
--bit-btg-clr-dark: #{$clr-fg-pri};
--bit-btg-clr-dark-hover: #{$clr-fg-pri-hover};
--bit-btg-clr-dark-active: #{$clr-fg-pri-active};
+ --bit-btg-clr-dis: #{$clr-fg-pri-dis};
+ --bit-btg-clr-dis-text: #{$clr-fg-pri-dis-text};
}
.bit-btg-sfg {
@@ -489,9 +540,12 @@
--bit-btg-clr-brd: #{$clr-fg-sec};
--bit-btg-clr-hover: #{$clr-fg-sec-hover};
--bit-btg-clr-active: #{$clr-fg-sec-active};
+ --bit-btg-clr-focus: #{$clr-fg-sec-focus};
--bit-btg-clr-dark: #{$clr-fg-sec};
--bit-btg-clr-dark-hover: #{$clr-fg-sec-hover};
--bit-btg-clr-dark-active: #{$clr-fg-sec-active};
+ --bit-btg-clr-dis: #{$clr-fg-sec-dis};
+ --bit-btg-clr-dis-text: #{$clr-fg-sec-dis-text};
}
.bit-btg-tfg {
@@ -500,9 +554,12 @@
--bit-btg-clr-brd: #{$clr-fg-ter};
--bit-btg-clr-hover: #{$clr-fg-ter-hover};
--bit-btg-clr-active: #{$clr-fg-ter-active};
+ --bit-btg-clr-focus: #{$clr-fg-ter-focus};
--bit-btg-clr-dark: #{$clr-fg-ter};
--bit-btg-clr-dark-hover: #{$clr-fg-ter-hover};
--bit-btg-clr-dark-active: #{$clr-fg-ter-active};
+ --bit-btg-clr-dis: #{$clr-fg-ter-dis};
+ --bit-btg-clr-dis-text: #{$clr-fg-ter-dis-text};
}
.bit-btg-pbr {
@@ -511,9 +568,12 @@
--bit-btg-clr-brd: #{$clr-brd-pri};
--bit-btg-clr-hover: #{$clr-brd-pri-hover};
--bit-btg-clr-active: #{$clr-brd-pri-active};
+ --bit-btg-clr-focus: #{$clr-brd-pri-focus};
--bit-btg-clr-dark: #{$clr-brd-pri};
--bit-btg-clr-dark-hover: #{$clr-brd-pri-hover};
--bit-btg-clr-dark-active: #{$clr-brd-pri-active};
+ --bit-btg-clr-dis: #{$clr-brd-pri-dis};
+ --bit-btg-clr-dis-text: #{$clr-brd-pri-dis-text};
}
.bit-btg-sbr {
@@ -522,9 +582,12 @@
--bit-btg-clr-brd: #{$clr-brd-sec};
--bit-btg-clr-hover: #{$clr-brd-sec-hover};
--bit-btg-clr-active: #{$clr-brd-sec-active};
+ --bit-btg-clr-focus: #{$clr-brd-sec-focus};
--bit-btg-clr-dark: #{$clr-brd-sec};
--bit-btg-clr-dark-hover: #{$clr-brd-sec-hover};
--bit-btg-clr-dark-active: #{$clr-brd-sec-active};
+ --bit-btg-clr-dis: #{$clr-brd-sec-dis};
+ --bit-btg-clr-dis-text: #{$clr-brd-sec-dis-text};
}
.bit-btg-tbr {
@@ -533,9 +596,12 @@
--bit-btg-clr-brd: #{$clr-brd-ter};
--bit-btg-clr-hover: #{$clr-brd-ter-hover};
--bit-btg-clr-active: #{$clr-brd-ter-active};
+ --bit-btg-clr-focus: #{$clr-brd-ter-focus};
--bit-btg-clr-dark: #{$clr-brd-ter};
--bit-btg-clr-dark-hover: #{$clr-brd-ter-hover};
--bit-btg-clr-dark-active: #{$clr-brd-ter-active};
+ --bit-btg-clr-dis: #{$clr-brd-ter-dis};
+ --bit-btg-clr-dis-text: #{$clr-brd-ter-dis-text};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.scss b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.scss
index 718e4299e3..28896ba626 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.scss
@@ -13,6 +13,21 @@
border-style: $shp-border-style;
border-radius: $shp-border-radius;
+ &:focus-visible,
+ &:has(:focus-visible) {
+ @include focus-ring(var(--bit-mnb-clr-focus));
+ }
+
+ &.bit-dis {
+ cursor: default;
+ pointer-events: none;
+ color: var(--bit-mnb-clr-dis-text);
+ --bit-mnb-clr-spb: var(--bit-mnb-clr-dis-text);
+ --bit-mnb-clr-txt: var(--bit-mnb-clr-dis-text);
+ --bit-mnb-clr-opb-txt: var(--bit-mnb-clr-dis-text);
+ --bit-mnb-clr-chb-txt: var(--bit-mnb-clr-dis-text);
+ }
+
@keyframes bit-mnb-spinner-animation {
0% {
transform: rotate(0deg);
@@ -22,13 +37,6 @@
transform: rotate(360deg);
}
}
-
- &.bit-dis {
- cursor: default;
- color: $clr-fg-dis;
- pointer-events: none;
- --bit-mnb-clr-spb: #{$clr-fg-dis};
- }
}
.bit-mnb-flw {
@@ -187,7 +195,7 @@
&[disabled] {
cursor: default;
- color: $clr-fg-dis;
+ color: var(--bit-mnb-clr-dis-text);
pointer-events: none;
}
@@ -225,8 +233,9 @@
}
&.bit-dis {
- border-color: $clr-brd-dis;
- background-color: $clr-bg-dis;
+ color: var(--bit-mnb-clr-dis-text);
+ border-color: var(--bit-mnb-clr-dis);
+ background-color: var(--bit-mnb-clr-dis);
}
}
@@ -247,7 +256,9 @@
}
&.bit-dis {
- border-color: $clr-brd-dis;
+ color: var(--bit-mnb-clr-dis-text);
+ border-color: var(--bit-mnb-clr-dis);
+ background-color: transparent;
}
}
@@ -256,6 +267,12 @@
border-color: transparent;
background-color: transparent;
--bit-mnb-clr-spb: var(--bit-mnb-clr);
+
+ &.bit-dis {
+ color: var(--bit-mnb-clr-dis-text);
+ border-color: transparent;
+ background-color: transparent;
+ }
}
.bit-mnb-pri {
@@ -263,6 +280,9 @@
--bit-mnb-clr-txt: #{$clr-pri-text};
--bit-mnb-clr-hover: #{$clr-pri-hover};
--bit-mnb-clr-active: #{$clr-pri-active};
+ --bit-mnb-clr-focus: #{$clr-pri-focus};
+ --bit-mnb-clr-dis: #{$clr-pri-dis};
+ --bit-mnb-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-mnb-sec {
@@ -270,6 +290,9 @@
--bit-mnb-clr-txt: #{$clr-sec-text};
--bit-mnb-clr-hover: #{$clr-sec-hover};
--bit-mnb-clr-active: #{$clr-sec-active};
+ --bit-mnb-clr-focus: #{$clr-sec-focus};
+ --bit-mnb-clr-dis: #{$clr-sec-dis};
+ --bit-mnb-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-mnb-ter {
@@ -277,6 +300,9 @@
--bit-mnb-clr-txt: #{$clr-ter-text};
--bit-mnb-clr-hover: #{$clr-ter-hover};
--bit-mnb-clr-active: #{$clr-ter-active};
+ --bit-mnb-clr-focus: #{$clr-ter-focus};
+ --bit-mnb-clr-dis: #{$clr-ter-dis};
+ --bit-mnb-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-mnb-inf {
@@ -284,6 +310,9 @@
--bit-mnb-clr-txt: #{$clr-inf-text};
--bit-mnb-clr-hover: #{$clr-inf-hover};
--bit-mnb-clr-active: #{$clr-inf-active};
+ --bit-mnb-clr-focus: #{$clr-inf-focus};
+ --bit-mnb-clr-dis: #{$clr-inf-dis};
+ --bit-mnb-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-mnb-suc {
@@ -291,6 +320,9 @@
--bit-mnb-clr-txt: #{$clr-suc-text};
--bit-mnb-clr-hover: #{$clr-suc-hover};
--bit-mnb-clr-active: #{$clr-suc-active};
+ --bit-mnb-clr-focus: #{$clr-suc-focus};
+ --bit-mnb-clr-dis: #{$clr-suc-dis};
+ --bit-mnb-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-mnb-wrn {
@@ -298,6 +330,9 @@
--bit-mnb-clr-txt: #{$clr-wrn-text};
--bit-mnb-clr-hover: #{$clr-wrn-hover};
--bit-mnb-clr-active: #{$clr-wrn-active};
+ --bit-mnb-clr-focus: #{$clr-wrn-focus};
+ --bit-mnb-clr-dis: #{$clr-wrn-dis};
+ --bit-mnb-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-mnb-swr {
@@ -305,6 +340,9 @@
--bit-mnb-clr-txt: #{$clr-swr-text};
--bit-mnb-clr-hover: #{$clr-swr-hover};
--bit-mnb-clr-active: #{$clr-swr-active};
+ --bit-mnb-clr-focus: #{$clr-swr-focus};
+ --bit-mnb-clr-dis: #{$clr-swr-dis};
+ --bit-mnb-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-mnb-err {
@@ -312,6 +350,9 @@
--bit-mnb-clr-txt: #{$clr-err-text};
--bit-mnb-clr-hover: #{$clr-err-hover};
--bit-mnb-clr-active: #{$clr-err-active};
+ --bit-mnb-clr-focus: #{$clr-err-focus};
+ --bit-mnb-clr-dis: #{$clr-err-dis};
+ --bit-mnb-clr-dis-text: #{$clr-err-dis-text};
}
.bit-mnb-pbg {
@@ -319,6 +360,9 @@
--bit-mnb-clr-txt: #{$clr-fg-pri};
--bit-mnb-clr-hover: #{$clr-bg-pri-hover};
--bit-mnb-clr-active: #{$clr-bg-pri-active};
+ --bit-mnb-clr-focus: #{$clr-bg-pri-focus};
+ --bit-mnb-clr-dis: #{$clr-bg-pri-dis};
+ --bit-mnb-clr-dis-text: #{$clr-bg-pri-dis-text};
}
.bit-mnb-sbg {
@@ -326,6 +370,9 @@
--bit-mnb-clr-txt: #{$clr-fg-pri};
--bit-mnb-clr-hover: #{$clr-bg-sec-hover};
--bit-mnb-clr-active: #{$clr-bg-sec-active};
+ --bit-mnb-clr-focus: #{$clr-bg-sec-focus};
+ --bit-mnb-clr-dis: #{$clr-bg-sec-dis};
+ --bit-mnb-clr-dis-text: #{$clr-bg-sec-dis-text};
}
.bit-mnb-tbg {
@@ -333,6 +380,9 @@
--bit-mnb-clr-txt: #{$clr-fg-pri};
--bit-mnb-clr-hover: #{$clr-bg-ter-hover};
--bit-mnb-clr-active: #{$clr-bg-ter-active};
+ --bit-mnb-clr-focus: #{$clr-bg-ter-focus};
+ --bit-mnb-clr-dis: #{$clr-bg-ter-dis};
+ --bit-mnb-clr-dis-text: #{$clr-bg-ter-dis-text};
}
.bit-mnb-pfg {
@@ -340,6 +390,9 @@
--bit-mnb-clr-txt: #{$clr-bg-pri};
--bit-mnb-clr-hover: #{$clr-fg-pri-hover};
--bit-mnb-clr-active: #{$clr-fg-pri-active};
+ --bit-mnb-clr-focus: #{$clr-fg-pri-focus};
+ --bit-mnb-clr-dis: #{$clr-fg-pri-dis};
+ --bit-mnb-clr-dis-text: #{$clr-fg-pri-dis-text};
}
.bit-mnb-sfg {
@@ -347,6 +400,9 @@
--bit-mnb-clr-txt: #{$clr-bg-pri};
--bit-mnb-clr-hover: #{$clr-fg-sec-hover};
--bit-mnb-clr-active: #{$clr-fg-sec-active};
+ --bit-mnb-clr-focus: #{$clr-fg-sec-focus};
+ --bit-mnb-clr-dis: #{$clr-fg-sec-dis};
+ --bit-mnb-clr-dis-text: #{$clr-fg-sec-dis-text};
}
.bit-mnb-tfg {
@@ -354,6 +410,9 @@
--bit-mnb-clr-txt: #{$clr-bg-pri};
--bit-mnb-clr-hover: #{$clr-fg-ter-hover};
--bit-mnb-clr-active: #{$clr-fg-ter-active};
+ --bit-mnb-clr-focus: #{$clr-fg-ter-focus};
+ --bit-mnb-clr-dis: #{$clr-fg-ter-dis};
+ --bit-mnb-clr-dis-text: #{$clr-fg-ter-dis-text};
}
.bit-mnb-pbr {
@@ -361,6 +420,9 @@
--bit-mnb-clr-txt: #{$clr-fg-pri};
--bit-mnb-clr-hover: #{$clr-brd-pri-hover};
--bit-mnb-clr-active: #{$clr-brd-pri-active};
+ --bit-mnb-clr-focus: #{$clr-brd-pri-focus};
+ --bit-mnb-clr-dis: #{$clr-brd-pri-dis};
+ --bit-mnb-clr-dis-text: #{$clr-brd-pri-dis-text};
}
.bit-mnb-sbr {
@@ -368,6 +430,9 @@
--bit-mnb-clr-txt: #{$clr-fg-pri};
--bit-mnb-clr-hover: #{$clr-brd-sec-hover};
--bit-mnb-clr-active: #{$clr-brd-sec-active};
+ --bit-mnb-clr-focus: #{$clr-brd-sec-focus};
+ --bit-mnb-clr-dis: #{$clr-brd-sec-dis};
+ --bit-mnb-clr-dis-text: #{$clr-brd-sec-dis-text};
}
.bit-mnb-tbr {
@@ -375,6 +440,9 @@
--bit-mnb-clr-txt: #{$clr-fg-pri};
--bit-mnb-clr-hover: #{$clr-brd-ter-hover};
--bit-mnb-clr-active: #{$clr-brd-ter-active};
+ --bit-mnb-clr-focus: #{$clr-brd-ter-focus};
+ --bit-mnb-clr-dis: #{$clr-brd-ter-dis};
+ --bit-mnb-clr-dis-text: #{$clr-brd-ter-dis-text};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ToggleButton/BitToggleButton.scss b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ToggleButton/BitToggleButton.scss
index d5cd81dc39..a4bb362b24 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ToggleButton/BitToggleButton.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/ToggleButton/BitToggleButton.scss
@@ -21,13 +21,11 @@
--bit-tgb-icn-margin: #{spacing(0.5)};
&:focus-visible {
- outline-offset: spacing(0.25);
- outline: spacing(0.25) solid var(--bit-tgb-clr);
+ @include focus-ring(var(--bit-tgb-clr-focus));
}
&.bit-dis {
cursor: default;
- color: $clr-fg-dis;
pointer-events: none;
}
@@ -156,8 +154,9 @@
}
&.bit-dis {
- border-color: $clr-brd-dis;
- background-color: $clr-bg-dis;
+ color: var(--bit-tgb-clr-dis-text);
+ border-color: var(--bit-tgb-clr-dis);
+ background-color: var(--bit-tgb-clr-dis);
}
}
@@ -181,7 +180,9 @@
}
&.bit-dis {
- border-color: $clr-brd-dis;
+ color: var(--bit-tgb-clr-dis-text);
+ border-color: var(--bit-tgb-clr-dis);
+ background-color: transparent;
}
}
@@ -203,6 +204,12 @@
border-color: var(--bit-tgb-clr-active);
background-color: var(--bit-tgb-clr-active);
}
+
+ &.bit-dis {
+ color: var(--bit-tgb-clr-dis-text);
+ border-color: transparent;
+ background-color: transparent;
+ }
}
.bit-tgb-chk {
@@ -228,9 +235,12 @@
--bit-tgb-clr: #{$clr-pri};
--bit-tgb-clr-hover: #{$clr-pri-hover};
--bit-tgb-clr-active: #{$clr-pri-active};
+ --bit-tgb-clr-focus: #{$clr-pri-focus};
--bit-tgb-clr-dark: #{$clr-pri-dark};
--bit-tgb-clr-dark-hover: #{$clr-pri-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-pri-dark-active};
+ --bit-tgb-clr-dis: #{$clr-pri-dis};
+ --bit-tgb-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-tgb-sec {
@@ -238,9 +248,12 @@
--bit-tgb-clr: #{$clr-sec};
--bit-tgb-clr-hover: #{$clr-sec-hover};
--bit-tgb-clr-active: #{$clr-sec-active};
+ --bit-tgb-clr-focus: #{$clr-sec-focus};
--bit-tgb-clr-dark: #{$clr-sec-dark};
--bit-tgb-clr-dark-hover: #{$clr-sec-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-sec-dark-active};
+ --bit-tgb-clr-dis: #{$clr-sec-dis};
+ --bit-tgb-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-tgb-ter {
@@ -248,9 +261,12 @@
--bit-tgb-clr: #{$clr-ter};
--bit-tgb-clr-hover: #{$clr-ter-hover};
--bit-tgb-clr-active: #{$clr-ter-active};
+ --bit-tgb-clr-focus: #{$clr-ter-focus};
--bit-tgb-clr-dark: #{$clr-ter-dark};
--bit-tgb-clr-dark-hover: #{$clr-ter-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-ter-dark-active};
+ --bit-tgb-clr-dis: #{$clr-ter-dis};
+ --bit-tgb-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-tgb-inf {
@@ -258,9 +274,12 @@
--bit-tgb-clr: #{$clr-inf};
--bit-tgb-clr-hover: #{$clr-inf-hover};
--bit-tgb-clr-active: #{$clr-inf-active};
+ --bit-tgb-clr-focus: #{$clr-inf-focus};
--bit-tgb-clr-dark: #{$clr-inf-dark};
--bit-tgb-clr-dark-hover: #{$clr-inf-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-inf-dark-active};
+ --bit-tgb-clr-dis: #{$clr-inf-dis};
+ --bit-tgb-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-tgb-suc {
@@ -268,9 +287,12 @@
--bit-tgb-clr: #{$clr-suc};
--bit-tgb-clr-hover: #{$clr-suc-hover};
--bit-tgb-clr-active: #{$clr-suc-active};
+ --bit-tgb-clr-focus: #{$clr-suc-focus};
--bit-tgb-clr-dark: #{$clr-suc-dark};
--bit-tgb-clr-dark-hover: #{$clr-suc-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-suc-dark-active};
+ --bit-tgb-clr-dis: #{$clr-suc-dis};
+ --bit-tgb-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-tgb-wrn {
@@ -278,9 +300,12 @@
--bit-tgb-clr: #{$clr-wrn};
--bit-tgb-clr-hover: #{$clr-wrn-hover};
--bit-tgb-clr-active: #{$clr-wrn-active};
+ --bit-tgb-clr-focus: #{$clr-wrn-focus};
--bit-tgb-clr-dark: #{$clr-wrn-dark};
--bit-tgb-clr-dark-hover: #{$clr-wrn-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-wrn-dark-active};
+ --bit-tgb-clr-dis: #{$clr-wrn-dis};
+ --bit-tgb-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-tgb-swr {
@@ -288,9 +313,12 @@
--bit-tgb-clr: #{$clr-swr};
--bit-tgb-clr-hover: #{$clr-swr-hover};
--bit-tgb-clr-active: #{$clr-swr-active};
+ --bit-tgb-clr-focus: #{$clr-swr-focus};
--bit-tgb-clr-dark: #{$clr-swr-dark};
--bit-tgb-clr-dark-hover: #{$clr-swr-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-swr-dark-active};
+ --bit-tgb-clr-dis: #{$clr-swr-dis};
+ --bit-tgb-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-tgb-err {
@@ -298,9 +326,12 @@
--bit-tgb-clr: #{$clr-err};
--bit-tgb-clr-hover: #{$clr-err-hover};
--bit-tgb-clr-active: #{$clr-err-active};
+ --bit-tgb-clr-focus: #{$clr-err-focus};
--bit-tgb-clr-dark: #{$clr-err-dark};
--bit-tgb-clr-dark-hover: #{$clr-err-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-err-dark-active};
+ --bit-tgb-clr-dis: #{$clr-err-dis};
+ --bit-tgb-clr-dis-text: #{$clr-err-dis-text};
}
.bit-tgb-pbg {
@@ -308,9 +339,12 @@
--bit-tgb-clr: #{$clr-bg-pri};
--bit-tgb-clr-hover: #{$clr-bg-pri-hover};
--bit-tgb-clr-active: #{$clr-bg-pri-active};
+ --bit-tgb-clr-focus: #{$clr-bg-pri-focus};
--bit-tgb-clr-dark: #{$clr-bg-pri-dark};
--bit-tgb-clr-dark-hover: #{$clr-bg-pri-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-bg-pri-dark-active};
+ --bit-tgb-clr-dis: #{$clr-bg-pri-dis};
+ --bit-tgb-clr-dis-text: #{$clr-bg-pri-dis-text};
}
.bit-tgb-sbg {
@@ -318,9 +352,12 @@
--bit-tgb-clr: #{$clr-bg-sec};
--bit-tgb-clr-hover: #{$clr-bg-sec-hover};
--bit-tgb-clr-active: #{$clr-bg-sec-active};
+ --bit-tgb-clr-focus: #{$clr-bg-sec-focus};
--bit-tgb-clr-dark: #{$clr-bg-sec-dark};
--bit-tgb-clr-dark-hover: #{$clr-bg-sec-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-bg-sec-dark-active};
+ --bit-tgb-clr-dis: #{$clr-bg-sec-dis};
+ --bit-tgb-clr-dis-text: #{$clr-bg-sec-dis-text};
}
.bit-tgb-tbg {
@@ -328,9 +365,12 @@
--bit-tgb-clr: #{$clr-bg-ter};
--bit-tgb-clr-hover: #{$clr-bg-ter-hover};
--bit-tgb-clr-active: #{$clr-bg-ter-active};
+ --bit-tgb-clr-focus: #{$clr-bg-ter-focus};
--bit-tgb-clr-dark: #{$clr-bg-ter-dark};
--bit-tgb-clr-dark-hover: #{$clr-bg-ter-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-bg-ter-dark-active};
+ --bit-tgb-clr-dis: #{$clr-bg-ter-dis};
+ --bit-tgb-clr-dis-text: #{$clr-bg-ter-dis-text};
}
.bit-tgb-pfg {
@@ -338,9 +378,12 @@
--bit-tgb-clr: #{$clr-fg-pri};
--bit-tgb-clr-hover: #{$clr-fg-pri-hover};
--bit-tgb-clr-active: #{$clr-fg-pri-active};
+ --bit-tgb-clr-focus: #{$clr-fg-pri-focus};
--bit-tgb-clr-dark: #{$clr-fg-pri-dark};
--bit-tgb-clr-dark-hover: #{$clr-fg-pri-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-fg-pri-dark-active};
+ --bit-tgb-clr-dis: #{$clr-fg-pri-dis};
+ --bit-tgb-clr-dis-text: #{$clr-fg-pri-dis-text};
}
.bit-tgb-sfg {
@@ -348,9 +391,12 @@
--bit-tgb-clr: #{$clr-fg-sec};
--bit-tgb-clr-hover: #{$clr-fg-sec-hover};
--bit-tgb-clr-active: #{$clr-fg-sec-active};
+ --bit-tgb-clr-focus: #{$clr-fg-sec-focus};
--bit-tgb-clr-dark: #{$clr-fg-sec-dark};
--bit-tgb-clr-dark-hover: #{$clr-fg-sec-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-fg-sec-dark-active};
+ --bit-tgb-clr-dis: #{$clr-fg-sec-dis};
+ --bit-tgb-clr-dis-text: #{$clr-fg-sec-dis-text};
}
.bit-tgb-tfg {
@@ -358,9 +404,12 @@
--bit-tgb-clr: #{$clr-fg-ter};
--bit-tgb-clr-hover: #{$clr-fg-ter-hover};
--bit-tgb-clr-active: #{$clr-fg-ter-active};
+ --bit-tgb-clr-focus: #{$clr-fg-ter-focus};
--bit-tgb-clr-dark: #{$clr-fg-ter-dark};
--bit-tgb-clr-dark-hover: #{$clr-fg-ter-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-fg-ter-dark-active};
+ --bit-tgb-clr-dis: #{$clr-fg-ter-dis};
+ --bit-tgb-clr-dis-text: #{$clr-fg-ter-dis-text};
}
.bit-tgb-pbr {
@@ -368,9 +417,12 @@
--bit-tgb-clr: #{$clr-brd-pri};
--bit-tgb-clr-hover: #{$clr-brd-pri-hover};
--bit-tgb-clr-active: #{$clr-brd-pri-active};
+ --bit-tgb-clr-focus: #{$clr-brd-pri-focus};
--bit-tgb-clr-dark: #{$clr-brd-pri-dark};
--bit-tgb-clr-dark-hover: #{$clr-brd-pri-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-brd-pri-dark-active};
+ --bit-tgb-clr-dis: #{$clr-brd-pri-dis};
+ --bit-tgb-clr-dis-text: #{$clr-brd-pri-dis-text};
}
.bit-tgb-sbr {
@@ -378,9 +430,12 @@
--bit-tgb-clr: #{$clr-brd-sec};
--bit-tgb-clr-hover: #{$clr-brd-sec-hover};
--bit-tgb-clr-active: #{$clr-brd-sec-active};
+ --bit-tgb-clr-focus: #{$clr-brd-sec-focus};
--bit-tgb-clr-dark: #{$clr-brd-sec-dark};
--bit-tgb-clr-dark-hover: #{$clr-brd-sec-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-brd-sec-dark-active};
+ --bit-tgb-clr-dis: #{$clr-brd-sec-dis};
+ --bit-tgb-clr-dis-text: #{$clr-brd-sec-dis-text};
}
.bit-tgb-tbr {
@@ -388,9 +443,12 @@
--bit-tgb-clr: #{$clr-brd-ter};
--bit-tgb-clr-hover: #{$clr-brd-ter-hover};
--bit-tgb-clr-active: #{$clr-brd-ter-active};
+ --bit-tgb-clr-focus: #{$clr-brd-ter-focus};
--bit-tgb-clr-dark: #{$clr-brd-ter-dark};
--bit-tgb-clr-dark-hover: #{$clr-brd-ter-dark-hover};
--bit-tgb-clr-dark-active: #{$clr-brd-ter-dark-active};
+ --bit-tgb-clr-dis: #{$clr-brd-ter-dis};
+ --bit-tgb-clr-dis-text: #{$clr-brd-ter-dis-text};
}
@@ -451,7 +509,7 @@
display: inline-grid;
place-items: center;
- > * {
+ >* {
grid-area: 1 / 1;
}
}
@@ -464,4 +522,4 @@
padding: var(--bit-tgb-ntx-pad);
--bit-tgb-icn-margin: 0;
--bit-tgb-fontsize: var(--bit-tgb-ntx-icn-size);
-}
+}
\ No newline at end of file
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.scss
index 61f4d88753..171205b4d0 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.scss
@@ -491,9 +491,8 @@
}
&:focus {
+ @include focus-underline-ring;
border: none;
- outline: none;
- border-bottom: $shp-border-width $shp-border-style $clr-brd-pri-active;
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Checkbox/BitCheckbox.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Checkbox/BitCheckbox.scss
index 1dd926cc60..5e687606d9 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Checkbox/BitCheckbox.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Checkbox/BitCheckbox.scss
@@ -3,11 +3,16 @@
.bit-chb {
cursor: pointer;
width: fit-content;
+ border-radius: $shp-border-radius;
font-family: $tg-font-family;
--bit-chb-ico-opa: 0;
--bit-chb-box-clr-brd: #{$clr-brd-pri};
--bit-chb-ico-clr: var(--bit-chb-clr-txt-sec);
+ &:focus-visible {
+ @include focus-ring(var(--bit-chb-clr-focus));
+ }
+
@media (hover: hover) {
&:hover {
--bit-chb-ico-opa: 1;
@@ -19,6 +24,10 @@
&.bit-inv {
--bit-chb-box-clr-brd: #{$clr-err};
+ &:focus-visible {
+ @include focus-ring($clr-err-focus);
+ }
+
&.bit-chb-ckd {
--bit-chb-box-clr-bg: #{$clr-err};
--bit-chb-box-clr-brd: #{$clr-err};
@@ -32,20 +41,20 @@
&.bit-dis {
cursor: default;
- color: $clr-fg-dis;
+ color: var(--bit-chb-clr-dis-text);
pointer-events: none;
- --bit-chb-ico-clr: #{$clr-fg-dis};
- --bit-chb-ico-clr-bg: #{$clr-bg-dis};
- --bit-chb-box-clr-brd: #{$clr-brd-dis};
+ --bit-chb-ico-clr: var(--bit-chb-clr-dis-text);
+ --bit-chb-ico-clr-bg: var(--bit-chb-clr-dis);
+ --bit-chb-box-clr-brd: var(--bit-chb-clr-dis);
&.bit-chb-ckd {
- --bit-chb-ico-clr: #{$clr-fg-dis};
- --bit-chb-box-clr-bg: #{$clr-bg-dis};
- --bit-chb-box-clr-brd: #{$clr-bg-dis};
+ --bit-chb-ico-clr: var(--bit-chb-clr-dis-text);
+ --bit-chb-box-clr-bg: var(--bit-chb-clr-dis);
+ --bit-chb-box-clr-brd: var(--bit-chb-clr-dis);
}
&.bit-chb-ind {
- --bit-chb-ico-clr-bg: #{$clr-bg-dis};
+ --bit-chb-ico-clr-bg: var(--bit-chb-clr-dis);
}
}
}
@@ -248,6 +257,9 @@
--bit-chb-clr-txt: #{$clr-pri-text};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-pri-text};
+ --bit-chb-clr-dis: #{$clr-pri-dis};
+ --bit-chb-clr-dis-text: #{$clr-pri-dis-text};
+ --bit-chb-clr-focus: #{$clr-pri-focus};
}
.bit-chb-sec {
@@ -256,6 +268,9 @@
--bit-chb-clr-txt: #{$clr-sec-text};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-sec-text};
+ --bit-chb-clr-dis: #{$clr-sec-dis};
+ --bit-chb-clr-dis-text: #{$clr-sec-dis-text};
+ --bit-chb-clr-focus: #{$clr-sec-focus};
}
.bit-chb-ter {
@@ -264,6 +279,9 @@
--bit-chb-clr-txt: #{$clr-ter-text};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-ter-text};
+ --bit-chb-clr-dis: #{$clr-ter-dis};
+ --bit-chb-clr-dis-text: #{$clr-ter-dis-text};
+ --bit-chb-clr-focus: #{$clr-ter-focus};
}
.bit-chb-inf {
@@ -272,6 +290,9 @@
--bit-chb-clr-txt: #{$clr-inf-text};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-inf-text};
+ --bit-chb-clr-dis: #{$clr-inf-dis};
+ --bit-chb-clr-dis-text: #{$clr-inf-dis-text};
+ --bit-chb-clr-focus: #{$clr-inf-focus};
}
.bit-chb-suc {
@@ -280,6 +301,9 @@
--bit-chb-clr-txt: #{$clr-suc-text};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-suc-text};
+ --bit-chb-clr-dis: #{$clr-suc-dis};
+ --bit-chb-clr-dis-text: #{$clr-suc-dis-text};
+ --bit-chb-clr-focus: #{$clr-suc-focus};
}
.bit-chb-wrn {
@@ -288,6 +312,9 @@
--bit-chb-clr-txt: #{$clr-wrn-text};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-wrn-text};
+ --bit-chb-clr-dis: #{$clr-wrn-dis};
+ --bit-chb-clr-dis-text: #{$clr-wrn-dis-text};
+ --bit-chb-clr-focus: #{$clr-wrn-focus};
}
.bit-chb-swr {
@@ -296,6 +323,9 @@
--bit-chb-clr-txt: #{$clr-swr-text};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-swr-text};
+ --bit-chb-clr-dis: #{$clr-swr-dis};
+ --bit-chb-clr-dis-text: #{$clr-swr-dis-text};
+ --bit-chb-clr-focus: #{$clr-swr-focus};
}
.bit-chb-err {
@@ -304,6 +334,9 @@
--bit-chb-clr-txt: #{$clr-err-text};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-err-text};
+ --bit-chb-clr-dis: #{$clr-err-dis};
+ --bit-chb-clr-dis-text: #{$clr-err-dis-text};
+ --bit-chb-clr-focus: #{$clr-err-focus};
}
.bit-chb-pbg {
@@ -312,6 +345,9 @@
--bit-chb-clr-txt: #{$clr-fg-pri};
--bit-chb-clr-txt-sec: #{$clr-bg-sec};
--bit-chb-clr-txt-hover: #{$clr-fg-pri-hover};
+ --bit-chb-clr-dis: #{$clr-bg-pri-dis};
+ --bit-chb-clr-dis-text: #{$clr-bg-pri-dis-text};
+ --bit-chb-clr-focus: #{$clr-bg-pri-focus};
}
.bit-chb-sbg {
@@ -320,6 +356,9 @@
--bit-chb-clr-txt: #{$clr-fg-sec};
--bit-chb-clr-txt-sec: #{$clr-bg-sec};
--bit-chb-clr-txt-hover: #{$clr-fg-sec-hover};
+ --bit-chb-clr-dis: #{$clr-bg-sec-dis};
+ --bit-chb-clr-dis-text: #{$clr-bg-sec-dis-text};
+ --bit-chb-clr-focus: #{$clr-bg-sec-focus};
}
.bit-chb-tbg {
@@ -328,6 +367,9 @@
--bit-chb-clr-txt: #{$clr-fg-ter};
--bit-chb-clr-txt-sec: #{$clr-bg-sec};
--bit-chb-clr-txt-hover: #{$clr-fg-ter-hover};
+ --bit-chb-clr-dis: #{$clr-bg-ter-dis};
+ --bit-chb-clr-dis-text: #{$clr-bg-ter-dis-text};
+ --bit-chb-clr-focus: #{$clr-bg-ter-focus};
}
.bit-chb-pfg {
@@ -336,6 +378,9 @@
--bit-chb-clr-txt: #{$clr-bg-pri};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-bg-pri-hover};
+ --bit-chb-clr-dis: #{$clr-fg-pri-dis};
+ --bit-chb-clr-dis-text: #{$clr-fg-pri-dis-text};
+ --bit-chb-clr-focus: #{$clr-fg-pri-focus};
}
.bit-chb-sfg {
@@ -344,6 +389,9 @@
--bit-chb-clr-txt: #{$clr-bg-sec};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-bg-sec-hover};
+ --bit-chb-clr-dis: #{$clr-fg-sec-dis};
+ --bit-chb-clr-dis-text: #{$clr-fg-sec-dis-text};
+ --bit-chb-clr-focus: #{$clr-fg-sec-focus};
}
.bit-chb-tfg {
@@ -352,6 +400,9 @@
--bit-chb-clr-txt: #{$clr-bg-ter};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-bg-ter-hover};
+ --bit-chb-clr-dis: #{$clr-fg-ter-dis};
+ --bit-chb-clr-dis-text: #{$clr-fg-ter-dis-text};
+ --bit-chb-clr-focus: #{$clr-fg-ter-focus};
}
.bit-chb-pbr {
@@ -360,6 +411,9 @@
--bit-chb-clr-txt: #{$clr-fg-pri};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-fg-pri-hover};
+ --bit-chb-clr-dis: #{$clr-brd-pri-dis};
+ --bit-chb-clr-dis-text: #{$clr-brd-pri-dis-text};
+ --bit-chb-clr-focus: #{$clr-brd-pri-focus};
}
.bit-chb-sbr {
@@ -368,6 +422,9 @@
--bit-chb-clr-txt: #{$clr-fg-sec};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-fg-sec-hover};
+ --bit-chb-clr-dis: #{$clr-brd-sec-dis};
+ --bit-chb-clr-dis-text: #{$clr-brd-sec-dis-text};
+ --bit-chb-clr-focus: #{$clr-brd-sec-focus};
}
.bit-chb-tbr {
@@ -376,6 +433,9 @@
--bit-chb-clr-txt: #{$clr-fg-ter};
--bit-chb-clr-txt-sec: #{$clr-fg-sec};
--bit-chb-clr-txt-hover: #{$clr-fg-ter-hover};
+ --bit-chb-clr-dis: #{$clr-brd-ter-dis};
+ --bit-chb-clr-dis-text: #{$clr-brd-ter-dis-text};
+ --bit-chb-clr-focus: #{$clr-brd-ter-focus};
}
.bit-chb-sm {
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/ChoiceGroup/BitChoiceGroup.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/ChoiceGroup/BitChoiceGroup.scss
index e0e852a80e..adc374bbeb 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/ChoiceGroup/BitChoiceGroup.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/ChoiceGroup/BitChoiceGroup.scss
@@ -24,6 +24,11 @@
}
&.bit-inv {
+ .bit-chg-ili:focus-visible,
+ input:focus-visible + .bit-chg-ili {
+ @include focus-ring($clr-err-focus);
+ }
+
.bit-chg-rad {
color: $clr-err;
border-color: $clr-err;
@@ -154,9 +159,15 @@
justify-content: center;
min-height: spacing(2.5);
transition: all 0.2s ease;
+ border-radius: $shp-border-radius;
padding-block-start: spacing(2.75);
border: $shp-border-width $shp-border-style transparent;
+ &:focus-visible,
+ input:focus-visible + & {
+ @include focus-ring(var(--bit-chg-clr-focus));
+ }
+
.bit-chg-rad {
position: absolute;
top: calc(var(--bit-chg-circle-size) / 4);
@@ -309,21 +320,21 @@
.bit-chg-rad {
cursor: default;
pointer-events: none;
- color: $clr-fg-dis;
+ color: var(--bit-chg-clr-dis-text);
&::before {
- border-color: $clr-brd-dis;
+ border-color: var(--bit-chg-clr-dis);
}
&::after {
- background-color: $clr-bg-dis;
+ background-color: var(--bit-chg-clr-dis);
}
}
.bit-chg-ili {
cursor: default;
border-color: transparent;
- color: $clr-fg-dis;
+ color: var(--bit-chg-clr-dis-text);
@media (hover: hover) {
&:hover {
@@ -333,11 +344,11 @@
}
.bit-chg-icw {
- color: $clr-fg-dis;
+ color: var(--bit-chg-clr-dis-text);
}
.bit-chg-ico {
- color: $clr-fg-dis;
+ color: var(--bit-chg-clr-dis-text);
}
}
@@ -346,6 +357,9 @@
--bit-chg-clr-bg: #{$clr-bg-sec};
--bit-chg-clr-brd: #{$clr-brd-pri};
--bit-chg-clr-hover: #{$clr-pri-hover};
+ --bit-chg-clr-dis: #{$clr-pri-dis};
+ --bit-chg-clr-dis-text: #{$clr-pri-dis-text};
+ --bit-chg-clr-focus: #{$clr-pri-focus};
}
.bit-chg-sec {
@@ -353,6 +367,9 @@
--bit-chg-clr-bg: #{$clr-bg-sec};
--bit-chg-clr-brd: #{$clr-brd-sec};
--bit-chg-clr-hover: #{$clr-sec-hover};
+ --bit-chg-clr-dis: #{$clr-sec-dis};
+ --bit-chg-clr-dis-text: #{$clr-sec-dis-text};
+ --bit-chg-clr-focus: #{$clr-sec-focus};
}
.bit-chg-ter {
@@ -360,6 +377,9 @@
--bit-chg-clr-bg: #{$clr-bg-sec};
--bit-chg-clr-brd: #{$clr-brd-ter};
--bit-chg-clr-hover: #{$clr-ter-hover};
+ --bit-chg-clr-dis: #{$clr-ter-dis};
+ --bit-chg-clr-dis-text: #{$clr-ter-dis-text};
+ --bit-chg-clr-focus: #{$clr-ter-focus};
}
.bit-chg-inf {
@@ -367,6 +387,9 @@
--bit-chg-clr-bg: #{$clr-bg-sec};
--bit-chg-clr-brd: #{$clr-inf};
--bit-chg-clr-hover: #{$clr-inf-hover};
+ --bit-chg-clr-dis: #{$clr-inf-dis};
+ --bit-chg-clr-dis-text: #{$clr-inf-dis-text};
+ --bit-chg-clr-focus: #{$clr-inf-focus};
}
.bit-chg-suc {
@@ -374,6 +397,9 @@
--bit-chg-clr-bg: #{$clr-bg-sec};
--bit-chg-clr-brd: #{$clr-suc};
--bit-chg-clr-hover: #{$clr-suc-hover};
+ --bit-chg-clr-dis: #{$clr-suc-dis};
+ --bit-chg-clr-dis-text: #{$clr-suc-dis-text};
+ --bit-chg-clr-focus: #{$clr-suc-focus};
}
.bit-chg-wrn {
@@ -381,6 +407,9 @@
--bit-chg-clr-brd: #{$clr-wrn};
--bit-chg-clr-hover: #{$clr-wrn-hover};
--bit-chg-clr-bg: #{$clr-bg-sec};
+ --bit-chg-clr-dis: #{$clr-wrn-dis};
+ --bit-chg-clr-dis-text: #{$clr-wrn-dis-text};
+ --bit-chg-clr-focus: #{$clr-wrn-focus};
}
.bit-chg-swr {
@@ -388,6 +417,9 @@
--bit-chg-clr-bg: #{$clr-bg-sec};
--bit-chg-clr-brd: #{$clr-swr};
--bit-chg-clr-hover: #{$clr-swr-hover};
+ --bit-chg-clr-dis: #{$clr-swr-dis};
+ --bit-chg-clr-dis-text: #{$clr-swr-dis-text};
+ --bit-chg-clr-focus: #{$clr-swr-focus};
}
.bit-chg-err {
@@ -395,6 +427,9 @@
--bit-chg-clr-brd: #{$clr-err};
--bit-chg-clr-hover: #{$clr-err-hover};
--bit-chg-clr-bg: #{$clr-bg-sec};
+ --bit-chg-clr-dis: #{$clr-err-dis};
+ --bit-chg-clr-dis-text: #{$clr-err-dis-text};
+ --bit-chg-clr-focus: #{$clr-err-focus};
}
.bit-chg-pbg {
@@ -402,6 +437,9 @@
--bit-chg-clr-bg: #{$clr-fg-pri};
--bit-chg-clr-brd: #{$clr-bg-pri};
--bit-chg-clr-hover: #{$clr-bg-pri-hover};
+ --bit-chg-clr-dis: #{$clr-bg-pri-dis};
+ --bit-chg-clr-dis-text: #{$clr-bg-pri-dis-text};
+ --bit-chg-clr-focus: #{$clr-bg-pri-focus};
}
.bit-chg-sbg {
@@ -409,6 +447,9 @@
--bit-chg-clr-bg: #{$clr-fg-pri};
--bit-chg-clr-brd: #{$clr-bg-sec};
--bit-chg-clr-hover: #{$clr-bg-sec-hover};
+ --bit-chg-clr-dis: #{$clr-bg-sec-dis};
+ --bit-chg-clr-dis-text: #{$clr-bg-sec-dis-text};
+ --bit-chg-clr-focus: #{$clr-bg-sec-focus};
}
.bit-chg-tbg {
@@ -416,6 +457,9 @@
--bit-chg-clr-brd: #{$clr-bg-ter};
--bit-chg-clr-hover: #{$clr-bg-ter-hover};
--bit-chg-clr-bg: #{$clr-fg-pri};
+ --bit-chg-clr-dis: #{$clr-bg-ter-dis};
+ --bit-chg-clr-dis-text: #{$clr-bg-ter-dis-text};
+ --bit-chg-clr-focus: #{$clr-bg-ter-focus};
}
.bit-chg-pfg {
@@ -423,6 +467,9 @@
--bit-chg-clr-bg: #{$clr-bg-sec};
--bit-chg-clr-brd: #{$clr-fg-pri};
--bit-chg-clr-hover: #{$clr-fg-pri-hover};
+ --bit-chg-clr-dis: #{$clr-fg-pri-dis};
+ --bit-chg-clr-dis-text: #{$clr-fg-pri-dis-text};
+ --bit-chg-clr-focus: #{$clr-fg-pri-focus};
}
.bit-chg-sfg {
@@ -430,6 +477,9 @@
--bit-chg-clr-brd: #{$clr-fg-sec};
--bit-chg-clr-hover: #{$clr-fg-sec-hover};
--bit-chg-clr-bg: #{$clr-bg-sec};
+ --bit-chg-clr-dis: #{$clr-fg-sec-dis};
+ --bit-chg-clr-dis-text: #{$clr-fg-sec-dis-text};
+ --bit-chg-clr-focus: #{$clr-fg-sec-focus};
}
.bit-chg-tfg {
@@ -437,6 +487,9 @@
--bit-chg-clr-bg: #{$clr-bg-sec};
--bit-chg-clr-brd: #{$clr-fg-ter};
--bit-chg-clr-hover: #{$clr-fg-ter-hover};
+ --bit-chg-clr-dis: #{$clr-fg-ter-dis};
+ --bit-chg-clr-dis-text: #{$clr-fg-ter-dis-text};
+ --bit-chg-clr-focus: #{$clr-fg-ter-focus};
}
.bit-chg-pbr {
@@ -444,6 +497,9 @@
--bit-chg-clr-brd: #{$clr-brd-pri};
--bit-chg-clr-hover: #{$clr-brd-pri-hover};
--bit-chg-clr-bg: #{$clr-bg-sec};
+ --bit-chg-clr-dis: #{$clr-brd-pri-dis};
+ --bit-chg-clr-dis-text: #{$clr-brd-pri-dis-text};
+ --bit-chg-clr-focus: #{$clr-brd-pri-focus};
}
.bit-chg-sbr {
@@ -451,6 +507,9 @@
--bit-chg-clr-bg: #{$clr-bg-sec};
--bit-chg-clr-brd: #{$clr-brd-sec};
--bit-chg-clr-hover: #{$clr-brd-sec-hover};
+ --bit-chg-clr-dis: #{$clr-brd-sec-dis};
+ --bit-chg-clr-dis-text: #{$clr-brd-sec-dis-text};
+ --bit-chg-clr-focus: #{$clr-brd-sec-focus};
}
.bit-chg-tbr {
@@ -458,6 +517,9 @@
--bit-chg-clr-bg: #{$clr-bg-sec};
--bit-chg-clr-brd: #{$clr-brd-ter};
--bit-chg-clr-hover: #{$clr-brd-ter-hover};
+ --bit-chg-clr-dis: #{$clr-brd-ter-dis};
+ --bit-chg-clr-dis-text: #{$clr-brd-ter-dis-text};
+ --bit-chg-clr-focus: #{$clr-brd-ter-focus};
}
.bit-chg-sm {
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.scss
index 969a882103..18b7f5c122 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.scss
@@ -44,6 +44,20 @@
.bit-ctp-icn {
border-color: $clr-err;
}
+
+ &.bit-ctp-und .bit-ctp-wrp {
+ // Underlined variant drops the icon border, so surface the error cue on the
+ // underline itself to keep the invalid state visible before focus.
+ border-bottom-color: $clr-err;
+ }
+
+ &.bit-ctp-foc:not(.bit-ctp-und) .bit-ctp-icn {
+ @include focus-ring($clr-err-focus);
+ }
+
+ &.bit-ctp-und.bit-ctp-foc .bit-ctp-icn {
+ @include focus-underline-ring($clr-err-focus);
+ }
}
}
@@ -121,14 +135,9 @@
}
}
-.bit-ctp-foc {
- .bit-ctp-icn::after {
- content: "";
- position: absolute;
- pointer-events: none;
- inset: spacing(-0.125);
- border-radius: $shp-border-radius;
- border: spacing(0.25) $shp-border-style $clr-pri;
+.bit-ctp-foc:not(.bit-ctp-und) {
+ .bit-ctp-icn {
+ @include focus-ring;
}
}
@@ -152,10 +161,8 @@
}
&.bit-ctp-foc {
- .bit-ctp-icn::after {
- border: none;
- border-radius: 0;
- border-bottom: spacing(0.25) $shp-border-style $clr-brd-pri;
+ .bit-ctp-icn {
+ @include focus-underline-ring;
}
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.scss
index 6d8fc51efa..a45adb4f16 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.scss
@@ -50,6 +50,20 @@
.bit-dtp-icn {
border-color: $clr-err;
}
+
+ &.bit-dtp-und .bit-dtp-wrp {
+ // Underlined variant drops the icon border, so surface the error cue on the
+ // underline itself to keep the invalid state visible before focus.
+ border-bottom-color: $clr-err;
+ }
+
+ &.bit-dtp-foc:not(.bit-dtp-und) .bit-dtp-icn {
+ @include focus-ring($clr-err-focus);
+ }
+
+ &.bit-dtp-und.bit-dtp-foc .bit-dtp-icn {
+ @include focus-underline-ring($clr-err-focus);
+ }
}
&.bit-rtl {
@@ -162,14 +176,9 @@
}
}
-.bit-dtp-foc {
- .bit-dtp-icn::after {
- content: "";
- position: absolute;
- pointer-events: none;
- inset: spacing(-0.125);
- border-radius: $shp-border-radius;
- border: spacing(0.25) $shp-border-style $clr-pri;
+.bit-dtp-foc:not(.bit-dtp-und) {
+ .bit-dtp-icn {
+ @include focus-ring;
}
}
@@ -655,9 +664,8 @@
}
&:focus {
+ @include focus-underline-ring;
border: none;
- border-bottom: $shp-border-width $shp-border-style $clr-brd-pri-active;
- outline: none;
}
&:disabled {
@@ -722,10 +730,8 @@
}
&.bit-dtp-foc {
- .bit-dtp-icn::after {
- border: none;
- border-radius: 0;
- border-bottom: spacing(0.25) $shp-border-style $clr-brd-pri;
+ .bit-dtp-icn {
+ @include focus-underline-ring;
}
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.scss
index 0add6f91e9..3f1124ff79 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.scss
@@ -47,9 +47,23 @@
}
&.bit-inv {
- .bit-dtrp-icp {
+ .bit-dtrp-icn {
border-color: $clr-err;
}
+
+ &.bit-dtrp-und .bit-dtrp-wrp {
+ // Underlined variant drops the icon border, so surface the error cue on the
+ // underline itself to keep the invalid state visible before focus.
+ border-bottom-color: $clr-err;
+ }
+
+ &.bit-dtrp-foc:not(.bit-dtrp-und) .bit-dtrp-icn {
+ @include focus-ring($clr-err-focus);
+ }
+
+ &.bit-dtrp-und.bit-dtrp-foc .bit-dtrp-icn {
+ @include focus-underline-ring($clr-err-focus);
+ }
}
&.bit-rtl {
@@ -162,14 +176,9 @@
}
}
-.bit-dtrp-foc {
- .bit-dtrp-icn::after {
- content: "";
- position: absolute;
- pointer-events: none;
- inset: spacing(-0.125);
- border-radius: $shp-border-radius;
- border: spacing(0.25) $shp-border-style $clr-pri;
+.bit-dtrp-foc:not(.bit-dtrp-und) {
+ .bit-dtrp-icn {
+ @include focus-ring;
}
}
@@ -688,9 +697,8 @@
}
&:focus {
+ @include focus-underline-ring;
border: none;
- outline: none;
- border-bottom: $shp-border-width $shp-border-style $clr-brd-pri-active;
}
&:disabled {
@@ -766,10 +774,8 @@
}
&.bit-dtrp-foc {
- .bit-dtrp-icn::after {
- border: none;
- border-radius: 0;
- border-bottom: spacing(0.25) $shp-border-style $clr-brd-pri;
+ .bit-dtrp-icn {
+ @include focus-underline-ring;
}
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.scss
index c1dddd43b9..424eb27b87 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.scss
@@ -31,8 +31,19 @@
}
&:focus {
- &::after {
- border-color: $clr-err;
+ @include focus-ring($clr-err-focus);
+ }
+ }
+
+ &.bit-drp-nbd {
+ .bit-drp-wrp {
+ &:focus {
+ outline: none;
+ box-shadow: none;
+ }
+
+ &:focus-visible {
+ @include focus-underline-ring($clr-err-focus);
}
}
}
@@ -54,12 +65,9 @@
&:focus {
color: $clr-fg-dis;
border-color: $clr-brd-dis;
-
- &::after {
- width: 0;
- height: 0;
- border: none;
- }
+ // Suppress the focus ring while disabled, including the forced-colors outline.
+ outline: none;
+ box-shadow: none;
}
}
@@ -105,9 +113,16 @@
}
&:focus {
- &::after {
- border: none;
- }
+ // Suppress the global focus ring on the no-border variant; the underline-ring
+ // below still applies for keyboard focus via :focus-visible.
+ outline: none;
+ box-shadow: none;
+ }
+
+ &:focus-visible {
+ // No-border variant: keep the no-border look but use an inset underline-style
+ // ring as a keyboard-only, high-contrast focus cue (WCAG 2.4.7 / 2.4.13).
+ @include focus-underline-ring(var(--bit-drp-clr-focus));
}
}
}
@@ -170,14 +185,7 @@
color: $clr-fg-pri;
}
- &::after {
- content: "";
- position: absolute;
- pointer-events: none;
- inset: spacing(-0.125);
- border-radius: $shp-border-radius;
- border: spacing(0.25) $shp-border-style var(--bit-drp-clr);
- }
+ @include focus-ring(var(--bit-drp-clr-focus));
}
}
@@ -805,100 +813,117 @@
--bit-drp-clr: #{$clr-pri};
--bit-drp-clr-hover: #{$clr-pri-hover};
--bit-drp-clr-text: #{$clr-pri-text};
+ --bit-drp-clr-focus: #{$clr-pri-focus};
}
.bit-drp-sec {
--bit-drp-clr: #{$clr-sec};
--bit-drp-clr-hover: #{$clr-sec-hover};
--bit-drp-clr-text: #{$clr-sec-text};
+ --bit-drp-clr-focus: #{$clr-sec-focus};
}
.bit-drp-ter {
--bit-drp-clr: #{$clr-ter};
--bit-drp-clr-hover: #{$clr-ter-hover};
--bit-drp-clr-text: #{$clr-ter-text};
+ --bit-drp-clr-focus: #{$clr-ter-focus};
}
.bit-drp-inf {
--bit-drp-clr: #{$clr-inf};
--bit-drp-clr-hover: #{$clr-inf-hover};
--bit-drp-clr-text: #{$clr-inf-text};
+ --bit-drp-clr-focus: #{$clr-inf-focus};
}
.bit-drp-suc {
--bit-drp-clr: #{$clr-suc};
--bit-drp-clr-hover: #{$clr-suc-hover};
--bit-drp-clr-text: #{$clr-suc-text};
+ --bit-drp-clr-focus: #{$clr-suc-focus};
}
.bit-drp-wrn {
--bit-drp-clr: #{$clr-wrn};
--bit-drp-clr-hover: #{$clr-wrn-hover};
--bit-drp-clr-text: #{$clr-wrn-text};
+ --bit-drp-clr-focus: #{$clr-wrn-focus};
}
.bit-drp-swr {
--bit-drp-clr: #{$clr-swr};
--bit-drp-clr-hover: #{$clr-swr-hover};
--bit-drp-clr-text: #{$clr-swr-text};
+ --bit-drp-clr-focus: #{$clr-swr-focus};
}
.bit-drp-err {
--bit-drp-clr: #{$clr-err};
--bit-drp-clr-hover: #{$clr-err-hover};
--bit-drp-clr-text: #{$clr-err-text};
+ --bit-drp-clr-focus: #{$clr-err-focus};
}
.bit-drp-pbg {
--bit-drp-clr: #{$clr-bg-pri};
--bit-drp-clr-hover: #{$clr-bg-pri-hover};
--bit-drp-clr-text: #{$clr-fg-pri};
+ --bit-drp-clr-focus: #{$clr-bg-pri-focus};
}
.bit-drp-sbg {
--bit-drp-clr: #{$clr-bg-sec};
--bit-drp-clr-hover: #{$clr-bg-sec-hover};
--bit-drp-clr-text: #{$clr-fg-sec};
+ --bit-drp-clr-focus: #{$clr-bg-sec-focus};
}
.bit-drp-tbg {
--bit-drp-clr: #{$clr-bg-ter};
--bit-drp-clr-hover: #{$clr-bg-ter-hover};
--bit-drp-clr-text: #{$clr-fg-ter};
+ --bit-drp-clr-focus: #{$clr-bg-ter-focus};
}
.bit-drp-pfg {
--bit-drp-clr: #{$clr-fg-pri};
--bit-drp-clr-hover: #{$clr-fg-pri-hover};
--bit-drp-clr-text: #{$clr-bg-pri};
+ --bit-drp-clr-focus: #{$clr-fg-pri-focus};
}
.bit-drp-sfg {
--bit-drp-clr: #{$clr-fg-sec};
--bit-drp-clr-hover: #{$clr-fg-sec-hover};
--bit-drp-clr-text: #{$clr-bg-sec};
+ --bit-drp-clr-focus: #{$clr-fg-sec-focus};
}
.bit-drp-tfg {
--bit-drp-clr: #{$clr-fg-ter};
--bit-drp-clr-hover: #{$clr-fg-ter-hover};
--bit-drp-clr-text: #{$clr-bg-ter};
+ --bit-drp-clr-focus: #{$clr-fg-ter-focus};
}
.bit-drp-pbr {
--bit-drp-clr: #{$clr-brd-pri};
--bit-drp-clr-hover: #{$clr-brd-pri-hover};
--bit-drp-clr-text: #{$clr-fg-pri};
+ --bit-drp-clr-focus: #{$clr-brd-pri-focus};
}
.bit-drp-sbr {
--bit-drp-clr: #{$clr-brd-sec};
--bit-drp-clr-hover: #{$clr-brd-sec-hover};
--bit-drp-clr-text: #{$clr-fg-sec};
+ --bit-drp-clr-focus: #{$clr-brd-sec-focus};
}
.bit-drp-tbr {
--bit-drp-clr: #{$clr-brd-ter};
--bit-drp-clr-hover: #{$clr-brd-ter-hover};
--bit-drp-clr-text: #{$clr-fg-ter};
+ --bit-drp-clr-focus: #{$clr-brd-ter-focus};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/NumberField/BitNumberField.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/NumberField/BitNumberField.scss
index 19c1e37c05..63aa870255 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/NumberField/BitNumberField.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/NumberField/BitNumberField.scss
@@ -52,10 +52,8 @@
border-color: $clr-err;
}
- &.bit-nfl-fcs {
- .bit-nfl-cnt::after {
- border-color: $clr-err;
- }
+ &.bit-nfl-fcs .bit-nfl-cnt {
+ @include focus-ring($clr-err-focus);
}
}
@@ -259,13 +257,8 @@
}
.bit-nfl-fcs {
- .bit-nfl-cnt::after {
- content: "";
- position: absolute;
- pointer-events: none;
- inset: spacing(-0.125);
- border-radius: $shp-border-radius;
- border: spacing(0.25) $shp-border-style $clr-pri;
+ .bit-nfl-cnt {
+ @include focus-ring;
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/OtpInput/BitOtpInput.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/OtpInput/BitOtpInput.scss
index 120f6352fd..ebbdfdea11 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/OtpInput/BitOtpInput.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/OtpInput/BitOtpInput.scss
@@ -23,6 +23,10 @@
.bit-otp-inp {
outline: none;
border-color: $clr-err;
+
+ &:focus-visible {
+ @include focus-ring($clr-err-focus);
+ }
}
}
}
@@ -65,9 +69,8 @@
-webkit-appearance: none
}
- &:focus {
- outline: none;
- border: spacing(0.25) $shp-border-style $clr-pri;
+ &:focus-visible {
+ @include focus-ring;
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.scss
index d218bd57eb..64b7e8a17e 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.scss
@@ -12,42 +12,42 @@
pointer-events: none;
.bit-srb-cnt {
- color: $clr-fg-dis;
- border-color: $clr-brd-dis;
- background-color: $clr-bg-dis;
+ color: var(--bit-srb-clr-dis-text);
+ border-color: var(--bit-srb-clr-dis);
+ background-color: var(--bit-srb-clr-dis);
}
.bit-srb-iwp {
i {
- color: $clr-fg-dis;
+ color: var(--bit-srb-clr-dis-text);
}
}
.bit-srb-pre,
.bit-srb-suf {
- color: $clr-fg-dis;
+ color: var(--bit-srb-clr-dis-text);
}
@media (hover: hover) {
&:hover {
.bit-srb-cnt {
- border-color: $clr-brd-dis;
+ border-color: var(--bit-srb-clr-dis);
}
.bit-srb-iwp {
i {
- color: $clr-fg-dis;
+ color: var(--bit-srb-clr-dis-text);
}
}
}
}
.bit-srb-sbn {
- border-color: $clr-brd-dis;
- background-color: $clr-bg-dis;
+ border-color: var(--bit-srb-clr-dis);
+ background-color: var(--bit-srb-clr-dis);
i {
- color: $clr-fg-dis;
+ color: var(--bit-srb-clr-dis-text);
}
}
@@ -84,13 +84,14 @@
}
&.bit-srb-foc {
- .bit-srb-cnt::after {
- border-color: $clr-err;
+ &:not(.bit-srb-und) .bit-srb-cnt {
+ @include focus-ring($clr-err-focus);
}
&.bit-srb-und {
- .bit-srb-cnt::after {
- border-color: $clr-err;
+ .bit-srb-cnt,
+ .bit-srb-sbn {
+ @include focus-underline-ring($clr-err-focus);
}
}
}
@@ -111,7 +112,7 @@
&.bit-srb-foc {
.bit-srb-sbn {
- border-color: var(--bit-srb-clr);
+ border-color: var(--bit-srb-clr-focus);
}
}
@@ -343,35 +344,27 @@
}
.bit-srb-sbn {
- border-color: var(--bit-srb-clr);
+ border-color: var(--bit-srb-clr-focus);
}
- .bit-srb-cnt::after {
- content: "";
- position: absolute;
- pointer-events: none;
- inset: spacing(-0.125);
- border-radius: $shp-border-radius;
- border: spacing(0.25) $shp-border-style var(--bit-srb-clr);
+ &:not(.bit-srb-und) .bit-srb-cnt {
+ @include focus-ring(var(--bit-srb-clr-focus));
}
&.bit-srb-und {
- .bit-srb-cnt::after,
- .bit-srb-sbn::after {
- content: "";
- border: none;
- position: absolute;
- pointer-events: none;
- inset: spacing(-0.125);
- border-bottom: spacing(0.25) $shp-border-style var(--bit-srb-clr);
+ .bit-srb-cnt,
+ .bit-srb-sbn {
+ @include focus-underline-ring(var(--bit-srb-clr-focus));
}
}
}
.bit-srb-nbr {
- .bit-srb-cnt,
- .bit-srb-cnt::after {
+ .bit-srb-cnt {
border: none;
+
+ // Keep box-shadow unset here so the focus-ring mixin applied via .bit-srb-foc
+ // (and any :focus / :focus-visible states) can still render the focus indicator.
}
.bit-srb-sbn {
@@ -554,6 +547,9 @@
--bit-srb-clr-txt: #{$clr-pri-text};
--bit-srb-clr-hover: #{$clr-pri-hover};
--bit-srb-clr-active: #{$clr-pri-active};
+ --bit-srb-clr-focus: #{$clr-pri-focus};
+ --bit-srb-clr-dis: #{$clr-pri-dis};
+ --bit-srb-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-srb-sec {
@@ -561,6 +557,9 @@
--bit-srb-clr-txt: #{$clr-sec-text};
--bit-srb-clr-hover: #{$clr-sec-hover};
--bit-srb-clr-active: #{$clr-sec-active};
+ --bit-srb-clr-focus: #{$clr-sec-focus};
+ --bit-srb-clr-dis: #{$clr-sec-dis};
+ --bit-srb-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-srb-ter {
@@ -568,6 +567,9 @@
--bit-srb-clr-txt: #{$clr-ter-text};
--bit-srb-clr-hover: #{$clr-ter-hover};
--bit-srb-clr-active: #{$clr-ter-active};
+ --bit-srb-clr-focus: #{$clr-ter-focus};
+ --bit-srb-clr-dis: #{$clr-ter-dis};
+ --bit-srb-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-srb-inf {
@@ -575,6 +577,9 @@
--bit-srb-clr-txt: #{$clr-inf-text};
--bit-srb-clr-hover: #{$clr-inf-hover};
--bit-srb-clr-active: #{$clr-inf-active};
+ --bit-srb-clr-focus: #{$clr-inf-focus};
+ --bit-srb-clr-dis: #{$clr-inf-dis};
+ --bit-srb-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-srb-suc {
@@ -582,6 +587,9 @@
--bit-srb-clr-txt: #{$clr-suc-text};
--bit-srb-clr-hover: #{$clr-suc-hover};
--bit-srb-clr-active: #{$clr-suc-active};
+ --bit-srb-clr-focus: #{$clr-suc-focus};
+ --bit-srb-clr-dis: #{$clr-suc-dis};
+ --bit-srb-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-srb-wrn {
@@ -589,6 +597,9 @@
--bit-srb-clr-txt: #{$clr-wrn-text};
--bit-srb-clr-hover: #{$clr-wrn-hover};
--bit-srb-clr-active: #{$clr-wrn-active};
+ --bit-srb-clr-focus: #{$clr-wrn-focus};
+ --bit-srb-clr-dis: #{$clr-wrn-dis};
+ --bit-srb-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-srb-swr {
@@ -596,6 +607,9 @@
--bit-srb-clr-txt: #{$clr-swr-text};
--bit-srb-clr-hover: #{$clr-swr-hover};
--bit-srb-clr-active: #{$clr-swr-active};
+ --bit-srb-clr-focus: #{$clr-swr-focus};
+ --bit-srb-clr-dis: #{$clr-swr-dis};
+ --bit-srb-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-srb-err {
@@ -603,6 +617,9 @@
--bit-srb-clr-txt: #{$clr-err-text};
--bit-srb-clr-hover: #{$clr-err-hover};
--bit-srb-clr-active: #{$clr-err-active};
+ --bit-srb-clr-focus: #{$clr-err-focus};
+ --bit-srb-clr-dis: #{$clr-err-dis};
+ --bit-srb-clr-dis-text: #{$clr-err-dis-text};
}
.bit-srb-pbg {
@@ -610,6 +627,9 @@
--bit-srb-clr-txt: #{$clr-fg-pri};
--bit-srb-clr-hover: #{$clr-bg-pri-hover};
--bit-srb-clr-active: #{$clr-bg-pri-active};
+ --bit-srb-clr-focus: #{$clr-bg-pri-focus};
+ --bit-srb-clr-dis: #{$clr-bg-pri-dis};
+ --bit-srb-clr-dis-text: #{$clr-bg-pri-dis-text};
}
.bit-srb-sbg {
@@ -617,6 +637,9 @@
--bit-srb-clr-txt: #{$clr-fg-pri};
--bit-srb-clr-hover: #{$clr-bg-sec-hover};
--bit-srb-clr-active: #{$clr-bg-sec-active};
+ --bit-srb-clr-focus: #{$clr-bg-sec-focus};
+ --bit-srb-clr-dis: #{$clr-bg-sec-dis};
+ --bit-srb-clr-dis-text: #{$clr-bg-sec-dis-text};
}
.bit-srb-tbg {
@@ -624,6 +647,9 @@
--bit-srb-clr-txt: #{$clr-fg-pri};
--bit-srb-clr-hover: #{$clr-bg-ter-hover};
--bit-srb-clr-active: #{$clr-bg-ter-active};
+ --bit-srb-clr-focus: #{$clr-bg-ter-focus};
+ --bit-srb-clr-dis: #{$clr-bg-ter-dis};
+ --bit-srb-clr-dis-text: #{$clr-bg-ter-dis-text};
}
.bit-srb-pfg {
@@ -631,6 +657,9 @@
--bit-srb-clr-txt: #{$clr-bg-pri};
--bit-srb-clr-hover: #{$clr-fg-pri-hover};
--bit-srb-clr-active: #{$clr-fg-pri-active};
+ --bit-srb-clr-focus: #{$clr-fg-pri-focus};
+ --bit-srb-clr-dis: #{$clr-fg-pri-dis};
+ --bit-srb-clr-dis-text: #{$clr-fg-pri-dis-text};
}
.bit-srb-sfg {
@@ -638,6 +667,9 @@
--bit-srb-clr-txt: #{$clr-bg-pri};
--bit-srb-clr-hover: #{$clr-fg-sec-hover};
--bit-srb-clr-active: #{$clr-fg-sec-active};
+ --bit-srb-clr-focus: #{$clr-fg-sec-focus};
+ --bit-srb-clr-dis: #{$clr-fg-sec-dis};
+ --bit-srb-clr-dis-text: #{$clr-fg-sec-dis-text};
}
.bit-srb-tfg {
@@ -645,6 +677,9 @@
--bit-srb-clr-txt: #{$clr-bg-pri};
--bit-srb-clr-hover: #{$clr-fg-ter-hover};
--bit-srb-clr-active: #{$clr-fg-ter-active};
+ --bit-srb-clr-focus: #{$clr-fg-ter-focus};
+ --bit-srb-clr-dis: #{$clr-fg-ter-dis};
+ --bit-srb-clr-dis-text: #{$clr-fg-ter-dis-text};
}
.bit-srb-pbr {
@@ -652,6 +687,9 @@
--bit-srb-clr-txt: #{$clr-fg-pri};
--bit-srb-clr-hover: #{$clr-brd-pri-hover};
--bit-srb-clr-active: #{$clr-brd-pri-active};
+ --bit-srb-clr-focus: #{$clr-brd-pri-focus};
+ --bit-srb-clr-dis: #{$clr-brd-pri-dis};
+ --bit-srb-clr-dis-text: #{$clr-brd-pri-dis-text};
}
.bit-srb-sbr {
@@ -659,6 +697,9 @@
--bit-srb-clr-txt: #{$clr-fg-pri};
--bit-srb-clr-hover: #{$clr-brd-sec-hover};
--bit-srb-clr-active: #{$clr-brd-sec-active};
+ --bit-srb-clr-focus: #{$clr-brd-sec-focus};
+ --bit-srb-clr-dis: #{$clr-brd-sec-dis};
+ --bit-srb-clr-dis-text: #{$clr-brd-sec-dis-text};
}
.bit-srb-tbr {
@@ -666,4 +707,7 @@
--bit-srb-clr-txt: #{$clr-fg-pri};
--bit-srb-clr-hover: #{$clr-brd-ter-hover};
--bit-srb-clr-active: #{$clr-brd-ter-active};
+ --bit-srb-clr-focus: #{$clr-brd-ter-focus};
+ --bit-srb-clr-dis: #{$clr-brd-ter-dis};
+ --bit-srb-clr-dis-text: #{$clr-brd-ter-dis-text};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Slider/BitSlider.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Slider/BitSlider.scss
index 1e82079532..c4b35b0db6 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Slider/BitSlider.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Slider/BitSlider.scss
@@ -15,11 +15,40 @@ $BgHover: $clr-pri-light;
background: none;
&:focus {
- outline: none;
+ outline: none; // The visible focus indicator lives on the thumb pseudo-element below.
}
}
- /**/
- /*webkit*/
+ /* webkit focus styles */
+ input:focus-visible::-webkit-slider-thumb,
+ input:focus::-webkit-slider-thumb {
+ border-color: $clr-pri-focus;
+
+ // Reference the global focus-ring shadow token so forced-colors and theme-level
+ // overrides (see forced-colors.fluent.scss) propagate to the slider thumb.
+ box-shadow: var(--bit-shd-focus-ring,
+ 0 0 0 #{$shp-focus-ring-offset} #{$clr-bg-pri-focus},
+ 0 0 0 calc(#{$shp-focus-ring-offset} + #{$shp-focus-ring-width}) #{$clr-pri-focus});
+
+ // In Windows High Contrast / forced-colors mode box-shadows are stripped, which would leave
+ // the focused thumb with no visible indicator. Restore a real outline (matching focus-ring).
+ @media (forced-colors: active) {
+ outline: #{$shp-focus-ring-width} solid Highlight;
+ }
+ }
+
+ input:focus-visible::-moz-range-thumb,
+ input:focus::-moz-range-thumb {
+ border-color: $clr-pri-focus;
+ box-shadow: var(--bit-shd-focus-ring,
+ 0 0 0 #{$shp-focus-ring-offset} #{$clr-bg-pri-focus},
+ 0 0 0 calc(#{$shp-focus-ring-offset} + #{$shp-focus-ring-width}) #{$clr-pri-focus});
+
+ @media (forced-colors: active) {
+ outline: #{$shp-focus-ring-width} solid Highlight;
+ }
+ }
+
+ /* webkit thumb */
input::-webkit-slider-thumb {
width: spacing(2);
height: spacing(2);
@@ -43,8 +72,7 @@ $BgHover: $clr-pri-light;
input::-webkit-slider-runnable-track {
background: linear-gradient($BgTrack, $BgTrack) 0/var(--sx) 100% no-repeat, $BgDefult;
}
- /**/
- /*mozilla*/
+ /* mozilla */
input::-moz-range-thumb {
width: spacing(2);
height: spacing(2);
@@ -67,8 +95,7 @@ $BgHover: $clr-pri-light;
input::-moz-range-track {
background: linear-gradient($BgTrack, $BgTrack) 0/var(--sx) 100% no-repeat, $BgDefult;
}
- /**/
- /*ms*/
+ /* ms */
input::-ms-fill-upper {
background: transparent;
border-color: transparent;
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TagsInput/BitTagsInput.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TagsInput/BitTagsInput.scss
index 4f9dcf4c00..d7aebfb981 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TagsInput/BitTagsInput.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TagsInput/BitTagsInput.scss
@@ -23,6 +23,12 @@
.bit-tgi-cnt {
border-color: $clr-err;
}
+
+ &.bit-tgi-fcs .bit-tgi-cnt {
+ @include focus-ring($clr-err-focus);
+
+ border-color: $clr-err-focus;
+ }
}
@media (hover: hover) {
@@ -42,19 +48,9 @@
.bit-tgi-fcs {
.bit-tgi-cnt {
- border-color: $clr-pri;
- outline: none;
-
- &::after {
- content: "";
- height: 2px;
- left: -1px;
- right: -1px;
- bottom: -1px;
- position: absolute;
- background-color: $clr-pri;
- border-radius: 0 0 $shp-border-radius $shp-border-radius;
- }
+ @include focus-ring;
+
+ border-color: $clr-pri-focus;
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TextField/BitTextField.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TextField/BitTextField.scss
index 0075196c42..4f8d713a31 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TextField/BitTextField.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TextField/BitTextField.scss
@@ -69,13 +69,13 @@
}
&.bit-tfl-fcs {
- .bit-tfl-fgp::after {
- border-color: $clr-err;
+ &:not(.bit-tfl-und) .bit-tfl-fgp {
+ @include focus-ring($clr-err-focus);
}
&.bit-tfl-und {
- .bit-tfl-wrp::after {
- border-color: $clr-err;
+ .bit-tfl-wrp {
+ @include focus-underline-ring($clr-err-focus);
}
}
}
@@ -196,7 +196,7 @@
font-family: $tg-font-family;
background-color: transparent;
border-radius: 0 spacing(0.125) spacing(0.125) 0;
- min-height: calc(var(--bit-spa-scaling-factor) * 3.75);
+ min-height: spacing(3.75);
i {
width: unset;
@@ -313,26 +313,15 @@
color: var(--bit-tfl-clr);
}
- .bit-tfl-fgp::after {
- content: "";
- position: absolute;
- pointer-events: none;
- inset: spacing(-0.125);
- border-radius: $shp-border-radius;
- border: spacing(0.25) $shp-border-style var(--bit-tfl-clr);
+ // The underlined variant is excluded so it gets only the bottom-rule treatment below - applying
+ // the ring mixin and negating its box-shadow would still leave its forced-colors outline behind.
+ &:not(.bit-tfl-und) .bit-tfl-fgp {
+ @include focus-ring(var(--bit-tfl-clr-focus));
}
&.bit-tfl-und {
- .bit-tfl-fgp::after {
- border: none;
- }
-
- .bit-tfl-wrp::after {
- content: "";
- position: absolute;
- pointer-events: none;
- inset: spacing(-0.125);
- border-bottom: spacing(0.25) $shp-border-style var(--bit-tfl-clr);
+ .bit-tfl-wrp {
+ @include focus-underline-ring(var(--bit-tfl-clr-focus));
}
}
}
@@ -427,70 +416,87 @@
.bit-tfl-pri {
--bit-tfl-clr: #{$clr-pri};
+ --bit-tfl-clr-focus: #{$clr-pri-focus};
}
.bit-tfl-sec {
--bit-tfl-clr: #{$clr-sec};
+ --bit-tfl-clr-focus: #{$clr-sec-focus};
}
.bit-tfl-ter {
--bit-tfl-clr: #{$clr-ter};
+ --bit-tfl-clr-focus: #{$clr-ter-focus};
}
.bit-tfl-inf {
--bit-tfl-clr: #{$clr-inf};
+ --bit-tfl-clr-focus: #{$clr-inf-focus};
}
.bit-tfl-suc {
--bit-tfl-clr: #{$clr-suc};
+ --bit-tfl-clr-focus: #{$clr-suc-focus};
}
.bit-tfl-wrn {
--bit-tfl-clr: #{$clr-wrn};
+ --bit-tfl-clr-focus: #{$clr-wrn-focus};
}
.bit-tfl-swr {
--bit-tfl-clr: #{$clr-swr};
+ --bit-tfl-clr-focus: #{$clr-swr-focus};
}
.bit-tfl-err {
--bit-tfl-clr: #{$clr-err};
+ --bit-tfl-clr-focus: #{$clr-err-focus};
}
.bit-tfl-pbg {
--bit-tfl-clr: #{$clr-bg-pri};
+ --bit-tfl-clr-focus: #{$clr-bg-pri-focus};
}
.bit-tfl-sbg {
--bit-tfl-clr: #{$clr-bg-sec};
+ --bit-tfl-clr-focus: #{$clr-bg-sec-focus};
}
.bit-tfl-tbg {
--bit-tfl-clr: #{$clr-bg-ter};
+ --bit-tfl-clr-focus: #{$clr-bg-ter-focus};
}
.bit-tfl-pfg {
--bit-tfl-clr: #{$clr-fg-pri};
+ --bit-tfl-clr-focus: #{$clr-fg-pri-focus};
}
.bit-tfl-sfg {
--bit-tfl-clr: #{$clr-fg-sec};
+ --bit-tfl-clr-focus: #{$clr-fg-sec-focus};
}
.bit-tfl-tfg {
--bit-tfl-clr: #{$clr-fg-ter};
+ --bit-tfl-clr-focus: #{$clr-fg-ter-focus};
}
.bit-tfl-pbr {
--bit-tfl-clr: #{$clr-brd-pri};
+ --bit-tfl-clr-focus: #{$clr-brd-pri-focus};
}
.bit-tfl-sbr {
--bit-tfl-clr: #{$clr-brd-sec};
+ --bit-tfl-clr-focus: #{$clr-brd-sec-focus};
}
.bit-tfl-tbr {
--bit-tfl-clr: #{$clr-brd-ter};
+ --bit-tfl-clr-focus: #{$clr-brd-ter-focus};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.scss
index 881531c550..ab3d952d13 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.scss
@@ -41,6 +41,20 @@
.bit-tpc-icn {
border-color: $clr-err;
}
+
+ &.bit-tpc-und .bit-tpc-wrp {
+ // Underlined variant drops the icon border, so surface the error cue on the
+ // underline itself to keep the invalid state visible before focus.
+ border-bottom-color: $clr-err;
+ }
+
+ &.bit-tpc-foc:not(.bit-tpc-und) .bit-tpc-icn {
+ @include focus-ring($clr-err-focus);
+ }
+
+ &.bit-tpc-und.bit-tpc-foc .bit-tpc-icn {
+ @include focus-underline-ring($clr-err-focus);
+ }
}
&.bit-rtl {
@@ -128,14 +142,9 @@
}
}
-.bit-tpc-foc {
- .bit-tpc-icn::after {
- content: "";
- position: absolute;
- pointer-events: none;
- inset: spacing(-0.125);
- border-radius: $shp-border-radius;
- border: spacing(0.25) $shp-border-style $clr-pri;
+.bit-tpc-foc:not(.bit-tpc-und) {
+ .bit-tpc-icn {
+ @include focus-ring;
}
}
@@ -159,10 +168,8 @@
}
&.bit-tpc-foc {
- .bit-tpc-icn::after {
- border: none;
- border-radius: 0;
- border-bottom: spacing(0.25) $shp-border-style $clr-brd-pri;
+ .bit-tpc-icn {
+ @include focus-underline-ring;
}
}
}
@@ -281,9 +288,8 @@
}
&:focus {
+ @include focus-underline-ring;
border: none;
- border-bottom: $shp-border-width $shp-border-style $clr-brd-pri-active;
- outline: none;
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Toggle/BitToggle.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Toggle/BitToggle.scss
index 7fcabb560e..4bdccccb21 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Toggle/BitToggle.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Toggle/BitToggle.scss
@@ -48,6 +48,10 @@
&.bit-inv {
.bit-tgl-btn {
border-color: $clr-err;
+
+ &:focus-visible {
+ @include focus-ring($clr-err-focus);
+ }
}
.bit-tgl-stx {
@@ -120,6 +124,10 @@
border-radius: spacing(1.25);
border: $shp-border-width $shp-border-style $clr-brd-pri;
+ &:focus-visible {
+ @include focus-ring;
+ }
+
@media (hover: hover) {
&:hover {
border-color: $clr-brd-pri-hover;
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Layouts/Grid/BitGrid.scss b/src/BlazorUI/Bit.BlazorUI/Components/Layouts/Grid/BitGrid.scss
index 319e63626e..edda089fa8 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Layouts/Grid/BitGrid.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Layouts/Grid/BitGrid.scss
@@ -1,4 +1,4 @@
-@import "../../../Styles/functions.scss";
+@import "../../../Styles/functions.scss";
@import "../../../Styles/media-queries.scss";
.bit-grd {
@@ -13,7 +13,7 @@
.bit-grd-itm {
flex-basis: calc((100% / var(--columns) * var(--span)) - var(--spacing));
- $breakpoints: ( 'xs': $brk-sm-min, 'sm': $brk-md-min, 'md': $brk-lg-min, 'lg': $brk-xl-min, 'xl': $brk-xxl-min );
+ $breakpoints: ( 'xs': 0, 'sm': $mq-sm-min, 'md': $mq-md-min, 'lg': $mq-lg-min, 'xl': $mq-xl-min, 'xxl': $mq-xxl-min );
@each $name, $breakpoint in $breakpoints {
@media (min-width: $breakpoint) {
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Lists/Timeline/BitTimeline.scss b/src/BlazorUI/Bit.BlazorUI/Components/Lists/Timeline/BitTimeline.scss
index 26d1728e43..b343510fd6 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Lists/Timeline/BitTimeline.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Lists/Timeline/BitTimeline.scss
@@ -17,9 +17,9 @@
--bit-tln-dvd-top: 0;
--bit-tln-dvd-height: 100%;
--bit-tln-dvd-width: #{spacing(0.25)};
- --bit-tln-clr-dis: #{$clr-fg-dis};
- --bit-tln-clr-bg-dis: #{$clr-bg-dis};
- --bit-tln-clr-brd-dis: #{$clr-brd-dis};
+ --bit-tln-clr-dis: var(--bit-tln-clr-dis-text, #{$clr-fg-dis});
+ --bit-tln-clr-bg-dis: var(--bit-tln-clr-dis-bg, #{$clr-bg-dis});
+ --bit-tln-clr-brd-dis: var(--bit-tln-clr-dis-bg, #{$clr-bg-dis});
&.bit-dis {
cursor: default;
@@ -49,7 +49,7 @@
user-select: none;
pointer-events: none;
color: var(--bit-tln-clr-dis);
- --bit-tln-dot-ico-clr: #{$clr-fg-dis};
+ --bit-tln-dot-ico-clr: var(--bit-tln-clr-dis);
}
}
@@ -181,48 +181,64 @@
--bit-tln-clr: #{$clr-pri};
--bit-tln-clr-fg: #{$clr-pri-text};
--bit-tln-clr-ico: #{$clr-pri-text};
+ --bit-tln-clr-dis-bg: #{$clr-pri-dis};
+ --bit-tln-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-tln-sec, .bit-tln-ise {
--bit-tln-clr: #{$clr-sec};
--bit-tln-clr-fg: #{$clr-sec-text};
--bit-tln-clr-ico: #{$clr-sec-text};
+ --bit-tln-clr-dis-bg: #{$clr-sec-dis};
+ --bit-tln-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-tln-ter, .bit-tln-ite {
--bit-tln-clr: #{$clr-ter};
--bit-tln-clr-fg: #{$clr-ter-text};
--bit-tln-clr-ico: #{$clr-ter-text};
+ --bit-tln-clr-dis-bg: #{$clr-ter-dis};
+ --bit-tln-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-tln-inf, .bit-tln-iin {
--bit-tln-clr: #{$clr-inf};
--bit-tln-clr-fg: #{$clr-inf-text};
--bit-tln-clr-ico: #{$clr-inf-text};
+ --bit-tln-clr-dis-bg: #{$clr-inf-dis};
+ --bit-tln-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-tln-suc, .bit-tln-isu {
--bit-tln-clr: #{$clr-suc};
--bit-tln-clr-fg: #{$clr-suc-text};
--bit-tln-clr-ico: #{$clr-suc-text};
+ --bit-tln-clr-dis-bg: #{$clr-suc-dis};
+ --bit-tln-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-tln-wrn, .bit-tln-iwr {
--bit-tln-clr: #{$clr-wrn};
--bit-tln-clr-fg: #{$clr-wrn-text};
--bit-tln-clr-ico: #{$clr-wrn-text};
+ --bit-tln-clr-dis-bg: #{$clr-wrn-dis};
+ --bit-tln-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-tln-swr, .bit-tln-isw {
--bit-tln-clr: #{$clr-swr};
--bit-tln-clr-fg: #{$clr-swr-text};
--bit-tln-clr-ico: #{$clr-swr-text};
+ --bit-tln-clr-dis-bg: #{$clr-swr-dis};
+ --bit-tln-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-tln-err, .bit-tln-ier {
--bit-tln-clr: #{$clr-err};
--bit-tln-clr-fg: #{$clr-err-text};
--bit-tln-clr-ico: #{$clr-err-text};
+ --bit-tln-clr-dis-bg: #{$clr-err-dis};
+ --bit-tln-clr-dis-text: #{$clr-err-dis-text};
}
.bit-tln-sm, .bit-tln-ism {
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.scss b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.scss
index 49baafcb2e..b49c8bc608 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.scss
@@ -67,6 +67,10 @@
background-color: $clr-bg-pri-hover;
}
}
+
+ &:focus-visible {
+ @include focus-ring;
+ }
}
.bit-brc-itm,
@@ -88,6 +92,10 @@
background-color: $clr-bg-pri-hover;
}
}
+
+ &:focus-visible {
+ @include focus-ring;
+ }
}
.bit-brc-rvi {
@@ -146,6 +154,11 @@
line-height: spacing(2.5);
background-color: transparent;
border: $shp-border-width $shp-border-style transparent;
+ border-radius: $shp-border-radius;
+
+ &:focus-visible {
+ @include focus-ring;
+ }
}
.bit-brc-ofi {
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.scss b/src/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.scss
index face6d7303..965cac30e4 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.scss
@@ -50,6 +50,11 @@
justify-content: center;
background-color: transparent;
padding: spacing(1.0) spacing(1.5);
+ border-radius: $shp-border-radius;
+
+ &:focus-visible {
+ @include focus-ring;
+ }
}
.bit-drm-trn {
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss
index 07576906f2..b1a1954607 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss
@@ -9,7 +9,7 @@
pointer-events: none;
.bit-nbr-itm {
- color: $clr-fg-dis;
+ color: var(--bit-nbr-clr-dis, var(--bit-nbr-clr, currentColor));
}
}
}
@@ -64,75 +64,92 @@
}
.bit-nbr-dis {
- color: $clr-fg-dis;
+ color: var(--bit-nbr-clr-dis, var(--bit-nbr-clr, currentColor));
pointer-events: none;
}
.bit-nbr-pri {
--bit-nbr-clr: #{$clr-pri};
+ --bit-nbr-clr-dis: #{$clr-pri-dis-text};
}
.bit-nbr-sec {
--bit-nbr-clr: #{$clr-sec};
+ --bit-nbr-clr-dis: #{$clr-sec-dis-text};
}
.bit-nbr-ter {
--bit-nbr-clr: #{$clr-ter};
+ --bit-nbr-clr-dis: #{$clr-ter-dis-text};
}
.bit-nbr-inf {
--bit-nbr-clr: #{$clr-inf};
+ --bit-nbr-clr-dis: #{$clr-inf-dis-text};
}
.bit-nbr-suc {
--bit-nbr-clr: #{$clr-suc};
+ --bit-nbr-clr-dis: #{$clr-suc-dis-text};
}
.bit-nbr-wrn {
--bit-nbr-clr: #{$clr-wrn};
+ --bit-nbr-clr-dis: #{$clr-wrn-dis-text};
}
.bit-nbr-swr {
--bit-nbr-clr: #{$clr-swr};
+ --bit-nbr-clr-dis: #{$clr-swr-dis-text};
}
.bit-nbr-err {
--bit-nbr-clr: #{$clr-err};
+ --bit-nbr-clr-dis: #{$clr-err-dis-text};
}
.bit-nbr-pbg {
--bit-nbr-clr: #{$clr-bg-pri};
+ --bit-nbr-clr-dis: #{$clr-bg-pri-dis-text};
}
.bit-nbr-sbg {
--bit-nbr-clr: #{$clr-bg-sec};
+ --bit-nbr-clr-dis: #{$clr-bg-sec-dis-text};
}
.bit-nbr-tbg {
--bit-nbr-clr: #{$clr-bg-ter};
+ --bit-nbr-clr-dis: #{$clr-bg-ter-dis-text};
}
.bit-nbr-pfg {
--bit-nbr-clr: #{$clr-fg-pri};
+ --bit-nbr-clr-dis: #{$clr-fg-pri-dis-text};
}
.bit-nbr-sfg {
--bit-nbr-clr: #{$clr-fg-sec};
+ --bit-nbr-clr-dis: #{$clr-fg-sec-dis-text};
}
.bit-nbr-tfg {
--bit-nbr-clr: #{$clr-fg-ter};
+ --bit-nbr-clr-dis: #{$clr-fg-ter-dis-text};
}
.bit-nbr-pbr {
--bit-nbr-clr: #{$clr-brd-pri};
+ --bit-nbr-clr-dis: #{$clr-brd-pri-dis-text};
}
.bit-nbr-sbr {
--bit-nbr-clr: #{$clr-brd-sec};
+ --bit-nbr-clr-dis: #{$clr-brd-sec-dis-text};
}
.bit-nbr-tbr {
--bit-nbr-clr: #{$clr-brd-ter};
+ --bit-nbr-clr-dis: #{$clr-brd-ter-dis-text};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.scss b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.scss
index be21d2f680..1b30c4b65c 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.scss
@@ -8,8 +8,8 @@
font-size: var(--bit-pgn-font-size);
--bit-pgn-ico-transform-end: scaleX(1);
--bit-pgn-ico-transform-start: scaleX(-1);
- --bit-pgn-clr-dis: #{$clr-bg-dis};
- --bit-pgn-clr-txt-dis: #{$clr-fg-dis};
+ --bit-pgn-clr-dis: var(--bit-pgn-clr-dis-bg, #{$clr-bg-dis});
+ --bit-pgn-clr-txt-dis: var(--bit-pgn-clr-dis-text, #{$clr-fg-dis});
--bit-pgn-clr-btn-txt-hover: var(--bit-pgn-clr-txt);
--bit-pgn-clr-btn-bg-hover: var(--bit-pgn-clr-hover);
--bit-pgn-clr-btn-brd-hover: var(--bit-pgn-clr-hover);
@@ -34,8 +34,8 @@
&.bit-dis {
cursor: default;
- color: $clr-fg-dis;
pointer-events: none;
+ color: var(--bit-pgn-clr-txt-dis);
}
}
@@ -90,7 +90,6 @@
&[disabled] {
cursor: default;
- color: $clr-fg-dis;
pointer-events: none;
color: var(--bit-pgn-clr-btn-txt-dis);
border-color: var(--bit-pgn-clr-btn-brd-dis);
@@ -151,6 +150,8 @@
--bit-pgn-clr-sel: #{$clr-pri-dark};
--bit-pgn-clr-sel-hover: #{$clr-pri-dark-hover};
--bit-pgn-clr-sel-active: #{$clr-pri-dark-active};
+ --bit-pgn-clr-dis-bg: #{$clr-pri-dis};
+ --bit-pgn-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-pgn-sec {
@@ -161,6 +162,8 @@
--bit-pgn-clr-sel: #{$clr-sec-dark};
--bit-pgn-clr-sel-hover: #{$clr-sec-dark-hover};
--bit-pgn-clr-sel-active: #{$clr-sec-dark-active};
+ --bit-pgn-clr-dis-bg: #{$clr-sec-dis};
+ --bit-pgn-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-pgn-ter {
@@ -171,6 +174,8 @@
--bit-pgn-clr-sel: #{$clr-ter-dark};
--bit-pgn-clr-sel-hover: #{$clr-ter-dark-hover};
--bit-pgn-clr-sel-active: #{$clr-ter-dark-active};
+ --bit-pgn-clr-dis-bg: #{$clr-ter-dis};
+ --bit-pgn-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-pgn-inf {
@@ -181,6 +186,8 @@
--bit-pgn-clr-sel: #{$clr-inf-dark};
--bit-pgn-clr-sel-hover: #{$clr-inf-dark-hover};
--bit-pgn-clr-sel-active: #{$clr-inf-dark-active};
+ --bit-pgn-clr-dis-bg: #{$clr-inf-dis};
+ --bit-pgn-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-pgn-suc {
@@ -191,6 +198,8 @@
--bit-pgn-clr-sel: #{$clr-suc-dark};
--bit-pgn-clr-sel-hover: #{$clr-suc-dark-hover};
--bit-pgn-clr-sel-active: #{$clr-suc-dark-active};
+ --bit-pgn-clr-dis-bg: #{$clr-suc-dis};
+ --bit-pgn-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-pgn-wrn {
@@ -201,6 +210,8 @@
--bit-pgn-clr-sel: #{$clr-wrn-dark};
--bit-pgn-clr-sel-hover: #{$clr-wrn-dark-hover};
--bit-pgn-clr-sel-active: #{$clr-wrn-dark-active};
+ --bit-pgn-clr-dis-bg: #{$clr-wrn-dis};
+ --bit-pgn-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-pgn-swr {
@@ -211,6 +222,8 @@
--bit-pgn-clr-sel: #{$clr-swr-dark};
--bit-pgn-clr-sel-hover: #{$clr-swr-dark-hover};
--bit-pgn-clr-sel-active: #{$clr-swr-dark-active};
+ --bit-pgn-clr-dis-bg: #{$clr-swr-dis};
+ --bit-pgn-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-pgn-err {
@@ -221,6 +234,8 @@
--bit-pgn-clr-sel: #{$clr-err-dark};
--bit-pgn-clr-sel-hover: #{$clr-err-dark-hover};
--bit-pgn-clr-sel-active: #{$clr-err-dark-active};
+ --bit-pgn-clr-dis-bg: #{$clr-err-dis};
+ --bit-pgn-clr-dis-text: #{$clr-err-dis-text};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pivot/BitPivot.scss b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pivot/BitPivot.scss
index b3b1abdc66..802b901d2c 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pivot/BitPivot.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pivot/BitPivot.scss
@@ -8,17 +8,17 @@
.content-container {
cursor: default;
pointer-events: none;
- color: $clr-fg-dis;
+ color: var(--bit-pvt-clr-dis-text);
}
.bit-pvti {
cursor: default;
pointer-events: none;
- color: $clr-fg-dis;
+ color: var(--bit-pvt-clr-dis-text);
}
.bit-pvti-sel::before {
- background-color: $clr-brd-dis;
+ background-color: var(--bit-pvt-clr-dis);
}
}
}
@@ -227,6 +227,10 @@
background-color: transparent;
color: $clr-fg-pri;
+ &:focus-visible {
+ @include focus-ring(var(--bit-pvt-clr-focus));
+ }
+
> span {
height: 100%;
display: flex;
@@ -239,10 +243,10 @@
&.bit-dis {
cursor: default;
pointer-events: none;
- color: $clr-fg-dis;
+ color: var(--bit-pvt-clr-dis-text);
&.bit-pvti-sel::before {
- background-color: $clr-brd-dis;
+ background-color: var(--bit-pvt-clr-dis);
}
}
}
@@ -266,6 +270,10 @@
line-height: spacing(5.5);
background-color: transparent;
+ &:focus-visible {
+ @include focus-ring(var(--bit-pvt-clr-focus));
+ }
+
&::before {
bottom: 0;
content: "";
@@ -308,104 +316,155 @@
.bit-pvt-pri {
--bit-pvt-clr: #{$clr-pri};
--bit-pvt-clr-hover: #{$clr-pri-hover};
+ --bit-pvt-clr-focus: #{$clr-pri-focus};
--bit-pvt-clr-text: #{$clr-pri-text};
+ --bit-pvt-clr-dis: #{$clr-pri-dis};
+ --bit-pvt-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-pvt-sec {
--bit-pvt-clr: #{$clr-sec};
--bit-pvt-clr-hover: #{$clr-sec-hover};
+ --bit-pvt-clr-focus: #{$clr-sec-focus};
--bit-pvt-clr-text: #{$clr-sec-text};
+ --bit-pvt-clr-dis: #{$clr-sec-dis};
+ --bit-pvt-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-pvt-ter {
--bit-pvt-clr: #{$clr-ter};
--bit-pvt-clr-hover: #{$clr-ter-hover};
+ --bit-pvt-clr-focus: #{$clr-ter-focus};
--bit-pvt-clr-text: #{$clr-ter-text};
+ --bit-pvt-clr-dis: #{$clr-ter-dis};
+ --bit-pvt-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-pvt-inf {
--bit-pvt-clr: #{$clr-inf};
--bit-pvt-clr-hover: #{$clr-inf-hover};
+ --bit-pvt-clr-focus: #{$clr-inf-focus};
--bit-pvt-clr-text: #{$clr-inf-text};
+ --bit-pvt-clr-dis: #{$clr-inf-dis};
+ --bit-pvt-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-pvt-suc {
--bit-pvt-clr: #{$clr-suc};
--bit-pvt-clr-hover: #{$clr-suc-hover};
+ --bit-pvt-clr-focus: #{$clr-suc-focus};
--bit-pvt-clr-text: #{$clr-suc-text};
+ --bit-pvt-clr-dis: #{$clr-suc-dis};
+ --bit-pvt-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-pvt-wrn {
--bit-pvt-clr: #{$clr-wrn};
--bit-pvt-clr-hover: #{$clr-wrn-hover};
+ --bit-pvt-clr-focus: #{$clr-wrn-focus};
--bit-pvt-clr-text: #{$clr-wrn-text};
+ --bit-pvt-clr-dis: #{$clr-wrn-dis};
+ --bit-pvt-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-pvt-swr {
--bit-pvt-clr: #{$clr-swr};
--bit-pvt-clr-hover: #{$clr-swr-hover};
+ --bit-pvt-clr-focus: #{$clr-swr-focus};
--bit-pvt-clr-text: #{$clr-swr-text};
+ --bit-pvt-clr-dis: #{$clr-swr-dis};
+ --bit-pvt-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-pvt-err {
--bit-pvt-clr: #{$clr-err};
--bit-pvt-clr-hover: #{$clr-err-hover};
+ --bit-pvt-clr-focus: #{$clr-err-focus};
--bit-pvt-clr-text: #{$clr-err-text};
+ --bit-pvt-clr-dis: #{$clr-err-dis};
+ --bit-pvt-clr-dis-text: #{$clr-err-dis-text};
}
.bit-pvt-pbg {
--bit-pvt-clr: #{$clr-bg-pri};
--bit-pvt-clr-hover: #{$clr-bg-pri-hover};
+ --bit-pvt-clr-focus: #{$clr-bg-pri-focus};
--bit-pvt-clr-text: #{$clr-fg-pri};
+ --bit-pvt-clr-dis: #{$clr-bg-pri-dis};
+ --bit-pvt-clr-dis-text: #{$clr-bg-pri-dis-text};
}
.bit-pvt-sbg {
--bit-pvt-clr: #{$clr-bg-sec};
--bit-pvt-clr-hover: #{$clr-bg-sec-hover};
+ --bit-pvt-clr-focus: #{$clr-bg-sec-focus};
--bit-pvt-clr-text: #{$clr-fg-pri};
+ --bit-pvt-clr-dis: #{$clr-bg-sec-dis};
+ --bit-pvt-clr-dis-text: #{$clr-bg-sec-dis-text};
}
.bit-pvt-tbg {
--bit-pvt-clr: #{$clr-bg-ter};
--bit-pvt-clr-hover: #{$clr-bg-ter-hover};
+ --bit-pvt-clr-focus: #{$clr-bg-ter-focus};
--bit-pvt-clr-text: #{$clr-fg-pri};
+ --bit-pvt-clr-dis: #{$clr-bg-ter-dis};
+ --bit-pvt-clr-dis-text: #{$clr-bg-ter-dis-text};
}
.bit-pvt-pfg {
--bit-pvt-clr: #{$clr-fg-pri};
--bit-pvt-clr-hover: #{$clr-fg-pri-hover};
+ --bit-pvt-clr-focus: #{$clr-fg-pri-focus};
--bit-pvt-clr-text: #{$clr-bg-pri};
+ --bit-pvt-clr-dis: #{$clr-fg-pri-dis};
+ --bit-pvt-clr-dis-text: #{$clr-fg-pri-dis-text};
}
.bit-pvt-sfg {
--bit-pvt-clr: #{$clr-fg-sec};
--bit-pvt-clr-hover: #{$clr-fg-sec-hover};
+ --bit-pvt-clr-focus: #{$clr-fg-sec-focus};
--bit-pvt-clr-text: #{$clr-bg-pri};
+ --bit-pvt-clr-dis: #{$clr-fg-sec-dis};
+ --bit-pvt-clr-dis-text: #{$clr-fg-sec-dis-text};
}
.bit-pvt-tfg {
--bit-pvt-clr: #{$clr-fg-ter};
--bit-pvt-clr-hover: #{$clr-fg-ter-hover};
+ --bit-pvt-clr-focus: #{$clr-fg-ter-focus};
--bit-pvt-clr-text: #{$clr-bg-pri};
+ --bit-pvt-clr-dis: #{$clr-fg-ter-dis};
+ --bit-pvt-clr-dis-text: #{$clr-fg-ter-dis-text};
}
.bit-pvt-pbr {
--bit-pvt-clr: #{$clr-brd-pri};
--bit-pvt-clr-hover: #{$clr-brd-pri-hover};
+ --bit-pvt-clr-focus: #{$clr-brd-pri-focus};
--bit-pvt-clr-text: #{$clr-fg-pri};
+ --bit-pvt-clr-dis: #{$clr-brd-pri-dis};
+ --bit-pvt-clr-dis-text: #{$clr-brd-pri-dis-text};
}
.bit-pvt-sbr {
--bit-pvt-clr: #{$clr-brd-sec};
--bit-pvt-clr-hover: #{$clr-brd-sec-hover};
+ --bit-pvt-clr-focus: #{$clr-brd-sec-focus};
--bit-pvt-clr-text: #{$clr-fg-pri};
+ --bit-pvt-clr-dis: #{$clr-brd-sec-dis};
+ --bit-pvt-clr-dis-text: #{$clr-brd-sec-dis-text};
}
.bit-pvt-tbr {
--bit-pvt-clr: #{$clr-brd-ter};
--bit-pvt-clr-hover: #{$clr-brd-ter-hover};
+ --bit-pvt-clr-focus: #{$clr-brd-ter-focus};
--bit-pvt-clr-text: #{$clr-fg-pri};
+ --bit-pvt-clr-dis: #{$clr-brd-ter-dis};
+ --bit-pvt-clr-dis-text: #{$clr-brd-ter-dis-text};
}
// #region Slide overflow behavior
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadge.scss b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadge.scss
index 29312bc1cc..21de67150c 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadge.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadge.scss
@@ -5,7 +5,7 @@
display: inline-block;
&.bit-dis {
- --bit-bdg-cnt-clr-txt: #{$clr-fg-dis};
+ --bit-bdg-cnt-clr-txt: var(--bit-bdg-clr-dis-text);
--bit-bdg-cnt-clr-bg: var(--bit-bdg-clr-bg-dis);
--bit-bdg-cnt-clr-brd: var(--bit-bdg-clr-brd-dis);
}
@@ -292,8 +292,8 @@
.bit-bdg-fil {
- --bit-bdg-clr-bg-dis: #{$clr-bg-dis};
- --bit-bdg-clr-brd-dis: #{$clr-brd-dis};
+ --bit-bdg-clr-bg-dis: var(--bit-bdg-clr-dis);
+ --bit-bdg-clr-brd-dis: var(--bit-bdg-clr-dis);
--bit-bdg-cnt-clr-bg: var(--bit-bdg-clr);
--bit-bdg-cnt-clr-brd: var(--bit-bdg-clr);
--bit-bdg-cnt-clr-txt: var(--bit-bdg-clr-txt);
@@ -301,7 +301,8 @@
.bit-bdg-otl {
--bit-bdg-cnt-clr-bg: transparent;
- --bit-bdg-clr-brd-dis: #{$clr-brd-dis};
+ --bit-bdg-clr-bg-dis: transparent;
+ --bit-bdg-clr-brd-dis: var(--bit-bdg-clr-dis);
--bit-bdg-cnt-clr-txt: var(--bit-bdg-clr);
--bit-bdg-cnt-clr-brd: var(--bit-bdg-clr);
}
@@ -309,6 +310,7 @@
.bit-bdg-txt {
--bit-bdg-cnt-clr-bg: transparent;
--bit-bdg-cnt-clr-brd: transparent;
+ --bit-bdg-clr-bg-dis: transparent;
--bit-bdg-clr-brd-dis: transparent;
--bit-bdg-cnt-clr-txt: var(--bit-bdg-clr);
}
@@ -317,41 +319,57 @@
.bit-bdg-pri {
--bit-bdg-clr: #{$clr-pri};
--bit-bdg-clr-txt: #{$clr-pri-text};
+ --bit-bdg-clr-dis: #{$clr-pri-dis};
+ --bit-bdg-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-bdg-sec {
--bit-bdg-clr: #{$clr-sec};
--bit-bdg-clr-txt: #{$clr-sec-text};
+ --bit-bdg-clr-dis: #{$clr-sec-dis};
+ --bit-bdg-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-bdg-ter {
--bit-bdg-clr: #{$clr-ter};
--bit-bdg-clr-txt: #{$clr-ter-text};
+ --bit-bdg-clr-dis: #{$clr-ter-dis};
+ --bit-bdg-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-bdg-inf {
--bit-bdg-clr: #{$clr-inf};
--bit-bdg-clr-txt: #{$clr-inf-text};
+ --bit-bdg-clr-dis: #{$clr-inf-dis};
+ --bit-bdg-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-bdg-suc {
--bit-bdg-clr: #{$clr-suc};
--bit-bdg-clr-txt: #{$clr-suc-text};
+ --bit-bdg-clr-dis: #{$clr-suc-dis};
+ --bit-bdg-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-bdg-wrn {
--bit-bdg-clr: #{$clr-wrn};
--bit-bdg-clr-txt: #{$clr-wrn-text};
+ --bit-bdg-clr-dis: #{$clr-wrn-dis};
+ --bit-bdg-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-bdg-swr {
--bit-bdg-clr: #{$clr-swr};
--bit-bdg-clr-txt: #{$clr-swr-text};
+ --bit-bdg-clr-dis: #{$clr-swr-dis};
+ --bit-bdg-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-bdg-err {
--bit-bdg-clr: #{$clr-err};
--bit-bdg-clr-txt: #{$clr-err-text};
+ --bit-bdg-clr-dis: #{$clr-err-dis};
+ --bit-bdg-clr-dis-text: #{$clr-err-dis-text};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Tag/BitTag.scss b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Tag/BitTag.scss
index dba1b9dab6..9effece144 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Tag/BitTag.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Tag/BitTag.scss
@@ -18,7 +18,6 @@
&.bit-dis {
cursor: default;
user-select: none;
- color: $clr-fg-dis;
pointer-events: none;
}
}
@@ -45,8 +44,9 @@
background-color: var(--bit-tag-clr);
&.bit-dis {
- border-color: $clr-brd-dis;
- background-color: $clr-bg-dis;
+ color: var(--bit-tag-clr-dis-text);
+ border-color: var(--bit-tag-clr-dis);
+ background-color: var(--bit-tag-clr-dis);
}
}
@@ -56,7 +56,8 @@
background-color: transparent;
&.bit-dis {
- border-color: $clr-brd-dis;
+ color: var(--bit-tag-clr-dis-text);
+ border-color: var(--bit-tag-clr-dis);
background-color: transparent;
}
}
@@ -67,6 +68,7 @@
background-color: transparent;
&.bit-dis {
+ color: var(--bit-tag-clr-dis-text);
border-color: transparent;
background-color: transparent;
}
@@ -75,86 +77,120 @@
.bit-tag-pri {
--bit-tag-clr: #{$clr-pri};
--bit-tag-clr-txt: #{$clr-pri-text};
+ --bit-tag-clr-dis: #{$clr-pri-dis};
+ --bit-tag-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-tag-sec {
--bit-tag-clr: #{$clr-sec};
--bit-tag-clr-txt: #{$clr-sec-text};
+ --bit-tag-clr-dis: #{$clr-sec-dis};
+ --bit-tag-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-tag-ter {
--bit-tag-clr: #{$clr-ter};
--bit-tag-clr-txt: #{$clr-ter-text};
+ --bit-tag-clr-dis: #{$clr-ter-dis};
+ --bit-tag-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-tag-inf {
--bit-tag-clr: #{$clr-inf};
--bit-tag-clr-txt: #{$clr-inf-text};
+ --bit-tag-clr-dis: #{$clr-inf-dis};
+ --bit-tag-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-tag-suc {
--bit-tag-clr: #{$clr-suc};
--bit-tag-clr-txt: #{$clr-suc-text};
+ --bit-tag-clr-dis: #{$clr-suc-dis};
+ --bit-tag-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-tag-wrn {
--bit-tag-clr: #{$clr-wrn};
--bit-tag-clr-txt: #{$clr-wrn-text};
+ --bit-tag-clr-dis: #{$clr-wrn-dis};
+ --bit-tag-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-tag-swr {
--bit-tag-clr: #{$clr-swr};
--bit-tag-clr-txt: #{$clr-swr-text};
+ --bit-tag-clr-dis: #{$clr-swr-dis};
+ --bit-tag-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-tag-err {
--bit-tag-clr: #{$clr-err};
--bit-tag-clr-txt: #{$clr-err-text};
+ --bit-tag-clr-dis: #{$clr-err-dis};
+ --bit-tag-clr-dis-text: #{$clr-err-dis-text};
}
.bit-tag-pbg {
--bit-tag-clr: #{$clr-bg-pri};
--bit-tag-clr-txt: #{$clr-fg-pri};
+ --bit-tag-clr-dis: #{$clr-bg-pri-dis};
+ --bit-tag-clr-dis-text: #{$clr-bg-pri-dis-text};
}
.bit-tag-sbg {
--bit-tag-clr: #{$clr-bg-sec};
--bit-tag-clr-txt: #{$clr-fg-pri};
+ --bit-tag-clr-dis: #{$clr-bg-sec-dis};
+ --bit-tag-clr-dis-text: #{$clr-bg-sec-dis-text};
}
.bit-tag-tbg {
--bit-tag-clr: #{$clr-bg-ter};
--bit-tag-clr-txt: #{$clr-fg-pri};
+ --bit-tag-clr-dis: #{$clr-bg-ter-dis};
+ --bit-tag-clr-dis-text: #{$clr-bg-ter-dis-text};
}
.bit-tag-pfg {
--bit-tag-clr: #{$clr-fg-pri};
--bit-tag-clr-txt: #{$clr-bg-pri};
+ --bit-tag-clr-dis: #{$clr-fg-pri-dis};
+ --bit-tag-clr-dis-text: #{$clr-fg-pri-dis-text};
}
.bit-tag-sfg {
--bit-tag-clr: #{$clr-fg-sec};
--bit-tag-clr-txt: #{$clr-bg-pri};
+ --bit-tag-clr-dis: #{$clr-fg-sec-dis};
+ --bit-tag-clr-dis-text: #{$clr-fg-sec-dis-text};
}
.bit-tag-tfg {
--bit-tag-clr: #{$clr-fg-ter};
--bit-tag-clr-txt: #{$clr-bg-pri};
+ --bit-tag-clr-dis: #{$clr-fg-ter-dis};
+ --bit-tag-clr-dis-text: #{$clr-fg-ter-dis-text};
}
.bit-tag-pbr {
--bit-tag-clr: #{$clr-brd-pri};
--bit-tag-clr-txt: #{$clr-fg-pri};
+ --bit-tag-clr-dis: #{$clr-brd-pri-dis};
+ --bit-tag-clr-dis-text: #{$clr-brd-pri-dis-text};
}
.bit-tag-sbr {
--bit-tag-clr: #{$clr-brd-sec};
--bit-tag-clr-txt: #{$clr-fg-pri};
+ --bit-tag-clr-dis: #{$clr-brd-sec-dis};
+ --bit-tag-clr-dis-text: #{$clr-brd-sec-dis-text};
}
.bit-tag-tbr {
--bit-tag-clr: #{$clr-brd-ter};
--bit-tag-clr-txt: #{$clr-fg-pri};
+ --bit-tag-clr-dis: #{$clr-brd-ter-dis};
+ --bit-tag-clr-dis-text: #{$clr-brd-ter-dis-text};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Icon/BitIcon.scss b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Icon/BitIcon.scss
index ad9a4a9583..ee4f143c0d 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Icon/BitIcon.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Icon/BitIcon.scss
@@ -7,7 +7,7 @@
border-width: $shp-border-width;
&.bit-dis {
- color: $clr-fg-dis;
+ color: var(--bit-ico-clr-dis-text);
}
}
@@ -16,12 +16,24 @@
color: var(--bit-ico-clr-txt);
border-color: var(--bit-ico-clr);
background-color: var(--bit-ico-clr);
+
+ &.bit-dis {
+ color: var(--bit-ico-clr-dis-text);
+ border-color: var(--bit-ico-clr-dis);
+ background-color: var(--bit-ico-clr-dis);
+ }
}
.bit-ico-out {
color: var(--bit-ico-clr);
border-color: var(--bit-ico-clr);
background-color: transparent;
+
+ &.bit-dis {
+ color: var(--bit-ico-clr-dis-text);
+ border-color: var(--bit-ico-clr-dis);
+ background-color: transparent;
+ }
}
.bit-ico-txt {
@@ -30,92 +42,130 @@
background-color: transparent;
border: none;
padding: 0;
+
+ &.bit-dis {
+ color: var(--bit-ico-clr-dis-text);
+ }
}
.bit-ico-pri {
--bit-ico-clr: #{$clr-pri};
--bit-ico-clr-txt: #{$clr-pri-text};
+ --bit-ico-clr-dis: #{$clr-pri-dis};
+ --bit-ico-clr-dis-text: #{$clr-pri-dis-text};
}
.bit-ico-sec {
--bit-ico-clr: #{$clr-sec};
--bit-ico-clr-txt: #{$clr-sec-text};
+ --bit-ico-clr-dis: #{$clr-sec-dis};
+ --bit-ico-clr-dis-text: #{$clr-sec-dis-text};
}
.bit-ico-ter {
--bit-ico-clr: #{$clr-ter};
--bit-ico-clr-txt: #{$clr-ter-text};
+ --bit-ico-clr-dis: #{$clr-ter-dis};
+ --bit-ico-clr-dis-text: #{$clr-ter-dis-text};
}
.bit-ico-inf {
--bit-ico-clr: #{$clr-inf};
--bit-ico-clr-txt: #{$clr-inf-text};
+ --bit-ico-clr-dis: #{$clr-inf-dis};
+ --bit-ico-clr-dis-text: #{$clr-inf-dis-text};
}
.bit-ico-suc {
--bit-ico-clr: #{$clr-suc};
--bit-ico-clr-txt: #{$clr-suc-text};
+ --bit-ico-clr-dis: #{$clr-suc-dis};
+ --bit-ico-clr-dis-text: #{$clr-suc-dis-text};
}
.bit-ico-wrn {
--bit-ico-clr: #{$clr-wrn};
--bit-ico-clr-txt: #{$clr-wrn-text};
+ --bit-ico-clr-dis: #{$clr-wrn-dis};
+ --bit-ico-clr-dis-text: #{$clr-wrn-dis-text};
}
.bit-ico-swr {
--bit-ico-clr: #{$clr-swr};
--bit-ico-clr-txt: #{$clr-swr-text};
+ --bit-ico-clr-dis: #{$clr-swr-dis};
+ --bit-ico-clr-dis-text: #{$clr-swr-dis-text};
}
.bit-ico-err {
--bit-ico-clr: #{$clr-err};
--bit-ico-clr-txt: #{$clr-err-text};
+ --bit-ico-clr-dis: #{$clr-err-dis};
+ --bit-ico-clr-dis-text: #{$clr-err-dis-text};
}
.bit-ico-pbg {
--bit-ico-clr: #{$clr-bg-pri};
--bit-ico-clr-txt: #{$clr-fg-pri};
+ --bit-ico-clr-dis: #{$clr-bg-pri-dis};
+ --bit-ico-clr-dis-text: #{$clr-bg-pri-dis-text};
}
.bit-ico-sbg {
--bit-ico-clr: #{$clr-bg-sec};
--bit-ico-clr-txt: #{$clr-fg-sec};
+ --bit-ico-clr-dis: #{$clr-bg-sec-dis};
+ --bit-ico-clr-dis-text: #{$clr-bg-sec-dis-text};
}
.bit-ico-tbg {
--bit-ico-clr: #{$clr-bg-ter};
--bit-ico-clr-txt: #{$clr-fg-ter};
+ --bit-ico-clr-dis: #{$clr-bg-ter-dis};
+ --bit-ico-clr-dis-text: #{$clr-bg-ter-dis-text};
}
.bit-ico-pfg {
--bit-ico-clr: #{$clr-fg-pri};
--bit-ico-clr-txt: #{$clr-bg-pri};
+ --bit-ico-clr-dis: #{$clr-fg-pri-dis};
+ --bit-ico-clr-dis-text: #{$clr-fg-pri-dis-text};
}
.bit-ico-sfg {
--bit-ico-clr: #{$clr-fg-sec};
--bit-ico-clr-txt: #{$clr-bg-sec};
+ --bit-ico-clr-dis: #{$clr-fg-sec-dis};
+ --bit-ico-clr-dis-text: #{$clr-fg-sec-dis-text};
}
.bit-ico-tfg {
--bit-ico-clr: #{$clr-fg-ter};
--bit-ico-clr-txt: #{$clr-bg-ter};
+ --bit-ico-clr-dis: #{$clr-fg-ter-dis};
+ --bit-ico-clr-dis-text: #{$clr-fg-ter-dis-text};
}
.bit-ico-pbr {
- --bit-ico-clr: #{$clr-bg-pri};
+ --bit-ico-clr: #{$clr-brd-pri};
--bit-ico-clr-txt: #{$clr-fg-pri};
+ --bit-ico-clr-dis: #{$clr-brd-pri-dis};
+ --bit-ico-clr-dis-text: #{$clr-brd-pri-dis-text};
}
.bit-ico-sbr {
- --bit-ico-clr: #{$clr-bg-sec};
+ --bit-ico-clr: #{$clr-brd-sec};
--bit-ico-clr-txt: #{$clr-fg-sec};
+ --bit-ico-clr-dis: #{$clr-brd-sec-dis};
+ --bit-ico-clr-dis-text: #{$clr-brd-sec-dis-text};
}
.bit-ico-tbr {
- --bit-ico-clr: #{$clr-bg-ter};
+ --bit-ico-clr: #{$clr-brd-ter};
--bit-ico-clr-txt: #{$clr-fg-ter};
+ --bit-ico-clr-dis: #{$clr-brd-ter-dis};
+ --bit-ico-clr-dis-text: #{$clr-brd-ter-dis-text};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.scss b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.scss
index 1402f2c809..ba388e114c 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/Link/BitLink.scss
@@ -11,6 +11,7 @@
color: var(--bit-lnk-clr);
font-family: $tg-font-family;
background-color: transparent;
+ border-radius: $shp-border-radius;
&:hover,
&:active,
@@ -18,6 +19,10 @@
color: var(--bit-lnk-clr-hover);
}
+ &:focus-visible {
+ @include focus-ring(var(--bit-lnk-clr-focus));
+ }
+
&:hover,
&:active {
text-decoration: underline;
@@ -25,12 +30,19 @@
&.bit-dis {
cursor: default;
- color: $clr-fg-dis;
+ color: var(--bit-lnk-clr-dis);
&:hover,
&:active,
&:focus {
- color: $clr-fg-dis;
+ color: var(--bit-lnk-clr-dis);
+ }
+
+ &:focus-visible {
+ // Cancel both layers of the focus-ring mixin applied above: the box-shadow ring and
+ // the forced-colors Highlight outline, so a disabled link never shows a focus indicator.
+ outline: none;
+ box-shadow: none;
}
&:hover,
@@ -73,84 +85,118 @@
.bit-lnk-pri {
--bit-lnk-clr: #{$clr-pri};
--bit-lnk-clr-hover: #{$clr-pri-hover};
+ --bit-lnk-clr-focus: #{$clr-pri-focus};
+ --bit-lnk-clr-dis: #{$clr-pri-dis-text};
}
.bit-lnk-sec {
--bit-lnk-clr: #{$clr-sec};
--bit-lnk-clr-hover: #{$clr-sec-hover};
+ --bit-lnk-clr-focus: #{$clr-sec-focus};
+ --bit-lnk-clr-dis: #{$clr-sec-dis-text};
}
.bit-lnk-ter {
--bit-lnk-clr: #{$clr-ter};
--bit-lnk-clr-hover: #{$clr-ter-hover};
+ --bit-lnk-clr-focus: #{$clr-ter-focus};
+ --bit-lnk-clr-dis: #{$clr-ter-dis-text};
}
.bit-lnk-inf {
--bit-lnk-clr: #{$clr-inf};
--bit-lnk-clr-hover: #{$clr-inf-hover};
+ --bit-lnk-clr-focus: #{$clr-inf-focus};
+ --bit-lnk-clr-dis: #{$clr-inf-dis-text};
}
.bit-lnk-suc {
--bit-lnk-clr: #{$clr-suc};
--bit-lnk-clr-hover: #{$clr-suc-hover};
+ --bit-lnk-clr-focus: #{$clr-suc-focus};
+ --bit-lnk-clr-dis: #{$clr-suc-dis-text};
}
.bit-lnk-wrn {
--bit-lnk-clr: #{$clr-wrn};
--bit-lnk-clr-hover: #{$clr-wrn-hover};
+ --bit-lnk-clr-focus: #{$clr-wrn-focus};
+ --bit-lnk-clr-dis: #{$clr-wrn-dis-text};
}
.bit-lnk-swr {
--bit-lnk-clr: #{$clr-swr};
--bit-lnk-clr-hover: #{$clr-swr-hover};
+ --bit-lnk-clr-focus: #{$clr-swr-focus};
+ --bit-lnk-clr-dis: #{$clr-swr-dis-text};
}
.bit-lnk-err {
--bit-lnk-clr: #{$clr-err};
--bit-lnk-clr-hover: #{$clr-err-hover};
+ --bit-lnk-clr-focus: #{$clr-err-focus};
+ --bit-lnk-clr-dis: #{$clr-err-dis-text};
}
.bit-lnk-pbg {
--bit-lnk-clr: #{$clr-bg-pri};
--bit-lnk-clr-hover: #{$clr-bg-pri-hover};
+ --bit-lnk-clr-focus: #{$clr-bg-pri-focus};
+ --bit-lnk-clr-dis: #{$clr-bg-pri-dis-text};
}
.bit-lnk-sbg {
--bit-lnk-clr: #{$clr-bg-sec};
--bit-lnk-clr-hover: #{$clr-bg-sec-hover};
+ --bit-lnk-clr-focus: #{$clr-bg-sec-focus};
+ --bit-lnk-clr-dis: #{$clr-bg-sec-dis-text};
}
.bit-lnk-tbg {
--bit-lnk-clr: #{$clr-bg-ter};
--bit-lnk-clr-hover: #{$clr-bg-ter-hover};
+ --bit-lnk-clr-focus: #{$clr-bg-ter-focus};
+ --bit-lnk-clr-dis: #{$clr-bg-ter-dis-text};
}
.bit-lnk-pfg {
--bit-lnk-clr: #{$clr-fg-pri};
--bit-lnk-clr-hover: #{$clr-fg-pri-hover};
+ --bit-lnk-clr-focus: #{$clr-fg-pri-focus};
+ --bit-lnk-clr-dis: #{$clr-fg-pri-dis-text};
}
.bit-lnk-sfg {
--bit-lnk-clr: #{$clr-fg-sec};
--bit-lnk-clr-hover: #{$clr-fg-sec-hover};
+ --bit-lnk-clr-focus: #{$clr-fg-sec-focus};
+ --bit-lnk-clr-dis: #{$clr-fg-sec-dis-text};
}
.bit-lnk-tfg {
--bit-lnk-clr: #{$clr-fg-ter};
--bit-lnk-clr-hover: #{$clr-fg-ter-hover};
+ --bit-lnk-clr-focus: #{$clr-fg-ter-focus};
+ --bit-lnk-clr-dis: #{$clr-fg-ter-dis-text};
}
.bit-lnk-pbr {
--bit-lnk-clr: #{$clr-brd-pri};
--bit-lnk-clr-hover: #{$clr-brd-pri-hover};
+ --bit-lnk-clr-focus: #{$clr-brd-pri-focus};
+ --bit-lnk-clr-dis: #{$clr-brd-pri-dis-text};
}
.bit-lnk-sbr {
--bit-lnk-clr: #{$clr-brd-sec};
--bit-lnk-clr-hover: #{$clr-brd-sec-hover};
+ --bit-lnk-clr-focus: #{$clr-brd-sec-focus};
+ --bit-lnk-clr-dis: #{$clr-brd-sec-dis-text};
}
.bit-lnk-tbr {
--bit-lnk-clr: #{$clr-brd-ter};
--bit-lnk-clr-hover: #{$clr-brd-ter-hover};
+ --bit-lnk-clr-focus: #{$clr-brd-ter-focus};
+ --bit-lnk-clr-dis: #{$clr-brd-ter-dis-text};
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQuery.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQuery.razor.cs
index ce88ddde5a..18afd359c1 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQuery.razor.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQuery.razor.cs
@@ -72,41 +72,42 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
if (IsDisposed) return;
- var query = Query ?? GetQuery(ScreenQuery);
-
- if (query.HasValue() && query != _query)
+ // A custom Query takes precedence; otherwise defer to the predefined ScreenQuery, whose
+ // media query is built on the JS side from the live --bit-bp-* theme breakpoints so a
+ // customized BitTheme.Layout.Breakpoints is honored (rather than baking fixed px here).
+ // A blank Query is treated as absent so a bound-but-empty value still lets ScreenQuery win.
+ var customQuery = Query.HasValue() ? Query : null;
+ var screenQuery = customQuery is null ? ScreenQuery?.ToString() : null;
+ var effectiveKey = customQuery ?? screenQuery;
+
+ if (effectiveKey.HasValue())
{
- _query = query;
- await _js.BitMediaQuerySetup(_Id, _query, _dotnetObj);
+ // For a predefined ScreenQuery the actual media-query expression is resolved on the JS
+ // side from the live --bit-bp-* theme breakpoints, so it can change while the enum name
+ // stays the same (e.g. after new breakpoints are applied). Re-invoke setup on every
+ // render in that case and let the JS side reuse the existing listener when the resolved
+ // expression is unchanged; a custom Query is verbatim, so the key comparison suffices.
+ if (effectiveKey != _query || screenQuery is not null)
+ {
+ _query = effectiveKey;
+ try
+ {
+ await _js.BitMediaQuerySetup(_Id, customQuery, screenQuery, _dotnetObj);
+ }
+ catch (JSDisconnectedException) { } // circuit gone; nothing to set up
+ }
}
- }
-
-
-
- private static string GetQuery(BitScreenQuery? query)
- {
- return query switch
+ else if (_query is not null)
{
- BitScreenQuery.Xs => "(max-width: 600px)",
- BitScreenQuery.Sm => "(min-width: 601px) and (max-width: 960px)",
- BitScreenQuery.Md => "(min-width: 961px) and (max-width: 1280px)",
- BitScreenQuery.Lg => "(min-width: 1281px) and (max-width: 1920px)",
- BitScreenQuery.Xl => "(min-width: 1921px) and (max-width: 2560px)",
- BitScreenQuery.Xxl => "(min-width: 2561px)",
-
- BitScreenQuery.LtSm => "(max-width: 600px)",
- BitScreenQuery.LtMd => "(max-width: 960px)",
- BitScreenQuery.LtLg => "(max-width: 1280px)",
- BitScreenQuery.LtXl => "(max-width: 1920px)",
- BitScreenQuery.LtXxl => "(max-width: 2560px)",
-
- BitScreenQuery.GtXs => "(min-width: 601px)",
- BitScreenQuery.GtSm => "(min-width: 961px)",
- BitScreenQuery.GtMd => "(min-width: 1281px)",
- BitScreenQuery.GtLg => "(min-width: 1921px)",
- BitScreenQuery.GtXl => "(min-width: 2561px)",
- _ => string.Empty
- };
+ // Neither a Query nor a ScreenQuery resolves anymore: tear down the previous listener
+ // and reset so a later (re)assignment sets up cleanly.
+ _query = null;
+ try
+ {
+ await _js.BitMediaQueryDispose(_Id);
+ }
+ catch (JSDisconnectedException) { } // circuit gone; nothing to tear down
+ }
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQuery.ts b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQuery.ts
index 793e27cad9..91c7bdb10c 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQuery.ts
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQuery.ts
@@ -1,16 +1,49 @@
namespace BitBlazorUI {
+ type BreakpointKey = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
+
export class MediaQuery {
private static _abortControllers: { [key: string]: AbortController } = {};
- public static async setup(id: string, query: string, dotnetObj: DotNetObject) {
- if (!query || !dotnetObj) return;
+ // The resolved media-query expression each listener was created with, so a repeated setup
+ // call can reuse the existing listener when the expression is unchanged and replace it when
+ // the theme breakpoints have changed the resolved query for the same ScreenQuery name.
+ private static _resolvedQueries: { [key: string]: string } = {};
+
+ // Fallback breakpoints (px), used only when the corresponding --bit-bp-* CSS variable is
+ // not resolvable. Kept in sync with the defaults published by media-queries.scss.
+ private static _defaultBreakpoints: Record = {
+ xs: '0',
+ sm: '600px',
+ md: '960px',
+ lg: '1280px',
+ xl: '1920px',
+ xxl: '2560px',
+ };
+
+ /**
+ * @param query A custom, verbatim media query (takes precedence when provided).
+ * @param screenQuery One of the predefined BitScreenQuery names (e.g. "Md", "LtLg", "GtSm").
+ * When set (and no custom query), the query is built from the live
+ * --bit-bp-* theme breakpoints so a customized theme is honored.
+ */
+ public static async setup(id: string, query: string | null, screenQuery: string | null, dotnetObj: DotNetObject) {
+ if (!dotnetObj) return;
+
+ const resolvedQuery = query || (screenQuery ? MediaQuery.buildScreenQuery(screenQuery, id) : '');
+ if (!resolvedQuery) return;
+
+ // The C# side re-invokes setup for screen queries on every render (the expression
+ // depends on the live --bit-bp-* breakpoints); keep the existing listener when the
+ // resolved expression is unchanged and only rebuild it when it actually differs.
+ if (MediaQuery._abortControllers[id] && MediaQuery._resolvedQueries[id] === resolvedQuery) return;
MediaQuery.dispose(id);
const ac = new AbortController();
MediaQuery._abortControllers[id] = ac;
+ MediaQuery._resolvedQueries[id] = resolvedQuery;
- const queryList = window.matchMedia(query);
+ const queryList = window.matchMedia(resolvedQuery);
queryList.addEventListener('change', async e => {
await handleMatchChange(e.matches);
@@ -24,6 +57,8 @@
}
public static dispose(id: string) {
+ delete MediaQuery._resolvedQueries[id];
+
const ac = MediaQuery._abortControllers[id];
if (!ac) return;
@@ -31,5 +66,69 @@
delete MediaQuery._abortControllers[id];
}
+
+ // Builds the media query for a predefined BitScreenQuery from the resolved theme breakpoints.
+ // Range bounds are half-open (min inclusive, max exclusive), so the upper edge is one CSS
+ // pixel below the next breakpoint - matching the packaged media-queries.scss mixins.
+ private static buildScreenQuery(screenQuery: string, id: string): string {
+ const bp = MediaQuery.resolveBreakpoints(id);
+ const min = (v: string) => `(min-width: ${v})`;
+ const max = (v: string) => `(max-width: ${MediaQuery.below(v)})`;
+
+ switch (screenQuery) {
+ case 'Xs': return `${min(bp.xs)} and ${max(bp.sm)}`;
+ case 'Sm': return `${min(bp.sm)} and ${max(bp.md)}`;
+ case 'Md': return `${min(bp.md)} and ${max(bp.lg)}`;
+ case 'Lg': return `${min(bp.lg)} and ${max(bp.xl)}`;
+ case 'Xl': return `${min(bp.xl)} and ${max(bp.xxl)}`;
+ case 'Xxl': return min(bp.xxl);
+
+ case 'LtSm': return max(bp.sm);
+ case 'LtMd': return max(bp.md);
+ case 'LtLg': return max(bp.lg);
+ case 'LtXl': return max(bp.xl);
+ case 'LtXxl': return max(bp.xxl);
+
+ case 'GtXs': return min(bp.sm);
+ case 'GtSm': return min(bp.md);
+ case 'GtMd': return min(bp.lg);
+ case 'GtLg': return min(bp.xl);
+ case 'GtXl': return min(bp.xxl);
+
+ default: return '';
+ }
+ }
+
+ // Reads the --bit-bp-* breakpoint tokens from the queried element's themed scope, so the
+ // breakpoints of an enclosing BitThemeProvider are honored. Custom properties inherit, so a
+ // document-root definition still resolves through the element; the root itself is only read
+ // directly when the element is not rendered (e.g. an OnChange-only usage with no content).
+ // Built-in defaults fill in for any token that is unset everywhere.
+ private static resolveBreakpoints(id: string): Record {
+ const element = document.getElementById(id) ?? document.documentElement;
+ const styles = typeof getComputedStyle === 'function'
+ ? getComputedStyle(element)
+ : null;
+
+ const read = (key: BreakpointKey) => {
+ const value = styles?.getPropertyValue(`--bit-bp-${key}`)?.trim();
+ return value || MediaQuery._defaultBreakpoints[key];
+ };
+
+ return { xs: read('xs'), sm: read('sm'), md: read('md'), lg: read('lg'), xl: read('xl'), xxl: read('xxl') };
+ }
+
+ // Returns the value one CSS pixel below `value`, for exclusive max-width bounds. A unitless
+ // or px value is decremented numerically ("960px" -> "959px"); any other unit (em/rem/…) is
+ // deferred to the browser via calc() so custom-unit breakpoints still work.
+ private static below(value: string): string {
+ const trimmed = value.trim();
+ const match = /^(-?\d*\.?\d+)px$/i.exec(trimmed) ?? /^(-?\d*\.?\d+)$/.exec(trimmed);
+ if (match) {
+ return `${parseFloat(match[1]) - 1}px`;
+ }
+
+ return `calc(${trimmed} - 1px)`;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQueryJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQueryJsRuntimeExtensions.cs
index 9ae62a7998..cb97634f1e 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQueryJsRuntimeExtensions.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitMediaQueryJsRuntimeExtensions.cs
@@ -2,12 +2,13 @@
internal static class BitMediaQueryJsRuntimeExtensions
{
- internal static ValueTask BitMediaQuerySetup(this IJSRuntime jsRuntime,
- string id,
- string query,
+ internal static ValueTask BitMediaQuerySetup(this IJSRuntime jsRuntime,
+ string id,
+ string? query,
+ string? screenQuery,
DotNetObjectReference? dotnetObj)
{
- return jsRuntime.InvokeVoid("BitBlazorUI.MediaQuery.setup", id, query, dotnetObj);
+ return jsRuntime.InvokeVoid("BitBlazorUI.MediaQuery.setup", id, query, screenQuery, dotnetObj);
}
internal static ValueTask BitMediaQueryDispose(this IJSRuntime jsRuntime, string id)
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitScreenQuery.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitScreenQuery.cs
index 5914d6217c..2b843937ee 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitScreenQuery.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/MediaQuery/BitScreenQuery.cs
@@ -3,85 +3,95 @@
///
/// The predefined screen media queries in the bit BlazorUI.
///
+///
+/// When used via , the effective media query is built at
+/// runtime from the live --bit-bp-* breakpoint tokens (i.e. from
+/// when you customize them), not from fixed values baked
+/// into the component - so overriding the theme breakpoints changes these queries too. The pixel
+/// values shown on each member below are the built-in defaults (used when the matching
+/// --bit-bp-* variable is unset). Range members are half-open: the upper bound is one CSS
+/// pixel below the next breakpoint. For a one-off breakpoint that isn't part of the theme scale,
+/// use with an explicit query string instead.
+///
public enum BitScreenQuery
{
///
- /// Extra small query: [@media screen and (max-width: 600px)]
+ /// Extra small query: [@media screen and (max-width: 599px)]
///
Xs,
///
- /// Small query: [@media screen and (min-width: 601px) and (max-width: 960px)]
+ /// Small query: [@media screen and (min-width: 600px) and (max-width: 959px)]
///
Sm,
///
- /// Medium query: [@media screen and (min-width: 961px) and (max-width: 1280px)]
+ /// Medium query: [@media screen and (min-width: 960px) and (max-width: 1279px)]
///
Md,
///
- /// Large query: [@media screen and (min-width: 1281px) and (max-width: 1920px)]
+ /// Large query: [@media screen and (min-width: 1280px) and (max-width: 1919px)]
///
Lg,
///
- /// Extra large query: [@media screen and (min-width: 1921px) and (max-width: 2560px)]
+ /// Extra large query: [@media screen and (min-width: 1920px) and (max-width: 2559px)]
///
Xl,
///
- /// Extra extra large query: [@media screen and (min-width: 2561px)]
+ /// Extra extra large query: [@media screen and (min-width: 2560px)]
///
Xxl,
///
- /// Less than small query: [@media screen and (max-width: 600px)]
+ /// Less than small query: [@media screen and (max-width: 599px)]
///
LtSm,
///
- /// Less than medium query: [@media screen and (max-width: 960px)]
+ /// Less than medium query: [@media screen and (max-width: 959px)]
///
LtMd,
///
- /// Less than large query: [@media screen and (max-width: 1280px)]
+ /// Less than large query: [@media screen and (max-width: 1279px)]
///
LtLg,
///
- /// Less than extra large query: [@media screen and (max-width: 1920px)]
+ /// Less than extra large query: [@media screen and (max-width: 1919px)]
///
LtXl,
///
- /// Less than extra extra large query: [@media screen and (max-width: 2560px)]
+ /// Less than extra extra large query: [@media screen and (max-width: 2559px)]
///
LtXxl,
///
- /// Greater than extra small query: [@media screen and (min-width: 601px)]
+ /// Greater than extra small query: [@media screen and (min-width: 600px)]
///
GtXs,
///
- /// Greater than small query: [@media screen and (min-width: 961px)]
+ /// Greater than small query: [@media screen and (min-width: 960px)]
///
GtSm,
///
- /// Greater than medium query: [@media screen and (min-width: 1281px)]
+ /// Greater than medium query: [@media screen and (min-width: 1280px)]
///
GtMd,
///
- /// Greater than large query: [@media screen and (min-width: 1921px)]
+ /// Greater than large query: [@media screen and (min-width: 1920px)]
///
GtLg,
///
- /// Greater than extra large query: [@media screen and (min-width: 2561px)]
+ /// Greater than extra large query: [@media screen and (min-width: 2560px)]
///
GtXl
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Extensions/IBitBlazorUIServiceCollectionExtensions.cs b/src/BlazorUI/Bit.BlazorUI/Extensions/IBitBlazorUIServiceCollectionExtensions.cs
index fd45c072d8..94cdb49624 100644
--- a/src/BlazorUI/Bit.BlazorUI/Extensions/IBitBlazorUIServiceCollectionExtensions.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Extensions/IBitBlazorUIServiceCollectionExtensions.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
namespace Bit.BlazorUI;
@@ -18,7 +19,24 @@ public static class IBitBlazorUIServiceCollectionExtensions
///
public static IServiceCollection AddBitBlazorUIServices(this IServiceCollection services, bool trySingleton = false)
{
- services.TryAddScoped();
+ services.TryAddScoped(sp =>
+ new BitThemeNotifications(sp.GetService()));
+
+ // BitThemeJsNotifierReceiver is internal (consumers should listen on
+ // BitThemeNotifications.ThemeChanged), but DI still resolves it for us.
+ services.TryAddScoped(sp =>
+ new BitThemeJsNotifierReceiver(
+ sp.GetRequiredService(),
+ sp.GetService()));
+
+ services.TryAddScoped(sp =>
+ new BitThemeManager(
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetService()));
+
+ services.TryAddScoped();
+
services.TryAddScoped();
if (trySingleton)
diff --git a/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/IJSRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/IJSRuntimeExtensions.cs
index ad822d6354..c754dfd48c 100644
--- a/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/IJSRuntimeExtensions.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/IJSRuntimeExtensions.cs
@@ -1,4 +1,5 @@
using System.Reflection;
+using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
namespace Bit.BlazorUI;
@@ -7,6 +8,11 @@ public static class IJSRuntimeExtensions
{
public const DynamicallyAccessedMemberTypes JsonSerialized = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties;
+ // Cache the per-runtime-type "is this runtime invalid?" probe. The probe is resolved once per
+ // concrete IJSRuntime type via reflection (the framework exposes no public API for this) and
+ // reused thereafter, so the reflection cost is paid once rather than on every interop call.
+ private static readonly ConcurrentDictionary> RuntimeInvalidProbes = new();
+
///
@@ -78,14 +84,47 @@ public static bool IsRuntimeInvalid(this IJSRuntime jsRuntime)
{
if (jsRuntime is null) return false;
- var type = jsRuntime.GetType();
+ // Resolve (and cache) a probe for this concrete runtime type. The probe is defensive: it
+ // relies on framework-internal type names / members that can shift between .NET releases,
+ // so any missing member or reflection failure is treated as "runtime is valid" (return
+ // false) rather than throwing - a wrong-but-safe answer that lets the interop call proceed
+ // and surface a real error, instead of crashing here.
+ var probe = RuntimeInvalidProbes.GetOrAdd(jsRuntime.GetType(), BuildRuntimeInvalidProbe);
+ return probe(jsRuntime);
+ }
- return type.Name switch
+ [SuppressMessage("Trimming", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "Framework-internal members probed reflectively; failures fall back to 'valid'.")]
+ private static Func BuildRuntimeInvalidProbe(Type type)
+ {
+ switch (type.Name)
{
- "UnsupportedJavaScriptRuntime" => true, // Prerendering
- "RemoteJSRuntime" => (bool)type.GetProperty("IsInitialized")!.GetValue(jsRuntime)! is false, // Blazor server
- "WebViewJSRuntime" => type.GetField("_ipcSender", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(jsRuntime) is null, // Blazor Hybrid
- _ => false // Blazor WASM
- };
+ case "UnsupportedJavaScriptRuntime": // Prerendering
+ return static _ => true;
+
+ case "RemoteJSRuntime": // Blazor Server
+ {
+ var isInitialized = type.GetProperty("IsInitialized", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+ if (isInitialized is null) return static _ => false;
+ return rt =>
+ {
+ try { return isInitialized.GetValue(rt) is false; }
+ catch { return false; }
+ };
+ }
+
+ case "WebViewJSRuntime": // Blazor Hybrid
+ {
+ var ipcSender = type.GetField("_ipcSender", BindingFlags.NonPublic | BindingFlags.Instance);
+ if (ipcSender is null) return static _ => false;
+ return rt =>
+ {
+ try { return ipcSender.GetValue(rt) is null; }
+ catch { return false; }
+ };
+ }
+
+ default: // Blazor WASM and anything else: assume valid.
+ return static _ => false;
+ }
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent-dark.scss b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent-dark.scss
index 991b0d46cb..80b0c72361 100644
--- a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent-dark.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent-dark.scss
@@ -9,3 +9,9 @@
@import "shapes.fluent.scss";
@import "typography.fluent.scss";
+
+@import "motion.fluent";
+
+@import "forced-colors.fluent";
+
+@import "semantic-tokens.fluent";
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent-light.scss b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent-light.scss
index b91212ae0c..e1af5ee056 100644
--- a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent-light.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent-light.scss
@@ -9,3 +9,9 @@
@import "shapes.fluent.scss";
@import "typography.fluent.scss";
+
+@import "motion.fluent";
+
+@import "forced-colors.fluent";
+
+@import "semantic-tokens.fluent";
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent.scss b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent.scss
index e01dddb9de..203b521173 100644
--- a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/bit.blazorui.fluent.scss
@@ -13,3 +13,9 @@
@import "shapes.fluent.scss";
@import "typography.fluent.scss";
+
+@import "motion.fluent";
+
+@import "forced-colors.fluent";
+
+@import "semantic-tokens.fluent";
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/colors.fluent-dark.scss b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/colors.fluent-dark.scss
index a9cf715ed3..dc9feda94a 100644
--- a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/colors.fluent-dark.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/colors.fluent-dark.scss
@@ -12,6 +12,8 @@
--bit-clr-pri-light-hover: #78B2FF;
--bit-clr-pri-light-active: #67A9FF;
--bit-clr-pri-text: #212121;
+ --bit-clr-pri-dis: #2A4A6B;
+ --bit-clr-pri-dis-text: #4A7AAF;
//secondary
--bit-clr-sec: #FF9E67;
--bit-clr-sec-hover: #E58E5D;
@@ -23,6 +25,8 @@
--bit-clr-sec-light-hover: #FFB185;
--bit-clr-sec-light-active: #FFA876;
--bit-clr-sec-text: #212121;
+ --bit-clr-sec-dis: #5C3A22;
+ --bit-clr-sec-dis-text: #A06840;
//tertiary
--bit-clr-ter: #E6E6E6;
--bit-clr-ter-hover: #CFCFCF;
@@ -34,6 +38,8 @@
--bit-clr-ter-light-hover: #EBEBEB;
--bit-clr-ter-light-active: #E9E9E9;
--bit-clr-ter-text: #212121;
+ --bit-clr-ter-dis: #3D3D3D;
+ --bit-clr-ter-dis-text: #6B6B6B;
//info
--bit-clr-inf: #ABACAE;
--bit-clr-inf-hover: #9A9B9D;
@@ -45,6 +51,8 @@
--bit-clr-inf-light-hover: #BCBDBE;
--bit-clr-inf-light-active: #B3B4B6;
--bit-clr-inf-text: #212121;
+ --bit-clr-inf-dis: #3A3D40;
+ --bit-clr-inf-dis-text: #6B7078;
//success
--bit-clr-suc: #7FB58A;
--bit-clr-suc-hover: #72A37C;
@@ -56,6 +64,8 @@
--bit-clr-suc-light-hover: #99C4A1;
--bit-clr-suc-light-active: #8CBC96;
--bit-clr-suc-text: #212121;
+ --bit-clr-suc-dis: #2A4A2E;
+ --bit-clr-suc-dis-text: #4A8050;
//warning
--bit-clr-wrn: #FFD07A;
--bit-clr-wrn-hover: #E5BB6E;
@@ -67,6 +77,8 @@
--bit-clr-wrn-light-hover: #FFD995;
--bit-clr-wrn-light-active: #FFD587;
--bit-clr-wrn-text: #212121;
+ --bit-clr-wrn-dis: #5C4A1F;
+ --bit-clr-wrn-dis-text: #A08540;
//severe-warning
--bit-clr-swr: #E8A874;
--bit-clr-swr-hover: #D19768;
@@ -78,6 +90,8 @@
--bit-clr-swr-light-hover: #EDB990;
--bit-clr-swr-light-active: #EAB182;
--bit-clr-swr-text: #212121;
+ --bit-clr-swr-dis: #5C3A1F;
+ --bit-clr-swr-dis-text: #A06830;
//error
--bit-clr-err: #DC7C7C;
--bit-clr-err-hover: #C67070;
@@ -89,6 +103,8 @@
--bit-clr-err-light-hover: #E39696;
--bit-clr-err-light-active: #E08989;
--bit-clr-err-text: #212121;
+ --bit-clr-err-dis: #5C2A2A;
+ --bit-clr-err-dis-text: #A05050;
//foregrounds
--bit-clr-fg-pri: #F6F7F8;
--bit-clr-fg-pri-hover: #DEDFE0;
@@ -99,6 +115,8 @@
--bit-clr-fg-pri-light: #FFFFFF;
--bit-clr-fg-pri-light-hover: #EDEDED;
--bit-clr-fg-pri-light-active: #DCDCDC;
+ --bit-clr-fg-pri-dis: #4A4A4A;
+ --bit-clr-fg-pri-dis-text: #3A3A3A;
--bit-clr-fg-sec: #ADB5BF;
--bit-clr-fg-sec-hover: #9DA4AD;
--bit-clr-fg-sec-active: #8B9199;
@@ -108,6 +126,8 @@
--bit-clr-fg-sec-light: #B8C1CC;
--bit-clr-fg-sec-light-hover: #A8B1BB;
--bit-clr-fg-sec-light-active: #979DA7;
+ --bit-clr-fg-sec-dis: #3D4450;
+ --bit-clr-fg-sec-dis-text: #2E343D;
--bit-clr-fg-ter: #616973;
--bit-clr-fg-ter-hover: #565D66;
--bit-clr-fg-ter-active: #4B5159;
@@ -117,6 +137,8 @@
--bit-clr-fg-ter-light: #717881;
--bit-clr-fg-ter-light-hover: #636A72;
--bit-clr-fg-ter-light-active: #575D64;
+ --bit-clr-fg-ter-dis: #2E333A;
+ --bit-clr-fg-ter-dis-text: #22272D;
--bit-clr-fg-dis: #666;
//backgrounds
--bit-clr-bg-pri: #010409;
@@ -128,6 +150,8 @@
--bit-clr-bg-pri-light: #030914;
--bit-clr-bg-pri-light-hover: #151C24;
--bit-clr-bg-pri-light-active: #111A24;
+ --bit-clr-bg-pri-dis: #0A0E13;
+ --bit-clr-bg-pri-dis-text: #3A3A3A;
--bit-clr-bg-sec: #101419;
--bit-clr-bg-sec-hover: #1C222B;
--bit-clr-bg-sec-active: #202731;
@@ -137,6 +161,8 @@
--bit-clr-bg-sec-light: #1A1F26;
--bit-clr-bg-sec-light-hover: #242A33;
--bit-clr-bg-sec-light-active: #2B323D;
+ --bit-clr-bg-sec-dis: #141920;
+ --bit-clr-bg-sec-dis-text: #3A3A3A;
--bit-clr-bg-ter: #181F26;
--bit-clr-bg-ter-hover: #1B232B;
--bit-clr-bg-ter-active: #202A33;
@@ -146,6 +172,8 @@
--bit-clr-bg-ter-light: #202830;
--bit-clr-bg-ter-light-hover: #212A33;
--bit-clr-bg-ter-light-active: #27313B;
+ --bit-clr-bg-ter-dis: #1C232B;
+ --bit-clr-bg-ter-dis-text: #3A3A3A;
--bit-clr-bg-dis: #222;
--bit-clr-bg-overlay: rgba(255, 255, 255, 0.2);
//borders
@@ -158,6 +186,8 @@
--bit-clr-brd-pri-light: #78838E;
--bit-clr-brd-pri-light-hover: #A1AFBD;
--bit-clr-brd-pri-light-active: #B5C3D3;
+ --bit-clr-brd-pri-dis: #3A4250;
+ --bit-clr-brd-pri-dis-text: #4A5260;
--bit-clr-brd-sec: #38424D;
--bit-clr-brd-sec-hover: #5E6F80;
--bit-clr-brd-sec-active: #718599;
@@ -167,6 +197,8 @@
--bit-clr-brd-sec-light: #424C57;
--bit-clr-brd-sec-light-hover: #68798A;
--bit-clr-brd-sec-light-active: #7E91A4;
+ --bit-clr-brd-sec-dis: #252D36;
+ --bit-clr-brd-sec-dis-text: #3A4250;
--bit-clr-brd-ter: #232933;
--bit-clr-brd-ter-hover: #465266;
--bit-clr-brd-ter-active: #586780;
@@ -176,7 +208,33 @@
--bit-clr-brd-ter-light: #2E343E;
--bit-clr-brd-ter-light-hover: #505B6F;
--bit-clr-brd-ter-light-active: #637189;
+ --bit-clr-brd-ter-dis: #1A1F27;
+ --bit-clr-brd-ter-dis-text: #2A3038;
--bit-clr-brd-dis: #21262D;
+
//req
--bit-clr-req: #A4262C;
+
+ //focus (a11y) - color of the focus indicator per role.
+ //Aliased to the role's base color so they track theme/brand overrides.
+ //Override these directly to differentiate focus from base color.
+ --bit-clr-pri-focus: var(--bit-clr-pri);
+ --bit-clr-sec-focus: var(--bit-clr-sec);
+ --bit-clr-ter-focus: var(--bit-clr-ter);
+ --bit-clr-inf-focus: var(--bit-clr-inf);
+ --bit-clr-suc-focus: var(--bit-clr-suc);
+ --bit-clr-wrn-focus: var(--bit-clr-wrn);
+ --bit-clr-swr-focus: var(--bit-clr-swr);
+ --bit-clr-err-focus: var(--bit-clr-err);
+ --bit-clr-fg-pri-focus: var(--bit-clr-fg-pri);
+ --bit-clr-fg-sec-focus: var(--bit-clr-fg-sec);
+ --bit-clr-fg-ter-focus: var(--bit-clr-fg-ter);
+ //The background roles are aliased to the foreground color instead of their own background,
+ //otherwise the focus indicator would be drawn in the very color it sits on and stay invisible.
+ --bit-clr-bg-pri-focus: var(--bit-clr-fg-pri);
+ --bit-clr-bg-sec-focus: var(--bit-clr-fg-pri);
+ --bit-clr-bg-ter-focus: var(--bit-clr-fg-pri);
+ --bit-clr-brd-pri-focus: var(--bit-clr-brd-pri);
+ --bit-clr-brd-sec-focus: var(--bit-clr-brd-sec);
+ --bit-clr-brd-ter-focus: var(--bit-clr-brd-ter);
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/colors.fluent-light.scss b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/colors.fluent-light.scss
index 8932a59828..bc6d4575e9 100644
--- a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/colors.fluent-light.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/colors.fluent-light.scss
@@ -14,6 +14,8 @@
--bit-clr-pri-light-hover: #8CC3EC;
--bit-clr-pri-light-active: #76B6E8;
--bit-clr-pri-text: #FFFFFF;
+ --bit-clr-pri-dis: #A3CFEF;
+ --bit-clr-pri-dis-text: #8BBDE5;
//secondary
--bit-clr-sec: #FF7D33;
--bit-clr-sec-hover: #F27730;
@@ -25,6 +27,8 @@
--bit-clr-sec-light-hover: #FFB185;
--bit-clr-sec-light-active: #FFA470;
--bit-clr-sec-text: #FFFFFF;
+ --bit-clr-sec-dis: #FFCFB3;
+ --bit-clr-sec-dis-text: #FFAD80;
//tertiary
--bit-clr-ter: #434343;
--bit-clr-ter-hover: #3C3C3C;
@@ -36,6 +40,8 @@
--bit-clr-ter-light-hover: #8E8E8E;
--bit-clr-ter-light-active: #7B7B7B;
--bit-clr-ter-text: #FFFFFF;
+ --bit-clr-ter-dis: #B8B8B8;
+ --bit-clr-ter-dis-text: #9E9E9E;
//info
--bit-clr-inf: #888F95;
--bit-clr-inf-hover: #81888E;
@@ -47,6 +53,8 @@
--bit-clr-inf-light-hover: #B8BCBF;
--bit-clr-inf-light-active: #ACB1B5;
--bit-clr-inf-text: #FFFFFF;
+ --bit-clr-inf-dis: #C3C7CA;
+ --bit-clr-inf-dis-text: #ACB1B5;
//success
--bit-clr-suc: #49A94C;
--bit-clr-suc-hover: #45A148;
@@ -58,6 +66,8 @@
--bit-clr-suc-light-hover: #92CB94;
--bit-clr-suc-light-active: #80C382;
--bit-clr-suc-text: #FFFFFF;
+ --bit-clr-suc-dis: #B5DDB7;
+ --bit-clr-suc-dis-text: #8CC98F;
//warning
--bit-clr-wrn: #F2B01F;
--bit-clr-wrn-hover: #E6A71D;
@@ -69,6 +79,8 @@
--bit-clr-wrn-light-hover: #F7D079;
--bit-clr-wrn-light-active: #F6C862;
--bit-clr-wrn-text: #FFFFFF;
+ --bit-clr-wrn-dis: #FBDFAA;
+ --bit-clr-wrn-dis-text: #F5C75E;
//severe-warning
--bit-clr-swr: #D47E30;
--bit-clr-swr-hover: #C9782E;
@@ -80,6 +92,8 @@
--bit-clr-swr-light-hover: #DD9859;
--bit-clr-swr-light-active: #DA914F;
--bit-clr-swr-text: #FFFFFF;
+ --bit-clr-swr-dis: #EABB8E;
+ --bit-clr-swr-dis-text: #D9A06A;
//error
--bit-clr-err: #DF3A2E;
--bit-clr-err-hover: #D4372C;
@@ -91,6 +105,8 @@
--bit-clr-err-light-hover: #E56158;
--bit-clr-err-light-active: #E4584D;
--bit-clr-err-text: #FFFFFF;
+ --bit-clr-err-dis: #F0ADA8;
+ --bit-clr-err-dis-text: #E8827A;
//foreground
--bit-clr-fg-pri: #1A1A1A;
--bit-clr-fg-pri-hover: #111111;
@@ -101,6 +117,8 @@
--bit-clr-fg-pri-light: #262626;
--bit-clr-fg-pri-light-hover: #191919;
--bit-clr-fg-pri-light-active: #111111;
+ --bit-clr-fg-pri-dis: #8A8A8A;
+ --bit-clr-fg-pri-dis-text: #BEBEBE;
--bit-clr-fg-sec: #5A5A5A;
--bit-clr-fg-sec-hover: #444444;
--bit-clr-fg-sec-active: #333333;
@@ -110,6 +128,8 @@
--bit-clr-fg-sec-light: #484848;
--bit-clr-fg-sec-light-hover: #515151;
--bit-clr-fg-sec-light-active: #414141;
+ --bit-clr-fg-sec-dis: #A3A3A3;
+ --bit-clr-fg-sec-dis-text: #C8C8C8;
--bit-clr-fg-ter: #999999;
--bit-clr-fg-ter-hover: #666666;
--bit-clr-fg-ter-active: #4D4D4D;
@@ -119,6 +139,8 @@
--bit-clr-fg-ter-light: #A7A7A7;
--bit-clr-fg-ter-light-hover: #747474;
--bit-clr-fg-ter-light-active: #5A5A5A;
+ --bit-clr-fg-ter-dis: #C8C8C8;
+ --bit-clr-fg-ter-dis-text: #DADADA;
--bit-clr-fg-dis: #AAAAAA;
//backgrounds
--bit-clr-bg-pri: #FFFFFF;
@@ -130,6 +152,8 @@
--bit-clr-bg-pri-light: #FFFFFF;
--bit-clr-bg-pri-light-hover: #F2F2F2;
--bit-clr-bg-pri-light-active: #EBEBEB;
+ --bit-clr-bg-pri-dis: #F7F7F7;
+ --bit-clr-bg-pri-dis-text: #CCCCCC;
--bit-clr-bg-sec: #F5F5F5;
--bit-clr-bg-sec-hover: #F2F1F0;
--bit-clr-bg-sec-active: #E8E7E6;
@@ -139,6 +163,8 @@
--bit-clr-bg-sec-light: #F5F5F5;
--bit-clr-bg-sec-light-hover: #F2F1F0;
--bit-clr-bg-sec-light-active: #E8E7E6;
+ --bit-clr-bg-sec-dis: #F9F9F9;
+ --bit-clr-bg-sec-dis-text: #CCCCCC;
--bit-clr-bg-ter: #EDECEB;
--bit-clr-bg-ter-hover: #E5E5E4;
--bit-clr-bg-ter-active: #E0DFDF;
@@ -148,6 +174,8 @@
--bit-clr-bg-ter-light: #F1F1F1;
--bit-clr-bg-ter-light-hover: #EDEDED;
--bit-clr-bg-ter-light-active: #E6E6E6;
+ --bit-clr-bg-ter-dis: #F5F5F4;
+ --bit-clr-bg-ter-dis-text: #CCCCCC;
--bit-clr-bg-dis: #F3F2F1;
--bit-clr-bg-overlay: rgba(0, 0, 0, 0.4);
//borders
@@ -160,6 +188,8 @@
--bit-clr-brd-pri-light: #898989;
--bit-clr-brd-pri-light-hover: #6C6B6B;
--bit-clr-brd-pri-light-active: #535353;
+ --bit-clr-brd-pri-dis: #BEBEBE;
+ --bit-clr-brd-pri-dis-text: #CCCCCC;
--bit-clr-brd-sec: #B0AFAE;
--bit-clr-brd-sec-hover: #7D7C7C;
--bit-clr-brd-sec-active: #636362;
@@ -169,6 +199,8 @@
--bit-clr-brd-sec-light: #BFBFBF;
--bit-clr-brd-sec-light-hover: #8A8A8A;
--bit-clr-brd-sec-light-active: #727272;
+ --bit-clr-brd-sec-dis: #D8D8D7;
+ --bit-clr-brd-sec-dis-text: #CCCCCC;
--bit-clr-brd-ter: #DEDDDC;
--bit-clr-brd-ter-hover: #CBCBCB;
--bit-clr-brd-ter-active: #B0B0B0;
@@ -178,7 +210,33 @@
--bit-clr-brd-ter-light: #E9E9E9;
--bit-clr-brd-ter-light-hover: #D7D7D7;
--bit-clr-brd-ter-light-active: #C1C1C1;
+ --bit-clr-brd-ter-dis: #EEEDEC;
+ --bit-clr-brd-ter-dis-text: #CCCCCC;
--bit-clr-brd-dis: #E5E3DF;
+
//req
--bit-clr-req: #A4262C;
+
+ //focus (a11y) - color of the focus indicator per role.
+ //Aliased to the role's base color so they track theme/brand overrides.
+ //Override these directly to differentiate focus from base color.
+ --bit-clr-pri-focus: var(--bit-clr-pri);
+ --bit-clr-sec-focus: var(--bit-clr-sec);
+ --bit-clr-ter-focus: var(--bit-clr-ter);
+ --bit-clr-inf-focus: var(--bit-clr-inf);
+ --bit-clr-suc-focus: var(--bit-clr-suc);
+ --bit-clr-wrn-focus: var(--bit-clr-wrn);
+ --bit-clr-swr-focus: var(--bit-clr-swr);
+ --bit-clr-err-focus: var(--bit-clr-err);
+ --bit-clr-fg-pri-focus: var(--bit-clr-fg-pri);
+ --bit-clr-fg-sec-focus: var(--bit-clr-fg-sec);
+ --bit-clr-fg-ter-focus: var(--bit-clr-fg-ter);
+ //The background roles are aliased to the foreground color instead of their own background,
+ //otherwise the focus indicator would be drawn in the very color it sits on and stay invisible.
+ --bit-clr-bg-pri-focus: var(--bit-clr-fg-pri);
+ --bit-clr-bg-sec-focus: var(--bit-clr-fg-pri);
+ --bit-clr-bg-ter-focus: var(--bit-clr-fg-pri);
+ --bit-clr-brd-pri-focus: var(--bit-clr-brd-pri);
+ --bit-clr-brd-sec-focus: var(--bit-clr-brd-sec);
+ --bit-clr-brd-ter-focus: var(--bit-clr-brd-ter);
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/forced-colors.fluent.scss b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/forced-colors.fluent.scss
new file mode 100644
index 0000000000..61af526970
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/forced-colors.fluent.scss
@@ -0,0 +1,54 @@
+// Windows High Contrast / forced-colors: preserve readability with system palette.
+@media (forced-colors: active) {
+ :root {
+ --bit-clr-fg-pri: CanvasText;
+ --bit-clr-fg-sec: CanvasText;
+ --bit-clr-fg-ter: CanvasText;
+ --bit-clr-bg-pri: Canvas;
+ --bit-clr-bg-sec: Canvas;
+ --bit-clr-bg-ter: Canvas;
+ --bit-clr-brd-pri: CanvasText;
+ --bit-clr-brd-sec: CanvasText;
+ --bit-clr-pri: LinkText;
+ --bit-clr-pri-hover: LinkText;
+ --bit-clr-pri-active: LinkText;
+ --bit-clr-pri-text: Canvas;
+ --bit-clr-err: CanvasText;
+ --bit-clr-suc: CanvasText;
+ --bit-clr-wrn: CanvasText;
+ --bit-shd-cal: none;
+ --bit-shd-sm: none;
+ --bit-shd-nm: none;
+ --bit-shd-md: none;
+ --bit-shd-lg: none;
+
+ // Focus must remain visible in High Contrast - use system Highlight color and a single solid ring.
+ --bit-clr-pri-focus: Highlight;
+ --bit-clr-sec-focus: Highlight;
+ --bit-clr-ter-focus: Highlight;
+ --bit-clr-inf-focus: Highlight;
+ --bit-clr-suc-focus: Highlight;
+ --bit-clr-wrn-focus: Highlight;
+ --bit-clr-swr-focus: Highlight;
+ --bit-clr-err-focus: Highlight;
+ --bit-clr-fg-pri-focus: Highlight;
+ --bit-clr-bg-pri-focus: Canvas;
+ --bit-clr-brd-pri-focus: Highlight;
+ --bit-clr-fg-sec-focus: Highlight;
+ --bit-clr-bg-sec-focus: Canvas;
+ --bit-clr-brd-sec-focus: Highlight;
+ --bit-clr-fg-ter-focus: Highlight;
+ --bit-clr-bg-ter-focus: Canvas;
+ --bit-clr-brd-ter-focus: Highlight;
+ }
+}
+
+// Stronger contrast preference (non-forced): deepen borders and primary text slightly.
+// Use a concrete value rather than self-referencing var(--bit-shp-brd-width, …) - a custom
+// property cannot reference its own current value (the cycle resolves to the fallback per
+// the CSS spec, which makes the intent unclear and brittle across engines).
+@media (prefers-contrast: more) {
+ :root {
+ --bit-shp-brd-width: 1px;
+ }
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/motion.fluent.scss b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/motion.fluent.scss
new file mode 100644
index 0000000000..2b0a55ccfd
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/motion.fluent.scss
@@ -0,0 +1,15 @@
+:root {
+ --bit-mot-duration: 200ms;
+ --bit-mot-duration-short: 100ms;
+ --bit-mot-duration-long: 300ms;
+ --bit-mot-easing: cubic-bezier(0.33, 0, 0.67, 1);
+ --bit-layout-density-scale: 1;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ :root {
+ --bit-mot-duration: 0ms;
+ --bit-mot-duration-short: 0ms;
+ --bit-mot-duration-long: 0ms;
+ }
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/semantic-tokens.fluent.scss b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/semantic-tokens.fluent.scss
new file mode 100644
index 0000000000..0d58bf1444
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/semantic-tokens.fluent.scss
@@ -0,0 +1,12 @@
+// Semantic aliases over primitive tokens - use in app CSS instead of raw --bit-clr-* where intent matters.
+:root {
+ --bit-sem-surface-page: var(--bit-clr-bg-pri);
+ --bit-sem-surface-elevated: var(--bit-clr-bg-sec);
+ --bit-sem-surface-muted: var(--bit-clr-bg-ter);
+ --bit-sem-text-primary: var(--bit-clr-fg-pri);
+ --bit-sem-text-secondary: var(--bit-clr-fg-sec);
+ --bit-sem-border-default: var(--bit-clr-brd-pri);
+ --bit-sem-accent-primary: var(--bit-clr-pri);
+ --bit-sem-focus-ring: var(--bit-shd-focus-ring);
+ --bit-sem-focus-color: var(--bit-clr-pri-focus);
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/shapes.fluent.scss b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/shapes.fluent.scss
index ab1287f1b6..3aa2a8a602 100644
--- a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/shapes.fluent.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/shapes.fluent.scss
@@ -16,4 +16,16 @@
--bit-shp-brd-radius: 0.125rem;
--bit-shp-brd-width: 0.0625rem;
--bit-shp-brd-style: solid;
+
+ //focus ring (a11y) - width is the visible thickness, offset is the gap between the element and the ring.
+ --bit-shp-focus-ring-width: 0.125rem; //2px
+ --bit-shp-focus-ring-offset: 0.125rem; //2px
+
+ //global focus shadow used by the focus-ring mixin and the .bit-css-shd-focus utility.
+ //Inner ring uses the page background (--bit-clr-bg-pri, not the focus-state background token) to create a separator between the element and the colored ring (WCAG 2.4.11/2.4.13).
+ //The mixin sets --bit-focus-ring-color/--bit-focus-ring-offset locally on the focused element so per-call overrides apply.
+ //In forced-colors (High Contrast) mode box-shadows are stripped entirely; the focus-ring mixin restores a real Highlight outline there instead.
+ --bit-shd-focus-ring:
+ 0 0 0 var(--bit-focus-ring-offset, var(--bit-shp-focus-ring-offset)) var(--bit-clr-bg-pri),
+ 0 0 0 calc(var(--bit-focus-ring-offset, var(--bit-shp-focus-ring-offset)) + var(--bit-shp-focus-ring-width)) var(--bit-focus-ring-color, var(--bit-clr-pri-focus));
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/bit-css.scss b/src/BlazorUI/Bit.BlazorUI/Styles/bit-css.scss
index 5fbd89cc52..338c3e5c9c 100644
--- a/src/BlazorUI/Bit.BlazorUI/Styles/bit-css.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/bit-css.scss
@@ -1,4 +1,4 @@
-@import "theme-variables.scss";
+@import "functions.scss";
//primary
.bit-css-clr-pri {
@@ -939,3 +939,41 @@
.bit-css-shp-brd-style {
border-style: $shp-border-style;
}
+
+//focus (a11y) - color tokens applied on :focus-visible.
+.bit-css-clr-pri-focus:focus-visible {
+ color: $clr-pri-focus;
+}
+
+.bit-css-clr-sec-focus:focus-visible {
+ color: $clr-sec-focus;
+}
+
+.bit-css-clr-ter-focus:focus-visible {
+ color: $clr-ter-focus;
+}
+
+.bit-css-clr-inf-focus:focus-visible {
+ color: $clr-inf-focus;
+}
+
+.bit-css-clr-suc-focus:focus-visible {
+ color: $clr-suc-focus;
+}
+
+.bit-css-clr-wrn-focus:focus-visible {
+ color: $clr-wrn-focus;
+}
+
+.bit-css-clr-swr-focus:focus-visible {
+ color: $clr-swr-focus;
+}
+
+.bit-css-clr-err-focus:focus-visible {
+ color: $clr-err-focus;
+}
+
+//focus ring utility - drop-in class to apply the global, theme-aware focus indicator.
+.bit-css-shd-focus-ring:focus-visible {
+ @include focus-ring;
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/functions.scss b/src/BlazorUI/Bit.BlazorUI/Styles/functions.scss
index feee193b9f..233bcba63d 100644
--- a/src/BlazorUI/Bit.BlazorUI/Styles/functions.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/functions.scss
@@ -18,3 +18,65 @@ $html-font-size: 16px;
@function spacing($spacingValue) {
@return calc($spacing-scaling-factor * $spacingValue);
}
+
+/*
+ * focus-ring($color, $offset)
+ * --------------------------------------------------
+ * Renders a consistent, theme-aware focus indicator across components.
+ *
+ * Behavior:
+ * - Uses a two-layer box-shadow: an inner ring matching the page background
+ * (separator) and an outer colored ring (visible indicator). This satisfies
+ * WCAG 2.4.11 (Focus Not Obscured) and 2.4.13 (Focus Appearance).
+ * - Width is driven by --bit-shp-focus-ring-width and offset by --bit-shp-focus-ring-offset,
+ * both globally themable.
+ * - Color defaults to --bit-clr-pri-focus and accepts any role token (e.g. $clr-err-focus)
+ * so invalid/success/etc. variants can opt into matching focus colors.
+ * - In Windows High Contrast / forced-colors mode box-shadows are stripped, so a real
+ * outline in the system Highlight color is rendered instead (matching focus-underline-ring).
+ *
+ * Usage:
+ * .my-component {
+ * &:focus-visible { @include focus-ring; }
+ * &.bit-inv:focus-visible { @include focus-ring($clr-err-focus); }
+ * }
+ */
+@mixin focus-ring($color: $clr-pri-focus, $offset: $shp-focus-ring-offset) {
+ outline: none; // Replace native outline with the themed ring below.
+ --bit-focus-ring-color: #{$color};
+ --bit-focus-ring-offset: #{$offset};
+
+ // Inline fallback mirrors the global --bit-shd-focus-ring expression in shapes.fluent.scss
+ // so focus indicators still render if a consumer-defined theme omits that token.
+ box-shadow: var(--bit-shd-focus-ring,
+ 0 0 0 var(--bit-focus-ring-offset, #{$offset}) var(--bit-clr-bg-pri),
+ 0 0 0 calc(var(--bit-focus-ring-offset, #{$offset}) + #{$shp-focus-ring-width}) var(--bit-focus-ring-color, #{$color}));
+
+ // In Windows High Contrast / forced-colors mode box-shadows are stripped, which would leave the
+ // focused element with no visible indicator. Restore a real outline so focus stays perceivable.
+ @media (forced-colors: active) {
+ outline: #{$shp-focus-ring-width} solid Highlight;
+
+ // Read the offset through the same custom property the box-shadow uses so a runtime
+ // override of --bit-focus-ring-offset stays in sync in High Contrast mode too.
+ outline-offset: var(--bit-focus-ring-offset, #{$offset});
+ }
+}
+
+/*
+ * focus-underline-ring($color)
+ * --------------------------------------------------
+ * Renders a focus indicator as a single bottom rule, used by "underlined"
+ * input variants (TextField/SearchBox/DatePicker .bit-*-und) where a full ring
+ * conflicts with the visual language of the component.
+ */
+@mixin focus-underline-ring($color: $clr-pri-focus) {
+ outline: none;
+ box-shadow: inset 0 calc(-1 * #{$shp-focus-ring-width}) 0 0 #{$color};
+
+ // In Windows High Contrast / forced-colors mode box-shadows are stripped, which would leave the
+ // focused input with no visible indicator. Restore a real outline so focus stays perceivable.
+ @media (forced-colors: active) {
+ outline: #{$shp-focus-ring-width} solid Highlight;
+ }
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/media-queries.scss b/src/BlazorUI/Bit.BlazorUI/Styles/media-queries.scss
index c1628fe3f8..ced80bd3d4 100644
--- a/src/BlazorUI/Bit.BlazorUI/Styles/media-queries.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/media-queries.scss
@@ -1,110 +1,120 @@
// https://github.com/Necromancerx/media-queries-scss-mixins
-$brk-xs-max: 600px;
-$brk-sm-min: 601px;
-$brk-sm-max: 960px;
-$brk-md-min: 961px;
-$brk-md-max: 1280px;
-$brk-lg-min: 1281px;
-$brk-lg-max: 1920px;
-$brk-xl-min: 1921px;
-$brk-xl-max: 2560px;
-$brk-xxl-min: 2561px;
+$mq-xs-max: 599px;
+$mq-sm-min: 600px;
+$mq-sm-max: 959px;
+$mq-md-min: 960px;
+$mq-md-max: 1279px;
+$mq-lg-min: 1280px;
+$mq-lg-max: 1919px;
+$mq-xl-min: 1920px;
+$mq-xl-max: 2559px;
+$mq-xxl-min: 2560px;
+
+:root {
+ --bit-bp-xs: 0;
+ --bit-bp-sm: #{$mq-sm-min};
+ --bit-bp-md: #{$mq-md-min};
+ --bit-bp-lg: #{$mq-lg-min};
+ --bit-bp-xl: #{$mq-xl-min};
+ --bit-bp-xxl: #{$mq-xxl-min};
+}
// media devices
@mixin xs {
- @media screen and (max-width: #{$brk-xs-max}) {
+ @media screen and (max-width: #{$mq-xs-max}) {
@content;
}
}
+
@mixin sm {
- @media screen and (min-width: #{$brk-sm-min}) and (max-width: #{$brk-sm-max}) {
+ @media screen and (min-width: #{$mq-sm-min}) and (max-width: #{$mq-sm-max}) {
@content;
}
}
@mixin md {
- @media screen and (min-width: #{$brk-md-min}) and (max-width: #{$brk-md-max}) {
+ @media screen and (min-width: #{$mq-md-min}) and (max-width: #{$mq-md-max}) {
@content;
}
}
@mixin lg {
- @media screen and (min-width: #{$brk-lg-min}) and (max-width: #{$brk-lg-max}) {
+ @media screen and (min-width: #{$mq-lg-min}) and (max-width: #{$mq-lg-max}) {
@content;
}
}
@mixin xl {
- @media screen and (min-width: #{$brk-xl-min}) and (max-width: #{$brk-xl-max}) {
+ @media screen and (min-width: #{$mq-xl-min}) and (max-width: #{$mq-xl-max}) {
@content;
}
}
@mixin xxl {
- @media screen and (min-width: #{$brk-xxl-min}) {
+ @media screen and (min-width: #{$mq-xxl-min}) {
@content;
}
}
// media lt queries
@mixin lt-sm {
- @media screen and (max-width: #{$brk-xs-max}) {
+ @media screen and (max-width: #{$mq-xs-max}) {
@content;
}
}
@mixin lt-md {
- @media screen and (max-width: #{$brk-sm-max}) {
+ @media screen and (max-width: #{$mq-sm-max}) {
@content;
}
}
@mixin lt-lg {
- @media screen and (max-width: #{$brk-md-max}) {
+ @media screen and (max-width: #{$mq-md-max}) {
@content;
}
}
@mixin lt-xl {
- @media screen and (max-width: #{$brk-lg-max}) {
+ @media screen and (max-width: #{$mq-lg-max}) {
@content;
}
}
@mixin lt-xxl {
- @media screen and (max-width: #{$brk-xl-max}) {
+ @media screen and (max-width: #{$mq-xl-max}) {
@content;
}
}
// media gt queries
@mixin gt-xs {
- @media screen and (min-width: #{$brk-sm-min}) {
+ @media screen and (min-width: #{$mq-sm-min}) {
@content;
}
}
@mixin gt-sm {
- @media screen and (min-width: #{$brk-md-min}) {
+ @media screen and (min-width: #{$mq-md-min}) {
@content;
}
}
@mixin gt-md {
- @media screen and (min-width: #{$brk-lg-min}) {
+ @media screen and (min-width: #{$mq-lg-min}) {
@content;
}
}
@mixin gt-lg {
- @media screen and (min-width: #{$brk-xl-min}) {
+ @media screen and (min-width: #{$mq-xl-min}) {
@content;
}
}
@mixin gt-xl {
- @media screen and (min-width: #{$brk-xxl-min}) {
+ @media screen and (min-width: #{$mq-xxl-min}) {
@content;
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/theme-variables.scss b/src/BlazorUI/Bit.BlazorUI/Styles/theme-variables.scss
index 66c82ab061..46c73eb04f 100644
--- a/src/BlazorUI/Bit.BlazorUI/Styles/theme-variables.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Styles/theme-variables.scss
@@ -105,6 +105,8 @@ $clr-fg-pri-dark-active: var(--bit-clr-fg-pri-dark-active);
$clr-fg-pri-light: var(--bit-clr-fg-pri-light);
$clr-fg-pri-light-hover: var(--bit-clr-fg-pri-light-hover);
$clr-fg-pri-light-active: var(--bit-clr-fg-pri-light-active);
+$clr-fg-pri-dis: var(--bit-clr-fg-pri-dis);
+$clr-fg-pri-dis-text: var(--bit-clr-fg-pri-dis-text);
$clr-fg-sec: var(--bit-clr-fg-sec);
$clr-fg-sec-hover: var(--bit-clr-fg-sec-hover);
$clr-fg-sec-active: var(--bit-clr-fg-sec-active);
@@ -114,6 +116,8 @@ $clr-fg-sec-dark-active: var(--bit-clr-fg-sec-dark-active);
$clr-fg-sec-light: var(--bit-clr-fg-sec-light);
$clr-fg-sec-light-hover: var(--bit-clr-fg-sec-light-hover);
$clr-fg-sec-light-active: var(--bit-clr-fg-sec-light-active);
+$clr-fg-sec-dis: var(--bit-clr-fg-sec-dis);
+$clr-fg-sec-dis-text: var(--bit-clr-fg-sec-dis-text);
$clr-fg-ter: var(--bit-clr-fg-ter);
$clr-fg-ter-hover: var(--bit-clr-fg-ter-hover);
$clr-fg-ter-active: var(--bit-clr-fg-ter-active);
@@ -123,6 +127,8 @@ $clr-fg-ter-dark-active: var(--bit-clr-fg-ter-dark-active);
$clr-fg-ter-light: var(--bit-clr-fg-ter-light);
$clr-fg-ter-light-hover: var(--bit-clr-fg-ter-light-hover);
$clr-fg-ter-light-active: var(--bit-clr-fg-ter-light-active);
+$clr-fg-ter-dis: var(--bit-clr-fg-ter-dis);
+$clr-fg-ter-dis-text: var(--bit-clr-fg-ter-dis-text);
$clr-fg-dis: var(--bit-clr-fg-dis);
//backgrounds
@@ -135,6 +141,8 @@ $clr-bg-pri-dark-active: var(--bit-clr-bg-pri-dark-active);
$clr-bg-pri-light: var(--bit-clr-bg-pri-light);
$clr-bg-pri-light-hover: var(--bit-clr-bg-pri-light-hover);
$clr-bg-pri-light-active: var(--bit-clr-bg-pri-light-active);
+$clr-bg-pri-dis: var(--bit-clr-bg-pri-dis);
+$clr-bg-pri-dis-text: var(--bit-clr-bg-pri-dis-text);
$clr-bg-sec: var(--bit-clr-bg-sec);
$clr-bg-sec-hover: var(--bit-clr-bg-sec-hover);
$clr-bg-sec-active: var(--bit-clr-bg-sec-active);
@@ -144,6 +152,8 @@ $clr-bg-sec-dark-active: var(--bit-clr-bg-sec-dark-active);
$clr-bg-sec-light: var(--bit-clr-bg-sec-light);
$clr-bg-sec-light-hover: var(--bit-clr-bg-sec-light-hover);
$clr-bg-sec-light-active: var(--bit-clr-bg-sec-light-active);
+$clr-bg-sec-dis: var(--bit-clr-bg-sec-dis);
+$clr-bg-sec-dis-text: var(--bit-clr-bg-sec-dis-text);
$clr-bg-ter: var(--bit-clr-bg-ter);
$clr-bg-ter-hover: var(--bit-clr-bg-ter-hover);
$clr-bg-ter-active: var(--bit-clr-bg-ter-active);
@@ -153,6 +163,8 @@ $clr-bg-ter-dark-active: var(--bit-clr-bg-ter-dark-active);
$clr-bg-ter-light: var(--bit-clr-bg-ter-light);
$clr-bg-ter-light-hover: var(--bit-clr-bg-ter-light-hover);
$clr-bg-ter-light-active: var(--bit-clr-bg-ter-light-active);
+$clr-bg-ter-dis: var(--bit-clr-bg-ter-dis);
+$clr-bg-ter-dis-text: var(--bit-clr-bg-ter-dis-text);
$clr-bg-dis: var(--bit-clr-bg-dis);
$clr-bg-overlay: var(--bit-clr-bg-overlay);
@@ -166,6 +178,8 @@ $clr-brd-pri-dark-active: var(--bit-clr-brd-pri-dark-active);
$clr-brd-pri-light: var(--bit-clr-brd-pri-light);
$clr-brd-pri-light-hover: var(--bit-clr-brd-pri-light-hover);
$clr-brd-pri-light-active: var(--bit-clr-brd-pri-light-active);
+$clr-brd-pri-dis: var(--bit-clr-brd-pri-dis);
+$clr-brd-pri-dis-text: var(--bit-clr-brd-pri-dis-text);
$clr-brd-sec: var(--bit-clr-brd-sec);
$clr-brd-sec-hover: var(--bit-clr-brd-sec-hover);
$clr-brd-sec-active: var(--bit-clr-brd-sec-active);
@@ -175,6 +189,8 @@ $clr-brd-sec-dark-active: var(--bit-clr-brd-sec-dark-active);
$clr-brd-sec-light: var(--bit-clr-brd-sec-light);
$clr-brd-sec-light-hover: var(--bit-clr-brd-sec-light-hover);
$clr-brd-sec-light-active: var(--bit-clr-brd-sec-light-active);
+$clr-brd-sec-dis: var(--bit-clr-brd-sec-dis);
+$clr-brd-sec-dis-text: var(--bit-clr-brd-sec-dis-text);
$clr-brd-ter: var(--bit-clr-brd-ter);
$clr-brd-ter-hover: var(--bit-clr-brd-ter-hover);
$clr-brd-ter-active: var(--bit-clr-brd-ter-active);
@@ -184,11 +200,50 @@ $clr-brd-ter-dark-active: var(--bit-clr-brd-ter-dark-active);
$clr-brd-ter-light: var(--bit-clr-brd-ter-light);
$clr-brd-ter-light-hover: var(--bit-clr-brd-ter-light-hover);
$clr-brd-ter-light-active: var(--bit-clr-brd-ter-light-active);
+$clr-brd-ter-dis: var(--bit-clr-brd-ter-dis);
+$clr-brd-ter-dis-text: var(--bit-clr-brd-ter-dis-text);
$clr-brd-dis: var(--bit-clr-brd-dis);
//required
$clr-req: var(--bit-clr-req);
+//disabled - per-role disabled colors
+$clr-pri-dis: var(--bit-clr-pri-dis);
+$clr-pri-dis-text: var(--bit-clr-pri-dis-text);
+$clr-sec-dis: var(--bit-clr-sec-dis);
+$clr-sec-dis-text: var(--bit-clr-sec-dis-text);
+$clr-ter-dis: var(--bit-clr-ter-dis);
+$clr-ter-dis-text: var(--bit-clr-ter-dis-text);
+$clr-inf-dis: var(--bit-clr-inf-dis);
+$clr-inf-dis-text: var(--bit-clr-inf-dis-text);
+$clr-suc-dis: var(--bit-clr-suc-dis);
+$clr-suc-dis-text: var(--bit-clr-suc-dis-text);
+$clr-wrn-dis: var(--bit-clr-wrn-dis);
+$clr-wrn-dis-text: var(--bit-clr-wrn-dis-text);
+$clr-swr-dis: var(--bit-clr-swr-dis);
+$clr-swr-dis-text: var(--bit-clr-swr-dis-text);
+$clr-err-dis: var(--bit-clr-err-dis);
+$clr-err-dis-text: var(--bit-clr-err-dis-text);
+
+//focus (a11y) - per-role focus indicator color, plus shape/shadow tokens consumed by the focus-ring mixin.
+$clr-pri-focus: var(--bit-clr-pri-focus);
+$clr-sec-focus: var(--bit-clr-sec-focus);
+$clr-ter-focus: var(--bit-clr-ter-focus);
+$clr-inf-focus: var(--bit-clr-inf-focus);
+$clr-suc-focus: var(--bit-clr-suc-focus);
+$clr-wrn-focus: var(--bit-clr-wrn-focus);
+$clr-swr-focus: var(--bit-clr-swr-focus);
+$clr-err-focus: var(--bit-clr-err-focus);
+$clr-fg-pri-focus: var(--bit-clr-fg-pri-focus);
+$clr-fg-sec-focus: var(--bit-clr-fg-sec-focus);
+$clr-fg-ter-focus: var(--bit-clr-fg-ter-focus);
+$clr-bg-pri-focus: var(--bit-clr-bg-pri-focus);
+$clr-bg-sec-focus: var(--bit-clr-bg-sec-focus);
+$clr-bg-ter-focus: var(--bit-clr-bg-ter-focus);
+$clr-brd-pri-focus: var(--bit-clr-brd-pri-focus);
+$clr-brd-sec-focus: var(--bit-clr-brd-sec-focus);
+$clr-brd-ter-focus: var(--bit-clr-brd-ter-focus);
+
//neutrals
$clr-ntr-white: var(--bit-clr-ntr-white);
$clr-ntr-black: var(--bit-clr-ntr-black);
@@ -268,6 +323,11 @@ $zindex-base: var(--bit-zin-base);
$shp-border-radius: var(--bit-shp-brd-radius);
$shp-border-width: var(--bit-shp-brd-width);
$shp-border-style: var(--bit-shp-brd-style);
+$shp-focus-ring-width: var(--bit-shp-focus-ring-width);
+$shp-focus-ring-offset: var(--bit-shp-focus-ring-offset);
+
+//shadows-focus
+$box-shadow-focus-ring: var(--bit-shd-focus-ring);
/*---- Typography ----*/
$tg-font-family: var(--bit-tpg-font-family);
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitAccentColorPresets.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitAccentColorPresets.cs
new file mode 100644
index 0000000000..4d575ec0bd
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitAccentColorPresets.cs
@@ -0,0 +1,14 @@
+namespace Bit.BlazorUI;
+
+///
+/// Sample accent (primary) hex values for branding - assign to or derive via .
+///
+public static class BitAccentColorPresets
+{
+ public const string Blue = "#1A86D8";
+ public const string Purple = "#8764B8";
+ public const string Green = "#107C10";
+ public const string Orange = "#CA5010";
+ public const string Teal = "#038387";
+ public const string Rose = "#C239B3";
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitExternalThemeJsExtensions.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitExternalThemeJsExtensions.cs
new file mode 100644
index 0000000000..e1643b94fc
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitExternalThemeJsExtensions.cs
@@ -0,0 +1,14 @@
+namespace Bit.BlazorUI;
+
+internal static class BitExternalThemeJsExtensions
+{
+ internal static ValueTask BitExternalThemeAttach(this IJSRuntime js, string linkElementId, string href)
+ {
+ return js.InvokeVoid("BitBlazorUI.ExternalTheme.attach", linkElementId, href);
+ }
+
+ internal static ValueTask BitExternalThemeDetach(this IJSRuntime js, string linkElementId)
+ {
+ return js.InvokeVoid("BitBlazorUI.ExternalTheme.detach", linkElementId);
+ }
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitExternalThemeLoader.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitExternalThemeLoader.cs
new file mode 100644
index 0000000000..0dc67476f3
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitExternalThemeLoader.cs
@@ -0,0 +1,112 @@
+namespace Bit.BlazorUI;
+
+///
+/// Loads optional alternate CSS bundles by id (creates or updates a <link rel="stylesheet"> ).
+/// Use only trusted/same-origin URLs. The loader rejects relative-with-scheme strings such as
+/// javascript: and data: , rejects protocol-relative URLs (//host/... );
+/// for absolute URLs the scheme must be http or https .
+///
+public sealed class BitExternalThemeLoader
+{
+ private readonly IJSRuntime _js;
+
+ public BitExternalThemeLoader(IJSRuntime js)
+ {
+ ArgumentNullException.ThrowIfNull(js);
+
+ _js = js;
+ }
+
+ ///
+ /// Creates (or updates) a <link rel="stylesheet"> element identified by
+ /// with the given .
+ ///
+ /// DOM id used to find / create the link element.
+ ///
+ /// Stylesheet URL. Only same-origin references are accepted: a same-origin relative path
+ /// (e.g. /css/dark.css ) or an absolute http/https URL that resolves to the current
+ /// origin. Non-http(s) schemes and protocol-relative URLs are rejected here, and the JS side
+ /// additionally enforces same-origin (cross-origin http/https URLs are rejected there).
+ ///
+ public ValueTask AttachStylesheetAsync(string linkElementId, string href)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(linkElementId);
+ ArgumentException.ThrowIfNullOrWhiteSpace(href);
+
+ // Defense-in-depth href validation. This C# check rejects `javascript:`/`data:`/`vbscript:`
+ // schemes and protocol-relative URLs up front so a tampered or user-supplied href can't
+ // attach a dangerous . The JS side (BitTheme.ts) repeats these checks and also
+ // enforces same-origin, rejecting cross-origin http/https URLs; this C# layer intentionally
+ // does not re-implement the origin comparison (it has no reliable view of the document
+ // origin) and leaves that final gate to the browser-side validator. Relative paths
+ // (no scheme) are allowed because that's the common same-origin case
+ // ("/css/dark.css", "themes/light.css").
+ ValidateHref(href);
+
+ return _js.BitExternalThemeAttach(linkElementId, href);
+ }
+
+ /// Removes a previously-attached <link rel="stylesheet"> by id.
+ /// DOM id originally passed to .
+ public ValueTask DetachStylesheetAsync(string linkElementId)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(linkElementId);
+
+ return _js.BitExternalThemeDetach(linkElementId);
+ }
+
+ private static void ValidateHref(string href)
+ {
+ // Protocol-relative URLs (e.g. "//evil.example/x.css") are not absolute URIs as far as
+ // Uri.TryCreate(Absolute) is concerned, so they would otherwise slip past the scheme
+ // check below and reach the browser, which resolves them to a cross-origin
+ // http(s)://evil.example/x.css. Reject them up front; callers must use a same-origin
+ // relative path or an explicit http/https URL. Browsers also normalize backslashes to
+ // forward slashes in URLs, so `\\host\x.css` is the same attack surface - block it too.
+ var trimmed = href.TrimStart();
+ if (trimmed.StartsWith("//", StringComparison.Ordinal) ||
+ trimmed.StartsWith(@"\\", StringComparison.Ordinal) ||
+ trimmed.StartsWith(@"/\", StringComparison.Ordinal) ||
+ trimmed.StartsWith(@"\/", StringComparison.Ordinal))
+ {
+ throw new ArgumentException(
+ "Stylesheet href must not be a protocol-relative URL. Use a same-origin relative path or an explicit http/https URL.",
+ nameof(href));
+ }
+
+ if (Uri.TryCreate(href, UriKind.Absolute, out var absolute))
+ {
+ // Reject every absolute-URL scheme other than http(s). That covers javascript:, data:,
+ // file:, vbscript:, blob:, etc. - none of which belong on a .
+ // Uri.UriSchemeHttp/Https are static readonly fields, not constants, so they can't be
+ // used in a relational pattern; compare with case-insensitive ordinal equality instead.
+ if (!string.Equals(absolute.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(absolute.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(
+ $"Stylesheet href scheme '{absolute.Scheme}' is not allowed. Use http, https, or a same-origin relative path.",
+ nameof(href));
+ }
+
+ return;
+ }
+
+ // Relative URLs are allowed. We still want to reject the `javascript:` form even when
+ // Uri.TryCreate refuses to parse it as absolute (it does for some inputs depending on
+ // platform), so a final string-prefix sanity check covers the residual surface.
+ if (LooksLikeDangerousRelativeScheme(trimmed))
+ {
+ throw new ArgumentException(
+ "Stylesheet href appears to use a non-http scheme (javascript:, data:, vbscript:). Use http, https, or a same-origin relative path.",
+ nameof(href));
+ }
+ }
+
+ private static bool LooksLikeDangerousRelativeScheme(string trimmed)
+ {
+ // Match common attack vectors that browsers used to (or still) accept on attribute values.
+ return trimmed.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase)
+ || trimmed.StartsWith("data:", StringComparison.OrdinalIgnoreCase)
+ || trimmed.StartsWith("vbscript:", StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitTheme.ts b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitTheme.ts
new file mode 100644
index 0000000000..2667277e7d
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitTheme.ts
@@ -0,0 +1,480 @@
+// Attribute / storage names - kept aligned with BitThemeAttributeNames.cs and BitThemeSsr.cs in
+// C#. If you rename a constant here, mirror the change there (the contract test under
+// Bit.BlazorUI.Tests.Utils.Theme will catch a mismatch).
+namespace BitBlazorUI {
+ const ATTR_THEME = 'bit-theme';
+ const ATTR_THEME_DEFAULT = 'bit-theme-default';
+ const ATTR_THEME_SYSTEM = 'bit-theme-system';
+ const ATTR_THEME_PERSIST = 'bit-theme-persist';
+ const ATTR_THEME_PERSIST_COOKIE = 'bit-theme-persist-cookie';
+ const ATTR_THEME_DARK = 'bit-theme-dark';
+ const ATTR_THEME_LIGHT = 'bit-theme-light';
+ const STORAGE_KEY = 'bit-current-theme';
+ // Kept aligned with BitThemeCookie.PreferenceCookieName in C#. When cookie persistence is
+ // enabled, the client mirrors the persisted preference into this cookie so the server can read
+ // the user's current choice (via BitThemeSsr.BuildRootThemeAttributes) and paint the matching
+ // theme on first server render - keeping the client (localStorage) and server (cookie) stores
+ // in sync instead of drifting apart.
+ const COOKIE_NAME = 'bit-theme-preference';
+ // ~400 days, the upper bound modern browsers clamp persistent cookies to.
+ const COOKIE_MAX_AGE_SECONDS = 34560000;
+
+ type onThemeChangeType = (newThemeName: string, oldThemeName: string) => void;
+
+ export interface ThemeOptions {
+ system?: boolean;
+ persist?: boolean;
+ /** Mirror the persisted preference into the COOKIE_NAME cookie so SSR stays in sync with client choices. */
+ persistCookie?: boolean;
+ theme?: string | null;
+ default?: string | null;
+ darkTheme?: string | null;
+ lightTheme?: string | null;
+ onChange?: onThemeChangeType;
+ }
+
+ interface ThemeSetOptions {
+ /** When true, startup init so resolved light/dark does not disable OS sync from bit-theme-system. */
+ fromInit?: boolean;
+ /** Reserved for internal OS refresh paths (same as normal set without touching follow-system flags). */
+ internalOsRefresh?: boolean;
+ }
+
+ export class Theme {
+ private static SYSTEM_THEME = 'system';
+ private static THEME_ATTRIBUTE = ATTR_THEME;
+ private static THEME_STORAGE_KEY = STORAGE_KEY;
+
+ private static _persist = false;
+ private static _persistCookie = false;
+ private static _darkTheme: string = 'dark';
+ private static _lightTheme: string = 'light';
+ private static _initOptions: ThemeOptions = {};
+ private static _currentTheme = Theme._lightTheme;
+ private static _onThemeChange: onThemeChangeType = () => { };
+
+ /** When true, user pinned an explicit theme via set (not system); disables following OS until set('system'). */
+ private static _stopFollowingSystem = false;
+
+ /**
+ * When true, the user explicitly opted into following the OS at runtime (e.g. via useSystem()),
+ * so we follow OS changes even without a persisted "system" preference or the bit-theme-system attribute.
+ * Cleared automatically when the user pins a concrete theme via set(...).
+ */
+ private static _runtimeFollowSystem = false;
+
+ private static _schemeMediaQuery: MediaQueryList | null = null;
+ private static _onSchemeChange = () => Theme.applyResolvedSystemThemeFromOs();
+
+ private static _dotnetNotifier: DotNetObject | null = null;
+
+ private static _appliedVarKeys = new WeakMap();
+
+ public static init(options: ThemeOptions) {
+ Object.assign(Theme._initOptions, options);
+
+ let deferPersist = false;
+ let deferPersistCookie = false;
+
+ if (Theme._initOptions.onChange) {
+ Theme._onThemeChange = Theme._initOptions.onChange;
+ }
+
+ if (Theme._initOptions.darkTheme) {
+ Theme._darkTheme = Theme._initOptions.darkTheme;
+ }
+
+ if (Theme._initOptions.lightTheme) {
+ Theme._lightTheme = Theme._initOptions.lightTheme;
+ }
+
+ // Cookie mirroring is independent of localStorage persistence: an SSR app may want the
+ // server to read the preference cookie without necessarily enabling localStorage.
+ // Derive from the merged _initOptions on every call (not just when truthy) so a later
+ // init({ persistCookie: false }) can clear the flag, while an init that omits the key
+ // keeps the previously merged value.
+ Theme._persistCookie = !!Theme._initOptions.persistCookie;
+
+ let theme = Theme._initOptions.theme || Theme._initOptions.default || Theme._lightTheme;
+
+ // A `system` opt-in comes from the JS `system: true` option / bit-theme-system attribute,
+ // OR from the base theme itself resolving to the "system" keyword (bit-theme="system" or
+ // bit-theme-default="system"). Capture it once so first-paint resolution, the persist
+ // deferral below and runtime OS-follow all treat these spellings identically.
+ const systemRequested = Theme._initOptions.system || theme === Theme.SYSTEM_THEME;
+
+ // Resolve the first-paint theme with the SAME precedence as the SSR inline script in
+ // BitThemeSsr.cs (BuildInlineScriptBody): a `system` opt-in follows the OS and takes
+ // precedence over an explicit bit-theme / bit-theme-default at first paint. A persisted
+ // preference (handled below) still wins over this.
+ //
+ // Previously this only resolved the OS theme when there was NO explicit theme/default,
+ // so a document with both bit-theme="..." and bit-theme-system painted the explicit
+ // theme on hydration while the SSR script painted the OS-resolved one - a flash of the
+ // wrong theme. Keeping the two code paths in lockstep avoids that.
+ if (systemRequested) {
+ theme = Theme.isSystemDark() ? Theme._darkTheme : Theme._lightTheme;
+ }
+
+ // Derive on every call (see persistCookie note above) so a later init({ persist: false })
+ // clears the flag, while an omitted key preserves the merged value.
+ Theme._persist = !!Theme._initOptions.persist;
+
+ // Restore a previously persisted preference. getPersisted() reads localStorage first and
+ // falls back to the preference cookie, so this also covers a cookie-only setup
+ // (persistCookie without persist) that a pure client-side render (no SSR inline script)
+ // would otherwise ignore - previously this read was gated on persist alone.
+ if (Theme._persist || Theme._persistCookie) {
+ const persisted = Theme.getPersisted();
+ if (persisted) {
+ theme = persisted;
+ // An explicit persisted preset (anything other than "system") means the user pinned a theme;
+ // stop following the OS even when is present.
+ Theme._stopFollowingSystem = persisted !== Theme.SYSTEM_THEME;
+ } else if (systemRequested) {
+ // System mode is enabled but no value has been persisted yet. Avoid writing the
+ // resolved light/dark theme to storage during the initial set() - otherwise the next
+ // init would treat that concrete value as an explicit user choice and stop following
+ // the OS. Disable BOTH stores for the initial set() and re-enable them afterwards so
+ // SYSTEM_THEME remains the effective persisted indicator until the user picks one.
+ deferPersist = Theme._persist;
+ deferPersistCookie = Theme._persistCookie;
+ Theme._persist = false;
+ Theme._persistCookie = false;
+ }
+ }
+
+ // Any `system` opt-in (system: true, bit-theme="system" or bit-theme-default="system")
+ // must on its own enable OS follow, otherwise setups without the bit-theme-system
+ // attribute and without a persisted "system" value would resolve the OS theme once at
+ // init but never get the prefers-color-scheme listener attached, because
+ // shouldFollowSystem() only considers the HTML attribute and persisted value. Runtime
+ // follow is still cleared when the user later pins a concrete theme via set(...).
+ if (systemRequested && !Theme._stopFollowingSystem) {
+ Theme._runtimeFollowSystem = true;
+ }
+
+ Theme.set(theme, { fromInit: true });
+
+ if (deferPersist) {
+ Theme._persist = true;
+ }
+
+ if (deferPersistCookie) {
+ Theme._persistCookie = true;
+ }
+ }
+
+ public static onChange(fn: onThemeChangeType) {
+ Theme._onThemeChange = fn;
+ }
+
+ public static get() {
+ // Report the theme that is actually applied: the bit-theme attribute is the source of
+ // truth (it is what is painted, and set()/OS-follow keep _currentTheme in sync with it).
+ // The persisted *preference* is a separate concept exposed via getPersisted(); the two
+ // can legitimately diverge - e.g. runtime OS-follow updates the attribute without
+ // rewriting storage - so get() must not substitute the persisted value here.
+ Theme._currentTheme = document.documentElement.getAttribute(Theme.THEME_ATTRIBUTE) || '';
+
+ return Theme._currentTheme;
+ }
+
+ public static set(themeName: string, options?: ThemeSetOptions) {
+ // Reject null / undefined / empty inputs up-front so we never call setAttribute(...) with
+ // a value that coerces to the literal string "null" or "undefined". The non-null
+ // assertion below was unsafe because getActualTheme can return null for null input.
+ if (!themeName) return Theme._currentTheme;
+
+ const fromInit = options?.fromInit === true;
+ const internalOs = options?.internalOsRefresh === true;
+
+ if (!fromInit && !internalOs) {
+ if (themeName === Theme.SYSTEM_THEME) {
+ Theme._stopFollowingSystem = false;
+ Theme._runtimeFollowSystem = true;
+ } else {
+ Theme._stopFollowingSystem = true;
+ Theme._runtimeFollowSystem = false;
+ }
+ }
+
+ const resolved = Theme.getActualTheme(themeName);
+ if (!resolved) return Theme._currentTheme;
+ Theme._currentTheme = resolved;
+
+ if (Theme._persist) {
+ // localStorage can throw in Safari private mode, in iframes that block storage,
+ // when over quota, or under restrictive document policies (e.g. file:// in some
+ // browsers). Theme persistence is best-effort - never let it break theme switching.
+ try {
+ localStorage.setItem(Theme.THEME_STORAGE_KEY, themeName);
+ } catch { /* persistence unavailable; continue without storing */ }
+ }
+
+ // Mirror the preference into the cookie so the server (BitThemeSsr.BuildRootThemeAttributes)
+ // can paint the matching theme on first render. Stored verbatim (the abstract key, e.g.
+ // "system" / "dark" / "fluent-light"), matching what localStorage holds.
+ if (Theme._persistCookie) {
+ Theme.writePreferenceCookie(themeName);
+ }
+
+ const oldTheme = document.documentElement.getAttribute(Theme.THEME_ATTRIBUTE) || '';
+
+ document.documentElement.setAttribute(Theme.THEME_ATTRIBUTE, Theme._currentTheme);
+
+ Theme.dispatchThemeChange(Theme._currentTheme, oldTheme);
+
+ Theme.syncSystemThemeListener();
+
+ return Theme._currentTheme;
+ }
+
+ public static toggleDarkLight() {
+ // Toggle relative to the configured dark theme: when the dark theme is active switch to
+ // light, otherwise switch to dark. Anchoring on the dark theme (rather than the light
+ // one) means a configured pair such as bit-theme-light="fluent-light" /
+ // bit-theme-dark="fluent-dark" toggles correctly in BOTH directions, and any other
+ // current value (a custom or unrecognized theme) resolves to the dark theme instead of
+ // silently collapsing to light.
+ Theme._currentTheme = Theme._currentTheme === Theme._darkTheme
+ ? Theme._lightTheme
+ : Theme._darkTheme;
+
+ Theme.set(Theme._currentTheme);
+
+ return Theme._currentTheme;
+ }
+
+ /** Pins storage (when persist is on) to system and follows OS light/dark until an explicit preset is set. */
+ public static useSystem() {
+ return Theme.set(Theme.SYSTEM_THEME);
+ }
+
+ public static applyTheme(theme: Record, element?: HTMLElement) {
+ const el = element || document.body;
+ const keys = Object.keys(theme);
+ const prev = Theme._appliedVarKeys.get(el) || [];
+ prev.filter(key => !keys.includes(key)).forEach(key => el.style.removeProperty(key));
+ keys.forEach(key => el.style.setProperty(key, theme[key]));
+ Theme._appliedVarKeys.set(el, keys);
+ }
+
+ /** Removes --bit-* properties previously applied by applyTheme on the target (default document.body). */
+ public static clearAppliedTheme(element?: HTMLElement) {
+ const el = element || document.body;
+ const keys = Theme._appliedVarKeys.get(el);
+ if (!keys || keys.length === 0) return;
+ keys.forEach(k => el.style.removeProperty(k));
+ Theme._appliedVarKeys.delete(el);
+ }
+
+ public static isSystemDark() {
+ return matchMedia('(prefers-color-scheme: dark)').matches;
+ }
+
+ public static getPersisted() {
+ if (Theme._persist) {
+ // Mirror the write side: localStorage.getItem can throw under the same conditions as
+ // setItem (Safari private mode, blocked storage, etc.). Treat failure as "no persisted
+ // value" so the rest of the resolution chain (system / default / lightTheme) takes over.
+ try {
+ const stored = localStorage.getItem(Theme.THEME_STORAGE_KEY);
+ if (stored) return stored;
+ } catch { /* fall through to cookie / null */ }
+ }
+
+ // Cookie-only persistence (or localStorage unavailable): read the same preference the
+ // server uses so the client and server agree on the stored choice.
+ if (Theme._persistCookie) {
+ return Theme.readPreferenceCookie();
+ }
+
+ return null;
+ }
+
+ public static registerDotNetNotifier(dotNetRef: DotNetObject) {
+ Theme._dotnetNotifier = dotNetRef;
+ }
+
+ public static unregisterDotNetNotifier() {
+ Theme._dotnetNotifier = null;
+ }
+
+ private static writePreferenceCookie(value: string) {
+ try {
+ if (typeof document === 'undefined' || !value) return;
+ const secure = location.protocol === 'https:' ? '; Secure' : '';
+ document.cookie =
+ `${COOKIE_NAME}=${encodeURIComponent(value)}; path=/; max-age=${COOKIE_MAX_AGE_SECONDS}; SameSite=Lax${secure}`;
+ } catch { /* cookies unavailable / blocked; best-effort like localStorage */ }
+ }
+
+ private static readPreferenceCookie(): string | null {
+ try {
+ if (typeof document === 'undefined' || !document.cookie) return null;
+ const prefix = `${COOKIE_NAME}=`;
+ const match = document.cookie
+ .split(';')
+ .map(c => c.trim())
+ .find(c => c.startsWith(prefix));
+ return match ? decodeURIComponent(match.substring(prefix.length)) : null;
+ } catch {
+ return null;
+ }
+ }
+
+ private static shouldFollowSystem(): boolean {
+ if (typeof document === 'undefined') return false;
+ if (Theme._stopFollowingSystem) return false;
+ // An explicitly persisted theme (anything other than SYSTEM_THEME) wins over the
+ // bit-theme-system attribute, otherwise a stale attribute could override the user's choice.
+ // Consider the persisted preference whenever any persistence source is active - including
+ // the cookie-only path (persistCookie without persist) that getPersisted() falls back to -
+ // so a persisted SYSTEM_THEME still attaches the OS-follow listener in a cookie-backed setup.
+ if (Theme._persist || Theme._persistCookie) {
+ const persisted = Theme.getPersisted();
+ if (persisted && persisted !== Theme.SYSTEM_THEME) return false;
+ if (persisted === Theme.SYSTEM_THEME) return true;
+ }
+ if (Theme._runtimeFollowSystem) return true;
+ if (document.documentElement.hasAttribute(ATTR_THEME_SYSTEM)) return true;
+ return false;
+ }
+
+ private static syncSystemThemeListener() {
+ Theme.detachSystemThemeListener();
+ if (!Theme.shouldFollowSystem()) return;
+ Theme.attachSystemThemeListener();
+ }
+
+ private static attachSystemThemeListener() {
+ if (!window.matchMedia) return;
+ Theme._schemeMediaQuery = matchMedia('(prefers-color-scheme: dark)');
+ const mq = Theme._schemeMediaQuery as MediaQueryList & { addListener?: (cb: () => void) => void };
+ if (typeof mq.addEventListener === 'function') {
+ mq.addEventListener('change', Theme._onSchemeChange);
+ } else {
+ mq.addListener?.(Theme._onSchemeChange);
+ }
+ }
+
+ private static detachSystemThemeListener() {
+ if (!Theme._schemeMediaQuery) return;
+ const mq = Theme._schemeMediaQuery as MediaQueryList & { removeListener?: (cb: () => void) => void };
+ if (typeof mq.removeEventListener === 'function') {
+ mq.removeEventListener('change', Theme._onSchemeChange);
+ } else {
+ mq.removeListener?.(Theme._onSchemeChange);
+ }
+ Theme._schemeMediaQuery = null;
+ }
+
+ private static applyResolvedSystemThemeFromOs() {
+ if (!Theme.shouldFollowSystem()) return;
+
+ const resolved = Theme.isSystemDark() ? Theme._darkTheme : Theme._lightTheme;
+ const oldTheme = document.documentElement.getAttribute(Theme.THEME_ATTRIBUTE) || '';
+
+ if (resolved === oldTheme) return;
+
+ Theme._currentTheme = resolved;
+ document.documentElement.setAttribute(Theme.THEME_ATTRIBUTE, resolved);
+ Theme.dispatchThemeChange(resolved, oldTheme);
+ }
+
+ private static dispatchThemeChange(newTheme: string, oldTheme: string) {
+ // Isolate the application callback: a throwing _onThemeChange must not abort the
+ // _dotnetNotifier notification below (nor the Theme.set flow into syncSystemThemeListener()).
+ try {
+ Theme._onThemeChange?.(newTheme, oldTheme);
+ } catch { /* application callback failed; not actionable here */ }
+ const n = Theme._dotnetNotifier;
+ if (n) {
+ // Swallow rejections so a disposed circuit / receiver does not surface as an
+ // unhandled promise rejection. Theme dispatch is fire-and-forget by design.
+ n.invokeMethodAsync('NotifyThemeChangedFromJs', newTheme, oldTheme)
+ .catch(() => { /* receiver gone or invocation failed; nothing actionable here */ });
+ }
+ }
+
+ private static getActualTheme(theme: string | null) {
+ if (theme === Theme.SYSTEM_THEME) {
+ return Theme.isSystemDark() ? Theme._darkTheme : Theme._lightTheme;
+ }
+
+ return theme;
+ }
+ }
+
+ /** Attach or swap alternate theme stylesheets at runtime (prefer same-origin / trusted URLs). */
+ export class ExternalTheme {
+ private static validateHref(href: string) {
+ const trimmed = href?.trimStart();
+ if (!trimmed) {
+ throw new Error('Stylesheet href is required.');
+ }
+ if (trimmed.startsWith('//') || trimmed.startsWith('\\\\') ||
+ trimmed.startsWith('/\\') || trimmed.startsWith('\\/')) {
+ throw new Error('Stylesheet href must not be a protocol-relative URL.');
+ }
+ if (/^javascript:/i.test(trimmed) || /^data:/i.test(trimmed) || /^vbscript:/i.test(trimmed)) {
+ throw new Error('Stylesheet href must not use a non-http scheme.');
+ }
+ let url: URL;
+ try {
+ url = new URL(href, document.baseURI);
+ } catch {
+ throw new Error('Stylesheet href is not a valid URL.');
+ }
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
+ throw new Error(`Stylesheet href scheme '${url.protocol}' is not allowed.`);
+ }
+ if (url.origin !== location.origin) {
+ throw new Error('Stylesheet href must be same-origin.');
+ }
+ }
+
+ public static attach(linkId: string, href: string) {
+ ExternalTheme.validateHref(href);
+
+ const existing = document.getElementById(linkId);
+ let link: HTMLLinkElement;
+ if (existing && existing.tagName === 'LINK') {
+ link = existing as HTMLLinkElement;
+ } else {
+ // No element, or an element with the same id but a different tag (e.g. a stale
+ // breakout defense-in-depth
+ case '\\': // CSS escape sequences
+ case '\0':
+ case '\n':
+ case '\r':
+ case '\f': // null / newlines / form feed
+ return true;
+ }
+ }
+
+ // Comment markers could swallow trailing declarations in the inline-style concatenation
+ // (e.g. "red/*" eating the following ";--next:..." up to a later "*/").
+ return value.Contains("/*", StringComparison.Ordinal)
+ || value.Contains("*/", StringComparison.Ordinal);
+ }
+
+ ///
+ /// Returns a copy of with every branch object non-null, WITHOUT mutating
+ /// the caller's instance. Used by and so a
+ /// hand-constructed sparse theme (null branches, reachable via the public setters) can be walked
+ /// without a .
+ ///
+ ///
+ /// Only the container types that hold sub-objects ( ,
+ /// , , )
+ /// are freshly allocated. Leaf-holding branch objects (the color/typography/etc. variants, whose
+ /// properties are all immutable s) are shared by reference: callers only ever
+ /// read from the normalized graph here - the merge writes into a separate fresh result - so no
+ /// shared object is mutated and no per-leaf copy is needed. Direct string leaves on the container
+ /// types (e.g. , ,
+ /// ) are carried across explicitly.
+ ///
+ private static BitTheme NormalizeToNew(BitTheme? src)
+ {
+ src ??= new BitTheme();
+
+ return new BitTheme
+ {
+ Color = NormalizeColors(src.Color),
+ BoxShadow = src.BoxShadow ?? new(),
+ Spacing = src.Spacing ?? new(),
+ ZIndex = src.ZIndex ?? new(),
+ Shape = src.Shape ?? new(),
+ Typography = NormalizeTypography(src.Typography),
+ Motion = src.Motion ?? new(),
+ Layout = NormalizeLayout(src.Layout),
+ };
+ }
+
+ private static BitThemeColors NormalizeColors(BitThemeColors? src)
+ {
+ src ??= new BitThemeColors();
+
+ return new BitThemeColors
+ {
+ Primary = src.Primary ?? new(),
+ Secondary = src.Secondary ?? new(),
+ Tertiary = src.Tertiary ?? new(),
+ Info = src.Info ?? new(),
+ Success = src.Success ?? new(),
+ Warning = src.Warning ?? new(),
+ SevereWarning = src.SevereWarning ?? new(),
+ Error = src.Error ?? new(),
+ Foreground = src.Foreground ?? new(),
+ Background = src.Background ?? new(),
+ Border = src.Border ?? new(),
+ Neutral = src.Neutral ?? new(),
+ Required = src.Required,
+ };
+ }
+
+ private static BitThemeTypography NormalizeTypography(BitThemeTypography? src)
+ {
+ src ??= new BitThemeTypography();
+
+ return new BitThemeTypography
+ {
+ FontFamily = src.FontFamily,
+ FontWeight = src.FontWeight,
+ LineHeight = src.LineHeight,
+ GutterSize = src.GutterSize,
+ H1 = src.H1 ?? new(),
+ H2 = src.H2 ?? new(),
+ H3 = src.H3 ?? new(),
+ H4 = src.H4 ?? new(),
+ H5 = src.H5 ?? new(),
+ H6 = src.H6 ?? new(),
+ Subtitle1 = src.Subtitle1 ?? new(),
+ Subtitle2 = src.Subtitle2 ?? new(),
+ Body1 = src.Body1 ?? new(),
+ Body2 = src.Body2 ?? new(),
+ Button = src.Button ?? new(),
+ Caption1 = src.Caption1 ?? new(),
+ Caption2 = src.Caption2 ?? new(),
+ Overline = src.Overline ?? new(),
+ Inherit = src.Inherit ?? new(),
+ };
+ }
+
+ private static BitThemeLayout NormalizeLayout(BitThemeLayout? src)
+ {
+ src ??= new BitThemeLayout();
+
+ return new BitThemeLayout
+ {
+ Direction = src.Direction,
+ DensityScale = src.DensityScale,
+ Breakpoints = src.Breakpoints ?? new(),
+ };
}
internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
{
var result = new BitTheme();
+ // Walk normalized COPIES so hand-constructed sparse themes (e.g. new BitTheme { Color = null }
+ // or new BitThemeColors { Primary = null }, both reachable via the public setters) can be
+ // traversed without NRE - WITHOUT mutating the caller's instances. (Previously this filled
+ // null branch objects in place on both passed themes as a side effect.)
+ bitTheme = NormalizeToNew(bitTheme);
+ other = NormalizeToNew(other);
+
result.Color.Primary.Main = bitTheme.Color.Primary.Main ?? other.Color.Primary.Main;
result.Color.Primary.MainHover = bitTheme.Color.Primary.MainHover ?? other.Color.Primary.MainHover;
result.Color.Primary.MainActive = bitTheme.Color.Primary.MainActive ?? other.Color.Primary.MainActive;
@@ -376,6 +606,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Primary.LightHover = bitTheme.Color.Primary.LightHover ?? other.Color.Primary.LightHover;
result.Color.Primary.LightActive = bitTheme.Color.Primary.LightActive ?? other.Color.Primary.LightActive;
result.Color.Primary.Text = bitTheme.Color.Primary.Text ?? other.Color.Primary.Text;
+ result.Color.Primary.Disabled = bitTheme.Color.Primary.Disabled ?? other.Color.Primary.Disabled;
+ result.Color.Primary.DisabledText = bitTheme.Color.Primary.DisabledText ?? other.Color.Primary.DisabledText;
+ result.Color.Primary.Focus = bitTheme.Color.Primary.Focus ?? other.Color.Primary.Focus;
result.Color.Secondary.Main = bitTheme.Color.Secondary.Main ?? other.Color.Secondary.Main;
result.Color.Secondary.MainHover = bitTheme.Color.Secondary.MainHover ?? other.Color.Secondary.MainHover;
@@ -387,6 +620,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Secondary.LightHover = bitTheme.Color.Secondary.LightHover ?? other.Color.Secondary.LightHover;
result.Color.Secondary.LightActive = bitTheme.Color.Secondary.LightActive ?? other.Color.Secondary.LightActive;
result.Color.Secondary.Text = bitTheme.Color.Secondary.Text ?? other.Color.Secondary.Text;
+ result.Color.Secondary.Disabled = bitTheme.Color.Secondary.Disabled ?? other.Color.Secondary.Disabled;
+ result.Color.Secondary.DisabledText = bitTheme.Color.Secondary.DisabledText ?? other.Color.Secondary.DisabledText;
+ result.Color.Secondary.Focus = bitTheme.Color.Secondary.Focus ?? other.Color.Secondary.Focus;
result.Color.Tertiary.Main = bitTheme.Color.Tertiary.Main ?? other.Color.Tertiary.Main;
result.Color.Tertiary.MainHover = bitTheme.Color.Tertiary.MainHover ?? other.Color.Tertiary.MainHover;
@@ -398,6 +634,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Tertiary.LightHover = bitTheme.Color.Tertiary.LightHover ?? other.Color.Tertiary.LightHover;
result.Color.Tertiary.LightActive = bitTheme.Color.Tertiary.LightActive ?? other.Color.Tertiary.LightActive;
result.Color.Tertiary.Text = bitTheme.Color.Tertiary.Text ?? other.Color.Tertiary.Text;
+ result.Color.Tertiary.Disabled = bitTheme.Color.Tertiary.Disabled ?? other.Color.Tertiary.Disabled;
+ result.Color.Tertiary.DisabledText = bitTheme.Color.Tertiary.DisabledText ?? other.Color.Tertiary.DisabledText;
+ result.Color.Tertiary.Focus = bitTheme.Color.Tertiary.Focus ?? other.Color.Tertiary.Focus;
result.Color.Info.Main = bitTheme.Color.Info.Main ?? other.Color.Info.Main;
result.Color.Info.MainHover = bitTheme.Color.Info.MainHover ?? other.Color.Info.MainHover;
@@ -409,6 +648,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Info.LightHover = bitTheme.Color.Info.LightHover ?? other.Color.Info.LightHover;
result.Color.Info.LightActive = bitTheme.Color.Info.LightActive ?? other.Color.Info.LightActive;
result.Color.Info.Text = bitTheme.Color.Info.Text ?? other.Color.Info.Text;
+ result.Color.Info.Disabled = bitTheme.Color.Info.Disabled ?? other.Color.Info.Disabled;
+ result.Color.Info.DisabledText = bitTheme.Color.Info.DisabledText ?? other.Color.Info.DisabledText;
+ result.Color.Info.Focus = bitTheme.Color.Info.Focus ?? other.Color.Info.Focus;
result.Color.Success.Main = bitTheme.Color.Success.Main ?? other.Color.Success.Main;
result.Color.Success.MainHover = bitTheme.Color.Success.MainHover ?? other.Color.Success.MainHover;
@@ -420,6 +662,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Success.LightHover = bitTheme.Color.Success.LightHover ?? other.Color.Success.LightHover;
result.Color.Success.LightActive = bitTheme.Color.Success.LightActive ?? other.Color.Success.LightActive;
result.Color.Success.Text = bitTheme.Color.Success.Text ?? other.Color.Success.Text;
+ result.Color.Success.Disabled = bitTheme.Color.Success.Disabled ?? other.Color.Success.Disabled;
+ result.Color.Success.DisabledText = bitTheme.Color.Success.DisabledText ?? other.Color.Success.DisabledText;
+ result.Color.Success.Focus = bitTheme.Color.Success.Focus ?? other.Color.Success.Focus;
result.Color.Warning.Main = bitTheme.Color.Warning.Main ?? other.Color.Warning.Main;
result.Color.Warning.MainHover = bitTheme.Color.Warning.MainHover ?? other.Color.Warning.MainHover;
@@ -431,6 +676,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Warning.LightHover = bitTheme.Color.Warning.LightHover ?? other.Color.Warning.LightHover;
result.Color.Warning.LightActive = bitTheme.Color.Warning.LightActive ?? other.Color.Warning.LightActive;
result.Color.Warning.Text = bitTheme.Color.Warning.Text ?? other.Color.Warning.Text;
+ result.Color.Warning.Disabled = bitTheme.Color.Warning.Disabled ?? other.Color.Warning.Disabled;
+ result.Color.Warning.DisabledText = bitTheme.Color.Warning.DisabledText ?? other.Color.Warning.DisabledText;
+ result.Color.Warning.Focus = bitTheme.Color.Warning.Focus ?? other.Color.Warning.Focus;
result.Color.SevereWarning.Main = bitTheme.Color.SevereWarning.Main ?? other.Color.SevereWarning.Main;
result.Color.SevereWarning.MainHover = bitTheme.Color.SevereWarning.MainHover ?? other.Color.SevereWarning.MainHover;
@@ -442,6 +690,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.SevereWarning.LightHover = bitTheme.Color.SevereWarning.LightHover ?? other.Color.SevereWarning.LightHover;
result.Color.SevereWarning.LightActive = bitTheme.Color.SevereWarning.LightActive ?? other.Color.SevereWarning.LightActive;
result.Color.SevereWarning.Text = bitTheme.Color.SevereWarning.Text ?? other.Color.SevereWarning.Text;
+ result.Color.SevereWarning.Disabled = bitTheme.Color.SevereWarning.Disabled ?? other.Color.SevereWarning.Disabled;
+ result.Color.SevereWarning.DisabledText = bitTheme.Color.SevereWarning.DisabledText ?? other.Color.SevereWarning.DisabledText;
+ result.Color.SevereWarning.Focus = bitTheme.Color.SevereWarning.Focus ?? other.Color.SevereWarning.Focus;
result.Color.Error.Main = bitTheme.Color.Error.Main ?? other.Color.Error.Main;
result.Color.Error.MainHover = bitTheme.Color.Error.MainHover ?? other.Color.Error.MainHover;
@@ -453,6 +704,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Error.LightHover = bitTheme.Color.Error.LightHover ?? other.Color.Error.LightHover;
result.Color.Error.LightActive = bitTheme.Color.Error.LightActive ?? other.Color.Error.LightActive;
result.Color.Error.Text = bitTheme.Color.Error.Text ?? other.Color.Error.Text;
+ result.Color.Error.Disabled = bitTheme.Color.Error.Disabled ?? other.Color.Error.Disabled;
+ result.Color.Error.DisabledText = bitTheme.Color.Error.DisabledText ?? other.Color.Error.DisabledText;
+ result.Color.Error.Focus = bitTheme.Color.Error.Focus ?? other.Color.Error.Focus;
result.Color.Foreground.Primary = bitTheme.Color.Foreground.Primary ?? other.Color.Foreground.Primary;
result.Color.Foreground.PrimaryHover = bitTheme.Color.Foreground.PrimaryHover ?? other.Color.Foreground.PrimaryHover;
@@ -463,6 +717,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Foreground.PrimaryLight = bitTheme.Color.Foreground.PrimaryLight ?? other.Color.Foreground.PrimaryLight;
result.Color.Foreground.PrimaryLightHover = bitTheme.Color.Foreground.PrimaryLightHover ?? other.Color.Foreground.PrimaryLightHover;
result.Color.Foreground.PrimaryLightActive = bitTheme.Color.Foreground.PrimaryLightActive ?? other.Color.Foreground.PrimaryLightActive;
+ result.Color.Foreground.PrimaryDisabled = bitTheme.Color.Foreground.PrimaryDisabled ?? other.Color.Foreground.PrimaryDisabled;
+ result.Color.Foreground.PrimaryDisabledText = bitTheme.Color.Foreground.PrimaryDisabledText ?? other.Color.Foreground.PrimaryDisabledText;
+ result.Color.Foreground.PrimaryFocus = bitTheme.Color.Foreground.PrimaryFocus ?? other.Color.Foreground.PrimaryFocus;
result.Color.Foreground.Secondary = bitTheme.Color.Foreground.Secondary ?? other.Color.Foreground.Secondary;
result.Color.Foreground.SecondaryHover = bitTheme.Color.Foreground.SecondaryHover ?? other.Color.Foreground.SecondaryHover;
result.Color.Foreground.SecondaryActive = bitTheme.Color.Foreground.SecondaryActive ?? other.Color.Foreground.SecondaryActive;
@@ -472,6 +729,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Foreground.SecondaryLight = bitTheme.Color.Foreground.SecondaryLight ?? other.Color.Foreground.SecondaryLight;
result.Color.Foreground.SecondaryLightHover = bitTheme.Color.Foreground.SecondaryLightHover ?? other.Color.Foreground.SecondaryLightHover;
result.Color.Foreground.SecondaryLightActive = bitTheme.Color.Foreground.SecondaryLightActive ?? other.Color.Foreground.SecondaryLightActive;
+ result.Color.Foreground.SecondaryDisabled = bitTheme.Color.Foreground.SecondaryDisabled ?? other.Color.Foreground.SecondaryDisabled;
+ result.Color.Foreground.SecondaryDisabledText = bitTheme.Color.Foreground.SecondaryDisabledText ?? other.Color.Foreground.SecondaryDisabledText;
+ result.Color.Foreground.SecondaryFocus = bitTheme.Color.Foreground.SecondaryFocus ?? other.Color.Foreground.SecondaryFocus;
result.Color.Foreground.Tertiary = bitTheme.Color.Foreground.Tertiary ?? other.Color.Foreground.Tertiary;
result.Color.Foreground.TertiaryHover = bitTheme.Color.Foreground.TertiaryHover ?? other.Color.Foreground.TertiaryHover;
result.Color.Foreground.TertiaryActive = bitTheme.Color.Foreground.TertiaryActive ?? other.Color.Foreground.TertiaryActive;
@@ -481,6 +741,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Foreground.TertiaryLight = bitTheme.Color.Foreground.TertiaryLight ?? other.Color.Foreground.TertiaryLight;
result.Color.Foreground.TertiaryLightHover = bitTheme.Color.Foreground.TertiaryLightHover ?? other.Color.Foreground.TertiaryLightHover;
result.Color.Foreground.TertiaryLightActive = bitTheme.Color.Foreground.TertiaryLightActive ?? other.Color.Foreground.TertiaryLightActive;
+ result.Color.Foreground.TertiaryDisabled = bitTheme.Color.Foreground.TertiaryDisabled ?? other.Color.Foreground.TertiaryDisabled;
+ result.Color.Foreground.TertiaryDisabledText = bitTheme.Color.Foreground.TertiaryDisabledText ?? other.Color.Foreground.TertiaryDisabledText;
+ result.Color.Foreground.TertiaryFocus = bitTheme.Color.Foreground.TertiaryFocus ?? other.Color.Foreground.TertiaryFocus;
result.Color.Foreground.Disabled = bitTheme.Color.Foreground.Disabled ?? other.Color.Foreground.Disabled;
result.Color.Background.Primary = bitTheme.Color.Background.Primary ?? other.Color.Background.Primary;
@@ -492,6 +755,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Background.PrimaryLight = bitTheme.Color.Background.PrimaryLight ?? other.Color.Background.PrimaryLight;
result.Color.Background.PrimaryLightHover = bitTheme.Color.Background.PrimaryLightHover ?? other.Color.Background.PrimaryLightHover;
result.Color.Background.PrimaryLightActive = bitTheme.Color.Background.PrimaryLightActive ?? other.Color.Background.PrimaryLightActive;
+ result.Color.Background.PrimaryDisabled = bitTheme.Color.Background.PrimaryDisabled ?? other.Color.Background.PrimaryDisabled;
+ result.Color.Background.PrimaryDisabledText = bitTheme.Color.Background.PrimaryDisabledText ?? other.Color.Background.PrimaryDisabledText;
+ result.Color.Background.PrimaryFocus = bitTheme.Color.Background.PrimaryFocus ?? other.Color.Background.PrimaryFocus;
result.Color.Background.Secondary = bitTheme.Color.Background.Secondary ?? other.Color.Background.Secondary;
result.Color.Background.SecondaryHover = bitTheme.Color.Background.SecondaryHover ?? other.Color.Background.SecondaryHover;
result.Color.Background.SecondaryActive = bitTheme.Color.Background.SecondaryActive ?? other.Color.Background.SecondaryActive;
@@ -501,6 +767,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Background.SecondaryLight = bitTheme.Color.Background.SecondaryLight ?? other.Color.Background.SecondaryLight;
result.Color.Background.SecondaryLightHover = bitTheme.Color.Background.SecondaryLightHover ?? other.Color.Background.SecondaryLightHover;
result.Color.Background.SecondaryLightActive = bitTheme.Color.Background.SecondaryLightActive ?? other.Color.Background.SecondaryLightActive;
+ result.Color.Background.SecondaryDisabled = bitTheme.Color.Background.SecondaryDisabled ?? other.Color.Background.SecondaryDisabled;
+ result.Color.Background.SecondaryDisabledText = bitTheme.Color.Background.SecondaryDisabledText ?? other.Color.Background.SecondaryDisabledText;
+ result.Color.Background.SecondaryFocus = bitTheme.Color.Background.SecondaryFocus ?? other.Color.Background.SecondaryFocus;
result.Color.Background.Tertiary = bitTheme.Color.Background.Tertiary ?? other.Color.Background.Tertiary;
result.Color.Background.TertiaryHover = bitTheme.Color.Background.TertiaryHover ?? other.Color.Background.TertiaryHover;
result.Color.Background.TertiaryActive = bitTheme.Color.Background.TertiaryActive ?? other.Color.Background.TertiaryActive;
@@ -510,6 +779,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Background.TertiaryLight = bitTheme.Color.Background.TertiaryLight ?? other.Color.Background.TertiaryLight;
result.Color.Background.TertiaryLightHover = bitTheme.Color.Background.TertiaryLightHover ?? other.Color.Background.TertiaryLightHover;
result.Color.Background.TertiaryLightActive = bitTheme.Color.Background.TertiaryLightActive ?? other.Color.Background.TertiaryLightActive;
+ result.Color.Background.TertiaryDisabled = bitTheme.Color.Background.TertiaryDisabled ?? other.Color.Background.TertiaryDisabled;
+ result.Color.Background.TertiaryDisabledText = bitTheme.Color.Background.TertiaryDisabledText ?? other.Color.Background.TertiaryDisabledText;
+ result.Color.Background.TertiaryFocus = bitTheme.Color.Background.TertiaryFocus ?? other.Color.Background.TertiaryFocus;
result.Color.Background.Disabled = bitTheme.Color.Background.Disabled ?? other.Color.Background.Disabled;
result.Color.Background.Overlay = bitTheme.Color.Background.Overlay ?? other.Color.Background.Overlay;
@@ -522,6 +794,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Border.PrimaryLight = bitTheme.Color.Border.PrimaryLight ?? other.Color.Border.PrimaryLight;
result.Color.Border.PrimaryLightHover = bitTheme.Color.Border.PrimaryLightHover ?? other.Color.Border.PrimaryLightHover;
result.Color.Border.PrimaryLightActive = bitTheme.Color.Border.PrimaryLightActive ?? other.Color.Border.PrimaryLightActive;
+ result.Color.Border.PrimaryDisabled = bitTheme.Color.Border.PrimaryDisabled ?? other.Color.Border.PrimaryDisabled;
+ result.Color.Border.PrimaryDisabledText = bitTheme.Color.Border.PrimaryDisabledText ?? other.Color.Border.PrimaryDisabledText;
+ result.Color.Border.PrimaryFocus = bitTheme.Color.Border.PrimaryFocus ?? other.Color.Border.PrimaryFocus;
result.Color.Border.Secondary = bitTheme.Color.Border.Secondary ?? other.Color.Border.Secondary;
result.Color.Border.SecondaryHover = bitTheme.Color.Border.SecondaryHover ?? other.Color.Border.SecondaryHover;
result.Color.Border.SecondaryActive = bitTheme.Color.Border.SecondaryActive ?? other.Color.Border.SecondaryActive;
@@ -531,6 +806,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Border.SecondaryLight = bitTheme.Color.Border.SecondaryLight ?? other.Color.Border.SecondaryLight;
result.Color.Border.SecondaryLightHover = bitTheme.Color.Border.SecondaryLightHover ?? other.Color.Border.SecondaryLightHover;
result.Color.Border.SecondaryLightActive = bitTheme.Color.Border.SecondaryLightActive ?? other.Color.Border.SecondaryLightActive;
+ result.Color.Border.SecondaryDisabled = bitTheme.Color.Border.SecondaryDisabled ?? other.Color.Border.SecondaryDisabled;
+ result.Color.Border.SecondaryDisabledText = bitTheme.Color.Border.SecondaryDisabledText ?? other.Color.Border.SecondaryDisabledText;
+ result.Color.Border.SecondaryFocus = bitTheme.Color.Border.SecondaryFocus ?? other.Color.Border.SecondaryFocus;
result.Color.Border.Tertiary = bitTheme.Color.Border.Tertiary ?? other.Color.Border.Tertiary;
result.Color.Border.TertiaryHover = bitTheme.Color.Border.TertiaryHover ?? other.Color.Border.TertiaryHover;
result.Color.Border.TertiaryActive = bitTheme.Color.Border.TertiaryActive ?? other.Color.Border.TertiaryActive;
@@ -540,6 +818,9 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Color.Border.TertiaryLight = bitTheme.Color.Border.TertiaryLight ?? other.Color.Border.TertiaryLight;
result.Color.Border.TertiaryLightHover = bitTheme.Color.Border.TertiaryLightHover ?? other.Color.Border.TertiaryLightHover;
result.Color.Border.TertiaryLightActive = bitTheme.Color.Border.TertiaryLightActive ?? other.Color.Border.TertiaryLightActive;
+ result.Color.Border.TertiaryDisabled = bitTheme.Color.Border.TertiaryDisabled ?? other.Color.Border.TertiaryDisabled;
+ result.Color.Border.TertiaryDisabledText = bitTheme.Color.Border.TertiaryDisabledText ?? other.Color.Border.TertiaryDisabledText;
+ result.Color.Border.TertiaryFocus = bitTheme.Color.Border.TertiaryFocus ?? other.Color.Border.TertiaryFocus;
result.Color.Border.Disabled = bitTheme.Color.Border.Disabled ?? other.Color.Border.Disabled;
result.Color.Required = bitTheme.Color.Required ?? other.Color.Required;
@@ -602,6 +883,7 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.BoxShadow.S22 = bitTheme.BoxShadow.S22 ?? other.BoxShadow.S22;
result.BoxShadow.S23 = bitTheme.BoxShadow.S23 ?? other.BoxShadow.S23;
result.BoxShadow.S24 = bitTheme.BoxShadow.S24 ?? other.BoxShadow.S24;
+ result.BoxShadow.FocusRing = bitTheme.BoxShadow.FocusRing ?? other.BoxShadow.FocusRing;
result.Spacing.ScalingFactor = bitTheme.Spacing.ScalingFactor ?? other.Spacing.ScalingFactor;
@@ -614,6 +896,8 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Shape.BorderRadius = bitTheme.Shape.BorderRadius ?? other.Shape.BorderRadius;
result.Shape.BorderWidth = bitTheme.Shape.BorderWidth ?? other.Shape.BorderWidth;
result.Shape.BorderStyle = bitTheme.Shape.BorderStyle ?? other.Shape.BorderStyle;
+ result.Shape.FocusRingWidth = bitTheme.Shape.FocusRingWidth ?? other.Shape.FocusRingWidth;
+ result.Shape.FocusRingOffset = bitTheme.Shape.FocusRingOffset ?? other.Shape.FocusRingOffset;
result.Typography.FontFamily = bitTheme.Typography.FontFamily ?? other.Typography.FontFamily;
result.Typography.FontWeight = bitTheme.Typography.FontWeight ?? other.Typography.FontWeight;
@@ -715,6 +999,20 @@ internal static BitTheme Merge(BitTheme bitTheme, BitTheme other)
result.Typography.Subtitle2.LineHeight = bitTheme.Typography.Subtitle2.LineHeight ?? other.Typography.Subtitle2.LineHeight;
result.Typography.Subtitle2.LetterSpacing = bitTheme.Typography.Subtitle2.LetterSpacing ?? other.Typography.Subtitle2.LetterSpacing;
+ result.Motion.Duration = bitTheme.Motion.Duration ?? other.Motion.Duration;
+ result.Motion.DurationShort = bitTheme.Motion.DurationShort ?? other.Motion.DurationShort;
+ result.Motion.DurationLong = bitTheme.Motion.DurationLong ?? other.Motion.DurationLong;
+ result.Motion.EasingStandard = bitTheme.Motion.EasingStandard ?? other.Motion.EasingStandard;
+
+ result.Layout.Direction = bitTheme.Layout.Direction ?? other.Layout.Direction;
+ result.Layout.DensityScale = bitTheme.Layout.DensityScale ?? other.Layout.DensityScale;
+ result.Layout.Breakpoints.Xs = bitTheme.Layout.Breakpoints.Xs ?? other.Layout.Breakpoints.Xs;
+ result.Layout.Breakpoints.Sm = bitTheme.Layout.Breakpoints.Sm ?? other.Layout.Breakpoints.Sm;
+ result.Layout.Breakpoints.Md = bitTheme.Layout.Breakpoints.Md ?? other.Layout.Breakpoints.Md;
+ result.Layout.Breakpoints.Lg = bitTheme.Layout.Breakpoints.Lg ?? other.Layout.Breakpoints.Lg;
+ result.Layout.Breakpoints.Xl = bitTheme.Layout.Breakpoints.Xl ?? other.Layout.Breakpoints.Xl;
+ result.Layout.Breakpoints.Xxl = bitTheme.Layout.Breakpoints.Xxl ?? other.Layout.Breakpoints.Xxl;
+
return result;
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeName.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeName.cs
new file mode 100644
index 0000000000..5350f29292
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeName.cs
@@ -0,0 +1,120 @@
+namespace Bit.BlazorUI;
+
+///
+/// Type-safe wrapper for the bit-theme attribute name. Use the static factories
+/// ( , , , etc.) for the well-known presets;
+/// call for app-specific theme names that match your CSS.
+///
+///
+///
+/// Existing callers can keep using the string constants on ;
+/// this struct is additive and provides an implicit conversion to so it slots
+/// straight into without ceremony.
+///
+///
+/// The wrapper normalizes the value (trim + lower-case invariant) so Custom("Fluent-Light")
+/// and BitThemeName.FluentLight compare equal. That mirrors the JS side, which writes the
+/// value verbatim onto document.documentElement and lets CSS selectors (:root[bit-theme="…"] )
+/// match in a case-sensitive fashion against the canonical lowercase form.
+///
+///
+public readonly struct BitThemeName : IEquatable
+{
+ private readonly string? _value;
+
+ private BitThemeName(string value)
+ {
+ _value = value;
+ }
+
+ /// The normalized theme name (trimmed, lower-case invariant).
+ public string Value => _value ?? BitThemePresets.Light;
+
+ /// Light preset ("light" ).
+ public static BitThemeName Light { get; } = new(BitThemePresets.Light);
+
+ /// Dark preset ("dark" ).
+ public static BitThemeName Dark { get; } = new(BitThemePresets.Dark);
+
+ /// Fluent base preset ("fluent" ).
+ public static BitThemeName Fluent { get; } = new(BitThemePresets.Fluent);
+
+ /// Fluent light preset ("fluent-light" ).
+ public static BitThemeName FluentLight { get; } = new(BitThemePresets.FluentLight);
+
+ /// Fluent dark preset ("fluent-dark" ).
+ public static BitThemeName FluentDark { get; } = new(BitThemePresets.FluentDark);
+
+ /// Special pseudo-preset that follows OS prefers-color-scheme ("system" ).
+ public static BitThemeName System { get; } = new(BitThemePresets.System);
+
+ ///
+ /// Wraps a custom theme name. Whitespace is trimmed and the result is lower-cased. The normalized
+ /// value must be a safe [a-z0-9-] token of at most 64 characters so it matches the SSR
+ /// theme-token parsing rules (see BitThemeSsr.NormalizeThemeToken ) and keeps runtime and
+ /// first-paint validation consistent for persisted custom themes.
+ ///
+ ///
+ /// When is null, empty, whitespace, longer than 64 characters, or contains
+ /// characters outside [a-z0-9-] after normalization.
+ ///
+ public static BitThemeName Custom(string name)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+
+ var token = NormalizeToken(name, out var error) ?? throw new ArgumentException(error, nameof(name));
+
+ return new BitThemeName(token);
+ }
+
+ ///
+ /// Normalizes (trim + lower-case invariant) and validates a theme token, returning
+ /// with a reason in when the value is blank, longer than 64 characters, or contains
+ /// characters outside [a-z0-9-] . Shared with BitThemeSsr.NormalizeThemeToken so runtime and
+ /// first-paint validation stay identical; each caller maps the failure onto its own contract
+ /// ( throws, the SSR helper returns ).
+ ///
+ internal static string? NormalizeToken(string? value, out string? error)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ error = "Theme name must not be empty or whitespace.";
+ return null;
+ }
+
+ var token = value.Trim().ToLowerInvariant();
+
+ if (token.Length > 64)
+ {
+ error = "Theme name must be at most 64 characters.";
+ return null;
+ }
+
+ foreach (var ch in token)
+ {
+ var allowed = (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '-';
+ if (allowed is false)
+ {
+ error = "Theme name may only contain lower-case letters, digits, and hyphens.";
+ return null;
+ }
+ }
+
+ error = null;
+ return token;
+ }
+
+ /// Implicitly converts to the underlying string so existing string-typed APIs accept the wrapper unchanged.
+ public static implicit operator string(BitThemeName name) => name.Value;
+
+ public override string ToString() => Value;
+
+ public bool Equals(BitThemeName other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
+
+ public override bool Equals(object? obj) => obj is BitThemeName other && Equals(other);
+
+ public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
+
+ public static bool operator ==(BitThemeName left, BitThemeName right) => left.Equals(right);
+ public static bool operator !=(BitThemeName left, BitThemeName right) => !left.Equals(right);
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeNotifications.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeNotifications.cs
new file mode 100644
index 0000000000..373aaafb5e
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeNotifications.cs
@@ -0,0 +1,64 @@
+using Microsoft.Extensions.Logging;
+
+namespace Bit.BlazorUI;
+
+///
+/// Raised when the global bit-theme document attribute changes (including OS-driven updates when following system theme).
+/// Subscribe in scoped components; requires interop at least once per circuit so the client script can notify .NET.
+///
+///
+///
+/// Subscribers MUST unsubscribe when their component is disposed.
+/// is registered as a scoped service, so a leaked handler keeps the (potentially disposed) component
+/// rooted for the lifetime of the circuit and produces re-renders on torn-down state.
+///
+///
+/// Handlers are invoked on whichever thread is called from (typically the JS
+/// interop callback thread). If you need to update Blazor component state, marshal back through
+/// ComponentBase.InvokeAsync(StateHasChanged) .
+///
+///
+/// A throwing handler does not prevent the remaining handlers from running and is logged when an
+/// is registered in DI.
+///
+///
+public sealed class BitThemeNotifications
+{
+ private readonly ILogger? _logger;
+
+ public BitThemeNotifications()
+ {
+ }
+
+ public BitThemeNotifications(ILoggerFactory? loggerFactory)
+ {
+ _logger = loggerFactory?.CreateLogger();
+ }
+
+ /// Fires after BitBlazorUI.Theme.set , toggleDarkLight , or prefers-color-scheme updates while following system theme.
+ public event EventHandler? ThemeChanged;
+
+ internal void Raise(string? newTheme, string? oldTheme)
+ {
+ var handler = ThemeChanged;
+ if (handler is null) return;
+
+ var args = new BitThemeChangedEventArgs(newTheme, oldTheme);
+
+ // Invoke each subscriber in isolation. EventHandler.Invoke would short-circuit the
+ // remaining delegates on the first throw, and any escaping exception from the JS-invoked
+ // path can fault the circuit. Per-subscriber try/catch + log is the correct contract for
+ // a notification service consumed by arbitrary user code.
+ foreach (var subscriber in handler.GetInvocationList())
+ {
+ try
+ {
+ ((EventHandler)subscriber).Invoke(this, args);
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "BitThemeNotifications.ThemeChanged handler threw and was suppressed.");
+ }
+ }
+ }
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeProvider.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeProvider.cs
index 9ccecda73e..984b5bab6e 100644
--- a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeProvider.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeProvider.cs
@@ -1,6 +1,6 @@
namespace Bit.BlazorUI;
-public partial class BitThemeProvider : ComponentBase
+public class BitThemeProvider : ComponentBase
{
///
/// The content of the ThemeProvider.
@@ -8,8 +8,18 @@ public partial class BitThemeProvider : ComponentBase
[Parameter] public RenderFragment? ChildContent { get; set; }
///
- /// The element used for the root node.
+ /// The element used for the root node. Ignored whenever no wrapping element is rendered - that is,
+ /// when there is nothing to apply at this layer: both and
+ /// are , or is
+ /// and an unnamed is passed through directly
+ /// (no to re-expose it under).
///
+ ///
+ /// When a wrapping element is rendered it defaults to display:contents so it produces no
+ /// layout box and stays transparent to the surrounding flex/grid layout, while still letting the
+ /// applied CSS custom properties inherit to descendants. Supply style="display:…" via the
+ /// splatted attributes to opt back into a real box (e.g. when you also set box-model styles on it).
+ ///
[Parameter] public string? RootElement { get; set; }
///
@@ -17,55 +27,213 @@ public partial class BitThemeProvider : ComponentBase
///
[Parameter] public BitTheme? Theme { get; set; }
+ ///
+ /// Opts this provider into treating and as
+ /// immutable ("frozen"). When , the provider skips the relatively
+ /// expensive per-render rebuild (the merge that allocates a fresh
+ /// all-token plus the ~300-entry CSS-variable string build) whenever both
+ /// references are unchanged since the last render - it just keeps the previously cached result.
+ ///
+ ///
+ /// This is a performance opt-in for the common case where callers hand the provider a stable
+ /// instance and never mutate it in place. The trade-off is that in-place
+ /// mutation of a frozen theme is NOT detected (the references don't change, so nothing is
+ /// rebuilt or re-cascaded); assign a new instance to apply changes.
+ /// Leave this (the default) to keep the mutation-in-place behavior where
+ /// every parameters update rebuilds from the current token values.
+ ///
+ [Parameter] public bool Frozen { get; set; }
+
///
/// Optional name for ; when set, consumers use [CascadingParameter(Name = …)] .
- /// The cascaded is the merge of with (same as inline CSS variables on this provider’s root).
+ /// The cascaded is the merge of with (same as inline CSS variables on this provider's root).
///
[Parameter] public string? ThemeName { get; set; }
+ ///
+ /// Catch-all for HTML attributes splatted onto the wrapping element (e.g. class , id ,
+ /// data-* , ARIA roles). Only emitted when this provider renders a wrapping element - that
+ /// is, when a local is applied (its CSS variables need a host element), or
+ /// when a cascading is re-exposed under a custom .
+ /// When there is nothing to apply at this layer (no local and no
+ /// ) the provider renders just and these
+ /// attributes are ignored.
+ ///
+ [Parameter(CaptureUnmatchedValues = true)]
+ public IReadOnlyDictionary? AdditionalAttributes { get; set; }
+
[CascadingParameter] public BitTheme? ParentTheme { get; set; }
-
- protected override void BuildRenderTree(RenderTreeBuilder builder)
+ // Cached merge result and the inline `style` string.
+ //
+ // BitTheme is mutable (every node is a class with public setters), so callers can keep a
+ // single Theme/ParentTheme instance and mutate sub-properties between renders. We therefore
+ // rebuild the merge + CSS string on every parameters update and use CSS-string equality as a
+ // cheap structural check: when the rebuilt output matches the cached one we keep the cached
+ // merged-theme reference, which keeps CascadingValue reference-stable so
+ // descendants don't re-render on parent re-renders that didn't actually change tokens.
+ // (CascadingValue for reference types uses ReferenceEquals for change detection, and
+ // BitThemeMapper.Merge produces a fresh BitTheme on every call.)
+ private BitTheme? _cachedMergedTheme;
+ private string? _cachedCssVarStyle;
+
+ // The Theme/ParentTheme references the cached result above was built from. Only consulted when
+ // Frozen is true, to short-circuit the merge + CSS rebuild when neither reference changed.
+ private BitTheme? _lastTheme;
+ private BitTheme? _lastParentTheme;
+
+ protected override void OnParametersSet()
{
- var seq = 0;
+ if (Theme is null && ParentTheme is null)
+ {
+ // No tokens to apply at this layer; we'll render ChildContent directly.
+ _cachedMergedTheme = null;
+ _cachedCssVarStyle = null;
+ return;
+ }
if (Theme is null)
{
- builder.AddContent(seq, ChildContent);
+ // Local theme not set, but a ParentTheme is cascading from above. Keep it as the value
+ // we would re-expose, and emit no inline style at this layer (the ancestor provider owns
+ // those CSS vars). BuildRenderTree only renders a wrapper + CascadingValue when a custom
+ // ThemeName needs the value re-exposed under that name; otherwise it renders ChildContent
+ // directly and relies on the ancestor's existing unnamed cascade, avoiding a redundant
+ // wrapper element and duplicate CascadingValue.
+ _cachedMergedTheme = ParentTheme;
+ _cachedCssVarStyle = null;
return;
}
- var mergedTheme = Theme!;
+ // Frozen fast path: when the caller opts into treating the theme references as immutable,
+ // skip the merge + CSS-string rebuild entirely if neither Theme nor ParentTheme changed by
+ // reference since the cached result was produced. _cachedCssVarStyle is only non-null after a
+ // rebuild in this branch, so this guards against reusing a stale cache from the Theme-null /
+ // no-tokens branches above.
+ if (Frozen
+ && _cachedCssVarStyle is not null
+ && ReferenceEquals(Theme, _lastTheme)
+ && ReferenceEquals(ParentTheme, _lastParentTheme))
+ {
+ return;
+ }
- if (ParentTheme is not null)
+ // Always produce a FRESH merged BitTheme (BitThemeMapper.Merge allocates a new instance),
+ // even when there is no ParentTheme. This keeps the CascadingValue change-detection
+ // behavior consistent: previously the no-parent path cascaded the caller's own Theme instance,
+ // so an in-place mutation of that instance produced the same reference and CascadingValue
+ // (which compares reference types with ReferenceEquals) never notified [CascadingParameter]
+ // consumers - whereas the with-parent path always allocated a new reference and did notify.
+ // Going through Merge in both cases means a token change yields a new reference (consumers are
+ // notified), while the CSS-string equality suppression below preserves the cached reference
+ // when nothing actually changed (consumers are not woken up needlessly).
+ var mergedTheme = BitThemeMapper.Merge(Theme, ParentTheme ?? new BitTheme());
+
+ var cssVarStyle = BuildCssVarStyle(mergedTheme);
+
+ // Record the references this result was built from so a subsequent Frozen render can detect
+ // that nothing changed by reference and skip the rebuild above.
+ _lastTheme = Theme;
+ _lastParentTheme = ParentTheme;
+
+ // Suppress propagation when the produced output is identical to the previous render -
+ // this preserves the cascaded reference and avoids waking up every descendant consumer
+ // when a parent re-renders without touching the theme.
+ if (_cachedMergedTheme is not null && string.Equals(_cachedCssVarStyle, cssVarStyle, StringComparison.Ordinal))
{
- mergedTheme = BitThemeMapper.Merge(Theme, ParentTheme);
+ return;
}
- var cssVars = BitThemeMapper.MapToCssVariables(mergedTheme);
- var cssVarStyle = string.Join(';', cssVars.Select(kv => $"{kv.Key}:{kv.Value}"));
+ _cachedMergedTheme = mergedTheme;
+ _cachedCssVarStyle = cssVarStyle;
+ }
- builder.OpenElement(seq++, RootElement ?? "div");
- builder.AddAttribute(seq++, "style", cssVarStyle);
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ if (_cachedMergedTheme is null)
+ {
+ // No local Theme override and no parent theme to re-cascade: render ChildContent as-is.
+ builder.AddContent(0, ChildContent);
+ return;
+ }
- builder.OpenComponent>(seq++);
- if (ThemeName is not null)
+ if (_cachedCssVarStyle is null && ThemeName is null)
+ {
+ // Nothing to apply at this layer: no CSS variables to emit (Theme is null - only a
+ // ParentTheme is cascading from above) and no custom ThemeName to re-expose the value
+ // under. The ancestor's unnamed CascadingValue already reaches our
+ // descendants, so rendering ChildContent directly preserves the cascade while avoiding
+ // a redundant wrapper element and a duplicate CascadingValue.
+ builder.AddContent(0, ChildContent);
+ return;
+ }
+
+ builder.OpenElement(1, RootElement ?? "div");
+
+ // Collect user attributes into a single dictionary (splatted via AddMultipleAttributes so
+ // the render-tree diff reconciles a changing attribute set correctly), pulling out any
+ // "style" entry first so we can merge it with our CSS-variable declarations. It is added
+ // separately below rather than left in the splat because last-write-wins semantics on
+ // RenderTreeBuilder mean a splat followed by AddAttribute("style", ...) would silently drop
+ // the user's style.
+ string? userStyle = null;
+ if (AdditionalAttributes is not null)
+ {
+ var attributes = new Dictionary(StringComparer.Ordinal);
+ foreach (var kv in AdditionalAttributes)
+ {
+ if (string.Equals(kv.Key, "style", StringComparison.OrdinalIgnoreCase))
+ {
+ userStyle = kv.Value?.ToString();
+ continue;
+ }
+
+ attributes[kv.Key] = kv.Value;
+ }
+
+ builder.AddMultipleAttributes(2, attributes);
+ }
+
+ // Compose the inline style. The wrapper defaults to `display:contents` so it produces no
+ // layout box of its own: the element must exist for CSS custom properties to inherit to
+ // descendants (inheritance follows the DOM tree, not the box tree), but it should not
+ // perturb the surrounding flex/grid layout or child selectors the way a default `div`
+ // block box would. The CSS-variable declarations come next, and the user-supplied style
+ // comes last so a caller can override both the layout mode (e.g. `style="display:flex"`)
+ // and any individual token on conflict.
+ var style = "display:contents";
+ if (_cachedCssVarStyle is not null)
{
- builder.AddAttribute(seq++, "Name", ThemeName);
- builder.AddAttribute(seq++, "Value", mergedTheme);
+ style = $"{style};{_cachedCssVarStyle}";
}
- else
+ if (userStyle is not null)
{
- builder.AddAttribute(seq++, "Value", mergedTheme);
+ style = $"{style};{userStyle}";
}
- builder.AddAttribute(seq++, "ChildContent", (RenderFragment)(builder2 => builder2.AddContent(seq, ChildContent)));
+
+ builder.AddAttribute(3, "style", style);
+
+ builder.OpenComponent>(4);
+ if (ThemeName is not null)
+ {
+ builder.AddAttribute(5, "Name", ThemeName);
+ }
+ builder.AddAttribute(6, "Value", _cachedMergedTheme);
+ // IMPORTANT: do NOT close over a mutable outer sequence counter. The lambda runs lazily
+ // during the cascade's render pass, so capturing a mutable local would feed Blazor a
+ // sequence number that varies per render and defeats its diff. Sequence numbers inside a
+ // RenderFragment are local to that fragment, so a constant is what we want.
+ builder.AddAttribute(7, "ChildContent", (RenderFragment)(b => b.AddContent(0, ChildContent)));
builder.CloseComponent();
builder.CloseElement();
+ }
- base.BuildRenderTree(builder);
+ private static string BuildCssVarStyle(BitTheme theme)
+ {
+ var cssVars = BitThemeUtilities.ToCssVariables(theme);
+ return string.Join(';', cssVars.Select(kv => $"{kv.Key}:{kv.Value}"));
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeSerialization.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeSerialization.cs
index 7163386f78..b07a41fb51 100644
--- a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeSerialization.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeSerialization.cs
@@ -1,6 +1,4 @@
-using System.Collections.Generic;
-using System.Reflection;
-using System.Text.Json;
+using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
@@ -41,6 +39,28 @@ public static BitTheme Deserialize(string? json)
return theme;
}
+ ///
+ /// Normalizes in place so every nested branch object is non-null,
+ /// matching a freshly-constructed new BitTheme() . Returns the same instance for
+ /// convenient chaining.
+ ///
+ ///
+ /// Exposed for the CSS-variable mapper and merge entry points ( ):
+ /// those walk the graph (theme.Color.Primary.Main , theme.Layout.Breakpoints.Xs , …)
+ /// and would throw on a hand-constructed sparse theme
+ /// (for example new BitTheme { Color = null } or new BitThemeColors { Primary = null } ,
+ /// both reachable because the branch setters are public). The deserialization path already runs
+ /// this; running it at the mapper/merge entry covers the common hand-constructed path too.
+ ///
+ internal static BitTheme Normalize(BitTheme theme)
+ {
+ ArgumentNullException.ThrowIfNull(theme);
+
+ EnsureNestedObjects(theme);
+
+ return theme;
+ }
+
/// Removes JSON object properties whose value is an empty object {} , depth-first, so parents collapse when all children were empty.
private static void PruneEmptyObjectNodes(JsonNode? node)
{
@@ -61,36 +81,75 @@ private static void PruneEmptyObjectNodes(JsonNode? node)
}
}
- /// Replaces null reference-type properties (except ) so the tree matches a new BitTheme() graph after sparse JSON.
-#pragma warning disable IL2072 // Activator.CreateInstance - BitTheme-related POCOs only, all have public parameterless constructors.
-#pragma warning disable IL2075 // GetType().GetProperties - only instances from BitTheme deserialization.
- private static void EnsureNestedObjects(object obj, HashSet? visited = null)
+ ///
+ /// Replaces branch objects on a so the
+ /// graph matches a freshly-constructed new BitTheme() . Callers that walk the graph (the
+ /// mapper, merge, derivation helpers) rely on every branch being non-null.
+ ///
+ ///
+ ///
+ /// This is a safety net for externally-authored JSON. The sparse format produced by
+ /// never emits a branch as explicit null (null leaves are omitted
+ /// and empty objects are pruned), and leaves absent
+ /// properties at their constructor-initialized new() values - so a framework round-trip
+ /// already yields a fully-populated graph. The case this guards is hand-written or third-party
+ /// JSON that contains an explicit "color": null (or similar), which the serializer would
+ /// otherwise honor by setting the branch to .
+ ///
+ ///
+ /// Previously this method walked the type via reflection. Reflection breaks under trimming /
+ /// AOT (IL2070/IL2075/IL3050) unless the entire graph is preserved by
+ /// [DynamicallyAccessedMembers] , which is hard to keep correct as the model evolves.
+ /// Walking the graph explicitly is verbose but trim-safe and removes the reflection
+ /// suppression pragmas that previously hid genuine warnings.
+ ///
+ ///
+ private static void EnsureNestedObjects(BitTheme theme)
{
- visited ??= new HashSet(ReferenceEqualityComparer.Instance);
- if (!visited.Add(obj))
- return;
-
- foreach (var prop in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
- {
- if (!prop.CanRead || !prop.CanWrite || prop.GetIndexParameters().Length > 0)
- continue;
+ // Top-level branches.
+ theme.Color ??= new BitThemeColors();
+ theme.BoxShadow ??= new BitThemeBoxShadows();
+ theme.Spacing ??= new BitThemeSpacings();
+ theme.ZIndex ??= new BitThemeZIndices();
+ theme.Shape ??= new BitThemeShapes();
+ theme.Typography ??= new BitThemeTypography();
+ theme.Motion ??= new BitThemeMotion();
+ theme.Layout ??= new BitThemeLayout();
- var pt = prop.PropertyType;
- if (pt == typeof(string) || pt.IsValueType)
- continue;
+ // Color branch.
+ var color = theme.Color;
+ color.Primary ??= new BitThemeColorVariants();
+ color.Secondary ??= new BitThemeColorVariants();
+ color.Tertiary ??= new BitThemeColorVariants();
+ color.Info ??= new BitThemeColorVariants();
+ color.Success ??= new BitThemeColorVariants();
+ color.Warning ??= new BitThemeColorVariants();
+ color.SevereWarning ??= new BitThemeColorVariants();
+ color.Error ??= new BitThemeColorVariants();
+ color.Foreground ??= new BitThemeGeneralColorVariants();
+ color.Background ??= new BitThemeBackgroundColorVariants();
+ color.Border ??= new BitThemeGeneralColorVariants();
+ color.Neutral ??= new BitThemeNeutralColorVariants();
- var val = prop.GetValue(obj);
- if (val is null)
- {
- val = Activator.CreateInstance(pt);
- if (val is null)
- continue;
- prop.SetValue(obj, val);
- }
+ // Typography branch.
+ var typography = theme.Typography;
+ typography.H1 ??= new BitThemeTypographyVariants();
+ typography.H2 ??= new BitThemeTypographyVariants();
+ typography.H3 ??= new BitThemeTypographyVariants();
+ typography.H4 ??= new BitThemeTypographyVariants();
+ typography.H5 ??= new BitThemeTypographyVariants();
+ typography.H6 ??= new BitThemeTypographyVariants();
+ typography.Subtitle1 ??= new BitThemeTypographyVariants();
+ typography.Subtitle2 ??= new BitThemeTypographyVariants();
+ typography.Body1 ??= new BitThemeTypographyVariants();
+ typography.Body2 ??= new BitThemeTypographyVariants();
+ typography.Button ??= new BitThemeLabelTypographyVariants();
+ typography.Caption1 ??= new BitThemeTypographyVariants();
+ typography.Caption2 ??= new BitThemeTypographyVariants();
+ typography.Overline ??= new BitThemeLabelTypographyVariants();
+ typography.Inherit ??= new BitThemeInheritTypographyVariants();
- EnsureNestedObjects(val, visited);
- }
+ // Layout branch.
+ theme.Layout.Breakpoints ??= new BitThemeBreakpoints();
}
-#pragma warning restore IL2075
-#pragma warning restore IL2072
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeSsr.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeSsr.cs
index e50663f56c..d2d510810f 100644
--- a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeSsr.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeSsr.cs
@@ -5,20 +5,168 @@ namespace Bit.BlazorUI;
/// Order matches bit-theme.ts init : base from attributes, then bit-theme-system (prefers-color-scheme), then bit-theme-persist (localStorage), then resolve stored system .
/// Emit at the start of <head> (before stylesheets) so the correct bit-theme attribute is set before first paint.
///
+///
+/// Attribute and storage key names come from so the inline script and the runtime BitBlazorUI.Theme client stay in sync.
+///
public static class BitThemeSsr
{
///
- /// Inline script only (no script tag). Wrap in a script element in your host page or layout.
+ /// Inline script body without a wrapping <script> tag. Concatenate into your own
+ /// <script> element when you need full control over attributes (CSP nonce,
+ /// type , defer , etc.).
///
- public const string InlineHeadScriptBody =
- "(function(){var r=document.documentElement,k='bit-current-theme',lt=r.getAttribute('bit-theme-light')||'light'," +
- "dk=r.getAttribute('bit-theme-dark')||'dark',m=window.matchMedia&&matchMedia('(prefers-color-scheme:dark)').matches," +
- "base=r.getAttribute('bit-theme')||r.getAttribute('bit-theme-default')||'light';" +
- "if(r.hasAttribute('bit-theme-system')){base=m?dk:lt;}" +
- "var cur=base;if(r.hasAttribute('bit-theme-persist')){try{cur=localStorage.getItem(k)||base;}catch(e){}}" +
- "if(cur==='system'){cur=m?dk:lt;}" +
- "r.setAttribute('bit-theme',cur);})();";
-
- /// Full script element markup for convenience.
- public static string InlineHeadScript => $"";
+ public static readonly string InlineHeadScriptBody = BuildInlineScriptBody();
+
+ ///
+ /// Full <script> markup ready to drop into <head> . Equivalent to
+ /// with a nonce.
+ ///
+ public static string InlineHeadScript => BuildInlineHeadScript(nonce: null);
+
+ ///
+ /// Builds the inline first-paint script and wraps it in a <script> element.
+ /// Pass to satisfy a script-src 'nonce-…' Content-Security-Policy.
+ ///
+ /// Optional CSP nonce. When supplied, the value is HTML-attribute-encoded and emitted as nonce="…" .
+ public static string BuildInlineHeadScript(string? nonce)
+ {
+ if (string.IsNullOrWhiteSpace(nonce))
+ {
+ return $"";
+ }
+
+ // HTML-attribute-encode the nonce so a tampered value cannot break out of the attribute.
+ // CSP nonces are base64url in practice, but defense-in-depth is cheap.
+ var safeNonce = HtmlEncodeAttribute(nonce);
+ return $"";
+ }
+
+ ///
+ /// Builds the bit-theme* attribute string to render on the root <html>
+ /// element from a persisted preference (typically read from the
+ /// cookie) so server-rendered markup paints
+ /// the correct theme before the client script runs.
+ ///
+ ///
+ /// The stored abstract theme key (for example dark , fluent-light , or
+ /// system ). Usually the cookie value; may be /blank when no
+ /// preference has been stored yet.
+ ///
+ ///
+ /// Optional theme to use when is missing, or as the
+ /// no-JS base when the OS is being followed.
+ ///
+ ///
+ /// An attribute fragment such as bit-theme="dark" or
+ /// bit-theme-system bit-theme-default="light" , ready to interpolate into the root
+ /// <html> tag.
+ ///
+ ///
+ ///
+ /// A concrete preference is emitted as bit-theme="…" so first paint is correct with no
+ /// flash. The special system value (and a missing preference with no default) emit the
+ /// bit-theme-system marker and defer the light/dark resolution to the client inline
+ /// script - the server cannot read prefers-color-scheme . A missing preference with a
+ /// concrete paints that default directly.
+ ///
+ ///
+ /// Values are normalized (trim + lower-case, matching ) and validated
+ /// against a strict [a-z0-9-] theme-name pattern; anything else is ignored and treated as
+ /// missing, so a tampered cookie cannot inject markup into the document.
+ ///
+ ///
+ /// This is the server half of cookie-based persistence: the app is responsible for writing the
+ /// cookie (client- or server-side) under .
+ ///
+ ///
+ public static string BuildRootThemeAttributes(string? persistedPreference, string? defaultTheme = null)
+ {
+ var preference = NormalizeThemeToken(persistedPreference);
+ var fallback = NormalizeThemeToken(defaultTheme);
+
+ // Explicit "system" → follow the OS; the inline head script resolves light/dark before
+ // paint. Carry a concrete default through for no-JS clients.
+ if (preference == BitThemePresets.System)
+ {
+ return fallback is null || fallback == BitThemePresets.System
+ ? BitThemeAttributeNames.ThemeSystem
+ : $"{BitThemeAttributeNames.ThemeSystem} {BitThemeAttributeNames.ThemeDefault}=\"{fallback}\"";
+ }
+
+ // Concrete persisted preference → paint it directly (no flash).
+ if (preference is not null)
+ {
+ return $"{BitThemeAttributeNames.Theme}=\"{preference}\"";
+ }
+
+ // No stored preference: use the configured default theme, or follow the OS if none.
+ if (fallback is not null && fallback != BitThemePresets.System)
+ {
+ return $"{BitThemeAttributeNames.Theme}=\"{fallback}\"";
+ }
+
+ return BitThemeAttributeNames.ThemeSystem;
+ }
+
+ ///
+ /// Normalizes a theme key (trim + lower-case invariant) and validates it as a safe
+ /// [a-z0-9-] token. Returns for blank or invalid input so callers
+ /// treat it as "no preference" - this also prevents a tampered cookie value from being emitted
+ /// into the document.
+ ///
+ private static string? NormalizeThemeToken(string? value) => BitThemeName.NormalizeToken(value, out _);
+
+ private static string BuildInlineScriptBody()
+ {
+ // Centralizing names through BitThemeAttributeNames means the inline script picks up renames
+ // automatically - the previous version hard-coded each attribute literal twice (here and in
+ // bit-theme.ts), which was a maintenance hazard.
+ var theme = BitThemeAttributeNames.Theme;
+ var themeDefault = BitThemeAttributeNames.ThemeDefault;
+ var themeSystem = BitThemeAttributeNames.ThemeSystem;
+ var themePersist = BitThemeAttributeNames.ThemePersist;
+ var themePersistCookie = BitThemeAttributeNames.ThemePersistCookie;
+ var themeDark = BitThemeAttributeNames.ThemeDark;
+ var themeLight = BitThemeAttributeNames.ThemeLight;
+ var storageKey = BitThemeAttributeNames.ThemeStorageKey;
+ var cookieName = BitThemeCookie.PreferenceCookieName;
+
+ // The inline script is intentionally compact; it runs on every first paint before stylesheets.
+ // Logic mirrors Theme.init in bit-theme.ts, including the localStorage→cookie persistence
+ // fallback so a cookie-only (SSR) persistence setup paints correctly with no flash.
+ return
+ "(function(){var r=document.documentElement," +
+ $"k='{storageKey}'," +
+ $"ck='{cookieName}'," +
+ $"lt=r.getAttribute('{themeLight}')||'light'," +
+ $"dk=r.getAttribute('{themeDark}')||'dark'," +
+ "m=window.matchMedia&&matchMedia('(prefers-color-scheme:dark)').matches," +
+ $"base=r.getAttribute('{theme}')||r.getAttribute('{themeDefault}')||'light';" +
+ $"if(r.hasAttribute('{themeSystem}')){{base=m?dk:lt;}}" +
+ "var cur=base,got=false;" +
+ $"if(r.hasAttribute('{themePersist}')){{try{{var s=localStorage.getItem(k);if(s){{cur=s;got=true;}}}}catch(e){{}}}}" +
+ $"if(!got&&r.hasAttribute('{themePersistCookie}')){{var cm=('; '+document.cookie).match('; '+ck+'=([^;]*)');if(cm){{try{{cur=decodeURIComponent(cm[1]);}}catch(e){{cur=cm[1];}}}}}}" +
+ "if(cur==='system'){cur=m?dk:lt;}" +
+ $"r.setAttribute('{theme}',cur);}})();";
+ }
+
+ private static string HtmlEncodeAttribute(string value)
+ {
+ // Minimal attribute-context encoding; we don't want a full WebUtility dep just for this and
+ // CSP nonces are constrained to base64url chars in practice. Encode the characters that can
+ // break out of a double-quoted attribute or alter parsing.
+ var builder = new System.Text.StringBuilder(value.Length);
+ foreach (var ch in value)
+ {
+ switch (ch)
+ {
+ case '&': builder.Append("&"); break;
+ case '"': builder.Append("""); break;
+ case '<': builder.Append("<"); break;
+ case '>': builder.Append(">"); break;
+ default: builder.Append(ch); break;
+ }
+ }
+ return builder.ToString();
+ }
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeUtilities.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeUtilities.cs
index 66e6827ede..1320de5ec6 100644
--- a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeUtilities.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitThemeUtilities.cs
@@ -5,13 +5,43 @@
///
public static class BitThemeUtilities
{
- /// Maps a theme to CSS custom property names and values for use with or inline styles.
+ ///
+ /// Maps a theme to CSS custom property names and values for use with or inline styles.
+ ///
+ ///
+ /// The result is recomputed on every call. is mutable, so caching by
+ /// instance would return stale values when callers mutate a theme between calls. The mapper
+ /// builds a ~280-entry dictionary of strings; for the typical call path through
+ /// this cost is dwarfed by the JS-interop
+ /// boundary. If you have a hot, allocation-sensitive path, hold the result yourself and pass
+ /// the same reference until you intentionally rebuild it.
+ ///
+ /// Token values that contain characters capable of escaping a single CSS declaration (for
+ /// example ; , { , } , < , > , or comment markers) are
+ /// dropped rather than emitted. Theme values can originate from untrusted sources (see
+ /// ), and the output is concatenated into an inline
+ /// style attribute by ; dropping injection-prone values
+ /// prevents a malicious token from adding extra CSS. A dropped token falls back to the
+ /// stylesheet default.
+ ///
+ ///
+ /// The passed is not mutated: a hand-constructed sparse theme (e.g.
+ /// new BitTheme { Color = null } ) is normalized onto an internal copy before mapping, so
+ /// any deliberate branches on the original instance are preserved.
+ ///
+ ///
public static IReadOnlyDictionary ToCssVariables(BitTheme? bitTheme)
{
return BitThemeMapper.MapToCssVariables(bitTheme ?? new BitTheme());
}
/// Merges two themes: wins; missing values fall back to .
+ ///
+ /// Returns a fresh ; the inputs are neither used as the result nor mutated.
+ /// A hand-constructed sparse input (with branch objects) is normalized
+ /// onto internal copies before merging, so deliberate null branches on the passed instances are
+ /// preserved.
+ ///
public static BitTheme Merge(BitTheme? overrides, BitTheme? baseline)
{
return BitThemeMapper.Merge(overrides ?? new BitTheme(), baseline ?? new BitTheme());
diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/bit-theme.ts b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/bit-theme.ts
deleted file mode 100644
index 2040d035a2..0000000000
--- a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/bit-theme.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-type onThemeChangeType = (newThemeName: string, oldThemeName: string) => void;
-
-interface BitThemeOptions {
- system?: boolean;
- persist?: boolean;
- theme?: string | null;
- default?: string | null;
- darkTheme?: string | null;
- lightTheme?: string | null;
- onChange?: onThemeChangeType;
-}
-
-class BitTheme {
- private static SYSTEM_THEME = 'system';
- private static THEME_ATTRIBUTE = 'bit-theme';
- private static THEME_STORAGE_KEY = 'bit-current-theme';
-
-
- private static _persist = false;
- private static _darkTheme: string = 'dark';
- private static _lightTheme: string = 'light';
- private static _initOptions: BitThemeOptions = {};
- private static _currentTheme = BitTheme._lightTheme;
- private static _onThemeChange: onThemeChangeType = () => { };
-
-
- public static init(options: BitThemeOptions) {
- Object.assign(BitTheme._initOptions, options);
-
- if (BitTheme._initOptions.onChange) {
- BitTheme._onThemeChange = BitTheme._initOptions.onChange;
- }
-
- if (BitTheme._initOptions.darkTheme) {
- BitTheme._darkTheme = BitTheme._initOptions.darkTheme;
- }
-
- if (BitTheme._initOptions.lightTheme) {
- BitTheme._lightTheme = BitTheme._initOptions.lightTheme;
- }
-
- let theme = BitTheme._initOptions.theme || BitTheme._initOptions.default || BitTheme._lightTheme;
-
- if (BitTheme._initOptions.system) {
- theme = BitTheme.isSystemDark() ? BitTheme._darkTheme : BitTheme._lightTheme;
- }
-
- if (BitTheme._initOptions.persist) {
- BitTheme._persist = true;
- theme = BitTheme.getPersisted() || theme;
- }
-
- BitTheme.set(theme);
- }
-
- public static onChange(fn: onThemeChangeType) {
- BitTheme._onThemeChange = fn;
- }
-
- public static get() {
- BitTheme._currentTheme = document.documentElement.getAttribute(BitTheme.THEME_ATTRIBUTE) || '';
-
- if (BitTheme._persist) {
- var theme = BitTheme.getActualTheme(BitTheme.getPersisted());
- BitTheme._currentTheme = theme || BitTheme._currentTheme;
- }
-
- return BitTheme._currentTheme;
- }
-
- public static set(themeName: string) {
- BitTheme._currentTheme = BitTheme.getActualTheme(themeName)!;
-
- if (BitTheme._persist) {
- localStorage.setItem(BitTheme.THEME_STORAGE_KEY, themeName);
- }
-
- const oldTheme = document.documentElement.getAttribute(BitTheme.THEME_ATTRIBUTE) || '';
-
- document.documentElement.setAttribute(BitTheme.THEME_ATTRIBUTE, BitTheme._currentTheme);
-
- BitTheme._onThemeChange?.(BitTheme._currentTheme, oldTheme);
-
- return BitTheme._currentTheme;
- }
-
- public static toggleDarkLight() {
- BitTheme._currentTheme = BitTheme._currentTheme === BitTheme._lightTheme
- ? BitTheme._darkTheme
- : BitTheme._lightTheme;
-
- BitTheme.set(BitTheme._currentTheme);
-
- return BitTheme._currentTheme;
- }
-
- public static applyBitTheme(theme: any, element?: HTMLElement) {
- const el = element || document.body;
- Object.keys(theme).forEach(key => el.style.setProperty(key, theme[key]));
- }
-
- public static isSystemDark() {
- return matchMedia('(prefers-color-scheme: dark)').matches;
- }
-
- public static getPersisted() {
- if (!BitTheme._persist) return null;
-
- return localStorage.getItem(BitTheme.THEME_STORAGE_KEY);
- }
-
- private static getActualTheme(theme: string | null) {
- if (theme === BitTheme.SYSTEM_THEME) {
- return BitTheme.isSystemDark() ? BitTheme._darkTheme : BitTheme._lightTheme;
- }
-
- return theme;
- }
-}
-
-(function () {
- const options = {
- system: document.documentElement.hasAttribute('bit-theme-system'),
- persist: document.documentElement.hasAttribute('bit-theme-persist'),
- theme: document.documentElement.getAttribute('bit-theme'),
- default: document.documentElement.getAttribute('bit-theme-default'),
- darkTheme: document.documentElement.getAttribute('bit-theme-dark'),
- lightTheme: document.documentElement.getAttribute('bit-theme-light'),
- };
-
- BitTheme.init(options);
-}());
-
-(window as any).BitTheme = BitTheme;
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj
index d75075f0e7..7d74170cd9 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj
@@ -39,6 +39,7 @@
+
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoPage.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoPage.razor.cs
index 6d3ba6036e..5cda9ce8eb 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoPage.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoPage.razor.cs
@@ -315,6 +315,6 @@ public partial class DemoPage
private readonly List _notInheritedComponents = [
- "CascadingValueProvider", "Chart", "DataGrid", "QuickGrid", "ModalService", "Params", "ProModalService"
+ "CascadingValueProvider", "Chart", "ChartLegacy", "DataGrid", "DataGridLegacy", "ModalService", "Params", "ProModalService"
];
}
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor
index 8e18c95075..3a73b9effb 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor
@@ -564,6 +564,39 @@
+
+ Disabled :
+
+
+ Primary
+ Secondary
+ Tertiary
+ Info
+ Success
+ Warning
+ SevereWarning
+ Error
+
+
+
+
+ PrimaryBackground
+ SecondaryBackground
+ TertiaryBackground
+
+
+
+
+ PrimaryForeground
+ SecondaryForeground
+ TertiaryForeground
+
+
+
+ PrimaryBorder
+ SecondaryBorder
+ TertiaryBorder
+
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor.samples.cs
index 38a512d841..f4b3dc64d2 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor.samples.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ActionButton/BitActionButtonDemo.razor.samples.cs
@@ -339,7 +339,31 @@ More info
TertiaryBorder
- ";
+
+
+
+Primary
+Secondary
+Tertiary
+Info
+Success
+Warning
+SevereWarning
+Error
+
+
+ PrimaryBackground
+ SecondaryBackground
+ TertiaryBackground
+
+
+PrimaryForeground
+SecondaryForeground
+TertiaryForeground
+
+PrimaryBorder
+SecondaryBorder
+TertiaryBorder ";
private readonly string example13RazorCode = @"
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor
index 12de1360a4..4b9aa1ed38 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor
@@ -662,6 +662,116 @@
TertiaryBorder
+
+ Disabled :
+
+
+ Primary
+ Primary
+ Primary
+
+
+
+ Secondary
+ Secondary
+ Secondary
+
+
+
+ Tertiary
+ Tertiary
+ Tertiary
+
+
+
+ Info
+ Info
+ Info
+
+
+
+ Success
+ Success
+ Success
+
+
+
+ Warning
+ Warning
+ Warning
+
+
+
+ SevereWarning
+ SevereWarning
+ SevereWarning
+
+
+
+ Error
+ Error
+ Error
+
+
+
+
+ PrimaryBackground
+ PrimaryBackground
+ PrimaryBackground
+
+
+
+ SecondaryBackground
+ SecondaryBackground
+ SecondaryBackground
+
+
+
+ TertiaryBackground
+ TertiaryBackground
+ TertiaryBackground
+
+
+
+
+
+ PrimaryForeground
+ PrimaryForeground
+ PrimaryForeground
+
+
+
+ SecondaryForeground
+ SecondaryForeground
+ SecondaryForeground
+
+
+
+ TertiaryForeground
+ TertiaryForeground
+ TertiaryForeground
+
+
+
+
+
+ PrimaryBorder
+ PrimaryBorder
+ PrimaryBorder
+
+
+
+ SecondaryBorder
+ SecondaryBorder
+ SecondaryBorder
+
+
+
+ TertiaryBorder
+ TertiaryBorder
+ TertiaryBorder
+
+
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor.samples.cs
index f902eab7c3..31b2e36cb5 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor.samples.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/Button/BitButtonDemo.razor.samples.cs
@@ -579,17 +579,19 @@ private async Task LoadingTemplateClick()
Error
-PrimaryBackground
-PrimaryBackground
-PrimaryBackground
-
-SecondaryBackground
-SecondaryBackground
-SecondaryBackground
-
-TertiaryBackground
-TertiaryBackground
-TertiaryBackground
+
+ PrimaryBackground
+ PrimaryBackground
+ PrimaryBackground
+
+ SecondaryBackground
+ SecondaryBackground
+ SecondaryBackground
+
+ TertiaryBackground
+ TertiaryBackground
+ TertiaryBackground
+
PrimaryForeground
@@ -615,7 +617,80 @@ private async Task LoadingTemplateClick()
TertiaryBorder
TertiaryBorder
-TertiaryBorder ";
+TertiaryBorder
+
+
+Disabled :
+
+Primary
+Primary
+Primary
+
+Secondary
+Secondary
+Secondary
+
+Tertiary
+Tertiary
+Tertiary
+
+Info
+Info
+Info
+
+Success
+Success
+Success
+
+Warning
+Warning
+Warning
+
+SevereWarning
+SevereWarning
+SevereWarning
+
+Error
+Error
+Error
+
+
+ PrimaryBackground
+ PrimaryBackground
+ PrimaryBackground
+
+ SecondaryBackground
+ SecondaryBackground
+ SecondaryBackground
+
+ TertiaryBackground
+ TertiaryBackground
+ TertiaryBackground
+
+
+PrimaryForeground
+PrimaryForeground
+PrimaryForeground
+
+SecondaryForeground
+SecondaryForeground
+SecondaryForeground
+
+TertiaryForeground
+TertiaryForeground
+TertiaryForeground
+
+PrimaryBorder
+PrimaryBorder
+PrimaryBorder
+
+SecondaryBorder
+SecondaryBorder
+SecondaryBorder
+
+TertiaryBorder
+TertiaryBorder
+TertiaryBorder ";
private readonly string example20RazorCode = @"
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/BitButtonGroupDemo.razor.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/BitButtonGroupDemo.razor.scss
index 3be6c0b610..988d2a6984 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/BitButtonGroupDemo.razor.scss
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/BitButtonGroupDemo.razor.scss
@@ -3,6 +3,11 @@
gap: 0.5rem;
display: flex;
flex-flow: column;
+
+ &.row {
+ flex-flow: row;
+ flex-wrap: wrap;
+ }
}
// Constrains the group so the overflow examples actually have something to overflow,
@@ -49,4 +54,4 @@
border-color: aliceblue;
background-color: crimson;
}
-}
+}
\ No newline at end of file
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupCustomDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupCustomDemo.razor
index c739d88c6f..fbc043beb6 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupCustomDemo.razor
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupCustomDemo.razor
@@ -556,126 +556,249 @@
border colors.
-
-
Primary
+
Primary
+
-
-
Secondary
+
Secondary
+
-
-
Tertiary
+
Tertiary
+
-
-
Info
+
Info
+
-
-
Success
+
Success
+
-
-
Warning
+
Warning
+
-
-
SevereWarning
+
SevereWarning
+
-
-
Error
+
Error
+
-
-
PrimaryBackground
+
PrimaryBackground
+
-
-
SecondaryBackground
+
SecondaryBackground
+
-
-
TertiaryBackground
+
TertiaryBackground
+
-
-
PrimaryForeground
+
PrimaryForeground
+
-
-
SecondaryForeground
+
SecondaryForeground
+
-
-
TertiaryForeground
+
TertiaryForeground
+
-
-
PrimaryBorder
+
PrimaryBorder
+
-
-
SecondaryBorder
+
SecondaryBorder
+
-
-
TertiaryBorder
+
TertiaryBorder
+
+
+
Disabled :
+
+
Primary
+
+
+
+
+
+
+
Secondary
+
+
+
+
+
+
+
Tertiary
+
+
+
+
+
+
+
Info
+
+
+
+
+
+
+
Success
+
+
+
+
+
+
+
Warning
+
+
+
+
+
+
+
SevereWarning
+
+
+
+
+
+
+
Error
+
+
+
+
+
+
+
+
PrimaryBackground
+
+
+
+
+
+
+
SecondaryBackground
+
+
+
+
+
+
+
TertiaryBackground
+
+
+
+
+
+
+
+
PrimaryForeground
+
+
+
+
+
+
+
SecondaryForeground
+
+
+
+
+
+
+
TertiaryForeground
+
+
+
+
+
+
+
PrimaryBorder
+
+
+
+
+
+
+
SecondaryBorder
+
+
+
+
+
+
+
TertiaryBorder
+
+
+
+
+
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupCustomDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupCustomDemo.razor.samples.cs
index 8eb0bd0f7f..fb65dcbda5 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupCustomDemo.razor.samples.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupCustomDemo.razor.samples.cs
@@ -734,7 +734,83 @@ protected override void OnInitialized()
- ";
+
+
+
+Disabled :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ";
private readonly string example21CsharpCode = @"
private BitButtonGroupNameSelectors nameSelector = new() { Text = { Selector = i => i.Name } };
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupItemDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupItemDemo.razor
index e7a1ffd408..bd47d0cc96 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupItemDemo.razor
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/ButtonGroup/_BitButtonGroupItemDemo.razor
@@ -505,126 +505,249 @@
border colors.
-
-
Primary
+
Primary
+
-
-
Secondary
+
Secondary
+
-
-
Tertiary
+
Tertiary
+
-
-
Info
+
Info
+
-
-
Success
+
Success
+
-
-
Warning
+
Warning
+
-
-
SevereWarning
+
SevereWarning
+
-
-
Error
+
Error
+
-
-
PrimaryBackground
+
PrimaryBackground
+
-
-
SecondaryBackground
+
SecondaryBackground
+
-
-
TertiaryBackground
+
TertiaryBackground
+
-
-
PrimaryForeground
+
PrimaryForeground
+
-
-
SecondaryForeground
+
SecondaryForeground
+
-
-
TertiaryForeground
+
TertiaryForeground
+