@cahyo-dimas/freeday 3.13.0 → 3.14.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 +63 -0
  2. package/COMPONENTS.md +45 -0
  3. package/README.id.md +1 -1
  4. package/README.md +1 -1
  5. package/adapters/react/components/FdyDrawer.tsx +4 -0
  6. package/adapters/react/index.d.ts +2 -0
  7. package/adapters/vue/components/FdyDrawer.vue +4 -0
  8. package/adapters/vue/index.d.ts +2 -0
  9. package/dist/freeday-app-shell.js +1 -0
  10. package/dist/freeday-autocomplete.js +1 -0
  11. package/dist/freeday-carousel.js +1 -0
  12. package/dist/freeday-cascade.js +1 -0
  13. package/dist/freeday-cfl.js +2 -0
  14. package/dist/freeday-chart.js +1 -0
  15. package/dist/freeday-chip.js +2 -0
  16. package/dist/freeday-datepicker.js +2 -0
  17. package/dist/freeday-datetime.js +1 -0
  18. package/dist/freeday-drawer.js +2 -0
  19. package/dist/freeday-form.js +1 -0
  20. package/dist/freeday-mask.js +2 -0
  21. package/dist/freeday-menu.js +1 -0
  22. package/dist/freeday-number.js +1 -0
  23. package/dist/freeday-rating.js +1 -0
  24. package/dist/freeday-search.js +1 -0
  25. package/dist/freeday-select.js +1 -0
  26. package/dist/freeday-slider.js +1 -0
  27. package/dist/freeday-sortable.js +222 -0
  28. package/dist/freeday-stepper.js +1 -0
  29. package/dist/freeday-table.js +1 -0
  30. package/dist/freeday-tabs.js +1 -0
  31. package/dist/freeday-timepicker.js +1 -0
  32. package/dist/freeday-tree.js +1 -0
  33. package/dist/freeday-unhydrated.js +9 -2
  34. package/dist/freeday-upload.js +1 -0
  35. package/dist/freeday.bundle.css +18 -0
  36. package/dist/freeday.css +18 -0
  37. package/dist/freeday.js +261 -2
  38. package/docs/agent-onboarding.md +2 -0
  39. package/docs/getting-started.md +1 -1
  40. package/docs/integrations.md +1 -0
  41. package/package.json +2 -2
  42. package/src/components/sortable.css +17 -0
@@ -77,6 +77,7 @@
77
77
 
78
78
  function initTable(root) {
79
79
  if (root.dataset.fdyTableReady === '1') return;
80
+ if (root.closest('[data-fdy-ignore]')) return;
80
81
  var table = root.querySelector('table.fdy-table');
81
82
  var tbody = table && table.querySelector('tbody');
82
83
  if (!table || !tbody) return;
@@ -16,6 +16,7 @@
16
16
 
17
17
  function initTabs(root) {
18
18
  if (root.dataset.fdyTabsReady === '1') return;
19
+ if (root.closest('[data-fdy-ignore]')) return;
19
20
  var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
20
21
  if (tabs.length === 0) return;
21
22
  root.dataset.fdyTabsReady = '1';
@@ -43,6 +43,7 @@
43
43
 
44
44
  function initTime(wrap) {
45
45
  if (wrap.dataset.fdyTpReady === '1') return;
46
+ if (wrap.closest('[data-fdy-ignore]')) return;
46
47
  wrap.dataset.fdyTpReady = '1';
47
48
  wrap.classList.add('fdy-timepicker');
48
49
 
@@ -66,6 +66,7 @@
66
66
 
67
67
  function initTree(tree) {
68
68
  if (tree.dataset.fdyTreeReady === '1') return;
69
+ if (tree.closest('[data-fdy-ignore]')) return;
69
70
  tree.dataset.fdyTreeReady = '1';
70
71
  Array.prototype.forEach.call(tree.querySelectorAll('input[type="checkbox"]'), function (box) {
71
72
  // A branch checkbox lives inside <summary>; keep its click from toggling the branch.
@@ -15,6 +15,11 @@
15
15
  * namespace that owns it. A hook whose enhancer is not loaded is skipped: that is a different
16
16
  * mistake, and this message would misname it. test/docs.test.mjs fails if an enhancer gains a
17
17
  * ready flag this table does not list.
18
+ *
19
+ * `data-fdy-ignore` on an element or any ancestor is the sanctioned way out: every enhancer skips
20
+ * that subtree, and this stays quiet about it. It is for markup whose behaviour something else
21
+ * owns: the kit's own Vue/React FdyDrawer, and kit classes an app uses for their look
22
+ * alone (CATATAN-FREEDAY §14: a chip matrix the app drives itself).
18
23
  */
19
24
  (function () {
20
25
  'use strict';
@@ -43,6 +48,7 @@
43
48
  ['[data-fdy-search]', 'fdySearchReady', 'FreedaySearch'],
44
49
  ['[data-fdy-combo]', 'fdyComboReady', 'FreedayCombo'],
45
50
  ['[data-fdy-slider]', 'fdySliderReady', 'FreedaySlider'],
51
+ ['[data-fdy-sortable]', 'fdySortableReady', 'FreedaySortable'],
46
52
  ['[data-fdy-stepper]', 'fdyStepperReady', 'FreedayStepper'],
47
53
  ['[data-fdy-table]', 'fdyTableReady', 'FreedayTable'],
48
54
  ['[data-fdy-tabs]', 'fdyTabsReady', 'FreedayTabs'],
@@ -59,12 +65,13 @@
59
65
  var hook = HOOKS[i];
60
66
  if (!window[hook[2]]) continue;
61
67
  var el = e.target.closest(hook[0]);
62
- if (!el || el.dataset[hook[1]] === '1' || warned.has(el)) continue;
68
+ if (!el || el.dataset[hook[1]] === '1' || warned.has(el) || el.closest('[data-fdy-ignore]')) continue;
63
69
  warned.add(el);
64
70
  console.warn('Freeday: this ' + hook[0] + ' was never enhanced, so it does nothing. Either it is '
65
71
  + 'outside every initAll() scope (Vue/React: inside the element given to useFreeday(); Blazor: '
66
72
  + 'FreedayBlazor.initAll after render; anything else: ' + hook[2] + '.initAll(el) once it is in '
67
- + 'the DOM), or its markup lacks a part the enhancer needs (see COMPONENTS.md).', el);
73
+ + 'the DOM), or its markup lacks a part the enhancer needs (see COMPONENTS.md). If the app '
74
+ + 'drives this element itself on purpose, mark it or an ancestor data-fdy-ignore.', el);
68
75
  }
69
76
  }
70
77
 
@@ -228,6 +228,7 @@
228
228
 
229
229
  function initDropzone(zone) {
230
230
  if (zone.dataset.fdyUploadReady === '1') return;
231
+ if (zone.closest('[data-fdy-ignore]')) return;
231
232
  zone.dataset.fdyUploadReady = '1';
232
233
 
233
234
  var input = zone.querySelector('input[type="file"]');
@@ -2900,6 +2900,24 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2900
2900
  .fdy-slider__value{font-family:var(--font-mono);font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--color-primary-strong);font-variant-numeric:tabular-nums;}
2901
2901
  .fdy-slider__scale{display:flex;justify-content:space-between;font-size:var(--text-xs);color:var(--color-text-muted);font-variant-numeric:tabular-nums;}
2902
2902
 
2903
+ /* Freeday. Sortable list (freeday-sortable.js). The kit styles the handle and the drag states only;
2904
+ * the list itself is whatever it already was: .fdy-list, a table body, plain rows.
2905
+ *
2906
+ * The handle is a control-height square so a thumb can hold it on a phone, and `touch-action:none`
2907
+ * keeps that drag from scrolling the page. Its glyph is the only thing marking it as a control, so
2908
+ * it takes --color-text-muted, not --color-text-subtle: a non-text control still owes 3:1. */
2909
+ .fdy-sortable__handle{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--control-h);height:var(--control-h);padding:0;border:0;border-radius:var(--radius-md);background:transparent;color:var(--color-text-muted);cursor:grab;touch-action:none;}
2910
+ .fdy-sortable__handle:hover{background:var(--color-surface-2);color:var(--color-text);}
2911
+ .fdy-sortable__handle:active{cursor:grabbing;}
2912
+ .fdy-sortable__handle[aria-disabled="true"]{cursor:not-allowed;opacity:.4;}
2913
+ .fdy-sortable__handle[aria-disabled="true"]:hover{background:transparent;color:var(--color-text-muted);}
2914
+ .fdy-sortable.is-sorting{-webkit-user-select:none;user-select:none;}
2915
+ .fdy-sortable > .is-dragging{opacity:.45;}
2916
+ /* The landing line. On a table row it is drawn on the cells: a box-shadow on a <tr> is not painted
2917
+ * by every engine the kit supports. */
2918
+ .fdy-sortable > :not(tr).is-drop-before,.fdy-sortable > tr.is-drop-before > *{box-shadow:inset 0 var(--focus-ring-width) 0 var(--color-primary);}
2919
+ .fdy-sortable > :not(tr).is-drop-after,.fdy-sortable > tr.is-drop-after > *{box-shadow:inset 0 calc(var(--focus-ring-width) * -1) 0 var(--color-primary);}
2920
+
2903
2921
  /* Freeday. Spinner */
2904
2922
  .fdy-spinner{display:inline-block;width:1.5rem;height:1.5rem;border:2.5px solid var(--color-border-strong);border-top-color:var(--color-primary);border-radius:var(--radius-full);animation:fdy-spin .7s linear infinite;}
2905
2923
  .fdy-spinner--sm{width:1rem;height:1rem;border-width:2px;}
package/dist/freeday.css CHANGED
@@ -1723,6 +1723,24 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1723
1723
  .fdy-slider__value{font-family:var(--font-mono);font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--color-primary-strong);font-variant-numeric:tabular-nums;}
1724
1724
  .fdy-slider__scale{display:flex;justify-content:space-between;font-size:var(--text-xs);color:var(--color-text-muted);font-variant-numeric:tabular-nums;}
1725
1725
 
1726
+ /* Freeday. Sortable list (freeday-sortable.js). The kit styles the handle and the drag states only;
1727
+ * the list itself is whatever it already was: .fdy-list, a table body, plain rows.
1728
+ *
1729
+ * The handle is a control-height square so a thumb can hold it on a phone, and `touch-action:none`
1730
+ * keeps that drag from scrolling the page. Its glyph is the only thing marking it as a control, so
1731
+ * it takes --color-text-muted, not --color-text-subtle: a non-text control still owes 3:1. */
1732
+ .fdy-sortable__handle{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--control-h);height:var(--control-h);padding:0;border:0;border-radius:var(--radius-md);background:transparent;color:var(--color-text-muted);cursor:grab;touch-action:none;}
1733
+ .fdy-sortable__handle:hover{background:var(--color-surface-2);color:var(--color-text);}
1734
+ .fdy-sortable__handle:active{cursor:grabbing;}
1735
+ .fdy-sortable__handle[aria-disabled="true"]{cursor:not-allowed;opacity:.4;}
1736
+ .fdy-sortable__handle[aria-disabled="true"]:hover{background:transparent;color:var(--color-text-muted);}
1737
+ .fdy-sortable.is-sorting{-webkit-user-select:none;user-select:none;}
1738
+ .fdy-sortable > .is-dragging{opacity:.45;}
1739
+ /* The landing line. On a table row it is drawn on the cells: a box-shadow on a <tr> is not painted
1740
+ * by every engine the kit supports. */
1741
+ .fdy-sortable > :not(tr).is-drop-before,.fdy-sortable > tr.is-drop-before > *{box-shadow:inset 0 var(--focus-ring-width) 0 var(--color-primary);}
1742
+ .fdy-sortable > :not(tr).is-drop-after,.fdy-sortable > tr.is-drop-after > *{box-shadow:inset 0 calc(var(--focus-ring-width) * -1) 0 var(--color-primary);}
1743
+
1726
1744
  /* Freeday. Spinner */
1727
1745
  .fdy-spinner{display:inline-block;width:1.5rem;height:1.5rem;border:2.5px solid var(--color-border-strong);border-top-color:var(--color-primary);border-radius:var(--radius-full);animation:fdy-spin .7s linear infinite;}
1728
1746
  .fdy-spinner--sm{width:1rem;height:1rem;border-width:2px;}
package/dist/freeday.js CHANGED
@@ -48,6 +48,7 @@
48
48
 
49
49
  function initShell(app) {
50
50
  if (app.dataset.fdyAppShellReady === '1') return;
51
+ if (app.closest('[data-fdy-ignore]')) return;
51
52
  app.dataset.fdyAppShellReady = '1';
52
53
 
53
54
  var toggle = app.querySelector('.fdy-app__navtoggle');
@@ -246,6 +247,7 @@
246
247
 
247
248
  function initAutocomplete(root) {
248
249
  if (root.dataset.fdyAcReady === '1') return;
250
+ if (root.closest('[data-fdy-ignore]')) return;
249
251
  root.dataset.fdyAcReady = '1';
250
252
 
251
253
  var input = root.querySelector('input[role="combobox"]') || root.querySelector('input');
@@ -660,6 +662,7 @@
660
662
 
661
663
  function initCarousel(root) {
662
664
  if (root.dataset.fdyCarouselReady === '1') return;
665
+ if (root.closest('[data-fdy-ignore]')) return;
663
666
  root.dataset.fdyCarouselReady = '1';
664
667
 
665
668
  var viewport = root.querySelector('.fdy-carousel__viewport');
@@ -840,6 +843,7 @@
840
843
 
841
844
  function initCascade(wrap) {
842
845
  if (wrap.dataset.fdyCascadeReady === '1') return;
846
+ if (wrap.closest('[data-fdy-ignore]')) return;
843
847
  wrap.dataset.fdyCascadeReady = '1';
844
848
  wrap.classList.add('fdy-cascade');
845
849
 
@@ -1305,6 +1309,7 @@
1305
1309
 
1306
1310
  function bindGroup(group) {
1307
1311
  if (group.dataset.fdyCflBound === '1') return;
1312
+ if (group.closest('[data-fdy-ignore]')) return;
1308
1313
  var dialog = document.getElementById(group.getAttribute('data-fdy-cfl'));
1309
1314
  if (!dialog) return;
1310
1315
  initDialog(dialog);
@@ -1316,6 +1321,7 @@
1316
1321
 
1317
1322
  function bindOpener(opener) {
1318
1323
  if (opener.dataset.fdyCflBound === '1') return;
1324
+ if (opener.closest('[data-fdy-ignore]')) return;
1319
1325
  var dialog = document.getElementById(opener.getAttribute('data-fdy-cfl-open'));
1320
1326
  if (!dialog) return;
1321
1327
  initDialog(dialog);
@@ -1834,6 +1840,7 @@
1834
1840
  // Auto-init renders once; explicit update() re-renders after data changes.
1835
1841
  function initChart(el) {
1836
1842
  if (el.dataset.fdyChartReady === '1') return;
1843
+ if (el.closest('[data-fdy-ignore]')) return;
1837
1844
  el.dataset.fdyChartReady = '1';
1838
1845
  renderChart(el);
1839
1846
  }
@@ -1874,6 +1881,7 @@
1874
1881
 
1875
1882
  function initRemove(btn) {
1876
1883
  if (btn.dataset.fdyChipRmReady === '1') return;
1884
+ if (btn.closest('[data-fdy-ignore]')) return;
1877
1885
  btn.dataset.fdyChipRmReady = '1';
1878
1886
  btn.addEventListener('click', function () {
1879
1887
  var chip = btn.closest('.fdy-chip');
@@ -1897,6 +1905,7 @@
1897
1905
 
1898
1906
  function initGroup(group) {
1899
1907
  if (group.dataset.fdyChipsReady === '1') return;
1908
+ if (group.closest('[data-fdy-ignore]')) return;
1900
1909
  group.dataset.fdyChipsReady = '1';
1901
1910
  var single = group.hasAttribute('data-single');
1902
1911
  if (!group.getAttribute('role')) group.setAttribute('role', 'group');
@@ -2228,6 +2237,7 @@
2228
2237
 
2229
2238
  function initPicker(wrap) {
2230
2239
  if (wrap.dataset.fdyDpReady === '1') return wrap._fdyDp || null;
2240
+ if (wrap.closest('[data-fdy-ignore]')) return null;
2231
2241
  wrap.dataset.fdyDpReady = '1';
2232
2242
  wrap.classList.add('fdy-datepicker');
2233
2243
 
@@ -2757,6 +2767,7 @@
2757
2767
 
2758
2768
  function initRange(root) {
2759
2769
  if (root.dataset.fdyDrReady === '1') return;
2770
+ if (root.closest('[data-fdy-ignore]')) return;
2760
2771
  root.dataset.fdyDrReady = '1';
2761
2772
  var fromEl = root.querySelector('[data-fdy-datepicker][data-role="from"]');
2762
2773
  var toEl = root.querySelector('[data-fdy-datepicker][data-role="to"]');
@@ -2846,6 +2857,7 @@
2846
2857
 
2847
2858
  function initDatetime(wrap) {
2848
2859
  if (wrap.dataset.fdyDtReady === '1') return;
2860
+ if (wrap.closest('[data-fdy-ignore]')) return;
2849
2861
  wrap.dataset.fdyDtReady = '1';
2850
2862
  wrap.classList.add('fdy-datetimepicker');
2851
2863
 
@@ -2924,6 +2936,7 @@
2924
2936
 
2925
2937
  function initDrawer(dialog) {
2926
2938
  if (dialog.dataset.fdyDrawerReady === '1') return;
2939
+ if (dialog.closest('[data-fdy-ignore]')) return;
2927
2940
  dialog.dataset.fdyDrawerReady = '1';
2928
2941
  // Click on the backdrop (target is the dialog box itself) closes it.
2929
2942
  dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
@@ -2934,6 +2947,7 @@
2934
2947
 
2935
2948
  function bindTrigger(btn) {
2936
2949
  if (btn.dataset.fdyDrawerBound === '1') return;
2950
+ if (btn.closest('[data-fdy-ignore]')) return;
2937
2951
  var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
2938
2952
  if (!dialog) return;
2939
2953
  initDrawer(dialog);
@@ -3151,6 +3165,7 @@
3151
3165
 
3152
3166
  function initForm(form) {
3153
3167
  if (form.dataset.fdyFormReady === '1') return;
3168
+ if (form.closest('[data-fdy-ignore]')) return;
3154
3169
  form.dataset.fdyFormReady = '1';
3155
3170
  form.setAttribute('novalidate', '');
3156
3171
  var eager = form.hasAttribute('data-fdy-eager');
@@ -3335,6 +3350,7 @@
3335
3350
 
3336
3351
  function initMask(el) {
3337
3352
  if (el.dataset.fdyMaskReady === '1') return;
3353
+ if (el.closest('[data-fdy-ignore]')) return;
3338
3354
  el.dataset.fdyMaskReady = '1';
3339
3355
  var pattern = el.getAttribute('data-fdy-mask') || '';
3340
3356
  if (!pattern) return;
@@ -3357,6 +3373,7 @@
3357
3373
 
3358
3374
  function initReveal(input) {
3359
3375
  if (input.dataset.fdyPwReady === '1') return;
3376
+ if (input.closest('[data-fdy-ignore]')) return;
3360
3377
  input.dataset.fdyPwReady = '1';
3361
3378
 
3362
3379
  var group = document.createElement('span');
@@ -3420,6 +3437,7 @@
3420
3437
 
3421
3438
  function initMenu(wrap) {
3422
3439
  if (wrap.dataset.fdyMenuReady === '1') return;
3440
+ if (wrap.closest('[data-fdy-ignore]')) return;
3423
3441
  wrap.dataset.fdyMenuReady = '1';
3424
3442
  var trigger = wrap.querySelector('[aria-haspopup="menu"]');
3425
3443
  var menu = wrap.querySelector('.fdy-menu, [role="menu"]');
@@ -3539,6 +3557,7 @@
3539
3557
 
3540
3558
  function initNumber(root) {
3541
3559
  if (root.dataset.fdyNumberReady === '1') return;
3560
+ if (root.closest('[data-fdy-ignore]')) return;
3542
3561
 
3543
3562
  var input = root.querySelector('input[type="number"]');
3544
3563
  var buttons = Array.prototype.slice.call(root.querySelectorAll('[data-fdy-number-step]'));
@@ -3717,6 +3736,7 @@
3717
3736
 
3718
3737
  function initRating(root) {
3719
3738
  if (root.dataset.fdyRatingReady === '1') return;
3739
+ if (root.closest('[data-fdy-ignore]')) return;
3720
3740
  root.dataset.fdyRatingReady = '1';
3721
3741
 
3722
3742
  var stars = Array.prototype.slice.call(root.querySelectorAll('.fdy-rating__star'));
@@ -3787,6 +3807,7 @@
3787
3807
 
3788
3808
  function initSearch(root) {
3789
3809
  if (root.dataset.fdySearchReady === '1') return;
3810
+ if (root.closest('[data-fdy-ignore]')) return;
3790
3811
  var input = root.querySelector('input');
3791
3812
  if (!input) return;
3792
3813
  root.dataset.fdySearchReady = '1';
@@ -3873,6 +3894,7 @@
3873
3894
 
3874
3895
  function initCombo(root) {
3875
3896
  if (root.dataset.fdyComboReady === '1') return; // idempotent
3897
+ if (root.closest('[data-fdy-ignore]')) return;
3876
3898
  var button = root.querySelector('.fdy-combo__button');
3877
3899
  var listbox = root.querySelector('.fdy-combo__listbox');
3878
3900
  var valueEl = root.querySelector('.fdy-combo__value');
@@ -4085,6 +4107,7 @@
4085
4107
 
4086
4108
  function initSlider(input) {
4087
4109
  if (input.dataset.fdySliderReady === '1') return;
4110
+ if (input.closest('[data-fdy-ignore]')) return;
4088
4111
  input.dataset.fdySliderReady = '1';
4089
4112
  var out = input.id ? document.querySelector('output[for="' + input.id + '"]') : null;
4090
4113
  if (!out) return;
@@ -4109,6 +4132,229 @@
4109
4132
  window.FreedaySlider = { init: initSlider, initAll: initAll };
4110
4133
  })();
4111
4134
 
4135
+ /* Freeday, sortable list enhancer (optional, zero-dependency).
4136
+ * Reorder the children of a list by dragging a handle, or by focusing it and pressing ↑ / ↓. The
4137
+ * keyboard path is not an extra: drag-only reordering locks out every keyboard and screen-reader
4138
+ * user from a settings screen, and that path is the part each app used to write for itself
4139
+ * (CATATAN-FREEDAY §15).
4140
+ *
4141
+ * Markup contract:
4142
+ * <ul class="fdy-sortable" data-fdy-sortable> (or a <tbody>, or a <div>)
4143
+ * <li data-key="ketua" data-label="Ketua">
4144
+ * <button class="fdy-sortable__handle" type="button">…icon…</button> Ketua
4145
+ * </li>
4146
+ * <li data-key="sistem" data-fdy-sort-fixed>…</li> (never moves, and nothing passes it)
4147
+ * </ul>
4148
+ * The items are the container's element children. `data-key` names each in the event; `data-label`
4149
+ * is what the handle's default aria-label and the announcement call it (the item text otherwise).
4150
+ * `data-disabled` on the container switches reordering off, e.g. while the list is filtered, and
4151
+ * the handles follow it through aria-disabled.
4152
+ *
4153
+ * Emits a bubbling, CANCELABLE "fdy-sort-change" on the container, detail {key, from, to, order},
4154
+ * `order` being every item's key in the new order. Not cancelled, the kit moves the element, which
4155
+ * is the whole job on a static page. A framework that renders the list from state cancels it and
4156
+ * reorders the state instead, because the DOM is the framework's and an element moved behind its
4157
+ * back leaves the two disagreeing about where each row is. Either way the kit announces the move
4158
+ * and, after a keyboard step, puts focus back on the moved item's handle (looked up by data-key,
4159
+ * since a framework re-render may have replaced the element).
4160
+ *
4161
+ * The drag shows where the item will land (is-drop-before / is-drop-after on the item it lands
4162
+ * against) and moves nothing until the pointer is released; Escape cancels it. Exposes
4163
+ * window.FreedaySortable.
4164
+ */
4165
+ (function () {
4166
+ 'use strict';
4167
+
4168
+ var TEXT = {
4169
+ handle: 'Move {label}. Drag, or press the up and down arrow keys.',
4170
+ moved: '{label} moved to position {position} of {total}.'
4171
+ };
4172
+ /* Kebab-cased for the lookup, see the note in freeday-upload.js. */
4173
+ function textAttr(root, key) {
4174
+ if (!root || !root.getAttribute) return null;
4175
+ var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
4176
+ return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
4177
+ }
4178
+ function textOf(root, key, vars) {
4179
+ var custom = textAttr(root, key);
4180
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
4181
+ for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
4182
+ return s;
4183
+ }
4184
+
4185
+ var HANDLE = '.fdy-sortable__handle';
4186
+
4187
+ var live = null;
4188
+ function announce(message) {
4189
+ if (live === null) {
4190
+ live = document.createElement('div');
4191
+ live.className = 'fdy-visually-hidden';
4192
+ live.setAttribute('aria-live', 'polite');
4193
+ document.body.appendChild(live);
4194
+ }
4195
+ /* The same sentence twice in a row is no change, and a live region announces changes. */
4196
+ live.textContent = live.textContent === message ? message + ' ' : message;
4197
+ }
4198
+
4199
+ function items(root) {
4200
+ return Array.prototype.slice.call(root.children);
4201
+ }
4202
+ function keyOf(item, index) {
4203
+ var key = item.getAttribute('data-key');
4204
+ return key !== null ? key : String(index);
4205
+ }
4206
+ function labelOf(item) {
4207
+ var label = item.getAttribute('data-label');
4208
+ return label !== null ? label : (item.textContent || '').replace(/\s+/g, ' ').trim();
4209
+ }
4210
+ function disabled(root) {
4211
+ return root.hasAttribute('data-disabled');
4212
+ }
4213
+ function itemOf(root, node) {
4214
+ for (var n = node; n && n !== root; n = n.parentElement) {
4215
+ if (n.parentElement === root) return n;
4216
+ }
4217
+ return null;
4218
+ }
4219
+
4220
+ /* A move is allowed only if it shifts no fixed item, which is the same as saying the item does
4221
+ not start fixed and does not cross one on its way. */
4222
+ function allowed(list, from, to) {
4223
+ if (from === to || to < 0 || to >= list.length) return false;
4224
+ var lo = Math.min(from, to), hi = Math.max(from, to);
4225
+ for (var i = lo; i <= hi; i++) if (list[i].hasAttribute('data-fdy-sort-fixed')) return false;
4226
+ return true;
4227
+ }
4228
+
4229
+ function syncHandles(root) {
4230
+ var off = disabled(root) ? 'true' : 'false';
4231
+ items(root).forEach(function (item) {
4232
+ var handle = item.querySelector(HANDLE);
4233
+ if (!handle) return;
4234
+ if (handle.getAttribute('aria-disabled') !== off) handle.setAttribute('aria-disabled', off);
4235
+ if (!handle.hasAttribute('aria-label')) handle.setAttribute('aria-label', textOf(root, 'handle', { label: labelOf(item) }));
4236
+ });
4237
+ }
4238
+
4239
+ function commit(root, from, to, refocus) {
4240
+ var list = items(root);
4241
+ var item = list[from];
4242
+ var key = keyOf(item, from);
4243
+ var keys = list.map(keyOf);
4244
+ keys.splice(to, 0, keys.splice(from, 1)[0]);
4245
+ var ev = new CustomEvent('fdy-sort-change', {
4246
+ bubbles: true,
4247
+ cancelable: true,
4248
+ detail: { key: key, from: from, to: to, order: keys }
4249
+ });
4250
+ root.dispatchEvent(ev);
4251
+ if (!ev.defaultPrevented) root.insertBefore(item, to > from ? list[to].nextSibling : list[to]);
4252
+ announce(textOf(root, 'moved', { label: labelOf(item), position: String(to + 1), total: String(list.length) }));
4253
+ if (!refocus) return;
4254
+ /* After the app's own render, which a framework runs after this handler returns. Moving a focused
4255
+ element drops its focus, and a keyed re-render may have replaced the element outright. */
4256
+ setTimeout(function () {
4257
+ var now = items(root);
4258
+ var moved = item.parentElement === root ? item : now.filter(function (el, i) { return keyOf(el, i) === key; })[0];
4259
+ var handle = moved ? moved.querySelector(HANDLE) : null;
4260
+ if (handle && document.activeElement !== handle) handle.focus();
4261
+ }, 0);
4262
+ }
4263
+
4264
+ function initSortable(root) {
4265
+ if (root.dataset.fdySortableReady === '1') return;
4266
+ if (root.closest('[data-fdy-ignore]')) return;
4267
+ root.dataset.fdySortableReady = '1';
4268
+ syncHandles(root);
4269
+ /* Rows a framework adds later get their aria-label, and data-disabled reaches every handle. */
4270
+ new MutationObserver(function () { syncHandles(root); })
4271
+ .observe(root, { childList: true, attributes: true, attributeFilter: ['data-disabled'] });
4272
+
4273
+ root.addEventListener('keydown', function (e) {
4274
+ var handle = e.target.closest ? e.target.closest(HANDLE) : null;
4275
+ if (!handle || !root.contains(handle) || disabled(root)) return;
4276
+ if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
4277
+ e.preventDefault();
4278
+ var list = items(root);
4279
+ var from = list.indexOf(itemOf(root, handle));
4280
+ var to = from + (e.key === 'ArrowUp' ? -1 : 1);
4281
+ if (from !== -1 && allowed(list, from, to)) commit(root, from, to, true);
4282
+ });
4283
+
4284
+ var drag = null;
4285
+
4286
+ function clearMarks() {
4287
+ items(root).forEach(function (el) { el.classList.remove('is-drop-before', 'is-drop-after'); });
4288
+ }
4289
+ function end(apply) {
4290
+ if (drag === null) return;
4291
+ var d = drag;
4292
+ drag = null;
4293
+ document.removeEventListener('keydown', onEscape, true);
4294
+ clearMarks();
4295
+ d.item.classList.remove('is-dragging');
4296
+ root.classList.remove('is-sorting');
4297
+ if (apply && d.started && d.to !== null) commit(root, d.from, d.to, false);
4298
+ }
4299
+ function onEscape(e) {
4300
+ if (e.key === 'Escape') { e.preventDefault(); end(false); }
4301
+ }
4302
+
4303
+ root.addEventListener('pointerdown', function (e) {
4304
+ var handle = e.target.closest ? e.target.closest(HANDLE) : null;
4305
+ if (!handle || !root.contains(handle) || disabled(root) || e.button !== 0) return;
4306
+ var item = itemOf(root, handle);
4307
+ if (!item) return;
4308
+ e.preventDefault();
4309
+ handle.focus();
4310
+ handle.setPointerCapture(e.pointerId);
4311
+ drag = { item: item, from: items(root).indexOf(item), started: false, to: null };
4312
+ document.addEventListener('keydown', onEscape, true);
4313
+ });
4314
+
4315
+ root.addEventListener('pointermove', function (e) {
4316
+ if (drag === null) return;
4317
+ /* No travel threshold: a press that never leaves its own row resolves to to === from, which
4318
+ allowed() refuses, so a click on the handle cannot reorder anything. */
4319
+ if (!drag.started) {
4320
+ drag.started = true;
4321
+ drag.item.classList.add('is-dragging');
4322
+ root.classList.add('is-sorting');
4323
+ }
4324
+ clearMarks();
4325
+ drag.to = null;
4326
+ var list = items(root);
4327
+ var over = itemOf(root, document.elementFromPoint(e.clientX, e.clientY));
4328
+ if (!over) return;
4329
+ var rect = over.getBoundingClientRect();
4330
+ var after = e.clientY > rect.top + rect.height / 2;
4331
+ var slot = list.indexOf(over) + (after ? 1 : 0);
4332
+ var to = slot > drag.from ? slot - 1 : slot;
4333
+ if (!allowed(list, drag.from, to)) return;
4334
+ drag.to = to;
4335
+ over.classList.add(after ? 'is-drop-after' : 'is-drop-before');
4336
+ });
4337
+
4338
+ root.addEventListener('pointerup', function () { end(true); });
4339
+ root.addEventListener('pointercancel', function () { end(false); });
4340
+ }
4341
+
4342
+ function initAll(context) {
4343
+ var root = context || document;
4344
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
4345
+ if (root.matches && root.matches('[data-fdy-sortable]')) initSortable(root);
4346
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-sortable]'), initSortable);
4347
+ }
4348
+
4349
+ if (document.readyState === 'loading') {
4350
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
4351
+ } else {
4352
+ initAll();
4353
+ }
4354
+
4355
+ window.FreedaySortable = { init: initSortable, initAll: initAll };
4356
+ })();
4357
+
4112
4358
  /* Freeday, stepper/wizard enhancer (optional, zero-dependency).
4113
4359
  * Drives a step header + panels: prev/next navigation, completed/active state, and
4114
4360
  * clicking a reached step to revisit it (linear, can't skip ahead). Auto-inits
@@ -4160,6 +4406,7 @@
4160
4406
 
4161
4407
  function initStepper(root) {
4162
4408
  if (root.dataset.fdyStepperReady === '1') return;
4409
+ if (root.closest('[data-fdy-ignore]')) return;
4163
4410
  root.dataset.fdyStepperReady = '1';
4164
4411
 
4165
4412
  var steps = Array.prototype.slice.call(root.querySelectorAll('.fdy-step'));
@@ -4359,6 +4606,7 @@
4359
4606
 
4360
4607
  function initTable(root) {
4361
4608
  if (root.dataset.fdyTableReady === '1') return;
4609
+ if (root.closest('[data-fdy-ignore]')) return;
4362
4610
  var table = root.querySelector('table.fdy-table');
4363
4611
  var tbody = table && table.querySelector('tbody');
4364
4612
  if (!table || !tbody) return;
@@ -4902,6 +5150,7 @@
4902
5150
 
4903
5151
  function initTabs(root) {
4904
5152
  if (root.dataset.fdyTabsReady === '1') return;
5153
+ if (root.closest('[data-fdy-ignore]')) return;
4905
5154
  var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
4906
5155
  if (tabs.length === 0) return;
4907
5156
  root.dataset.fdyTabsReady = '1';
@@ -5017,6 +5266,7 @@
5017
5266
 
5018
5267
  function initTime(wrap) {
5019
5268
  if (wrap.dataset.fdyTpReady === '1') return;
5269
+ if (wrap.closest('[data-fdy-ignore]')) return;
5020
5270
  wrap.dataset.fdyTpReady = '1';
5021
5271
  wrap.classList.add('fdy-timepicker');
5022
5272
 
@@ -5453,6 +5703,7 @@
5453
5703
 
5454
5704
  function initTree(tree) {
5455
5705
  if (tree.dataset.fdyTreeReady === '1') return;
5706
+ if (tree.closest('[data-fdy-ignore]')) return;
5456
5707
  tree.dataset.fdyTreeReady = '1';
5457
5708
  Array.prototype.forEach.call(tree.querySelectorAll('input[type="checkbox"]'), function (box) {
5458
5709
  // A branch checkbox lives inside <summary>; keep its click from toggling the branch.
@@ -5500,6 +5751,11 @@
5500
5751
  * namespace that owns it. A hook whose enhancer is not loaded is skipped: that is a different
5501
5752
  * mistake, and this message would misname it. test/docs.test.mjs fails if an enhancer gains a
5502
5753
  * ready flag this table does not list.
5754
+ *
5755
+ * `data-fdy-ignore` on an element or any ancestor is the sanctioned way out: every enhancer skips
5756
+ * that subtree, and this stays quiet about it. It is for markup whose behaviour something else
5757
+ * owns: the kit's own Vue/React FdyDrawer, and kit classes an app uses for their look
5758
+ * alone (CATATAN-FREEDAY §14: a chip matrix the app drives itself).
5503
5759
  */
5504
5760
  (function () {
5505
5761
  'use strict';
@@ -5528,6 +5784,7 @@
5528
5784
  ['[data-fdy-search]', 'fdySearchReady', 'FreedaySearch'],
5529
5785
  ['[data-fdy-combo]', 'fdyComboReady', 'FreedayCombo'],
5530
5786
  ['[data-fdy-slider]', 'fdySliderReady', 'FreedaySlider'],
5787
+ ['[data-fdy-sortable]', 'fdySortableReady', 'FreedaySortable'],
5531
5788
  ['[data-fdy-stepper]', 'fdyStepperReady', 'FreedayStepper'],
5532
5789
  ['[data-fdy-table]', 'fdyTableReady', 'FreedayTable'],
5533
5790
  ['[data-fdy-tabs]', 'fdyTabsReady', 'FreedayTabs'],
@@ -5544,12 +5801,13 @@
5544
5801
  var hook = HOOKS[i];
5545
5802
  if (!window[hook[2]]) continue;
5546
5803
  var el = e.target.closest(hook[0]);
5547
- if (!el || el.dataset[hook[1]] === '1' || warned.has(el)) continue;
5804
+ if (!el || el.dataset[hook[1]] === '1' || warned.has(el) || el.closest('[data-fdy-ignore]')) continue;
5548
5805
  warned.add(el);
5549
5806
  console.warn('Freeday: this ' + hook[0] + ' was never enhanced, so it does nothing. Either it is '
5550
5807
  + 'outside every initAll() scope (Vue/React: inside the element given to useFreeday(); Blazor: '
5551
5808
  + 'FreedayBlazor.initAll after render; anything else: ' + hook[2] + '.initAll(el) once it is in '
5552
- + 'the DOM), or its markup lacks a part the enhancer needs (see COMPONENTS.md).', el);
5809
+ + 'the DOM), or its markup lacks a part the enhancer needs (see COMPONENTS.md). If the app '
5810
+ + 'drives this element itself on purpose, mark it or an ancestor data-fdy-ignore.', el);
5553
5811
  }
5554
5812
  }
5555
5813
 
@@ -5790,6 +6048,7 @@
5790
6048
 
5791
6049
  function initDropzone(zone) {
5792
6050
  if (zone.dataset.fdyUploadReady === '1') return;
6051
+ if (zone.closest('[data-fdy-ignore]')) return;
5793
6052
  zone.dataset.fdyUploadReady = '1';
5794
6053
 
5795
6054
  var input = zone.querySelector('input[type="file"]');
@@ -118,6 +118,8 @@ Recent additions most likely to replace something an app hand-rolled (all detail
118
118
 
119
119
  | Reach for | Instead of |
120
120
  |---|---|
121
+ | `data-fdy-sortable` + `.fdy-sortable__handle` | SortableJS plus a hand-written arrow-key path, or a number box for an `order` column |
122
+ | `data-fdy-ignore` | narrowing `useFreeday()` to dodge kit markup the app drives itself, which leaves every form outside the scope dead |
121
123
  | `.fdy-btn--stretch` | a hand-rolled overlay to make a card or list row clickable |
122
124
  | `[data-fdy-number]` | a bare `<input type="number">` with the browser's own spin buttons |
123
125
  | `clearable` on `FdyCfl` / `FdyDatepicker` | a value that can be set but never unset |
@@ -198,7 +198,7 @@ live docs also have a copy button per component.
198
198
  ```bash
199
199
  npm i @cahyo-dimas/freeday
200
200
  ```
201
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.13.0"` (public npm package). `dist/` is
201
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.14.0"` (public npm package). `dist/` is
202
202
  committed and published → no build step; `npm ci` runs without auth.
203
203
 
204
204
  ### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
@@ -42,6 +42,7 @@ Every integration comes down to one of these:
42
42
  | `freeday-table` | `fdy-table-change` · `fdy-row-select` | `FreedayTable` |
43
43
  | `freeday-chip` | `fdy-chip-change` `{value,pressed,selected}` · `fdy-chip-remove` `{value}`, cancelable | `FreedayChip` |
44
44
  | `freeday-upload` | `fdy-upload-add` / `fdy-upload-remove` (cancelable) | `FreedayUpload` |
45
+ | `freeday-sortable` | `fdy-sort-change` `{key,from,to,order}`, cancelable | `FreedaySortable` |
45
46
  | `freeday-stepper` | `fdy-step-change` | `FreedayStepper` |
46
47
  | `freeday-carousel` | `fdy-carousel-change` | `FreedayCarousel` |
47
48
  | `freeday-breakpoint` | `fdy-breakpoint-change` | `FreedayBreakpoint` |