diff --git a/Directory.Build.props b/Directory.Build.props index 17f65f5..493ac9b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,5 +13,6 @@ MIT enable enable + $(NoWarn);CS8601;CS8602;CS8603;CS8604;CS8613;CS8618;CS8619;CS8620;CS8625;CS8629;CS8632;CS8669;CS8609;CS8767 diff --git a/src/SufiChain.SufiBlazor/Components/Forms/SbMarkEditor.razor b/src/SufiChain.SufiBlazor/Components/Forms/SbMarkEditor.razor index 0a23721..b518685 100644 --- a/src/SufiChain.SufiBlazor/Components/Forms/SbMarkEditor.razor +++ b/src/SufiChain.SufiBlazor/Components/Forms/SbMarkEditor.razor @@ -19,6 +19,7 @@ SourceLanguage="@ResolveSourceLanguage()" UseToolbarContributors="@UseToolbarContributors" IncludeDefaultToolbarItems="@IncludeDefaultToolbarItems" + ToolbarScope="@ToolbarScope" HideToolbar="@HideToolbar" ToolbarItems="@ToolbarItems" OnShortcut="@OnShortcut" diff --git a/src/SufiChain.SufiBlazor/Components/Forms/SbMarkEditor.razor.cs b/src/SufiChain.SufiBlazor/Components/Forms/SbMarkEditor.razor.cs index f482686..5ab5e36 100644 --- a/src/SufiChain.SufiBlazor/Components/Forms/SbMarkEditor.razor.cs +++ b/src/SufiChain.SufiBlazor/Components/Forms/SbMarkEditor.razor.cs @@ -23,6 +23,11 @@ public partial class SbMarkEditor [Parameter] public string? SourceLanguage { get; set; } [Parameter] public bool UseToolbarContributors { get; set; } [Parameter] public bool IncludeDefaultToolbarItems { get; set; } = true; + /// + /// Optional toolbar scope forwarded to to + /// filter which registered instances run. + /// + [Parameter] public string? ToolbarScope { get; set; } [Parameter] public bool HideToolbar { get; set; } [Parameter] public IReadOnlyList? ToolbarItems { get; set; } [Parameter] public EventCallback OnShortcut { get; set; } diff --git a/src/SufiChain.SufiBlazor/Components/Forms/SbMarkdownEditor.razor.cs b/src/SufiChain.SufiBlazor/Components/Forms/SbMarkdownEditor.razor.cs index d3b4202..d73dd6a 100644 --- a/src/SufiChain.SufiBlazor/Components/Forms/SbMarkdownEditor.razor.cs +++ b/src/SufiChain.SufiBlazor/Components/Forms/SbMarkdownEditor.razor.cs @@ -28,6 +28,14 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable [Parameter] public string? SourceLanguage { get; set; } [Parameter] public bool UseToolbarContributors { get; set; } [Parameter] public bool IncludeDefaultToolbarItems { get; set; } = true; + /// + /// Optional toolbar scope that filters which + /// instances run for this editor. Contributors whose Scope is non-null + /// only execute when it matches this value. Contributors with a null scope + /// always run. This prevents page-specific toolbar items from leaking across + /// navigations within the same Blazor circuit. + /// + [Parameter] public string? ToolbarScope { get; set; } [Parameter] public bool HideToolbar { get; set; } [Parameter] public IReadOnlyList? ToolbarItems { get; set; } [Parameter] public EventCallback OnShortcut { get; set; } @@ -56,6 +64,7 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable private SbMarkdownEditorMode _lastEditorMode; private string? _lastSourceLanguage; private bool _lastIncludeDefaultToolbarItems; + private string? _lastToolbarScope; private List _toolbarItems = new(); private bool _useFallback; private bool _disposed; @@ -88,6 +97,7 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable _lastEditorMode = EditorMode; _lastSourceLanguage = SourceLanguage; _lastIncludeDefaultToolbarItems = IncludeDefaultToolbarItems; + _lastToolbarScope = ToolbarScope; await InvokeAsync(StateHasChanged); return; } @@ -115,11 +125,13 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable else if (Value != _lastRenderedValue || EditorMode != _lastEditorMode || SourceLanguage != _lastSourceLanguage || - IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems) + IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems || + ToolbarScope != _lastToolbarScope) { if (EditorMode != _lastEditorMode || SourceLanguage != _lastSourceLanguage || - IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems) + IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems || + ToolbarScope != _lastToolbarScope) { await ReinitializeEditorAsync(); } @@ -145,7 +157,8 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable _toolbarItems = await ToolbarService.GetToolbarItemsAsync( editorId, includeDefaults: IncludeDefaultToolbarItems, - includeContributors: true); + includeContributors: true, + scope: ToolbarScope); } else if (ToolbarItems != null) { @@ -261,6 +274,7 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable _lastEditorMode = EditorMode; _lastSourceLanguage = SourceLanguage; _lastIncludeDefaultToolbarItems = IncludeDefaultToolbarItems; + _lastToolbarScope = ToolbarScope; await InvokeAsync(StateHasChanged); } diff --git a/src/SufiChain.SufiBlazor/Components/Forms/SbRichTextEditor.razor b/src/SufiChain.SufiBlazor/Components/Forms/SbRichTextEditor.razor index 83c76ce..a56e495 100644 --- a/src/SufiChain.SufiBlazor/Components/Forms/SbRichTextEditor.razor +++ b/src/SufiChain.SufiBlazor/Components/Forms/SbRichTextEditor.razor @@ -382,6 +382,16 @@ [Parameter] public bool IncludeDefaultToolbarItems { get; set; } = true; + /// + /// Optional toolbar scope that filters which + /// instances run for this editor. Contributors whose Scope is non-null + /// only execute when it matches this value. Contributors with a null scope + /// always run. This prevents page-specific toolbar items from leaking across + /// navigations within the same Blazor circuit. + /// + [Parameter] + public string? ToolbarScope { get; set; } + #region Localization Parameters /// @@ -568,10 +578,11 @@ // Get contributed items _contributedToolbarItems = await _toolbarService.GetToolbarItemsAsync( _editorId, - IncludeDefaultToolbarItems); + IncludeDefaultToolbarItems, + ToolbarScope); // Build a map of contributed items for click handling - var contributedItems = await _toolbarService.GetContributedItemsAsync(_editorId); + var contributedItems = await _toolbarService.GetContributedItemsAsync(_editorId, ToolbarScope); _contributedItemsMap = contributedItems .Where(item => item.OnClickAsync != null) .ToDictionary(item => item.Id, item => item); diff --git a/src/SufiChain.SufiBlazor/Components/Forms/SbSelectOption.razor b/src/SufiChain.SufiBlazor/Components/Forms/SbSelectOption.razor index abbd368..a45e6b3 100644 --- a/src/SufiChain.SufiBlazor/Components/Forms/SbSelectOption.razor +++ b/src/SufiChain.SufiBlazor/Components/Forms/SbSelectOption.razor @@ -1,5 +1,6 @@ @namespace SufiChain.SufiBlazor.Components.Forms @typeparam TValue +@implements IDisposable @* This component is used as a child of SbSelect to define options declaratively. *@ @* It registers itself with the parent SbSelect via cascading parameter. *@ @@ -54,4 +55,9 @@ Disabled = Disabled }); } + + public void Dispose() + { + Context?.UnregisterOption(Value); + } } diff --git a/src/SufiChain.SufiBlazor/Components/Forms/SbSimpleSelect.razor b/src/SufiChain.SufiBlazor/Components/Forms/SbSimpleSelect.razor index 00bed75..d623c9a 100644 --- a/src/SufiChain.SufiBlazor/Components/Forms/SbSimpleSelect.razor +++ b/src/SufiChain.SufiBlazor/Components/Forms/SbSimpleSelect.razor @@ -220,15 +220,36 @@ void ISbSelectOptionContext.RegisterOption(SbSelectOptionInfo option) { - if (!_options.Any(o => EqualityComparer.Default.Equals(o.Value, option.Value))) + // Update existing entry in place so Text/ChildContent changes propagate; + // otherwise add. We must NOT clear _options here or in OnParametersSet, + // because Blazor skips calling OnParametersSet on SbSelectOption children + // whose parameters haven't changed, which would leave _options empty and + // cause GetDisplayText to fall back to Value.ToString() (e.g. a Guid). + var existingIndex = _options.FindIndex(o => + EqualityComparer.Default.Equals(o.Value, option.Value)); + if (existingIndex >= 0) + { + _options[existingIndex] = option; + } + else { _options.Add(option); } } + void ISbSelectOptionContext.UnregisterOption(TValue? value) + { + var index = _options.FindIndex(o => + EqualityComparer.Default.Equals(o.Value, value)); + if (index >= 0) + { + _options.RemoveAt(index); + } + } + protected override void OnParametersSet() { - _options.Clear(); + // Intentionally do NOT clear _options here. See RegisterOption note above. } private IEnumerable> FilteredOptions diff --git a/src/SufiChain.SufiBlazor/Contracts/Editors/IMdToolbarContributor.cs b/src/SufiChain.SufiBlazor/Contracts/Editors/IMdToolbarContributor.cs index 306bd74..2a023db 100644 --- a/src/SufiChain.SufiBlazor/Contracts/Editors/IMdToolbarContributor.cs +++ b/src/SufiChain.SufiBlazor/Contracts/Editors/IMdToolbarContributor.cs @@ -15,6 +15,15 @@ public interface IMdToolbarContributor /// int Order => 100; + /// + /// The toolbar scope this contributor belongs to. When non-null, the + /// contributor only runs on editors whose ToolbarScope parameter + /// matches this value. When null (default), the contributor runs on every + /// editor instance — use this for self-filtering contributors (e.g. ones + /// that check a host registration) or for globally-applicable items. + /// + string? Scope => null; + /// /// Configure the toolbar by adding custom items to the context. /// @@ -30,6 +39,13 @@ public class MdToolbarContext public IServiceProvider ServiceProvider { get; } public string? EditorId { get; set; } + /// + /// The scope declared by the editor instance via its ToolbarScope + /// parameter. Contributors whose + /// is non-null only run when this value matches. + /// + public string? Scope { get; set; } + public MdToolbarContext(IServiceProvider serviceProvider) { ServiceProvider = serviceProvider; diff --git a/src/SufiChain.SufiBlazor/Contracts/Editors/IRteToolbarContributor.cs b/src/SufiChain.SufiBlazor/Contracts/Editors/IRteToolbarContributor.cs index 999d8f5..ccb6eb8 100644 --- a/src/SufiChain.SufiBlazor/Contracts/Editors/IRteToolbarContributor.cs +++ b/src/SufiChain.SufiBlazor/Contracts/Editors/IRteToolbarContributor.cs @@ -16,6 +16,15 @@ public interface IRteToolbarContributor /// int Order => 100; + /// + /// The toolbar scope this contributor belongs to. When non-null, the + /// contributor only runs on editors whose ToolbarScope parameter + /// matches this value. When null (default), the contributor runs on every + /// editor instance — use this for self-filtering contributors or for + /// globally-applicable items (e.g. culture-aware font selectors). + /// + string? Scope => null; + /// /// Configure the toolbar by adding custom items to the context. /// @@ -43,6 +52,13 @@ public class RteToolbarContext /// public string? EditorId { get; set; } + /// + /// The scope declared by the editor instance via its ToolbarScope + /// parameter. Contributors whose + /// is non-null only run when this value matches. + /// + public string? Scope { get; set; } + public RteToolbarContext(IServiceProvider serviceProvider) { ServiceProvider = serviceProvider; diff --git a/src/SufiChain.SufiBlazor/Contracts/Editors/MdToolbarService.cs b/src/SufiChain.SufiBlazor/Contracts/Editors/MdToolbarService.cs index cd3472c..2f21400 100644 --- a/src/SufiChain.SufiBlazor/Contracts/Editors/MdToolbarService.cs +++ b/src/SufiChain.SufiBlazor/Contracts/Editors/MdToolbarService.cs @@ -24,7 +24,8 @@ public class MdToolbarService : IMdToolbarService public async Task> GetToolbarItemsAsync( string? editorId = null, bool? includeDefaults = null, - bool includeContributors = true) + bool includeContributors = true, + string? scope = null) { var shouldIncludeDefaults = includeDefaults ?? _options.IncludeDefaultItems; var allItems = new List(); @@ -36,8 +37,12 @@ public class MdToolbarService : IMdToolbarService if (includeContributors) { - var context = new MdToolbarContext(_serviceProvider) { EditorId = editorId }; - foreach (var contributor in GetContributors().OrderBy(c => c.Order)) + var context = new MdToolbarContext(_serviceProvider) + { + EditorId = editorId, + Scope = scope + }; + foreach (var contributor in GetContributors(scope).OrderBy(c => c.Order)) { await contributor.ConfigureToolbarAsync(context); } @@ -80,11 +85,12 @@ public class MdToolbarService : IMdToolbarService } } - private IEnumerable GetContributors() + private IEnumerable GetContributors(string? scope) { foreach (var contributorType in _options.Contributors) { - if (_serviceProvider.GetService(contributorType) is IMdToolbarContributor contributor) + if (_serviceProvider.GetService(contributorType) is IMdToolbarContributor contributor + && (contributor.Scope == null || string.Equals(contributor.Scope, scope, StringComparison.Ordinal))) { yield return contributor; } @@ -122,6 +128,7 @@ public interface IMdToolbarService Task> GetToolbarItemsAsync( string? editorId = null, bool? includeDefaults = null, - bool includeContributors = true); + bool includeContributors = true, + string? scope = null); Task ExecuteItemActionAsync(MdToolbarContributedItem item, MdToolbarActionContext actionContext); } diff --git a/src/SufiChain.SufiBlazor/Contracts/Editors/RteToolbarService.cs b/src/SufiChain.SufiBlazor/Contracts/Editors/RteToolbarService.cs index 8f75c5e..e59870a 100644 --- a/src/SufiChain.SufiBlazor/Contracts/Editors/RteToolbarService.cs +++ b/src/SufiChain.SufiBlazor/Contracts/Editors/RteToolbarService.cs @@ -28,9 +28,11 @@ public class RteToolbarService : IRteToolbarService /// /// The editor instance ID. /// Whether to include default toolbar items. + /// Optional toolbar scope that filters which contributors run. public async Task> GetToolbarItemsAsync( string? editorId = null, - bool? includeDefaults = null) + bool? includeDefaults = null, + string? scope = null) { var shouldIncludeDefaults = includeDefaults ?? _options.IncludeDefaultItems; var allItems = new List(); @@ -42,9 +44,13 @@ public class RteToolbarService : IRteToolbarService } // Get contributed items from all registered contributors - var context = new RteToolbarContext(_serviceProvider) { EditorId = editorId }; + var context = new RteToolbarContext(_serviceProvider) + { + EditorId = editorId, + Scope = scope + }; - var contributors = GetContributors(); + var contributors = GetContributors(scope); foreach (var contributor in contributors.OrderBy(c => c.Order)) { await contributor.ConfigureToolbarAsync(context); @@ -84,11 +90,17 @@ public class RteToolbarService : IRteToolbarService /// /// Get the contributed items only (without defaults). /// - public async Task> GetContributedItemsAsync(string? editorId = null) + public async Task> GetContributedItemsAsync( + string? editorId = null, + string? scope = null) { - var context = new RteToolbarContext(_serviceProvider) { EditorId = editorId }; + var context = new RteToolbarContext(_serviceProvider) + { + EditorId = editorId, + Scope = scope + }; - var contributors = GetContributors(); + var contributors = GetContributors(scope); foreach (var contributor in contributors.OrderBy(c => c.Order)) { await contributor.ConfigureToolbarAsync(context); @@ -110,12 +122,13 @@ public class RteToolbarService : IRteToolbarService } } - private IEnumerable GetContributors() + private IEnumerable GetContributors(string? scope) { foreach (var contributorType in _options.Contributors) { var contributor = _serviceProvider.GetService(contributorType) as IRteToolbarContributor; - if (contributor != null) + if (contributor != null + && (contributor.Scope == null || string.Equals(contributor.Scope, scope, StringComparison.Ordinal))) { yield return contributor; } @@ -201,12 +214,17 @@ public interface IRteToolbarService /// /// Get all toolbar items including default and contributed items. /// - Task> GetToolbarItemsAsync(string? editorId = null, bool? includeDefaults = null); + Task> GetToolbarItemsAsync( + string? editorId = null, + bool? includeDefaults = null, + string? scope = null); /// /// Get the contributed items only (without defaults). /// - Task> GetContributedItemsAsync(string? editorId = null); + Task> GetContributedItemsAsync( + string? editorId = null, + string? scope = null); /// /// Execute a contributed toolbar item's click handler. diff --git a/src/SufiChain.SufiBlazor/Contracts/Forms/ISbSelectOptionContext.cs b/src/SufiChain.SufiBlazor/Contracts/Forms/ISbSelectOptionContext.cs index 490369d..c524357 100644 --- a/src/SufiChain.SufiBlazor/Contracts/Forms/ISbSelectOptionContext.cs +++ b/src/SufiChain.SufiBlazor/Contracts/Forms/ISbSelectOptionContext.cs @@ -13,6 +13,12 @@ internal interface ISbSelectOptionContext /// /// The option information to register. void RegisterOption(SbSelectOptionInfo option); + + /// + /// Removes a previously registered option (e.g. when its SbSelectOption is disposed). + /// + /// The value of the option to unregister. + void UnregisterOption(TValue? value); } /// diff --git a/src/SufiChain.SufiBlazor/SufiChain.SufiBlazor.csproj b/src/SufiChain.SufiBlazor/SufiChain.SufiBlazor.csproj index 304f6d7..3f61155 100644 --- a/src/SufiChain.SufiBlazor/SufiChain.SufiBlazor.csproj +++ b/src/SufiChain.SufiBlazor/SufiChain.SufiBlazor.csproj @@ -15,6 +15,7 @@ true + $(NoWarn);1591 diff --git a/src/SufiChain.SufiBlazor/wwwroot/sufiblazor.css b/src/SufiChain.SufiBlazor/wwwroot/sufiblazor.css index 84f6c2b..4dcb5e4 100644 --- a/src/SufiChain.SufiBlazor/wwwroot/sufiblazor.css +++ b/src/SufiChain.SufiBlazor/wwwroot/sufiblazor.css @@ -8528,3 +8528,1091 @@ body.sb-resizing { border-left: none; border-right: 4px solid var(--sb-color-primary); } + +/* ============================================ + CSS Coverage Gap Fixes + Classes emitted by components that had no matching rule. + Organized by component. Uses --sb-* design tokens and matches + neighboring component conventions. + ============================================ */ + +/* ---- Shared required-asterisk pattern (form fields) ---- */ +.sb-autocomplete__required, +.sb-colorpicker__required, +.sb-daterangepicker__required, +.sb-multi-select__required, +.sb-property-grid__required, +.sb-select__required, +.sb-textarea__required, +.sb-timepicker__required { + color: var(--sb-color-danger); + margin-inline-start: var(--sb-space-1); +} + +/* ---- Shared field-label pattern ---- */ +.sb-select__label, +.sb-textarea__label { + display: block; + font-size: var(--sb-font-size-sm); + font-weight: var(--sb-font-weight-medium); + color: var(--sb-color-text); + margin-bottom: var(--sb-space-1); +} + +/* ============================================ + SbAccordionItem + ============================================ */ +.sb-accordion-item__title { + display: block; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: inherit; + font-weight: var(--sb-font-weight-medium); +} + +.sb-accordion-item__body { + min-width: 0; +} + +/* ============================================ + SbAddButton + ============================================ */ +.sb-add-button-container { + position: relative; + display: inline-block; +} + +.sb-add-button__label { + font-size: var(--sb-font-size-sm); + font-weight: var(--sb-font-weight-medium); +} + +.sb-add-button__menu { + position: absolute; + z-index: var(--sb-z-dropdown); + inset-inline-start: 0; + margin-top: var(--sb-space-1); + min-width: 220px; + padding: var(--sb-space-1); + background-color: var(--sb-color-surface); + border: 1px solid var(--sb-color-border); + border-radius: var(--sb-radius-md); + box-shadow: var(--sb-shadow-lg); +} + +.sb-add-button__option { + display: flex; + align-items: flex-start; + gap: var(--sb-space-2); + width: 100%; + padding: var(--sb-space-2) var(--sb-space-3); + border: none; + background: transparent; + color: var(--sb-color-text); + font-family: inherit; + font-size: var(--sb-font-size-sm); + text-align: start; + cursor: pointer; + border-radius: var(--sb-radius-sm); + transition: background-color var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-add-button__option:hover:not(:disabled) { + background-color: var(--sb-color-surface-variant); +} + +.sb-add-button__option:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.sb-add-button__option-icon { + display: inline-flex; + align-items: center; + flex-shrink: 0; + color: var(--sb-color-text-muted); +} + +.sb-add-button__option-label { + font-weight: var(--sb-font-weight-medium); + color: var(--sb-color-text); +} + +.sb-add-button__option-description { + display: block; + font-size: var(--sb-font-size-xs); + color: var(--sb-color-text-muted); +} + +/* ============================================ + SbBreadcrumb + ============================================ */ +.sb-breadcrumb__icon { + display: inline-flex; + align-items: center; + flex-shrink: 0; + color: var(--sb-color-text-muted); +} + +/* ============================================ + SbContextMenu + ============================================ */ +.sb-context-menu-trigger { + display: inline-flex; + cursor: context-menu; +} + +/* ============================================ + SbDataGrid + ============================================ */ +.sb-datagrid__action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + border: none; + border-radius: var(--sb-radius-sm); + background: transparent; + color: var(--sb-color-text-secondary); + cursor: pointer; + transition: background-color var(--sb-duration-fast) var(--sb-easing-default), + color var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-datagrid__action-btn:hover { + background-color: var(--sb-color-surface-variant); + color: var(--sb-color-text); +} + +.sb-datagrid__cell--actions { + width: 1%; + min-width: 80px; + white-space: nowrap; + text-align: center; +} + +.sb-datagrid__editor--text { + width: 100%; + min-width: 0; + box-sizing: border-box; + padding: var(--sb-space-1) var(--sb-space-2); + border: 1px solid var(--sb-color-primary); + border-radius: var(--sb-radius-sm); + background-color: var(--sb-color-surface); + color: var(--sb-color-text); + font-family: inherit; + font-size: var(--sb-font-size-sm); + outline: none; + box-shadow: 0 0 0 3px var(--sb-color-primary-light); +} + +.sb-datagrid__sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* ============================================ + SbDropZone + ============================================ */ +.sb-dropzone__indicator { + font-size: var(--sb-font-size-sm); + color: var(--sb-color-primary); + margin-top: var(--sb-space-1); +} + +.sb-dropzone__placeholder { + color: var(--sb-color-text-muted); + font-size: var(--sb-font-size-sm); +} + +/* ============================================ + SbEmptyState + ============================================ */ +.sb-empty-state__icon-text { + font-size: var(--sb-font-size-sm); + color: var(--sb-color-text-secondary); + margin-top: var(--sb-space-2); +} + +/* ============================================ + SbFilterBar (component emits sb-filter-bar; legacy CSS used sb-filterbar) + ============================================ */ +.sb-filter-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sb-space-2); + padding: var(--sb-space-3); + background-color: var(--sb-color-surface-variant); + border-radius: var(--sb-radius-md); +} + +.sb-filter-bar__search { + position: relative; + display: flex; + align-items: center; + flex: 1; + min-width: 200px; +} + +.sb-filter-bar__search-input { + flex: 1; + min-width: 0; + height: 36px; + padding: var(--sb-space-2) var(--sb-space-3); + border: 1px solid var(--sb-color-border); + border-radius: var(--sb-radius-md); + background-color: var(--sb-color-surface); + color: var(--sb-color-text); + font-family: inherit; + font-size: var(--sb-font-size-sm); + transition: border-color var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-filter-bar__search-input:focus { + outline: none; + border-color: var(--sb-color-primary); + box-shadow: 0 0 0 3px var(--sb-color-primary-light); +} + +.sb-filter-bar__search-clear { + position: absolute; + inset-inline-end: var(--sb-space-1); + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: none; + border-radius: var(--sb-radius-full); + background: transparent; + color: var(--sb-color-text-muted); + font-size: var(--sb-font-size-md); + line-height: 1; + cursor: pointer; +} + +.sb-filter-bar__search-clear:hover { + background-color: var(--sb-color-surface-variant); + color: var(--sb-color-text); +} + +.sb-filter-bar__filters { + display: flex; + flex-wrap: wrap; + gap: var(--sb-space-2); +} + +.sb-filter-bar__active { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sb-space-1); + width: 100%; +} + +.sb-filter-bar__tag { + display: inline-flex; + align-items: center; + gap: var(--sb-space-1); + padding: var(--sb-space-1) var(--sb-space-2); + background-color: var(--sb-color-primary-light); + color: var(--sb-color-primary); + border-radius: var(--sb-radius-full); + font-size: var(--sb-font-size-xs); +} + +.sb-filter-bar__tag-label { + font-weight: var(--sb-font-weight-medium); +} + +.sb-filter-bar__tag-value { + color: var(--sb-color-text); +} + +.sb-filter-bar__tag-remove { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + padding: 0; + border: none; + border-radius: var(--sb-radius-full); + background: transparent; + color: var(--sb-color-primary); + font-size: 14px; + line-height: 1; + cursor: pointer; +} + +.sb-filter-bar__tag-remove:hover { + background-color: var(--sb-color-primary); + color: var(--sb-color-on-primary); +} + +.sb-filter-bar__clear-all { + padding: 0; + border: none; + background: transparent; + color: var(--sb-color-text-secondary); + font-family: inherit; + font-size: var(--sb-font-size-xs); + cursor: pointer; + text-decoration: underline; +} + +.sb-filter-bar__clear-all:hover { + color: var(--sb-color-text); +} + +.sb-filter-bar__actions { + display: flex; + align-items: center; + gap: var(--sb-space-2); + margin-inline-start: auto; +} + +/* ============================================ + SbForm + ============================================ */ +.sb-form { + display: flex; + flex-direction: column; + gap: var(--sb-space-4); +} + +.sb-form__content { + display: flex; + flex-direction: column; + gap: var(--sb-space-4); +} + +.sb-form__footer { + display: flex; + justify-content: flex-end; + align-items: center; + gap: var(--sb-space-2); + margin-top: var(--sb-space-2); + padding-top: var(--sb-space-4); + border-top: 1px solid var(--sb-color-border); +} + +/* ============================================ + SbFormField + ============================================ */ +.sb-form-field__input { + display: flex; + flex-direction: column; + gap: var(--sb-space-1); + min-width: 0; +} + +/* ============================================ + SbInlineToolbar (component emits __btn/__separator; legacy CSS used __button/__divider) + ============================================ */ +.sb-inline-toolbar__btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + border: none; + border-radius: var(--sb-radius-sm); + background: transparent; + color: var(--sb-color-text); + cursor: pointer; + transition: background-color var(--sb-duration-fast) var(--sb-easing-default), + color var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-inline-toolbar__btn:hover:not(:disabled) { + background-color: var(--sb-color-surface-variant); +} + +.sb-inline-toolbar__btn--active { + background-color: var(--sb-color-primary-light); + color: var(--sb-color-primary); +} + +.sb-inline-toolbar__btn--danger { + color: var(--sb-color-danger); +} + +.sb-inline-toolbar__btn--danger:hover:not(:disabled) { + background-color: var(--sb-color-danger-light, #fee2e2); + color: var(--sb-color-danger); +} + +.sb-inline-toolbar__btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.sb-inline-toolbar__icon { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: var(--sb-font-size-md); +} + +.sb-inline-toolbar__label { + font-size: var(--sb-font-size-sm); + margin-inline-start: var(--sb-space-1); +} + +.sb-inline-toolbar__separator { + width: 1px; + height: 20px; + background-color: var(--sb-color-border); + flex-shrink: 0; +} + +/* ============================================ + SbInspectorPanel + ============================================ */ +.sb-inspector-panel__body { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: var(--sb-space-3); +} + +.sb-inspector-panel__header-actions { + display: flex; + align-items: center; + gap: var(--sb-space-1); +} + +.sb-inspector-panel__toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + border: none; + border-radius: var(--sb-radius-sm); + background: transparent; + color: var(--sb-color-text-muted); + cursor: pointer; + transition: background-color var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-inspector-panel__toggle:hover { + background-color: var(--sb-color-surface-variant); + color: var(--sb-color-text); +} + +/* ============================================ + SbInspectorSection + ============================================ */ +.sb-inspector-section__title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--sb-color-text); +} + +.sb-inspector-section__badge { + font-size: var(--sb-font-size-xs); + color: var(--sb-color-text-muted); +} + +.sb-inspector-section__chevron { + flex-shrink: 0; + color: var(--sb-color-text-muted); + transition: transform var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-inspector-section--expanded .sb-inspector-section__chevron { + transform: rotate(90deg); +} + +[dir="rtl"] .sb-inspector-section--expanded .sb-inspector-section__chevron { + transform: rotate(-90deg); +} + +/* ============================================ + SbMarkdownEditor + ============================================ */ +.sb-markdown-editor__fallback { + white-space: pre-wrap; + margin: 0; + padding: var(--sb-space-3); + background-color: var(--sb-color-surface-variant); + border-radius: var(--sb-radius-md); + font-family: var(--sb-font-family-mono); + font-size: var(--sb-font-size-sm); + color: var(--sb-color-text); +} + +.sb-markdown-editor__toolbar-icon { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: var(--sb-font-size-md); +} + +/* ============================================ + SbMetric + ============================================ */ +.sb-metric__number { + display: inline-block; +} + +.sb-metric__prefix, +.sb-metric__suffix { + font-size: 0.6em; + font-weight: var(--sb-font-weight-medium); + color: var(--sb-color-text-muted); + margin-inline: var(--sb-space-1); +} + +.sb-metric__content { + margin-top: var(--sb-space-2); + font-size: var(--sb-font-size-sm); + color: var(--sb-color-text-secondary); +} + +.sb-metric__progress { + height: 4px; + margin-top: var(--sb-space-2); + background-color: var(--sb-color-surface-variant); + border-radius: var(--sb-radius-full); + overflow: hidden; +} + +.sb-metric__progress-bar { + height: 100%; + background-color: var(--sb-color-primary); + border-radius: var(--sb-radius-full); + transition: width var(--sb-duration-slow) var(--sb-easing-default); +} + +.sb-metric--small .sb-metric__value { + font-size: var(--sb-font-size-lg); +} + +.sb-metric--medium .sb-metric__value { + font-size: var(--sb-font-size-xl); +} + +.sb-metric--large .sb-metric__value { + font-size: var(--sb-font-size-3xl); +} + +/* ============================================ + SbPropertyGrid + ============================================ */ +.sb-property-grid__value { + min-width: 0; +} + +/* ============================================ + SbResizable (component emits full direction names; legacy CSS used compass abbreviations) + ============================================ */ +.sb-resizable__handle--top { + top: 0; + left: 0; + right: 0; + width: 100%; + height: 4px; + cursor: ns-resize; +} + +.sb-resizable__handle--bottom { + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 4px; + cursor: ns-resize; +} + +.sb-resizable__handle--left { + top: 0; + bottom: 0; + left: 0; + width: 4px; + height: 100%; + cursor: ew-resize; +} + +.sb-resizable__handle--right { + top: 0; + bottom: 0; + right: 0; + width: 4px; + height: 100%; + cursor: ew-resize; +} + +.sb-resizable__handle--top-left { + top: 0; + left: 0; + width: 12px; + height: 12px; + cursor: nwse-resize; +} + +.sb-resizable__handle--top-right { + top: 0; + right: 0; + width: 12px; + height: 12px; + cursor: nesw-resize; +} + +.sb-resizable__handle--bottom-left { + bottom: 0; + left: 0; + width: 12px; + height: 12px; + cursor: nesw-resize; +} + +.sb-resizable__handle--bottom-right { + bottom: 0; + right: 0; + width: 12px; + height: 12px; + cursor: nwse-resize; +} + +/* ============================================ + SbSlugEditor + ============================================ */ +.sb-slug-editor { + display: flex; + flex-direction: column; + gap: var(--sb-space-2); + width: 100%; +} + +.sb-slug-editor--error .sb-slug-editor__input-row { + border-color: var(--sb-color-danger); +} + +.sb-slug-editor--error .sb-slug-editor__input-row:focus-within { + border-color: var(--sb-color-danger); + box-shadow: 0 0 0 3px var(--sb-color-danger-light); +} + +.sb-slug-editor__input-row { + display: flex; + align-items: stretch; + border: 1px solid var(--sb-color-border); + border-radius: var(--sb-radius-md); + background-color: var(--sb-color-surface); + overflow: hidden; + transition: border-color var(--sb-duration-fast) var(--sb-easing-default), + box-shadow var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-slug-editor__input-row:focus-within { + border-color: var(--sb-color-primary); + box-shadow: 0 0 0 3px var(--sb-color-primary-light); +} + +.sb-slug-editor__base-url { + display: inline-flex; + align-items: center; + padding: var(--sb-space-2) var(--sb-space-3); + background-color: var(--sb-color-surface-variant); + color: var(--sb-color-text-muted); + font-size: var(--sb-font-size-sm); + white-space: nowrap; + border-inline-end: 1px solid var(--sb-color-border); +} + +.sb-slug-editor__input-wrapper { + flex: 1; + min-width: 0; + display: flex; +} + +.sb-slug-editor__input { + flex: 1; + min-width: 0; + height: 40px; + padding: var(--sb-space-2) var(--sb-space-3); + border: none; + background: transparent; + color: var(--sb-color-text); + font-family: inherit; + font-size: var(--sb-font-size-md); + outline: none; +} + +.sb-slug-editor__input::placeholder { + color: var(--sb-color-text-muted); +} + +.sb-slug-editor__input:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +.sb-slug-editor__preview { + display: flex; + align-items: center; + gap: var(--sb-space-1); + padding: var(--sb-space-1) var(--sb-space-2); + background-color: var(--sb-color-surface-variant); + border-radius: var(--sb-radius-sm); + font-size: var(--sb-font-size-xs); + color: var(--sb-color-text-secondary); +} + +.sb-slug-editor__preview-label { + font-weight: var(--sb-font-weight-medium); + color: var(--sb-color-text-muted); + flex-shrink: 0; +} + +.sb-slug-editor__preview-url { + color: var(--sb-color-primary); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sb-slug-editor__error { + font-size: var(--sb-font-size-xs); + color: var(--sb-color-danger); +} + +.sb-slug-editor__generate { + display: inline-flex; + align-items: center; + gap: var(--sb-space-1); + padding: var(--sb-space-1) var(--sb-space-3); + border: 1px solid var(--sb-color-border); + border-radius: var(--sb-radius-md); + background-color: var(--sb-color-surface); + color: var(--sb-color-text); + font-family: inherit; + font-size: var(--sb-font-size-sm); + cursor: pointer; + align-self: flex-start; + transition: background-color var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-slug-editor__generate:hover:not(:disabled) { + background-color: var(--sb-color-surface-variant); +} + +.sb-slug-editor__generate:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* ============================================ + SbSortableList + ============================================ */ +.sb-sortable-list--horizontal { + flex-direction: row; + align-items: center; +} + +.sb-sortable-list__item { + display: flex; + align-items: center; + gap: var(--sb-space-2); + padding: var(--sb-space-2) var(--sb-space-3); + background-color: var(--sb-color-surface); + border: 1px solid var(--sb-color-border); + border-radius: var(--sb-radius-md); + transition: box-shadow var(--sb-duration-fast) var(--sb-easing-default), + border-color var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-sortable-list__item--dragging { + box-shadow: var(--sb-shadow-lg); + opacity: 0.8; +} + +.sb-sortable-list__item--drop-target { + border-color: var(--sb-color-primary); + background-color: var(--sb-color-primary-light); +} + +.sb-sortable-list__handle { + display: inline-flex; + align-items: center; + flex-shrink: 0; + color: var(--sb-color-text-muted); + cursor: grab; +} + +.sb-sortable-list__handle:active { + cursor: grabbing; +} + +.sb-sortable-list__content { + flex: 1; + min-width: 0; +} + +.sb-sortable-list__remove { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: none; + border-radius: var(--sb-radius-sm); + background: transparent; + color: var(--sb-color-text-muted); + font-size: var(--sb-font-size-md); + line-height: 1; + cursor: pointer; + flex-shrink: 0; + transition: background-color var(--sb-duration-fast) var(--sb-easing-default), + color var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-sortable-list__remove:hover { + background-color: var(--sb-color-danger-light, #fee2e2); + color: var(--sb-color-danger); +} + +.sb-sortable-list__empty { + padding: var(--sb-space-4); + text-align: center; + color: var(--sb-color-text-muted); + font-size: var(--sb-font-size-sm); +} + +/* ============================================ + SbSplitPane + ============================================ */ +.sb-split-pane__panel--first, +.sb-split-pane__panel--second { + flex: 1; + min-width: 0; + min-height: 0; + overflow: auto; +} + +.sb-split-pane__divider-handle { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--sb-color-border-strong); + border-radius: var(--sb-radius-sm); + pointer-events: none; +} + +.sb-split-pane--horizontal > .sb-split-pane__divider .sb-split-pane__divider-handle { + width: 2px; + height: 24px; +} + +.sb-split-pane--vertical > .sb-split-pane__divider .sb-split-pane__divider-handle { + width: 24px; + height: 2px; +} + +/* ============================================ + SbStatusPill + ============================================ */ +.sb-status-pill__label { + min-width: 0; + white-space: nowrap; +} + +/* ============================================ + SbStep + ============================================ */ +.sb-step-content { + margin-top: var(--sb-space-4); + padding-top: var(--sb-space-4); + border-top: 1px solid var(--sb-color-border); +} + +/* ============================================ + SbTable + ============================================ */ +.sb-table__head th { + background-color: var(--sb-color-surface-variant); + font-weight: var(--sb-font-weight-semibold); + color: var(--sb-color-text); + text-align: start; +} + +.sb-table__foot td { + font-weight: var(--sb-font-weight-medium); + border-top: 2px solid var(--sb-color-border); +} + +.sb-table__row { + transition: background-color var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-table__row:hover { + background-color: var(--sb-color-surface-variant); +} + +/* ============================================ + SbTab / SbTabs + ============================================ */ +.sb-tab-panel { + display: block; + min-width: 0; +} + +.sb-tab-panel[hidden] { + display: none; +} + +.sb-tabs__content { + flex: 1; + min-width: 0; +} + +.sb-tabs__tab-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ============================================ + SbTagInput + ============================================ */ +.sb-tag-input__tag-text { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ============================================ + SbTextArea + ============================================ */ +.sb-textarea__resize-handle { + position: absolute; + bottom: var(--sb-space-1); + inset-inline-end: var(--sb-space-1); + width: 12px; + height: 12px; + cursor: nwse-resize; + color: var(--sb-color-border-strong); + pointer-events: none; +} + +/* ============================================ + SbTextField + ============================================ */ +.sb-text-field__adornment--start, +.sb-text-field__adornment--end { + flex-shrink: 0; +} + +/* ============================================ + SbTimePicker + ============================================ */ +.sb-timepicker__column--period { + min-width: 80px; +} + +/* ============================================ + SbToastHost + ============================================ */ +.sb-toast-host__item { + pointer-events: auto; + animation: sb-toast-in 0.3s var(--sb-easing-default); +} + +/* ============================================ + SbTreeViewNode + ============================================ */ +.sb-treeview-node__chevron { + display: inline-flex; + align-items: center; + justify-content: center; + transition: transform var(--sb-duration-fast) var(--sb-easing-default); +} + +.sb-treeview-node__checkbox { + flex-shrink: 0; + width: 16px; + height: 16px; + margin: 0; + cursor: pointer; + accent-color: var(--sb-color-primary); +} + +/* ============================================ + SbConfirmDialog (explicit icon-variant modifiers; legacy CSS used descendant form) + ============================================ */ +.sb-confirm-dialog__icon--default { + background-color: var(--sb-color-primary-light); + color: var(--sb-color-primary); +} + +.sb-confirm-dialog__icon--danger { + background-color: var(--sb-color-danger-light, #fee2e2); + color: var(--sb-color-danger); +} + +.sb-confirm-dialog__icon--warning { + background-color: var(--sb-color-warning-light, #fef3c7); + color: var(--sb-color-warning); +} + +.sb-confirm-dialog__icon--info { + background-color: var(--sb-color-info-light, #e0f2fe); + color: var(--sb-color-info); +} + +.sb-confirm-dialog__icon--success { + background-color: var(--sb-color-success-light, #dcfce7); + color: var(--sb-color-success); +} + +/* ============================================ + SbColorPicker (remaining sub-elements) + ============================================ */ +.sb-colorpicker__preview { + flex-shrink: 0; + width: 24px; + height: 24px; + border: 1px solid var(--sb-color-border); + border-radius: var(--sb-radius-sm); + background-color: var(--sb-color-surface); +} + +.sb-colorpicker__opacity-slider { + width: 100%; + margin: var(--sb-space-1) 0 0; + cursor: pointer; + accent-color: var(--sb-color-primary); +} + +/* ============================================ + SbTable (tbody structural rule) + ============================================ */ +.sb-table__body { + vertical-align: top; +}