@cahyo-dimas/freeday 1.53.0 → 2.0.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 +657 -535
- package/COMPONENTS.md +153 -135
- package/README.id.md +46 -45
- package/README.md +46 -43
- package/USAGE.md +39 -35
- package/adapters/blazor/FdyAppShell.razor +50 -0
- package/adapters/blazor/FdyAppShell.razor.cs +114 -0
- package/adapters/blazor/FdyCfl.razor.cs +1 -1
- package/adapters/blazor/FdyChart.razor +1 -1
- package/adapters/blazor/FdyChart.razor.cs +3 -3
- package/adapters/blazor/FdyChartSeries.cs +1 -1
- package/adapters/blazor/FdyCombo.razor.cs +2 -2
- package/adapters/blazor/FdyModal.razor +1 -1
- package/adapters/blazor/FdyTable.razor.cs +7 -7
- package/adapters/blazor/FdyTableFilter.razor +1 -1
- package/adapters/blazor/FdyTableFooter.razor +1 -1
- package/adapters/blazor/FdyTableFooter.razor.cs +4 -4
- package/adapters/blazor/Freeday.Blazor.csproj +1 -1
- package/adapters/blazor/FreedayComponentBase.cs +1 -1
- package/adapters/blazor/TableModel.cs +3 -3
- package/adapters/blazor/TableTypes.cs +3 -3
- package/adapters/blazor/freeday-blazor.js +5 -5
- package/adapters/core/app-shell.d.ts +30 -0
- package/adapters/core/app-shell.js +134 -0
- package/adapters/core/table-model.d.ts +2 -2
- package/adapters/core/table-model.js +8 -8
- package/adapters/react/components/FdyAppShell.tsx +159 -0
- package/adapters/react/components/FdyAutocomplete.tsx +1 -1
- package/adapters/react/components/FdyCascade.tsx +1 -1
- package/adapters/react/components/FdyCfl.tsx +11 -11
- package/adapters/react/components/FdyDateRange.tsx +2 -2
- package/adapters/react/components/FdyDatepicker.tsx +5 -5
- package/adapters/react/components/FdyDrawer.tsx +1 -1
- package/adapters/react/components/FdyModal.tsx +2 -2
- package/adapters/react/components/FdyTable.tsx +7 -7
- package/adapters/react/components/FdyTableFilter.tsx +1 -1
- package/adapters/react/components/FdyTableFooter.tsx +5 -5
- package/adapters/react/index.d.ts +2 -1
- package/adapters/react/index.js +1 -0
- package/adapters/react/useFreeday.js +3 -3
- package/adapters/vue/components/FdyAppShell.vue +160 -0
- package/adapters/vue/components/FdyAutocomplete.vue +1 -1
- package/adapters/vue/components/FdyCascade.vue +1 -1
- package/adapters/vue/components/FdyCfl.vue +8 -8
- package/adapters/vue/components/FdyChart.vue +2 -2
- package/adapters/vue/components/FdyCombo.vue +1 -1
- package/adapters/vue/components/FdyDateRange.vue +2 -2
- package/adapters/vue/components/FdyDatepicker.vue +5 -5
- package/adapters/vue/components/FdyDrawer.vue +3 -3
- package/adapters/vue/components/FdyModal.vue +3 -3
- package/adapters/vue/components/FdyTable.vue +7 -7
- package/adapters/vue/components/FdyTableFilter.vue +1 -1
- package/adapters/vue/components/FdyTableFooter.vue +5 -5
- package/adapters/vue/index.d.ts +3 -2
- package/adapters/vue/index.js +1 -0
- package/adapters/vue/useFreeday.js +3 -3
- package/dist/asset.d.ts +3 -3
- package/dist/freeday-app-shell.js +49 -9
- package/dist/freeday-autocomplete.js +2 -2
- package/dist/freeday-breakpoint.js +1 -1
- package/dist/freeday-carousel.js +5 -5
- package/dist/freeday-cascade.js +7 -7
- package/dist/freeday-cfl.js +9 -9
- package/dist/freeday-chart.js +11 -11
- package/dist/freeday-chip.js +3 -3
- package/dist/freeday-datepicker.js +11 -7
- package/dist/freeday-datetime.js +5 -5
- package/dist/freeday-drawer.js +1 -1
- package/dist/freeday-form.js +14 -14
- package/dist/freeday-mask.js +6 -6
- package/dist/freeday-menu.js +1 -1
- package/dist/freeday-number.js +3 -3
- package/dist/freeday-popover.js +3 -3
- package/dist/freeday-rating.js +2 -2
- package/dist/freeday-select.js +5 -5
- package/dist/freeday-slider.js +2 -2
- package/dist/freeday-stepper.js +7 -7
- package/dist/freeday-table.js +15 -15
- package/dist/freeday-tabs.js +1 -1
- package/dist/freeday-timepicker.js +2 -2
- package/dist/freeday-toast.js +8 -8
- package/dist/freeday-tree.js +3 -3
- package/dist/freeday-upload.js +25 -25
- package/dist/freeday.bundle.css +176 -176
- package/dist/freeday.css +170 -170
- package/dist/freeday.d.ts +2 -2
- package/dist/freeday.js +202 -158
- package/dist/freeday.tokens.css +5 -5
- package/docs/agent-onboarding.md +42 -42
- package/docs/getting-started.md +35 -35
- package/docs/integrations.md +18 -18
- package/docs/reference-screen.html +14 -14
- package/package.json +5 -4
- package/src/base.css +6 -6
- package/src/components/accordion.css +4 -4
- package/src/components/alert.css +1 -1
- package/src/components/app-shell.css +10 -10
- package/src/components/appbar.css +3 -3
- package/src/components/autocomplete.css +1 -1
- package/src/components/avatar.css +3 -3
- package/src/components/badge.css +5 -5
- package/src/components/breadcrumb.css +1 -1
- package/src/components/breakpoints.css +4 -4
- package/src/components/button.css +13 -13
- package/src/components/card.css +8 -8
- package/src/components/carousel.css +2 -2
- package/src/components/cascade.css +2 -2
- package/src/components/cfl.css +4 -4
- package/src/components/chart.css +3 -3
- package/src/components/chip.css +2 -2
- package/src/components/combo.css +3 -3
- package/src/components/composition.css +7 -7
- package/src/components/datepicker.css +6 -6
- package/src/components/datetimepicker.css +1 -1
- package/src/components/description-list.css +1 -1
- package/src/components/divider.css +1 -1
- package/src/components/drawer.css +1 -1
- package/src/components/file-upload.css +5 -5
- package/src/components/filterbar.css +7 -7
- package/src/components/form-grid.css +2 -2
- package/src/components/input-group.css +6 -6
- package/src/components/input.css +6 -6
- package/src/components/kbd.css +1 -1
- package/src/components/list.css +6 -6
- package/src/components/menu.css +3 -3
- package/src/components/modal.css +1 -1
- package/src/components/pagination.css +1 -1
- package/src/components/progress.css +1 -1
- package/src/components/rating.css +2 -2
- package/src/components/selection.css +4 -4
- package/src/components/skeleton.css +1 -1
- package/src/components/slider.css +1 -1
- package/src/components/spinner.css +1 -1
- package/src/components/states.css +1 -1
- package/src/components/stepper.css +2 -2
- package/src/components/table.css +15 -15
- package/src/components/tabs.css +3 -3
- package/src/components/timeline.css +1 -1
- package/src/components/timepicker.css +1 -1
- package/src/components/toast.css +3 -3
- package/src/components/tooltip.css +1 -1
- package/src/components/tree.css +2 -2
- package/tokens/breakpoints.d.ts +2 -2
- package/tokens/breakpoints.mjs +3 -3
- package/tokens/tokens.json +1 -1
package/USAGE.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Freeday — Usage doctrine
|
|
2
2
|
|
|
3
|
-
Freeday ships tokens and components. This file ships the **decisions
|
|
3
|
+
Freeday ships tokens and components. This file ships the **decisions**: which token to use when,
|
|
4
4
|
how a page is assembled, what earns emphasis. A component library enforces consistent *values*; a
|
|
5
5
|
design system also enforces consistent *decisions*. Skim this once before building a screen; it is
|
|
6
6
|
what makes screens built by different people (or different sessions) look like one product.
|
|
7
7
|
|
|
8
|
-
The rules are opinionated on purpose. When one conflicts with a real need, break it deliberately
|
|
8
|
+
The rules are opinionated on purpose. When one conflicts with a real need, break it deliberately, and
|
|
9
9
|
but start here, not from a blank page.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
## 1. Type roles
|
|
13
|
+
## 1. Type roles: three title levels, not eleven
|
|
14
14
|
|
|
15
15
|
Do **not** reuse `.fdy-card__title` for a page title. Collapsing page/section/card into one style is
|
|
16
16
|
the single biggest cause of "flat grey mush". Pick the role, not the size:
|
|
@@ -30,31 +30,31 @@ font size.
|
|
|
30
30
|
|
|
31
31
|
## Borders: two roles, two contrast contracts
|
|
32
32
|
|
|
33
|
-
`--color-border` (and `-muted` / `-strong`) is **decoration
|
|
33
|
+
`--color-border` (and `-muted` / `-strong`) is **decoration**: a card edge, a table rule, a
|
|
34
34
|
separator. Nothing in WCAG requires it to be visible, and it is deliberately faint (≈1.3:1).
|
|
35
35
|
|
|
36
|
-
`--color-control-border` is the **boundary of a control
|
|
36
|
+
`--color-control-border` is the **boundary of a control**: input, textarea, checkbox, radio,
|
|
37
37
|
switch, dropzone. WCAG 1.4.11 requires 3:1 against every surface it can sit on, so it is
|
|
38
38
|
necessarily darker than a card edge; that difference is the contract, not an inconsistency.
|
|
39
39
|
|
|
40
40
|
It is tuned to clear that floor with margin (≈3.4–3.9 depending on surface) rather than to be as
|
|
41
41
|
dark as it can be: a boundary that reads as loud as body text makes a form feel heavier than it is.
|
|
42
|
-
Do not "harmonise" a control border down to `--color-border
|
|
42
|
+
Do not "harmonise" a control border down to `--color-border`. That drops a required 3:1 boundary to
|
|
43
43
|
1.3:1.
|
|
44
44
|
|
|
45
|
-
## 2. Spacing rhythm
|
|
45
|
+
## 2. Spacing rhythm: three gaps, always from the scale
|
|
46
46
|
|
|
47
47
|
Never a loose value; always `var(--space-N)` (a 4px scale). Three rhythms carry most layouts, and the
|
|
48
48
|
composition primitives apply them for you:
|
|
49
49
|
|
|
50
|
-
- **Between page sections:** `--space-8
|
|
51
|
-
- **Within a group** (heading ↔ its body, cards in a list): `--space-4
|
|
50
|
+
- **Between page sections:** `--space-8`, which `.fdy-page` sets between its children.
|
|
51
|
+
- **Within a group** (heading ↔ its body, cards in a list): `--space-4`, which `.fdy-page-section` sets.
|
|
52
52
|
- **Inside a control/card** (label ↔ input, icon ↔ text): `--space-2` / `--space-3`.
|
|
53
53
|
|
|
54
54
|
Reserve `--space-1` for hairline pairs and `--space-10`+ for deliberate breathing room (a hero, an
|
|
55
|
-
empty state). Don't scatter `--space-6` everywhere
|
|
55
|
+
empty state). Don't scatter `--space-6` everywhere. A page with one gap value has no rhythm.
|
|
56
56
|
|
|
57
|
-
## 3. Elevation
|
|
57
|
+
## 3. Elevation: most surfaces are flat
|
|
58
58
|
|
|
59
59
|
Shadow is a signal, not decoration. There are two families, and the difference matters:
|
|
60
60
|
|
|
@@ -67,23 +67,23 @@ Shadow is a signal, not decoration. There are two families, and the difference m
|
|
|
67
67
|
| Overlay | `--shadow-4` | `.fdy-drawer` |
|
|
68
68
|
| **Lift** | `--shadow-lift` / `--shadow-lift-hover` | **`.fdy-card`** (and `--elevated` / `--interactive:hover`), `.fdy-modal` |
|
|
69
69
|
|
|
70
|
-
**`.fdy-card` is a lifted surface, not a hairline one
|
|
70
|
+
**`.fdy-card` is a lifted surface, not a hairline one.** `--shadow-lift` is a real 34px lift, ~6×
|
|
71
71
|
heavier than `--shadow-1`. That is the whole point of a card, and it is also why a *stack* of them
|
|
72
72
|
reads wrong: a list of ten cards is ten objects floating off the page. For rows, use the flat
|
|
73
73
|
container **`.fdy-list` / `.fdy-list__row`** (hairline border, `--color-border-muted` dividers, no
|
|
74
74
|
shadow). Reach for `.fdy-card` when something genuinely is one pickable object.
|
|
75
75
|
|
|
76
|
-
If every box on the screen has the same card shadow, none of them read as special
|
|
76
|
+
If every box on the screen has the same card shadow, none of them read as special. That's the
|
|
77
77
|
"identical card grid" failure. Prefer flat sections with **one** raised element that matters.
|
|
78
78
|
|
|
79
|
-
## 4. Emphasis
|
|
79
|
+
## 4. Emphasis: exactly one primary per screen
|
|
80
80
|
|
|
81
81
|
`.fdy-btn` is *already* the primary action (there is no `--primary` modifier). Use it **once** per
|
|
82
|
-
screen
|
|
82
|
+
screen: the one thing you want the user to do. Everything else is `--ghost` or `--text`. Two primary
|
|
83
83
|
buttons on a screen means neither is. The same rule governs colour fills and `--shadow`: one focal
|
|
84
84
|
point, everything around it quiet.
|
|
85
85
|
|
|
86
|
-
## 5. Colour
|
|
86
|
+
## 5. Colour: semantic is reserved; categorical is `--tone`
|
|
87
87
|
|
|
88
88
|
- **Accent** (`--color-primary`, and `--color-accent` sparingly): interactive + brand. This is your
|
|
89
89
|
one accent hue.
|
|
@@ -91,19 +91,19 @@ point, everything around it quiet.
|
|
|
91
91
|
= caution only, `--color-danger` = destructive/error only. Never decorative. Encode state in a pill
|
|
92
92
|
or chip, not just colour.
|
|
93
93
|
- **Categorical** (`--tone-1` … `--tone-8`, the general alias of the validated chart palette): N
|
|
94
|
-
visually-distinct **non-semantic** colours
|
|
94
|
+
visually-distinct **non-semantic** colours: avatar tones, category chips, tags, legend swatches.
|
|
95
95
|
Use the modifiers `.fdy-avatar--tone-N` / `.fdy-chip--tone-N` (both stay WCAG AA in light & dark),
|
|
96
96
|
and hash a stable index off the full string so the same category always gets the same colour.
|
|
97
97
|
- **Surfaces:** most backgrounds are `--color-surface`; `--color-surface-2`/`-3` for a recessed area;
|
|
98
98
|
`--color-primary-soft` only when you want a tinted callout, not as a default panel colour.
|
|
99
99
|
|
|
100
|
-
## 5b. Theme
|
|
100
|
+
## 5b. Theme: global by default, per-subtree when a region is inverted
|
|
101
101
|
|
|
102
102
|
`data-theme="light|dark"` redefines the semantic tokens. Set it on `<html>` and it themes the app;
|
|
103
103
|
that is the normal case and nothing about it has changed.
|
|
104
104
|
|
|
105
105
|
**It is also per-subtree.** The two explicit selectors are bare `[data-theme="dark"]` /
|
|
106
|
-
`[data-theme="light"]`, and semantic tokens are inheriting custom properties
|
|
106
|
+
`[data-theme="light"]`, and semantic tokens are inheriting custom properties, so a
|
|
107
107
|
`<section data-theme="dark">` inverts that region and **every Freeday component inside it follows**:
|
|
108
108
|
card surfaces, inputs, buttons, and text roles like `.fdy-title-page` that set
|
|
109
109
|
`color: var(--color-text)` explicitly. A dark brand panel beside a light sign-in form needs no
|
|
@@ -112,40 +112,40 @@ in turn.
|
|
|
112
112
|
|
|
113
113
|
Do **not** invert a region by hand with `--color-inverse-*` plus `color-mix`. That pair is right for
|
|
114
114
|
a one-off band of your own markup, but it stops scaling the moment the region contains a real
|
|
115
|
-
component
|
|
115
|
+
component. Anything that sets its own colour from a token never sees your override, and you end up
|
|
116
116
|
restating colours per element.
|
|
117
117
|
|
|
118
118
|
The **system** default (`@media (prefers-color-scheme: dark)`) stays root-scoped on purpose: that
|
|
119
119
|
rule is about the document, and un-rooting it would re-darken the children of a light island.
|
|
120
120
|
|
|
121
|
-
## 6. Density
|
|
121
|
+
## 6. Density: `compact` for data-dense screens
|
|
122
122
|
|
|
123
123
|
`data-density="compact"` tightens control height **and** the mid-range spacing scale
|
|
124
124
|
(`--space-3`…`--space-6` step down a notch), so cards, toolbars and tables get denser. Use it on
|
|
125
125
|
table-heavy back-office screens; leave `comfortable` (the default) for forms.
|
|
126
126
|
|
|
127
127
|
**It is per-subtree, not only global.** The selector is a bare `[data-density="compact"]` and these
|
|
128
|
-
are inheriting custom properties, so the attribute works on `<html>` *or* on any wrapper
|
|
128
|
+
are inheriting custom properties, so the attribute works on `<html>` *or* on any wrapper: a route
|
|
129
129
|
container, a single `<section>`. An app whose two list screens are dense and whose three form screens
|
|
130
130
|
are not should scope it per screen rather than densifying everything. Set it at one level per screen,
|
|
131
131
|
never per component.
|
|
132
132
|
|
|
133
133
|
**And it goes both ways.** `data-density="comfortable"` is a real rule, not just the absence of
|
|
134
|
-
`compact`, so an app that is dense *at the root* can opt one region back out
|
|
134
|
+
`compact`, so an app that is dense *at the root* can opt one region back out: a header, a toolbar,
|
|
135
135
|
a footer shared with a sibling product that has to line up pixel for pixel. Inheritance only runs
|
|
136
136
|
downhill; without that rule a compact root left every subtree compact and a `comfortable` wrapper
|
|
137
137
|
changed nothing. Do not restate the five values by hand: that copy goes stale the moment the kit
|
|
138
138
|
retunes a step.
|
|
139
139
|
|
|
140
|
-
## 6b. Navigation: one component, two orientations
|
|
140
|
+
## 6b. Navigation: one component, two orientations, and never a tab role
|
|
141
141
|
|
|
142
142
|
A navigation link is an `<a class="fdy-nav__item">` marked **`aria-current="page"`**. That does not
|
|
143
143
|
change with the layout:
|
|
144
144
|
|
|
145
|
-
- **Sidebar app
|
|
146
|
-
- **Top-nav app
|
|
145
|
+
- **Sidebar app:** `.fdy-nav` inside `.fdy-app__sidebar` (the default).
|
|
146
|
+
- **Top-nav app:** `.fdy-nav.fdy-nav--horizontal` inside `.fdy-appbar` or `.fdy-app__topbar`, with
|
|
147
147
|
no sidebar. Same items, same states; on `.fdy-appbar--primary` the links go on-colour for you.
|
|
148
|
-
- **Routed sub-navigation that should look like tabs** (`/settings/profile` · `/settings/billing`)
|
|
148
|
+
- **Routed sub-navigation that should look like tabs** (`/settings/profile` · `/settings/billing`):
|
|
149
149
|
put `.fdy-tabs__list` / `.fdy-tabs__tab` on plain `<a>`s. Those classes honour `aria-current="page"`
|
|
150
150
|
as well as `aria-selected="true"` precisely for this. Do **not** add `role="tab"`/`role="tablist"`
|
|
151
151
|
or `freeday-tabs.js`: those promise a roving-tabindex, arrow-key, one-panel-per-tab contract that
|
|
@@ -154,38 +154,42 @@ change with the layout:
|
|
|
154
154
|
Reserve the full `.fdy-tabs` component (with its roles and its enhancer) for **in-page** tabs, where
|
|
155
155
|
nothing navigates.
|
|
156
156
|
|
|
157
|
-
**Toggles:** a button that is on/off carries `aria-pressed`, and the kit styles it
|
|
157
|
+
**Toggles:** a button that is on/off carries `aria-pressed`, and the kit styles it: soft primary
|
|
158
158
|
fill on `--ghost`/`--text`, a sunken gradient on the solid button. A `.fdy-btn-group` of `--ghost`
|
|
159
159
|
buttons with exactly one `aria-pressed="true"` **is** the segmented control; don't hand-tint it.
|
|
160
160
|
|
|
161
161
|
## 7. Assemble the page from the frame down
|
|
162
162
|
|
|
163
|
-
1. **Shell:** every application starts inside **`.fdy-app
|
|
163
|
+
1. **Shell:** every application starts inside **`.fdy-app`**, a flex row of `__sidebar` (with
|
|
164
164
|
`__brand` + `.fdy-nav`) and `__content` (which holds `__topbar` + `__main`, plus `__navtoggle`
|
|
165
|
-
and `__backdrop`). The nesting is fixed; don't hand-roll a shell from flexbox
|
|
166
|
-
|
|
165
|
+
and `__backdrop`). The nesting is fixed; don't hand-roll a shell from flexbox. **Take the
|
|
166
|
+
behaviour too:** `<FdyAppShell>` in Vue/React/Blazor, or `data-fdy-app` plus
|
|
167
|
+
`freeday-app-shell.js` in plain markup. Until 1.53.0 the kit shipped the classes and left every
|
|
168
|
+
consumer to write the toggle, Escape, focus trap, focus restore and `inert` themselves, and the
|
|
169
|
+
two copies in this repo's own docs had already drifted apart; a nav panel you cannot escape from
|
|
170
|
+
with the keyboard is the usual result. Skeleton: `COMPONENTS.md` §App shell; a working screen:
|
|
167
171
|
`docs/reference-screen.html`.
|
|
168
172
|
2. **Page:** wrap the screen body in **`.fdy-page`** (vertical section rhythm), opening with a
|
|
169
173
|
**`.fdy-page__header`** (eyebrow + `.fdy-title-page` + `.fdy-page__desc` on the left, the one
|
|
170
174
|
primary action on the right).
|
|
171
175
|
3. **Sections:** each region is a **`.fdy-page-section`** (a `.fdy-title-section` + optional
|
|
172
176
|
`.fdy-toolbar`, then its body).
|
|
173
|
-
**Toolbar or filter bar
|
|
177
|
+
**Toolbar or filter bar? Pick by whether the fields carry visible labels.** `.fdy-toolbar` is
|
|
174
178
|
`align-items:center`, right for bare controls (buttons, a search box, chips); put a labelled
|
|
175
179
|
`.fdy-field` in it and that field sits half a label-height low against its neighbours. Fields
|
|
176
180
|
with visible labels belong in **`.fdy-filterbar`**, which is `align-items:flex-end` for exactly
|
|
177
181
|
this reason (and has the width rhythm `--w-sm`…`--w-grow`). In a toolbar, label fields with
|
|
178
182
|
`.fdy-visually-hidden` or a placeholder.
|
|
179
|
-
4. **KPIs:** a **`.fdy-stats`** grid of **`.fdy-stat`** tiles
|
|
183
|
+
4. **KPIs:** a **`.fdy-stats`** grid of **`.fdy-stat`** tiles, deliberately *not* cards, so a metric
|
|
180
184
|
strip doesn't become an identical-card grid. Wrap in `.fdy-stats--boxed` for one shared strip.
|
|
181
185
|
5. **Content:** components (`.fdy-card`, `.fdy-datatable`, `.fdy-chart`, …) go inside sections.
|
|
182
186
|
|
|
183
187
|
Freeday deliberately owns **components + tokens, not layout**. Everything above is layout in the kit's
|
|
184
188
|
own language; for the rest (grids, one-off spacing), pair a utility framework run **utilities-only,
|
|
185
|
-
preflight-off
|
|
189
|
+
preflight-off**, and build its theme on `var(--space-N)` so the two systems agree. See
|
|
186
190
|
`docs/getting-started.md` §Core concepts.
|
|
187
191
|
|
|
188
192
|
---
|
|
189
193
|
|
|
190
194
|
*Layout classes here live in `src/components/composition.css`. If a screen needs a primitive that
|
|
191
|
-
isn't here, it probably belongs here
|
|
195
|
+
isn't here, it probably belongs here. Open an issue rather than re-inventing it per screen.*
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
@namespace Freeday.Blazor
|
|
2
|
+
@inherits FreedayComponentBase<FdyAppShell>
|
|
3
|
+
|
|
4
|
+
@* Blazor wrapper over freeday's .fdy-app shell. Unlike FdyModal and FdyDrawer, which drive a native
|
|
5
|
+
<dialog> through the bridge, this one hands the whole behaviour to freeday-app-shell.js via
|
|
6
|
+
`data-fdy-app` and binds to it: the enhancer announces changes as a bubbling `fdy-app-nav` event
|
|
7
|
+
and accepts them through FreedayAppShell.setVisible. That is the bridge's own doctrine: the
|
|
8
|
+
enhancers stay the source of truth, and it keeps the focus trap from existing a third time. *@
|
|
9
|
+
<div @ref="Root" class="fdy-app" data-fdy-app>
|
|
10
|
+
@SkipContent
|
|
11
|
+
|
|
12
|
+
<aside class="fdy-app__sidebar">
|
|
13
|
+
@BrandContent
|
|
14
|
+
@NavContent
|
|
15
|
+
</aside>
|
|
16
|
+
|
|
17
|
+
<div class="fdy-app__content">
|
|
18
|
+
<header class="fdy-app__topbar">
|
|
19
|
+
<button class="fdy-app__navtoggle" type="button" aria-label="@ToggleLabel">
|
|
20
|
+
@if (ToggleIcon is not null)
|
|
21
|
+
{
|
|
22
|
+
@ToggleIcon
|
|
23
|
+
}
|
|
24
|
+
else
|
|
25
|
+
{
|
|
26
|
+
<text>☰</text>
|
|
27
|
+
}
|
|
28
|
+
</button>
|
|
29
|
+
|
|
30
|
+
<h1 class="fdy-app__title">
|
|
31
|
+
@if (TitleContent is not null)
|
|
32
|
+
{
|
|
33
|
+
@TitleContent
|
|
34
|
+
}
|
|
35
|
+
else
|
|
36
|
+
{
|
|
37
|
+
@Title
|
|
38
|
+
}
|
|
39
|
+
</h1>
|
|
40
|
+
|
|
41
|
+
@TopbarContent
|
|
42
|
+
</header>
|
|
43
|
+
|
|
44
|
+
<main class="fdy-app__main">
|
|
45
|
+
@ChildContent
|
|
46
|
+
</main>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="fdy-app__backdrop"></div>
|
|
50
|
+
</div>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
using Microsoft.AspNetCore.Components;
|
|
2
|
+
using Microsoft.JSInterop;
|
|
3
|
+
|
|
4
|
+
namespace Freeday.Blazor;
|
|
5
|
+
|
|
6
|
+
public partial class FdyAppShell
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// Whether the nav is visible to the reader, one idea, in both modes: above the nav breakpoint
|
|
10
|
+
/// a hidden nav is a collapsed column, below it a visible nav is an overlay. The kit owns that
|
|
11
|
+
/// mapping so an app never reasons about the viewport to answer a question about its own UI.
|
|
12
|
+
/// <para>
|
|
13
|
+
/// Leave it <c>null</c> (the default) and the shell keeps its own, starting from the viewport:
|
|
14
|
+
/// a column on a wide screen, hidden on a narrow one. That default is why this is nullable,
|
|
15
|
+
/// a caller cannot express it as a single initial value before it knows the viewport. Bind it
|
|
16
|
+
/// (<c>@bind-NavOpen</c>) when the app wants to drive it from a menu or a saved preference.
|
|
17
|
+
/// </para>
|
|
18
|
+
/// </summary>
|
|
19
|
+
[Parameter] public bool? NavOpen { get; set; }
|
|
20
|
+
[Parameter] public EventCallback<bool?> NavOpenChanged { get; set; }
|
|
21
|
+
|
|
22
|
+
/// <summary>Plain-text page title; ignored when <see cref="TitleContent"/> is supplied.</summary>
|
|
23
|
+
[Parameter] public string Title { get; set; } = string.Empty;
|
|
24
|
+
[Parameter] public RenderFragment? TitleContent { get; set; }
|
|
25
|
+
|
|
26
|
+
/// <summary>The brand block at the top of the sidebar.</summary>
|
|
27
|
+
[Parameter] public RenderFragment? BrandContent { get; set; }
|
|
28
|
+
|
|
29
|
+
/// <summary>The navigation itself, normally a <c>.fdy-nav</c> list.</summary>
|
|
30
|
+
[Parameter] public RenderFragment? NavContent { get; set; }
|
|
31
|
+
|
|
32
|
+
/// <summary>Topbar actions, rendered after the title.</summary>
|
|
33
|
+
[Parameter] public RenderFragment? TopbarContent { get; set; }
|
|
34
|
+
|
|
35
|
+
/// <summary>The skip link, if the app ships one. Rendered as the shell's first child.</summary>
|
|
36
|
+
[Parameter] public RenderFragment? SkipContent { get; set; }
|
|
37
|
+
|
|
38
|
+
/// <summary>The page itself.</summary>
|
|
39
|
+
[Parameter] public RenderFragment? ChildContent { get; set; }
|
|
40
|
+
|
|
41
|
+
/// <summary>Custom glyph for the nav toggle; a hamburger is used when null.</summary>
|
|
42
|
+
[Parameter] public RenderFragment? ToggleIcon { get; set; }
|
|
43
|
+
|
|
44
|
+
[Parameter] public string ToggleLabel { get; set; } = "Toggle navigation";
|
|
45
|
+
|
|
46
|
+
private int _navToken;
|
|
47
|
+
private bool? _lastNavOpen;
|
|
48
|
+
|
|
49
|
+
protected override async ValueTask HydrateAsync()
|
|
50
|
+
{
|
|
51
|
+
await JS.InvokeVoidAsync("FreedayBlazor.initAll", Root);
|
|
52
|
+
_navToken = await JS.InvokeAsync<int>(
|
|
53
|
+
"FreedayBlazor.on", Root, "fdy-app-nav", Self, nameof(OnNavChanged));
|
|
54
|
+
|
|
55
|
+
if (NavOpen is bool wanted)
|
|
56
|
+
{
|
|
57
|
+
// A caller that bound a value is authoritative, push it down.
|
|
58
|
+
await JS.InvokeVoidAsync("FreedayAppShell.setVisible", Root, wanted);
|
|
59
|
+
_lastNavOpen = wanted;
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
{
|
|
63
|
+
// Nobody bound one, so adopt what the shell decided from the viewport. Without this the
|
|
64
|
+
// bound value would claim `false` next to a nav that is plainly a visible column.
|
|
65
|
+
bool visible = await JS.InvokeAsync<bool>("FreedayAppShell.isVisible", Root);
|
|
66
|
+
_lastNavOpen = visible;
|
|
67
|
+
await SetNavOpenAsync(visible);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected override async Task OnParametersSetAsync()
|
|
72
|
+
{
|
|
73
|
+
// Only reconcile once the shell is wired, and only when the caller actually changed it,
|
|
74
|
+
// echoing our own event back down would fight the enhancer for the same state.
|
|
75
|
+
if (_navToken != 0 && NavOpen is bool wanted && wanted != _lastNavOpen)
|
|
76
|
+
{
|
|
77
|
+
_lastNavOpen = wanted;
|
|
78
|
+
await JS.InvokeVoidAsync("FreedayAppShell.setVisible", Root, wanted);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// <summary>Invoked by the bridge when the shell's nav visibility changes for any reason,
|
|
83
|
+
/// the toggle, Esc, the backdrop, following a nav link, or the viewport crossing the
|
|
84
|
+
/// breakpoint.</summary>
|
|
85
|
+
[JSInvokable]
|
|
86
|
+
public async Task OnNavChanged(NavDetail detail)
|
|
87
|
+
{
|
|
88
|
+
_lastNavOpen = detail.Visible;
|
|
89
|
+
await SetNavOpenAsync(detail.Visible);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async Task SetNavOpenAsync(bool visible)
|
|
93
|
+
{
|
|
94
|
+
if (NavOpenChanged.HasDelegate)
|
|
95
|
+
{
|
|
96
|
+
await NavOpenChanged.InvokeAsync(visible);
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
{
|
|
100
|
+
NavOpen = visible;
|
|
101
|
+
StateHasChanged();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
protected override async ValueTask CleanupAsync()
|
|
106
|
+
{
|
|
107
|
+
if (_navToken != 0)
|
|
108
|
+
{
|
|
109
|
+
await JS.InvokeVoidAsync("FreedayBlazor.off", _navToken);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public sealed record NavDetail(bool Visible);
|
|
114
|
+
}
|
|
@@ -195,7 +195,7 @@ public partial class FdyCfl<TRow>
|
|
|
195
195
|
else _picked.RemoveAt(at);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
/* A click means "tick this" in multi and "this is my answer" in single
|
|
198
|
+
/* A click means "tick this" in multi and "this is my answer" in single, the whole difference. */
|
|
199
199
|
private async Task RowClickAsync(TRow row)
|
|
200
200
|
{
|
|
201
201
|
if (Multiple) TogglePick(row);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
@* A thin Blazor wrapper over freeday's zero-dependency chart renderer (freeday-chart.js). Charts
|
|
4
4
|
are not form controls, so re-implementing the SVG maths in C# would only duplicate them. Instead
|
|
5
5
|
Blazor owns the outer <div> and its data-* attributes; after every render this calls
|
|
6
|
-
FreedayChart.update() (via the bridge) to repaint the SVG from those attributes
|
|
6
|
+
FreedayChart.update() (via the bridge) to repaint the SVG from those attributes, giving reactive,
|
|
7
7
|
typed charts with no external dependency. Because the renderer paints with CSS var()/--chart-*
|
|
8
8
|
tokens, charts also recolour on data-theme change for free. Requires dist/freeday.js loaded so
|
|
9
9
|
window.FreedayChart exists; until then the slotted fallback content stays visible. *@
|
|
@@ -32,7 +32,7 @@ public partial class FdyChart : ComponentBase
|
|
|
32
32
|
/// <summary>Legend visibility: <c>auto</c> | <c>always</c> | <c>none</c>.</summary>
|
|
33
33
|
[Parameter] public string? Legend { get; set; }
|
|
34
34
|
|
|
35
|
-
/// <summary>Per-series colour override
|
|
35
|
+
/// <summary>Per-series colour override, semantic token names (primary/accent/success/…) or
|
|
36
36
|
/// categorical slots <c>chart-1</c>..<c>chart-8</c>. Omit for the default chart palette.</summary>
|
|
37
37
|
[Parameter] public IReadOnlyList<string>? Colors { get; set; }
|
|
38
38
|
|
|
@@ -72,14 +72,14 @@ public partial class FdyChart : ComponentBase
|
|
|
72
72
|
private string? ColorsAttr => Colors is null ? null : string.Join(",", Colors);
|
|
73
73
|
private string? StackedAttr => Stacked ? string.Empty : null; // presence attribute
|
|
74
74
|
|
|
75
|
-
// Signature of every data-affecting attribute
|
|
75
|
+
// Signature of every data-affecting attribute, the C# analogue of the Vue/React watch list.
|
|
76
76
|
private string DataSignature => string.Join("", Type, SeriesAttr, ValuesAttr, LabelsAttr,
|
|
77
77
|
Format, StackedAttr, Legend, ColorsAttr, Color, Center);
|
|
78
78
|
|
|
79
79
|
private string? _lastSignature;
|
|
80
80
|
|
|
81
81
|
// Repaint only when the chart's own data changed (first render, or a data-* attribute differs
|
|
82
|
-
// since the last paint)
|
|
82
|
+
// since the last paint), so an unrelated parent re-render doesn't needlessly repaint the SVG.
|
|
83
83
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
84
84
|
{
|
|
85
85
|
string sig = DataSignature;
|
|
@@ -3,7 +3,7 @@ using System.Text.Json.Serialization;
|
|
|
3
3
|
namespace Freeday.Blazor;
|
|
4
4
|
|
|
5
5
|
/// <summary>One data series in a multi-series chart (line/area/bar). Serialised to the renderer's
|
|
6
|
-
/// <c>data-series</c> JSON shape <c>{ label, values, role? }</c
|
|
6
|
+
/// <c>data-series</c> JSON shape <c>{ label, values, role? }</c>, hence the camelCase names.</summary>
|
|
7
7
|
public sealed record FdyChartSeries(
|
|
8
8
|
[property: JsonPropertyName("label")] string Label,
|
|
9
9
|
[property: JsonPropertyName("values")] IReadOnlyList<double> Values,
|
|
@@ -34,7 +34,7 @@ public partial class FdyCombo<TValue>
|
|
|
34
34
|
private bool _ready;
|
|
35
35
|
|
|
36
36
|
// The enhancer owns this combo's DOM after hydration (open/close, aria-selected, the button
|
|
37
|
-
// label). Re-rendering here would fight it
|
|
37
|
+
// label). Re-rendering here would fight it, e.g. reconciling the listbox mid-close leaves it
|
|
38
38
|
// stuck open. Render once for the initial markup, then never again; push external Value changes
|
|
39
39
|
// through the enhancer via comboSetValue instead.
|
|
40
40
|
protected override bool ShouldRender() => !Hydrated;
|
|
@@ -60,7 +60,7 @@ public partial class FdyCombo<TValue>
|
|
|
60
60
|
|
|
61
61
|
protected override async Task OnParametersSetAsync()
|
|
62
62
|
{
|
|
63
|
-
// Push an externally-changed Value onto the enhancer-owned DOM (silent
|
|
63
|
+
// Push an externally-changed Value onto the enhancer-owned DOM (silent, no fdy-change echo).
|
|
64
64
|
if (_ready && !EqualityComparer<TValue>.Default.Equals(Value, _lastValue))
|
|
65
65
|
{
|
|
66
66
|
_lastValue = Value;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
@* Controlled wrapper over freeday's .fdy-modal native <dialog>. Blazor can't call
|
|
5
5
|
showModal()/close() from C#, so open/close + Esc/backdrop dismissal route through the
|
|
6
6
|
freeday-blazor.js bridge. Native <dialog> gives the focus trap, focus restore, top-layer
|
|
7
|
-
stacking and inert background for free
|
|
7
|
+
stacking and inert background for free; this only reconciles it with Blazor state. *@
|
|
8
8
|
<dialog @ref="Root" class="@ModalClass" aria-labelledby="@_titleId">
|
|
9
9
|
<div class="fdy-modal__header">
|
|
10
10
|
<h3 id="@_titleId" class="fdy-modal__title">
|
|
@@ -30,8 +30,8 @@ public partial class FdyTable<TRow>
|
|
|
30
30
|
[Parameter] public int PageSize { get; set; }
|
|
31
31
|
|
|
32
32
|
/// <summary>
|
|
33
|
-
/// Controlled client-side page index (0-based). Set it
|
|
34
|
-
/// <see cref="Page"
|
|
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
35
|
/// what lets an EXTERNAL pager drive the table: a responsive screen that hides the datatable below
|
|
36
36
|
/// the <c>md</c> breakpoint and renders a card list from <see cref="Process"/> can render one pager
|
|
37
37
|
/// for both breakpoints and bind it here. Leave null for the internal index (unchanged default).
|
|
@@ -45,14 +45,14 @@ public partial class FdyTable<TRow>
|
|
|
45
45
|
/// Offer a rows-per-page control in the footer, beside the range and the pager. Leave null for
|
|
46
46
|
/// none (unchanged default). Every back office has one, and a table that renders two thirds of
|
|
47
47
|
/// its own footer forces the app to rebuild all three to add the last (#008).
|
|
48
|
-
/// Server mode reports the pick through <see cref="PageChanged"
|
|
48
|
+
/// Server mode reports the pick through <see cref="PageChanged"/>, same callback as a page
|
|
49
49
|
/// click, with a new size. Client mode applies it internally and also raises
|
|
50
50
|
/// <see cref="PageSizeChanged"/>, so the control works with nothing wired.
|
|
51
51
|
/// </summary>
|
|
52
52
|
[Parameter] public IReadOnlyList<int>? PageSizes { get; set; }
|
|
53
53
|
|
|
54
54
|
/// <summary>Raised in client mode when the reader picks a new rows-per-page. The table has
|
|
55
|
-
/// already applied it
|
|
55
|
+
/// already applied it, this is for a caller that wants to persist the choice.</summary>
|
|
56
56
|
[Parameter] public EventCallback<int> PageSizeChanged { get; set; }
|
|
57
57
|
|
|
58
58
|
[Parameter] public int? PageIndex { get; set; }
|
|
@@ -62,7 +62,7 @@ public partial class FdyTable<TRow>
|
|
|
62
62
|
[Parameter] public EventCallback<int> PageIndexChanged { get; set; }
|
|
63
63
|
|
|
64
64
|
/// <summary>Raised whenever the processed page of rows (after filter/sort/paginate) or the total
|
|
65
|
-
/// changes, in BOTH modes
|
|
65
|
+
/// changes, in BOTH modes, so the same processed set can drive a card list, a summary or an
|
|
66
66
|
/// export without re-deriving the pipeline. Mirrors <c>process</c> in the Vue/React adapters.</summary>
|
|
67
67
|
[Parameter] public EventCallback<FdyTableProcess<TRow>> Process { get; set; }
|
|
68
68
|
|
|
@@ -113,7 +113,7 @@ public partial class FdyTable<TRow>
|
|
|
113
113
|
FiltersControlled ? (Filters ?? EmptyFilters) : _internalFilters;
|
|
114
114
|
|
|
115
115
|
/* Client-mode rows-per-page. PageSize is a plain parameter with no callback, so a footer control
|
|
116
|
-
* that only reported would do nothing in the app that wired nothing
|
|
116
|
+
* that only reported would do nothing in the app that wired nothing, the table applies the pick
|
|
117
117
|
* itself and reports it. An explicit change to the parameter wins back (see OnParametersSet). */
|
|
118
118
|
private int? _internalPageSize;
|
|
119
119
|
private int _prevPageSize;
|
|
@@ -298,7 +298,7 @@ public partial class FdyTable<TRow>
|
|
|
298
298
|
|
|
299
299
|
// Enter/Space activate a focused row. Blazor's KeyboardEventArgs can't tell whether the row
|
|
300
300
|
// itself vs an inner control is the target, so activatable rows should not embed focusable
|
|
301
|
-
// controls (the common drill-in-row pattern)
|
|
301
|
+
// controls (the common drill-in-row pattern), matching the Vue guard's practical effect.
|
|
302
302
|
private async Task OnRowKeydown(KeyboardEventArgs e, TRow row)
|
|
303
303
|
{
|
|
304
304
|
if (!RowActivatable) return;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
@* Internal to FdyTable: one column's header funnel button + its type-aware filter popover
|
|
4
4
|
(text / enum / number / date), over freeday's .fdy-table__filterbtn + .fdy-filter* classes.
|
|
5
5
|
The panel is position:fixed (top layer) so it escapes the table's overflow; light-dismiss
|
|
6
|
-
(outside pointer / Esc) is wired through FreedayBlazor.onOutside. Purely controlled
|
|
6
|
+
(outside pointer / Esc) is wired through FreedayBlazor.onOutside. Purely controlled: it renders
|
|
7
7
|
the current Filter and raises the next one (or null to clear). *@
|
|
8
8
|
<span @ref="_root" class="fdy-table__filterwrap">
|
|
9
9
|
<button type="button"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@* The band under a table: what you are looking at, how much of it you see, where you are.
|
|
2
2
|
|
|
3
3
|
FdyTable renders this itself, so most screens never use it directly. It is public for the one
|
|
4
|
-
shape that cannot use the table's own
|
|
4
|
+
shape that cannot use the table's own: a RESPONSIVE list, where a .fdy-datatable at lg and a
|
|
5
5
|
.fdy-list below it are two renderings of ONE page of rows. A footer inside the table is inside the
|
|
6
6
|
half that is hidden on a phone, so those screens render it once, outside both (improvement notes
|
|
7
7
|
#005 and #008, from IDU_EMATE_APPL_WEB). *@
|
|
@@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Components;
|
|
|
3
3
|
namespace Freeday.Blazor;
|
|
4
4
|
|
|
5
5
|
/// <summary>
|
|
6
|
-
/// The table footer
|
|
6
|
+
/// The table footer, range, optional rows-per-page, pager, as a component of its own.
|
|
7
7
|
/// It owns nothing: <see cref="Page"/> in, <see cref="PageChanged"/> out, the same contract as
|
|
8
8
|
/// <c>FdyTable</c>'s server mode.
|
|
9
9
|
/// </summary>
|
|
@@ -15,7 +15,7 @@ public partial class FdyTableFooter
|
|
|
15
15
|
[Parameter] public EventCallback<FdyPageState> PageChanged { get; set; }
|
|
16
16
|
|
|
17
17
|
/// <summary>
|
|
18
|
-
/// Offer a rows-per-page control. Leave null for none
|
|
18
|
+
/// Offer a rows-per-page control. Leave null for none; the footer is then range + pager,
|
|
19
19
|
/// exactly as before. Picking a size raises <see cref="PageChanged"/> with the new size and the
|
|
20
20
|
/// index that still holds the row the reader was looking at.
|
|
21
21
|
/// </summary>
|
|
@@ -28,7 +28,7 @@ public partial class FdyTableFooter
|
|
|
28
28
|
private IReadOnlyList<FdyComboOption<string>> SizeOptions =>
|
|
29
29
|
Sizes.Select(size => new FdyComboOption<string>(size.ToString(), size.ToString())).ToList();
|
|
30
30
|
|
|
31
|
-
// The combo is labelled by the visible word beside it
|
|
31
|
+
// The combo is labelled by the visible word beside it, a <label for> cannot reach inside a
|
|
32
32
|
// component, and an aria-label would leave that word attached to nothing.
|
|
33
33
|
private readonly string SizeLabelId = $"fdy-rows-{Guid.NewGuid():N}";
|
|
34
34
|
|
|
@@ -40,7 +40,7 @@ public partial class FdyTableFooter
|
|
|
40
40
|
|
|
41
41
|
private bool HasPager => Page.Size > 0 && TotalPages > 1;
|
|
42
42
|
|
|
43
|
-
// One page and no size control means there is nothing here to say
|
|
43
|
+
// One page and no size control means there is nothing here to say, the table has always
|
|
44
44
|
// withheld the whole band in that case, and this is where that decision now lives.
|
|
45
45
|
private bool Visible => HasPager || Sizes.Count > 0;
|
|
46
46
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Freeday
|
|
1
|
+
<!-- Freeday. Blazor component library (Razor Class Library).
|
|
2
2
|
Typed wrappers over the vanilla `fdy-*` enhancers; the host page must load
|
|
3
3
|
dist/freeday.js + adapters/blazor/freeday-blazor.js. Targets net8.0 (LTS) so it can
|
|
4
4
|
be referenced from net8/9/10 apps. Consume via <ProjectReference> to this .csproj
|
|
@@ -36,7 +36,7 @@ public abstract class FreedayComponentBase<TSelf> : ComponentBase, IAsyncDisposa
|
|
|
36
36
|
/// registered enhancer over <see cref="Root"/>; override to call a specific enhancer.</summary>
|
|
37
37
|
protected virtual ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayBlazor.initAll", Root);
|
|
38
38
|
|
|
39
|
-
/// <summary>Runs once after the first render, right after <see cref="HydrateAsync"
|
|
39
|
+
/// <summary>Runs once after the first render, right after <see cref="HydrateAsync"/>, wire event
|
|
40
40
|
/// subscriptions here.</summary>
|
|
41
41
|
protected virtual ValueTask OnHydratedAsync() => ValueTask.CompletedTask;
|
|
42
42
|
|
|
@@ -6,7 +6,7 @@ using System.Text.RegularExpressions;
|
|
|
6
6
|
namespace Freeday.Blazor;
|
|
7
7
|
|
|
8
8
|
/// <summary>
|
|
9
|
-
/// The sort / filter / paginate logic for <see cref="FdyTable{TRow}"
|
|
9
|
+
/// The sort / filter / paginate logic for <see cref="FdyTable{TRow}"/>, the C# counterpart of the
|
|
10
10
|
/// framework-agnostic adapters/core/table-model.js shared by the Vue and React adapters. Kept as a
|
|
11
11
|
/// separate pure-function class (no rendering, no state) so it mirrors that reference faithfully and
|
|
12
12
|
/// stays unit-testable. Every method is pure: inputs are never mutated (rows are copied before sort).
|
|
@@ -40,7 +40,7 @@ public static class TableModel
|
|
|
40
40
|
: 0;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/// <summary>ISO calendar day (yyyy-MM-dd)
|
|
43
|
+
/// <summary>ISO calendar day (yyyy-MM-dd), lexicographically comparable.</summary>
|
|
44
44
|
private static string DateOnly(object? v)
|
|
45
45
|
{
|
|
46
46
|
if (v is DateTime dt) return dt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
|
@@ -167,7 +167,7 @@ public static class TableModel
|
|
|
167
167
|
return rows.Skip(pageIndex * pageSize).Take(pageSize).ToList();
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
/// <summary>Distinct non-empty cell texts for a column, naturally sorted
|
|
170
|
+
/// <summary>Distinct non-empty cell texts for a column, naturally sorted, the enum-filter source.</summary>
|
|
171
171
|
public static List<string> DistinctValues<TRow>(IReadOnlyList<TRow> rows, FdyTableColumn<TRow> column)
|
|
172
172
|
{
|
|
173
173
|
HashSet<string> seen = new();
|