@cahyo-dimas/freeday 3.12.1 → 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 (44) hide show
  1. package/CHANGELOG.md +133 -9
  2. package/COMPONENTS.md +76 -1
  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-breakpoint.js +12 -2
  12. package/dist/freeday-carousel.js +1 -0
  13. package/dist/freeday-cascade.js +1 -0
  14. package/dist/freeday-cfl.js +5 -1
  15. package/dist/freeday-chart.js +1 -0
  16. package/dist/freeday-chip.js +12 -4
  17. package/dist/freeday-datepicker.js +2 -0
  18. package/dist/freeday-datetime.js +1 -0
  19. package/dist/freeday-drawer.js +2 -0
  20. package/dist/freeday-form.js +1 -0
  21. package/dist/freeday-mask.js +2 -0
  22. package/dist/freeday-menu.js +1 -0
  23. package/dist/freeday-number.js +1 -0
  24. package/dist/freeday-rating.js +1 -0
  25. package/dist/freeday-search.js +1 -0
  26. package/dist/freeday-select.js +1 -0
  27. package/dist/freeday-slider.js +1 -0
  28. package/dist/freeday-sortable.js +222 -0
  29. package/dist/freeday-stepper.js +1 -0
  30. package/dist/freeday-table.js +1 -0
  31. package/dist/freeday-tabs.js +1 -0
  32. package/dist/freeday-timepicker.js +1 -0
  33. package/dist/freeday-tree.js +1 -0
  34. package/dist/freeday-unhydrated.js +83 -0
  35. package/dist/freeday-upload.js +6 -3
  36. package/dist/freeday.bundle.css +20 -1
  37. package/dist/freeday.css +20 -1
  38. package/dist/freeday.js +366 -10
  39. package/docs/agent-onboarding.md +4 -1
  40. package/docs/getting-started.md +1 -1
  41. package/docs/integrations.md +3 -2
  42. package/package.json +2 -2
  43. package/src/components/input.css +2 -1
  44. package/src/components/sortable.css +17 -0
@@ -24,6 +24,7 @@
24
24
 
25
25
  function initSearch(root) {
26
26
  if (root.dataset.fdySearchReady === '1') return;
27
+ if (root.closest('[data-fdy-ignore]')) return;
27
28
  var input = root.querySelector('input');
28
29
  if (!input) return;
29
30
  root.dataset.fdySearchReady = '1';
@@ -32,6 +32,7 @@
32
32
 
33
33
  function initCombo(root) {
34
34
  if (root.dataset.fdyComboReady === '1') return; // idempotent
35
+ if (root.closest('[data-fdy-ignore]')) return;
35
36
  var button = root.querySelector('.fdy-combo__button');
36
37
  var listbox = root.querySelector('.fdy-combo__listbox');
37
38
  var valueEl = root.querySelector('.fdy-combo__value');
@@ -12,6 +12,7 @@
12
12
 
13
13
  function initSlider(input) {
14
14
  if (input.dataset.fdySliderReady === '1') return;
15
+ if (input.closest('[data-fdy-ignore]')) return;
15
16
  input.dataset.fdySliderReady = '1';
16
17
  var out = input.id ? document.querySelector('output[for="' + input.id + '"]') : null;
17
18
  if (!out) return;
@@ -0,0 +1,222 @@
1
+ /* Freeday, sortable list enhancer (optional, zero-dependency).
2
+ * Reorder the children of a list by dragging a handle, or by focusing it and pressing ↑ / ↓. The
3
+ * keyboard path is not an extra: drag-only reordering locks out every keyboard and screen-reader
4
+ * user from a settings screen, and that path is the part each app used to write for itself
5
+ * (CATATAN-FREEDAY §15).
6
+ *
7
+ * Markup contract:
8
+ * <ul class="fdy-sortable" data-fdy-sortable> (or a <tbody>, or a <div>)
9
+ * <li data-key="ketua" data-label="Ketua">
10
+ * <button class="fdy-sortable__handle" type="button">…icon…</button> Ketua
11
+ * </li>
12
+ * <li data-key="sistem" data-fdy-sort-fixed>…</li> (never moves, and nothing passes it)
13
+ * </ul>
14
+ * The items are the container's element children. `data-key` names each in the event; `data-label`
15
+ * is what the handle's default aria-label and the announcement call it (the item text otherwise).
16
+ * `data-disabled` on the container switches reordering off, e.g. while the list is filtered, and
17
+ * the handles follow it through aria-disabled.
18
+ *
19
+ * Emits a bubbling, CANCELABLE "fdy-sort-change" on the container, detail {key, from, to, order},
20
+ * `order` being every item's key in the new order. Not cancelled, the kit moves the element, which
21
+ * is the whole job on a static page. A framework that renders the list from state cancels it and
22
+ * reorders the state instead, because the DOM is the framework's and an element moved behind its
23
+ * back leaves the two disagreeing about where each row is. Either way the kit announces the move
24
+ * and, after a keyboard step, puts focus back on the moved item's handle (looked up by data-key,
25
+ * since a framework re-render may have replaced the element).
26
+ *
27
+ * The drag shows where the item will land (is-drop-before / is-drop-after on the item it lands
28
+ * against) and moves nothing until the pointer is released; Escape cancels it. Exposes
29
+ * window.FreedaySortable.
30
+ */
31
+ (function () {
32
+ 'use strict';
33
+
34
+ var TEXT = {
35
+ handle: 'Move {label}. Drag, or press the up and down arrow keys.',
36
+ moved: '{label} moved to position {position} of {total}.'
37
+ };
38
+ /* Kebab-cased for the lookup, see the note in freeday-upload.js. */
39
+ function textAttr(root, key) {
40
+ if (!root || !root.getAttribute) return null;
41
+ var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
42
+ return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
43
+ }
44
+ function textOf(root, key, vars) {
45
+ var custom = textAttr(root, key);
46
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
47
+ for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
48
+ return s;
49
+ }
50
+
51
+ var HANDLE = '.fdy-sortable__handle';
52
+
53
+ var live = null;
54
+ function announce(message) {
55
+ if (live === null) {
56
+ live = document.createElement('div');
57
+ live.className = 'fdy-visually-hidden';
58
+ live.setAttribute('aria-live', 'polite');
59
+ document.body.appendChild(live);
60
+ }
61
+ /* The same sentence twice in a row is no change, and a live region announces changes. */
62
+ live.textContent = live.textContent === message ? message + ' ' : message;
63
+ }
64
+
65
+ function items(root) {
66
+ return Array.prototype.slice.call(root.children);
67
+ }
68
+ function keyOf(item, index) {
69
+ var key = item.getAttribute('data-key');
70
+ return key !== null ? key : String(index);
71
+ }
72
+ function labelOf(item) {
73
+ var label = item.getAttribute('data-label');
74
+ return label !== null ? label : (item.textContent || '').replace(/\s+/g, ' ').trim();
75
+ }
76
+ function disabled(root) {
77
+ return root.hasAttribute('data-disabled');
78
+ }
79
+ function itemOf(root, node) {
80
+ for (var n = node; n && n !== root; n = n.parentElement) {
81
+ if (n.parentElement === root) return n;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /* A move is allowed only if it shifts no fixed item, which is the same as saying the item does
87
+ not start fixed and does not cross one on its way. */
88
+ function allowed(list, from, to) {
89
+ if (from === to || to < 0 || to >= list.length) return false;
90
+ var lo = Math.min(from, to), hi = Math.max(from, to);
91
+ for (var i = lo; i <= hi; i++) if (list[i].hasAttribute('data-fdy-sort-fixed')) return false;
92
+ return true;
93
+ }
94
+
95
+ function syncHandles(root) {
96
+ var off = disabled(root) ? 'true' : 'false';
97
+ items(root).forEach(function (item) {
98
+ var handle = item.querySelector(HANDLE);
99
+ if (!handle) return;
100
+ if (handle.getAttribute('aria-disabled') !== off) handle.setAttribute('aria-disabled', off);
101
+ if (!handle.hasAttribute('aria-label')) handle.setAttribute('aria-label', textOf(root, 'handle', { label: labelOf(item) }));
102
+ });
103
+ }
104
+
105
+ function commit(root, from, to, refocus) {
106
+ var list = items(root);
107
+ var item = list[from];
108
+ var key = keyOf(item, from);
109
+ var keys = list.map(keyOf);
110
+ keys.splice(to, 0, keys.splice(from, 1)[0]);
111
+ var ev = new CustomEvent('fdy-sort-change', {
112
+ bubbles: true,
113
+ cancelable: true,
114
+ detail: { key: key, from: from, to: to, order: keys }
115
+ });
116
+ root.dispatchEvent(ev);
117
+ if (!ev.defaultPrevented) root.insertBefore(item, to > from ? list[to].nextSibling : list[to]);
118
+ announce(textOf(root, 'moved', { label: labelOf(item), position: String(to + 1), total: String(list.length) }));
119
+ if (!refocus) return;
120
+ /* After the app's own render, which a framework runs after this handler returns. Moving a focused
121
+ element drops its focus, and a keyed re-render may have replaced the element outright. */
122
+ setTimeout(function () {
123
+ var now = items(root);
124
+ var moved = item.parentElement === root ? item : now.filter(function (el, i) { return keyOf(el, i) === key; })[0];
125
+ var handle = moved ? moved.querySelector(HANDLE) : null;
126
+ if (handle && document.activeElement !== handle) handle.focus();
127
+ }, 0);
128
+ }
129
+
130
+ function initSortable(root) {
131
+ if (root.dataset.fdySortableReady === '1') return;
132
+ if (root.closest('[data-fdy-ignore]')) return;
133
+ root.dataset.fdySortableReady = '1';
134
+ syncHandles(root);
135
+ /* Rows a framework adds later get their aria-label, and data-disabled reaches every handle. */
136
+ new MutationObserver(function () { syncHandles(root); })
137
+ .observe(root, { childList: true, attributes: true, attributeFilter: ['data-disabled'] });
138
+
139
+ root.addEventListener('keydown', function (e) {
140
+ var handle = e.target.closest ? e.target.closest(HANDLE) : null;
141
+ if (!handle || !root.contains(handle) || disabled(root)) return;
142
+ if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
143
+ e.preventDefault();
144
+ var list = items(root);
145
+ var from = list.indexOf(itemOf(root, handle));
146
+ var to = from + (e.key === 'ArrowUp' ? -1 : 1);
147
+ if (from !== -1 && allowed(list, from, to)) commit(root, from, to, true);
148
+ });
149
+
150
+ var drag = null;
151
+
152
+ function clearMarks() {
153
+ items(root).forEach(function (el) { el.classList.remove('is-drop-before', 'is-drop-after'); });
154
+ }
155
+ function end(apply) {
156
+ if (drag === null) return;
157
+ var d = drag;
158
+ drag = null;
159
+ document.removeEventListener('keydown', onEscape, true);
160
+ clearMarks();
161
+ d.item.classList.remove('is-dragging');
162
+ root.classList.remove('is-sorting');
163
+ if (apply && d.started && d.to !== null) commit(root, d.from, d.to, false);
164
+ }
165
+ function onEscape(e) {
166
+ if (e.key === 'Escape') { e.preventDefault(); end(false); }
167
+ }
168
+
169
+ root.addEventListener('pointerdown', function (e) {
170
+ var handle = e.target.closest ? e.target.closest(HANDLE) : null;
171
+ if (!handle || !root.contains(handle) || disabled(root) || e.button !== 0) return;
172
+ var item = itemOf(root, handle);
173
+ if (!item) return;
174
+ e.preventDefault();
175
+ handle.focus();
176
+ handle.setPointerCapture(e.pointerId);
177
+ drag = { item: item, from: items(root).indexOf(item), started: false, to: null };
178
+ document.addEventListener('keydown', onEscape, true);
179
+ });
180
+
181
+ root.addEventListener('pointermove', function (e) {
182
+ if (drag === null) return;
183
+ /* No travel threshold: a press that never leaves its own row resolves to to === from, which
184
+ allowed() refuses, so a click on the handle cannot reorder anything. */
185
+ if (!drag.started) {
186
+ drag.started = true;
187
+ drag.item.classList.add('is-dragging');
188
+ root.classList.add('is-sorting');
189
+ }
190
+ clearMarks();
191
+ drag.to = null;
192
+ var list = items(root);
193
+ var over = itemOf(root, document.elementFromPoint(e.clientX, e.clientY));
194
+ if (!over) return;
195
+ var rect = over.getBoundingClientRect();
196
+ var after = e.clientY > rect.top + rect.height / 2;
197
+ var slot = list.indexOf(over) + (after ? 1 : 0);
198
+ var to = slot > drag.from ? slot - 1 : slot;
199
+ if (!allowed(list, drag.from, to)) return;
200
+ drag.to = to;
201
+ over.classList.add(after ? 'is-drop-after' : 'is-drop-before');
202
+ });
203
+
204
+ root.addEventListener('pointerup', function () { end(true); });
205
+ root.addEventListener('pointercancel', function () { end(false); });
206
+ }
207
+
208
+ function initAll(context) {
209
+ var root = context || document;
210
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
211
+ if (root.matches && root.matches('[data-fdy-sortable]')) initSortable(root);
212
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-sortable]'), initSortable);
213
+ }
214
+
215
+ if (document.readyState === 'loading') {
216
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
217
+ } else {
218
+ initAll();
219
+ }
220
+
221
+ window.FreedaySortable = { init: initSortable, initAll: initAll };
222
+ })();
@@ -49,6 +49,7 @@
49
49
 
50
50
  function initStepper(root) {
51
51
  if (root.dataset.fdyStepperReady === '1') return;
52
+ if (root.closest('[data-fdy-ignore]')) return;
52
53
  root.dataset.fdyStepperReady = '1';
53
54
 
54
55
  var steps = Array.prototype.slice.call(root.querySelectorAll('.fdy-step'));
@@ -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.
@@ -0,0 +1,83 @@
1
+ /* Freeday, un-hydrated hook warning (optional, zero-dependency, no markup of its own).
2
+ * Every enhancer marks the element it took over (`data-fdy-*-ready`). Markup that no initAll()
3
+ * ever reached carries the hook and not the mark, and it fails in the worst way available: it
4
+ * renders, looks finished, and does nothing, with no error anywhere. A `data-fdy-validate` form
5
+ * outside the subtree handed to useFreeday() neither paints a message nor fires fdy-form-valid,
6
+ * so its save button reads as dead (CATATAN-FREEDAY §10). The enhancer knows the attribute is
7
+ * there; the person who wrote it does not.
8
+ *
9
+ * So on the first click, focus or submit inside such an element this logs ONE console.warn per
10
+ * element, naming the hook, with the element itself as the second argument so devtools links it.
11
+ * Interaction, not load, is the trigger on purpose: a framework mounts and hydrates in between,
12
+ * and only once a person is touching the widget is "not enhanced yet" the same as "never".
13
+ *
14
+ * HOOKS pairs each hook selector with the ready flag its enhancer stamps on THAT element and the
15
+ * namespace that owns it. A hook whose enhancer is not loaded is skipped: that is a different
16
+ * mistake, and this message would misname it. test/docs.test.mjs fails if an enhancer gains a
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).
23
+ */
24
+ (function () {
25
+ 'use strict';
26
+
27
+ var HOOKS = [
28
+ ['[data-fdy-app]', 'fdyAppShellReady', 'FreedayAppShell'],
29
+ ['[data-fdy-autocomplete]', 'fdyAcReady', 'FreedayAutocomplete'],
30
+ ['[data-fdy-carousel]', 'fdyCarouselReady', 'FreedayCarousel'],
31
+ ['[data-fdy-cascade]', 'fdyCascadeReady', 'FreedayCascade'],
32
+ ['[data-fdy-cfl]', 'fdyCflBound', 'FreedayCfl'],
33
+ ['[data-fdy-cfl-open]', 'fdyCflBound', 'FreedayCfl'],
34
+ ['[data-fdy-chart]', 'fdyChartReady', 'FreedayChart'],
35
+ ['[data-fdy-chips]', 'fdyChipsReady', 'FreedayChip'],
36
+ ['.fdy-chip__remove', 'fdyChipRmReady', 'FreedayChip'],
37
+ ['[data-fdy-datepicker]', 'fdyDpReady', 'FreedayDatepicker'],
38
+ ['[data-fdy-daterange]', 'fdyDrReady', 'FreedayDatepicker'],
39
+ ['[data-fdy-datetimepicker]', 'fdyDtReady', 'FreedayDatetime'],
40
+ ['[data-fdy-drawer]', 'fdyDrawerBound', 'FreedayDrawer'],
41
+ ['.fdy-drawer', 'fdyDrawerReady', 'FreedayDrawer'],
42
+ ['[data-fdy-validate]', 'fdyFormReady', 'FreedayForm'],
43
+ ['[data-fdy-mask]', 'fdyMaskReady', 'FreedayMask'],
44
+ ['[data-fdy-password]', 'fdyPwReady', 'FreedayMask'],
45
+ ['[data-fdy-menu]', 'fdyMenuReady', 'FreedayMenu'],
46
+ ['[data-fdy-number]', 'fdyNumberReady', 'FreedayNumber'],
47
+ ['[data-fdy-rating]', 'fdyRatingReady', 'FreedayRating'],
48
+ ['[data-fdy-search]', 'fdySearchReady', 'FreedaySearch'],
49
+ ['[data-fdy-combo]', 'fdyComboReady', 'FreedayCombo'],
50
+ ['[data-fdy-slider]', 'fdySliderReady', 'FreedaySlider'],
51
+ ['[data-fdy-sortable]', 'fdySortableReady', 'FreedaySortable'],
52
+ ['[data-fdy-stepper]', 'fdyStepperReady', 'FreedayStepper'],
53
+ ['[data-fdy-table]', 'fdyTableReady', 'FreedayTable'],
54
+ ['[data-fdy-tabs]', 'fdyTabsReady', 'FreedayTabs'],
55
+ ['[data-fdy-timepicker]', 'fdyTpReady', 'FreedayTimepicker'],
56
+ ['[data-fdy-tree]', 'fdyTreeReady', 'FreedayTree'],
57
+ ['[data-fdy-dropzone]', 'fdyUploadReady', 'FreedayUpload']
58
+ ];
59
+
60
+ var warned = typeof WeakSet === 'function' ? new WeakSet() : null;
61
+
62
+ function check(e) {
63
+ if (!warned || !e.target || !e.target.closest) return;
64
+ for (var i = 0; i < HOOKS.length; i++) {
65
+ var hook = HOOKS[i];
66
+ if (!window[hook[2]]) continue;
67
+ var el = e.target.closest(hook[0]);
68
+ if (!el || el.dataset[hook[1]] === '1' || warned.has(el) || el.closest('[data-fdy-ignore]')) continue;
69
+ warned.add(el);
70
+ console.warn('Freeday: this ' + hook[0] + ' was never enhanced, so it does nothing. Either it is '
71
+ + 'outside every initAll() scope (Vue/React: inside the element given to useFreeday(); Blazor: '
72
+ + 'FreedayBlazor.initAll after render; anything else: ' + hook[2] + '.initAll(el) once it is in '
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);
75
+ }
76
+ }
77
+
78
+ ['click', 'focusin', 'submit'].forEach(function (type) {
79
+ document.addEventListener(type, check, true);
80
+ });
81
+
82
+ window.FreedayUnhydrated = { hooks: HOOKS };
83
+ })();
@@ -18,7 +18,8 @@
18
18
  * Emits bubbling CustomEvents. BOTH on the dropzone element, which is the one target a consumer
19
19
  * needs to listen on:
20
20
  * "fdy-upload-add" {file, rejected, reason, row}
21
- * "fdy-upload-remove" {file}
21
+ * "fdy-upload-remove" {file}, cancelable: preventDefault() keeps the row (e.g. to confirm
22
+ * removing a file the server already holds, then call row.el.remove())
22
23
  * `row` is the state machine over the rendered .fdy-file:
23
24
  * .ready() (rest, where a dropped file starts) / .uploading() / .setProgress(pct) /
24
25
  * .waiting(label) (sent, awaiting the server) / .done() / .fail(msg) / .el
@@ -111,8 +112,9 @@
111
112
  Firing on both would look safer, but it makes the pair asymmetric, one `add` and, for
112
113
  anyone delegating on a common ancestor, two `remove`s, which is its own silent bug. It
113
114
  also fixes the listless case: a row that was never attached bubbles to nothing at all. */
114
- zone.dispatchEvent(new CustomEvent('fdy-upload-remove', { bubbles: true, detail: { file: file } }));
115
- el.remove();
115
+ var ev = new CustomEvent('fdy-upload-remove', { bubbles: true, cancelable: true, detail: { file: file } });
116
+ zone.dispatchEvent(ev);
117
+ if (!ev.defaultPrevented) el.remove();
116
118
  });
117
119
  el.appendChild(icon);
118
120
  el.appendChild(meta);
@@ -226,6 +228,7 @@
226
228
 
227
229
  function initDropzone(zone) {
228
230
  if (zone.dataset.fdyUploadReady === '1') return;
231
+ if (zone.closest('[data-fdy-ignore]')) return;
229
232
  zone.dataset.fdyUploadReady = '1';
230
233
 
231
234
  var input = zone.querySelector('input[type="file"]');
@@ -2696,7 +2696,8 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2696
2696
  remember to mark aria-hidden, and it cannot be forgotten. */
2697
2697
  .fdy-label--required::after{content:"*" / "";margin-left:var(--space-1);color:var(--color-danger);}
2698
2698
  .fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
2699
- .fdy-help--error{color:var(--color-danger);}
2699
+ /* --danger is the tone word everywhere else in the kit; help text takes either (CATATAN-FREEDAY §11). */
2700
+ .fdy-help--error,.fdy-help--danger{color:var(--color-danger);}
2700
2701
 
2701
2702
  /* Freeday. Keyboard key */
2702
2703
  .fdy-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:1.5rem;height:1.5rem;padding:0 var(--space-2);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text);background:var(--color-surface-2);border:var(--bw) solid var(--color-border-strong);border-bottom-width:2px;border-radius:var(--radius-sm);}
@@ -2899,6 +2900,24 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2899
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;}
2900
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;}
2901
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
+
2902
2921
  /* Freeday. Spinner */
2903
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;}
2904
2923
  .fdy-spinner--sm{width:1rem;height:1rem;border-width:2px;}
package/dist/freeday.css CHANGED
@@ -1519,7 +1519,8 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1519
1519
  remember to mark aria-hidden, and it cannot be forgotten. */
1520
1520
  .fdy-label--required::after{content:"*" / "";margin-left:var(--space-1);color:var(--color-danger);}
1521
1521
  .fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
1522
- .fdy-help--error{color:var(--color-danger);}
1522
+ /* --danger is the tone word everywhere else in the kit; help text takes either (CATATAN-FREEDAY §11). */
1523
+ .fdy-help--error,.fdy-help--danger{color:var(--color-danger);}
1523
1524
 
1524
1525
  /* Freeday. Keyboard key */
1525
1526
  .fdy-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:1.5rem;height:1.5rem;padding:0 var(--space-2);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text);background:var(--color-surface-2);border:var(--bw) solid var(--color-border-strong);border-bottom-width:2px;border-radius:var(--radius-sm);}
@@ -1722,6 +1723,24 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1722
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;}
1723
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;}
1724
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
+
1725
1744
  /* Freeday. Spinner */
1726
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;}
1727
1746
  .fdy-spinner--sm{width:1rem;height:1rem;border-width:2px;}