@cahyo-dimas/freeday 1.23.0 → 1.24.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,49 @@
3
3
  Semua perubahan penting dicatat di sini. Format longgar mengikuti
4
4
  [Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
5
5
 
6
+ ## [1.24.0] — 2026-08-13
7
+ Improvement note #41: the upload row had no "chosen, not yet sent" state, so every consumer-driven
8
+ integration showed a transfer that had not started.
9
+ ### Fixed
10
+ - **A dropped file now rests until the consumer starts the transfer.** `handleFiles` called
11
+ `row.uploading()` unconditionally — and *before* dispatching `fdy-upload-add`, so a consumer could
12
+ not pre-empt it. Between the drop and the app's own submit button (which may be a minute, while the
13
+ user fills in the rest of the form) the row claimed to be uploading, with a progress bar that never
14
+ moved. A user watching that reasonably concludes the upload has hung and reports a bug against a
15
+ transfer that was never started. The state machine was missing its start state: `done()` claims
16
+ success, `fail()` claims an error, and `uploading()` was where it already was.
17
+ The demo path is unchanged — with `data-fdy-upload-simulate` the kit *is* performing a transfer, so
18
+ showing one stays correct. That attribute already marks the only place the old default was right,
19
+ which is why this changes the default rather than adding an opt-in: making correct integrations opt
20
+ in to correctness is backwards.
21
+ - **`fdy-upload-add` no longer depends on rendering a row.** The guard was `if (!list || !fileList)
22
+ return;`, so a dropzone with no file list lost the event that tells the app a file arrived —
23
+ "bring your own row" silently cost you the notification. Rendering and announcing are now separate:
24
+ no list means no row is attached, and the event still fires with a working `detail.row`.
25
+ Deliberately **no new attribute** for this. The reporter proposed `data-rows="off"`; `list` is used
26
+ in exactly two places, so decoupling covers the same case without growing the API surface, and no
27
+ page can be relying on "no event".
28
+ ### Added
29
+ - **`row.ready()`** — the rest state, and the way back to it (a failed attempt the user will retry).
30
+ It reuses the existing `dropProgress()`, so the bar is removed exactly as `done()`/`fail()` do it.
31
+ ### Docs
32
+ - **`COMPONENTS.md` documents the row state machine at all.** `uploading()` / `setProgress()` /
33
+ `done()` / `fail()` had **zero** mentions anywhere in the shipped docs — a consumer holding
34
+ `detail.row` had no supported way to know they existed, which is a fair part of why the old default
35
+ went unquestioned. The section now carries the state table, a worked example, the
36
+ bring-your-own-row position, and `data-fdy-upload-simulate` marked demo-only.
37
+ Two corrections to the report while transcribing it: the function is `handleFiles` (not `addFiles`)
38
+ and the attribute is `data-fdy-upload-simulate` (not `data-simulate`) — the latter matters, since
39
+ documenting the wrong name would have consumers set an attribute that does nothing.
40
+ ### Added — guards
41
+ - **`browser/upload-states.mjs`** — drops a real `File` and asserts the rest state shows no progress
42
+ bar, that `uploading()` → `setProgress()` → `done()` still chains, that the simulate path is
43
+ untouched, and that a listless dropzone still dispatches. Mutation-checked on both halves.
44
+ The fixture wraps the listless dropzone in its own container **on purpose**: with no
45
+ `data-filelist` the enhancer falls back to `parentNode.querySelector('.fdy-filelist')`, so a bare
46
+ dropzone sharing a parent with another list adopts it — the first version of this guard was
47
+ testing nothing, and the mutation run is what exposed that.
48
+
6
49
  ## [1.23.0] — 2026-08-13
7
50
  Consumption round 6 (`improvement-notes/006`). The reporter filed two of the three as **their own**
8
51
  bugs rather than the kit's — and they were right about the code, but in both cases the kit had a way
package/COMPONENTS.md CHANGED
@@ -478,9 +478,41 @@ Click/Enter opens the file dialog; drop works too. Needs `freeday-upload.js`.
478
478
  `<input type="file">`
479
479
  - List: `.fdy-filelist` (+`--grid`) of `.fdy-file` (+`--success`, `--error`) · `__icon` `__meta`
480
480
  `__name` `__sub` `__progress` `__remove`
481
- - Attributes: `data-max-size` (bytes), `data-filelist="#id"`
481
+ - Attributes: `data-max-size` (bytes), `data-filelist="#id"`, `data-fdy-upload-simulate` (demo only
482
+ — the kit fakes a transfer to `done()`; never set it in an app)
482
483
  - A11y: the dropzone is `role="button" tabindex="0"` + `aria-label`.
483
- - Wire `fdy-upload-add` to your real upload; render explicit progress/success/error state.
484
+
485
+ **The row is yours to drive.** `fdy-upload-add` carries `detail.row`, a small state machine over the
486
+ rendered `.fdy-file`. A dropped file **rests** — it shows its size and nothing else — until you say a
487
+ transfer started; the kit never claims one it is not performing.
488
+
489
+ | `detail.row` | State it renders |
490
+ |---|---|
491
+ | *(initial)* | **rest** — chosen, not sent. Size only, no progress bar. |
492
+ | `.uploading()` | in flight — adds the progress bar |
493
+ | `.setProgress(pct)` | moves the bar (0–100) |
494
+ | `.done()` | success — drops the bar, `.fdy-file--success` |
495
+ | `.fail(msg)` | error — drops the bar, `.fdy-file--error`, `msg` replaces the sub-line |
496
+ | `.ready()` | back to **rest** (e.g. after a failed attempt the user will retry) |
497
+ | `.el` | the row element |
498
+
499
+ ```js
500
+ zone.addEventListener('fdy-upload-add', (e) => {
501
+ if (e.detail.rejected) return; // the kit already rendered the reason
502
+ const { file, row } = e.detail; // row is at rest — nothing is in flight yet
503
+ submitBtn.onclick = async () => {
504
+ row.uploading();
505
+ await send(file, (pct) => row.setProgress(pct));
506
+ row.done();
507
+ };
508
+ });
509
+ ```
510
+
511
+ **Bring your own row:** omit the file list entirely (no `data-filelist`, and no `.fdy-filelist`
512
+ sibling) and the enhancer renders nothing while still dispatching `fdy-upload-add` — `detail.row`
513
+ still works, its element simply isn't attached. Note the fallback when `data-filelist` is absent is
514
+ `parentNode.querySelector('.fdy-filelist')`, so a bare dropzone will adopt a list that happens to
515
+ share its parent; give the dropzone its own container if you mean "no list".
484
516
 
485
517
  ## Form validation — `data-fdy-validate`
486
518
  Native Constraint Validation wired to accessible inline errors: `aria-invalid` +
package/README.id.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **Lebih banyak _free day_ buat dev — UI kit-nya sudah siap pakai.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.23.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.23.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.24.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.24.0)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic — satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs — the UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.23.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.23.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.24.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.24.0)
9
9
 
10
10
  A token-driven, framework-agnostic UI kit — one source of truth for color, typography,
11
11
  spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -95,6 +95,16 @@
95
95
  }
96
96
  return {
97
97
  el: el,
98
+ /* The state a row starts in: chosen, not yet sent. The size alone — it makes no claim about
99
+ a transfer, and no progress bar is shown, because nothing is in flight. Without this the
100
+ state machine had no start state: done() claims success, fail() claims an error, and
101
+ uploading() is a lie until the consumer actually sends the file. */
102
+ ready: function () {
103
+ el.classList.remove('fdy-file--error', 'fdy-file--success');
104
+ icon.innerHTML = FILE_ICON;
105
+ sub.textContent = fmtSize(file.size);
106
+ dropProgress();
107
+ },
98
108
  uploading: function () {
99
109
  el.classList.remove('fdy-file--error', 'fdy-file--success');
100
110
  icon.innerHTML = FILE_ICON;
@@ -171,19 +181,30 @@
171
181
  });
172
182
  if (input) input.addEventListener('change', function () { handleFiles(input.files); input.value = ''; });
173
183
 
184
+ /* The list is optional: rendering a row and announcing the file are separate jobs. A consumer
185
+ that wants its own markup simply provides no list — it still gets `fdy-upload-add`, and
186
+ `detail.row` still works (its element is just never attached). Gating the EVENT on the list
187
+ meant "bring your own row" silently cost you the notification that a file had arrived. */
174
188
  function handleFiles(fileList) {
175
- if (!list || !fileList) return;
189
+ if (!fileList) return;
176
190
  Array.prototype.slice.call(fileList).forEach(function (file) {
177
191
  var reason = null;
178
192
  if (!accepts(file, acceptAttr)) reason = 'Tipe berkas tidak didukung.';
179
193
  else if (maxSize && file.size > maxSize) reason = 'Ukuran melebihi batas (' + fmtSize(maxSize) + ').';
180
194
  var row = makeRow(file);
181
- list.appendChild(row.el);
195
+ if (list) list.appendChild(row.el);
182
196
  if (reason) {
183
197
  row.fail(reason);
184
- } else {
198
+ } else if (simulate) {
199
+ /* The kit is driving (demo path): keep showing a transfer, because there is one. */
185
200
  row.uploading();
186
- if (simulate) simulateUpload(row);
201
+ simulateUpload(row);
202
+ } else {
203
+ /* The CONSUMER owns the transfer. Rest until it says otherwise by calling row.uploading():
204
+ a file that has only been chosen must not claim to be uploading. A progress bar that
205
+ never moves reads as a hung upload, and gets reported as a bug against a transfer that
206
+ was never started. */
207
+ row.ready();
187
208
  }
188
209
  zone.dispatchEvent(new CustomEvent('fdy-upload-add', {
189
210
  bubbles: true, detail: { file: file, rejected: !!reason, reason: reason, row: row }
package/dist/freeday.js CHANGED
@@ -3670,6 +3670,16 @@
3670
3670
  }
3671
3671
  return {
3672
3672
  el: el,
3673
+ /* The state a row starts in: chosen, not yet sent. The size alone — it makes no claim about
3674
+ a transfer, and no progress bar is shown, because nothing is in flight. Without this the
3675
+ state machine had no start state: done() claims success, fail() claims an error, and
3676
+ uploading() is a lie until the consumer actually sends the file. */
3677
+ ready: function () {
3678
+ el.classList.remove('fdy-file--error', 'fdy-file--success');
3679
+ icon.innerHTML = FILE_ICON;
3680
+ sub.textContent = fmtSize(file.size);
3681
+ dropProgress();
3682
+ },
3673
3683
  uploading: function () {
3674
3684
  el.classList.remove('fdy-file--error', 'fdy-file--success');
3675
3685
  icon.innerHTML = FILE_ICON;
@@ -3746,19 +3756,30 @@
3746
3756
  });
3747
3757
  if (input) input.addEventListener('change', function () { handleFiles(input.files); input.value = ''; });
3748
3758
 
3759
+ /* The list is optional: rendering a row and announcing the file are separate jobs. A consumer
3760
+ that wants its own markup simply provides no list — it still gets `fdy-upload-add`, and
3761
+ `detail.row` still works (its element is just never attached). Gating the EVENT on the list
3762
+ meant "bring your own row" silently cost you the notification that a file had arrived. */
3749
3763
  function handleFiles(fileList) {
3750
- if (!list || !fileList) return;
3764
+ if (!fileList) return;
3751
3765
  Array.prototype.slice.call(fileList).forEach(function (file) {
3752
3766
  var reason = null;
3753
3767
  if (!accepts(file, acceptAttr)) reason = 'Tipe berkas tidak didukung.';
3754
3768
  else if (maxSize && file.size > maxSize) reason = 'Ukuran melebihi batas (' + fmtSize(maxSize) + ').';
3755
3769
  var row = makeRow(file);
3756
- list.appendChild(row.el);
3770
+ if (list) list.appendChild(row.el);
3757
3771
  if (reason) {
3758
3772
  row.fail(reason);
3759
- } else {
3773
+ } else if (simulate) {
3774
+ /* The kit is driving (demo path): keep showing a transfer, because there is one. */
3760
3775
  row.uploading();
3761
- if (simulate) simulateUpload(row);
3776
+ simulateUpload(row);
3777
+ } else {
3778
+ /* The CONSUMER owns the transfer. Rest until it says otherwise by calling row.uploading():
3779
+ a file that has only been chosen must not claim to be uploading. A progress bar that
3780
+ never moves reads as a hung upload, and gets reported as a bug against a transfer that
3781
+ was never started. */
3782
+ row.ready();
3762
3783
  }
3763
3784
  zone.dispatchEvent(new CustomEvent('fdy-upload-add', {
3764
3785
  bubbles: true, detail: { file: file, rejected: !!reason, reason: reason, row: row }
@@ -184,7 +184,7 @@ live docs also have a copy button per component.
184
184
  ```bash
185
185
  npm i @cahyo-dimas/freeday
186
186
  ```
187
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.23.0"` (public npm package). `dist/` is
187
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.24.0"` (public npm package). `dist/` is
188
188
  committed and published → no build step; `npm ci` runs without auth.
189
189
 
190
190
  ### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -71,7 +71,7 @@
71
71
  "scripts": {
72
72
  "build": "node tokens/build.mjs",
73
73
  "test": "node --test",
74
- "test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs",
74
+ "test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs",
75
75
  "prepack": "node tokens/build.mjs",
76
76
  "version": "node tokens/build.mjs && git add dist",
77
77
  "typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"