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;
+}