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