@cahyo-dimas/freeday 1.17.0 → 1.20.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 (52) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/COMPONENTS.md +748 -0
  3. package/README.id.md +20 -2
  4. package/README.md +32 -8
  5. package/USAGE.md +127 -0
  6. package/adapters/blazor/FdyTable.razor.cs +66 -7
  7. package/adapters/blazor/TableTypes.cs +6 -0
  8. package/adapters/react/components/FdyTable.tsx +43 -8
  9. package/adapters/vue/components/FdyDrawer.vue +5 -2
  10. package/adapters/vue/components/FdyModal.vue +5 -2
  11. package/adapters/vue/components/FdyTable.vue +42 -6
  12. package/dist/freeday.bundle.css +120 -9
  13. package/dist/freeday.css +103 -8
  14. package/dist/freeday.tokens.css +17 -1
  15. package/docs/agent-onboarding.md +151 -0
  16. package/docs/getting-started.md +454 -0
  17. package/docs/integrations.md +321 -0
  18. package/docs/reference-screen.html +461 -0
  19. package/package.json +11 -3
  20. package/src/components/avatar.css +8 -8
  21. package/src/components/card.css +3 -0
  22. package/src/components/chip.css +12 -0
  23. package/src/components/composition.css +49 -0
  24. package/src/components/list.css +29 -0
  25. package/tokens/breakpoints.d.ts +3 -0
  26. package/tokens/breakpoints.mjs +8 -1
  27. package/tokens/tokens.json +14 -2
  28. package/src/components/.gitkeep +0 -0
  29. package/src/freeday-autocomplete.js +0 -135
  30. package/src/freeday-breakpoint.js +0 -51
  31. package/src/freeday-carousel.js +0 -111
  32. package/src/freeday-cascade.js +0 -256
  33. package/src/freeday-cfl.js +0 -213
  34. package/src/freeday-chart.js +0 -429
  35. package/src/freeday-chip.js +0 -83
  36. package/src/freeday-datepicker.js +0 -321
  37. package/src/freeday-datetime.js +0 -83
  38. package/src/freeday-drawer.js +0 -43
  39. package/src/freeday-form.js +0 -181
  40. package/src/freeday-mask.js +0 -114
  41. package/src/freeday-menu.js +0 -93
  42. package/src/freeday-popover.js +0 -69
  43. package/src/freeday-rating.js +0 -50
  44. package/src/freeday-select.js +0 -218
  45. package/src/freeday-slider.js +0 -34
  46. package/src/freeday-stepper.js +0 -90
  47. package/src/freeday-table.js +0 -475
  48. package/src/freeday-tabs.js +0 -68
  49. package/src/freeday-timepicker.js +0 -180
  50. package/src/freeday-toast.js +0 -105
  51. package/src/freeday-tree.js +0 -94
  52. package/src/freeday-upload.js +0 -206
@@ -0,0 +1,454 @@
1
+ # Freeday — Getting Started (per stack)
2
+
3
+ A step-by-step guide to adopting Freeday in **your new project**. Pick your stack:
4
+
5
+ **[Static HTML](#static-html-no-build)** · **[Vue 3 (Vite)](#vue-3-vite)** · **[React (Vite)](#react-vite)** · **[Blazor (WASM)](#blazor-wasm)**
6
+
7
+ > **Component reference** (each component's exact markup + ARIA): live docs →
8
+ > <https://cahyo-dimas.github.io/freeday-ui-kit/> (open a component section, copy its markup).
9
+ > **Ecosystem library map & how to bridge:** [`integrations.md`](integrations.md).
10
+
11
+ ---
12
+
13
+ ## Core concepts (read once, applies to every stack)
14
+
15
+ Freeday = **CSS** (semantic tokens + `fdy-*` classes) + **zero-dependency JS enhancers** (optional).
16
+
17
+ 1. **Static vs interactive.** Static components (button, card, badge, plain input, layout) need
18
+ only the **`fdy-*` classes** — no JS. Interactive components (select/combo, cascade, date/time
19
+ picker, table, dropzone, form validation, input mask, chip) need the **JS enhancers**.
20
+ 2. **The enhancer is the source of truth.** You don't re-implement components; the enhancer owns
21
+ the widget's DOM. You **listen for `fdy-*` events** (all bubbling `CustomEvent`s, data in
22
+ `event.detail`) → store them in your framework state. Event/API contract table:
23
+ [`integrations.md` §Event & API contract](integrations.md).
24
+ 3. **Hydrate dynamic DOM.** Enhancers auto-init once on `DOMContentLoaded`. DOM an SPA renders
25
+ **after** that must be re-hydrated: `window.Freeday<X>.initAll(el)` (idempotent, safe to repeat).
26
+ Each framework's adapter wraps this — you don't call it manually.
27
+ 4. **Theme via `data-*` on `<html>`.** `data-theme="light|dark"` (all semantic tokens switch) +
28
+ `data-density="comfortable|compact"` (control height, for data-dense screens). Change at runtime:
29
+ `document.documentElement.dataset.theme = 'dark'`.
30
+ 5. **3-tier token rule.** Components only touch **Tier 2/3** (`var(--color-primary)`,
31
+ `var(--space-4)`, `var(--radius-md)`…). **Never** write raw hex/px.
32
+ 6. **Scope: components + tokens, deliberately *not* layout.** Freeday ships components and tokens;
33
+ the only layout helpers are `.fdy-hidden` / `.fdy-visually-hidden`. Stacks, grids, gaps and sizing
34
+ come from **your** layout layer — pair Freeday with a utility framework (Tailwind, UnoCSS…) run
35
+ **utilities-only, preflight OFF** (Freeday's `base.css` is your reset). Two consequences worth
36
+ knowing up front:
37
+ - **base.css is a *light* reset** — it does not strip `ul`/`ol`/`p` margins. With preflight off, a
38
+ semantic `<ul>` keeps native bullets + a 40px indent; add **`.fdy-list-reset`** (or use a Freeday
39
+ list component) on such lists.
40
+ - **The spacing scale is public.** `--space-0`…`--space-24`, `--radius-*`, `--dur-*` etc. are real
41
+ custom properties in `dist/freeday.tokens.css` — **define your utility theme in terms of them**
42
+ (`spacing: { 4: 'var(--space-4)' }`) so both systems stay in step. `data-density="compact"` steps
43
+ `--control-h` **and** the mid-range spacing scale (`--space-3`…`--space-6`), so Freeday components
44
+ densify — and if your utility theme is built on `var(--space-N)`, density reaches your utilities too.
45
+ 7. **Load the fonts — the package does not.** The type tokens *name* **Sora** (display), **IBM Plex
46
+ Sans** (body) and **JetBrains Mono** (data), but Freeday bundles no `@font-face` and no font files.
47
+ Load them yourself, or the kit renders in the system fallback — which reads as "unfinished design",
48
+ not "missing dependency". One line with [Fontsource](https://fontsource.org):
49
+ ```css
50
+ @import '@fontsource/sora/600.css'; @import '@fontsource/sora/700.css';
51
+ @import '@fontsource-variable/ibm-plex-sans'; @import '@fontsource/jetbrains-mono/500.css';
52
+ ```
53
+ (Or a `<link>` to your own self-hosted copies, or override `--font-display`/`--font-body`/`--font-mono`
54
+ to faces you already ship. If you keep a system-sans fallback, consider softening
55
+ `--tracking-tighter` on headings — it's tuned for Sora's proportions.)
56
+ 8. **Start from the shell, then compose.** Every application goes inside **`.fdy-app`** (see below);
57
+ inside it, assemble screens from the composition primitives — `.fdy-page`, `.fdy-page__header`,
58
+ `.fdy-page-section`, `.fdy-toolbar`, `.fdy-stats`/`.fdy-stat` — and the type roles (`.fdy-title-page`
59
+ / `-section` / `-card`), not by re-using `.fdy-card__title` for everything. **Which token/role/shadow
60
+ to use when lives in [`USAGE.md`](../USAGE.md)** — read it once; it's what makes screens cohere.
61
+
62
+ ---
63
+
64
+ ## The app shell (start here)
65
+
66
+ Every Freeday application goes inside **`.fdy-app`** — the frame that holds a top bar, a sidebar, and
67
+ the scrolling content. Don't hand-roll one from flexbox; the responsive sidebar + backdrop are built in.
68
+ The nav toggle is one line: toggle `.fdy-app--nav-open` (mobile drawer) / `.fdy-app--nav-collapsed`
69
+ (desktop) on the `.fdy-app` element from the `__navtoggle` button's click.
70
+
71
+ The nesting is not free-form — `.fdy-app` is a flex **row** of `[sidebar | content]`, and `__content`
72
+ is the column that holds the topbar and the main area (it gives the sticky topbar a tall containing
73
+ block to travel in). The brand belongs in the **sidebar**, sized to match the topbar's height:
74
+
75
+ ```html
76
+ <div class="fdy-app">
77
+ <a class="fdy-skip" href="#main">Skip to content</a>
78
+
79
+ <aside class="fdy-app__sidebar">
80
+ <a class="fdy-app__brand" href="/">
81
+ <span class="fdy-app__brand-mark"><!-- logo --></span>
82
+ <span class="fdy-app__brand-text">
83
+ <span class="fdy-app__brand-title">Acme</span>
84
+ <span class="fdy-app__brand-subtitle">Finance</span><!-- optional -->
85
+ </span>
86
+ </a>
87
+ <nav class="fdy-nav"><!-- .fdy-nav__item … --></nav>
88
+ </aside>
89
+
90
+ <div class="fdy-app__content">
91
+ <header class="fdy-app__topbar">
92
+ <button class="fdy-app__navtoggle" aria-label="Toggle navigation"><!-- hamburger svg --></button>
93
+ <h1 class="fdy-app__title">Invoices</h1><!-- auto-spacer: what follows goes right -->
94
+ <!-- topbar actions … -->
95
+ </header>
96
+ <main class="fdy-app__main" id="main">
97
+ <!-- YOUR SCREEN: a .fdy-page … (see USAGE.md) -->
98
+ </main>
99
+ </div>
100
+
101
+ <div class="fdy-app__backdrop"></div>
102
+ </div>
103
+ ```
104
+
105
+ `.fdy-app__main` already carries the page padding (`--space-8`, `--space-5` on mobile) — don't wrap
106
+ your screen in another padded box. The toggle's two states split at **720px**: above it,
107
+ `.fdy-app--nav-collapsed` collapses the sidebar to zero width; at or below it,
108
+ `.fdy-app--nav-open` slides the sidebar in as an off-canvas drawer over the backdrop. A complete,
109
+ working version of all of this — including the toggle script — is
110
+ [`reference-screen.html`](reference-screen.html).
111
+
112
+ Then compose the screen inside `__main` with `.fdy-page` / `.fdy-page__header` / `.fdy-page-section`
113
+ / `.fdy-stats` and the type roles. The live **App shell** + **Sidebar menu** demos in
114
+ [`docs/index.html`](index.html) are copy-pasteable; **[`USAGE.md`](../USAGE.md)** says which role and
115
+ token to use where.
116
+
117
+ ---
118
+
119
+ ## Static HTML (no build)
120
+
121
+ Good for plain `.html` pages / templates — no bundler, no npm.
122
+
123
+ ### 1. Get the dist files into your project
124
+ `dist/` is committed, so there's no build step. Easiest way — use npm once just to download, then
125
+ copy the files (vendor them):
126
+ ```bash
127
+ npm i @cahyo-dimas/freeday
128
+ cp -r node_modules/@cahyo-dimas/freeday/dist ./assets/freeday # copy into your project
129
+ ```
130
+ (or `git clone` the repo and copy `dist/`, or download the files one by one). The minimum you need:
131
+ `freeday.bundle.css` (tokens + components in one) and `freeday.js` (all enhancers).
132
+
133
+ ### 2. Set the theme on `<html>` + link the CSS
134
+ ```html
135
+ <!doctype html>
136
+ <html lang="en" data-theme="light" data-density="comfortable">
137
+ <head>
138
+ <meta charset="utf-8">
139
+ <meta name="viewport" content="width=device-width, initial-scale=1">
140
+ <link rel="stylesheet" href="assets/freeday/freeday.bundle.css">
141
+ </head>
142
+ ```
143
+ > Two-file alternative: `freeday.tokens.css` (tokens) + `freeday.css` (components).
144
+
145
+ ### 3. Load the enhancers before `</body>`
146
+ ```html
147
+ <script src="assets/freeday/freeday.js" defer></script>
148
+ <!-- or pick per-file: freeday-select.js, freeday-table.js, freeday-datepicker.js, … -->
149
+ </body>
150
+ ```
151
+
152
+ ### 4. Use `fdy-*` classes + `data-fdy-*` hooks
153
+ ```html
154
+ <button class="fdy-btn fdy-btn--primary" type="button">Save</button>
155
+
156
+ <div data-fdy-datepicker></div> <!-- enhancer auto-inits on DOMContentLoaded -->
157
+ ```
158
+ Listen for events as needed; for DOM you add **dynamically** after load, re-hydrate:
159
+ ```html
160
+ <script>
161
+ document.addEventListener('fdy-datepicker-change', (e) => console.log(e.detail.value));
162
+ // after inserting new markup dynamically:
163
+ // window.FreedayDatepicker.initAll(containerEl);
164
+ </script>
165
+ ```
166
+
167
+ ### 5. Copy component markup
168
+ From **[`COMPONENTS.md`](../COMPONENTS.md)** (every component's classes + minimal markup, shipped in
169
+ the package) or **[`reference-screen.html`](reference-screen.html)** for a whole assembled screen. The
170
+ live docs also have a copy button per component.
171
+
172
+ ---
173
+
174
+ ## Vue 3 (Vite)
175
+
176
+ ### 1. Install
177
+ ```bash
178
+ npm i @cahyo-dimas/freeday
179
+ ```
180
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.20.0"` (public npm package). `dist/` is
181
+ committed and published → no build step; `npm ci` runs without auth.
182
+
183
+ ### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
184
+ ```ts
185
+ import { createApp } from 'vue';
186
+ import '@cahyo-dimas/freeday/css'; // tokens + components (single file)
187
+ import '@cahyo-dimas/freeday'; // side-effect: registers every window.Freeday* enhancer
188
+ import App from './App.vue';
189
+
190
+ createApp(App).mount('#app');
191
+ ```
192
+
193
+ ### 3. Set the theme on the root (`index.html`)
194
+ ```html
195
+ <html lang="en" data-theme="light" data-density="comfortable">
196
+ ```
197
+
198
+ ### 4. Use `fdy-*` + hydrate via `useFreeday`
199
+ Call `useFreeday(root)` **once** per component; put `ref="root"` on the subtree container. `fdy-*`
200
+ events are bubbling `CustomEvent`s → use native `v-on` (`@fdy-*`) and read `event.detail` (typed).
201
+ ```vue
202
+ <script setup lang="ts">
203
+ import { ref, reactive } from 'vue';
204
+ import { useFreeday } from '@cahyo-dimas/freeday/vue';
205
+ import type { FdyCascadeChangeDetail, FdyDatepickerChangeDetail } from '@cahyo-dimas/freeday/vue';
206
+
207
+ const root = ref<HTMLElement | null>(null);
208
+ useFreeday(root); // hydrate [data-fdy-*] in the subtree, on each mount + update (idempotent)
209
+
210
+ const form = reactive({ category: '', dueDate: '' });
211
+ const onCascade = (e: Event) => { form.category = (e as CustomEvent<FdyCascadeChangeDetail>).detail.value; };
212
+ const onDate = (e: Event) => { form.dueDate = (e as CustomEvent<FdyDatepickerChangeDetail>).detail.value; };
213
+ </script>
214
+
215
+ <template>
216
+ <div ref="root">
217
+ <button class="fdy-btn fdy-btn--primary" type="button">Save</button>
218
+ <div data-fdy-cascade @fdy-cascade-change="onCascade">…</div>
219
+ <div data-fdy-datepicker @fdy-datepicker-change="onDate">…</div>
220
+ </div>
221
+ </template>
222
+ ```
223
+
224
+ **Gotcha:** if TypeScript complains about `import '@cahyo-dimas/freeday/css'`, make sure `env.d.ts`
225
+ has `/// <reference types="vite/client" />`. For **Nuxt/SSR**, enhancers are client-only — wrap them
226
+ in `onMounted`/`<ClientOnly>`.
227
+
228
+ Full working example: [`examples/vue-faktur/`](../examples/vue-faktur/).
229
+
230
+ ---
231
+
232
+ ## React (Vite)
233
+
234
+ ### 1. Install
235
+ ```bash
236
+ npm i @cahyo-dimas/freeday
237
+ ```
238
+
239
+ ### 2. Import the CSS + enhancers **once** in your entry (`src/main.tsx`)
240
+ ```tsx
241
+ import { StrictMode } from 'react';
242
+ import { createRoot } from 'react-dom/client';
243
+ import '@cahyo-dimas/freeday/css'; // tokens + components
244
+ import '@cahyo-dimas/freeday'; // registers every window.Freeday* enhancer
245
+ import { App } from './App';
246
+
247
+ createRoot(document.getElementById('root')!).render(
248
+ <StrictMode><App /></StrictMode>,
249
+ );
250
+ ```
251
+
252
+ ### 3. Set the theme on the root (`index.html`)
253
+ ```html
254
+ <html lang="en" data-theme="light" data-density="comfortable">
255
+ ```
256
+
257
+ ### 4. Use `fdy-*` + hydrate via the `useFreeday` hook
258
+ React has no native `on:fdy-*` handler → since the events **bubble**, attach one set of listeners on
259
+ `root` via `useEffect` (clean up on unmount). Read `event.detail` (typed).
260
+ ```tsx
261
+ import { useRef, useEffect } from 'react';
262
+ import { useFreeday } from '@cahyo-dimas/freeday/react';
263
+ import type { FdyCascadeChangeDetail, FdyDatepickerChangeDetail } from '@cahyo-dimas/freeday/react';
264
+
265
+ export function Panel() {
266
+ const root = useRef<HTMLDivElement>(null);
267
+ useFreeday(root); // hydrate subtree on mount + every commit (idempotent)
268
+
269
+ useEffect(() => {
270
+ const el = root.current;
271
+ if (!el) return;
272
+ const onCascade = (e: Event) => { /* (e as CustomEvent<FdyCascadeChangeDetail>).detail.value */ };
273
+ const onDate = (e: Event) => { /* (e as CustomEvent<FdyDatepickerChangeDetail>).detail.value */ };
274
+ el.addEventListener('fdy-cascade-change', onCascade);
275
+ el.addEventListener('fdy-datepicker-change', onDate);
276
+ return () => {
277
+ el.removeEventListener('fdy-cascade-change', onCascade);
278
+ el.removeEventListener('fdy-datepicker-change', onDate);
279
+ };
280
+ }, []);
281
+
282
+ return (
283
+ <div ref={root}>
284
+ <button className="fdy-btn fdy-btn--primary" type="button">Save</button>
285
+ <div data-fdy-cascade />
286
+ <div data-fdy-datepicker />
287
+ </div>
288
+ );
289
+ }
290
+ ```
291
+
292
+ **Gotcha:** because the enhancer owns the widget DOM, don't double-control it from React — store the
293
+ value from `event.detail` in state/ref; don't set the DOM `value` back. `StrictMode` mounts twice in
294
+ dev; `useFreeday` is idempotent, so it's safe.
295
+
296
+ ### 5. Alternative: typed controlled components (`FdyCombo` · `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart`)
297
+ For fields you'd normally write as a native `<select>`/`<input type="date">`,
298
+ `@cahyo-dimas/freeday/react` also exports typed **controlled** components — plain `value`/`onChange`,
299
+ no manual event bubbling (parity with the Vue `v-model` components above):
300
+ ```tsx
301
+ import { FdyCombo } from '@cahyo-dimas/freeday/react';
302
+ import type { FdyComboOption } from '@cahyo-dimas/freeday/react';
303
+
304
+ type Status = 'draft' | 'sent' | 'paid';
305
+ const options: ReadonlyArray<FdyComboOption<Status>> = [
306
+ { value: 'draft', label: 'Draft' },
307
+ { value: 'sent', label: 'Sent' },
308
+ { value: 'paid', label: 'Paid' },
309
+ ];
310
+
311
+ function StatusField({ value, onChange }: { value: Status; onChange: (v: Status) => void }) {
312
+ return <FdyCombo<Status> value={value} options={options} onChange={onChange} ariaLabelledby="lbl-status" />;
313
+ }
314
+ ```
315
+ `FdyDatepicker`, `FdyCfl` (async choose-from-list), and `FdyChart` share the same shape (typed
316
+ `value`/`onChange`, or `series`/`values` for `FdyChart`) — see [`integrations.md`](integrations.md)
317
+ and `examples/react-faktur/src/App.tsx` for the full patterns. **Vite works with no extra config**
318
+ (esbuild transpiles the `.tsx` source directly); **Next.js** consumers may need
319
+ `transpilePackages: ['@cahyo-dimas/freeday']` in `next.config.js`.
320
+
321
+ Full working example: [`examples/react-faktur/`](../examples/react-faktur/).
322
+
323
+ ---
324
+
325
+ ## Blazor (WASM)
326
+
327
+ Blazor doesn't use npm — Freeday is served as **static files** in `wwwroot/`.
328
+
329
+ > **Prefer the native components?** Jump to [§4 — the `Freeday.Blazor` RCL](#4-recommended-native-typed-components-freedayblazor-rcl):
330
+ > typed `<FdyX>` with `@bind`, no manual JS interop. Steps 1–2 (assets + scripts) still apply; step 3
331
+ > below (the raw enhancer + event bridge) is the underlying mechanism and the fallback for markup the
332
+ > RCL doesn't cover.
333
+
334
+ ### 1. Place the assets in `wwwroot/freeday/`
335
+ Copy 3 files into `wwwroot/freeday/`: `freeday.bundle.css`, `freeday.js` (from `dist/`), and
336
+ `freeday-blazor.js` (from `adapters/blazor/`). Manually, **or** automatically via an MSBuild target
337
+ (put the Freeday repo near your project and adjust the path) in `.csproj`:
338
+ ```xml
339
+ <Target Name="CopyFreedayAssets" BeforeTargets="ResolveStaticWebAssetsInputs;Build">
340
+ <ItemGroup>
341
+ <_FreedaySrc Include="PATH\dist\freeday.bundle.css;PATH\dist\freeday.js;PATH\adapters\blazor\freeday-blazor.js" />
342
+ </ItemGroup>
343
+ <Copy SourceFiles="@(_FreedaySrc)" DestinationFolder="$(MSBuildProjectDirectory)\wwwroot\freeday" SkipUnchangedFiles="true" />
344
+ </Target>
345
+ ```
346
+
347
+ ### 2. Set the theme + load the assets in `wwwroot/index.html`
348
+ Load `freeday.js` then `freeday-blazor.js` **before** `blazor.webassembly.js`:
349
+ ```html
350
+ <html lang="en" data-theme="light" data-density="comfortable">
351
+ <head>
352
+ <link rel="stylesheet" href="freeday/freeday.bundle.css" />
353
+ </head>
354
+ <body>
355
+ <div id="app">Loading…</div>
356
+ <script src="freeday/freeday.js"></script>
357
+ <script src="freeday/freeday-blazor.js"></script>
358
+ <script src="_framework/blazor.webassembly.js"></script>
359
+ </body>
360
+ ```
361
+ > Use the global IIFE (`window.FreedayBlazor`), **not** an ES module — so it passes strict-MIME on static hosts.
362
+
363
+ ### 3. Hydrate + bridge events in code-behind (`.razor.cs`)
364
+ In `OnAfterRenderAsync(firstRender)`: `initAll`, then `on(...)` per event → `[JSInvokable]` methods.
365
+ Release them in `DisposeAsync`.
366
+ ```csharp
367
+ public partial class Panel : ComponentBase, IAsyncDisposable
368
+ {
369
+ [Inject] private IJSRuntime JS { get; set; } = default!;
370
+ private ElementReference _root;
371
+ private DotNetObjectReference<Panel>? _self;
372
+ private readonly List<int> _tokens = new();
373
+
374
+ protected override async Task OnAfterRenderAsync(bool firstRender)
375
+ {
376
+ if (!firstRender) return;
377
+ await JS.InvokeVoidAsync("FreedayBlazor.initAll", _root); // hydrate the Blazor markup
378
+ _self = DotNetObjectReference.Create(this);
379
+ _tokens.Add(await JS.InvokeAsync<int>("FreedayBlazor.on", _root, "fdy-cascade-change", _self, nameof(OnCascade)));
380
+ _tokens.Add(await JS.InvokeAsync<int>("FreedayBlazor.on", _root, "fdy-datepicker-change", _self, nameof(OnDate)));
381
+ }
382
+
383
+ [JSInvokable] public void OnCascade(CascadeDetail d) { /* d.Value / d.Path */ StateHasChanged(); }
384
+ [JSInvokable] public void OnDate(ValueDetail d) { /* d.Value */ StateHasChanged(); }
385
+
386
+ public async ValueTask DisposeAsync()
387
+ {
388
+ foreach (var t in _tokens)
389
+ try { await JS.InvokeVoidAsync("FreedayBlazor.off", t); } catch (JSDisconnectedException) { }
390
+ _self?.Dispose();
391
+ }
392
+
393
+ public sealed record CascadeDetail(string Value, string Path, string[] Labels);
394
+ public sealed record ValueDetail(string Value);
395
+ }
396
+ ```
397
+ ```razor
398
+ @* Panel.razor — @ref on the subtree container, fdy-* classes + data-fdy-* hooks in the markup *@
399
+ <div @ref="_root">
400
+ <button class="fdy-btn fdy-btn--primary" type="button">Save</button>
401
+ <div data-fdy-cascade></div>
402
+ <div data-fdy-datepicker></div>
403
+ </div>
404
+ ```
405
+ Extras: `FreedayBlazor.toast(new { variant, title, message })` for toasts; `FreedayBlazor.toggleTheme()`
406
+ to flip the theme. Event DTOs are deserialized case-insensitively by Blazor.
407
+
408
+ ### 4. Recommended: native typed components (`Freeday.Blazor` RCL)
409
+
410
+ Instead of hand-writing `fdy-*` markup + the interop above, reference the **Razor Class Library** and
411
+ use typed `<FdyX>` components with `@bind` — the Blazor equivalent of the Vue `v-model` / React
412
+ `value`/`onChange` adapters. Place the Freeday repo near your solution and add a project reference:
413
+ ```xml
414
+ <!-- YourApp.csproj -->
415
+ <ProjectReference Include="PATH\adapters\blazor\Freeday.Blazor.csproj" />
416
+ ```
417
+ ```razor
418
+ @* _Imports.razor *@
419
+ @using Freeday.Blazor
420
+ ```
421
+ Load `freeday.js` + `freeday-blazor.js` exactly as in step 2 (the components still hydrate over the
422
+ kit's CSS/enhancers), then bind:
423
+ ```razor
424
+ @* Invoice.razor — no @ref, no manual JS interop, no [JSInvokable] *@
425
+ <FdyCombo TValue="string" @bind-Value="_status" Options="_statusOptions" AriaLabelledby="lbl-status" />
426
+ <FdyDatepicker @bind-Value="_dueDate" Label="Due date" />
427
+ <FdyTable TRow="Invoice" Columns="_cols" Rows="_rows" RowKey="@(i => i.Code)"
428
+ PageSize="10" RowActivatable="true" RowActivate="OpenDetail" />
429
+ <FdyChart Type="donut" Values="_byCity" Labels="_cityLabels" AriaLabel="Revenue by city" />
430
+ <FdyDrawer @bind-Open="_drawerOpen" Title="Detail" Side="right">…</FdyDrawer>
431
+ ```
432
+ Ten components at parity with the Vue/React adapters: **`FdyModal`** · **`FdyDrawer`** (`@bind-Open`,
433
+ `Title`, `Size`/`Side`, `Dismissible`) · **`FdyCombo<TValue>`** · **`FdyDatepicker`** ·
434
+ **`FdyAutocomplete`** · **`FdyCascade`** · **`FdyDateRange`** (`@bind-From`/`@bind-To`) ·
435
+ **`FdyCfl<TRow>`** (async `LoadPage`) · **`FdyChart`** · **`FdyTable<TRow>`** (client sort/filter/page,
436
+ or controlled `Sort`/`Filters`/`Page` for a server-paged table; `RowActivatable`, `RowDetail`). Each
437
+ `select`-type control also takes `Disabled`/`Readonly`/`Invalid`. The RCL targets **net8.0** and is
438
+ consumed as source (`<ProjectReference>`); `.NET bin/obj` never ships in the npm tarball.
439
+
440
+ Full working example (all ten): [`examples/blazor-faktur/`](../examples/blazor-faktur/) —
441
+ `Pages/ComponentsDemo.razor`.
442
+
443
+ ---
444
+
445
+ ## Verify (every stack)
446
+
447
+ Run the project → check two things:
448
+ 1. **CSS connected** — buttons/cards are styled (not plain HTML).
449
+ 2. **Enhancers connected** — interactive components come alive (e.g. datepicker/combo open on
450
+ click), and `event.detail` reaches your state.
451
+
452
+ If the visuals are plain → the CSS didn't load. If visuals are fine but widgets are dead → the
453
+ enhancers aren't hydrated (make sure `import '@cahyo-dimas/freeday'` / `<script freeday.js>` is
454
+ present, and the adapter/`initAll` is called for dynamic DOM).