Dreamine.Web 1.0.0.0
WPF와 Blazor를 한 코드 흐름으로 연결하고 반복적인 MVVM 코드를 줄이는 오픈소스 FullKit 공식 웹 애플리케이션입니다.
로딩중...
검색중...
일치하는것 없음
JsonPlaygroundStore.cs
이 파일의 문서화 페이지로 가기
1using System.IO;
2using System.Text.Json;
4
6
16{
25 private readonly string _path;
34 private List<PlaygroundDemo>? _cache;
43 private static readonly SemaphoreSlim _lock = new(1, 1);
44
53 private static readonly JsonSerializerOptions _json = new()
54 {
55 WriteIndented = true,
56 PropertyNamingPolicy = JsonNamingPolicy.CamelCase
57 };
58
76 {
77 var dir = opts.ResolvedDataPath;
78 Directory.CreateDirectory(dir);
79 _path = Path.Combine(dir, "playground.json");
80 }
81
98 public async Task<List<PlaygroundDemo>> GetAllAsync()
99 {
100 if (_cache is not null) return _cache;
101
102 await _lock.WaitAsync();
103 try
104 {
105 if (_cache is not null) return _cache;
106
107 if (!File.Exists(_path))
108 {
110 await PersistAsync(_cache);
111 return _cache;
112 }
113
114 var json = await File.ReadAllTextAsync(_path);
115 _cache = JsonSerializer.Deserialize<List<PlaygroundDemo>>(json, _json) ?? [];
116
117 var seed = SeedDefaults();
118
119 // Add seed items that are not present in JSON yet.
120 var missing = seed.Where(s => !_cache.Any(c => c.Id == s.Id)).ToList();
121 var changed = false;
122 if (missing.Count > 0)
123 {
124 _cache.AddRange(missing);
125 changed = true;
126 }
127
128 changed |= RepairSeedCodeFields(_cache, seed);
129 if (changed) await PersistAsync(_cache);
130
131 return _cache;
132 }
133 finally { _lock.Release(); }
134 }
135
160 public async Task<PlaygroundDemo?> GetAsync(string id)
161 {
162 var all = await GetAllAsync();
163 return all.FirstOrDefault(x => x.Id == id);
164 }
165
190 public async Task SaveAsync(PlaygroundDemo demo)
191 {
192 await _lock.WaitAsync();
193 try
194 {
195 var all = _cache ?? [];
196 var idx = all.FindIndex(x => x.Id == demo.Id);
197 demo.UpdatedAt = DateTime.UtcNow;
198 if (idx >= 0) all[idx] = demo;
199 else all.Add(demo);
200 _cache = all;
201 await PersistAsync(all);
202 }
203 finally { _lock.Release(); }
204 }
205
230 public async Task DeleteAsync(string id)
231 {
232 await _lock.WaitAsync();
233 try
234 {
235 var all = _cache ?? [];
236 all.RemoveAll(x => x.Id == id);
237 _cache = all;
238 await PersistAsync(all);
239 }
240 finally { _lock.Release(); }
241 }
242
267 private async Task PersistAsync(List<PlaygroundDemo> list)
268 {
269 var json = JsonSerializer.Serialize(list, _json);
270 await File.WriteAllTextAsync(_path, json);
271 }
272
305 private static bool RepairSeedCodeFields(List<PlaygroundDemo> demos, List<PlaygroundDemo> seed)
306 {
307 var changed = false;
308
309 foreach (var demo in demos)
310 {
311 var seedDemo = seed.FirstOrDefault(x => x.Id == demo.Id);
312 if (seedDemo is null) continue; // \uAD00\uB9AC\uC790\uAC00 \uC0C8\uB85C \uCD94\uAC00\uD55C \uB370\uBAA8\uB294 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uB294\uB2E4.
313
314 // \uCF54\uB4DC \uC2A4\uB2C8\uD3AB(View/ViewModel)\uC740 "\uC2E4\uC81C \uC2E4\uD589 \uCF54\uB4DC\uC758 \uBB38\uC11C"\uB2E4.
315 // \uCCB4\uD5D8\uC5D0 \uBCF4\uC774\uB294 \uCF54\uB4DC\uAC00 \uC2E4\uC81C \uCEF4\uD3EC\uB10C\uD2B8 \uC0AC\uC6A9\uBC95\uACFC \uC5B4\uAE0B\uB098\uBA74 \uC548 \uB418\uBBC0\uB85C
316 // \uD56D\uC0C1 \uC2DC\uB4DC\uB97C \uC815\uB2F5\uC73C\uB85C \uB3D9\uAE30\uD654\uD55C\uB2E4(\uAD00\uB9AC\uC790 \uD3B8\uC9D1 \uB300\uC0C1\uC774 \uC544\uB2C8\uB2E4).
317 changed |= SyncCodeField(seedDemo.BlazorCode, demo.BlazorCode, v => demo.BlazorCode = v);
318 changed |= SyncCodeField(seedDemo.VmCode, demo.VmCode, v => demo.VmCode = v);
319 changed |= SyncCodeField(seedDemo.WpfCode, demo.WpfCode, v => demo.WpfCode = v);
320 changed |= SyncCodeField(seedDemo.WinFormsCode, demo.WinFormsCode, v => demo.WinFormsCode = v);
321 changed |= SyncCodeField(seedDemo.MauiCode, demo.MauiCode, v => demo.MauiCode = v);
322
323 // \uC601\uBB38 \uC81C\uBAA9/\uC124\uBA85\uC740 \uBE44\uC5B4 \uC788\uC744 \uB54C\uB9CC \uCC44\uC6B4\uB2E4(\uC81C\uBAA9\u00B7\uC124\uBA85\uC740 \uAD00\uB9AC\uC790 \uD3B8\uC9D1 \uBCF4\uC874).
324 if (string.IsNullOrWhiteSpace(demo.TitleEn) && !string.IsNullOrWhiteSpace(seedDemo.TitleEn))
325 {
326 demo.TitleEn = seedDemo.TitleEn;
327 changed = true;
328 }
329
330 if (string.IsNullOrWhiteSpace(demo.DescriptionEn) && !string.IsNullOrWhiteSpace(seedDemo.DescriptionEn))
331 {
332 demo.DescriptionEn = seedDemo.DescriptionEn;
333 changed = true;
334 }
335 }
336
337 return changed;
338 }
339
380 private static bool SyncCodeField(string seedValue, string current, Action<string> apply)
381 {
382 if (!string.IsNullOrWhiteSpace(seedValue) && !string.Equals(seedValue, current, StringComparison.Ordinal))
383 {
384 apply(seedValue);
385 return true;
386 }
387 return false;
388 }
389
390 // ══════════════════════════════════════════════════════════
391 // Seed data - currently hard-coded control demos
392 // ══════════════════════════════════════════════════════════
409 private static List<PlaygroundDemo> SeedDefaults() =>
410 [
411 new()
412 {
413 Id = "bulb", SortOrder = 5, NavLabel = "Light Bulb",
414 Title = "전구 — 버튼 & 체크박스",
415 TitleEn = "Light Bulb — Button & Checkbox",
416 Description = "버튼(커맨드)과 체크박스(양방향 바인딩)가 같은 상태를 조작합니다. 로직은 Event, 상태는 Model, 노출은 ViewModel — Dreamine의 3분할 그대로입니다.",
417 DescriptionEn = "A button (command) and a checkbox (two-way binding) drive the same state. Logic in Event, state in Model, exposure in ViewModel.",
418 WpfShot = "/img/demos/lightbulb-wpf.svg", WinFormsShot = "/img/demos/lightbulb-winforms.svg", MauiShot = "/img/demos/lightbulb-maui.svg",
419 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
420<DreamineLightBulb IsOn=""@Bulb.IsOn"" Diameter=""120"" />
421<DreamineButton Variant=""DreamineButtonVariant.Primary"" OnClick=""Bulb.Toggle"">Toggle</DreamineButton>
422<DreamineCheckBox @bind-Checked=""Bulb.IsOn"">Power</DreamineCheckBox>
423<span>@Bulb.StatusText · toggled @Bulb.ToggleCount times</span>",
424 VmCode = @"public sealed class LightBulbModel { public bool IsOn; public int ToggleCount; }
425
426public sealed class LightBulbEvent
427{
428 private readonly LightBulbModel _m;
429 public LightBulbEvent(LightBulbModel m) => _m = m;
430 public void Toggle() { _m.IsOn = !_m.IsOn; _m.ToggleCount++; }
431}
432
433public sealed class LightBulbViewModel : ViewModelBase
434{
435 public bool IsOn { get; set; }
436 public int ToggleCount { get; }
437 public string StatusText => IsOn ? ""ON"" : ""OFF"";
438}",
439 WpfCode = @"<!-- WPF (XAML) -->
440<StackPanel>
441 <ctrl:DreamineLightBulb IsOn=""{Binding IsOn}"" Diameter=""96"" />
442 <CheckBox Content=""Power"" IsChecked=""{Binding IsOn}"" />
443 <Button Content=""Toggle"" Command=""{Binding ToggleCommand}"" />
444 <TextBlock Text=""{Binding ToggleCount, StringFormat=Toggled {0} times}"" />
445</StackPanel>",
446 WinFormsCode = @"var bulb = new DreamineLightBulb { Diameter = 96 };
447
448toggleButton.Click += (_, _) => vm.ToggleCommand.Execute(null);
449checkPower.DataBindings.Add(""Checked"", vm, nameof(vm.IsOn),
450 false, DataSourceUpdateMode.OnPropertyChanged);
451
452vm.PropertyChanged += (_, _) => bulb.IsOn = vm.IsOn;
453bulb.IsOn = vm.IsOn;",
454 MauiCode = @"<dc:DreamineLightBulb IsOn=""{Binding IsOn}"" Diameter=""112"" />
455<CheckBox IsChecked=""{Binding IsOn}"" />
456<Button Text=""Toggle"" Command=""{Binding ToggleCommand}"" />
457<Label Text=""{Binding ToggleCount, StringFormat='Toggled {0} times'}"" />"
458 },
459 new()
460 {
461 Id = "button", SortOrder = 10, NavLabel = "Button",
462 Title = "Button — DreamineButton",
463 Description = "커맨드에 바인딩된 버튼. 클릭할 때마다 Event가 카운트를 올리고 로그를 남깁니다.",
464 DescriptionEn = "A command-bound button. Each click increments the count and appends a log via the Event.",
465 WpfShot = "/img/demos/button-wpf.svg", WinFormsShot = "/img/demos/button-winforms.svg", MauiShot = "/img/demos/button-maui.svg",
466 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
467<DreamineButton Variant=""DreamineButtonVariant.Primary"" OnClick=""Vm.ClickMe"">Primary</DreamineButton>
468<DreamineButton OnClick=""Vm.ClickMe"">Secondary</DreamineButton>
469<DreamineButton Variant=""DreamineButtonVariant.Danger"" OnClick=""Vm.ClickMe"">Danger</DreamineButton>
470<span>Clicks @Vm.ClickCount</span>
471@foreach (var line in Vm.ActivityLog) { <li>@line</li> }",
472 VmCode = @"// Shared ViewModel (Dreamine Source Generator)
473[DreamineProperty] private int _clickCount;
474public ObservableCollection<string> ActivityLog { get; } = new();
475
476[DreamineCommand]
477private void ClickMe()
478{
479 ClickCount++;
480 ActivityLog.Insert(0, $""[{DateTime.Now:HH:mm:ss}] Clicked ({ClickCount})"");
481}",
482 WpfCode = @"<!-- xmlns:ctrl=""...Dreamine.UI.Wpf.Controls"" -->
483<ctrl:DreamineButton Content=""Primary"" Background=""#1E90FF"" Foreground=""White""
484 Width=""100"" Height=""34""
485 Command=""{Binding ClickMeCommand}"" />
486<ctrl:DreamineButton Content=""Blue Glow"" ShineColor=""#1E90FF""
487 Command=""{Binding ClickMeCommand}"" />
488<TextBlock Text=""{Binding ClickCount, StringFormat='Click count: {0}'}"" />",
489 WinFormsCode = @"var btnPrimary = new DreamineButton {
490 Text = ""Primary"", Width = 100, Height = 34,
491 BackColor = Color.FromArgb(0x1E, 0x90, 0xFF),
492 ForeColor = Color.White
493};
494btnPrimary.Click += (_, _) => vm.ClickMeCommand.Execute(null);
495
496var lblCount = new Label();
497vm.PropertyChanged += (_, _) =>
498 lblCount.Text = $""Click count: {vm.ClickCount}"";",
499 MauiCode = @"<!-- .NET MAUI -->
500<Button Text=""Primary"" BackgroundColor=""#1E90FF"" TextColor=""White""
501 Command=""{Binding ClickMeCommand}"" />
502<Button Text=""Secondary"" Command=""{Binding ClickMeCommand}"" />
503<Label Text=""{Binding ClickCount, StringFormat='Click count: {0}'}"" />"
504 },
505 new()
506 {
507 Id = "checkbox", SortOrder = 20, NavLabel = "CheckBox",
508 Title = "CheckBox — DreamineCheckBox",
509 Description = "IsChecked 양방향 바인딩. 마지막 항목은 IsEnabled=False로 비활성 상태입니다.",
510 DescriptionEn = "IsChecked two-way binding. The last item is disabled (IsEnabled=False).",
511 WpfShot = "/img/demos/checkbox-wpf.svg", WinFormsShot = "/img/demos/checkbox-winforms.svg", MauiShot = "/img/demos/checkbox-maui.svg",
512 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
513<DreamineCheckBox @bind-Checked=""Vm.Check1"">Check 1</DreamineCheckBox>
514<DreamineCheckBox @bind-Checked=""Vm.Check2"">Check 2</DreamineCheckBox>
515<DreamineCheckBox @bind-Checked=""Vm.Check3"" Disabled=""true"">Check 3</DreamineCheckBox>
516<p>Check1 = @Vm.Check1 · Check2 = @Vm.Check2</p>",
517 VmCode = @"[DreamineProperty] private bool _check1 = true;
518[DreamineProperty] private bool _check2;
519[DreamineProperty] private bool _check3;",
520 WpfCode = @"<ctrl:DreamineCheckBox Content=""Check 1 (checked by default)"" IsChecked=""{Binding Check1}"" />
521<ctrl:DreamineCheckBox Content=""Check 2"" IsChecked=""{Binding Check2}"" />
522<ctrl:DreamineCheckBox Content=""Check 3 (disabled)"" IsChecked=""{Binding Check3}"" IsEnabled=""False"" />",
523 WinFormsCode = @"var chk1 = new DreamineCheckBox { Text = ""Check 1"" };
524chk1.DataBindings.Add(""Checked"", vm, nameof(vm.Check1),
525 false, DataSourceUpdateMode.OnPropertyChanged);
526
527var chk2 = new DreamineCheckBox { Text = ""Check 2"" };
528chk2.DataBindings.Add(""Checked"", vm, nameof(vm.Check2),
529 false, DataSourceUpdateMode.OnPropertyChanged);
530
531var chk3 = new DreamineCheckBox { Text = ""Check 3 (disabled)"", Enabled = false };
532chk3.DataBindings.Add(""Checked"", vm, nameof(vm.Check3));",
533 MauiCode = @"<ctrl:DreamineCheckBox Text=""Check 1"" IsChecked=""{Binding Check1}"" />
534<ctrl:DreamineCheckBox Text=""Check 2"" IsChecked=""{Binding Check2}"" />
535<ctrl:DreamineCheckBox Text=""Check 3 (disabled)"" IsChecked=""{Binding Check3}""
536 IsEnabled=""False"" />"
537 },
538 new()
539 {
540 Id = "radio", SortOrder = 30, NavLabel = "RadioButton",
541 Title = "RadioButton — DreamineRadioButton",
542 Description = "같은 GroupName으로 묶인 라디오. 선택 시 RelayCommand<string>로 값을 전달합니다.",
543 DescriptionEn = "Radios sharing a GroupName; selection passes the value via RelayCommand<string>.",
544 WpfShot = "/img/demos/radio-wpf.svg", WinFormsShot = "/img/demos/radio-winforms.svg", MauiShot = "/img/demos/radio-maui.svg",
545 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
546@foreach (var opt in new[] { ""Option A"", ""Option B"", ""Option C"" })
547{
548 <DreamineRadioButton Name=""grp"" Value=""@opt""
549 SelectedValue=""@Vm.SelectedRadio""
550 SelectedValueChanged=""Vm.SelectRadio"">@opt</DreamineRadioButton>
551}
552<p>Selected: @Vm.SelectedRadio</p>",
553 VmCode = @"[DreamineProperty] private string _selectedRadio = ""Option A"";
554
555[DreamineCommand]
556private void SelectRadio(string option) => SelectedRadio = option;",
557 WpfCode = @"<ctrl:DreamineRadioButton Content=""Option A"" GroupName=""demo"" IsChecked=""True""
558 ctrl:DreamineRadioButton.Command=""{Binding SelectRadioCommand}""
559 ctrl:DreamineRadioButton.CommandParameter=""Option A""
560 ctrl:DreamineRadioButton.CommandTriggerName=""Click"" />
561<ctrl:DreamineRadioButton Content=""Option B"" GroupName=""demo""
562 ctrl:DreamineRadioButton.Command=""{Binding SelectRadioCommand}""
563 ctrl:DreamineRadioButton.CommandParameter=""Option B"" ... />
564<TextBlock Text=""{Binding SelectedRadio, StringFormat='Selected: {0}'}"" />",
565 WinFormsCode = @"var radA = new DreamineRadioButton { Text = ""Option A"", Checked = true };
566var radB = new DreamineRadioButton { Text = ""Option B"" };
567var radC = new DreamineRadioButton { Text = ""Option C"" };
568
569radA.CheckedChanged += (_, _) => { if (radA.Checked) vm.SelectedRadio = ""Option A""; };
570radB.CheckedChanged += (_, _) => { if (radB.Checked) vm.SelectedRadio = ""Option B""; };
571radC.CheckedChanged += (_, _) => { if (radC.Checked) vm.SelectedRadio = ""Option C""; };",
572 MauiCode = @"<RadioButton Content=""Option A"" Value=""Option A""
573 GroupName=""demo"" IsChecked=""True"" />
574<RadioButton Content=""Option B"" Value=""Option B""
575 GroupName=""demo"" />
576<RadioButton Content=""Option C"" Value=""Option C""
577 GroupName=""demo"" />
578<Label Text=""{Binding SelectedRadio, StringFormat='Selected: {0}'}"" />"
579 },
580 new()
581 {
582 Id = "checkled", SortOrder = 40, NavLabel = "CheckLed",
583 Title = "CheckLed — DreamineCheckLed",
584 Description = "Dreamine 시그니처 LED 인디케이터. IsOn / IsPulse 를 바인딩합니다 (장비 상태 표시용).",
585 DescriptionEn = "Dreamine's signature LED indicator. Bind IsOn / IsPulse (great for equipment status).",
586 WpfShot = "/img/demos/checkled-wpf.svg", WinFormsShot = "/img/demos/checkled-winforms.svg", MauiShot = "/img/demos/checkled-maui.svg",
587 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
588<DreamineCheckLed IsOn=""Vm.LedIsOn"" IsPulse=""Vm.LedIsPulse"" Diameter=""34"" />
589<DreamineButton OnClick=""Vm.ToggleLed"">ON / OFF</DreamineButton>
590<DreamineButton OnClick=""Vm.TogglePulse"">Pulse</DreamineButton>
591<p>IsOn = @Vm.LedIsOn · IsPulse = @Vm.LedIsPulse</p>",
592 VmCode = @"[DreamineProperty] private bool _ledIsOn = true;
593[DreamineProperty] private bool _ledIsPulse;
594
595[DreamineCommand] private void ToggleLed() => LedIsOn = !LedIsOn;
596[DreamineCommand] private void TogglePulse() => LedIsPulse = !LedIsPulse;",
597 WpfCode = @"<ctrl:DreamineCheckLed IsOn=""{Binding LedIsOn}""
598 IsPulse=""{Binding LedIsPulse}""
599 Width=""36"" Height=""36"" />
600<ctrl:DreamineButton Content=""ON / OFF"" Command=""{Binding ToggleLedCommand}"" />
601<ctrl:DreamineButton Content=""Pulse"" Command=""{Binding TogglePulseCommand}"" />",
602 WinFormsCode = @"var led = new DreamineCheckLed { Width = 36, Height = 36 };
603led.DataBindings.Add(""IsOn"", vm, nameof(vm.LedIsOn));
604led.DataBindings.Add(""IsPulse"", vm, nameof(vm.LedIsPulse));
605
606var btnToggle = new DreamineButton { Text = ""ON / OFF"" };
607btnToggle.Click += (_, _) => vm.ToggleLedCommand.Execute(null);
608
609var btnPulse = new DreamineButton { Text = ""Pulse"" };
610btnPulse.Click += (_, _) => vm.TogglePulseCommand.Execute(null);",
611 MauiCode = @"<ctrl:DreamineCheckLed IsOn=""{Binding LedIsOn}""
612 IsPulse=""{Binding LedIsPulse}""
613 WidthRequest=""36"" HeightRequest=""36"" />
614<Button Text=""ON / OFF"" Command=""{Binding ToggleLedCommand}"" />
615<Button Text=""Pulse"" Command=""{Binding TogglePulseCommand}"" />"
616 },
617 new()
618 {
619 Id = "textbox", SortOrder = 50, NavLabel = "TextBox",
620 Title = "TextBox / PasswordBox — DreamineTextBox",
621 Description = "Hint(placeholder) 지원 입력 컨트롤. Clear 커맨드로 값을 비웁니다.",
622 DescriptionEn = "Input controls with Hint (placeholder). Clear via command.",
623 WpfShot = "/img/demos/textbox-wpf.svg", WinFormsShot = "/img/demos/textbox-winforms.svg", MauiShot = "/img/demos/textbox-maui.svg",
624 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
625<DreamineTextBox @bind-Value=""Vm.TextInput"" Hint=""Type here...""
626 OnFocus=""() => _showKeyboard = true"" />
627<DreamineButton Size=""DreamineButtonSize.Small"" OnClick=""Vm.ClearText"">Clear</DreamineButton>
628<p>Value: @Vm.TextInput</p>
629
630@* 가상 키보드는 대상 텍스트박스 바로 아래에 표시 — 시선이 튀지 않게. *@
631@if (_showKeyboard)
632{
633 <DreamineVirtualKeyboard IsVisible=""_showKeyboard""
634 Value=""@Vm.TextInput""
635 ValueChanged=""v => Vm.TextInput = v""
636 OnEnter=""() => _showKeyboard = false"" />
637}
638
639<DreamineTextBox @bind-Value=""Vm.Password"" IsPassword=""true"" Hint=""Password..."" />
640<DreamineButton Size=""DreamineButtonSize.Small"" OnClick=""Vm.ClearPassword"">Clear</DreamineButton>
641<p>Length: @Vm.Password.Length</p>
642
643@code { private bool _showKeyboard; }",
644 VmCode = @"[DreamineProperty] private string _textInput = string.Empty;
645[DreamineProperty] private string _password = string.Empty;
646
647[DreamineCommand] private void ClearText() => TextInput = string.Empty;
648[DreamineCommand] private void ClearPassword() => Password = string.Empty;",
649 WpfCode = @"<ctrl:DreamineTextBox
650 Text=""{Binding TextInput, UpdateSourceTrigger=PropertyChanged}""
651 Hint=""Type text here..."" Height=""40"" />
652<ctrl:DreamineButton Content=""Clear"" Command=""{Binding ClearTextCommand}"" />
653
654<ctrl:DreaminePasswordBox
655 Password=""{Binding Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}""
656 Hint=""Enter password..."" Height=""40"" />
657<TextBlock Text=""{Binding Password.Length, StringFormat='Length: {0} chars'}"" />",
658 WinFormsCode = @"var txt = new DreamineTextBox { Hint = ""Type here..."" };
659txt.DataBindings.Add(""Text"", vm, nameof(vm.TextInput),
660 false, DataSourceUpdateMode.OnPropertyChanged);
661
662var pwd = new DreaminePasswordBox { Hint = ""Password..."" };
663pwd.DataBindings.Add(""Password"", vm, nameof(vm.Password),
664 false, DataSourceUpdateMode.OnPropertyChanged);
665
666var btnClear = new DreamineButton { Text = ""Clear"" };
667btnClear.Click += (_, _) => vm.ClearTextCommand.Execute(null);",
668 MauiCode = @"<Entry Placeholder=""Type here...""
669 Text=""{Binding TextInput}"" />
670<Entry Placeholder=""Password..."" IsPassword=""True""
671 Text=""{Binding Password}"" />
672<Button Text=""Clear"" Command=""{Binding ClearTextCommand}"" />"
673 },
674 new()
675 {
676 Id = "combobox", SortOrder = 60, NavLabel = "ComboBox",
677 Title = "ComboBox — DreamineComboBox",
678 Description = "ItemsSource / SelectedItem 바인딩.",
679 DescriptionEn = "ItemsSource / SelectedItem binding.",
680 WpfShot = "/img/demos/combobox-wpf.svg", WinFormsShot = "/img/demos/combobox-winforms.svg", MauiShot = "/img/demos/combobox-maui.svg",
681 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
682<DreamineComboBox Items=""Vm.FruitItems"" @bind-SelectedItem=""Vm.SelectedFruit"" />
683<span>Selected: @Vm.SelectedFruit</span>",
684 VmCode = @"public string[] FruitItems { get; } =
685 { ""Apple"", ""Banana"", ""Cherry"", ""Grape"", ""Mango"", ""Melon"" };
686
687[DreamineProperty] private string _selectedFruit = ""Cherry"";",
688 WpfCode = @"<ctrl:DreamineComboBox
689 ItemsSource=""{Binding FruitItems}""
690 SelectedItem=""{Binding SelectedFruit}""
691 Width=""200"" Height=""36"" />
692<TextBlock Text=""{Binding SelectedFruit, StringFormat='Selected item: {0}'}"" />",
693 WinFormsCode = @"var cmb = new DreamineComboBox();
694cmb.DataSource = vm.FruitItems;
695cmb.DataBindings.Add(""SelectedItem"", vm, nameof(vm.SelectedFruit),
696 false, DataSourceUpdateMode.OnPropertyChanged);
697
698vm.PropertyChanged += (_, _) =>
699 lblSelected.Text = $""Selected: {vm.SelectedFruit}"";",
700 MauiCode = @"<Picker Title=""Select fruit""
701 ItemsSource=""{Binding FruitItems}""
702 SelectedItem=""{Binding SelectedFruit}"" />
703<Label Text=""{Binding SelectedFruit, StringFormat='Selected: {0}'}"" />"
704 },
705 new()
706 {
707 Id = "expander", SortOrder = 70, NavLabel = "Expander",
708 Title = "Expander — DreamineExpander",
709 Description = "IsExpanded를 VM에 바인딩해 코드에서도 펼침/접힘을 제어합니다.",
710 DescriptionEn = "Bind IsExpanded to the VM to control expand/collapse from code too.",
711 WpfShot = "/img/demos/expander-wpf.svg", WinFormsShot = "/img/demos/expander-winforms.svg", MauiShot = "/img/demos/expander-maui.svg",
712 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
713<DreamineExpander Header=""Expand / Collapse"" @bind-IsExpanded=""Vm.IsExpanded"">
714 Any content can go here. IsExpanded = @Vm.IsExpanded
715</DreamineExpander>",
716 VmCode = @"[DreamineProperty] private bool _isExpanded = true;
717
718[DreamineCommand] private void ToggleExpand() => IsExpanded = !IsExpanded;",
719 WpfCode = @"<ctrl:DreamineExpander Header=""Expand / Collapse""
720 IsExpanded=""{Binding IsExpanded}"">
721 <StackPanel Margin=""16,8"">
722 <TextBlock Text=""Any content can go here."" />
723 </StackPanel>
724</ctrl:DreamineExpander>",
725 WinFormsCode = @"var exp = new DreamineExpander { HeaderText = ""Expand / Collapse"" };
726exp.DataBindings.Add(""IsExpanded"", vm, nameof(vm.IsExpanded),
727 false, DataSourceUpdateMode.OnPropertyChanged);
728
729exp.ContentPanel.Controls.Add(new Label {
730 Text = ""Any content can go here."",
731 Dock = DockStyle.Fill
732});",
733 MauiCode = @"<ctrl:DreamineExpander Header=""Expand / Collapse""
734 IsExpanded=""{Binding IsExpanded}"">
735 <Label Text=""Any content can go here.""
736 Margin=""16,8"" />
737</ctrl:DreamineExpander>"
738 },
739 new()
740 {
741 Id = "numeric", SortOrder = 80, NavLabel = "Numeric",
742 Title = "Numeric — NumericRangeBehavior",
743 Description = "0~100 범위로 자동 보정(Clamp)되는 숫자 입력. 슬라이더/숫자 어느 쪽으로 바꿔도 값이 동기화됩니다.",
744 DescriptionEn = "Numeric input clamped to 0–100. Slider and number stay in sync.",
745 WpfShot = "/img/demos/numeric-wpf.svg", WinFormsShot = "/img/demos/numeric-winforms.svg", MauiShot = "",
746 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
747<DreamineNumericSlider @bind-Value=""Vm.NumericInput"" Min=""0"" Max=""100"" />
748<span>Value: @Vm.NumericInput</span>",
749 VmCode = @"private double _numericInput = 50;
750public double NumericInput
751{
752 get => _numericInput;
753 set { _numericInput = Math.Clamp(value, 0, 100); OnPropertyChanged(); }
754}",
755 WpfCode = @"<!-- behaviors:...Dreamine.UI.Wpf.Behaviors -->
756<TextBox Text=""{Binding NumericInput, UpdateSourceTrigger=PropertyChanged}""
757 behaviors:NumericRangeBehavior.IsEnabled=""True""
758 behaviors:NumericRangeBehavior.Min=""0""
759 behaviors:NumericRangeBehavior.Max=""100""
760 behaviors:NumericRangeBehavior.Mode=""Clamp"" Width=""120"" />
761<!-- Values outside the range are clamped to 0-100 automatically. -->",
762 WinFormsCode = @"// WinForms uses the standard NumericUpDown with manual clamping.
763var nud = new NumericUpDown {
764 Minimum = 0, Maximum = 100, Value = 50,
765 DecimalPlaces = 0
766};
767nud.DataBindings.Add(""Value"", vm, nameof(vm.NumericInput),
768 false, DataSourceUpdateMode.OnPropertyChanged);",
769 MauiCode = @"<Slider Minimum=""0"" Maximum=""100""
770 Value=""{Binding NumericInput}"" />
771<Entry Keyboard=""Numeric""
772 Text=""{Binding NumericInput}"" />
773<Label Text=""{Binding NumericInput, StringFormat='Value: {0:F0}'}"" />"
774 },
775 new()
776 {
777 Id = "listbox", SortOrder = 90, NavLabel = "ListBox",
778 Title = "ListBox — DreamineListBox",
779 Description = "선택(클릭) + 더블클릭 시 커맨드 실행(활성화).",
780 DescriptionEn = "Select (click) + run a command on double-click (activation).",
781 WpfShot = "/img/demos/listbox-wpf.svg", WinFormsShot = "/img/demos/listbox-winforms.svg", MauiShot = "/img/demos/listbox-maui.svg",
782 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
783<DreamineListBox Items=""Vm.FruitItems""
784 @bind-SelectedItem=""Vm.SelectedFruit""
785 OnActivate=""Vm.ActivateListItem"" />
786<p>Selected: @Vm.SelectedFruit · @Vm.ListActivated</p>",
787 VmCode = @"public string[] FruitItems { get; } =
788 { ""Apple"", ""Banana"", ""Cherry"", ""Grape"", ""Mango"", ""Melon"" };
789
790[DreamineProperty] private string _selectedFruit = ""Cherry"";
791[DreamineProperty] private string _listActivated = ""(double-click an item)"";
792
793[DreamineCommand]
794private void ActivateListItem(string item) => ListActivated = $""Activated: {item}"";",
795 WpfCode = @"<ctrl:DreamineListBox
796 ItemsSource=""{Binding FruitItems}""
797 SelectedItem=""{Binding SelectedFruit}""
798 ctrl:DreamineListBox.Command=""{Binding ListBoxActivatedCommand}""
799 ctrl:DreamineListBox.CommandParameter=""{Binding SelectedFruit}""
800 ctrl:DreamineListBox.CommandTriggerName=""MouseDoubleClick"" />",
801 WinFormsCode = @"var lst = new DreamineListBox();
802lst.DataSource = vm.FruitItems;
803lst.DataBindings.Add(""SelectedItem"", vm, nameof(vm.SelectedFruit),
804 false, DataSourceUpdateMode.OnPropertyChanged);
805
806lst.DoubleClick += (_, _) =>
807 vm.ListBoxActivatedCommand.Execute(vm.SelectedFruit);",
808 MauiCode = @"<CollectionView ItemsSource=""{Binding FruitItems}""
809 SelectionMode=""Single""
810 SelectedItem=""{Binding SelectedFruit}"">
811 <CollectionView.ItemTemplate>
812 <DataTemplate>
813 <Label Text=""{Binding .}"" Padding=""12,8"" />
814 </DataTemplate>
815 </CollectionView.ItemTemplate>
816</CollectionView>"
817 },
818 new()
819 {
820 Id = "counter", SortOrder = 100, NavLabel = "Counter",
821 Title = "Counter — Increment / Reset",
822 Description = "SampleCrossUi의 카운터 데모. Increment는 서비스를 거쳐 값을 올리고, Reset은 0으로 초기화합니다. 로그가 자동으로 쌓입니다.",
823 DescriptionEn = "The SampleCrossUi counter demo. Increment goes through a service, Reset zeroes the count, and logs accumulate automatically.",
824 WpfShot = "/img/demos/counter-wpf.svg", WinFormsShot = "/img/demos/counter-winforms.svg", MauiShot = "/img/demos/counter-maui.svg",
825 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
826<span class=""dw-counter-value"">@Vm.Count</span>
827<DreamineButton Variant=""DreamineButtonVariant.Primary"" OnClick=""Vm.Increment"">Increment</DreamineButton>
828<DreamineButton OnClick=""Vm.ResetCounter"">Reset</DreamineButton>
829@foreach (var line in Vm.CounterLogs) { <li>@line</li> }",
830 VmCode = @"[DreamineProperty] private int _count;
831public ObservableCollection<string> CounterLogs { get; } = new();
832
833[DreamineCommand]
834private void Increment()
835{
836 Count++;
837 CounterLogs.Insert(0, $""[{DateTime.Now:HH:mm:ss}] Incremented → {Count}"");
838}
839
840[DreamineCommand]
841private void ResetCounter()
842{
843 Count = 0;
844 CounterLogs.Insert(0, $""[{DateTime.Now:HH:mm:ss}] Counter reset."");
845}",
846 WpfCode = @"<!-- CounterView.xaml — SampleCrossUi.Wpf -->
847<ctrl:DreamineLabel Content=""{Binding Count, StringFormat='Count: {0}'}""
848 FontSize=""52"" FontWeight=""Bold"" />
849
850<ctrl:DreamineButton Content=""Increment""
851 Command=""{Binding IncrementCommand}"" Width=""130"" Height=""40"" />
852<ctrl:DreamineButton Content=""Reset""
853 Command=""{Binding ResetCommand}"" Width=""100"" Height=""40"" />
854
855<ListBox ItemsSource=""{Binding Logs}""
856 behaviors:AutoScrollListBoxBehavior.IsEnabled=""True"" />",
857 WinFormsCode = @"// CounterPage — SampleCrossUi.WinForms
858var lblCount = new Label { Font = new Font(""Segoe UI"", 52, FontStyle.Bold) };
859vm.PropertyChanged += (_, _) =>
860 lblCount.Text = $""Count: {vm.Count}"";
861
862var btnInc = new DreamineButton { Text = ""Increment"" };
863btnInc.Click += (_, _) => vm.IncrementCommand.Execute(null);
864
865var btnReset = new DreamineButton { Text = ""Reset"" };
866btnReset.Click += (_, _) => vm.ResetCommand.Execute(null);",
867 MauiCode = @"<!-- CounterPage — SampleCrossUi.Maui -->
868<Label Text=""{Binding Count, StringFormat='Count: {0}'}""
869 FontSize=""52"" FontAttributes=""Bold""
870 HorizontalOptions=""Center"" />
871<Button Text=""Increment"" Command=""{Binding IncrementCommand}"" />
872<Button Text=""Reset"" Command=""{Binding ResetCommand}"" />"
873 },
874 new()
875 {
876 Id = "datagrid", SortOrder = 110, NavLabel = "DataGrid",
877 Title = "DataGrid — DreamineDataGrid",
878 Description = "ItemsSource 바인딩 + ClickToDeselect Behavior. 읽기 전용 테이블에 장비 상태를 표시합니다.",
879 DescriptionEn = "ItemsSource binding + ClickToDeselect Behavior. Displays equipment status in a read-only table.",
880 WpfShot = "/img/demos/datagrid-wpf.svg", WinFormsShot = "/img/demos/datagrid-winforms.svg", MauiShot = "/img/demos/datagrid-maui.svg",
881 BlazorCode = @"@* @using Dreamine.UI.Blazor *@
882<DreamineDataGrid TItem=""GridRow"" Items=""Vm.GridRows"" @bind-SelectedItem=""Vm.SelectedRow"">
883 <HeaderContent><th>No</th><th>Name</th><th>Status</th></HeaderContent>
884 <RowContent>
885 <td>@context.No</td><td>@context.Name</td><td>@context.Status</td>
886 </RowContent>
887</DreamineDataGrid>",
888 VmCode = @"public ObservableCollection<GridRow> GridRows { get; } = new()
889{
890 new() { No = 1, Name = ""Device A"", Status = ""Running"" },
891 new() { No = 2, Name = ""Device B"", Status = ""Stopped"" },
892 new() { No = 3, Name = ""Device C"", Status = ""Running"" },
893 new() { No = 4, Name = ""Device D"", Status = ""Error"" },
894};
895
896[DreamineProperty] private GridRow? _selectedRow;",
897 WpfCode = @"<ctrl:DreamineDataGrid
898 ItemsSource=""{Binding GridRows}""
899 AutoGenerateColumns=""False""
900 behaviors:DataGridBehaviors.EnableClickToDeselect=""True""
901 IsReadOnly=""True"">
902 <ctrl:DreamineDataGrid.Columns>
903 <DataGridTextColumn Header=""No"" Binding=""{Binding No}"" Width=""50"" />
904 <DataGridTextColumn Header=""Name"" Binding=""{Binding Name}"" Width=""150"" />
905 <DataGridTextColumn Header=""Status"" Binding=""{Binding Status}"" Width=""120"" />
906 </ctrl:DreamineDataGrid.Columns>
907</ctrl:DreamineDataGrid>",
908 WinFormsCode = @"var dgv = new DreamineDataGrid {
909 AutoGenerateColumns = false, ReadOnly = true
910};
911dgv.Columns.Add(new DataGridViewTextBoxColumn {
912 HeaderText = ""No"", DataPropertyName = ""No"", Width = 50 });
913dgv.Columns.Add(new DataGridViewTextBoxColumn {
914 HeaderText = ""Name"", DataPropertyName = ""Name"", Width = 150 });
915dgv.Columns.Add(new DataGridViewTextBoxColumn {
916 HeaderText = ""Status"", DataPropertyName = ""Status"", Width = 120 });
917
918dgv.DataSource = new BindingSource { DataSource = vm.GridRows };",
919 MauiCode = @"<!-- MAUI - table-like layout with CollectionView -->
920<CollectionView ItemsSource=""{Binding GridRows}""
921 SelectionMode=""Single""
922 SelectedItem=""{Binding SelectedRow}"">
923 <CollectionView.ItemTemplate>
924 <DataTemplate>
925 <Grid ColumnDefinitions=""50,*,120"" Padding=""8,4"">
926 <Label Grid.Column=""0"" Text=""{Binding No}"" />
927 <Label Grid.Column=""1"" Text=""{Binding Name}"" />
928 <Label Grid.Column=""2"" Text=""{Binding Status}"" />
929 </Grid>
930 </DataTemplate>
931 </CollectionView.ItemTemplate>
932</CollectionView>"
933 },
934 new()
935 {
936 Id = "timespinner", SortOrder = 120, NavLabel = "TimeSpinner",
937 Title = "TimeSpinner — DreamineTimeSpinner",
938 Description = "시:분:초를 ▲▼ 버튼으로 조절합니다. WPF에서는 스크롤 휠과 키보드도 지원합니다.",
939 DescriptionEn = "Adjust hours, minutes, seconds with ▲▼ buttons. WPF also supports scroll wheel and keyboard.",
940 WpfShot = "/img/demos/timespinner-wpf.svg", WinFormsShot = "", MauiShot = "/img/demos/timespinner-maui.svg",
941 BlazorCode = @"<div class=""dw-timespinner"">
942 <div>
943 <button @onclick=""@(() => Vm.Hours++)"">▲</button>
944 <input type=""number"" min=""0"" max=""23"" @bind=""Vm.Hours"" />
945 <button @onclick=""@(() => Vm.Hours--)"">▼</button>
946 </div>
947 <span>:</span>
948 <!-- Minutes and seconds use the same structure. -->
949</div>
950<p>Time: @Vm.TimeDisplay</p>",
951 VmCode = @"[DreamineProperty] private int _hours = 9;
952[DreamineProperty] private int _minutes = 30;
953[DreamineProperty] private int _seconds;
954
955public string TimeDisplay => $""{Hours:D2}:{Minutes:D2}:{Seconds:D2}"";",
956 WpfCode = @"<ctrl:DreamineTimeSpinner
957 Time=""{Binding Time, Mode=TwoWay}""
958 Width=""190"" Height=""40""
959 Foreground=""White"" Background=""#FF162040"" />
960<TextBlock Text=""{Binding Time, StringFormat='Selected time: {0:hh\\:mm\\:ss}'}"" />",
961 WinFormsCode = @"var picker = new DateTimePicker {
962 Format = DateTimePickerFormat.Custom,
963 CustomFormat = ""HH:mm:ss"",
964 ShowUpDown = true
965};
966picker.DataBindings.Add(""Value"", vm, nameof(vm.Time),
967 false, DataSourceUpdateMode.OnPropertyChanged);",
968 MauiCode = @"<!-- MAUI - TimePicker -->
969<TimePicker Time=""{Binding Time}""
970 Format=""HH:mm:ss"" />
971<Label Text=""{Binding Time, StringFormat='Selected time: {0:hh\\:mm\\:ss}'}"" />"
972 },
973 new()
974 {
975 Id = "image", SortOrder = 130, NavLabel = "Image",
976 Title = "Image — DreamineImage",
977 Description = "CornerRadius로 둥근 모서리, ClickCommand로 클릭 이벤트를 바인딩합니다. FallbackSource로 로드 실패 시 대체 이미지도 지정 가능합니다.",
978 DescriptionEn = "CornerRadius for rounded corners, ClickCommand for click events. FallbackSource provides a fallback on load failure.",
979 WpfShot = "/img/demos/image-wpf.svg", WinFormsShot = "", MauiShot = "/img/demos/image-maui.svg",
980 BlazorCode = @"<div class=""dw-image-demo"" @onclick=""Vm.ClickImage"">
981 <svg viewBox=""0 0 100 100"" width=""80"" height=""80"">
982 <circle cx=""50"" cy=""50"" r=""50"" fill=""#1E90FF""/>
983 <circle cx=""50"" cy=""50"" r=""22"" fill=""#fff""/>
984 </svg>
985</div>
986<span>Image clicks @Vm.ImageClickCount</span>",
987 VmCode = @"[DreamineProperty] private int _imageClickCount;
988
989[DreamineCommand]
990private void ClickImage() => ImageClickCount++;",
991 WpfCode = @"<ctrl:DreamineImage
992 Source=""{StaticResource DemoDrawingImage}""
993 CornerRadius=""12""
994 Width=""64"" Height=""64""
995 ClickCommand=""{Binding ClickMeCommand}"" />
996<!-- FallbackSource can provide a replacement image when loading fails. -->",
997 WinFormsCode = @"var image = new PictureBox {
998 ImageLocation = ""demo_icon.png"",
999 SizeMode = PictureBoxSizeMode.Zoom,
1000 Width = 64,
1001 Height = 64
1002};
1003image.Click += (_, _) => vm.ClickMeCommand.Execute(null);",
1004 MauiCode = @"<Image Source=""demo_icon.png""
1005 WidthRequest=""64"" HeightRequest=""64"">
1006 <Image.GestureRecognizers>
1007 <TapGestureRecognizer Command=""{Binding ClickMeCommand}"" />
1008 </Image.GestureRecognizers>
1009</Image>"
1010 },
1011 new()
1012 {
1013 Id = "popup", SortOrder = 140, NavLabel = "Popup",
1014 Title = "Popup — MessageBox & BlinkPopup",
1015 Description = "메시지 박스와 점멸(Blink) 팝업. 설비 알람처럼 색이 번갈아 깜빡이는 모달을 띄우고, 자동 닫힘(카운트다운)도 지원합니다. 결과는 await로 받습니다.",
1016 DescriptionEn = "Message box and blinking popup. Show an alarm-style modal whose colors alternate, with optional auto-close countdown. The result is awaited.",
1017 WpfShot = "", WinFormsShot = "", MauiShot = "",
1018 BlazorCode = @"@* @using Dreamine.UI.Blazor
1019 @inject DreamineDialogService Dialog
1020 페이지 어딘가에 <DreamineDialogHost /> 를 한 번 배치해야 합니다. *@
1021<DreamineButton OnClick=""ShowMessageBox"">DreamineMessageBox (5s)</DreamineButton>
1022<DreamineButton Variant=""DreamineButtonVariant.Danger"" OnClick=""ShowAlarm"">ALARM (blink)</DreamineButton>
1023
1024@code {
1025 private async Task ShowMessageBox()
1026 {
1027 var r = await Dialog.ShowMessageBoxAsync(
1028 ""Message box demo."", ""MessageBox"", autoClickDelaySeconds: 5);
1029 }
1030
1031 private async Task ShowAlarm()
1032 {
1033 var r = await Dialog.ShowBlinkAsync(new BlinkPopupOptions
1034 {
1035 Title = ""⚠ ALARM"", Message = ""Equipment fault detected."",
1036 UseBlink = true, BlinkIntervalMs = 400,
1037 Color1 = ""#B41E1E"", Color2 = ""#500A0A"",
1038 ForegroundColor = ""#FFD700"", OkText = ""OK"", CancelText = ""Cancel""
1039 });
1040 }
1041}",
1042 VmCode = @"// DreamineDialogService는 DI에 Scoped로 등록합니다.
1043// builder.Services.AddScoped<DreamineDialogService>();
1044// WPF의 IPopupService / WinForms·MAUI의 DreamineMessageBox·DreamineBlinkPopup과
1045// 같은 역할을 하며, Blazor에서는 <DreamineDialogHost /> 오버레이가 실제로 그립니다.
1046DreamineDialogResult result = await Dialog.ShowMessageBoxAsync(message, title);",
1047 WpfCode = @"// WPF — Dreamine.UI.Abstractions.Popup.IPopupService
1048var result = _popup.ShowMessageBox(
1049 ""Message box demo."", ""MessageBox"",
1050 autoClickDelaySeconds: 5);
1051
1052_popup.ShowBlink(new BlinkPopupOptions
1053{
1054 Title = ""⚠ ALARM"", Message = ""Equipment fault detected."",
1055 UseBlink = true, BlinkIntervalMs = 400,
1056 Color1 = ""#B41E1E"", Color2 = ""#500A0A"",
1057 ForegroundColor = ""#FFD700""
1058});",
1059 WinFormsCode = @"// WinForms — DreamineMessageBox / DreamineBlinkPopup
1060var result = DreamineMessageBox.Show(
1061 ""Message box demo."", ""MessageBox"",
1062 autoClickDelaySeconds: 5);
1063
1064DreamineBlinkPopup.Show(new BlinkPopupOptions
1065{
1066 Title = ""⚠ ALARM"", Message = ""Equipment fault detected."",
1067 UseBlink = true, BlinkIntervalMs = 400,
1068 Color1 = Color.FromArgb(0xB4, 0x1E, 0x1E),
1069 Color2 = Color.FromArgb(0x50, 0x0A, 0x0A)
1070});",
1071 MauiCode = @"<!-- MAUI — same DreamineMessageBox / DreamineBlinkPopup API -->
1072var result = await DreamineMessageBox.ShowAsync(
1073 ""Message box demo."", ""MessageBox"",
1074 autoClickDelaySeconds: 5);
1075
1076await DreamineBlinkPopup.ShowAsync(new BlinkPopupOptions
1077{
1078 Title = ""⚠ ALARM"", Message = ""Equipment fault detected."",
1079 UseBlink = true, BlinkIntervalMs = 400,
1080 Color1 = ""#B41E1E"", Color2 = ""#500A0A""
1081});"
1082 },
1083 ];
1084}
async Task< List< PlaygroundDemo > > GetAllAsync()
async Task SaveAsync(PlaygroundDemo demo)
static bool SyncCodeField(string seedValue, string current, Action< string > apply)
static bool RepairSeedCodeFields(List< PlaygroundDemo > demos, List< PlaygroundDemo > seed)
async Task< PlaygroundDemo?> GetAsync(string id)
async Task PersistAsync(List< PlaygroundDemo > list)
static List< PlaygroundDemo > SeedDefaults()
static readonly JsonSerializerOptions _json