@cahyo-dimas/freeday 3.10.0 → 3.12.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 (41) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/COMPONENTS.md +72 -3
  3. package/README.id.md +4 -4
  4. package/README.md +3 -3
  5. package/adapters/blazor/FdyCascade.razor +2 -1
  6. package/adapters/blazor/FdyCascade.razor.cs +7 -0
  7. package/adapters/blazor/FdyCombo.razor +2 -1
  8. package/adapters/blazor/FdyCombo.razor.cs +7 -0
  9. package/adapters/blazor/FdyDateRange.razor +20 -2
  10. package/adapters/blazor/FdyDateRange.razor.cs +20 -0
  11. package/adapters/blazor/FdyDatepicker.razor +1 -0
  12. package/adapters/blazor/FdyDatepicker.razor.cs +7 -0
  13. package/adapters/blazor/FdyMonthpicker.razor +1 -0
  14. package/adapters/blazor/FdyMonthpicker.razor.cs +7 -0
  15. package/adapters/blazor/FdyTimepicker.razor +24 -0
  16. package/adapters/blazor/FdyTimepicker.razor.cs +86 -0
  17. package/adapters/react/components/FdyCascade.tsx +10 -1
  18. package/adapters/react/components/FdyCombo.tsx +9 -0
  19. package/adapters/react/components/FdyDateRange.tsx +29 -0
  20. package/adapters/react/components/FdyDatepicker.tsx +10 -1
  21. package/adapters/react/components/FdyMonthpicker.tsx +10 -1
  22. package/adapters/react/components/FdyTimepicker.tsx +221 -0
  23. package/adapters/react/index.d.ts +1 -0
  24. package/adapters/react/index.js +1 -0
  25. package/adapters/vue/components/FdyCascade.vue +10 -1
  26. package/adapters/vue/components/FdyCombo.vue +9 -0
  27. package/adapters/vue/components/FdyDateRange.vue +29 -0
  28. package/adapters/vue/components/FdyDatepicker.vue +10 -1
  29. package/adapters/vue/components/FdyMonthpicker.vue +10 -1
  30. package/adapters/vue/components/FdyTimepicker.vue +285 -0
  31. package/adapters/vue/index.d.ts +1 -0
  32. package/adapters/vue/index.js +1 -0
  33. package/dist/freeday-cascade.js +5 -0
  34. package/dist/freeday-datepicker.js +5 -0
  35. package/dist/freeday-form.js +60 -0
  36. package/dist/freeday-timepicker.js +49 -1
  37. package/dist/freeday.js +119 -1
  38. package/docs/agent-onboarding.md +3 -3
  39. package/docs/getting-started.md +5 -5
  40. package/docs/integrations.md +2 -2
  41. package/package.json +2 -2
package/dist/freeday.js CHANGED
@@ -944,6 +944,11 @@
944
944
  if (!node) return;
945
945
  if (node.children) { drill(i); return; }
946
946
  selectedValue = node.value;
947
+ /* Mirror the value onto the ROOT, which until now only seeded it. `freeday-select.js` has
948
+ always done this, so this makes the family agree rather than inventing a second idea, and
949
+ it is what lets `freeday-form.js` require a kit control without knowing which control it
950
+ is: one attribute instead of five event names (#096). */
951
+ wrap.setAttribute('data-value', node.value);
947
952
  var labels = stack.map(function (n) { return n.label; }).concat([node.label]);
948
953
  valueSpan.textContent = labels.join(sep);
949
954
  valueSpan.classList.remove('fdy-cascade__value--placeholder');
@@ -2262,6 +2267,11 @@
2262
2267
  wrap.appendChild(panel);
2263
2268
 
2264
2269
  function updateDisplay() {
2270
+ /* Mirror the value onto the ROOT, which until now only seeded it. `freeday-select.js` has
2271
+ always done this, so this makes the family agree rather than inventing a second idea, and
2272
+ it is what lets `freeday-form.js` require a kit control without knowing which control it
2273
+ is: one attribute instead of five event names (#096). */
2274
+ wrap.setAttribute('data-value', selected ? (monthOnly ? toISOMonth(selected) : toISO(selected)) : '');
2265
2275
  if (selected) {
2266
2276
  /* Through the same `Intl` formatter the month CELLS and the day grid's title already use,
2267
2277
  which is the whole reason this mode exists: <input type="month"> names the month in the
@@ -3011,6 +3021,36 @@
3011
3021
 
3012
3022
  function fieldOf(el) { return el.closest('.fdy-field') || el.parentElement; }
3013
3023
 
3024
+ /* A kit control (combo, datepicker, monthpicker, timepicker, cascade) is NOT a form control.
3025
+ * Its trigger is a <button>, so `willValidate` is false and `form.elements` never offers it --
3026
+ * `required` on one meant nothing, and a form could not demand that it be filled (#096).
3027
+ *
3028
+ * Nothing here names a component. Two attributes carry it:
3029
+ * data-fdy-required the app asking for it, on the control's ROOT
3030
+ * data-value the live value, which every value-bearing kit control now mirrors
3031
+ * and the element that takes aria-invalid and focus is found by `aria-haspopup`, which every
3032
+ * popup-style kit trigger already sets. `FdyAutocomplete` and `FdyCfl` are absent on purpose:
3033
+ * they render a real <input>, so the NATIVE `required` is what fits them, and routing them
3034
+ * through here would give one field two sources of truth. No prop exposes that yet, which is a
3035
+ * separate gap and is written down in NEXT-UP.md rather than half-closed here. */
3036
+ function kitRoots(form) {
3037
+ return Array.prototype.slice.call(form.querySelectorAll('[data-fdy-required]'));
3038
+ }
3039
+
3040
+ /* The trigger, or the root itself if a control ever ships without one, so a missing trigger
3041
+ degrades to a visible error rather than a thrown TypeError. */
3042
+ function kitAria(root) {
3043
+ return root.querySelector('[aria-haspopup]') || root;
3044
+ }
3045
+
3046
+ function kitMessage(root) {
3047
+ if ((root.getAttribute('data-value') || '') !== '') return '';
3048
+ var form = root.closest('form');
3049
+ return root.getAttribute('data-fdy-msg-required')
3050
+ || root.getAttribute('data-fdy-msg')
3051
+ || textOf(form, 'required');
3052
+ }
3053
+
3014
3054
  function ensureErrorEl(el) {
3015
3055
  var field = fieldOf(el);
3016
3056
  var errEl = field ? field.querySelector('[data-fdy-error]') : null;
@@ -3101,6 +3141,21 @@
3101
3141
  function controls() {
3102
3142
  return Array.prototype.filter.call(form.elements, isCandidate);
3103
3143
  }
3144
+ function validateKit(root) {
3145
+ var msg = kitMessage(root);
3146
+ paint(kitAria(root), msg);
3147
+ return !msg;
3148
+ }
3149
+ /* The value lives in an attribute, and each control changes it through its own event name
3150
+ (fdy-change, fdy-datepicker-change, fdy-time-select, fdy-cascade-change). Observing the
3151
+ attribute is what keeps this enhancer from having to know any of them. */
3152
+ function kitLiveBind(root) {
3153
+ if (root.dataset.fdyKitBound === '1') return;
3154
+ root.dataset.fdyKitBound = '1';
3155
+ new MutationObserver(function () {
3156
+ if (submitted || eager) validateKit(root);
3157
+ }).observe(root, { attributes: true, attributeFilter: ['data-value'] });
3158
+ }
3104
3159
  function validateOne(el) {
3105
3160
  var msg = messageFor(el);
3106
3161
  paint(el, msg);
@@ -3115,6 +3170,7 @@
3115
3170
  }
3116
3171
 
3117
3172
  Array.prototype.forEach.call(controls(), liveBind);
3173
+ kitRoots(form).forEach(kitLiveBind);
3118
3174
 
3119
3175
  form.addEventListener('submit', function (e) {
3120
3176
  submitted = true;
@@ -3123,6 +3179,18 @@
3123
3179
  liveBind(el);
3124
3180
  if (!validateOne(el)) invalid.push(el);
3125
3181
  });
3182
+ kitRoots(form).forEach(function (root) {
3183
+ kitLiveBind(root);
3184
+ if (!validateKit(root)) invalid.push(kitAria(root));
3185
+ });
3186
+ /* DOM order, not native-then-kit: `invalid[0]` is what gets revealed and focused, and a
3187
+ reader sent to the second field while the first is also red learns the wrong thing. */
3188
+ invalid.sort(function (a, b) {
3189
+ var rel = a.compareDocumentPosition(b);
3190
+ if (rel & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
3191
+ if (rel & Node.DOCUMENT_POSITION_PRECEDING) return 1;
3192
+ return 0;
3193
+ });
3126
3194
  if (invalid.length) {
3127
3195
  e.preventDefault();
3128
3196
  reveal(invalid[0]);
@@ -3139,11 +3207,13 @@
3139
3207
  submitted = true;
3140
3208
  var invalid = [];
3141
3209
  Array.prototype.forEach.call(controls(), function (el) { if (!validateOne(el)) invalid.push(el); });
3210
+ kitRoots(form).forEach(function (root) { if (!validateKit(root)) invalid.push(kitAria(root)); });
3142
3211
  return invalid;
3143
3212
  },
3144
3213
  reset: function () {
3145
3214
  submitted = false;
3146
3215
  Array.prototype.forEach.call(controls(), function (el) { paint(el, ''); });
3216
+ kitRoots(form).forEach(function (root) { paint(kitAria(root), ''); });
3147
3217
  }
3148
3218
  };
3149
3219
  form._fdyForm = api;
@@ -4945,6 +5015,14 @@
4945
5015
  trigger.setAttribute('aria-haspopup', 'listbox');
4946
5016
  trigger.setAttribute('aria-expanded', 'false');
4947
5017
  trigger.setAttribute('aria-label', label);
5018
+ /* The trigger is a <button>, which is NOT a labelable element, so a form's own `<label for>`
5019
+ can never activate it -- but an id is still what `aria-describedby` targets and what a host
5020
+ needs to point at this control. Both were unreadable from the seed until now, so a Blazor
5021
+ host could not name its own error text. Mirrors the datepicker's seed. */
5022
+ var seedId = wrap.getAttribute('data-id');
5023
+ if (seedId) trigger.id = seedId;
5024
+ var describedby = wrap.getAttribute('data-describedby');
5025
+ if (describedby) trigger.setAttribute('aria-describedby', describedby);
4948
5026
  var valueSpan = document.createElement('span');
4949
5027
  valueSpan.className = 'fdy-timepicker__value';
4950
5028
  trigger.appendChild(valueSpan);
@@ -4982,8 +5060,37 @@
4982
5060
  wrap.appendChild(panel);
4983
5061
 
4984
5062
  var active = -1;
5063
+ /* A host that renders its seed ONCE and then never re-renders it (Blazor: ShouldRender is
5064
+ false after hydration) still has to be able to disable, lock or invalidate the field
5065
+ afterwards. Without this the parameter was there and nothing read it. Mirrors
5066
+ FreedayDatepicker.setState so the two controls of one family answer the same call. */
5067
+ var stateDisabled = wrap.hasAttribute('data-disabled');
5068
+ var stateReadonly = wrap.hasAttribute('data-readonly');
5069
+ var stateInvalid = wrap.hasAttribute('data-invalid');
5070
+
5071
+ function applyState() {
5072
+ trigger.disabled = stateDisabled;
5073
+ if (stateReadonly) trigger.setAttribute('aria-readonly', 'true');
5074
+ else trigger.removeAttribute('aria-readonly');
5075
+ if (stateInvalid) trigger.setAttribute('aria-invalid', 'true');
5076
+ else trigger.removeAttribute('aria-invalid');
5077
+ wrap.classList.toggle('fdy-timepicker--error', stateInvalid);
5078
+ if ((stateDisabled || stateReadonly) && !panel.hidden) close(false);
5079
+ }
5080
+ function setState(next) {
5081
+ if (!next) return;
5082
+ if (next.disabled != null) stateDisabled = !!next.disabled;
5083
+ if (next.readonly != null) stateReadonly = !!next.readonly;
5084
+ if (next.invalid != null) stateInvalid = !!next.invalid;
5085
+ applyState();
5086
+ }
4985
5087
 
4986
5088
  function updateDisplay() {
5089
+ /* Mirror the value onto the ROOT, which until now only seeded it. `freeday-select.js` has
5090
+ always done this, so this makes the family agree rather than inventing a second idea, and
5091
+ it is what lets `freeday-form.js` require a kit control without knowing which control it
5092
+ is: one attribute instead of five event names (#096). */
5093
+ wrap.setAttribute('data-value', value);
4987
5094
  if (value) {
4988
5095
  valueSpan.textContent = value;
4989
5096
  valueSpan.classList.remove('fdy-timepicker__value--placeholder');
@@ -5010,6 +5117,7 @@
5010
5117
  var _pop = null;
5011
5118
  function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
5012
5119
  function open() {
5120
+ if (stateDisabled || stateReadonly) return;
5013
5121
  if (!panel.hidden) return;
5014
5122
  var p = popCtl(); if (p) p.show(); else panel.hidden = false;
5015
5123
  trigger.setAttribute('aria-expanded', 'true');
@@ -5060,10 +5168,12 @@
5060
5168
  });
5061
5169
 
5062
5170
  updateDisplay();
5171
+ applyState();
5063
5172
 
5064
5173
  var api = {
5065
5174
  wrap: wrap,
5066
5175
  getValue: function () { return value; },
5176
+ setState: setState,
5067
5177
  clear: function () { value = ''; for (var i = 0; i < opts.length; i++) opts[i].setAttribute('aria-selected', 'false'); updateDisplay(); }
5068
5178
  };
5069
5179
  wrap._fdyTp = api;
@@ -5083,7 +5193,15 @@
5083
5193
  initAll();
5084
5194
  }
5085
5195
 
5086
- window.FreedayTimepicker = { init: initTime, initAll: initAll };
5196
+ window.FreedayTimepicker = {
5197
+ init: initTime,
5198
+ initAll: initAll,
5199
+ /* See api.setState above: the one call a render-once host needs. */
5200
+ setState: function (root, state) {
5201
+ var api = root && root._fdyTp ? root._fdyTp : null;
5202
+ if (api && api.setState) api.setState(state);
5203
+ }
5204
+ };
5087
5205
  })();
5088
5206
 
5089
5207
  /* Freeday, toast API (optional, zero-dependency).
@@ -33,9 +33,9 @@ and the code looks correct and fails later.**
33
33
  | Blazor (net8.0) | `@using Freeday.Blazor` (RCL) | `@bind-Value` |
34
34
  | Static HTML, Svelte, server-rendered templates… | no wrapper: raw markup + the enhancer script | `fdy-*` DOM events |
35
35
 
36
- The twelve: **FdyCombo · FdyDatepicker · FdyMonthpicker · FdyDateRange · FdyAutocomplete ·
37
- FdyCascade · FdyCfl · FdyChart · FdyTable · FdyModal · FdyDrawer · FdyAppShell**. In
38
- Vue/React/Blazor, **never hand-write the raw markup + enhancer for these twelve.** The raw path *appears* to work: the enhancer auto-initialises
36
+ The thirteen: **FdyCombo · FdyDatepicker · FdyMonthpicker · FdyDateRange · FdyTimepicker ·
37
+ FdyAutocomplete · FdyCascade · FdyCfl · FdyChart · FdyTable · FdyModal · FdyDrawer · FdyAppShell**. In
38
+ Vue/React/Blazor, **never hand-write the raw markup + enhancer for these thirteen.** The raw path *appears* to work: the enhancer auto-initialises
39
39
  once on `DOMContentLoaded` and the first render is correct. Then it fails quietly: DOM your framework
40
40
  renders later is never hydrated, and the widget's state lives in the DOM instead of in your
41
41
  framework's state.
@@ -21,8 +21,8 @@ Freeday = **CSS** (semantic tokens + `fdy-*` classes) + **zero-dependency JS enh
21
21
  the enhancer owns the widget's DOM. You **listen for `fdy-*` events** (all bubbling
22
22
  `CustomEvent`s, data in `event.detail`) → store them in your framework state. Event/API contract
23
23
  table: [`integrations.md` §Event & API contract](integrations.md).
24
- **On Vue, React or Blazor this is not the path to take for twelve of the components.** `FdyCombo`,
25
- `FdyDatepicker`, `FdyMonthpicker`, `FdyDateRange`, `FdyAutocomplete`, `FdyCascade`, `FdyCfl`,
24
+ **On Vue, React or Blazor this is not the path to take for thirteen of the components.** `FdyCombo`,
25
+ `FdyDatepicker`, `FdyMonthpicker`, `FdyDateRange`, `FdyTimepicker`, `FdyAutocomplete`, `FdyCascade`, `FdyCfl`,
26
26
  `FdyChart`, `FdyTable`, `FdyModal`, `FdyDrawer`, `FdyAppShell` ship typed wrappers that own the state properly (Vue and
27
27
  React re-implement the interaction natively; Blazor wraps the enhancer over interop). Use them;
28
28
  the raw path is for the components without a wrapper, and for stacks without an adapter.
@@ -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.10.0"` (public npm package). `dist/` is
201
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.12.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`)
@@ -451,9 +451,9 @@ kit's CSS/enhancers), then bind:
451
451
  <FdyChart Type="donut" Values="_byCity" Labels="_cityLabels" AriaLabel="Revenue by city" />
452
452
  <FdyDrawer @bind-Open="_drawerOpen" Title="Detail" Side="right">…</FdyDrawer>
453
453
  ```
454
- Twelve components at parity with the Vue/React adapters: **`FdyModal`** · **`FdyDrawer`** (`@bind-Open`,
454
+ Thirteen components at parity with the Vue/React adapters: **`FdyModal`** · **`FdyDrawer`** (`@bind-Open`,
455
455
  `Title`, `Size`/`Side`, `Dismissible`) · **`FdyCombo<TValue>`** · **`FdyDatepicker`** ·
456
- **`FdyMonthpicker`** (`@bind-Value` as `yyyy-MM`) · **`FdyAutocomplete`** · **`FdyCascade`** · **`FdyDateRange`** (`@bind-From`/`@bind-To`) ·
456
+ **`FdyMonthpicker`** (`@bind-Value` as `yyyy-MM`) · **`FdyTimepicker`** (`@bind-Value` as `HH:MM`) · **`FdyAutocomplete`** · **`FdyCascade`** · **`FdyDateRange`** (`@bind-From`/`@bind-To`) ·
457
457
  **`FdyCfl<TRow>`** (async `LoadPage`) · **`FdyChart`** · **`FdyTable<TRow>`** (client sort/filter/page,
458
458
  or controlled `Sort`/`Filters`/`Page` for a server-paged table; `RowActivatable`, `RowDetail`) ·
459
459
  **`FdyAppShell`** (`@bind-NavOpen`). Each
@@ -154,7 +154,7 @@ function Panel(): JSX.Element {
154
154
  ```
155
155
  > **Controlled alternative (parity with Vue's `v-model`):** `@cahyo-dimas/freeday/react` also exports
156
156
  > the typed components `FdyCombo` / `FdyDatepicker` / `FdyMonthpicker` / `FdyDateRange` /
157
- > `FdyAutocomplete` / `FdyCascade` / `FdyCfl` / `FdyChart` / `FdyTable` / `FdyModal` / `FdyDrawer` /
157
+ > `FdyTimepicker` / `FdyAutocomplete` / `FdyCascade` / `FdyCfl` / `FdyChart` / `FdyTable` / `FdyModal` / `FdyDrawer` /
158
158
  > `FdyAppShell`, with plain
159
159
  > `value`/`onChange` (props in, events out), no `data-fdy-*` + manual event listener:
160
160
  > ```tsx
@@ -200,7 +200,7 @@ document.addEventListener('fdy-form-invalid', (e) =>
200
200
 
201
201
  > **Native components (parity with Vue's `v-model` / React's `value`/`onChange`):** the
202
202
  > **`Freeday.Blazor`** RCL (net8.0, `adapters/blazor/`) ships the typed components `FdyCombo<TValue>` /
203
- > `FdyDatepicker` / `FdyMonthpicker` / `FdyDateRange` / `FdyAutocomplete` / `FdyCascade` /
203
+ > `FdyDatepicker` / `FdyMonthpicker` / `FdyDateRange` / `FdyTimepicker` / `FdyAutocomplete` / `FdyCascade` /
204
204
  > `FdyCfl<TRow>` / `FdyChart` / `FdyTable<TRow>` / `FdyModal` / `FdyDrawer` / `FdyAppShell` with `@bind`, no `@ref` / manual `initAll` / `[JSInvokable]`:
205
205
  > ```razor
206
206
  > @using Freeday.Blazor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "3.10.0",
3
+ "version": "3.12.0",
4
4
  "description": "Freeday: token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -96,7 +96,7 @@
96
96
  "scripts": {
97
97
  "build": "node tokens/build.mjs",
98
98
  "test": "node --test",
99
- "test:browser": "node --test --test-concurrency=3 browser/color-mix.mjs browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/field-width.mjs browser/cfl-multi.mjs browser/cfl-dialog-only.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs browser/bottom-nav.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/tabs-vertical.mjs browser/menu-align.mjs browser/cfl-narrow.mjs browser/form-reveal.mjs browser/confirm-search-copy.mjs browser/daterange-settled.mjs browser/display-parts.mjs browser/table-empty.mjs browser/table-footer-text.mjs browser/month-mode.mjs browser/monthpicker.mjs browser/page-columns.mjs browser/table-hover.mjs browser/typed-popover.mjs browser/filter-parity.mjs browser/filter-wrap.mjs browser/filter-stack.mjs browser/harness-contract.mjs",
99
+ "test:browser": "node --test --test-concurrency=3 browser/color-mix.mjs browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/field-width.mjs browser/cfl-multi.mjs browser/cfl-dialog-only.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs browser/bottom-nav.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/tabs-vertical.mjs browser/menu-align.mjs browser/cfl-narrow.mjs browser/form-reveal.mjs browser/confirm-search-copy.mjs browser/daterange-settled.mjs browser/display-parts.mjs browser/table-empty.mjs browser/table-footer-text.mjs browser/month-mode.mjs browser/monthpicker.mjs browser/page-columns.mjs browser/table-hover.mjs browser/typed-popover.mjs browser/filter-parity.mjs browser/filter-wrap.mjs browser/filter-stack.mjs browser/timepicker.mjs browser/form-kit-required.mjs browser/harness-contract.mjs",
100
100
  "prepack": "node tokens/build.mjs",
101
101
  "version": "node tokens/build.mjs && git add dist",
102
102
  "typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit",