@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/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).
@@ -3295,16 +3319,26 @@
3295
3319
  })();
3296
3320
 
3297
3321
  /* Freeday — toast API (optional, zero-dependency).
3298
- * Freeday.toast({ variant, title, message, timeout }) shows a transient
3322
+ * Freeday.toast({ variant, title, message, timeout, key }) shows a transient
3299
3323
  * notification in a live region (created on first use). Returns the element.
3300
3324
  * variant: 'success' | 'warning' | 'danger' | 'info' (default: neutral)
3301
3325
  * timeout: ms before auto-dismiss (default 4000; 0 = sticky)
3326
+ * key: optional stable id — a new toast with the same key REPLACES the
3327
+ * existing one in place instead of stacking a duplicate (e.g. a burst
3328
+ * of identical failures shows one toast, refreshed).
3329
+ * Freeday.dismiss(nodeOrKey) closes a toast early (pass the returned element or its key).
3302
3330
  * Danger toasts use role="alert"; others role="status".
3303
3331
  */
3304
3332
  (function () {
3305
3333
  'use strict';
3306
3334
 
3307
3335
  var region = null;
3336
+ var keyed = {}; // key -> current node, for replace-in-place + dismiss-by-key
3337
+
3338
+ function forget(node) {
3339
+ var k = node && node._fdyKey;
3340
+ if (k != null && keyed[k] === node) delete keyed[k];
3341
+ }
3308
3342
 
3309
3343
  function ensureRegion() {
3310
3344
  if (region && document.body.contains(region)) return region;
@@ -3320,7 +3354,9 @@
3320
3354
  }
3321
3355
 
3322
3356
  function dismiss(toast) {
3357
+ if (typeof toast === 'string') toast = keyed[toast]; // dismiss by key
3323
3358
  if (!toast || toast.dataset.leaving === '1') return;
3359
+ forget(toast);
3324
3360
  toast.dataset.leaving = '1';
3325
3361
  toast.classList.add('is-leaving');
3326
3362
  var remove = function () { if (toast.parentNode) toast.parentNode.removeChild(toast); };
@@ -3360,6 +3396,14 @@
3360
3396
  node.appendChild(accent);
3361
3397
  node.appendChild(body);
3362
3398
  node.appendChild(close);
3399
+
3400
+ // Replace-in-place: a same-key toast removes the previous one so bursts don't stack.
3401
+ if (opts.key != null) {
3402
+ var prev = keyed[opts.key];
3403
+ if (prev && prev.parentNode) prev.parentNode.removeChild(prev);
3404
+ node._fdyKey = opts.key;
3405
+ keyed[opts.key] = node;
3406
+ }
3363
3407
  region.appendChild(node);
3364
3408
 
3365
3409
  var timeout = opts.timeout == null ? 4000 : opts.timeout;
@@ -3377,6 +3421,7 @@
3377
3421
 
3378
3422
  window.Freeday = window.Freeday || {};
3379
3423
  window.Freeday.toast = toast;
3424
+ window.Freeday.dismiss = dismiss; // dismiss(nodeOrKey)
3380
3425
  })();
3381
3426
 
3382
3427
  /* Freeday — tree checkbox cascade (optional, zero-dependency).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/base.css CHANGED
@@ -15,6 +15,11 @@ a { color: var(--color-primary); }
15
15
  :focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-width); border-radius: var(--radius-xs); }
16
16
  :where(button, input, select, textarea) { font: inherit; }
17
17
  .fdy-visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
18
+ /* Opt-in list reset. base.css is intentionally a *light* reset — it does NOT strip list/paragraph
19
+ * margins (see the kit's own list components, which each reset themselves). If you run a utility
20
+ * framework with its preflight OFF (the recommended pairing), a semantic <ul>/<ol> keeps native
21
+ * bullets + a 40px indent; add this class (or a Freeday list component) to normalise it. */
22
+ .fdy-list-reset { list-style: none; margin: 0; padding: 0; }
18
23
  @media (prefers-reduced-motion: reduce) {
19
24
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
20
25
  }
@@ -6,3 +6,16 @@
6
6
  .fdy-avatar-group{display:inline-flex;}
7
7
  .fdy-avatar-group .fdy-avatar{border:2px solid var(--color-surface);margin-left:calc(var(--space-2)*-1);}
8
8
  .fdy-avatar-group .fdy-avatar:first-child{margin-left:0;}
9
+ /* Decorative identity tones — a soft tint of a categorical chart colour with a theme-aware,
10
+ * text-leaning foreground so initials keep WCAG AA (verified >=4.5:1 in light & dark by
11
+ * test/contrast.test.mjs). Use to disambiguate same-initial avatars (e.g. many "PT …" names):
12
+ * hash a stable 1..8 index off the FULL name in your app, then apply .fdy-avatar--tone-<n>. */
13
+ .fdy-avatar--tone-1,.fdy-avatar--tone-2,.fdy-avatar--tone-3,.fdy-avatar--tone-4,.fdy-avatar--tone-5,.fdy-avatar--tone-6,.fdy-avatar--tone-7,.fdy-avatar--tone-8{background:color-mix(in srgb,var(--_fdy-avatar-tone) 18%,var(--color-surface));color:color-mix(in srgb,var(--_fdy-avatar-tone) 50%,var(--color-text));}
14
+ .fdy-avatar--tone-1{--_fdy-avatar-tone:var(--chart-1);}
15
+ .fdy-avatar--tone-2{--_fdy-avatar-tone:var(--chart-2);}
16
+ .fdy-avatar--tone-3{--_fdy-avatar-tone:var(--chart-3);}
17
+ .fdy-avatar--tone-4{--_fdy-avatar-tone:var(--chart-4);}
18
+ .fdy-avatar--tone-5{--_fdy-avatar-tone:var(--chart-5);}
19
+ .fdy-avatar--tone-6{--_fdy-avatar-tone:var(--chart-6);}
20
+ .fdy-avatar--tone-7{--_fdy-avatar-tone:var(--chart-7);}
21
+ .fdy-avatar--tone-8{--_fdy-avatar-tone:var(--chart-8);}
@@ -3,6 +3,10 @@
3
3
  .fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
4
4
  .fdy-card--interactive{cursor:pointer;}
5
5
  .fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
6
+ /* When the whole card IS the control — a real <button> so the row is keyboard-reachable and
7
+ * announced — reset the UA button box. Note: it does NOT reset background/border, because
8
+ * .fdy-card already sets its own surface + border (resetting them here would strip the card). */
9
+ .fdy-card--button{display:block;width:100%;text-align:inherit;color:inherit;appearance:none;-webkit-appearance:none;}
6
10
  .fdy-card__body{padding:var(--space-5);}
7
11
  .fdy-card__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0 0 var(--space-1);}
8
12
  .fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
@@ -3,5 +3,10 @@
3
3
  .fdy-skeleton--text{height:.8rem;margin:.3rem 0;}
4
4
  .fdy-skeleton--title{height:1.2rem;width:40%;}
5
5
  .fdy-skeleton--circle{border-radius:var(--radius-full);}
6
+ /* Size-matched to .fdy-avatar (2.5rem) and its --sm/--lg, so a skeleton reserves the exact avatar
7
+ * box and data landing causes no layout shift (a plain --circle has no dimensions of its own). */
8
+ .fdy-skeleton--avatar{width:2.5rem;height:2.5rem;border-radius:var(--radius-full);}
9
+ .fdy-skeleton--avatar-sm{width:2rem;height:2rem;border-radius:var(--radius-full);}
10
+ .fdy-skeleton--avatar-lg{width:3.5rem;height:3.5rem;border-radius:var(--radius-full);}
6
11
  @keyframes fdy-skeleton{to{background-position:-200% 0;}}
7
12
  @media (prefers-reduced-motion:reduce){.fdy-skeleton{animation:none;}}
@@ -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
  })();
@@ -1,14 +1,24 @@
1
1
  /* Freeday — toast API (optional, zero-dependency).
2
- * Freeday.toast({ variant, title, message, timeout }) shows a transient
2
+ * Freeday.toast({ variant, title, message, timeout, key }) shows a transient
3
3
  * notification in a live region (created on first use). Returns the element.
4
4
  * variant: 'success' | 'warning' | 'danger' | 'info' (default: neutral)
5
5
  * timeout: ms before auto-dismiss (default 4000; 0 = sticky)
6
+ * key: optional stable id — a new toast with the same key REPLACES the
7
+ * existing one in place instead of stacking a duplicate (e.g. a burst
8
+ * of identical failures shows one toast, refreshed).
9
+ * Freeday.dismiss(nodeOrKey) closes a toast early (pass the returned element or its key).
6
10
  * Danger toasts use role="alert"; others role="status".
7
11
  */
8
12
  (function () {
9
13
  'use strict';
10
14
 
11
15
  var region = null;
16
+ var keyed = {}; // key -> current node, for replace-in-place + dismiss-by-key
17
+
18
+ function forget(node) {
19
+ var k = node && node._fdyKey;
20
+ if (k != null && keyed[k] === node) delete keyed[k];
21
+ }
12
22
 
13
23
  function ensureRegion() {
14
24
  if (region && document.body.contains(region)) return region;
@@ -24,7 +34,9 @@
24
34
  }
25
35
 
26
36
  function dismiss(toast) {
37
+ if (typeof toast === 'string') toast = keyed[toast]; // dismiss by key
27
38
  if (!toast || toast.dataset.leaving === '1') return;
39
+ forget(toast);
28
40
  toast.dataset.leaving = '1';
29
41
  toast.classList.add('is-leaving');
30
42
  var remove = function () { if (toast.parentNode) toast.parentNode.removeChild(toast); };
@@ -64,6 +76,14 @@
64
76
  node.appendChild(accent);
65
77
  node.appendChild(body);
66
78
  node.appendChild(close);
79
+
80
+ // Replace-in-place: a same-key toast removes the previous one so bursts don't stack.
81
+ if (opts.key != null) {
82
+ var prev = keyed[opts.key];
83
+ if (prev && prev.parentNode) prev.parentNode.removeChild(prev);
84
+ node._fdyKey = opts.key;
85
+ keyed[opts.key] = node;
86
+ }
67
87
  region.appendChild(node);
68
88
 
69
89
  var timeout = opts.timeout == null ? 4000 : opts.timeout;
@@ -81,4 +101,5 @@
81
101
 
82
102
  window.Freeday = window.Freeday || {};
83
103
  window.Freeday.toast = toast;
104
+ window.Freeday.dismiss = dismiss; // dismiss(nodeOrKey)
84
105
  })();