@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.
- package/CHANGELOG.md +202 -0
- package/COMPONENTS.md +748 -0
- package/README.id.md +20 -2
- package/README.md +32 -8
- package/USAGE.md +127 -0
- package/adapters/blazor/FdyTable.razor.cs +66 -7
- package/adapters/blazor/TableTypes.cs +6 -0
- package/adapters/react/components/FdyTable.tsx +43 -8
- package/adapters/vue/components/FdyDrawer.vue +5 -2
- package/adapters/vue/components/FdyModal.vue +5 -2
- package/adapters/vue/components/FdyTable.vue +42 -6
- package/dist/freeday.bundle.css +120 -9
- package/dist/freeday.css +103 -8
- package/dist/freeday.tokens.css +17 -1
- package/docs/agent-onboarding.md +151 -0
- package/docs/getting-started.md +454 -0
- package/docs/integrations.md +321 -0
- package/docs/reference-screen.html +461 -0
- package/package.json +11 -3
- package/src/components/avatar.css +8 -8
- package/src/components/card.css +3 -0
- package/src/components/chip.css +12 -0
- package/src/components/composition.css +49 -0
- package/src/components/list.css +29 -0
- package/tokens/breakpoints.d.ts +3 -0
- package/tokens/breakpoints.mjs +8 -1
- package/tokens/tokens.json +14 -2
- package/src/components/.gitkeep +0 -0
- package/src/freeday-autocomplete.js +0 -135
- package/src/freeday-breakpoint.js +0 -51
- package/src/freeday-carousel.js +0 -111
- package/src/freeday-cascade.js +0 -256
- package/src/freeday-cfl.js +0 -213
- package/src/freeday-chart.js +0 -429
- package/src/freeday-chip.js +0 -83
- package/src/freeday-datepicker.js +0 -321
- package/src/freeday-datetime.js +0 -83
- package/src/freeday-drawer.js +0 -43
- package/src/freeday-form.js +0 -181
- package/src/freeday-mask.js +0 -114
- package/src/freeday-menu.js +0 -93
- package/src/freeday-popover.js +0 -69
- package/src/freeday-rating.js +0 -50
- package/src/freeday-select.js +0 -218
- package/src/freeday-slider.js +0 -34
- package/src/freeday-stepper.js +0 -90
- package/src/freeday-table.js +0 -475
- package/src/freeday-tabs.js +0 -68
- package/src/freeday-timepicker.js +0 -180
- package/src/freeday-toast.js +0 -105
- package/src/freeday-tree.js +0 -94
- package/src/freeday-upload.js +0 -206
package/README.id.md
CHANGED
|
@@ -5,13 +5,21 @@
|
|
|
5
5
|
> **Lebih banyak _free day_ buat dev — UI kit-nya sudah siap pakai.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.20.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`.
|
|
12
12
|
**Referensi hidup:** **[cahyo-dimas.github.io/freeday-ui-kit](https://cahyo-dimas.github.io/freeday-ui-kit/)** — atau buka `docs/index.html` langsung di browser.
|
|
13
13
|
|
|
14
14
|
> 🚀 **Baru mau pakai di project?** Langkah demi langkah per stack (HTML · Vue · React · Blazor): **[`docs/getting-started.md`](docs/getting-started.md)**.
|
|
15
|
+
>
|
|
16
|
+
> 📘 **Class apa saja yang ada, dan markup-nya?** Seluruh permukaan publik dalam satu file:
|
|
17
|
+
> **[`COMPONENTS.md`](COMPONENTS.md)**. **Kapan pakai yang mana:** **[`USAGE.md`](USAGE.md)**.
|
|
18
|
+
> Satu layar utuh yang sudah dirakit: **[`docs/reference-screen.html`](docs/reference-screen.html)**.
|
|
19
|
+
>
|
|
20
|
+
> 🤖 **Ngoding pakai AI agent (atau vibe code)?** Tak ada model yang tahu Freeday dari training —
|
|
21
|
+
> berikan **[`docs/agent-onboarding.md`](docs/agent-onboarding.md)** (blok instruksi siap-tempel +
|
|
22
|
+
> tabel mapping migrasi).
|
|
15
23
|
|
|
16
24
|
## Build
|
|
17
25
|
```bash
|
|
@@ -49,8 +57,18 @@ ter-publish → install tanpa build step; minify diserahkan ke bundler konsumen.
|
|
|
49
57
|
Kelas komponen berprefix `fdy-` (mis. `fdy-btn`, `fdy-card`, `fdy-badge`). Pakai langsung di
|
|
50
58
|
markup framework apa pun — Vue, React, Blazor, atau HTML polos.
|
|
51
59
|
|
|
60
|
+
> **⚠️ Muat font-nya sendiri — paket ini tidak.** Token tipe menamai **Sora** / **IBM Plex Sans** /
|
|
61
|
+
> **JetBrains Mono** tapi Freeday tak membundel file font. Muat (mis. `@import '@fontsource/sora/700.css'`
|
|
62
|
+
> …) atau override `--font-display`/`--font-body`/`--font-mono` — kalau tidak, kit jatuh ke fallback
|
|
63
|
+
> sistem dan terlihat "belum jadi". Detail: [`docs/getting-started.md`](docs/getting-started.md).
|
|
64
|
+
|
|
65
|
+
> **Token/role mana dipakai kapan → [`USAGE.md`](USAGE.md).** Freeday menjaga konsistensi *nilai*;
|
|
66
|
+
> `USAGE.md` = doktrin yang menjaga konsistensi *keputusan* — role tipe (`.fdy-title-page/-section/-card`),
|
|
67
|
+
> ritme spasi, elevasi, satu-primary-per-layar, palet kategorikal `--tone-1…8`, dan primitif komposisi
|
|
68
|
+
> halaman (`.fdy-page`, `.fdy-page-section`, `.fdy-stats`). Tiap app mulai di `.fdy-app`.
|
|
69
|
+
|
|
52
70
|
> **Scope: komponen + token, bukan layout.** Freeday punya komponen & design token; helper layout
|
|
53
|
-
> cuma `.fdy-hidden` / `.fdy-visually-hidden
|
|
71
|
+
> cuma `.fdy-hidden` / `.fdy-visually-hidden` plus primitif komposisi di atas. Layout grid dari layer-mu sendiri —
|
|
54
72
|
> pasangkan dengan utility framework (Tailwind, UnoCSS…) mode **utilities-only, preflight OFF**
|
|
55
73
|
> (`base.css` Freeday = reset-nya). `base.css` itu reset *ringan* (tak me-reset margin `ul`/`ol`/`p` —
|
|
56
74
|
> pakai `.fdy-list-reset` atau komponen list Freeday), dan skala spacing/radius/durasi adalah custom
|
package/README.md
CHANGED
|
@@ -5,13 +5,21 @@
|
|
|
5
5
|
> **More free days for devs — the UI kit is ready to use.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.20.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`.
|
|
12
12
|
**Living reference:** **[cahyo-dimas.github.io/freeday-ui-kit](https://cahyo-dimas.github.io/freeday-ui-kit/)** — or open `docs/index.html` directly in a browser.
|
|
13
13
|
|
|
14
14
|
> 🚀 **Starting a project?** Step-by-step per stack (HTML · Vue · React · Blazor): **[`docs/getting-started.md`](docs/getting-started.md)**.
|
|
15
|
+
>
|
|
16
|
+
> 📘 **Which class exists, and what's its markup?** The whole public surface in one flat file:
|
|
17
|
+
> **[`COMPONENTS.md`](COMPONENTS.md)**. **Which one to reach for, and when:** **[`USAGE.md`](USAGE.md)**.
|
|
18
|
+
> A full screen assembled: **[`docs/reference-screen.html`](docs/reference-screen.html)**.
|
|
19
|
+
>
|
|
20
|
+
> 🤖 **Building with an AI agent (or vibe-coding)?** No model knows Freeday from training — give it
|
|
21
|
+
> **[`docs/agent-onboarding.md`](docs/agent-onboarding.md)** (paste-ready project instructions +
|
|
22
|
+
> a migration mapping table).
|
|
15
23
|
|
|
16
24
|
## Build
|
|
17
25
|
```bash
|
|
@@ -49,13 +57,23 @@ bundler. Because it's on **public npm**, `npm ci` runs in CI without auth or an
|
|
|
49
57
|
Component classes are prefixed `fdy-` (e.g. `fdy-btn`, `fdy-card`, `fdy-badge`). Use them directly
|
|
50
58
|
in any framework's markup — Vue, React, Blazor, or plain HTML.
|
|
51
59
|
|
|
60
|
+
> **⚠️ Load the fonts — the package doesn't.** The type tokens name **Sora** / **IBM Plex Sans** /
|
|
61
|
+
> **JetBrains Mono** but Freeday bundles no font files. Load them (e.g. `@import '@fontsource/sora/700.css'`
|
|
62
|
+
> …) or override `--font-display`/`--font-body`/`--font-mono` — otherwise the kit renders in the system
|
|
63
|
+
> fallback and looks unfinished. See [`docs/getting-started.md` §Core concepts](docs/getting-started.md).
|
|
64
|
+
|
|
65
|
+
> **Which token/role to use when → [`USAGE.md`](USAGE.md).** Freeday enforces consistent *values*;
|
|
66
|
+
> `USAGE.md` is the doctrine that also makes *decisions* consistent — type roles (`.fdy-title-page/-section/-card`),
|
|
67
|
+
> spacing rhythm, elevation, one-primary-per-screen, the `--tone-1…8` categorical palette, and the page-
|
|
68
|
+
> composition primitives (`.fdy-page`, `.fdy-page-section`, `.fdy-stats`). Every app starts in `.fdy-app`.
|
|
69
|
+
|
|
52
70
|
> **Scope: components + tokens, not layout.** Freeday owns components and design tokens; the only
|
|
53
|
-
> layout helpers are `.fdy-hidden` / `.fdy-visually-hidden
|
|
54
|
-
> with a utility framework (Tailwind, UnoCSS…) run **utilities-only
|
|
55
|
-
> `base.css` is the reset). `base.css` is a *light* reset (it doesn't
|
|
56
|
-
> use `.fdy-list-reset` or a Freeday list component), and the
|
|
57
|
-
> public custom properties (`--space-0`…`--space-24`, …) you can
|
|
58
|
-
> notes: [`docs/getting-started.md` §Core concepts](docs/getting-started.md).
|
|
71
|
+
> layout helpers are `.fdy-hidden` / `.fdy-visually-hidden` plus the page-composition primitives above.
|
|
72
|
+
> Bring your own grid layer — pair it with a utility framework (Tailwind, UnoCSS…) run **utilities-only
|
|
73
|
+
> with preflight OFF** (Freeday's `base.css` is the reset). `base.css` is a *light* reset (it doesn't
|
|
74
|
+
> strip `ul`/`ol`/`p` margins — use `.fdy-list-reset` or a Freeday list component), and the
|
|
75
|
+
> spacing/radius/duration scales are public custom properties (`--space-0`…`--space-24`, …) you can
|
|
76
|
+
> build your utility theme on. Full notes: [`docs/getting-started.md` §Core concepts](docs/getting-started.md).
|
|
59
77
|
|
|
60
78
|
> `.fdy-btn` is **already** the primary button — there's no separate `.fdy-btn--primary` modifier.
|
|
61
79
|
> Modifiers for other variants: `--ghost`, `--danger`, `--text`, `--sm`, `--lg`, `--icon`
|
|
@@ -177,10 +195,16 @@ src/components/*.css one file per component (fdy-*)
|
|
|
177
195
|
src/*.js optional JS enhancers (reference, vanilla)
|
|
178
196
|
dist/ build output (COMMITTED):
|
|
179
197
|
freeday.tokens.css semantic tokens (light/dark/compact)
|
|
180
|
-
freeday.css
|
|
198
|
+
freeday.css every component (NO tokens)
|
|
199
|
+
freeday.bundle.css tokens + components — link this one
|
|
181
200
|
freeday.js bundle of every enhancer (single <script>)
|
|
182
201
|
freeday-*.js per-file enhancers
|
|
202
|
+
COMPONENTS.md every class + markup skeleton + a11y contract
|
|
203
|
+
USAGE.md the usage doctrine (which token/role when)
|
|
183
204
|
docs/index.html living reference / demo site
|
|
205
|
+
docs/reference-screen.html one complete screen, assembled
|
|
206
|
+
docs/agent-onboarding.md onboarding for AI coding agents
|
|
207
|
+
reference/ input material, never shipped (port source + layout archetypes)
|
|
184
208
|
```
|
|
185
209
|
|
|
186
210
|
## Component inventory
|
package/USAGE.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Freeday — Usage doctrine
|
|
2
|
+
|
|
3
|
+
Freeday ships tokens and components. This file ships the **decisions** — which token to use when,
|
|
4
|
+
how a page is assembled, what earns emphasis. A component library enforces consistent *values*; a
|
|
5
|
+
design system also enforces consistent *decisions*. Skim this once before building a screen; it is
|
|
6
|
+
what makes screens built by different people (or different sessions) look like one product.
|
|
7
|
+
|
|
8
|
+
The rules are opinionated on purpose. When one conflicts with a real need, break it deliberately —
|
|
9
|
+
but start here, not from a blank page.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Type roles — three title levels, not eleven
|
|
14
|
+
|
|
15
|
+
Do **not** reuse `.fdy-card__title` for a page title. Collapsing page/section/card into one style is
|
|
16
|
+
the single biggest cause of "flat grey mush". Pick the role, not the size:
|
|
17
|
+
|
|
18
|
+
| Role | Class | When |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Eyebrow | `.fdy-eyebrow` | A small uppercase label above a page/section title. Optional. |
|
|
21
|
+
| Page title | `.fdy-title-page` | One per screen. The `<h1>`. |
|
|
22
|
+
| Section title | `.fdy-title-section` | A region within the page (`<h2>`). |
|
|
23
|
+
| Card / row title | `.fdy-title-card` (or `.fdy-card__title`) | A title inside a card or list row (`<h3>`). |
|
|
24
|
+
| Body | *(default)* | Running text. |
|
|
25
|
+
| Muted / caption | `.fdy-text-muted`, `.fdy-text-caption` | Secondary text, timestamps, help. |
|
|
26
|
+
|
|
27
|
+
Everything else is body. If you reach for a fourth title size, you probably need a section, not a
|
|
28
|
+
font size.
|
|
29
|
+
|
|
30
|
+
## 2. Spacing rhythm — three gaps, always from the scale
|
|
31
|
+
|
|
32
|
+
Never a loose value; always `var(--space-N)` (a 4px scale). Three rhythms carry most layouts, and the
|
|
33
|
+
composition primitives apply them for you:
|
|
34
|
+
|
|
35
|
+
- **Between page sections:** `--space-8` — `.fdy-page` sets this gap between its children.
|
|
36
|
+
- **Within a group** (heading ↔ its body, cards in a list): `--space-4` — `.fdy-page-section` sets it.
|
|
37
|
+
- **Inside a control/card** (label ↔ input, icon ↔ text): `--space-2` / `--space-3`.
|
|
38
|
+
|
|
39
|
+
Reserve `--space-1` for hairline pairs and `--space-10`+ for deliberate breathing room (a hero, an
|
|
40
|
+
empty state). Don't scatter `--space-6` everywhere — a page with one gap value has no rhythm.
|
|
41
|
+
|
|
42
|
+
## 3. Elevation — most surfaces are flat
|
|
43
|
+
|
|
44
|
+
Shadow is a signal, not decoration. There are two families, and the difference matters:
|
|
45
|
+
|
|
46
|
+
| Level | Token | What actually uses it |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| Flat | *(none)* | Page sections, `.fdy-page`, the app shell, `.fdy-toolbar` |
|
|
49
|
+
| Hairline | `--shadow-1` | Bordered data containers: `.fdy-list`, `.fdy-table-wrap`, `.fdy-datatable`, `.fdy-stats--boxed` |
|
|
50
|
+
| Raised | `--shadow-2` | `.fdy-tooltip`, `.fdy-appbar--elevated` |
|
|
51
|
+
| Floating | `--shadow-3` | Things that float over the page: `.fdy-menu`, `.fdy-filter`, `.fdy-toast`, `.fdy-fab` |
|
|
52
|
+
| Overlay | `--shadow-4` | `.fdy-drawer` |
|
|
53
|
+
| **Lift** | `--shadow-lift` / `--shadow-lift-hover` | **`.fdy-card`** (and `--elevated` / `--interactive:hover`), `.fdy-modal` |
|
|
54
|
+
|
|
55
|
+
**`.fdy-card` is a lifted surface, not a hairline one** — `--shadow-lift` is a real 34px lift, ~6×
|
|
56
|
+
heavier than `--shadow-1`. That is the whole point of a card, and it is also why a *stack* of them
|
|
57
|
+
reads wrong: a list of ten cards is ten objects floating off the page. For rows, use the flat
|
|
58
|
+
container **`.fdy-list` / `.fdy-list__row`** (hairline border, `--color-border-muted` dividers, no
|
|
59
|
+
shadow). Reach for `.fdy-card` when something genuinely is one pickable object.
|
|
60
|
+
|
|
61
|
+
If every box on the screen has the same card shadow, none of them read as special — that's the
|
|
62
|
+
"identical card grid" failure. Prefer flat sections with **one** raised element that matters.
|
|
63
|
+
|
|
64
|
+
## 4. Emphasis — exactly one primary per screen
|
|
65
|
+
|
|
66
|
+
`.fdy-btn` is *already* the primary action (there is no `--primary` modifier). Use it **once** per
|
|
67
|
+
screen — the one thing you want the user to do. Everything else is `--ghost` or `--text`. Two primary
|
|
68
|
+
buttons on a screen means neither is. The same rule governs colour fills and `--shadow`: one focal
|
|
69
|
+
point, everything around it quiet.
|
|
70
|
+
|
|
71
|
+
## 5. Colour — semantic is reserved; categorical is `--tone`
|
|
72
|
+
|
|
73
|
+
- **Accent** (`--color-primary`, and `--color-accent` sparingly): interactive + brand. This is your
|
|
74
|
+
one accent hue.
|
|
75
|
+
- **Semantic is reserved** and is *not* your accent: `--color-success` = good only, `--color-warning`
|
|
76
|
+
= caution only, `--color-danger` = destructive/error only. Never decorative. Encode state in a pill
|
|
77
|
+
or chip, not just colour.
|
|
78
|
+
- **Categorical** (`--tone-1` … `--tone-8`, the general alias of the validated chart palette): N
|
|
79
|
+
visually-distinct **non-semantic** colours — avatar tones, category chips, tags, legend swatches.
|
|
80
|
+
Use the modifiers `.fdy-avatar--tone-N` / `.fdy-chip--tone-N` (both stay WCAG AA in light & dark),
|
|
81
|
+
and hash a stable index off the full string so the same category always gets the same colour.
|
|
82
|
+
- **Surfaces:** most backgrounds are `--color-surface`; `--color-surface-2`/`-3` for a recessed area;
|
|
83
|
+
`--color-primary-soft` only when you want a tinted callout, not as a default panel colour.
|
|
84
|
+
|
|
85
|
+
## 6. Density — `compact` for data-dense screens
|
|
86
|
+
|
|
87
|
+
`data-density="compact"` tightens control height **and** the mid-range spacing scale
|
|
88
|
+
(`--space-3`…`--space-6` step down a notch), so cards, toolbars and tables get denser. Use it on
|
|
89
|
+
table-heavy back-office screens; leave `comfortable` (the default) for forms.
|
|
90
|
+
|
|
91
|
+
**It is per-subtree, not only global.** The selector is a bare `[data-density="compact"]` and these
|
|
92
|
+
are inheriting custom properties, so the attribute works on `<html>` *or* on any wrapper — a route
|
|
93
|
+
container, a single `<section>`. An app whose two list screens are dense and whose three form screens
|
|
94
|
+
are not should scope it per screen rather than densifying everything. Set it at one level per screen,
|
|
95
|
+
never per component.
|
|
96
|
+
|
|
97
|
+
## 7. Assemble the page from the frame down
|
|
98
|
+
|
|
99
|
+
1. **Shell:** every application starts inside **`.fdy-app`** — a flex row of `__sidebar` (with
|
|
100
|
+
`__brand` + `.fdy-nav`) and `__content` (which holds `__topbar` + `__main`, plus `__navtoggle`
|
|
101
|
+
and `__backdrop`). The nesting is fixed; don't hand-roll a shell from flexbox — the toggle and
|
|
102
|
+
backdrop plumbing are already there. Skeleton: `COMPONENTS.md` §App shell; a working screen:
|
|
103
|
+
`docs/reference-screen.html`.
|
|
104
|
+
2. **Page:** wrap the screen body in **`.fdy-page`** (vertical section rhythm), opening with a
|
|
105
|
+
**`.fdy-page__header`** (eyebrow + `.fdy-title-page` + `.fdy-page__desc` on the left, the one
|
|
106
|
+
primary action on the right).
|
|
107
|
+
3. **Sections:** each region is a **`.fdy-page-section`** (a `.fdy-title-section` + optional
|
|
108
|
+
`.fdy-toolbar`, then its body).
|
|
109
|
+
**Toolbar or filter bar — pick by whether the fields carry visible labels.** `.fdy-toolbar` is
|
|
110
|
+
`align-items:center`, right for bare controls (buttons, a search box, chips); put a labelled
|
|
111
|
+
`.fdy-field` in it and that field sits half a label-height low against its neighbours. Fields
|
|
112
|
+
with visible labels belong in **`.fdy-filterbar`**, which is `align-items:flex-end` for exactly
|
|
113
|
+
this reason (and has the width rhythm `--w-sm`…`--w-grow`). In a toolbar, label fields with
|
|
114
|
+
`.fdy-visually-hidden` or a placeholder.
|
|
115
|
+
4. **KPIs:** a **`.fdy-stats`** grid of **`.fdy-stat`** tiles — deliberately *not* cards, so a metric
|
|
116
|
+
strip doesn't become an identical-card grid. Wrap in `.fdy-stats--boxed` for one shared strip.
|
|
117
|
+
5. **Content:** components (`.fdy-card`, `.fdy-datatable`, `.fdy-chart`, …) go inside sections.
|
|
118
|
+
|
|
119
|
+
Freeday deliberately owns **components + tokens, not layout**. Everything above is layout in the kit's
|
|
120
|
+
own language; for the rest (grids, one-off spacing), pair a utility framework run **utilities-only,
|
|
121
|
+
preflight-off** — and build its theme on `var(--space-N)` so the two systems agree. See
|
|
122
|
+
`docs/getting-started.md` §Core concepts.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
*Layout classes here live in `src/components/composition.css`. If a screen needs a primitive that
|
|
127
|
+
isn't here, it probably belongs here — open an issue rather than re-inventing it per screen.*
|
|
@@ -29,6 +29,24 @@ public partial class FdyTable<TRow>
|
|
|
29
29
|
/// <summary>Client-side page size when <see cref="Page"/> is absent; 0 = render all rows (no pager).</summary>
|
|
30
30
|
[Parameter] public int PageSize { get; set; }
|
|
31
31
|
|
|
32
|
+
/// <summary>
|
|
33
|
+
/// Controlled client-side page index (0-based). Set it — with <see cref="PageSize"/>, without
|
|
34
|
+
/// <see cref="Page"/> — to own the page while the table keeps doing filter/sort/paginate. That is
|
|
35
|
+
/// what lets an EXTERNAL pager drive the table: a responsive screen that hides the datatable below
|
|
36
|
+
/// the <c>md</c> breakpoint and renders a card list from <see cref="Process"/> can render one pager
|
|
37
|
+
/// for both breakpoints and bind it here. Leave null for the internal index (unchanged default).
|
|
38
|
+
/// </summary>
|
|
39
|
+
[Parameter] public int? PageIndex { get; set; }
|
|
40
|
+
|
|
41
|
+
/// <summary>Raised in client mode with <see cref="PageIndex"/> set: the table asks for a new
|
|
42
|
+
/// 0-based index (pager click, reset to 0 after sort/filter, or a clamp after filtering).</summary>
|
|
43
|
+
[Parameter] public EventCallback<int> PageIndexChanged { get; set; }
|
|
44
|
+
|
|
45
|
+
/// <summary>Raised whenever the processed page of rows (after filter/sort/paginate) or the total
|
|
46
|
+
/// changes, in BOTH modes — so the same processed set can drive a card list, a summary or an
|
|
47
|
+
/// export without re-deriving the pipeline. Mirrors <c>process</c> in the Vue/React adapters.</summary>
|
|
48
|
+
[Parameter] public EventCallback<FdyTableProcess<TRow>> Process { get; set; }
|
|
49
|
+
|
|
32
50
|
[Parameter] public bool Loading { get; set; }
|
|
33
51
|
[Parameter] public string LoadingText { get; set; } = "Loading…";
|
|
34
52
|
[Parameter] public string EmptyText { get; set; } = "No data";
|
|
@@ -55,10 +73,19 @@ public partial class FdyTable<TRow>
|
|
|
55
73
|
|
|
56
74
|
private List<TRow> _displayRows = new();
|
|
57
75
|
private int _totalCount;
|
|
76
|
+
// Recompute() is synchronous (it runs from OnParametersSet), but notifying the parent is async.
|
|
77
|
+
// Both notifications are therefore queued here and flushed in OnAfterRenderAsync.
|
|
78
|
+
private int? _pendingClamp;
|
|
79
|
+
private bool _processDirty;
|
|
58
80
|
|
|
59
81
|
private static readonly IReadOnlyDictionary<string, FdyColumnFilter> EmptyFilters = new Dictionary<string, FdyColumnFilter>();
|
|
60
82
|
|
|
61
83
|
private bool ServerPaged => Page is not null;
|
|
84
|
+
|
|
85
|
+
// Client-side page index: the parameter when the parent owns it, the field otherwise. Every read
|
|
86
|
+
// goes through ClientPageIndex and every write through SetClientPageAsync.
|
|
87
|
+
private bool PageIndexControlled => PageIndex is not null;
|
|
88
|
+
private int ClientPageIndex => PageIndexControlled ? Math.Max(0, PageIndex!.Value) : _internalPageIndex;
|
|
62
89
|
private bool SortControlled => ServerPaged || SortChanged.HasDelegate;
|
|
63
90
|
private bool FiltersControlled => ServerPaged || FiltersChanged.HasDelegate;
|
|
64
91
|
|
|
@@ -67,7 +94,7 @@ public partial class FdyTable<TRow>
|
|
|
67
94
|
FiltersControlled ? (Filters ?? EmptyFilters) : _internalFilters;
|
|
68
95
|
|
|
69
96
|
private int PageSizeEff => ServerPaged ? Page!.Size : PageSize;
|
|
70
|
-
private int CurrentPage1 => (ServerPaged ? Page!.Index :
|
|
97
|
+
private int CurrentPage1 => (ServerPaged ? Page!.Index : ClientPageIndex) + 1;
|
|
71
98
|
private int TotalCount => _totalCount;
|
|
72
99
|
private int TotalPages => PageSizeEff > 0 ? Math.Max(1, (int)Math.Ceiling((double)TotalCount / PageSizeEff)) : 1;
|
|
73
100
|
private bool HasPager => PageSizeEff > 0 && TotalPages > 1;
|
|
@@ -77,6 +104,20 @@ public partial class FdyTable<TRow>
|
|
|
77
104
|
|
|
78
105
|
protected override void OnParametersSet() => Recompute();
|
|
79
106
|
|
|
107
|
+
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
108
|
+
{
|
|
109
|
+
if (_pendingClamp is int clamp)
|
|
110
|
+
{
|
|
111
|
+
_pendingClamp = null;
|
|
112
|
+
await PageIndexChanged.InvokeAsync(clamp);
|
|
113
|
+
}
|
|
114
|
+
if (_processDirty)
|
|
115
|
+
{
|
|
116
|
+
_processDirty = false;
|
|
117
|
+
if (Process.HasDelegate) await Process.InvokeAsync(new FdyTableProcess<TRow>(_displayRows, _totalCount));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
80
121
|
// Recompute the visible rows from the current effective sort/filter/page. Called on every
|
|
81
122
|
// parameter change and after any internal-state mutation (sort/filter/page click).
|
|
82
123
|
private void Recompute()
|
|
@@ -91,13 +132,19 @@ public partial class FdyTable<TRow>
|
|
|
91
132
|
{
|
|
92
133
|
// Keep the page in range when a filter shrank the row set.
|
|
93
134
|
int tp = Math.Max(1, (int)Math.Ceiling((double)_totalCount / PageSize));
|
|
94
|
-
if (
|
|
95
|
-
|
|
135
|
+
if (ClientPageIndex > tp - 1)
|
|
136
|
+
{
|
|
137
|
+
if (PageIndexControlled) _pendingClamp = Math.Max(0, tp - 1);
|
|
138
|
+
else _internalPageIndex = Math.Max(0, tp - 1);
|
|
139
|
+
}
|
|
140
|
+
_displayRows = TableModel.Paginate(filteredSorted, Math.Min(ClientPageIndex, tp - 1), PageSize);
|
|
96
141
|
}
|
|
97
142
|
else
|
|
98
143
|
{
|
|
99
144
|
_displayRows = filteredSorted;
|
|
100
145
|
}
|
|
146
|
+
|
|
147
|
+
_processDirty = true;
|
|
101
148
|
}
|
|
102
149
|
|
|
103
150
|
private FdyColumnFilter? GetFilter(string key) => EffectiveFilters.TryGetValue(key, out FdyColumnFilter? f) ? f : null;
|
|
@@ -126,8 +173,7 @@ public partial class FdyTable<TRow>
|
|
|
126
173
|
else
|
|
127
174
|
{
|
|
128
175
|
_internalSort = next;
|
|
129
|
-
|
|
130
|
-
Recompute();
|
|
176
|
+
await SetClientPageAsync(0);
|
|
131
177
|
}
|
|
132
178
|
}
|
|
133
179
|
|
|
@@ -143,8 +189,7 @@ public partial class FdyTable<TRow>
|
|
|
143
189
|
else
|
|
144
190
|
{
|
|
145
191
|
_internalFilters = next;
|
|
146
|
-
|
|
147
|
-
Recompute();
|
|
192
|
+
await SetClientPageAsync(0);
|
|
148
193
|
}
|
|
149
194
|
}
|
|
150
195
|
|
|
@@ -157,6 +202,20 @@ public partial class FdyTable<TRow>
|
|
|
157
202
|
await PageChanged.InvokeAsync(new FdyPageState(index0, Page!.Size, Page.Total));
|
|
158
203
|
}
|
|
159
204
|
else
|
|
205
|
+
{
|
|
206
|
+
await SetClientPageAsync(index0);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// One write path for the client index: ask the parent when controlled, mutate the field when not.
|
|
211
|
+
private async Task SetClientPageAsync(int index0)
|
|
212
|
+
{
|
|
213
|
+
if (PageIndexControlled)
|
|
214
|
+
{
|
|
215
|
+
if (index0 != PageIndex) await PageIndexChanged.InvokeAsync(index0);
|
|
216
|
+
else Recompute();
|
|
217
|
+
}
|
|
218
|
+
else
|
|
160
219
|
{
|
|
161
220
|
_internalPageIndex = index0;
|
|
162
221
|
Recompute();
|
|
@@ -21,6 +21,12 @@ public sealed record FdySortState(string Key, FdySortDir Dir);
|
|
|
21
21
|
/// <see cref="FdyTable{TRow}.Page"/> switches the table into server mode.</summary>
|
|
22
22
|
public sealed record FdyPageState(int Index, int Size, int Total);
|
|
23
23
|
|
|
24
|
+
/// <summary>The processed page of rows (after filter/sort/paginate) plus the total row count, as
|
|
25
|
+
/// raised by <see cref="FdyTable{TRow}.Process"/>. Lets a consumer render the same processed set
|
|
26
|
+
/// somewhere else — a card list below the <c>md</c> breakpoint, a summary, an export — without
|
|
27
|
+
/// re-deriving the pipeline. Mirrors the <c>process</c> event in the Vue/React adapters.</summary>
|
|
28
|
+
public sealed record FdyTableProcess<TRow>(IReadOnlyList<TRow> Rows, int Total);
|
|
29
|
+
|
|
24
30
|
/// <summary>The active filter for a single column (a closed set of shapes, one per filter type).</summary>
|
|
25
31
|
public abstract record FdyColumnFilter
|
|
26
32
|
{
|
|
@@ -44,6 +44,17 @@ export interface FdyTableProps<Row extends object> {
|
|
|
44
44
|
onPageChange?: (page: FdyPageState) => void;
|
|
45
45
|
/** Client-side page size when `page` is absent; 0/undefined = render all rows (no pager). */
|
|
46
46
|
pageSize?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Controlled client-side page index (0-based). Provide it — with `pageSize`, without `page` — to
|
|
49
|
+
* own the page while the table keeps doing filter/sort/paginate. This is what lets an EXTERNAL
|
|
50
|
+
* pager drive the table: a responsive screen that hides `.fdy-datatable` below `md` and renders a
|
|
51
|
+
* card list from `onProcess` can render one pager for both breakpoints and point it here.
|
|
52
|
+
* Omit for the internal index (unchanged default).
|
|
53
|
+
*/
|
|
54
|
+
pageIndex?: number;
|
|
55
|
+
/** Client mode with `pageIndex` provided: the table asks for a new 0-based index (pager click, a
|
|
56
|
+
* reset to 0 after sort/filter, or a clamp when filtering shrank the set). */
|
|
57
|
+
onPageIndexChange?: (index: number) => void;
|
|
47
58
|
loading?: boolean;
|
|
48
59
|
emptyText?: string;
|
|
49
60
|
ariaLabel?: string;
|
|
@@ -57,6 +68,10 @@ export interface FdyTableProps<Row extends object> {
|
|
|
57
68
|
rowClass?: (row: Row) => string | undefined;
|
|
58
69
|
/** A row was activated (click, or Enter/Space while the row itself is focused). */
|
|
59
70
|
onRowActivate?: (row: Row) => void;
|
|
71
|
+
/** Called with the processed page of rows (after filter/sort/paginate) plus the total row count —
|
|
72
|
+
* in BOTH modes, whenever they change. Lets a consumer render the SAME processed set elsewhere
|
|
73
|
+
* (a `< md` card list, a "selected" summary, export-to-CSV) without re-deriving the pipeline. */
|
|
74
|
+
onProcess?: (result: { rows: Row[]; total: number }) => void;
|
|
60
75
|
/** Controlled: row keys whose detail is shown as a full-width row beneath them. */
|
|
61
76
|
expandedKeys?: ReadonlyArray<string | number>;
|
|
62
77
|
/** Renders the expandable detail row for an expanded row (React equivalent of Vue's `row-detail` slot). */
|
|
@@ -91,14 +106,26 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
91
106
|
|
|
92
107
|
const totalCount: number = serverPaged ? (props.page as FdyPageState).total : filteredSorted.length;
|
|
93
108
|
|
|
109
|
+
/* Client-side page index: the prop when the parent owns it, internal state otherwise. Every read
|
|
110
|
+
* goes through clientPageIndex and every write through setClientPage, so controlled and
|
|
111
|
+
* uncontrolled behave identically apart from where the number lives. */
|
|
112
|
+
const pageIndexControlled: boolean = props.pageIndex !== undefined;
|
|
113
|
+
const clientPageIndex: number = pageIndexControlled ? Math.max(0, props.pageIndex as number) : internalPageIndex;
|
|
114
|
+
const onPageIndexChange = props.onPageIndexChange;
|
|
115
|
+
function setClientPage(index0: number): void {
|
|
116
|
+
if (pageIndexControlled) {
|
|
117
|
+
if (index0 !== props.pageIndex) onPageIndexChange?.(index0);
|
|
118
|
+
} else setInternalPageIndex(index0);
|
|
119
|
+
}
|
|
120
|
+
|
|
94
121
|
const displayRows: Row[] = useMemo(() => {
|
|
95
122
|
if (serverPaged) return props.rows.slice();
|
|
96
|
-
if (props.pageSize && props.pageSize > 0) return paginate(filteredSorted,
|
|
123
|
+
if (props.pageSize && props.pageSize > 0) return paginate(filteredSorted, clientPageIndex, props.pageSize);
|
|
97
124
|
return filteredSorted;
|
|
98
|
-
}, [serverPaged, props.rows, props.pageSize, filteredSorted,
|
|
125
|
+
}, [serverPaged, props.rows, props.pageSize, filteredSorted, clientPageIndex]);
|
|
99
126
|
|
|
100
127
|
const pageSizeEff: number = serverPaged ? (props.page as FdyPageState).size : (props.pageSize ?? 0);
|
|
101
|
-
const currentPage1: number = (serverPaged ? (props.page as FdyPageState).index :
|
|
128
|
+
const currentPage1: number = (serverPaged ? (props.page as FdyPageState).index : clientPageIndex) + 1;
|
|
102
129
|
const totalPages: number = pageSizeEff > 0 ? Math.max(1, Math.ceil(totalCount / pageSizeEff)) : 1;
|
|
103
130
|
const hasPager: boolean = pageSizeEff > 0 && totalPages > 1;
|
|
104
131
|
const pages: Array<number | 'ellipsis'> = pageWindow(currentPage1, totalPages);
|
|
@@ -107,8 +134,16 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
107
134
|
|
|
108
135
|
// Client mode: keep the page in range when a filter shrinks the row set.
|
|
109
136
|
useEffect((): void => {
|
|
110
|
-
if (!serverPaged &&
|
|
111
|
-
|
|
137
|
+
if (!serverPaged && clientPageIndex > totalPages - 1) setClientPage(Math.max(0, totalPages - 1));
|
|
138
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- setClientPage is derived from these
|
|
139
|
+
}, [serverPaged, clientPageIndex, totalPages]);
|
|
140
|
+
|
|
141
|
+
// Surface the processed page + total to the parent (both modes), so the same result can drive a
|
|
142
|
+
// responsive card list / summary / export without re-implementing filter/sort/paginate.
|
|
143
|
+
const onProcess = props.onProcess;
|
|
144
|
+
useEffect((): void => {
|
|
145
|
+
onProcess?.({ rows: displayRows, total: totalCount });
|
|
146
|
+
}, [onProcess, displayRows, totalCount]);
|
|
112
147
|
|
|
113
148
|
function ariaSortOf(col: FdyTableColumn<Row>): 'ascending' | 'descending' | undefined {
|
|
114
149
|
if (effectiveSort === null || effectiveSort.key !== col.key) return undefined;
|
|
@@ -123,7 +158,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
123
158
|
if (sortControlled) props.onSortChange?.(next);
|
|
124
159
|
else {
|
|
125
160
|
setInternalSort(next);
|
|
126
|
-
|
|
161
|
+
setClientPage(0);
|
|
127
162
|
}
|
|
128
163
|
}
|
|
129
164
|
function onFilterChange(col: FdyTableColumn<Row>, filter: FdyColumnFilter | null): void {
|
|
@@ -133,7 +168,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
133
168
|
if (filtersControlled) props.onFiltersChange?.(nextMap);
|
|
134
169
|
else {
|
|
135
170
|
setInternalFilters(nextMap);
|
|
136
|
-
|
|
171
|
+
setClientPage(0);
|
|
137
172
|
}
|
|
138
173
|
}
|
|
139
174
|
function goTo(page1: number): void {
|
|
@@ -143,7 +178,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
143
178
|
const p: FdyPageState = props.page as FdyPageState;
|
|
144
179
|
props.onPageChange?.({ index: index0, size: p.size, total: p.total });
|
|
145
180
|
} else {
|
|
146
|
-
|
|
181
|
+
setClientPage(index0);
|
|
147
182
|
}
|
|
148
183
|
}
|
|
149
184
|
|
|
@@ -8,12 +8,15 @@ import { computed, onMounted, useId, watch, type ComputedRef, type Ref, ref } fr
|
|
|
8
8
|
// focus trap, focus restore, top-layer stacking and inert background; `dismissible` (default true)
|
|
9
9
|
// gates Esc + backdrop dismissal.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// dismissible MUST go through withDefaults: Vue's boolean-cast gives an omitted Boolean prop
|
|
12
|
+
// `false`, not `undefined`, so a bare `props.dismissible !== false` would make an un-annotated
|
|
13
|
+
// drawer non-dismissible (no Esc, no backdrop, no close button) — the opposite of the default.
|
|
14
|
+
const props = withDefaults(defineProps<{
|
|
12
15
|
open: boolean;
|
|
13
16
|
title: string;
|
|
14
17
|
side?: 'left' | 'right';
|
|
15
18
|
dismissible?: boolean;
|
|
16
|
-
}>();
|
|
19
|
+
}>(), { dismissible: true });
|
|
17
20
|
|
|
18
21
|
const emit = defineEmits<{
|
|
19
22
|
close: [];
|
|
@@ -10,12 +10,15 @@ import { computed, onMounted, useId, watch, type ComputedRef, type Ref, ref } fr
|
|
|
10
10
|
// Native <dialog> already provides the focus trap, focus restore, top-layer stacking and inert
|
|
11
11
|
// background — the wrapper only avoids breaking them. `dismissible` (default true) gates Esc + backdrop.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// dismissible MUST go through withDefaults: Vue's boolean-cast gives an omitted Boolean prop
|
|
14
|
+
// `false`, not `undefined`, so a bare `props.dismissible !== false` would make an un-annotated
|
|
15
|
+
// modal non-dismissible (no Esc, no backdrop, no close button) — the opposite of the default.
|
|
16
|
+
const props = withDefaults(defineProps<{
|
|
14
17
|
open: boolean;
|
|
15
18
|
title: string;
|
|
16
19
|
size?: 'sm' | 'md' | 'lg' | 'wide';
|
|
17
20
|
dismissible?: boolean;
|
|
18
|
-
}>();
|
|
21
|
+
}>(), { dismissible: true });
|
|
19
22
|
|
|
20
23
|
const emit = defineEmits<{
|
|
21
24
|
close: [];
|
|
@@ -41,6 +41,14 @@ const props = defineProps<{
|
|
|
41
41
|
page?: FdyPageState;
|
|
42
42
|
/** Client-side page size when `page` is absent; 0/undefined = render all rows (no pager). */
|
|
43
43
|
pageSize?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Controlled client-side page index (0-based). Provide it — with `pageSize`, without `page` — to
|
|
46
|
+
* own the page while the table keeps doing filter/sort/paginate. This is what lets an EXTERNAL
|
|
47
|
+
* pager drive the table: a responsive screen that hides `.fdy-datatable` below `md` and renders a
|
|
48
|
+
* card list from the `process` event can render one pager for both breakpoints and point it here.
|
|
49
|
+
* Omit for the internal index (unchanged default).
|
|
50
|
+
*/
|
|
51
|
+
pageIndex?: number;
|
|
44
52
|
loading?: boolean;
|
|
45
53
|
emptyText?: string;
|
|
46
54
|
ariaLabel?: string;
|
|
@@ -56,8 +64,15 @@ const emit = defineEmits<{
|
|
|
56
64
|
'update:sort': [sort: FdySortState | null];
|
|
57
65
|
'update:filters': [filters: FdyFilterMap];
|
|
58
66
|
'update:page': [page: FdyPageState];
|
|
67
|
+
/** Client mode with `pageIndex` provided: the table asks for a new 0-based index (pager click, or
|
|
68
|
+
* a reset to 0 after sort/filter, or a clamp when filtering shrank the set). */
|
|
69
|
+
'update:pageIndex': [index: number];
|
|
59
70
|
/** A row was activated (click, or Enter/Space while the row itself is focused). */
|
|
60
71
|
'row-activate': [row: Row];
|
|
72
|
+
/** The processed page of rows (after filter/sort/paginate) plus the total row count — fires in
|
|
73
|
+
* BOTH modes whenever they change. Lets a consumer render the SAME processed set elsewhere
|
|
74
|
+
* (a `< md` card list, a "selected" summary, export-to-CSV) without re-deriving the pipeline. */
|
|
75
|
+
'process': [result: { rows: Row[]; total: number }];
|
|
61
76
|
}>();
|
|
62
77
|
|
|
63
78
|
const internalSort: Ref<FdySortState | null> = ref(null);
|
|
@@ -94,9 +109,22 @@ const filteredSorted: ComputedRef<Row[]> = computed((): Row[] => {
|
|
|
94
109
|
const totalCount: ComputedRef<number> = computed((): number =>
|
|
95
110
|
serverPaged.value ? (props.page as FdyPageState).total : filteredSorted.value.length,
|
|
96
111
|
);
|
|
112
|
+
/* Client-side page index: the prop when the parent owns it, the internal ref otherwise. Every read
|
|
113
|
+
* goes through clientPageIndex and every write through setClientPage, so controlled and uncontrolled
|
|
114
|
+
* behave identically apart from where the number lives. */
|
|
115
|
+
const pageIndexControlled: ComputedRef<boolean> = computed((): boolean => props.pageIndex !== undefined);
|
|
116
|
+
const clientPageIndex: ComputedRef<number> = computed((): number =>
|
|
117
|
+
pageIndexControlled.value ? Math.max(0, props.pageIndex as number) : internalPageIndex.value,
|
|
118
|
+
);
|
|
119
|
+
function setClientPage(index0: number): void {
|
|
120
|
+
if (pageIndexControlled.value) {
|
|
121
|
+
if (index0 !== props.pageIndex) emit('update:pageIndex', index0);
|
|
122
|
+
} else internalPageIndex.value = index0;
|
|
123
|
+
}
|
|
124
|
+
|
|
97
125
|
const displayRows: ComputedRef<Row[]> = computed((): Row[] => {
|
|
98
126
|
if (serverPaged.value) return props.rows.slice();
|
|
99
|
-
if (props.pageSize && props.pageSize > 0) return paginate(filteredSorted.value,
|
|
127
|
+
if (props.pageSize && props.pageSize > 0) return paginate(filteredSorted.value, clientPageIndex.value, props.pageSize);
|
|
100
128
|
return filteredSorted.value;
|
|
101
129
|
});
|
|
102
130
|
|
|
@@ -104,7 +132,7 @@ const pageSizeEff: ComputedRef<number> = computed((): number =>
|
|
|
104
132
|
serverPaged.value ? (props.page as FdyPageState).size : (props.pageSize ?? 0),
|
|
105
133
|
);
|
|
106
134
|
const currentPage1: ComputedRef<number> = computed((): number =>
|
|
107
|
-
(serverPaged.value ? (props.page as FdyPageState).index :
|
|
135
|
+
(serverPaged.value ? (props.page as FdyPageState).index : clientPageIndex.value) + 1,
|
|
108
136
|
);
|
|
109
137
|
const totalPages: ComputedRef<number> = computed((): number =>
|
|
110
138
|
pageSizeEff.value > 0 ? Math.max(1, Math.ceil(totalCount.value / pageSizeEff.value)) : 1,
|
|
@@ -122,9 +150,17 @@ const rangeTo: ComputedRef<number> = computed((): number =>
|
|
|
122
150
|
|
|
123
151
|
// Client mode: keep the page in range when a filter shrinks the row set.
|
|
124
152
|
watch(totalPages, (tp: number): void => {
|
|
125
|
-
if (!serverPaged.value &&
|
|
153
|
+
if (!serverPaged.value && clientPageIndex.value > tp - 1) setClientPage(Math.max(0, tp - 1));
|
|
126
154
|
});
|
|
127
155
|
|
|
156
|
+
// Surface the processed page + total to the parent (both modes), so the same result can drive a
|
|
157
|
+
// responsive card list / summary / export without re-implementing filter/sort/paginate.
|
|
158
|
+
watch(
|
|
159
|
+
[displayRows, totalCount],
|
|
160
|
+
(): void => emit('process', { rows: displayRows.value, total: totalCount.value }),
|
|
161
|
+
{ immediate: true },
|
|
162
|
+
);
|
|
163
|
+
|
|
128
164
|
function ariaSortOf(col: FdyTableColumn<Row>): 'ascending' | 'descending' | undefined {
|
|
129
165
|
const s: FdySortState | null = effectiveSort.value;
|
|
130
166
|
if (s === null || s.key !== col.key) return undefined;
|
|
@@ -140,7 +176,7 @@ function onSort(col: FdyTableColumn<Row>): void {
|
|
|
140
176
|
if (sortControlled.value) emit('update:sort', next);
|
|
141
177
|
else {
|
|
142
178
|
internalSort.value = next;
|
|
143
|
-
|
|
179
|
+
setClientPage(0);
|
|
144
180
|
}
|
|
145
181
|
}
|
|
146
182
|
function onFilterChange(col: FdyTableColumn<Row>, filter: FdyColumnFilter | null): void {
|
|
@@ -150,7 +186,7 @@ function onFilterChange(col: FdyTableColumn<Row>, filter: FdyColumnFilter | null
|
|
|
150
186
|
if (filtersControlled.value) emit('update:filters', nextMap);
|
|
151
187
|
else {
|
|
152
188
|
internalFilters.value = nextMap;
|
|
153
|
-
|
|
189
|
+
setClientPage(0);
|
|
154
190
|
}
|
|
155
191
|
}
|
|
156
192
|
function goTo(page1: number): void {
|
|
@@ -160,7 +196,7 @@ function goTo(page1: number): void {
|
|
|
160
196
|
const p: FdyPageState = props.page as FdyPageState;
|
|
161
197
|
emit('update:page', { index: index0, size: p.size, total: p.total });
|
|
162
198
|
} else {
|
|
163
|
-
|
|
199
|
+
setClientPage(index0);
|
|
164
200
|
}
|
|
165
201
|
}
|
|
166
202
|
|