@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
package/CHANGELOG.md CHANGED
@@ -3,6 +3,58 @@
3
3
  Semua perubahan penting dicatat di sini. Format longgar mengikuti
4
4
  [Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
5
5
 
6
+ ## [1.17.0] — 2026-08-11
7
+ ### Added
8
+ - **Avatar identity tones (`--tone-1`…`--tone-8`).** Decorative tints (from the categorical chart
9
+ palette) with a theme-aware, text-leaning foreground, so same-initial avatars — common where many
10
+ names share a prefix — stay distinguishable. Verified WCAG **AA** (≥4.5:1) in light **and** dark,
11
+ guarded by `test/contrast.test.mjs` (which gained a `color-mix()` evaluator). Hash a stable index
12
+ off the full name, not the initials.
13
+ - **Size-matched skeletons (`.fdy-skeleton--avatar` + `--avatar-sm`/`--avatar-lg`).** Reserve the exact
14
+ `.fdy-avatar` box (2 / 2.5 / 3.5rem) so data landing causes no layout shift (a bare `--circle` has no
15
+ dimensions of its own).
16
+ - **`.fdy-card--button`.** When the whole card *is* the control — a real `<button>` picker row, so it is
17
+ keyboard-reachable and announced — this resets the UA button box **without** stripping the card's own
18
+ surface/border.
19
+ - **`.fdy-list-reset`.** Opt-in list reset for consumers who pair Freeday with a utility framework run
20
+ preflight-off (`base.css` is intentionally a *light* reset — it does not strip `ul`/`ol`/`p` margins).
21
+ - **Toast `key` + `Freeday.dismiss()`.** `toast({ …, key })` replaces an existing same-key toast in
22
+ place instead of stacking a duplicate (a burst of identical failures shows one, refreshed);
23
+ `Freeday.dismiss(node | key)` closes one early. `toast()` already returned the element — both are now
24
+ in the `window.Freeday` type declarations.
25
+ ### Docs
26
+ - State the scope boundary (components + tokens, **not** layout; pair with a utility framework run
27
+ preflight-off) in the README and getting-started; document that `--space-*`/`--radius-*` are public
28
+ tokens to build a utility theme on, and that `data-density` only re-scales control height.
29
+ - Input docs now lead with `aria-invalid="true"` as the canonical accessible-field pattern (it does
30
+ both the visual state and the one screen readers act on); `.fdy-input--error` is the fallback.
31
+
32
+ Additive; no breaking changes. Gate: `node --test` 22/22 (incl. avatar-tone contrast, both themes) ·
33
+ `typecheck:react` 0 · new CSS/JS browser-verified in real Chrome. From real-app consumption feedback.
34
+
35
+ ## [1.16.0] — 2026-08-11
36
+ ### Added
37
+ - **Native Blazor component library (`adapters/blazor/`) — first release, 10/10 parity.** A Razor
38
+ Class Library (`Freeday.Blazor`, net8.0) so Blazor consumers get typed `<FdyX>` components with
39
+ `@bind`, instead of hand-writing `fdy-*` markup + JS interop. Ten components at parity with the
40
+ Vue/React adapters: `FdyModal`, `FdyDrawer`, `FdyCombo<TValue>`, `FdyDatepicker`, `FdyAutocomplete`,
41
+ `FdyCascade`, `FdyDateRange`, `FdyCfl<TRow>`, `FdyChart`, and `FdyTable<TRow>`. Two patterns:
42
+ *wrapper-over-enhancer* (hydrate the vanilla enhancer once, `ShouldRender() => false` so Blazor
43
+ never fights the enhancer-owned DOM, push external value imperatively) and *controlled*
44
+ (`FdyModal`/`FdyDrawer`/`FdyCfl` render their own DOM; only the `<dialog>` show/close/Esc/backdrop
45
+ go through interop). Shared CRTP base `FreedayComponentBase<TSelf>` types the `DotNetObjectReference`
46
+ so `[JSInvokable]` dispatch resolves. Consumed via `<ProjectReference>`; the host loads
47
+ `dist/freeday.js` + `adapters/blazor/freeday-blazor.js` as `<script>`s.
48
+ - **`FdyTable<TRow>`** is a full controlled data table — client mode (sort + four column-filter types
49
+ + pagination over `Rows`) and server mode (set `Page` → the headers/filters/pager only raise
50
+ `SortChanged`/`FiltersChanged`/`PageChanged` intent), plus row activation, expandable detail rows,
51
+ cell templates, and loading/empty states. Its sort/filter/paginate logic is `TableModel` — a pure
52
+ C# port of `adapters/core/table-model.js`.
53
+ - **`freeday-blazor.js` bridge**: `chartUpdate` (repaint a chart from its data-* attributes) and a
54
+ generic `onOutside` light-dismiss primitive (outside-pointer / Esc).
55
+ - **`src/freeday-select.js`** gained an additive `setValue` (silent programmatic select) so a Blazor
56
+ `@bind-Value` can push an external combo value without the enhancer echoing an `fdy-change` back.
57
+
6
58
  ## [1.15.0] — 2026-08-10
7
59
  ### Added
8
60
  - **Type declarations for every export (#40).** Only `./vue` and `./react` shipped a `types`
package/README.id.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **Lebih banyak _free day_ buat dev — UI kit-nya sudah siap pakai.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.15.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.15.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.17.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.17.0)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic — satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -49,6 +49,14 @@ ter-publish → install tanpa build step; minify diserahkan ke bundler konsumen.
49
49
  Kelas komponen berprefix `fdy-` (mis. `fdy-btn`, `fdy-card`, `fdy-badge`). Pakai langsung di
50
50
  markup framework apa pun — Vue, React, Blazor, atau HTML polos.
51
51
 
52
+ > **Scope: komponen + token, bukan layout.** Freeday punya komponen & design token; helper layout
53
+ > cuma `.fdy-hidden` / `.fdy-visually-hidden`. Layout (stack/grid/gap) dari layer-mu sendiri —
54
+ > pasangkan dengan utility framework (Tailwind, UnoCSS…) mode **utilities-only, preflight OFF**
55
+ > (`base.css` Freeday = reset-nya). `base.css` itu reset *ringan* (tak me-reset margin `ul`/`ol`/`p` —
56
+ > pakai `.fdy-list-reset` atau komponen list Freeday), dan skala spacing/radius/durasi adalah custom
57
+ > property publik (`--space-0`…`--space-24`, …) yang bisa jadi dasar theme utility-mu. Detail:
58
+ > [`docs/getting-started.md` §Core concepts](docs/getting-started.md).
59
+
52
60
  > `.fdy-btn` **sudah** tombol primary — tak ada modifier `.fdy-btn--primary` terpisah.
53
61
  > Modifier untuk varian lain: `--ghost`, `--danger`, `--text`, `--sm`, `--lg`, `--icon`
54
62
  > (lihat `docs/index.html`).
@@ -126,17 +134,19 @@ import { useFreeday } from '@cahyo-dimas/freeday/react';
126
134
  const root = useRef<HTMLDivElement>(null);
127
135
  useFreeday(root); // event fdy-* bubbling → listen di root
128
136
  ```
129
- ```csharp
130
- // Blazor — @cahyo-dimas/freeday/blazor (window.FreedayBlazor via JS interop)
131
- await JS.InvokeVoidAsync("FreedayBlazor.initAll", _root);
132
- await JS.InvokeAsync<int>("FreedayBlazor.on", _root, "fdy-cascade-change", _self, nameof(OnCascade));
137
+ ```razor
138
+ @* Blazor — RCL Freeday.Blazor (net8.0): komponen native typed <FdyX> dengan @bind, 10/10 parity *@
139
+ @using Freeday.Blazor
140
+ <FdyCombo TValue="string" @bind-Value="_status" Options="_statusOptions" AriaLabelledby="lbl" />
141
+ <FdyTable TRow="Invoice" Columns="_cols" Rows="_rows" RowKey="@(i => i.Code)" PageSize="10" />
142
+ @* Fallback untuk markup mentah: interop window.FreedayBlazor.initAll / .on *@
133
143
  ```
134
144
 
135
145
  | Framework | Adapter | Contoh jalan |
136
146
  |---|---|---|
137
- | Vue 3 | `@cahyo-dimas/freeday/vue` | [`examples/vue-faktur/`](examples/vue-faktur/) (`npm install && npm run dev`) |
138
- | React 19 | `@cahyo-dimas/freeday/react` | [`examples/react-faktur/`](examples/react-faktur/) (`npm install && npm run dev`) |
139
- | Blazor WASM (.NET 10) | `@cahyo-dimas/freeday/blazor` | [`examples/blazor-faktur/`](examples/blazor-faktur/) (`dotnet run`) |
147
+ | Vue 3 | `@cahyo-dimas/freeday/vue` (`useFreeday` + komponen `v-model`) | [`examples/vue-faktur/`](examples/vue-faktur/) (`npm install && npm run dev`) |
148
+ | React 19 | `@cahyo-dimas/freeday/react` (`useFreeday` + komponen `value`/`onChange`) | [`examples/react-faktur/`](examples/react-faktur/) (`npm install && npm run dev`) |
149
+ | Blazor WASM (net8.0) | RCL `Freeday.Blazor` (`<FdyX>` + `@bind`) · interop `/blazor` | [`examples/blazor-faktur/`](examples/blazor-faktur/) (`dotnet run`) |
140
150
 
141
151
  Peta library & pola lengkap: [`docs/integrations.md`](docs/integrations.md).
142
152
 
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs — the UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.15.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.15.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.17.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.17.0)
9
9
 
10
10
  A token-driven, framework-agnostic UI kit — one source of truth for color, typography,
11
11
  spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -49,6 +49,14 @@ bundler. Because it's on **public npm**, `npm ci` runs in CI without auth or an
49
49
  Component classes are prefixed `fdy-` (e.g. `fdy-btn`, `fdy-card`, `fdy-badge`). Use them directly
50
50
  in any framework's markup — Vue, React, Blazor, or plain HTML.
51
51
 
52
+ > **Scope: components + tokens, not layout.** Freeday owns components and design tokens; the only
53
+ > layout helpers are `.fdy-hidden` / `.fdy-visually-hidden`. Bring your own layout layer — pair it
54
+ > with a utility framework (Tailwind, UnoCSS…) run **utilities-only with preflight OFF** (Freeday's
55
+ > `base.css` is the reset). `base.css` is a *light* reset (it doesn't strip `ul`/`ol`/`p` margins —
56
+ > use `.fdy-list-reset` or a Freeday list component), and the spacing/radius/duration scales are
57
+ > public custom properties (`--space-0`…`--space-24`, …) you can build your utility theme on. Full
58
+ > notes: [`docs/getting-started.md` §Core concepts](docs/getting-started.md).
59
+
52
60
  > `.fdy-btn` is **already** the primary button — there's no separate `.fdy-btn--primary` modifier.
53
61
  > Modifiers for other variants: `--ghost`, `--danger`, `--text`, `--sm`, `--lg`, `--icon`
54
62
  > (see `docs/index.html`).
@@ -126,17 +134,19 @@ import { useFreeday } from '@cahyo-dimas/freeday/react';
126
134
  const root = useRef<HTMLDivElement>(null);
127
135
  useFreeday(root); // fdy-* events bubble → listen at the root
128
136
  ```
129
- ```csharp
130
- // Blazor — @cahyo-dimas/freeday/blazor (window.FreedayBlazor via JS interop)
131
- await JS.InvokeVoidAsync("FreedayBlazor.initAll", _root);
132
- await JS.InvokeAsync<int>("FreedayBlazor.on", _root, "fdy-cascade-change", _self, nameof(OnCascade));
137
+ ```razor
138
+ @* Blazor — Freeday.Blazor RCL (net8.0): native typed <FdyX> with @bind, 10/10 parity *@
139
+ @using Freeday.Blazor
140
+ <FdyCombo TValue="string" @bind-Value="_status" Options="_statusOptions" AriaLabelledby="lbl" />
141
+ <FdyTable TRow="Invoice" Columns="_cols" Rows="_rows" RowKey="@(i => i.Code)" PageSize="10" />
142
+ @* Fallback for raw markup: window.FreedayBlazor.initAll / .on interop *@
133
143
  ```
134
144
 
135
145
  | Framework | Adapter | Working example |
136
146
  |---|---|---|
137
- | Vue 3 | `@cahyo-dimas/freeday/vue` | [`examples/vue-faktur/`](examples/vue-faktur/) (`npm install && npm run dev`) |
138
- | React 19 | `@cahyo-dimas/freeday/react` | [`examples/react-faktur/`](examples/react-faktur/) (`npm install && npm run dev`) |
139
- | Blazor WASM (.NET 10) | `@cahyo-dimas/freeday/blazor` | [`examples/blazor-faktur/`](examples/blazor-faktur/) (`dotnet run`) |
147
+ | Vue 3 | `@cahyo-dimas/freeday/vue` (`useFreeday` + `v-model` components) | [`examples/vue-faktur/`](examples/vue-faktur/) (`npm install && npm run dev`) |
148
+ | React 19 | `@cahyo-dimas/freeday/react` (`useFreeday` + `value`/`onChange` components) | [`examples/react-faktur/`](examples/react-faktur/) (`npm install && npm run dev`) |
149
+ | Blazor WASM (net8.0) | `Freeday.Blazor` RCL (`<FdyX>` + `@bind`) · `/blazor` interop | [`examples/blazor-faktur/`](examples/blazor-faktur/) (`dotnet run`) |
140
150
 
141
151
  Full library map and patterns: [`docs/integrations.md`](docs/integrations.md).
142
152
 
@@ -0,0 +1,11 @@
1
+ namespace Freeday.Blazor;
2
+
3
+ /// <summary>A column in the <see cref="FdyCfl{TRow}"/> results table.</summary>
4
+ /// <param name="Header">The column heading.</param>
5
+ /// <param name="Cell">Projects a row to the cell's display text.</param>
6
+ public sealed record CflColumn<TRow>(string Header, Func<TRow, string> Cell);
7
+
8
+ /// <summary>One page of results returned by <see cref="FdyCfl{TRow}"/>'s data source.</summary>
9
+ /// <param name="Rows">The rows in this page.</param>
10
+ /// <param name="HasMore">True if another page can be requested (drives "load more").</param>
11
+ public sealed record CflPage<TRow>(IReadOnlyList<TRow> Rows, bool HasMore);
@@ -0,0 +1,19 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyAutocomplete>
3
+
4
+ @* Typed wrapper over freeday-autocomplete.js (editable combobox). The enhancer filters the
5
+ option list as the user types and, on pick, fills the input + emits fdy-autocomplete-select
6
+ with the chosen text. Value is that text. The enhancer owns the input/listbox after
7
+ hydration (ShouldRender => false); the initial value seeds the input. *@
8
+ <div @ref="Root" data-fdy-autocomplete class="fdy-autocomplete">
9
+ <input class="fdy-input" role="combobox" aria-expanded="false" aria-autocomplete="list"
10
+ aria-controls="@_listId" autocomplete="off" value="@Value"
11
+ placeholder="@Placeholder" aria-label="@AriaLabel" />
12
+ <ul class="fdy-autocomplete__listbox" id="@_listId" role="listbox" hidden>
13
+ @foreach (string item in Options)
14
+ {
15
+ <li class="fdy-autocomplete__option" role="option">@item</li>
16
+ }
17
+ <li class="fdy-autocomplete__empty" hidden>@EmptyText</li>
18
+ </ul>
19
+ </div>
@@ -0,0 +1,40 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyAutocomplete
7
+ {
8
+ /// <summary>The selected text. Two-way bindable via <c>@bind-Value</c>.</summary>
9
+ [Parameter] public string? Value { get; set; }
10
+ [Parameter] public EventCallback<string?> ValueChanged { get; set; }
11
+
12
+ /// <summary>The suggestions to filter and choose from (their text is the value).</summary>
13
+ [Parameter, EditorRequired] public IReadOnlyList<string> Options { get; set; } = Array.Empty<string>();
14
+
15
+ [Parameter] public string? Placeholder { get; set; }
16
+ [Parameter] public string? AriaLabel { get; set; }
17
+
18
+ /// <summary>Text shown when no option matches the typed query.</summary>
19
+ [Parameter] public string EmptyText { get; set; } = "No results";
20
+
21
+ private readonly string _listId = $"fdy-ac-{Guid.NewGuid():N}";
22
+
23
+ protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayAutocomplete.init", Root);
24
+
25
+ protected override async ValueTask OnHydratedAsync()
26
+ => await SubscribeAsync("fdy-autocomplete-select", nameof(OnSelect));
27
+
28
+ protected override bool ShouldRender() => !Hydrated;
29
+
30
+ /// <summary>Invoked by the bridge when the user picks a suggestion.</summary>
31
+ [JSInvokable]
32
+ public async Task OnSelect(SelectDetail detail)
33
+ {
34
+ Value = detail.Value;
35
+ await ValueChanged.InvokeAsync(detail.Value);
36
+ }
37
+
38
+ /// <summary>The (DOM-safe) detail of the enhancer's <c>fdy-autocomplete-select</c> event.</summary>
39
+ public sealed record SelectDetail(string? Value);
40
+ }
@@ -0,0 +1,33 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyCascade>
3
+
4
+ @* Typed wrapper over freeday-cascade.js. The enhancer reads the hidden nested <ul> as its data
5
+ model (a <li> with a child <ul> is a branch; without, a leaf), then REMOVES the source list and
6
+ builds a drill-down popover. Because it removes/owns that DOM, we render the tree once and never
7
+ re-render (ShouldRender => false). The chosen leaf value flows back through @bind-Value. *@
8
+ <div @ref="Root" data-fdy-cascade
9
+ data-label="@Label"
10
+ data-value="@Value"
11
+ data-placeholder="@Placeholder"
12
+ data-separator="@Separator">
13
+ <ul>
14
+ @foreach (FdyCascadeNode node in Nodes)
15
+ {
16
+ @RenderNode(node)
17
+ }
18
+ </ul>
19
+ </div>
20
+
21
+ @code {
22
+ // Recursive: each <li> carries its own label text (before any nested <ul>) + data-value; a
23
+ // nested <ul> makes it a branch. Matches the enhancer's parse(ul) contract.
24
+ private RenderFragment RenderNode(FdyCascadeNode node) =>@<li data-value="@node.Value">@node.Label@if (node.Children is { Count: > 0 })
25
+ {
26
+ <ul>
27
+ @foreach (FdyCascadeNode child in node.Children)
28
+ {
29
+ @RenderNode(child)
30
+ }
31
+ </ul>
32
+ }</li>;
33
+ }
@@ -0,0 +1,38 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyCascade
7
+ {
8
+ /// <summary>The hierarchy to drill through.</summary>
9
+ [Parameter, EditorRequired] public IReadOnlyList<FdyCascadeNode> Nodes { get; set; } = Array.Empty<FdyCascadeNode>();
10
+
11
+ /// <summary>The chosen leaf value. Two-way bindable via <c>@bind-Value</c>.</summary>
12
+ [Parameter] public string? Value { get; set; }
13
+ [Parameter] public EventCallback<string?> ValueChanged { get; set; }
14
+
15
+ [Parameter] public string? Label { get; set; }
16
+ [Parameter] public string? Placeholder { get; set; }
17
+
18
+ /// <summary>Separator between labels in the displayed path (default " / ").</summary>
19
+ [Parameter] public string Separator { get; set; } = " / ";
20
+
21
+ protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayCascade.init", Root);
22
+
23
+ protected override async ValueTask OnHydratedAsync()
24
+ => await SubscribeAsync("fdy-cascade-change", nameof(OnChange));
25
+
26
+ protected override bool ShouldRender() => !Hydrated;
27
+
28
+ /// <summary>Invoked by the bridge when the user selects a leaf.</summary>
29
+ [JSInvokable]
30
+ public async Task OnChange(ChangeDetail detail)
31
+ {
32
+ Value = detail.Value;
33
+ await ValueChanged.InvokeAsync(detail.Value);
34
+ }
35
+
36
+ /// <summary>The detail shape of the enhancer's <c>fdy-cascade-change</c> event.</summary>
37
+ public sealed record ChangeDetail(string? Value, string? Path, string[]? Labels);
38
+ }
@@ -0,0 +1,8 @@
1
+ namespace Freeday.Blazor;
2
+
3
+ /// <summary>A node in an <see cref="FdyCascade"/> hierarchy. A node with children is a branch
4
+ /// (drills in); a node without is a leaf (selects, storing the full path as the value).</summary>
5
+ /// <param name="Value">The value stored when this leaf is chosen (branches pass it through their leaves).</param>
6
+ /// <param name="Label">The visible text.</param>
7
+ /// <param name="Children">Child nodes, or null for a leaf.</param>
8
+ public sealed record FdyCascadeNode(string Value, string Label, IReadOnlyList<FdyCascadeNode>? Children = null);
@@ -0,0 +1,93 @@
1
+ @namespace Freeday.Blazor
2
+ @typeparam TRow where TRow : class
3
+ @inherits FreedayComponentBase<FdyCfl<TRow>>
4
+
5
+ @* Controlled *async* choose-from-list. Unlike the enhancer-wrapper components, this is a
6
+ Blazor-native controlled component (like FdyTable): the read-only field + the .fdy-modal--cfl
7
+ dialog are rendered and re-rendered by Blazor, rows come from an async LoadPage callback, and
8
+ the <dialog> is opened/closed through the same freeday-blazor.js interop FdyModal uses. *@
9
+
10
+ <div class="@FieldGroupClass">
11
+ <input class="fdy-input" type="text" readonly value="@DisplayValue"
12
+ placeholder="@Placeholder" aria-labelledby="@AriaLabelledby"
13
+ aria-invalid="@(Invalid ? "true" : null)" aria-readonly="@(Readonly ? "true" : null)" />
14
+ <button class="fdy-input-group__btn" type="button" @onclick="OpenAsync"
15
+ disabled="@(Disabled || Readonly)" aria-label="@SearchPlaceholder">
16
+ <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
17
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
18
+ <circle cx="11" cy="11" r="7"></circle><path d="m21 21-4.3-4.3"></path>
19
+ </svg>
20
+ </button>
21
+ </div>
22
+
23
+ <dialog @ref="Root" class="fdy-modal fdy-modal--cfl" aria-labelledby="@_titleId">
24
+ <div class="fdy-modal__header">
25
+ <h3 id="@_titleId" class="fdy-modal__title">@Title</h3>
26
+ <button class="fdy-modal__close" type="button" aria-label="@CloseLabel" @onclick="CloseAsync">&times;</button>
27
+ </div>
28
+
29
+ <div class="fdy-modal__body">
30
+ <div class="fdy-cfl__search">
31
+ <input class="fdy-input" type="search" data-fdy-cfl-search
32
+ placeholder="@SearchPlaceholder" aria-label="@SearchPlaceholder"
33
+ @bind="_query" @bind:event="oninput" @bind:after="OnQueryChangedAsync" />
34
+ </div>
35
+
36
+ <div class="fdy-cfl__results">
37
+ @if (IsInitialLoading)
38
+ {
39
+ <p class="fdy-cfl__empty" role="status">@LoadingText</p>
40
+ }
41
+ else if (IsBlockingError)
42
+ {
43
+ <div class="fdy-cfl__empty" role="alert">
44
+ <span>@(_error ?? ErrorText)</span>
45
+ <button class="fdy-btn fdy-btn--sm" type="button" @onclick="RetryAsync">@RetryText</button>
46
+ </div>
47
+ }
48
+ else if (IsEmpty)
49
+ {
50
+ <p class="fdy-cfl__empty">@EmptyText</p>
51
+ }
52
+ else
53
+ {
54
+ <table class="fdy-table" aria-label="@Title">
55
+ <thead>
56
+ <tr>
57
+ @foreach (CflColumn<TRow> col in Columns)
58
+ {
59
+ <th scope="col">@col.Header</th>
60
+ }
61
+ </tr>
62
+ </thead>
63
+ <tbody>
64
+ @foreach (TRow row in _rows)
65
+ {
66
+ <tr class="fdy-cfl__row" tabindex="0" @key="RowKey(row)" @onclick="() => ChooseAsync(row)">
67
+ @foreach (CflColumn<TRow> col in Columns)
68
+ {
69
+ <td>@col.Cell(row)</td>
70
+ }
71
+ </tr>
72
+ }
73
+ </tbody>
74
+ </table>
75
+
76
+ @if (_error is not null)
77
+ {
78
+ <div class="fdy-cfl__empty" role="alert" style="padding:var(--space-4) var(--space-5)">
79
+ <span>@_error</span>
80
+ <button class="fdy-btn fdy-btn--sm" type="button" @onclick="RetryAsync">@RetryText</button>
81
+ </div>
82
+ }
83
+ @if (_hasMore)
84
+ {
85
+ <div class="fdy-cfl__actions">
86
+ <button class="fdy-btn fdy-btn--ghost fdy-btn--sm" type="button"
87
+ disabled="@_loading" @onclick="LoadMoreAsync">@MoreText</button>
88
+ </div>
89
+ }
90
+ }
91
+ </div>
92
+ </div>
93
+ </dialog>
@@ -0,0 +1,146 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyCfl<TRow>
7
+ {
8
+ /// <summary>The picked row (or null). Two-way bindable via <c>@bind-Value</c>.</summary>
9
+ [Parameter] public TRow? Value { get; set; }
10
+ [Parameter] public EventCallback<TRow?> ValueChanged { get; set; }
11
+
12
+ /// <summary>Async data source: <c>(query, page) =&gt; a page of rows</c>. The caller owns paging
13
+ /// and server-side search; page 0 is the first page.</summary>
14
+ [Parameter, EditorRequired] public Func<string, int, Task<CflPage<TRow>>> LoadPage { get; set; } = default!;
15
+
16
+ /// <summary>Result table columns.</summary>
17
+ [Parameter, EditorRequired] public IReadOnlyList<CflColumn<TRow>> Columns { get; set; } = Array.Empty<CflColumn<TRow>>();
18
+
19
+ /// <summary>Projects the picked row to the text shown in the read-only field.</summary>
20
+ [Parameter, EditorRequired] public Func<TRow, string> Display { get; set; } = default!;
21
+
22
+ /// <summary>A stable key per row (for diffing).</summary>
23
+ [Parameter, EditorRequired] public Func<TRow, string> RowKey { get; set; } = default!;
24
+
25
+ [Parameter] public string Title { get; set; } = "Choose data";
26
+ [Parameter] public string? Placeholder { get; set; }
27
+ [Parameter] public string SearchPlaceholder { get; set; } = "Search…";
28
+ [Parameter] public string LoadingText { get; set; } = "Loading…";
29
+ [Parameter] public string EmptyText { get; set; } = "No results.";
30
+ [Parameter] public string ErrorText { get; set; } = "Failed to load.";
31
+ [Parameter] public string RetryText { get; set; } = "Try again";
32
+ [Parameter] public string MoreText { get; set; } = "Load more";
33
+ [Parameter] public string CloseLabel { get; set; } = "Close";
34
+
35
+ [Parameter] public bool Disabled { get; set; }
36
+
37
+ /// <summary>Locked/view mode: shows the value but the search dialog can't be opened.</summary>
38
+ [Parameter] public bool Readonly { get; set; }
39
+ [Parameter] public bool Invalid { get; set; }
40
+ [Parameter] public string? AriaLabelledby { get; set; }
41
+
42
+ /// <summary>Debounce (ms) between a keystroke and the search request.</summary>
43
+ [Parameter] public int SearchDebounceMs { get; set; } = 250;
44
+
45
+ private readonly string _titleId = $"fdy-cfl-{Guid.NewGuid():N}-title";
46
+ private int _dialogToken;
47
+ private bool _open;
48
+ private string _query = string.Empty;
49
+ private readonly List<TRow> _rows = new();
50
+ private int _page;
51
+ private bool _hasMore;
52
+ private bool _loading;
53
+ private string? _error;
54
+ private int _reqSeq; // out-of-order response guard
55
+ private int _debounceSeq; // supersedes stale debounced searches
56
+
57
+ private string DisplayValue => Value is null ? string.Empty : Display(Value);
58
+ private string FieldGroupClass => Invalid ? "fdy-input-group fdy-input-group--error" : "fdy-input-group";
59
+ private bool IsInitialLoading => _loading && _rows.Count == 0;
60
+ private bool IsBlockingError => _error is not null && _rows.Count == 0;
61
+ private bool IsEmpty => !_loading && _error is null && _rows.Count == 0;
62
+
63
+ // The dialog content is Blazor-rendered (so no ShouldRender override); only its show/close +
64
+ // Esc/backdrop dismissal go through interop.
65
+ protected override async ValueTask HydrateAsync()
66
+ => _dialogToken = await JS.InvokeAsync<int>("FreedayBlazor.dialogInit", Root, Self, nameof(OnDismiss), true);
67
+
68
+ private async Task OpenAsync()
69
+ {
70
+ if (Disabled || Readonly || _open) return;
71
+ _open = true;
72
+ _query = string.Empty;
73
+ await JS.InvokeVoidAsync("FreedayBlazor.dialogShow", Root);
74
+ await LoadAsync(reset: true);
75
+ }
76
+
77
+ private async Task CloseAsync()
78
+ {
79
+ if (!_open) return;
80
+ _open = false;
81
+ await JS.InvokeVoidAsync("FreedayBlazor.dialogClose", Root);
82
+ }
83
+
84
+ /// <summary>Invoked by the bridge on Esc / backdrop dismiss.</summary>
85
+ [JSInvokable]
86
+ public Task OnDismiss() => CloseAsync();
87
+
88
+ private async Task OnQueryChangedAsync()
89
+ {
90
+ int seq = ++_debounceSeq;
91
+ if (SearchDebounceMs > 0)
92
+ {
93
+ await Task.Delay(SearchDebounceMs);
94
+ if (seq != _debounceSeq) return; // a newer keystroke superseded this one
95
+ }
96
+ await LoadAsync(reset: true);
97
+ }
98
+
99
+ private async Task LoadAsync(bool reset)
100
+ {
101
+ _loading = true;
102
+ _error = null;
103
+ StateHasChanged();
104
+ int seq = ++_reqSeq;
105
+ try
106
+ {
107
+ CflPage<TRow> result = await LoadPage(_query, reset ? 0 : _page);
108
+ if (seq != _reqSeq) return; // a newer request superseded this response
109
+ if (reset) _rows.Clear();
110
+ _rows.AddRange(result.Rows);
111
+ _hasMore = result.HasMore;
112
+ _page = reset ? 1 : _page + 1;
113
+ }
114
+ catch (Exception ex)
115
+ {
116
+ if (seq == _reqSeq) _error = string.IsNullOrEmpty(ex.Message) ? ErrorText : ex.Message;
117
+ }
118
+ finally
119
+ {
120
+ if (seq == _reqSeq)
121
+ {
122
+ _loading = false;
123
+ StateHasChanged();
124
+ }
125
+ }
126
+ }
127
+
128
+ private Task LoadMoreAsync() => _hasMore && !_loading ? LoadAsync(reset: false) : Task.CompletedTask;
129
+
130
+ private Task RetryAsync() => LoadAsync(reset: true);
131
+
132
+ private async Task ChooseAsync(TRow row)
133
+ {
134
+ Value = row;
135
+ await ValueChanged.InvokeAsync(row);
136
+ await CloseAsync();
137
+ }
138
+
139
+ protected override async ValueTask CleanupAsync()
140
+ {
141
+ if (_dialogToken != 0)
142
+ {
143
+ await JS.InvokeVoidAsync("FreedayBlazor.dialogDispose", _dialogToken);
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,25 @@
1
+ @namespace Freeday.Blazor
2
+
3
+ @* A thin Blazor wrapper over freeday's zero-dependency chart renderer (freeday-chart.js). Charts
4
+ are not form controls, so re-implementing the SVG maths in C# would only duplicate them. Instead
5
+ Blazor owns the outer <div> and its data-* attributes; after every render this calls
6
+ FreedayChart.update() (via the bridge) to repaint the SVG from those attributes — giving reactive,
7
+ typed charts with no external dependency. Because the renderer paints with CSS var()/--chart-*
8
+ tokens, charts also recolour on data-theme change for free. Requires dist/freeday.js loaded so
9
+ window.FreedayChart exists; until then the slotted fallback content stays visible. *@
10
+ <div @ref="Root"
11
+ class="@RootClass"
12
+ data-fdy-chart="@Type"
13
+ data-series="@SeriesAttr"
14
+ data-values="@ValuesAttr"
15
+ data-labels="@LabelsAttr"
16
+ data-fdy-format="@Format"
17
+ data-fdy-stacked="@StackedAttr"
18
+ data-fdy-legend="@Legend"
19
+ data-fdy-colors="@ColorsAttr"
20
+ data-fdy-color="@Color"
21
+ data-fdy-center="@Center"
22
+ role="img"
23
+ aria-label="@AriaLabel">
24
+ @ChildContent
25
+ </div>