@cahyo-dimas/freeday 1.18.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 +162 -0
- package/COMPONENTS.md +748 -0
- package/README.id.md +9 -1
- package/README.md +16 -2
- package/USAGE.md +34 -11
- package/adapters/blazor/FdyTable.razor.cs +66 -7
- package/adapters/blazor/TableTypes.cs +6 -0
- package/adapters/react/components/FdyTable.tsx +32 -8
- package/adapters/vue/components/FdyTable.vue +30 -6
- package/dist/freeday.bundle.css +35 -1
- package/dist/freeday.css +30 -0
- package/dist/freeday.tokens.css +5 -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 +9 -3
- package/src/components/list.css +29 -0
- package/tokens/breakpoints.d.ts +3 -0
- package/tokens/breakpoints.mjs +8 -1
- 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
|
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
|
|
@@ -187,10 +195,16 @@ src/components/*.css one file per component (fdy-*)
|
|
|
187
195
|
src/*.js optional JS enhancers (reference, vanilla)
|
|
188
196
|
dist/ build output (COMMITTED):
|
|
189
197
|
freeday.tokens.css semantic tokens (light/dark/compact)
|
|
190
|
-
freeday.css
|
|
198
|
+
freeday.css every component (NO tokens)
|
|
199
|
+
freeday.bundle.css tokens + components — link this one
|
|
191
200
|
freeday.js bundle of every enhancer (single <script>)
|
|
192
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)
|
|
193
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)
|
|
194
208
|
```
|
|
195
209
|
|
|
196
210
|
## Component inventory
|
package/USAGE.md
CHANGED
|
@@ -41,12 +41,22 @@ empty state). Don't scatter `--space-6` everywhere — a page with one gap value
|
|
|
41
41
|
|
|
42
42
|
## 3. Elevation — most surfaces are flat
|
|
43
43
|
|
|
44
|
-
Shadow is a signal, not decoration.
|
|
44
|
+
Shadow is a signal, not decoration. There are two families, and the difference matters:
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
50
60
|
|
|
51
61
|
If every box on the screen has the same card shadow, none of them read as special — that's the
|
|
52
62
|
"identical card grid" failure. Prefer flat sections with **one** raised element that matters.
|
|
@@ -74,21 +84,34 @@ point, everything around it quiet.
|
|
|
74
84
|
|
|
75
85
|
## 6. Density — `compact` for data-dense screens
|
|
76
86
|
|
|
77
|
-
`data-density="compact"`
|
|
87
|
+
`data-density="compact"` tightens control height **and** the mid-range spacing scale
|
|
78
88
|
(`--space-3`…`--space-6` step down a notch), so cards, toolbars and tables get denser. Use it on
|
|
79
|
-
table-heavy back-office screens; leave `comfortable` (the default) for forms
|
|
80
|
-
|
|
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.
|
|
81
96
|
|
|
82
97
|
## 7. Assemble the page from the frame down
|
|
83
98
|
|
|
84
|
-
1. **Shell:** every application starts inside **`.fdy-app`**
|
|
85
|
-
`
|
|
86
|
-
|
|
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`.
|
|
87
104
|
2. **Page:** wrap the screen body in **`.fdy-page`** (vertical section rhythm), opening with a
|
|
88
105
|
**`.fdy-page__header`** (eyebrow + `.fdy-title-page` + `.fdy-page__desc` on the left, the one
|
|
89
106
|
primary action on the right).
|
|
90
107
|
3. **Sections:** each region is a **`.fdy-page-section`** (a `.fdy-title-section` + optional
|
|
91
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.
|
|
92
115
|
4. **KPIs:** a **`.fdy-stats`** grid of **`.fdy-stat`** tiles — deliberately *not* cards, so a metric
|
|
93
116
|
strip doesn't become an identical-card grid. Wrap in `.fdy-stats--boxed` for one shared strip.
|
|
94
117
|
5. **Content:** components (`.fdy-card`, `.fdy-datatable`, `.fdy-chart`, …) go inside sections.
|
|
@@ -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;
|
|
@@ -95,14 +106,26 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
95
106
|
|
|
96
107
|
const totalCount: number = serverPaged ? (props.page as FdyPageState).total : filteredSorted.length;
|
|
97
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
|
+
|
|
98
121
|
const displayRows: Row[] = useMemo(() => {
|
|
99
122
|
if (serverPaged) return props.rows.slice();
|
|
100
|
-
if (props.pageSize && props.pageSize > 0) return paginate(filteredSorted,
|
|
123
|
+
if (props.pageSize && props.pageSize > 0) return paginate(filteredSorted, clientPageIndex, props.pageSize);
|
|
101
124
|
return filteredSorted;
|
|
102
|
-
}, [serverPaged, props.rows, props.pageSize, filteredSorted,
|
|
125
|
+
}, [serverPaged, props.rows, props.pageSize, filteredSorted, clientPageIndex]);
|
|
103
126
|
|
|
104
127
|
const pageSizeEff: number = serverPaged ? (props.page as FdyPageState).size : (props.pageSize ?? 0);
|
|
105
|
-
const currentPage1: number = (serverPaged ? (props.page as FdyPageState).index :
|
|
128
|
+
const currentPage1: number = (serverPaged ? (props.page as FdyPageState).index : clientPageIndex) + 1;
|
|
106
129
|
const totalPages: number = pageSizeEff > 0 ? Math.max(1, Math.ceil(totalCount / pageSizeEff)) : 1;
|
|
107
130
|
const hasPager: boolean = pageSizeEff > 0 && totalPages > 1;
|
|
108
131
|
const pages: Array<number | 'ellipsis'> = pageWindow(currentPage1, totalPages);
|
|
@@ -111,8 +134,9 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
111
134
|
|
|
112
135
|
// Client mode: keep the page in range when a filter shrinks the row set.
|
|
113
136
|
useEffect((): void => {
|
|
114
|
-
if (!serverPaged &&
|
|
115
|
-
|
|
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]);
|
|
116
140
|
|
|
117
141
|
// Surface the processed page + total to the parent (both modes), so the same result can drive a
|
|
118
142
|
// responsive card list / summary / export without re-implementing filter/sort/paginate.
|
|
@@ -134,7 +158,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
134
158
|
if (sortControlled) props.onSortChange?.(next);
|
|
135
159
|
else {
|
|
136
160
|
setInternalSort(next);
|
|
137
|
-
|
|
161
|
+
setClientPage(0);
|
|
138
162
|
}
|
|
139
163
|
}
|
|
140
164
|
function onFilterChange(col: FdyTableColumn<Row>, filter: FdyColumnFilter | null): void {
|
|
@@ -144,7 +168,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
144
168
|
if (filtersControlled) props.onFiltersChange?.(nextMap);
|
|
145
169
|
else {
|
|
146
170
|
setInternalFilters(nextMap);
|
|
147
|
-
|
|
171
|
+
setClientPage(0);
|
|
148
172
|
}
|
|
149
173
|
}
|
|
150
174
|
function goTo(page1: number): void {
|
|
@@ -154,7 +178,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
154
178
|
const p: FdyPageState = props.page as FdyPageState;
|
|
155
179
|
props.onPageChange?.({ index: index0, size: p.size, total: p.total });
|
|
156
180
|
} else {
|
|
157
|
-
|
|
181
|
+
setClientPage(index0);
|
|
158
182
|
}
|
|
159
183
|
}
|
|
160
184
|
|
|
@@ -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,6 +64,9 @@ 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];
|
|
61
72
|
/** The processed page of rows (after filter/sort/paginate) plus the total row count — fires in
|
|
@@ -98,9 +109,22 @@ const filteredSorted: ComputedRef<Row[]> = computed((): Row[] => {
|
|
|
98
109
|
const totalCount: ComputedRef<number> = computed((): number =>
|
|
99
110
|
serverPaged.value ? (props.page as FdyPageState).total : filteredSorted.value.length,
|
|
100
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
|
+
|
|
101
125
|
const displayRows: ComputedRef<Row[]> = computed((): Row[] => {
|
|
102
126
|
if (serverPaged.value) return props.rows.slice();
|
|
103
|
-
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);
|
|
104
128
|
return filteredSorted.value;
|
|
105
129
|
});
|
|
106
130
|
|
|
@@ -108,7 +132,7 @@ const pageSizeEff: ComputedRef<number> = computed((): number =>
|
|
|
108
132
|
serverPaged.value ? (props.page as FdyPageState).size : (props.pageSize ?? 0),
|
|
109
133
|
);
|
|
110
134
|
const currentPage1: ComputedRef<number> = computed((): number =>
|
|
111
|
-
(serverPaged.value ? (props.page as FdyPageState).index :
|
|
135
|
+
(serverPaged.value ? (props.page as FdyPageState).index : clientPageIndex.value) + 1,
|
|
112
136
|
);
|
|
113
137
|
const totalPages: ComputedRef<number> = computed((): number =>
|
|
114
138
|
pageSizeEff.value > 0 ? Math.max(1, Math.ceil(totalCount.value / pageSizeEff.value)) : 1,
|
|
@@ -126,7 +150,7 @@ const rangeTo: ComputedRef<number> = computed((): number =>
|
|
|
126
150
|
|
|
127
151
|
// Client mode: keep the page in range when a filter shrinks the row set.
|
|
128
152
|
watch(totalPages, (tp: number): void => {
|
|
129
|
-
if (!serverPaged.value &&
|
|
153
|
+
if (!serverPaged.value && clientPageIndex.value > tp - 1) setClientPage(Math.max(0, tp - 1));
|
|
130
154
|
});
|
|
131
155
|
|
|
132
156
|
// Surface the processed page + total to the parent (both modes), so the same result can drive a
|
|
@@ -152,7 +176,7 @@ function onSort(col: FdyTableColumn<Row>): void {
|
|
|
152
176
|
if (sortControlled.value) emit('update:sort', next);
|
|
153
177
|
else {
|
|
154
178
|
internalSort.value = next;
|
|
155
|
-
|
|
179
|
+
setClientPage(0);
|
|
156
180
|
}
|
|
157
181
|
}
|
|
158
182
|
function onFilterChange(col: FdyTableColumn<Row>, filter: FdyColumnFilter | null): void {
|
|
@@ -162,7 +186,7 @@ function onFilterChange(col: FdyTableColumn<Row>, filter: FdyColumnFilter | null
|
|
|
162
186
|
if (filtersControlled.value) emit('update:filters', nextMap);
|
|
163
187
|
else {
|
|
164
188
|
internalFilters.value = nextMap;
|
|
165
|
-
|
|
189
|
+
setClientPage(0);
|
|
166
190
|
}
|
|
167
191
|
}
|
|
168
192
|
function goTo(page1: number): void {
|
|
@@ -172,7 +196,7 @@ function goTo(page1: number): void {
|
|
|
172
196
|
const p: FdyPageState = props.page as FdyPageState;
|
|
173
197
|
emit('update:page', { index: index0, size: p.size, total: p.total });
|
|
174
198
|
} else {
|
|
175
|
-
|
|
199
|
+
setClientPage(index0);
|
|
176
200
|
}
|
|
177
201
|
}
|
|
178
202
|
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -347,7 +347,11 @@
|
|
|
347
347
|
--chart-tick: var(--slate-400);
|
|
348
348
|
--focus-ring: var(--azure-600);
|
|
349
349
|
}
|
|
350
|
-
:root
|
|
350
|
+
/* Density is deliberately NOT scoped to :root. These are custom properties, so they inherit —
|
|
351
|
+
* putting the attribute on any ancestor (a route wrapper, one section) densifies just that
|
|
352
|
+
* subtree, which is how density is actually decided: per screen, not per app. The root still
|
|
353
|
+
* matches, so setting data-density on the html element keeps working exactly as before. */
|
|
354
|
+
[data-density="compact"] {
|
|
351
355
|
--space-3: 0.625rem;
|
|
352
356
|
--space-4: 0.75rem;
|
|
353
357
|
--space-5: 1rem;
|
|
@@ -1184,6 +1188,36 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1184
1188
|
/* Freeday — Keyboard key */
|
|
1185
1189
|
.fdy-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:1.5rem;height:1.5rem;padding:0 var(--space-2);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text);background:var(--color-surface-2);border:var(--bw) solid var(--color-border-strong);border-bottom-width:2px;border-radius:var(--radius-sm);}
|
|
1186
1190
|
|
|
1191
|
+
/* Freeday — List: the FLAT row container.
|
|
1192
|
+
*
|
|
1193
|
+
* USAGE.md §3 puts list rows among the surfaces that should be flat, but until now the only
|
|
1194
|
+
* container the kit shipped was .fdy-card — which carries --shadow-lift (a real 34px lift). A
|
|
1195
|
+
* responsive table that becomes a list below `md` therefore had to choose between a stack of
|
|
1196
|
+
* shadowed cards (against the doctrine) or a hand-built box, which needs a colour and so escapes
|
|
1197
|
+
* the token system. This is that missing container: one flat surface, hairline dividers, no shadow.
|
|
1198
|
+
*
|
|
1199
|
+
* Not to be confused with .fdy-list-reset (base.css), which only strips UA bullets/indent.
|
|
1200
|
+
* Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
|
|
1201
|
+
.fdy-list{list-style:none;margin:0;padding:0;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;}
|
|
1202
|
+
.fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
|
|
1203
|
+
/* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
|
|
1204
|
+
* of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
|
|
1205
|
+
* <li>s — a bare `.fdy-list__row + .fdy-list__row` silently matches nothing there). */
|
|
1206
|
+
.fdy-list__row + .fdy-list__row,
|
|
1207
|
+
.fdy-list > * + * > .fdy-list__row{border-top:var(--bw) solid var(--color-border-muted);}
|
|
1208
|
+
/* A row that is itself the control: render it as <button>/<a> and the UA box is reset without
|
|
1209
|
+
* touching the list surface (same contract as .fdy-card--button). */
|
|
1210
|
+
.fdy-list__row--button{width:100%;text-align:inherit;color:inherit;font:inherit;background:none;border:0;appearance:none;-webkit-appearance:none;cursor:pointer;}
|
|
1211
|
+
.fdy-list__row--button:hover,.fdy-list__row--interactive:hover{background:var(--color-surface-2);}
|
|
1212
|
+
.fdy-list__row--button:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--color-primary);}
|
|
1213
|
+
.fdy-list__row--interactive{cursor:pointer;}
|
|
1214
|
+
/* Row internals: a title/meta stack that truncates, and a trailing slot pinned right. */
|
|
1215
|
+
.fdy-list__main{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;flex:1;}
|
|
1216
|
+
.fdy-list__title{font-weight:var(--weight-medium);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
1217
|
+
.fdy-list__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;}
|
|
1218
|
+
.fdy-list__aside{flex:none;display:flex;align-items:center;gap:var(--space-2);}
|
|
1219
|
+
/* Density: rows tighten with the rest of the kit (--space-* mid-range steps under compact). */
|
|
1220
|
+
|
|
1187
1221
|
/* Freeday — Menu (action popup) + Split button. Enhanced by freeday-menu.js. */
|
|
1188
1222
|
.fdy-menu-wrap{position:relative;display:inline-flex;}
|
|
1189
1223
|
.fdy-menu{position:absolute;top:calc(100% + var(--space-1));left:0;z-index:130;min-width:11rem;margin:0;padding:var(--space-1);list-style:none;background:var(--color-surface);color:var(--color-text);border:var(--bw) solid var(--color-border-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-3);}
|
package/dist/freeday.css
CHANGED
|
@@ -827,6 +827,36 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
827
827
|
/* Freeday — Keyboard key */
|
|
828
828
|
.fdy-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:1.5rem;height:1.5rem;padding:0 var(--space-2);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text);background:var(--color-surface-2);border:var(--bw) solid var(--color-border-strong);border-bottom-width:2px;border-radius:var(--radius-sm);}
|
|
829
829
|
|
|
830
|
+
/* Freeday — List: the FLAT row container.
|
|
831
|
+
*
|
|
832
|
+
* USAGE.md §3 puts list rows among the surfaces that should be flat, but until now the only
|
|
833
|
+
* container the kit shipped was .fdy-card — which carries --shadow-lift (a real 34px lift). A
|
|
834
|
+
* responsive table that becomes a list below `md` therefore had to choose between a stack of
|
|
835
|
+
* shadowed cards (against the doctrine) or a hand-built box, which needs a colour and so escapes
|
|
836
|
+
* the token system. This is that missing container: one flat surface, hairline dividers, no shadow.
|
|
837
|
+
*
|
|
838
|
+
* Not to be confused with .fdy-list-reset (base.css), which only strips UA bullets/indent.
|
|
839
|
+
* Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
|
|
840
|
+
.fdy-list{list-style:none;margin:0;padding:0;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;}
|
|
841
|
+
.fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
|
|
842
|
+
/* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
|
|
843
|
+
* of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
|
|
844
|
+
* <li>s — a bare `.fdy-list__row + .fdy-list__row` silently matches nothing there). */
|
|
845
|
+
.fdy-list__row + .fdy-list__row,
|
|
846
|
+
.fdy-list > * + * > .fdy-list__row{border-top:var(--bw) solid var(--color-border-muted);}
|
|
847
|
+
/* A row that is itself the control: render it as <button>/<a> and the UA box is reset without
|
|
848
|
+
* touching the list surface (same contract as .fdy-card--button). */
|
|
849
|
+
.fdy-list__row--button{width:100%;text-align:inherit;color:inherit;font:inherit;background:none;border:0;appearance:none;-webkit-appearance:none;cursor:pointer;}
|
|
850
|
+
.fdy-list__row--button:hover,.fdy-list__row--interactive:hover{background:var(--color-surface-2);}
|
|
851
|
+
.fdy-list__row--button:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--color-primary);}
|
|
852
|
+
.fdy-list__row--interactive{cursor:pointer;}
|
|
853
|
+
/* Row internals: a title/meta stack that truncates, and a trailing slot pinned right. */
|
|
854
|
+
.fdy-list__main{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;flex:1;}
|
|
855
|
+
.fdy-list__title{font-weight:var(--weight-medium);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
856
|
+
.fdy-list__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;}
|
|
857
|
+
.fdy-list__aside{flex:none;display:flex;align-items:center;gap:var(--space-2);}
|
|
858
|
+
/* Density: rows tighten with the rest of the kit (--space-* mid-range steps under compact). */
|
|
859
|
+
|
|
830
860
|
/* Freeday — Menu (action popup) + Split button. Enhanced by freeday-menu.js. */
|
|
831
861
|
.fdy-menu-wrap{position:relative;display:inline-flex;}
|
|
832
862
|
.fdy-menu{position:absolute;top:calc(100% + var(--space-1));left:0;z-index:130;min-width:11rem;margin:0;padding:var(--space-1);list-style:none;background:var(--color-surface);color:var(--color-text);border:var(--bw) solid var(--color-border-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-3);}
|
package/dist/freeday.tokens.css
CHANGED
|
@@ -346,7 +346,11 @@
|
|
|
346
346
|
--chart-tick: var(--slate-400);
|
|
347
347
|
--focus-ring: var(--azure-600);
|
|
348
348
|
}
|
|
349
|
-
:root
|
|
349
|
+
/* Density is deliberately NOT scoped to :root. These are custom properties, so they inherit —
|
|
350
|
+
* putting the attribute on any ancestor (a route wrapper, one section) densifies just that
|
|
351
|
+
* subtree, which is how density is actually decided: per screen, not per app. The root still
|
|
352
|
+
* matches, so setting data-density on the html element keeps working exactly as before. */
|
|
353
|
+
[data-density="compact"] {
|
|
350
354
|
--space-3: 0.625rem;
|
|
351
355
|
--space-4: 0.75rem;
|
|
352
356
|
--space-5: 1rem;
|