@cahyo-dimas/freeday 3.7.0 → 3.9.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 +197 -0
- package/COMPONENTS.md +214 -18
- package/README.id.md +4 -4
- package/README.md +4 -3
- package/adapters/blazor/FdyDatepicker.razor +3 -1
- package/adapters/blazor/FdyDatepicker.razor.cs +36 -1
- package/adapters/blazor/FdyMonthpicker.razor +29 -0
- package/adapters/blazor/FdyMonthpicker.razor.cs +102 -0
- package/adapters/blazor/FdyTable.razor +6 -1
- package/adapters/blazor/FdyTable.razor.cs +33 -0
- package/adapters/blazor/FdyTableFooter.razor +6 -6
- package/adapters/blazor/FdyTableFooter.razor.cs +36 -0
- package/adapters/core/month-grid.d.ts +116 -0
- package/adapters/core/month-grid.js +234 -0
- package/adapters/react/components/FdyDatepicker.tsx +164 -166
- package/adapters/react/components/FdyMonthpicker.tsx +400 -0
- package/adapters/react/components/FdyTable.tsx +38 -2
- package/adapters/react/components/FdyTableFilter.tsx +13 -1
- package/adapters/react/components/FdyTableFooter.tsx +37 -6
- package/adapters/react/index.d.ts +6 -2
- package/adapters/react/index.js +1 -0
- package/adapters/react/usePopover.ts +54 -9
- package/adapters/vue/components/FdyDatepicker.vue +158 -160
- package/adapters/vue/components/FdyMonthpicker.vue +387 -0
- package/adapters/vue/components/FdyTable.vue +33 -1
- package/adapters/vue/components/FdyTableFilter.vue +13 -1
- package/adapters/vue/components/FdyTableFooter.vue +39 -6
- package/adapters/vue/index.d.ts +16 -0
- package/adapters/vue/index.js +1 -0
- package/adapters/vue/usePopover.ts +53 -10
- package/dist/freeday-datepicker.js +68 -14
- package/dist/freeday-menu.js +15 -1
- package/dist/freeday-popover.js +33 -11
- package/dist/freeday-table.js +64 -5
- package/dist/freeday-tabs.js +21 -2
- package/dist/freeday.bundle.css +50 -2
- package/dist/freeday.css +50 -2
- package/dist/freeday.js +201 -33
- package/docs/agent-onboarding.md +3 -3
- package/docs/getting-started.md +6 -6
- package/docs/integrations.md +22 -4
- package/package.json +2 -2
- package/src/components/table.css +19 -2
- package/src/components/tabs.css +31 -0
package/dist/freeday.css
CHANGED
|
@@ -1821,8 +1821,25 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1821
1821
|
.fdy-table__filterbtn.is-active{color:var(--color-primary-strong);}
|
|
1822
1822
|
.fdy-table__filterbtn svg{width:.85rem;height:.85rem;}
|
|
1823
1823
|
|
|
1824
|
-
/* Filter popover
|
|
1825
|
-
|
|
1824
|
+
/* Filter popover. `position:fixed` because the panel is positioned by freeday-popover.js (and by
|
|
1825
|
+
usePopover on the typed path) with inline viewport coordinates, in the top layer, which is what
|
|
1826
|
+
escapes a card's or a scroll container's clip; the panel itself is a child of its own <th> on
|
|
1827
|
+
both paths, so an open modal <dialog> does not make it inert. It is the one kit panel that is
|
|
1828
|
+
fixed rather than absolute, and that is load-bearing: #079 cannot reach a panel which never
|
|
1829
|
+
resolves a `top:calc(100% + …)` against the viewport, which browser/filter-parity.mjs measures.
|
|
1830
|
+
`min-width` is the panel's own, so neither path may floor it to the width of a 24px funnel.
|
|
1831
|
+
|
|
1832
|
+
The typography block is a RESET of what the panel inherits from that <th>, and it has to name
|
|
1833
|
+
every inherited property the header rule sets, not the ones that looked worst. A header is
|
|
1834
|
+
uppercase, tracked, semibold, muted, 12px and `white-space:nowrap`; a numeric column adds mono
|
|
1835
|
+
and, on the typed path, an inline `text-align:right` that no ancestor rule could outrank anyway.
|
|
1836
|
+
None of that belongs on a panel of labels and inputs, and the four it reset from the start were
|
|
1837
|
+
the four that SHOWED: an 18rem cap hides a nowrap panel's damage, because the box stays 18rem
|
|
1838
|
+
wide while the text runs out of the list, which then scrolls sideways to reach a value nobody
|
|
1839
|
+
can read. `font-size` and `font-family` reach only the text that declares neither, the range
|
|
1840
|
+
separator and the title, which is exactly why they went unnoticed. Measured as geometry in
|
|
1841
|
+
browser/filter-wrap.mjs rather than asserted as declarations. */
|
|
1842
|
+
.fdy-filter{position:fixed;z-index:130;min-width:14rem;max-width:18rem;padding:var(--space-3);background:var(--color-surface);border:var(--bw) solid var(--color-border-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-3);text-align:left;white-space:normal;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-regular);text-transform:none;letter-spacing:normal;color:var(--color-text);}
|
|
1826
1843
|
.fdy-filter__title{font-size:var(--text-xs);font-weight:var(--weight-semibold);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-wide);margin-bottom:var(--space-2);}
|
|
1827
1844
|
.fdy-filter__list{display:flex;flex-direction:column;gap:var(--space-1);max-height:12rem;overflow:auto;}
|
|
1828
1845
|
.fdy-filter__check{display:flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm);color:var(--color-text);cursor:pointer;}
|
|
@@ -1879,6 +1896,37 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1879
1896
|
/* Reveal the newly-shown panel on tab switch (fade + rise), reduced-motion gated. */
|
|
1880
1897
|
@media (prefers-reduced-motion:no-preference){.fdy-tabs__panel:not([hidden]){animation:fdy-combo-in var(--dur-base) var(--ease-standard);}}
|
|
1881
1898
|
|
|
1899
|
+
/* VERTICAL VARIANT: the same tabs turned down the page, list on the leading side, panel beside it.
|
|
1900
|
+
*
|
|
1901
|
+
* Two markers, one each, and they are not interchangeable. The CLASS decides the look, because a
|
|
1902
|
+
* stylesheet cannot reach a parent from a child and the two-column box belongs to `.fdy-tabs`
|
|
1903
|
+
* itself. The ATTRIBUTE `aria-orientation="vertical"` on the tablist decides the keyboard, because
|
|
1904
|
+
* that is the thing an assistive technology announces and APG requires it on a vertical tablist
|
|
1905
|
+
* anyway. `freeday-tabs.js` writes the attribute when only the class is present, so an app states
|
|
1906
|
+
* the axis once and the two can never disagree.
|
|
1907
|
+
*
|
|
1908
|
+
* Selection is a SOFT FILL here, not a leading edge bar. Sideways, the marker is an underline under
|
|
1909
|
+
* the tab it belongs to; turned 90 degrees that line lands between two tabs and points at the wrong
|
|
1910
|
+
* one. The kit already has an answer for "one item in a vertical list is where you are" and spends
|
|
1911
|
+
* it in four places (`.fdy-nav__item[aria-current]`, the combo, cascade and autocomplete options):
|
|
1912
|
+
* `--color-primary-soft` behind `--color-primary-strong`. Both are precomputed tone tokens, so this
|
|
1913
|
+
* needs no color-mix() and no alpha of its own.
|
|
1914
|
+
*
|
|
1915
|
+
* What it deliberately does NOT do: pick a column width (set `--fdy-tabs-list-w`, default `auto`),
|
|
1916
|
+
* and collapse to one column on a narrow screen. Whether these tabs are a whole page, a card or a
|
|
1917
|
+
* drawer is the page's knowledge, not the component's, so the breakpoint is the page's to write. */
|
|
1918
|
+
.fdy-tabs--vertical{display:flex;align-items:flex-start;gap:var(--space-5);}
|
|
1919
|
+
/* `overflow:visible` on both axes, because the base strip scrolls sideways: a scroll container
|
|
1920
|
+
clips to its padding box, and a column has no sideways overflow to scroll, only a focus ring to
|
|
1921
|
+
lose. `box-shadow:none` drops the strip's own bottom line with it. */
|
|
1922
|
+
.fdy-tabs--vertical .fdy-tabs__list{flex:none;flex-direction:column;width:var(--fdy-tabs-list-w,auto);overflow:visible;box-shadow:none;}
|
|
1923
|
+
/* `white-space:normal` and `text-align:start` are corrections, not taste: a tab in a column of
|
|
1924
|
+
fixed width that may not wrap will overflow it, and a button centres its label by default. */
|
|
1925
|
+
.fdy-tabs--vertical .fdy-tabs__tab{border-bottom:0;border-radius:var(--radius-md);text-align:start;white-space:normal;}
|
|
1926
|
+
.fdy-tabs--vertical .fdy-tabs__tab:focus-visible{border-radius:var(--radius-md);}
|
|
1927
|
+
.fdy-tabs--vertical .fdy-tabs__tab[aria-selected="true"],.fdy-tabs--vertical .fdy-tabs__tab[aria-current="page"]{background:var(--color-primary-soft);color:var(--color-primary-strong);font-weight:var(--weight-semibold);}
|
|
1928
|
+
.fdy-tabs--vertical .fdy-tabs__panel{flex:1;min-width:0;padding:0;}
|
|
1929
|
+
|
|
1882
1930
|
/* Freeday. Timeline (vertical event feed: dot markers on a connector line). Pure CSS. */
|
|
1883
1931
|
.fdy-timeline{list-style:none;margin:0;padding:0;}
|
|
1884
1932
|
.fdy-timeline__item{position:relative;padding:0 0 var(--space-5) var(--space-6);border-left:2px solid var(--color-border);}
|
package/dist/freeday.js
CHANGED
|
@@ -2098,6 +2098,8 @@
|
|
|
2098
2098
|
* <div data-fdy-datepicker data-role="to" data-placeholder="To"></div>
|
|
2099
2099
|
* </div>
|
|
2100
2100
|
* The range links the two: the end can never precede the start (out-of-range days disable).
|
|
2101
|
+
* - Month: <div data-fdy-datepicker data-mode="month" data-value="2026-10"></div>
|
|
2102
|
+
* The calendar opens ON the month grid and stops there; the value is "YYYY-MM".
|
|
2101
2103
|
*
|
|
2102
2104
|
* Emits a bubbling "fdy-datepicker-change" CustomEvent on the wrapper: detail {value, date}.
|
|
2103
2105
|
* Keyboard in the grid: ←/→ ±1 day, ↑/↓ ±1 week, Home/End week edge, PageUp/Down ±month,
|
|
@@ -2126,6 +2128,11 @@
|
|
|
2126
2128
|
var TEXT = {
|
|
2127
2129
|
label: 'Date',
|
|
2128
2130
|
placeholder: 'Choose a date',
|
|
2131
|
+
/* The month mode's own pair. Reusing `label` and `placeholder` would name a month field "Date"
|
|
2132
|
+
and ask the reader to "Choose a date" on a control that cannot express a day, so every caller
|
|
2133
|
+
would have to pass both attributes to undo a default that was simply wrong for the mode. */
|
|
2134
|
+
labelMonth: 'Month',
|
|
2135
|
+
placeholderMonth: 'Choose a month',
|
|
2129
2136
|
prevMonth: 'Previous month',
|
|
2130
2137
|
nextMonth: 'Next month',
|
|
2131
2138
|
prevYear: 'Previous year',
|
|
@@ -2156,6 +2163,7 @@
|
|
|
2156
2163
|
function uid(p) { uidSeq += 1; return p + '-' + uidSeq; }
|
|
2157
2164
|
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
|
2158
2165
|
function toISO(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
|
|
2166
|
+
function toISOMonth(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1); }
|
|
2159
2167
|
function parseISO(s) {
|
|
2160
2168
|
if (!s) return null;
|
|
2161
2169
|
var p = String(s).split('-');
|
|
@@ -2163,6 +2171,17 @@
|
|
|
2163
2171
|
var d = new Date(Number(p[0]), Number(p[1]) - 1, Number(p[2]));
|
|
2164
2172
|
return isNaN(d.getTime()) ? null : d;
|
|
2165
2173
|
}
|
|
2174
|
+
/* Month mode reads BOTH shapes, and the day it drops is the point: a bound of "2026-03" lands on
|
|
2175
|
+
March 1st, and monthDisabled then compares a whole month against it correctly at either end,
|
|
2176
|
+
so an app can pass the same strings it stores. A full date still works for a host that keeps
|
|
2177
|
+
its bounds in one format across a form. */
|
|
2178
|
+
function parseISOMonth(s) {
|
|
2179
|
+
if (!s) return null;
|
|
2180
|
+
var p = String(s).split('-');
|
|
2181
|
+
if (p.length < 2) return null;
|
|
2182
|
+
var d = new Date(Number(p[0]), Number(p[1]) - 1, p.length > 2 ? Number(p[2]) : 1);
|
|
2183
|
+
return isNaN(d.getTime()) ? null : d;
|
|
2184
|
+
}
|
|
2166
2185
|
function startOfDay(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); }
|
|
2167
2186
|
function sameDay(a, b) { return !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); }
|
|
2168
2187
|
function addDays(d, n) { var x = new Date(d); x.setDate(x.getDate() + n); return x; }
|
|
@@ -2189,11 +2208,16 @@
|
|
|
2189
2208
|
wrap.dataset.fdyDpReady = '1';
|
|
2190
2209
|
wrap.classList.add('fdy-datepicker');
|
|
2191
2210
|
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
var
|
|
2196
|
-
var
|
|
2211
|
+
/* `data-mode="month"` makes the month grid the DESTINATION rather than a waypoint: the panel
|
|
2212
|
+
opens on it, Enter and a click commit from it, the day grid is never built, and the value is
|
|
2213
|
+
"YYYY-MM". Anything else, including the attribute's absence, is the day picker unchanged. */
|
|
2214
|
+
var monthOnly = wrap.getAttribute('data-mode') === 'month';
|
|
2215
|
+
var parseVal = monthOnly ? parseISOMonth : parseISO;
|
|
2216
|
+
var placeholder = wrap.getAttribute('data-placeholder') || textOf(wrap, monthOnly ? 'placeholderMonth' : 'placeholder');
|
|
2217
|
+
var label = wrap.getAttribute('data-label') || textOf(wrap, monthOnly ? 'labelMonth' : 'label');
|
|
2218
|
+
var selected = parseVal(wrap.getAttribute('data-value'));
|
|
2219
|
+
var minDate = parseVal(wrap.getAttribute('data-min'));
|
|
2220
|
+
var maxDate = parseVal(wrap.getAttribute('data-max'));
|
|
2197
2221
|
var rangeStart = null, rangeEnd = null; // for in-range shading
|
|
2198
2222
|
var view = startOfDay(selected || new Date());
|
|
2199
2223
|
view = new Date(view.getFullYear(), view.getMonth(), 1);
|
|
@@ -2203,7 +2227,7 @@
|
|
|
2203
2227
|
August 2026 to March 2022 is fifty-three of them. Shift+PageUp already jumped a year, but a
|
|
2204
2228
|
shortcut nobody can see is not an affordance, the repo's own test helper clicked "previous
|
|
2205
2229
|
month" twenty-four times to reach a date two years back. */
|
|
2206
|
-
var mode = 'days';
|
|
2230
|
+
var mode = monthOnly ? 'months' : 'days';
|
|
2207
2231
|
var focusMonth = null;
|
|
2208
2232
|
var focusYear = null;
|
|
2209
2233
|
|
|
@@ -2239,7 +2263,10 @@
|
|
|
2239
2263
|
|
|
2240
2264
|
function updateDisplay() {
|
|
2241
2265
|
if (selected) {
|
|
2242
|
-
|
|
2266
|
+
/* Through the same `Intl` formatter the month CELLS and the day grid's title already use,
|
|
2267
|
+
which is the whole reason this mode exists: <input type="month"> names the month in the
|
|
2268
|
+
browser's language, this names it in the page's. */
|
|
2269
|
+
valueSpan.textContent = (monthOnly ? monthNameFmt : valueFmt).format(selected);
|
|
2243
2270
|
valueSpan.classList.remove('fdy-datepicker__value--placeholder');
|
|
2244
2271
|
} else {
|
|
2245
2272
|
valueSpan.textContent = placeholder;
|
|
@@ -2283,6 +2310,10 @@
|
|
|
2283
2310
|
}
|
|
2284
2311
|
|
|
2285
2312
|
function render() {
|
|
2313
|
+
/* The floor of the drill, stated once here rather than at every caller that could lower it:
|
|
2314
|
+
in month mode there is no level below months, so the day grid is unreachable by
|
|
2315
|
+
construction instead of by every path remembering not to ask for it. */
|
|
2316
|
+
if (monthOnly && mode === 'days') mode = 'months';
|
|
2286
2317
|
if (mode === 'years') { renderYears(); return; }
|
|
2287
2318
|
if (mode === 'months') { renderMonths(); return; }
|
|
2288
2319
|
panel.innerHTML = '';
|
|
@@ -2387,7 +2418,7 @@
|
|
|
2387
2418
|
}
|
|
2388
2419
|
if (monthDisabled(year, m)) btn.disabled = true;
|
|
2389
2420
|
btn.tabIndex = m === focusMonth ? 0 : -1;
|
|
2390
|
-
btn.addEventListener('click', (function (mm) { return function () {
|
|
2421
|
+
btn.addEventListener('click', (function (mm) { return function () { chooseMonth(mm); }; })(m));
|
|
2391
2422
|
grid.appendChild(btn);
|
|
2392
2423
|
}
|
|
2393
2424
|
grid.addEventListener('keydown', onMonthKey);
|
|
@@ -2501,6 +2532,28 @@
|
|
|
2501
2532
|
if (cell) cell.focus();
|
|
2502
2533
|
}
|
|
2503
2534
|
|
|
2535
|
+
/* What a month cell DOES, decided in one place for the click handler and the Enter key alike.
|
|
2536
|
+
In day mode a month is a step on the way down; in month mode it is the answer. */
|
|
2537
|
+
function chooseMonth(m) {
|
|
2538
|
+
if (monthOnly) pickMonth(m);
|
|
2539
|
+
else openMonth(m);
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
function pickMonth(m) {
|
|
2543
|
+
var year = view.getFullYear();
|
|
2544
|
+
if (monthDisabled(year, m)) return;
|
|
2545
|
+
selected = new Date(year, m, 1);
|
|
2546
|
+
focusMonth = m;
|
|
2547
|
+
focusDate = selected;
|
|
2548
|
+
updateDisplay();
|
|
2549
|
+
/* The same event name, so a host already listening keeps listening, and `date` is still a
|
|
2550
|
+
real Date (the first of the month) rather than a second shape to parse. */
|
|
2551
|
+
wrap.dispatchEvent(new CustomEvent('fdy-datepicker-change', {
|
|
2552
|
+
bubbles: true, detail: { value: toISOMonth(selected), date: new Date(selected) }
|
|
2553
|
+
}));
|
|
2554
|
+
close(true);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2504
2557
|
/* Picking a month is navigation, not selection: it drops back to the day grid with the roving
|
|
2505
2558
|
cell clamped into the new month, so nothing is committed until a DAY is chosen. */
|
|
2506
2559
|
function openMonth(m) {
|
|
@@ -2532,7 +2585,7 @@
|
|
|
2532
2585
|
case 'PageUp': moveMonthFocus(focusMonth, -1); break;
|
|
2533
2586
|
case 'PageDown': moveMonthFocus(focusMonth, 1); break;
|
|
2534
2587
|
case 'Enter':
|
|
2535
|
-
case ' ':
|
|
2588
|
+
case ' ': chooseMonth(focusMonth); break;
|
|
2536
2589
|
case 'Escape': close(true); break;
|
|
2537
2590
|
default: handled = false;
|
|
2538
2591
|
}
|
|
@@ -2626,14 +2679,15 @@
|
|
|
2626
2679
|
function open() {
|
|
2627
2680
|
if (stateDisabled || stateReadonly) return;
|
|
2628
2681
|
if (!panel.hidden) return;
|
|
2629
|
-
mode = 'days';
|
|
2682
|
+
mode = monthOnly ? 'months' : 'days';
|
|
2630
2683
|
focusDate = selected || focusDate || new Date();
|
|
2631
2684
|
view = new Date(focusDate.getFullYear(), focusDate.getMonth(), 1);
|
|
2685
|
+
if (monthOnly) focusMonth = view.getMonth();
|
|
2632
2686
|
var p = popCtl(); if (p) p.show(); else panel.hidden = false;
|
|
2633
2687
|
trigger.setAttribute('aria-expanded', 'true');
|
|
2634
2688
|
trigger.classList.add('is-open');
|
|
2635
2689
|
render();
|
|
2636
|
-
focusFocusDate();
|
|
2690
|
+
if (monthOnly) focusMonthCell(); else focusFocusDate();
|
|
2637
2691
|
document.addEventListener('click', onDocClick, true);
|
|
2638
2692
|
}
|
|
2639
2693
|
|
|
@@ -2659,10 +2713,10 @@
|
|
|
2659
2713
|
var api = {
|
|
2660
2714
|
wrap: wrap,
|
|
2661
2715
|
setState: setState,
|
|
2662
|
-
getValue: function () { return selected ? toISO(selected) : ''; },
|
|
2716
|
+
getValue: function () { return selected ? (monthOnly ? toISOMonth(selected) : toISO(selected)) : ''; },
|
|
2663
2717
|
clear: function () { selected = null; updateDisplay(); if (!panel.hidden) render(); },
|
|
2664
|
-
setMin: function (iso) { minDate =
|
|
2665
|
-
setMax: function (iso) { maxDate =
|
|
2718
|
+
setMin: function (iso) { minDate = parseVal(iso); if (!panel.hidden) render(); },
|
|
2719
|
+
setMax: function (iso) { maxDate = parseVal(iso); if (!panel.hidden) render(); },
|
|
2666
2720
|
setRange: function (fromIso, toIso) {
|
|
2667
2721
|
rangeStart = parseISO(fromIso);
|
|
2668
2722
|
rangeEnd = parseISO(toIso);
|
|
@@ -3295,7 +3349,21 @@
|
|
|
3295
3349
|
its[(index + its.length) % its.length].focus();
|
|
3296
3350
|
}
|
|
3297
3351
|
var _pop = null;
|
|
3298
|
-
|
|
3352
|
+
/* `.fdy-menu--end` is the kit's existing spelling for "align this menu to the right", and the
|
|
3353
|
+
stylesheet has always honoured it (left:auto;right:0). The popover path replaces that with
|
|
3354
|
+
inline `fixed` coordinates, so without this line the class was silently dropped the moment
|
|
3355
|
+
the menu went into the top layer: an account menu in a page gutter overshot its avatar and
|
|
3356
|
+
ran to 4px off the screen edge. Reported from an app that patched it with a `translate`
|
|
3357
|
+
hack keyed to the kit's own GAP. */
|
|
3358
|
+
function popCtl() {
|
|
3359
|
+
if (_pop === null && window.FreedayPopover) {
|
|
3360
|
+
_pop = window.FreedayPopover.attach(menu, trigger, {
|
|
3361
|
+
matchWidth: false,
|
|
3362
|
+
align: menu.classList.contains('fdy-menu--end') ? 'end' : 'start'
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
return _pop;
|
|
3366
|
+
}
|
|
3299
3367
|
function open(index) {
|
|
3300
3368
|
var p = popCtl(); if (p) p.show(); else menu.hidden = false;
|
|
3301
3369
|
trigger.setAttribute('aria-expanded', 'true');
|
|
@@ -3469,7 +3537,8 @@
|
|
|
3469
3537
|
* Used by the select/combo, datepicker, timepicker, cascade, autocomplete and menu enhancers;
|
|
3470
3538
|
* the freeday/vue adapter ships an equivalent `usePopover` composable.
|
|
3471
3539
|
*
|
|
3472
|
-
* attach(panel, trigger) -> { show(), hide() }
|
|
3540
|
+
* attach(panel, trigger, opts) -> { show(), hide() }, where opts is { matchWidth, align } and
|
|
3541
|
+
* both are described on place() below. show()/hide() also toggle the panel's [hidden]
|
|
3473
3542
|
* attribute so callers keep using `!panel.hidden` for open-state and the `:not([hidden])`
|
|
3474
3543
|
* entrance animation still fires. On browsers without the Popover API it degrades to that
|
|
3475
3544
|
* [hidden] toggle over the panel's existing absolute-positioned CSS (clipped, but functional).
|
|
@@ -3482,35 +3551,56 @@
|
|
|
3482
3551
|
&& typeof HTMLElement.prototype.showPopover === 'function';
|
|
3483
3552
|
|
|
3484
3553
|
// Position `panel` (already shown, so it has a measurable box) against `trigger`'s rect.
|
|
3485
|
-
//
|
|
3486
|
-
//
|
|
3487
|
-
|
|
3488
|
-
|
|
3554
|
+
// opts is the object attach() takes, and both keys are optional:
|
|
3555
|
+
// matchWidth (default true) floors the panel to the trigger width, right for input-shaped
|
|
3556
|
+
// dropdowns (combo/cascade/date/time), skipped for menus that keep their own min-width.
|
|
3557
|
+
// align 'start' (default) lines the panel's LEFT edge up with the trigger's left edge;
|
|
3558
|
+
// 'end' lines its RIGHT edge up with the trigger's right edge, which is what a
|
|
3559
|
+
// control parked in a right-hand gutter needs. The panel is kept inside the
|
|
3560
|
+
// viewport either way, so 'end' near the left edge slides right instead of
|
|
3561
|
+
// hanging off it. freeday-menu.js reads it off `.fdy-menu--end`.
|
|
3562
|
+
function place(panel, trigger, opts) {
|
|
3563
|
+
var o = opts || {};
|
|
3564
|
+
/* Take the panel out of the flow BEFORE measuring anything, because until this runs the panel
|
|
3565
|
+
* is still `position:absolute` with `top:calc(100% + …)` from its component's stylesheet. An
|
|
3566
|
+
* author rule outranks the popover UA style, and in the top layer that `100%` resolves against
|
|
3567
|
+
* the viewport, so a panel that has just been shown sits one viewport height down the page and
|
|
3568
|
+
* drags the document's scrollable overflow with it. Where scrollbars take layout space (every
|
|
3569
|
+
* Windows and Linux browser; macOS overlay scrollbars hide it completely) the document gains a
|
|
3570
|
+
* classic scrollbar for the duration, documentElement.clientWidth drops by its width, and the
|
|
3571
|
+
* page relays out 15px narrower. A trigger rect read in that instant is a rect from a layout
|
|
3572
|
+
* that is about to be undone by the next three lines: measured 961 instead of 976 for an avatar
|
|
3573
|
+
* in a 24px gutter, so the menu landed a scrollbar's width to the left of it. */
|
|
3489
3574
|
panel.style.position = 'fixed';
|
|
3490
3575
|
panel.style.margin = '0';
|
|
3491
3576
|
panel.style.inset = 'auto'; // clear the UA popover centering
|
|
3492
|
-
|
|
3577
|
+
var r = trigger.getBoundingClientRect();
|
|
3578
|
+
if (o.matchWidth !== false) panel.style.minWidth = r.width + 'px'; // at least as wide as the trigger
|
|
3493
3579
|
var ph = panel.offsetHeight, pw = panel.offsetWidth;
|
|
3494
3580
|
var vw = document.documentElement.clientWidth, vh = document.documentElement.clientHeight;
|
|
3495
3581
|
var below = vh - r.bottom - GAP, above = r.top - GAP;
|
|
3496
3582
|
// Prefer below; flip above only when it doesn't fit below and above has more room.
|
|
3497
3583
|
var top = (ph <= below || below >= above) ? (r.bottom + GAP) : Math.max(GAP, r.top - GAP - ph);
|
|
3498
|
-
var left = r.left;
|
|
3499
|
-
|
|
3584
|
+
var left = (o.align === 'end') ? (r.right - pw) : r.left;
|
|
3585
|
+
/* Clamped on BOTH sides, and that is not belt-and-braces: the first line is what keeps a
|
|
3586
|
+
start-aligned panel on screen, the second is what keeps an end-aligned one on screen when
|
|
3587
|
+
the panel is wider than the room to the left of its trigger. Keeping only one of them
|
|
3588
|
+
trades an overflow on one edge for an overflow on the other. */
|
|
3589
|
+
if (left + pw > vw - GAP) left = vw - GAP - pw;
|
|
3590
|
+
if (left < GAP) left = GAP;
|
|
3500
3591
|
panel.style.top = Math.round(top) + 'px';
|
|
3501
3592
|
panel.style.left = Math.round(left) + 'px';
|
|
3502
3593
|
}
|
|
3503
3594
|
|
|
3504
3595
|
function attach(panel, trigger, opts) {
|
|
3505
|
-
var matchWidth = !(opts && opts.matchWidth === false);
|
|
3506
3596
|
if (supported) panel.setAttribute('popover', 'manual');
|
|
3507
|
-
function reposition() { if (supported && panel.matches(':popover-open')) place(panel, trigger,
|
|
3597
|
+
function reposition() { if (supported && panel.matches(':popover-open')) place(panel, trigger, opts); }
|
|
3508
3598
|
return {
|
|
3509
3599
|
show: function () {
|
|
3510
3600
|
panel.hidden = false;
|
|
3511
3601
|
if (supported) {
|
|
3512
3602
|
if (!panel.matches(':popover-open')) panel.showPopover();
|
|
3513
|
-
place(panel, trigger,
|
|
3603
|
+
place(panel, trigger, opts);
|
|
3514
3604
|
window.addEventListener('scroll', reposition, true); // capture: any scrolling ancestor
|
|
3515
3605
|
window.addEventListener('resize', reposition);
|
|
3516
3606
|
}
|
|
@@ -4118,7 +4208,10 @@
|
|
|
4118
4208
|
*
|
|
4119
4209
|
* Column filters: mark a header `<th data-fdy-filter="text|enum|number">` and a funnel
|
|
4120
4210
|
* button is injected; text filters by contains, enum by a value checklist, number by a
|
|
4121
|
-
* min/max range. All active column filters AND the global search must pass (logical AND).
|
|
4211
|
+
* min/max range. All active column filters AND the global search must pass (logical AND). The
|
|
4212
|
+
* panel is anchored to its funnel by freeday-popover.js, as in every other enhancer with a
|
|
4213
|
+
* dropdown, so load that file too (dist/freeday.js already has it); without it the panel still
|
|
4214
|
+
* opens, at whatever position its stylesheet leaves it.
|
|
4122
4215
|
*
|
|
4123
4216
|
* Bulk actions: an optional `[data-fdy-table-bulk]` bar (hidden by default) with a
|
|
4124
4217
|
* `[data-fdy-table-bulk-count]` label and a `[data-fdy-table-bulk-clear]` button shows
|
|
@@ -4348,6 +4441,8 @@
|
|
|
4348
4441
|
function closePopover(returnFocus) {
|
|
4349
4442
|
if (!openPopover) return;
|
|
4350
4443
|
var btn = openPopover._trigger;
|
|
4444
|
+
// Out of the top layer first, which also drops the reposition listeners attach() added.
|
|
4445
|
+
if (openPopover._pop) openPopover._pop.hide();
|
|
4351
4446
|
openPopover.remove();
|
|
4352
4447
|
openPopover = null;
|
|
4353
4448
|
window.removeEventListener('scroll', closeOnScroll, true);
|
|
@@ -4469,11 +4564,65 @@
|
|
|
4469
4564
|
foot.appendChild(reset); foot.appendChild(done);
|
|
4470
4565
|
pop.appendChild(foot);
|
|
4471
4566
|
|
|
4472
|
-
|
|
4567
|
+
/* Positioned by freeday-popover.js, like every other enhancer with a dropdown. This used to
|
|
4568
|
+
* be a fourth copy of that arithmetic, and the copies had drifted: it kept an 8px margin
|
|
4569
|
+
* where the kit's gap is 4, measured against window.innerWidth (which counts a classic
|
|
4570
|
+
* scrollbar the layout no longer has, so the clamp parked the panel under it), aligned the
|
|
4571
|
+
* left edges only, and never reached the top layer. The typed FdyTableFilter asks for
|
|
4572
|
+
* `align:'end'`, so a funnel in the rightmost column opened differently depending on which
|
|
4573
|
+
* path rendered the table, and the two paths are supposed to be interchangeable.
|
|
4574
|
+
* align 'end': the funnel is a 24px button in a column header and the panel is 14rem, so
|
|
4575
|
+
* lining up their LEFT edges aims the panel at nothing anybody can see, and in the rightmost
|
|
4576
|
+
* column it ran past the page. place() still clamps both sides, so the first column is safe.
|
|
4577
|
+
* matchWidth false: `.fdy-filter` asks the stylesheet for min-width:14rem, and a floor
|
|
4578
|
+
* taken from a 24px funnel is an inline style that outranks it.
|
|
4579
|
+
* In the top layer the panel is a child of the <th>, where FdyTableFilter renders it, rather
|
|
4580
|
+
* than of <body>. The top layer is what escapes an ancestor's overflow clip now, so the
|
|
4581
|
+
* <body> trick has nothing left to do, and it cost something real: everything outside an open
|
|
4582
|
+
* modal <dialog> is inert, so a filter panel on <body> painted a form that refused focus and
|
|
4583
|
+
* every click for any table inside a modal. */
|
|
4584
|
+
/* WHERE it comes to rest depends on whether the top layer is available, and the degraded path
|
|
4585
|
+
* is not an edge case: `supported` is read once when freeday-popover.js loads, the Popover API
|
|
4586
|
+
* landed in Chrome 114 / Safari 17 / Firefox 125, and the README promises Chrome 105 /
|
|
4587
|
+
* Safari 15.4 / Firefox 121. Chrome 105..113, Safari 15.4 and 16, and Firefox 121..124 are all
|
|
4588
|
+
* inside what the kit supports and all of them come through here.
|
|
4589
|
+
*
|
|
4590
|
+
* In the top layer the parent decides nothing about paint order, so the panel stays in the
|
|
4591
|
+
* <th> and the modal fix above holds. Without it the <th> is a CAGE:
|
|
4592
|
+
* `.fdy-table--sticky-head thead th` is `position:sticky; z-index:2`, and a positioned box
|
|
4593
|
+
* with a z-index is a stacking context, so the panel's own 130 is sorted inside that 2 and
|
|
4594
|
+
* every page overlay from 3 up paints over it. Measured on a phone: 224x49px of the panel,
|
|
4595
|
+
* its Reset and Close buttons, under `.fdy-bottomnav`'s z-index 50, and the hit test at that
|
|
4596
|
+
* point returned the bar. Raising the 130 cannot reach it, a number raised inside a stacking
|
|
4597
|
+
* context never leaves that context; and the context has to exist, the 2 is what keeps a
|
|
4598
|
+
* sticky header above the z-index:1 frozen body cells scrolling under it. So the panel is what
|
|
4599
|
+
* moves.
|
|
4600
|
+
*
|
|
4601
|
+
* It moves to `root`, the element that carries `data-fdy-table`, and not to <body>. `root` is
|
|
4602
|
+
* documented as `.fdy-datatable`: no position, no z-index, no transform, so no stacking
|
|
4603
|
+
* context and no containing block for a fixed box, which is all the panel needs. And it is
|
|
4604
|
+
* INSIDE whatever dialog the table is in, so the inert fault that sent the panel here in the
|
|
4605
|
+
* first place cannot come back, with no `closest('dialog')` special case to get wrong.
|
|
4606
|
+
*
|
|
4607
|
+
* Re-parenting alone would trade one fault for another, so it comes with place(). A fixed box
|
|
4608
|
+
* with auto offsets takes its STATIC position, and in a sticky cell inside a scrolled
|
|
4609
|
+
* scrollport that spot is nowhere in particular: measured 99px below the funnel it belongs to,
|
|
4610
|
+
* with the panel's foot 12px off the bottom of a 740px screen, and at the end of the datatable
|
|
4611
|
+
* once it is parented to `root`. place() is the kit's one copy of that arithmetic and it needs
|
|
4612
|
+
* nothing from the Popover API, it only writes inline coordinates. The panel closes on any
|
|
4613
|
+
* scroll (closeOnScroll, below), so a fixed panel that does not follow the page is not a
|
|
4614
|
+
* second fault.
|
|
4615
|
+
*
|
|
4616
|
+
* Where the helper is absent ENTIRELY nothing moves: there would be nothing to place the panel
|
|
4617
|
+
* with, and the static position inside the <th> at least lands near its own funnel. */
|
|
4618
|
+
var popover = window.FreedayPopover || null;
|
|
4619
|
+
var degraded = popover !== null && popover.supported !== true;
|
|
4620
|
+
var opts = { matchWidth: false, align: 'end' };
|
|
4621
|
+
(degraded ? root : btn.parentNode).appendChild(pop);
|
|
4473
4622
|
pop._trigger = btn;
|
|
4474
|
-
|
|
4475
|
-
pop.
|
|
4476
|
-
|
|
4623
|
+
pop._pop = popover === null ? null : popover.attach(pop, btn, opts);
|
|
4624
|
+
if (pop._pop) pop._pop.show();
|
|
4625
|
+
if (degraded) popover.place(pop, btn, opts);
|
|
4477
4626
|
openPopover = pop;
|
|
4478
4627
|
window.addEventListener('scroll', closeOnScroll, true);
|
|
4479
4628
|
window.addEventListener('resize', closeOnScroll);
|
|
@@ -4653,6 +4802,12 @@
|
|
|
4653
4802
|
* Markup: a [data-fdy-tabs] with a [role="tablist"] of
|
|
4654
4803
|
* <button role="tab" aria-controls="PANEL" aria-selected> and matching
|
|
4655
4804
|
* <div role="tabpanel" id="PANEL" aria-labelledby="TAB" [hidden]>.
|
|
4805
|
+
*
|
|
4806
|
+
* The arrow AXIS comes from the tablist's `aria-orientation`: `vertical` answers to Up and
|
|
4807
|
+
* Down, anything else to Left and Right. One axis per tablist, which is what APG maps, and the
|
|
4808
|
+
* other pair is left entirely alone rather than bound as a second axis: Left and Right over a
|
|
4809
|
+
* column of tabs belong to whatever else is on the page, and a widget that preventDefaults a key
|
|
4810
|
+
* it has no meaning for has taken it from the page silently. Home/End answer on both.
|
|
4656
4811
|
*/
|
|
4657
4812
|
(function () {
|
|
4658
4813
|
'use strict';
|
|
@@ -4663,6 +4818,19 @@
|
|
|
4663
4818
|
if (tabs.length === 0) return;
|
|
4664
4819
|
root.dataset.fdyTabsReady = '1';
|
|
4665
4820
|
|
|
4821
|
+
/* The look follows `.fdy-tabs--vertical`, the behaviour follows `aria-orientation`, and this
|
|
4822
|
+
makes the second agree with the first so an app states the axis once. The attribute always
|
|
4823
|
+
wins when it is there: it is what an assistive technology announces, so nothing here may
|
|
4824
|
+
overwrite one an author wrote. */
|
|
4825
|
+
var list = root.querySelector('[role="tablist"]');
|
|
4826
|
+
if (list !== null && list.getAttribute('aria-orientation') === null
|
|
4827
|
+
&& root.classList.contains('fdy-tabs--vertical')) {
|
|
4828
|
+
list.setAttribute('aria-orientation', 'vertical');
|
|
4829
|
+
}
|
|
4830
|
+
var vertical = list !== null && list.getAttribute('aria-orientation') === 'vertical';
|
|
4831
|
+
var NEXT_KEY = vertical ? 'ArrowDown' : 'ArrowRight';
|
|
4832
|
+
var PREV_KEY = vertical ? 'ArrowUp' : 'ArrowLeft';
|
|
4833
|
+
|
|
4666
4834
|
function panelFor(tab) {
|
|
4667
4835
|
var id = tab.getAttribute('aria-controls');
|
|
4668
4836
|
return id ? document.getElementById(id) : null;
|
|
@@ -4682,9 +4850,9 @@
|
|
|
4682
4850
|
tab.addEventListener('click', function () { select(tab, false); });
|
|
4683
4851
|
tab.addEventListener('keydown', function (e) {
|
|
4684
4852
|
switch (e.key) {
|
|
4685
|
-
case
|
|
4853
|
+
case NEXT_KEY:
|
|
4686
4854
|
e.preventDefault(); select(tabs[(i + 1) % tabs.length], true); break;
|
|
4687
|
-
case
|
|
4855
|
+
case PREV_KEY:
|
|
4688
4856
|
e.preventDefault(); select(tabs[(i - 1 + tabs.length) % tabs.length], true); break;
|
|
4689
4857
|
case 'Home':
|
|
4690
4858
|
e.preventDefault(); select(tabs[0], true); break;
|
package/docs/agent-onboarding.md
CHANGED
|
@@ -33,9 +33,9 @@ and the code looks correct and fails later.**
|
|
|
33
33
|
| Blazor (net8.0) | `@using Freeday.Blazor` (RCL) | `@bind-Value` |
|
|
34
34
|
| Static HTML, Svelte, server-rendered templates… | no wrapper: raw markup + the enhancer script | `fdy-*` DOM events |
|
|
35
35
|
|
|
36
|
-
The
|
|
37
|
-
FdyChart · FdyTable · FdyModal · FdyDrawer · FdyAppShell**. In
|
|
38
|
-
markup + enhancer for these
|
|
36
|
+
The twelve: **FdyCombo · FdyDatepicker · FdyMonthpicker · FdyDateRange · FdyAutocomplete ·
|
|
37
|
+
FdyCascade · FdyCfl · FdyChart · FdyTable · FdyModal · FdyDrawer · FdyAppShell**. In
|
|
38
|
+
Vue/React/Blazor, **never hand-write the raw markup + enhancer for these twelve.** The raw path *appears* to work: the enhancer auto-initialises
|
|
39
39
|
once on `DOMContentLoaded` and the first render is correct. Then it fails quietly: DOM your framework
|
|
40
40
|
renders later is never hydrated, and the widget's state lives in the DOM instead of in your
|
|
41
41
|
framework's state.
|
package/docs/getting-started.md
CHANGED
|
@@ -21,9 +21,9 @@ Freeday = **CSS** (semantic tokens + `fdy-*` classes) + **zero-dependency JS enh
|
|
|
21
21
|
the enhancer owns the widget's DOM. You **listen for `fdy-*` events** (all bubbling
|
|
22
22
|
`CustomEvent`s, data in `event.detail`) → store them in your framework state. Event/API contract
|
|
23
23
|
table: [`integrations.md` §Event & API contract](integrations.md).
|
|
24
|
-
**On Vue, React or Blazor this is not the path to take for
|
|
25
|
-
`FdyDatepicker`, `FdyDateRange`, `FdyAutocomplete`, `FdyCascade`, `FdyCfl`,
|
|
26
|
-
`FdyTable`, `FdyModal`, `FdyDrawer`, `FdyAppShell` ship typed wrappers that own the state properly (Vue and
|
|
24
|
+
**On Vue, React or Blazor this is not the path to take for twelve of the components.** `FdyCombo`,
|
|
25
|
+
`FdyDatepicker`, `FdyMonthpicker`, `FdyDateRange`, `FdyAutocomplete`, `FdyCascade`, `FdyCfl`,
|
|
26
|
+
`FdyChart`, `FdyTable`, `FdyModal`, `FdyDrawer`, `FdyAppShell` ship typed wrappers that own the state properly (Vue and
|
|
27
27
|
React re-implement the interaction natively; Blazor wraps the enhancer over interop). Use them;
|
|
28
28
|
the raw path is for the components without a wrapper, and for stacks without an adapter.
|
|
29
29
|
3. **Hydrate dynamic DOM.** Enhancers auto-init once on `DOMContentLoaded`. DOM an SPA renders
|
|
@@ -191,7 +191,7 @@ live docs also have a copy button per component.
|
|
|
191
191
|
```bash
|
|
192
192
|
npm i @cahyo-dimas/freeday
|
|
193
193
|
```
|
|
194
|
-
Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.
|
|
194
|
+
Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.9.0"` (public npm package). `dist/` is
|
|
195
195
|
committed and published → no build step; `npm ci` runs without auth.
|
|
196
196
|
|
|
197
197
|
### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
|
|
@@ -444,9 +444,9 @@ kit's CSS/enhancers), then bind:
|
|
|
444
444
|
<FdyChart Type="donut" Values="_byCity" Labels="_cityLabels" AriaLabel="Revenue by city" />
|
|
445
445
|
<FdyDrawer @bind-Open="_drawerOpen" Title="Detail" Side="right">…</FdyDrawer>
|
|
446
446
|
```
|
|
447
|
-
|
|
447
|
+
Twelve components at parity with the Vue/React adapters: **`FdyModal`** · **`FdyDrawer`** (`@bind-Open`,
|
|
448
448
|
`Title`, `Size`/`Side`, `Dismissible`) · **`FdyCombo<TValue>`** · **`FdyDatepicker`** ·
|
|
449
|
-
**`FdyAutocomplete`** · **`FdyCascade`** · **`FdyDateRange`** (`@bind-From`/`@bind-To`) ·
|
|
449
|
+
**`FdyMonthpicker`** (`@bind-Value` as `yyyy-MM`) · **`FdyAutocomplete`** · **`FdyCascade`** · **`FdyDateRange`** (`@bind-From`/`@bind-To`) ·
|
|
450
450
|
**`FdyCfl<TRow>`** (async `LoadPage`) · **`FdyChart`** · **`FdyTable<TRow>`** (client sort/filter/page,
|
|
451
451
|
or controlled `Sort`/`Filters`/`Page` for a server-paged table; `RowActivatable`, `RowDetail`) ·
|
|
452
452
|
**`FdyAppShell`** (`@bind-NavOpen`). Each
|
package/docs/integrations.md
CHANGED
|
@@ -153,8 +153,9 @@ function Panel(): JSX.Element {
|
|
|
153
153
|
}
|
|
154
154
|
```
|
|
155
155
|
> **Controlled alternative (parity with Vue's `v-model`):** `@cahyo-dimas/freeday/react` also exports
|
|
156
|
-
> the typed components `FdyCombo` / `FdyDatepicker` / `
|
|
157
|
-
> `FdyCascade` / `FdyCfl` / `FdyChart` / `FdyTable` / `FdyModal` / `FdyDrawer` /
|
|
156
|
+
> the typed components `FdyCombo` / `FdyDatepicker` / `FdyMonthpicker` / `FdyDateRange` /
|
|
157
|
+
> `FdyAutocomplete` / `FdyCascade` / `FdyCfl` / `FdyChart` / `FdyTable` / `FdyModal` / `FdyDrawer` /
|
|
158
|
+
> `FdyAppShell`, with plain
|
|
158
159
|
> `value`/`onChange` (props in, events out), no `data-fdy-*` + manual event listener:
|
|
159
160
|
> ```tsx
|
|
160
161
|
> import { FdyCombo } from '@cahyo-dimas/freeday/react';
|
|
@@ -199,8 +200,8 @@ document.addEventListener('fdy-form-invalid', (e) =>
|
|
|
199
200
|
|
|
200
201
|
> **Native components (parity with Vue's `v-model` / React's `value`/`onChange`):** the
|
|
201
202
|
> **`Freeday.Blazor`** RCL (net8.0, `adapters/blazor/`) ships the typed components `FdyCombo<TValue>` /
|
|
202
|
-
> `FdyDatepicker` / `
|
|
203
|
-
> `FdyTable<TRow>` / `FdyModal` / `FdyDrawer` / `FdyAppShell` with `@bind`, no `@ref` / manual `initAll` / `[JSInvokable]`:
|
|
203
|
+
> `FdyDatepicker` / `FdyMonthpicker` / `FdyDateRange` / `FdyAutocomplete` / `FdyCascade` /
|
|
204
|
+
> `FdyCfl<TRow>` / `FdyChart` / `FdyTable<TRow>` / `FdyModal` / `FdyDrawer` / `FdyAppShell` with `@bind`, no `@ref` / manual `initAll` / `[JSInvokable]`:
|
|
204
205
|
> ```razor
|
|
205
206
|
> @using Freeday.Blazor
|
|
206
207
|
> <FdyCombo TValue="string" @bind-Value="_status" Options="_statusOptions" AriaLabelledby="lbl-status" />
|
|
@@ -327,9 +328,26 @@ Use the `app-` prefix for its classes, and reuse the kit's plumbing rather than
|
|
|
327
328
|
```ts
|
|
328
329
|
import { usePopover } from '@cahyo-dimas/freeday/vue'; // or '/react'
|
|
329
330
|
usePopover(panelRef, triggerRef, open); // panel needs popover="manual"
|
|
331
|
+
usePopover(panelRef, triggerRef, open, 'end'); // right edges together, for a panel
|
|
332
|
+
// wider than a trigger in a gutter
|
|
333
|
+
usePopover(panelRef, triggerRef, open, 'end', false); // and let the panel keep its own width
|
|
330
334
|
```
|
|
331
335
|
|
|
332
336
|
This is what every kit dropdown uses to escape an ancestor's overflow clip, because `.fdy-card` is
|
|
333
337
|
`overflow:hidden`, so a panel positioned inside one is otherwise cut at the card's edge. It is
|
|
334
338
|
exported so a control you build behaves like one the kit ships, and improves when the kit's
|
|
335
339
|
positioning does.
|
|
340
|
+
|
|
341
|
+
The fourth argument is the alignment, `'start'` (the default, left edges together) or `'end'`
|
|
342
|
+
(right edges together). Reach for `'end'` when the panel is much wider than its trigger and the
|
|
343
|
+
trigger sits near the right of something, an icon in a page gutter or a funnel in a table
|
|
344
|
+
header. The panel is kept inside the viewport on both sides either way, so `'end'` beside the
|
|
345
|
+
left edge slides right rather than off the screen.
|
|
346
|
+
|
|
347
|
+
The fifth is `matchWidth`, and it defaults to `true`: the panel is floored to its trigger's width,
|
|
348
|
+
which is what makes a listbox look like part of the control it drops out of. Pass `false` when your
|
|
349
|
+
panel has a width of its own in CSS, because the floor is written as an inline style and an inline
|
|
350
|
+
style outranks your rule. The kit's own funnel panel is the case that needs it, a 14rem form hanging
|
|
351
|
+
off a 24px icon; `FdyTableFilter` passes `false` and the nine other typed dropdowns pass nothing.
|
|
352
|
+
Both arguments are plain values rather than an options object, so the React port can list them among
|
|
353
|
+
an effect's dependencies and you never have to memoise anything.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cahyo-dimas/freeday",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "Freeday: token-driven, framework-agnostic UI KIT (design source-of-truth).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"scripts": {
|
|
97
97
|
"build": "node tokens/build.mjs",
|
|
98
98
|
"test": "node --test",
|
|
99
|
-
"test:browser": "node --test --test-concurrency=3 browser/color-mix.mjs browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/field-width.mjs browser/cfl-multi.mjs browser/cfl-dialog-only.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs browser/bottom-nav.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/cfl-narrow.mjs browser/form-reveal.mjs browser/confirm-search-copy.mjs browser/daterange-settled.mjs browser/display-parts.mjs browser/table-empty.mjs browser/page-columns.mjs browser/table-hover.mjs browser/harness-contract.mjs",
|
|
99
|
+
"test:browser": "node --test --test-concurrency=3 browser/color-mix.mjs browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/field-width.mjs browser/cfl-multi.mjs browser/cfl-dialog-only.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs browser/bottom-nav.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/tabs-vertical.mjs browser/menu-align.mjs browser/cfl-narrow.mjs browser/form-reveal.mjs browser/confirm-search-copy.mjs browser/daterange-settled.mjs browser/display-parts.mjs browser/table-empty.mjs browser/table-footer-text.mjs browser/month-mode.mjs browser/monthpicker.mjs browser/page-columns.mjs browser/table-hover.mjs browser/typed-popover.mjs browser/filter-parity.mjs browser/filter-wrap.mjs browser/filter-stack.mjs browser/harness-contract.mjs",
|
|
100
100
|
"prepack": "node tokens/build.mjs",
|
|
101
101
|
"version": "node tokens/build.mjs && git add dist",
|
|
102
102
|
"typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit",
|
package/src/components/table.css
CHANGED
|
@@ -140,8 +140,25 @@
|
|
|
140
140
|
.fdy-table__filterbtn.is-active{color:var(--color-primary-strong);}
|
|
141
141
|
.fdy-table__filterbtn svg{width:.85rem;height:.85rem;}
|
|
142
142
|
|
|
143
|
-
/* Filter popover
|
|
144
|
-
|
|
143
|
+
/* Filter popover. `position:fixed` because the panel is positioned by freeday-popover.js (and by
|
|
144
|
+
usePopover on the typed path) with inline viewport coordinates, in the top layer, which is what
|
|
145
|
+
escapes a card's or a scroll container's clip; the panel itself is a child of its own <th> on
|
|
146
|
+
both paths, so an open modal <dialog> does not make it inert. It is the one kit panel that is
|
|
147
|
+
fixed rather than absolute, and that is load-bearing: #079 cannot reach a panel which never
|
|
148
|
+
resolves a `top:calc(100% + …)` against the viewport, which browser/filter-parity.mjs measures.
|
|
149
|
+
`min-width` is the panel's own, so neither path may floor it to the width of a 24px funnel.
|
|
150
|
+
|
|
151
|
+
The typography block is a RESET of what the panel inherits from that <th>, and it has to name
|
|
152
|
+
every inherited property the header rule sets, not the ones that looked worst. A header is
|
|
153
|
+
uppercase, tracked, semibold, muted, 12px and `white-space:nowrap`; a numeric column adds mono
|
|
154
|
+
and, on the typed path, an inline `text-align:right` that no ancestor rule could outrank anyway.
|
|
155
|
+
None of that belongs on a panel of labels and inputs, and the four it reset from the start were
|
|
156
|
+
the four that SHOWED: an 18rem cap hides a nowrap panel's damage, because the box stays 18rem
|
|
157
|
+
wide while the text runs out of the list, which then scrolls sideways to reach a value nobody
|
|
158
|
+
can read. `font-size` and `font-family` reach only the text that declares neither, the range
|
|
159
|
+
separator and the title, which is exactly why they went unnoticed. Measured as geometry in
|
|
160
|
+
browser/filter-wrap.mjs rather than asserted as declarations. */
|
|
161
|
+
.fdy-filter{position:fixed;z-index:130;min-width:14rem;max-width:18rem;padding:var(--space-3);background:var(--color-surface);border:var(--bw) solid var(--color-border-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-3);text-align:left;white-space:normal;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-regular);text-transform:none;letter-spacing:normal;color:var(--color-text);}
|
|
145
162
|
.fdy-filter__title{font-size:var(--text-xs);font-weight:var(--weight-semibold);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-wide);margin-bottom:var(--space-2);}
|
|
146
163
|
.fdy-filter__list{display:flex;flex-direction:column;gap:var(--space-1);max-height:12rem;overflow:auto;}
|
|
147
164
|
.fdy-filter__check{display:flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm);color:var(--color-text);cursor:pointer;}
|