@cahyo-dimas/freeday 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.id.md +18 -8
  3. package/README.md +18 -8
  4. package/adapters/blazor/CflTypes.cs +11 -0
  5. package/adapters/blazor/FdyAutocomplete.razor +19 -0
  6. package/adapters/blazor/FdyAutocomplete.razor.cs +40 -0
  7. package/adapters/blazor/FdyCascade.razor +33 -0
  8. package/adapters/blazor/FdyCascade.razor.cs +38 -0
  9. package/adapters/blazor/FdyCascadeNode.cs +8 -0
  10. package/adapters/blazor/FdyCfl.razor +93 -0
  11. package/adapters/blazor/FdyCfl.razor.cs +146 -0
  12. package/adapters/blazor/FdyChart.razor +25 -0
  13. package/adapters/blazor/FdyChart.razor.cs +90 -0
  14. package/adapters/blazor/FdyChartSeries.cs +10 -0
  15. package/adapters/blazor/FdyCombo.razor +28 -0
  16. package/adapters/blazor/FdyCombo.razor.cs +86 -0
  17. package/adapters/blazor/FdyComboOption.cs +7 -0
  18. package/adapters/blazor/FdyDateRange.razor +16 -0
  19. package/adapters/blazor/FdyDateRange.razor.cs +58 -0
  20. package/adapters/blazor/FdyDatepicker.razor +13 -0
  21. package/adapters/blazor/FdyDatepicker.razor.cs +39 -0
  22. package/adapters/blazor/FdyDrawer.razor +36 -0
  23. package/adapters/blazor/FdyDrawer.razor.cs +88 -0
  24. package/adapters/blazor/FdyModal.razor +36 -0
  25. package/adapters/blazor/FdyModal.razor.cs +88 -0
  26. package/adapters/blazor/FdyTable.razor +140 -0
  27. package/adapters/blazor/FdyTable.razor.cs +200 -0
  28. package/adapters/blazor/FdyTableFilter.razor +77 -0
  29. package/adapters/blazor/FdyTableFilter.razor.cs +117 -0
  30. package/adapters/blazor/Freeday.Blazor.csproj +27 -0
  31. package/adapters/blazor/FreedayComponentBase.cs +73 -0
  32. package/adapters/blazor/TableModel.cs +196 -0
  33. package/adapters/blazor/TableTypes.cs +69 -0
  34. package/adapters/blazor/_Imports.razor +5 -0
  35. package/adapters/blazor/freeday-blazor.js +101 -3
  36. package/dist/freeday-select.js +32 -8
  37. package/dist/freeday-toast.js +22 -1
  38. package/dist/freeday.bundle.css +27 -0
  39. package/dist/freeday.css +27 -0
  40. package/dist/freeday.d.ts +5 -0
  41. package/dist/freeday.js +54 -9
  42. package/package.json +1 -1
  43. package/src/base.css +5 -0
  44. package/src/components/avatar.css +13 -0
  45. package/src/components/card.css +4 -0
  46. package/src/components/skeleton.css +5 -0
  47. package/src/freeday-select.js +32 -8
  48. package/src/freeday-toast.js +22 -1
@@ -0,0 +1,90 @@
1
+ using System.Globalization;
2
+ using System.Text.Json;
3
+ using Microsoft.AspNetCore.Components;
4
+ using Microsoft.JSInterop;
5
+
6
+ namespace Freeday.Blazor;
7
+
8
+ // Not a FreedayComponentBase: a chart has no fdy-* events and no <dialog>, and must repaint on
9
+ // EVERY data change (not hydrate-once), so it drives its own render lifecycle directly.
10
+ public partial class FdyChart : ComponentBase
11
+ {
12
+ [Inject] private IJSRuntime JS { get; set; } = default!;
13
+
14
+ /// <summary>Chart kind: <c>line</c> | <c>area</c> | <c>bar</c> | <c>sparkline</c> | <c>donut</c>.</summary>
15
+ [Parameter, EditorRequired] public string Type { get; set; } = default!;
16
+
17
+ /// <summary>Multi-series data (line/area/bar). Takes precedence over <see cref="Values"/>.</summary>
18
+ [Parameter] public IReadOnlyList<FdyChartSeries>? Series { get; set; }
19
+
20
+ /// <summary>Single-series shortcut (sparkline/donut/simple bar).</summary>
21
+ [Parameter] public IReadOnlyList<double>? Values { get; set; }
22
+
23
+ /// <summary>Category axis labels.</summary>
24
+ [Parameter] public IReadOnlyList<string>? Labels { get; set; }
25
+
26
+ /// <summary>Value format: <c>number</c> | <c>percent</c> | <c>currency</c>.</summary>
27
+ [Parameter] public string? Format { get; set; }
28
+
29
+ /// <summary>Stack the series (bar/area). Switches a bar chart into cartesian layout.</summary>
30
+ [Parameter] public bool Stacked { get; set; }
31
+
32
+ /// <summary>Legend visibility: <c>auto</c> | <c>always</c> | <c>none</c>.</summary>
33
+ [Parameter] public string? Legend { get; set; }
34
+
35
+ /// <summary>Per-series colour override — semantic token names (primary/accent/success/…) or
36
+ /// categorical slots <c>chart-1</c>..<c>chart-8</c>. Omit for the default chart palette.</summary>
37
+ [Parameter] public IReadOnlyList<string>? Colors { get; set; }
38
+
39
+ /// <summary>Single-colour override for a one-series chart.</summary>
40
+ [Parameter] public string? Color { get; set; }
41
+
42
+ /// <summary>Donut centre label.</summary>
43
+ [Parameter] public string? Center { get; set; }
44
+
45
+ [Parameter] public string? AriaLabel { get; set; }
46
+
47
+ /// <summary>Fallback content shown until the renderer paints (e.g. before dist/freeday.js loads).</summary>
48
+ [Parameter] public RenderFragment? ChildContent { get; set; }
49
+
50
+ /// <summary>The chart root; Blazor owns its attributes, the renderer paints its SVG children.</summary>
51
+ protected ElementReference Root;
52
+
53
+ private static readonly JsonSerializerOptions SeriesJson = new() { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull };
54
+
55
+ // Cartesian (axes) for line/area, or a bar with multiple series / stacking; those get their
56
+ // layout class from the renderer. sparkline/donut/simple-bar need their layout class up front.
57
+ private bool IsCartesian =>
58
+ Type == "line" || Type == "area" || (Type == "bar" && (Series is not null || Stacked));
59
+
60
+ private string RootClass => Type switch
61
+ {
62
+ "sparkline" => "fdy-sparkline",
63
+ "donut" => "fdy-donut",
64
+ "bar" when !IsCartesian => "fdy-bars",
65
+ _ => string.Empty,
66
+ };
67
+
68
+ // null → Blazor omits the attribute entirely (the renderer then falls back correctly).
69
+ private string? SeriesAttr => Series is null ? null : JsonSerializer.Serialize(Series, SeriesJson);
70
+ private string? ValuesAttr => Values is null ? null : string.Join(",", Values.Select(v => v.ToString(CultureInfo.InvariantCulture)));
71
+ private string? LabelsAttr => Labels is null ? null : string.Join(",", Labels);
72
+ private string? ColorsAttr => Colors is null ? null : string.Join(",", Colors);
73
+ private string? StackedAttr => Stacked ? string.Empty : null; // presence attribute
74
+
75
+ // Signature of every data-affecting attribute — the C# analogue of the Vue/React watch list.
76
+ private string DataSignature => string.Join("", Type, SeriesAttr, ValuesAttr, LabelsAttr,
77
+ Format, StackedAttr, Legend, ColorsAttr, Color, Center);
78
+
79
+ private string? _lastSignature;
80
+
81
+ // Repaint only when the chart's own data changed (first render, or a data-* attribute differs
82
+ // since the last paint) — so an unrelated parent re-render doesn't needlessly repaint the SVG.
83
+ protected override async Task OnAfterRenderAsync(bool firstRender)
84
+ {
85
+ string sig = DataSignature;
86
+ if (!firstRender && sig == _lastSignature) return;
87
+ _lastSignature = sig;
88
+ await JS.InvokeVoidAsync("FreedayBlazor.chartUpdate", Root);
89
+ }
90
+ }
@@ -0,0 +1,10 @@
1
+ using System.Text.Json.Serialization;
2
+
3
+ namespace Freeday.Blazor;
4
+
5
+ /// <summary>One data series in a multi-series chart (line/area/bar). Serialised to the renderer's
6
+ /// <c>data-series</c> JSON shape <c>{ label, values, role? }</c> — hence the camelCase names.</summary>
7
+ public sealed record FdyChartSeries(
8
+ [property: JsonPropertyName("label")] string Label,
9
+ [property: JsonPropertyName("values")] IReadOnlyList<double> Values,
10
+ [property: JsonPropertyName("role")] string? Role = null);
@@ -0,0 +1,28 @@
1
+ @namespace Freeday.Blazor
2
+ @typeparam TValue
3
+ @inherits FreedayComponentBase<FdyCombo<TValue>>
4
+
5
+ @* Typed wrapper over freeday's .fdy-combo (WAI-ARIA select-only combobox). Blazor renders the
6
+ markup the freeday-select.js enhancer expects; the enhancer owns the open/keyboard/focus
7
+ behaviour and emits fdy-change, which flows back to @bind-Value. An externally-changed Value
8
+ is pushed onto the enhancer silently (no event echo). *@
9
+ <div @ref="Root" class="@RootClass" data-fdy-combo data-value="@SelectedKey">
10
+ <button id="@ButtonId" type="button" class="fdy-combo__button" role="combobox"
11
+ aria-haspopup="listbox" aria-expanded="false"
12
+ aria-labelledby="@AriaLabelledby"
13
+ aria-invalid="@(Invalid ? "true" : null)"
14
+ aria-readonly="@(Readonly ? "true" : null)"
15
+ disabled="@Disabled">
16
+ <span class="fdy-combo__value@(SelectedOption is null ? " fdy-combo__value--placeholder" : "")">@SelectedLabel</span>
17
+ </button>
18
+ <ul class="fdy-combo__listbox" role="listbox" aria-labelledby="@AriaLabelledby" hidden>
19
+ @foreach (FdyComboOption<TValue> opt in Options)
20
+ {
21
+ bool selected = Equals(opt.Value, Value);
22
+ <li class="fdy-combo__option" role="option" data-value="@KeyOf(opt.Value)"
23
+ aria-selected="@(selected ? "true" : "false")">
24
+ <span class="fdy-combo__check">@(selected ? "✓" : "")</span>@opt.Label
25
+ </li>
26
+ }
27
+ </ul>
28
+ </div>
@@ -0,0 +1,86 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyCombo<TValue>
7
+ {
8
+ /// <summary>The selected value. Two-way bindable via <c>@bind-Value</c>.</summary>
9
+ [Parameter, EditorRequired] public TValue Value { get; set; } = default!;
10
+ [Parameter] public EventCallback<TValue> ValueChanged { get; set; }
11
+
12
+ /// <summary>The options to choose from.</summary>
13
+ [Parameter, EditorRequired] public IReadOnlyList<FdyComboOption<TValue>> Options { get; set; }
14
+ = Array.Empty<FdyComboOption<TValue>>();
15
+
16
+ /// <summary>Shown when no option matches the current value.</summary>
17
+ [Parameter] public string? Placeholder { get; set; }
18
+
19
+ /// <summary>Id of the element labelling the combobox (for accessibility).</summary>
20
+ [Parameter] public string? AriaLabelledby { get; set; }
21
+
22
+ /// <summary>Explicit id for the combobox button (else an auto id is used).</summary>
23
+ [Parameter] public string? Id { get; set; }
24
+
25
+ [Parameter] public bool Disabled { get; set; }
26
+
27
+ /// <summary>Locked/view mode: stays focusable and shows its value but can't be opened or changed.</summary>
28
+ [Parameter] public bool Readonly { get; set; }
29
+
30
+ [Parameter] public bool Invalid { get; set; }
31
+
32
+ private readonly string _autoId = $"fdy-combo-{Guid.NewGuid():N}";
33
+ private TValue _lastValue = default!;
34
+ private bool _ready;
35
+
36
+ // The enhancer owns this combo's DOM after hydration (open/close, aria-selected, the button
37
+ // label). Re-rendering here would fight it — e.g. reconciling the listbox mid-close leaves it
38
+ // stuck open. Render once for the initial markup, then never again; push external Value changes
39
+ // through the enhancer via comboSetValue instead.
40
+ protected override bool ShouldRender() => !Hydrated;
41
+
42
+ private string ButtonId => Id ?? _autoId;
43
+ private string RootClass => Invalid ? "fdy-combo fdy-combo--error" : "fdy-combo";
44
+ private FdyComboOption<TValue>? SelectedOption
45
+ => Options.FirstOrDefault(o => EqualityComparer<TValue>.Default.Equals(o.Value, Value));
46
+ private string SelectedLabel => SelectedOption?.Label ?? Placeholder ?? string.Empty;
47
+ private string SelectedKey => KeyOf(Value);
48
+
49
+ private static string KeyOf(TValue value) => value?.ToString() ?? string.Empty;
50
+
51
+ // The enhancer owns this combo's DOM; init it directly (initAll would skip Root itself).
52
+ protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayCombo.init", Root);
53
+
54
+ protected override async ValueTask OnHydratedAsync()
55
+ {
56
+ _lastValue = Value;
57
+ await SubscribeAsync("fdy-change", nameof(OnFdyChange));
58
+ _ready = true;
59
+ }
60
+
61
+ protected override async Task OnParametersSetAsync()
62
+ {
63
+ // Push an externally-changed Value onto the enhancer-owned DOM (silent — no fdy-change echo).
64
+ if (_ready && !EqualityComparer<TValue>.Default.Equals(Value, _lastValue))
65
+ {
66
+ _lastValue = Value;
67
+ await JS.InvokeVoidAsync("FreedayBlazor.comboSetValue", Root, KeyOf(Value));
68
+ }
69
+ }
70
+
71
+ /// <summary>Invoked by the bridge when the user picks an option (enhancer's fdy-change).</summary>
72
+ [JSInvokable]
73
+ public async Task OnFdyChange(ChangeDetail detail)
74
+ {
75
+ FdyComboOption<TValue>? match = Options.FirstOrDefault(o => KeyOf(o.Value) == detail.Value);
76
+ if (match is not null)
77
+ {
78
+ _lastValue = match.Value;
79
+ Value = match.Value;
80
+ await ValueChanged.InvokeAsync(match.Value);
81
+ }
82
+ }
83
+
84
+ /// <summary>The detail shape of the enhancer's <c>fdy-change</c> event.</summary>
85
+ public sealed record ChangeDetail(string Value);
86
+ }
@@ -0,0 +1,7 @@
1
+ namespace Freeday.Blazor;
2
+
3
+ /// <summary>One selectable option for <see cref="FdyCombo{TValue}"/>.</summary>
4
+ /// <typeparam name="TValue">The bound value type (string, an enum, an int, …).</typeparam>
5
+ /// <param name="Value">The value bound through <c>@bind-Value</c> when this option is picked.</param>
6
+ /// <param name="Label">The visible text.</param>
7
+ public sealed record FdyComboOption<TValue>(TValue Value, string Label);
@@ -0,0 +1,16 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyDateRange>
3
+
4
+ @* Two linked seed datepickers (freeday-datepicker.js data-fdy-daterange: the end can't precede
5
+ the start). The daterange sits INSIDE Root so initAll(Root) finds and links it as a descendant.
6
+ Both children emit fdy-datepicker-change; the bridge's dateRangeOn tags each with its data-role
7
+ so From/To bind separately. Enhancer-owned DOM → render once (ShouldRender => false). *@
8
+ <div @ref="Root">
9
+ <div data-fdy-daterange role="group" aria-label="@AriaLabel">
10
+ <div data-fdy-datepicker data-role="from"
11
+ data-value="@From" data-placeholder="@FromPlaceholder" data-min="@Min" data-max="@Max"></div>
12
+ <span class="fdy-daterange__sep">@Separator</span>
13
+ <div data-fdy-datepicker data-role="to"
14
+ data-value="@To" data-placeholder="@ToPlaceholder" data-min="@Min" data-max="@Max"></div>
15
+ </div>
16
+ </div>
@@ -0,0 +1,58 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyDateRange
7
+ {
8
+ /// <summary>Range start (ISO <c>yyyy-MM-dd</c> or ""). Two-way bindable via <c>@bind-From</c>.</summary>
9
+ [Parameter] public string? From { get; set; }
10
+ [Parameter] public EventCallback<string?> FromChanged { get; set; }
11
+
12
+ /// <summary>Range end (ISO <c>yyyy-MM-dd</c> or ""). Two-way bindable via <c>@bind-To</c>.</summary>
13
+ [Parameter] public string? To { get; set; }
14
+ [Parameter] public EventCallback<string?> ToChanged { get; set; }
15
+
16
+ [Parameter] public string? AriaLabel { get; set; }
17
+ [Parameter] public string? FromPlaceholder { get; set; }
18
+ [Parameter] public string? ToPlaceholder { get; set; }
19
+ [Parameter] public string? Min { get; set; }
20
+ [Parameter] public string? Max { get; set; }
21
+ [Parameter] public string Separator { get; set; } = "–";
22
+
23
+ private int _token;
24
+
25
+ // initAll(Root) links the daterange (descendant) and inits both child pickers.
26
+ protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayDatepicker.initAll", Root);
27
+
28
+ protected override async ValueTask OnHydratedAsync()
29
+ => _token = await JS.InvokeAsync<int>("FreedayBlazor.dateRangeOn", Root, Self, nameof(OnChange));
30
+
31
+ protected override bool ShouldRender() => !Hydrated;
32
+
33
+ /// <summary>Invoked by the bridge for either child's change; routes by data-role.</summary>
34
+ [JSInvokable]
35
+ public async Task OnChange(RangeDetail detail)
36
+ {
37
+ if (detail.Role == "from")
38
+ {
39
+ From = detail.Value;
40
+ await FromChanged.InvokeAsync(detail.Value);
41
+ }
42
+ else if (detail.Role == "to")
43
+ {
44
+ To = detail.Value;
45
+ await ToChanged.InvokeAsync(detail.Value);
46
+ }
47
+ }
48
+
49
+ protected override async ValueTask CleanupAsync()
50
+ {
51
+ if (_token != 0)
52
+ {
53
+ await JS.InvokeVoidAsync("FreedayBlazor.off", _token);
54
+ }
55
+ }
56
+
57
+ public sealed record RangeDetail(string? Value, string? Role);
58
+ }
@@ -0,0 +1,13 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyDatepicker>
3
+
4
+ @* Seed-based wrapper over freeday-datepicker.js: the enhancer builds the input-styled
5
+ trigger + calendar popover from this empty div and emits fdy-datepicker-change. The
6
+ enhancer owns that DOM, so we render the seed once (ShouldRender => false) and read the
7
+ picked value back through @bind-Value. Value is an ISO date string ("yyyy-MM-dd") or "". *@
8
+ <div @ref="Root" data-fdy-datepicker
9
+ data-value="@Value"
10
+ data-label="@Label"
11
+ data-placeholder="@Placeholder"
12
+ data-min="@Min"
13
+ data-max="@Max"></div>
@@ -0,0 +1,39 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyDatepicker
7
+ {
8
+ /// <summary>Selected date as an ISO string (<c>yyyy-MM-dd</c>), or "" when unset.
9
+ /// Two-way bindable via <c>@bind-Value</c>.</summary>
10
+ [Parameter] public string? Value { get; set; }
11
+ [Parameter] public EventCallback<string?> ValueChanged { get; set; }
12
+
13
+ /// <summary>Accessible label for the trigger.</summary>
14
+ [Parameter] public string? Label { get; set; }
15
+ [Parameter] public string? Placeholder { get; set; }
16
+
17
+ /// <summary>Earliest selectable date (ISO <c>yyyy-MM-dd</c>); days before it are disabled.</summary>
18
+ [Parameter] public string? Min { get; set; }
19
+ [Parameter] public string? Max { get; set; }
20
+
21
+ protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayDatepicker.init", Root);
22
+
23
+ protected override async ValueTask OnHydratedAsync()
24
+ => await SubscribeAsync("fdy-datepicker-change", nameof(OnChange));
25
+
26
+ // The enhancer owns the built trigger + calendar; never let Blazor re-render the seed.
27
+ protected override bool ShouldRender() => !Hydrated;
28
+
29
+ /// <summary>Invoked by the bridge when the user picks (or clears) a date.</summary>
30
+ [JSInvokable]
31
+ public async Task OnChange(ChangeDetail detail)
32
+ {
33
+ Value = detail.Value;
34
+ await ValueChanged.InvokeAsync(detail.Value);
35
+ }
36
+
37
+ /// <summary>The detail shape of the enhancer's <c>fdy-datepicker-change</c> event.</summary>
38
+ public sealed record ChangeDetail(string? Value, string? Date);
39
+ }
@@ -0,0 +1,36 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyDrawer>
3
+
4
+ @* Controlled wrapper over freeday's .fdy-drawer native <dialog> side panel. Same contract and
5
+ bridge as FdyModal (open/close + Esc/backdrop route through freeday-blazor.js) applied to a
6
+ panel that anchors left (default) or right. Native <dialog> gives the focus trap, focus
7
+ restore, top-layer stacking and inert background for free. *@
8
+ <dialog @ref="Root" class="@DrawerClass" aria-labelledby="@_titleId">
9
+ <div class="fdy-drawer__header">
10
+ <h3 id="@_titleId" class="fdy-drawer__title">
11
+ @if (TitleContent is not null)
12
+ {
13
+ @TitleContent
14
+ }
15
+ else
16
+ {
17
+ @Title
18
+ }
19
+ </h3>
20
+ @if (Dismissible)
21
+ {
22
+ <button class="fdy-drawer__close" type="button" aria-label="@CloseLabel" @onclick="DismissAsync">&times;</button>
23
+ }
24
+ </div>
25
+
26
+ <div class="fdy-drawer__body">
27
+ @ChildContent
28
+ </div>
29
+
30
+ @if (FooterContent is not null)
31
+ {
32
+ <div class="fdy-drawer__footer">
33
+ @FooterContent
34
+ </div>
35
+ }
36
+ </dialog>
@@ -0,0 +1,88 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyDrawer
7
+ {
8
+ /// <summary>Whether the drawer is open. Two-way bindable via <c>@bind-Open</c>.</summary>
9
+ [Parameter] public bool Open { get; set; }
10
+ [Parameter] public EventCallback<bool> OpenChanged { get; set; }
11
+
12
+ /// <summary>Plain-text title; ignored when <see cref="TitleContent"/> is supplied.</summary>
13
+ [Parameter] public string Title { get; set; } = string.Empty;
14
+ [Parameter] public RenderFragment? TitleContent { get; set; }
15
+
16
+ /// <summary>The drawer body.</summary>
17
+ [Parameter] public RenderFragment? ChildContent { get; set; }
18
+
19
+ /// <summary>Optional footer (action buttons). Omitted entirely when null.</summary>
20
+ [Parameter] public RenderFragment? FooterContent { get; set; }
21
+
22
+ /// <summary>Which edge the panel anchors to: <c>left</c> (default) or <c>right</c>.</summary>
23
+ [Parameter] public string Side { get; set; } = "left";
24
+
25
+ /// <summary>When true (default), Esc and a backdrop click dismiss the drawer and a close
26
+ /// button is shown. Set false for a panel the user must resolve via an explicit action.</summary>
27
+ [Parameter] public bool Dismissible { get; set; } = true;
28
+
29
+ [Parameter] public string CloseLabel { get; set; } = "Close";
30
+
31
+ /// <summary>Raised after any dismissal (close button, Esc, or backdrop).</summary>
32
+ [Parameter] public EventCallback OnClose { get; set; }
33
+
34
+ private readonly string _titleId = $"fdy-drawer-{Guid.NewGuid():N}-title";
35
+ private int _dialogToken;
36
+ private bool _lastOpen;
37
+
38
+ private string DrawerClass => Side == "right" ? "fdy-drawer fdy-drawer--right" : "fdy-drawer";
39
+
40
+ protected override async ValueTask HydrateAsync()
41
+ {
42
+ _dialogToken = await JS.InvokeAsync<int>(
43
+ "FreedayBlazor.dialogInit", Root, Self, nameof(OnDismiss), Dismissible);
44
+ await SyncAsync();
45
+ }
46
+
47
+ protected override async Task OnParametersSetAsync()
48
+ {
49
+ // Only reconcile once the dialog is wired (token != 0) and the open state actually changed.
50
+ if (_dialogToken != 0 && Open != _lastOpen)
51
+ {
52
+ await SyncAsync();
53
+ }
54
+ }
55
+
56
+ private async ValueTask SyncAsync()
57
+ {
58
+ _lastOpen = Open;
59
+ await JS.InvokeVoidAsync(Open ? "FreedayBlazor.dialogShow" : "FreedayBlazor.dialogClose", Root);
60
+ }
61
+
62
+ /// <summary>Invoked by the bridge when the user dismisses via Esc or a backdrop click.</summary>
63
+ [JSInvokable]
64
+ public Task OnDismiss() => DismissAsync();
65
+
66
+ private async Task DismissAsync()
67
+ {
68
+ if (OpenChanged.HasDelegate)
69
+ {
70
+ await OpenChanged.InvokeAsync(false);
71
+ }
72
+ else
73
+ {
74
+ Open = false;
75
+ _lastOpen = false;
76
+ await JS.InvokeVoidAsync("FreedayBlazor.dialogClose", Root);
77
+ }
78
+ await OnClose.InvokeAsync();
79
+ }
80
+
81
+ protected override async ValueTask CleanupAsync()
82
+ {
83
+ if (_dialogToken != 0)
84
+ {
85
+ await JS.InvokeVoidAsync("FreedayBlazor.dialogDispose", _dialogToken);
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,36 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyModal>
3
+
4
+ @* Controlled wrapper over freeday's .fdy-modal native <dialog>. Blazor can't call
5
+ showModal()/close() from C#, so open/close + Esc/backdrop dismissal route through the
6
+ freeday-blazor.js bridge. Native <dialog> gives the focus trap, focus restore, top-layer
7
+ stacking and inert background for free — this only reconciles it with Blazor state. *@
8
+ <dialog @ref="Root" class="@ModalClass" aria-labelledby="@_titleId">
9
+ <div class="fdy-modal__header">
10
+ <h3 id="@_titleId" class="fdy-modal__title">
11
+ @if (TitleContent is not null)
12
+ {
13
+ @TitleContent
14
+ }
15
+ else
16
+ {
17
+ @Title
18
+ }
19
+ </h3>
20
+ @if (Dismissible)
21
+ {
22
+ <button class="fdy-modal__close" type="button" aria-label="@CloseLabel" @onclick="DismissAsync">&times;</button>
23
+ }
24
+ </div>
25
+
26
+ <div class="fdy-modal__body">
27
+ @ChildContent
28
+ </div>
29
+
30
+ @if (FooterContent is not null)
31
+ {
32
+ <div class="fdy-modal__footer">
33
+ @FooterContent
34
+ </div>
35
+ }
36
+ </dialog>
@@ -0,0 +1,88 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyModal
7
+ {
8
+ /// <summary>Whether the dialog is open. Two-way bindable via <c>@bind-Open</c>.</summary>
9
+ [Parameter] public bool Open { get; set; }
10
+ [Parameter] public EventCallback<bool> OpenChanged { get; set; }
11
+
12
+ /// <summary>Plain-text title; ignored when <see cref="TitleContent"/> is supplied.</summary>
13
+ [Parameter] public string Title { get; set; } = string.Empty;
14
+ [Parameter] public RenderFragment? TitleContent { get; set; }
15
+
16
+ /// <summary>The dialog body.</summary>
17
+ [Parameter] public RenderFragment? ChildContent { get; set; }
18
+
19
+ /// <summary>Optional footer (action buttons). Omitted entirely when null.</summary>
20
+ [Parameter] public RenderFragment? FooterContent { get; set; }
21
+
22
+ /// <summary>Size modifier: <c>sm</c> | <c>md</c> | <c>lg</c> | <c>wide</c>.</summary>
23
+ [Parameter] public string? Size { get; set; }
24
+
25
+ /// <summary>When true (default), Esc and a backdrop click dismiss the dialog and a close
26
+ /// button is shown. Set false for a modal the user must resolve via an explicit action.</summary>
27
+ [Parameter] public bool Dismissible { get; set; } = true;
28
+
29
+ [Parameter] public string CloseLabel { get; set; } = "Close";
30
+
31
+ /// <summary>Raised after any dismissal (close button, Esc, or backdrop).</summary>
32
+ [Parameter] public EventCallback OnClose { get; set; }
33
+
34
+ private readonly string _titleId = $"fdy-modal-{Guid.NewGuid():N}-title";
35
+ private int _dialogToken;
36
+ private bool _lastOpen;
37
+
38
+ private string ModalClass => Size is null ? "fdy-modal" : $"fdy-modal fdy-modal--{Size}";
39
+
40
+ protected override async ValueTask HydrateAsync()
41
+ {
42
+ _dialogToken = await JS.InvokeAsync<int>(
43
+ "FreedayBlazor.dialogInit", Root, Self, nameof(OnDismiss), Dismissible);
44
+ await SyncAsync();
45
+ }
46
+
47
+ protected override async Task OnParametersSetAsync()
48
+ {
49
+ // Only reconcile once the dialog is wired (token != 0) and the open state actually changed.
50
+ if (_dialogToken != 0 && Open != _lastOpen)
51
+ {
52
+ await SyncAsync();
53
+ }
54
+ }
55
+
56
+ private async ValueTask SyncAsync()
57
+ {
58
+ _lastOpen = Open;
59
+ await JS.InvokeVoidAsync(Open ? "FreedayBlazor.dialogShow" : "FreedayBlazor.dialogClose", Root);
60
+ }
61
+
62
+ /// <summary>Invoked by the bridge when the user dismisses via Esc or a backdrop click.</summary>
63
+ [JSInvokable]
64
+ public Task OnDismiss() => DismissAsync();
65
+
66
+ private async Task DismissAsync()
67
+ {
68
+ if (OpenChanged.HasDelegate)
69
+ {
70
+ await OpenChanged.InvokeAsync(false);
71
+ }
72
+ else
73
+ {
74
+ Open = false;
75
+ _lastOpen = false;
76
+ await JS.InvokeVoidAsync("FreedayBlazor.dialogClose", Root);
77
+ }
78
+ await OnClose.InvokeAsync();
79
+ }
80
+
81
+ protected override async ValueTask CleanupAsync()
82
+ {
83
+ if (_dialogToken != 0)
84
+ {
85
+ await JS.InvokeVoidAsync("FreedayBlazor.dialogDispose", _dialogToken);
86
+ }
87
+ }
88
+ }