@cahyo-dimas/freeday 1.14.0 → 1.16.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 (42) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.id.md +1 -1
  3. package/README.md +1 -1
  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/asset.d.ts +6 -0
  37. package/dist/freeday-select.js +32 -8
  38. package/dist/freeday.d.ts +24 -0
  39. package/dist/freeday.js +32 -8
  40. package/package.json +14 -8
  41. package/src/freeday-select.js +32 -8
  42. package/tokens/breakpoints.d.ts +8 -0
@@ -0,0 +1,196 @@
1
+ using System.Collections.Concurrent;
2
+ using System.Globalization;
3
+ using System.Reflection;
4
+ using System.Text.RegularExpressions;
5
+
6
+ namespace Freeday.Blazor;
7
+
8
+ /// <summary>
9
+ /// The sort / filter / paginate logic for <see cref="FdyTable{TRow}"/> — the C# counterpart of the
10
+ /// framework-agnostic adapters/core/table-model.js shared by the Vue and React adapters. Kept as a
11
+ /// separate pure-function class (no rendering, no state) so it mirrors that reference faithfully and
12
+ /// stays unit-testable. Every method is pure: inputs are never mutated (rows are copied before sort).
13
+ /// </summary>
14
+ public static class TableModel
15
+ {
16
+ private static readonly ConcurrentDictionary<(Type, string), PropertyInfo?> PropCache = new();
17
+ private static readonly Regex NonNumeric = new(@"[^\d.-]", RegexOptions.Compiled);
18
+
19
+ /// <summary>Numeric value, tolerant of formatted strings ("Rp 1.000"); non-numeric -> 0.</summary>
20
+ private static double ToNumber(object? v)
21
+ {
22
+ if (v is null) return 0;
23
+ if (v is double d) return double.IsFinite(d) ? d : 0;
24
+ if (v is IConvertible && v is not string)
25
+ {
26
+ try { return Convert.ToDouble(v, CultureInfo.InvariantCulture); } catch { /* fall through */ }
27
+ }
28
+ string cleaned = NonNumeric.Replace(v.ToString() ?? string.Empty, string.Empty);
29
+ return double.TryParse(cleaned, NumberStyles.Any, CultureInfo.InvariantCulture, out double n) ? n : 0;
30
+ }
31
+
32
+ /// <summary>Epoch ms; unparseable -> 0.</summary>
33
+ private static double ToTime(object? v)
34
+ {
35
+ if (v is null) return 0;
36
+ if (v is DateTime dt) return new DateTimeOffset(dt.ToUniversalTime()).ToUnixTimeMilliseconds();
37
+ if (v is DateTimeOffset dto) return dto.ToUnixTimeMilliseconds();
38
+ return DateTimeOffset.TryParse(v.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTimeOffset parsed)
39
+ ? parsed.ToUnixTimeMilliseconds()
40
+ : 0;
41
+ }
42
+
43
+ /// <summary>ISO calendar day (yyyy-MM-dd) — lexicographically comparable.</summary>
44
+ private static string DateOnly(object? v)
45
+ {
46
+ if (v is DateTime dt) return dt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
47
+ if (v is DateTimeOffset dto) return dto.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
48
+ string s = v?.ToString() ?? string.Empty;
49
+ return s.Length >= 10 ? s[..10] : s;
50
+ }
51
+
52
+ /// <summary>Raw cell value for a column: the accessor if present, else the property named Key.</summary>
53
+ public static object? CellValue<TRow>(TRow row, FdyTableColumn<TRow> column)
54
+ {
55
+ if (column.Value is not null) return column.Value(row);
56
+ PropertyInfo? prop = PropCache.GetOrAdd((typeof(TRow), column.Key),
57
+ static k => k.Item1.GetProperty(k.Item2, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase));
58
+ return prop?.GetValue(row);
59
+ }
60
+
61
+ /// <summary>Cell value as trimmed display text (null -> "").</summary>
62
+ public static string CellText<TRow>(TRow row, FdyTableColumn<TRow> column)
63
+ {
64
+ object? v = CellValue(row, column);
65
+ return v is null ? string.Empty : (Convert.ToString(v, CultureInfo.CurrentCulture) ?? string.Empty).Trim();
66
+ }
67
+
68
+ /// <summary>Effective comparator type: explicit SortType, else derived from the filter type.</summary>
69
+ public static FdyColumnType ColumnSortType<TRow>(FdyTableColumn<TRow> column)
70
+ {
71
+ if (column.SortType is { } t) return t;
72
+ return column.Filter switch
73
+ {
74
+ FdyColumnFilterType.Number => FdyColumnType.Number,
75
+ FdyColumnFilterType.Date => FdyColumnType.Date,
76
+ _ => FdyColumnType.Text,
77
+ };
78
+ }
79
+
80
+ /// <summary>Ascending comparison of two raw cell values under a comparator type.</summary>
81
+ public static int CompareBy(FdyColumnType type, object? a, object? b) => type switch
82
+ {
83
+ FdyColumnType.Number => ToNumber(a).CompareTo(ToNumber(b)),
84
+ FdyColumnType.Date => ToTime(a).CompareTo(ToTime(b)),
85
+ _ => string.Compare(a?.ToString() ?? string.Empty, b?.ToString() ?? string.Empty,
86
+ StringComparison.CurrentCultureIgnoreCase),
87
+ };
88
+
89
+ /// <summary>True when a filter would actually narrow the rows (worth applying / showing as active).</summary>
90
+ public static bool IsFilterActive(FdyColumnFilter? filter) => filter switch
91
+ {
92
+ FdyColumnFilter.Text t => t.Value.Trim().Length > 0,
93
+ FdyColumnFilter.Enum e => e.Values.Count > 0,
94
+ FdyColumnFilter.Number n => n.Min is not null || n.Max is not null,
95
+ FdyColumnFilter.Date d => d.From is not null || d.To is not null,
96
+ _ => false,
97
+ };
98
+
99
+ private static bool RowPassesFilter<TRow>(TRow row, FdyTableColumn<TRow> column, FdyColumnFilter filter)
100
+ {
101
+ switch (filter)
102
+ {
103
+ case FdyColumnFilter.Text t:
104
+ {
105
+ string needle = t.Value.Trim().ToLowerInvariant();
106
+ return needle.Length == 0 || CellText(row, column).ToLowerInvariant().Contains(needle);
107
+ }
108
+ case FdyColumnFilter.Enum e:
109
+ return e.Values.Count == 0 || e.Values.Contains(CellText(row, column));
110
+ case FdyColumnFilter.Number n:
111
+ {
112
+ double v = ToNumber(CellValue(row, column));
113
+ if (n.Min is { } min && v < min) return false;
114
+ if (n.Max is { } max && v > max) return false;
115
+ return true;
116
+ }
117
+ case FdyColumnFilter.Date d:
118
+ {
119
+ string day = DateOnly(CellValue(row, column));
120
+ if (d.From is { } from && string.CompareOrdinal(day, from) < 0) return false;
121
+ if (d.To is { } to && string.CompareOrdinal(day, to) > 0) return false;
122
+ return true;
123
+ }
124
+ default:
125
+ return true;
126
+ }
127
+ }
128
+
129
+ /// <summary>Rows passing every active column filter (logical AND). Input is never mutated.</summary>
130
+ public static List<TRow> FilterRows<TRow>(
131
+ IReadOnlyList<TRow> rows,
132
+ IReadOnlyList<FdyTableColumn<TRow>> columns,
133
+ IReadOnlyDictionary<string, FdyColumnFilter> filters)
134
+ {
135
+ List<(FdyTableColumn<TRow> Col, FdyColumnFilter Filter)> active = new();
136
+ foreach (FdyTableColumn<TRow> col in columns)
137
+ {
138
+ if (filters.TryGetValue(col.Key, out FdyColumnFilter? f) && IsFilterActive(f))
139
+ {
140
+ active.Add((col, f));
141
+ }
142
+ }
143
+ if (active.Count == 0) return rows.ToList();
144
+ return rows.Where(row => active.All(e => RowPassesFilter(row, e.Col, e.Filter))).ToList();
145
+ }
146
+
147
+ /// <summary>Rows sorted by the given sort state. Stable via OrderBy; input is never mutated.</summary>
148
+ public static List<TRow> SortRows<TRow>(
149
+ IReadOnlyList<TRow> rows,
150
+ IReadOnlyList<FdyTableColumn<TRow>> columns,
151
+ FdySortState? sort)
152
+ {
153
+ if (sort is null) return rows.ToList();
154
+ FdyTableColumn<TRow>? column = columns.FirstOrDefault(c => c.Key == sort.Key);
155
+ if (column is null) return rows.ToList();
156
+ FdyColumnType type = ColumnSortType(column);
157
+ int factor = sort.Dir == FdySortDir.Desc ? -1 : 1;
158
+ return rows
159
+ .OrderBy(r => r, Comparer<TRow>.Create((a, b) => CompareBy(type, CellValue(a, column), CellValue(b, column)) * factor))
160
+ .ToList();
161
+ }
162
+
163
+ /// <summary>Page slice (0-based page index). A non-positive size returns all rows.</summary>
164
+ public static List<TRow> Paginate<TRow>(IReadOnlyList<TRow> rows, int pageIndex, int pageSize)
165
+ {
166
+ if (pageSize <= 0) return rows.ToList();
167
+ return rows.Skip(pageIndex * pageSize).Take(pageSize).ToList();
168
+ }
169
+
170
+ /// <summary>Distinct non-empty cell texts for a column, naturally sorted — the enum-filter source.</summary>
171
+ public static List<string> DistinctValues<TRow>(IReadOnlyList<TRow> rows, FdyTableColumn<TRow> column)
172
+ {
173
+ HashSet<string> seen = new();
174
+ List<string> outList = new();
175
+ foreach (TRow row in rows)
176
+ {
177
+ string v = CellText(row, column);
178
+ if (v.Length > 0 && seen.Add(v)) outList.Add(v);
179
+ }
180
+ outList.Sort((a, b) => string.Compare(a, b, StringComparison.CurrentCultureIgnoreCase));
181
+ return outList;
182
+ }
183
+
184
+ /// <summary>Page-number window: first and last page always shown, current ±1, null for the
185
+ /// "ellipsis" gaps between. <paramref name="current"/> and returned page numbers are 1-based.</summary>
186
+ public static List<int?> PageWindow(int current, int totalPages)
187
+ {
188
+ List<int?> outList = new();
189
+ for (int p = 1; p <= totalPages; p++)
190
+ {
191
+ if (p == 1 || p == totalPages || (p >= current - 1 && p <= current + 1)) outList.Add(p);
192
+ else if (outList.Count == 0 || outList[^1] is not null) outList.Add(null);
193
+ }
194
+ return outList;
195
+ }
196
+ }
@@ -0,0 +1,69 @@
1
+ using Microsoft.AspNetCore.Components;
2
+
3
+ namespace Freeday.Blazor;
4
+
5
+ /// <summary>Sort direction for a column.</summary>
6
+ public enum FdySortDir { Asc, Desc }
7
+
8
+ /// <summary>A column's comparator type — how its values are ordered when sorting.</summary>
9
+ public enum FdyColumnType { Text, Number, Date }
10
+
11
+ /// <summary>Cell alignment. Maps to <c>text-align</c> on the header and body cells.</summary>
12
+ public enum FdyColumnAlign { Left, Right, Center }
13
+
14
+ /// <summary>The kind of column filter to offer (a funnel popover in the header).</summary>
15
+ public enum FdyColumnFilterType { Text, Enum, Number, Date }
16
+
17
+ /// <summary>Which column is sorted and in what direction.</summary>
18
+ public sealed record FdySortState(string Key, FdySortDir Dir);
19
+
20
+ /// <summary>Server-driven pagination state (0-based page index). Its presence on
21
+ /// <see cref="FdyTable{TRow}.Page"/> switches the table into server mode.</summary>
22
+ public sealed record FdyPageState(int Index, int Size, int Total);
23
+
24
+ /// <summary>The active filter for a single column (a closed set of shapes, one per filter type).</summary>
25
+ public abstract record FdyColumnFilter
26
+ {
27
+ public sealed record Text(string Value) : FdyColumnFilter;
28
+ public sealed record Enum(IReadOnlyList<string> Values) : FdyColumnFilter;
29
+ public sealed record Number(double? Min, double? Max) : FdyColumnFilter;
30
+ public sealed record Date(string? From, string? To) : FdyColumnFilter;
31
+ }
32
+
33
+ /// <summary>One column definition. <typeparamref name="TRow"/> is the row type.</summary>
34
+ /// <typeparam name="TRow">The row model.</typeparam>
35
+ public sealed class FdyTableColumn<TRow>
36
+ {
37
+ /// <summary>Row property key; the sort/filter identity and, when <see cref="Value"/> is unset,
38
+ /// the default cell accessor (resolved by reflection against a property of this name).</summary>
39
+ public required string Key { get; init; }
40
+
41
+ /// <summary>Header label.</summary>
42
+ public required string Label { get; init; }
43
+
44
+ /// <summary>Show a sort toggle in the header.</summary>
45
+ public bool Sortable { get; init; }
46
+
47
+ /// <summary>Offer a column filter of this type (null = no filter).</summary>
48
+ public FdyColumnFilterType? Filter { get; init; }
49
+
50
+ /// <summary>Cell alignment (null = left).</summary>
51
+ public FdyColumnAlign? Align { get; init; }
52
+
53
+ /// <summary>Render cells in the monospace data font (<c>.fdy-mono</c>).</summary>
54
+ public bool Mono { get; init; }
55
+
56
+ /// <summary>Override the comparator type; defaults to one derived from <see cref="Filter"/>, else Text.</summary>
57
+ public FdyColumnType? SortType { get; init; }
58
+
59
+ /// <summary>Typed value accessor for sort/filter/default cell text; defaults to the property named
60
+ /// <see cref="Key"/> (reflection). Prefer setting this for strongly-typed, allocation-free access.</summary>
61
+ public Func<TRow, object?>? Value { get; init; }
62
+
63
+ /// <summary>Explicit options for an enum filter; defaults to the distinct values in the current
64
+ /// rows. Provide this in server-paged mode, where the rows on screen are only one page.</summary>
65
+ public IReadOnlyList<string>? Options { get; init; }
66
+
67
+ /// <summary>Custom cell template; defaults to the accessor's text.</summary>
68
+ public RenderFragment<TRow>? Cell { get; init; }
69
+ }
@@ -0,0 +1,5 @@
1
+ @using System.Threading.Tasks
2
+ @using Microsoft.AspNetCore.Components
3
+ @using Microsoft.AspNetCore.Components.Web
4
+ @using Microsoft.JSInterop
5
+ @using Freeday.Blazor
@@ -55,11 +55,44 @@
55
55
  return token;
56
56
  }
57
57
 
58
- // Remove a subscription created by on().
58
+ // Subscribe to a daterange's child datepicker changes, forwarding {value, role} so a Blazor
59
+ // wrapper can bind From/To separately — both children emit the same fdy-datepicker-change, so
60
+ // the originating child's data-role is what tells them apart.
61
+ function dateRangeOn(element, dotNetRef, methodName) {
62
+ var handler = function (e) {
63
+ var origin = e.target && e.target.closest ? e.target.closest('[data-role]') : null;
64
+ dotNetRef.invokeMethodAsync(methodName, {
65
+ value: (e.detail && e.detail.value) || '',
66
+ role: origin ? origin.getAttribute('data-role') : null
67
+ });
68
+ };
69
+ element.addEventListener('fdy-datepicker-change', handler);
70
+ var token = ++seq;
71
+ subs[token] = { el: element, type: 'fdy-datepicker-change', fn: handler };
72
+ return token;
73
+ }
74
+
75
+ // Fire a .NET callback when a pointerdown lands outside `element`, or Escape is pressed —
76
+ // the light-dismiss primitive for Blazor-owned popovers (e.g. FdyTable's column filter, which
77
+ // is position:fixed in the top layer and so escapes the table's overflow). Returns an off() token.
78
+ function onOutside(element, dotNetRef, methodName) {
79
+ if (!element) return 0;
80
+ var onPointer = function (e) { if (!element.contains(e.target)) dotNetRef.invokeMethodAsync(methodName); };
81
+ var onKey = function (e) { if (e.key === 'Escape') dotNetRef.invokeMethodAsync(methodName); };
82
+ document.addEventListener('pointerdown', onPointer, true);
83
+ document.addEventListener('keydown', onKey, true);
84
+ var token = ++seq;
85
+ subs[token] = { el: document, type: 'pointerdown', fn: onPointer, key: onKey, capture: true };
86
+ return token;
87
+ }
88
+
89
+ // Remove a subscription created by on() / onOutside(); removes with the same capture flag it
90
+ // was added with (a bubble-phase listener can't be removed with capture:true and vice versa).
59
91
  function off(token) {
60
92
  var s = subs[token];
61
93
  if (s) {
62
- s.el.removeEventListener(s.type, s.fn);
94
+ s.el.removeEventListener(s.type, s.fn, s.capture === true);
95
+ if (s.key) s.el.removeEventListener('keydown', s.key, true);
63
96
  delete subs[token];
64
97
  }
65
98
  }
@@ -69,11 +102,76 @@
69
102
  if (window.Freeday && typeof window.Freeday.toast === 'function') window.Freeday.toast(options);
70
103
  }
71
104
 
105
+ // Passthrough to a combo's programmatic setter (window.FreedayCombo.setValue) so a
106
+ // Blazor @bind-Value can push an external value change onto the enhancer-owned DOM
107
+ // without the enhancer echoing an fdy-change back.
108
+ function comboSetValue(element, value) {
109
+ if (window.FreedayCombo && typeof window.FreedayCombo.setValue === 'function') {
110
+ window.FreedayCombo.setValue(element, value);
111
+ }
112
+ }
113
+
114
+ // Repaint a chart from its current data-* attributes. FdyChart keeps the outer <div> +
115
+ // attributes under Blazor's control and calls this after each render, so the SVG re-renders
116
+ // when the bound data changes (window.FreedayChart.update === renderChart).
117
+ function chartUpdate(element) {
118
+ if (element && window.FreedayChart && typeof window.FreedayChart.update === 'function') {
119
+ window.FreedayChart.update(element);
120
+ }
121
+ }
122
+
123
+ // --- Native <dialog> control for FdyModal ---------------------------------
124
+ // The kit styles `.fdy-modal` as a <dialog> but nothing drives it; Blazor can't call
125
+ // showModal()/close() from C#, and Esc/backdrop dismissal must route through .NET state
126
+ // (not close the DOM behind its back). dialogInit wires cancel + backdrop → a [JSInvokable]
127
+ // dismiss callback; showModal()/close() are guarded (showModal on an open dialog throws).
128
+ var dialogSubs = {};
129
+ var dialogSeq = 0;
130
+
131
+ function dialogInit(dialog, dotNetRef, dismissMethod, dismissible) {
132
+ if (!dialog) return 0;
133
+ var onCancel = function (e) {
134
+ e.preventDefault(); // stop the native close; app state is the single source of truth
135
+ if (dismissible) dotNetRef.invokeMethodAsync(dismissMethod);
136
+ };
137
+ var onClick = function (e) {
138
+ // The ::backdrop is not a separate element — a click whose target is the dialog box
139
+ // itself (not its content) is a backdrop click.
140
+ if (dismissible && e.target === dialog) dotNetRef.invokeMethodAsync(dismissMethod);
141
+ };
142
+ dialog.addEventListener('cancel', onCancel);
143
+ dialog.addEventListener('click', onClick);
144
+ var token = ++dialogSeq;
145
+ dialogSubs[token] = { el: dialog, cancel: onCancel, click: onClick };
146
+ return token;
147
+ }
148
+
149
+ function dialogShow(dialog) {
150
+ if (dialog && typeof dialog.showModal === 'function' && !dialog.open) dialog.showModal();
151
+ }
152
+
153
+ function dialogClose(dialog) {
154
+ if (dialog && dialog.open) dialog.close();
155
+ }
156
+
157
+ function dialogDispose(token) {
158
+ var s = dialogSubs[token];
159
+ if (s) {
160
+ s.el.removeEventListener('cancel', s.cancel);
161
+ s.el.removeEventListener('click', s.click);
162
+ delete dialogSubs[token];
163
+ }
164
+ }
165
+
72
166
  // Flip the document theme (data-theme on <html>), for the demo toggle.
73
167
  function toggleTheme() {
74
168
  var e = document.documentElement;
75
169
  e.setAttribute('data-theme', e.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
76
170
  }
77
171
 
78
- window.FreedayBlazor = { initAll: initAll, on: on, off: off, toast: toast, toggleTheme: toggleTheme };
172
+ window.FreedayBlazor = {
173
+ initAll: initAll, on: on, off: off, onOutside: onOutside, toast: toast, toggleTheme: toggleTheme,
174
+ comboSetValue: comboSetValue, dateRangeOn: dateRangeOn, chartUpdate: chartUpdate,
175
+ dialogInit: dialogInit, dialogShow: dialogShow, dialogClose: dialogClose, dialogDispose: dialogDispose,
176
+ };
79
177
  })();
@@ -0,0 +1,6 @@
1
+ // Shared type stub for every export imported purely for its side effects — the CSS
2
+ // bundles (`./css`, `./css/components`, `./tokens`) and the individual enhancer scripts
3
+ // (`./enhancers/*`). TypeScript only needs the `types` condition to *resolve* the module;
4
+ // there is no value surface to describe. (The root `.` import is different — it augments
5
+ // window.Freeday — so it points at ./freeday.d.ts instead.)
6
+ export {};
@@ -91,7 +91,11 @@
91
91
  setHighlight(-1);
92
92
  if (openClose === close) openClose = null;
93
93
  }
94
- function choose(opt) {
94
+ // Mark an option selected + reflect it in the button. `silent` skips the change
95
+ // event, close, and focus — used by the programmatic setValue() below so a host
96
+ // (e.g. a Blazor/framework wrapper) can push a value in without echoing an event
97
+ // back to itself or stealing focus. User-driven picks go through choose() (loud).
98
+ function select(opt, silent) {
95
99
  options.forEach(function (other) {
96
100
  var on = other === opt;
97
101
  other.setAttribute('aria-selected', on ? 'true' : 'false');
@@ -101,12 +105,24 @@
101
105
  root.setAttribute('data-value', opt.getAttribute('data-value') || '');
102
106
  valueEl.textContent = optionLabel(opt);
103
107
  valueEl.classList.remove('fdy-combo__value--placeholder');
104
- root.dispatchEvent(new CustomEvent('fdy-change', {
105
- bubbles: true,
106
- detail: { value: opt.getAttribute('data-value') }
107
- }));
108
- close();
109
- button.focus();
108
+ if (!silent) {
109
+ root.dispatchEvent(new CustomEvent('fdy-change', {
110
+ bubbles: true,
111
+ detail: { value: opt.getAttribute('data-value') }
112
+ }));
113
+ close();
114
+ button.focus();
115
+ }
116
+ }
117
+ function choose(opt) { select(opt, false); }
118
+ // Programmatic set (no event, no focus change). No-op if no option matches.
119
+ function setValue(value) {
120
+ for (var i = 0; i < options.length; i++) {
121
+ if ((options[i].getAttribute('data-value') || '') === String(value)) {
122
+ select(options[i], true);
123
+ return;
124
+ }
125
+ }
110
126
  }
111
127
 
112
128
  button.addEventListener('click', function () {
@@ -167,6 +183,9 @@
167
183
  root.addEventListener('focusout', function (e) {
168
184
  if (!root.contains(e.relatedTarget)) close();
169
185
  });
186
+
187
+ // Per-instance API for programmatic control (host wrappers / two-way binding).
188
+ root._fdyCombo = { setValue: setValue };
170
189
  }
171
190
 
172
191
  function initAll(context) {
@@ -190,5 +209,10 @@
190
209
  initAll();
191
210
  }
192
211
 
193
- window.FreedayCombo = { init: initCombo, initAll: initAll };
212
+ // Programmatic set on an already-initialised combo root (no fdy-change dispatched).
213
+ function setValue(root, value) {
214
+ if (root && root._fdyCombo) root._fdyCombo.setValue(value);
215
+ }
216
+
217
+ window.FreedayCombo = { init: initCombo, initAll: initAll, setValue: setValue };
194
218
  })();
@@ -0,0 +1,24 @@
1
+ // Type surface for the root side-effect import `@cahyo-dimas/freeday`, which loads every
2
+ // enhancer and registers the `window.Freeday` global. Authored from src/freeday-toast.js
3
+ // — keep in sync with it. (The per-component window.Freeday<Name> enhancer namespaces are
4
+ // intentionally not typed here; no consumer reconstructs those.)
5
+
6
+ export interface FreedayToastOptions {
7
+ /** Accent + ARIA role. Omitted = neutral. 'danger' uses role="alert"; the rest role="status". */
8
+ variant?: 'success' | 'warning' | 'danger' | 'info';
9
+ title?: string;
10
+ message?: string;
11
+ /** Milliseconds before auto-dismiss. Default 4000; 0 = sticky. */
12
+ timeout?: number;
13
+ }
14
+
15
+ export interface FreedayGlobal {
16
+ /** Show a transient toast in a live region (created on first use). Returns the toast element. */
17
+ toast(opts?: FreedayToastOptions): HTMLElement;
18
+ }
19
+
20
+ declare global {
21
+ interface Window {
22
+ Freeday?: FreedayGlobal;
23
+ }
24
+ }
package/dist/freeday.js CHANGED
@@ -2340,7 +2340,11 @@
2340
2340
  setHighlight(-1);
2341
2341
  if (openClose === close) openClose = null;
2342
2342
  }
2343
- function choose(opt) {
2343
+ // Mark an option selected + reflect it in the button. `silent` skips the change
2344
+ // event, close, and focus — used by the programmatic setValue() below so a host
2345
+ // (e.g. a Blazor/framework wrapper) can push a value in without echoing an event
2346
+ // back to itself or stealing focus. User-driven picks go through choose() (loud).
2347
+ function select(opt, silent) {
2344
2348
  options.forEach(function (other) {
2345
2349
  var on = other === opt;
2346
2350
  other.setAttribute('aria-selected', on ? 'true' : 'false');
@@ -2350,12 +2354,24 @@
2350
2354
  root.setAttribute('data-value', opt.getAttribute('data-value') || '');
2351
2355
  valueEl.textContent = optionLabel(opt);
2352
2356
  valueEl.classList.remove('fdy-combo__value--placeholder');
2353
- root.dispatchEvent(new CustomEvent('fdy-change', {
2354
- bubbles: true,
2355
- detail: { value: opt.getAttribute('data-value') }
2356
- }));
2357
- close();
2358
- button.focus();
2357
+ if (!silent) {
2358
+ root.dispatchEvent(new CustomEvent('fdy-change', {
2359
+ bubbles: true,
2360
+ detail: { value: opt.getAttribute('data-value') }
2361
+ }));
2362
+ close();
2363
+ button.focus();
2364
+ }
2365
+ }
2366
+ function choose(opt) { select(opt, false); }
2367
+ // Programmatic set (no event, no focus change). No-op if no option matches.
2368
+ function setValue(value) {
2369
+ for (var i = 0; i < options.length; i++) {
2370
+ if ((options[i].getAttribute('data-value') || '') === String(value)) {
2371
+ select(options[i], true);
2372
+ return;
2373
+ }
2374
+ }
2359
2375
  }
2360
2376
 
2361
2377
  button.addEventListener('click', function () {
@@ -2416,6 +2432,9 @@
2416
2432
  root.addEventListener('focusout', function (e) {
2417
2433
  if (!root.contains(e.relatedTarget)) close();
2418
2434
  });
2435
+
2436
+ // Per-instance API for programmatic control (host wrappers / two-way binding).
2437
+ root._fdyCombo = { setValue: setValue };
2419
2438
  }
2420
2439
 
2421
2440
  function initAll(context) {
@@ -2439,7 +2458,12 @@
2439
2458
  initAll();
2440
2459
  }
2441
2460
 
2442
- window.FreedayCombo = { init: initCombo, initAll: initAll };
2461
+ // Programmatic set on an already-initialised combo root (no fdy-change dispatched).
2462
+ function setValue(root, value) {
2463
+ if (root && root._fdyCombo) root._fdyCombo.setValue(value);
2464
+ }
2465
+
2466
+ window.FreedayCombo = { init: initCombo, initAll: initAll, setValue: setValue };
2443
2467
  })();
2444
2468
 
2445
2469
  /* Freeday — slider value binding (optional, zero-dependency).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,17 +25,18 @@
25
25
  "adapters",
26
26
  "tokens/tokens.json",
27
27
  "tokens/breakpoints.mjs",
28
+ "tokens/breakpoints.d.ts",
28
29
  "README.md",
29
30
  "README.id.md",
30
31
  "CHANGELOG.md"
31
32
  ],
32
33
  "exports": {
33
- ".": "./dist/freeday.js",
34
- "./css": "./dist/freeday.bundle.css",
35
- "./css/components": "./dist/freeday.css",
36
- "./tokens": "./dist/freeday.tokens.css",
37
- "./breakpoints": "./tokens/breakpoints.mjs",
38
- "./enhancers/*": "./dist/freeday-*.js",
34
+ ".": { "types": "./dist/freeday.d.ts", "default": "./dist/freeday.js" },
35
+ "./css": { "types": "./dist/asset.d.ts", "default": "./dist/freeday.bundle.css" },
36
+ "./css/components": { "types": "./dist/asset.d.ts", "default": "./dist/freeday.css" },
37
+ "./tokens": { "types": "./dist/asset.d.ts", "default": "./dist/freeday.tokens.css" },
38
+ "./breakpoints": { "types": "./tokens/breakpoints.d.ts", "default": "./tokens/breakpoints.mjs" },
39
+ "./enhancers/*": { "types": "./dist/asset.d.ts", "default": "./dist/freeday-*.js" },
39
40
  "./vue": {
40
41
  "types": "./adapters/vue/index.d.ts",
41
42
  "default": "./adapters/vue/index.js"
@@ -62,12 +63,17 @@
62
63
  "scripts": {
63
64
  "build": "node tokens/build.mjs",
64
65
  "test": "node --test",
66
+ "test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs",
65
67
  "prepack": "node tokens/build.mjs",
66
68
  "version": "node tokens/build.mjs && git add dist",
67
69
  "typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"
68
70
  },
69
71
  "devDependencies": {
70
72
  "@types/react": "^19.2.17",
71
- "typescript": "^7.0.2"
73
+ "esbuild": "^0.28.1",
74
+ "react": "^19.2.8",
75
+ "react-dom": "^19.2.8",
76
+ "typescript": "^7.0.2",
77
+ "vue": "^3.5.40"
72
78
  }
73
79
  }