diff --git a/MatrixRoomUtils.Web/Shared/SimpleComponents/DictionaryEditor.razor b/MatrixRoomUtils.Web/Shared/SimpleComponents/DictionaryEditor.razor
new file mode 100644
index 0000000..4d90c57
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/SimpleComponents/DictionaryEditor.razor
@@ -0,0 +1,41 @@
+@using MatrixRoomUtils.Core.Extensions
+<table>
+ @foreach(var i in Items.Keys)
+ {
+ var key = i;
+ <input value="@Items[key]" @oninput="(obj) => inputChanged(obj, key)">
+ <button @onclick="() => { Items.Remove(key); ItemsChanged.InvokeAsync(); }">Remove</button>
+ <br/>
+ }
+</table>
+<button @onclick="() => { Items.Add(string.Empty, default); ItemsChanged.InvokeAsync(); }">Add</button>
+
+@code {
+
+ [Parameter]
+ public Dictionary<string, object> Items { get; set; } = new();
+
+ [Parameter, EditorRequired]
+ public EventCallback ItemsChanged { get; set; }
+
+ [Parameter]
+ public Func<string,string>? KeyFormatter { get; set; }
+
+ [Parameter]
+ public Action? OnFocusLost { get; set; }
+
+
+ protected override Task OnInitializedAsync()
+ {
+ Console.WriteLine($"DictionaryEditor initialized with {Items.Count} items: {Items.ToJson()}");
+ return base.OnInitializedAsync();
+ }
+
+ private void inputChanged(ChangeEventArgs obj, string key)
+ {
+ Console.WriteLine($"StringListEditor inputChanged {key} {obj.Value}");
+ Items[key] = obj.Value.ToString();
+ ItemsChanged.InvokeAsync();
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/SimpleComponents/FancyTextBox.razor b/MatrixRoomUtils.Web/Shared/SimpleComponents/FancyTextBox.razor
new file mode 100644
index 0000000..9c325a7
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/SimpleComponents/FancyTextBox.razor
@@ -0,0 +1,35 @@
+@inject IJSRuntime JsRuntime
+@if (isVisible)
+{
+ <input autofocus @bind="Value" @onfocusout="() => { isVisible = false; ValueChanged.InvokeAsync(Value); }" @ref="elementToFocus"/>
+}
+else
+{
+ <span tabindex="0" style="border-bottom: #ccc solid 1px; height: 1.4em; display: inline-block; @(string.IsNullOrEmpty(Value) ? "min-width: 50px;" : "")" @onfocusin="() => isVisible = true">@(Formatter?.Invoke(Value) ?? (IsPassword ? string.Join("", Value.Select(x=>'*')) : Value))</span>
+}
+
+@code {
+
+ [Parameter]
+ public string Value { get; set; }
+
+ [Parameter]
+ public bool IsPassword { get; set; } = false;
+
+ [Parameter]
+ public EventCallback<string> ValueChanged { get; set; }
+
+ [Parameter]
+ public Func<string?, string>? Formatter { get; set; }
+
+
+ private bool isVisible { get; set; } = false;
+
+ private ElementReference elementToFocus;
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await JsRuntime.InvokeVoidAsync("BlazorFocusElement", elementToFocus);
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/SimpleComponents/StringListEditor.razor b/MatrixRoomUtils.Web/Shared/SimpleComponents/StringListEditor.razor
new file mode 100644
index 0000000..fe3a938
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/SimpleComponents/StringListEditor.razor
@@ -0,0 +1,31 @@
+@for (int i = 0; i < Items.Count; i++)
+{
+ var self = i;
+ <button @onclick="() => { Items.RemoveAt(self); ItemsChanged.InvokeAsync(); }">Remove</button>
+ <FancyTextBox Value="@Items[self]" ValueChanged="@(obj => inputChanged(obj, self))"/>
+ <br/>
+}
+<button @onclick="() => { Items.Add(string.Empty); ItemsChanged.InvokeAsync(); }">Add</button>
+
+@code {
+
+ [Parameter]
+ public List<string> Items { get; set; } = new List<string>();
+
+ [Parameter, EditorRequired]
+ public EventCallback ItemsChanged { get; set; }
+
+ protected override Task OnInitializedAsync()
+ {
+ Console.WriteLine($"StringListEditor initialized with {Items.Count} items: {string.Join(",", Items)}");
+ return base.OnInitializedAsync();
+ }
+
+ private void inputChanged(string obj, int i)
+ {
+ Console.WriteLine($"StringListEditor inputChanged {i} {obj}");
+ Items[i] = obj;
+ ItemsChanged.InvokeAsync();
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Web/Shared/SimpleComponents/ToggleSlider.razor b/MatrixRoomUtils.Web/Shared/SimpleComponents/ToggleSlider.razor
new file mode 100644
index 0000000..49a363d
--- /dev/null
+++ b/MatrixRoomUtils.Web/Shared/SimpleComponents/ToggleSlider.razor
@@ -0,0 +1,70 @@
+<input type="checkbox"/><span>@ChildContent</span>
+
+<div class="container">
+ <label class="switch" for="checkbox">
+ <input type="checkbox" id="checkbox" @bind="Value"/>
+ <div class="slider round"></div>
+ </label>
+</div>
+
+<style>
+ .switch {
+ display: inline-block;
+ height: 16px;
+ position: relative;
+ width: 32px;
+ }
+
+ .switch input {
+ display:none;
+ }
+
+ .slider {
+ background-color: #ccc;
+ bottom: 0;
+ cursor: pointer;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ transition: .4s;
+ }
+
+ .slider:before {
+ background-color: #fff;
+ bottom: -5px;
+ content: "";
+ height: 26px;
+ left: -8px;
+ position: absolute;
+ transition: .4s;
+ width: 26px;
+ }
+
+ input:checked + .slider {
+ background-color: #66bb6a;
+ }
+
+ input:checked + .slider:before {
+ transform: translateX(24px);
+ }
+
+ .slider.round {
+ border-radius: 24px;
+ }
+
+ .slider.round:before {
+ border-radius: 50%;
+ }
+</style>
+
+@code {
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ [Parameter]
+ public bool Value { get; set; }
+ [Parameter]
+ public EventCallback<bool> ValueChanged { get; set; }
+
+}
\ No newline at end of file
|